uql-orm 0.74.0 → 0.74.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.
@@ -199,7 +199,7 @@ function detectIndexDrifts(diff) {
199
199
  table: idxDiff.table,
200
200
  index: idxDiff.name,
201
201
  details: `Index "${idxDiff.name}" exists in database but not defined in entity`,
202
- suggestion: 'Add @Field({ index }) or create migration to drop',
202
+ suggestion: 'Declare it, or drop it via migration: generate:entities drops one uql named',
203
203
  });
204
204
  }
205
205
  else {
@@ -211,7 +211,7 @@ function detectIndexDrifts(diff) {
211
211
  table: idxDiff.table,
212
212
  index: idxDiff.name,
213
213
  details: `Index "${idxDiff.name}" differs from the entity (${idxDiff.description})`,
214
- suggestion: 'Drop and recreate the index via migration',
214
+ suggestion: 'Recreate it via migration, which generate:entities writes',
215
215
  });
216
216
  }
217
217
  }
@@ -33,6 +33,7 @@ export declare class MongoSchemaGenerator extends MongoDialect implements Schema
33
33
  generateDropTable(tableName: string): string;
34
34
  generateAlterTable(diff: SchemaDiff): string[];
35
35
  generateAlterTableDown(diff: SchemaDiff): string[];
36
+ private dropIndexCommand;
36
37
  /** An index as MongoDB's key spec (`-1` descending, `'text'` full-text), refusing the SQL-only options. */
37
38
  generateCreateIndex(tableName: string, index: IndexSchema): string;
38
39
  /** An Atlas vector search index: its vector field first, then each field a `$vectorSearch` pre-filters on. */
@@ -1,6 +1,7 @@
1
1
  import { getMeta } from '../../entity/index.js';
2
2
  import { MongoDialect } from '../../mongo/mongoDialect.js';
3
3
  import { textLanguage } from '../../mongo/textLanguage.js';
4
+ import { indexChanges } from '../../schema/indexDifferences.js';
4
5
  import { QueryRaw, } from '../../type/index.js';
5
6
  import { indexDistance, unsupportedVectorMetric } from '../../type/vector.js';
6
7
  import { declaredIndexes, declaredIndexName, renderIndexColumn } from '../../util/ddlExpression.util.js';
@@ -8,6 +9,7 @@ import { fulltextConfig, fulltextWeights } from '../../util/dialect.util.js';
8
9
  import { assertIndexFeatures, assertIndexType } from '../ddl/indexDdl.js';
9
10
  import { assertIndexPredicate, refusedIndexPredicate } from '../indexPredicate.js';
10
11
  import { renderIndexDefinition } from './definitionToNode.js';
12
+ import { indexNodeToSchema } from './indexNodeToSchema.js';
11
13
  import { serializeMongoCommand } from './mongoCommand.js';
12
14
  /** The index types a key spec can say, a plain key or `'text'`, and Atlas's vector search index. */
13
15
  const MONGO_INDEX_TYPES = new Set(['btree', 'fulltext', 'vectorSearch']);
@@ -105,12 +107,21 @@ export class MongoSchemaGenerator extends MongoDialect {
105
107
  return serializeMongoCommand({ action: 'dropCollection', name: tableName });
106
108
  }
107
109
  generateAlterTable(diff) {
108
- return (diff.indexesToAdd ?? []).map((index) => this.generateCreateIndex(diff.tableName, index));
110
+ return [
111
+ ...(diff.indexesToDrop ?? []).map((index) => this.dropIndexCommand(diff.tableName, index)),
112
+ ...(diff.indexesToAdd ?? []).map((index) => this.generateCreateIndex(diff.tableName, index)),
113
+ ];
109
114
  }
110
115
  generateAlterTableDown(diff) {
111
- return (diff.indexesToAdd ?? []).map((index) => index.type === 'vectorSearch'
112
- ? serializeMongoCommand({ action: 'dropSearchIndex', collection: diff.tableName, name: index.name })
113
- : this.generateDropIndex(diff.tableName, index.name));
116
+ return [
117
+ ...(diff.indexesToAdd ?? []).map((index) => this.dropIndexCommand(diff.tableName, index)),
118
+ ...(diff.indexesToDrop ?? []).map((index) => this.generateCreateIndex(diff.tableName, index)),
119
+ ];
120
+ }
121
+ dropIndexCommand(tableName, index) {
122
+ return index.type === 'vectorSearch'
123
+ ? serializeMongoCommand({ action: 'dropSearchIndex', collection: tableName, name: index.name })
124
+ : this.generateDropIndex(tableName, index.name);
114
125
  }
