uql-orm 0.80.0 → 0.82.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 (123) hide show
  1. package/README.md +3 -3
  2. package/dist/browser/querier/httpQuerier.d.ts +2 -2
  3. package/dist/browser/querier/httpQuerier.js +2 -1
  4. package/dist/browser/type/clientQuerier.d.ts +2 -2
  5. package/dist/browser/uql-browser.min.js +2 -2
  6. package/dist/browser/uql-browser.min.js.map +9 -8
  7. package/dist/bunSql/bunSql.util.js +2 -1
  8. package/dist/dialect/abstractSqlDialect.d.ts +15 -5
  9. package/dist/dialect/abstractSqlDialect.js +109 -40
  10. package/dist/dialect/operators.d.ts +7 -1
  11. package/dist/dialect/operators.js +13 -1
  12. package/dist/dialect/vectorSqlDialect.d.ts +2 -0
  13. package/dist/dialect/vectorSqlDialect.js +4 -0
  14. package/dist/entity/metadata/definition.d.ts +1 -2
  15. package/dist/entity/metadata/definition.js +37 -39
  16. package/dist/http/handler.js +5 -4
  17. package/dist/http/query.d.ts +1 -1
  18. package/dist/http/query.js +2 -2
  19. package/dist/index.d.ts +1 -0
  20. package/dist/index.js +1 -0
  21. package/dist/migrate/acquireQuerierForMigrations.js +2 -1
  22. package/dist/migrate/assertCliConfig.js +7 -6
  23. package/dist/migrate/bin.js +0 -0
  24. package/dist/migrate/builder/expressions.d.ts +2 -0
  25. package/dist/migrate/builder/expressions.js +26 -1
  26. package/dist/migrate/cli-config.js +5 -4
  27. package/dist/migrate/cli.js +1 -1
  28. package/dist/migrate/codegen/entityCodeGenerator.js +2 -2
  29. package/dist/migrate/codegen/indexDecoratorSource.d.ts +3 -2
  30. package/dist/migrate/codegen/indexDecoratorSource.js +5 -23
  31. package/dist/migrate/ddl/indexDdl.js +4 -3
  32. package/dist/migrate/ddl/mssqlTableDdl.d.ts +4 -4
  33. package/dist/migrate/ddl/mssqlTableDdl.js +20 -14
  34. package/dist/migrate/ddl/mysqlIndexDdl.d.ts +2 -2
  35. package/dist/migrate/ddl/mysqlIndexDdl.js +12 -10
  36. package/dist/migrate/ddl/pgIndexDdl.d.ts +2 -1
  37. package/dist/migrate/ddl/pgIndexDdl.js +10 -7
  38. package/dist/migrate/ddl/sqliteIndexDdl.js +2 -1
  39. package/dist/migrate/ddl/tableDdl.d.ts +3 -2
  40. package/dist/migrate/ddl/tableDdl.js +11 -8
  41. package/dist/migrate/drift/driftDetector.d.ts +4 -5
  42. package/dist/migrate/drift/driftDetector.js +21 -21
  43. package/dist/migrate/generator/definitionToNode.d.ts +1 -1
  44. package/dist/migrate/generator/definitionToNode.js +9 -20
  45. package/dist/migrate/generator/mongoCommand.js +2 -1
  46. package/dist/migrate/generator/mongoSchemaGenerator.d.ts +1 -1
  47. package/dist/migrate/generator/mongoSchemaGenerator.js +17 -24
  48. package/dist/migrate/index.d.ts +2 -1
  49. package/dist/migrate/index.js +1 -0
  50. package/dist/migrate/indexPredicate.js +2 -1
  51. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.d.ts +5 -7
  52. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +13 -19
  53. package/dist/migrate/introspection/baseSqlIntrospector.js +7 -18
  54. package/dist/migrate/introspection/mongoIntrospector.d.ts +1 -1
  55. package/dist/migrate/introspection/mongoIntrospector.js +5 -4
  56. package/dist/migrate/introspection/mssqlIntrospector.js +2 -1
  57. package/dist/migrate/introspection/mysqlIntrospector.d.ts +15 -5
  58. package/dist/migrate/introspection/mysqlIntrospector.js +32 -4
  59. package/dist/migrate/introspection/postgresIntrospector.d.ts +29 -21
  60. package/dist/migrate/introspection/postgresIntrospector.js +63 -46
  61. package/dist/migrate/introspection/sqliteIntrospector.js +11 -9
  62. package/dist/migrate/migrationTarget.js +2 -1
  63. package/dist/migrate/migrator.d.ts +5 -0
  64. package/dist/migrate/migrator.js +35 -45
  65. package/dist/migrate/schemaChange.d.ts +18 -0
  66. package/dist/migrate/schemaChange.js +37 -0
  67. package/dist/migrate/schemaGenerator.d.ts +13 -14
  68. package/dist/migrate/schemaGenerator.js +88 -181
  69. package/dist/migrate/triggerSql.d.ts +1 -1
  70. package/dist/migrate/triggerSql.js +77 -61
  71. package/dist/mongo/mongoDialect.d.ts +1 -3
  72. package/dist/mongo/mongoDialect.js +9 -14
  73. package/dist/mongo/mongodbQuerier.js +3 -7
  74. package/dist/querier/abstractQuerier.d.ts +9 -4
  75. package/dist/querier/abstractQuerier.js +26 -19
  76. package/dist/querier/abstractQuerierPool.d.ts +3 -3
  77. package/dist/querier/abstractSqlQuerier.d.ts +2 -2
  78. package/dist/querier/abstractSqlQuerier.js +1 -1
  79. package/dist/querier/abstractSqlQuerierPool.d.ts +2 -2
  80. package/dist/querier/queryError.d.ts +2 -2
  81. package/dist/schema/indexDifferences.d.ts +22 -6
  82. package/dist/schema/indexDifferences.js +23 -8
  83. package/dist/schema/matchByKey.d.ts +10 -0
  84. package/dist/schema/matchByKey.js +18 -0
  85. package/dist/schema/schemaAST.d.ts +6 -2
  86. package/dist/schema/schemaAST.js +7 -3
  87. package/dist/schema/schemaASTBuilder.d.ts +2 -0
  88. package/dist/schema/schemaASTBuilder.js +17 -11
  89. package/dist/schema/schemaASTDiffer.d.ts +2 -3
  90. package/dist/schema/schemaASTDiffer.js +15 -36
  91. package/dist/schema/types.d.ts +14 -15
  92. package/dist/type/dialect.d.ts +23 -1
  93. package/dist/type/entity.d.ts +15 -11
  94. package/dist/type/logger.d.ts +2 -2
  95. package/dist/type/migration.d.ts +32 -50
  96. package/dist/type/querier.d.ts +3 -3
  97. package/dist/type/query.d.ts +3 -13
  98. package/dist/type/queryAggregate.d.ts +4 -10
  99. package/dist/type/queryRaw.d.ts +17 -3
  100. package/dist/type/queryRaw.js +2 -1
  101. package/dist/type/queryWhere.d.ts +7 -7
  102. package/dist/type/universalQuerier.d.ts +3 -3
  103. package/dist/type/vector.d.ts +2 -1
  104. package/dist/type/vector.js +2 -1
  105. package/dist/util/ddlExpression.util.d.ts +5 -1
  106. package/dist/util/ddlExpression.util.js +6 -2
  107. package/dist/util/dialect.util.d.ts +13 -5
  108. package/dist/util/dialect.util.js +28 -20
  109. package/dist/util/field.util.d.ts +4 -4
  110. package/dist/util/field.util.js +10 -2
  111. package/dist/util/hook.util.d.ts +1 -1
  112. package/dist/util/hook.util.js +8 -1
  113. package/dist/util/index.d.ts +1 -0
  114. package/dist/util/index.js +1 -0
  115. package/dist/util/logger.d.ts +3 -3
  116. package/dist/util/object.util.js +3 -2
  117. package/dist/util/raw.d.ts +6 -7
  118. package/dist/util/raw.js +10 -12
  119. package/dist/util/sqlLiteral.js +3 -2
  120. package/dist/util/triggerWrite.d.ts +15 -0
  121. package/dist/util/triggerWrite.js +20 -0
  122. package/package.json +1 -1
  123. package/skills/uql-orm/SKILL.md +3 -3
