uql-orm 0.80.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 (56) hide show
  1. package/dist/dialect/vectorSqlDialect.d.ts +2 -0
  2. package/dist/dialect/vectorSqlDialect.js +4 -0
  3. package/dist/migrate/builder/expressions.d.ts +2 -0
  4. package/dist/migrate/builder/expressions.js +24 -0
  5. package/dist/migrate/cli.js +1 -1
  6. package/dist/migrate/codegen/entityCodeGenerator.js +2 -2
  7. package/dist/migrate/codegen/indexDecoratorSource.d.ts +3 -2
  8. package/dist/migrate/codegen/indexDecoratorSource.js +5 -23
  9. package/dist/migrate/ddl/mssqlTableDdl.d.ts +4 -4
  10. package/dist/migrate/ddl/mssqlTableDdl.js +20 -14
  11. package/dist/migrate/ddl/mysqlIndexDdl.d.ts +2 -2
  12. package/dist/migrate/ddl/mysqlIndexDdl.js +7 -6
  13. package/dist/migrate/ddl/pgIndexDdl.d.ts +2 -1
  14. package/dist/migrate/ddl/pgIndexDdl.js +8 -6
  15. package/dist/migrate/ddl/tableDdl.d.ts +3 -2
  16. package/dist/migrate/ddl/tableDdl.js +9 -7
  17. package/dist/migrate/drift/driftDetector.d.ts +4 -5
  18. package/dist/migrate/drift/driftDetector.js +21 -21
  19. package/dist/migrate/generator/definitionToNode.d.ts +1 -1
  20. package/dist/migrate/generator/definitionToNode.js +9 -20
  21. package/dist/migrate/generator/mongoSchemaGenerator.d.ts +1 -1
  22. package/dist/migrate/generator/mongoSchemaGenerator.js +11 -19
  23. package/dist/migrate/index.d.ts +2 -1
  24. package/dist/migrate/index.js +1 -0
  25. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.d.ts +5 -7
  26. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +11 -18
  27. package/dist/migrate/introspection/baseSqlIntrospector.js +7 -18
  28. package/dist/migrate/introspection/mongoIntrospector.d.ts +1 -1
  29. package/dist/migrate/introspection/mongoIntrospector.js +3 -3
  30. package/dist/migrate/introspection/mssqlIntrospector.js +2 -1
  31. package/dist/migrate/introspection/mysqlIntrospector.d.ts +15 -5
  32. package/dist/migrate/introspection/mysqlIntrospector.js +32 -4
  33. package/dist/migrate/introspection/postgresIntrospector.d.ts +29 -21
  34. package/dist/migrate/introspection/postgresIntrospector.js +63 -46
  35. package/dist/migrate/introspection/sqliteIntrospector.js +11 -9
  36. package/dist/migrate/migrator.d.ts +5 -0
  37. package/dist/migrate/migrator.js +33 -44
  38. package/dist/migrate/schemaChange.d.ts +18 -0
  39. package/dist/migrate/schemaChange.js +37 -0
  40. package/dist/migrate/schemaGenerator.d.ts +13 -14
  41. package/dist/migrate/schemaGenerator.js +83 -177
  42. package/dist/schema/indexDifferences.d.ts +22 -6
  43. package/dist/schema/indexDifferences.js +23 -8
  44. package/dist/schema/matchByKey.d.ts +10 -0
  45. package/dist/schema/matchByKey.js +18 -0
  46. package/dist/schema/schemaAST.d.ts +6 -2
  47. package/dist/schema/schemaAST.js +7 -3
  48. package/dist/schema/schemaASTBuilder.d.ts +2 -0
  49. package/dist/schema/schemaASTBuilder.js +15 -10
  50. package/dist/schema/schemaASTDiffer.d.ts +2 -3
  51. package/dist/schema/schemaASTDiffer.js +15 -36
  52. package/dist/schema/types.d.ts +14 -15
  53. package/dist/type/migration.d.ts +32 -50
  54. package/dist/util/ddlExpression.util.d.ts +5 -1
  55. package/dist/util/ddlExpression.util.js +6 -2
  56. package/package.json +1 -1
@@ -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;
@@ -1,37 +1,21 @@
1
1
  import { areTypesEqual, isBreakingTypeChange } from './canonicalType.js';
2
- import { describeIndexDifferences, indexNameStem } from './indexDifferences.js';
2
+ import { describeIndexDifferences, pairIndexes } from './indexDifferences.js';
3
+ import { matchByKey } from './matchByKey.js';
3
4
  import { DEFAULT_FOREIGN_KEY_ACTION } from './types.js';
