uql-orm 0.79.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 (110) hide show
  1. package/dist/browser/uql-browser.min.js.map +2 -2
  2. package/dist/cockroachdb/cockroachDialect.js +5 -1
  3. package/dist/dialect/abstractDialect.d.ts +1 -31
  4. package/dist/dialect/abstractDialect.js +3 -27
  5. package/dist/dialect/abstractSqlDialect.d.ts +25 -56
  6. package/dist/dialect/abstractSqlDialect.js +78 -145
  7. package/dist/dialect/aliases.d.ts +5 -0
  8. package/dist/dialect/aliases.js +5 -0
  9. package/dist/dialect/mysqlLikeSqlDialect.d.ts +4 -2
  10. package/dist/dialect/mysqlLikeSqlDialect.js +14 -1
  11. package/dist/dialect/operators.d.ts +66 -0
  12. package/dist/dialect/operators.js +129 -0
  13. package/dist/dialect/pgLikeSqlDialect.d.ts +4 -1
  14. package/dist/dialect/pgLikeSqlDialect.js +16 -3
  15. package/dist/dialect/vectorSqlDialect.d.ts +2 -0
  16. package/dist/dialect/vectorSqlDialect.js +4 -0
  17. package/dist/entity/decorator/entity.d.ts +6 -1
  18. package/dist/entity/decorator/entity.js +12 -1
  19. package/dist/entity/index.d.ts +1 -1
  20. package/dist/entity/index.js +1 -1
  21. package/dist/entity/metadata/definition.d.ts +6 -1
  22. package/dist/entity/metadata/definition.js +19 -0
  23. package/dist/migrate/builder/expressions.d.ts +2 -0
  24. package/dist/migrate/builder/expressions.js +24 -0
  25. package/dist/migrate/cli.js +1 -1
  26. package/dist/migrate/codegen/entityCodeGenerator.js +2 -2
  27. package/dist/migrate/codegen/entityTypes.js +1 -2
  28. package/dist/migrate/codegen/indexDecoratorSource.d.ts +3 -2
  29. package/dist/migrate/codegen/indexDecoratorSource.js +5 -23
  30. package/dist/migrate/ddl/mssqlIndexDdl.d.ts +5 -0
  31. package/dist/migrate/ddl/mssqlIndexDdl.js +10 -0
  32. package/dist/migrate/ddl/mssqlTableDdl.d.ts +6 -4
  33. package/dist/migrate/ddl/mssqlTableDdl.js +25 -14
  34. package/dist/migrate/ddl/mysqlIndexDdl.d.ts +2 -2
  35. package/dist/migrate/ddl/mysqlIndexDdl.js +7 -6
  36. package/dist/migrate/ddl/pgIndexDdl.d.ts +2 -1
  37. package/dist/migrate/ddl/pgIndexDdl.js +8 -6
  38. package/dist/migrate/ddl/tableDdl.d.ts +5 -2
  39. package/dist/migrate/ddl/tableDdl.js +13 -7
  40. package/dist/migrate/drift/driftDetector.d.ts +4 -5
  41. package/dist/migrate/drift/driftDetector.js +21 -21
  42. package/dist/migrate/generator/definitionToNode.d.ts +1 -1
  43. package/dist/migrate/generator/definitionToNode.js +9 -20
  44. package/dist/migrate/generator/mongoSchemaGenerator.d.ts +5 -1
  45. package/dist/migrate/generator/mongoSchemaGenerator.js +20 -18
  46. package/dist/migrate/index.d.ts +2 -1
  47. package/dist/migrate/index.js +1 -0
  48. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.d.ts +16 -6
  49. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +18 -4
  50. package/dist/migrate/introspection/baseSqlIntrospector.js +7 -18
  51. package/dist/migrate/introspection/mongoIntrospector.d.ts +3 -1
  52. package/dist/migrate/introspection/mongoIntrospector.js +7 -3
  53. package/dist/migrate/introspection/mssqlIntrospector.d.ts +1 -0
  54. package/dist/migrate/introspection/mssqlIntrospector.js +9 -0
  55. package/dist/migrate/introspection/mysqlIntrospector.d.ts +16 -5
  56. package/dist/migrate/introspection/mysqlIntrospector.js +39 -2
  57. package/dist/migrate/introspection/postgresIntrospector.d.ts +30 -21
  58. package/dist/migrate/introspection/postgresIntrospector.js +70 -43
  59. package/dist/migrate/introspection/sqliteIntrospector.d.ts +1 -0
  60. package/dist/migrate/introspection/sqliteIntrospector.js +13 -8
  61. package/dist/migrate/migrator.d.ts +33 -1
  62. package/dist/migrate/migrator.js +111 -43
  63. package/dist/migrate/schemaChange.d.ts +18 -0
  64. package/dist/migrate/schemaChange.js +37 -0
  65. package/dist/migrate/schemaGenerator.d.ts +25 -15
  66. package/dist/migrate/schemaGenerator.js +130 -182
  67. package/dist/migrate/storage/databaseStorage.d.ts +4 -0
  68. package/dist/migrate/storage/databaseStorage.js +14 -8
  69. package/dist/migrate/triggerSql.d.ts +24 -0
  70. package/dist/migrate/triggerSql.js +229 -0
  71. package/dist/mongo/mongoDialect.d.ts +0 -21
  72. package/dist/mongo/mongoDialect.js +105 -100
  73. package/dist/mongo/mongodbQuerier.js +17 -1
  74. package/dist/mssql/mssqlDialect.d.ts +18 -7
  75. package/dist/mssql/mssqlDialect.js +77 -33
  76. package/dist/mssql/mssqlQuerier.js +2 -2
  77. package/dist/schema/canonicalType.d.ts +6 -1
  78. package/dist/schema/canonicalType.js +14 -0
  79. package/dist/schema/indexDifferences.d.ts +22 -6
  80. package/dist/schema/indexDifferences.js +23 -8
  81. package/dist/schema/matchByKey.d.ts +10 -0
  82. package/dist/schema/matchByKey.js +18 -0
  83. package/dist/schema/schemaAST.d.ts +6 -2
  84. package/dist/schema/schemaAST.js +7 -3
  85. package/dist/schema/schemaASTBuilder.d.ts +4 -8
  86. package/dist/schema/schemaASTBuilder.js +20 -29
  87. package/dist/schema/schemaASTDiffer.d.ts +2 -3
  88. package/dist/schema/schemaASTDiffer.js +15 -36
  89. package/dist/schema/types.d.ts +14 -15
  90. package/dist/sqlite/sqliteDialect.d.ts +1 -1
  91. package/dist/sqlite/sqliteDialect.js +12 -3
  92. package/dist/type/dialect.d.ts +69 -9
  93. package/dist/type/entity.d.ts +96 -3
  94. package/dist/type/migration.d.ts +43 -44
  95. package/dist/type/query.d.ts +13 -4
  96. package/dist/type/queryWhere.d.ts +4 -2
  97. package/dist/util/ddlExpression.util.d.ts +5 -1
  98. package/dist/util/ddlExpression.util.js +6 -2
  99. package/dist/util/field.util.d.ts +9 -1
  100. package/dist/util/field.util.js +14 -2
  101. package/dist/util/fieldOption.util.d.ts +2 -2
  102. package/dist/util/fieldOption.util.js +2 -2
  103. package/dist/util/raw.d.ts +9 -1
  104. package/dist/util/raw.js +36 -11
  105. package/dist/util/sql.util.d.ts +12 -0
  106. package/dist/util/sql.util.js +24 -3
  107. package/dist/util/uqlError.d.ts +2 -0
  108. package/dist/util/uqlError.js +4 -0
  109. package/package.json +4 -4
  110. package/skills/uql-orm/SKILL.md +11 -6
