@travetto/model-sql 7.0.0-rc.1 → 7.0.0-rc.3
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/package.json +7 -7
- package/src/config.ts +5 -5
- package/src/connection/base.ts +30 -30
- package/src/connection/decorator.ts +17 -17
- package/src/dialect/base.ts +225 -157
- package/src/service.ts +52 -59
- package/src/table-manager.ts +91 -61
- package/src/types.ts +1 -1
- package/src/util.ts +75 -75
- package/support/test/query.ts +2 -2
package/src/dialect/base.ts
CHANGED
|
@@ -9,13 +9,19 @@ import { DeleteWrapper, InsertWrapper, DialectState } from '../internal/types.ts
|
|
|
9
9
|
import { Connection } from '../connection/base.ts';
|
|
10
10
|
import { VisitStack } from '../types.ts';
|
|
11
11
|
|
|
12
|
-
const
|
|
12
|
+
const PointConcrete = toConcrete<Point>();
|
|
13
13
|
|
|
14
14
|
interface Alias {
|
|
15
15
|
alias: string;
|
|
16
16
|
path: VisitStack[];
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
export type SQLTableDescription = {
|
|
20
|
+
columns: { name: string, type: string, is_notnull: boolean }[];
|
|
21
|
+
foreignKeys: { name: string, from_column: string, to_column: string, to_table: string }[];
|
|
22
|
+
indices: { name: string, columns: { name: string, desc: boolean }[], is_unique: boolean }[];
|
|
23
|
+
};
|
|
24
|
+
|
|
19
25
|
@Schema()
|
|
20
26
|
class Total {
|
|
21
27
|
total: number;
|
|
@@ -24,7 +30,7 @@ class Total {
|
|
|
24
30
|
function makeField(name: string, type: Class, required: boolean, extra: Partial<SchemaFieldConfig>): SchemaFieldConfig {
|
|
25
31
|
return {
|
|
26
32
|
name,
|
|
27
|
-
|
|
33
|
+
class: null!,
|
|
28
34
|
type,
|
|
29
35
|
array: false,
|
|
30
36
|
...(required ? { required: { active: true } } : {}),
|
|
@@ -39,20 +45,20 @@ export abstract class SQLDialect implements DialectState {
|
|
|
39
45
|
/**
|
|
40
46
|
* Default length of unique ids
|
|
41
47
|
*/
|
|
42
|
-
|
|
48
|
+
ID_LENGTH = 32;
|
|
43
49
|
|
|
44
50
|
/**
|
|
45
51
|
* Hash Length
|
|
46
52
|
*/
|
|
47
|
-
|
|
53
|
+
HASH_LENGTH = 64;
|
|
48
54
|
|
|
49
55
|
/**
|
|
50
56
|
* Default length for varchar
|
|
51
57
|
*/
|
|
52
|
-
|
|
58
|
+
DEFAULT_STRING_LENGTH = 1024;
|
|
53
59
|
|
|
54
60
|
/**
|
|
55
|
-
* Mapping between query
|
|
61
|
+
* Mapping between query operators and SQL operations
|
|
56
62
|
*/
|
|
57
63
|
SQL_OPS = {
|
|
58
64
|
$and: 'AND',
|
|
@@ -95,8 +101,8 @@ export abstract class SQLDialect implements DialectState {
|
|
|
95
101
|
* Column types with inputs
|
|
96
102
|
*/
|
|
97
103
|
PARAMETERIZED_COLUMN_TYPES: Record<'VARCHAR' | 'DECIMAL', (...values: number[]) => string> = {
|
|
98
|
-
VARCHAR:
|
|
99
|
-
DECIMAL: (
|
|
104
|
+
VARCHAR: count => `VARCHAR(${count})`,
|
|
105
|
+
DECIMAL: (digits, precision) => `DECIMAL(${digits},${precision})`
|
|
100
106
|
};
|
|
101
107
|
|
|
102
108
|
ID_AFFIX = '`';
|
|
@@ -105,8 +111,8 @@ export abstract class SQLDialect implements DialectState {
|
|
|
105
111
|
* Generate an id field
|
|
106
112
|
*/
|
|
107
113
|
idField = makeField('id', String, true, {
|
|
108
|
-
maxlength: {
|
|
109
|
-
minlength: {
|
|
114
|
+
maxlength: { limit: this.ID_LENGTH },
|
|
115
|
+
minlength: { limit: this.ID_LENGTH }
|
|
110
116
|
});
|
|
111
117
|
|
|
112
118
|
/**
|
|
@@ -118,8 +124,8 @@ export abstract class SQLDialect implements DialectState {
|
|
|
118
124
|
* Parent path reference
|
|
119
125
|
*/
|
|
120
126
|
parentPathField = makeField('__parent_path', String, true, {
|
|
121
|
-
maxlength: {
|
|
122
|
-
minlength: {
|
|
127
|
+
maxlength: { limit: this.HASH_LENGTH },
|
|
128
|
+
minlength: { limit: this.HASH_LENGTH },
|
|
123
129
|
required: { active: true }
|
|
124
130
|
});
|
|
125
131
|
|
|
@@ -127,8 +133,8 @@ export abstract class SQLDialect implements DialectState {
|
|
|
127
133
|
* Path reference
|
|
128
134
|
*/
|
|
129
135
|
pathField = makeField('__path', String, true, {
|
|
130
|
-
maxlength: {
|
|
131
|
-
minlength: {
|
|
136
|
+
maxlength: { limit: this.HASH_LENGTH },
|
|
137
|
+
minlength: { limit: this.HASH_LENGTH },
|
|
132
138
|
required: { active: true }
|
|
133
139
|
});
|
|
134
140
|
|
|
@@ -137,38 +143,43 @@ export abstract class SQLDialect implements DialectState {
|
|
|
137
143
|
rootAlias = '_ROOT';
|
|
138
144
|
|
|
139
145
|
aliasCache = new Map<Class, Map<string, Alias>>();
|
|
140
|
-
|
|
146
|
+
namespacePrefix: string;
|
|
141
147
|
|
|
142
|
-
constructor(
|
|
148
|
+
constructor(namespacePrefix: string) {
|
|
143
149
|
this.namespace = this.namespace.bind(this);
|
|
144
150
|
this.table = this.table.bind(this);
|
|
145
|
-
this.
|
|
146
|
-
this.
|
|
151
|
+
this.identifier = this.identifier.bind(this);
|
|
152
|
+
this.namespacePrefix = namespacePrefix ? `${namespacePrefix}_` : namespacePrefix;
|
|
147
153
|
}
|
|
148
154
|
|
|
149
155
|
/**
|
|
150
156
|
* Get connection
|
|
151
157
|
*/
|
|
152
|
-
abstract get
|
|
158
|
+
abstract get connection(): Connection<unknown>;
|
|
153
159
|
|
|
154
160
|
/**
|
|
155
161
|
* Hash a value
|
|
156
162
|
*/
|
|
157
|
-
abstract hash(
|
|
163
|
+
abstract hash(input: string): string;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Describe a table structure
|
|
167
|
+
*/
|
|
168
|
+
abstract describeTable(table: string): Promise<SQLTableDescription | undefined>;
|
|
158
169
|
|
|
159
170
|
executeSQL<T>(sql: string): Promise<{ records: T[], count: number }> {
|
|
160
|
-
return this.
|
|
171
|
+
return this.connection.execute<T>(this.connection.active, sql);
|
|
161
172
|
}
|
|
162
173
|
|
|
163
174
|
/**
|
|
164
175
|
* Identify a name or field (escape it)
|
|
165
176
|
*/
|
|
166
|
-
|
|
177
|
+
identifier(field: SchemaFieldConfig | string): string {
|
|
167
178
|
if (field === '*') {
|
|
168
179
|
return field;
|
|
169
180
|
} else {
|
|
170
|
-
const name = (typeof field === '
|
|
171
|
-
return `${this.ID_AFFIX}${name
|
|
181
|
+
const name = (typeof field === 'string') ? field : field.name;
|
|
182
|
+
return `${this.ID_AFFIX}${name}${this.ID_AFFIX}`;
|
|
172
183
|
}
|
|
173
184
|
}
|
|
174
185
|
|
|
@@ -189,44 +200,44 @@ export abstract class SQLDialect implements DialectState {
|
|
|
189
200
|
/**
|
|
190
201
|
* Convert value to SQL valid representation
|
|
191
202
|
*/
|
|
192
|
-
resolveValue(
|
|
203
|
+
resolveValue(config: SchemaFieldConfig, value: unknown): string {
|
|
193
204
|
if (value === undefined || value === null) {
|
|
194
205
|
return 'NULL';
|
|
195
|
-
} else if (
|
|
206
|
+
} else if (config.type === String) {
|
|
196
207
|
if (value instanceof RegExp) {
|
|
197
|
-
const
|
|
198
|
-
return this.quote(
|
|
208
|
+
const regexSource = DataUtil.toRegex(value).source.replace(/\\b/g, this.regexWordBoundary);
|
|
209
|
+
return this.quote(regexSource);
|
|
199
210
|
} else {
|
|
200
211
|
return this.quote(castTo(value));
|
|
201
212
|
}
|
|
202
|
-
} else if (
|
|
213
|
+
} else if (config.type === Boolean) {
|
|
203
214
|
return `${value ? 'TRUE' : 'FALSE'}`;
|
|
204
|
-
} else if (
|
|
215
|
+
} else if (config.type === Number) {
|
|
205
216
|
return `${value}`;
|
|
206
|
-
} else if (
|
|
217
|
+
} else if (config.type === Date) {
|
|
207
218
|
if (typeof value === 'string' && TimeUtil.isTimeSpan(value)) {
|
|
208
219
|
return this.resolveDateValue(TimeUtil.fromNow(value));
|
|
209
220
|
} else {
|
|
210
221
|
return this.resolveDateValue(DataUtil.coerceType(value, Date, true));
|
|
211
222
|
}
|
|
212
|
-
} else if (
|
|
223
|
+
} else if (config.type === PointConcrete && Array.isArray(value)) {
|
|
213
224
|
return `point(${value[0]},${value[1]})`;
|
|
214
|
-
} else if (
|
|
225
|
+
} else if (config.type === Object) {
|
|
215
226
|
return this.quote(JSON.stringify(value).replace(/[']/g, "''"));
|
|
216
227
|
}
|
|
217
|
-
throw new AppError(`Unknown value type for field ${
|
|
228
|
+
throw new AppError(`Unknown value type for field ${config.name}, ${value}`, { category: 'data' });
|
|
218
229
|
}
|
|
219
230
|
|
|
220
231
|
/**
|
|
221
232
|
* Get column type from field config
|
|
222
233
|
*/
|
|
223
|
-
getColumnType(
|
|
234
|
+
getColumnType(config: SchemaFieldConfig): string {
|
|
224
235
|
let type: string = '';
|
|
225
236
|
|
|
226
|
-
if (
|
|
237
|
+
if (config.type === Number) {
|
|
227
238
|
type = this.COLUMN_TYPES.INT;
|
|
228
|
-
if (
|
|
229
|
-
const [digits, decimals] =
|
|
239
|
+
if (config.precision) {
|
|
240
|
+
const [digits, decimals] = config.precision;
|
|
230
241
|
if (decimals) {
|
|
231
242
|
type = this.PARAMETERIZED_COLUMN_TYPES.DECIMAL(digits, decimals);
|
|
232
243
|
} else if (digits) {
|
|
@@ -245,19 +256,19 @@ export abstract class SQLDialect implements DialectState {
|
|
|
245
256
|
} else {
|
|
246
257
|
type = this.COLUMN_TYPES.INT;
|
|
247
258
|
}
|
|
248
|
-
} else if (
|
|
259
|
+
} else if (config.type === Date) {
|
|
249
260
|
type = this.COLUMN_TYPES.TIMESTAMP;
|
|
250
|
-
} else if (
|
|
261
|
+
} else if (config.type === Boolean) {
|
|
251
262
|
type = this.COLUMN_TYPES.BOOLEAN;
|
|
252
|
-
} else if (
|
|
253
|
-
if (
|
|
263
|
+
} else if (config.type === String) {
|
|
264
|
+
if (config.specifiers?.includes('text')) {
|
|
254
265
|
type = this.COLUMN_TYPES.TEXT;
|
|
255
266
|
} else {
|
|
256
|
-
type = this.PARAMETERIZED_COLUMN_TYPES.VARCHAR(
|
|
267
|
+
type = this.PARAMETERIZED_COLUMN_TYPES.VARCHAR(config.maxlength?.limit ?? this.DEFAULT_STRING_LENGTH);
|
|
257
268
|
}
|
|
258
|
-
} else if (
|
|
269
|
+
} else if (config.type === PointConcrete) {
|
|
259
270
|
type = this.COLUMN_TYPES.POINT;
|
|
260
|
-
} else if (
|
|
271
|
+
} else if (config.type === Object) {
|
|
261
272
|
type = this.COLUMN_TYPES.JSON;
|
|
262
273
|
}
|
|
263
274
|
|
|
@@ -267,12 +278,13 @@ export abstract class SQLDialect implements DialectState {
|
|
|
267
278
|
/**
|
|
268
279
|
* FieldConfig to Column definition
|
|
269
280
|
*/
|
|
270
|
-
getColumnDefinition(
|
|
271
|
-
const type = this.getColumnType(
|
|
281
|
+
getColumnDefinition(config: SchemaFieldConfig, overrideRequired?: boolean): string | undefined {
|
|
282
|
+
const type = this.getColumnType(config);
|
|
272
283
|
if (!type) {
|
|
273
284
|
return;
|
|
274
285
|
}
|
|
275
|
-
|
|
286
|
+
const required = overrideRequired ? true : (config.required?.active ?? false);
|
|
287
|
+
return `${this.identifier(config)} ${type} ${required ? 'NOT NULL' : ''}`;
|
|
276
288
|
}
|
|
277
289
|
|
|
278
290
|
/**
|
|
@@ -297,7 +309,7 @@ export abstract class SQLDialect implements DialectState {
|
|
|
297
309
|
*/
|
|
298
310
|
getDropColumnSQL(stack: VisitStack[]): string {
|
|
299
311
|
const field = stack.at(-1)!;
|
|
300
|
-
return `ALTER TABLE ${this.parentTable(stack)} DROP COLUMN ${this.
|
|
312
|
+
return `ALTER TABLE ${this.parentTable(stack)} DROP COLUMN ${this.identifier(field.name)};`;
|
|
301
313
|
}
|
|
302
314
|
|
|
303
315
|
/**
|
|
@@ -317,7 +329,7 @@ export abstract class SQLDialect implements DialectState {
|
|
|
317
329
|
* Determine table/field namespace for a given stack location
|
|
318
330
|
*/
|
|
319
331
|
namespace(stack: VisitStack[]): string {
|
|
320
|
-
return `${this.
|
|
332
|
+
return `${this.namespacePrefix}${SQLModelUtil.buildTable(stack)}`;
|
|
321
333
|
}
|
|
322
334
|
|
|
323
335
|
/**
|
|
@@ -331,7 +343,7 @@ export abstract class SQLDialect implements DialectState {
|
|
|
331
343
|
* Determine table name for a given stack location
|
|
332
344
|
*/
|
|
333
345
|
table(stack: VisitStack[]): string {
|
|
334
|
-
return this.
|
|
346
|
+
return this.identifier(this.namespace(stack));
|
|
335
347
|
}
|
|
336
348
|
|
|
337
349
|
/**
|
|
@@ -351,8 +363,8 @@ export abstract class SQLDialect implements DialectState {
|
|
|
351
363
|
/**
|
|
352
364
|
* Alias a field for usage
|
|
353
365
|
*/
|
|
354
|
-
alias(field: string |
|
|
355
|
-
return `${alias}.${this.
|
|
366
|
+
alias(field: string | SchemaFieldConfig, alias: string = this.rootAlias): string {
|
|
367
|
+
return `${alias}.${this.identifier(field)}`;
|
|
356
368
|
}
|
|
357
369
|
|
|
358
370
|
/**
|
|
@@ -376,12 +388,12 @@ export abstract class SQLDialect implements DialectState {
|
|
|
376
388
|
},
|
|
377
389
|
onSub: ({ descend, config, path }) => {
|
|
378
390
|
const table = resolve(path);
|
|
379
|
-
clauses.set(table, { alias: `${config.name.
|
|
391
|
+
clauses.set(table, { alias: `${config.name.charAt(0)}${idx++}`, path });
|
|
380
392
|
return descend();
|
|
381
393
|
},
|
|
382
394
|
onSimple: ({ config, path }) => {
|
|
383
395
|
const table = resolve(path);
|
|
384
|
-
clauses.set(table, { alias: `${config.name.
|
|
396
|
+
clauses.set(table, { alias: `${config.name.charAt(0)}${idx++}`, path });
|
|
385
397
|
}
|
|
386
398
|
});
|
|
387
399
|
|
|
@@ -404,13 +416,13 @@ export abstract class SQLDialect implements DialectState {
|
|
|
404
416
|
/**
|
|
405
417
|
* Generate WHERE field clause
|
|
406
418
|
*/
|
|
407
|
-
getWhereFieldSQL(stack: VisitStack[],
|
|
419
|
+
getWhereFieldSQL(stack: VisitStack[], input: Record<string, unknown>): string {
|
|
408
420
|
const items = [];
|
|
409
421
|
const { foreignMap, localMap } = SQLModelUtil.getFieldsByLocation(stack);
|
|
410
422
|
const SQL_OPS = this.SQL_OPS;
|
|
411
423
|
|
|
412
|
-
for (const key of Object.keys(
|
|
413
|
-
const top =
|
|
424
|
+
for (const key of Object.keys(input)) {
|
|
425
|
+
const top = input[key];
|
|
414
426
|
const field = localMap[key] ?? foreignMap[key];
|
|
415
427
|
if (!field) {
|
|
416
428
|
throw new Error(`Unknown field: ${key}`);
|
|
@@ -420,7 +432,7 @@ export abstract class SQLDialect implements DialectState {
|
|
|
420
432
|
// If dealing with simple external
|
|
421
433
|
sStack.push({
|
|
422
434
|
name: field.name,
|
|
423
|
-
|
|
435
|
+
class: null!,
|
|
424
436
|
type: field.type
|
|
425
437
|
});
|
|
426
438
|
}
|
|
@@ -432,38 +444,38 @@ export abstract class SQLDialect implements DialectState {
|
|
|
432
444
|
const inner = this.getWhereFieldSQL(sStack, top);
|
|
433
445
|
items.push(inner);
|
|
434
446
|
} else {
|
|
435
|
-
const
|
|
447
|
+
const value = top[subKey];
|
|
436
448
|
const resolve = this.resolveValue.bind(this, field);
|
|
437
449
|
|
|
438
450
|
switch (subKey) {
|
|
439
451
|
case '$nin': case '$in': {
|
|
440
|
-
const arr = (Array.isArray(
|
|
452
|
+
const arr = (Array.isArray(value) ? value : [value]).map(item => resolve(item));
|
|
441
453
|
items.push(`${sPath} ${SQL_OPS[subKey]} (${arr.join(',')})`);
|
|
442
454
|
break;
|
|
443
455
|
}
|
|
444
456
|
case '$all': {
|
|
445
457
|
const set = new Set();
|
|
446
|
-
const arr = [
|
|
458
|
+
const arr = [value].flat().filter(item => !set.has(item) && !!set.add(item)).map(item => resolve(item));
|
|
447
459
|
const valueTable = this.parentTable(sStack);
|
|
448
460
|
const alias = `_all_${sStack.length}`;
|
|
449
|
-
const pPath = this.
|
|
461
|
+
const pPath = this.identifier(this.parentPathField.name);
|
|
450
462
|
const rpPath = this.resolveName([...sStack, field, this.parentPathField]);
|
|
451
463
|
|
|
452
464
|
items.push(`${arr.length} = (
|
|
453
|
-
SELECT COUNT(DISTINCT ${alias}.${this.
|
|
465
|
+
SELECT COUNT(DISTINCT ${alias}.${this.identifier(field.name)})
|
|
454
466
|
FROM ${valueTable} ${alias}
|
|
455
467
|
WHERE ${alias}.${pPath} = ${rpPath}
|
|
456
|
-
AND ${alias}.${this.
|
|
468
|
+
AND ${alias}.${this.identifier(field.name)} IN (${arr.join(',')})
|
|
457
469
|
)`);
|
|
458
470
|
break;
|
|
459
471
|
}
|
|
460
472
|
case '$regex': {
|
|
461
|
-
const
|
|
462
|
-
const
|
|
463
|
-
const ins =
|
|
473
|
+
const regex = DataUtil.toRegex(castTo(value));
|
|
474
|
+
const regexSource = regex.source;
|
|
475
|
+
const ins = regex.flags && regex.flags.includes('i');
|
|
464
476
|
|
|
465
|
-
if (/^[\^]\S+[.][*][$]?$/.test(
|
|
466
|
-
const inner =
|
|
477
|
+
if (/^[\^]\S+[.][*][$]?$/.test(regexSource)) {
|
|
478
|
+
const inner = regexSource.substring(1, regexSource.length - 2);
|
|
467
479
|
if (!ins || SQL_OPS.$ilike) {
|
|
468
480
|
items.push(`${sPath} ${ins ? SQL_OPS.$ilike : SQL_OPS.$like} ${resolve(`${inner}%`)}`);
|
|
469
481
|
} else {
|
|
@@ -471,11 +483,11 @@ export abstract class SQLDialect implements DialectState {
|
|
|
471
483
|
}
|
|
472
484
|
} else {
|
|
473
485
|
if (!ins || SQL_OPS.$iregex) {
|
|
474
|
-
const
|
|
475
|
-
items.push(`${sPath} ${SQL_OPS[!ins ? subKey : '$iregex']} ${
|
|
486
|
+
const result = resolve(value);
|
|
487
|
+
items.push(`${sPath} ${SQL_OPS[!ins ? subKey : '$iregex']} ${result}`);
|
|
476
488
|
} else {
|
|
477
|
-
const
|
|
478
|
-
items.push(`LOWER(${sPath}) ${SQL_OPS[subKey]} ${
|
|
489
|
+
const result = resolve(new RegExp(regexSource.toLowerCase(), regex.flags));
|
|
490
|
+
items.push(`LOWER(${sPath}) ${SQL_OPS[subKey]} ${result}`);
|
|
479
491
|
}
|
|
480
492
|
}
|
|
481
493
|
break;
|
|
@@ -484,31 +496,31 @@ export abstract class SQLDialect implements DialectState {
|
|
|
484
496
|
if (field.array) {
|
|
485
497
|
const valueTable = this.parentTable(sStack);
|
|
486
498
|
const alias = `_all_${sStack.length}`;
|
|
487
|
-
const pPath = this.
|
|
499
|
+
const pPath = this.identifier(this.parentPathField.name);
|
|
488
500
|
const rpPath = this.resolveName([...sStack, field, this.parentPathField]);
|
|
489
501
|
|
|
490
|
-
items.push(`0 ${!
|
|
491
|
-
SELECT COUNT(${alias}.${this.
|
|
502
|
+
items.push(`0 ${!value ? '=' : '<>'} (
|
|
503
|
+
SELECT COUNT(${alias}.${this.identifier(field.name)})
|
|
492
504
|
FROM ${valueTable} ${alias}
|
|
493
505
|
WHERE ${alias}.${pPath} = ${rpPath}
|
|
494
506
|
)`);
|
|
495
507
|
} else {
|
|
496
|
-
items.push(`${sPath} ${
|
|
508
|
+
items.push(`${sPath} ${value ? SQL_OPS.$isNot : SQL_OPS.$is} NULL`);
|
|
497
509
|
}
|
|
498
510
|
break;
|
|
499
511
|
}
|
|
500
512
|
case '$ne': case '$eq': {
|
|
501
|
-
if (
|
|
513
|
+
if (value === null || value === undefined) {
|
|
502
514
|
items.push(`${sPath} ${subKey === '$ne' ? SQL_OPS.$isNot : SQL_OPS.$is} NULL`);
|
|
503
515
|
} else {
|
|
504
|
-
const base = `${sPath} ${SQL_OPS[subKey]} ${resolve(
|
|
516
|
+
const base = `${sPath} ${SQL_OPS[subKey]} ${resolve(value)}`;
|
|
505
517
|
items.push(subKey === '$ne' ? `(${base} OR ${sPath} ${SQL_OPS.$is} NULL)` : base);
|
|
506
518
|
}
|
|
507
519
|
break;
|
|
508
520
|
}
|
|
509
521
|
case '$lt': case '$gt': case '$gte': case '$lte': {
|
|
510
522
|
const subItems = TypedObject.keys(castTo<typeof SQL_OPS>(top))
|
|
511
|
-
.map(
|
|
523
|
+
.map(subSubKey => `${sPath} ${SQL_OPS[subSubKey]} ${resolve(top[subSubKey])}`);
|
|
512
524
|
items.push(subItems.length > 1 ? `(${subItems.join(` ${SQL_OPS.$and} `)})` : subItems[0]);
|
|
513
525
|
break;
|
|
514
526
|
}
|
|
@@ -534,17 +546,17 @@ export abstract class SQLDialect implements DialectState {
|
|
|
534
546
|
/**
|
|
535
547
|
* Grouping of where clauses
|
|
536
548
|
*/
|
|
537
|
-
getWhereGroupingSQL<T>(cls: Class<T>,
|
|
549
|
+
getWhereGroupingSQL<T>(cls: Class<T>, clause: WhereClause<T>): string {
|
|
538
550
|
const SQL_OPS = this.SQL_OPS;
|
|
539
551
|
|
|
540
|
-
if (ModelQueryUtil.has$And(
|
|
541
|
-
return `(${
|
|
542
|
-
} else if (ModelQueryUtil.has$Or(
|
|
543
|
-
return `(${
|
|
544
|
-
} else if (ModelQueryUtil.has$Not(
|
|
545
|
-
return `${SQL_OPS.$not} (${this.getWhereGroupingSQL<T>(cls,
|
|
552
|
+
if (ModelQueryUtil.has$And(clause)) {
|
|
553
|
+
return `(${clause.$and.map(item => this.getWhereGroupingSQL<T>(cls, item)).join(` ${SQL_OPS.$and} `)})`;
|
|
554
|
+
} else if (ModelQueryUtil.has$Or(clause)) {
|
|
555
|
+
return `(${clause.$or.map(item => this.getWhereGroupingSQL<T>(cls, item)).join(` ${SQL_OPS.$or} `)})`;
|
|
556
|
+
} else if (ModelQueryUtil.has$Not(clause)) {
|
|
557
|
+
return `${SQL_OPS.$not} (${this.getWhereGroupingSQL<T>(cls, clause.$not)})`;
|
|
546
558
|
} else {
|
|
547
|
-
return this.getWhereFieldSQL(SQLModelUtil.classToStack(cls),
|
|
559
|
+
return this.getWhereFieldSQL(SQLModelUtil.classToStack(cls), clause);
|
|
548
560
|
}
|
|
549
561
|
}
|
|
550
562
|
|
|
@@ -563,8 +575,8 @@ export abstract class SQLDialect implements DialectState {
|
|
|
563
575
|
getOrderBySQL<T>(cls: Class<T>, sortBy?: SortClause<T>[]): string {
|
|
564
576
|
return !sortBy ?
|
|
565
577
|
'' :
|
|
566
|
-
`ORDER BY ${SQLModelUtil.orderBy(cls, sortBy).map((
|
|
567
|
-
`${this.resolveName(
|
|
578
|
+
`ORDER BY ${SQLModelUtil.orderBy(cls, sortBy).map((item) =>
|
|
579
|
+
`${this.resolveName(item.stack)} ${item.asc ? 'ASC' : 'DESC'}`
|
|
568
580
|
).join(', ')}`;
|
|
569
581
|
}
|
|
570
582
|
|
|
@@ -591,7 +603,7 @@ export abstract class SQLDialect implements DialectState {
|
|
|
591
603
|
const tables = [...aliases.keys()].toSorted((a, b) => a.length - b.length); // Shortest first
|
|
592
604
|
return `FROM ${tables.map((table) => {
|
|
593
605
|
const { alias, path } = aliases.get(table)!;
|
|
594
|
-
let from = `${this.
|
|
606
|
+
let from = `${this.identifier(table)} ${alias}`;
|
|
595
607
|
if (path.length > 1) {
|
|
596
608
|
const key = this.namespaceParent(path);
|
|
597
609
|
const { alias: parentAlias } = aliases.get(key)!;
|
|
@@ -620,7 +632,7 @@ LEFT OUTER JOIN ${from} ON
|
|
|
620
632
|
const sortFields = !query.sort ?
|
|
621
633
|
'' :
|
|
622
634
|
SQLModelUtil.orderBy(cls, query.sort)
|
|
623
|
-
.map(
|
|
635
|
+
.map(item => this.resolveName(item.stack))
|
|
624
636
|
.join(', ');
|
|
625
637
|
|
|
626
638
|
// TODO: Really confused on this
|
|
@@ -649,21 +661,15 @@ ${this.getLimitSQL(cls, query)}`;
|
|
|
649
661
|
(array ? [castTo<SchemaFieldConfig>(config)] : []);
|
|
650
662
|
|
|
651
663
|
if (!parent) {
|
|
652
|
-
let idField = fields.find(
|
|
664
|
+
let idField = fields.find(field => field.name === this.idField.name);
|
|
653
665
|
if (!idField) {
|
|
654
666
|
fields.push(idField = this.idField);
|
|
655
|
-
} else {
|
|
656
|
-
idField.maxlength = { n: this.ID_LEN };
|
|
657
667
|
}
|
|
658
668
|
}
|
|
659
669
|
|
|
660
670
|
const fieldSql = fields
|
|
661
|
-
.map(
|
|
662
|
-
|
|
663
|
-
return f.name === this.idField.name && !parent ?
|
|
664
|
-
def.replace('DEFAULT NULL', 'NOT NULL') : def;
|
|
665
|
-
})
|
|
666
|
-
.filter(x => !!x.trim())
|
|
671
|
+
.map(field => this.getColumnDefinition(field, field.name === this.idField.name && !parent) || '')
|
|
672
|
+
.filter(line => !!line.trim())
|
|
667
673
|
.join(',\n ');
|
|
668
674
|
|
|
669
675
|
const out = `
|
|
@@ -671,11 +677,11 @@ CREATE TABLE IF NOT EXISTS ${this.table(stack)} (
|
|
|
671
677
|
${fieldSql}${fieldSql.length ? ',' : ''}
|
|
672
678
|
${this.getColumnDefinition(this.pathField)} UNIQUE,
|
|
673
679
|
${!parent ?
|
|
674
|
-
`PRIMARY KEY (${this.
|
|
680
|
+
`PRIMARY KEY (${this.identifier(this.idField)})` :
|
|
675
681
|
`${this.getColumnDefinition(this.parentPathField)},
|
|
676
682
|
${array ? `${this.getColumnDefinition(this.idxField)},` : ''}
|
|
677
|
-
PRIMARY KEY (${this.
|
|
678
|
-
FOREIGN KEY (${this.
|
|
683
|
+
PRIMARY KEY (${this.identifier(this.pathField)}),
|
|
684
|
+
FOREIGN KEY (${this.identifier(this.parentPathField)}) REFERENCES ${this.parentTable(stack)}(${this.identifier(this.pathField)}) ON DELETE CASCADE`}
|
|
679
685
|
);`;
|
|
680
686
|
return out;
|
|
681
687
|
}
|
|
@@ -714,25 +720,41 @@ CREATE TABLE IF NOT EXISTS ${this.table(stack)} (
|
|
|
714
720
|
return indices.map(idx => this.getCreateIndexSQL(cls, idx));
|
|
715
721
|
}
|
|
716
722
|
|
|
723
|
+
/**
|
|
724
|
+
* Get index name
|
|
725
|
+
*/
|
|
726
|
+
getIndexName<T extends ModelType>(cls: Class<T>, idx: IndexConfig<ModelType>): string {
|
|
727
|
+
const table = this.namespace(SQLModelUtil.classToStack(cls));
|
|
728
|
+
return ['idx', table, idx.name.toLowerCase().replaceAll('-', '_')].join('_');
|
|
729
|
+
}
|
|
730
|
+
|
|
717
731
|
/**
|
|
718
732
|
* Get CREATE INDEX sql
|
|
719
733
|
*/
|
|
720
734
|
getCreateIndexSQL<T extends ModelType>(cls: Class<T>, idx: IndexConfig<T>): string {
|
|
721
735
|
const table = this.namespace(SQLModelUtil.classToStack(cls));
|
|
722
|
-
const fields: [string, boolean][] = idx.fields.map(
|
|
723
|
-
const key = TypedObject.keys(
|
|
724
|
-
const
|
|
725
|
-
if (DataUtil.isPlainObject(
|
|
736
|
+
const fields: [string, boolean][] = idx.fields.map(field => {
|
|
737
|
+
const key = TypedObject.keys(field)[0];
|
|
738
|
+
const value = field[key];
|
|
739
|
+
if (DataUtil.isPlainObject(value)) {
|
|
726
740
|
throw new Error('Unable to supported nested fields for indices');
|
|
727
741
|
}
|
|
728
|
-
return [castTo(key), typeof
|
|
742
|
+
return [castTo(key), typeof value === 'number' ? value === 1 : (!!value)];
|
|
729
743
|
});
|
|
730
|
-
const constraint =
|
|
731
|
-
return `CREATE ${idx.type === 'unique' ? 'UNIQUE ' : ''}INDEX ${constraint} ON ${this.
|
|
732
|
-
.map(([name, sel]) => `${this.
|
|
744
|
+
const constraint = this.getIndexName(cls, idx);
|
|
745
|
+
return `CREATE ${idx.type === 'unique' ? 'UNIQUE ' : ''}INDEX ${constraint} ON ${this.identifier(table)} (${fields
|
|
746
|
+
.map(([name, sel]) => `${this.identifier(name)} ${sel ? 'ASC' : 'DESC'}`)
|
|
733
747
|
.join(', ')});`;
|
|
734
748
|
}
|
|
735
749
|
|
|
750
|
+
/**
|
|
751
|
+
* Get DROP INDEX sql
|
|
752
|
+
*/
|
|
753
|
+
getDropIndexSQL<T extends ModelType>(cls: Class<T>, idx: IndexConfig<T> | string): string {
|
|
754
|
+
const constraint = typeof idx === 'string' ? idx : this.getIndexName(cls, idx);
|
|
755
|
+
return `DROP INDEX ${this.identifier(constraint)} ;`;
|
|
756
|
+
}
|
|
757
|
+
|
|
736
758
|
/**
|
|
737
759
|
* Drop all tables for a given class
|
|
738
760
|
*/
|
|
@@ -765,30 +787,30 @@ CREATE TABLE IF NOT EXISTS ${this.table(stack)} (
|
|
|
765
787
|
getInsertSQL(stack: VisitStack[], instances: InsertWrapper['records']): string | undefined {
|
|
766
788
|
const config = stack.at(-1)!;
|
|
767
789
|
const columns = SQLModelUtil.getFieldsByLocation(stack).local
|
|
768
|
-
.filter(
|
|
769
|
-
.toSorted((a, b) => a.name.
|
|
770
|
-
const columnNames = columns.map(
|
|
790
|
+
.filter(field => !SchemaRegistryIndex.has(field.type))
|
|
791
|
+
.toSorted((a, b) => a.name.localeCompare(b.name));
|
|
792
|
+
const columnNames = columns.map(column => column.name);
|
|
771
793
|
|
|
772
794
|
const hasParent = stack.length > 1;
|
|
773
795
|
const isArray = !!config.array;
|
|
774
796
|
|
|
775
797
|
if (isArray) {
|
|
776
798
|
const newInstances: typeof instances = [];
|
|
777
|
-
for (const
|
|
778
|
-
if (
|
|
799
|
+
for (const instance of instances) {
|
|
800
|
+
if (instance.value === null || instance.value === undefined) {
|
|
779
801
|
continue;
|
|
780
|
-
} else if (Array.isArray(
|
|
781
|
-
const name =
|
|
782
|
-
for (const sel of
|
|
802
|
+
} else if (Array.isArray(instance.value)) {
|
|
803
|
+
const name = instance.stack.at(-1)!.name;
|
|
804
|
+
for (const sel of instance.value) {
|
|
783
805
|
newInstances.push({
|
|
784
|
-
stack:
|
|
806
|
+
stack: instance.stack,
|
|
785
807
|
value: {
|
|
786
808
|
[name]: sel
|
|
787
809
|
}
|
|
788
810
|
});
|
|
789
811
|
}
|
|
790
812
|
} else {
|
|
791
|
-
newInstances.push(
|
|
813
|
+
newInstances.push(instance);
|
|
792
814
|
}
|
|
793
815
|
}
|
|
794
816
|
instances = newInstances;
|
|
@@ -798,7 +820,8 @@ CREATE TABLE IF NOT EXISTS ${this.table(stack)} (
|
|
|
798
820
|
return;
|
|
799
821
|
}
|
|
800
822
|
|
|
801
|
-
const matrix = instances.map(inst => columns.map(
|
|
823
|
+
const matrix = instances.map(inst => columns.map(column =>
|
|
824
|
+
this.resolveValue(column, castTo<Record<string, unknown>>(inst.value)[column.name])));
|
|
802
825
|
|
|
803
826
|
columnNames.push(this.pathField.name);
|
|
804
827
|
if (hasParent) {
|
|
@@ -824,7 +847,7 @@ CREATE TABLE IF NOT EXISTS ${this.table(stack)} (
|
|
|
824
847
|
}
|
|
825
848
|
|
|
826
849
|
return `
|
|
827
|
-
INSERT INTO ${this.table(stack)} (${columnNames.map(this.
|
|
850
|
+
INSERT INTO ${this.table(stack)} (${columnNames.map(this.identifier).join(', ')})
|
|
828
851
|
VALUES
|
|
829
852
|
${matrix.map(row => `(${row.join(', ')})`).join(',\n')};`;
|
|
830
853
|
}
|
|
@@ -854,8 +877,8 @@ UPDATE ${this.table(stack)} ${this.rootAlias}
|
|
|
854
877
|
SET
|
|
855
878
|
${Object
|
|
856
879
|
.entries(data)
|
|
857
|
-
.filter(([
|
|
858
|
-
.map(([
|
|
880
|
+
.filter(([key]) => key in localMap)
|
|
881
|
+
.map(([key, value]) => `${this.identifier(key)}=${this.resolveValue(localMap[key], value)}`).join(', ')}
|
|
859
882
|
${this.getWhereSQL(type, where)};`;
|
|
860
883
|
}
|
|
861
884
|
|
|
@@ -868,18 +891,18 @@ ${this.getWhereSQL(type, where)};`;
|
|
|
868
891
|
}
|
|
869
892
|
|
|
870
893
|
/**
|
|
871
|
-
|
|
872
|
-
|
|
894
|
+
* Get elements by ids
|
|
895
|
+
*/
|
|
873
896
|
getSelectRowsByIdsSQL(stack: VisitStack[], ids: string[], select: SchemaFieldConfig[] = []): string {
|
|
874
897
|
const config = stack.at(-1)!;
|
|
875
898
|
const orderBy = !config.array ?
|
|
876
899
|
'' :
|
|
877
|
-
`ORDER BY ${this.rootAlias}.${this.idxField.name
|
|
900
|
+
`ORDER BY ${this.rootAlias}.${this.idxField.name} ASC`;
|
|
878
901
|
|
|
879
902
|
const idField = (stack.length > 1 ? this.parentPathField : this.idField);
|
|
880
903
|
|
|
881
904
|
return `
|
|
882
|
-
SELECT ${select.length ? select.map(
|
|
905
|
+
SELECT ${select.length ? select.map(field => this.alias(field)).join(',') : '*'}
|
|
883
906
|
FROM ${this.table(stack)} ${this.rootAlias}
|
|
884
907
|
WHERE ${this.alias(idField)} IN (${ids.map(id => this.resolveValue(idField, id)).join(', ')})
|
|
885
908
|
${orderBy};`;
|
|
@@ -904,9 +927,9 @@ ${this.getWhereSQL(cls, where!)}`;
|
|
|
904
927
|
|
|
905
928
|
await SQLModelUtil.visitSchema(SchemaRegistryIndex.getConfig(cls), {
|
|
906
929
|
onRoot: async (config) => {
|
|
907
|
-
const
|
|
930
|
+
const fieldSet = buildSet(items); // Already filtered by initial select query
|
|
908
931
|
selectStack.push(select);
|
|
909
|
-
stack.push(
|
|
932
|
+
stack.push(fieldSet);
|
|
910
933
|
await config.descend();
|
|
911
934
|
},
|
|
912
935
|
onSub: async ({ config, descend, fields, path }) => {
|
|
@@ -917,28 +940,28 @@ ${this.getWhereSQL(cls, where!)}`;
|
|
|
917
940
|
const subSelectTop: SelectClause<T> | undefined = castTo(selectTop?.[fieldKey]);
|
|
918
941
|
|
|
919
942
|
// See if a selection exists at all
|
|
920
|
-
const
|
|
921
|
-
.filter(
|
|
943
|
+
const selected: SchemaFieldConfig[] = subSelectTop ? fields
|
|
944
|
+
.filter(field => typeof subSelectTop === 'object' && subSelectTop[castTo<typeof fieldKey>(field.name)] === 1)
|
|
922
945
|
: [];
|
|
923
946
|
|
|
924
|
-
if (
|
|
925
|
-
|
|
947
|
+
if (selected.length) {
|
|
948
|
+
selected.push(this.pathField, this.parentPathField);
|
|
926
949
|
if (config.array) {
|
|
927
|
-
|
|
950
|
+
selected.push(this.idxField);
|
|
928
951
|
}
|
|
929
952
|
}
|
|
930
953
|
|
|
931
954
|
// If children and selection exists
|
|
932
|
-
if (ids.length && (!subSelectTop ||
|
|
955
|
+
if (ids.length && (!subSelectTop || selected)) {
|
|
933
956
|
const { records: children } = await this.executeSQL<unknown[]>(this.getSelectRowsByIdsSQL(
|
|
934
957
|
path,
|
|
935
958
|
ids,
|
|
936
|
-
|
|
959
|
+
selected
|
|
937
960
|
));
|
|
938
961
|
|
|
939
|
-
const
|
|
962
|
+
const fieldSet = buildSet(children, config);
|
|
940
963
|
try {
|
|
941
|
-
stack.push(
|
|
964
|
+
stack.push(fieldSet);
|
|
942
965
|
selectStack.push(subSelectTop);
|
|
943
966
|
await descend();
|
|
944
967
|
} finally {
|
|
@@ -982,30 +1005,34 @@ ${this.getWhereSQL(cls, where!)}`;
|
|
|
982
1005
|
async bulkProcess(deletes: DeleteWrapper[], inserts: InsertWrapper[], upserts: InsertWrapper[], updates: InsertWrapper[]): Promise<BulkResponse> {
|
|
983
1006
|
const out = {
|
|
984
1007
|
counts: {
|
|
985
|
-
delete: deletes.reduce((
|
|
1008
|
+
delete: deletes.reduce((count, item) => count + item.ids.length, 0),
|
|
986
1009
|
error: 0,
|
|
987
|
-
insert: inserts.filter(
|
|
988
|
-
update: updates.filter(
|
|
989
|
-
upsert: upserts.filter(
|
|
1010
|
+
insert: inserts.filter(item => item.stack.length === 1).reduce((count, item) => count + item.records.length, 0),
|
|
1011
|
+
update: updates.filter(item => item.stack.length === 1).reduce((count, item) => count + item.records.length, 0),
|
|
1012
|
+
upsert: upserts.filter(item => item.stack.length === 1).reduce((count, item) => count + item.records.length, 0)
|
|
990
1013
|
},
|
|
991
1014
|
errors: [],
|
|
992
1015
|
insertedIds: new Map()
|
|
993
1016
|
};
|
|
994
1017
|
|
|
995
1018
|
// Full removals
|
|
996
|
-
await Promise.all(deletes.map(
|
|
1019
|
+
await Promise.all(deletes.map(item => this.deleteByIds(item.stack, item.ids)));
|
|
997
1020
|
|
|
998
1021
|
// Adding deletes
|
|
999
1022
|
if (upserts.length || updates.length) {
|
|
1000
1023
|
const idx = this.idField.name;
|
|
1001
1024
|
|
|
1002
1025
|
await Promise.all([
|
|
1003
|
-
...upserts
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1026
|
+
...upserts
|
|
1027
|
+
.filter(item => item.stack.length === 1)
|
|
1028
|
+
.map(item =>
|
|
1029
|
+
this.deleteByIds(item.stack, item.records.map(value => castTo<Record<string, string>>(value.value)[idx]))
|
|
1030
|
+
),
|
|
1031
|
+
...updates
|
|
1032
|
+
.filter(item => item.stack.length === 1)
|
|
1033
|
+
.map(item =>
|
|
1034
|
+
this.deleteByIds(item.stack, item.records.map(value => castTo<Record<string, string>>(value.value)[idx]))
|
|
1035
|
+
),
|
|
1009
1036
|
]);
|
|
1010
1037
|
}
|
|
1011
1038
|
|
|
@@ -1014,20 +1041,61 @@ ${this.getWhereSQL(cls, where!)}`;
|
|
|
1014
1041
|
if (!items.length) {
|
|
1015
1042
|
continue;
|
|
1016
1043
|
}
|
|
1017
|
-
let
|
|
1044
|
+
let level = 1; // Add by level
|
|
1018
1045
|
for (; ;) { // Loop until done
|
|
1019
|
-
const leveled = items.filter(
|
|
1046
|
+
const leveled = items.filter(insertWrapper => insertWrapper.stack.length === level);
|
|
1020
1047
|
if (!leveled.length) {
|
|
1021
1048
|
break;
|
|
1022
1049
|
}
|
|
1023
1050
|
await Promise.all(leveled
|
|
1024
|
-
.map(
|
|
1051
|
+
.map(inserted => this.getInsertSQL(inserted.stack, inserted.records))
|
|
1025
1052
|
.filter(sql => !!sql)
|
|
1026
1053
|
.map(sql => this.executeSQL(sql!)));
|
|
1027
|
-
|
|
1054
|
+
level += 1;
|
|
1028
1055
|
}
|
|
1029
1056
|
}
|
|
1030
1057
|
|
|
1031
1058
|
return out;
|
|
1032
1059
|
}
|
|
1060
|
+
|
|
1061
|
+
/**
|
|
1062
|
+
* Determine if a column has changed
|
|
1063
|
+
*/
|
|
1064
|
+
isColumnChanged(requested: SchemaFieldConfig, existing: SQLTableDescription['columns'][number],): boolean {
|
|
1065
|
+
const requestedColumnType = this.getColumnType(requested);
|
|
1066
|
+
const result =
|
|
1067
|
+
(requested.name !== this.idField.name && !!requested.required?.active !== !!existing.is_notnull)
|
|
1068
|
+
|| (requestedColumnType.toUpperCase() !== existing.type.toUpperCase());
|
|
1069
|
+
|
|
1070
|
+
return result;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
/**
|
|
1074
|
+
* Determine if an index has changed
|
|
1075
|
+
*/
|
|
1076
|
+
isIndexChanged(requested: IndexConfig<ModelType>, existing: SQLTableDescription['indices'][number]): boolean {
|
|
1077
|
+
let result =
|
|
1078
|
+
(existing.is_unique && requested.type !== 'unique')
|
|
1079
|
+
|| requested.fields.length !== existing.columns.length;
|
|
1080
|
+
|
|
1081
|
+
for (let i = 0; i < requested.fields.length && !result; i++) {
|
|
1082
|
+
const [[key, value]] = Object.entries(requested.fields[i]);
|
|
1083
|
+
const desc = value === -1;
|
|
1084
|
+
result ||= key !== existing.columns[i].name && desc !== existing.columns[i].desc;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
return result;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
/**
|
|
1091
|
+
* Enforce the dialect specific id length
|
|
1092
|
+
*/
|
|
1093
|
+
enforceIdLength(cls: Class<ModelType>): void {
|
|
1094
|
+
const config = SchemaRegistryIndex.getConfig(cls);
|
|
1095
|
+
const idField = config.fields[this.idField.name];
|
|
1096
|
+
if (idField) {
|
|
1097
|
+
idField.maxlength = { limit: this.ID_LENGTH };
|
|
1098
|
+
idField.minlength = { limit: this.ID_LENGTH };
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1033
1101
|
}
|