@balena/abstract-sql-compiler
Advanced tools
Comparing version 7.10.2 to 7.11.0-generalize-rule-referenced-fields-3e77ac1aed9f3fb596f6a01a08a1800fead1e860
@@ -7,2 +7,6 @@ # Change Log | ||
## 7.11.0 - 2021-03-01 | ||
* Generalize/share the referenced fields code and cover more cases [Pagan Gazzard] | ||
## 7.10.2 - 2021-02-12 | ||
@@ -9,0 +13,0 @@ |
@@ -7,3 +7,14 @@ "use strict"; | ||
const AbstractSQLOptimiser_1 = require("./AbstractSQLOptimiser"); | ||
const getScope = (rulePart, scope) => { | ||
const getReferencedFields = (ruleBody) => { | ||
const referencedFields = exports.getRuleReferencedFields(ruleBody); | ||
return _.mapValues(referencedFields, ({ update }) => _.uniq(update)); | ||
}; | ||
exports.getReferencedFields = getReferencedFields; | ||
var IsSafe; | ||
(function (IsSafe) { | ||
IsSafe["Insert"] = "ins"; | ||
IsSafe["Delete"] = "del"; | ||
IsSafe["Unknown"] = ""; | ||
})(IsSafe || (IsSafe = {})); | ||
const getRuleReferencedScope = (rulePart, scope, isSafe) => { | ||
scope = { ...scope }; | ||
@@ -20,6 +31,6 @@ const fromNodes = rulePart.filter(AbstractSQLCompiler_1.isFromNode); | ||
case 'Table': | ||
scope[alias] = from[1]; | ||
scope[alias] = { tableName: from[1], isSafe }; | ||
break; | ||
case 'SelectQuery': | ||
scope[alias] = ''; | ||
scope[alias] = { tableName: '', isSafe }; | ||
break; | ||
@@ -31,3 +42,3 @@ default: | ||
else if (nested[0] === 'Table') { | ||
scope[nested[1]] = nested[1]; | ||
scope[nested[1]] = { tableName: nested[1], isSafe }; | ||
} | ||
@@ -40,3 +51,3 @@ else { | ||
}; | ||
const $getReferencedFields = (referencedFields, rulePart, scope = {}) => { | ||
const $getRuleReferencedFields = (referencedFields, rulePart, isSafe, scope = {}) => { | ||
if (!Array.isArray(rulePart)) { | ||
@@ -47,19 +58,29 @@ return; | ||
case 'SelectQuery': | ||
scope = getScope(rulePart, scope); | ||
scope = getRuleReferencedScope(rulePart, scope, isSafe); | ||
rulePart.forEach((node) => { | ||
$getReferencedFields(referencedFields, node, scope); | ||
$getRuleReferencedFields(referencedFields, node, isSafe, scope); | ||
}); | ||
break; | ||
return; | ||
case 'ReferencedField': | ||
let tableName = rulePart[1]; | ||
const aliasName = rulePart[1]; | ||
const fieldName = rulePart[2]; | ||
if (typeof tableName !== 'string' || typeof fieldName !== 'string') { | ||
if (typeof aliasName !== 'string' || typeof fieldName !== 'string') { | ||
throw new Error(`Invalid ReferencedField: ${rulePart}`); | ||
} | ||
tableName = scope[tableName]; | ||
if (tableName !== '') { | ||
if (referencedFields[tableName] == null) { | ||
referencedFields[tableName] = []; | ||
const a = scope[aliasName]; | ||
if (a.tableName !== '') { | ||
if (referencedFields[a.tableName] == null) { | ||
referencedFields[a.tableName] = { | ||
create: [], | ||
update: [], | ||
delete: [], | ||
}; | ||
} | ||
referencedFields[tableName].push(fieldName); | ||
if (a.isSafe !== IsSafe.Insert) { | ||
referencedFields[a.tableName].create.push(fieldName); | ||
} | ||
if (a.isSafe !== IsSafe.Delete) { | ||
referencedFields[a.tableName].delete.push(fieldName); | ||
} | ||
referencedFields[a.tableName].update.push(fieldName); | ||
} | ||
@@ -69,73 +90,33 @@ return; | ||
throw new Error('Cannot find queried fields for unreferenced fields'); | ||
case 'Not': | ||
case 'NotExists': | ||
if (isSafe === IsSafe.Insert) { | ||
isSafe = IsSafe.Delete; | ||
} | ||
else if (isSafe === IsSafe.Delete) { | ||
isSafe = IsSafe.Insert; | ||
} | ||
case 'And': | ||
case 'Exists': | ||
rulePart.forEach((node) => { | ||
$getRuleReferencedFields(referencedFields, node, isSafe, scope); | ||
}); | ||
return; | ||
default: | ||
rulePart.forEach((node) => { | ||
$getReferencedFields(referencedFields, node, scope); | ||
$getRuleReferencedFields(referencedFields, node, IsSafe.Unknown, scope); | ||
}); | ||
} | ||
}; | ||
const getReferencedFields = (ruleBody) => { | ||
const getRuleReferencedFields = (ruleBody) => { | ||
ruleBody = AbstractSQLOptimiser_1.AbstractSQLOptimiser(ruleBody); | ||
const referencedFields = {}; | ||
$getReferencedFields(referencedFields, ruleBody); | ||
return _.mapValues(referencedFields, _.uniq); | ||
}; | ||
exports.getReferencedFields = getReferencedFields; | ||
const dealiasTableNode = (n) => { | ||
if (AbstractSQLCompiler_1.isTableNode(n)) { | ||
return n; | ||
} | ||
if (n[0] === 'Alias' && AbstractSQLCompiler_1.isTableNode(n[1])) { | ||
return n[1]; | ||
} | ||
}; | ||
const getRuleReferencedFields = (ruleBody) => { | ||
ruleBody = AbstractSQLOptimiser_1.AbstractSQLOptimiser(ruleBody); | ||
let referencedFields = {}; | ||
const deletable = new Set(); | ||
if (ruleBody[0] === 'NotExists') { | ||
const s = ruleBody[1]; | ||
if (s[0] === 'SelectQuery') { | ||
s.forEach((m) => { | ||
if (!AbstractSQLCompiler_1.isFromNode(m)) { | ||
return; | ||
} | ||
const table = dealiasTableNode(m[1]); | ||
if (table == null) { | ||
return; | ||
} | ||
deletable.add(table[1]); | ||
}); | ||
$getRuleReferencedFields(referencedFields, ruleBody, IsSafe.Insert); | ||
for (const tableName of Object.keys(referencedFields)) { | ||
const tableRefs = referencedFields[tableName]; | ||
for (const method of Object.keys(tableRefs)) { | ||
tableRefs[method] = _.uniq(tableRefs[method]); | ||
} | ||
} | ||
$getReferencedFields(referencedFields, ruleBody); | ||
referencedFields = _.mapValues(referencedFields, _.uniq); | ||
const refFields = {}; | ||
for (const f of Object.keys(referencedFields)) { | ||
refFields[f] = { | ||
create: referencedFields[f], | ||
update: referencedFields[f], | ||
delete: referencedFields[f], | ||
}; | ||
if (deletable.has(f)) { | ||
const countFroms = (n) => { | ||
let count = 0; | ||
n.forEach((p) => { | ||
var _a; | ||
if (Array.isArray(p)) { | ||
if (AbstractSQLCompiler_1.isFromNode(p) && ((_a = dealiasTableNode(p[1])) === null || _a === void 0 ? void 0 : _a[1]) === f) { | ||
count++; | ||
} | ||
else { | ||
count += countFroms(p); | ||
} | ||
} | ||
}); | ||
return count; | ||
}; | ||
if (countFroms(ruleBody) === 1) { | ||
refFields[f].delete = []; | ||
} | ||
} | ||
} | ||
return refFields; | ||
return referencedFields; | ||
}; | ||
@@ -142,0 +123,0 @@ exports.getRuleReferencedFields = getRuleReferencedFields; |
{ | ||
"name": "@balena/abstract-sql-compiler", | ||
"version": "7.10.2", | ||
"version": "7.11.0-generalize-rule-referenced-fields-3e77ac1aed9f3fb596f6a01a08a1800fead1e860", | ||
"description": "A translator for abstract sql into sql.", | ||
@@ -5,0 +5,0 @@ "main": "out/AbstractSQLCompiler.js", |
@@ -8,5 +8,2 @@ import * as _ from 'lodash'; | ||
isFromNode, | ||
isTableNode, | ||
SelectQueryNode, | ||
TableNode, | ||
} from './AbstractSQLCompiler'; | ||
@@ -25,5 +22,33 @@ import { AbstractSQLOptimiser } from './AbstractSQLOptimiser'; | ||
type Scope = _.Dictionary<string>; | ||
export const getReferencedFields: EngineInstance['getReferencedFields'] = ( | ||
ruleBody, | ||
) => { | ||
const referencedFields = getRuleReferencedFields(ruleBody); | ||
const getScope = (rulePart: AbstractSqlQuery, scope: Scope): Scope => { | ||
return _.mapValues(referencedFields, ({ update }) => _.uniq(update)); | ||
}; | ||
export interface RuleReferencedFields { | ||
[alias: string]: { | ||
create: string[]; | ||
update: string[]; | ||
delete: string[]; | ||
}; | ||
} | ||
enum IsSafe { | ||
Insert = 'ins', | ||
Delete = 'del', | ||
Unknown = '', | ||
} | ||
type RuleReferencedScope = { | ||
[aliasName: string]: { | ||
tableName: string; | ||
isSafe: IsSafe; | ||
}; | ||
}; | ||
const getRuleReferencedScope = ( | ||
rulePart: AbstractSqlQuery, | ||
scope: RuleReferencedScope, | ||
isSafe: IsSafe, | ||
): RuleReferencedScope => { | ||
scope = { ...scope }; | ||
@@ -40,3 +65,3 @@ const fromNodes = rulePart.filter(isFromNode); | ||
case 'Table': | ||
scope[alias] = from[1]; | ||
scope[alias] = { tableName: from[1], isSafe }; | ||
break; | ||
@@ -47,3 +72,3 @@ case 'SelectQuery': | ||
// fields that don't affect the end result and avoid false positives | ||
scope[alias] = ''; | ||
scope[alias] = { tableName: '', isSafe }; | ||
break; | ||
@@ -54,3 +79,3 @@ default: | ||
} else if (nested[0] === 'Table') { | ||
scope[nested[1]] = nested[1]; | ||
scope[nested[1]] = { tableName: nested[1], isSafe }; | ||
} else { | ||
@@ -62,6 +87,7 @@ throw Error(`Unsupported FromNode for scoping: ${nested[0]}`); | ||
}; | ||
const $getReferencedFields = ( | ||
referencedFields: ReferencedFields, | ||
const $getRuleReferencedFields = ( | ||
referencedFields: RuleReferencedFields, | ||
rulePart: AbstractSqlQuery, | ||
scope: Scope = {}, | ||
isSafe: IsSafe, | ||
scope: RuleReferencedScope = {}, | ||
) => { | ||
@@ -74,21 +100,31 @@ if (!Array.isArray(rulePart)) { | ||
// Update the current scope before trying to resolve field references | ||
scope = getScope(rulePart, scope); | ||
scope = getRuleReferencedScope(rulePart, scope, isSafe); | ||
rulePart.forEach((node: AbstractSqlQuery) => { | ||
$getReferencedFields(referencedFields, node, scope); | ||
$getRuleReferencedFields(referencedFields, node, isSafe, scope); | ||
}); | ||
break; | ||
return; | ||
case 'ReferencedField': | ||
let tableName = rulePart[1]; | ||
const aliasName = rulePart[1]; | ||
const fieldName = rulePart[2]; | ||
if (typeof tableName !== 'string' || typeof fieldName !== 'string') { | ||
if (typeof aliasName !== 'string' || typeof fieldName !== 'string') { | ||
throw new Error(`Invalid ReferencedField: ${rulePart}`); | ||
} | ||
tableName = scope[tableName]; | ||
const a = scope[aliasName]; | ||
// The scoped tableName is empty in the case of an aliased from query | ||
// and those fields will be covered when we recurse into them | ||
if (tableName !== '') { | ||
if (referencedFields[tableName] == null) { | ||
referencedFields[tableName] = []; | ||
if (a.tableName !== '') { | ||
if (referencedFields[a.tableName] == null) { | ||
referencedFields[a.tableName] = { | ||
create: [], | ||
update: [], | ||
delete: [], | ||
}; | ||
} | ||
referencedFields[tableName].push(fieldName); | ||
if (a.isSafe !== IsSafe.Insert) { | ||
referencedFields[a.tableName].create.push(fieldName); | ||
} | ||
if (a.isSafe !== IsSafe.Delete) { | ||
referencedFields[a.tableName].delete.push(fieldName); | ||
} | ||
referencedFields[a.tableName].update.push(fieldName); | ||
} | ||
@@ -98,33 +134,23 @@ return; | ||
throw new Error('Cannot find queried fields for unreferenced fields'); | ||
case 'Not': | ||
case 'NotExists': | ||
// When hitting a `Not` we invert the safety rule | ||
if (isSafe === IsSafe.Insert) { | ||
isSafe = IsSafe.Delete; | ||
} else if (isSafe === IsSafe.Delete) { | ||
isSafe = IsSafe.Insert; | ||
} | ||
// Fallthrough | ||
case 'And': | ||
case 'Exists': | ||
rulePart.forEach((node: AbstractSqlQuery) => { | ||
$getRuleReferencedFields(referencedFields, node, isSafe, scope); | ||
}); | ||
return; | ||
default: | ||
rulePart.forEach((node: AbstractSqlQuery) => { | ||
$getReferencedFields(referencedFields, node, scope); | ||
$getRuleReferencedFields(referencedFields, node, IsSafe.Unknown, scope); | ||
}); | ||
} | ||
}; | ||
export const getReferencedFields: EngineInstance['getReferencedFields'] = ( | ||
ruleBody, | ||
) => { | ||
ruleBody = AbstractSQLOptimiser(ruleBody); | ||
const referencedFields: ReferencedFields = {}; | ||
$getReferencedFields(referencedFields, ruleBody); | ||
return _.mapValues(referencedFields, _.uniq); | ||
}; | ||
const dealiasTableNode = (n: AbstractSqlQuery): TableNode | undefined => { | ||
if (isTableNode(n)) { | ||
return n; | ||
} | ||
if (n[0] === 'Alias' && isTableNode(n[1])) { | ||
return n[1]; | ||
} | ||
}; | ||
export interface RuleReferencedFields { | ||
[alias: string]: { | ||
create: string[]; | ||
update: string[]; | ||
delete: string[]; | ||
}; | ||
} | ||
export const getRuleReferencedFields: EngineInstance['getRuleReferencedFields'] = ( | ||
@@ -134,53 +160,14 @@ ruleBody, | ||
ruleBody = AbstractSQLOptimiser(ruleBody); | ||
let referencedFields: ReferencedFields = {}; | ||
const deletable = new Set<string>(); | ||
if (ruleBody[0] === 'NotExists') { | ||
const s = ruleBody[1] as SelectQueryNode; | ||
if (s[0] === 'SelectQuery') { | ||
s.forEach((m) => { | ||
if (!isFromNode(m)) { | ||
return; | ||
} | ||
const table = dealiasTableNode(m[1]); | ||
if (table == null) { | ||
// keep this from node for later checking if we didn't optimize out | ||
return; | ||
} | ||
deletable.add(table[1]); | ||
}); | ||
const referencedFields: RuleReferencedFields = {}; | ||
$getRuleReferencedFields(referencedFields, ruleBody, IsSafe.Insert); | ||
for (const tableName of Object.keys(referencedFields)) { | ||
const tableRefs = referencedFields[tableName]; | ||
for (const method of Object.keys(tableRefs) as Array< | ||
keyof typeof tableRefs | ||
>) { | ||
tableRefs[method] = _.uniq(tableRefs[method]); | ||
} | ||
} | ||
$getReferencedFields(referencedFields, ruleBody); | ||
referencedFields = _.mapValues(referencedFields, _.uniq); | ||
const refFields: RuleReferencedFields = {}; | ||
for (const f of Object.keys(referencedFields)) { | ||
refFields[f] = { | ||
create: referencedFields[f], | ||
update: referencedFields[f], | ||
delete: referencedFields[f], | ||
}; | ||
if (deletable.has(f)) { | ||
const countFroms = (n: AbstractSqlType[]) => { | ||
let count = 0; | ||
n.forEach((p) => { | ||
if (Array.isArray(p)) { | ||
if (isFromNode(p) && dealiasTableNode(p[1])?.[1] === f) { | ||
count++; | ||
} else { | ||
count += countFroms(p as AbstractSqlType[]); | ||
} | ||
} | ||
}); | ||
return count; | ||
}; | ||
// It's only deletable if there's just a single ref | ||
if (countFroms(ruleBody) === 1) { | ||
refFields[f].delete = []; | ||
} | ||
} | ||
} | ||
return refFields; | ||
return referencedFields; | ||
}; | ||
@@ -187,0 +174,0 @@ |
import { expect } from 'chai'; | ||
import * as AbstractSqlCompiler from '../../src/AbstractSQLCompiler'; | ||
describe('getReferencedFields', () => { | ||
it('should work with selected fields', () => { | ||
describe('getRuleReferencedFields', () => { | ||
it('should work with single table SELECT NOT EXISTS', () => { | ||
expect( | ||
@@ -41,2 +41,74 @@ AbstractSqlCompiler.postgres.getRuleReferencedFields([ | ||
}); | ||
it('should work with multi table SELECT NOT EXISTS', () => { | ||
expect( | ||
AbstractSqlCompiler.postgres.getRuleReferencedFields([ | ||
'Not', | ||
[ | ||
'Exists', | ||
[ | ||
'SelectQuery', | ||
['Select', []], | ||
['From', ['test', 'test.0']], | ||
['From', ['test', 'test.1']], | ||
[ | ||
'Where', | ||
[ | ||
'Not', | ||
[ | ||
'And', | ||
[ | ||
'LessThan', | ||
['ReferencedField', 'test.1', 'id'], | ||
['ReferencedField', 'test.0', 'id'], | ||
], | ||
['Exists', ['ReferencedField', 'test.0', 'id']], | ||
], | ||
], | ||
], | ||
], | ||
], | ||
] as AbstractSqlCompiler.AbstractSqlQuery), | ||
).to.deep.equal({ | ||
test: { | ||
create: ['id'], | ||
update: ['id'], | ||
delete: [], | ||
}, | ||
}); | ||
}); | ||
it('should work with single table SELECT EXISTS', () => { | ||
expect( | ||
AbstractSqlCompiler.postgres.getRuleReferencedFields([ | ||
'Exists', | ||
[ | ||
'SelectQuery', | ||
['Select', []], | ||
['From', ['test', 'test.0']], | ||
[ | ||
'Where', | ||
[ | ||
'Not', | ||
[ | ||
'And', | ||
[ | ||
'LessThan', | ||
['Integer', 0], | ||
['ReferencedField', 'test.0', 'id'], | ||
], | ||
['Exists', ['ReferencedField', 'test.0', 'id']], | ||
], | ||
], | ||
], | ||
], | ||
] as AbstractSqlCompiler.AbstractSqlQuery), | ||
).to.deep.equal({ | ||
test: { | ||
create: [], | ||
update: ['id'], | ||
delete: ['id'], | ||
}, | ||
}); | ||
}); | ||
}); |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
601463
13305
2