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
@@ -1,17 +1,19 @@
1
1
  import { getMeta } from '../entity/index.js';
2
- import { canonicalToSql, engineType, isVectorCategory } from '../schema/canonicalType.js';
2
+ import { canonicalToSql, engineType, isVectorCategory, resolveColumnCanonicalType } from '../schema/canonicalType.js';
3
3
  import { indexChanges } from '../schema/indexDifferences.js';
4
- import { buildSchemaAST, resolveColumnCanonicalType } from '../schema/schemaASTBuilder.js';
4
+ import { buildSchemaAST } from '../schema/schemaASTBuilder.js';
5
5
  import { diffRelationshipNodes, diffTable } from '../schema/schemaASTDiffer.js';
6
6
  import { isAutoIncrement, qualifyName } from '../util/index.js';
7
- import { derivedCheckName, derivedForeignKeyName, derivedPrimaryKeyName } from '../util/sql.util.js';
8
- import { formatDefaultValue, SqlExpression } from './builder/expressions.js';
7
+ import { derivedCheckName, derivedForeignKeyName, derivedPrimaryKeyName, isOwnedName } from '../util/sql.util.js';
8
+ import { sameDefault } from './builder/expressions.js';
9
9
  import { splitSqlStatements } from './builder/splitSqlStatements.js';
10
10
  import { indexDdlFor, tableDdlFor } from './ddl/index.js';
11
11
  import { sizedType } from './ddl/tableDdl.js';
12
12
  import { columnForeignKey, columnIndex, fullColumnDefinitionToNode, renderIndexDefinition, tableDefinitionToNode, } from './generator/definitionToNode.js';
13
13
  import { indexNodeToSchema } from './generator/indexNodeToSchema.js';
14
14
  import { assertIndexPredicate } from './indexPredicate.js';
