uql-orm 0.86.0 → 0.88.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 (37) hide show
  1. package/dist/dialect/abstractSqlDialect.d.ts +1 -1
  2. package/dist/dialect/mysqlLikeSqlDialect.js +1 -3
  3. package/dist/dialect/pgLikeSqlDialect.js +1 -3
  4. package/dist/migrate/ddl/tableDdl.d.ts +5 -0
  5. package/dist/migrate/ddl/tableDdl.js +18 -7
  6. package/dist/migrate/ddl/tableRebuild.d.ts +17 -0
  7. package/dist/migrate/ddl/tableRebuild.js +56 -0
  8. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.d.ts +3 -1
  9. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +7 -1
  10. package/dist/migrate/introspection/baseSqlIntrospector.d.ts +4 -2
  11. package/dist/migrate/introspection/baseSqlIntrospector.js +29 -4
  12. package/dist/migrate/introspection/sqliteIntrospector.d.ts +3 -3
  13. package/dist/migrate/introspection/sqliteIntrospector.js +9 -7
  14. package/dist/migrate/migrationTarget.js +27 -1
  15. package/dist/migrate/migrator.d.ts +20 -3
  16. package/dist/migrate/migrator.js +108 -17
  17. package/dist/migrate/schemaChange.d.ts +12 -1
  18. package/dist/migrate/schemaChange.js +28 -0
  19. package/dist/migrate/schemaGenerator.d.ts +19 -9
  20. package/dist/migrate/schemaGenerator.js +81 -42
  21. package/dist/migrate/triggerSql.js +19 -14
  22. package/dist/mongo/mongoDialect.js +1 -3
  23. package/dist/mssql/mssqlDialect.js +1 -3
  24. package/dist/schema/indexDifferences.d.ts +2 -1
  25. package/dist/schema/indexDifferences.js +2 -1
  26. package/dist/schema/matchByKey.d.ts +9 -0
  27. package/dist/schema/matchByKey.js +18 -0
  28. package/dist/schema/schemaAST.js +1 -0
  29. package/dist/schema/schemaASTDiffer.d.ts +13 -0
  30. package/dist/schema/schemaASTDiffer.js +35 -4
  31. package/dist/schema/types.d.ts +5 -1
  32. package/dist/sqlite/sqliteDialect.d.ts +0 -1
  33. package/dist/sqlite/sqliteDialect.js +1 -4
  34. package/dist/type/dialect.d.ts +4 -10
  35. package/dist/type/migration.d.ts +45 -4
  36. package/package.json +1 -1
  37. package/skills/uql-orm/SKILL.md +1 -1
@@ -53,8 +53,9 @@ function triggerStatements(dialect, meta, trigger, name) {
53
53
  const table = dialect.escapedTableName(meta);
54
54
  const names = rowNames(dialect);
55
55
  const rows = [rowRefs(meta.entity, names.$new), rowRefs(meta.entity, names.$old)];
56
- const sql = dialect.compileDdl(body(...rows), meta.entity, { rows: rowsFrom(dialect, meta, operation) });
57
- const guard = triggerGuard(dialect, meta, trigger, rows, names);
56
+ const source = rowsFrom(dialect, meta, operation, trigger.of ?? []);
57
+ const sql = dialect.compileDdl(body(...rows), meta.entity, { rows: source });
58
+ const guard = triggerGuard(dialect, meta, trigger, rows, names, source);
58
59
  const inBody = features.guards !== 'clause';
59
60
  const guarded = !guard || !inBody
60
61
  ? sql
@@ -115,12 +116,16 @@ function triggerBody(dialect, meta, trigger, before) {
115
116
  }
116
117
  return body;
117
118
  }
