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
@@ -30,6 +30,8 @@ export declare abstract class VectorSqlDialect extends AbstractDialect {
30
30
  readonly vectorMetrics: ReadonlyMap<VectorDistance, VectorMetric>;
31
31
  /** Whether this engine has a vector index: the one a metric's `index` names. */
32
32
  hasVectorIndex(): boolean;
33
+ /** The distance a vector index built for `metric`, as {@link vectorMetrics} names it for an index, measures. */
34
+ indexedDistance(metric: string | undefined): VectorDistance | undefined;
33
35
  /** Quotes an identifier; supplied by the SQL dialect built on top of this layer. */
34
36
  abstract escapeId(val: string | undefined, forbidQualified?: boolean, addDot?: boolean): string;
35
37
  /** What a distance expression reads, for a `$sort` and a `$near` alike. */
@@ -45,6 +45,10 @@ export class VectorSqlDialect extends AbstractDialect {
45
45
  hasVectorIndex() {
46
46
  return [...this.vectorMetrics.values()].some((metric) => metric.index);
47
47
  }
48
+ /** The distance a vector index built for `metric`, as {@link vectorMetrics} names it for an index, measures. */
49
+ indexedDistance(metric) {
50
+ return [...this.vectorMetrics].find(([, { index }]) => index === metric)?.[0];
51
+ }
48
52
  /** What a distance expression reads, for a `$sort` and a `$near` alike. */
49
53
  resolveVectorDistance(meta, key, search) {
50
54
  const field = meta.fields[key];
@@ -63,3 +63,5 @@ export declare const expr: {
63
63
  * the result needs wrapping, which MySQL demands on its large types whatever the value.
64
64
  */
65
65
  export declare function formatDefaultValue(value: unknown, dialect: AbstractSqlDialect, columnType?: string): string;
66
+ /** Whether a stored default is the declared one, as the engine reprints it: `'a'::character varying` is `'a'`. */
67
+ export declare function sameDefault(desired: unknown, current: unknown, dialect: AbstractSqlDialect): boolean;
@@ -83,6 +83,30 @@ export function formatDefaultValue(value, dialect, columnType) {
83
83
  const { wrapTypes } = DIALECT_DEFAULTS[dialect.dialectName];
84
84
  return columnType !== undefined && wrapTypes?.test(columnType) ? `(${sql})` : sql;
85
85
  }
86
+ /** Whether a stored default is the declared one, as the engine reprints it: `'a'::character varying` is `'a'`. */
87
+ export function sameDefault(desired, current, dialect) {
88
+ if (current === desired)
89
+ return true;
90
+ // Both spellings of "no default" are the same fact, and engines disagree on which they report:
91
+ // MariaDB says `null` where MySQL says nothing at all. Reading them as different values asked to
92
+ // `MODIFY` every nullable column, on every sync, forever.
93
+ if (current == null || desired == null)
94
+ return current == null && desired == null;
95
+ const normalize = (value) => {
96
+ // Render first: the desired side may be a symbolic expression, the current side is always the
97
+ // engine's own text, and `{"kind":"now"}` matches no spelling of `CURRENT_TIMESTAMP`.
98
+ const val = SqlExpression.isExpression(value) ? formatDefaultValue(value, dialect) : value;
99
+ if (typeof val === 'string') {
100
+ let s = val.replace(/::[a-z_]+(\s+[a-z_]+)*(\[\])?$/i, '');
101
+ s = s.replace(/^'(.*)'$/, '$1');
102
+ if (s.toLowerCase() === 'null')
103
+ return 'null';
104
+ return s;
105
+ }
106
+ return typeof val === 'object' ? JSON.stringify(val) : String(val);
107
+ };
108
+ return normalize(current) === normalize(desired);
109
+ }
86
110
  /**
87
111
  * Quoting is the dialect's `escape`, so a backslash in a default is escaped the way the engine reads
88
112
  * it - MySQL takes `'a\b'` as a backspace where Postgres takes it literally. Only the cases `escape`
@@ -247,7 +247,7 @@ export async function runDriftCheck(migrator, config) {
247
247
  // `unknown` and type drift compares equal, silently reporting a mismatched column as in sync.
248
248
  const report = detectDrift(expectedAST, actualAST, {
249
249
  dialect: config.pool?.dialect,
250
- indexFacets: migrator.schemaIntrospector.indexFacets,
250
+ defaultsEqual: generator.defaultsEqual,
251
251
  excludeTables: [config.tableName ?? DEFAULT_MIGRATIONS_TABLE],
252
252
  });
253
253
  printDriftReport(report);
@@ -252,8 +252,8 @@ export class EntityCodeGenerator {
252
252
  }
253
253
  const member = (param, column) => memberSource(param, this.options.propertyNameTransformer(column.name));
254
254
  const own = lowerFirst(this.options.classNameTransformer(rel.from.table.name));
255
- const key = rel.to.table.primaryKey;
256
- if (rel.from.columns.length === 1 && key.length === 1 && rel.to.columns[0].name === key[0].name) {
255
+ const key = rel.to.table.primaryKey?.columns ?? [];
256
+ if (rel.from.columns.length === 1 && key.length === 1 && rel.to.columns[0].name === key[0]) {
257
257
  return `(${own}) => ${member(own, rel.from.columns[0])}`;
258
258
  }
259
259
  const target = lowerFirst(relatedClassName);
@@ -1,8 +1,9 @@
1
1
  import type { IndexNode } from '../../schema/types.js';
2
2
  /**
3
3
  * Whether `@Field({ index })` can carry the whole index. It says only "this column is indexed under
4
- * this name", so anything else the index declares - an expression, a predicate, uniqueness, an access
5
- * method, stored columns, a stored order - has to be written out as an `@Index` instead.
4
+ * this name", and `unique` beside it that the index is unique, so anything else the index declares - an
5
+ * expression, a predicate, an access method, stored columns, a stored order - has to be written out as
6
+ * an `@Index` instead.
6
7
  */
7
8
  export declare function isPlainFieldIndex(index: IndexNode): boolean;
8
9
  /**
@@ -1,24 +1,5 @@
1
1
  import { isVectorIndexType } from '../../type/index.js';
2
2
  import { memberSource, rawTag } from './sourceLiteral.js';
3
- /**
4
- * A vector index carries its metric in the operator class pgvector names after it
5
- * (`vector_cosine_ops`), which is the only place introspection can recover it from. `@Index` requires
6
- * a `distance` beside a vector `type`, so emitting the type without one would not compile.
7
- */
8
- const DISTANCE_BY_OPS_SUFFIX = new Map([
9
- ['cosine', 'cosine'],
10
- ['l2', 'l2'],
11
- ['ip', 'inner'],
12
- ['l1', 'l1'],
13
- ]);
14
- function vectorDistance(index) {
15
- if (index.distance) {
16
- return index.distance;
17
- }
18
- const opsClass = index.entries.map((entry) => entry.opsClass).find(Boolean);
19
- const suffix = opsClass?.match(/_(\w+)_ops$/)?.[1];
20
- return suffix === undefined ? undefined : DISTANCE_BY_OPS_SUFFIX.get(suffix);
21
- }
22
3
  /**
23
4
  * The per-entry modifiers worth writing into an entity, which is not everything introspection reports.
24
5
  * Postgres states an entry in full - a plain column comes back `order: 'asc', nulls: 'last'` - and
@@ -37,8 +18,9 @@ function significantModifiers(entry) {
37
18
  }
38
19
  /**
39
20
  * Whether `@Field({ index })` can carry the whole index. It says only "this column is indexed under
40
- * this name", so anything else the index declares - an expression, a predicate, uniqueness, an access
41
- * method, stored columns, a stored order - has to be written out as an `@Index` instead.
21
+ * this name", and `unique` beside it that the index is unique, so anything else the index declares - an
22
+ * expression, a predicate, an access method, stored columns, a stored order - has to be written out as
23
+ * an `@Index` instead.
42
24
  */
43
25
  export function isPlainFieldIndex(index) {
44
26
  const entries = index.entries;
@@ -46,7 +28,6 @@ export function isPlainFieldIndex(index) {
46
28
  return (entries.length === 1 &&
47
29
  entry !== undefined &&
48
30
  !entry.expression &&
49
- !index.unique &&
50
31
  index.where === undefined &&
51
32
  // Postgres names an access method on every index, so the default one still counts as plain.
52
33
  (index.type === undefined || index.type === 'btree') &&
@@ -60,8 +41,9 @@ export function isPlainFieldIndex(index) {
60
41
  */
61
42
  export function buildIndexDecoratorSource(index, propertyName, param) {
62
43
  const entries = index.entries.map((entry) => indexEntrySource(entry, propertyName, param)).join(', ');
44
+ // `@Index` requires a `distance` beside a vector `type`, as the introspector reads it back.
63
45
  const isVector = isVectorIndexType(index.type);
64
- const distance = isVector ? vectorDistance(index) : undefined;
46
+ const distance = isVector ? index.distance : undefined;
65
47
  const options = [];
66
48
  if (index.name)
67
49
  options.push(`name: '${index.name}'`);
@@ -2,15 +2,15 @@ import type { ColumnSchema } from '../../type/index.js';
2
2
  import { TableDdl } from './tableDdl.js';
3
3
  /**
4
4
  * SQL Server keeps a column's `DEFAULT`, `CHECK` and `UNIQUE` as constraints under names it picks, and
5
- * refuses to drop or retype the column past one, so they go first - looked up by column, as no two
6
- * databases name them alike. Renames are `sp_rename`, T-SQL having no `RENAME` clause.
5
+ * refuses to drop or retype the column past one or past an index over it, so they go first - looked up
6
+ * by column, as no two databases name them alike. Renames are `sp_rename`, T-SQL having no `RENAME` clause.
7
7
  */
8
8
  export declare class MsSqlTableDdl extends TableDdl {
9
9
  /** T-SQL has no `IF NOT EXISTS` on a table, so the create is guarded by a lookup in the same statement. */
10
10
  createTable(target: string, ifNotExists: boolean): string;
11
11
  /** T-SQL rejects the optional `COLUMN` keyword after `ADD`. */
12
12
  addColumn(table: string, definition: string): string;
13
- /** Its constraints go with the column, as they do on every other engine. */
13
+ /** Its constraints and indexes go with the column, as they do on every other engine. */
14
14
  dropColumn(table: string, column: string): string[];
15
15
  /**
16
16
  * `ALTER COLUMN` takes the type and nullability alone, so the default is dropped and added back as a
@@ -21,5 +21,5 @@ export declare class MsSqlTableDdl extends TableDdl {
21
21
  renameTable(oldName: string, newName: string): string;
22
22
  storedGeneratedColumn(_type: string, expression: string): string;
23
23
  /** One statement, so a split on `;` cannot part the lookup from the `EXEC` it feeds. */
24
- private dropConstraints;
24
+ private dropPinning;
25
25
  }
@@ -1,19 +1,23 @@
1
1
  import { escapeSingleQuotes } from '../../util/sqlLiteral.js';
2
2
  import { sizedType, TableDdl } from './tableDdl.js';
3
- /** The constraints of each kind on column `c`, the `sys.columns` row {@link MsSqlTableDdl} reads. */
4
- const CONSTRAINTS_ON = {
5
- default: /*sql*/ `SELECT d.name FROM sys.default_constraints d
3
+ /** What pins column `c`, the `sys.columns` row {@link MsSqlTableDdl} reads: each constraint, then each index, by kind. */
4
+ const PINNED_BY = {
5
+ default: /*sql*/ `SELECT d.name, 0 AS is_index FROM sys.default_constraints d
6
6
  WHERE d.parent_object_id = c.object_id AND d.parent_column_id = c.column_id`,
7
- check: /*sql*/ `SELECT k.name FROM sys.check_constraints k
7
+ check: /*sql*/ `SELECT k.name, 0 AS is_index FROM sys.check_constraints k
8
8
  WHERE k.parent_object_id = c.object_id AND k.parent_column_id = c.column_id`,
9
- unique: /*sql*/ `SELECT u.name FROM sys.key_constraints u
9
+ unique: /*sql*/ `SELECT u.name, 0 AS is_index FROM sys.key_constraints u
10
10
  JOIN sys.index_columns ic ON ic.object_id = u.parent_object_id AND ic.index_id = u.unique_index_id
11
11
  WHERE u.parent_object_id = c.object_id AND u.type = 'UQ' AND ic.column_id = c.column_id`,
12
+ index: /*sql*/ `SELECT DISTINCT i.name, 1 AS is_index FROM sys.indexes i
13
+ JOIN sys.index_columns ic ON ic.object_id = i.object_id AND ic.index_id = i.index_id
14
+ WHERE i.object_id = c.object_id AND ic.column_id = c.column_id
15
+ AND i.is_primary_key = 0 AND i.is_unique_constraint = 0`,
12
16
  };
13
17
  /**
14
18
  * SQL Server keeps a column's `DEFAULT`, `CHECK` and `UNIQUE` as constraints under names it picks, and
15
- * refuses to drop or retype the column past one, so they go first - looked up by column, as no two
16
- * databases name them alike. Renames are `sp_rename`, T-SQL having no `RENAME` clause.
19
+ * refuses to drop or retype the column past one or past an index over it, so they go first - looked up
20
+ * by column, as no two databases name them alike. Renames are `sp_rename`, T-SQL having no `RENAME` clause.
17
21
  */
18
22
  export class MsSqlTableDdl extends TableDdl {
19
23
  /** T-SQL has no `IF NOT EXISTS` on a table, so the create is guarded by a lookup in the same statement. */
@@ -25,9 +29,9 @@ export class MsSqlTableDdl extends TableDdl {
25
29
  addColumn(table, definition) {
26
30
  return /*sql*/ `ALTER TABLE ${this.dialect.escapeId(table)} ADD ${definition};`;
27
31
  }
28
- /** Its constraints go with the column, as they do on every other engine. */
32
+ /** Its constraints and indexes go with the column, as they do on every other engine. */
29
33
  dropColumn(table, column) {
30
- return [this.dropConstraints(table, column, Object.values(CONSTRAINTS_ON)), ...super.dropColumn(table, column)];
34
+ return [this.dropPinning(table, column, Object.values(PINNED_BY)), ...super.dropColumn(table, column)];
31
35
  }
32
36
  /**
33
37
  * `ALTER COLUMN` takes the type and nullability alone, so the default is dropped and added back as a
@@ -37,7 +41,7 @@ export class MsSqlTableDdl extends TableDdl {
37
41
  const target = this.dialect.escapeId(table);
38
42
  const name = this.dialect.escapeId(column.name);
39
43
  const statements = [
40
- this.dropConstraints(table, column.name, [CONSTRAINTS_ON.default]),
44
+ this.dropPinning(table, column.name, [PINNED_BY.default]),
41
45
  /*sql*/ `ALTER TABLE ${target} ALTER COLUMN ${name} ${sizedType(column)} ${column.nullable ? 'NULL' : 'NOT NULL'};`,
42
46
  ];
43
47
  if (column.defaultValue !== undefined) {
@@ -56,10 +60,12 @@ export class MsSqlTableDdl extends TableDdl {
56
60
  return /*sql*/ `AS (${expression}) PERSISTED`;
57
61
  }
58
62
  /** One statement, so a split on `;` cannot part the lookup from the `EXEC` it feeds. */
59
- dropConstraints(table, column, kinds) {
60
- const target = this.dialect.escapeId(table);
61
- return (`DECLARE @drop nvarchar(max) = (SELECT STRING_AGG(N'ALTER TABLE ${escapeSingleQuotes(target)} DROP CONSTRAINT ' ` +
62
- `+ QUOTENAME(pinned.name), N'; ') FROM sys.columns c CROSS APPLY (${kinds.join(' UNION ALL ')}) pinned ` +
63
+ dropPinning(table, column, kinds) {
64
+ const target = escapeSingleQuotes(this.dialect.escapeId(table));
65
+ return (`DECLARE @drop nvarchar(max) = (SELECT STRING_AGG(CASE pinned.is_index ` +
66
+ `WHEN 1 THEN N'DROP INDEX ' + QUOTENAME(pinned.name) + N' ON ${target}' ` +
67
+ `ELSE N'ALTER TABLE ${target} DROP CONSTRAINT ' + QUOTENAME(pinned.name) END, N'; ') ` +
68
+ `FROM sys.columns c CROSS APPLY (${kinds.join(' UNION ALL ')}) pinned ` +
63
69
  `WHERE c.object_id = OBJECT_ID(${this.dialect.escape(target)}) AND c.name = ${this.dialect.escape(column)}) ` +
64
70
  'EXEC (@drop);');
65
71
  }
@@ -48,8 +48,8 @@ export declare class MariaIndexDdl extends MysqlLikeIndexDdl {
48
48
  protected readonly indexTypeHints: Map<"brin" | "btree" | "fulltext" | "gin" | "gist" | "hash" | "hnsw" | "ivfflat" | "vector" | "vectorSearch", string>;
49
49
  /**
50
50
  * `M=n DISTANCE=metric`, trailing its `CREATE VECTOR INDEX`. The metric names are MariaDB's own
51
- * (`euclidean`, not `l2`), and an unsupported one throws rather than being dropped, which would
52
- * silently build the index on euclidean - its default - instead of what the entity asked for.
51
+ * (`euclidean`, not `l2`), stated even for the default distance, cosine, since MariaDB's own is
52
+ * euclidean; and an unsupported one throws rather than silently building on euclidean.
53
53
  */
54
54
  protected indexTuning(index: IndexSchema): string;
55
55
  }
@@ -1,5 +1,5 @@
1
1
  import { jsonTypeMode } from '../../dialect/jsonSql.js';
2
- import { unsupportedVectorMetric, VECTOR_INDEX_TYPES } from '../../type/vector.js';
2
+ import { indexDistance, isVectorIndexType, unsupportedVectorMetric, VECTOR_INDEX_TYPES } from '../../type/vector.js';
3
3
  import { IndexDdl } from './indexDdl.js';
4
4
  /**
5
5
  * A full-text index is its own keyword here (`CREATE FULLTEXT INDEX ... (cols)`); `USING fulltext` is
@@ -84,15 +84,16 @@ export class MariaIndexDdl extends MysqlLikeIndexDdl {
84
84
  ]);
85
85
  /**
86
86
  * `M=n DISTANCE=metric`, trailing its `CREATE VECTOR INDEX`. The metric names are MariaDB's own
87
- * (`euclidean`, not `l2`), and an unsupported one throws rather than being dropped, which would
88
- * silently build the index on euclidean - its default - instead of what the entity asked for.
87
+ * (`euclidean`, not `l2`), stated even for the default distance, cosine, since MariaDB's own is
88
+ * euclidean; and an unsupported one throws rather than silently building on euclidean.
89
89
  */
90
90
  indexTuning(index) {
91
91
  let tuning = super.indexTuning(index) + (index.m === undefined ? '' : ` M=${index.m}`);
92
- if (index.distance) {
93
- const metric = this.dialect.vectorMetrics.get(index.distance)?.index;
92
+ if (isVectorIndexType(index.type)) {
93
+ const distance = indexDistance(index);
94
+ const metric = this.dialect.vectorMetrics.get(distance)?.index;
94
95
  if (!metric) {
95
- throw unsupportedVectorMetric(this.dialect.dialectName, index.distance, index.name);
96
+ throw unsupportedVectorMetric(this.dialect.dialectName, distance, index.name);
96
97
  }
97
98
  tuning += ` DISTANCE=${metric}`;
98
99
  }
@@ -10,7 +10,8 @@ export declare class PgIndexDdl extends IndexDdl {
10
10
  protected indexAccessMethod(index: IndexSchema): string;
11
11
  /**
12
12
  * A vector index's operator class, `{type}_{metric}_ops` (`halfvec_cosine_ops`), refusing a metric it
13
- * lacks rather than build with the default; any other entry takes the class it declares.
13
+ * lacks rather than build with the default; any other entry takes the class it declares. Stated even
14
+ * for the default distance: pgvector's own default class is L2, which a cosine search never uses.
14
15
  */
15
16
  protected indexColumnOpsClass(entry: IndexColumnSchema, index: IndexSchema): string;
16
17
  protected indexInclude(index: IndexSchema): string;
@@ -1,4 +1,4 @@
1
- import { unsupportedVectorMetric } from '../../type/vector.js';
1
+ import { indexDistance, unsupportedVectorMetric } from '../../type/vector.js';
2
2
  import { IndexDdl } from './indexDdl.js';
3
3
  /** `CREATE INDEX ... USING hnsw ("embedding" vector_cosine_ops) WITH (m = ...)`, pgvector's form. */
4
4
  export class PgIndexDdl extends IndexDdl {
@@ -33,20 +33,22 @@ export class PgIndexDdl extends IndexDdl {
33
33
  }
34
34
  /**
35
35
  * A vector index's operator class, `{type}_{metric}_ops` (`halfvec_cosine_ops`), refusing a metric it
36
- * lacks rather than build with the default; any other entry takes the class it declares.
36
+ * lacks rather than build with the default; any other entry takes the class it declares. Stated even
37
+ * for the default distance: pgvector's own default class is L2, which a cosine search never uses.
37
38
  */
38
39
  indexColumnOpsClass(entry, index) {
39
- if (!this.isVectorIndex(index) || !index.distance) {
40
+ if (!this.isVectorIndex(index)) {
40
41
  return entry.opsClass ? ` ${entry.opsClass}` : '';
41
42
  }
42
- const metric = this.dialect.vectorMetrics.get(index.distance)?.index;
43
+ const distance = indexDistance(index);
44
+ const metric = this.dialect.vectorMetrics.get(distance)?.index;
43
45
  if (!metric) {
44
- throw unsupportedVectorMetric(this.dialect.dialectName, index.distance, index.name);
46
+ throw unsupportedVectorMetric(this.dialect.dialectName, distance, index.name);
45
47
  }
46
48
  const vectorType = this.dialect.supportedVectorType(index.vectorType ?? 'vector');
47
49
  const opsClass = `${vectorType}_${metric}_ops`;
48
50
  // IVFFlat has neither a sparsevec nor an L1 operator class; HNSW has all of them (pgvector 0.8.2).
49
- if (index.type === 'ivfflat' && (vectorType === 'sparsevec' || index.distance === 'l1')) {
51
+ if (index.type === 'ivfflat' && (vectorType === 'sparsevec' || distance === 'l1')) {
50
52
  throw new TypeError(`ivfflat has no ${opsClass} operator class (index "${index.name}"); use hnsw`);
51
53
  }
52
54
  return ` ${opsClass}`;
@@ -18,9 +18,10 @@ export declare class TableDdl {
18
18
  dropColumn(table: string, column: string): string[];
19
19
  /**
20
20
  * What changes `column` to what it now declares. `definition` is the whole column, which MySQL's
21
- * `MODIFY COLUMN` restates; Postgres takes each change as a clause of its own.
21
+ * `MODIFY COLUMN` restates; Postgres takes each change as a clause of its own, so given what the
22
+ * column was (`from`), only the clauses that changed.
22
23
  */
23
- alterColumn(table: string, column: ColumnSchema, definition: string): string[];
24
+ alterColumn(table: string, column: ColumnSchema, definition: string, from?: ColumnSchema): string[];
24
25
  renameColumn(table: string, oldName: string, newName: string): string;
25
26
  renameTable(oldName: string, newName: string): string;
26
27
  /** A stored generated column's type, with the clause computing it. */
@@ -1,4 +1,4 @@
1
- import { formatDefaultValue } from '../builder/expressions.js';
1
+ import { formatDefaultValue, sameDefault } from '../builder/expressions.js';
2
2
  /**
3
3
  * A column's type with the size it was read back with, unless its spelling already carries one:
4
4
  * introspection reports `VARCHAR` and `255` apart, where a type from an entity is already whole.
@@ -34,9 +34,10 @@ export class TableDdl {
34
34
  }
35
35
  /**
36
36
  * What changes `column` to what it now declares. `definition` is the whole column, which MySQL's
37
- * `MODIFY COLUMN` restates; Postgres takes each change as a clause of its own.
37
+ * `MODIFY COLUMN` restates; Postgres takes each change as a clause of its own, so given what the
38
+ * column was (`from`), only the clauses that changed.
38
39
  */
39
- alterColumn(table, column, definition) {
40
+ alterColumn(table, column, definition, from) {
40
41
  if (this.dialect.alterColumnSyntax === 'none') {
41
42
  throw new TypeError(`${this.dialect}: Cannot alter column "${column.name}" - you must recreate the table. ` +
42
43
  `This database does not support ALTER COLUMN.`);
@@ -47,10 +48,11 @@ export class TableDdl {
47
48
  }
48
49
  const alter = `ALTER TABLE ${target} ALTER COLUMN ${this.dialect.escapeId(column.name)}`;
49
50
  return [
50
- `${alter} TYPE ${column.type};`,
51
- `${alter} ${column.nullable ? 'DROP NOT NULL' : 'SET NOT NULL'};`,
52
- column.defaultValue === undefined ? `${alter} DROP DEFAULT;` : `${alter} SET${this.defaultClause(column)};`,
53
- ];
51
+ (!from || from.type !== column.type) && `${alter} TYPE ${column.type};`,
52
+ (!from || from.nullable !== column.nullable) && `${alter} ${column.nullable ? 'DROP NOT NULL' : 'SET NOT NULL'};`,
53
+ (!from || !sameDefault(column.defaultValue, from.defaultValue, this.dialect)) &&
54
+ (column.defaultValue === undefined ? `${alter} DROP DEFAULT;` : `${alter} SET${this.defaultClause(column)};`),
55
+ ].filter((statement) => statement !== false);
54
56
  }
55
57
  renameColumn(table, oldName, newName) {
56
58
  const [target, from, to] = [table, oldName, newName].map((name) => this.dialect.escapeId(name));
@@ -5,7 +5,6 @@
5
5
  * actual database schema.
6
6
  */
7
7
  import type { AbstractDialect } from '../../dialect/abstractDialect.js';
8
- import type { IndexFacet } from '../../schema/indexDifferences.js';
9
8
  import type { SchemaAST } from '../../schema/schemaAST.js';
10
9
  import type { DriftReport } from '../../schema/types.js';
11
10
  /**
@@ -18,15 +17,15 @@ export interface DriftDetectorOptions {
18
17
  checkNullable?: boolean;
19
18
  /** Include index differences */
20
19
  checkIndexes?: boolean;
21
- /** `indexFacets` of the introspector that produced the actual schema; anything else goes uncompared. */
22
- indexFacets?: ReadonlySet<IndexFacet>;
23
20
  /** Include foreign key differences */
24
21
  checkForeignKeys?: boolean;
25
22
  /**
26
- * Include default value differences. Off by default: an engine reports a default as it stored it
27
- * (`now()`, `CURRENT_TIMESTAMP`, `'active'::text`), which rarely matches the entity's literal.
23
+ * Include default value differences. Off unless {@link defaultsEqual} is given: without it defaults
24
+ * compare as written, and an engine reports one as it stored it (`now()`, `'active'::text`).
28
25
  */
29
26
  checkDefaults?: boolean;
27
+ /** How the engine's generator compares a default, so drift reports the ones a migration would change. */
28
+ defaultsEqual?: (expected: unknown, actual: unknown) => boolean;
30
29
  /**
31
30
  * Tables to leave out of the comparison. The migrations bookkeeping table belongs here - it exists in
32
31
  * the database by design and has no entity, so reporting it as unexpected told every project to
@@ -5,15 +5,15 @@
5
5
  * actual database schema.
6
6
  */
7
7
  import { canonicalToSql, engineType } from '../../schema/canonicalType.js';
8
- import { diffSchemas, referentialActions } from '../../schema/schemaASTDiffer.js';
8
+ import { defaultsEqualAsWritten, diffSchemas, referentialActions } from '../../schema/schemaASTDiffer.js';
9
9
  function resolveOptions(options) {
10
10
  return {
11
11
  checkTypes: options.checkTypes ?? true,
12
12
  checkNullable: options.checkNullable ?? true,
13
13
  checkIndexes: options.checkIndexes ?? true,
14
- indexFacets: options.indexFacets ?? new Set(),
15
14
  checkForeignKeys: options.checkForeignKeys ?? true,
16
- checkDefaults: options.checkDefaults ?? false,
15
+ checkDefaults: options.checkDefaults ?? options.defaultsEqual !== undefined,
16
+ defaultsEqual: options.defaultsEqual ?? defaultsEqualAsWritten,
17
17
  excludeTables: options.excludeTables ?? [],
18
18
  dialect: options.dialect,
19
19
  };
@@ -27,9 +27,9 @@ export function detectDrift(expectedAST, actualAST, options = {}) {
27
27
  const { dialect } = opts;
28
28
  const diff = diffSchemas(expectedAST, actualAST, {
29
29
  compareIndexes: opts.checkIndexes,
30
- indexFacets: opts.indexFacets,
31
30
  compareRelationships: opts.checkForeignKeys,
32
31
  excludeTables: opts.excludeTables,
32
+ defaultsEqual: opts.defaultsEqual,
33
33
  // Without a dialect there is no engine to compare through, and `formatType` below then reports no
34
34
  // type drift at all.
35
35
  ...(dialect && { normalizeType: engineType(dialect) }),
@@ -59,10 +59,14 @@ function detectPrimaryKeyDrifts(diff) {
59
59
  type: 'constraint_mismatch',
60
60
  severity: 'critical',
61
61
  table: pkDiff.table,
62
- details: `Primary key of "${pkDiff.table}" is (${pkDiff.actual.join(', ') || 'none'}) in the database but (${pkDiff.expected.join(', ') || 'none'}) in the entity`,
62
+ details: `Primary key of "${pkDiff.table}" is (${keyColumns(pkDiff.actual)}) in the database but (${keyColumns(pkDiff.expected)}) in the entity`,
63
63
  suggestion: 'Generate a migration to change the primary key',
64
64
  }));
65
65
  }
66
+ /** A key's columns as a drift names them, `none` where there is no key. */
67
+ function keyColumns(key) {
68
+ return key?.columns.join(', ') || 'none';
69
+ }
66
70
  /**
67
71
  * Detect table-level drifts (missing/unexpected tables).
68
72
  */
@@ -121,7 +125,7 @@ function detectColumnDrifts(diff, opts) {
121
125
  return drifts;
122
126
  }
123
127
  /**
124
- * Add drifts for column alterations (type/nullable mismatches).
128
+ * Add drifts for column alterations: type, nullability and default.
125
129
  */
126
130
  function addAlterColumnDrifts(colDiff, drifts, opts) {
127
131
  // An auto-increment key is created through the dialect's `serialPrimaryKey`, whose spelling the
@@ -159,21 +163,17 @@ function addAlterColumnDrifts(colDiff, drifts, opts) {
159
163
  suggestion: 'Align nullable setting in entity or database',
160
164
  });
161
165
  }
162
- if (opts.checkDefaults) {
163
- const expected = String(colDiff.expected.defaultValue ?? 'NULL');
164
- const actual = String(colDiff.actual.defaultValue ?? 'NULL');
165
- if (expected !== actual) {
166
- drifts.push({
167
- type: 'constraint_mismatch',
168
- severity: 'info',
169
- table: colDiff.table,
170
- column: colDiff.column,
171
- expected,
172
- actual,
173
- details: `Default mismatch for "${colDiff.column}"`,
174
- suggestion: 'Align the default in the entity or the database',
175
- });
176
- }
166
+ if (opts.checkDefaults && !opts.defaultsEqual(colDiff.expected.defaultValue, colDiff.actual.defaultValue)) {
167
+ drifts.push({
168
+ type: 'constraint_mismatch',
169
+ severity: 'info',
170
+ table: colDiff.table,
171
+ column: colDiff.column,
172
+ expected: String(colDiff.expected.defaultValue ?? 'NULL'),
173
+ actual: String(colDiff.actual.defaultValue ?? 'NULL'),
174
+ details: `Default mismatch for "${colDiff.column}"`,
175
+ suggestion: 'Align the default in the entity or the database',
176
+ });
177
177
  }
178
178
  }
179
179
  /**
@@ -14,7 +14,7 @@ export declare function tableDefinitionToNode(def: TableDefinition, render: (sql
14
14
  */
15
15
  export declare function fullColumnDefinitionToNode(col: FullColumnDefinition, tableName: string): ColumnNode;
16
16
  /**
17
- * The index a column-level `index` declares, or nothing.
17
+ * The index a column-level `index` or `unique` declares, or nothing: a unique column is a unique index.
18
18
  *
19
19
  * Shared with `TableBuilder.build`, which lifts these into the table it is creating: written twice,
20
20
  * `addColumn` had no lift at all and silently emitted a column with no index.
@@ -1,3 +1,4 @@
1
+ import { createTableNode, keyOfColumns } from '../../schema/schemaAST.js';
1
2
  import { renderIndexColumn } from '../../util/ddlExpression.util.js';
2
3
  import { derivedForeignKeyName, derivedIndexName } from '../../util/sql.util.js';
3
4
  /** A table the builder names but has not seen, which the generator reads only the name of. */
@@ -10,29 +11,17 @@ function unresolvedTable(name) {
10
11
  * `render`. Free functions and not generator methods: the dialect reaches them only through `render`.
11
12
  */
12
13
  export function tableDefinitionToNode(def, render) {
13
- const columns = new Map();
14
- const pkNodes = [];
15
- const table = {
16
- name: def.name,
17
- columns,
18
- primaryKey: [], // placeholder
19
- indexes: [],
20
- incomingRelations: [],
21
- outgoingRelations: [],
22
- comment: def.comment,
23
- };
14
+ const table = { ...createTableNode(def.name), comment: def.comment };
15
+ const { columns } = table;
24
16
  for (const colDef of def.columns) {
25
17
  const node = fullColumnDefinitionToNode(colDef, def.name);
26
18
  node.table = table;
27
19
  columns.set(node.name, node);
28
- if (node.isPrimaryKey) {
29
- pkNodes.push(node);
30
- }
31
20
  }
32
- const finalPrimaryKey = def.primaryKey
33
- ? def.primaryKey.map((name) => columns.get(name)).filter((c) => c !== undefined)
34
- : pkNodes;
35
- table.primaryKey = finalPrimaryKey;
21
+ // A declared key keeps only the columns the table has, in its own order.
22
+ table.primaryKey = def.primaryKey
23
+ ? { columns: def.primaryKey.filter((name) => columns.has(name)) }
24
+ : keyOfColumns(columns.values());
36
25
  for (const idxDef of def.indexes) {
37
26
  table.indexes.push({ ...renderIndexDefinition(idxDef, render), table });
38
27
  }
@@ -64,13 +53,13 @@ export function fullColumnDefinitionToNode(col, tableName) {
64
53
  return { ...column, table: unresolvedTable(tableName), referencedBy: [] };
65
54
  }
66
55
  /**
67
- * The index a column-level `index` declares, or nothing.
56
+ * The index a column-level `index` or `unique` declares, or nothing: a unique column is a unique index.
68
57
  *
69
58
  * Shared with `TableBuilder.build`, which lifts these into the table it is creating: written twice,
70
59
  * `addColumn` had no lift at all and silently emitted a column with no index.
71
60
  */
72
61
  export function columnIndex(tableName, col) {
73
- if (!col.index) {
62
+ if (!col.index && !col.isUnique) {
74
63
  return undefined;
75
64
  }
76
65
  return {
@@ -31,8 +31,8 @@ export declare class MongoSchemaGenerator extends MongoDialect implements Schema
31
31
  ifNotExists?: boolean;
32
32
  }): string[];
33
33
  generateDropTable(tableName: string): string;
34
+ /** A collection's indexes: each dropped, then each created, an alter as both. */
34
35
  generateAlterTable(diff: SchemaDiff): string[];
35
- generateAlterTableDown(diff: SchemaDiff): string[];
36
36
  /** MongoDB has no triggers, and a write to an entity declaring one is refused, so there is none to reconcile. */
37
37
  generateTriggers(): string[];
38
38
  generateTriggersDown(): string[];