@travetto/model-sql 7.0.0-rc.1 → 7.0.0-rc.2
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/connection/base.ts +30 -30
- package/src/connection/decorator.ts +17 -17
- package/src/dialect/base.ts +155 -150
- package/src/service.ts +47 -46
- package/src/table-manager.ts +29 -29
- package/src/types.ts +1 -1
- package/src/util.ts +70 -70
package/src/dialect/base.ts
CHANGED
|
@@ -9,7 +9,7 @@ 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;
|
|
@@ -39,20 +39,20 @@ export abstract class SQLDialect implements DialectState {
|
|
|
39
39
|
/**
|
|
40
40
|
* Default length of unique ids
|
|
41
41
|
*/
|
|
42
|
-
|
|
42
|
+
ID_LENGTH = 32;
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
45
|
* Hash Length
|
|
46
46
|
*/
|
|
47
|
-
|
|
47
|
+
HASH_LENGTH = 64;
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
50
|
* Default length for varchar
|
|
51
51
|
*/
|
|
52
|
-
|
|
52
|
+
DEFAULT_STRING_LENGTH = 1024;
|
|
53
53
|
|
|
54
54
|
/**
|
|
55
|
-
* Mapping between query
|
|
55
|
+
* Mapping between query operators and SQL operations
|
|
56
56
|
*/
|
|
57
57
|
SQL_OPS = {
|
|
58
58
|
$and: 'AND',
|
|
@@ -95,8 +95,8 @@ export abstract class SQLDialect implements DialectState {
|
|
|
95
95
|
* Column types with inputs
|
|
96
96
|
*/
|
|
97
97
|
PARAMETERIZED_COLUMN_TYPES: Record<'VARCHAR' | 'DECIMAL', (...values: number[]) => string> = {
|
|
98
|
-
VARCHAR:
|
|
99
|
-
DECIMAL: (
|
|
98
|
+
VARCHAR: count => `VARCHAR(${count})`,
|
|
99
|
+
DECIMAL: (digits, precision) => `DECIMAL(${digits},${precision})`
|
|
100
100
|
};
|
|
101
101
|
|
|
102
102
|
ID_AFFIX = '`';
|
|
@@ -105,8 +105,8 @@ export abstract class SQLDialect implements DialectState {
|
|
|
105
105
|
* Generate an id field
|
|
106
106
|
*/
|
|
107
107
|
idField = makeField('id', String, true, {
|
|
108
|
-
maxlength: { n: this.
|
|
109
|
-
minlength: { n: this.
|
|
108
|
+
maxlength: { n: this.ID_LENGTH },
|
|
109
|
+
minlength: { n: this.ID_LENGTH }
|
|
110
110
|
});
|
|
111
111
|
|
|
112
112
|
/**
|
|
@@ -118,8 +118,8 @@ export abstract class SQLDialect implements DialectState {
|
|
|
118
118
|
* Parent path reference
|
|
119
119
|
*/
|
|
120
120
|
parentPathField = makeField('__parent_path', String, true, {
|
|
121
|
-
maxlength: { n: this.
|
|
122
|
-
minlength: { n: this.
|
|
121
|
+
maxlength: { n: this.HASH_LENGTH },
|
|
122
|
+
minlength: { n: this.HASH_LENGTH },
|
|
123
123
|
required: { active: true }
|
|
124
124
|
});
|
|
125
125
|
|
|
@@ -127,8 +127,8 @@ export abstract class SQLDialect implements DialectState {
|
|
|
127
127
|
* Path reference
|
|
128
128
|
*/
|
|
129
129
|
pathField = makeField('__path', String, true, {
|
|
130
|
-
maxlength: { n: this.
|
|
131
|
-
minlength: { n: this.
|
|
130
|
+
maxlength: { n: this.HASH_LENGTH },
|
|
131
|
+
minlength: { n: this.HASH_LENGTH },
|
|
132
132
|
required: { active: true }
|
|
133
133
|
});
|
|
134
134
|
|
|
@@ -137,38 +137,38 @@ export abstract class SQLDialect implements DialectState {
|
|
|
137
137
|
rootAlias = '_ROOT';
|
|
138
138
|
|
|
139
139
|
aliasCache = new Map<Class, Map<string, Alias>>();
|
|
140
|
-
|
|
140
|
+
namespacePrefix: string;
|
|
141
141
|
|
|
142
|
-
constructor(
|
|
142
|
+
constructor(namespacePrefix: string) {
|
|
143
143
|
this.namespace = this.namespace.bind(this);
|
|
144
144
|
this.table = this.table.bind(this);
|
|
145
|
-
this.
|
|
146
|
-
this.
|
|
145
|
+
this.identifier = this.identifier.bind(this);
|
|
146
|
+
this.namespacePrefix = namespacePrefix ? `${namespacePrefix}_` : namespacePrefix;
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
/**
|
|
150
150
|
* Get connection
|
|
151
151
|
*/
|
|
152
|
-
abstract get
|
|
152
|
+
abstract get connection(): Connection<unknown>;
|
|
153
153
|
|
|
154
154
|
/**
|
|
155
155
|
* Hash a value
|
|
156
156
|
*/
|
|
157
|
-
abstract hash(
|
|
157
|
+
abstract hash(input: string): string;
|
|
158
158
|
|
|
159
159
|
executeSQL<T>(sql: string): Promise<{ records: T[], count: number }> {
|
|
160
|
-
return this.
|
|
160
|
+
return this.connection.execute<T>(this.connection.active, sql);
|
|
161
161
|
}
|
|
162
162
|
|
|
163
163
|
/**
|
|
164
164
|
* Identify a name or field (escape it)
|
|
165
165
|
*/
|
|
166
|
-
|
|
166
|
+
identifier(field: SchemaFieldConfig | string): string {
|
|
167
167
|
if (field === '*') {
|
|
168
168
|
return field;
|
|
169
169
|
} else {
|
|
170
|
-
const name = (typeof field === '
|
|
171
|
-
return `${this.ID_AFFIX}${name
|
|
170
|
+
const name = (typeof field === 'string') ? field : field.name;
|
|
171
|
+
return `${this.ID_AFFIX}${name}${this.ID_AFFIX}`;
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
174
|
|
|
@@ -189,44 +189,44 @@ export abstract class SQLDialect implements DialectState {
|
|
|
189
189
|
/**
|
|
190
190
|
* Convert value to SQL valid representation
|
|
191
191
|
*/
|
|
192
|
-
resolveValue(
|
|
192
|
+
resolveValue(config: SchemaFieldConfig, value: unknown): string {
|
|
193
193
|
if (value === undefined || value === null) {
|
|
194
194
|
return 'NULL';
|
|
195
|
-
} else if (
|
|
195
|
+
} else if (config.type === String) {
|
|
196
196
|
if (value instanceof RegExp) {
|
|
197
|
-
const
|
|
198
|
-
return this.quote(
|
|
197
|
+
const regexSource = DataUtil.toRegex(value).source.replace(/\\b/g, this.regexWordBoundary);
|
|
198
|
+
return this.quote(regexSource);
|
|
199
199
|
} else {
|
|
200
200
|
return this.quote(castTo(value));
|
|
201
201
|
}
|
|
202
|
-
} else if (
|
|
202
|
+
} else if (config.type === Boolean) {
|
|
203
203
|
return `${value ? 'TRUE' : 'FALSE'}`;
|
|
204
|
-
} else if (
|
|
204
|
+
} else if (config.type === Number) {
|
|
205
205
|
return `${value}`;
|
|
206
|
-
} else if (
|
|
206
|
+
} else if (config.type === Date) {
|
|
207
207
|
if (typeof value === 'string' && TimeUtil.isTimeSpan(value)) {
|
|
208
208
|
return this.resolveDateValue(TimeUtil.fromNow(value));
|
|
209
209
|
} else {
|
|
210
210
|
return this.resolveDateValue(DataUtil.coerceType(value, Date, true));
|
|
211
211
|
}
|
|
212
|
-
} else if (
|
|
212
|
+
} else if (config.type === PointConcrete && Array.isArray(value)) {
|
|
213
213
|
return `point(${value[0]},${value[1]})`;
|
|
214
|
-
} else if (
|
|
214
|
+
} else if (config.type === Object) {
|
|
215
215
|
return this.quote(JSON.stringify(value).replace(/[']/g, "''"));
|
|
216
216
|
}
|
|
217
|
-
throw new AppError(`Unknown value type for field ${
|
|
217
|
+
throw new AppError(`Unknown value type for field ${config.name}, ${value}`, { category: 'data' });
|
|
218
218
|
}
|
|
219
219
|
|
|
220
220
|
/**
|
|
221
221
|
* Get column type from field config
|
|
222
222
|
*/
|
|
223
|
-
getColumnType(
|
|
223
|
+
getColumnType(config: SchemaFieldConfig): string {
|
|
224
224
|
let type: string = '';
|
|
225
225
|
|
|
226
|
-
if (
|
|
226
|
+
if (config.type === Number) {
|
|
227
227
|
type = this.COLUMN_TYPES.INT;
|
|
228
|
-
if (
|
|
229
|
-
const [digits, decimals] =
|
|
228
|
+
if (config.precision) {
|
|
229
|
+
const [digits, decimals] = config.precision;
|
|
230
230
|
if (decimals) {
|
|
231
231
|
type = this.PARAMETERIZED_COLUMN_TYPES.DECIMAL(digits, decimals);
|
|
232
232
|
} else if (digits) {
|
|
@@ -245,19 +245,19 @@ export abstract class SQLDialect implements DialectState {
|
|
|
245
245
|
} else {
|
|
246
246
|
type = this.COLUMN_TYPES.INT;
|
|
247
247
|
}
|
|
248
|
-
} else if (
|
|
248
|
+
} else if (config.type === Date) {
|
|
249
249
|
type = this.COLUMN_TYPES.TIMESTAMP;
|
|
250
|
-
} else if (
|
|
250
|
+
} else if (config.type === Boolean) {
|
|
251
251
|
type = this.COLUMN_TYPES.BOOLEAN;
|
|
252
|
-
} else if (
|
|
253
|
-
if (
|
|
252
|
+
} else if (config.type === String) {
|
|
253
|
+
if (config.specifiers?.includes('text')) {
|
|
254
254
|
type = this.COLUMN_TYPES.TEXT;
|
|
255
255
|
} else {
|
|
256
|
-
type = this.PARAMETERIZED_COLUMN_TYPES.VARCHAR(
|
|
256
|
+
type = this.PARAMETERIZED_COLUMN_TYPES.VARCHAR(config.maxlength ? config.maxlength.n : this.DEFAULT_STRING_LENGTH);
|
|
257
257
|
}
|
|
258
|
-
} else if (
|
|
258
|
+
} else if (config.type === PointConcrete) {
|
|
259
259
|
type = this.COLUMN_TYPES.POINT;
|
|
260
|
-
} else if (
|
|
260
|
+
} else if (config.type === Object) {
|
|
261
261
|
type = this.COLUMN_TYPES.JSON;
|
|
262
262
|
}
|
|
263
263
|
|
|
@@ -267,12 +267,12 @@ export abstract class SQLDialect implements DialectState {
|
|
|
267
267
|
/**
|
|
268
268
|
* FieldConfig to Column definition
|
|
269
269
|
*/
|
|
270
|
-
getColumnDefinition(
|
|
271
|
-
const type = this.getColumnType(
|
|
270
|
+
getColumnDefinition(config: SchemaFieldConfig): string | undefined {
|
|
271
|
+
const type = this.getColumnType(config);
|
|
272
272
|
if (!type) {
|
|
273
273
|
return;
|
|
274
274
|
}
|
|
275
|
-
return `${this.
|
|
275
|
+
return `${this.identifier(config)} ${type} ${(config.required?.active !== false) ? 'NOT NULL' : 'DEFAULT NULL'}`;
|
|
276
276
|
}
|
|
277
277
|
|
|
278
278
|
/**
|
|
@@ -297,7 +297,7 @@ export abstract class SQLDialect implements DialectState {
|
|
|
297
297
|
*/
|
|
298
298
|
getDropColumnSQL(stack: VisitStack[]): string {
|
|
299
299
|
const field = stack.at(-1)!;
|
|
300
|
-
return `ALTER TABLE ${this.parentTable(stack)} DROP COLUMN ${this.
|
|
300
|
+
return `ALTER TABLE ${this.parentTable(stack)} DROP COLUMN ${this.identifier(field.name)};`;
|
|
301
301
|
}
|
|
302
302
|
|
|
303
303
|
/**
|
|
@@ -317,7 +317,7 @@ export abstract class SQLDialect implements DialectState {
|
|
|
317
317
|
* Determine table/field namespace for a given stack location
|
|
318
318
|
*/
|
|
319
319
|
namespace(stack: VisitStack[]): string {
|
|
320
|
-
return `${this.
|
|
320
|
+
return `${this.namespacePrefix}${SQLModelUtil.buildTable(stack)}`;
|
|
321
321
|
}
|
|
322
322
|
|
|
323
323
|
/**
|
|
@@ -331,7 +331,7 @@ export abstract class SQLDialect implements DialectState {
|
|
|
331
331
|
* Determine table name for a given stack location
|
|
332
332
|
*/
|
|
333
333
|
table(stack: VisitStack[]): string {
|
|
334
|
-
return this.
|
|
334
|
+
return this.identifier(this.namespace(stack));
|
|
335
335
|
}
|
|
336
336
|
|
|
337
337
|
/**
|
|
@@ -351,8 +351,8 @@ export abstract class SQLDialect implements DialectState {
|
|
|
351
351
|
/**
|
|
352
352
|
* Alias a field for usage
|
|
353
353
|
*/
|
|
354
|
-
alias(field: string |
|
|
355
|
-
return `${alias}.${this.
|
|
354
|
+
alias(field: string | SchemaFieldConfig, alias: string = this.rootAlias): string {
|
|
355
|
+
return `${alias}.${this.identifier(field)}`;
|
|
356
356
|
}
|
|
357
357
|
|
|
358
358
|
/**
|
|
@@ -376,12 +376,12 @@ export abstract class SQLDialect implements DialectState {
|
|
|
376
376
|
},
|
|
377
377
|
onSub: ({ descend, config, path }) => {
|
|
378
378
|
const table = resolve(path);
|
|
379
|
-
clauses.set(table, { alias: `${config.name.
|
|
379
|
+
clauses.set(table, { alias: `${config.name.charAt(0)}${idx++}`, path });
|
|
380
380
|
return descend();
|
|
381
381
|
},
|
|
382
382
|
onSimple: ({ config, path }) => {
|
|
383
383
|
const table = resolve(path);
|
|
384
|
-
clauses.set(table, { alias: `${config.name.
|
|
384
|
+
clauses.set(table, { alias: `${config.name.charAt(0)}${idx++}`, path });
|
|
385
385
|
}
|
|
386
386
|
});
|
|
387
387
|
|
|
@@ -404,13 +404,13 @@ export abstract class SQLDialect implements DialectState {
|
|
|
404
404
|
/**
|
|
405
405
|
* Generate WHERE field clause
|
|
406
406
|
*/
|
|
407
|
-
getWhereFieldSQL(stack: VisitStack[],
|
|
407
|
+
getWhereFieldSQL(stack: VisitStack[], input: Record<string, unknown>): string {
|
|
408
408
|
const items = [];
|
|
409
409
|
const { foreignMap, localMap } = SQLModelUtil.getFieldsByLocation(stack);
|
|
410
410
|
const SQL_OPS = this.SQL_OPS;
|
|
411
411
|
|
|
412
|
-
for (const key of Object.keys(
|
|
413
|
-
const top =
|
|
412
|
+
for (const key of Object.keys(input)) {
|
|
413
|
+
const top = input[key];
|
|
414
414
|
const field = localMap[key] ?? foreignMap[key];
|
|
415
415
|
if (!field) {
|
|
416
416
|
throw new Error(`Unknown field: ${key}`);
|
|
@@ -432,38 +432,38 @@ export abstract class SQLDialect implements DialectState {
|
|
|
432
432
|
const inner = this.getWhereFieldSQL(sStack, top);
|
|
433
433
|
items.push(inner);
|
|
434
434
|
} else {
|
|
435
|
-
const
|
|
435
|
+
const value = top[subKey];
|
|
436
436
|
const resolve = this.resolveValue.bind(this, field);
|
|
437
437
|
|
|
438
438
|
switch (subKey) {
|
|
439
439
|
case '$nin': case '$in': {
|
|
440
|
-
const arr = (Array.isArray(
|
|
440
|
+
const arr = (Array.isArray(value) ? value : [value]).map(item => resolve(item));
|
|
441
441
|
items.push(`${sPath} ${SQL_OPS[subKey]} (${arr.join(',')})`);
|
|
442
442
|
break;
|
|
443
443
|
}
|
|
444
444
|
case '$all': {
|
|
445
445
|
const set = new Set();
|
|
446
|
-
const arr = [
|
|
446
|
+
const arr = [value].flat().filter(item => !set.has(item) && !!set.add(item)).map(item => resolve(item));
|
|
447
447
|
const valueTable = this.parentTable(sStack);
|
|
448
448
|
const alias = `_all_${sStack.length}`;
|
|
449
|
-
const pPath = this.
|
|
449
|
+
const pPath = this.identifier(this.parentPathField.name);
|
|
450
450
|
const rpPath = this.resolveName([...sStack, field, this.parentPathField]);
|
|
451
451
|
|
|
452
452
|
items.push(`${arr.length} = (
|
|
453
|
-
SELECT COUNT(DISTINCT ${alias}.${this.
|
|
453
|
+
SELECT COUNT(DISTINCT ${alias}.${this.identifier(field.name)})
|
|
454
454
|
FROM ${valueTable} ${alias}
|
|
455
455
|
WHERE ${alias}.${pPath} = ${rpPath}
|
|
456
|
-
AND ${alias}.${this.
|
|
456
|
+
AND ${alias}.${this.identifier(field.name)} IN (${arr.join(',')})
|
|
457
457
|
)`);
|
|
458
458
|
break;
|
|
459
459
|
}
|
|
460
460
|
case '$regex': {
|
|
461
|
-
const
|
|
462
|
-
const
|
|
463
|
-
const ins =
|
|
461
|
+
const regex = DataUtil.toRegex(castTo(value));
|
|
462
|
+
const regexSource = regex.source;
|
|
463
|
+
const ins = regex.flags && regex.flags.includes('i');
|
|
464
464
|
|
|
465
|
-
if (/^[\^]\S+[.][*][$]?$/.test(
|
|
466
|
-
const inner =
|
|
465
|
+
if (/^[\^]\S+[.][*][$]?$/.test(regexSource)) {
|
|
466
|
+
const inner = regexSource.substring(1, regexSource.length - 2);
|
|
467
467
|
if (!ins || SQL_OPS.$ilike) {
|
|
468
468
|
items.push(`${sPath} ${ins ? SQL_OPS.$ilike : SQL_OPS.$like} ${resolve(`${inner}%`)}`);
|
|
469
469
|
} else {
|
|
@@ -471,11 +471,11 @@ export abstract class SQLDialect implements DialectState {
|
|
|
471
471
|
}
|
|
472
472
|
} else {
|
|
473
473
|
if (!ins || SQL_OPS.$iregex) {
|
|
474
|
-
const
|
|
475
|
-
items.push(`${sPath} ${SQL_OPS[!ins ? subKey : '$iregex']} ${
|
|
474
|
+
const result = resolve(value);
|
|
475
|
+
items.push(`${sPath} ${SQL_OPS[!ins ? subKey : '$iregex']} ${result}`);
|
|
476
476
|
} else {
|
|
477
|
-
const
|
|
478
|
-
items.push(`LOWER(${sPath}) ${SQL_OPS[subKey]} ${
|
|
477
|
+
const result = resolve(new RegExp(regexSource.toLowerCase(), regex.flags));
|
|
478
|
+
items.push(`LOWER(${sPath}) ${SQL_OPS[subKey]} ${result}`);
|
|
479
479
|
}
|
|
480
480
|
}
|
|
481
481
|
break;
|
|
@@ -484,31 +484,31 @@ export abstract class SQLDialect implements DialectState {
|
|
|
484
484
|
if (field.array) {
|
|
485
485
|
const valueTable = this.parentTable(sStack);
|
|
486
486
|
const alias = `_all_${sStack.length}`;
|
|
487
|
-
const pPath = this.
|
|
487
|
+
const pPath = this.identifier(this.parentPathField.name);
|
|
488
488
|
const rpPath = this.resolveName([...sStack, field, this.parentPathField]);
|
|
489
489
|
|
|
490
|
-
items.push(`0 ${!
|
|
491
|
-
SELECT COUNT(${alias}.${this.
|
|
490
|
+
items.push(`0 ${!value ? '=' : '<>'} (
|
|
491
|
+
SELECT COUNT(${alias}.${this.identifier(field.name)})
|
|
492
492
|
FROM ${valueTable} ${alias}
|
|
493
493
|
WHERE ${alias}.${pPath} = ${rpPath}
|
|
494
494
|
)`);
|
|
495
495
|
} else {
|
|
496
|
-
items.push(`${sPath} ${
|
|
496
|
+
items.push(`${sPath} ${value ? SQL_OPS.$isNot : SQL_OPS.$is} NULL`);
|
|
497
497
|
}
|
|
498
498
|
break;
|
|
499
499
|
}
|
|
500
500
|
case '$ne': case '$eq': {
|
|
501
|
-
if (
|
|
501
|
+
if (value === null || value === undefined) {
|
|
502
502
|
items.push(`${sPath} ${subKey === '$ne' ? SQL_OPS.$isNot : SQL_OPS.$is} NULL`);
|
|
503
503
|
} else {
|
|
504
|
-
const base = `${sPath} ${SQL_OPS[subKey]} ${resolve(
|
|
504
|
+
const base = `${sPath} ${SQL_OPS[subKey]} ${resolve(value)}`;
|
|
505
505
|
items.push(subKey === '$ne' ? `(${base} OR ${sPath} ${SQL_OPS.$is} NULL)` : base);
|
|
506
506
|
}
|
|
507
507
|
break;
|
|
508
508
|
}
|
|
509
509
|
case '$lt': case '$gt': case '$gte': case '$lte': {
|
|
510
510
|
const subItems = TypedObject.keys(castTo<typeof SQL_OPS>(top))
|
|
511
|
-
.map(
|
|
511
|
+
.map(subSubKey => `${sPath} ${SQL_OPS[subSubKey]} ${resolve(top[subSubKey])}`);
|
|
512
512
|
items.push(subItems.length > 1 ? `(${subItems.join(` ${SQL_OPS.$and} `)})` : subItems[0]);
|
|
513
513
|
break;
|
|
514
514
|
}
|
|
@@ -534,17 +534,17 @@ export abstract class SQLDialect implements DialectState {
|
|
|
534
534
|
/**
|
|
535
535
|
* Grouping of where clauses
|
|
536
536
|
*/
|
|
537
|
-
getWhereGroupingSQL<T>(cls: Class<T>,
|
|
537
|
+
getWhereGroupingSQL<T>(cls: Class<T>, clause: WhereClause<T>): string {
|
|
538
538
|
const SQL_OPS = this.SQL_OPS;
|
|
539
539
|
|
|
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,
|
|
540
|
+
if (ModelQueryUtil.has$And(clause)) {
|
|
541
|
+
return `(${clause.$and.map(item => this.getWhereGroupingSQL<T>(cls, item)).join(` ${SQL_OPS.$and} `)})`;
|
|
542
|
+
} else if (ModelQueryUtil.has$Or(clause)) {
|
|
543
|
+
return `(${clause.$or.map(item => this.getWhereGroupingSQL<T>(cls, item)).join(` ${SQL_OPS.$or} `)})`;
|
|
544
|
+
} else if (ModelQueryUtil.has$Not(clause)) {
|
|
545
|
+
return `${SQL_OPS.$not} (${this.getWhereGroupingSQL<T>(cls, clause.$not)})`;
|
|
546
546
|
} else {
|
|
547
|
-
return this.getWhereFieldSQL(SQLModelUtil.classToStack(cls),
|
|
547
|
+
return this.getWhereFieldSQL(SQLModelUtil.classToStack(cls), clause);
|
|
548
548
|
}
|
|
549
549
|
}
|
|
550
550
|
|
|
@@ -563,8 +563,8 @@ export abstract class SQLDialect implements DialectState {
|
|
|
563
563
|
getOrderBySQL<T>(cls: Class<T>, sortBy?: SortClause<T>[]): string {
|
|
564
564
|
return !sortBy ?
|
|
565
565
|
'' :
|
|
566
|
-
`ORDER BY ${SQLModelUtil.orderBy(cls, sortBy).map((
|
|
567
|
-
`${this.resolveName(
|
|
566
|
+
`ORDER BY ${SQLModelUtil.orderBy(cls, sortBy).map((item) =>
|
|
567
|
+
`${this.resolveName(item.stack)} ${item.asc ? 'ASC' : 'DESC'}`
|
|
568
568
|
).join(', ')}`;
|
|
569
569
|
}
|
|
570
570
|
|
|
@@ -591,7 +591,7 @@ export abstract class SQLDialect implements DialectState {
|
|
|
591
591
|
const tables = [...aliases.keys()].toSorted((a, b) => a.length - b.length); // Shortest first
|
|
592
592
|
return `FROM ${tables.map((table) => {
|
|
593
593
|
const { alias, path } = aliases.get(table)!;
|
|
594
|
-
let from = `${this.
|
|
594
|
+
let from = `${this.identifier(table)} ${alias}`;
|
|
595
595
|
if (path.length > 1) {
|
|
596
596
|
const key = this.namespaceParent(path);
|
|
597
597
|
const { alias: parentAlias } = aliases.get(key)!;
|
|
@@ -620,7 +620,7 @@ LEFT OUTER JOIN ${from} ON
|
|
|
620
620
|
const sortFields = !query.sort ?
|
|
621
621
|
'' :
|
|
622
622
|
SQLModelUtil.orderBy(cls, query.sort)
|
|
623
|
-
.map(
|
|
623
|
+
.map(item => this.resolveName(item.stack))
|
|
624
624
|
.join(', ');
|
|
625
625
|
|
|
626
626
|
// TODO: Really confused on this
|
|
@@ -649,21 +649,21 @@ ${this.getLimitSQL(cls, query)}`;
|
|
|
649
649
|
(array ? [castTo<SchemaFieldConfig>(config)] : []);
|
|
650
650
|
|
|
651
651
|
if (!parent) {
|
|
652
|
-
let idField = fields.find(
|
|
652
|
+
let idField = fields.find(field => field.name === this.idField.name);
|
|
653
653
|
if (!idField) {
|
|
654
654
|
fields.push(idField = this.idField);
|
|
655
655
|
} else {
|
|
656
|
-
idField.maxlength = { n: this.
|
|
656
|
+
idField.maxlength = { n: this.ID_LENGTH };
|
|
657
657
|
}
|
|
658
658
|
}
|
|
659
659
|
|
|
660
660
|
const fieldSql = fields
|
|
661
|
-
.map(
|
|
662
|
-
const def = this.getColumnDefinition(
|
|
663
|
-
return
|
|
661
|
+
.map(field => {
|
|
662
|
+
const def = this.getColumnDefinition(field) || '';
|
|
663
|
+
return field.name === this.idField.name && !parent ?
|
|
664
664
|
def.replace('DEFAULT NULL', 'NOT NULL') : def;
|
|
665
665
|
})
|
|
666
|
-
.filter(
|
|
666
|
+
.filter(line => !!line.trim())
|
|
667
667
|
.join(',\n ');
|
|
668
668
|
|
|
669
669
|
const out = `
|
|
@@ -671,11 +671,11 @@ CREATE TABLE IF NOT EXISTS ${this.table(stack)} (
|
|
|
671
671
|
${fieldSql}${fieldSql.length ? ',' : ''}
|
|
672
672
|
${this.getColumnDefinition(this.pathField)} UNIQUE,
|
|
673
673
|
${!parent ?
|
|
674
|
-
`PRIMARY KEY (${this.
|
|
674
|
+
`PRIMARY KEY (${this.identifier(this.idField)})` :
|
|
675
675
|
`${this.getColumnDefinition(this.parentPathField)},
|
|
676
676
|
${array ? `${this.getColumnDefinition(this.idxField)},` : ''}
|
|
677
|
-
PRIMARY KEY (${this.
|
|
678
|
-
FOREIGN KEY (${this.
|
|
677
|
+
PRIMARY KEY (${this.identifier(this.pathField)}),
|
|
678
|
+
FOREIGN KEY (${this.identifier(this.parentPathField)}) REFERENCES ${this.parentTable(stack)}(${this.identifier(this.pathField)}) ON DELETE CASCADE`}
|
|
679
679
|
);`;
|
|
680
680
|
return out;
|
|
681
681
|
}
|
|
@@ -719,17 +719,17 @@ CREATE TABLE IF NOT EXISTS ${this.table(stack)} (
|
|
|
719
719
|
*/
|
|
720
720
|
getCreateIndexSQL<T extends ModelType>(cls: Class<T>, idx: IndexConfig<T>): string {
|
|
721
721
|
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(
|
|
722
|
+
const fields: [string, boolean][] = idx.fields.map(field => {
|
|
723
|
+
const key = TypedObject.keys(field)[0];
|
|
724
|
+
const value = field[key];
|
|
725
|
+
if (DataUtil.isPlainObject(value)) {
|
|
726
726
|
throw new Error('Unable to supported nested fields for indices');
|
|
727
727
|
}
|
|
728
|
-
return [castTo(key), typeof
|
|
728
|
+
return [castTo(key), typeof value === 'number' ? value === 1 : (!!value)];
|
|
729
729
|
});
|
|
730
|
-
const constraint = `idx_${table}_${fields.map(([
|
|
731
|
-
return `CREATE ${idx.type === 'unique' ? 'UNIQUE ' : ''}INDEX ${constraint} ON ${this.
|
|
732
|
-
.map(([name, sel]) => `${this.
|
|
730
|
+
const constraint = `idx_${table}_${fields.map(([field]) => field).join('_')}`;
|
|
731
|
+
return `CREATE ${idx.type === 'unique' ? 'UNIQUE ' : ''}INDEX ${constraint} ON ${this.identifier(table)} (${fields
|
|
732
|
+
.map(([name, sel]) => `${this.identifier(name)} ${sel ? 'ASC' : 'DESC'}`)
|
|
733
733
|
.join(', ')});`;
|
|
734
734
|
}
|
|
735
735
|
|
|
@@ -765,30 +765,30 @@ CREATE TABLE IF NOT EXISTS ${this.table(stack)} (
|
|
|
765
765
|
getInsertSQL(stack: VisitStack[], instances: InsertWrapper['records']): string | undefined {
|
|
766
766
|
const config = stack.at(-1)!;
|
|
767
767
|
const columns = SQLModelUtil.getFieldsByLocation(stack).local
|
|
768
|
-
.filter(
|
|
769
|
-
.toSorted((a, b) => a.name.
|
|
770
|
-
const columnNames = columns.map(
|
|
768
|
+
.filter(field => !SchemaRegistryIndex.has(field.type))
|
|
769
|
+
.toSorted((a, b) => a.name.localeCompare(b.name));
|
|
770
|
+
const columnNames = columns.map(column => column.name);
|
|
771
771
|
|
|
772
772
|
const hasParent = stack.length > 1;
|
|
773
773
|
const isArray = !!config.array;
|
|
774
774
|
|
|
775
775
|
if (isArray) {
|
|
776
776
|
const newInstances: typeof instances = [];
|
|
777
|
-
for (const
|
|
778
|
-
if (
|
|
777
|
+
for (const instance of instances) {
|
|
778
|
+
if (instance.value === null || instance.value === undefined) {
|
|
779
779
|
continue;
|
|
780
|
-
} else if (Array.isArray(
|
|
781
|
-
const name =
|
|
782
|
-
for (const sel of
|
|
780
|
+
} else if (Array.isArray(instance.value)) {
|
|
781
|
+
const name = instance.stack.at(-1)!.name;
|
|
782
|
+
for (const sel of instance.value) {
|
|
783
783
|
newInstances.push({
|
|
784
|
-
stack:
|
|
784
|
+
stack: instance.stack,
|
|
785
785
|
value: {
|
|
786
786
|
[name]: sel
|
|
787
787
|
}
|
|
788
788
|
});
|
|
789
789
|
}
|
|
790
790
|
} else {
|
|
791
|
-
newInstances.push(
|
|
791
|
+
newInstances.push(instance);
|
|
792
792
|
}
|
|
793
793
|
}
|
|
794
794
|
instances = newInstances;
|
|
@@ -798,7 +798,8 @@ CREATE TABLE IF NOT EXISTS ${this.table(stack)} (
|
|
|
798
798
|
return;
|
|
799
799
|
}
|
|
800
800
|
|
|
801
|
-
const matrix = instances.map(inst => columns.map(
|
|
801
|
+
const matrix = instances.map(inst => columns.map(column =>
|
|
802
|
+
this.resolveValue(column, castTo<Record<string, unknown>>(inst.value)[column.name])));
|
|
802
803
|
|
|
803
804
|
columnNames.push(this.pathField.name);
|
|
804
805
|
if (hasParent) {
|
|
@@ -824,7 +825,7 @@ CREATE TABLE IF NOT EXISTS ${this.table(stack)} (
|
|
|
824
825
|
}
|
|
825
826
|
|
|
826
827
|
return `
|
|
827
|
-
INSERT INTO ${this.table(stack)} (${columnNames.map(this.
|
|
828
|
+
INSERT INTO ${this.table(stack)} (${columnNames.map(this.identifier).join(', ')})
|
|
828
829
|
VALUES
|
|
829
830
|
${matrix.map(row => `(${row.join(', ')})`).join(',\n')};`;
|
|
830
831
|
}
|
|
@@ -854,8 +855,8 @@ UPDATE ${this.table(stack)} ${this.rootAlias}
|
|
|
854
855
|
SET
|
|
855
856
|
${Object
|
|
856
857
|
.entries(data)
|
|
857
|
-
.filter(([
|
|
858
|
-
.map(([
|
|
858
|
+
.filter(([key]) => key in localMap)
|
|
859
|
+
.map(([key, value]) => `${this.identifier(key)}=${this.resolveValue(localMap[key], value)}`).join(', ')}
|
|
859
860
|
${this.getWhereSQL(type, where)};`;
|
|
860
861
|
}
|
|
861
862
|
|
|
@@ -874,12 +875,12 @@ ${this.getWhereSQL(type, where)};`;
|
|
|
874
875
|
const config = stack.at(-1)!;
|
|
875
876
|
const orderBy = !config.array ?
|
|
876
877
|
'' :
|
|
877
|
-
`ORDER BY ${this.rootAlias}.${this.idxField.name
|
|
878
|
+
`ORDER BY ${this.rootAlias}.${this.idxField.name} ASC`;
|
|
878
879
|
|
|
879
880
|
const idField = (stack.length > 1 ? this.parentPathField : this.idField);
|
|
880
881
|
|
|
881
882
|
return `
|
|
882
|
-
SELECT ${select.length ? select.map(
|
|
883
|
+
SELECT ${select.length ? select.map(field => this.alias(field)).join(',') : '*'}
|
|
883
884
|
FROM ${this.table(stack)} ${this.rootAlias}
|
|
884
885
|
WHERE ${this.alias(idField)} IN (${ids.map(id => this.resolveValue(idField, id)).join(', ')})
|
|
885
886
|
${orderBy};`;
|
|
@@ -904,9 +905,9 @@ ${this.getWhereSQL(cls, where!)}`;
|
|
|
904
905
|
|
|
905
906
|
await SQLModelUtil.visitSchema(SchemaRegistryIndex.getConfig(cls), {
|
|
906
907
|
onRoot: async (config) => {
|
|
907
|
-
const
|
|
908
|
+
const fieldSet = buildSet(items); // Already filtered by initial select query
|
|
908
909
|
selectStack.push(select);
|
|
909
|
-
stack.push(
|
|
910
|
+
stack.push(fieldSet);
|
|
910
911
|
await config.descend();
|
|
911
912
|
},
|
|
912
913
|
onSub: async ({ config, descend, fields, path }) => {
|
|
@@ -917,28 +918,28 @@ ${this.getWhereSQL(cls, where!)}`;
|
|
|
917
918
|
const subSelectTop: SelectClause<T> | undefined = castTo(selectTop?.[fieldKey]);
|
|
918
919
|
|
|
919
920
|
// See if a selection exists at all
|
|
920
|
-
const
|
|
921
|
-
.filter(
|
|
921
|
+
const selected: SchemaFieldConfig[] = subSelectTop ? fields
|
|
922
|
+
.filter(field => typeof subSelectTop === 'object' && subSelectTop[castTo<typeof fieldKey>(field.name)] === 1)
|
|
922
923
|
: [];
|
|
923
924
|
|
|
924
|
-
if (
|
|
925
|
-
|
|
925
|
+
if (selected.length) {
|
|
926
|
+
selected.push(this.pathField, this.parentPathField);
|
|
926
927
|
if (config.array) {
|
|
927
|
-
|
|
928
|
+
selected.push(this.idxField);
|
|
928
929
|
}
|
|
929
930
|
}
|
|
930
931
|
|
|
931
932
|
// If children and selection exists
|
|
932
|
-
if (ids.length && (!subSelectTop ||
|
|
933
|
+
if (ids.length && (!subSelectTop || selected)) {
|
|
933
934
|
const { records: children } = await this.executeSQL<unknown[]>(this.getSelectRowsByIdsSQL(
|
|
934
935
|
path,
|
|
935
936
|
ids,
|
|
936
|
-
|
|
937
|
+
selected
|
|
937
938
|
));
|
|
938
939
|
|
|
939
|
-
const
|
|
940
|
+
const fieldSet = buildSet(children, config);
|
|
940
941
|
try {
|
|
941
|
-
stack.push(
|
|
942
|
+
stack.push(fieldSet);
|
|
942
943
|
selectStack.push(subSelectTop);
|
|
943
944
|
await descend();
|
|
944
945
|
} finally {
|
|
@@ -982,30 +983,34 @@ ${this.getWhereSQL(cls, where!)}`;
|
|
|
982
983
|
async bulkProcess(deletes: DeleteWrapper[], inserts: InsertWrapper[], upserts: InsertWrapper[], updates: InsertWrapper[]): Promise<BulkResponse> {
|
|
983
984
|
const out = {
|
|
984
985
|
counts: {
|
|
985
|
-
delete: deletes.reduce((
|
|
986
|
+
delete: deletes.reduce((count, item) => count + item.ids.length, 0),
|
|
986
987
|
error: 0,
|
|
987
|
-
insert: inserts.filter(
|
|
988
|
-
update: updates.filter(
|
|
989
|
-
upsert: upserts.filter(
|
|
988
|
+
insert: inserts.filter(item => item.stack.length === 1).reduce((count, item) => count + item.records.length, 0),
|
|
989
|
+
update: updates.filter(item => item.stack.length === 1).reduce((count, item) => count + item.records.length, 0),
|
|
990
|
+
upsert: upserts.filter(item => item.stack.length === 1).reduce((count, item) => count + item.records.length, 0)
|
|
990
991
|
},
|
|
991
992
|
errors: [],
|
|
992
993
|
insertedIds: new Map()
|
|
993
994
|
};
|
|
994
995
|
|
|
995
996
|
// Full removals
|
|
996
|
-
await Promise.all(deletes.map(
|
|
997
|
+
await Promise.all(deletes.map(item => this.deleteByIds(item.stack, item.ids)));
|
|
997
998
|
|
|
998
999
|
// Adding deletes
|
|
999
1000
|
if (upserts.length || updates.length) {
|
|
1000
1001
|
const idx = this.idField.name;
|
|
1001
1002
|
|
|
1002
1003
|
await Promise.all([
|
|
1003
|
-
...upserts
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1004
|
+
...upserts
|
|
1005
|
+
.filter(item => item.stack.length === 1)
|
|
1006
|
+
.map(item =>
|
|
1007
|
+
this.deleteByIds(item.stack, item.records.map(value => castTo<Record<string, string>>(value.value)[idx]))
|
|
1008
|
+
),
|
|
1009
|
+
...updates
|
|
1010
|
+
.filter(item => item.stack.length === 1)
|
|
1011
|
+
.map(item =>
|
|
1012
|
+
this.deleteByIds(item.stack, item.records.map(value => castTo<Record<string, string>>(value.value)[idx]))
|
|
1013
|
+
),
|
|
1009
1014
|
]);
|
|
1010
1015
|
}
|
|
1011
1016
|
|
|
@@ -1014,17 +1019,17 @@ ${this.getWhereSQL(cls, where!)}`;
|
|
|
1014
1019
|
if (!items.length) {
|
|
1015
1020
|
continue;
|
|
1016
1021
|
}
|
|
1017
|
-
let
|
|
1022
|
+
let level = 1; // Add by level
|
|
1018
1023
|
for (; ;) { // Loop until done
|
|
1019
|
-
const leveled = items.filter(
|
|
1024
|
+
const leveled = items.filter(insertWrapper => insertWrapper.stack.length === level);
|
|
1020
1025
|
if (!leveled.length) {
|
|
1021
1026
|
break;
|
|
1022
1027
|
}
|
|
1023
1028
|
await Promise.all(leveled
|
|
1024
|
-
.map(
|
|
1029
|
+
.map(inserted => this.getInsertSQL(inserted.stack, inserted.records))
|
|
1025
1030
|
.filter(sql => !!sql)
|
|
1026
1031
|
.map(sql => this.executeSQL(sql!)));
|
|
1027
|
-
|
|
1032
|
+
level += 1;
|
|
1028
1033
|
}
|
|
1029
1034
|
}
|
|
1030
1035
|
|