@travetto/model-sql 8.0.0-alpha.1 → 8.0.0-alpha.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +8 -7
- package/src/dialect/base.ts +54 -29
- package/src/service.ts +119 -4
- package/src/table-manager.ts +11 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/model-sql",
|
|
3
|
-
"version": "8.0.0-alpha.
|
|
3
|
+
"version": "8.0.0-alpha.11",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "SQL backing for the travetto model module, with real-time modeling support for SQL schemas.",
|
|
6
6
|
"keywords": [
|
|
@@ -28,14 +28,15 @@
|
|
|
28
28
|
"directory": "module/model-sql"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@travetto/config": "^8.0.0-alpha.
|
|
32
|
-
"@travetto/context": "^8.0.0-alpha.
|
|
33
|
-
"@travetto/model": "^8.0.0-alpha.
|
|
34
|
-
"@travetto/model-
|
|
31
|
+
"@travetto/config": "^8.0.0-alpha.10",
|
|
32
|
+
"@travetto/context": "^8.0.0-alpha.10",
|
|
33
|
+
"@travetto/model": "^8.0.0-alpha.10",
|
|
34
|
+
"@travetto/model-indexed": "^8.0.0-alpha.11",
|
|
35
|
+
"@travetto/model-query": "^8.0.0-alpha.10"
|
|
35
36
|
},
|
|
36
37
|
"peerDependencies": {
|
|
37
|
-
"@travetto/cli": "^8.0.0-alpha.
|
|
38
|
-
"@travetto/test": "^8.0.0-alpha.
|
|
38
|
+
"@travetto/cli": "^8.0.0-alpha.15",
|
|
39
|
+
"@travetto/test": "^8.0.0-alpha.10"
|
|
39
40
|
},
|
|
40
41
|
"peerDependenciesMeta": {
|
|
41
42
|
"@travetto/cli": {
|
package/src/dialect/base.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/* eslint-disable @stylistic/indent */
|
|
2
2
|
import { DataUtil, type SchemaFieldConfig, SchemaRegistryIndex, type Point } from '@travetto/schema';
|
|
3
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 } from '@travetto/model-query';
|
|
5
|
-
import type
|
|
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';
|
|
6
7
|
|
|
7
8
|
import { SQLModelUtil } from '../util.ts';
|
|
8
9
|
import type { DeleteWrapper, InsertWrapper, DialectState } from '../internal/types.ts';
|
|
@@ -717,14 +718,14 @@ CREATE TABLE IF NOT EXISTS ${this.table(stack)} (
|
|
|
717
718
|
/**
|
|
718
719
|
* Get all create indices need for a given class
|
|
719
720
|
*/
|
|
720
|
-
getCreateAllIndicesSQL<T extends ModelType>(cls: Class<T>, indices: IndexConfig
|
|
721
|
-
return indices.map(idx => this.getCreateIndexSQL(cls, idx));
|
|
721
|
+
getCreateAllIndicesSQL<T extends ModelType>(cls: Class<T>, indices: IndexConfig[]): string[] {
|
|
722
|
+
return indices.map(idx => this.getCreateIndexSQL(cls, idx)).filter((sql): sql is string => !!sql);
|
|
722
723
|
}
|
|
723
724
|
|
|
724
725
|
/**
|
|
725
726
|
* Get index name
|
|
726
727
|
*/
|
|
727
|
-
getIndexName<T extends ModelType>(cls: Class<T>, idx: IndexConfig
|
|
728
|
+
getIndexName<T extends ModelType>(cls: Class<T>, idx: IndexConfig): string {
|
|
728
729
|
const table = this.namespace(SQLModelUtil.classToStack(cls));
|
|
729
730
|
return ['idx', table, idx.name.toLowerCase().replaceAll('-', '_')].join('_');
|
|
730
731
|
}
|
|
@@ -732,26 +733,43 @@ CREATE TABLE IF NOT EXISTS ${this.table(stack)} (
|
|
|
732
733
|
/**
|
|
733
734
|
* Get CREATE INDEX sql
|
|
734
735
|
*/
|
|
735
|
-
getCreateIndexSQL<T extends ModelType>(cls: Class<T>, idx: IndexConfig
|
|
736
|
+
getCreateIndexSQL<T extends ModelType>(cls: Class<T>, idx: IndexConfig): string | undefined {
|
|
737
|
+
const constraint = this.getIndexName(cls, idx);
|
|
736
738
|
const table = this.namespace(SQLModelUtil.classToStack(cls));
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
const
|
|
740
|
-
|
|
741
|
-
|
|
739
|
+
|
|
740
|
+
if (isModelQueryIndex(idx)) {
|
|
741
|
+
const fields: [string, boolean][] = idx.fields.map(field => {
|
|
742
|
+
const key = TypedObject.keys(field)[0];
|
|
743
|
+
const value = field[key];
|
|
744
|
+
if (DataUtil.isPlainObject(value)) {
|
|
745
|
+
throw new IndexNotSupported(cls, idx, 'Only indexed and query indices are supported in SQL');
|
|
746
|
+
}
|
|
747
|
+
return [castTo(key), typeof value === 'number' ? value === 1 : (!!value)];
|
|
748
|
+
});
|
|
749
|
+
return `CREATE ${idx.type === 'query:unique' ? 'UNIQUE ' : ''}INDEX ${constraint} ON ${this.identifier(table)} (${fields
|
|
750
|
+
.map(([name, sel]) => `${this.identifier(name)} ${sel ? 'ASC' : 'DESC'}`)
|
|
751
|
+
.join(', ')});`;
|
|
752
|
+
} else if (isModelIndexedIndex(idx)) {
|
|
753
|
+
if ([...idx.sortTemplate, ...idx.keyTemplate].find(field => field.path.length > 1)) {
|
|
754
|
+
console.debug('Nested fields are not supported in ModelIndexed indices SQL', { index: idx.name });
|
|
755
|
+
return;
|
|
742
756
|
}
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
757
|
+
const fields = [...idx.sortTemplate, ...idx.keyTemplate]
|
|
758
|
+
.map(({ path, value }) => `${this.identifier(path.join('_'))} ${value === -1 ? 'DESC' : 'ASC'}`)
|
|
759
|
+
.join(', ');
|
|
760
|
+
switch (idx.type) {
|
|
761
|
+
case 'indexed:keyed': return `CREATE ${idx.unique ? 'UNIQUE ' : ''}INDEX ${constraint} ON ${this.identifier(table)} (${fields});`;
|
|
762
|
+
case 'indexed:sorted': return `CREATE INDEX ${constraint} ON ${this.identifier(table)} (${fields});`;
|
|
763
|
+
}
|
|
764
|
+
} else {
|
|
765
|
+
throw new IndexNotSupported(cls, idx, 'Only indexed and query indices are supported in SQL');
|
|
766
|
+
}
|
|
749
767
|
}
|
|
750
768
|
|
|
751
769
|
/**
|
|
752
770
|
* Get DROP INDEX sql
|
|
753
771
|
*/
|
|
754
|
-
getDropIndexSQL<T extends ModelType>(cls: Class<T>, idx: IndexConfig
|
|
772
|
+
getDropIndexSQL<T extends ModelType>(cls: Class<T>, idx: IndexConfig | string): string {
|
|
755
773
|
const constraint = typeof idx === 'string' ? idx : this.getIndexName(cls, idx);
|
|
756
774
|
return `DROP INDEX ${this.identifier(constraint)} ;`;
|
|
757
775
|
}
|
|
@@ -1074,18 +1092,25 @@ ${this.getWhereSQL(cls, where!)}`;
|
|
|
1074
1092
|
/**
|
|
1075
1093
|
* Determine if an index has changed
|
|
1076
1094
|
*/
|
|
1077
|
-
isIndexChanged(requested: IndexConfig
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1095
|
+
isIndexChanged(requested: IndexConfig, existing: SQLTableDescription['indices'][number]): boolean {
|
|
1096
|
+
if (isModelQueryIndex(requested)) {
|
|
1097
|
+
let result =
|
|
1098
|
+
(existing.is_unique && requested.type !== 'query:unique')
|
|
1099
|
+
|| requested.fields.length !== existing.columns.length;
|
|
1100
|
+
|
|
1101
|
+
for (let i = 0; i < requested.fields.length && !result; i++) {
|
|
1102
|
+
const [[key, value]] = Object.entries(requested.fields[i]);
|
|
1103
|
+
const desc = value === -1;
|
|
1104
|
+
result ||= key !== existing.columns[i].name && desc !== existing.columns[i].desc;
|
|
1105
|
+
}
|
|
1087
1106
|
|
|
1088
|
-
|
|
1107
|
+
return result;
|
|
1108
|
+
} else if (isModelIndexedIndex(requested)) {
|
|
1109
|
+
// TODO: Fill this out
|
|
1110
|
+
return false;
|
|
1111
|
+
} else {
|
|
1112
|
+
throw new IndexNotSupported(requested.class, requested, 'Only indexed and query indices are supported in SQL');
|
|
1113
|
+
}
|
|
1089
1114
|
}
|
|
1090
1115
|
|
|
1091
1116
|
/**
|
package/src/service.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type ModelType,
|
|
3
|
-
type BulkOperation, type BulkResponse, type ModelCrudSupport, type ModelStorageSupport, type ModelBulkSupport,
|
|
4
|
-
|
|
5
|
-
ModelExpiryUtil, ModelCrudUtil, ModelStorageUtil, ModelBulkUtil,
|
|
3
|
+
type BulkOperation, type BulkResponse, type ModelCrudSupport, type ModelStorageSupport, type ModelBulkSupport, NotFoundError,
|
|
4
|
+
ModelRegistryIndex, ExistsError, type OptionalId, type ModelIdSource, ModelExpiryUtil, ModelCrudUtil, ModelStorageUtil, ModelBulkUtil,
|
|
6
5
|
} from '@travetto/model';
|
|
7
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
type ModelIndexedSupport, type KeyedIndexSelection, type KeyedIndexBody, type ListPageOptions, ModelIndexedUtil,
|
|
8
|
+
type SingleItemIndex, type SortedIndexSelection, type ListPageResult, type SortedIndex, type FullKeyedIndexBody,
|
|
9
|
+
type FullKeyedIndexWithPartialBody, ModelIndexedComputedIndex
|
|
10
|
+
} from '@travetto/model-indexed';
|
|
11
|
+
import { castTo, type Class, JSONUtil } from '@travetto/runtime';
|
|
8
12
|
import { DataUtil } from '@travetto/schema';
|
|
9
13
|
import type { AsyncContext } from '@travetto/context';
|
|
10
14
|
import { Injectable, PostConstruct } from '@travetto/di';
|
|
@@ -33,6 +37,7 @@ export class SQLModelService implements
|
|
|
33
37
|
ModelCrudSupport, ModelStorageSupport,
|
|
34
38
|
ModelBulkSupport, ModelQuerySupport,
|
|
35
39
|
ModelQueryCrudSupport, ModelQueryFacetSupport,
|
|
40
|
+
ModelIndexedSupport,
|
|
36
41
|
ModelQuerySuggestSupport {
|
|
37
42
|
|
|
38
43
|
#manager: TableManager;
|
|
@@ -326,4 +331,114 @@ export class SQLModelService implements
|
|
|
326
331
|
return result;
|
|
327
332
|
});
|
|
328
333
|
}
|
|
334
|
+
|
|
335
|
+
// Indexed support
|
|
336
|
+
@Connected()
|
|
337
|
+
async getByIndex<
|
|
338
|
+
T extends ModelType,
|
|
339
|
+
K extends KeyedIndexSelection<T>,
|
|
340
|
+
S extends SortedIndexSelection<T>
|
|
341
|
+
>(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: FullKeyedIndexBody<T, K, S>): Promise<T> {
|
|
342
|
+
const computed = ModelIndexedComputedIndex.get(idx, body).validate({ sort: true });
|
|
343
|
+
const results = await this.query(cls, castTo({ where: computed.project({ sort: true, includeId: true }) }));
|
|
344
|
+
if (results.length !== 1) {
|
|
345
|
+
throw new NotFoundError(`${cls.name}: ${idx}`, computed.getKey({ sort: true }));
|
|
346
|
+
}
|
|
347
|
+
return results[0];
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
@Connected()
|
|
351
|
+
@Transactional()
|
|
352
|
+
async deleteByIndex<
|
|
353
|
+
T extends ModelType,
|
|
354
|
+
K extends KeyedIndexSelection<T>,
|
|
355
|
+
S extends SortedIndexSelection<T>
|
|
356
|
+
>(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: FullKeyedIndexBody<T, K, S>): Promise<void> {
|
|
357
|
+
const computed = ModelIndexedComputedIndex.get(idx, body).validate({ sort: true });
|
|
358
|
+
const count = await this.deleteByQuery(cls, castTo({ where: computed.project({ sort: true, includeId: true }) }));
|
|
359
|
+
if (count === 0) {
|
|
360
|
+
throw new NotFoundError(`${cls.name}: ${idx}`, computed.getKey({ sort: true }));
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
@Connected()
|
|
365
|
+
@Transactional()
|
|
366
|
+
upsertByIndex<
|
|
367
|
+
T extends ModelType,
|
|
368
|
+
K extends KeyedIndexSelection<T>,
|
|
369
|
+
S extends SortedIndexSelection<T>
|
|
370
|
+
>(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: OptionalId<T>): Promise<T> {
|
|
371
|
+
return ModelIndexedUtil.naiveUpsert(this, cls, idx, body);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
@Connected()
|
|
375
|
+
@Transactional()
|
|
376
|
+
updateByIndex<
|
|
377
|
+
T extends ModelType,
|
|
378
|
+
K extends KeyedIndexSelection<T>,
|
|
379
|
+
S extends SortedIndexSelection<T>
|
|
380
|
+
>(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: T): Promise<T> {
|
|
381
|
+
return ModelIndexedUtil.naiveUpdate(this, cls, idx, body);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
@Connected()
|
|
385
|
+
@Transactional()
|
|
386
|
+
async updatePartialByIndex<
|
|
387
|
+
T extends ModelType,
|
|
388
|
+
K extends KeyedIndexSelection<T>,
|
|
389
|
+
S extends SortedIndexSelection<T>
|
|
390
|
+
>(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: FullKeyedIndexWithPartialBody<T, K, S>): Promise<T> {
|
|
391
|
+
const item = await ModelCrudUtil.naivePartialUpdate(cls, () => this.getByIndex(cls, idx, castTo(body)), castTo(body));
|
|
392
|
+
return this.update(cls, item);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
@Connected()
|
|
396
|
+
async pageByIndex<
|
|
397
|
+
T extends ModelType,
|
|
398
|
+
K extends KeyedIndexSelection<T>,
|
|
399
|
+
S extends SortedIndexSelection<T>
|
|
400
|
+
>(
|
|
401
|
+
cls: Class<T>,
|
|
402
|
+
idx: SortedIndex<T, K, S>,
|
|
403
|
+
body: KeyedIndexBody<T, K>,
|
|
404
|
+
options?: ListPageOptions
|
|
405
|
+
): Promise<ListPageResult<T>> {
|
|
406
|
+
const offset = options?.offset ? JSONUtil.fromBase64<number>(options.offset) : 0;
|
|
407
|
+
const limit = options?.limit ?? 100;
|
|
408
|
+
const computed = ModelIndexedComputedIndex.get(idx, body).validate();
|
|
409
|
+
|
|
410
|
+
const items = await this.query(cls, castTo({
|
|
411
|
+
where: computed.project(),
|
|
412
|
+
sort: idx.sortTemplate.map(part => ({ [part.path.join('.')]: part.value })),
|
|
413
|
+
limit, offset
|
|
414
|
+
}));
|
|
415
|
+
return { items, nextOffset: items.length ? JSONUtil.toBase64(offset + items.length) : undefined };
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
@ConnectedIterator()
|
|
419
|
+
async * listByIndex<
|
|
420
|
+
T extends ModelType,
|
|
421
|
+
K extends KeyedIndexSelection<T>,
|
|
422
|
+
S extends SortedIndexSelection<T>
|
|
423
|
+
>(
|
|
424
|
+
cls: Class<T>,
|
|
425
|
+
idx: SortedIndex<T, K, S>,
|
|
426
|
+
body: KeyedIndexBody<T, K>,
|
|
427
|
+
): AsyncIterable<T> {
|
|
428
|
+
const computed = ModelIndexedComputedIndex.get(idx, body).validate();
|
|
429
|
+
let offset = 0;
|
|
430
|
+
while (offset >= 0) {
|
|
431
|
+
const items = await this.query(cls, castTo({
|
|
432
|
+
where: computed.project(),
|
|
433
|
+
sort: idx.sortTemplate.map(part => ({ [part.path.join('.')]: part.value })),
|
|
434
|
+
limit: 100, offset
|
|
435
|
+
}));
|
|
436
|
+
if (items.length === 0) {
|
|
437
|
+
offset = -1;
|
|
438
|
+
} else {
|
|
439
|
+
offset += items.length;
|
|
440
|
+
yield* items;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
329
444
|
}
|
package/src/table-manager.ts
CHANGED
|
@@ -45,7 +45,7 @@ export class TableManager {
|
|
|
45
45
|
for (const command of this.#dialect.getCreateAllTablesSQL(cls)) {
|
|
46
46
|
out.push(command);
|
|
47
47
|
}
|
|
48
|
-
const indices = ModelRegistryIndex.
|
|
48
|
+
const indices = ModelRegistryIndex.getIndices(cls);
|
|
49
49
|
if (indices) {
|
|
50
50
|
for (const command of this.#dialect.getCreateAllIndicesSQL(cls, indices)) {
|
|
51
51
|
out.push(command);
|
|
@@ -64,7 +64,8 @@ export class TableManager {
|
|
|
64
64
|
const existingFields = new Map(found?.columns.map(column => [column.name, column]) ?? []);
|
|
65
65
|
const existingIndices = new Map(found?.indices.map(index => [index.name, index]) ?? []);
|
|
66
66
|
const model = path.length === 1 ? ModelRegistryIndex.getConfig(type) : undefined;
|
|
67
|
-
const
|
|
67
|
+
const indices = model ? ModelRegistryIndex.getIndices(type) : undefined;
|
|
68
|
+
const requestedIndices = new Map((indices ?? []).map(index => [this.#dialect.getIndexName(type, index), index]) ?? []);
|
|
68
69
|
|
|
69
70
|
// Manage fields
|
|
70
71
|
if (!existingFields.size) {
|
|
@@ -98,10 +99,16 @@ export class TableManager {
|
|
|
98
99
|
// Manage indices
|
|
99
100
|
for (const index of requestedIndices.keys()) {
|
|
100
101
|
if (!existingIndices.has(index)) {
|
|
101
|
-
|
|
102
|
+
const sql = this.#dialect.getCreateIndexSQL(type, requestedIndices.get(index)!);
|
|
103
|
+
if (sql) {
|
|
104
|
+
sqlCommands.createIndex.push(sql);
|
|
105
|
+
}
|
|
102
106
|
} else if (this.#dialect.isIndexChanged(requestedIndices.get(index)!, existingIndices.get(index)!)) {
|
|
103
107
|
sqlCommands.dropIndex.push(this.#dialect.getDropIndexSQL(type, existingIndices.get(index)!.name));
|
|
104
|
-
|
|
108
|
+
const sql = this.#dialect.getCreateIndexSQL(type, requestedIndices.get(index)!);
|
|
109
|
+
if (sql) {
|
|
110
|
+
sqlCommands.createIndex.push(sql);
|
|
111
|
+
}
|
|
105
112
|
}
|
|
106
113
|
}
|
|
107
114
|
|