@travetto/model-sql 8.0.0-alpha.24 → 8.0.0-alpha.26
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/README.md +29 -9
- package/__index__.ts +3 -6
- package/package.json +8 -8
- package/src/connection.ts +218 -0
- package/src/dialect.ts +799 -0
- package/src/schema.ts +46 -0
- package/src/service.ts +715 -386
- package/src/types.ts +15 -9
- package/support/test/query.ts +111 -72
- package/src/config.ts +0 -45
- package/src/connection/base.ts +0 -188
- package/src/connection/decorator.ts +0 -54
- package/src/dialect/base.ts +0 -1142
- package/src/internal/types.ts +0 -64
- package/src/table-manager.ts +0 -169
- package/src/util.ts +0 -331
package/src/dialect/base.ts
DELETED
|
@@ -1,1142 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @stylistic/indent */
|
|
2
|
-
import { DataUtil, type SchemaFieldConfig, SchemaRegistryIndex, type Point } from '@travetto/schema';
|
|
3
|
-
import { type Class, RuntimeError, TypedObject, TimeUtil, castTo, castKey, toConcrete, JSONUtil } from '@travetto/runtime';
|
|
4
|
-
import { type SelectClause, type Query, type SortClause, type WhereClause, type RetainQueryPrimitiveFields, ModelQueryUtil, isModelQueryIndex } from '@travetto/model-query';
|
|
5
|
-
import { IndexNotSupported, type BulkResponse, type IndexConfig, type ModelType } from '@travetto/model';
|
|
6
|
-
import { isModelIndexedIndex } from '@travetto/model-indexed';
|
|
7
|
-
|
|
8
|
-
import { SQLModelUtil } from '../util.ts';
|
|
9
|
-
import type { DeleteWrapper, InsertWrapper, DialectState } from '../internal/types.ts';
|
|
10
|
-
import type { Connection } from '../connection/base.ts';
|
|
11
|
-
import type { VisitStack } from '../types.ts';
|
|
12
|
-
|
|
13
|
-
const PointConcrete = toConcrete<Point>();
|
|
14
|
-
|
|
15
|
-
interface Alias {
|
|
16
|
-
alias: string;
|
|
17
|
-
path: VisitStack[];
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export type SQLTableDescription = {
|
|
21
|
-
columns: { name: string, type: string, is_not_null: boolean }[];
|
|
22
|
-
foreignKeys: { name: string, from_column: string, to_column: string, to_table: string }[];
|
|
23
|
-
indices: { name: string, columns: { name: string, desc: boolean }[], is_unique: boolean }[];
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
function makeField(name: string, type: Class, required: boolean, extra: Partial<SchemaFieldConfig>): SchemaFieldConfig {
|
|
27
|
-
return {
|
|
28
|
-
name,
|
|
29
|
-
class: null!,
|
|
30
|
-
type,
|
|
31
|
-
array: false,
|
|
32
|
-
...(required ? { required: { active: true } } : {}),
|
|
33
|
-
...extra
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Base sql dialect
|
|
39
|
-
*/
|
|
40
|
-
export abstract class SQLDialect implements DialectState {
|
|
41
|
-
/**
|
|
42
|
-
* Default length of unique ids
|
|
43
|
-
*/
|
|
44
|
-
ID_LENGTH = 32;
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Hash Length
|
|
48
|
-
*/
|
|
49
|
-
HASH_LENGTH = 64;
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Default length for varchar
|
|
53
|
-
*/
|
|
54
|
-
DEFAULT_STRING_LENGTH = 1024;
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Mapping between query operators and SQL operations
|
|
58
|
-
*/
|
|
59
|
-
SQL_OPS = {
|
|
60
|
-
$and: 'AND',
|
|
61
|
-
$or: 'OR',
|
|
62
|
-
$not: 'NOT',
|
|
63
|
-
$all: '=ALL',
|
|
64
|
-
$regex: undefined,
|
|
65
|
-
$iregex: undefined,
|
|
66
|
-
$in: 'IN',
|
|
67
|
-
$nin: 'NOT IN',
|
|
68
|
-
$eq: '=',
|
|
69
|
-
$ne: '<>',
|
|
70
|
-
$gte: '>=',
|
|
71
|
-
$like: 'LIKE',
|
|
72
|
-
$ilike: 'ILIKE',
|
|
73
|
-
$lte: '<=',
|
|
74
|
-
$gt: '>',
|
|
75
|
-
$lt: '<',
|
|
76
|
-
$is: 'IS',
|
|
77
|
-
$isNot: 'IS NOT'
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Column type mapping
|
|
82
|
-
*/
|
|
83
|
-
COLUMN_TYPES = {
|
|
84
|
-
JSON: '',
|
|
85
|
-
POINT: 'POINT',
|
|
86
|
-
BOOLEAN: 'BOOLEAN',
|
|
87
|
-
TINYINT: 'TINYINT',
|
|
88
|
-
SMALLINT: 'SMALLINT',
|
|
89
|
-
MEDIUMINT: 'MEDIUMINT',
|
|
90
|
-
INT: 'INT',
|
|
91
|
-
BIGINT: 'BIGINT',
|
|
92
|
-
TIMESTAMP: 'TIMESTAMP',
|
|
93
|
-
TEXT: 'TEXT'
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Column types with inputs
|
|
98
|
-
*/
|
|
99
|
-
PARAMETERIZED_COLUMN_TYPES: Record<'VARCHAR' | 'DECIMAL', (...values: number[]) => string> = {
|
|
100
|
-
VARCHAR: count => `VARCHAR(${count})`,
|
|
101
|
-
DECIMAL: (digits, precision) => `DECIMAL(${digits},${precision})`
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
ID_AFFIX = '`';
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Generate an id field
|
|
108
|
-
*/
|
|
109
|
-
idField = makeField('id', String, true, {
|
|
110
|
-
maxlength: { limit: this.ID_LENGTH },
|
|
111
|
-
minlength: { limit: this.ID_LENGTH }
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Generate an idx field
|
|
116
|
-
*/
|
|
117
|
-
idxField = makeField('__idx', Number, true, {});
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Parent path reference
|
|
121
|
-
*/
|
|
122
|
-
parentPathField = makeField('__parent_path', String, true, {
|
|
123
|
-
maxlength: { limit: this.HASH_LENGTH },
|
|
124
|
-
minlength: { limit: this.HASH_LENGTH },
|
|
125
|
-
required: { active: true }
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Path reference
|
|
130
|
-
*/
|
|
131
|
-
pathField = makeField('__path', String, true, {
|
|
132
|
-
maxlength: { limit: this.HASH_LENGTH },
|
|
133
|
-
minlength: { limit: this.HASH_LENGTH },
|
|
134
|
-
required: { active: true }
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
regexWordBoundary = '\\b';
|
|
138
|
-
|
|
139
|
-
rootAlias = '_ROOT';
|
|
140
|
-
|
|
141
|
-
aliasCache = new Map<Class, Map<string, Alias>>();
|
|
142
|
-
namespacePrefix: string;
|
|
143
|
-
|
|
144
|
-
constructor(namespacePrefix: string) {
|
|
145
|
-
this.namespace = this.namespace.bind(this);
|
|
146
|
-
this.table = this.table.bind(this);
|
|
147
|
-
this.identifier = this.identifier.bind(this);
|
|
148
|
-
this.namespacePrefix = namespacePrefix ? `${namespacePrefix}_` : namespacePrefix;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Get connection
|
|
153
|
-
*/
|
|
154
|
-
abstract get connection(): Connection<unknown>;
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Hash a value
|
|
158
|
-
*/
|
|
159
|
-
abstract hash(input: string): string;
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Describe a table structure
|
|
163
|
-
*/
|
|
164
|
-
abstract describeTable(table: string): Promise<SQLTableDescription | undefined>;
|
|
165
|
-
|
|
166
|
-
executeSQL<T>(sql: string): Promise<{ records: T[], count: number }> {
|
|
167
|
-
return this.connection.execute<T>(this.connection.active, sql);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Identify a name or field (escape it)
|
|
172
|
-
*/
|
|
173
|
-
identifier(field: SchemaFieldConfig | string): string {
|
|
174
|
-
if (field === '*') {
|
|
175
|
-
return field;
|
|
176
|
-
} else {
|
|
177
|
-
const name = (typeof field === 'string') ? field : field.name;
|
|
178
|
-
return `${this.ID_AFFIX}${name}${this.ID_AFFIX}`;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
quote(text: string): string {
|
|
183
|
-
return `'${text.replace(/[']/g, "''")}'`;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Resolve date value
|
|
188
|
-
* @param value
|
|
189
|
-
* @returns
|
|
190
|
-
*/
|
|
191
|
-
resolveDateValue(value: Date): string {
|
|
192
|
-
const [day, time] = value.toISOString().split(/[TZ]/);
|
|
193
|
-
return this.quote(`${day} ${time}`);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Convert value to SQL valid representation
|
|
198
|
-
*/
|
|
199
|
-
resolveValue(config: SchemaFieldConfig, value: unknown): string {
|
|
200
|
-
if (value === undefined || value === null) {
|
|
201
|
-
return 'NULL';
|
|
202
|
-
} else if (config.type === String) {
|
|
203
|
-
if (value instanceof RegExp) {
|
|
204
|
-
const regexSource = DataUtil.toRegex(value).source.replace(/\\b/g, this.regexWordBoundary);
|
|
205
|
-
return this.quote(regexSource);
|
|
206
|
-
} else {
|
|
207
|
-
return this.quote(castTo(value));
|
|
208
|
-
}
|
|
209
|
-
} else if (config.type === Boolean) {
|
|
210
|
-
return `${value ? 'TRUE' : 'FALSE'}`;
|
|
211
|
-
} else if (config.type === castTo(BigInt)) {
|
|
212
|
-
return value.toString();
|
|
213
|
-
} else if (config.type === Number) {
|
|
214
|
-
return `${value}`;
|
|
215
|
-
} else if (config.type === Date) {
|
|
216
|
-
if (typeof value === 'string' && TimeUtil.isTimeSpan(value)) {
|
|
217
|
-
return this.resolveDateValue(TimeUtil.fromNow(value));
|
|
218
|
-
} else {
|
|
219
|
-
return this.resolveDateValue(DataUtil.coerceType(value, Date, true));
|
|
220
|
-
}
|
|
221
|
-
} else if (config.type === PointConcrete && Array.isArray(value)) {
|
|
222
|
-
return `point(${value[0]},${value[1]})`;
|
|
223
|
-
} else if (config.type === Object) {
|
|
224
|
-
return this.quote(JSONUtil.toUTF8(value).replaceAll("'", "''"));
|
|
225
|
-
}
|
|
226
|
-
throw new RuntimeError(`Unknown value type for field ${config.name}, ${value}`, { category: 'data' });
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* Get column type from field config
|
|
231
|
-
*/
|
|
232
|
-
getColumnType(config: SchemaFieldConfig): string {
|
|
233
|
-
let type: string = '';
|
|
234
|
-
|
|
235
|
-
if (config.type === castTo(BigInt)) {
|
|
236
|
-
type = this.COLUMN_TYPES.BIGINT;
|
|
237
|
-
} else if (config.type === Number) {
|
|
238
|
-
type = this.COLUMN_TYPES.INT;
|
|
239
|
-
if (config.precision) {
|
|
240
|
-
const [digits, decimals] = config.precision;
|
|
241
|
-
if (decimals) {
|
|
242
|
-
type = this.PARAMETERIZED_COLUMN_TYPES.DECIMAL(digits, decimals);
|
|
243
|
-
} else if (digits) {
|
|
244
|
-
if (digits < 3) {
|
|
245
|
-
type = this.COLUMN_TYPES.TINYINT;
|
|
246
|
-
} else if (digits < 5) {
|
|
247
|
-
type = this.COLUMN_TYPES.SMALLINT;
|
|
248
|
-
} else if (digits < 7) {
|
|
249
|
-
type = this.COLUMN_TYPES.MEDIUMINT;
|
|
250
|
-
} else if (digits < 10) {
|
|
251
|
-
type = this.COLUMN_TYPES.INT;
|
|
252
|
-
} else {
|
|
253
|
-
type = this.COLUMN_TYPES.BIGINT;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
} else {
|
|
257
|
-
type = this.COLUMN_TYPES.INT;
|
|
258
|
-
}
|
|
259
|
-
} else if (config.type === Date) {
|
|
260
|
-
type = this.COLUMN_TYPES.TIMESTAMP;
|
|
261
|
-
} else if (config.type === Boolean) {
|
|
262
|
-
type = this.COLUMN_TYPES.BOOLEAN;
|
|
263
|
-
} else if (config.type === String) {
|
|
264
|
-
if (config.specifiers?.includes('text')) {
|
|
265
|
-
type = this.COLUMN_TYPES.TEXT;
|
|
266
|
-
} else {
|
|
267
|
-
type = this.PARAMETERIZED_COLUMN_TYPES.VARCHAR(config.maxlength?.limit ?? this.DEFAULT_STRING_LENGTH);
|
|
268
|
-
}
|
|
269
|
-
} else if (config.type === PointConcrete) {
|
|
270
|
-
type = this.COLUMN_TYPES.POINT;
|
|
271
|
-
} else if (config.type === Object) {
|
|
272
|
-
type = this.COLUMN_TYPES.JSON;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
return type;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* FieldConfig to Column definition
|
|
280
|
-
*/
|
|
281
|
-
getColumnDefinition(config: SchemaFieldConfig, overrideRequired?: boolean): string | undefined {
|
|
282
|
-
const type = this.getColumnType(config);
|
|
283
|
-
if (!type) {
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
const required = overrideRequired ? true : (config.required?.active ?? false);
|
|
287
|
-
return `${this.identifier(config)} ${type} ${required ? 'NOT NULL' : ''}`;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* Delete query and return count removed
|
|
292
|
-
*/
|
|
293
|
-
async deleteAndGetCount<T extends ModelType>(cls: Class<T>, query: Query<T>): Promise<number> {
|
|
294
|
-
const { count } = await this.executeSQL<T>(this.getDeleteSQL(SQLModelUtil.classToStack(cls), query.where));
|
|
295
|
-
return DataUtil.coerceType(count, Number);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
* Get the count for a given query
|
|
300
|
-
*/
|
|
301
|
-
async getCountForQuery<T extends ModelType>(cls: Class<T>, query: Query<T>): Promise<number> {
|
|
302
|
-
const { records } = await this.executeSQL<{ total: number }>(
|
|
303
|
-
this.getQueryCountSQL(cls,
|
|
304
|
-
ModelQueryUtil.getWhereClause(cls, query.where)
|
|
305
|
-
)
|
|
306
|
-
);
|
|
307
|
-
return DataUtil.coerceType(records[0].total, Number);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
* Remove a sql column
|
|
312
|
-
*/
|
|
313
|
-
getDropColumnSQL(stack: VisitStack[]): string {
|
|
314
|
-
const field = stack.at(-1)!;
|
|
315
|
-
return `ALTER TABLE ${this.parentTable(stack)} DROP COLUMN ${this.identifier(field.name)};`;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
/**
|
|
319
|
-
* Add a sql column
|
|
320
|
-
*/
|
|
321
|
-
getAddColumnSQL(stack: VisitStack[]): string {
|
|
322
|
-
const field: SchemaFieldConfig = castTo(stack.at(-1));
|
|
323
|
-
return `ALTER TABLE ${this.parentTable(stack)} ADD COLUMN ${this.getColumnDefinition(field)};`;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
/**
|
|
327
|
-
* Modify a sql column
|
|
328
|
-
*/
|
|
329
|
-
abstract getModifyColumnSQL(stack: VisitStack[]): string;
|
|
330
|
-
|
|
331
|
-
/**
|
|
332
|
-
* Determine table/field namespace for a given stack location
|
|
333
|
-
*/
|
|
334
|
-
namespace(stack: VisitStack[]): string {
|
|
335
|
-
return `${this.namespacePrefix}${SQLModelUtil.buildTable(stack)}`;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
/**
|
|
339
|
-
* Determine namespace for a given stack location - 1
|
|
340
|
-
*/
|
|
341
|
-
namespaceParent(stack: VisitStack[]): string {
|
|
342
|
-
return this.namespace(stack.slice(0, stack.length - 1));
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
/**
|
|
346
|
-
* Determine table name for a given stack location
|
|
347
|
-
*/
|
|
348
|
-
table(stack: VisitStack[]): string {
|
|
349
|
-
return this.identifier(this.namespace(stack));
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
/**
|
|
353
|
-
* Determine parent table name for a given stack location
|
|
354
|
-
*/
|
|
355
|
-
parentTable(stack: VisitStack[]): string {
|
|
356
|
-
return this.table(stack.slice(0, stack.length - 1));
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
/**
|
|
360
|
-
* Get lookup key for cls and name
|
|
361
|
-
*/
|
|
362
|
-
getKey(cls: Class, name: string): string {
|
|
363
|
-
return `${cls.name}:${name}`;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
/**
|
|
367
|
-
* Alias a field for usage
|
|
368
|
-
*/
|
|
369
|
-
alias(field: string | SchemaFieldConfig, alias: string = this.rootAlias): string {
|
|
370
|
-
return `${alias}.${this.identifier(field)}`;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
/**
|
|
374
|
-
* Get alias cache for the stack
|
|
375
|
-
*/
|
|
376
|
-
getAliasCache(stack: VisitStack[], resolve: (path: VisitStack[]) => string): Map<string, Alias> {
|
|
377
|
-
const cls = stack[0].type;
|
|
378
|
-
|
|
379
|
-
if (this.aliasCache.has(cls)) {
|
|
380
|
-
return this.aliasCache.get(cls)!;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
const clauses = new Map<string, Alias>();
|
|
384
|
-
let idx = 0;
|
|
385
|
-
|
|
386
|
-
SQLModelUtil.visitSchemaSync(SchemaRegistryIndex.getConfig(cls), {
|
|
387
|
-
onRoot: ({ descend, path }) => {
|
|
388
|
-
const table = resolve(path);
|
|
389
|
-
clauses.set(table, { alias: this.rootAlias, path });
|
|
390
|
-
return descend();
|
|
391
|
-
},
|
|
392
|
-
onSub: ({ descend, config, path }) => {
|
|
393
|
-
const table = resolve(path);
|
|
394
|
-
clauses.set(table, { alias: `${config.name.charAt(0)}${idx++}`, path });
|
|
395
|
-
return descend();
|
|
396
|
-
},
|
|
397
|
-
onSimple: ({ config, path }) => {
|
|
398
|
-
const table = resolve(path);
|
|
399
|
-
clauses.set(table, { alias: `${config.name.charAt(0)}${idx++}`, path });
|
|
400
|
-
}
|
|
401
|
-
});
|
|
402
|
-
|
|
403
|
-
this.aliasCache.set(cls, clauses);
|
|
404
|
-
|
|
405
|
-
return clauses;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
/**
|
|
409
|
-
* Resolve field name for given location in stack
|
|
410
|
-
*/
|
|
411
|
-
resolveName(stack: VisitStack[]): string {
|
|
412
|
-
const path = this.namespaceParent(stack);
|
|
413
|
-
const name = stack.at(-1)!.name;
|
|
414
|
-
const cache = this.getAliasCache(stack, this.namespace);
|
|
415
|
-
const base = cache.get(path)!;
|
|
416
|
-
return this.alias(name, base.alias);
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
/**
|
|
420
|
-
* Generate WHERE field clause
|
|
421
|
-
*/
|
|
422
|
-
getWhereFieldSQL(stack: VisitStack[], input: Record<string, unknown>): string {
|
|
423
|
-
const items = [];
|
|
424
|
-
const { foreignMap, localMap } = SQLModelUtil.getFieldsByLocation(stack);
|
|
425
|
-
const SQL_OPS = this.SQL_OPS;
|
|
426
|
-
|
|
427
|
-
for (const key of Object.keys(input)) {
|
|
428
|
-
const top = input[key];
|
|
429
|
-
const field = localMap[key] ?? foreignMap[key];
|
|
430
|
-
if (!field) {
|
|
431
|
-
throw new Error(`Unknown field: ${key}`);
|
|
432
|
-
}
|
|
433
|
-
const sStack = [...stack, field];
|
|
434
|
-
if (key in foreignMap && field.array && !SchemaRegistryIndex.has(field.type)) {
|
|
435
|
-
// If dealing with simple external
|
|
436
|
-
sStack.push({
|
|
437
|
-
name: field.name,
|
|
438
|
-
class: null!,
|
|
439
|
-
type: field.type
|
|
440
|
-
});
|
|
441
|
-
}
|
|
442
|
-
const sPath = this.resolveName(sStack);
|
|
443
|
-
|
|
444
|
-
if (DataUtil.isPlainObject(top)) {
|
|
445
|
-
const subKey = Object.keys(top)[0];
|
|
446
|
-
if (!subKey.startsWith('$')) {
|
|
447
|
-
const inner = this.getWhereFieldSQL(sStack, top);
|
|
448
|
-
items.push(inner);
|
|
449
|
-
} else {
|
|
450
|
-
const value = top[subKey];
|
|
451
|
-
const resolve = this.resolveValue.bind(this, field);
|
|
452
|
-
|
|
453
|
-
switch (subKey) {
|
|
454
|
-
case '$nin': case '$in': {
|
|
455
|
-
const arr = (Array.isArray(value) ? value : [value]).map(resolve);
|
|
456
|
-
items.push(`${sPath} ${SQL_OPS[subKey]} (${arr.join(',')})`);
|
|
457
|
-
break;
|
|
458
|
-
}
|
|
459
|
-
case '$all': {
|
|
460
|
-
const set = new Set();
|
|
461
|
-
const arr = [value].flat().filter(item => !set.has(item) && !!set.add(item)).map(resolve);
|
|
462
|
-
const valueTable = this.parentTable(sStack);
|
|
463
|
-
const alias = `_all_${sStack.length}`;
|
|
464
|
-
const pPath = this.identifier(this.parentPathField.name);
|
|
465
|
-
const rpPath = this.resolveName([...sStack, field, this.parentPathField]);
|
|
466
|
-
|
|
467
|
-
items.push(`${arr.length} = (
|
|
468
|
-
SELECT COUNT(DISTINCT ${alias}.${this.identifier(field.name)})
|
|
469
|
-
FROM ${valueTable} ${alias}
|
|
470
|
-
WHERE ${alias}.${pPath} = ${rpPath}
|
|
471
|
-
AND ${alias}.${this.identifier(field.name)} IN (${arr.join(',')})
|
|
472
|
-
)`);
|
|
473
|
-
break;
|
|
474
|
-
}
|
|
475
|
-
case '$regex': {
|
|
476
|
-
const regex = DataUtil.toRegex(castTo(value));
|
|
477
|
-
const regexSource = regex.source;
|
|
478
|
-
const ins = regex.flags && regex.flags.includes('i');
|
|
479
|
-
|
|
480
|
-
if (/^[\^]\S+[.][*][$]?$/.test(regexSource)) {
|
|
481
|
-
const inner = regexSource.substring(1, regexSource.length - 2);
|
|
482
|
-
if (!ins || SQL_OPS.$ilike) {
|
|
483
|
-
items.push(`${sPath} ${ins ? SQL_OPS.$ilike : SQL_OPS.$like} ${resolve(`${inner}%`)}`);
|
|
484
|
-
} else {
|
|
485
|
-
items.push(`LOWER(${sPath}) ${SQL_OPS.$like} LOWER(${resolve(`${inner}%`)})`);
|
|
486
|
-
}
|
|
487
|
-
} else {
|
|
488
|
-
if (!ins || SQL_OPS.$iregex) {
|
|
489
|
-
const result = resolve(value);
|
|
490
|
-
items.push(`${sPath} ${SQL_OPS[!ins ? subKey : '$iregex']} ${result}`);
|
|
491
|
-
} else {
|
|
492
|
-
const result = resolve(new RegExp(regexSource.toLowerCase(), regex.flags));
|
|
493
|
-
items.push(`LOWER(${sPath}) ${SQL_OPS[subKey]} ${result}`);
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
break;
|
|
497
|
-
}
|
|
498
|
-
case '$exists': {
|
|
499
|
-
if (field.array) {
|
|
500
|
-
const valueTable = this.parentTable(sStack);
|
|
501
|
-
const alias = `_all_${sStack.length}`;
|
|
502
|
-
const pPath = this.identifier(this.parentPathField.name);
|
|
503
|
-
const rpPath = this.resolveName([...sStack, field, this.parentPathField]);
|
|
504
|
-
|
|
505
|
-
items.push(`0 ${!value ? '=' : '<>'} (
|
|
506
|
-
SELECT COUNT(${alias}.${this.identifier(field.name)})
|
|
507
|
-
FROM ${valueTable} ${alias}
|
|
508
|
-
WHERE ${alias}.${pPath} = ${rpPath}
|
|
509
|
-
)`);
|
|
510
|
-
} else {
|
|
511
|
-
items.push(`${sPath} ${value ? SQL_OPS.$isNot : SQL_OPS.$is} NULL`);
|
|
512
|
-
}
|
|
513
|
-
break;
|
|
514
|
-
}
|
|
515
|
-
case '$ne': case '$eq': {
|
|
516
|
-
if (value === null || value === undefined) {
|
|
517
|
-
items.push(`${sPath} ${subKey === '$ne' ? SQL_OPS.$isNot : SQL_OPS.$is} NULL`);
|
|
518
|
-
} else {
|
|
519
|
-
const base = `${sPath} ${SQL_OPS[subKey]} ${resolve(value)}`;
|
|
520
|
-
items.push(subKey === '$ne' ? `(${base} OR ${sPath} ${SQL_OPS.$is} NULL)` : base);
|
|
521
|
-
}
|
|
522
|
-
break;
|
|
523
|
-
}
|
|
524
|
-
case '$lt': case '$gt': case '$gte': case '$lte': {
|
|
525
|
-
const subItems = TypedObject.keys(castTo<typeof SQL_OPS>(top))
|
|
526
|
-
.map(subSubKey => `${sPath} ${SQL_OPS[subSubKey]} ${resolve(top[subSubKey])}`);
|
|
527
|
-
items.push(subItems.length > 1 ? `(${subItems.join(` ${SQL_OPS.$and} `)})` : subItems[0]);
|
|
528
|
-
break;
|
|
529
|
-
}
|
|
530
|
-
case '$near':
|
|
531
|
-
case '$unit':
|
|
532
|
-
case '$maxDistance':
|
|
533
|
-
case '$geoWithin':
|
|
534
|
-
throw new Error('Geo-spatial queries are not currently supported in SQL');
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
// Handle operations
|
|
538
|
-
} else {
|
|
539
|
-
items.push(`${sPath} ${SQL_OPS.$eq} ${this.resolveValue(field, top)}`);
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
if (items.length === 0) {
|
|
543
|
-
return 'TRUE';
|
|
544
|
-
} else if (items.length === 1) {
|
|
545
|
-
return items[0];
|
|
546
|
-
} else {
|
|
547
|
-
return `(${items.join(` ${SQL_OPS.$and} `)})`;
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
/**
|
|
552
|
-
* Grouping of where clauses
|
|
553
|
-
*/
|
|
554
|
-
getWhereGroupingSQL<T>(cls: Class<T>, clause: WhereClause<T>): string {
|
|
555
|
-
const SQL_OPS = this.SQL_OPS;
|
|
556
|
-
|
|
557
|
-
if (ModelQueryUtil.has$And(clause)) {
|
|
558
|
-
return `(${clause.$and.map(item => this.getWhereGroupingSQL<T>(cls, item)).join(` ${SQL_OPS.$and} `)})`;
|
|
559
|
-
} else if (ModelQueryUtil.has$Or(clause)) {
|
|
560
|
-
return `(${clause.$or.map(item => this.getWhereGroupingSQL<T>(cls, item)).join(` ${SQL_OPS.$or} `)})`;
|
|
561
|
-
} else if (ModelQueryUtil.has$Not(clause)) {
|
|
562
|
-
return `${SQL_OPS.$not} (${this.getWhereGroupingSQL<T>(cls, clause.$not)})`;
|
|
563
|
-
} else {
|
|
564
|
-
return this.getWhereFieldSQL(SQLModelUtil.classToStack(cls), clause);
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
/**
|
|
569
|
-
* Generate WHERE clause
|
|
570
|
-
*/
|
|
571
|
-
getWhereSQL<T>(cls: Class<T>, where?: WhereClause<T>): string {
|
|
572
|
-
return !where || !Object.keys(where).length ?
|
|
573
|
-
'' :
|
|
574
|
-
`WHERE ${this.getWhereGroupingSQL(cls, castTo(where))}`;
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
/**
|
|
578
|
-
* Generate ORDER BY clause
|
|
579
|
-
*/
|
|
580
|
-
getOrderBySQL<T>(cls: Class<T>, sortBy?: SortClause<T>[]): string {
|
|
581
|
-
return !sortBy ?
|
|
582
|
-
'' :
|
|
583
|
-
`ORDER BY ${SQLModelUtil.orderBy(cls, sortBy).map((item) =>
|
|
584
|
-
`${this.resolveName(item.stack)} ${item.asc ? 'ASC' : 'DESC'}`
|
|
585
|
-
).join(', ')}`;
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
/**
|
|
589
|
-
* Generate SELECT clause
|
|
590
|
-
*/
|
|
591
|
-
getSelectSQL<T>(cls: Class<T>, select?: SelectClause<T>): string {
|
|
592
|
-
const stack = SQLModelUtil.classToStack(cls);
|
|
593
|
-
const columns = select && SQLModelUtil.select(cls, select).map((sel) => this.resolveName([...stack, sel]));
|
|
594
|
-
if (columns) {
|
|
595
|
-
columns.unshift(this.alias(this.pathField));
|
|
596
|
-
}
|
|
597
|
-
return !columns ?
|
|
598
|
-
`SELECT ${this.rootAlias}.* ` :
|
|
599
|
-
`SELECT ${columns.join(', ')}`;
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
/**
|
|
603
|
-
* Generate FROM clause
|
|
604
|
-
*/
|
|
605
|
-
getFromSQL<T>(cls: Class<T>): string {
|
|
606
|
-
const stack = SQLModelUtil.classToStack(cls);
|
|
607
|
-
const aliases = this.getAliasCache(stack, this.namespace);
|
|
608
|
-
const tables = [...aliases.keys()].toSorted((a, b) => a.length - b.length); // Shortest first
|
|
609
|
-
return `FROM ${tables.map((table) => {
|
|
610
|
-
const { alias, path } = aliases.get(table)!;
|
|
611
|
-
let from = `${this.identifier(table)} ${alias}`;
|
|
612
|
-
if (path.length > 1) {
|
|
613
|
-
const key = this.namespaceParent(path);
|
|
614
|
-
const { alias: parentAlias } = aliases.get(key)!;
|
|
615
|
-
from = `
|
|
616
|
-
LEFT OUTER JOIN ${from} ON
|
|
617
|
-
${this.alias(this.parentPathField, alias)} = ${this.alias(this.pathField, parentAlias)}
|
|
618
|
-
`;
|
|
619
|
-
}
|
|
620
|
-
return from;
|
|
621
|
-
}).join('\n')}`;
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
/**
|
|
625
|
-
* Generate LIMIT clause
|
|
626
|
-
*/
|
|
627
|
-
getLimitSQL<T>(cls: Class<T>, query?: Query<T>): string {
|
|
628
|
-
return !query || (!query.limit && !query.offset) ?
|
|
629
|
-
'' :
|
|
630
|
-
`LIMIT ${query.limit ?? 200} OFFSET ${query.offset ?? 0}`;
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
/**
|
|
634
|
-
* Generate GROUP BY clause
|
|
635
|
-
*/
|
|
636
|
-
getGroupBySQL<T>(cls: Class<T>, query: Query<T>): string {
|
|
637
|
-
const sortFields = !query.sort ?
|
|
638
|
-
'' :
|
|
639
|
-
SQLModelUtil.orderBy(cls, query.sort)
|
|
640
|
-
.map(item => this.resolveName(item.stack))
|
|
641
|
-
.join(', ');
|
|
642
|
-
|
|
643
|
-
return `GROUP BY ${this.alias(this.idField)}${sortFields ? `, ${sortFields}` : ''}`;
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
/**
|
|
647
|
-
* Generate full query
|
|
648
|
-
*/
|
|
649
|
-
getQuerySQL<T>(cls: Class<T>, query: Query<T>, where?: WhereClause<T>): string {
|
|
650
|
-
return `
|
|
651
|
-
${this.getSelectSQL(cls, query.select)}
|
|
652
|
-
${this.getFromSQL(cls)}
|
|
653
|
-
${this.getWhereSQL(cls, where)}
|
|
654
|
-
${this.getGroupBySQL(cls, query)}
|
|
655
|
-
${this.getOrderBySQL(cls, query.sort)}
|
|
656
|
-
${this.getLimitSQL(cls, query)}`;
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
getCreateTableSQL(stack: VisitStack[]): string {
|
|
660
|
-
const config = stack.at(-1)!;
|
|
661
|
-
const parent = stack.length > 1;
|
|
662
|
-
const array = parent && config.array;
|
|
663
|
-
const fields = SchemaRegistryIndex.has(config.type) ?
|
|
664
|
-
[...SQLModelUtil.getFieldsByLocation(stack).local] :
|
|
665
|
-
(array ? [castTo<SchemaFieldConfig>(config)] : []);
|
|
666
|
-
|
|
667
|
-
if (!parent) {
|
|
668
|
-
const idField = fields.find(field => field.name === this.idField.name);
|
|
669
|
-
if (!idField) {
|
|
670
|
-
fields.push(this.idField);
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
const fieldSql = fields
|
|
675
|
-
.map(field => this.getColumnDefinition(field, field.name === this.idField.name && !parent) || '')
|
|
676
|
-
.filter(line => !!line.trim())
|
|
677
|
-
.join(',\n ');
|
|
678
|
-
|
|
679
|
-
const out = `
|
|
680
|
-
CREATE TABLE IF NOT EXISTS ${this.table(stack)} (
|
|
681
|
-
${fieldSql}${fieldSql.length ? ',' : ''}
|
|
682
|
-
${this.getColumnDefinition(this.pathField)} UNIQUE,
|
|
683
|
-
${!parent ?
|
|
684
|
-
`PRIMARY KEY (${this.identifier(this.idField)})` :
|
|
685
|
-
`${this.getColumnDefinition(this.parentPathField)},
|
|
686
|
-
${array ? `${this.getColumnDefinition(this.idxField)},` : ''}
|
|
687
|
-
PRIMARY KEY (${this.identifier(this.pathField)}),
|
|
688
|
-
FOREIGN KEY (${this.identifier(this.parentPathField)}) REFERENCES ${this.parentTable(stack)}(${this.identifier(this.pathField)}) ON DELETE CASCADE`}
|
|
689
|
-
);`;
|
|
690
|
-
return out;
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
/**
|
|
694
|
-
* Generate drop SQL
|
|
695
|
-
*/
|
|
696
|
-
getDropTableSQL(stack: VisitStack[]): string {
|
|
697
|
-
return `DROP TABLE IF EXISTS ${this.table(stack)}; `;
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
/**
|
|
701
|
-
* Generate truncate SQL
|
|
702
|
-
*/
|
|
703
|
-
getTruncateTableSQL(stack: VisitStack[]): string {
|
|
704
|
-
return `TRUNCATE ${this.table(stack)}; `;
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
/**
|
|
708
|
-
* Get all table create queries for a class
|
|
709
|
-
*/
|
|
710
|
-
getCreateAllTablesSQL(cls: Class): string[] {
|
|
711
|
-
const out: string[] = [];
|
|
712
|
-
SQLModelUtil.visitSchemaSync(SchemaRegistryIndex.getConfig(cls), {
|
|
713
|
-
onRoot: ({ path, descend }) => { out.push(this.getCreateTableSQL(path)); descend(); },
|
|
714
|
-
onSub: ({ path, descend }) => { out.push(this.getCreateTableSQL(path)); descend(); },
|
|
715
|
-
onSimple: ({ path }) => out.push(this.getCreateTableSQL(path))
|
|
716
|
-
});
|
|
717
|
-
return out;
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
/**
|
|
721
|
-
* Get all create indices need for a given class
|
|
722
|
-
*/
|
|
723
|
-
getCreateAllIndicesSQL<T extends ModelType>(cls: Class<T>, indices: IndexConfig[]): string[] {
|
|
724
|
-
return indices.map(idx => this.getCreateIndexSQL(cls, idx)).filter((sql): sql is string => !!sql);
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
/**
|
|
728
|
-
* Get index name
|
|
729
|
-
*/
|
|
730
|
-
getIndexName<T extends ModelType>(cls: Class<T>, idx: IndexConfig): string {
|
|
731
|
-
const table = this.namespace(SQLModelUtil.classToStack(cls));
|
|
732
|
-
return ['idx', table, idx.name.toLowerCase().replaceAll('-', '_')].join('_');
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
/**
|
|
736
|
-
* Get CREATE INDEX sql
|
|
737
|
-
*/
|
|
738
|
-
getCreateIndexSQL<T extends ModelType>(cls: Class<T>, idx: IndexConfig): string | undefined {
|
|
739
|
-
const constraint = this.getIndexName(cls, idx);
|
|
740
|
-
const table = this.namespace(SQLModelUtil.classToStack(cls));
|
|
741
|
-
|
|
742
|
-
if (isModelQueryIndex(idx)) {
|
|
743
|
-
const fields: [string, boolean][] = idx.fields.map(field => {
|
|
744
|
-
const key = TypedObject.keys(field)[0];
|
|
745
|
-
const value = field[key];
|
|
746
|
-
if (DataUtil.isPlainObject(value)) {
|
|
747
|
-
throw new IndexNotSupported(cls, idx, 'Only indexed and query indices are supported in SQL');
|
|
748
|
-
}
|
|
749
|
-
return [castTo(key), typeof value === 'number' ? value === 1 : (!!value)];
|
|
750
|
-
});
|
|
751
|
-
return `CREATE ${idx.unique ? 'UNIQUE ' : ''}INDEX ${constraint} ON ${this.identifier(table)} (${fields
|
|
752
|
-
.map(([name, sel]) => `${this.identifier(name)} ${sel ? 'ASC' : 'DESC'}`)
|
|
753
|
-
.join(', ')});`;
|
|
754
|
-
} else if (isModelIndexedIndex(idx)) {
|
|
755
|
-
const all = [...idx.keyTemplate, ...idx.sortTemplate];
|
|
756
|
-
if (all.find(field => field.path.length > 1)) {
|
|
757
|
-
console.debug('Nested fields are not supported in ModelIndexed indices SQL', { index: idx.name });
|
|
758
|
-
return;
|
|
759
|
-
}
|
|
760
|
-
const fields = all
|
|
761
|
-
.map(({ path, value }) => `${this.identifier(path.join('_'))} ${value === -1 ? 'DESC' : 'ASC'}`)
|
|
762
|
-
.join(', ');
|
|
763
|
-
switch (idx.type) {
|
|
764
|
-
case 'indexed:keyed': return `CREATE ${idx.unique ? 'UNIQUE ' : ''}INDEX ${constraint} ON ${this.identifier(table)} (${fields});`;
|
|
765
|
-
case 'indexed:sorted': return `CREATE INDEX ${constraint} ON ${this.identifier(table)} (${fields});`;
|
|
766
|
-
}
|
|
767
|
-
} else {
|
|
768
|
-
throw new IndexNotSupported(cls, idx, 'Only indexed and query indices are supported in SQL');
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
/**
|
|
773
|
-
* Get DROP INDEX sql
|
|
774
|
-
*/
|
|
775
|
-
getDropIndexSQL<T extends ModelType>(cls: Class<T>, idx: IndexConfig | string): string {
|
|
776
|
-
const constraint = typeof idx === 'string' ? idx : this.getIndexName(cls, idx);
|
|
777
|
-
return `DROP INDEX ${this.identifier(constraint)} ;`;
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
/**
|
|
781
|
-
* Drop all tables for a given class
|
|
782
|
-
*/
|
|
783
|
-
getDropAllTablesSQL<T extends ModelType>(cls: Class<T>): string[] {
|
|
784
|
-
const out: string[] = [];
|
|
785
|
-
SQLModelUtil.visitSchemaSync(SchemaRegistryIndex.getConfig(cls), {
|
|
786
|
-
onRoot: ({ path, descend }) => { descend(); out.push(this.getDropTableSQL(path)); },
|
|
787
|
-
onSub: ({ path, descend }) => { descend(); out.push(this.getDropTableSQL(path)); },
|
|
788
|
-
onSimple: ({ path }) => out.push(this.getDropTableSQL(path))
|
|
789
|
-
});
|
|
790
|
-
return out;
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
/**
|
|
794
|
-
* Truncate all tables for a given class
|
|
795
|
-
*/
|
|
796
|
-
getTruncateAllTablesSQL<T extends ModelType>(cls: Class<T>): string[] {
|
|
797
|
-
const out: string[] = [];
|
|
798
|
-
SQLModelUtil.visitSchemaSync(SchemaRegistryIndex.getConfig(cls), {
|
|
799
|
-
onRoot: ({ path, descend }) => { descend(); out.push(this.getTruncateTableSQL(path)); },
|
|
800
|
-
onSub: ({ path, descend }) => { descend(); out.push(this.getTruncateTableSQL(path)); },
|
|
801
|
-
onSimple: ({ path }) => out.push(this.getTruncateTableSQL(path))
|
|
802
|
-
});
|
|
803
|
-
return out;
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
/**
|
|
807
|
-
* Get INSERT sql for a given instance and a specific stack location
|
|
808
|
-
*/
|
|
809
|
-
getInsertSQL(stack: VisitStack[], instances: InsertWrapper['records']): string | undefined {
|
|
810
|
-
const config = stack.at(-1)!;
|
|
811
|
-
const columns = SQLModelUtil.getFieldsByLocation(stack).local
|
|
812
|
-
.filter(field => !SchemaRegistryIndex.has(field.type))
|
|
813
|
-
.toSorted((a, b) => a.name.localeCompare(b.name));
|
|
814
|
-
const columnNames = columns.map(column => column.name);
|
|
815
|
-
|
|
816
|
-
const hasParent = stack.length > 1;
|
|
817
|
-
const isArray = !!config.array;
|
|
818
|
-
|
|
819
|
-
if (isArray) {
|
|
820
|
-
const newInstances: typeof instances = [];
|
|
821
|
-
for (const instance of instances) {
|
|
822
|
-
if (instance.value === null || instance.value === undefined) {
|
|
823
|
-
continue;
|
|
824
|
-
} else if (Array.isArray(instance.value)) {
|
|
825
|
-
const name = instance.stack.at(-1)!.name;
|
|
826
|
-
for (const sel of instance.value) {
|
|
827
|
-
newInstances.push({
|
|
828
|
-
stack: instance.stack,
|
|
829
|
-
value: {
|
|
830
|
-
[name]: sel
|
|
831
|
-
}
|
|
832
|
-
});
|
|
833
|
-
}
|
|
834
|
-
} else {
|
|
835
|
-
newInstances.push(instance);
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
instances = newInstances;
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
if (!instances.length) {
|
|
842
|
-
return;
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
const matrix = instances.map(inst => columns.map(column =>
|
|
846
|
-
this.resolveValue(column, castTo<Record<string, unknown>>(inst.value)[column.name])));
|
|
847
|
-
|
|
848
|
-
columnNames.push(this.pathField.name);
|
|
849
|
-
if (hasParent) {
|
|
850
|
-
columnNames.push(this.parentPathField.name);
|
|
851
|
-
if (isArray) {
|
|
852
|
-
columnNames.push(this.idxField.name);
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
const idx = config.index ?? 0;
|
|
857
|
-
|
|
858
|
-
for (let i = 0; i < matrix.length; i++) {
|
|
859
|
-
const { stack: elStack } = instances[i];
|
|
860
|
-
if (hasParent) {
|
|
861
|
-
matrix[i].push(this.hash(`${SQLModelUtil.buildPath(elStack)}${isArray ? `[${i + idx}]` : ''}`));
|
|
862
|
-
matrix[i].push(this.hash(SQLModelUtil.buildPath(elStack.slice(0, elStack.length - 1))));
|
|
863
|
-
if (isArray) {
|
|
864
|
-
matrix[i].push(this.resolveValue(this.idxField, i + idx));
|
|
865
|
-
}
|
|
866
|
-
} else {
|
|
867
|
-
matrix[i].push(this.hash(SQLModelUtil.buildPath(elStack)));
|
|
868
|
-
}
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
return `
|
|
872
|
-
INSERT INTO ${this.table(stack)} (${columnNames.map(this.identifier).join(', ')})
|
|
873
|
-
VALUES
|
|
874
|
-
${matrix.map(row => `(${row.join(', ')})`).join(',\n')};`;
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
/**
|
|
878
|
-
* Get ALL Insert queries as needed
|
|
879
|
-
*/
|
|
880
|
-
getAllInsertSQL<T extends ModelType>(cls: Class<T>, instance: T): string[] {
|
|
881
|
-
const out: string[] = [];
|
|
882
|
-
const add = (text?: string): void => { text && out.push(text); };
|
|
883
|
-
SQLModelUtil.visitSchemaInstance(cls, instance, {
|
|
884
|
-
onRoot: ({ value, path }) => add(this.getInsertSQL(path, [{ stack: path, value }])),
|
|
885
|
-
onSub: ({ value, path }) => add(this.getInsertSQL(path, [{ stack: path, value }])),
|
|
886
|
-
onSimple: ({ value, path }) => add(this.getInsertSQL(path, [{ stack: path, value }]))
|
|
887
|
-
});
|
|
888
|
-
return out;
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
/**
|
|
892
|
-
* Simple data base updates
|
|
893
|
-
*/
|
|
894
|
-
getUpdateSQL(stack: VisitStack[], data: Record<string, unknown>, where?: WhereClause<unknown>): string {
|
|
895
|
-
const { type } = stack.at(-1)!;
|
|
896
|
-
const { localMap } = SQLModelUtil.getFieldsByLocation(stack);
|
|
897
|
-
return `
|
|
898
|
-
UPDATE ${this.table(stack)} ${this.rootAlias}
|
|
899
|
-
SET
|
|
900
|
-
${Object
|
|
901
|
-
.entries(data)
|
|
902
|
-
.filter(([key]) => key in localMap)
|
|
903
|
-
.map(([key, value]) => `${this.identifier(key)}=${this.resolveValue(localMap[key], value)}`).join(', ')}
|
|
904
|
-
${this.getWhereSQL(type, where)};`;
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
getDeleteSQL(stack: VisitStack[], where?: WhereClause<unknown>): string {
|
|
908
|
-
const { type } = stack.at(-1)!;
|
|
909
|
-
return `
|
|
910
|
-
DELETE
|
|
911
|
-
FROM ${this.table(stack)} ${this.rootAlias}
|
|
912
|
-
${this.getWhereSQL(type, where)};`;
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
/**
|
|
916
|
-
* Get elements by ids
|
|
917
|
-
*/
|
|
918
|
-
getSelectRowsByIdsSQL(stack: VisitStack[], ids: string[], select: SchemaFieldConfig[] = []): string {
|
|
919
|
-
const config = stack.at(-1)!;
|
|
920
|
-
const orderBy = !config.array ?
|
|
921
|
-
'' :
|
|
922
|
-
`ORDER BY ${this.rootAlias}.${this.idxField.name} ASC`;
|
|
923
|
-
|
|
924
|
-
const idField = (stack.length > 1 ? this.parentPathField : this.idField);
|
|
925
|
-
|
|
926
|
-
return `
|
|
927
|
-
SELECT ${select.length ? select.map(field => this.alias(field)).join(',') : '*'}
|
|
928
|
-
FROM ${this.table(stack)} ${this.rootAlias}
|
|
929
|
-
WHERE ${this.alias(idField)} IN (${ids.map(id => this.resolveValue(idField, id)).join(', ')})
|
|
930
|
-
${orderBy};`;
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
/**
|
|
934
|
-
* Get COUNT(1) query
|
|
935
|
-
*/
|
|
936
|
-
getQueryCountSQL<T>(cls: Class<T>, where?: WhereClause<T>): string {
|
|
937
|
-
return `
|
|
938
|
-
SELECT COUNT(DISTINCT ${this.rootAlias}.id) as total
|
|
939
|
-
${this.getFromSQL(cls)}
|
|
940
|
-
${this.getWhereSQL(cls, where!)}`;
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
async fetchDependents<T>(cls: Class<T>, items: T[], select?: SelectClause<T>): Promise<T[]> {
|
|
944
|
-
const stack: Record<string, unknown>[] = [];
|
|
945
|
-
const selectStack: (SelectClause<T> | undefined)[] = [];
|
|
946
|
-
|
|
947
|
-
const buildSet = (children: unknown[], field?: SchemaFieldConfig): Record<string, unknown> =>
|
|
948
|
-
SQLModelUtil.collectDependents(this, stack.at(-1)!, children, field);
|
|
949
|
-
|
|
950
|
-
await SQLModelUtil.visitSchema(SchemaRegistryIndex.getConfig(cls), {
|
|
951
|
-
onRoot: async (config) => {
|
|
952
|
-
const fieldSet = buildSet(items); // Already filtered by initial select query
|
|
953
|
-
selectStack.push(select);
|
|
954
|
-
stack.push(fieldSet);
|
|
955
|
-
await config.descend();
|
|
956
|
-
},
|
|
957
|
-
onSub: async ({ config, descend, fields, path }) => {
|
|
958
|
-
const top = stack.at(-1)!;
|
|
959
|
-
const ids = Object.keys(top);
|
|
960
|
-
const selectTop = selectStack.at(-1)!;
|
|
961
|
-
const fieldKey = castKey<RetainQueryPrimitiveFields<T>>(config.name);
|
|
962
|
-
const subSelectTop: SelectClause<T> | undefined = castTo(selectTop?.[fieldKey]);
|
|
963
|
-
|
|
964
|
-
// See if a selection exists at all
|
|
965
|
-
const selected: SchemaFieldConfig[] = subSelectTop ? fields
|
|
966
|
-
.filter(field => typeof subSelectTop === 'object' && subSelectTop[castTo<typeof fieldKey>(field.name)] === 1)
|
|
967
|
-
: [];
|
|
968
|
-
|
|
969
|
-
if (selected.length) {
|
|
970
|
-
selected.push(this.pathField, this.parentPathField);
|
|
971
|
-
if (config.array) {
|
|
972
|
-
selected.push(this.idxField);
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
// If children and selection exists
|
|
977
|
-
if (ids.length && (!subSelectTop || selected)) {
|
|
978
|
-
const { records: children } = await this.executeSQL<unknown[]>(this.getSelectRowsByIdsSQL(
|
|
979
|
-
path,
|
|
980
|
-
ids,
|
|
981
|
-
selected
|
|
982
|
-
));
|
|
983
|
-
|
|
984
|
-
const fieldSet = buildSet(children, config);
|
|
985
|
-
try {
|
|
986
|
-
stack.push(fieldSet);
|
|
987
|
-
selectStack.push(subSelectTop);
|
|
988
|
-
await descend();
|
|
989
|
-
} finally {
|
|
990
|
-
selectStack.pop();
|
|
991
|
-
stack.pop();
|
|
992
|
-
}
|
|
993
|
-
}
|
|
994
|
-
},
|
|
995
|
-
onSimple: async ({ config, path }): Promise<void> => {
|
|
996
|
-
const top = stack.at(-1)!;
|
|
997
|
-
const ids = Object.keys(top);
|
|
998
|
-
if (ids.length) {
|
|
999
|
-
const { records: matching } = await this.executeSQL(this.getSelectRowsByIdsSQL(
|
|
1000
|
-
path,
|
|
1001
|
-
ids
|
|
1002
|
-
));
|
|
1003
|
-
buildSet(matching, config);
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
});
|
|
1007
|
-
|
|
1008
|
-
return items;
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
/**
|
|
1012
|
-
* Delete all ids
|
|
1013
|
-
*/
|
|
1014
|
-
async deleteByIds(stack: VisitStack[], ids: string[]): Promise<number> {
|
|
1015
|
-
return this.deleteAndGetCount<ModelType>(stack.at(-1)!.type, {
|
|
1016
|
-
where: {
|
|
1017
|
-
[stack.length === 1 ? this.idField.name : this.pathField.name]: {
|
|
1018
|
-
$in: ids
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1021
|
-
});
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
/**
|
|
1025
|
-
* Do bulk process
|
|
1026
|
-
*/
|
|
1027
|
-
async bulkProcess(deletes: DeleteWrapper[], inserts: InsertWrapper[], upserts: InsertWrapper[], updates: InsertWrapper[]): Promise<BulkResponse> {
|
|
1028
|
-
const out = {
|
|
1029
|
-
counts: {
|
|
1030
|
-
delete: deletes.reduce((count, item) => count + item.ids.length, 0),
|
|
1031
|
-
error: 0,
|
|
1032
|
-
insert: inserts.filter(item => item.stack.length === 1).reduce((count, item) => count + item.records.length, 0),
|
|
1033
|
-
update: updates.filter(item => item.stack.length === 1).reduce((count, item) => count + item.records.length, 0),
|
|
1034
|
-
upsert: upserts.filter(item => item.stack.length === 1).reduce((count, item) => count + item.records.length, 0)
|
|
1035
|
-
},
|
|
1036
|
-
errors: [],
|
|
1037
|
-
insertedIds: new Map()
|
|
1038
|
-
};
|
|
1039
|
-
|
|
1040
|
-
// Full removals
|
|
1041
|
-
await Promise.all(deletes.map(item => this.deleteByIds(item.stack, item.ids)));
|
|
1042
|
-
|
|
1043
|
-
// Adding deletes
|
|
1044
|
-
if (upserts.length || updates.length) {
|
|
1045
|
-
const idx = this.idField.name;
|
|
1046
|
-
|
|
1047
|
-
await Promise.all([
|
|
1048
|
-
...upserts
|
|
1049
|
-
.filter(item => item.stack.length === 1)
|
|
1050
|
-
.map(item =>
|
|
1051
|
-
this.deleteByIds(item.stack, item.records.map(value => castTo<Record<string, string>>(value.value)[idx]))
|
|
1052
|
-
),
|
|
1053
|
-
...updates
|
|
1054
|
-
.filter(item => item.stack.length === 1)
|
|
1055
|
-
.map(item =>
|
|
1056
|
-
this.deleteByIds(item.stack, item.records.map(value => castTo<Record<string, string>>(value.value)[idx]))
|
|
1057
|
-
),
|
|
1058
|
-
]);
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
// Adding
|
|
1062
|
-
for (const items of [inserts, upserts, updates]) {
|
|
1063
|
-
if (!items.length) {
|
|
1064
|
-
continue;
|
|
1065
|
-
}
|
|
1066
|
-
let level = 1; // Add by level
|
|
1067
|
-
for (; ;) { // Loop until done
|
|
1068
|
-
const leveled = items.filter(insertWrapper => insertWrapper.stack.length === level);
|
|
1069
|
-
if (!leveled.length) {
|
|
1070
|
-
break;
|
|
1071
|
-
}
|
|
1072
|
-
await Promise.all(leveled
|
|
1073
|
-
.map(inserted => this.getInsertSQL(inserted.stack, inserted.records))
|
|
1074
|
-
.filter(sql => !!sql)
|
|
1075
|
-
.map(sql => this.executeSQL(sql!)));
|
|
1076
|
-
level += 1;
|
|
1077
|
-
}
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
return out;
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
/**
|
|
1084
|
-
* Determine if a column has changed
|
|
1085
|
-
*/
|
|
1086
|
-
isColumnChanged(requested: SchemaFieldConfig, existing: SQLTableDescription['columns'][number],): boolean {
|
|
1087
|
-
const requestedColumnType = this.getColumnType(requested);
|
|
1088
|
-
const result =
|
|
1089
|
-
(requested.name !== this.idField.name && !!requested.required?.active !== !!existing.is_not_null)
|
|
1090
|
-
|| (requestedColumnType.toUpperCase() !== existing.type.toUpperCase());
|
|
1091
|
-
|
|
1092
|
-
return result;
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
/**
|
|
1096
|
-
* Determine if an index has changed
|
|
1097
|
-
*/
|
|
1098
|
-
isIndexChanged(requested: IndexConfig, existing: SQLTableDescription['indices'][number]): boolean {
|
|
1099
|
-
if (isModelQueryIndex(requested)) {
|
|
1100
|
-
const uniqueChanged = (existing.is_unique && !requested.unique);
|
|
1101
|
-
const columnSizeChanged = requested.fields.length !== existing.columns.length;
|
|
1102
|
-
let result = uniqueChanged || columnSizeChanged;
|
|
1103
|
-
for (let i = 0; i < requested.fields.length && !result; i++) {
|
|
1104
|
-
const [[key, value]] = Object.entries(requested.fields[i]);
|
|
1105
|
-
const desc = value === -1;
|
|
1106
|
-
result ||= key !== existing.columns[i].name && desc !== existing.columns[i].desc;
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
return result;
|
|
1110
|
-
} else if (isModelIndexedIndex(requested)) {
|
|
1111
|
-
const keys = Object.entries(requested.key);
|
|
1112
|
-
const sort = Object.entries(requested.sort);
|
|
1113
|
-
const all = [...keys, ...sort];
|
|
1114
|
-
|
|
1115
|
-
const uniqueChanged = (requested.type === 'indexed:keyed' && existing.is_unique && !requested.unique);
|
|
1116
|
-
const columnSizeChanged = all.length !== existing.columns.length;
|
|
1117
|
-
let result = uniqueChanged || columnSizeChanged;
|
|
1118
|
-
|
|
1119
|
-
for (let i = 0; i < all.length && !result; i++) {
|
|
1120
|
-
const [key, value] = all[i];
|
|
1121
|
-
const desc = value === -1;
|
|
1122
|
-
result ||= key !== existing.columns[i].name && desc !== existing.columns[i].desc;
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
return result;
|
|
1126
|
-
} else {
|
|
1127
|
-
throw new IndexNotSupported(requested.class, requested, 'Only indexed and query indices are supported in SQL');
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
/**
|
|
1132
|
-
* Enforce the dialect specific id length
|
|
1133
|
-
*/
|
|
1134
|
-
enforceIdLength(cls: Class<ModelType>): void {
|
|
1135
|
-
const config = SchemaRegistryIndex.getConfig(cls);
|
|
1136
|
-
const idField = config.fields[this.idField.name];
|
|
1137
|
-
if (idField) {
|
|
1138
|
-
idField.maxlength = { limit: this.ID_LENGTH };
|
|
1139
|
-
idField.minlength = { limit: this.ID_LENGTH };
|
|
1140
|
-
}
|
|
1141
|
-
}
|
|
1142
|
-
}
|