uql-orm 0.42.0 → 0.42.1

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 (47) hide show
  1. package/dist/browser/querier/httpQuerier.d.ts +3 -3
  2. package/dist/browser/type/clientQuerier.d.ts +3 -3
  3. package/dist/browser/uql-browser.min.js +2 -2
  4. package/dist/browser/uql-browser.min.js.map +4 -4
  5. package/dist/dialect/abstractSqlDialect.d.ts +31 -2
  6. package/dist/dialect/abstractSqlDialect.js +41 -10
  7. package/dist/dialect/mysqlLikeSqlDialect.d.ts +2 -1
  8. package/dist/dialect/mysqlLikeSqlDialect.js +3 -1
  9. package/dist/dialect/pgLikeSqlDialect.d.ts +1 -1
  10. package/dist/dialect/pgLikeSqlDialect.js +2 -1
  11. package/dist/entity/decorator/entity.d.ts +1 -1
  12. package/dist/entity/decorator/entity.js +1 -1
  13. package/dist/maria/mariaDialect.js +2 -1
  14. package/dist/migrate/builder/tableBuilder.js +5 -4
  15. package/dist/migrate/drift/driftDetector.js +16 -0
  16. package/dist/migrate/generator/mongoSchemaGenerator.d.ts +7 -0
  17. package/dist/migrate/generator/mongoSchemaGenerator.js +24 -28
  18. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.d.ts +9 -1
  19. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +11 -2
  20. package/dist/migrate/introspection/baseSqlIntrospector.js +6 -3
  21. package/dist/migrate/introspection/postgresIntrospector.js +1 -1
  22. package/dist/migrate/introspection/sqliteIntrospector.js +7 -3
  23. package/dist/migrate/migrator.js +6 -0
  24. package/dist/migrate/schemaGenerator.d.ts +43 -33
  25. package/dist/migrate/schemaGenerator.js +156 -139
  26. package/dist/mongo/mongoDialect.js +22 -8
  27. package/dist/postgres/postgresDialect.js +1 -1
  28. package/dist/querier/abstractQuerier.js +18 -7
  29. package/dist/querier/relationCount.js +9 -7
  30. package/dist/schema/indexDifferences.d.ts +28 -0
  31. package/dist/schema/indexDifferences.js +46 -0
  32. package/dist/schema/schemaASTBuilder.js +26 -32
  33. package/dist/schema/schemaASTDiffer.d.ts +27 -1
  34. package/dist/schema/schemaASTDiffer.js +54 -18
  35. package/dist/schema/types.d.ts +46 -7
  36. package/dist/sqlite/sqliteDialect.d.ts +2 -1
  37. package/dist/sqlite/sqliteDialect.js +4 -1
  38. package/dist/type/dialect.d.ts +6 -0
  39. package/dist/type/migration.d.ts +19 -0
  40. package/dist/util/field.util.d.ts +11 -1
  41. package/dist/util/field.util.js +12 -0
  42. package/dist/util/object.util.js +11 -3
  43. package/dist/util/relationQuery.util.d.ts +10 -0
  44. package/dist/util/relationQuery.util.js +22 -2
  45. package/dist/util/sql.util.d.ts +24 -7
  46. package/dist/util/sql.util.js +75 -10
  47. package/package.json +1 -1
@@ -9,6 +9,34 @@ import type { IndexNode } from './types.js';
9
9
  * back, MySQL emits one it cannot describe afterwards.
10
10
  */
11
11
  export type IndexFacet = 'order' | 'nulls' | 'opsClass' | 'accessMethod' | 'include';
