diff --git a/src/AbstractSQLCompiler.ts b/src/AbstractSQLCompiler.ts index 6ea19e70..48b22596 100644 --- a/src/AbstractSQLCompiler.ts +++ b/src/AbstractSQLCompiler.ts @@ -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: @@ -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); } @@ -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}";`], @@ -785,6 +794,7 @@ $$;`); check.abstractSql as AbstractSqlQuery, engine, true, + abstractSqlModel, ); createSqlElements.push(`\ ${comment}${constraintName}CHECK (${sql})`); @@ -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 { @@ -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, diff --git a/src/AbstractSQLOptimiser.ts b/src/AbstractSQLOptimiser.ts index eb0cfae6..8acc5c62 100644 --- a/src/AbstractSQLOptimiser.ts +++ b/src/AbstractSQLOptimiser.ts @@ -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'; @@ -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 = any>(fn: F) => { return (...args: Parameters): ReturnType => { const result = fn(...args); @@ -436,6 +442,10 @@ const typeRules: Dictionary = { }, From: (args) => { checkArgs('From', args, 1); + const maybeAlias = args[0] as AliasNode; + 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'), @@ -898,30 +908,72 @@ const typeRules: Dictionary = { } return ['Duration', duration] as AbstractSqlQuery; }, - Exists: (args) => { - checkArgs('Exists', args, 1); - const arg = getAbstractSqlQuery(args, 0); - const [type, ...rest] = arg; - switch (type) { - case 'SelectQuery': - case 'UnionQuery': - return ['Exists', typeRules[type](rest)]; - default: - return ['Exists', AnyValue(arg)]; - } - }, - NotExists: (args) => { - checkArgs('NotExists', args, 1); - const arg = getAbstractSqlQuery(args, 0); - const [type, ...rest] = arg; - switch (type) { - case 'SelectQuery': - case 'UnionQuery': - return ['NotExists', typeRules[type](rest)]; - default: - return ['NotExists', AnyValue(arg)]; - } - }, + Exists: tryMatches( + Helper((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; + } + }), + (args) => { + checkArgs('Exists', args, 1); + const arg = getAbstractSqlQuery(args, 0); + const [type, ...rest] = arg; + switch (type) { + case 'SelectQuery': + case 'UnionQuery': + return ['Exists', typeRules[type](rest)]; + default: + return ['Exists', AnyValue(arg)]; + } + }, + ), + NotExists: tryMatches( + Helper((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; + } + }), + (args) => { + checkArgs('NotExists', args, 1); + const arg = getAbstractSqlQuery(args, 0); + const [type, ...rest] = arg; + switch (type) { + case 'SelectQuery': + case 'UnionQuery': + return ['NotExists', typeRules[type](rest)]; + default: + return ['NotExists', AnyValue(arg)]; + } + }, + ), Not: tryMatches( Helper((args) => { checkArgs('Not', args, 1); @@ -1218,10 +1270,13 @@ const typeRules: Dictionary = { 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': diff --git a/src/AbstractSQLSchemaOptimiser.ts b/src/AbstractSQLSchemaOptimiser.ts index 06c2ca80..1538b635 100644 --- a/src/AbstractSQLSchemaOptimiser.ts +++ b/src/AbstractSQLSchemaOptimiser.ts @@ -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 ( diff --git a/test/abstract-sql/schema-rule-to-check.ts b/test/abstract-sql/schema-rule-to-check.ts index 05a0507b..2d252cf7 100644 --- a/test/abstract-sql/schema-rule-to-check.ts +++ b/test/abstract-sql/schema-rule-to-check.ts @@ -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") );`, ]); }); @@ -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', }, ], @@ -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") );`, ]); }); diff --git a/test/sbvr/pilots.js b/test/sbvr/pilots.js index 6f70a7a0..f40e4283 100644 --- a/test/sbvr/pilots.js +++ b/test/sbvr/pilots.js @@ -298,8 +298,7 @@ SELECT NOT EXISTS ( SELECT COUNT(*) FROM "pilot" AS "pilot.1", "pilot-can fly-plane" AS "pilot.1-can fly-plane.0" - WHERE "pilot.1"."name" IS NOT NULL - AND "pilot.1-can fly-plane.0"."pilot" = "pilot.1"."id" + WHERE "pilot.1-can fly-plane.0"."pilot" = "pilot.1"."id" AND "pilot.1-can fly-plane.0"."can fly-plane" = "plane.0"."id" ) >= 3 AND "plane.0"."name" IS NULL @@ -314,7 +313,6 @@ SELECT NOT EXISTS ( FROM "pilot" AS "pilot.0" WHERE NOT ( 0 < "pilot.0"."years of experience" - AND "pilot.0"."years of experience" IS NOT NULL ) ) AS "result";`, ); @@ -347,8 +345,7 @@ SELECT NOT EXISTS ( FROM "pilot-can fly-plane" AS "pilot.0-can fly-plane.1" WHERE "pilot.0-can fly-plane.1"."pilot" = "pilot.0"."id" ) >= 2 - OR 5 < "pilot.0"."years of experience" - AND "pilot.0"."years of experience" IS NOT NULL) + OR 5 < "pilot.0"."years of experience") ) ) AS "result";`, ); @@ -367,7 +364,6 @@ SELECT NOT EXISTS ( ) >= 2) AND NOT ( 5 < "pilot.0"."years of experience" - AND "pilot.0"."years of experience" IS NOT NULL ) ) AS "result";`, ); @@ -391,7 +387,6 @@ SELECT NOT EXISTS ( ) = 1) AND NOT ( 5 < "pilot.0"."years of experience" - AND "pilot.0"."years of experience" IS NOT NULL ) ) AS "result";`, ); @@ -496,7 +491,6 @@ SELECT NOT EXISTS ( WHERE "pilot.0-can fly-plane.1"."pilot" = "pilot.0"."id" ) >= 2 AND 5 < "pilot.0"."years of experience" - AND "pilot.0"."years of experience" IS NOT NULL ) ) AS "result";`, ); @@ -515,7 +509,6 @@ SELECT NOT EXISTS ( ) >= 2 AND NOT ( 5 < "pilot.0"."years of experience" - AND "pilot.0"."years of experience" IS NOT NULL ) ) AS "result";`, ); @@ -625,7 +618,6 @@ SELECT NOT EXISTS ( ) = 1) AND NOT ( 5 < "pilot.0"."years of experience" - AND "pilot.0"."years of experience" IS NOT NULL ) ) AS "result";`, ); @@ -649,7 +641,6 @@ SELECT NOT EXISTS ( AND "pilot.0"."is experienced" = 1) AND NOT ( 5 < "pilot.0"."years of experience" - AND "pilot.0"."years of experience" IS NOT NULL ) ) AS "result";`, ); @@ -676,7 +667,6 @@ SELECT NOT EXISTS ( ) AND NOT ( 5 < "pilot.0"."years of experience" - AND "pilot.0"."years of experience" IS NOT NULL ) ) AS "result";`, ); @@ -700,7 +690,6 @@ SELECT NOT EXISTS ( ) = 1) AND NOT ( 5 < "pilot.0"."years of experience" - AND "pilot.0"."years of experience" IS NOT NULL ) ) AS "result";`, ); @@ -725,11 +714,9 @@ SELECT NOT EXISTS ( ) >= 11 ) OR 10 < LENGTH("pilot.0"."name") - AND LENGTH("pilot.0"."name") IS NOT NULL - AND "pilot.0"."name" IS NOT NULL) + AND LENGTH("pilot.0"."name") IS NOT NULL) AND NOT ( 5 < "pilot.0"."years of experience" - AND "pilot.0"."years of experience" IS NOT NULL ) ) AS "result";`, ); @@ -752,11 +739,9 @@ SELECT NOT EXISTS ( WHERE "pilot.0-can fly-plane.2"."pilot" = "pilot.0"."id" ) = 1 AND 10 < LENGTH("pilot.0"."name") - AND LENGTH("pilot.0"."name") IS NOT NULL - AND "pilot.0"."name" IS NOT NULL) + AND LENGTH("pilot.0"."name") IS NOT NULL) AND NOT ( 5 < "pilot.0"."years of experience" - AND "pilot.0"."years of experience" IS NOT NULL ) ) AS "result";`, ); @@ -779,11 +764,9 @@ SELECT NOT EXISTS ( WHERE "pilot.0-can fly-plane.2"."pilot" = "pilot.0"."id" ) = 1 AND 10 < LENGTH("pilot.0"."name") - AND LENGTH("pilot.0"."name") IS NOT NULL - AND "pilot.0"."name" IS NOT NULL) + AND LENGTH("pilot.0"."name") IS NOT NULL) AND NOT ( 5 < "pilot.0"."years of experience" - AND "pilot.0"."years of experience" IS NOT NULL ) ) AS "result";`, ); @@ -806,11 +789,9 @@ SELECT NOT EXISTS ( WHERE "pilot.0-can fly-plane.2"."pilot" = "pilot.0"."id" ) = 1 AND 10 < LENGTH("pilot.0"."name") - AND LENGTH("pilot.0"."name") IS NOT NULL - AND "pilot.0"."name" IS NOT NULL) + AND LENGTH("pilot.0"."name") IS NOT NULL) AND NOT ( 5 < "pilot.0"."years of experience" - AND "pilot.0"."years of experience" IS NOT NULL ) ) AS "result";`, ); @@ -839,7 +820,6 @@ SELECT NOT EXISTS ( ) >= 3 AND NOT ( 5 < "pilot.0"."years of experience" - AND "pilot.0"."years of experience" IS NOT NULL ) ) AS "result";`, ); @@ -870,7 +850,6 @@ SELECT NOT EXISTS ( ) >= 3) AND NOT ( 5 < "pilot.0"."years of experience" - AND "pilot.0"."years of experience" IS NOT NULL ) ) AS "result";`, ); @@ -890,8 +869,7 @@ SELECT NOT EXISTS ( ) >= 11 ) OR 10 < LENGTH("pilot.0"."name") - AND LENGTH("pilot.0"."name") IS NOT NULL - AND "pilot.0"."name" IS NOT NULL) + AND LENGTH("pilot.0"."name") IS NOT NULL) AND ( SELECT COUNT(*) FROM "pilot-can fly-plane" AS "pilot.0-can fly-plane.4" @@ -899,7 +877,6 @@ SELECT NOT EXISTS ( ) >= 3 AND NOT ( 5 < "pilot.0"."years of experience" - AND "pilot.0"."years of experience" IS NOT NULL ) ) AS "result";`, ); @@ -918,7 +895,6 @@ SELECT NOT EXISTS ( ) = 1 AND 10 < LENGTH("pilot.0"."name") AND LENGTH("pilot.0"."name") IS NOT NULL - AND "pilot.0"."name" IS NOT NULL OR ( SELECT COUNT(*) FROM "pilot-can fly-plane" AS "pilot.0-can fly-plane.4" @@ -926,7 +902,6 @@ SELECT NOT EXISTS ( ) >= 3) AND NOT ( 5 < "pilot.0"."years of experience" - AND "pilot.0"."years of experience" IS NOT NULL ) ) AS "result";`, ); @@ -957,7 +932,6 @@ SELECT NOT EXISTS ( ) >= 3 AND NOT ( 5 < "pilot.0"."years of experience" - AND "pilot.0"."years of experience" IS NOT NULL ) ) AS "result";`, ); @@ -988,7 +962,6 @@ SELECT NOT EXISTS ( ) >= 3) AND NOT ( 5 < "pilot.0"."years of experience" - AND "pilot.0"."years of experience" IS NOT NULL ) ) AS "result";`, );