4
5
  /**
5
6
  * Default diff options.
6
7
  */
7
8
  const DEFAULT_OPTIONS = {
8
9
  compareIndexes: true,
9
- indexFacets: new Set(),
10
10
  compareRelationships: true,
11
11
  normalizeType: (type) => type,
12
- defaultsEqual: (expected, actual) => normalizeDefault(expected) === normalizeDefault(actual),
12
+ defaultsEqual: defaultsEqualAsWritten,
13
13
  ignoreCase: false,
14
14
  excludeTables: [],
15
15
  };
16
16
  function nameNormalizer(opts) {
17
17
  return opts.ignoreCase ? (name) => name.toLowerCase() : (name) => name;
18
18
  }
19
- /**
20
- * The only three ways two keyed collections can differ, which is the shape of every comparison here:
21
- * tables, columns, indexes and relationships all key by name and then split the same way.
22
- */
23
- function matchByKey(source, target, key) {
24
- const sourceByKey = new Map([...source].map((item) => [key(item), item]));
25
- const targetByKey = new Map([...target].map((item) => [key(item), item]));
26
- return {
27
- created: [...sourceByKey].filter(([at]) => !targetByKey.has(at)).map(([, item]) => item),
28
- dropped: [...targetByKey].filter(([at]) => !sourceByKey.has(at)).map(([, item]) => item),
29
- matched: [...sourceByKey].flatMap(([at, item]) => {
30
- const counterpart = targetByKey.get(at);
31
- return counterpart ? [[item, counterpart]] : [];
32
- }),
33
- };
34
- }
35
19
  /** How a relationship diff names the pair it is about, whichever way it differs. */
