@balena/abstract-sql-compiler 10.2.7 → 10.2.8-build-lodash-08b051cf4150dfa99f3cfafeb3011244760e9d38-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.versionbot/CHANGELOG.yml +77 -1
- package/CHANGELOG.md +12 -0
- package/out/AbstractSQLCompiler.d.ts +6 -7
- package/out/AbstractSQLCompiler.js +7 -2
- package/out/AbstractSQLCompiler.js.map +1 -1
- package/out/AbstractSQLOptimiser.js +23 -29
- package/out/AbstractSQLOptimiser.js.map +1 -1
- package/out/AbstractSQLRules2SQL.js +9 -13
- package/out/AbstractSQLRules2SQL.js.map +1 -1
- package/out/AbstractSQLSchemaOptimiser.js +3 -3
- package/out/AbstractSQLSchemaOptimiser.js.map +1 -1
- package/out/referenced-fields.js +10 -10
- package/out/referenced-fields.js.map +1 -1
- package/package.json +3 -5
- package/src/AbstractSQLCompiler.ts +24 -14
- package/src/AbstractSQLOptimiser.ts +54 -38
- package/src/AbstractSQLRules2SQL.ts +11 -18
- package/src/AbstractSQLSchemaOptimiser.ts +3 -4
- package/src/referenced-fields.ts +10 -7
@@ -1,6 +1,3 @@
|
|
1
|
-
import _ from 'lodash';
|
2
|
-
|
3
|
-
import type { Dictionary } from 'lodash';
|
4
1
|
import type {
|
5
2
|
AbstractSqlQuery,
|
6
3
|
AbstractSqlType,
|
@@ -107,6 +104,7 @@ import type {
|
|
107
104
|
StartsWithNode,
|
108
105
|
EscapeForLikeNode,
|
109
106
|
EqualsAnyNode,
|
107
|
+
NotInNode,
|
110
108
|
} from './AbstractSQLCompiler';
|
111
109
|
import { isFieldTypeNode } from './AbstractSQLCompiler';
|
112
110
|
import * as AbstractSQLRules2SQL from './AbstractSQLRules2SQL';
|
@@ -125,6 +123,8 @@ type OptimisationMatchFn<T extends AnyTypeNodes> =
|
|
125
123
|
type MetaMatchFn<T extends AnyTypeNodes> = (args: AbstractSqlQuery) => T;
|
126
124
|
type MatchFn<T extends AnyTypeNodes> = (args: AbstractSqlType[]) => T;
|
127
125
|
|
126
|
+
const identity = <T>(x: T): T => x;
|
127
|
+
|
128
128
|
let helped = false;
|
129
129
|
let noBinds = false;
|
130
130
|
const Helper = <F extends (...args: any[]) => any>(fn: F) => {
|
@@ -328,9 +328,8 @@ const AnyNotNullValue = (args: any): boolean => {
|
|
328
328
|
return args != null && args !== 'Null' && args[0] !== 'Null';
|
329
329
|
};
|
330
330
|
|
331
|
-
const FieldOp =
|
332
|
-
(
|
333
|
-
(args) => {
|
331
|
+
const FieldOp = <T extends string>(type: T) =>
|
332
|
+
((args) => {
|
334
333
|
if (
|
335
334
|
AnyNotNullValue(args[0]) === false ||
|
336
335
|
AnyNotNullValue(args[1]) === false
|
@@ -344,7 +343,7 @@ const FieldOp =
|
|
344
343
|
} else {
|
345
344
|
return false;
|
346
345
|
}
|
347
|
-
}
|
346
|
+
}) satisfies OptimisationMatchFn<AnyTypeNodes>;
|
348
347
|
const FieldEquals = FieldOp('Equals');
|
349
348
|
const FieldNotEquals = FieldOp('NotEquals');
|
350
349
|
|
@@ -420,7 +419,7 @@ const ConcatenateWithSeparator: MatchFn<ConcatenateWithSeparatorNode> = (
|
|
420
419
|
];
|
421
420
|
};
|
422
421
|
|
423
|
-
const Text = matchArgs<TextNode>('Text',
|
422
|
+
const Text = matchArgs<TextNode>('Text', identity);
|
424
423
|
|
425
424
|
const Value = (arg: string | AbstractSqlQuery): ValuesNodeTypes => {
|
426
425
|
switch (arg) {
|
@@ -707,20 +706,20 @@ const typeRules = {
|
|
707
706
|
},
|
708
707
|
Average: matchArgs('Average', NumericValue),
|
709
708
|
Sum: matchArgs('Sum', NumericValue),
|
710
|
-
Field: matchArgs<FieldNode>('Field',
|
709
|
+
Field: matchArgs<FieldNode>('Field', identity),
|
711
710
|
ReferencedField: matchArgs<ReferencedFieldNode>(
|
712
711
|
'ReferencedField',
|
713
|
-
|
714
|
-
|
712
|
+
identity,
|
713
|
+
identity,
|
715
714
|
),
|
716
|
-
Cast: matchArgs<CastNode>('Cast', AnyValue,
|
715
|
+
Cast: matchArgs<CastNode>('Cast', AnyValue, identity),
|
717
716
|
// eslint-disable-next-line id-denylist
|
718
717
|
Number: NumberMatch('Number'),
|
719
718
|
Real: NumberMatch('Real'),
|
720
719
|
Integer: NumberMatch('Integer'),
|
721
720
|
// eslint-disable-next-line id-denylist
|
722
|
-
Boolean: matchArgs<BooleanNode>('Boolean',
|
723
|
-
EmbeddedText: matchArgs('EmbeddedText',
|
721
|
+
Boolean: matchArgs<BooleanNode>('Boolean', identity),
|
722
|
+
EmbeddedText: matchArgs('EmbeddedText', identity),
|
724
723
|
Null: matchArgs<NullNode>('Null'),
|
725
724
|
CurrentTimestamp: matchArgs<CurrentTimestampNode>('CurrentTimestamp'),
|
726
725
|
CurrentDate: matchArgs<CurrentDateNode>('CurrentDate'),
|
@@ -903,7 +902,7 @@ const typeRules = {
|
|
903
902
|
return ['TextArray', ...args.map(TextValue)];
|
904
903
|
},
|
905
904
|
ToJSON: matchArgs<ToJSONNode>('ToJSON', AnyValue),
|
906
|
-
Any: matchArgs<AnyNode>('Any', AnyValue,
|
905
|
+
Any: matchArgs<AnyNode>('Any', AnyValue, identity),
|
907
906
|
Coalesce: (args): CoalesceNode => {
|
908
907
|
checkMinArgs('Coalesce', args, 2);
|
909
908
|
return [
|
@@ -956,7 +955,7 @@ const typeRules = {
|
|
956
955
|
checkMinArgs('And', args, 2);
|
957
956
|
// Collapse nested ANDs.
|
958
957
|
let maybeHelped = false;
|
959
|
-
const conditions =
|
958
|
+
const conditions = args.flatMap((arg) => {
|
960
959
|
if (!isAbstractSqlQuery(arg)) {
|
961
960
|
throw new SyntaxError(
|
962
961
|
`Expected AbstractSqlQuery array but got ${typeof arg}`,
|
@@ -1002,7 +1001,17 @@ const typeRules = {
|
|
1002
1001
|
Helper<OptimisationMatchFn<AndNode>>((args) => {
|
1003
1002
|
checkMinArgs('And', args, 2);
|
1004
1003
|
// Optimise id != 1 AND id != 2 AND id != 3 -> id NOT IN [1, 2, 3]
|
1005
|
-
const fieldBuckets:
|
1004
|
+
const fieldBuckets: Record<
|
1005
|
+
string,
|
1006
|
+
Array<
|
1007
|
+
[
|
1008
|
+
'NotEquals' | 'NotIn',
|
1009
|
+
FieldNode | ReferencedFieldNode,
|
1010
|
+
AbstractSqlType,
|
1011
|
+
...AbstractSqlType[],
|
1012
|
+
]
|
1013
|
+
>
|
1014
|
+
> = {};
|
1006
1015
|
const others: AnyTypeNodes[] = [];
|
1007
1016
|
let maybeHelped = false;
|
1008
1017
|
args.map((arg) => {
|
@@ -1014,7 +1023,7 @@ const typeRules = {
|
|
1014
1023
|
if (arg[0] === 'NotEquals') {
|
1015
1024
|
const fieldBool = FieldNotEquals(arg.slice(1));
|
1016
1025
|
if (fieldBool !== false) {
|
1017
|
-
const fieldRef = fieldBool[1]
|
1026
|
+
const fieldRef = `${fieldBool[1]}`;
|
1018
1027
|
if (fieldBuckets[fieldRef] == null) {
|
1019
1028
|
fieldBuckets[fieldRef] = [fieldBool];
|
1020
1029
|
} else {
|
@@ -1025,13 +1034,13 @@ const typeRules = {
|
|
1025
1034
|
return;
|
1026
1035
|
}
|
1027
1036
|
} else if (arg[0] === 'NotIn') {
|
1028
|
-
const fieldRef = arg[1]
|
1037
|
+
const fieldRef = `${arg[1]}`;
|
1029
1038
|
if (fieldBuckets[fieldRef] == null) {
|
1030
|
-
fieldBuckets[fieldRef] = [arg];
|
1039
|
+
fieldBuckets[fieldRef] = [arg as NotInNode];
|
1031
1040
|
} else {
|
1032
1041
|
// We're adding a second match, so that means we can optimise
|
1033
1042
|
maybeHelped = true;
|
1034
|
-
fieldBuckets[fieldRef].push(arg);
|
1043
|
+
fieldBuckets[fieldRef].push(arg as NotInNode);
|
1035
1044
|
}
|
1036
1045
|
return;
|
1037
1046
|
}
|
@@ -1049,7 +1058,7 @@ const typeRules = {
|
|
1049
1058
|
return [
|
1050
1059
|
'NotIn',
|
1051
1060
|
fieldBucket[0][1],
|
1052
|
-
...
|
1061
|
+
...fieldBucket.flatMap((field) => field.slice(2)),
|
1053
1062
|
];
|
1054
1063
|
}
|
1055
1064
|
});
|
@@ -1081,7 +1090,7 @@ const typeRules = {
|
|
1081
1090
|
checkMinArgs('Or', args, 2);
|
1082
1091
|
// Collapse nested ORs.
|
1083
1092
|
let maybeHelped = false;
|
1084
|
-
const conditions =
|
1093
|
+
const conditions = args.flatMap((arg) => {
|
1085
1094
|
if (!isAbstractSqlQuery(arg)) {
|
1086
1095
|
throw new SyntaxError(
|
1087
1096
|
`Expected AbstractSqlQuery array but got ${typeof arg}`,
|
@@ -1127,7 +1136,17 @@ const typeRules = {
|
|
1127
1136
|
Helper<OptimisationMatchFn<InNode | OrNode>>((args) => {
|
1128
1137
|
checkMinArgs('Or', args, 2);
|
1129
1138
|
// Optimise id = 1 OR id = 2 OR id = 3 -> id IN [1, 2, 3]
|
1130
|
-
const fieldBuckets:
|
1139
|
+
const fieldBuckets: Record<
|
1140
|
+
string,
|
1141
|
+
Array<
|
1142
|
+
[
|
1143
|
+
'Equals' | 'In',
|
1144
|
+
FieldNode | ReferencedFieldNode,
|
1145
|
+
AbstractSqlType,
|
1146
|
+
...AbstractSqlType[],
|
1147
|
+
]
|
1148
|
+
>
|
1149
|
+
> = {};
|
1131
1150
|
const others: AnyTypeNodes[] = [];
|
1132
1151
|
let maybeHelped = false;
|
1133
1152
|
args.map((arg) => {
|
@@ -1139,7 +1158,7 @@ const typeRules = {
|
|
1139
1158
|
if (arg[0] === 'Equals') {
|
1140
1159
|
const fieldBool = FieldEquals(arg.slice(1));
|
1141
1160
|
if (fieldBool !== false) {
|
1142
|
-
const fieldRef = fieldBool[1]
|
1161
|
+
const fieldRef = `${fieldBool[1]}`;
|
1143
1162
|
if (fieldBuckets[fieldRef] == null) {
|
1144
1163
|
fieldBuckets[fieldRef] = [fieldBool];
|
1145
1164
|
} else {
|
@@ -1150,13 +1169,13 @@ const typeRules = {
|
|
1150
1169
|
return;
|
1151
1170
|
}
|
1152
1171
|
} else if (arg[0] === 'In') {
|
1153
|
-
const fieldRef = arg[1]
|
1172
|
+
const fieldRef = `${arg[1]}`;
|
1154
1173
|
if (fieldBuckets[fieldRef] == null) {
|
1155
|
-
fieldBuckets[fieldRef] = [arg];
|
1174
|
+
fieldBuckets[fieldRef] = [arg as InNode];
|
1156
1175
|
} else {
|
1157
1176
|
// We're adding a second match, so that means we can optimise
|
1158
1177
|
maybeHelped = true;
|
1159
|
-
fieldBuckets[fieldRef].push(arg);
|
1178
|
+
fieldBuckets[fieldRef].push(arg as InNode);
|
1160
1179
|
}
|
1161
1180
|
return;
|
1162
1181
|
}
|
@@ -1174,7 +1193,7 @@ const typeRules = {
|
|
1174
1193
|
return [
|
1175
1194
|
'In',
|
1176
1195
|
fieldBucket[0][1],
|
1177
|
-
...
|
1196
|
+
...fieldBucket.flatMap((field) => field.slice(2)),
|
1178
1197
|
];
|
1179
1198
|
}
|
1180
1199
|
});
|
@@ -1212,24 +1231,21 @@ const typeRules = {
|
|
1212
1231
|
),
|
1213
1232
|
Text,
|
1214
1233
|
Value: Text,
|
1215
|
-
Date: matchArgs('Date',
|
1234
|
+
Date: matchArgs('Date', identity),
|
1216
1235
|
Duration: (args): DurationNode => {
|
1217
1236
|
checkArgs('Duration', args, 1);
|
1218
1237
|
|
1219
|
-
|
1238
|
+
const duration = args[0] as DurationNode[1];
|
1220
1239
|
if (duration == null || typeof duration !== 'object') {
|
1221
1240
|
throw new SyntaxError(
|
1222
1241
|
`Duration must be an object, got ${typeof duration}`,
|
1223
1242
|
);
|
1224
1243
|
}
|
1225
|
-
|
1226
|
-
|
1227
|
-
.omitBy(_.isNil)
|
1228
|
-
.value();
|
1229
|
-
if (_(duration).omit('negative').isEmpty()) {
|
1244
|
+
const { negative, day, hour, minute, second } = duration;
|
1245
|
+
if (day == null && hour == null && minute == null && second == null) {
|
1230
1246
|
throw new SyntaxError('Invalid duration');
|
1231
1247
|
}
|
1232
|
-
return ['Duration',
|
1248
|
+
return ['Duration', { negative, day, hour, minute, second }];
|
1233
1249
|
},
|
1234
1250
|
Exists: tryMatches<ExistsNode | BooleanNode>(
|
1235
1251
|
Helper<OptimisationMatchFn<BooleanNode>>((args) => {
|
@@ -1582,7 +1598,7 @@ const typeRules = {
|
|
1582
1598
|
],
|
1583
1599
|
),
|
1584
1600
|
),
|
1585
|
-
} satisfies
|
1601
|
+
} satisfies Record<string, MatchFn<AnyTypeNodes>>;
|
1586
1602
|
|
1587
1603
|
export const AbstractSQLOptimiser = (
|
1588
1604
|
abstractSQL: AbstractSqlQuery,
|
@@ -1,8 +1,4 @@
|
|
1
|
-
import _ from 'lodash';
|
2
|
-
|
3
1
|
import sbvrTypes from '@balena/sbvr-types';
|
4
|
-
|
5
|
-
import type { Dictionary } from 'lodash';
|
6
2
|
import type {
|
7
3
|
AbstractSqlQuery,
|
8
4
|
AbstractSqlType,
|
@@ -36,7 +32,7 @@ type MetaMatchFn = (args: AbstractSqlQuery, indent: string) => string;
|
|
36
32
|
type MatchFn = (args: AbstractSqlType[], indent: string) => string;
|
37
33
|
|
38
34
|
let fieldOrderings: Binding[] = [];
|
39
|
-
let fieldOrderingsLookup:
|
35
|
+
let fieldOrderingsLookup: Record<string, number> = {};
|
40
36
|
let engine: Engines = Engines.postgres;
|
41
37
|
let noBinds = false;
|
42
38
|
|
@@ -702,7 +698,7 @@ const AddBind = (bind: Binding): string => {
|
|
702
698
|
}
|
703
699
|
};
|
704
700
|
|
705
|
-
const typeRules:
|
701
|
+
const typeRules: Record<string, MatchFn> = {
|
706
702
|
UnionQuery: (args, indent) => {
|
707
703
|
checkMinArgs('UnionQuery', args, 2);
|
708
704
|
return args
|
@@ -1340,31 +1336,28 @@ const typeRules: Dictionary<MatchFn> = {
|
|
1340
1336
|
throw new SyntaxError('Durations not supported on: ' + engine);
|
1341
1337
|
}
|
1342
1338
|
// TODO: The abstract sql type should accommodate this
|
1343
|
-
|
1339
|
+
const duration = args[0] as DurationNode[1];
|
1344
1340
|
if (duration == null || typeof duration !== 'object') {
|
1345
1341
|
throw new SyntaxError(
|
1346
1342
|
`Duration must be an object, got ${typeof duration}`,
|
1347
1343
|
);
|
1348
1344
|
}
|
1349
|
-
|
1350
|
-
|
1351
|
-
.omitBy(_.isNil)
|
1352
|
-
.value() as Dictionary<string>;
|
1353
|
-
if (_(duration).omit('negative').isEmpty()) {
|
1345
|
+
const { negative, day, hour, minute, second } = duration;
|
1346
|
+
if (day == null && hour == null && minute == null && second == null) {
|
1354
1347
|
throw new SyntaxError('Invalid duration');
|
1355
1348
|
}
|
1356
1349
|
return (
|
1357
1350
|
"INTERVAL '" +
|
1358
|
-
(
|
1359
|
-
(
|
1351
|
+
(negative ? '-' : '') +
|
1352
|
+
(day ?? '0') +
|
1360
1353
|
' ' +
|
1361
|
-
(
|
1362
|
-
(
|
1354
|
+
(negative ? '-' : '') +
|
1355
|
+
(hour ?? '0') +
|
1363
1356
|
':' +
|
1364
|
-
(
|
1357
|
+
(minute ?? '0') +
|
1365
1358
|
':' +
|
1366
1359
|
// Force seconds to be at least 0.0 - required for mysql
|
1367
|
-
Number(
|
1360
|
+
Number(second ?? 0).toLocaleString('en', {
|
1368
1361
|
minimumFractionDigits: 1,
|
1369
1362
|
}) +
|
1370
1363
|
"'" +
|
@@ -7,7 +7,6 @@ export const enum Engines {
|
|
7
7
|
import { AbstractSQLOptimiser } from './AbstractSQLOptimiser';
|
8
8
|
export { Binding, SqlResult } from './AbstractSQLRules2SQL';
|
9
9
|
import sbvrTypes from '@balena/sbvr-types';
|
10
|
-
import _ from 'lodash';
|
11
10
|
import type {
|
12
11
|
AbstractSqlModel,
|
13
12
|
AbstractSqlQuery,
|
@@ -80,7 +79,8 @@ export const optimizeSchema = (
|
|
80
79
|
count === 1 &&
|
81
80
|
(ruleBody[0] === 'NotExists' ||
|
82
81
|
(ruleBody[0] === 'Equals' &&
|
83
|
-
|
82
|
+
ruleBody[2][0] === 'Number' &&
|
83
|
+
ruleBody[2][1] === 0)) &&
|
84
84
|
isSelectQueryNode(ruleBody[1])
|
85
85
|
) {
|
86
86
|
const selectQueryNodes = ruleBody[1].slice(1);
|
@@ -120,8 +120,7 @@ export const optimizeSchema = (
|
|
120
120
|
convertReferencedFieldsToFields(whereNode);
|
121
121
|
|
122
122
|
const tableName = fromNode[1];
|
123
|
-
const table =
|
124
|
-
abstractSqlModel.tables,
|
123
|
+
const table = Object.values(abstractSqlModel.tables).find(
|
125
124
|
(t) => t.name === tableName,
|
126
125
|
);
|
127
126
|
if (table) {
|
package/src/referenced-fields.ts
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
import _ from 'lodash';
|
2
1
|
import type {
|
3
2
|
AbstractSqlQuery,
|
4
3
|
AbstractSqlType,
|
@@ -76,7 +75,11 @@ export const getReferencedFields: EngineInstance['getReferencedFields'] = (
|
|
76
75
|
) => {
|
77
76
|
const referencedFields = getRuleReferencedFields(ruleBody);
|
78
77
|
|
79
|
-
|
78
|
+
const result: { [key: string]: string[] } = {};
|
79
|
+
for (const key of Object.keys(referencedFields)) {
|
80
|
+
result[key] = [...new Set(referencedFields[key].update)];
|
81
|
+
}
|
82
|
+
return result;
|
80
83
|
};
|
81
84
|
|
82
85
|
export interface RuleReferencedFields {
|
@@ -267,7 +270,8 @@ export const getRuleReferencedFields: EngineInstance['getRuleReferencedFields']
|
|
267
270
|
const referencedFields: RuleReferencedFields = {};
|
268
271
|
if (
|
269
272
|
ruleBody[0] === 'Equals' &&
|
270
|
-
|
273
|
+
ruleBody[2][0] === 'Number' &&
|
274
|
+
ruleBody[2][1] === 0 &&
|
271
275
|
isSelectQueryNode(ruleBody[1])
|
272
276
|
) {
|
273
277
|
const select = ruleBody[1].find(isSelectNode)!;
|
@@ -281,7 +285,7 @@ export const getRuleReferencedFields: EngineInstance['getRuleReferencedFields']
|
|
281
285
|
for (const method of Object.keys(tableRefs) as Array<
|
282
286
|
keyof typeof tableRefs
|
283
287
|
>) {
|
284
|
-
tableRefs[method] =
|
288
|
+
tableRefs[method] = [...new Set(tableRefs[method])];
|
285
289
|
}
|
286
290
|
}
|
287
291
|
|
@@ -321,10 +325,9 @@ const checkQuery = (query: AbstractSqlQuery): ModifiedFields | undefined => {
|
|
321
325
|
return { table: tableName, action: 'delete' };
|
322
326
|
}
|
323
327
|
|
324
|
-
const fields =
|
328
|
+
const fields = query
|
325
329
|
.filter((v): v is FieldsNode => v != null && v[0] === 'Fields')
|
326
|
-
.flatMap((v) => v[1])
|
327
|
-
.value();
|
330
|
+
.flatMap((v) => v[1]);
|
328
331
|
return { table: tableName, action: 'update', fields };
|
329
332
|
};
|
330
333
|
export const getModifiedFields: EngineInstance['getModifiedFields'] = (
|