Skip to content

Commit

Permalink
Add the ability to optimize generated sql using model info
Browse files Browse the repository at this point in the history
We automatically use the model info to optimize rules/check constraints
when compiling the schema. Currently it supports optimizing exists/not
exists checks for referenced field nodes.

Change-type: minor
  • Loading branch information
Page- committed Jan 1, 2021
1 parent 5a48b64 commit 5503416
Show file tree
Hide file tree
Showing 5 changed files with 185 additions and 60 deletions.
28 changes: 21 additions & 7 deletions src/AbstractSQLCompiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -571,16 +571,19 @@ export function compileRule(
abstractSQL: UpsertQueryNode,
engine: Engines,
noBinds: true,
abstractSqlModel?: AbstractSqlModel,
): [string, string];
export function compileRule(
abstractSQL: AbstractSqlQuery,
engine: Engines,
noBinds: true,
abstractSqlModel?: AbstractSqlModel,
): string;
export function compileRule(
abstractSQL: UpsertQueryNode,
engine: Engines,
noBinds?: false,
abstractSqlModel?: AbstractSqlModel,
): [SqlResult, SqlResult];
export function compileRule(
abstractSQL:
Expand All @@ -591,23 +594,27 @@ export function compileRule(
| DeleteQueryNode,
engine: Engines,
noBinds?: false,
abstractSqlModel?: AbstractSqlModel,
): SqlResult;
export function compileRule(
abstractSQL: AbstractSqlQuery,
engine: Engines,
noBinds?: false,
abstractSqlModel?: AbstractSqlModel,
): SqlResult | [SqlResult, SqlResult];
export function compileRule(
abstractSQL: AbstractSqlQuery,
engine: Engines,
noBinds?: boolean,
abstractSqlModel?: AbstractSqlModel,
): SqlResult | [SqlResult, SqlResult] | string;
export function compileRule(
abstractSQL: AbstractSqlQuery,
engine: Engines,
noBinds = false,
abstractSqlModel?: AbstractSqlModel,
): SqlResult | [SqlResult, SqlResult] | string | [string, string] {
abstractSQL = AbstractSQLOptimiser(abstractSQL, noBinds);
abstractSQL = AbstractSQLOptimiser(abstractSQL, noBinds, abstractSqlModel);
return AbstractSQLRules2SQL(abstractSQL, engine, noBinds);
}

Expand Down Expand Up @@ -700,10 +707,12 @@ $$;`);
createSQL: [
`\
CREATE ${orReplaceStr}VIEW "${table.name}" AS (
${compileRule(definitionAbstractSql as AbstractSqlQuery, engine, true).replace(
/^/gm,
' ',
)}
${compileRule(
definitionAbstractSql as AbstractSqlQuery,
engine,
true,
abstractSqlModel,
).replace(/^/gm, ' ')}
);`,
],
dropSQL: [`DROP VIEW "${table.name}";`],
Expand Down Expand Up @@ -785,6 +794,7 @@ $$;`);
check.abstractSql as AbstractSqlQuery,
engine,
true,
abstractSqlModel,
);
createSqlElements.push(`\
${comment}${constraintName}CHECK (${sql})`);
Expand Down Expand Up @@ -910,6 +920,8 @@ CREATE TABLE ${ifNotExistsStr}"${table.name}" (
const { query: ruleSQL, bindings: ruleBindings } = compileRule(
ruleBody,
engine,
undefined,
abstractSqlModel,
) as SqlResult;
let referencedFields: ReferencedFields | undefined;
try {
Expand Down Expand Up @@ -941,8 +953,10 @@ const generateExport = (engine: Engines, ifNotExists: boolean) => {
return {
optimizeSchema,
compileSchema: _.partial(compileSchema, _, engine, ifNotExists),
compileRule: (abstractSQL: AbstractSqlQuery) =>
compileRule(abstractSQL, engine, false),
compileRule: (
abstractSQL: AbstractSqlQuery,
abstractSqlModel?: AbstractSqlModel,
) => compileRule(abstractSQL, engine, false, abstractSqlModel),
dataTypeValidate,
getReferencedFields,
getModifiedFields,
Expand Down
51 changes: 51 additions & 0 deletions src/AbstractSQLOptimiser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ import * as _ from 'lodash';

import { Dictionary } from 'lodash';
import {
AbstractSqlModel,
AbstractSqlQuery,
AbstractSqlType,
AliasNode,
DurationNode,
ReferencedFieldNode,
ReplaceNode,
TableNode,
} from './AbstractSQLCompiler';
import * as AbstractSQLRules2SQL from './AbstractSQLRules2SQL';

Expand Down Expand Up @@ -35,7 +39,9 @@ const escapeForLike = (str: AbstractSqlType): ReplaceNode => [
];

let helped = false;
let aliases: { [alias: string]: string } = {};
let noBinds = false;
let abstractSqlModel: AbstractSqlModel | undefined;
const Helper = <F extends (...args: any[]) => any>(fn: F) => {
return (...args: Parameters<F>): ReturnType<F> => {
const result = fn(...args);
Expand Down Expand Up @@ -454,6 +460,10 @@ const typeRules: Dictionary<MatchFn> = {
},
From: (args) => {
checkArgs('From', args, 1);
const maybeAlias = args[0] as AliasNode<TableNode>;
if (maybeAlias[0] === 'Alias' && maybeAlias[1][0] === 'Table') {
aliases[maybeAlias[2]] = maybeAlias[1][1];
}
return ['From', MaybeAlias(args[0] as AbstractSqlQuery, FromMatch)];
},
Join: JoinMatch('Join'),
Expand Down Expand Up @@ -933,6 +943,25 @@ const typeRules: Dictionary<MatchFn> = {
return ['Duration', duration] as AbstractSqlQuery;
},
Exists: tryMatches(
Helper<OptimisationMatchFn>((args) => {
if (abstractSqlModel == null) {
return false;
}
checkArgs('Exists', args, 1);
const arg = getAbstractSqlQuery(args, 0);
switch (arg[0]) {
case 'ReferencedField':
const [, aliasName, fieldName] = arg as ReferencedFieldNode;
const tableName = aliases[aliasName] ?? aliasName;
const table = abstractSqlModel.tables[tableName];
const field = table?.fields.find((f) => f.fieldName === fieldName);
if (field?.required === true) {
return ['Boolean', true] as AbstractSqlQuery;
}
default:
return false;
}
}),
Helper<OptimisationMatchFn>((args) => {
checkArgs('Exists', args, 1);
const arg = getAbstractSqlQuery(args, 0);
Expand All @@ -955,6 +984,25 @@ const typeRules: Dictionary<MatchFn> = {
},
),
NotExists: tryMatches(
Helper<OptimisationMatchFn>((args) => {
if (abstractSqlModel == null) {
return false;
}
checkArgs('Exists', args, 1);
const arg = getAbstractSqlQuery(args, 0);
switch (arg[0]) {
case 'ReferencedField':
const [, aliasName, fieldName] = arg as ReferencedFieldNode;
const tableName = aliases[aliasName] ?? aliasName;
const table = abstractSqlModel.tables[tableName];
const field = table?.fields.find((f) => f.fieldName === fieldName);
if (field?.required === true) {
return ['Boolean', false] as AbstractSqlQuery;
}
default:
return false;
}
}),
Helper<OptimisationMatchFn>((args) => {
checkArgs('Exists', args, 1);
const arg = getAbstractSqlQuery(args, 0);
Expand Down Expand Up @@ -1272,10 +1320,13 @@ const typeRules: Dictionary<MatchFn> = {
export const AbstractSQLOptimiser = (
abstractSQL: AbstractSqlQuery,
$noBinds = false,
$abstractSqlModel?: AbstractSqlModel,
): AbstractSqlQuery => {
noBinds = $noBinds;
abstractSqlModel = $abstractSqlModel;
do {
helped = false;
aliases = {};
const [type, ...rest] = abstractSQL;
switch (type) {
case 'SelectQuery':
Expand Down
2 changes: 1 addition & 1 deletion src/AbstractSQLSchemaOptimiser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export const optimizeSchema = (
}

// Optimize the rule body, this also normalizes it making the check constraint check easier
ruleBody = AbstractSQLOptimiser(ruleBody, true);
ruleBody = AbstractSQLOptimiser(ruleBody, true, abstractSqlModel);

const count = countFroms(ruleBody);
if (
Expand Down
81 changes: 77 additions & 4 deletions test/abstract-sql/schema-rule-to-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,80 @@ CREATE TABLE IF NOT EXISTS "test" (
"id" INTEGER NULL PRIMARY KEY
, -- It is necessary that each test has an id that is greater than 0.
CONSTRAINT "test$hkEwz3pzAqalNu6crijhhdWJ0ffUvqRGK8rMkQbViPg=" CHECK (0 < "id"
AND "id" IS NOT NULL)
AND \"id\" IS NOT NULL)
);`,
]);
});

it('should optimize null checks for a required field', () => {
expect(
generateSchema({
synonyms: {},
relationships: {},
tables: {
test: {
name: 'test',
resourceName: 'test',
idField: 'id',
fields: [
{
fieldName: 'id',
dataType: 'Integer',
required: true,
index: 'PRIMARY KEY',
},
],
indexes: [],
primitive: false,
},
},
rules: [
[
'Rule',
[
'Body',
[
'Not',
[
'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,
[
'StructuredEnglish',
'It is necessary that each test has an id that is greater than 0.',
],
],
],
}),
)
.to.have.property('createSchema')
.that.deep.equals([
`\
CREATE TABLE IF NOT EXISTS "test" (
"id" INTEGER NOT NULL PRIMARY KEY
, -- It is necessary that each test has an id that is greater than 0.
CONSTRAINT "test$TIITyGYLwuTGGJjwAk8awbiE/hnw6y8rue+hQ8Pp7as=" CHECK (0 < "id")
);`,
]);
});
Expand All @@ -95,6 +168,7 @@ it('should correctly shorten a converted check rule with a long name', () => {
{
fieldName: 'id',
dataType: 'Integer',
required: true,
index: 'PRIMARY KEY',
},
],
Expand Down Expand Up @@ -163,10 +237,9 @@ it('should correctly shorten a converted check rule with a long name', () => {
.that.deep.equals([
`\
CREATE TABLE IF NOT EXISTS "test_table_with_very_very_long_name" (
"id" INTEGER NULL PRIMARY KEY
"id" INTEGER NOT NULL PRIMARY KEY
, -- It is necessary that each test_table_with_very_very_long_name has an id that is greater than 0.
CONSTRAINT "test_table_with_very_very_long$9z+XEkP4EI1mhDQ8SiLulo2NLmenGY1C" CHECK (0 < "id"
AND "id" IS NOT NULL)
CONSTRAINT "test_table_with_very_very_long$/rDs8gDAB2Zoc7woBPozVMLKpx9jNTNa" CHECK (0 < "id")
);`,
]);
});
Loading

0 comments on commit 5503416

Please sign in to comment.