115
126
  /** An index as MongoDB's key spec (`-1` descending, `'text'` full-text), refusing the SQL-only options. */
116
127
  generateCreateIndex(tableName, index) {
@@ -200,15 +211,16 @@ export class MongoSchemaGenerator extends MongoDialect {
200
211
  if (!currentTable) {
201
212
  return { tableName: collectionName, type: 'create' };
202
213
  }
203
- const existingIndexes = new Set(currentTable.indexes.map((i) => i.name));
204
- const indexesToAdd = this.indexesOf(meta, collectionName).filter((index) => !existingIndexes.has(index.name));
205
- if (indexesToAdd.length === 0) {
214
+ // By name: MongoDB lists a text index's fields alphabetically, so a shape would not match its own.
215
+ const { toAdd, toDrop } = indexChanges(collectionName, this.indexesOf(meta, collectionName), currentTable.indexes, (index) => index.name);
216
+ if (!toAdd.length && !toDrop.length) {
206
217
  return undefined;
207
218
  }
208
219
  return {
209
220
  tableName: collectionName,
210
221
  type: 'alter',
211
- indexesToAdd,
222
+ indexesToAdd: toAdd.length ? toAdd : undefined,
223
+ indexesToDrop: toDrop.length ? toDrop.map(indexNodeToSchema) : undefined,
212
224
  };
213
225
  }
214
226
  }
@@ -189,15 +189,16 @@ export class Migrator {
189
189
  this.logger.logInfo('No schema changes detected.');
190
190
  return '';
191
191
  }
192
+ // Diff by diff in reverse, each rolled back in the order its generator wrote it.
192
193
  const down = [
193
- ...created.map((tableName) => generator.generateDropTable(tableName, { ifExists: true })),
194
- ...altered.flatMap((diff) => generator.generateAlterTableDown(diff)),
194
+ ...altered.toReversed().flatMap((diff) => generator.generateAlterTableDown(diff)),
195
+ ...created.toReversed().map((tableName) => generator.generateDropTable(tableName, { ifExists: true })),
195
196
  ];
196
197
  const { emit } = this.target.source;
197
198
  const filePath = await this.writeMigration(name, {
198
199
  docExtraLines: ['Generated from entity definitions'],
199
200
  upInner: emit(up),
200
- downInner: emit(down.reverse()),
201
+ downInner: emit(down),
201
202
  });
202
203
  this.logger.logInfo(`Created migration from entities: ${filePath}`);
203
204
  return filePath;
@@ -345,7 +346,13 @@ export class Migrator {
345
346
  this.logger.logSkippedMigration(`[AutoSync] Skipped altering ${filteredDiff.foreignKeysToAlter.length} foreign keys in table '${diff.tableName}': ${filteredDiff.foreignKeysToAlter.map((fk) => fk.to.name).join(', ')} (safe mode active). Use a migration or { safe: false } to apply.`);
346
347
  delete filteredDiff.foreignKeysToAlter;
347
348
  }
348
- delete filteredDiff.indexesToDrop;
349
+ if (filteredDiff.indexesToDrop?.length) {
350
+ // An index recreated under its old name is a drop and an add, held back together.
351
+ const dropped = new Set(filteredDiff.indexesToDrop.map((index) => index.name));
352
+ this.logger.logSkippedMigration(`[AutoSync] Skipped dropping ${dropped.size} indexes in table '${diff.tableName}': ${[...dropped].join(', ')} (safe mode active). Use a migration or { safe: false } to apply.`);
353
+ filteredDiff.indexesToAdd = filteredDiff.indexesToAdd?.filter((index) => !dropped.has(index.name));
354
+ delete filteredDiff.indexesToDrop;
355
+ }
349
356
  delete filteredDiff.foreignKeysToDrop;
350
357
  }