15
+ import { added, alterations, dropped, nonEmpty, sides } from './schemaChange.js';
16
+ import { dropTrigger, renderTrigger, stampTriggers } from './triggerSql.js';
15
17
  /**
16
18
  * Unified SQL schema generator.
17
19
  * Parameterized by dialect to handle Postgres, MySQL, MariaDB, and SQLite.
@@ -77,6 +79,7 @@ export class SqlSchemaGenerator {
77
79
  return buildEntityAST(this, entities, {
78
80
  defaultForeignKeyAction: this.defaultForeignKeyAction,
79
81
  textScoreIndexes: this.dialect.features.textScoreIndexes,
82
+ vectorIndexRequiresNotNull: this.features.vectorIndexRequiresNotNull,
80
83
  });
81
84
  }
82
85
  /**
@@ -98,8 +101,49 @@ export class SqlSchemaGenerator {
98
101
  statements.push(...this.addForeignKeyStatements(qualifyName(table.name, table.schema), table.outgoingRelations.map(foreignKeyOf)));
99
102
  }
100
103
  }
104
+ // Triggers last: each needs its own table, and a body may read any other the same schema just made.
105
+ const made = new Set(tables.map((table) => qualifyName(table.name, table.schema)));
106
+ statements.push(...entities
107
+ .filter((entity) => made.has(this.resolveTableName(getMeta(entity))))
108
+ .flatMap((entity) => this.generateTriggers(entity)));
101
109
  return statements;
102
110
  }
111
+ /**
112
+ * The installed triggers on `entity`'s table it does not declare, and the declared ones not installed,
113
+ * compared by name alone: a name carries a hash of the trigger's SQL, so an edited one is a new name.
114
+ * Every trigger counts - the ones it authored, and one per event each stamp names.
115
+ */
116
+ triggerChanges(entity, installed) {
117
+ const meta = getMeta(entity);
118
+ const triggers = [...(meta.triggers ?? []), ...stampTriggers(this.dialect, meta)];
119
+ const rendered = triggers.map((trigger, i) => renderTrigger(this.dialect, meta, trigger, i));
120
+ const declared = new Set(rendered.map((trigger) => trigger.name));
121
+ return {
122
+ stale: [...installed]
123
+ .filter(([name]) => isOwnedName(name) && !declared.has(name))
124
+ .map(([name, statements]) => ({ name, statements })),
125
+ missing: rendered.filter((trigger) => !installed.has(trigger.name)),
126
+ };
127
+ }
128
+ generateTriggers(entity, installed = new Map()) {
129
+ const { stale, missing } = this.triggerChanges(entity, installed);
130
+ return this.swapTriggers(entity, stale, missing);
131
+ }
132
+ generateTriggersDown(entity, installed = new Map()) {
133
+ const { stale, missing } = this.triggerChanges(entity, installed);
134
+ return this.swapTriggers(entity, missing, stale);
135
+ }
136
+ /** `dropped` taken off `entity`'s table and `created` put on, which is a reconcile read either way. */
137
+ swapTriggers(entity, dropped, created) {
138
+ return [
139
+ ...this.generateTriggerDrops(entity, dropped.map((trigger) => trigger.name)),
140
+ ...created.flatMap((trigger) => trigger.statements.map((sql) => `${sql};`)),
141
+ ];
142
+ }
143
+ generateTriggerDrops(entity, names) {
144
+ const meta = getMeta(entity);
145
+ return names.filter(isOwnedName).flatMap((name) => dropTrigger(this.dialect, meta, name).map((sql) => `${sql};`));
146
+ }
103
147
  /**
104
148
  * One statement per distinct schema the tables being created live in, in first-seen order. Only
105
149
  * the tables actually being created, so a narrowed `only` does not declare namespaces it is not
@@ -131,97 +175,45 @@ export class SqlSchemaGenerator {
131
175
  const cascade = options.cascade && this.features.dropTableCascade ? ' CASCADE' : '';
132
176
  return `DROP TABLE ${ifExists}${this.escapeId(tableName)}${cascade};`;
133
177
  }
178
+ /**
179
+ * The statements taking a table through `diff`, in the one order both directions need: whatever holds
180
+ * something down goes before it and comes back after it. A foreign key holds its columns and the key it
181
+ * points at, so it goes first and comes back last; the key holds its columns; and some engines drop an
182
+ * index along with its column, which would leave nothing to name. An alter is its drop, then its add.
183
+ */
134
184
  generateAlterTable(diff) {
135
- const statements = [];
136
- // Before the columns, because a key column being added cannot be part of the old key, and after
137
- // it is dropped the table is free to take the new one below.
138
- if (diff.primaryKey?.from.length) {
139
- statements.push(this.generateDropPrimaryKeySql(diff.tableName, diff.primaryKey.fromName));
140
- }
141
- // Before the columns: a constraint holds its columns down, so one the entity dropped cannot go
142
- // while a foreign key still names it. An alter is a drop and an add, and this is its drop half.
143
- statements.push(...this.dropForeignKeyStatements(diff.tableName, [
144
- ...(diff.foreignKeysToDrop ?? []),
145
- ...(diff.foreignKeysToAlter ?? []).map((it) => constraintNameOf(diff.tableName, it.from)),
146
- ]));
147
- // Before the adds, which may reuse a dropped index's name, and before the columns: some engines
148
- // drop an index along with its column, which would leave nothing here to name.
149
- statements.push(...this.dropIndexStatements(diff.tableName, diff.indexesToDrop, diff.schema));
150
- for (const column of diff.columnsToAdd ?? []) {
151
- this.assertColumnAddable(diff.tableName, column);
152
- statements.push(this.tableDdl.addColumn(diff.tableName, this.generateColumnDefinitionFromSchema(column)));
153
- statements.push(...this.generateColumnCommentStatement(diff.tableName, column, diff.schema));
154
- }
155
- statements.push(...this.alterColumnStatements(diff.tableName, (diff.columnsToAlter ?? []).map((it) => it.to)));
156
- for (const columnName of diff.columnsToDrop ?? []) {
157
- statements.push(...this.tableDdl.dropColumn(diff.tableName, columnName));
158
- }
159
- statements.push(...this.addIndexStatements(diff.tableName, diff.indexesToAdd));
160
- // Last, so every column it names exists by now.
161
- if (diff.primaryKey?.to.length) {
162
- statements.push(this.generateAddPrimaryKeySql(diff.tableName, diff.primaryKey.to));
163
- }
164
- // After the columns, for the same reason the key is: a constraint cannot name one that is not
165
- // there yet. The add half of an alter rides along, its drop having gone out above.
166
- statements.push(...this.addForeignKeyStatements(diff.tableName, [
167
- ...(diff.foreignKeysToAdd ?? []),
168
- ...(diff.foreignKeysToAlter ?? []).map((it) => it.to),
169
- ]));
170
- return statements;
185
+ const { tableName, schema, primaryKey } = diff;
186
+ const { columns } = diff;
187
+ return [
188
+ ...sides(diff.foreignKeys, 'from').map((foreignKey) => this.generateDropForeignKeySql(tableName, constraintNameOf(tableName, foreignKey))),
189
+ ...(primaryKey?.from ? [this.generateDropPrimaryKeySql(tableName, primaryKey.from.name)] : []),
190
+ ...sides(diff.indexes, 'from').map((index) => this.generateDropIndex(tableName, index.name, schema)),
191
+ ...added(columns).flatMap((column) => this.addColumnStatements(tableName, column, schema)),
192
+ ...alterations(columns).flatMap(({ from, to }) => this.tableDdl.alterColumn(tableName, to, this.generateColumnDefinitionFromSchema(to), from)),
193
+ ...dropped(columns).flatMap((column) => this.tableDdl.dropColumn(tableName, column.name)),
194
+ ...this.addIndexStatements(tableName, sides(diff.indexes, 'to')),
195
+ ...(primaryKey?.to ? [this.generateAddPrimaryKeySql(tableName, primaryKey.to.columns, primaryKey.to.name)] : []),
196
+ ...this.addForeignKeyStatements(tableName, sides(diff.foreignKeys, 'to')),
197
+ ];
171
198
  }
