uql-orm 0.79.0 → 0.81.0

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.
Files changed (110) hide show
  1. package/dist/browser/uql-browser.min.js.map +2 -2
  2. package/dist/cockroachdb/cockroachDialect.js +5 -1
  3. package/dist/dialect/abstractDialect.d.ts +1 -31
  4. package/dist/dialect/abstractDialect.js +3 -27
  5. package/dist/dialect/abstractSqlDialect.d.ts +25 -56
  6. package/dist/dialect/abstractSqlDialect.js +78 -145
  7. package/dist/dialect/aliases.d.ts +5 -0
  8. package/dist/dialect/aliases.js +5 -0
  9. package/dist/dialect/mysqlLikeSqlDialect.d.ts +4 -2
  10. package/dist/dialect/mysqlLikeSqlDialect.js +14 -1
  11. package/dist/dialect/operators.d.ts +66 -0
  12. package/dist/dialect/operators.js +129 -0
  13. package/dist/dialect/pgLikeSqlDialect.d.ts +4 -1
  14. package/dist/dialect/pgLikeSqlDialect.js +16 -3
  15. package/dist/dialect/vectorSqlDialect.d.ts +2 -0
  16. package/dist/dialect/vectorSqlDialect.js +4 -0
  17. package/dist/entity/decorator/entity.d.ts +6 -1
  18. package/dist/entity/decorator/entity.js +12 -1
  19. package/dist/entity/index.d.ts +1 -1
  20. package/dist/entity/index.js +1 -1
  21. package/dist/entity/metadata/definition.d.ts +6 -1
  22. package/dist/entity/metadata/definition.js +19 -0
  23. package/dist/migrate/builder/expressions.d.ts +2 -0
  24. package/dist/migrate/builder/expressions.js +24 -0
  25. package/dist/migrate/cli.js +1 -1
  26. package/dist/migrate/codegen/entityCodeGenerator.js +2 -2
  27. package/dist/migrate/codegen/entityTypes.js +1 -2
  28. package/dist/migrate/codegen/indexDecoratorSource.d.ts +3 -2
  29. package/dist/migrate/codegen/indexDecoratorSource.js +5 -23
  30. package/dist/migrate/ddl/mssqlIndexDdl.d.ts +5 -0
  31. package/dist/migrate/ddl/mssqlIndexDdl.js +10 -0
  32. package/dist/migrate/ddl/mssqlTableDdl.d.ts +6 -4
  33. package/dist/migrate/ddl/mssqlTableDdl.js +25 -14
  34. package/dist/migrate/ddl/mysqlIndexDdl.d.ts +2 -2
  35. package/dist/migrate/ddl/mysqlIndexDdl.js +7 -6
  36. package/dist/migrate/ddl/pgIndexDdl.d.ts +2 -1
  37. package/dist/migrate/ddl/pgIndexDdl.js +8 -6
  38. package/dist/migrate/ddl/tableDdl.d.ts +5 -2
  39. package/dist/migrate/ddl/tableDdl.js +13 -7
  40. package/dist/migrate/drift/driftDetector.d.ts +4 -5
  41. package/dist/migrate/drift/driftDetector.js +21 -21
  42. package/dist/migrate/generator/definitionToNode.d.ts +1 -1
  43. package/dist/migrate/generator/definitionToNode.js +9 -20
  44. package/dist/migrate/generator/mongoSchemaGenerator.d.ts +5 -1
  45. package/dist/migrate/generator/mongoSchemaGenerator.js +20 -18
  46. package/dist/migrate/index.d.ts +2 -1
  47. package/dist/migrate/index.js +1 -0
  48. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.d.ts +16 -6
  49. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +18 -4
  50. package/dist/migrate/introspection/baseSqlIntrospector.js +7 -18
  51. package/dist/migrate/introspection/mongoIntrospector.d.ts +3 -1
  52. package/dist/migrate/introspection/mongoIntrospector.js +7 -3
  53. package/dist/migrate/introspection/mssqlIntrospector.d.ts +1 -0
  54. package/dist/migrate/introspection/mssqlIntrospector.js +9 -0
  55. package/dist/migrate/introspection/mysqlIntrospector.d.ts +16 -5
  56. package/dist/migrate/introspection/mysqlIntrospector.js +39 -2
  57. package/dist/migrate/introspection/postgresIntrospector.d.ts +30 -21
  58. package/dist/migrate/introspection/postgresIntrospector.js +70 -43
  59. package/dist/migrate/introspection/sqliteIntrospector.d.ts +1 -0
  60. package/dist/migrate/introspection/sqliteIntrospector.js +13 -8
  61. package/dist/migrate/migrator.d.ts +33 -1
  62. package/dist/migrate/migrator.js +111 -43
  63. package/dist/migrate/schemaChange.d.ts +18 -0
  64. package/dist/migrate/schemaChange.js +37 -0
  65. package/dist/migrate/schemaGenerator.d.ts +25 -15
  66. package/dist/migrate/schemaGenerator.js +130 -182
  67. package/dist/migrate/storage/databaseStorage.d.ts +4 -0
  68. package/dist/migrate/storage/databaseStorage.js +14 -8
  69. package/dist/migrate/triggerSql.d.ts +24 -0
  70. package/dist/migrate/triggerSql.js +229 -0
  71. package/dist/mongo/mongoDialect.d.ts +0 -21
  72. package/dist/mongo/mongoDialect.js +105 -100
  73. package/dist/mongo/mongodbQuerier.js +17 -1
  74. package/dist/mssql/mssqlDialect.d.ts +18 -7
  75. package/dist/mssql/mssqlDialect.js +77 -33
  76. package/dist/mssql/mssqlQuerier.js +2 -2
  77. package/dist/schema/canonicalType.d.ts +6 -1
  78. package/dist/schema/canonicalType.js +14 -0
  79. package/dist/schema/indexDifferences.d.ts +22 -6
  80. package/dist/schema/indexDifferences.js +23 -8
  81. package/dist/schema/matchByKey.d.ts +10 -0
  82. package/dist/schema/matchByKey.js +18 -0
  83. package/dist/schema/schemaAST.d.ts +6 -2
  84. package/dist/schema/schemaAST.js +7 -3
  85. package/dist/schema/schemaASTBuilder.d.ts +4 -8
  86. package/dist/schema/schemaASTBuilder.js +20 -29
  87. package/dist/schema/schemaASTDiffer.d.ts +2 -3
  88. package/dist/schema/schemaASTDiffer.js +15 -36
  89. package/dist/schema/types.d.ts +14 -15
  90. package/dist/sqlite/sqliteDialect.d.ts +1 -1
  91. package/dist/sqlite/sqliteDialect.js +12 -3
  92. package/dist/type/dialect.d.ts +69 -9
  93. package/dist/type/entity.d.ts +96 -3
  94. package/dist/type/migration.d.ts +43 -44
  95. package/dist/type/query.d.ts +13 -4
  96. package/dist/type/queryWhere.d.ts +4 -2
  97. package/dist/util/ddlExpression.util.d.ts +5 -1
  98. package/dist/util/ddlExpression.util.js +6 -2
  99. package/dist/util/field.util.d.ts +9 -1
  100. package/dist/util/field.util.js +14 -2
  101. package/dist/util/fieldOption.util.d.ts +2 -2
  102. package/dist/util/fieldOption.util.js +2 -2
  103. package/dist/util/raw.d.ts +9 -1
  104. package/dist/util/raw.js +36 -11
  105. package/dist/util/sql.util.d.ts +12 -0
  106. package/dist/util/sql.util.js +24 -3
  107. package/dist/util/uqlError.d.ts +2 -0
  108. package/dist/util/uqlError.js +4 -0
  109. package/package.json +4 -4
  110. package/skills/uql-orm/SKILL.md +11 -6