351
358
  if (!options.drop && filteredDiff.columnsToDrop?.length) {
@@ -62,12 +62,16 @@ export declare class SqlSchemaGenerator implements SchemaGenerator {
62
62
  private addForeignKeyStatements;
63
63
  /** `DROP CONSTRAINT` for each of `constraintNames`, the mirror of {@link addForeignKeyStatements}. */
64
64
  private dropForeignKeyStatements;
65
+ /** The `ALTER COLUMN` restating each of `columns`. */
66
+ private alterColumnStatements;
67
+ /** An index added to a table that may already have rows: its `CREATE`, then what the engine needs after. */
68
+ private addIndexStatements;
69
+ /** `DROP INDEX` for each of `indexes`, the mirror of {@link addIndexStatements}. */
70
+ private dropIndexStatements;
65
71
  generateAlterTableDown(diff: SchemaDiff): string[];
66
72
  generateCreateIndex(tableName: string, index: IndexSchema, options?: {
67
73
  ifNotExists?: boolean;
68
74
  }): string;
69
- /** An index added to a table that may already have rows: its `CREATE`, then what the engine needs after. */
70
- private addIndexStatements;
71
75
  /**
72
76
  * `schema` is the table's, because that is where its indexes live. MySQL takes it from the table
73
77
  * operand instead, which is already qualified.
@@ -106,13 +110,6 @@ export declare class SqlSchemaGenerator implements SchemaGenerator {
106
110
  * drift detection runs, with types normalized as the engine stores them.
107
111
  */
108
112
  diffSchema(entity: Type<object>, currentTable: TableNode | undefined, desiredAst?: SchemaAST): SchemaDiff | undefined;
109
- /**
110
- * Indexes the entity declares that the table does not already have, in any shape.
111
- *
112
- * Additive only: an index the entity does not name may well have been created deliberately outside
113
- * the ORM, and dropping it is a decision for a reviewed migration.
114
- */
115
- private missingIndexes;
116
113
  protected diffOptions(): DiffOptions;
117
114
  /** Spread, not copied field by field, so a field the node gains cannot go missing here. */
118
115
  private columnNodeToSchema;
@@ -1,6 +1,6 @@
1
1
  import { getMeta } from '../entity/index.js';
2
2
  import { canonicalToSql, engineType, isVectorCategory } from '../schema/canonicalType.js';
3
- import { indexSignature } from '../schema/indexDifferences.js';
3
+ import { indexChanges } from '../schema/indexDifferences.js';
4
4
  import { buildSchemaAST, resolveColumnCanonicalType } from '../schema/schemaASTBuilder.js';
5
5
  import { diffRelationshipNodes, diffTable } from '../schema/schemaASTDiffer.js';
6
6
  import { isAutoIncrement, qualifyName } from '../util/index.js';
@@ -144,40 +144,19 @@ export class SqlSchemaGenerator {
144
144
  ...(diff.foreignKeysToDrop ?? []),
145
145
  ...(diff.foreignKeysToAlter ?? []).map((it) => constraintNameOf(diff.tableName, it.from)),
146
146
  ]));