172
199
  /** `ADD CONSTRAINT` for each of `foreignKeys`. */
173
200
  addForeignKeyStatements(tableName, foreignKeys) {
174
201
  return foreignKeys.map((foreignKey) => this.generateAddForeignKeySql(tableName, foreignKey));
175
202
  }
176
- /** `DROP CONSTRAINT` for each of `constraintNames`, the mirror of {@link addForeignKeyStatements}. */
177
- dropForeignKeyStatements(tableName, constraintNames) {
178
- return constraintNames.map((name) => this.generateDropForeignKeySql(tableName, name));
179
- }
180
- /** The `ALTER COLUMN` restating each of `columns`. */
181
- alterColumnStatements(tableName, columns) {
182
- return columns.flatMap((column) => this.generateAlterColumnStatements(tableName, column, this.generateColumnDefinitionFromSchema(column)));
183
- }
184
203
  /** An index added to a table that may already have rows: its `CREATE`, then what the engine needs after. */
185
- addIndexStatements(tableName, indexes = []) {
204
+ addIndexStatements(tableName, indexes) {
186
205
  return indexes.flatMap((index) => [
187
206
  this.generateCreateIndex(tableName, index),
188
207
  ...this.indexDdl.settleStatements(tableName, index),
189
208
  ]);
190
209
  }
191
- /** `DROP INDEX` for each of `indexes`, the mirror of {@link addIndexStatements}. */
192
- dropIndexStatements(tableName, indexes = [], schema) {
193
- return indexes.map((index) => this.generateDropIndex(tableName, index.name, schema));
194
- }
195
- generateAlterTableDown(diff) {
196
- const statements = [];
197
- // Constraints first, mirroring the up direction: the up added them last, so the down drops them
198
- // first, and a column it is about to drop is then free of anything naming it.
199
- statements.push(...this.dropForeignKeyStatements(diff.tableName, [
200
- ...(diff.foreignKeysToAdd ?? []).map((it) => constraintNameOf(diff.tableName, it)),
201
- ...(diff.foreignKeysToAlter ?? []).map((it) => constraintNameOf(diff.tableName, it.to)),
202
- ]));
203
- // The key next, for the same reason: a column the up added cannot be dropped below while the new
204
- // key still names it. Restored under the name the database gave it, which is what the table had
205
- // before, rather than a derived one that was never on it.
206
- if (diff.primaryKey?.to.length) {
207
- statements.push(this.generateDropPrimaryKeySql(diff.tableName, derivedPrimaryKeyName(diff.tableName, diff.primaryKey.to)));
208
- }
209
- for (const column of diff.columnsToAdd ?? []) {
210
- statements.push(...this.tableDdl.dropColumn(diff.tableName, column.name));
211
- }
212
- statements.push(...this.alterColumnStatements(diff.tableName, (diff.columnsToAlter ?? []).map((it) => it.from)));
213
- statements.push(...this.dropIndexStatements(diff.tableName, diff.indexesToAdd, diff.schema));
214
- statements.push(...this.addIndexStatements(diff.tableName, diff.indexesToDrop));
215
- if (diff.primaryKey?.from.length) {
216
- statements.push(this.generateAddPrimaryKeySql(diff.tableName, diff.primaryKey.from, diff.primaryKey.fromName));
217
- }
218
- // The constraint the up replaced, back under the name the database had for it. A foreign key the
219
- // up *dropped* is not restored: only its name survived the diff, never what it pointed at.
220
- statements.push(...this.addForeignKeyStatements(diff.tableName, (diff.foreignKeysToAlter ?? []).map((it) => it.from)));
221
- if (diff.columnsToDrop?.length || diff.foreignKeysToDrop?.length) {
222
- statements.push(`-- TODO: Manual reversal needed for dropped columns/foreign keys`);
223
- }
224
- return statements;
210
+ /** A column added to a table that exists, and its comment where the engine keeps one apart. */
211
+ addColumnStatements(tableName, column, schema) {
212
+ this.assertColumnAddable(tableName, column);
213
+ return [
214
+ this.tableDdl.addColumn(tableName, this.generateColumnDefinitionFromSchema(column)),
215
+ ...this.generateColumnCommentStatement(tableName, column, schema),
216
+ ];
225
217
  }
226
218
  generateCreateIndex(tableName, index, options = {}) {
227
219
  return this.indexDdl.getCreateIndexStatement(tableName, index, options);
@@ -246,8 +238,8 @@ export class SqlSchemaGenerator {
246
238
  /**
247
239
  * The one place a column definition is spelled, so the `ColumnSchema` and `ColumnNode` paths cannot
248
240
  * drift. A key column states `NOT NULL` rather than leave it to the key: SQLite lets a key column hold
249
- * NULL otherwise, and SQL Server adds no key over a nullable column. `UNIQUE` is left to the key. An
250
- * enum's `CHECK` comes last, the only place MariaDB takes it.
241
+ * NULL otherwise, and SQL Server adds no key over a nullable column. Never `UNIQUE`: a unique column is
242
+ * a unique index, which the table creates beside it. An enum's `CHECK` comes last, the only place MariaDB takes it.
251
243
  */
252
244
  renderColumn(column) {
253
245
  const type = column.generatedAs
@@ -257,9 +249,6 @@ export class SqlSchemaGenerator {
257
249
  if (!column.nullable) {
258
250
  def += ' NOT NULL';
259
251
  }
260
- if (column.isUnique && !column.isPrimaryKey) {
261
- def += ' UNIQUE';
262
- }
263
252
  def += this.tableDdl.defaultClause(column);
264
253
  if (column.comment) {
265
254
  def += this.generateColumnComment(column.comment);
@@ -325,72 +314,61 @@ export class SqlSchemaGenerator {
325
314
  if (!desired) {
326
315
  return undefined;
327
316
  }
328
- // Indexes are matched here rather than by the differ, which pairs them by name so that a changed
329
- // one reads as one index that altered. A migration needs the opposite: an index already in the
330
- // table, under whatever name, must not be created again, and one whose shape differs is dropped
331
- // and created anew - no engine alters an index's columns or uniqueness.
332
317
  const tableDiff = diffTable(desired, currentTable, { ...this.diffOptions(), compareIndexes: false });
333
- const indexes = indexChanges(currentTable.name, desired.indexes, currentTable.indexes);
334
- const indexesToAdd = indexes.toAdd.map(indexNodeToSchema);
335
- const indexesToDrop = indexes.toDrop.map(indexNodeToSchema);
336
- const columnDiffs = tableDiff?.columnDiffs ?? [];
337
- const columnsToAdd = columnDiffs.flatMap((it) => (it.type === 'add' ? [this.columnNodeToSchema(it.expected)] : []));
338
- const columnsToDrop = columnDiffs.flatMap((it) => (it.type === 'drop' ? [it.column] : []));
339
- // Without its values: an alter restates the whole column, and MySQL answers a restated `CHECK` by
340
- // adding a *second* constraint rather than replacing the first, so the column would accumulate one
341
- // per alter. An enum's values reach the database with the column and are never restated - which is
342
- // also why changing them is a hand-written migration. See architecture/roadmap.md.
343
- const columnsToAlter = columnDiffs.flatMap((it) => it.type === 'alter'
344
- ? [
345
- {
346
- from: this.columnNodeToSchema(it.actual),
347
- to: { ...this.columnNodeToSchema(it.expected), enum: undefined },
348
- },
349
- ]
350
- : []);
351
- const primaryKey = tableDiff?.primaryKeyDiff && {
352
- from: tableDiff.primaryKeyDiff.actual,
353
- to: tableDiff.primaryKeyDiff.expected,
354
- fromName: tableDiff.primaryKeyDiff.actualName,
318
+ const indexes = indexChanges(currentTable.name, desired.indexes, currentTable.indexes, currentTable.indexFacets);
319
+ // An alter's `to` without its values: an alter restates the whole column, and MySQL answers a
320
+ // restated `CHECK` by adding a *second* constraint rather than replacing the first, so the column
321
+ // would accumulate one per alter. An enum's values reach the database with the column and are never
322
+ // restated - which is also why changing them is a hand-written migration. See architecture/roadmap.md.
323
+ const columns = (tableDiff?.columnDiffs ?? []).map((it) => {
324
+ if (it.type === 'add') {
325
+ return { to: this.columnNodeToSchema(it.expected) };
326
+ }
327
+ if (it.type === 'drop') {
328
+ return { from: this.columnNodeToSchema(it.actual) };
329
+ }
330
+ return {
331
+ from: this.columnNodeToSchema(it.actual),
332
+ to: { ...this.columnNodeToSchema(it.expected), enum: undefined },
333
+ };
334
+ });
335
+ const keyDiff = tableDiff?.primaryKeyDiff;
336
+ // The key added named as this generator names it, so the rollback can drop it by that name.
337
+ const primaryKey = keyDiff && {
338
+ from: keyDiff.actual,
339
+ to: keyDiff.expected && {
340
+ columns: keyDiff.expected.columns,
341
+ name: derivedPrimaryKeyName(tableName, keyDiff.expected.columns),
342
+ },
355
343
  };
356
344
  // This table's own foreign keys. None where the engine cannot alter one (SQLite, short of rebuilding
357
345
  // the table), since a difference nothing can apply would throw on every sync; `drift:check` names it.
358
346
  const relationDiffs = this.features.foreignKeyAlter
359
347
  ? diffRelationshipNodes(desired.outgoingRelations, currentTable.outgoingRelations, this.diffOptions())
360
348
  : [];
361
- const foreignKeysToAdd = relationDiffs.flatMap((it) => (it.type === 'create' ? [foreignKeyOf(it.expected)] : []));
362
- const foreignKeysToDrop = relationDiffs.flatMap((it) => (it.type === 'drop' ? [it.name] : []));
363
- const foreignKeysToAlter = relationDiffs.flatMap((it) => it.type === 'alter' ? [{ from: foreignKeyOf(it.actual), to: foreignKeyOf(it.expected) }] : []);
364
- if (!columnsToAdd.length &&
365
- !columnsToAlter.length &&
366
- !columnsToDrop.length &&
367
- !indexesToAdd.length &&
368
- !indexesToDrop.length &&
369
- !foreignKeysToAdd.length &&
370
- !foreignKeysToDrop.length &&
371
- !foreignKeysToAlter.length &&
372
- !primaryKey) {
373
- return undefined;
374
- }
375
- return {
349
+ const foreignKeys = relationDiffs.map(({ actual, expected }) => ({
350
+ from: actual && foreignKeyOf(actual),
351
+ to: expected && foreignKeyOf(expected),
352
+ }));
353
+ const alter = {
376
354
  tableName,
377
355
  schema,
378
356
  type: 'alter',
379
357
  primaryKey,
380
- columnsToAdd: columnsToAdd.length ? columnsToAdd : undefined,
381
- columnsToAlter: columnsToAlter.length ? columnsToAlter : undefined,
382
- columnsToDrop: columnsToDrop.length ? columnsToDrop : undefined,
383
- indexesToAdd: indexesToAdd.length ? indexesToAdd : undefined,
384
- indexesToDrop: indexesToDrop.length ? indexesToDrop : undefined,
385
- foreignKeysToAdd: foreignKeysToAdd.length ? foreignKeysToAdd : undefined,
386
- foreignKeysToDrop: foreignKeysToDrop.length ? foreignKeysToDrop : undefined,
387
- foreignKeysToAlter: foreignKeysToAlter.length ? foreignKeysToAlter : undefined,
358
+ columns: nonEmpty(columns),
359
+ indexes: nonEmpty([
360
+ ...indexes.toAdd.map((to) => ({ to: indexNodeToSchema(to) })),
361
+ ...indexes.toDrop.map((from) => ({ from: indexNodeToSchema(from) })),
362
+ ...indexes.toAlter.map(({ from, to }) => ({ from: indexNodeToSchema(from), to: indexNodeToSchema(to) })),
363
+ ]),
364
+ foreignKeys: nonEmpty(foreignKeys),
388
365
  };
366
+ return alter.primaryKey || alter.columns || alter.indexes || alter.foreignKeys ? alter : undefined;
389
367
  }
390
368
  diffOptions() {
391
369
  return {
392
370
  normalizeType: engineType(this.dialect),
393
- defaultsEqual: (expected, actual) => this.isDefaultValueEqual(actual, expected),
371
+ defaultsEqual: this.defaultsEqual,
394
372
  };
395
373
  }
396
374
  /** Spread, not copied field by field, so a field the node gains cannot go missing here. */
@@ -398,58 +376,28 @@ export class SqlSchemaGenerator {
398
376
  const { table: _table, referencedBy: _referencedBy, references: _references, ...column } = col;
399
377
  return { ...column, type: this.columnSqlType(col) };
400
378
  }
401
- /**
402
- * Compare two default values for equality
403
- */
404
- isDefaultValueEqual(current, desired) {
405
- if (current === desired)
406
- return true;
407
- // Both spellings of "no default" are the same fact, and engines disagree on which they report:
408
- // MariaDB says `null` where MySQL says nothing at all. Reading them as different values asked to
409
- // `MODIFY` every nullable column, on every sync, forever.
410
- if (current == null || desired == null)
411
- return current == null && desired == null;
412
- const normalize = (value) => {
413
- // Render first: the desired side may be a symbolic expression, the current side is always the
414
- // engine's own text, and `{"kind":"now"}` matches no spelling of `CURRENT_TIMESTAMP`.
415
- const val = SqlExpression.isExpression(value) ? formatDefaultValue(value, this.dialect) : value;
416
- if (typeof val === 'string') {
417
- let s = val.replace(/::[a-z_]+(\s+[a-z_]+)*(\[\])?$/i, '');
418
- s = s.replace(/^'(.*)'$/, '$1');
419
- if (s.toLowerCase() === 'null')
420
- return 'null';
421
- return s;
422
- }
423
- return typeof val === 'object' ? JSON.stringify(val) : String(val);
424
- };
425
- return normalize(current) === normalize(desired);
426
- }
379
+ /** Whether a column's stored default is the one the entity declares, as this engine reprints it. */
380
+ defaultsEqual = (desired, current) => sameDefault(desired, current, this.dialect);
427
381
  generateCreateTableFromNode(table, options = {}) {
428
382
  const columns = [];
429
383
  const constraints = [];
430
- // MariaDB rejects a `VECTOR INDEX` whose column is nullable ("All parts of a VECTOR index must
431
- // be NOT NULL"), so being indexed decides it rather than the entity's own nullability.
432
- const indexedVectorColumns = new Set(this.features.vectorIndexRequiresNotNull
433
- ? table.indexes.filter((index) => index.type === 'vector').flatMap((idx) => idx.entries.map((e) => e.column))
434
- : []);
435
384
  for (const col of table.columns.values()) {
436
- const colDef = this.generateColumnFromNode(indexedVectorColumns.has(col.name) ? { ...col, nullable: false } : col);
437
- columns.push(colDef);
385
+ columns.push(this.generateColumnFromNode(col));
438
386
  }
439
387
  // Every key, of any width, as one named constraint beside the checks and foreign keys - so a
440
388
  // later `DROP` has something to name. The exception is a dialect whose serial type states the key
441
389
  // itself (SQLite's `INTEGER PRIMARY KEY AUTOINCREMENT`, which cannot be split): there the column
442
390
  // has already declared it, and saying it again is a second primary key.
391
+ const key = table.primaryKey;
443
392
  const declaredByColumn = this.dialect.features.serialDeclaresPrimaryKey &&
444
- table.primaryKey.length === 1 &&
445
- table.primaryKey[0].isAutoIncrement;
446
- if (table.primaryKey.length && !declaredByColumn) {
447
- const pkColumns = table.primaryKey.map((c) => c.name);
448
- const pkName = table.primaryKeyName ?? derivedPrimaryKeyName(table.name, pkColumns);
449
- const pkCols = pkColumns.map((c) => this.escapeId(c)).join(', ');
450
- constraints.push(`CONSTRAINT ${this.escapeId(pkName)} PRIMARY KEY (${pkCols})`);
451
- }
452
- (table.checks ?? []).forEach((check, i) => {
393
+ key?.columns.length === 1 &&
394
+ table.columns.get(key.columns[0])?.isAutoIncrement;
395
+ if (key && !declaredByColumn) {
396
+ const name = key.name ?? derivedPrimaryKeyName(table.name, key.columns);
397
+ const columns = key.columns.map((column) => this.escapeId(column)).join(', ');
398
+ constraints.push(`CONSTRAINT ${this.escapeId(name)} PRIMARY KEY (${columns})`);
399
+ }
400
+ table.checks.forEach((check, i) => {
453
401
  const name = check.name ?? derivedCheckName(table.name, i + 1);
454
402
  constraints.push(`CONSTRAINT ${this.escapeId(name)} CHECK (${check.expression})`);
455
403
  });
@@ -457,8 +405,8 @@ export class SqlSchemaGenerator {
457
405
  const refTable = this.dialect.escapeQualifiedId(rel.to.table.name, rel.to.table.schema);
458
406
  constraints.push(this.foreignKeyConstraint(table.name, foreignKeyOf(rel), refTable));
459
407
  }
460
- const ifNotExists = options.ifNotExists && this.features.ifNotExists ? 'IF NOT EXISTS ' : '';
461
- let createSql = `CREATE TABLE ${ifNotExists}${this.dialect.escapeQualifiedId(table.name, table.schema)} (\n`;
408
+ const target = this.dialect.escapeQualifiedId(table.name, table.schema);
409
+ let createSql = `${this.tableDdl.createTable(target, !!options.ifNotExists)} (\n`;
462
410
  createSql += columns.map((col) => ` ${col}`).join(',\n');
463
411
  if (constraints.length > 0) {
464
412
  createSql += ',\n';
@@ -13,6 +13,10 @@ export declare class DatabaseMigrationStorage implements MigrationStorage {
13
13
  tableName?: string;
14
14
  });
15
15
  ensureStorage(): Promise<void>;
16
+ /**
17
+ * Rendered by the schema generator rather than written out, so each engine gets its own spelling: SQL
18
+ * Server has no `CREATE TABLE IF NOT EXISTS`, and its `TIMESTAMP` is a row version that takes no default.
19
+ */
16
20
  private createTableIfNotExists;
17
21
  executed(): Promise<string[]>;
18
22
  /**
@@ -1,4 +1,7 @@
1
1
  import { withSqlQuerierForMigrations } from '../acquireQuerierForMigrations.js';
2
+ import { expr } from '../builder/expressions.js';
3
+ import { TableBuilder } from '../builder/tableBuilder.js';
4
+ import { SqlSchemaGenerator } from '../schemaGenerator.js';
2
5
  /** Where executed migrations are recorded when the config does not name a table. */
3
6
  export const DEFAULT_MIGRATIONS_TABLE = 'uql_migrations';
4
7
  /**
@@ -22,15 +25,18 @@ export class DatabaseMigrationStorage {
22
25
  this.storageInitialized = true;
23
26
  });
24
27
  }
28
+ /**
29
+ * Rendered by the schema generator rather than written out, so each engine gets its own spelling: SQL
30
+ * Server has no `CREATE TABLE IF NOT EXISTS`, and its `TIMESTAMP` is a row version that takes no default.
31
+ */
25
32
  async createTableIfNotExists(querier) {
26
- const { dialect } = querier;
27
- const sql = /*sql*/ `
28
- CREATE TABLE IF NOT EXISTS ${dialect.escapeId(this.tableName)} (
29
- ${dialect.escapeId('name')} VARCHAR(255) PRIMARY KEY,
30
- ${dialect.escapeId('executed_at')} TIMESTAMP DEFAULT CURRENT_TIMESTAMP
31
- )
32
- `;
33
- await querier.run(sql);
33
+ const table = new TableBuilder(this.tableName);
34
+ table.string('name', { length: 255, primaryKey: true });
35
+ table.timestamp('executed_at', { defaultValue: expr.now() });
36
+ const generator = new SqlSchemaGenerator(querier.dialect);
37
+ for (const sql of generator.generateCreateTableFromDefinition(table.build(), { ifNotExists: true })) {
38
+ await querier.run(sql);
39
+ }
34
40
  }
35
41
  async executed() {
36
42
  await this.ensureStorage();
@@ -0,0 +1,24 @@
1
+ import type { AbstractSqlDialect } from '../dialect/index.js';
2
+ import type { EntityMeta, EntityTriggerMeta } from '../type/index.js';
3
+ /** A trigger as uql installs it: the identifier, and the statements creating it under that identifier. */
4
+ export type RenderedTrigger = {
5
+ readonly name: string;
6
+ readonly statements: readonly string[];
7
+ };
8
+ /**
9
+ * One trigger for `dialect`, named for its table and label and ending in a hash of its own SQL. That hash
10
+ * is the whole of change detection: a trigger is in place exactly when its name is installed, and an
11
+ * edited one is a new name, created while the old one drops as no longer declared.
12
+ */
13
+ export declare function renderTrigger<E>(dialect: AbstractSqlDialect, meta: EntityMeta<E>, trigger: EntityTriggerMeta<E>, position: number): RenderedTrigger;
14
+ /**
15
+ * What removes one trigger: the trigger, named with its table only where the engine scopes the name to
16
+ * one, and on the Postgres family the function holding its body, which dropping a trigger leaves behind.
17
+ */
18
+ export declare function dropTrigger<E>(dialect: AbstractSqlDialect, meta: EntityMeta<E>, name: string): string[];
19
+ /**
20
+ * The triggers a stamp needs: one per event it names, each assigning the field's expression to its
21
+ * column. Generated rather than authored, so unlike an authored body it renders on every engine from
22
+ * one declaration - and not at all on the MySQL family, whose columns stamp themselves.
23
+ */
24
+ export declare function stampTriggers<E>(dialect: AbstractSqlDialect, meta: EntityMeta<E>): EntityTriggerMeta<E>[];