12
+ /**
13
+ * Whether the table already has this index, for the additive sync that only ever *creates* one.
14
+ *
15
+ * Its shape, never its name: the table's indexes were named by whoever created them, so an index
16
+ * that is already there must not be created a second time under a name we happen to prefer. A
17
+ * derived name is no handle at all - the convention can change, an engine silently truncates one
18
+ * past its identifier limit, and SQLite reports names it made up.
19
+ *
20
+ * Uniqueness counts, because a unique index and a plain one over the same columns enforce different
21
+ * things and no engine can alter one into the other. An index over an expression or a JSON path has
22
+ * no comparable columns - engines reprint SQL text from their parse tree, the same reason
23
+ * {@link describeIndexDifferences} leaves those entries alone - so it falls back to its name.
24
+ */
25
+ export declare function indexSignature(index: Pick<IndexNode, 'name' | 'entries' | 'unique'>): string;
26
+ /**
27
+ * A constraint name without its kind marker.
28
+ *
29
+ * What pairs two sides of a *report*: an index whose uniqueness or columns changed is one index that
30
+ * differs, not one dropped and another created, and only a handle independent of its shape can say
31
+ * so. Stripping the marker is what lets `idx_User_email`, named before the convention moved it to
32
+ * the end, recognise the `User__email_idx` derived for it now, so upgrading reports no drift.
33
+ *
34
+ * Exactly one marker, and the trailing one first. Stripping both ends would eat a leading marker
35
+ * that belongs to the *table* - an index over `pk_registry` is not a primary key - leaving it unable
36
+ * to pair with its own older name. The separator is levelled last, since only one convention doubles
37
+ * it.
38
+ */
39
+ export declare function indexNameStem(name: string): string;
12
40
  /**
13
41
  * Everything an index differs by, named, or nothing when the two match.
14
42
  *
@@ -1,3 +1,49 @@
1
+ /**
2
+ * Whether the table already has this index, for the additive sync that only ever *creates* one.
3
+ *
4
+ * Its shape, never its name: the table's indexes were named by whoever created them, so an index
5
+ * that is already there must not be created a second time under a name we happen to prefer. A
6
+ * derived name is no handle at all - the convention can change, an engine silently truncates one
7
+ * past its identifier limit, and SQLite reports names it made up.
8
+ *
9
+ * Uniqueness counts, because a unique index and a plain one over the same columns enforce different
10
+ * things and no engine can alter one into the other. An index over an expression or a JSON path has
11
+ * no comparable columns - engines reprint SQL text from their parse tree, the same reason
12
+ * {@link describeIndexDifferences} leaves those entries alone - so it falls back to its name.
13
+ */
14
+ export function indexSignature(index) {
15
+ const comparable = !index.entries.some((entry) => entry.expression || entry.jsonPath || entry.jsonArray);
16
+ const identity = comparable
17
+ ? index.entries.map((entry) => entry.column).join(',')
18
+ : `name:${indexNameStem(index.name)}`;
19
+ return `${index.unique ? 'unique' : 'plain'}(${identity})`;
20
+ }
21
+ /**
22
+ * A constraint name without its kind marker.
23
+ *
24
+ * What pairs two sides of a *report*: an index whose uniqueness or columns changed is one index that
25
+ * differs, not one dropped and another created, and only a handle independent of its shape can say
26
+ * so. Stripping the marker is what lets `idx_User_email`, named before the convention moved it to
27
+ * the end, recognise the `User__email_idx` derived for it now, so upgrading reports no drift.
28
+ *
29
+ * Exactly one marker, and the trailing one first. Stripping both ends would eat a leading marker
30
+ * that belongs to the *table* - an index over `pk_registry` is not a primary key - leaving it unable
31
+ * to pair with its own older name. The separator is levelled last, since only one convention doubles
32
+ * it.
33
+ */
34
+ export function indexNameStem(name) {
35
+ const withoutSuffix = name.replace(KIND_SUFFIX, '');
36
+ const bare = withoutSuffix === name ? name.replace(KIND_PREFIX, '') : withoutSuffix;
37
+ return bare.replace(/__/g, '_');
38
+ }
39
+ /** What this version emits. */
40
+ const KIND_SUFFIX = /_(?:idx|fk|ck|pk|uk|uq)$/i;
41
+ /**
42
+ * What it only ever *reads*: uql wrote `idx_User_email` until 0.42.1, and a database it did not
43
+ * create at all - the one `generate:from-db` points at - most often spells it that way too. Tried
44
+ * second, so a name already marked at the end keeps a leading `pk_` that is part of its table.
45
+ */
46
+ const KIND_PREFIX = /^(?:idx|fk|ck|pk|uk|uq)_/i;
1
47
  /**
2
48
  * Everything an index differs by, named, or nothing when the two match.
3
49
  *
@@ -6,6 +6,7 @@
6
6
  * - Database introspection results (TableSchema[])
7
7
  */
8
8
  import { getMeta, soleIdOf } from '../entity/metadata/definition.js';
9
+ import { isSoleIdField } from '../util/field.util.js';
9
10
  import { derivedForeignKeyName, derivedIndexName, qualifyName } from '../util/sql.util.js';
10
11
  import { fieldOptionsToCanonical } from './canonicalType.js';
11
12
  import { createTableNode, SchemaAST } from './schemaAST.js';