@@ -4,7 +4,7 @@ import { BYTES_PREFIX } from '../dialect/hydrateColumn.js';
4
4
  import { jsonArraySlotArgs, jsonPath, jsonSlotArgs } from '../dialect/jsonSql.js';
5
5
  import { MergeSqlDialect } from '../dialect/mergeSqlDialect.js';
6
6
  import { getMeta } from '../entity/index.js';
7
- import { fieldOptionsToCanonical } from '../schema/canonicalType.js';
7
+ import { canonicalToSql, fieldOptionsToCanonical, resolveColumnCanonicalType } from '../schema/canonicalType.js';
8
8
  import { QueryRaw } from '../type/index.js';
9
9
  import { parseQueryLock } from '../type/index.js';
10
10
  import { isAutoIncrement } from '../util/field.util.js';
@@ -13,10 +13,8 @@ import { escapeSingleQuotes } from '../util/sqlLiteral.js';
13
13
  import { UqlUsageError } from '../util/uqlError.js';
14
14
  /** What SQL Server has. */
15
15
  const MSSQL_FEATURES = {
16
- // Neither object takes an `IF NOT EXISTS`; both need a `sys` catalogue lookup around them, which
17
- // the generator does not emit.
18
- ifNotExists: false,
19
- indexIfNotExists: false,
16
+ // No `IF NOT EXISTS` on an index, so `MsSqlIndexDdl` guards the create with a `sys.indexes` lookup.
17
+ indexIfNotExists: true,
20
18
  schemas: true,
21
19
  dropTableCascade: false,
22
20
  foreignKeyAlter: true,
@@ -40,6 +38,16 @@ const MSSQL_FEATURES = {
40
38
  narrowVectorTypes: false,
41
39
  vectorTuningNeedsTransaction: false,
42
40
  serialDeclaresPrimaryKey: false,
41
+ triggers: {
42
+ preamble: 'SET NOCOUNT ON;',
43
+ assignsRow: false,
44
+ body: 'inline',
45
+ guards: 'beginEnd',
46
+ layout: 'tableFirst',
47
+ rows: 'set',
48
+ scope: 'schema',
49
+ before: false,
50
+ },
43
51
  };
44
52
  /** The `type` `OPENJSON` reports for the JSON scalar an element is compared with; anything else binds as a string. */
45
53
  function openJsonType(value) {
@@ -48,6 +56,14 @@ function openJsonType(value) {
48
56
  }
49
57
  return typeof value === 'boolean' ? 3 : 1;
50
58
  }
59
+ /** The table variable an `OUTPUT` fills, `_uql`-prefixed as every name uql invents is. */
60
+ const OUTPUT_TABLE = '@_uql_output';
61
+ /** The one key column and its field, or nothing on a composite key. */
62
+ function soleId(meta) {
63
+ const [key] = meta.ids;
64
+ const field = meta.ids.length === 1 ? meta.fields[key] : undefined;
65
+ return field && { key, field };
66
+ }
51
67
  /** Microsoft SQL Server 2017 and up. Identifiers are `"`-quoted, the ANSI spelling `tedious` enables. */
52
68
  export class MsSqlDialect extends MergeSqlDialect {
53
69
  features = MSSQL_FEATURES;
@@ -74,6 +90,8 @@ export class MsSqlDialect extends MergeSqlDialect {
74
90
  maxBindValues = 2100;
75
91
  /** `OUTPUT` has no trailing form: it sits between the column list and `VALUES`. */
76
92
  returningPosition = 'after-target';
93
+ /** `OUTPUT` reads the written row off the `INSERTED` pseudo-table. */
94
+ returnedRowPrefix = 'INSERTED.';
77
95
  insertIdSource = 'returning';
78
96
  /** Holds the update key lock across the insert; without it two concurrent upserts of one key race. */
79
97
  mergeTargetHint = ' WITH (HOLDLOCK)';
@@ -96,30 +114,20 @@ export class MsSqlDialect extends MergeSqlDialect {
96
114
  }
97
115
  super.pager(ctx, opts, sorted);
98
116
  }
99
- /**
100
- * `SET IDENTITY_INSERT` around an insert that states a key the engine would generate, which it otherwise
101
- * refuses; turned off in the same batch, since one table per session may hold it.
102
- */
103
117
  insert(ctx, entity, payload, opts) {
104
- const table = this.identityInsertTarget(entity, payload);
105
- if (table) {
106
- ctx.append(`SET IDENTITY_INSERT ${table} ON; `);
107
- }
108
- super.insert(ctx, entity, payload, opts);
109
- if (table) {
110
- ctx.append(`; SET IDENTITY_INSERT ${table} OFF`);
111
- }
118
+ this.writeRows(ctx, getMeta(entity), payload, () => {
119
+ super.insert(ctx, entity, payload, opts);
120
+ ctx.append(';');
121
+ });
112
122
  }
113
123
  /** The table to toggle, or nothing when no record writes a key the engine would have generated. */
114
- identityInsertTarget(entity, payload) {
115
- const meta = getMeta(entity);
116
- const [idKey] = meta.ids;
117
- const field = meta.ids.length === 1 ? meta.fields[idKey] : undefined;
118
- if (!field || !isAutoIncrement(field, true)) {
124
+ identityInsertTarget(meta, payload) {
125
+ const id = soleId(meta);
126
+ if (!id || !isAutoIncrement(id.field, true)) {
119
127
  return undefined;
120
128
  }
121
129
  const records = Array.isArray(payload) ? payload : [payload];
122
- const stated = records.some((record) => record[idKey] !== undefined);
130
+ const stated = records.some((record) => record[id.key] !== undefined);
123
131
  return stated ? this.escapedTableName(meta) : undefined;
124
132
  }
125
133
  /** A `DECIMAL` declared `String`, converted before it crosses the wire, where `tedious` would round it. */
@@ -151,17 +159,44 @@ export class MsSqlDialect extends MergeSqlDialect {
151
159
  placeholder(index) {
152
160
  return `@p${index}`;
153
161
  }
154
- /** `OUTPUT` reads the written row out of the `INSERTED` pseudo-table rather than `RETURNING` it. */
162
+ /**
163
+ * `OUTPUT` reads the written row out of the `INSERTED` pseudo-table rather than `RETURNING` it, and
164
+ * always `INTO` a table variable: SQL Server refuses an `OUTPUT` straight back to the client on a table
165
+ * carrying a trigger, and through a table it works whether one is there or not.
166
+ */
155
167
  returningId(meta) {
156
168
  const expression = this.returningIdExpression(meta);
157
- return expression ? `OUTPUT ${expression}` : '';
169
+ return expression ? `OUTPUT ${expression} INTO ${OUTPUT_TABLE}` : '';
170
+ }
171
+ upsert(ctx, entity, conflictPaths, payload, extraReturning = '') {
172
+ this.writeRows(ctx, getMeta(entity), payload, () => super.upsert(ctx, entity, conflictPaths, payload, extraReturning));
158
173
  }
159
- returningIdExpression(meta) {
160
- const [idKey] = meta.ids;
161
- return meta.ids.length === 1 ? `INSERTED.${this.escapeId(this.columnOf(meta, idKey))} ${this.escapeId('id')}` : '';
174
+ /**
175
+ * A statement writing rows, `write`, wrapped in what SQL Server needs around one. The table variable its
176
+ * `OUTPUT` fills, typed as the id column is, and the read handing that back, uncounted so the batch
177
+ * reports the rows written alone; none on a composite key, which has no id to hand back. And `SET
178
+ * IDENTITY_INSERT` where a row states a key the engine would generate, turned off in the same batch.
179
+ */
180
+ writeRows(ctx, meta, payload, write) {
181
+ const id = soleId(meta);
182
+ const column = this.escapeId('id');
183
+ const identity = this.identityInsertTarget(meta, payload);
184
+ if (id) {
185
+ ctx.append(`DECLARE ${OUTPUT_TABLE} TABLE (${column} ${canonicalToSql(resolveColumnCanonicalType(id.field), this)}); `);
186
+ }
187
+ if (identity) {
188
+ ctx.append(`SET IDENTITY_INSERT ${identity} ON; `);
189
+ }
190
+ write();
191
+ if (identity) {
192
+ ctx.append(` SET IDENTITY_INSERT ${identity} OFF;`);
193
+ }
194
+ if (id) {
195
+ ctx.append(` SET NOCOUNT ON; SELECT ${column} FROM ${OUTPUT_TABLE}; SET NOCOUNT OFF;`);
196
+ }
162
197
  }
163
198
  mergeReturning(expression) {
164
- return `OUTPUT ${expression}`;
199
+ return `OUTPUT ${expression} INTO ${OUTPUT_TABLE}`;
165
200
  }
166
201
  /** A row lock is a hint on the table here, which `rowLocks.placement` says instead of a trailing clause. */
167
202
  lockHint(q) {
@@ -217,9 +252,7 @@ export class MsSqlDialect extends MergeSqlDialect {
217
252
  }
218
253
  /** There is no `CREATE SCHEMA IF NOT EXISTS`, and `CREATE SCHEMA` has to be alone in its batch. */
219
254
  createSchemaSql(schema) {
220
- const literal = escapeSingleQuotes(schema);
221
- const quoted = escapeSingleQuotes(this.escapeId(schema, true));
222
- return `IF SCHEMA_ID(N'${literal}') IS NULL EXEC(N'CREATE SCHEMA ${quoted}')`;
255
+ return `IF SCHEMA_ID(${this.escape(schema)}) IS NULL EXEC(${this.escape(`CREATE SCHEMA ${this.escapeId(schema, true)}`)})`;
223
256
  }
224
257
  /** The estimate the engine already keeps per partition, live without a stats refresh. */
225
258
  estimatedCount(ctx, entity) {
@@ -229,8 +262,19 @@ export class MsSqlDialect extends MergeSqlDialect {
229
262
  ` JOIN sys.schemas s ON s.schema_id = o.schema_id` +
230
263
  ` WHERE p.index_id IN (0, 1) AND o.name = `);
231
264
  ctx.addValue(this.resolveTableAlias(meta));
265
+ // The connection's own default schema where the entity names none, as the introspector reads it.
266
+ const schema = this.resolveSchema(meta);
232
267
  ctx.append(' AND s.name = ');
233
- ctx.addValue(this.resolveSchema(meta) ?? 'dbo');
268
+ if (schema === undefined) {
269
+ ctx.append('SCHEMA_NAME()');
270
+ }
271
+ else {
272
+ ctx.addValue(schema);
273
+ }
274
+ }
275
+ /** Null-safe on every version, `EXCEPT` comparing NULLs as equal: `IS DISTINCT FROM` needs 2022. */
276
+ neExpr(field, ph) {
277
+ return `EXISTS (SELECT ${field} EXCEPT SELECT ${ph})`;
234
278
  }
235
279
  numericCast(expr) {
236
280
  return `TRY_CAST(${expr} AS FLOAT)`;
@@ -25,8 +25,8 @@ export class MsSqlQuerier extends AbstractPoolQuerier {
25
25
  async internalRun(query, values) {
26
26
  const res = await this.#request(values).query(query);
27
27
  return this.buildUpdateResult({
28
- // `rowsAffected` carries one entry per statement, and a `MERGE` upsert emits its `OUTPUT`
29
- // alongside the write, so the counts are summed rather than read at [0].
28
+ // `rowsAffected` carries one entry per statement, and a write runs in a batch of several - the table
29
+ // variable its ids go through, the read handing them back uncounted - so the counts are summed.
30
30
  changes: res.rowsAffected.reduce((total, count) => total + count, 0),
31
31
  rows: decodeWireTypes(res.recordset, res.recordset?.columns),
32
32
  });
@@ -1,6 +1,6 @@
1
1
  import type { AbstractDialect } from '../dialect/abstractDialect.js';
2
2
  import type { VectorCast } from '../dialect/vectorCast.js';
3
- import type { ColumnType, FieldOptions } from '../type/entity.js';
3
+ import type { ColumnType, EntityGetter, FieldMeta, FieldOptions } from '../type/entity.js';
4
4
  import type { CanonicalType, TypeCategory } from './types.js';
5
5
  /** Whether a category is one of the vector types, narrowing it to the cast pgvector names use. */
6
6
  export declare function isVectorCategory(category: TypeCategory | undefined): category is VectorCast;
@@ -51,3 +51,8 @@ export declare function isBreakingTypeChange(from: CanonicalType, to: CanonicalT
51
51
  * Get the UQL ColumnType that best matches a canonical type.
52
52
  */
53
53
  export declare function canonicalToColumnType(type: CanonicalType): ColumnType;
54
+ /**
55
+ * A field's canonical type, taken from the referenced key where the field gave `references` and no
56
+ * `type` (`typeFromReference`), so a foreign key matches the key it points at; `columnType` always wins.
57
+ */
58
+ export declare function resolveColumnCanonicalType(field: FieldMeta, seen?: Set<EntityGetter>): CanonicalType;
@@ -1,4 +1,5 @@
1
1
  // Canonical types, between an engine's SQL types and TypeScript's.
2
+ import { fieldOf, getMeta, soleIdOf } from '../entity/metadata/definition.js';
2
3
  import { QueryRaw } from '../type/index.js';
3
4
  import { columnFamily, isIntegerColumn } from '../util/field.util.js';
4
5
  import { constantSql } from '../util/raw.js';
@@ -456,3 +457,16 @@ export function canonicalToColumnType(type) {
456
457
  return 'sparsevec';
457
458
  }
458
459
  }
460
+ /**
461
+ * A field's canonical type, taken from the referenced key where the field gave `references` and no
462
+ * `type` (`typeFromReference`), so a foreign key matches the key it points at; `columnType` always wins.
463
+ */
464
+ export function resolveColumnCanonicalType(field, seen = new Set()) {
465
+ const hasExplicitType = !!field.columnType || !field.typeFromReference;
466
+ if (!hasExplicitType && field.references && !seen.has(field.references)) {
467
+ seen.add(field.references);
468
+ const referencedMeta = getMeta(field.references());
469
+ return resolveColumnCanonicalType(fieldOf(referencedMeta, soleIdOf(referencedMeta, 'a foreign key')), seen);
470
+ }
471
+ return fieldOptionsToCanonical(field);
472
+ }
@@ -1,10 +1,12 @@
1
+ import type { IndexSchema } from '../type/index.js';
1
2
  import type { IndexNode } from './types.js';
2
3
  /**
3
4
  * What an introspector reports about an index, and so all a diff may compare; apart from `IndexFeature`, what an engine emits.
4
- * `vector` is whether it is a vector index at all, for an engine with one vector index whatever type declared it.
5
+ * `vector` is whether it is a vector index at all, for an engine with one vector index whatever type declared it;
6
+ * `distance` is the metric a vector index was built for.
5
7
  * `textIndex` is a text index's weights and language, kept by an engine that lists its fields in no declared order.
6
8
  */
7
- export type IndexFacet = 'order' | 'nulls' | 'opsClass' | 'accessMethod' | 'include' | 'vector' | 'textIndex';
9
+ export type IndexFacet = 'order' | 'nulls' | 'opsClass' | 'accessMethod' | 'include' | 'vector' | 'distance' | 'textIndex';
8
10
  type ComparableIndex = Pick<IndexNode, 'name' | 'entries' | 'unique'>;
9
11
  /**
10
12
  * Whether the table has this index already, by shape rather than name, uniqueness included. An index
@@ -17,16 +19,30 @@ export declare function indexSignature(index: ComparableIndex): string;
17
19
  */
18
20
  export declare function indexNameStem(name: string): string;
19
21
  /**
20
- * The indexes a table lacks and the ones it no longer needs, matched by `keyOf`. Only an index uql
21
- * named, or whose name the entity claims, is dropped: any other may have been made outside the ORM.
22
+ * Pairs by name, then what is left by shape: an index the database has under another name is still
23
+ * the one asked for. What stays unpaired is created or dropped.
22
24
  */
23
- export declare function indexChanges<I extends ComparableIndex>(table: string, declared: readonly I[], current: readonly IndexNode[], keyOf?: (index: ComparableIndex) => string): {
25
+ export declare function pairIndexes<S extends ComparableIndex, T extends ComparableIndex>(source: readonly S[], target: readonly T[], normalizeName?: (name: string) => string): {
26
+ created: S[];
27
+ dropped: T[];
28
+ matched: (readonly [S, T])[];
29
+ };
30
+ /**
31
+ * The indexes a table lacks, the ones it no longer needs, and the ones to rebuild, differing in what
32
+ * `facets` let the engine report. Only an unpaired index uql named, or whose name the entity claims, is
33
+ * dropped: any other may have been made outside the ORM.
34
+ */
35
+ export declare function indexChanges<I extends IndexSchema>(table: string, declared: readonly I[], current: readonly IndexNode[], facets: ReadonlySet<IndexFacet>): {
24
36
  toAdd: I[];
25
37
  toDrop: IndexNode[];
38
+ toAlter: {
39
+ from: IndexNode;
40
+ to: I;
41
+ }[];
26
42
  };
27
43
  /**
28
44
  * What two indexes differ by, comparing only what both sides state structurally: an expression, a JSON
29
45
  * path or a predicate is reprinted by the engine, so never compared.
30
46
  */
31
- export declare function describeIndexDifferences(source: IndexNode, target: IndexNode, facets: ReadonlySet<IndexFacet>): string[];
47
+ export declare function describeIndexDifferences(source: IndexSchema, target: IndexSchema, facets: ReadonlySet<IndexFacet>): string[];
32
48
  export {};
@@ -1,6 +1,7 @@
1
- import { isVectorIndexType } from '../type/vector.js';
1
+ import { indexDistance, isVectorIndexType } from '../type/vector.js';
2
2
  import { fulltextConfig } from '../util/dialect.util.js';
3
3
  import { derivedIndexName } from '../util/sql.util.js';
4
+ import { matchByKey } from './matchByKey.js';
4
5
  /** An entry the engine reprints in its own words, so never compared as written. */
5
6
  function isReprinted(entry) {
6
7
  return Boolean(entry.expression || entry.jsonPath || entry.jsonArray);
@@ -26,17 +27,27 @@ export function indexNameStem(name) {
26
27
  return bare.replace(/__/g, '_');
27
28
  }
28
29
  /**
29
- * The indexes a table lacks and the ones it no longer needs, matched by `keyOf`. Only an index uql
30
- * named, or whose name the entity claims, is dropped: any other may have been made outside the ORM.
30
+ * Pairs by name, then what is left by shape: an index the database has under another name is still
31
+ * the one asked for. What stays unpaired is created or dropped.
31
32
  */
32
- export function indexChanges(table, declared, current, keyOf = indexSignature) {
33
- const present = new Set(current.map(keyOf));
34
- const wanted = new Set(declared.map(keyOf));
33
+ export function pairIndexes(source, target, normalizeName = (name) => name) {
34
+ const byName = matchByKey(source, target, (index) => normalizeName(indexNameStem(index.name)));
35
+ const byShape = matchByKey(byName.created, byName.dropped, indexSignature);
36
+ return { created: byShape.created, dropped: byShape.dropped, matched: [...byName.matched, ...byShape.matched] };
37
+ }
38
+ /**
39
+ * The indexes a table lacks, the ones it no longer needs, and the ones to rebuild, differing in what
40
+ * `facets` let the engine report. Only an unpaired index uql named, or whose name the entity claims, is
41
+ * dropped: any other may have been made outside the ORM.
42
+ */
43
+ export function indexChanges(table, declared, current, facets) {
44
+ const { created, dropped, matched } = pairIndexes(declared, current);
35
45
  const claimed = new Set(declared.map((index) => index.name));
36
46
  const owned = (index) => claimed.has(index.name) || hasDerivedName(table, index);
37
47
  return {
38
- toAdd: declared.filter((index) => !present.has(keyOf(index))),
39
- toDrop: current.filter((index) => !wanted.has(keyOf(index)) && owned(index)),
48
+ toAdd: created,
49
+ toDrop: dropped.filter(owned),
50
+ toAlter: matched.flatMap(([to, from]) => (describeIndexDifferences(to, from, facets).length ? [{ from, to }] : [])),
40
51
  };
41
52
  }
42
53
  /**
@@ -85,6 +96,10 @@ export function describeIndexDifferences(source, target, facets) {
85
96
  if (facets.has('accessMethod') && (source.type ?? 'btree') !== (target.type ?? 'btree')) {
86
97
  differences.push(`type: ${target.type ?? 'btree'} -> ${source.type ?? 'btree'}`);
87
98
  }
99
+ const bothVector = isVectorIndexType(source.type) && isVectorIndexType(target.type);
100
+ if (facets.has('distance') && bothVector && indexDistance(source) !== indexDistance(target)) {
101
+ differences.push(`distance: ${indexDistance(target)} -> ${indexDistance(source)}`);
102
+ }
88
103
  if (facets.has('vector') && isVectorIndexType(source.type) !== isVectorIndexType(target.type)) {
89
104
  const [expected, actual] = [source, target].map((index) => (isVectorIndexType(index.type) ? 'yes' : 'no'));
90
105
  differences.push(`vector index: ${actual} -> ${expected}`);
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The only three ways two keyed collections can differ, which is the shape of every comparison in
3
+ * the schema diff: tables, columns, indexes and relationships all key and then split the same way.
4
+ * One counterpart each, so two items sharing a key (a duplicate index) are not collapsed into one.
5
+ */
6
+ export declare function matchByKey<S, T>(source: Iterable<S>, target: Iterable<T>, key: (item: S | T) => string): {
7
+ created: S[];
8
+ dropped: T[];
9
+ matched: (readonly [S, T])[];
10
+ };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The only three ways two keyed collections can differ, which is the shape of every comparison in
3
+ * the schema diff: tables, columns, indexes and relationships all key and then split the same way.
4
+ * One counterpart each, so two items sharing a key (a duplicate index) are not collapsed into one.
5
+ */
6
+ export function matchByKey(source, target, key) {
7
+ const unpaired = Map.groupBy(target, key);
8
+ const created = [];
9
+ const matched = [];
10
+ for (const item of source) {
11
+ const counterpart = unpaired.get(key(item))?.shift();
12
+ if (counterpart === undefined)
13
+ created.push(item);
14
+ else
15
+ matched.push([item, counterpart]);
16
+ }
17
+ return { created, dropped: [...unpaired.values()].flat(), matched };
18
+ }
@@ -1,6 +1,10 @@
1
- import type { IndexNode, RelationshipNode, TableNode } from './types.js';
1
+ import type { PrimaryKeySchema } from '../type/migration.js';
2
+ import type { IndexFacet } from './indexDifferences.js';
3
+ import type { ColumnNode, IndexNode, RelationshipNode, TableNode } from './types.js';
2
4
  /** A table node with its collections empty, ready to be filled. */
3
- export declare function createTableNode(name: string, schema?: string, comment?: string): TableNode;
5
+ export declare function createTableNode(name: string, schema?: string, indexFacets?: ReadonlySet<IndexFacet>): TableNode;
6
+ /** The key the columns' own flags say, in their order, for a source that reports no key of its own. */
7
+ export declare function keyOfColumns(columns: Iterable<Pick<ColumnNode, 'name' | 'isPrimaryKey'>>): PrimaryKeySchema | undefined;
4
8
  /** A database schema as a graph: tables, the foreign keys between them, and their indexes. */
5
9
  export declare class SchemaAST {
6
10
  readonly tables: Map<string, TableNode>;
@@ -1,19 +1,23 @@
1
1
  import { qualifyName } from '../util/sql.util.js';
2
2
  import { createOrder, dropOrder } from './dependencyGraph.js';
3
3
  /** A table node with its collections empty, ready to be filled. */
4
- export function createTableNode(name, schema, comment) {
4
+ export function createTableNode(name, schema, indexFacets = new Set()) {
5
5
  return {
6
6
  name,
7
7
  schema,
8
- comment,
8
+ indexFacets,
9
9
  columns: new Map(),
10
- primaryKey: [],
11
10
  indexes: [],
12
11
  checks: [],
13
12
  incomingRelations: [],
14
13
  outgoingRelations: [],
15
14
  };
16
15
  }
16
+ /** The key the columns' own flags say, in their order, for a source that reports no key of its own. */
17
+ export function keyOfColumns(columns) {
18
+ const keyColumns = [...columns].filter((column) => column.isPrimaryKey).map((column) => column.name);
19
+ return keyColumns.length ? { columns: keyColumns } : undefined;
20
+ }
17
21
  /** A database schema as a graph: tables, the foreign keys between them, and their indexes. */
18
22
  export class SchemaAST {
19
23
  tables = new Map();
@@ -1,8 +1,7 @@
1
- import type { EntityGetter } from '../type/entity.js';
2
- import type { EntityMeta, EntityWhereMeta, FieldMeta, FieldOptions, Type } from '../type/index.js';
1
+ import type { EntityMeta, EntityWhereMeta, FieldOptions, Type } from '../type/index.js';
3
2
  import type { NamingStrategy } from '../type/namingStrategy.js';
4
3
  import { SchemaAST } from './schemaAST.js';
5
- import { type CanonicalType, type ForeignKeyAction } from './types.js';
4
+ import { type ForeignKeyAction } from './types.js';
6
5
  /**
7
6
  * Options for building SchemaAST from entities.
8
7
  */
@@ -26,6 +25,8 @@ export interface BuildSchemaASTOptions {
26
25
  compileIndexPredicate?: (where: EntityWhereMeta<object>, entity: Type<object>, indexName: string) => string;
27
26
  /** Whether a weighted fulltext index declares one of its own for each heavier column, as MySQL scores through one. */
28
27
  textScoreIndexes?: boolean;
28
+ /** Whether a column a vector index covers is `NOT NULL` whatever the entity declares, as MariaDB demands. */
29
+ vectorIndexRequiresNotNull?: boolean;
29
30
  }
30
31
  /**
31
32
  * Build a SchemaAST from entity classes (decorated with `@Entity`, `@Field`, etc.).
@@ -34,8 +35,3 @@ export interface BuildSchemaASTOptions {
34
35
  * resolves against a table another entity declares, and an index against the columns of its own.
35
36
  */
36
37
  export declare function buildSchemaAST(entities: readonly Type<object>[], options?: BuildSchemaASTOptions): SchemaAST;
37
- /**
38
- * A field's canonical type, taken from the referenced key where the field gave `references` and no
39
- * `type` (`typeFromReference`), so a foreign key matches the key it points at; `columnType` always wins.
40
- */
41
- export declare function resolveColumnCanonicalType(field: FieldMeta, seen?: Set<EntityGetter>): CanonicalType;
@@ -1,14 +1,12 @@
1
- import { fieldOf, foreignKeysOf, getMeta, soleIdOf } from '../entity/metadata/definition.js';
1
+ import { fieldOf, foreignKeysOf, getMeta } from '../entity/metadata/definition.js';
2
2
  import { declaredIndexes, declaredIndexName, renderIndexColumn } from '../util/ddlExpression.util.js';
3
3
  import { fulltextWeights, textWeightSteps } from '../util/dialect.util.js';
4
- import { isInlinedExpression } from '../util/field.util.js';
5
- import { isSoleIdField } from '../util/field.util.js';
6
- import { isAutoIncrement } from '../util/field.util.js';
4
+ import { isAutoIncrement, isInlinedExpression, isSoleIdField } from '../util/field.util.js';
7
5
  import { definedEntries } from '../util/object.util.js';
8
6
  import { derivedForeignKeyName, derivedIndexName, qualifyName } from '../util/sql.util.js';
9
- import { fieldOptionsToCanonical } from './canonicalType.js';
10
- import { createTableNode, SchemaAST } from './schemaAST.js';
11
- import { DEFAULT_FOREIGN_KEY_ACTION, } from './types.js';
7
+ import { resolveColumnCanonicalType } from './canonicalType.js';
8
+ import { createTableNode, keyOfColumns, SchemaAST } from './schemaAST.js';
9
+ import { DEFAULT_FOREIGN_KEY_ACTION } from './types.js';
12
10
  /**
13
11
  * Build a SchemaAST from entity classes (decorated with `@Entity`, `@Field`, etc.).
14
12
  *
@@ -28,6 +26,7 @@ export function buildSchemaAST(entities, options = {}) {
28
26
  compileDdl,
29
27
  compileIndexPredicate: options.compileIndexPredicate ?? compileDdl,
30
28
  textScoreIndexes: options.textScoreIndexes ?? false,
29
+ vectorIndexRequiresNotNull: options.vectorIndexRequiresNotNull ?? false,
31
30
  };
32
31
  for (const pass of [addTableFromEntity, addRelationshipsFromEntity, addIndexesFromEntity]) {
33
32
  for (const entity of entities) {
@@ -40,18 +39,11 @@ export function buildSchemaAST(entities, options = {}) {
40
39
  function refuseDdl() {
41
40
  throw new TypeError('building the schema of an entity that declares SQL (a check, a stored computed column, an index expression or predicate) needs a dialect to render it: pass `compileDdl`, as `buildEntityAST` does');
42
41
  }
43
- /**
44
- * A field's canonical type, taken from the referenced key where the field gave `references` and no
45
- * `type` (`typeFromReference`), so a foreign key matches the key it points at; `columnType` always wins.
46
- */
47
- export function resolveColumnCanonicalType(field, seen = new Set()) {
48
- const hasExplicitType = !!field.columnType || !field.typeFromReference;
49
- if (!hasExplicitType && field.references && !seen.has(field.references)) {
50
- seen.add(field.references);
51
- const referencedMeta = getMeta(field.references());
52
- return resolveColumnCanonicalType(fieldOf(referencedMeta, soleIdOf(referencedMeta, 'a foreign key')), seen);
53
- }
54
- return fieldOptionsToCanonical(field);
42
+ /** The entries a vector index of `meta` covers: the members it names, and any expression. */
43
+ function vectorIndexedEntries(meta) {
44
+ return new Set((meta.indexes ?? [])
45
+ .filter((index) => index.type === 'vector')
46
+ .flatMap((index) => index.columns.map((entry) => entry.column)));
55
47
  }
56
48
  /**
57
49
  * Add a table from entity metadata.
@@ -59,8 +51,9 @@ export function resolveColumnCanonicalType(field, seen = new Set()) {
59
51
  function addTableFromEntity(ctx, meta) {
60
52
  const tableName = ctx.resolveTableName(meta);
61
53
  const table = createTableNode(tableName, ctx.resolveSchema(meta));
62
- const { columns, primaryKey } = table;
63
- table.checks?.push(...(meta.checks ?? []).map(({ name, where }) => ({ name, expression: ctx.compileDdl(where, meta.entity) })));
54
+ const { columns } = table;
55
+ table.checks.push(...(meta.checks ?? []).map(({ name, where }) => ({ name, expression: ctx.compileDdl(where, meta.entity) })));
56
+ const notNull = ctx.vectorIndexRequiresNotNull ? vectorIndexedEntries(meta) : new Set();
64
57
  // Add columns from fields
65
58
  for (const [key, field] of definedEntries(meta.fields)) {
66
59
  // An inlined expression has no column; a stored one is a column like any other.
@@ -75,12 +68,13 @@ function addTableFromEntity(ctx, meta) {
75
68
  type,
76
69
  // A primary key is NOT NULL in every engine, whatever the entity's property says: `id?: number`
77
70
  // is optional because the database assigns it, not because the column accepts a null.
78
- nullable: isPrimaryKey ? false : (field.nullable ?? true),
71
+ nullable: isPrimaryKey || notNull.has(key) ? false : (field.nullable ?? true),
79
72
  defaultValue: field.defaultValue,
80
73
  isPrimaryKey,
81
74
  isAutoIncrement: isAutoIncrement(field, isSoleKey),
82
75
  isUnique: field.unique ?? false,
83
- generatedAs: field.computed && ctx.compileDdl(field.computed, meta.entity),
76
+ // A stamp is filled by a trigger, so it is a column like any other; only `stored: true` generates.
77
+ generatedAs: field.stored === true && field.computed ? ctx.compileDdl(field.computed, meta.entity) : undefined,
84
78
  comment: field.comment,
85
79
  enum: field.enum,
86
80
  table,
@@ -88,10 +82,8 @@ function addTableFromEntity(ctx, meta) {
88
82
  references: undefined,
89
83
  };
90
84
  columns.set(columnName, column);
91
- if (field.isId) {
92
- primaryKey.push(column);
93
- }
94
85
  }
86
+ table.primaryKey = keyOfColumns(columns.values());
95
87
  ctx.ast.addTable(table);
96
88
  }
97
89
  /** The node an entity maps to, found under the key {@link SchemaAST} stores it by. */
@@ -171,11 +163,10 @@ function addForeignKeyIndexes(ctx, meta, table) {
171
163
  });
172
164
  }
173
165
  }
174
- /** Whether the key, a unique column or an index already leads with `columns`, which is all a lookup needs. */
166
+ /** Whether the key or an index already leads with `columns`, which is all a lookup needs. */
175
167
  function isIndexedBy(table, columns) {
176
168
  const leads = (indexed) => columns.every((column, at) => indexed[at] === column);
177
- return (leads(table.primaryKey.map((column) => column.name)) ||
178
- (columns.length === 1 && table.columns.get(columns[0])?.isUnique === true) ||
169
+ return (leads(table.primaryKey?.columns ?? []) ||
179
170
  table.indexes.some((index) => leads(index.entries.map((entry) => entry.expression || entry.jsonPath || entry.jsonArray ? undefined : entry.column))));
180
171
  }
181
172
  /** An `include` column is named like any other, so a naming strategy has to reach it too. */
@@ -1,4 +1,3 @@
1
- import { type IndexFacet } from './indexDifferences.js';
2
1
  import type { SchemaAST } from './schemaAST.js';
3
2
  import type { CanonicalType } from './types.js';
4
3
  import type { ColumnDiff, ForeignKeyAction, IndexDiff, RelationshipDiff, RelationshipNode, SchemaDiffResult, TableDiff, TableNode } from './types.js';
@@ -8,8 +7,6 @@ import type { ColumnDiff, ForeignKeyAction, IndexDiff, RelationshipDiff, Relatio
8
7
  export interface DiffOptions {
9
8
  /** Compare indexes */
10
9
  compareIndexes?: boolean;
11
- /** What the target side can report about an index, normally an introspector's `indexFacets`. Anything left out is not compared. */
12
- indexFacets?: ReadonlySet<IndexFacet>;
13
10
  /** Compare foreign keys/relationships */
14
11
  compareRelationships?: boolean;
15
12
  /** Ignore case differences in names */
@@ -38,3 +35,5 @@ export declare function referentialActions(rel: RelationshipNode): {
38
35
  readonly onDelete: ForeignKeyAction;
39
36
  readonly onUpdate: ForeignKeyAction;
40
37
  };
38
+ /** Two defaults compared as written, where no dialect reprints them: `now()` and `CURRENT_TIMESTAMP` are one. */
39
+ export declare function defaultsEqualAsWritten(expected: unknown, actual: unknown): boolean;