147
- // Add new columns
148
- if (diff.columnsToAdd?.length) {
149
- for (const column of diff.columnsToAdd) {
150
- this.assertColumnAddable(diff.tableName, column);
151
- statements.push(this.tableDdl.addColumn(diff.tableName, this.generateColumnDefinitionFromSchema(column)));
152
- statements.push(...this.generateColumnCommentStatement(diff.tableName, column, diff.schema));
153
- }
154
- }
155
- // Alter existing columns
156
- if (diff.columnsToAlter?.length) {
157
- for (const { to } of diff.columnsToAlter) {
158
- const colDef = this.generateColumnDefinitionFromSchema(to);
159
- const colStatements = this.generateAlterColumnStatements(diff.tableName, to, colDef);
160
- statements.push(...colStatements);
161
- }
162
- }
163
- // Drop columns
164
- if (diff.columnsToDrop?.length) {
165
- for (const columnName of diff.columnsToDrop) {
166
- statements.push(...this.tableDdl.dropColumn(diff.tableName, columnName));
167
- }
168
- }
169
- // Add indexes
170
- if (diff.indexesToAdd?.length) {
171
- for (const index of diff.indexesToAdd) {
172
- statements.push(...this.addIndexStatements(diff.tableName, index));
173
- }
174
- }
175
- // Drop indexes
176
- if (diff.indexesToDrop?.length) {
177
- for (const indexName of diff.indexesToDrop) {
178
- statements.push(this.generateDropIndex(diff.tableName, indexName, diff.schema));
179
- }
180
- }
147
+ // Before the adds, which may reuse a dropped index's name, and before the columns: some engines
148
+ // drop an index along with its column, which would leave nothing here to name.
149
+ statements.push(...this.dropIndexStatements(diff.tableName, diff.indexesToDrop, diff.schema));
150
+ for (const column of diff.columnsToAdd ?? []) {
151
+ this.assertColumnAddable(diff.tableName, column);
152
+ statements.push(this.tableDdl.addColumn(diff.tableName, this.generateColumnDefinitionFromSchema(column)));
153
+ statements.push(...this.generateColumnCommentStatement(diff.tableName, column, diff.schema));
154
+ }
155
+ statements.push(...this.alterColumnStatements(diff.tableName, (diff.columnsToAlter ?? []).map((it) => it.to)));
156
+ for (const columnName of diff.columnsToDrop ?? []) {
157
+ statements.push(...this.tableDdl.dropColumn(diff.tableName, columnName));
158
+ }
159
+ statements.push(...this.addIndexStatements(diff.tableName, diff.indexesToAdd));
181
160
  // Last, so every column it names exists by now.