@@ -1,3 +1,4 @@
1
+ import type { IndexFacet } from '../../schema/indexDifferences.js';
1
2
  import type { ColumnSchema, ForeignKeySchema, IndexSchema } from '../../type/index.js';
2
3
  import { AbstractSqlSchemaIntrospector, type JoinedForeignKeyRow, type TableRowReader } from './abstractSqlSchemaIntrospector.js';
3
4
  /**
@@ -6,6 +7,7 @@ import { AbstractSqlSchemaIntrospector, type JoinedForeignKeyRow, type TableRowR
6
7
  */
7
8
  export declare class MysqlSchemaIntrospector extends AbstractSqlSchemaIntrospector {
8
9
  protected readonly defaultSchemaExpr = "DATABASE()";
10
+ protected triggersQuery(): string;
9
11
  protected getTableNamesQuery(): string;
10
12
  protected tableExistsQuery(): string;
11
13
  protected parseTableExistsResult([row]: {
@@ -16,11 +18,7 @@ export declare class MysqlSchemaIntrospector extends AbstractSqlSchemaIntrospect
16
18
  protected getForeignKeysQuery(_tableName: string): string;
17
19
  protected getPrimaryKeyQuery(_tableName: string): string;
18
20
  protected mapColumnsResult(_read: TableRowReader, _tableName: string, results: MysqlColumnRow[]): Promise<ColumnSchema[]>;
19
- protected mapIndexesResult(_read: TableRowReader, _tableName: string, results: {
20
- index_name: string;
21
- columns: string;
22
- is_unique: number;
23
- }[]): Promise<IndexSchema[]>;
21
+ protected mapIndexesResult(_read: TableRowReader, _tableName: string, results: MysqlIndexRow[]): Promise<IndexSchema[]>;
24
22
  protected mapForeignKeysResult(_read: TableRowReader, _tableName: string, results: JoinedForeignKeyRow[]): Promise<ForeignKeySchema[]>;
25
23
  /**
26
24
  * MariaDB prints a string default as the literal it is (`'it''s'`). MySQL prints one bare, save an
@@ -51,6 +49,19 @@ type MysqlColumnRow = {
51
49
  * JSON, got LONGTEXT", flagged as data loss) on a table uql created itself.
52
50
  */
53
51
  export declare class MariadbSchemaIntrospector extends MysqlSchemaIntrospector {
52
+ /** Whether an index is MariaDB's vector index, and the distance it was built for. */
53
+ readonly indexFacets: ReadonlySet<IndexFacet>;
54
+ /**
55
+ * A vector index's distance is kept only in the table's own definition, ``VECTOR KEY `ix` (`vec`)
56
+ * `DISTANCE`='cosine'``, and left out there for MariaDB's default, euclidean.
57
+ */
58
+ protected mapIndexesResult(read: TableRowReader, tableName: string, results: MysqlIndexRow[]): Promise<IndexSchema[]>;
54
59
  protected mapColumnsResult(read: TableRowReader, tableName: string, results: MysqlColumnRow[]): Promise<ColumnSchema[]>;
55
60
  }
61
+ type MysqlIndexRow = {
62
+ index_name: string;
63
+ columns: string;
64
+ is_unique: number;
65
+ method: string;
66
+ };
56
67
  export {};
@@ -7,6 +7,15 @@ import { AbstractSqlSchemaIntrospector, } from './abstractSqlSchemaIntrospector.
7
7
  export class MysqlSchemaIntrospector extends AbstractSqlSchemaIntrospector {
8
8
  // A MySQL "schema" is a database, so the connection's own is what `DATABASE()` reports.
9
9
  defaultSchemaExpr = 'DATABASE()';
10
+ triggersQuery() {
11
+ return /*sql*/ `
12
+ SELECT TRIGGER_NAME AS name,
13
+ CONCAT('CREATE TRIGGER \`', TRIGGER_SCHEMA, '\`.\`', TRIGGER_NAME, '\` ', ACTION_TIMING, ' ', EVENT_MANIPULATION,
14
+ ' ON \`', EVENT_OBJECT_SCHEMA, '\`.\`', EVENT_OBJECT_TABLE, '\` FOR EACH ROW ', ACTION_STATEMENT) AS definition
15
+ FROM information_schema.TRIGGERS
16
+ WHERE TRIGGER_SCHEMA = ${this.schemaExpr} AND EVENT_OBJECT_TABLE = ${this.dialect.placeholder(1)}
17
+ `;
18
+ }
10
19
  getTableNamesQuery() {
11
20
  return /*sql*/ `
12
21
  SELECT TABLE_NAME as table_name
@@ -53,7 +62,8 @@ export class MysqlSchemaIntrospector extends AbstractSqlSchemaIntrospector {
53
62
  SELECT
54
63
  INDEX_NAME as index_name,
55
64
  GROUP_CONCAT(COALESCE(COLUMN_NAME, '') ORDER BY SEQ_IN_INDEX) as columns,
56
- NOT NON_UNIQUE as is_unique
65
+ NOT NON_UNIQUE as is_unique,
66
+ MAX(INDEX_TYPE) as method
57
67
  FROM information_schema.STATISTICS
58
68
  WHERE TABLE_SCHEMA = ${this.schemaExpr}
59
69
  AND TABLE_NAME = ?
@@ -101,7 +111,8 @@ export class MysqlSchemaIntrospector extends AbstractSqlSchemaIntrospector {
101
111
  isPrimaryKey: row.column_key === 'PRI',
102
112
  isAutoIncrement: row.extra.toLowerCase().includes('auto_increment'),
103
113
  isUnique: row.column_key === 'UNI',
104
- 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),
105
116
  precision: this.toNumber(row.numeric_precision),
106
117
  scale: this.toNumber(row.numeric_scale),
107
118
  comment: row.column_comment || undefined,
@@ -111,6 +122,7 @@ export class MysqlSchemaIntrospector extends AbstractSqlSchemaIntrospector {
111
122
  async mapIndexesResult(_read, _tableName, results) {
112
123
  return results.map((row) => ({
113
124
  name: row.index_name,
125
+ ...(row.method === 'VECTOR' && { type: 'vector' }),
114
126
  // A functional or multi-valued key part has no `COLUMN_NAME` - the `COALESCE` above keeps its
115
127
  // place in the list, and it is reported as the expression it is, which is what stops diffing
116
128
  // from comparing an entry list the server cannot state against the entity's own.
@@ -155,6 +167,31 @@ export class MysqlSchemaIntrospector extends AbstractSqlSchemaIntrospector {
155
167
  * JSON, got LONGTEXT", flagged as data loss) on a table uql created itself.
156
168
  */
157
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
+ }
158
195
  async mapColumnsResult(read, tableName, results) {
159
196
  const columns = await super.mapColumnsResult(read, tableName, results);
160
197
  const checks = await read(
@@ -11,6 +11,7 @@ export declare class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrosp
11
11
  * database reprints in its own words, so they are not comparable and are not claimed here.
12
12
  */
13
13
  readonly indexFacets: ReadonlySet<IndexFacet>;
14
+ protected triggersQuery(): string;
14
15
  protected getTableNamesQuery(): string;
15
16
  protected tableExistsQuery(): string;
16
17
  protected parseTableExistsResult([row]: RawRow[]): boolean;
@@ -21,7 +22,11 @@ export declare class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrosp
21
22
  * database scans meet that table every time something else is migrating.
22
23
  *
23
24
  * `attgenerated` rather than `is_generated`, which cannot part a stored generated column from the
24
- * 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.
25
30
  */
26
31
  protected getColumnsQuery(_tableName: string): string;
27
32
  /**
@@ -31,24 +36,27 @@ export declare class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrosp
31
36
  * while `pg_get_indexdef` reprints an identifier *quoted*, so a camelCase column came back as
32
37
  * `"tenantId"` and matched no column of the table. Prisma and drizzle-kit both split it this way.
33
38
  *
34
- * Indexes backing a constraint are left out, primary keys among them: `@Field({ unique })` emits a
35
- * `UNIQUE` constraint and no index, so reporting the index Postgres builds underneath it told every
36
- * 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.
37
42
  */
38
43
  protected getIndexesQuery(_tableName: string): string;
39
- /**
40
- * Constraint kinds whose backing index is the constraint itself rather than an index anyone asked
41
- * for. Postgres builds one for `PRIMARY KEY`, `UNIQUE` and `EXCLUDE`, and only for those: a plain
42
- * `CREATE UNIQUE INDEX` has no `pg_constraint` row at all, so it survives.
43
- *
44
- * Nothing to do with {@link indexFacets}, which says which *attributes* of an index diffing may
45
- * compare. This one decides which indexes are reported at all.
46
- */
47
- 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;
48
50
  /** From `pg_constraint`, whose key arrays keep each column paired with the one it references. */
49
51
  protected getForeignKeysQuery(_tableName: string): string;
50
52
  protected getPrimaryKeyQuery(_tableName: string): string;
51
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;
52
60
  protected mapIndexesResult(_read: TableRowReader, _tableName: string, results: PostgresIndexRow[]): Promise<IndexSchema[]>;
53
61
  protected mapForeignKeysResult(_read: TableRowReader, _tableName: string, results: PostgresForeignKeyRow[]): Promise<ForeignKeySchema[]>;
54
62
  protected normalizeType(dataType: string, udtName: string): string;
@@ -72,18 +80,18 @@ declare const FOREIGN_KEY_ACTION_CODES: {
72
80
  * CockroachDB answers the same catalogue queries and differs only in what it can express: v26.2.5
73
81
  * still rejects `NULLS FIRST/LAST` and operator classes as "unimplemented", and it sorts nulls first
74
82
  * on an ASC column where Postgres sorts them last. Reading a nulls order back would therefore report
75
- * every ascending index as drifted, against an entity that could not have asked for one. Its access
76
- * 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.
77
84
  */
78
85
  export declare class CockroachSchemaIntrospector extends PostgresSchemaIntrospector {
79
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";
80
89
  /**
81
- * `'u'` is missing on purpose. CockroachDB registers a `UNIQUE` constraint for a plain `CREATE
82
- * UNIQUE INDEX` too, naming it after the index, so filtering on it would hide every unique index a
83
- * user asked for and report it missing forever. It leaves no way to tell the two apart, so the
84
- * 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.
85
92
  */
86
- 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";
87
95
  }
88
96
  type PostgresForeignKeyRow = {
89
97
  constraint_name: string;
@@ -103,13 +111,14 @@ type PostgresIndexRow = {
103
111
  is_expression: boolean;
104
112
  entry: string;
105
113
  descending: boolean;
106
- nulls_first: boolean;
114
+ nulls_first: boolean | null;
107
115
  ops_class: string | null;
108
116
  };
109
117
  type PostgresColumnRow = {
110
118
  column_name: string;
111
119
  data_type: string;
112
120
  udt_name: string;
121
+ formatted_type: string | null;
113
122
  is_nullable: string;
114
123
  column_default: string | null;
115
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,7 +16,17 @@ export class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
15
16
  'opsClass',
16
17
  'accessMethod',
17
18
  'include',
19
+ 'distance',
18
20
  ]);
21
+ triggersQuery() {
22
+ return /*sql*/ `
23
+ SELECT t.tgname AS name, pg_get_triggerdef(t.oid) AS definition, pg_get_functiondef(t.tgfoid) AS requires
24
+ FROM pg_trigger t
25
+ JOIN pg_class c ON c.oid = t.tgrelid
26
+ JOIN pg_namespace n ON n.oid = c.relnamespace
27
+ WHERE NOT t.tgisinternal AND n.nspname = ${this.schemaExpr} AND c.relname = ${this.dialect.placeholder(1)}
28
+ `;
29
+ }
19
30
  getTableNamesQuery() {
20
31
  return /*sql*/ `
21
32
  SELECT table_name
@@ -44,7 +55,11 @@ export class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
44
55
  * database scans meet that table every time something else is migrating.
45
56
  *
46
57
  * `attgenerated` rather than `is_generated`, which cannot part a stored generated column from the
47
- * 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.
48
63
  */
49
64
  getColumnsQuery(_tableName) {
50
65
  return /*sql*/ `
@@ -59,11 +74,9 @@ export class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
59
74
  c.numeric_scale,
60
75
  c.is_identity,
61
76
  c.identity_generation,
62
- CASE WHEN (
63
- SELECT a.attgenerated FROM pg_catalog.pg_attribute a
64
- WHERE a.attrelid = to_regclass(quote_ident(c.table_schema) || '.' || quote_ident(c.table_name))
65
- AND a.attname = c.column_name
66
- ) = '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,
67
80
  EXISTS (
68
81
  SELECT 1 FROM information_schema.table_constraints tc
69
82
  JOIN information_schema.key_column_usage kcu USING (constraint_schema, constraint_name)
@@ -73,19 +86,19 @@ export class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
73
86
  AND kcu.column_name = c.column_name
74
87
  ) AS is_primary_key,
75
88
  EXISTS (
76
- SELECT 1 FROM information_schema.table_constraints tc
77
- JOIN information_schema.key_column_usage kcu USING (constraint_schema, constraint_name)
78
- WHERE tc.table_schema = c.table_schema
79
- AND tc.table_name = c.table_name
80
- AND tc.constraint_type = 'UNIQUE'
81
- GROUP BY tc.constraint_name
82
- 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
83
93
  ) AS is_unique,
84
94
  pg_catalog.col_description(
85
95
  to_regclass(quote_ident(c.table_schema) || '.' || quote_ident(c.table_name)),
86
96
  c.ordinal_position
87
97
  ) AS column_comment
88
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
89
102
  WHERE c.table_schema = ${this.schemaExpr}
90
103
  AND c.table_name = $1
91
104
  ORDER BY c.ordinal_position
@@ -98,23 +111,23 @@ export class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
98
111
  * while `pg_get_indexdef` reprints an identifier *quoted*, so a camelCase column came back as
99
112
  * `"tenantId"` and matched no column of the table. Prisma and drizzle-kit both split it this way.
100
113
  *
101
- * Indexes backing a constraint are left out, primary keys among them: `@Field({ unique })` emits a
102
- * `UNIQUE` constraint and no index, so reporting the index Postgres builds underneath it told every
103
- * 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.
104
117
  */
105
118
  getIndexesQuery(_tableName) {
106
119
  return /*sql*/ `
107
120
  SELECT
108
121
  i.relname AS index_name,
109
122
  ix.indisunique AS is_unique,
110
- am.amname AS method,
123
+ ${this.indexMethodSql} AS method,
111
124
  pg_get_expr(ix.indpred, ix.indrelid, true) AS predicate,
112
125
  k.n <= ix.indnkeyatts AS is_key,
113
126
  k.attnum = 0 AS is_expression,
114
127
  COALESCE(a.attname::text, pg_get_indexdef(ix.indexrelid, k.n::int, true)) AS entry,
115
128
  (ix.indoption[k.n - 1] & 1) <> 0 AS descending,
116
- (ix.indoption[k.n - 1] & 2) <> 0 AS nulls_first,
117
- 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
118
131
  FROM pg_class t
119
132
  JOIN pg_index ix ON t.oid = ix.indrelid
120
133
  JOIN pg_class i ON i.oid = ix.indexrelid
@@ -127,22 +140,17 @@ export class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
127
140
  AND n.nspname = ${this.schemaExpr}
128
141
  AND NOT ix.indisprimary
129
142
  AND NOT EXISTS (
130
- SELECT 1 FROM pg_constraint con
131
- WHERE con.conindid = ix.indexrelid
132
- 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'
133
144
  )
134
145
  ORDER BY i.relname, k.n
135
146
  `;
136
147
  }
137
- /**
138
- * Constraint kinds whose backing index is the constraint itself rather than an index anyone asked
139
- * for. Postgres builds one for `PRIMARY KEY`, `UNIQUE` and `EXCLUDE`, and only for those: a plain
140
- * `CREATE UNIQUE INDEX` has no `pg_constraint` row at all, so it survives.
141
- *
142
- * Nothing to do with {@link indexFacets}, which says which *attributes* of an index diffing may
143
- * compare. This one decides which indexes are reported at all.
144
- */
145
- 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';
146
154
  /** From `pg_constraint`, whose key arrays keep each column paired with the one it references. */
147
155
  getForeignKeysQuery(_tableName) {
148
156
  const columnsOf = (keys, table) => /*sql*/ `ARRAY_TO_JSON(ARRAY(
@@ -184,7 +192,7 @@ export class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
184
192
  async mapColumnsResult(_read, _tableName, results) {
185
193
  return results.map((row) => ({
186
194
  name: row.column_name,
187
- type: this.normalizeType(row.data_type, row.udt_name),
195
+ type: row.formatted_type?.toUpperCase() ?? this.normalizeType(row.data_type, row.udt_name),
188
196
  nullable: row.is_nullable === 'YES',
189
197
  defaultValue: this.parseDefaultValue(row.column_default),
190
198
  isPrimaryKey: row.is_primary_key,
@@ -197,11 +205,25 @@ export class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
197
205
  generatedAs: row.generated_as ?? undefined,
198
206
  }));
199
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
+ }
200
222
  async mapIndexesResult(_read, _tableName, results) {
201
223
  // One row per index entry, ordered by position, so the rows of an index are its entries in order.
202
224
  return [...Map.groupBy(results, (row) => row.index_name)].map(([name, rows]) => {
203
225
  const include = rows.filter((row) => !row.is_key).map((row) => row.entry);
204
- return {
226
+ return this.withVectorDistance({
205
227
  name,
206
228
  entries: rows.filter((row) => row.is_key).map(mapIndexEntry),
207
229
  unique: rows[0].is_unique,
@@ -209,7 +231,7 @@ export class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
209
231
  where: rows[0].predicate ?? undefined,
210
232
  include: include.length > 0 ? include : undefined,
211
233
  ...fulltextIndex(rows),
212
- };
234
+ });
213
235
  });
214
236
  }
215
237
  async mapForeignKeysResult(_read, _tableName, results) {
@@ -305,7 +327,7 @@ function mapIndexEntry(row) {
305
327
  column: row.entry,
306
328
  ...(row.is_expression && { expression: true }),
307
329
  order: row.descending ? 'desc' : 'asc',
308
- nulls: row.nulls_first ? 'first' : 'last',
330
+ ...(row.nulls_first !== null && { nulls: row.nulls_first ? 'first' : 'last' }),
309
331
  ...(row.ops_class && { opsClass: row.ops_class }),
310
332
  };
311
333
  }
@@ -313,16 +335,21 @@ function mapIndexEntry(row) {
313
335
  * CockroachDB answers the same catalogue queries and differs only in what it can express: v26.2.5
314
336
  * still rejects `NULLS FIRST/LAST` and operator classes as "unimplemented", and it sorts nulls first
315
337
  * on an ASC column where Postgres sorts them last. Reading a nulls order back would therefore report
316
- * every ascending index as drifted, against an entity that could not have asked for one. Its access
317
- * 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.
318
339
  */
319
340
  export class CockroachSchemaIntrospector extends PostgresSchemaIntrospector {
320
- 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';
321
349
  /**
322
- * `'u'` is missing on purpose. CockroachDB registers a `UNIQUE` constraint for a plain `CREATE
323
- * UNIQUE INDEX` too, naming it after the index, so filtering on it would hide every unique index a
324
- * user asked for and report it missing forever. It leaves no way to tell the two apart, so the
325
- * 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.
326
352
  */
327
- 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`;
328
355
  }
@@ -8,6 +8,7 @@ export declare class SqliteSchemaIntrospector extends AbstractSqlSchemaIntrospec
8
8
  /** Whether an index is libSQL's vector index, where the engine has one; elsewhere a declared one is built plain. */
9
9
  readonly indexFacets: ReadonlySet<IndexFacet>;
10
10
  /** Not SQLite's own tables, nor the ones libSQL keeps a vector index in: its metadata and `<index>_shadow`. */
11
+ protected triggersQuery(): string;
11
12
  protected getTableNamesQuery(): string;
12
13
  protected tableExistsQuery(): string;
13
14
  protected parseTableExistsResult([row]: SqliteCountRow[]): boolean;
@@ -5,8 +5,11 @@ 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
+ triggersQuery() {
11
+ return /*sql*/ `SELECT name, sql AS definition FROM sqlite_master WHERE type = 'trigger' AND tbl_name = ?`;
12
+ }
10
13
  getTableNamesQuery() {
11
14
  return /*sql*/ `
12
15
  SELECT name
@@ -86,15 +89,12 @@ export class SqliteSchemaIntrospector extends AbstractSqlSchemaIntrospector {
86
89
  const indexSchemas = [];
87
90
  for (const index of results) {
88
91
  const columns = await this.getIndexColumns(read, index.name);
89
- // Include user-created indexes ('c') and multi-column unique constraints ('u')
90
- // Skip primary key indexes ('pk') and single-column unique constraints
91
- const isUserCreated = index.origin === 'c';
92
- 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.
93
93
  // `PRAGMA index_info` names an expression entry `null` (its `cid` is -2), and the expression text
94
94
  // lives only in `sqlite_master.sql`. Reporting `{ column: null }` put a column literally named
95
95
  // `null` into the diff, so an index UQL cannot describe is left out, libSQL's vector index aside.
96
96
  const named = columns.filter((column) => column.name !== null);
97
- if (!isUserCreated && !isCompositeUnique) {
97
+ if (index.origin === 'pk') {
98
98
  continue;
99
99
  }
100
100
  if (named.length === columns.length) {
@@ -168,8 +168,13 @@ export class SqliteSchemaIntrospector extends AbstractSqlSchemaIntrospector {
168
168
  return undefined;
169
169
  }
170
170
  const metric = row.sql?.match(/'metric=(\w+)'/i)?.[1]?.toLowerCase();
171
- const distances = new Map([...this.dialect.vectorMetrics].map(([distance, { index }]) => [index, distance]));
172
- 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
+ };
173
178
  }
174
179
  /** The statement that created the table, which is where SQLite keeps every expression it was given. */
175
180
  async getTableDdl(read, tableName) {
@@ -63,6 +63,33 @@ export declare class Migrator {
63
63
  * Generate a migration based on entity schema differences
64
64
  */
65
65
  generateFromEntities(name: string): Promise<string>;
66
+ /**
67
+ * Each entity on a table this plan does not create - a new one carries its triggers in its `CREATE` -
68
+ * beside the triggers uql has installed there, read once per schema. Kept only where either side has
69
+ * any: one declaring none on a table holding none has nothing to reconcile.
70
+ */
71
+ private installedTriggers;
72
+ /**
73
+ * The alters, with the triggers reconciled around them. Postgres refuses to retype or drop a column a
74
+ * trigger names, so every trigger on a table whose columns change comes off before the alters and what
75
+ * its entity declares goes back on after. `down` is lazy: SQLite cannot express every alter's inverse.
76
+ */
77
+ private alterPlan;
78
+ /**
79
+ * Each entity's triggers taken from what the catalogue holds to what it declares. Against the catalogue
80
+ * rather than a diff, because a trigger hangs off a table whose columns may be unchanged: a body edited
81
+ * on a settled table appears in no diff at all.
82
+ */
83
+ private reconcileTriggers;
84
+ /**
85
+ * The inverse: what the reconcile created dropped, and what it dropped restored as the engine reprints
86
+ * it. Read off the catalogue rather than recorded by uql, and exactly right for restoring one.
87
+ */
88
+ private revertedTriggers;
89
+ /** The entities whose tables are among `created`. */
90
+ private createdEntities;
91
+ /** The table `entity` maps to, as a diff names it. */
92
+ private tableOf;
66
93
  /**
67
94
  * Get all schema differences between entities and database
68
95
  */
@@ -82,7 +109,7 @@ export declare class Migrator {
82
109
  * settle instead of colliding; an existing one still pays for introspection, as a diff needs columns.
83
110
  */
84
111
  private planEntity;
85
- /** The same for one entity against the table it already has, and nothing where the two agree. */
112
+ /** The diff for one entity against the table it already has, and none where the two agree. */
86
113
  private alterFromEntity;
87
114
  /** The configured entities, with `entity` among them however the migrator was built. */
88
115
  private entitiesWith;
@@ -101,6 +128,11 @@ export declare class Migrator {
101
128
  * never asks for the rollback, which on SQLite cannot even be expressed (no `ALTER COLUMN`).
102
129
  */
103
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
+ */
104
136
  protected filterDiff(diff: SchemaDiff, options: {
105
137
  safe?: boolean;
106
138
  drop?: boolean;