@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.
@@ -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 PointImpl = toConcrete<Point>();
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
- owner: null!,
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
- ID_LEN = 32;
48
+ ID_LENGTH = 32;
43
49
 
44
50
  /**
45
51
  * Hash Length
46
52
  */
47
- HASH_LEN = 64;
53
+ HASH_LENGTH = 64;
48
54
 
49
55
  /**
50
56
  * Default length for varchar
51
57
  */
52
- DEFAULT_STRING_LEN = 1024;
58
+ DEFAULT_STRING_LENGTH = 1024;
53
59
 
54
60
  /**
55
- * Mapping between query ops and SQL operations
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: n => `VARCHAR(${n})`,
99
- DECIMAL: (d, p) => `DECIMAL(${d},${p})`
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: { n: this.ID_LEN },
109
- minlength: { n: this.ID_LEN }
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: { n: this.HASH_LEN },
122
- minlength: { n: this.HASH_LEN },
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: { n: this.HASH_LEN },
131
- minlength: { n: this.HASH_LEN },
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
- ns: string;
146
+ namespacePrefix: string;
141
147
 
142
- constructor(ns: string) {
148
+ constructor(namespacePrefix: string) {
143
149
  this.namespace = this.namespace.bind(this);
144
150
  this.table = this.table.bind(this);
145
- this.ident = this.ident.bind(this);
146
- this.ns = ns ? `${ns}_` : ns;
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 conn(): Connection<unknown>;
158
+ abstract get connection(): Connection<unknown>;
153
159
 
154
160
  /**
155
161
  * Hash a value
156
162
  */