@@ -9,11 +9,11 @@ export class MysqlSchemaIntrospector extends AbstractSqlSchemaIntrospector {
9
9
  defaultSchemaExpr = 'DATABASE()';
10
10
  triggersQuery() {
11
11
  return /*sql*/ `
12
- SELECT EVENT_OBJECT_TABLE AS \`table\`, TRIGGER_NAME AS name,
12
+ SELECT TRIGGER_NAME AS name,
13
13
  CONCAT('CREATE TRIGGER \`', TRIGGER_SCHEMA, '\`.\`', TRIGGER_NAME, '\` ', ACTION_TIMING, ' ', EVENT_MANIPULATION,
14
14
  ' ON \`', EVENT_OBJECT_SCHEMA, '\`.\`', EVENT_OBJECT_TABLE, '\` FOR EACH ROW ', ACTION_STATEMENT) AS definition
15
15
  FROM information_schema.TRIGGERS
16
- WHERE TRIGGER_SCHEMA = ${this.schemaExpr}
16
+ WHERE TRIGGER_SCHEMA = ${this.schemaExpr} AND EVENT_OBJECT_TABLE = ${this.dialect.placeholder(1)}
17
17
  `;
18
18
  }
19
19
  getTableNamesQuery() {
@@ -62,7 +62,8 @@ export class MysqlSchemaIntrospector extends AbstractSqlSchemaIntrospector {
62
62
  SELECT
63
63
  INDEX_NAME as index_name,
64
64
  GROUP_CONCAT(COALESCE(COLUMN_NAME, '') ORDER BY SEQ_IN_INDEX) as columns,
65
- NOT NON_UNIQUE as is_unique
65
+ NOT NON_UNIQUE as is_unique,
66
+ MAX(INDEX_TYPE) as method
66
67
  FROM information_schema.STATISTICS
67
68
  WHERE TABLE_SCHEMA = ${this.schemaExpr}
68
69
  AND TABLE_NAME = ?
@@ -110,7 +111,8 @@ export class MysqlSchemaIntrospector extends AbstractSqlSchemaIntrospector {
110
111
  isPrimaryKey: row.column_key === 'PRI',
111
112
  isAutoIncrement: row.extra.toLowerCase().includes('auto_increment'),
112
113
  isUnique: row.column_key === 'UNI',
113
- length: this.toNumber(row.character_maximum_length),
114
+ // A `VECTOR`'s is its bytes, four a dimension, which `column_type` already states as dimensions.
115
+ length: /^vector/i.test(row.column_type) ? undefined : this.toNumber(row.character_maximum_length),
114
116
  precision: this.toNumber(row.numeric_precision),
115
117
  scale: this.toNumber(row.numeric_scale),
116
118
  comment: row.column_comment || undefined,
@@ -120,6 +122,7 @@ export class MysqlSchemaIntrospector extends AbstractSqlSchemaIntrospector {
120
122
  async mapIndexesResult(_read, _tableName, results) {
121
123
  return results.map((row) => ({
122
124
  name: row.index_name,
125
+ ...(row.method === 'VECTOR' && { type: 'vector' }),
123
126
  // A functional or multi-valued key part has no `COLUMN_NAME` - the `COALESCE` above keeps its
124
127
  // place in the list, and it is reported as the expression it is, which is what stops diffing
125
128
  // from comparing an entry list the server cannot state against the entity's own.
@@ -164,6 +167,31 @@ export class MysqlSchemaIntrospector extends AbstractSqlSchemaIntrospector {
164
167
  * JSON, got LONGTEXT", flagged as data loss) on a table uql created itself.
165
168
  */
166
169
  export class MariadbSchemaIntrospector extends MysqlSchemaIntrospector {
170
+ /** Whether an index is MariaDB's vector index, and the distance it was built for. */
171
+ indexFacets = new Set(['vector', 'distance']);
172
+ /**
173
+ * A vector index's distance is kept only in the table's own definition, ``VECTOR KEY `ix` (`vec`)
174
+ * `DISTANCE`='cosine'``, and left out there for MariaDB's default, euclidean.
175
+ */
176
+ async mapIndexesResult(read, tableName, results) {
177
+ const indexes = await super.mapIndexesResult(read, tableName, results);
178
+ if (!indexes.some((index) => index.type === 'vector')) {
179
+ return indexes;
180
+ }
181
+ const qualified = [this.schema, tableName]
182
+ .filter((name) => name !== undefined)
183
+ .map((name) => this.dialect.escapeId(name));
184
+ const [row] = await read(/*sql*/ `SHOW CREATE TABLE ${qualified.join('.')}`);
185
+ const lines = row['Create Table'].split('\n');
186
+ return indexes.map((index) => {
187
+ if (index.type !== 'vector') {
188
+ return index;
189
+ }
190
+ const line = lines.find((it) => it.includes(`VECTOR KEY \`${index.name}\``));
191
+ const metric = line?.match(/`DISTANCE`='(\w+)'/)?.[1] ?? 'euclidean';
192
+ return { ...index, distance: this.dialect.indexedDistance(metric) };
193
+ });
194
+ }
167
195
  async mapColumnsResult(read, tableName, results) {
168
196
  const columns = await super.mapColumnsResult(read, tableName, results);
169
197
  const checks = await read(
@@ -22,7 +22,11 @@ export declare class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrosp
22
22
  * database scans meet that table every time something else is migrating.
23
23
  *
24
24
  * `attgenerated` rather than `is_generated`, which cannot part a stored generated column from the
25
- * virtual one Postgres 18 added and uql never declares. CockroachDB states it too.
25
+ * virtual one Postgres 18 added and uql never declares. CockroachDB states it too. `format_type` for an
26
+ * extension type's modifier, which `information_schema` drops: a `vector(256)` read back as `vector`.
27
+ * CockroachDB names that type `vector` where Postgres says `USER-DEFINED`.
28
+ *
29
+ * A column is unique by a unique index over it alone, a constraint's or its own, as every engine reads it.
26
30
  */
27
31
  protected getColumnsQuery(_tableName: string): string;
28
32
  /**
@@ -32,24 +36,27 @@ export declare class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrosp
32
36
  * while `pg_get_indexdef` reprints an identifier *quoted*, so a camelCase column came back as
33
37
  * `"tenantId"` and matched no column of the table. Prisma and drizzle-kit both split it this way.
34
38
  *
35
- * Indexes backing a constraint are left out, primary keys among them: `@Field({ unique })` emits a
36
- * `UNIQUE` constraint and no index, so reporting the index Postgres builds underneath it told every
37
- * project it had an index its entities never asked for.
39
+ * The key's index and an `EXCLUDE`'s are left out. A `UNIQUE` constraint's index stays, as SQL Server
40
+ * and the MySQL family report theirs: the diff reads one over a single column as that column's
41
+ * uniqueness, and one over several as the unique `@Index` it is.
38
42
  */
39
43
  protected getIndexesQuery(_tableName: string): string;
40
- /**
41
- * Constraint kinds whose backing index is the constraint itself rather than an index anyone asked
42
- * for. Postgres builds one for `PRIMARY KEY`, `UNIQUE` and `EXCLUDE`, and only for those: a plain
43
- * `CREATE UNIQUE INDEX` has no `pg_constraint` row at all, so it survives.
44
- *
45
- * Nothing to do with {@link indexFacets}, which says which *attributes* of an index diffing may
46
- * compare. This one decides which indexes are reported at all.
47
- */
48
- protected readonly constraintIndexTypes: readonly string[];
44
+ /** Whether an entry sorts nulls first, which Postgres states on every entry. */
45
+ protected readonly nullsFirstSql: string;
46
+ /** An index's access method, which is the type it declares. */
47
+ protected readonly indexMethodSql: string;
48
+ /** An entry's operator class, where it is not the default for its type. */
49
+ protected readonly opsClassSql: string;
49
50
  /** From `pg_constraint`, whose key arrays keep each column paired with the one it references. */
50
51
  protected getForeignKeysQuery(_tableName: string): string;
51
52
  protected getPrimaryKeyQuery(_tableName: string): string;
52
53
  protected mapColumnsResult(_read: TableRowReader, _tableName: string, results: PostgresColumnRow[]): Promise<ColumnSchema[]>;
54
+ /**
55
+ * A vector index keeps its distance as its vector column's operator class, `{type}_{metric}_ops`, which
56
+ * is how `PgIndexDdl` writes the entity's `distance`; read back as that `distance`, so the two compare.
57
+ * No class named is the engine's default, L2.
58
+ */
59
+ private withVectorDistance;
53
60
  protected mapIndexesResult(_read: TableRowReader, _tableName: string, results: PostgresIndexRow[]): Promise<IndexSchema[]>;
54
61
  protected mapForeignKeysResult(_read: TableRowReader, _tableName: string, results: PostgresForeignKeyRow[]): Promise<ForeignKeySchema[]>;
55
62
  protected normalizeType(dataType: string, udtName: string): string;
@@ -73,18 +80,18 @@ declare const FOREIGN_KEY_ACTION_CODES: {
73
80
  * CockroachDB answers the same catalogue queries and differs only in what it can express: v26.2.5
74
81
  * still rejects `NULLS FIRST/LAST` and operator classes as "unimplemented", and it sorts nulls first
75
82
  * on an ASC column where Postgres sorts them last. Reading a nulls order back would therefore report
76
- * every ascending index as drifted, against an entity that could not have asked for one. Its access
77
- * method, `prefix`, needs nothing: a method that is not a known index type is reported as no type.
83
+ * every ascending index as drifted, against an entity that could not have asked for one.
78
84
  */
79
85
  export declare class CockroachSchemaIntrospector extends PostgresSchemaIntrospector {
80
86
  readonly indexFacets: ReadonlySet<IndexFacet>;
87
+ /** None: it rejects a stated nulls order, so reading one back gives an index it would refuse to rebuild. */
88
+ protected readonly nullsFirstSql = "NULL::BOOL";
81
89
  /**
82
- * `'u'` is missing on purpose. CockroachDB registers a `UNIQUE` constraint for a plain `CREATE
83
- * UNIQUE INDEX` too, naming it after the index, so filtering on it would hide every unique index a
84
- * user asked for and report it missing forever. It leaves no way to tell the two apart, so the
85
- * index a `@Field({ unique })` builds underneath itself stays visible there.
90
+ * Every index reports the access method `prefix` and no operator class, so a vector index is read off
91
+ * its definition, `USING cspann (vec vector_cosine_ops)`: its type, and its last key's class.
86
92
  */
87
- protected readonly constraintIndexTypes: readonly string[];
93
+ protected readonly indexMethodSql = "CASE WHEN pg_get_indexdef(ix.indexrelid) LIKE '% USING cspann %' THEN 'vector' ELSE am.amname END";
94
+ protected readonly opsClassSql = "CASE WHEN k.n = ix.indnkeyatts THEN substring(pg_get_indexdef(ix.indexrelid) from '(\\w+_ops)\\)') END";
88
95
  }
89
96
  type PostgresForeignKeyRow = {
90
97
  constraint_name: string;
@@ -104,13 +111,14 @@ type PostgresIndexRow = {
104
111
  is_expression: boolean;
105
112
  entry: string;
106
113
  descending: boolean;
107
- nulls_first: boolean;
114
+ nulls_first: boolean | null;
108
115
  ops_class: string | null;
109
116
  };
110
117
  type PostgresColumnRow = {
111
118
  column_name: string;
112
119
  data_type: string;
113
120
  udt_name: string;
121
+ formatted_type: string | null;
114
122
  is_nullable: string;
115
123
  column_default: string | null;
116
124
  is_primary_key: boolean;
@@ -1,4 +1,5 @@
1
1
  import { INDEX_TYPES } from '../../schema/types.js';
2
+ import { isVectorIndexType } from '../../type/vector.js';
2
3
  import { AbstractSqlSchemaIntrospector } from './abstractSqlSchemaIntrospector.js';
3
4
  /**
4
5
  * PostgreSQL schema introspector
@@ -15,15 +16,15 @@ export class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
15
16
  'opsClass',
16
17
  'accessMethod',
17
18
  'include',
19
+ 'distance',
18
20
  ]);
19
21
  triggersQuery() {
20
22
  return /*sql*/ `
21
- SELECT c.relname AS "table", t.tgname AS name, pg_get_triggerdef(t.oid) AS definition,
22
- pg_get_functiondef(t.tgfoid) AS requires
23
+ SELECT t.tgname AS name, pg_get_triggerdef(t.oid) AS definition, pg_get_functiondef(t.tgfoid) AS requires
23
24
  FROM pg_trigger t
24
25
  JOIN pg_class c ON c.oid = t.tgrelid
25
26
  JOIN pg_namespace n ON n.oid = c.relnamespace
26
- WHERE NOT t.tgisinternal AND n.nspname = ${this.schemaExpr}
27
+ WHERE NOT t.tgisinternal AND n.nspname = ${this.schemaExpr} AND c.relname = ${this.dialect.placeholder(1)}
27
28
  `;
28
29
  }
29
30
  getTableNamesQuery() {
@@ -54,7 +55,11 @@ export class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
54
55
  * database scans meet that table every time something else is migrating.
55
56
  *
56
57
  * `attgenerated` rather than `is_generated`, which cannot part a stored generated column from the
57
- * virtual one Postgres 18 added and uql never declares. CockroachDB states it too.
58
+ * virtual one Postgres 18 added and uql never declares. CockroachDB states it too. `format_type` for an
59
+ * extension type's modifier, which `information_schema` drops: a `vector(256)` read back as `vector`.
60
+ * CockroachDB names that type `vector` where Postgres says `USER-DEFINED`.
61
+ *
62
+ * A column is unique by a unique index over it alone, a constraint's or its own, as every engine reads it.
58
63
  */
59
64
  getColumnsQuery(_tableName) {
60
65
  return /*sql*/ `
@@ -69,11 +74,9 @@ export class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
69
74
  c.numeric_scale,
70
75
  c.is_identity,
71
76
  c.identity_generation,
72
- CASE WHEN (
73
- SELECT a.attgenerated FROM pg_catalog.pg_attribute a
74
- WHERE a.attrelid = to_regclass(quote_ident(c.table_schema) || '.' || quote_ident(c.table_name))
75
- AND a.attname = c.column_name
76
- ) = 's' THEN c.generation_expression END AS generated_as,
77
+ CASE WHEN a.attgenerated = 's' THEN c.generation_expression END AS generated_as,
78
+ CASE WHEN c.data_type IN ('USER-DEFINED', 'vector') AND a.atttypmod > -1
79
+ THEN format_type(a.atttypid, a.atttypmod) END AS formatted_type,
77
80
  EXISTS (
78
81
  SELECT 1 FROM information_schema.table_constraints tc
79
82
  JOIN information_schema.key_column_usage kcu USING (constraint_schema, constraint_name)
@@ -83,19 +86,19 @@ export class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
83
86
  AND kcu.column_name = c.column_name
84
87
  ) AS is_primary_key,
85
88
  EXISTS (
86
- SELECT 1 FROM information_schema.table_constraints tc
87
- JOIN information_schema.key_column_usage kcu USING (constraint_schema, constraint_name)
88
- WHERE tc.table_schema = c.table_schema
89
- AND tc.table_name = c.table_name
90
- AND tc.constraint_type = 'UNIQUE'
91
- GROUP BY tc.constraint_name
92
- HAVING COUNT(*) = 1 AND MIN(kcu.column_name) = c.column_name
89
+ SELECT 1 FROM pg_catalog.pg_index ix
90
+ WHERE ix.indrelid = a.attrelid AND ix.indisunique AND NOT ix.indisprimary
91
+ AND ix.indnkeyatts = 1 AND ix.indkey[0] = a.attnum
92
+ AND ix.indpred IS NULL AND ix.indexprs IS NULL
93
93
  ) AS is_unique,
94
94
  pg_catalog.col_description(
95
95
  to_regclass(quote_ident(c.table_schema) || '.' || quote_ident(c.table_name)),
96
96
  c.ordinal_position
97
97
  ) AS column_comment
98
98
  FROM information_schema.columns c
99
+ LEFT JOIN pg_catalog.pg_attribute a
100
+ ON a.attrelid = to_regclass(quote_ident(c.table_schema) || '.' || quote_ident(c.table_name))
101
+ AND a.attname = c.column_name
99
102
  WHERE c.table_schema = ${this.schemaExpr}
100
103
  AND c.table_name = $1
101
104
  ORDER BY c.ordinal_position
@@ -108,23 +111,23 @@ export class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
108
111
  * while `pg_get_indexdef` reprints an identifier *quoted*, so a camelCase column came back as
109
112
  * `"tenantId"` and matched no column of the table. Prisma and drizzle-kit both split it this way.
110
113
  *
111
- * Indexes backing a constraint are left out, primary keys among them: `@Field({ unique })` emits a
112
- * `UNIQUE` constraint and no index, so reporting the index Postgres builds underneath it told every
113
- * project it had an index its entities never asked for.
114
+ * The key's index and an `EXCLUDE`'s are left out. A `UNIQUE` constraint's index stays, as SQL Server
115
+ * and the MySQL family report theirs: the diff reads one over a single column as that column's
116
+ * uniqueness, and one over several as the unique `@Index` it is.
114
117
  */
115
118
  getIndexesQuery(_tableName) {
116
119
  return /*sql*/ `
117
120
  SELECT
118
121
  i.relname AS index_name,
119
122
  ix.indisunique AS is_unique,
120
- am.amname AS method,
123
+ ${this.indexMethodSql} AS method,
121
124
  pg_get_expr(ix.indpred, ix.indrelid, true) AS predicate,
122
125
  k.n <= ix.indnkeyatts AS is_key,
123
126
  k.attnum = 0 AS is_expression,
124
127
  COALESCE(a.attname::text, pg_get_indexdef(ix.indexrelid, k.n::int, true)) AS entry,
125
128
  (ix.indoption[k.n - 1] & 1) <> 0 AS descending,
126
- (ix.indoption[k.n - 1] & 2) <> 0 AS nulls_first,
127
- CASE WHEN op.opcdefault THEN NULL ELSE op.opcname END AS ops_class
129
+ ${this.nullsFirstSql} AS nulls_first,
130
+ ${this.opsClassSql} AS ops_class
128
131
  FROM pg_class t
129
132
  JOIN pg_index ix ON t.oid = ix.indrelid
130
133
  JOIN pg_class i ON i.oid = ix.indexrelid
@@ -137,22 +140,17 @@ export class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
137
140
  AND n.nspname = ${this.schemaExpr}
138
141
  AND NOT ix.indisprimary
139
142
  AND NOT EXISTS (
140
- SELECT 1 FROM pg_constraint con
141
- WHERE con.conindid = ix.indexrelid
142
- AND con.contype IN (${this.constraintIndexTypes.map((type) => `'${type}'`).join(', ')})
143
+ SELECT 1 FROM pg_constraint con WHERE con.conindid = ix.indexrelid AND con.contype = 'x'
143
144
  )
144
145
  ORDER BY i.relname, k.n
145
146
  `;
146
147
  }
147
- /**
148
- * Constraint kinds whose backing index is the constraint itself rather than an index anyone asked
149
- * for. Postgres builds one for `PRIMARY KEY`, `UNIQUE` and `EXCLUDE`, and only for those: a plain
150
- * `CREATE UNIQUE INDEX` has no `pg_constraint` row at all, so it survives.
151
- *
152
- * Nothing to do with {@link indexFacets}, which says which *attributes* of an index diffing may
153
- * compare. This one decides which indexes are reported at all.
154
- */
155
- constraintIndexTypes = ['p', 'u', 'x'];
148
+ /** Whether an entry sorts nulls first, which Postgres states on every entry. */
149
+ nullsFirstSql = '(ix.indoption[k.n - 1] & 2) <> 0';
150
+ /** An index's access method, which is the type it declares. */
151
+ indexMethodSql = 'am.amname';
152
+ /** An entry's operator class, where it is not the default for its type. */
153
+ opsClassSql = 'CASE WHEN op.opcdefault THEN NULL ELSE op.opcname END';
156
154
  /** From `pg_constraint`, whose key arrays keep each column paired with the one it references. */
157
155
  getForeignKeysQuery(_tableName) {
158
156
  const columnsOf = (keys, table) => /*sql*/ `ARRAY_TO_JSON(ARRAY(
@@ -194,7 +192,7 @@ export class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
194
192
  async mapColumnsResult(_read, _tableName, results) {
195
193
  return results.map((row) => ({
196
194
  name: row.column_name,
197
- type: this.normalizeType(row.data_type, row.udt_name),
195
+ type: row.formatted_type?.toUpperCase() ?? this.normalizeType(row.data_type, row.udt_name),
198
196
  nullable: row.is_nullable === 'YES',
199
197
  defaultValue: this.parseDefaultValue(row.column_default),
200
198
  isPrimaryKey: row.is_primary_key,
@@ -207,11 +205,25 @@ export class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
207
205
  generatedAs: row.generated_as ?? undefined,
208
206
  }));
209
207
  }
208
+ /**
209
+ * A vector index keeps its distance as its vector column's operator class, `{type}_{metric}_ops`, which
210
+ * is how `PgIndexDdl` writes the entity's `distance`; read back as that `distance`, so the two compare.
211
+ * No class named is the engine's default, L2.
212
+ */
213
+ withVectorDistance(index) {
214
+ if (!isVectorIndexType(index.type)) {
215
+ return index;
216
+ }
217
+ const opsClass = index.entries.find((entry) => entry.opsClass)?.opsClass;
218
+ const distance = this.dialect.indexedDistance(opsClass ? /_([a-z0-9]+)_ops$/.exec(opsClass)?.[1] : 'l2');
219
+ const entries = index.entries.map(({ opsClass: _opsClass, ...entry }) => entry);
220
+ return distance ? { ...index, distance, entries } : index;
221
+ }
210
222
  async mapIndexesResult(_read, _tableName, results) {
211
223
  // One row per index entry, ordered by position, so the rows of an index are its entries in order.
212
224
  return [...Map.groupBy(results, (row) => row.index_name)].map(([name, rows]) => {
213
225
  const include = rows.filter((row) => !row.is_key).map((row) => row.entry);
214
- return {
226
+ return this.withVectorDistance({
215
227
  name,
216
228
  entries: rows.filter((row) => row.is_key).map(mapIndexEntry),
217
229
  unique: rows[0].is_unique,
@@ -219,7 +231,7 @@ export class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
219
231
  where: rows[0].predicate ?? undefined,
220
232
  include: include.length > 0 ? include : undefined,
221
233
  ...fulltextIndex(rows),
222
- };
234
+ });
223
235
  });
224
236
  }
225
237
  async mapForeignKeysResult(_read, _tableName, results) {
@@ -315,7 +327,7 @@ function mapIndexEntry(row) {
315
327
  column: row.entry,
316
328
  ...(row.is_expression && { expression: true }),
317
329
  order: row.descending ? 'desc' : 'asc',
318
- nulls: row.nulls_first ? 'first' : 'last',
330
+ ...(row.nulls_first !== null && { nulls: row.nulls_first ? 'first' : 'last' }),
319
331
  ...(row.ops_class && { opsClass: row.ops_class }),
320
332
  };
321
333
  }
@@ -323,16 +335,21 @@ function mapIndexEntry(row) {
323
335
  * CockroachDB answers the same catalogue queries and differs only in what it can express: v26.2.5
324
336
  * still rejects `NULLS FIRST/LAST` and operator classes as "unimplemented", and it sorts nulls first
325
337
  * on an ASC column where Postgres sorts them last. Reading a nulls order back would therefore report
326
- * every ascending index as drifted, against an entity that could not have asked for one. Its access
327
- * method, `prefix`, needs nothing: a method that is not a known index type is reported as no type.
338
+ * every ascending index as drifted, against an entity that could not have asked for one.
328
339
  */
329
340
  export class CockroachSchemaIntrospector extends PostgresSchemaIntrospector {
330
- indexFacets = new Set(['order', 'include']);
341
+ indexFacets = new Set([
342
+ 'order',
343
+ 'include',
344
+ 'vector',
345
+ 'distance',
346
+ ]);
347
+ /** None: it rejects a stated nulls order, so reading one back gives an index it would refuse to rebuild. */
348
+ nullsFirstSql = 'NULL::BOOL';
331
349
  /**
332
- * `'u'` is missing on purpose. CockroachDB registers a `UNIQUE` constraint for a plain `CREATE
333
- * UNIQUE INDEX` too, naming it after the index, so filtering on it would hide every unique index a
334
- * user asked for and report it missing forever. It leaves no way to tell the two apart, so the
335
- * index a `@Field({ unique })` builds underneath itself stays visible there.
350
+ * Every index reports the access method `prefix` and no operator class, so a vector index is read off
351
+ * its definition, `USING cspann (vec vector_cosine_ops)`: its type, and its last key's class.
336
352
  */
337
- constraintIndexTypes = ['p', 'x'];
353
+ indexMethodSql = `CASE WHEN pg_get_indexdef(ix.indexrelid) LIKE '% USING cspann %' THEN 'vector' ELSE am.amname END`;
354
+ opsClassSql = `CASE WHEN k.n = ix.indnkeyatts THEN substring(pg_get_indexdef(ix.indexrelid) from '(\\w+_ops)\\)') END`;
338
355
  }
@@ -5,10 +5,10 @@ import { AbstractSqlSchemaIntrospector } from './abstractSqlSchemaIntrospector.j
5
5
  */
6
6
  export class SqliteSchemaIntrospector extends AbstractSqlSchemaIntrospector {
7
7
  /** Whether an index is libSQL's vector index, where the engine has one; elsewhere a declared one is built plain. */
8
- indexFacets = new Set(this.dialect.hasVectorIndex() ? ['vector'] : []);
8
+ indexFacets = new Set(this.dialect.hasVectorIndex() ? ['vector', 'distance'] : []);
9
9
  /** Not SQLite's own tables, nor the ones libSQL keeps a vector index in: its metadata and `<index>_shadow`. */
10
10
  triggersQuery() {
11
- return /*sql*/ `SELECT tbl_name AS \`table\`, name, sql AS definition FROM sqlite_master WHERE type = 'trigger'`;
11
+ return /*sql*/ `SELECT name, sql AS definition FROM sqlite_master WHERE type = 'trigger' AND tbl_name = ?`;
12
12
  }
13
13
  getTableNamesQuery() {
14
14
  return /*sql*/ `
@@ -89,15 +89,12 @@ export class SqliteSchemaIntrospector extends AbstractSqlSchemaIntrospector {
89
89
  const indexSchemas = [];
90
90
  for (const index of results) {
91
91
  const columns = await this.getIndexColumns(read, index.name);
92
- // Include user-created indexes ('c') and multi-column unique constraints ('u')
93
- // Skip primary key indexes ('pk') and single-column unique constraints
94
- const isUserCreated = index.origin === 'c';
95
- const isCompositeUnique = index.origin === 'u' && columns.length > 1;
92
+ // A unique constraint's index ('u') is reported as every engine reports it, and only the key's ('pk') left out.
96
93
  // `PRAGMA index_info` names an expression entry `null` (its `cid` is -2), and the expression text
97
94
  // lives only in `sqlite_master.sql`. Reporting `{ column: null }` put a column literally named
98
95
  // `null` into the diff, so an index UQL cannot describe is left out, libSQL's vector index aside.
99
96
  const named = columns.filter((column) => column.name !== null);
100
- if (!isUserCreated && !isCompositeUnique) {
97
+ if (index.origin === 'pk') {
101
98
  continue;
102
99
  }
103
100
  if (named.length === columns.length) {
@@ -171,8 +168,13 @@ export class SqliteSchemaIntrospector extends AbstractSqlSchemaIntrospector {
171
168
  return undefined;
172
169
  }
173
170
  const metric = row.sql?.match(/'metric=(\w+)'/i)?.[1]?.toLowerCase();
174
- const distances = new Map([...this.dialect.vectorMetrics].map(([distance, { index }]) => [index, distance]));
175
- return { name: indexName, entries: [{ column }], unique: false, type: 'vector', distance: distances.get(metric) };
171
+ return {
172
+ name: indexName,
173
+ entries: [{ column }],
174
+ unique: false,
175
+ type: 'vector',
176
+ distance: this.dialect.indexedDistance(metric),
177
+ };
176
178
  }
177
179
  /** The statement that created the table, which is where SQLite keeps every expression it was given. */
178
180
  async getTableDdl(read, tableName) {
@@ -1,4 +1,5 @@
1
1
  import { isMongoQuerier, isSqlQuerier, } from '../type/index.js';
2
+ import { UqlUsageError } from '../util/uqlError.js';
2
3
  import { withMongoQuerierForMigrations, withSqlQuerierForMigrations } from './acquireQuerierForMigrations.js';
3
4
  import { MigrationBuilder } from './builder/migrationBuilder.js';
4
5
  import { migrationSource } from './codegen/migrationFile.js';
@@ -46,5 +47,5 @@ export async function migrationBuilderFor(querier) {
46
47
  if (isMongoQuerier(querier)) {
47
48
  return new MigrationBuilder(await mongoSchemaGenerator(), mongoSession(querier).run);
48
49
  }
49
- throw new TypeError('A migration builder needs a SQL or a MongoDB querier');
50
+ throw new UqlUsageError('A migration builder needs a SQL or a MongoDB querier');
50
51
  }
@@ -128,6 +128,11 @@ export declare class Migrator {
128
128
  * never asks for the rollback, which on SQLite cannot even be expressed (no `ALTER COLUMN`).
129
129
  */
130
130
  private pendingChanges;
131
+ /**
132
+ * Safe mode only adds: a change with a `from` drops or rebuilds what the table holds, so it is held,
133
+ * and so is a key whole, which rebuilds an index over every row and fails where a column holds a null.
134
+ * Without `drop`, a column's drop is held too.
135
+ */
131
136
  protected filterDiff(diff: SchemaDiff, options: {
132
137
  safe?: boolean;
133
138
  drop?: boolean;
@@ -5,9 +5,11 @@ import { getEntities, getMeta } from '../entity/index.js';
5
5
  import { SchemaAST } from '../schema/index.js';
6
6
  import { hasTriggers } from '../util/field.util.js';
7
7
  import { LoggerWrapper } from '../util/index.js';
8
+ import { UqlUsageError } from '../util/uqlError.js';
8
9
  import { buildMigrationModule } from './codegen/migrationFile.js';
9
10
  import { introspectorFor } from './introspection/registry.js';
10
11
  import { migrationBuilderFor, migrationTargetFor } from './migrationTarget.js';
12
+ import { dropped, nonEmpty, reverseDiff, sides } from './schemaChange.js';
11
13
  /**
12
14
  * Main class for managing database migrations
13
15
  */
@@ -95,7 +97,7 @@ export class Migrator {
95
97
  if (options.to) {
96
98
  const toIndex = selected.findIndex((m) => m.name === options.to);
97
99
  if (toIndex === -1) {
98
- throw new TypeError(`Migration '${options.to}' not found`);
100
+ throw new UqlUsageError(`Migration '${options.to}' not found`);
99
101
  }
100
102
  selected = selected.slice(0, toIndex + 1);
101
103
  }
@@ -215,17 +217,10 @@ export class Migrator {
215
217
  // Tables this plan creates are left out: their `CREATE` carries their triggers, and asking the
216
218
  // catalogue about a table that is not there yet fails outright on some engines.
217
219
  const wanted = entities.filter((entity) => !fresh.has(this.tableOf(entity)));
218
- const bySchema = new Map();
219
220
  const state = [];
220
221
  for (const entity of wanted) {
221
222
  const meta = getMeta(entity);
222
- const schema = dialect.resolveSchema(meta);
223
- let owned = bySchema.get(schema);
224
- if (!owned) {
225
- owned = await this.schemaIntrospectorFor(schema).ownedTriggers();
226
- bySchema.set(schema, owned);
227
- }
228
- const installed = owned.get(dialect.resolveTableAlias(meta)) ?? new Map();
223
+ const installed = await this.schemaIntrospectorFor(dialect.resolveSchema(meta)).ownedTriggers(dialect.resolveTableAlias(meta));
229
224
  // An installed trigger alone keeps it: an entity that stopped declaring one has it to drop.
230
225
  if (installed.size || hasTriggers(meta)) {
231
226
  state.push({ entity, installed });
@@ -239,7 +234,7 @@ export class Migrator {
239
234
  * its entity declares goes back on after. `down` is lazy: SQLite cannot express every alter's inverse.
240
235
  */
241
236
  alterPlan(generator, altered, state) {
242
- const changing = new Set(altered.filter((diff) => diff.columnsToAlter?.length || diff.columnsToDrop?.length).map((diff) => diff.tableName));
237
+ const changing = new Set(altered.filter((diff) => sides(diff.columns, 'from').length).map((diff) => diff.tableName));
243
238
  const cleared = state.filter(({ entity }) => changing.has(this.tableOf(entity)));
244
239
  const after = state.map((it) => (cleared.includes(it) ? { entity: it.entity, installed: new Map() } : it));
245
240
  return {
@@ -250,7 +245,7 @@ export class Migrator {
250
245
  ],
251
246
  down: () => [
252
247
  ...this.revertedTriggers(generator, after),
253
- ...altered.toReversed().flatMap((diff) => generator.generateAlterTableDown(diff)),
248
+ ...altered.toReversed().flatMap((diff) => generator.generateAlterTable(reverseDiff(diff))),
254
249
  ...cleared.flatMap(({ installed }) => [...installed.values()].flat().map((sql) => `${sql};`)),
255
250
  ],
256
251
  };
@@ -400,45 +395,40 @@ export class Migrator {
400
395
  altered: diffs.filter((diff) => diff.type === 'alter'),
401
396
  };
402
397
  }
398
+ /**
399
+ * Safe mode only adds: a change with a `from` drops or rebuilds what the table holds, so it is held,
400
+ * and so is a key whole, which rebuilds an index over every row and fails where a column holds a null.
401
+ * Without `drop`, a column's drop is held too.
402
+ */
403
403
  filterDiff(diff, options) {
404
- const filteredDiff = { ...diff };
405
- if (options.safe !== false) {
406
- // In safe mode, we only allow additions (creating tables/columns)
407
- // We block drops and alterations to prevent accidental data loss
408
- if (filteredDiff.columnsToDrop?.length) {
409
- this.logger.logSkippedMigration(`[AutoSync] Skipped dropping ${filteredDiff.columnsToDrop.length} columns in table '${diff.tableName}': ${filteredDiff.columnsToDrop.join(', ')} (safe mode active)`);
410
- delete filteredDiff.columnsToDrop;
411
- }
412
- if (filteredDiff.columnsToAlter?.length) {
413
- this.logger.logSkippedMigration(`[AutoSync] Skipped altering ${filteredDiff.columnsToAlter.length} columns in table '${diff.tableName}': ${filteredDiff.columnsToAlter.map((c) => c.to.name).join(', ')} (safe mode active). Use a migration or { safe: false } to apply.`);
414
- delete filteredDiff.columnsToAlter;
415
- }
416
- if (filteredDiff.primaryKey) {
417
- // Rewriting a key drops a constraint and rebuilds an index over the whole table, and fails
418
- // outright where the new columns are null on rows that already exist. Firmly not additive.
419
- this.logger.logSkippedMigration(`[AutoSync] Skipped changing the primary key of '${diff.tableName}' from (${filteredDiff.primaryKey.from.join(', ')}) to (${filteredDiff.primaryKey.to.join(', ')}) (safe mode active). Use a migration or { safe: false } to apply.`);
420
- delete filteredDiff.primaryKey;
404
+ const safe = options.safe !== false;
405
+ const skip = (what, names, fix) => {
406
+ if (names.length) {
407
+ this.logger.logSkippedMigration(`[AutoSync] Skipped ${names.length} ${what} in table '${diff.tableName}': ${names.join(', ')} (${fix}).`);
421
408
  }
422
- if (filteredDiff.foreignKeysToAlter?.length) {
423
- // Altering one is dropping it and adding it back, so letting the add through while the drop
424
- // is held would emit `ADD CONSTRAINT` for a constraint the table still has.
425
- 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.`);
426
- delete filteredDiff.foreignKeysToAlter;
427
- }
428
- if (filteredDiff.indexesToDrop?.length) {
429
- // An index recreated under its old name is a drop and an add, held back together.
430
- const dropped = new Set(filteredDiff.indexesToDrop.map((index) => index.name));
431
- 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.`);
432
- filteredDiff.indexesToAdd = filteredDiff.indexesToAdd?.filter((index) => !dropped.has(index.name));
433
- delete filteredDiff.indexesToDrop;
409
+ };
410
+ const safeFix = 'safe mode active. Use a migration or { safe: false } to apply';
411
+ const additive = (what, changes, nameOf) => {
412
+ if (!safe) {
413
+ return changes;
434
414
  }
435
- delete filteredDiff.foreignKeysToDrop;
415
+ skip(`${what} changes`, sides(changes, 'from').map(nameOf), safeFix);
416
+ return nonEmpty((changes ?? []).filter((change) => change.from === undefined));
417
+ };
418
+ const columns = additive('column', diff.columns, (column) => column.name);
419
+ if (safe && diff.primaryKey) {
420
+ skip('primary key changes', [diff.tableName], safeFix);
436
421
  }
437
- if (!options.drop && filteredDiff.columnsToDrop?.length) {
438
- this.logger.logSkippedMigration(`[AutoSync] Skipped dropping ${filteredDiff.columnsToDrop.length} columns in table '${diff.tableName}' (drop: false). Use { drop: true } to apply.`);
439
- delete filteredDiff.columnsToDrop;
422
+ if (!options.drop) {
423
+ skip('column drops', dropped(columns).map((column) => column.name), 'drop: false. Use { drop: true } to apply');
440
424
  }
441
- return filteredDiff;
425
+ return {
426
+ ...diff,
427
+ primaryKey: safe ? undefined : diff.primaryKey,
428
+ columns: options.drop ? columns : nonEmpty((columns ?? []).filter((change) => change.to !== undefined)),
429
+ indexes: additive('index', diff.indexes, (index) => index.name),
430
+ foreignKeys: additive('foreign key', diff.foreignKeys, (foreignKey) => foreignKey.name ?? foreignKey.columns.join(', ')),
431
+ };
442
432
  }
443
433
  /** Runs the statements a generator wrote, in one transaction where the engine takes DDL in one. */
444
434
  async executeSyncStatements(statements, options) {
@@ -0,0 +1,18 @@
1
+ import type { Change, SchemaDiff } from '../type/index.js';
2
+ /** Each change's end on `side`, where it has one: what a drop half removes (`from`), or an add half creates (`to`). */
3
+ export declare function sides<T>(changes: readonly Change<T>[] | undefined, side: 'from' | 'to'): T[];
4
+ /** What the changes add: each `to` with no `from`. */
5
+ export declare function added<T>(changes?: readonly Change<T>[]): T[];
6
+ /** What the changes drop: each `from` with no `to`. */
7
+ export declare function dropped<T>(changes?: readonly Change<T>[]): T[];
8
+ /** The changes that alter an object in place, which only a column can. */
9
+ export declare function alterations<T>(changes?: readonly Change<T>[]): {
10
+ readonly from: T;
11
+ readonly to: T;
12
+ }[];
13
+ /** `items`, or nothing where it has none, so an empty change list is left off a diff. */
14
+ export declare function nonEmpty<T>(items: readonly T[]): readonly T[] | undefined;
15
+ /** `change` undone: an add becomes a drop, a drop an add, and an alter runs the other way. */
16
+ export declare function swap<T>({ from, to }: Change<T>): Change<T>;
17
+ /** `diff` undone: every change swapped, which is what a migration's `down` runs. */
18
+ export declare function reverseDiff(diff: SchemaDiff): SchemaDiff;