118
- /** The one condition both guards reduce to: any watched column that moved, and whatever `where` asks. */
119
- function triggerGuard(dialect, meta, trigger, rows, names) {
119
+ /**
120
+ * The one condition both guards reduce to: any watched column that moved, and whatever `where` asks. On a
121
+ * set-based engine a watched column narrows `source` already, so the guard asks whether it holds a row.
122
+ */
123
+ function triggerGuard(dialect, meta, trigger, rows, names, source) {
120
124
  const moved = movedColumns(dialect, meta, trigger.of ?? [], names);
125
+ const watched = moved && (source ? `EXISTS (SELECT 1 ${source})` : moved);
121
126
  return [
122
- ...(moved ? [moved] : []),
123
- ...(trigger.where ? condition(dialect, meta, trigger.where, rows, names, Boolean(moved)) : []),
127
+ ...(watched ? [watched] : []),
128
+ ...(trigger.where ? condition(dialect, meta, trigger.where, rows, names, Boolean(watched)) : []),
124
129
  ].join(' AND ');
125
130
  }
126
131
  /** The PL/pgSQL function holding a body. `OR REPLACE`, since a dropped table leaves its function behind. */
@@ -210,7 +215,7 @@ function dollarQuote(body) {
210
215
  }
211
216
  /**
212
217
  * Whether any watched column moved, null-safely, or `undefined` where none is watched: the two records
213
- * compared on a row-based engine, and on a set-based one the same question over a join of its two tables.
218
+ * compared on a row-based engine, the two tables' rows on a set-based one.
214
219
  */
215
220
  function movedColumns(dialect, meta, of, { $new: newName, $old: oldName }) {
216
221
  if (!of.length) {
@@ -220,16 +225,15 @@ function movedColumns(dialect, meta, of, { $new: newName, $old: oldName }) {
220
225
  const column = dialect.escapedColumnName(meta, key);
221
226
  return dialect.neExpr(`${oldName}.${column}`, `${newName}.${column}`);
222
227
  });
223
- const moved = differs.length > 1 ? `(${differs.join(' OR ')})` : differs.join('');
224
- const source = rowsFrom(dialect, meta, 'UPDATE');
225
- return source ? `EXISTS (SELECT 1 ${source} WHERE ${moved})` : moved;
228
+ return differs.length > 1 ? `(${differs.join(' OR ')})` : differs.join('');
226
229
  }
227
230
  /**
228
231
  * Where a set-based engine's body reads the rows it fires for, as the `FROM` a statement names them in:
229
- * `inserted` on an insert, `deleted` on a delete, and on an update both, joined on the whole key. None on a
230
- * row-based engine, whose body reads `NEW` and `OLD` bare.
232
+ * `inserted` on an insert, `deleted` on a delete, and on an update both, joined on the whole key and on a
233
+ * watched column having moved, so the body writes for those rows alone, as a per-row engine fires for them.
234
+ * None on a row-based engine, whose body reads `NEW` and `OLD` bare.
231
235
  */
232
- function rowsFrom(dialect, meta, operation) {
236
+ function rowsFrom(dialect, meta, operation, of) {
233
237
  if (dialect.features.triggers.rows !== 'set') {
234
238
  return undefined;
235
239
  }
@@ -241,5 +245,6 @@ function rowsFrom(dialect, meta, operation) {
241
245
  const column = dialect.escapedColumnName(meta, id);
242
246
  return `${$new}.${column} = ${$old}.${column}`;
243
247
  });
244
- return `FROM ${$new} JOIN ${$old} ON ${keyed.join(' AND ')}`;
248
+ const moved = movedColumns(dialect, meta, of, { $new, $old });
249
+ return `FROM ${$new} JOIN ${$old} ON ${[...keyed, ...(moved ? [moved] : [])].join(' AND ')}`;
245
250
  }
@@ -19,9 +19,7 @@ export const mongoDialectFeatures = {
19
19
  indexIfNotExists: false,
20
20
  schemas: false, // the connection picks the database, and a collection name takes no dot
21
21
  dropTableCascade: false,
22
- foreignKeyAlter: false,
23
- primaryKeyAlter: false,
24
- generatedColumnAdd: false,
22
+ rebuildsTables: false,
25
23
  commentSyntax: 'none',
26
24
  vectorIndexRequiresNotNull: false,
27
25
  vectorSupportsLength: false,
@@ -17,9 +17,7 @@ const MSSQL_FEATURES = {
17
17
  indexIfNotExists: true,
18
18
  schemas: true,
19
19
  dropTableCascade: false,
20
- foreignKeyAlter: true,
21
- primaryKeyAlter: true,
22
- generatedColumnAdd: true,
20
+ rebuildsTables: false,
23
21
  // Extended properties are out-of-band metadata with their own procedures, not comments.
24
22
  commentSyntax: 'none',
25
23
  vectorIndexRequiresNotNull: false,
@@ -30,7 +30,7 @@ export declare function pairIndexes<S extends ComparableIndex, T extends Compara
30
30
  /**
31
31
  * The indexes a table lacks, the ones it no longer needs, and the ones to rebuild, differing in what
32
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.
33
+ * dropped: any other may have been made outside the ORM, so it is `kept`.
34
34
  */
35
35
  export declare function indexChanges<I extends IndexSchema>(table: string, declared: readonly I[], current: readonly IndexNode[], facets: ReadonlySet<IndexFacet>): {
36
36
  toAdd: I[];
@@ -39,6 +39,7 @@ export declare function indexChanges<I extends IndexSchema>(table: string, decla
39
39
  from: IndexNode;
40
40
  to: I;
41
41
  }[];
42
+ kept: IndexNode[];
42
43
  };
43
44
  /**
44
45
  * What two indexes differ by, comparing only what both sides state structurally: an expression, a JSON
@@ -38,7 +38,7 @@ export function pairIndexes(source, target, normalizeName = (name) => name) {
38
38
  /**
39
39
  * The indexes a table lacks, the ones it no longer needs, and the ones to rebuild, differing in what
40
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.
41
+ * dropped: any other may have been made outside the ORM, so it is `kept`.
42
42
  */
43
43
  export function indexChanges(table, declared, current, facets) {
44
44
  const { created, dropped, matched } = pairIndexes(declared, current);
@@ -47,6 +47,7 @@ export function indexChanges(table, declared, current, facets) {
47
47
  return {
48
48
  toAdd: created,
49
49
  toDrop: dropped.filter(owned),
50
+ kept: dropped.filter((index) => !owned(index)),
50
51
  toAlter: matched.flatMap(([to, from]) => (describeIndexDifferences(to, from, facets).length ? [{ from, to }] : [])),
51
52
  };
52
53
  }
@@ -8,3 +8,12 @@ export declare function matchByKey<S, T>(source: Iterable<S>, target: Iterable<T
8
8
  dropped: T[];
9
9
  matched: (readonly [S, T])[];
10
10
  };
11
+ /**
12
+ * What {@link matchByKey} left unpaired, paired where `same` finds exactly one counterpart on each side:
13
+ * an item two others could be is ambiguous, so it stays created or dropped.
14
+ */
15
+ export declare function pairUnique<S, T>(created: readonly S[], dropped: readonly T[], same: (source: S, target: T) => boolean): {
16
+ created: S[];
17
+ dropped: T[];
18
+ matched: (readonly [S, T & ({} | null)])[];
19
+ };
@@ -16,3 +16,21 @@ export function matchByKey(source, target, key) {
16
16
  }
17
17
  return { created, dropped: [...unpaired.values()].flat(), matched };
18
18
  }
19
+ /**
20
+ * What {@link matchByKey} left unpaired, paired where `same` finds exactly one counterpart on each side:
21
+ * an item two others could be is ambiguous, so it stays created or dropped.
22
+ */
23
+ export function pairUnique(created, dropped, same) {
24
+ const matched = created.flatMap((source) => {
25
+ const [target, ...others] = dropped.filter((candidate) => same(source, candidate));
26
+ return target !== undefined && !others.length && created.filter((other) => same(other, target)).length === 1
27
+ ? [[source, target]]
28
+ : [];
29
+ });
30
+ const paired = new Set(matched.flat());
31
+ return {
32
+ created: created.filter((item) => !paired.has(item)),
33
+ dropped: dropped.filter((item) => !paired.has(item)),
34
+ matched,
35
+ };
36
+ }
@@ -9,6 +9,7 @@ export function createTableNode(name, schema, indexFacets = new Set()) {
9
9
  columns: new Map(),
10
10
  indexes: [],
11
11
  checks: [],
12
+ externalForeignKeys: [],
12
13
  incomingRelations: [],
13
14
  outgoingRelations: [],
14
15
  };
@@ -1,3 +1,4 @@
1
+ import type { ColumnRenames, Rename } from '../type/migration.js';
1
2
  import type { SchemaAST } from './schemaAST.js';
2
3
  import type { CanonicalType } from './types.js';
3
4
  import type { ColumnDiff, ForeignKeyAction, IndexDiff, RelationshipDiff, RelationshipNode, SchemaDiffResult, TableDiff, TableNode } from './types.js';
@@ -28,6 +29,18 @@ export declare function diffTable(source: TableNode, target: TableNode, options?
28
29
  readonly columnDiffs: ColumnDiff[];
29
30
  readonly indexDiffs: IndexDiff[];
30
31
  }) | undefined;
32
+ /**
33
+ * The columns renamed in the tables both sides name, by qualified table:
34
+ * a new column identical to exactly one the entity no longer names, and to no other. The dropped side is
35
+ * always the entity's own table, so a wrong guess renames, keeping the data, and never drops it.
36
+ */
37
+ export declare function columnRenames(desired: SchemaAST, actual: SchemaAST, options?: DiffOptions): ColumnRenames;
38
+ /**
39
+ * Tables the database holds that a new one is identical to but for its name, each only where it is the one
40
+ * match on both sides. Suggested, never applied: the database's side is a table no entity names, which may
41
+ * be another application's rather than one this schema renamed.
42
+ */
43
+ export declare function tableRenameCandidates(desired: SchemaAST, actual: SchemaAST, options?: DiffOptions): Rename[];
31
44
  /** The differences between two lists of foreign keys, matched by their columns and never by the name the engine gave them. */
32
45
  export declare function diffRelationshipNodes(source: readonly RelationshipNode[], target: readonly RelationshipNode[], opts?: DiffOptions): RelationshipDiff[];
33
46
  /** A relationship's `ON DELETE` and `ON UPDATE`, an unstated one read as the action the database applies. */
@@ -1,6 +1,7 @@
1
+ import { qualifyName } from '../util/sql.util.js';
1
2
  import { areTypesEqual, isBreakingTypeChange } from './canonicalType.js';
2
3
  import { describeIndexDifferences, pairIndexes } from './indexDifferences.js';
3
- import { matchByKey } from './matchByKey.js';
4
+ import { matchByKey, pairUnique } from './matchByKey.js';
4
5
  import { DEFAULT_FOREIGN_KEY_ACTION } from './types.js';
5
6
  /**
6
7
  * Default diff options.
@@ -23,9 +24,7 @@ function relationEnds(relation) {
23
24
  /** The differences between the expected schema (the entities) and the actual one (the database). */
24
25
  export function diffSchemas(source, target, options = {}) {
25
26
  const opts = { ...DEFAULT_OPTIONS, ...options };
26
- const normalizeName = nameNormalizer(opts);
27
- const included = (tables) => [...tables].filter((table) => !opts.excludeTables.includes(table.name));
28
- const { created: tablesToCreate, dropped: tablesToDrop, matched, } = matchByKey(included(source.tables.values()), included(target.tables.values()), (table) => normalizeName(table.name));
27
+ const { created: tablesToCreate, dropped: tablesToDrop, matched } = matchTables(source, target, opts);
29
28
  const tablesToAlter = matched
30
29
  .map(([sourceTable, targetTable]) => diffTable(sourceTable, targetTable, opts))
31
30
  .filter((tableDiff) => tableDiff !== undefined);
@@ -104,6 +103,38 @@ function diffTableColumns(source, target, opts) {
104
103
  .filter((diff) => diff !== undefined),
105
104
  ];
106
105
  }
106
+ /**
107
+ * The columns renamed in the tables both sides name, by qualified table:
108
+ * a new column identical to exactly one the entity no longer names, and to no other. The dropped side is
109
+ * always the entity's own table, so a wrong guess renames, keeping the data, and never drops it.
110
+ */
111
+ export function columnRenames(desired, actual, options = {}) {
112
+ const opts = { ...DEFAULT_OPTIONS, ...options };
113
+ const normalizeName = nameNormalizer(opts);
114
+ return new Map(matchTables(desired, actual, opts).matched.flatMap(([expected, current]) => {
115
+ const { created, dropped } = matchByKey(expected.columns.values(), current.columns.values(), (column) => normalizeName(column.name));
116
+ const { matched } = pairUnique(created, dropped, (to, from) => !diffColumn(expected.name, to, from, opts));
117
+ const table = qualifyName(current.name, current.schema);
118
+ return matched.length ? [[table, matched.map(([to, from]) => ({ from: from.name, to: to.name }))]] : [];
119
+ }));
120
+ }
121
+ /**
122
+ * Tables the database holds that a new one is identical to but for its name, each only where it is the one
123
+ * match on both sides. Suggested, never applied: the database's side is a table no entity names, which may
124
+ * be another application's rather than one this schema renamed.
125
+ */
126
+ export function tableRenameCandidates(desired, actual, options = {}) {
127
+ const opts = { ...DEFAULT_OPTIONS, ...options };
128
+ const { created, dropped } = matchTables(desired, actual, opts);
129
+ const same = (to, from) => to.schema === from.schema && !diffTable(to, from, { ...opts, compareIndexes: false });
130
+ return pairUnique(created, dropped, same).matched.map(([to, from]) => ({ from: from.name, to: to.name }));
131
+ }
132
+ /** The two sides' tables paired by name, those `excludeTables` names left out of both. */
133
+ function matchTables(desired, actual, opts) {
134
+ const normalizeName = nameNormalizer(opts);
135
+ const included = (tables) => [...tables].filter((table) => !opts.excludeTables.includes(table.name));
136
+ return matchByKey(included(desired.tables.values()), included(actual.tables.values()), (table) => normalizeName(table.name));
137
+ }
107
138
  /** Compare indexes between two tables, paired by {@link pairIndexes}, in what the target's reader reports. */
108
139
  function diffTableIndexes(source, target, opts) {
109
140
  const { created, dropped, matched } = pairIndexes(source.indexes, target.indexes, nameNormalizer(opts));
@@ -1,4 +1,4 @@
1
- import type { IndexSchema, PrimaryKeySchema } from '../type/migration.js';
1
+ import type { ForeignKeySchema, IndexSchema, PrimaryKeySchema, StoredDefinition } from '../type/migration.js';
2
2
  import type { IndexFacet } from './indexDifferences.js';
3
3
  /**
4
4
  * Type categories universal across SQL dialects.
@@ -123,6 +123,10 @@ export interface TableNode {
123
123
  readonly checks: CheckSchema[];
124
124
  /** Optional table comment */
125
125
  readonly comment?: string;
126
+ /** The statements the engine keeps for the table, where it keeps them; none on a table built from entities. */
127
+ definition?: readonly StoredDefinition[];
128
+ /** The foreign keys to tables this AST does not hold, which a rebuild keeps as they are. */
129
+ readonly externalForeignKeys: ForeignKeySchema[];
126
130
  /** Relationships pointing TO this table (other tables referencing this one) */
127
131
  incomingRelations: RelationshipNode[];
128
132
  /** Relationships pointing FROM this table (this table referencing others) */
@@ -13,7 +13,6 @@ export declare class SqliteDialect extends AbstractSqlDialect {
13
13
  readonly commitTransactionCommand = "COMMIT";
14
14
  readonly rollbackTransactionCommand = "ROLLBACK";
15
15
  readonly isolationLevelStrategy = "none";
16
- readonly alterColumnSyntax = "none";
17
16
  readonly booleanLiteral = "integer";
18
17
  /** SQLite's own cap on a function call before 3.48, which libSQL and `bun:sqlite`'s build still have. */
19
18
  readonly maxFunctionArgs: number;
@@ -21,9 +21,7 @@ export const SQLITE_FEATURES = {
21
21
  indexIfNotExists: true,
22
22
  schemas: false, // SQLite's namespaces are attached database files, not declared objects
23
23
  dropTableCascade: false,
24
- foreignKeyAlter: false, // SQLite does not support adding FKs to existing tables
25
- primaryKeyAlter: false, // nor changing a key: the only route is rebuilding the table
26
- generatedColumnAdd: false, // accepted in a CREATE TABLE, rejected in an ALTER
24
+ rebuildsTables: true,
27
25
  commentSyntax: 'none',
28
26
  vectorIndexRequiresNotNull: false,
29
27
  vectorSupportsLength: true,
@@ -62,7 +60,6 @@ export class SqliteDialect extends AbstractSqlDialect {
62
60
  commitTransactionCommand = 'COMMIT';
63
61
  rollbackTransactionCommand = 'ROLLBACK';
64
62
  isolationLevelStrategy = 'none';
65
- alterColumnSyntax = 'none';
66
63
  booleanLiteral = 'integer';
67
64
  /** SQLite's own cap on a function call before 3.48, which libSQL and `bun:sqlite`'s build still have. */
68
65
  maxFunctionArgs = 127;
@@ -87,18 +87,12 @@ export interface DialectFeatures {
87
87
  */
88
88
  readonly schemas: boolean;
89
89
  readonly dropTableCascade: boolean;
90
- readonly foreignKeyAlter: boolean;
91
90
  /**
92
- * Whether a table's primary key can be changed on an existing table. False on SQLite, whose only
93
- * route is rebuilding the table - so a migration that would change one is refused by name rather
94
- * than emitting DDL the engine rejects.
91
+ * Whether the engine changes a column, a key or a foreign key, or adds a stored generated column, only
92
+ * by rebuilding the table, as SQLite does: a generated migration copies the table into a new one, and
93
+ * the migration builder refuses the change by name.
95
94
  */
96
- readonly primaryKeyAlter: boolean;
97
- /**
98
- * Whether a stored generated column can be added to an existing table. SQLite takes one only in a
99
- * `CREATE TABLE`, so a sync that would add one is refused by name.
100
- */
101
- readonly generatedColumnAdd: boolean;
95
+ readonly rebuildsTables: boolean;
102
96
  /** Where a comment goes: in the declaration (MySQL family), a `COMMENT ON` of its own (Postgres family), or nowhere. */
103
97
  readonly commentSyntax: 'inline' | 'statement' | 'none';
104
98
  /**
@@ -1,6 +1,7 @@
1
1
  import type { AnyMigrationOperation } from '../migrate/builder/types.js';
2
2
  import type { IndexFacet } from '../schema/indexDifferences.js';
3
3
  import type { SchemaAST } from '../schema/schemaAST.js';
4
+ import type { DiffOptions } from '../schema/schemaASTDiffer.js';
4
5
  import type { ColumnNode, ForeignKeyAction, IndexType, TableNode } from '../schema/types.js';
5
6
  import type { EntityMeta, EntityWhereMeta, FieldOptions, IndexColumnSchema, IndexedVectorField, LoggingOptions, Querier, SqlQuerier, Type, VectorIndexOptions } from './index.js';
6
7
  /**
@@ -121,7 +122,20 @@ export interface TableSchema {
121
122
  readonly primaryKey?: PrimaryKeySchema;
122
123
  readonly indexes?: IndexSchema[];
123
124
  readonly foreignKeys?: ForeignKeySchema[];
125
+ /** The statements the engine keeps for the table, where it keeps them: SQLite's `sqlite_master`. */
126
+ readonly definition?: readonly StoredDefinition[];
124
127
  }
128
+ /** A statement exactly as the engine keeps it, which only it can say all of: a `CHECK`, an index over an expression. */
129
+ export type StoredDefinition = {
130
+ readonly kind: 'table' | 'index' | 'trigger';
131
+ readonly name: string;
132
+ readonly sql: string;
133
+ };
134
+ /** One side of a rebuilt table: its `CREATE TABLE` and what goes back on it, and the columns holding stored values, under this side's names. */
135
+ export type RebuiltTable = {
136
+ readonly statements: readonly string[];
137
+ readonly columns: readonly string[];
138
+ };
125
139
  /**
126
140
  * Represents an index in a database table
127
141
  */
@@ -173,6 +187,17 @@ export interface ForeignKeySchema {
173
187
  * change is undone by swapping its ends. No engine alters an index, a key or a foreign key in place, so
174
188
  * an alter of one is its drop and its add, which safe mode holds back together.
175
189
  */
190
+ /** A column's change, and whether it can lose what the column holds: a drop, or a retype that narrows it. */
191
+ export type ColumnChange = Change<ColumnSchema> & {
192
+ readonly isBreaking?: boolean;
193
+ };
194
+ /** A name changed, `from` the database's `to` the entity's. */
195
+ export type Rename = {
196
+ readonly from: string;
197
+ readonly to: string;
198
+ };
199
+ /** Renamed columns by qualified table name. */
200
+ export type ColumnRenames = ReadonlyMap<string, readonly Rename[]>;
176
201
  export interface Change<T> {
177
202
  readonly from?: T;
178
203
  readonly to?: T;
@@ -199,9 +224,19 @@ export interface SchemaDiff {
199
224
  readonly schema?: string;
200
225
  readonly type: 'create' | 'alter' | 'drop';
201
226
  readonly primaryKey?: Change<PrimaryKeySchema>;
202
- readonly columns?: readonly Change<ColumnSchema>[];
227
+ readonly columns?: readonly ColumnChange[];
203
228
  readonly indexes?: readonly Change<IndexSchema>[];
204
229
  readonly foreignKeys?: readonly Change<ForeignKeySchema>[];
230
+ /** Columns renamed in place, `from` the database's name `to` the entity's, which the other changes already use. */
231
+ readonly renamedColumns?: readonly Rename[];
232
+ /**
233
+ * The table copied into a new one, which is how an engine that {@link DialectFeatures.rebuildsTables}
234
+ * applies the changes above. Its column renames are carried by the copy.
235
+ */
236
+ readonly rebuild?: {
237
+ readonly from: RebuiltTable;
238
+ readonly to: RebuiltTable;
239
+ };
205
240
  }
206
241
  /**
207
242
  * What every sync entry point takes: `safe` keeps it additive, `drop` lets it remove a column, and
@@ -288,10 +323,13 @@ export interface SchemaGenerator {
288
323
  /**
289
324
  * An entity's differences from its table. `desiredAst`, from {@link buildAST}, has to span every entity
290
325
  * a foreign key here points at, or those keys read as matching.
326
+ * `renamedColumns` are columns `currentTable` already holds under their new names.
291
327
  */
292
- diffSchema(entity: Type<object>, currentTable: TableNode | undefined, desiredAst?: SchemaAST): SchemaDiff | undefined;
328
+ diffSchema(entity: Type<object>, currentTable: TableNode | undefined, desiredAst?: SchemaAST, renamedColumns?: readonly Rename[]): SchemaDiff | undefined;
293
329
  /** The entities as one AST, built once per run for every {@link diffSchema}. Absent on MongoDB, which diffs only indexes. */
294
330
  buildAST?(entities: readonly Type<object>[]): SchemaAST;
331
+ /** How this engine's diff compares types and defaults. Absent where {@link buildAST} is. */
332
+ diffOptions?(): DiffOptions;
295
333
  /**
296
334
  * The table's key: {@link resolveTableAlias} behind {@link resolveSchema}, which is how a
297
335
  * `SchemaAST` stores it and how a diff finds it again.
@@ -326,8 +364,11 @@ export interface SchemaIntrospector {
326
364
  * the database side never reports it, and no migration can close the gap.
327
365
  */
328
366
  readonly indexFacets: ReadonlySet<IndexFacet>;
329
- /** The whole database, or just the tables named. Names nothing matches are left out. */
330
- introspect(tables?: readonly string[]): Promise<SchemaAST>;
367
+ /**
368
+ * The whole database, or just the tables named. Names nothing matches are left out. `renames` reads
369
+ * each column under the name it is being renamed to, so a diff compares it as the column it becomes.
370
+ */
371
+ introspect(tables?: readonly string[], renames?: ColumnRenames): Promise<SchemaAST>;
331
372
  /**
332
373
  * Get all table names in the database
333
374
  */
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "homepage": "https://uql-orm.dev",
4
4
  "description": "JSON-native TypeScript ORM for Bun, Browsers, Edge, Deno, Node, Workers. Supports PostgreSQL, PGlite, MySQL, MariaDB, SQLite, CockroachDB, SQL Server, Turso, Neon, Cloudflare D1 and MongoDB. Queries are plain JSON, typed to the leaf.",
5
5
  "license": "MIT",
6
- "version": "0.86.0",
6
+ "version": "0.88.0",
7
7
  "type": "module",
8
8
  "engines": {
9
9
  "node": ">=24"
@@ -154,7 +154,7 @@ transaction. A querier from `pool.getQuerier()` is yours to release: bind it wit
154
154
  ## Migrations
155
155
 
156
156
  `npx uql-migrate` reads `uql.config.ts`. `sync` creates what the entities imply (development only);
157
- `generate:entities` writes the diff as a migration file to review; `up` applies migrations; `generate:from-db`
157
+ `generate:entities` writes the diff as a migration file to review, renaming a column its field was renamed from, printing `renameTable` for a table that may have been, rebuilding a SQLite table for what it cannot alter, and refusing a required column with no default on a table holding rows; `up` applies migrations; `generate:from-db`
158
158
  writes entity classes from an existing database; `drift:check` fails when the database no longer matches.
159
159
  Triggers are part of the diff: uql installs its own under `_uql_`-prefixed names and never touches another.
160
160