@travetto/model-sql 8.0.0-alpha.23 → 8.0.0-alpha.25
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 +2 -2
- package/__index__.ts +2 -2
- package/package.json +8 -8
- package/src/config.ts +1 -1
- package/src/connection/base.ts +8 -7
- package/src/connection/decorator.ts +5 -10
- package/src/dialect/base.ts +152 -112
- package/src/internal/types.ts +2 -2
- package/src/service.ts +139 -116
- package/src/table-manager.ts +20 -12
- package/src/types.ts +1 -1
- package/src/util.ts +43 -22
- package/support/test/query.ts +8 -5
package/src/dialect/base.ts
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
|
-
|
|
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';
|
|
1
|
+
import { type BulkResponse, type IndexConfig, IndexNotSupported, type ModelType } from '@travetto/model';
|
|
6
2
|
import { isModelIndexedIndex } from '@travetto/model-indexed';
|
|
3
|
+
import {
|
|
4
|
+
isModelQueryIndex,
|
|
5
|
+
ModelQueryUtil,
|
|
6
|
+
type Query,
|
|
7
|
+
type RetainQueryPrimitiveFields,
|
|
8
|
+
type SelectClause,
|
|
9
|
+
type SortClause,
|
|
10
|
+
type WhereClause
|
|
11
|
+
} from '@travetto/model-query';
|
|
12
|
+
import { type Class, castKey, castTo, JSONUtil, RuntimeError, TimeUtil, TypedObject, toConcrete } from '@travetto/runtime';
|
|
13
|
+
import { DataUtil, type Point, type SchemaFieldConfig, SchemaRegistryIndex } from '@travetto/schema';
|
|
7
14
|
|
|
8
|
-
import { SQLModelUtil } from '../util.ts';
|
|
9
|
-
import type { DeleteWrapper, InsertWrapper, DialectState } from '../internal/types.ts';
|
|
10
15
|
import type { Connection } from '../connection/base.ts';
|
|
16
|
+
import type { DeleteWrapper, DialectState, InsertWrapper } from '../internal/types.ts';
|
|
11
17
|
import type { VisitStack } from '../types.ts';
|
|
18
|
+
import { SQLModelUtil } from '../util.ts';
|
|
12
19
|
|
|
13
20
|
const PointConcrete = toConcrete<Point>();
|
|
14
21
|
|
|
@@ -18,9 +25,9 @@ interface Alias {
|
|
|
18
25
|
}
|
|
19
26
|
|
|
20
27
|
export type SQLTableDescription = {
|
|
21
|
-
columns: { name: string
|
|
22
|
-
foreignKeys: { name: string
|
|
23
|
-
indices: { name: string
|
|
28
|
+
columns: { name: string; type: string; is_not_null: boolean }[];
|
|
29
|
+
foreignKeys: { name: string; from_column: string; to_column: string; to_table: string }[];
|
|
30
|
+
indices: { name: string; columns: { name: string; desc: boolean }[]; is_unique: boolean }[];
|
|
24
31
|
};
|
|
25
32
|
|
|
26
33
|
function makeField(name: string, type: Class, required: boolean, extra: Partial<SchemaFieldConfig>): SchemaFieldConfig {
|
|
@@ -163,7 +170,7 @@ export abstract class SQLDialect implements DialectState {
|
|
|
163
170
|
*/
|
|
164
171
|
abstract describeTable(table: string): Promise<SQLTableDescription | undefined>;
|
|
165
172
|
|
|
166
|
-
executeSQL<T>(sql: string): Promise<{ records: T[]
|
|
173
|
+
executeSQL<T>(sql: string): Promise<{ records: T[]; count: number }> {
|
|
167
174
|
return this.connection.execute<T>(this.connection.active, sql);
|
|
168
175
|
}
|
|
169
176
|
|
|
@@ -174,7 +181,7 @@ export abstract class SQLDialect implements DialectState {
|
|
|
174
181
|
if (field === '*') {
|
|
175
182
|
return field;
|
|
176
183
|
} else {
|
|
177
|
-
const name =
|
|
184
|
+
const name = typeof field === 'string' ? field : field.name;
|
|
178
185
|
return `${this.ID_AFFIX}${name}${this.ID_AFFIX}`;
|
|
179
186
|
}
|
|
180
187
|
}
|
|
@@ -300,9 +307,7 @@ export abstract class SQLDialect implements DialectState {
|
|
|
300
307
|
*/
|
|
301
308
|
async getCountForQuery<T extends ModelType>(cls: Class<T>, query: Query<T>): Promise<number> {
|
|
302
309
|
const { records } = await this.executeSQL<{ total: number }>(
|
|
303
|
-
this.getQueryCountSQL(cls,
|
|
304
|
-
ModelQueryUtil.getWhereClause(cls, query.where)
|
|
305
|
-
)
|
|
310
|
+
this.getQueryCountSQL(cls, ModelQueryUtil.getWhereClause(cls, query.where))
|
|
306
311
|
);
|
|
307
312
|
return DataUtil.coerceType(records[0].total, Number);
|
|
308
313
|
}
|
|
@@ -451,14 +456,18 @@ export abstract class SQLDialect implements DialectState {
|
|
|
451
456
|
const resolve = this.resolveValue.bind(this, field);
|
|
452
457
|
|
|
453
458
|
switch (subKey) {
|
|
454
|
-
case '$nin':
|
|
459
|
+
case '$nin':
|
|
460
|
+
case '$in': {
|
|
455
461
|
const arr = (Array.isArray(value) ? value : [value]).map(resolve);
|
|
456
462
|
items.push(`${sPath} ${SQL_OPS[subKey]} (${arr.join(',')})`);
|
|
457
463
|
break;
|
|
458
464
|
}
|
|
459
465
|
case '$all': {
|
|
460
466
|
const set = new Set();
|
|
461
|
-
const arr = [value]
|
|
467
|
+
const arr = [value]
|
|
468
|
+
.flat()
|
|
469
|
+
.filter(item => !set.has(item) && !!set.add(item))
|
|
470
|
+
.map(resolve);
|
|
462
471
|
const valueTable = this.parentTable(sStack);
|
|
463
472
|
const alias = `_all_${sStack.length}`;
|
|
464
473
|
const pPath = this.identifier(this.parentPathField.name);
|
|
@@ -475,7 +484,7 @@ export abstract class SQLDialect implements DialectState {
|
|
|
475
484
|
case '$regex': {
|
|
476
485
|
const regex = DataUtil.toRegex(castTo(value));
|
|
477
486
|
const regexSource = regex.source;
|
|
478
|
-
const ins = regex.flags
|
|
487
|
+
const ins = regex.flags?.includes('i');
|
|
479
488
|
|
|
480
489
|
if (/^[\^]\S+[.][*][$]?$/.test(regexSource)) {
|
|
481
490
|
const inner = regexSource.substring(1, regexSource.length - 2);
|
|
@@ -512,7 +521,8 @@ export abstract class SQLDialect implements DialectState {
|
|
|
512
521
|
}
|
|
513
522
|
break;
|
|
514
523
|
}
|
|
515
|
-
case '$ne':
|
|
524
|
+
case '$ne':
|
|
525
|
+
case '$eq': {
|
|
516
526
|
if (value === null || value === undefined) {
|
|
517
527
|
items.push(`${sPath} ${subKey === '$ne' ? SQL_OPS.$isNot : SQL_OPS.$is} NULL`);
|
|
518
528
|
} else {
|
|
@@ -521,9 +531,13 @@ export abstract class SQLDialect implements DialectState {
|
|
|
521
531
|
}
|
|
522
532
|
break;
|
|
523
533
|
}
|
|
524
|
-
case '$lt':
|
|
525
|
-
|
|
526
|
-
|
|
534
|
+
case '$lt':
|
|
535
|
+
case '$gt':
|
|
536
|
+
case '$gte':
|
|
537
|
+
case '$lte': {
|
|
538
|
+
const subItems = TypedObject.keys(castTo<typeof SQL_OPS>(top)).map(
|
|
539
|
+
subSubKey => `${sPath} ${SQL_OPS[subSubKey]} ${resolve(top[subSubKey])}`
|
|
540
|
+
);
|
|
527
541
|
items.push(subItems.length > 1 ? `(${subItems.join(` ${SQL_OPS.$and} `)})` : subItems[0]);
|
|
528
542
|
break;
|
|
529
543
|
}
|
|
@@ -569,20 +583,18 @@ export abstract class SQLDialect implements DialectState {
|
|
|
569
583
|
* Generate WHERE clause
|
|
570
584
|
*/
|
|
571
585
|
getWhereSQL<T>(cls: Class<T>, where?: WhereClause<T>): string {
|
|
572
|
-
return !where || !Object.keys(where).length ?
|
|
573
|
-
'' :
|
|
574
|
-
`WHERE ${this.getWhereGroupingSQL(cls, castTo(where))}`;
|
|
586
|
+
return !where || !Object.keys(where).length ? '' : `WHERE ${this.getWhereGroupingSQL(cls, castTo(where))}`;
|
|
575
587
|
}
|
|
576
588
|
|
|
577
589
|
/**
|
|
578
590
|
* Generate ORDER BY clause
|
|
579
591
|
*/
|
|
580
592
|
getOrderBySQL<T>(cls: Class<T>, sortBy?: SortClause<T>[]): string {
|
|
581
|
-
return !sortBy
|
|
582
|
-
''
|
|
583
|
-
`ORDER BY ${SQLModelUtil.orderBy(cls, sortBy)
|
|
584
|
-
|
|
585
|
-
|
|
593
|
+
return !sortBy
|
|
594
|
+
? ''
|
|
595
|
+
: `ORDER BY ${SQLModelUtil.orderBy(cls, sortBy)
|
|
596
|
+
.map(item => `${this.resolveName(item.stack)} ${item.asc ? 'ASC' : 'DESC'}`)
|
|
597
|
+
.join(', ')}`;
|
|
586
598
|
}
|
|
587
599
|
|
|
588
600
|
/**
|
|
@@ -590,13 +602,11 @@ export abstract class SQLDialect implements DialectState {
|
|
|
590
602
|
*/
|
|
591
603
|
getSelectSQL<T>(cls: Class<T>, select?: SelectClause<T>): string {
|
|
592
604
|
const stack = SQLModelUtil.classToStack(cls);
|
|
593
|
-
const columns = select && SQLModelUtil.select(cls, select).map(
|
|
605
|
+
const columns = select && SQLModelUtil.select(cls, select).map(sel => this.resolveName([...stack, sel]));
|
|
594
606
|
if (columns) {
|
|
595
607
|
columns.unshift(this.alias(this.pathField));
|
|
596
608
|
}
|
|
597
|
-
return !columns ?
|
|
598
|
-
`SELECT ${this.rootAlias}.* ` :
|
|
599
|
-
`SELECT ${columns.join(', ')}`;
|
|
609
|
+
return !columns ? `SELECT ${this.rootAlias}.* ` : `SELECT ${columns.join(', ')}`;
|
|
600
610
|
}
|
|
601
611
|
|
|
602
612
|
/**
|
|
@@ -606,39 +616,39 @@ export abstract class SQLDialect implements DialectState {
|
|
|
606
616
|
const stack = SQLModelUtil.classToStack(cls);
|
|
607
617
|
const aliases = this.getAliasCache(stack, this.namespace);
|
|
608
618
|
const tables = [...aliases.keys()].toSorted((a, b) => a.length - b.length); // Shortest first
|
|
609
|
-
return `FROM ${tables
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
619
|
+
return `FROM ${tables
|
|
620
|
+
.map(table => {
|
|
621
|
+
const { alias, path } = aliases.get(table)!;
|
|
622
|
+
let from = `${this.identifier(table)} ${alias}`;
|
|
623
|
+
if (path.length > 1) {
|
|
624
|
+
const key = this.namespaceParent(path);
|
|
625
|
+
const { alias: parentAlias } = aliases.get(key)!;
|
|
626
|
+
from = `
|
|
616
627
|
LEFT OUTER JOIN ${from} ON
|
|
617
628
|
${this.alias(this.parentPathField, alias)} = ${this.alias(this.pathField, parentAlias)}
|
|
618
629
|
`;
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
630
|
+
}
|
|
631
|
+
return from;
|
|
632
|
+
})
|
|
633
|
+
.join('\n')}`;
|
|
622
634
|
}
|
|
623
635
|
|
|
624
636
|
/**
|
|
625
637
|
* Generate LIMIT clause
|
|
626
638
|
*/
|
|
627
639
|
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}`;
|
|
640
|
+
return !query || (!query.limit && !query.offset) ? '' : `LIMIT ${query.limit ?? 200} OFFSET ${query.offset ?? 0}`;
|
|
631
641
|
}
|
|
632
642
|
|
|
633
643
|
/**
|
|
634
644
|
* Generate GROUP BY clause
|
|
635
645
|
*/
|
|
636
646
|
getGroupBySQL<T>(cls: Class<T>, query: Query<T>): string {
|
|
637
|
-
const sortFields = !query.sort
|
|
638
|
-
''
|
|
639
|
-
SQLModelUtil.orderBy(cls, query.sort)
|
|
640
|
-
|
|
641
|
-
|
|
647
|
+
const sortFields = !query.sort
|
|
648
|
+
? ''
|
|
649
|
+
: SQLModelUtil.orderBy(cls, query.sort)
|
|
650
|
+
.map(item => this.resolveName(item.stack))
|
|
651
|
+
.join(', ');
|
|
642
652
|
|
|
643
653
|
return `GROUP BY ${this.alias(this.idField)}${sortFields ? `, ${sortFields}` : ''}`;
|
|
644
654
|
}
|
|
@@ -660,9 +670,11 @@ ${this.getLimitSQL(cls, query)}`;
|
|
|
660
670
|
const config = stack.at(-1)!;
|
|
661
671
|
const parent = stack.length > 1;
|
|
662
672
|
const array = parent && config.array;
|
|
663
|
-
const fields = SchemaRegistryIndex.has(config.type)
|
|
664
|
-
[...SQLModelUtil.getFieldsByLocation(stack).local]
|
|
665
|
-
|
|
673
|
+
const fields = SchemaRegistryIndex.has(config.type)
|
|
674
|
+
? [...SQLModelUtil.getFieldsByLocation(stack).local]
|
|
675
|
+
: array
|
|
676
|
+
? [castTo<SchemaFieldConfig>(config)]
|
|
677
|
+
: [];
|
|
666
678
|
|
|
667
679
|
if (!parent) {
|
|
668
680
|
const idField = fields.find(field => field.name === this.idField.name);
|
|
@@ -680,12 +692,14 @@ ${this.getLimitSQL(cls, query)}`;
|
|
|
680
692
|
CREATE TABLE IF NOT EXISTS ${this.table(stack)} (
|
|
681
693
|
${fieldSql}${fieldSql.length ? ',' : ''}
|
|
682
694
|
${this.getColumnDefinition(this.pathField)} UNIQUE,
|
|
683
|
-
${
|
|
684
|
-
|
|
685
|
-
|
|
695
|
+
${
|
|
696
|
+
!parent
|
|
697
|
+
? `PRIMARY KEY (${this.identifier(this.idField)})`
|
|
698
|
+
: `${this.getColumnDefinition(this.parentPathField)},
|
|
686
699
|
${array ? `${this.getColumnDefinition(this.idxField)},` : ''}
|
|
687
700
|
PRIMARY KEY (${this.identifier(this.pathField)}),
|
|
688
|
-
FOREIGN KEY (${this.identifier(this.parentPathField)}) REFERENCES ${this.parentTable(stack)}(${this.identifier(this.pathField)}) ON DELETE CASCADE`
|
|
701
|
+
FOREIGN KEY (${this.identifier(this.parentPathField)}) REFERENCES ${this.parentTable(stack)}(${this.identifier(this.pathField)}) ON DELETE CASCADE`
|
|
702
|
+
}
|
|
689
703
|
);`;
|
|
690
704
|
return out;
|
|
691
705
|
}
|
|
@@ -710,8 +724,14 @@ CREATE TABLE IF NOT EXISTS ${this.table(stack)} (
|
|
|
710
724
|
getCreateAllTablesSQL(cls: Class): string[] {
|
|
711
725
|
const out: string[] = [];
|
|
712
726
|
SQLModelUtil.visitSchemaSync(SchemaRegistryIndex.getConfig(cls), {
|
|
713
|
-
onRoot: ({ path, descend }) => {
|
|
714
|
-
|
|
727
|
+
onRoot: ({ path, descend }) => {
|
|
728
|
+
out.push(this.getCreateTableSQL(path));
|
|
729
|
+
descend();
|
|
730
|
+
},
|
|
731
|
+
onSub: ({ path, descend }) => {
|
|
732
|
+
out.push(this.getCreateTableSQL(path));
|
|
733
|
+
descend();
|
|
734
|
+
},
|
|
715
735
|
onSimple: ({ path }) => out.push(this.getCreateTableSQL(path))
|
|
716
736
|
});
|
|
717
737
|
return out;
|
|
@@ -746,7 +766,7 @@ CREATE TABLE IF NOT EXISTS ${this.table(stack)} (
|
|
|
746
766
|
if (DataUtil.isPlainObject(value)) {
|
|
747
767
|
throw new IndexNotSupported(cls, idx, 'Only indexed and query indices are supported in SQL');
|
|
748
768
|
}
|
|
749
|
-
return [castTo(key), typeof value === 'number' ? value === 1 :
|
|
769
|
+
return [castTo(key), typeof value === 'number' ? value === 1 : !!value];
|
|
750
770
|
});
|
|
751
771
|
return `CREATE ${idx.unique ? 'UNIQUE ' : ''}INDEX ${constraint} ON ${this.identifier(table)} (${fields
|
|
752
772
|
.map(([name, sel]) => `${this.identifier(name)} ${sel ? 'ASC' : 'DESC'}`)
|
|
@@ -757,12 +777,12 @@ CREATE TABLE IF NOT EXISTS ${this.table(stack)} (
|
|
|
757
777
|
console.debug('Nested fields are not supported in ModelIndexed indices SQL', { index: idx.name });
|
|
758
778
|
return;
|
|
759
779
|
}
|
|
760
|
-
const fields = all
|
|
761
|
-
.map(({ path, value }) => `${this.identifier(path.join('_'))} ${value === -1 ? 'DESC' : 'ASC'}`)
|
|
762
|
-
.join(', ');
|
|
780
|
+
const fields = all.map(({ path, value }) => `${this.identifier(path.join('_'))} ${value === -1 ? 'DESC' : 'ASC'}`).join(', ');
|
|
763
781
|
switch (idx.type) {
|
|
764
|
-
case 'indexed:keyed':
|
|
765
|
-
|
|
782
|
+
case 'indexed:keyed':
|
|
783
|
+
return `CREATE ${idx.unique ? 'UNIQUE ' : ''}INDEX ${constraint} ON ${this.identifier(table)} (${fields});`;
|
|
784
|
+
case 'indexed:sorted':
|
|
785
|
+
return `CREATE INDEX ${constraint} ON ${this.identifier(table)} (${fields});`;
|
|
766
786
|
}
|
|
767
787
|
} else {
|
|
768
788
|
throw new IndexNotSupported(cls, idx, 'Only indexed and query indices are supported in SQL');
|
|
@@ -783,8 +803,14 @@ CREATE TABLE IF NOT EXISTS ${this.table(stack)} (
|
|
|
783
803
|
getDropAllTablesSQL<T extends ModelType>(cls: Class<T>): string[] {
|
|
784
804
|
const out: string[] = [];
|
|
785
805
|
SQLModelUtil.visitSchemaSync(SchemaRegistryIndex.getConfig(cls), {
|
|
786
|
-
onRoot: ({ path, descend }) => {
|
|
787
|
-
|
|
806
|
+
onRoot: ({ path, descend }) => {
|
|
807
|
+
descend();
|
|
808
|
+
out.push(this.getDropTableSQL(path));
|
|
809
|
+
},
|
|
810
|
+
onSub: ({ path, descend }) => {
|
|
811
|
+
descend();
|
|
812
|
+
out.push(this.getDropTableSQL(path));
|
|
813
|
+
},
|
|
788
814
|
onSimple: ({ path }) => out.push(this.getDropTableSQL(path))
|
|
789
815
|
});
|
|
790
816
|
return out;
|
|
@@ -796,8 +822,14 @@ CREATE TABLE IF NOT EXISTS ${this.table(stack)} (
|
|
|
796
822
|
getTruncateAllTablesSQL<T extends ModelType>(cls: Class<T>): string[] {
|
|
797
823
|
const out: string[] = [];
|
|
798
824
|
SQLModelUtil.visitSchemaSync(SchemaRegistryIndex.getConfig(cls), {
|
|
799
|
-
onRoot: ({ path, descend }) => {
|
|
800
|
-
|
|
825
|
+
onRoot: ({ path, descend }) => {
|
|
826
|
+
descend();
|
|
827
|
+
out.push(this.getTruncateTableSQL(path));
|
|
828
|
+
},
|
|
829
|
+
onSub: ({ path, descend }) => {
|
|
830
|
+
descend();
|
|
831
|
+
out.push(this.getTruncateTableSQL(path));
|
|
832
|
+
},
|
|
801
833
|
onSimple: ({ path }) => out.push(this.getTruncateTableSQL(path))
|
|
802
834
|
});
|
|
803
835
|
return out;
|
|
@@ -808,8 +840,8 @@ CREATE TABLE IF NOT EXISTS ${this.table(stack)} (
|
|
|
808
840
|
*/
|
|
809
841
|
getInsertSQL(stack: VisitStack[], instances: InsertWrapper['records']): string | undefined {
|
|
810
842
|
const config = stack.at(-1)!;
|
|
811
|
-
const columns = SQLModelUtil.getFieldsByLocation(stack)
|
|
812
|
-
.filter(field => !SchemaRegistryIndex.has(field.type))
|
|
843
|
+
const columns = SQLModelUtil.getFieldsByLocation(stack)
|
|
844
|
+
.local.filter(field => !SchemaRegistryIndex.has(field.type))
|
|
813
845
|
.toSorted((a, b) => a.name.localeCompare(b.name));
|
|
814
846
|
const columnNames = columns.map(column => column.name);
|
|
815
847
|
|
|
@@ -820,7 +852,7 @@ CREATE TABLE IF NOT EXISTS ${this.table(stack)} (
|
|
|
820
852
|
const newInstances: typeof instances = [];
|
|
821
853
|
for (const instance of instances) {
|
|
822
854
|
if (instance.value === null || instance.value === undefined) {
|
|
823
|
-
|
|
855
|
+
// Continue
|
|
824
856
|
} else if (Array.isArray(instance.value)) {
|
|
825
857
|
const name = instance.stack.at(-1)!.name;
|
|
826
858
|
for (const sel of instance.value) {
|
|
@@ -842,8 +874,9 @@ CREATE TABLE IF NOT EXISTS ${this.table(stack)} (
|
|
|
842
874
|
return;
|
|
843
875
|
}
|
|
844
876
|
|
|
845
|
-
const matrix = instances.map(inst =>
|
|
846
|
-
this.resolveValue(column, castTo<Record<string, unknown>>(inst.value)[column.name]))
|
|
877
|
+
const matrix = instances.map(inst =>
|
|
878
|
+
columns.map(column => this.resolveValue(column, castTo<Record<string, unknown>>(inst.value)[column.name]))
|
|
879
|
+
);
|
|
847
880
|
|
|
848
881
|
columnNames.push(this.pathField.name);
|
|
849
882
|
if (hasParent) {
|
|
@@ -879,7 +912,9 @@ ${matrix.map(row => `(${row.join(', ')})`).join(',\n')};`;
|
|
|
879
912
|
*/
|
|
880
913
|
getAllInsertSQL<T extends ModelType>(cls: Class<T>, instance: T): string[] {
|
|
881
914
|
const out: string[] = [];
|
|
882
|
-
const add = (text?: string): void => {
|
|
915
|
+
const add = (text?: string): void => {
|
|
916
|
+
text && out.push(text);
|
|
917
|
+
};
|
|
883
918
|
SQLModelUtil.visitSchemaInstance(cls, instance, {
|
|
884
919
|
onRoot: ({ value, path }) => add(this.getInsertSQL(path, [{ stack: path, value }])),
|
|
885
920
|
onSub: ({ value, path }) => add(this.getInsertSQL(path, [{ stack: path, value }])),
|
|
@@ -897,10 +932,10 @@ ${matrix.map(row => `(${row.join(', ')})`).join(',\n')};`;
|
|
|
897
932
|
return `
|
|
898
933
|
UPDATE ${this.table(stack)} ${this.rootAlias}
|
|
899
934
|
SET
|
|
900
|
-
${Object
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
935
|
+
${Object.entries(data)
|
|
936
|
+
.filter(([key]) => key in localMap)
|
|
937
|
+
.map(([key, value]) => `${this.identifier(key)}=${this.resolveValue(localMap[key], value)}`)
|
|
938
|
+
.join(', ')}
|
|
904
939
|
${this.getWhereSQL(type, where)};`;
|
|
905
940
|
}
|
|
906
941
|
|
|
@@ -917,11 +952,9 @@ ${this.getWhereSQL(type, where)};`;
|
|
|
917
952
|
*/
|
|
918
953
|
getSelectRowsByIdsSQL(stack: VisitStack[], ids: string[], select: SchemaFieldConfig[] = []): string {
|
|
919
954
|
const config = stack.at(-1)!;
|
|
920
|
-
const orderBy = !config.array ?
|
|
921
|
-
'' :
|
|
922
|
-
`ORDER BY ${this.rootAlias}.${this.idxField.name} ASC`;
|
|
955
|
+
const orderBy = !config.array ? '' : `ORDER BY ${this.rootAlias}.${this.idxField.name} ASC`;
|
|
923
956
|
|
|
924
|
-
const idField =
|
|
957
|
+
const idField = stack.length > 1 ? this.parentPathField : this.idField;
|
|
925
958
|
|
|
926
959
|
return `
|
|
927
960
|
SELECT ${select.length ? select.map(field => this.alias(field)).join(',') : '*'}
|
|
@@ -948,7 +981,7 @@ ${this.getWhereSQL(cls, where!)}`;
|
|
|
948
981
|
SQLModelUtil.collectDependents(this, stack.at(-1)!, children, field);
|
|
949
982
|
|
|
950
983
|
await SQLModelUtil.visitSchema(SchemaRegistryIndex.getConfig(cls), {
|
|
951
|
-
onRoot: async
|
|
984
|
+
onRoot: async config => {
|
|
952
985
|
const fieldSet = buildSet(items); // Already filtered by initial select query
|
|
953
986
|
selectStack.push(select);
|
|
954
987
|
stack.push(fieldSet);
|
|
@@ -962,8 +995,8 @@ ${this.getWhereSQL(cls, where!)}`;
|
|
|
962
995
|
const subSelectTop: SelectClause<T> | undefined = castTo(selectTop?.[fieldKey]);
|
|
963
996
|
|
|
964
997
|
// See if a selection exists at all
|
|
965
|
-
const selected: SchemaFieldConfig[] = subSelectTop
|
|
966
|
-
.filter(field => typeof subSelectTop === 'object' && subSelectTop[castTo<typeof fieldKey>(field.name)] === 1)
|
|
998
|
+
const selected: SchemaFieldConfig[] = subSelectTop
|
|
999
|
+
? fields.filter(field => typeof subSelectTop === 'object' && subSelectTop[castTo<typeof fieldKey>(field.name)] === 1)
|
|
967
1000
|
: [];
|
|
968
1001
|
|
|
969
1002
|
if (selected.length) {
|
|
@@ -975,11 +1008,7 @@ ${this.getWhereSQL(cls, where!)}`;
|
|
|
975
1008
|
|
|
976
1009
|
// If children and selection exists
|
|
977
1010
|
if (ids.length && (!subSelectTop || selected)) {
|
|
978
|
-
const { records: children } = await this.executeSQL<unknown[]>(this.getSelectRowsByIdsSQL(
|
|
979
|
-
path,
|
|
980
|
-
ids,
|
|
981
|
-
selected
|
|
982
|
-
));
|
|
1011
|
+
const { records: children } = await this.executeSQL<unknown[]>(this.getSelectRowsByIdsSQL(path, ids, selected));
|
|
983
1012
|
|
|
984
1013
|
const fieldSet = buildSet(children, config);
|
|
985
1014
|
try {
|
|
@@ -996,10 +1025,7 @@ ${this.getWhereSQL(cls, where!)}`;
|
|
|
996
1025
|
const top = stack.at(-1)!;
|
|
997
1026
|
const ids = Object.keys(top);
|
|
998
1027
|
if (ids.length) {
|
|
999
|
-
const { records: matching } = await this.executeSQL(this.getSelectRowsByIdsSQL(
|
|
1000
|
-
path,
|
|
1001
|
-
ids
|
|
1002
|
-
));
|
|
1028
|
+
const { records: matching } = await this.executeSQL(this.getSelectRowsByIdsSQL(path, ids));
|
|
1003
1029
|
buildSet(matching, config);
|
|
1004
1030
|
}
|
|
1005
1031
|
}
|
|
@@ -1024,7 +1050,12 @@ ${this.getWhereSQL(cls, where!)}`;
|
|
|
1024
1050
|
/**
|
|
1025
1051
|
* Do bulk process
|
|
1026
1052
|
*/
|
|
1027
|
-
async bulkProcess(
|
|
1053
|
+
async bulkProcess(
|
|
1054
|
+
deletes: DeleteWrapper[],
|
|
1055
|
+
inserts: InsertWrapper[],
|
|
1056
|
+
upserts: InsertWrapper[],
|
|
1057
|
+
updates: InsertWrapper[]
|
|
1058
|
+
): Promise<BulkResponse> {
|
|
1028
1059
|
const out = {
|
|
1029
1060
|
counts: {
|
|
1030
1061
|
delete: deletes.reduce((count, item) => count + item.ids.length, 0),
|
|
@@ -1048,13 +1079,19 @@ ${this.getWhereSQL(cls, where!)}`;
|
|
|
1048
1079
|
...upserts
|
|
1049
1080
|
.filter(item => item.stack.length === 1)
|
|
1050
1081
|
.map(item =>
|
|
1051
|
-
this.deleteByIds(
|
|
1082
|
+
this.deleteByIds(
|
|
1083
|
+
item.stack,
|
|
1084
|
+
item.records.map(value => castTo<Record<string, string>>(value.value)[idx])
|
|
1085
|
+
)
|
|
1052
1086
|
),
|
|
1053
1087
|
...updates
|
|
1054
1088
|
.filter(item => item.stack.length === 1)
|
|
1055
1089
|
.map(item =>
|
|
1056
|
-
this.deleteByIds(
|
|
1057
|
-
|
|
1090
|
+
this.deleteByIds(
|
|
1091
|
+
item.stack,
|
|
1092
|
+
item.records.map(value => castTo<Record<string, string>>(value.value)[idx])
|
|
1093
|
+
)
|
|
1094
|
+
)
|
|
1058
1095
|
]);
|
|
1059
1096
|
}
|
|
1060
1097
|
|
|
@@ -1064,15 +1101,18 @@ ${this.getWhereSQL(cls, where!)}`;
|
|
|
1064
1101
|
continue;
|
|
1065
1102
|
}
|
|
1066
1103
|
let level = 1; // Add by level
|
|
1067
|
-
for (
|
|
1104
|
+
for (;;) {
|
|
1105
|
+
// Loop until done
|
|
1068
1106
|
const leveled = items.filter(insertWrapper => insertWrapper.stack.length === level);
|
|
1069
1107
|
if (!leveled.length) {
|
|
1070
1108
|
break;
|
|
1071
1109
|
}
|
|
1072
|
-
await Promise.all(
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1110
|
+
await Promise.all(
|
|
1111
|
+
leveled
|
|
1112
|
+
.map(inserted => this.getInsertSQL(inserted.stack, inserted.records))
|
|
1113
|
+
.filter(sql => !!sql)
|
|
1114
|
+
.map(sql => this.executeSQL(sql!))
|
|
1115
|
+
);
|
|
1076
1116
|
level += 1;
|
|
1077
1117
|
}
|
|
1078
1118
|
}
|
|
@@ -1083,11 +1123,11 @@ ${this.getWhereSQL(cls, where!)}`;
|
|
|
1083
1123
|
/**
|
|
1084
1124
|
* Determine if a column has changed
|
|
1085
1125
|
*/
|
|
1086
|
-
isColumnChanged(requested: SchemaFieldConfig, existing: SQLTableDescription['columns'][number]
|
|
1126
|
+
isColumnChanged(requested: SchemaFieldConfig, existing: SQLTableDescription['columns'][number]): boolean {
|
|
1087
1127
|
const requestedColumnType = this.getColumnType(requested);
|
|
1088
1128
|
const result =
|
|
1089
|
-
(requested.name !== this.idField.name && !!requested.required?.active !== !!existing.is_not_null)
|
|
1090
|
-
|
|
1129
|
+
(requested.name !== this.idField.name && !!requested.required?.active !== !!existing.is_not_null) ||
|
|
1130
|
+
requestedColumnType.toUpperCase() !== existing.type.toUpperCase();
|
|
1091
1131
|
|
|
1092
1132
|
return result;
|
|
1093
1133
|
}
|
|
@@ -1097,7 +1137,7 @@ ${this.getWhereSQL(cls, where!)}`;
|
|
|
1097
1137
|
*/
|
|
1098
1138
|
isIndexChanged(requested: IndexConfig, existing: SQLTableDescription['indices'][number]): boolean {
|
|
1099
1139
|
if (isModelQueryIndex(requested)) {
|
|
1100
|
-
const uniqueChanged =
|
|
1140
|
+
const uniqueChanged = existing.is_unique && !requested.unique;
|
|
1101
1141
|
const columnSizeChanged = requested.fields.length !== existing.columns.length;
|
|
1102
1142
|
let result = uniqueChanged || columnSizeChanged;
|
|
1103
1143
|
for (let i = 0; i < requested.fields.length && !result; i++) {
|
|
@@ -1112,7 +1152,7 @@ ${this.getWhereSQL(cls, where!)}`;
|
|
|
1112
1152
|
const sort = Object.entries(requested.sort);
|
|
1113
1153
|
const all = [...keys, ...sort];
|
|
1114
1154
|
|
|
1115
|
-
const uniqueChanged =
|
|
1155
|
+
const uniqueChanged = requested.type === 'indexed:keyed' && existing.is_unique && !requested.unique;
|
|
1116
1156
|
const columnSizeChanged = all.length !== existing.columns.length;
|
|
1117
1157
|
let result = uniqueChanged || columnSizeChanged;
|
|
1118
1158
|
|
|
@@ -1139,4 +1179,4 @@ ${this.getWhereSQL(cls, where!)}`;
|
|
|
1139
1179
|
idField.minlength = { limit: this.ID_LENGTH };
|
|
1140
1180
|
}
|
|
1141
1181
|
}
|
|
1142
|
-
}
|
|
1182
|
+
}
|
package/src/internal/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { SchemaClassConfig, SchemaFieldConfig } from '@travetto/schema';
|
|
2
2
|
|
|
3
3
|
import type { VisitStack } from '../types.ts';
|
|
4
4
|
|
|
@@ -7,7 +7,7 @@ import type { VisitStack } from '../types.ts';
|
|
|
7
7
|
*/
|
|
8
8
|
export interface InsertWrapper {
|
|
9
9
|
stack: VisitStack[];
|
|
10
|
-
records: { stack: VisitStack[]
|
|
10
|
+
records: { stack: VisitStack[]; value: unknown }[];
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
/**
|