uql-orm 0.25.0 → 0.26.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 (58) hide show
  1. package/README.md +9 -8
  2. package/dist/browser/uql-browser.min.js.map +1 -1
  3. package/dist/cockroachdb/cockroachDialect.js +1 -1
  4. package/dist/dialect/indexSqlDialect.d.ts +3 -2
  5. package/dist/dialect/indexSqlDialect.js +10 -8
  6. package/dist/dialect/mysqlLikeSqlDialect.d.ts +0 -2
  7. package/dist/dialect/mysqlLikeSqlDialect.js +0 -7
  8. package/dist/dialect/pgLikeSqlDialect.js +1 -0
  9. package/dist/maria/mariaDialect.js +1 -1
  10. package/dist/migrate/bin.js +0 -0
  11. package/dist/migrate/builder/migrationBuilder.js +1 -1
  12. package/dist/migrate/builder/tableBuilder.js +2 -2
  13. package/dist/migrate/cli.js +4 -6
  14. package/dist/migrate/codegen/entityCodeGenerator.d.ts +5 -0
  15. package/dist/migrate/codegen/entityCodeGenerator.js +32 -27
  16. package/dist/migrate/codegen/fieldOptionsSource.d.ts +1 -1
  17. package/dist/migrate/codegen/fieldOptionsSource.js +6 -1
  18. package/dist/migrate/codegen/indexDecoratorSource.d.ts +14 -0
  19. package/dist/migrate/codegen/indexDecoratorSource.js +105 -0
  20. package/dist/migrate/drift/driftDetector.d.ts +5 -50
  21. package/dist/migrate/drift/driftDetector.js +215 -224
  22. package/dist/migrate/drift/index.d.ts +1 -1
  23. package/dist/migrate/drift/index.js +1 -1
  24. package/dist/migrate/generator/definitionToNode.d.ts +9 -0
  25. package/dist/migrate/generator/definitionToNode.js +79 -0
  26. package/dist/migrate/generator/indexNodeToSchema.js +4 -2
  27. package/dist/migrate/generator/mongoSchemaGenerator.js +3 -3
  28. package/dist/migrate/introspection/baseSqlIntrospector.d.ts +3 -0
  29. package/dist/migrate/introspection/baseSqlIntrospector.js +13 -5
  30. package/dist/migrate/introspection/mongoIntrospector.d.ts +3 -0
  31. package/dist/migrate/introspection/mongoIntrospector.js +5 -10
  32. package/dist/migrate/introspection/mysqlIntrospector.js +1 -1
  33. package/dist/migrate/introspection/postgresIntrospector.d.ts +57 -5
  34. package/dist/migrate/introspection/postgresIntrospector.js +93 -10
  35. package/dist/migrate/introspection/sqliteIntrospector.js +1 -1
  36. package/dist/migrate/migrator.js +3 -2
  37. package/dist/migrate/schemaGenerator.d.ts +26 -3
  38. package/dist/migrate/schemaGenerator.js +53 -92
  39. package/dist/schema/index.d.ts +3 -3
  40. package/dist/schema/index.js +2 -2
  41. package/dist/schema/indexColumns.d.ts +10 -0
  42. package/dist/schema/indexColumns.js +11 -0
  43. package/dist/schema/indexDifferences.d.ts +22 -0
  44. package/dist/schema/indexDifferences.js +49 -0
  45. package/dist/schema/schemaAST.js +2 -8
  46. package/dist/schema/schemaASTBuilder.d.ts +6 -59
  47. package/dist/schema/schemaASTBuilder.js +208 -233
  48. package/dist/schema/schemaASTDiffer.d.ts +9 -56
  49. package/dist/schema/schemaASTDiffer.js +229 -393
  50. package/dist/schema/types.d.ts +5 -12
  51. package/dist/schema/types.js +15 -0
  52. package/dist/type/dialect.d.ts +7 -2
  53. package/dist/type/dialect.js +1 -0
  54. package/dist/type/entity.d.ts +12 -10
  55. package/dist/type/migration.d.ts +14 -1
  56. package/dist/util/string.util.js +6 -1
  57. package/package.json +3 -4
  58. package/LICENSE.md +0 -22