182
161
  if (diff.primaryKey?.to.length) {
183
162
  statements.push(this.generateAddPrimaryKeySql(diff.tableName, diff.primaryKey.to));
@@ -198,6 +177,21 @@ export class SqlSchemaGenerator {
198
177
  dropForeignKeyStatements(tableName, constraintNames) {
199
178
  return constraintNames.map((name) => this.generateDropForeignKeySql(tableName, name));
200
179
  }
180
+ /** The `ALTER COLUMN` restating each of `columns`. */
181
+ alterColumnStatements(tableName, columns) {
182
+ return columns.flatMap((column) => this.generateAlterColumnStatements(tableName, column, this.generateColumnDefinitionFromSchema(column)));
183
+ }
184
+ /** An index added to a table that may already have rows: its `CREATE`, then what the engine needs after. */
185
+ addIndexStatements(tableName, indexes = []) {
186
+ return indexes.flatMap((index) => [
187
+ this.generateCreateIndex(tableName, index),
188
+ ...this.indexDdl.settleStatements(tableName, index),
189
+ ]);
190
+ }
191
+ /** `DROP INDEX` for each of `indexes`, the mirror of {@link addIndexStatements}. */
192
+ dropIndexStatements(tableName, indexes = [], schema) {
193
+ return indexes.map((index) => this.generateDropIndex(tableName, index.name, schema));
194
+ }
201
195
  generateAlterTableDown(diff) {
202
196
  const statements = [];
203
197
  // Constraints first, mirroring the up direction: the up added them last, so the down drops them
@@ -212,44 +206,26 @@ export class SqlSchemaGenerator {
212
206
  if (diff.primaryKey?.to.length) {
213
207
  statements.push(this.generateDropPrimaryKeySql(diff.tableName, derivedPrimaryKeyName(diff.tableName, diff.primaryKey.to)));
214
208
  }
215
- // Reverse column additions by dropping them
216
- if (diff.columnsToAdd?.length) {
217
- for (const column of diff.columnsToAdd) {
218
- statements.push(...this.tableDdl.dropColumn(diff.tableName, column.name));
219
- }
220
- }
221
- // Reverse column alterations by restoring original schema
222
- if (diff.columnsToAlter?.length) {
223
- for (const { from } of diff.columnsToAlter) {
224
- const colDef = this.generateColumnDefinitionFromSchema(from);
225
- const colStatements = this.generateAlterColumnStatements(diff.tableName, from, colDef);
226
- statements.push(...colStatements);
227
- }
228
- }
229
- // Reverse index additions by dropping them
230
- if (diff.indexesToAdd?.length) {
231
- for (const index of diff.indexesToAdd) {
232
- statements.push(this.generateDropIndex(diff.tableName, index.name, diff.schema));
233
- }
209
+ for (const column of diff.columnsToAdd ?? []) {
210
+ statements.push(...this.tableDdl.dropColumn(diff.tableName, column.name));
234
211
  }
212
+ statements.push(...this.alterColumnStatements(diff.tableName, (diff.columnsToAlter ?? []).map((it) => it.from)));
213
+ statements.push(...this.dropIndexStatements(diff.tableName, diff.indexesToAdd, diff.schema));
214
+ statements.push(...this.addIndexStatements(diff.tableName, diff.indexesToDrop));
235
215
  if (diff.primaryKey?.from.length) {
236
216
  statements.push(this.generateAddPrimaryKeySql(diff.tableName, diff.primaryKey.from, diff.primaryKey.fromName));
237
217
  }
238
218
  // The constraint the up replaced, back under the name the database had for it. A foreign key the
239
219
  // up *dropped* is not restored: only its name survived the diff, never what it pointed at.
240
220
  statements.push(...this.addForeignKeyStatements(diff.tableName, (diff.foreignKeysToAlter ?? []).map((it) => it.from)));
241
- if (diff.columnsToDrop?.length || diff.indexesToDrop?.length || diff.foreignKeysToDrop?.length) {
242
- statements.push(`-- TODO: Manual reversal needed for dropped columns/indexes/foreign keys`);
221
+ if (diff.columnsToDrop?.length || diff.foreignKeysToDrop?.length) {
222
+ statements.push(`-- TODO: Manual reversal needed for dropped columns/foreign keys`);
243
223
  }
244
224
  return statements;
245
225
  }
246
226
  generateCreateIndex(tableName, index, options = {}) {
247
227
  return this.indexDdl.getCreateIndexStatement(tableName, index, options);
248
228
  }
249
- /** An index added to a table that may already have rows: its `CREATE`, then what the engine needs after. */
250
- addIndexStatements(tableName, index) {
251
- return [this.generateCreateIndex(tableName, index), ...this.indexDdl.settleStatements(tableName, index)];
252
- }
253
229
  /**
254
230
  * `schema` is the table's, because that is where its indexes live. MySQL takes it from the table
255
231
  * operand instead, which is already qualified.
@@ -351,10 +327,12 @@ export class SqlSchemaGenerator {
351
327
  }
352
328
  // Indexes are matched here rather than by the differ, which pairs them by name so that a changed
353
329
  // one reads as one index that altered. A migration needs the opposite: an index already in the
354
- // table, under whatever name, must not be created again, and one whose shape differs is a
355
- // separate index rather than a change - no engine alters an index's columns or uniqueness.
330
+ // table, under whatever name, must not be created again, and one whose shape differs is dropped
331
+ // and created anew - no engine alters an index's columns or uniqueness.
356
332
  const tableDiff = diffTable(desired, currentTable, { ...this.diffOptions(), compareIndexes: false });
357
- const indexesToAdd = this.missingIndexes(desired, currentTable);
333
+ const indexes = indexChanges(currentTable.name, desired.indexes, currentTable.indexes);
334
+ const indexesToAdd = indexes.toAdd.map(indexNodeToSchema);
335
+ const indexesToDrop = indexes.toDrop.map(indexNodeToSchema);
358
336
  const columnDiffs = tableDiff?.columnDiffs ?? [];
359
337
  const columnsToAdd = columnDiffs.flatMap((it) => (it.type === 'add' ? [this.columnNodeToSchema(it.expected)] : []));
360
338
  const columnsToDrop = columnDiffs.flatMap((it) => (it.type === 'drop' ? [it.column] : []));
@@ -387,6 +365,7 @@ export class SqlSchemaGenerator {
387
365
  !columnsToAlter.length &&
388
366
  !columnsToDrop.length &&
389
367
  !indexesToAdd.length &&
368
+ !indexesToDrop.length &&
390
369
  !foreignKeysToAdd.length &&
391
370
  !foreignKeysToDrop.length &&
392
371
  !foreignKeysToAlter.length &&
@@ -402,21 +381,12 @@ export class SqlSchemaGenerator {
402
381
  columnsToAlter: columnsToAlter.length ? columnsToAlter : undefined,
403
382
  columnsToDrop: columnsToDrop.length ? columnsToDrop : undefined,
404
383
  indexesToAdd: indexesToAdd.length ? indexesToAdd : undefined,
384
+ indexesToDrop: indexesToDrop.length ? indexesToDrop : undefined,
405
385
  foreignKeysToAdd: foreignKeysToAdd.length ? foreignKeysToAdd : undefined,
406
386
  foreignKeysToDrop: foreignKeysToDrop.length ? foreignKeysToDrop : undefined,
407
387
  foreignKeysToAlter: foreignKeysToAlter.length ? foreignKeysToAlter : undefined,
408
388
  };
409
389
  }
410
- /**
411
- * Indexes the entity declares that the table does not already have, in any shape.
412
- *
413
- * Additive only: an index the entity does not name may well have been created deliberately outside
414
- * the ORM, and dropping it is a decision for a reviewed migration.
415
- */
416
- missingIndexes(desired, currentTable) {
417
- const present = new Set(currentTable.indexes.map(indexSignature));
418
- return desired.indexes.filter((index) => !present.has(indexSignature(index))).map(indexNodeToSchema);
419
- }
420
390
  diffOptions() {
421
391
  return {
422
392
  normalizeType: engineType(this.dialect),
@@ -563,7 +533,9 @@ export class SqlSchemaGenerator {
563
533
  case 'alterColumn':
564
534
  return this.generateAlterColumnSql(operation.tableName, operation.columnName, operation.changes);
565
535
  case 'createIndex':
566
- return this.addIndexStatements(operation.tableName, renderIndexDefinition(operation.index, (sql) => this.dialect.compileDdl(sql)));
536
+ return this.addIndexStatements(operation.tableName, [
537
+ renderIndexDefinition(operation.index, (sql) => this.dialect.compileDdl(sql)),
538
+ ]);
567
539
  case 'dropIndex':
568
540
  return [this.generateDropIndex(operation.tableName, operation.indexName)];
569
541
  case 'addForeignKey':
@@ -5,18 +5,28 @@ import type { IndexNode } from './types.js';
5
5
  * `textIndex` is a text index's weights and language, kept by an engine that lists its fields in no declared order.
6
6
  */
7
7
  export type IndexFacet = 'order' | 'nulls' | 'opsClass' | 'accessMethod' | 'include' | 'vector' | 'textIndex';
8
+ type ComparableIndex = Pick<IndexNode, 'name' | 'entries' | 'unique'>;
8
9
  /**
9
10
  * Whether the table has this index already, by shape rather than name, uniqueness included. An index
10
11
  * over an expression, whose text the engine reprints, falls back to its name.
11
12
  */
12
- export declare function indexSignature(index: Pick<IndexNode, 'name' | 'entries' | 'unique'>): string;
13
+ export declare function indexSignature(index: ComparableIndex): string;
13
14
  /**
14
15
  * A constraint name without its kind marker, pairing an index with its older spelling
15
16
  * (`idx_User_email` with `User__email_idx`). Only one marker, the trailing one first.
16
17
  */
17
18
  export declare function indexNameStem(name: string): string;
19
+ /**
20
+ * The indexes a table lacks and the ones it no longer needs, matched by `keyOf`. Only an index uql
21
+ * named, or whose name the entity claims, is dropped: any other may have been made outside the ORM.
22
+ */
23
+ export declare function indexChanges<I extends ComparableIndex>(table: string, declared: readonly I[], current: readonly IndexNode[], keyOf?: (index: ComparableIndex) => string): {
24
+ toAdd: I[];
25
+ toDrop: IndexNode[];
26
+ };
18
27
  /**
19
28
  * What two indexes differ by, comparing only what both sides state structurally: an expression, a JSON
20
29
  * path or a predicate is reprinted by the engine, so never compared.
21
30
  */
22
31
  export declare function describeIndexDifferences(source: IndexNode, target: IndexNode, facets: ReadonlySet<IndexFacet>): string[];
32
+ export {};
@@ -1,11 +1,16 @@
1
1
  import { isVectorIndexType } from '../type/vector.js';
2
2
  import { fulltextConfig } from '../util/dialect.util.js';
3
+ import { derivedIndexName } from '../util/sql.util.js';
4
+ /** An entry the engine reprints in its own words, so never compared as written. */
5
+ function isReprinted(entry) {
6
+ return Boolean(entry.expression || entry.jsonPath || entry.jsonArray);
7
+ }
3
8
  /**
4
9
  * Whether the table has this index already, by shape rather than name, uniqueness included. An index
5
10
  * over an expression, whose text the engine reprints, falls back to its name.
6
11
  */
7
12
  export function indexSignature(index) {
8
- const comparable = !index.entries.some((entry) => entry.expression || entry.jsonPath || entry.jsonArray);
13
+ const comparable = !index.entries.some(isReprinted);
9
14
  const identity = comparable
10
15
  ? index.entries.map((entry) => entry.column).join(',')
11
16
  : `name:${indexNameStem(index.name)}`;
@@ -20,6 +25,33 @@ export function indexNameStem(name) {
20
25
  const bare = withoutSuffix === name ? name.replace(KIND_PREFIX, '') : withoutSuffix;
21
26
  return bare.replace(/__/g, '_');
22
27
  }
28
+ /**
29
+ * The indexes a table lacks and the ones it no longer needs, matched by `keyOf`. Only an index uql
30
+ * named, or whose name the entity claims, is dropped: any other may have been made outside the ORM.
31
+ */
32
+ export function indexChanges(table, declared, current, keyOf = indexSignature) {
33
+ const present = new Set(current.map(keyOf));
34
+ const wanted = new Set(declared.map(keyOf));
35
+ const claimed = new Set(declared.map((index) => index.name));
36
+ const owned = (index) => claimed.has(index.name) || hasDerivedName(table, index);
37
+ return {
38
+ toAdd: declared.filter((index) => !present.has(keyOf(index))),
39
+ toDrop: current.filter((index) => !wanted.has(keyOf(index)) && owned(index)),
40
+ };
41
+ }
42
+ /**
43
+ * Whether uql named the index itself, from its own columns: `Order__total_idx`, its unique `_uk`, or
44
+ * the `idx_Order_total` it wrote until 0.42.1.
45
+ */
46
+ function hasDerivedName(table, index) {
47
+ const parts = index.entries.map((entry, at) => (isReprinted(entry) ? `expr${at}` : entry.column));
48
+ const derived = [
49
+ derivedIndexName(table, parts),
50
+ derivedIndexName(table, parts, true),
51
+ `idx_${table}_${parts.join('_')}`,
52
+ ];
53
+ return derived.includes(index.name);
54
+ }
23
55
  /** What this version emits. */
24
56
  const KIND_SUFFIX = /_(?:idx|fk|ck|pk|uk|uq)$/i;
25
57
  /**
@@ -34,7 +66,7 @@ const KIND_PREFIX = /^(?:idx|fk|ck|pk|uk|uq)_/i;
34
66
  */
35
67
  export function describeIndexDifferences(source, target, facets) {
36
68
  const differences = [];
37
- const comparableEntries = ![...source.entries, ...target.entries].some((entry) => entry.expression || entry.jsonPath || entry.jsonArray);
69
+ const comparableEntries = ![...source.entries, ...target.entries].some(isReprinted);
38
70
  if (comparableEntries) {
39
71
  const [sourceColumns, targetColumns] = [source, target].map((index) => textFieldOrder(index, facets, index.entries.map((entry) => entrySignature(entry, facets))).join(', '));
40
72
  if (sourceColumns !== targetColumns) {
@@ -203,7 +203,8 @@ export interface SchemaDiff {
203
203
  }[];
204
204
  readonly columnsToDrop?: string[];
205
205
  readonly indexesToAdd?: IndexSchema[];
206
- readonly indexesToDrop?: string[];
206
+ /** Whole rather than by name, so the rollback can create each again. */
207
+ readonly indexesToDrop?: IndexSchema[];
207
208
  readonly foreignKeysToAdd?: ForeignKeySchema[];
208
209
  /** Dropped under the name the *database* reported, which is the only name a `DROP` can use. */
209
210
  readonly foreignKeysToDrop?: string[];
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.74.0",
6
+ "version": "0.74.1",
7
7
  "type": "module",
8
8
  "engines": {
9
9
  "node": ">=24"