@@ -86,7 +87,7 @@ function addTableFromEntity(ctx, meta) {
86
87
  const columnName = ctx.resolveColumnName(key, field);
87
88
  const type = resolveColumnCanonicalType(field);
88
89
  const isPrimaryKey = field.isId === true;
89
- const isSoleKey = isPrimaryKey && meta.ids.length === 1;
90
+ const isSoleKey = isSoleIdField(meta, field);
90
91
  const column = {
91
92
  name: columnName,
92
93
  type,
@@ -95,9 +96,7 @@ function addTableFromEntity(ctx, meta) {
95
96
  nullable: isPrimaryKey ? false : (field.nullable ?? true),
96
97
  defaultValue: field.defaultValue,
97
98
  isPrimaryKey,
98
- // Only a sole integer key defaults to auto-increment: a composite's columns are values the
99
- // caller supplies, and the serial type carries an inline `PRIMARY KEY` the table already states.
100
- isAutoIncrement: field.autoIncrement ?? (isPrimaryKey && isSoleKey && type.category === 'integer'),
99
+ isAutoIncrement: field.autoIncrement ?? (isSoleKey && type.category === 'integer'),
101
100
  isUnique: field.unique ?? false,
102
101
  comment: field.comment,
103
102
  enum: field.enum,
@@ -142,37 +141,32 @@ function addRelationshipsFromEntity(ctx, meta) {
142
141
  // the engine requires the referenced columns to match a unique constraint as a whole.
143
142
  const localColumns = [];
144
143
  const foreignColumns = [];
145
- let firstLocalField;
146
- for (const { local: localPropName, foreign: foreignPropName } of relation.references) {
147
- const localField = meta.fields[localPropName];
148
- const foreignField = relatedMeta.fields[foreignPropName];
149
- if (!localField || !foreignField) {
150
- continue;
151
- }
152
- const localColumn = table.columns.get(ctx.resolveColumnName(localPropName, localField));
153
- const foreignColumn = relatedTable.columns.get(ctx.resolveColumnName(foreignPropName, foreignField));
154
- if (!localColumn || !foreignColumn) {
155
- continue;
156
- }
157
- firstLocalField ??= localField;
144
+ for (const { local: localProp, foreign: foreignProp } of relation.references) {
145
+ const localField = meta.fields[localProp];
146
+ const foreignField = relatedMeta.fields[foreignProp];
147
+ const localColumn = localField && table.columns.get(ctx.resolveColumnName(localProp, localField));
148
+ const foreignColumn = foreignField && relatedTable.columns.get(ctx.resolveColumnName(foreignProp, foreignField));
149
+ if (!localColumn || !foreignColumn)
150
+ break;
158
151
  localColumns.push(localColumn);
159
152
  foreignColumns.push(foreignColumn);
160
153
  }
161
- if (localColumns.length === relation.references.length && firstLocalField) {
162
- const relNode = {
163
- name: derivedForeignKeyName(table.name, localColumns.map((column) => column.name)),
164
- type: relation.cardinality === 'm1' ? 'ManyToOne' : 'OneToOne',
165
- from: { table, columns: localColumns },
166
- to: { table: relatedTable, columns: foreignColumns },
167
- // Falls back to the FK column's own `onDelete`, which is what makes a bare `@Field({
168
- // references, onDelete })` work with no relation declared at all.
169
- onDelete: relation.onDelete ?? firstLocalField.onDelete ?? ctx.defaultForeignKeyAction,
170
- onUpdate: relation.onUpdate ?? ctx.defaultForeignKeyAction,
171
- confidence: 1.0,
172
- inferredFrom: 'entity_decorator',
173
- };
174
- ctx.ast.addRelationship(relNode);
175
- }
154
+ // A pair that cannot be resolved drops the whole constraint: half of one enforces a rule
155
+ // nobody declared, over a subset of the key.
156
+ if (localColumns.length !== relation.references.length)
157
+ continue;
158
+ ctx.ast.addRelationship({
159
+ name: derivedForeignKeyName(table.name, localColumns.map((column) => column.name)),
160
+ type: relation.cardinality === 'm1' ? 'ManyToOne' : 'OneToOne',
161
+ from: { table, columns: localColumns },
162
+ to: { table: relatedTable, columns: foreignColumns },
163
+ // Falls back to the FK column's own `onDelete`, which is what makes a bare `@Field({
164
+ // references, onDelete })` work with no relation declared at all.
165
+ onDelete: relation.onDelete ?? meta.fields[relation.references[0].local]?.onDelete ?? ctx.defaultForeignKeyAction,
166
+ onUpdate: relation.onUpdate ?? ctx.defaultForeignKeyAction,
167
+ confidence: 1.0,
168
+ inferredFrom: 'entity_decorator',
169
+ });
176
170
  }
177
171
  }
178
172
  }
@@ -9,7 +9,8 @@
9
9
  */
10
10
  import { type IndexFacet } from './indexDifferences.js';
11
11
  import type { SchemaAST } from './schemaAST.js';
12
- import type { SchemaDiffResult } from './types.js';
12
+ import type { CanonicalType } from './types.js';
13
+ import type { SchemaDiffResult, TableDiff, TableNode } from './types.js';
13
14
  /**
14
15
  * Options for schema diffing.
15
16
  */
@@ -24,6 +25,23 @@ export interface DiffOptions {
24
25
  ignoreCase?: boolean;
25
26
  /** Tables to exclude from comparison */
26
27
  excludeTables?: string[];
28
+ /**
29
+ * A type as the engine would actually store it, for the caller that has a dialect.
30
+ *
31
+ * Several canonical types share one storage type per engine - a `boolean` is `TINYINT(1)` on MySQL
32
+ * and `INTEGER` on SQLite - so comparing them canonically reports an alteration on every sync for
33
+ * those columns. Passing both sides through the engine first is what settles that, and it is the
34
+ * only thing here a dialect is needed for, so it arrives as a function rather than as a dependency.
35
+ */
36
+ normalizeType?: (type: CanonicalType) => CanonicalType;
37
+ /**
38
+ * Whether two defaults are the same value, for the caller that has a dialect.
39
+ *
40
+ * A database reprints a default from its parse tree, so `'active'` comes back as
41
+ * `'active'::character varying` on Postgres and a symbolic `now()` matches no spelling of
42
+ * `CURRENT_TIMESTAMP`. Undoing that needs the dialect that wrote it, so it arrives as a function.
43
+ */
44
+ defaultsEqual?: (expected: unknown, actual: unknown) => boolean;
27
45
  }
28
46
  /**
29
47
  * Compare two schemas and return the differences.
@@ -34,3 +52,11 @@ export interface DiffOptions {
34
52
  * @returns Detailed diff result
35
53
  */
36
54
  export declare function diffSchemas(source: SchemaAST, target: SchemaAST, options?: DiffOptions): SchemaDiffResult;
55
+ /**
56
+ * Compare two tables and return the differences.
57
+ *
58
+ * Exported because it is also how a migration is planned: the generator diffs one entity's table
59
+ * against the one the database reported, then projects the result into a `SchemaDiff`. One
60
+ * comparison serves both, so drift and migrations can no longer disagree about what has changed.
61
+ */
62
+ export declare function diffTable(source: TableNode, target: TableNode, options?: DiffOptions): TableDiff | undefined;
@@ -8,7 +8,7 @@
8
8
  * - Schema synchronization
9
9
  */
10
10
  import { areTypesEqual, isBreakingTypeChange } from './canonicalType.js';
11
- import { describeIndexDifferences } from './indexDifferences.js';
11
+ import { describeIndexDifferences, indexNameStem } from './indexDifferences.js';
12
12
  import { DEFAULT_FOREIGN_KEY_ACTION } from './types.js';
13
13
  /**
14
14
  * Default diff options.
@@ -17,6 +17,8 @@ const DEFAULT_OPTIONS = {
17
17
  compareIndexes: true,
18
18
  indexFacets: new Set(),
19
19
  compareRelationships: true,
20
+ normalizeType: (type) => type,
21
+ defaultsEqual: (expected, actual) => normalizeDefault(expected) === normalizeDefault(actual),
20
22
  ignoreCase: false,
21
23
  excludeTables: [],
22
24
  };
@@ -61,20 +63,25 @@ export function diffSchemas(source, target, options = {}) {
61
63
  .filter((tableDiff) => tableDiff !== undefined);
62
64
  const columnDiffs = tablesToAlter.flatMap((tableDiff) => tableDiff.columnDiffs ?? []);
63
65
  const indexDiffs = tablesToAlter.flatMap((tableDiff) => tableDiff.indexDiffs ?? []);
66
+ const primaryKeyDiffs = tablesToAlter.flatMap((tableDiff) => tableDiff.primaryKeyDiff ?? []);
64
67
  // Relationships span tables, so they are compared over the whole schema rather than per table.
65
68
  const relationshipDiffs = opts.compareRelationships ? diffRelationships(source, target, opts) : [];
66
69
  const hasDifferences = tablesToCreate.length > 0 ||
67
70
  tablesToDrop.length > 0 ||
68
71
  tablesToAlter.length > 0 ||
69
72
  relationshipDiffs.length > 0 ||
70
- indexDiffs.length > 0;
71
- const hasBreakingChanges = tablesToDrop.length > 0 || columnDiffs.some((d) => d.isBreaking);
73
+ indexDiffs.length > 0 ||
74
+ primaryKeyDiffs.length > 0;
75
+ // Rewriting a key drops a constraint and rebuilds an index over the whole table, and fails outright
76
+ // where its new columns are null on rows that already exist. Breaking by any measure.
77
+ const hasBreakingChanges = tablesToDrop.length > 0 || primaryKeyDiffs.length > 0 || columnDiffs.some((d) => d.isBreaking);
72
78
  return {
73
79
  tablesToCreate,
74
80
  tablesToDrop,
75
81
  tablesToAlter,
76
82
  columnDiffs,
77
83
  indexDiffs,
84
+ primaryKeyDiffs,
78
85
  relationshipDiffs,
79
86
  hasDifferences,
80
87
  hasBreakingChanges,
@@ -82,14 +89,35 @@ export function diffSchemas(source, target, options = {}) {
82
89
  }
83
90
  /**
84
91
  * Compare two tables and return the differences.
92
+ *
93
+ * Exported because it is also how a migration is planned: the generator diffs one entity's table
94
+ * against the one the database reported, then projects the result into a `SchemaDiff`. One
95
+ * comparison serves both, so drift and migrations can no longer disagree about what has changed.
85
96
  */
86
- function diffTable(source, target, opts) {
97
+ export function diffTable(source, target, options = {}) {
98
+ const opts = { ...DEFAULT_OPTIONS, ...options };
87
99
  const columnDiffs = diffTableColumns(source, target, opts);
88
100
  const indexDiffs = opts.compareIndexes ? diffTableIndexes(source, target, opts) : [];
89
- if (columnDiffs.length === 0 && indexDiffs.length === 0) {
101
+ const primaryKeyDiff = diffPrimaryKey(source, target);
102
+ if (columnDiffs.length === 0 && indexDiffs.length === 0 && !primaryKeyDiff) {
90
103
  return undefined;
91
104
  }
92
- return { name: source.name, type: 'alter', columnDiffs, indexDiffs };
105
+ return { name: source.name, type: 'alter', columnDiffs, indexDiffs, primaryKeyDiff };
106
+ }
107
+ /**
108
+ * The two keys, where they hold different columns.
109
+ *
110
+ * Compared by columns and in order. Not by name: the engine named the constraint on every table that
111
+ * already exists, so matching on one would report every table as drifted the moment the convention
112
+ * that derives names changes.
113
+ */
114
+ function diffPrimaryKey(source, target) {
115
+ const expected = source.primaryKey.map((column) => column.name);
116
+ const actual = target.primaryKey.map((column) => column.name);
117
+ if (expected.length === actual.length && expected.every((column, i) => column === actual[i])) {
118
+ return undefined;
119
+ }
120
+ return { table: source.name, expected, actual, actualName: target.primaryKeyName };
93
121
  }
94
122
  /**
95
123
  * Compare columns between two tables.
@@ -114,7 +142,7 @@ function diffTableColumns(source, target, opts) {
114
142
  description: `Drop column "${column.name}"`,
115
143
  })),
116
144
  ...matched
117
- .map(([sourceColumn, targetColumn]) => diffColumn(source.name, sourceColumn, targetColumn))
145
+ .map(([sourceColumn, targetColumn]) => diffColumn(source.name, sourceColumn, targetColumn, opts))
118
146
  .filter((diff) => diff !== undefined),
119
147
  ];
120
148
  }
@@ -123,7 +151,7 @@ function diffTableColumns(source, target, opts) {
123
151
  */
124
152
  function diffTableIndexes(source, target, opts) {
125
153
  const normalizeName = nameNormalizer(opts);
126
- const { created, dropped, matched } = matchByKey(source.indexes, target.indexes, (index) => normalizeName(index.name));
154
+ const { created, dropped, matched } = matchByKey(source.indexes, target.indexes, (index) => normalizeName(indexNameStem(index.name)));
127
155
  return [
128
156
  ...created.map((index) => ({ name: index.name, table: source.name, type: 'create', expected: index })),
129
157
  ...dropped.map((index) => ({ name: index.name, table: target.name, type: 'drop', actual: index })),
@@ -135,26 +163,34 @@ function diffTableIndexes(source, target, opts) {
135
163
  /**
136
164
  * Compare two columns and return the difference.
137
165
  */
138
- function diffColumn(tableName, source, target) {
166
+ function diffColumn(tableName, source, target, opts) {
139
167
  const differences = [];
140
- // Compare types
141
- if (!areTypesEqual(source.type, target.type)) {
168
+ // Two things a key column implies rather than states, and catalogues report inconsistently: its
169
+ // type, which is the dialect's serial spelling rather than one the entity chose and does not round
170
+ // trip (`BIGINT UNSIGNED AUTO_INCREMENT` reads back as `BIGINT(20) UNSIGNED`), and its nullability,
171
+ // which is NOT NULL in every engine whatever is reported - SQLite's `PRAGMA table_info` says
172
+ // `notnull: 0` for the `INTEGER PRIMARY KEY` that is the table's own rowid. Comparing either asked
173
+ // to rewrite the column on every sync, and on SQLite, which cannot alter one at all, failed
174
+ // outright. Everything else about a key column is still compared, which the blanket "never alter a
175
+ // key column" rule these two replace used to hide.
176
+ const generatedType = source.isAutoIncrement && target.isAutoIncrement;
177
+ const impliedNotNull = source.isPrimaryKey && target.isPrimaryKey;
178
+ if (!generatedType && !areTypesEqual(opts.normalizeType(source.type), opts.normalizeType(target.type))) {
142
179
  differences.push(`type: ${formatType(source.type)} → ${formatType(target.type)}`);
143
180
  }
144
- // Compare nullability
145
- if (source.nullable !== target.nullable) {
181
+ if (!impliedNotNull && source.nullable !== target.nullable) {
146
182
  differences.push(`nullable: ${target.nullable} → ${source.nullable}`);
147
183
  }
148
184
  // Compare unique constraint
149
185
  if (source.isUnique !== target.isUnique) {
150
186
  differences.push(`unique: ${target.isUnique} → ${source.isUnique}`);
151
187
  }
152
- // Compare auto-increment
153
- if (source.isAutoIncrement !== target.isAutoIncrement) {
154
- differences.push(`autoIncrement: ${target.isAutoIncrement} → ${source.isAutoIncrement}`);
155
- }
188
+ // Auto-increment is deliberately not compared. No engine turns a column into an identity, or out of
189
+ // one, without rewriting the table, and there is no DDL here that does it - so a difference could
190
+ // only ever be reported, never settled, and the statements emitted for it (a bare `ALTER COLUMN
191
+ // TYPE`) do not change it. The same rule `describeIndexDifferences` follows for what it cannot read.
156
192
  // Compare default values (if both defined)
157
- if (normalizeDefault(source.defaultValue) !== normalizeDefault(target.defaultValue)) {
193
+ if (!opts.defaultsEqual(source.defaultValue, target.defaultValue)) {
158
194
  differences.push(`default: ${target.defaultValue ?? 'NULL'} → ${source.defaultValue ?? 'NULL'}`);
159
195
  }
160
196
  if (differences.length === 0) {
@@ -122,7 +122,7 @@ export interface ColumnNode {
122
122
  export interface TableNode {
123
123
  /**
124
124
  * The table's own name, never qualified. Everything derived from a table reads this: an index or
125
- * constraint name is a single identifier, and `idx_sales.Order_total` is a syntax error.
125
+ * constraint name is a single identifier, and `sales.Order_total_idx` is a syntax error.
126
126
  */
127
127
  readonly name: string;
128
128
  /**
@@ -133,8 +133,14 @@ export interface TableNode {
133
133
  readonly schema?: string;
134
134
  /** Map of column name to column node */
135
135
  readonly columns: Map<string, ColumnNode>;
136
- /** Primary key columns (supports composite keys) */
136
+ /** Primary key columns, in key order (supports composite keys) */
137
137
  readonly primaryKey: ColumnNode[];
138
+ /**
139
+ * What the constraint is called, where a name is known: read back from the database on an
140
+ * introspected table, absent on one built from entities, where nothing has named it yet. A `DROP`
141
+ * is the only thing that needs it - see {@link TableSchema.primaryKeyName}.
142
+ */
143
+ primaryKeyName?: string;
138
144
  /** Indexes on this table */
139
145
  readonly indexes: IndexNode[];
140
146
  /** `CHECK` constraints on this table. Optional: a node can be built without ever naming one. */
@@ -151,7 +157,7 @@ export interface TableNode {
151
157
  * Represents a foreign key relationship between tables.
152
158
  */
153
159
  export interface RelationshipNode {
154
- /** Constraint name (e.g., fk_posts_author_id) */
160
+ /** Constraint name (e.g., posts_author_id_fk) */
155
161
  readonly name: string;
156
162
  /** Type of relationship */
157
163
  readonly type: RelationshipType;
@@ -202,13 +208,28 @@ export interface SchemaAST {
202
208
  }
203
209
  /**
204
210
  * Difference between two column definitions.
211
+ *
212
+ * A union rather than one shape with two optional sides, so which node is present follows from the
213
+ * kind of difference: an added column has only the `expected` one, a dropped column only the
214
+ * `actual` one, and an altered column both. Stated as optionals, every reader had to assert its way
215
+ * past a `undefined` the kind had already ruled out.
205
216
  */
206
- export interface ColumnDiff {
217
+ export type ColumnDiff = ColumnDiffBase & ({
218
+ readonly type: 'add';
219
+ readonly expected: ColumnNode;
220
+ readonly actual?: undefined;
221
+ } | {
222
+ readonly type: 'drop';
223
+ readonly expected?: undefined;
224
+ readonly actual: ColumnNode;
225
+ } | {
226
+ readonly type: 'alter';
227
+ readonly expected: ColumnNode;
228
+ readonly actual: ColumnNode;
229
+ });
230
+ interface ColumnDiffBase {
207
231
  readonly table: string;
208
232
  readonly column: string;
209
- readonly type: 'add' | 'drop' | 'alter';
210
- readonly expected?: ColumnNode;
211
- readonly actual?: ColumnNode;
212
233
  /** Whether this change could cause data loss */
213
234
  readonly isBreaking?: boolean;
214
235
  readonly description?: string;
@@ -221,6 +242,21 @@ export interface TableDiff {
221
242
  readonly type: 'create' | 'drop' | 'alter';
222
243
  readonly columnDiffs?: ColumnDiff[];
223
244
  readonly indexDiffs?: IndexDiff[];
245
+ /** Set only where the two keys hold different columns. See {@link PrimaryKeyDiff}. */
246
+ readonly primaryKeyDiff?: PrimaryKeyDiff;
247
+ }
248
+ /**
249
+ * Two primary keys that hold different columns.
250
+ *
251
+ * By columns and in order, never by name: `(a, b)` is a different key from `(b, a)`, while the same
252
+ * key called `Member_pkey` on one side and `Member__userId_pk` on the other is one key, not two.
253
+ */
254
+ export interface PrimaryKeyDiff {
255
+ readonly table: string;
256
+ readonly expected: string[];
257
+ readonly actual: string[];
258
+ /** What the *actual* side calls its constraint, which is the only name a `DROP` can use. */
259
+ readonly actualName?: string;
224
260
  }
225
261
  /**
226
262
  * Difference between two index definitions.
@@ -258,6 +294,8 @@ export interface SchemaDiffResult {
258
294
  readonly columnDiffs: ColumnDiff[];
259
295
  /** All index diffs */
260
296
  readonly indexDiffs: IndexDiff[];
297
+ /** Every table whose primary key holds different columns than the entity declares. */
298
+ readonly primaryKeyDiffs: PrimaryKeyDiff[];
261
299
  /** All relationship/FK diffs */
262
300
  readonly relationshipDiffs: RelationshipDiff[];
263
301
  /** Whether there are any differences */
@@ -321,3 +359,4 @@ export interface DriftReport {
321
359
  readonly info: number;
322
360
  };
323
361
  }
362
+ export {};
@@ -5,7 +5,8 @@ export declare class SqliteDialect extends AbstractSqlDialect {
5
5
  protected readonly featureDefaults: DialectFeatures;
6
6
  readonly dialectName = "sqlite";
7
7
  readonly escapeIdChar = "`";
8
- readonly serialPrimaryKey = "INTEGER PRIMARY KEY AUTOINCREMENT";
8
+ readonly serialType = "INTEGER PRIMARY KEY AUTOINCREMENT";
9
+ readonly serialDeclaresPrimaryKey = true;
9
10
  readonly tableOptions = "";
10
11
  readonly beginTransactionCommand = "BEGIN TRANSACTION";
11
12
  readonly commitTransactionCommand = "COMMIT";
@@ -13,6 +13,7 @@ export class SqliteDialect extends AbstractSqlDialect {
13
13
  dropTableCascade: false,
14
14
  renameColumn: true,
15
15
  foreignKeyAlter: false, // SQLite does not support adding FKs to existing tables
16
+ primaryKeyAlter: false, // nor changing a key: the only route is rebuilding the table
16
17
  columnComment: false, // SQLite does not support column comments
17
18
  vectorIndexRequiresNotNull: false,
18
19
  vectorSupportsLength: false,
@@ -21,7 +22,9 @@ export class SqliteDialect extends AbstractSqlDialect {
21
22
  };
22
23
  dialectName = 'sqlite';
23
24
  escapeIdChar = '`';
24
- serialPrimaryKey = 'INTEGER PRIMARY KEY AUTOINCREMENT';
25
+ serialType = 'INTEGER PRIMARY KEY AUTOINCREMENT';
26
+ // `AUTOINCREMENT` is only legal in that exact phrase, so the key cannot be lifted to table level.
27
+ serialDeclaresPrimaryKey = true;
25
28
  tableOptions = '';
26
29
  beginTransactionCommand = 'BEGIN TRANSACTION';
27
30
  commitTransactionCommand = 'COMMIT';
@@ -91,6 +91,12 @@ export interface EngineFeatures {
91
91
  readonly dropTableCascade: boolean;
92
92
  readonly renameColumn: boolean;
93
93
  readonly foreignKeyAlter: boolean;
94
+ /**
95
+ * Whether a table's primary key can be changed on an existing table. False on SQLite, whose only
96
+ * route is rebuilding the table - so a migration that would change one is refused by name rather
97
+ * than emitting DDL the engine rejects.
98
+ */
99
+ readonly primaryKeyAlter: boolean;
94
100
  /** Whether the dialect supports inline COMMENT on columns (MySQL/MariaDB). */
95
101
  readonly columnComment: boolean;
96
102
  /**
@@ -123,7 +123,14 @@ export interface ColumnSchema {
123
123
  export interface TableSchema {
124
124
  readonly name: string;
125
125
  readonly columns: ColumnSchema[];
126
+ /** The key's columns **in order**, which is what a composite is: `(a, b)` is not `(b, a)`. */
126
127
  readonly primaryKey?: string[];
128
+ /**
129
+ * What the engine calls the key's constraint, where it names one at all - Postgres's `Member_pkey`,
130
+ * MySQL's literal `PRIMARY`, nothing on SQLite. Only a `DROP` needs it, and only the name the
131
+ * database actually reported will do: a derived one would name a constraint that is not there.
132
+ */
133
+ readonly primaryKeyName?: string;
127
134
  readonly indexes?: IndexSchema[];
128
135
  readonly foreignKeys?: ForeignKeySchema[];
129
136
  }
@@ -176,6 +183,18 @@ export interface SchemaDiff {
176
183
  */
177
184
  readonly schema?: string;
178
185
  readonly type: 'create' | 'alter' | 'drop';
186
+ /**
187
+ * The key the table has against the key the entity declares, set only when they differ.
188
+ *
189
+ * Compared by columns, never by name: the engine named the existing one, so requiring a derived
190
+ * name to match would rewrite the primary key of every table on the first migration after
191
+ * upgrading. `fromName` is what the database reported, and the only name a `DROP` can use.
192
+ */
193
+ readonly primaryKey?: {
194
+ readonly from: string[];
195
+ readonly to: string[];
196
+ readonly fromName?: string;
197
+ };
179
198
  readonly columnsToAdd?: ColumnSchema[];
180
199
  readonly columnsToAlter?: {
181
200
  from: ColumnSchema;
@@ -1,4 +1,4 @@
1
- import type { FieldOptions } from '../type/index.js';
1
+ import type { EntityMeta, FieldOptions } from '../type/index.js';
2
2
  /**
3
3
  * Checks if a field type is numeric (Number, BigInt, or explicit numeric logical types)
4
4
  */
@@ -11,6 +11,16 @@ export declare function isBooleanType(type: unknown): boolean;
11
11
  * Checks if a field type is JSON
12
12
  */
13
13
  export declare function isJsonType(type: unknown): boolean;
14
+ /**
15
+ * Whether the field is the entity's *whole* primary key - the only kind a serial can stand in for,
16
+ * and the only one that may state `PRIMARY KEY` in its own column definition.
17
+ *
18
+ * One column of a composite is a value the caller supplies, and the table states the key over every
19
+ * column at once. Asked in one place because the two schema paths - the AST that builds a
20
+ * `CREATE TABLE` and the diff that builds an `ALTER` - have to answer it the same way, and each
21
+ * answering for itself is what put a serial `PRIMARY KEY` on both columns of a composite.
22
+ */
23
+ export declare function isSoleIdField<E>(meta: EntityMeta<E>, field: FieldOptions): boolean;
14
24
  /**
15
25
  * Checks if a field should be treated as auto-incrementing.
16
26
  */
@@ -52,6 +52,18 @@ export function isJsonType(type) {
52
52
  }
53
53
  return false;
54
54
  }
55
+ /**
56
+ * Whether the field is the entity's *whole* primary key - the only kind a serial can stand in for,
57
+ * and the only one that may state `PRIMARY KEY` in its own column definition.
58
+ *
59
+ * One column of a composite is a value the caller supplies, and the table states the key over every
60
+ * column at once. Asked in one place because the two schema paths - the AST that builds a
61
+ * `CREATE TABLE` and the diff that builds an `ALTER` - have to answer it the same way, and each
62
+ * answering for itself is what put a serial `PRIMARY KEY` on both columns of a composite.
63
+ */
64
+ export function isSoleIdField(meta, field) {
65
+ return field.isId === true && meta.ids.length === 1;
66
+ }
55
67
  /**
56
68
  * Checks if a field should be treated as auto-incrementing.
57
69
  */
@@ -69,7 +69,15 @@ export function getFieldKeys(fields) {
69
69
  * is what a `$where` map and a composite key's id object both are; an array is a list of either.
70
70
  */
71
71
  export function isScalarId(value) {
72
- return (typeof value !== 'object' ||
73
- value === null ||
74
- (!Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype));
72
+ if (typeof value !== 'object' || value === null) {
73
+ return true;
74
+ }
75
+ if (Array.isArray(value)) {
76
+ return false;
77
+ }
78
+ // `null` as well as `Object.prototype`: an object with no prototype is what a query-string parser
79
+ // hands back (`qs`, express's `req.params`), and reading one as a bare id would name one column
80
+ // with a map of several.
81
+ const proto = Object.getPrototypeOf(value);
82
+ return proto !== Object.prototype && proto !== null;
75
83
  }
@@ -32,6 +32,16 @@ export declare function parentJoins(relOpts: Pick<RelationMeta, 'references' | '
32
32
  * which is the mistake this module exists to prevent.
33
33
  */
34
34
  export declare function targetKeyColumns(relOpts: Pick<RelationMeta, 'references'>, parentKeyCount: number): string[];
35
+ /** `{ joined column: true }`: the projection or grouping that keeps a parent's key on the rows read. */
36
+ export declare function joinedColumns(joins: readonly ParentJoin[]): Record<string, true>;
37
+ /**
38
+ * A parent row keyed by the columns a relation joins *from*, and a child or tally row keyed by the
39
+ * columns it carries that key in. The two halves of matching children to parents: they must agree on
40
+ * every column, so each is read through `joins` rather than through the parent's own key list - which
41
+ * is the same set only for a to-many, and silently a different one otherwise.
42
+ */
43
+ export declare function parentRowKey(joins: readonly ParentJoin[], parent: unknown): string;
44
+ export declare function joinedRowKey(joins: readonly ParentJoin[], row: unknown): string;
35
45
  /**
36
46
  * `{ joined column: every parent's value for it }`, the filter that fetches a whole page of parents'
37
47
  * children in one statement.