157
- abstract hash(inp: string): string;
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.conn.execute<T>(this.conn.active, sql);
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
- ident(field: SchemaFieldConfig | string | symbol): string {
177
+ identifier(field: SchemaFieldConfig | string): string {
167
178
  if (field === '*') {
168
179
  return field;
169
180
  } else {
170
- const name = (typeof field === 'symbol' || typeof field === 'string') ? field : field.name;
171
- return `${this.ID_AFFIX}${name.toString()}${this.ID_AFFIX}`;
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(conf: SchemaFieldConfig, value: unknown): string {
203
+ resolveValue(config: SchemaFieldConfig, value: unknown): string {
193
204
  if (value === undefined || value === null) {
194
205
  return 'NULL';
195
- } else if (conf.type === String) {
206
+ } else if (config.type === String) {
196
207
  if (value instanceof RegExp) {
197
- const src = DataUtil.toRegex(value).source.replace(/\\b/g, this.regexWordBoundary);
198
- return this.quote(src);
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 (conf.type === Boolean) {
213
+ } else if (config.type === Boolean) {
203
214
  return `${value ? 'TRUE' : 'FALSE'}`;
204
- } else if (conf.type === Number) {
215
+ } else if (config.type === Number) {
205
216
  return `${value}`;
206
- } else if (conf.type === Date) {
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 (conf.type === PointImpl && Array.isArray(value)) {
223
+ } else if (config.type === PointConcrete && Array.isArray(value)) {
213
224
  return `point(${value[0]},${value[1]})`;
214
- } else if (conf.type === Object) {
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 ${conf.name.toString()}, ${value}`, { category: 'data' });
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(conf: SchemaFieldConfig): string {
234
+ getColumnType(config: SchemaFieldConfig): string {
224
235
  let type: string = '';
225
236
 
226
- if (conf.type === Number) {
237
+ if (config.type === Number) {
227
238
  type = this.COLUMN_TYPES.INT;
228
- if (conf.precision) {
229
- const [digits, decimals] = conf.precision;
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 (conf.type === Date) {
259
+ } else if (config.type === Date) {
249
260
  type = this.COLUMN_TYPES.TIMESTAMP;
250
- } else if (conf.type === Boolean) {
261
+ } else if (config.type === Boolean) {
251
262
  type = this.COLUMN_TYPES.BOOLEAN;
252
- } else if (conf.type === String) {
253
- if (conf.specifiers?.includes('text')) {
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(conf.maxlength ? conf.maxlength.n : this.DEFAULT_STRING_LEN);
267
+ type = this.PARAMETERIZED_COLUMN_TYPES.VARCHAR(config.maxlength?.limit ?? this.DEFAULT_STRING_LENGTH);
257
268
  }
258
- } else if (conf.type === PointImpl) {
269
+ } else if (config.type === PointConcrete) {
259
270
  type = this.COLUMN_TYPES.POINT;
260
- } else if (conf.type === Object) {
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(conf: SchemaFieldConfig): string | undefined {
271
- const type = this.getColumnType(conf);
281
+ getColumnDefinition(config: SchemaFieldConfig, overrideRequired?: boolean): string | undefined {
282
+ const type = this.getColumnType(config);
272
283
  if (!type) {
273
284
  return;
274
285
  }
275
- return `${this.ident(conf)} ${type} ${(conf.required?.active !== false) ? 'NOT NULL' : 'DEFAULT NULL'}`;
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.ident(field.name)};`;
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.ns}${SQLModelUtil.buildTable(stack)}`;
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.ident(this.namespace(stack));
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 | symbol | SchemaFieldConfig, alias: string = this.rootAlias): string {
355
- return `${alias}.${this.ident(field)}`;
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.toString().charAt(0)}${idx++}`, path });
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.toString().charAt(0)}${idx++}`, path });
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[], o: Record<string, unknown>): string {
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(o)) {
413
- const top = o[key];
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
- owner: null!,
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 v = top[subKey];
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(v) ? v : [v]).map(el => resolve(el));
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 = [v].flat().filter(x => !set.has(x) && !!set.add(x)).map(el => resolve(el));
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.ident(this.parentPathField.name);
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.ident(field.name)})
465
+ SELECT COUNT(DISTINCT ${alias}.${this.identifier(field.name)})
454
466
  FROM ${valueTable} ${alias}
455
467
  WHERE ${alias}.${pPath} = ${rpPath}
456
- AND ${alias}.${this.ident(field.name)} IN (${arr.join(',')})
468
+ AND ${alias}.${this.identifier(field.name)} IN (${arr.join(',')})
457
469
  )`);
458
470
  break;
459
471
  }
460
472
  case '$regex': {
461
- const re = DataUtil.toRegex(castTo(v));
462
- const src = re.source;
463
- const ins = re.flags && re.flags.includes('i');
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(src)) {
466
- const inner = src.substring(1, src.length - 2);
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 val = resolve(v);
475
- items.push(`${sPath} ${SQL_OPS[!ins ? subKey : '$iregex']} ${val}`);
486
+ const result = resolve(value);
487
+ items.push(`${sPath} ${SQL_OPS[!ins ? subKey : '$iregex']} ${result}`);
476
488
  } else {
477
- const val = resolve(new RegExp(src.toLowerCase(), re.flags));
478
- items.push(`LOWER(${sPath}) ${SQL_OPS[subKey]} ${val}`);
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.ident(this.parentPathField.name);
499
+ const pPath = this.identifier(this.parentPathField.name);
488
500
  const rpPath = this.resolveName([...sStack, field, this.parentPathField]);
489
501
 
490
- items.push(`0 ${!v ? '=' : '<>'} (
491
- SELECT COUNT(${alias}.${this.ident(field.name)})
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} ${v ? SQL_OPS.$isNot : SQL_OPS.$is} NULL`);
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 (v === null || v === undefined) {
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(v)}`;
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(ssk => `${sPath} ${SQL_OPS[ssk]} ${resolve(top[ssk])}`);
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>, o: WhereClause<T>): string {
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(o)) {
541
- return `(${o.$and.map(x => this.getWhereGroupingSQL<T>(cls, x)).join(` ${SQL_OPS.$and} `)})`;
542
- } else if (ModelQueryUtil.has$Or(o)) {
543
- return `(${o.$or.map(x => this.getWhereGroupingSQL<T>(cls, x)).join(` ${SQL_OPS.$or} `)})`;
544
- } else if (ModelQueryUtil.has$Not(o)) {
545
- return `${SQL_OPS.$not} (${this.getWhereGroupingSQL<T>(cls, o.$not)})`;
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), o);
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((ob) =>
567
- `${this.resolveName(ob.stack)} ${ob.asc ? 'ASC' : 'DESC'}`
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.ident(table)} ${alias}`;
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(x => this.resolveName(x.stack))
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(x => x.name === this.idField.name);
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(f => {
662
- const def = this.getColumnDefinition(f) || '';
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.ident(this.idField)})` :
680
+ `PRIMARY KEY (${this.identifier(this.idField)})` :
675
681
  `${this.getColumnDefinition(this.parentPathField)},
676
682
  ${array ? `${this.getColumnDefinition(this.idxField)},` : ''}
677
- PRIMARY KEY (${this.ident(this.pathField)}),
678
- FOREIGN KEY (${this.ident(this.parentPathField)}) REFERENCES ${this.parentTable(stack)}(${this.ident(this.pathField)}) ON DELETE CASCADE`}
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(x => {
723
- const key = TypedObject.keys(x)[0];
724
- const val = x[key];
725
- if (DataUtil.isPlainObject(val)) {
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 val === 'number' ? val === 1 : (!!val)];
742
+ return [castTo(key), typeof value === 'number' ? value === 1 : (!!value)];
729
743
  });
730
- const constraint = `idx_${table}_${fields.map(([f]) => f).join('_')}`;
731
- return `CREATE ${idx.type === 'unique' ? 'UNIQUE ' : ''}INDEX ${constraint} ON ${this.ident(table)} (${fields
732
- .map(([name, sel]) => `${this.ident(name)} ${sel ? 'ASC' : 'DESC'}`)
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(x => !SchemaRegistryIndex.has(x.type))
769
- .toSorted((a, b) => a.name.toString().localeCompare(b.name.toString()));
770
- const columnNames = columns.map(c => c.name);
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 el of instances) {
778
- if (el.value === null || el.value === undefined) {
799
+ for (const instance of instances) {
800
+ if (instance.value === null || instance.value === undefined) {
779
801
  continue;
780
- } else if (Array.isArray(el.value)) {
781
- const name = el.stack.at(-1)!.name;
782
- for (const sel of el.value) {
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: el.stack,
806
+ stack: instance.stack,
785
807
  value: {
786
808
  [name]: sel
787
809
  }
788
810
  });
789
811
  }
790
812
  } else {
791
- newInstances.push(el);
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(c => this.resolveValue(c, castTo<Record<string | symbol, unknown>>(inst.value)[c.name])));
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.ident).join(', ')})
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(([k]) => k in localMap)
858
- .map(([k, v]) => `${this.ident(k)}=${this.resolveValue(localMap[k], v)}`).join(', ')}
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
- * Get elements by ids
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.toString()} ASC`;
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(x => this.alias(x)).join(',') : '*'}
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 res = buildSet(items); // Already filtered by initial select query
930
+ const fieldSet = buildSet(items); // Already filtered by initial select query
908
931
  selectStack.push(select);
909
- stack.push(res);
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 sel: SchemaFieldConfig[] = subSelectTop ? fields
921
- .filter(f => typeof subSelectTop === 'object' && subSelectTop[castTo<typeof fieldKey>(f.name)] === 1)
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 (sel.length) {
925
- sel.push(this.pathField, this.parentPathField);
947
+ if (selected.length) {
948
+ selected.push(this.pathField, this.parentPathField);
926
949
  if (config.array) {
927
- sel.push(this.idxField);
950
+ selected.push(this.idxField);
928
951
  }
929
952
  }
930
953
 
931
954
  // If children and selection exists
932
- if (ids.length && (!subSelectTop || sel)) {
955
+ if (ids.length && (!subSelectTop || selected)) {
933
956
  const { records: children } = await this.executeSQL<unknown[]>(this.getSelectRowsByIdsSQL(
934
957
  path,
935
958
  ids,
936
- sel
959
+ selected
937
960
  ));
938
961
 
939
- const res = buildSet(children, config);
962
+ const fieldSet = buildSet(children, config);
940
963
  try {
941
- stack.push(res);
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((acc, el) => acc + el.ids.length, 0),
1008
+ delete: deletes.reduce((count, item) => count + item.ids.length, 0),
986
1009
  error: 0,
987
- insert: inserts.filter(x => x.stack.length === 1).reduce((acc, el) => acc + el.records.length, 0),
988
- update: updates.filter(x => x.stack.length === 1).reduce((acc, el) => acc + el.records.length, 0),
989
- upsert: upserts.filter(x => x.stack.length === 1).reduce((acc, el) => acc + el.records.length, 0)
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(el => this.deleteByIds(el.stack, el.ids)));
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.filter(x => x.stack.length === 1).map(i =>
1004
- this.deleteByIds(i.stack, i.records.map(v => castTo<Record<string | symbol, string>>(v.value)[idx]))
1005
- ),
1006
- ...updates.filter(x => x.stack.length === 1).map(i =>
1007
- this.deleteByIds(i.stack, i.records.map(v => castTo<Record<string | symbol, string>>(v.value)[idx]))
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 lvl = 1; // Add by level
1044
+ let level = 1; // Add by level
1018
1045
  for (; ;) { // Loop until done
1019
- const leveled = items.filter(f => f.stack.length === lvl);
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(iw => this.getInsertSQL(iw.stack, iw.records))
1051
+ .map(inserted => this.getInsertSQL(inserted.stack, inserted.records))
1025
1052
  .filter(sql => !!sql)
1026
1053
  .map(sql => this.executeSQL(sql!)));
1027
- lvl += 1;
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
  }