36
20
  function relationEnds(relation) {
37
21
  return { name: relation.name, fromTable: relation.from.table.name, toTable: relation.to.table.name };
@@ -86,12 +70,12 @@ export function diffTable(source, target, options = {}) {
86
70
  }
87
71
  /** The two keys where they hold different columns, compared in order and never by the name the engine gave them. */
88
72
  function diffPrimaryKey(source, target) {
89
- const expected = source.primaryKey.map((column) => column.name);
90
- const actual = target.primaryKey.map((column) => column.name);
73
+ const expected = source.primaryKey?.columns ?? [];
74
+ const actual = target.primaryKey?.columns ?? [];
91
75
  if (expected.length === actual.length && expected.every((column, i) => column === actual[i])) {
92
76
  return undefined;
93
77
  }
94
- return { table: source.name, expected, actual, actualName: target.primaryKeyName };
78
+ return { table: source.name, expected: source.primaryKey, actual: target.primaryKey };
95
79
  }
96
80
  /**
97
81
  * Compare columns between two tables.
@@ -120,17 +104,14 @@ function diffTableColumns(source, target, opts) {
120
104
  .filter((diff) => diff !== undefined),
121
105
  ];
122
106
  }
123
- /**
124
- * Compare indexes between two tables.
125
- */
107
+ /** Compare indexes between two tables, paired by {@link pairIndexes}, in what the target's reader reports. */
126
108
  function diffTableIndexes(source, target, opts) {
127
- const normalizeName = nameNormalizer(opts);
128
- const { created, dropped, matched } = matchByKey(source.indexes, target.indexes, (index) => normalizeName(indexNameStem(index.name)));
109
+ const { created, dropped, matched } = pairIndexes(source.indexes, target.indexes, nameNormalizer(opts));
129
110
  return [
130
111
  ...created.map((index) => ({ name: index.name, table: source.name, type: 'create', expected: index })),
131
112
  ...dropped.map((index) => ({ name: index.name, table: target.name, type: 'drop', actual: index })),
132
113
  ...matched
133
- .map(([sourceIndex, targetIndex]) => diffIndex(source.name, sourceIndex, targetIndex, opts.indexFacets))
114
+ .map(([sourceIndex, targetIndex]) => diffIndex(source.name, sourceIndex, targetIndex, target.indexFacets))
134
115
  .filter((diff) => diff !== undefined),
135
116
  ];
136
117
  }
@@ -158,12 +139,9 @@ function diffColumn(tableName, source, target, opts) {
158
139
  if (!impliedNotNull && source.nullable !== target.nullable) {
159
140
  differences.push(`nullable: ${target.nullable} -> ${source.nullable}`);
160
141
  }
161
- // Compare unique constraint
162
- if (source.isUnique !== target.isUnique) {
163
- differences.push(`unique: ${target.isUnique} -> ${source.isUnique}`);
164
- }
165
142
  // Not compared, since no statement this generator emits could settle a difference: `isAutoIncrement`,
166
- // `enum` (a check the database reprints), `generatedAs`, and `comment`.
143
+ // `enum` (a check the database reprints), `generatedAs`, and `comment`. Nor `isUnique`: a unique
144
+ // column is a unique index, compared with the indexes.
167
145
  // Compare default values (if both defined)
168
146
  if (!opts.defaultsEqual(source.defaultValue, target.defaultValue)) {
169
147
  differences.push(`default: ${target.defaultValue ?? 'NULL'} -> ${source.defaultValue ?? 'NULL'}`);
@@ -262,9 +240,10 @@ function formatType(type) {
262
240
  result += ' unsigned';
263
241
  return result;
264
242
  }
265
- /**
266
- * Normalize default values for comparison.
267
- */
243
+ /** Two defaults compared as written, where no dialect reprints them: `now()` and `CURRENT_TIMESTAMP` are one. */
244
+ export function defaultsEqualAsWritten(expected, actual) {
245
+ return normalizeDefault(expected) === normalizeDefault(actual);
246
+ }
268
247
  function normalizeDefault(value) {
269
248
  if (value === undefined || value === null)
270
249
  return '';
@@ -1,4 +1,5 @@
1
- import type { IndexSchema } from '../type/migration.js';
1
+ import type { IndexSchema, PrimaryKeySchema } from '../type/migration.js';
2
+ import type { IndexFacet } from './indexDifferences.js';
2
3
  /**
3
4
  * Type categories universal across SQL dialects.
4
5
  * These represent logical/semantic types, not specific SQL types.
@@ -109,18 +110,17 @@ export interface TableNode {
109
110
  readonly schema?: string;
110
111
  /** Map of column name to column node */
111
112
  readonly columns: Map<string, ColumnNode>;
112
- /** Primary key columns, in key order (supports composite keys) */
113
- readonly primaryKey: ColumnNode[];
114
- /**
115
- * What the constraint is called, where a name is known: read back from the database on an
116
- * introspected table, absent on one built from entities, where nothing has named it yet. A `DROP`
117
- * is the only thing that needs it - see {@link TableSchema.primaryKeyName}.
118
- */
119
- primaryKeyName?: string;
113
+ /** The table's key, named where the database reported a name; none on a table without one. */
114
+ primaryKey?: PrimaryKeySchema;
120
115
  /** Indexes on this table */
121
116
  readonly indexes: IndexNode[];
122
- /** `CHECK` constraints on this table. Optional: a node can be built without ever naming one. */
123
- readonly checks?: CheckSchema[];
117
+ /**
118
+ * What the introspector that read this table reports about an index, and so all an index diff against
119
+ * it may compare. None on a table built from entities.
120
+ */
121
+ readonly indexFacets: ReadonlySet<IndexFacet>;
122
+ /** `CHECK` constraints on this table. */
123
+ readonly checks: CheckSchema[];
124
124
  /** Optional table comment */
125
125
  readonly comment?: string;
126
126
  /** Relationships pointing TO this table (other tables referencing this one) */
@@ -214,10 +214,9 @@ export interface TableDiff {
214
214
  */
215
215
  export interface PrimaryKeyDiff {
216
216
  readonly table: string;
217
- readonly expected: string[];
218
- readonly actual: string[];
219
- /** What the *actual* side calls its constraint, which is the only name a `DROP` can use. */
220
- readonly actualName?: string;
217
+ readonly expected?: PrimaryKeySchema;
218
+ /** Named as the database reported it, which is the only name a `DROP` can use. */
219
+ readonly actual?: PrimaryKeySchema;
221
220
  }
222
221
  /**
223
222
  * Difference between two index definitions.
@@ -118,14 +118,7 @@ export interface ColumnSchema extends Omit<ColumnNode, 'type' | 'table' | 'refer
118
118
  export interface TableSchema {
119
119
  readonly name: string;
120
120
  readonly columns: ColumnSchema[];
121
- /** The key's columns **in order**, which is what a composite is: `(a, b)` is not `(b, a)`. */
122
- readonly primaryKey?: string[];
123
- /**
124
- * What the engine calls the key's constraint, where it names one at all - Postgres's `Member_pkey`,
125
- * MySQL's literal `PRIMARY`, nothing on SQLite. Only a `DROP` needs it, and only the name the
126
- * database actually reported will do: a derived one would name a constraint that is not there.
127
- */
128
- readonly primaryKeyName?: string;
121
+ readonly primaryKey?: PrimaryKeySchema;
129
122
  readonly indexes?: IndexSchema[];
130
123
  readonly foreignKeys?: ForeignKeySchema[];
131
124
  }
@@ -176,8 +169,26 @@ export interface ForeignKeySchema {
176
169
  readonly onUpdate?: ForeignKeyAction;
177
170
  }
178
171
  /**
179
- * Represents a difference between current and desired schema
172
+ * One object's change: added (`to` alone), dropped (`from` alone), or altered (both), each whole so the
173
+ * change is undone by swapping its ends. No engine alters an index, a key or a foreign key in place, so
174
+ * an alter of one is its drop and its add, which safe mode holds back together.
180
175
  */
176
+ export interface Change<T> {
177
+ readonly from?: T;
178
+ readonly to?: T;
179
+ }
180
+ /** A primary key, whichever side it is read from: the entities, the database, or a diff between them. */
181
+ export interface PrimaryKeySchema {
182
+ /** Its columns **in order**, which is what a composite is: `(a, b)` is not `(b, a)`. */
183
+ readonly columns: readonly string[];
184
+ /**
185
+ * What the engine calls its constraint, where it names one at all - Postgres's `Member_pkey`, MySQL's
186
+ * literal `PRIMARY`, nothing on SQLite. Only a `DROP` needs it, and only the name the database
187
+ * reported will do: a derived one would name a constraint that is not there.
188
+ */
189
+ readonly name?: string;
190
+ }
191
+ /** A table's differences from what its entity declares, or the table to create or drop. */
181
192
  export interface SchemaDiff {
182
193
  /** Qualified where the table has a schema, since it is also the key the table is found under. */
183
194
  readonly tableName: string;
@@ -187,36 +198,10 @@ export interface SchemaDiff {
187
198
  */
188
199
  readonly schema?: string;
189
200
  readonly type: 'create' | 'alter' | 'drop';
190
- /**
191
- * The table's key against the entity's, where their columns differ; `fromName` is the name the
192
- * database reported, which is what a `DROP` needs.
193
- */
194
- readonly primaryKey?: {
195
- readonly from: string[];
196
- readonly to: string[];
197
- readonly fromName?: string;
198
- };
199
- readonly columnsToAdd?: ColumnSchema[];
200
- readonly columnsToAlter?: {
201
- from: ColumnSchema;
202
- to: ColumnSchema;
203
- }[];
204
- readonly columnsToDrop?: string[];
205
- readonly indexesToAdd?: IndexSchema[];
206
- /** Whole rather than by name, so the rollback can create each again. */
207
- readonly indexesToDrop?: IndexSchema[];
208
- readonly foreignKeysToAdd?: ForeignKeySchema[];
209
- /** Dropped under the name the *database* reported, which is the only name a `DROP` can use. */
210
- readonly foreignKeysToDrop?: string[];
211
- /**
212
- * A constraint whose referential actions changed. Its own field rather than a pair of entries in
213
- * the two above, because no engine alters an action in place: it is a drop and an add that have to
214
- * travel together, and safe mode has to hold back both or neither.
215
- */
216
- readonly foreignKeysToAlter?: {
217
- readonly from: ForeignKeySchema;
218
- readonly to: ForeignKeySchema;
219
- }[];
201
+ readonly primaryKey?: Change<PrimaryKeySchema>;
202
+ readonly columns?: readonly Change<ColumnSchema>[];
203
+ readonly indexes?: readonly Change<IndexSchema>[];
204
+ readonly foreignKeys?: readonly Change<ForeignKeySchema>[];
220
205
  }
221
206
  /**
222
207
  * What every sync entry point takes: `safe` keeps it additive, `drop` lets it remove a column, and
@@ -254,6 +239,8 @@ export type InstalledTriggers = ReadonlyMap<string, readonly string[]>;
254
239
  * Interface for generating DDL statements from entity metadata
255
240
  */
256
241
  export interface SchemaGenerator {
242
+ /** Whether a column's stored default is the one the entity declares, as the engine reprints it. Absent where columns are not compared. */
243
+ readonly defaultsEqual?: (expected: unknown, actual: unknown) => boolean;
257
244
  /** The whole schema for `entities`, tables then the foreign keys between them, which need every entity at once. */
258
245
  generateCreateSchema(entities: readonly Type<object>[], options?: CreateSchemaOptions): string[];
259
246
  /**
@@ -273,14 +260,8 @@ export interface SchemaGenerator {
273
260
  generateTriggersDown(entity: Type<object>, installed?: InstalledTriggers): string[];
274
261
  /** A `DROP` for each trigger uql owns among `names` on `entity`'s table, whatever the entity declares. */
275
262
  generateTriggerDrops(entity: Type<object>, names: readonly string[]): string[];
276
- /**
277
- * Generate ALTER TABLE statements based on schema diff
278
- */
263
+ /** The statements taking a table through `diff`; its rollback is the diff reversed, see `reverseDiff`. */
279
264
  generateAlterTable(diff: SchemaDiff): string[];
280
- /**
281
- * Generate rollback (down) statements for ALTER TABLE based on schema diff
282
- */
283
- generateAlterTableDown(diff: SchemaDiff): string[];
284
265
  /**
285
266
  * Generate CREATE INDEX statement
286
267
  */
@@ -333,11 +314,12 @@ export interface SchemaGenerator {
333
314
  */
334
315
  export interface SchemaIntrospector {
335
316
  /**
336
- * Every trigger uql installed in this schema, by table and then by name, each with the statements that
337
- * recreate it as it stands. The names say which to drop once an entity no longer declares them; the
338
- * statements are what a rollback puts back, read off the engine rather than recorded anywhere by uql.
317
+ * Every trigger uql installed on `table`, by name, each with the statements that recreate it as it
318
+ * stands. The names say which to drop once an entity no longer declares them; the statements are what
319
+ * a rollback puts back, read off the engine rather than recorded anywhere by uql. One table's alone,
320
+ * so reading it never meets a trigger another writer is dropping from some other table.
339
321
  */
340
- ownedTriggers(): Promise<Map<string, InstalledTriggers>>;
322
+ ownedTriggers(table: string): Promise<InstalledTriggers>;
341
323
  /**
342
324
  * What this introspector can read back about an index, and so all that diffing may compare.
343
325
  * Comparing a feature it cannot read reports the same drift forever: the entity side declares it,
@@ -4,7 +4,11 @@ import { type EntityIndexColumn, type EntityIndexMeta, type EntityMeta, type Ind
4
4
  * object reach the schema as one: a column read off the refs as its key, any other `raw` as it is.
5
5
  */
6
6
  export declare function normalizeIndexColumn(entry: IndexColumnInput): EntityIndexColumn;
7
- /** Every index an entity declares: each `@Field({ index })` as the one-column `@Index` it is, then its `@Index`es. */
7
+ /**
8
+ * Every index an entity declares: each `@Field({ index })` or `@Field({ unique })` as the one-column
9
+ * `@Index` it is, then its `@Index`es. A unique column is a unique index, the one spelling of uniqueness
10
+ * every engine can add and drop.
11
+ */
8
12
  export declare function declaredIndexes<E>(meta: EntityMeta<E>): EntityIndexMeta<E>[];
9
13
  /** An index entry as the schema holds it, its expression rendered to text by `render`. */
10
14
  export declare function renderIndexColumn(entry: EntityIndexColumn, render: (sql: QueryRaw) => string): IndexColumnSchema;
@@ -9,9 +9,13 @@ export function normalizeIndexColumn(entry) {
9
9
  const { column, ...modifiers } = typeof entry === 'string' || entry instanceof QueryRaw ? { column: entry } : entry;
10
10
  return { ...modifiers, column: column instanceof ColumnRef ? column.key : column };
11
11
  }
12
- /** Every index an entity declares: each `@Field({ index })` as the one-column `@Index` it is, then its `@Index`es. */
12
+ /**
13
+ * Every index an entity declares: each `@Field({ index })` or `@Field({ unique })` as the one-column
14
+ * `@Index` it is, then its `@Index`es. A unique column is a unique index, the one spelling of uniqueness
15
+ * every engine can add and drop.
16
+ */
13
17
  export function declaredIndexes(meta) {
14
- const fieldIndexes = definedEntries(meta.fields).flatMap(([key, field]) => field.index
18
+ const fieldIndexes = definedEntries(meta.fields).flatMap(([key, field]) => field.index || field.unique
15
19
  ? [
16
20
  {
17
21
  columns: [{ column: key }],
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "homepage": "https://uql-orm.dev",
4
4
  "description": "The 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.80.0",
6
+ "version": "0.81.0",
7
7
  "type": "module",
8
8
  "engines": {
9
9
  "node": ">=24"