@@ -10,256 +10,231 @@ import { fieldOptionsToCanonical } from './canonicalType.js';
10
10
  import { SchemaAST } from './schemaAST.js';
11
11
  import { DEFAULT_FOREIGN_KEY_ACTION } from './types.js';
12
12
  /**
13
- * Builds SchemaAST instances from various sources.
13
+ * Build a SchemaAST from entity classes (decorated with `@Entity`, `@Field`, etc.).
14
+ *
15
+ * Three passes, because each needs the one before it to have finished for *every* entity: a relation
16
+ * resolves against a table another entity declares, and an index against the columns of its own.
14
17
  */
15
- export class SchemaASTBuilder {
16
- namingStrategy;
17
- defaultForeignKeyAction;
18
- ast;
19
- constructor(namingStrategy, defaultForeignKeyAction = DEFAULT_FOREIGN_KEY_ACTION) {
20
- this.namingStrategy = namingStrategy;
21
- this.defaultForeignKeyAction = defaultForeignKeyAction;
22
- this.ast = new SchemaAST();
23
- }
24
- /**
25
- * Reset the builder for a new schema.
26
- */
27
- reset() {
28
- this.ast = new SchemaAST();
29
- return this;
30
- }
31
- /**
32
- * Get the built AST.
33
- */
34
- getAST() {
35
- return this.ast;
36
- }
37
- /**
38
- * Build AST from entity classes (decorated with @Entity, @Field, etc.)
39
- */
40
- fromEntities(entities, options = {}) {
41
- this.reset();
42
- const namingStrategy = options.namingStrategy ?? this.namingStrategy;
43
- const resolveTableName = options.resolveTableName ?? ((e, m) => namingStrategy?.tableName(m.name ?? e.name) ?? m.name ?? e.name);
44
- const resolveColumnName = options.resolveColumnName ?? ((k, f) => namingStrategy?.columnName(f.name ?? k) ?? f.name ?? k);
45
- // First pass: create all tables and columns
46
- for (const entity of entities) {
47
- const meta = getMeta(entity);
48
- this.addTableFromEntity(entity, meta, resolveTableName, resolveColumnName);
49
- }
50
- // Second pass: create relationships from relation decorators
51
- for (const entity of entities) {
52
- const meta = getMeta(entity);
53
- this.addRelationshipsFromEntity(entity, meta, resolveTableName, resolveColumnName, options);
54
- }
55
- // Third pass: create indexes from field options
18
+ export function buildSchemaAST(entities, options = {}) {
19
+ const { namingStrategy } = options;
20
+ const ctx = {
21
+ ast: new SchemaAST(),
22
+ resolveTableName: options.resolveTableName ?? ((e, m) => namingStrategy?.tableName(m.name ?? e.name) ?? m.name ?? e.name),
23
+ resolveColumnName: options.resolveColumnName ?? ((k, f) => namingStrategy?.columnName(f.name ?? k) ?? f.name ?? k),
24
+ defaultForeignKeyAction: options.defaultForeignKeyAction ?? DEFAULT_FOREIGN_KEY_ACTION,
25
+ };
26
+ for (const pass of [addTableFromEntity, addRelationshipsFromEntity, addIndexesFromEntity]) {
56
27
  for (const entity of entities) {
57
- const meta = getMeta(entity);
58
- this.addIndexesFromEntity(entity, meta, resolveTableName, resolveColumnName);
28
+ pass(ctx, entity, getMeta(entity));
59
29
  }
60
- return this.ast;
61
30
  }
62
- /**
63
- * Resolve the canonical type for a field, inheriting from the referenced
64
- * entity's primary key when the field is a foreign-key reference
65
- * (`@Field({ references: () => SomeEntity })`) with no explicit type of its
66
- * own.
67
- *
68
- * Without this, a field like `creatorId?: UUID` (a bare TypeScript alias for
69
- * `string`, erased at runtime) falls back to the generic string inference in
70
- * {@link fieldOptionsToCanonical} and gets typed as TEXT/VARCHAR - producing a
71
- * foreign key column whose type doesn't match the UUID primary key it
72
- * references, which Postgres (and most databases) reject outright.
73
- *
74
- * `field.typeFromReference` (set by `defineField`, see entity/metadata/definition.ts)
75
- * is what distinguishes "no type was given" from "the decorator explicitly set
76
- * a type" - including explicit constructor overrides like `type: BigInt`, which
77
- * a value-based check (e.g. `typeof field.type === 'string'`) would miss since
78
- * reflection also produces constructor values like `String`/`Number`.
79
- * `columnType` remains the unambiguous, always-respected explicit override.
80
- */
81
- resolveColumnCanonicalType(field, seen = new Set()) {
82
- const hasExplicitType = !!field.columnType || !field.typeFromReference;
83
- if (!hasExplicitType && field.references && !seen.has(field.references)) {
84
- seen.add(field.references);
85
- const referencedMeta = getMeta(field.references());
86
- const referencedIdField = referencedMeta.fields[referencedMeta.id];
87
- if (referencedIdField) {
88
- return this.resolveColumnCanonicalType(referencedIdField, seen);
89
- }
31
+ return ctx.ast;
32
+ }
33
+ /**
34
+ * Resolve the canonical type for a field, inheriting from the referenced
35
+ * entity's primary key when the field is a foreign-key reference
36
+ * (`@Field({ references: () => SomeEntity })`) with no explicit type of its
37
+ * own.
38
+ *
39
+ * Without this, a field like `creatorId?: UUID` (a bare TypeScript alias for
40
+ * `string`, erased at runtime) falls back to the generic string inference in
41
+ * {@link fieldOptionsToCanonical} and gets typed as TEXT/VARCHAR - producing a
42
+ * foreign key column whose type doesn't match the UUID primary key it
43
+ * references, which Postgres (and most databases) reject outright.
44
+ *
45
+ * `field.typeFromReference` (set by `defineField`, see entity/metadata/definition.ts)
46
+ * is what distinguishes "no type was given" from "the decorator explicitly set
47
+ * a type" - including explicit constructor overrides like `type: BigInt`, which
48
+ * a value-based check (e.g. `typeof field.type === 'string'`) would miss since
49
+ * reflection also produces constructor values like `String`/`Number`.
50
+ * `columnType` remains the unambiguous, always-respected explicit override.
51
+ */
52
+ function resolveColumnCanonicalType(field, seen = new Set()) {
53
+ const hasExplicitType = !!field.columnType || !field.typeFromReference;
54
+ if (!hasExplicitType && field.references && !seen.has(field.references)) {
55
+ seen.add(field.references);
56
+ const referencedMeta = getMeta(field.references());
57
+ const referencedIdField = referencedMeta.fields[referencedMeta.id];
58
+ if (referencedIdField) {
59
+ return resolveColumnCanonicalType(referencedIdField, seen);
90
60
  }
91
- return fieldOptionsToCanonical(field, field.type);
92
61
  }
93
- /**
94
- * Add a table from entity metadata.
95
- */
96
- addTableFromEntity(entity, meta, resolveTableName, resolveColumnName) {
97
- const tableName = resolveTableName(entity, meta);
98
- const columns = new Map();
99
- const primaryKey = [];
100
- // Create placeholder table (will be fully initialized below)
101
- const table = {
102
- name: tableName,
103
- columns,
104
- primaryKey,
105
- indexes: [],
106
- schema: this.ast,
107
- incomingRelations: [],
108
- outgoingRelations: [],
62
+ return fieldOptionsToCanonical(field, field.type);
63
+ }
64
+ /**
65
+ * Add a table from entity metadata.
66
+ */
67
+ function addTableFromEntity(ctx, entity, meta) {
68
+ const tableName = ctx.resolveTableName(entity, meta);
69
+ const columns = new Map();
70
+ const primaryKey = [];
71
+ // Create placeholder table (will be fully initialized below)
72
+ const table = {
73
+ name: tableName,
74
+ columns,
75
+ primaryKey,
76
+ indexes: [],
77
+ schema: ctx.ast,
78
+ incomingRelations: [],
79
+ outgoingRelations: [],
80
+ };
81
+ // Add columns from fields
82
+ const fields = meta.fields;
83
+ for (const key of Object.keys(fields)) {
84
+ const field = fields[key];
85
+ if (!field)
86
+ continue;
87
+ // Skip virtual fields
88
+ if (field.virtual)
89
+ continue;
90
+ const columnName = ctx.resolveColumnName(key, field);
91
+ const type = resolveColumnCanonicalType(field);
92
+ const isPrimaryKey = key === meta.id;
93
+ const column = {
94
+ name: columnName,
95
+ type,
96
+ // A primary key is NOT NULL in every engine, whatever the entity's property says: `id?: number`
97
+ // is optional because the database assigns it, not because the column accepts a null.
98
+ nullable: isPrimaryKey ? false : (field.nullable ?? true),
99
+ defaultValue: field.defaultValue,
100
+ isPrimaryKey,
101
+ isAutoIncrement: field.autoIncrement ?? (isPrimaryKey && type.category === 'integer'),
102
+ isUnique: field.unique ?? false,
103
+ comment: field.comment,
104
+ table,
105
+ referencedBy: [],
106
+ references: undefined,
109
107
  };
110
- // Add columns from fields
111
- const fields = meta.fields;
112
- for (const key of Object.keys(fields)) {
113
- const field = fields[key];
114
- if (!field)
115
- continue;
116
- // Skip virtual fields
117
- if (field.virtual)
118
- continue;
119
- const columnName = resolveColumnName(key, field);
120
- const type = this.resolveColumnCanonicalType(field);
121
- const column = {
122
- name: columnName,
123
- type,
124
- nullable: field.nullable ?? true,
125
- defaultValue: field.defaultValue,
126
- isPrimaryKey: key === meta.id,
127
- isAutoIncrement: field.autoIncrement ?? (key === meta.id && type.category === 'integer'),
128
- isUnique: field.unique ?? false,
129
- comment: field.comment,
130
- table,
131
- referencedBy: [],
132
- references: undefined,
133
- };
134
- columns.set(columnName, column);
135
- if (key === meta.id) {
136
- primaryKey.push(column);
137
- }
108
+ columns.set(columnName, column);
109
+ if (key === meta.id) {
110
+ primaryKey.push(column);
138
111
  }
139
- this.ast.addTable(table);
140
112
  }
141
- /**
142
- * Add relationships from entity relation decorators.
143
- */
144
- addRelationshipsFromEntity(entity, meta, resolveTableName, resolveColumnName, options) {
145
- const tableName = resolveTableName(entity, meta);
146
- const table = this.ast.getTable(tableName);
147
- if (!table)
148
- return;
149
- const relations = meta.relations;
150
- for (const key of Object.keys(relations)) {
151
- const relation = relations[key];
152
- if (!relation)
113
+ ctx.ast.addTable(table);
114
+ }
115
+ /**
116
+ * Add relationships from entity relation decorators.
117
+ */
118
+ function addRelationshipsFromEntity(ctx, entity, meta) {
119
+ const tableName = ctx.resolveTableName(entity, meta);
120
+ const table = ctx.ast.getTable(tableName);
121
+ if (!table)
122
+ return;
123
+ const relations = meta.relations;
124
+ for (const key of Object.keys(relations)) {
125
+ const relation = relations[key];
126
+ if (!relation)
127
+ continue;
128
+ const relatedEntity = relation.entity();
129
+ const relatedMeta = getMeta(relatedEntity);
130
+ const relatedTableName = ctx.resolveTableName(relatedEntity, relatedMeta);
131
+ const relatedTable = ctx.ast.getTable(relatedTableName);
132
+ if (!relatedTable)
133
+ continue;
134
+ // Only the owning side gets the FK. `mappedBy` marks the inverse side of a one-to-one, whose
135
+ // `references` describe how to join back (its own primary key against the owner's FK column) -
136
+ // reading those as a foreign key emitted a reversed constraint (`User(id) REFERENCES
137
+ // user_profile(creatorId)`), which SQLite rejects outright as a foreign key mismatch.
138
+ const ownsForeignKey = relation.cardinality === 'm1' || (relation.cardinality === '11' && !relation.mappedBy);
139
+ if (ownsForeignKey) {
140
+ const localPropName = relation.references[0].local;
141
+ const foreignPropName = relation.references[0].foreign;
142
+ const localField = meta.fields[localPropName];
143
+ if (!localField)
153
144
  continue;
154
- const relatedEntity = relation.entity();
155
- const relatedMeta = getMeta(relatedEntity);
156
- const relatedTableName = resolveTableName(relatedEntity, relatedMeta);
157
- const relatedTable = this.ast.getTable(relatedTableName);
158
- if (!relatedTable)
145
+ const localColName = ctx.resolveColumnName(localPropName, localField);
146
+ const foreignField = relatedMeta.fields[foreignPropName];
147
+ if (!foreignField)
159
148
  continue;
160
- // Only the owning side gets the FK. `mappedBy` marks the inverse side of a one-to-one, whose
161
- // `references` describe how to join back (its own primary key against the owner's FK column) -
162
- // reading those as a foreign key emitted a reversed constraint (`User(id) REFERENCES
163
- // user_profile(creatorId)`), which SQLite rejects outright as a foreign key mismatch.
164
- const ownsForeignKey = relation.cardinality === 'm1' || (relation.cardinality === '11' && !relation.mappedBy);
165
- if (ownsForeignKey) {
166
- const localPropName = relation.references[0].local;
167
- const foreignPropName = relation.references[0].foreign;
168
- const localField = meta.fields[localPropName];
169
- if (!localField)
170
- continue;
171
- const localColName = resolveColumnName(localPropName, localField);
172
- const foreignField = relatedMeta.fields[foreignPropName];
173
- if (!foreignField)
174
- continue;
175
- const foreignColName = resolveColumnName(foreignPropName, foreignField);
176
- const localColumn = table.columns.get(localColName);
177
- const foreignColumn = relatedTable.columns.get(foreignColName);
178
- if (localColumn && foreignColumn) {
179
- const relNode = {
180
- name: `fk_${tableName}_${localColName}`,
181
- type: relation.cardinality === 'm1' ? 'ManyToOne' : 'OneToOne',
182
- from: { table, columns: [localColumn] },
183
- to: { table: relatedTable, columns: [foreignColumn] },
184
- // The relation's own action wins over the global default, so one relation can cascade in the
185
- // database while the rest stay on `NO ACTION`.
186
- onDelete: relation.onDelete ?? options.defaultForeignKeyAction ?? this.defaultForeignKeyAction,
187
- onUpdate: relation.onUpdate ?? options.defaultForeignKeyAction ?? this.defaultForeignKeyAction,
188
- confidence: 1.0,
189
- inferredFrom: 'entity_decorator',
190
- };
191
- this.ast.addRelationship(relNode);
192
- }
149
+ const foreignColName = ctx.resolveColumnName(foreignPropName, foreignField);
150
+ const localColumn = table.columns.get(localColName);
151
+ const foreignColumn = relatedTable.columns.get(foreignColName);
152
+ if (localColumn && foreignColumn) {
153
+ const relNode = {
154
+ name: `fk_${tableName}_${localColName}`,
155
+ type: relation.cardinality === 'm1' ? 'ManyToOne' : 'OneToOne',
156
+ from: { table, columns: [localColumn] },
157
+ to: { table: relatedTable, columns: [foreignColumn] },
158
+ // Falls back to the FK column's own `onDelete`, which is what makes a bare `@Field({
159
+ // references, onDelete })` work with no relation declared at all.
160
+ onDelete: relation.onDelete ?? localField.onDelete ?? ctx.defaultForeignKeyAction,
161
+ onUpdate: relation.onUpdate ?? ctx.defaultForeignKeyAction,
162
+ confidence: 1.0,
163
+ inferredFrom: 'entity_decorator',
164
+ };
165
+ ctx.ast.addRelationship(relNode);
193
166
  }
194
167
  }
195
168
  }
196
- /**
197
- * Add indexes from field options (`@Field({ index })`) and from `@Index([...])`, which have nothing
198
- * in common beyond their target table.
199
- */
200
- addIndexesFromEntity(entity, meta, resolveTableName, resolveColumnName) {
201
- const tableName = resolveTableName(entity, meta);
202
- const table = this.ast.getTable(tableName);
203
- if (!table)
204
- return;
205
- for (const key of Object.keys(meta.fields)) {
206
- const field = meta.fields[key];
207
- if (!field?.index)
208
- continue;
209
- const column = table.columns.get(resolveColumnName(key, field));
210
- if (!column)
211
- continue;
212
- this.ast.addIndex({
213
- name: typeof field.index === 'string' ? field.index : `idx_${tableName}_${column.name}`,
214
- table,
215
- columns: [column],
216
- unique: field.unique ?? false,
217
- source: 'entity',
218
- syncStatus: 'entity_only',
219
- });
220
- }
221
- for (const idxMeta of meta.indexes ?? []) {
222
- this.addCompositeIndex(table, meta, idxMeta, resolveColumnName);
223
- }
224
- }
225
- /**
226
- * One `@Index([...])`. Its entries keep the authored form (expression, prefix length, order) with
227
- * names resolved, so the generator renders exactly what was declared; `columns` is the resolvable
228
- * subset, which is what diffing and introspection compare.
229
- */
230
- addCompositeIndex(table, meta, idxMeta, resolveColumnName) {
231
- // An entry survives if it is an expression (nothing to resolve) or names a column that exists;
232
- // an index left with none is dropped, the same as one naming only unknown columns always was.
233
- const entries = idxMeta.columns
234
- .map((entry) => {
235
- if (entry.expression)
236
- return entry;
237
- const field = meta.fields[entry.column];
238
- const column = field && resolveColumnName(entry.column, field);
239
- return column && table.columns.has(column) ? { ...entry, column } : undefined;
240
- })
241
- .filter((entry) => entry !== undefined);
242
- if (!entries.length)
243
- return;
244
- const columns = entries
245
- .map((entry) => (entry.expression ? undefined : table.columns.get(entry.column)))
246
- .filter((column) => column !== undefined);
247
- const named = columns.length ? columns.map((column) => column.name) : entries.map((_, at) => `expr${at}`);
248
- this.ast.addIndex({
249
- name: idxMeta.name ?? `idx_${table.name}_${named.join('_')}`,
169
+ }
170
+ /**
171
+ * Add indexes from field options (`@Field({ index })`) and from `@Index([...])`, which have nothing
172
+ * in common beyond their target table.
173
+ */
174
+ function addIndexesFromEntity(ctx, entity, meta) {
175
+ const tableName = ctx.resolveTableName(entity, meta);
176
+ const table = ctx.ast.getTable(tableName);
177
+ if (!table)
178
+ return;
179
+ for (const key of Object.keys(meta.fields)) {
180
+ const field = meta.fields[key];
181
+ if (!field?.index)
182
+ continue;
183
+ const column = table.columns.get(ctx.resolveColumnName(key, field));
184
+ if (!column)
185
+ continue;
186
+ ctx.ast.addIndex({
187
+ name: typeof field.index === 'string' ? field.index : `idx_${tableName}_${column.name}`,
250
188
  table,
251
- columns,
252
- entries,
253
- include: idxMeta.include,
254
- unique: idxMeta.unique ?? false,
255
- type: idxMeta.type,
256
- where: idxMeta.where,
257
- distance: idxMeta.distance,
258
- m: idxMeta.m,
259
- efConstruction: idxMeta.efConstruction,
260
- lists: idxMeta.lists,
189
+ entries: [{ column: column.name }],
190
+ unique: field.unique ?? false,
261
191
  source: 'entity',
262
192
  syncStatus: 'entity_only',
263
193
  });
264
194
  }
195
+ for (const idxMeta of meta.indexes ?? []) {
196
+ addCompositeIndex(ctx, table, meta, idxMeta);
197
+ }
198
+ }
199
+ /**
200
+ * One `@Index([...])`. Its entries keep the authored form (expression, prefix length, order) with
201
+ * names resolved, so the generator renders exactly what was declared; `columns` is the resolvable
202
+ * subset, which is what diffing and introspection compare.
203
+ */
204
+ /** An `include` column is named like any other, so a naming strategy has to reach it too. */
205
+ function resolveIncludeColumn(ctx, meta, column) {
206
+ const field = meta.fields[column];
207
+ return field ? ctx.resolveColumnName(column, field) : column;
208
+ }
209
+ function addCompositeIndex(ctx, table, meta, idxMeta) {
210
+ // An entry survives if it is an expression (nothing to resolve) or names a column that exists;
211
+ // an index left with none is dropped, the same as one naming only unknown columns always was.
212
+ const entries = idxMeta.columns
213
+ .map((entry) => {
214
+ if (entry.expression)
215
+ return entry;
216
+ const field = meta.fields[entry.column];
217
+ const column = field && ctx.resolveColumnName(entry.column, field);
218
+ return column && table.columns.has(column) ? { ...entry, column } : undefined;
219
+ })
220
+ .filter((entry) => entry !== undefined);
221
+ if (!entries.length)
222
+ return;
223
+ // An index over expressions alone has no column names to build a default name from.
224
+ const named = entries.map((entry, at) => (entry.expression ? `expr${at}` : entry.column));
225
+ ctx.ast.addIndex({
226
+ name: idxMeta.name ?? `idx_${table.name}_${named.join('_')}`,
227
+ table,
228
+ entries,
229
+ include: idxMeta.include?.map((column) => resolveIncludeColumn(ctx, meta, column)),
230
+ unique: idxMeta.unique ?? false,
231
+ type: idxMeta.type,
232
+ where: idxMeta.where,
233
+ distance: idxMeta.distance,
234
+ m: idxMeta.m,
235
+ efConstruction: idxMeta.efConstruction,
236
+ lists: idxMeta.lists,
237
+ source: 'entity',
238
+ syncStatus: 'entity_only',
239
+ });
265
240
  }
@@ -7,6 +7,7 @@
7
7
  * - Drift detection (expected vs actual)
8
8
  * - Schema synchronization
9
9
  */
10
+ import { type IndexFacet } from './indexDifferences.js';
10
11
  import type { SchemaAST } from './schemaAST.js';
11
12
  import type { SchemaDiffResult } from './types.js';
12
13
  /**
@@ -15,6 +16,8 @@ import type { SchemaDiffResult } from './types.js';
15
16
  export interface DiffOptions {
16
17
  /** Compare indexes */
17
18
  compareIndexes?: boolean;
19
+ /** What the target side can report about an index, normally an introspector's `indexFacets`. Anything left out is not compared. */
20
+ indexFacets?: ReadonlySet<IndexFacet>;
18
21
  /** Compare foreign keys/relationships */
19
22
  compareRelationships?: boolean;
20
23
  /** Ignore case differences in names */
@@ -23,61 +26,11 @@ export interface DiffOptions {
23
26
  excludeTables?: string[];
24
27
  }
25
28
  /**
26
- * Compares two SchemaAST instances and produces a detailed diff.
27
- */
28
- export declare class SchemaASTDiffer {
29
- /**
30
- * Compare two schemas and return the differences.
31
- *
32
- * @param source - The "expected" or "desired" schema (e.g., from entities)
33
- * @param target - The "actual" or "current" schema (e.g., from database)
34
- * @param options - Diff options
35
- * @returns Detailed diff result
36
- */
37
- diff(source: SchemaAST, target: SchemaAST, options?: DiffOptions): SchemaDiffResult;
38
- /**
39
- * Compare two tables and return the differences.
40
- */
41
- private diffTable;
42
- /**
43
- * Compare columns between two tables.
44
- */
45
- private diffTableColumns;
46
- /**
47
- * Compare indexes between two tables.
48
- */
49
- private diffTableIndexes;
50
- /**
51
- * Compare two columns and return the difference.
52
- */
53
- private diffColumn;
54
- /**
55
- * Compare two indexes and return the difference.
56
- */
57
- private diffIndex;
58
- /**
59
- * Compare relationships at the schema level.
60
- */
61
- private diffRelationships;
62
- /**
63
- * Compare two relationships.
64
- */
65
- private diffRelationship;
66
- /**
67
- * Generate a unique key for a relationship based on its structure.
68
- */
69
- private getRelationshipKey;
70
- /**
71
- * Format a canonical type for display.
72
- */
73
- private formatType;
74
- /**
75
- * Normalize default values for comparison.
76
- */
77
- private normalizeDefault;
78
- }
79
- /**
80
- * Create a differ and run a comparison.
81
- * Convenience function for one-off comparisons.
29
+ * Compare two schemas and return the differences.
30
+ *
31
+ * @param source - The "expected" or "desired" schema (e.g., from entities)
32
+ * @param target - The "actual" or "current" schema (e.g., from database)
33
+ * @param options - Diff options
34
+ * @returns Detailed diff result
82
35
  */
83
36
  export declare function diffSchemas(source: SchemaAST, target: SchemaAST, options?: DiffOptions): SchemaDiffResult;