uql-orm 0.37.1 → 0.39.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 (85) hide show
  1. package/dist/browser/uql-browser.min.js +2 -2
  2. package/dist/browser/uql-browser.min.js.map +5 -5
  3. package/dist/cockroachdb/cockroachDialect.d.ts +5 -24
  4. package/dist/cockroachdb/cockroachDialect.js +3 -29
  5. package/dist/dialect/abstractSqlDialect.d.ts +42 -12
  6. package/dist/dialect/abstractSqlDialect.js +117 -48
  7. package/dist/dialect/aliases.d.ts +6 -0
  8. package/dist/dialect/aliases.js +6 -0
  9. package/dist/dialect/index.d.ts +0 -5
  10. package/dist/dialect/index.js +2 -5
  11. package/dist/dialect/jsonSql.d.ts +17 -2
  12. package/dist/dialect/jsonSql.js +15 -0
  13. package/dist/dialect/mysqlLikeSqlDialect.d.ts +13 -14
  14. package/dist/dialect/mysqlLikeSqlDialect.js +16 -20
  15. package/dist/dialect/pgLikeSqlDialect.d.ts +17 -20
  16. package/dist/dialect/pgLikeSqlDialect.js +30 -62
  17. package/dist/dialect/vectorCast.d.ts +0 -9
  18. package/dist/dialect/vectorCast.js +0 -8
  19. package/dist/dialect/vectorSqlDialect.d.ts +47 -17
  20. package/dist/dialect/vectorSqlDialect.js +78 -30
  21. package/dist/entity/decorator/entity.d.ts +1 -1
  22. package/dist/entity/metadata/definition.d.ts +1 -1
  23. package/dist/entity/metadata/definition.js +4 -3
  24. package/dist/http/query.js +3 -2
  25. package/dist/libsql/libsqlDialect.d.ts +2 -2
  26. package/dist/libsql/libsqlDialect.js +3 -3
  27. package/dist/maria/mariaDialect.d.ts +12 -17
  28. package/dist/maria/mariaDialect.js +21 -36
  29. package/dist/maria/mariaVectorMetrics.d.ts +8 -0
  30. package/dist/maria/mariaVectorMetrics.js +10 -0
  31. package/dist/maria/mariadbQuerier.d.ts +5 -0
  32. package/dist/maria/mariadbQuerier.js +5 -0
  33. package/dist/migrate/builder/migrationBuilder.d.ts +8 -17
  34. package/dist/migrate/builder/migrationBuilder.js +48 -136
  35. package/dist/migrate/builder/types.d.ts +0 -2
  36. package/dist/migrate/ddl/index.d.ts +11 -0
  37. package/dist/migrate/ddl/index.js +34 -0
  38. package/dist/{dialect/indexSqlDialect.d.ts → migrate/ddl/indexDdl.d.ts} +23 -19
  39. package/dist/migrate/ddl/indexDdl.js +126 -0
  40. package/dist/migrate/ddl/mysqlIndexDdl.d.ts +52 -0
  41. package/dist/migrate/ddl/mysqlIndexDdl.js +125 -0
  42. package/dist/migrate/ddl/pgIndexDdl.d.ts +36 -0
  43. package/dist/migrate/ddl/pgIndexDdl.js +87 -0
  44. package/dist/migrate/drift/driftDetector.js +6 -1
  45. package/dist/migrate/generator/mongoSchemaGenerator.js +1 -1
  46. package/dist/migrate/index.d.ts +1 -0
  47. package/dist/migrate/index.js +2 -0
  48. package/dist/migrate/introspection/mysqlIntrospector.d.ts +9 -3
  49. package/dist/migrate/introspection/mysqlIntrospector.js +27 -5
  50. package/dist/migrate/migrator.js +3 -2
  51. package/dist/migrate/schemaGenerator.d.ts +6 -6
  52. package/dist/migrate/schemaGenerator.js +13 -22
  53. package/dist/mongo/mongoDialect.d.ts +1 -2
  54. package/dist/mongo/mongoDialect.js +29 -22
  55. package/dist/mongo/mongodbQuerier.js +1 -1
  56. package/dist/mysql/mysqlDialect.d.ts +3 -6
  57. package/dist/mysql/mysqlDialect.js +4 -11
  58. package/dist/postgres/postgresDialect.d.ts +2 -0
  59. package/dist/postgres/postgresDialect.js +2 -0
  60. package/dist/querier/abstractSqlQuerier.d.ts +11 -0
  61. package/dist/querier/abstractSqlQuerier.js +35 -0
  62. package/dist/schema/canonicalType.js +35 -36
  63. package/dist/schema/indexDifferences.d.ts +2 -1
  64. package/dist/schema/indexDifferences.js +3 -2
  65. package/dist/sqlite/sqliteDialect.d.ts +2 -2
  66. package/dist/sqlite/sqliteDialect.js +5 -5
  67. package/dist/turso/tursoDialect.d.ts +2 -2
  68. package/dist/turso/tursoDialect.js +4 -4
  69. package/dist/type/dialect.d.ts +21 -7
  70. package/dist/type/dialect.js +17 -0
  71. package/dist/type/entity.d.ts +117 -8
  72. package/dist/type/entity.js +7 -0
  73. package/dist/type/query.d.ts +18 -0
  74. package/dist/type/query.js +6 -0
  75. package/dist/type/queryWhere.d.ts +46 -2
  76. package/dist/type/vector.d.ts +45 -7
  77. package/dist/type/vector.js +16 -1
  78. package/dist/util/dialect.util.d.ts +20 -1
  79. package/dist/util/dialect.util.js +49 -4
  80. package/dist/util/object.util.d.ts +7 -1
  81. package/dist/util/object.util.js +8 -0
  82. package/dist/util/relationQuery.util.d.ts +3 -1
  83. package/dist/util/relationQuery.util.js +8 -3
  84. package/package.json +1 -1
  85. package/dist/dialect/indexSqlDialect.js +0 -103
@@ -0,0 +1,126 @@
1
+ import { jsonTypeMode } from '../../dialect/jsonSql.js';
2
+ import { INDEX_FEATURE_LABELS, } from '../../type/index.js';
3
+ import { getKeys } from '../../util/index.js';
4
+ /**
5
+ * What in an index asks for each feature. A `Record` over the feature union rather than a list, so a
6
+ * feature added to {@link INDEX_FEATURE_LABELS} cannot reach a dialect without the test that decides
7
+ * whether an index wants it - which is how a JSON path once slipped past the entry comparison.
8
+ */
9
+ const INDEX_FEATURE_PROBES = {
10
+ expression: (index) => index.entries.some((entry) => entry.expression),
11
+ jsonPath: (index) => index.entries.some((entry) => entry.jsonPath),
12
+ jsonArray: (index) => index.entries.some((entry) => entry.jsonArray),
13
+ partial: (index) => index.where !== undefined,
14
+ prefixLength: (index) => index.entries.some((entry) => entry.length !== undefined),
15
+ nullsOrder: (index) => index.entries.some((entry) => entry.nulls !== undefined),
16
+ opsClass: (index) => index.entries.some((entry) => entry.opsClass !== undefined),
17
+ include: (index) => Boolean(index.include?.length),
18
+ };
19
+ /**
20
+ * `CREATE INDEX` for SQL dialects: the statement and the fragments each engine spells differently.
21
+ * The migrator's rather than the dialect's, since {@link SqlSchemaGenerator} is the only thing that
22
+ * emits DDL - which is what keeps a `CAST(... ARRAY)` table out of every runtime consumer's entry.
23
+ * The form here is the portable one, which SQLite (and so libSQL, Turso and D1) takes verbatim: no
24
+ * access-method clause, no operator classes, no tuning parameters.
25
+ */
26
+ export class IndexDdl {
27
+ dialect;
28
+ constructor(dialect) {
29
+ this.dialect = dialect;
30
+ }
31
+ getCreateIndexStatement(tableName, index, opts = {}) {
32
+ this.assertIndexFeatures(index);
33
+ const unique = index.unique ? 'UNIQUE ' : '';
34
+ const ifNotExists = (opts.ifNotExists ?? this.dialect.features.indexIfNotExists) ? 'IF NOT EXISTS ' : '';
35
+ const columns = index.entries.map((entry) => this.indexColumn(entry, index)).join(', ');
36
+ return (`CREATE ${unique}${this.indexKeyword(index)} ${ifNotExists}${this.dialect.escapeId(index.name)} ` +
37
+ `ON ${this.dialect.escapeId(tableName)}${this.indexAccessMethod(index)} (${columns})` +
38
+ `${this.indexInclude(index)}${this.indexTuning(index)}${this.indexPredicate(index)};`);
39
+ }
40
+ /**
41
+ * Index features this dialect can express. Everything here is supported by at least one engine and
42
+ * refused by at least one other, so an index asking for a missing one is rejected rather than
43
+ * emitted: each of them is a hard error at the server, not a slower plan.
44
+ */
45
+ indexFeatures = new Set([
46
+ 'expression',
47
+ 'partial',
48
+ 'jsonPath',
49
+ ]);
50
+ assertIndexFeatures(index) {
51
+ for (const feature of getKeys(INDEX_FEATURE_PROBES)) {
52
+ if (INDEX_FEATURE_PROBES[feature](index) && !this.indexFeatures.has(feature)) {
53
+ throw new TypeError(`${this.dialect.dialectName} does not support ${INDEX_FEATURE_LABELS[feature]} (index "${index.name}")`);
54
+ }
55
+ }
56
+ }
57
+ /**
58
+ * Index types this dialect spells as a keyword of their own (`FULLTEXT INDEX`, `VECTOR INDEX`)
59
+ * rather than as an access method after the table. One table drives both, so a type that is a
60
+ * keyword here can never also leak out as a ` USING` clause the engine has no word for.
61
+ */
62
+ indexTypeKeywords = new Map();
63
+ /** The keyword an index type replaces `INDEX` with, or `INDEX` for the types that do not. */
64
+ indexKeyword(index) {
65
+ return (index.type && this.indexTypeKeywords.get(index.type)) || 'INDEX';
66
+ }
67
+ /** One index entry: what is indexed, its operator class if any, then its stored order. */
68
+ indexColumn(entry, index) {
69
+ return `${this.indexColumnTarget(entry)}${this.indexColumnOpsClass(entry, index)}${this.indexColumnOrder(entry)}`;
70
+ }
71
+ /**
72
+ * A quoted column, optionally prefix-limited, or an expression in its own parentheses - the form
73
+ * `((lower("email")))` that MySQL requires and Postgres, CockroachDB and SQLite all accept, so one
74
+ * rendering serves every engine that has expression indexes. A JSON entry is one of those too, its
75
+ * expression compiled from the path rather than written out by the caller.
76
+ */
77
+ indexColumnTarget(entry) {
78
+ if (entry.expression) {
79
+ return `(${entry.column})`;
80
+ }
81
+ const column = this.dialect.escapeId(entry.column);
82
+ if (entry.jsonPath) {
83
+ return `(${this.dialect.jsonPathExpr(column, entry.jsonPath.path, jsonTypeMode(entry.jsonPath.type))})`;
84
+ }
85
+ if (entry.jsonArray) {
86
+ return `(${this.jsonArrayIndexExpr(column, entry.jsonArray)})`;
87
+ }
88
+ return entry.length === undefined ? column : `${column}(${entry.length})`;
89
+ }
90
+ /**
91
+ * One key per *element* of the JSON array, which is MySQL's multi-valued index and nothing else's -
92
+ * every other dialect refuses `jsonArray` in {@link assertIndexFeatures} and never reaches this.
93
+ */
94
+ jsonArrayIndexExpr(_escapedColumn, _json) {
95
+ throw new TypeError(`${this.dialect.dialectName} has no multi-valued index`);
96
+ }
97
+ /** Postgres-wire dialects put a vector or user-declared operator class here. */
98
+ indexColumnOpsClass(_entry, _index) {
99
+ return '';
100
+ }
101
+ /** `ASC` is every engine's default, so only `DESC` is worth emitting. */
102
+ indexColumnOrder(entry) {
103
+ const order = entry.order === 'desc' ? ' DESC' : '';
104
+ return entry.nulls ? `${order} NULLS ${entry.nulls.toUpperCase()}` : order;
105
+ }
106
+ /** ` INCLUDE (...)`: non-key columns stored for index-only scans. Postgres-wire only. */
107
+ indexInclude(_index) {
108
+ return '';
109
+ }
110
+ /** ` USING <method>`, which SQLite's grammar has no place for at all. */
111
+ indexAccessMethod(_index) {
112
+ return '';
113
+ }
114
+ /** pgvector's ` WITH (m = ..., ef_construction = ..., lists = ...)`. */
115
+ indexTuning(_index) {
116
+ return '';
117
+ }
118
+ /**
119
+ * The partial-index predicate. Engines without one reject the index in {@link assertIndexFeatures}
120
+ * rather than reaching here: silently widening a partial unique index changes which rows the
121
+ * database accepts.
122
+ */
123
+ indexPredicate(index) {
124
+ return index.where ? ` WHERE ${index.where}` : '';
125
+ }
126
+ }
@@ -0,0 +1,52 @@
1
+ import type { IndexType } from '../../schema/types.js';
2
+ import type { IndexJsonArray, IndexSchema } from '../../type/index.js';
3
+ import { IndexDdl } from './indexDdl.js';
4
+ /** `CREATE INDEX ... USING btree`, plus the types this family spells as a keyword instead. */
5
+ export declare class MysqlLikeIndexDdl extends IndexDdl {
6
+ protected readonly indexFeatures: Set<"expression" | "include" | "jsonArray" | "jsonPath" | "nullsOrder" | "opsClass" | "partial" | "prefixLength">;
7
+ protected readonly indexTypeKeywords: ReadonlyMap<IndexType, string>;
8
+ /**
9
+ * ` USING btree|hash`, for the types this family does *not* spell as a keyword of its own. A vector
10
+ * type that is neither is one this engine has no index for at all, refused here rather than
11
+ * compiled into a ` USING hnsw` the server can only answer with a syntax error: which of them a
12
+ * dialect *does* have is `indexTypeKeywords`, so declaring one there is all it takes to serve it.
13
+ */
14
+ protected indexAccessMethod(index: IndexSchema): string;
15
+ /** What to do instead, appended to the refusal above. */
16
+ protected readonly vectorIndexHint: string;
17
+ }
18
+ export declare class MySqlIndexDdl extends MysqlLikeIndexDdl {
19
+ /** The multi-valued index is the only JSON index MySQL has - see `IndexFeature` for why. */
20
+ protected readonly indexFeatures: Set<"expression" | "include" | "jsonArray" | "jsonPath" | "nullsOrder" | "opsClass" | "partial" | "prefixLength">;
21
+ /**
22
+ * `CAST(col AS CHAR(64) ARRAY)`, over the column itself where the array is the whole document -
23
+ * which is what `$all` reads, and what its `JSON_CONTAINS(col, ?)` is matched against. A `path`
24
+ * indexes the array at that path instead, as `'tags.ids': { $all: [...] }` reads it.
25
+ */
26
+ protected jsonArrayIndexExpr(escapedColumn: string, json: IndexJsonArray): string;
27
+ /**
28
+ * MySQL has no vector index of any kind, so one is refused rather than compiled to DDL the server
29
+ * rejects: `USING hnsw` is a syntax error, and MariaDB's `VECTOR INDEX` is not MySQL syntax either.
30
+ * Verified against 26.7, which does have `VECTOR` columns and `STRING_TO_VECTOR`, but no distance
31
+ * function outside HeatWave - hence nothing to index for.
32
+ */
33
+ protected readonly vectorIndexHint = ". Vector search on MySQL needs HeatWave";
34
+ }
35
+ export declare class MariaIndexDdl extends MysqlLikeIndexDdl {
36
+ /**
37
+ * MariaDB has no functional indexes: `CREATE INDEX ... ((lower(col)))` is a syntax error even on
38
+ * 12.3, where the documented workaround is a generated column. So it keeps the prefix lengths the
39
+ * family shares and drops expressions - and with them both JSON index forms, which are expressions.
40
+ */
41
+ protected readonly indexFeatures: Set<"expression" | "include" | "jsonArray" | "jsonPath" | "nullsOrder" | "opsClass" | "partial" | "prefixLength">;
42
+ /** The family's, plus a vector index of its own: `CREATE VECTOR INDEX ... ON t (col)`, 11.7+. */
43
+ protected readonly indexTypeKeywords: ReadonlyMap<IndexType, string>;
44
+ /**
45
+ * `M=n DISTANCE=metric`, trailing its `CREATE VECTOR INDEX`. The metric names are MariaDB's own
46
+ * (`euclidean`, not `l2`), and an unsupported one throws rather than being dropped, which would
47
+ * silently build the index on euclidean - its default - instead of what the entity asked for.
48
+ */
49
+ protected indexTuning(index: IndexSchema): string;
50
+ /** `vector` is its own keyword above; pgvector's names are not access methods it has. */
51
+ protected readonly vectorIndexHint = "; declare type: 'vector' instead";
52
+ }
@@ -0,0 +1,125 @@
1
+ import { jsonPath } from '../../dialect/jsonSql.js';
2
+ import { MARIA_VECTOR_METRICS } from '../../maria/mariaVectorMetrics.js';
3
+ import { isVectorIndexType, unsupportedVectorMetric } from '../../type/vector.js';
4
+ import { IndexDdl } from './indexDdl.js';
5
+ /**
6
+ * A full-text index is its own keyword here (`CREATE FULLTEXT INDEX ... (cols)`); `USING fulltext` is
7
+ * a syntax error, so it is the keyword that changes rather than the access method.
8
+ */
9
+ const MYSQL_LIKE_INDEX_KEYWORDS = new Map([['fulltext', 'FULLTEXT INDEX']]);
10
+ /** `CREATE INDEX ... USING btree`, plus the types this family spells as a keyword instead. */
11
+ export class MysqlLikeIndexDdl extends IndexDdl {
12
+ indexFeatures = new Set(['expression', 'prefixLength']);
13
+ indexTypeKeywords = MYSQL_LIKE_INDEX_KEYWORDS;
14
+ /**
15
+ * ` USING btree|hash`, for the types this family does *not* spell as a keyword of its own. A vector
16
+ * type that is neither is one this engine has no index for at all, refused here rather than
17
+ * compiled into a ` USING hnsw` the server can only answer with a syntax error: which of them a
18
+ * dialect *does* have is `indexTypeKeywords`, so declaring one there is all it takes to serve it.
19
+ */
20
+ indexAccessMethod(index) {
21
+ const type = index.type;
22
+ if (!type || this.indexTypeKeywords.has(type)) {
23
+ return '';
24
+ }
25
+ if (isVectorIndexType(type)) {
26
+ throw new TypeError(`${this.dialect.dialectName} has no ${type} index (index "${index.name}")${this.vectorIndexHint}`);
27
+ }
28
+ return ` USING ${type}`;
29
+ }
30
+ /** What to do instead, appended to the refusal above. */
31
+ vectorIndexHint = '';
32
+ }
33
+ export class MySqlIndexDdl extends MysqlLikeIndexDdl {
34
+ /** The multi-valued index is the only JSON index MySQL has - see `IndexFeature` for why. */
35
+ indexFeatures = new Set(['expression', 'prefixLength', 'jsonArray']);
36
+ /**
37
+ * `CAST(col AS CHAR(64) ARRAY)`, over the column itself where the array is the whole document -
38
+ * which is what `$all` reads, and what its `JSON_CONTAINS(col, ?)` is matched against. A `path`
39
+ * indexes the array at that path instead, as `'tags.ids': { $all: [...] }` reads it.
40
+ */
41
+ jsonArrayIndexExpr(escapedColumn, json) {
42
+ const source = json.path ? `${escapedColumn}->${jsonPath(json.path)}` : escapedColumn;
43
+ return `CAST(${source} AS ${arrayCastType(json)} ARRAY)`;
44
+ }
45
+ /**
46
+ * MySQL has no vector index of any kind, so one is refused rather than compiled to DDL the server
47
+ * rejects: `USING hnsw` is a syntax error, and MariaDB's `VECTOR INDEX` is not MySQL syntax either.
48
+ * Verified against 26.7, which does have `VECTOR` columns and `STRING_TO_VECTOR`, but no distance
49
+ * function outside HeatWave - hence nothing to index for.
50
+ */
51
+ vectorIndexHint = '. Vector search on MySQL needs HeatWave';
52
+ }
53
+ export class MariaIndexDdl extends MysqlLikeIndexDdl {
54
+ /**
55
+ * MariaDB has no functional indexes: `CREATE INDEX ... ((lower(col)))` is a syntax error even on
56
+ * 12.3, where the documented workaround is a generated column. So it keeps the prefix lengths the
57
+ * family shares and drops expressions - and with them both JSON index forms, which are expressions.
58
+ */
59
+ indexFeatures = new Set(['prefixLength']);
60
+ /** The family's, plus a vector index of its own: `CREATE VECTOR INDEX ... ON t (col)`, 11.7+. */
61
+ indexTypeKeywords = new Map([
62
+ ...MYSQL_LIKE_INDEX_KEYWORDS,
63
+ ['vector', 'VECTOR INDEX'],
64
+ ]);
65
+ /**
66
+ * `M=n DISTANCE=metric`, trailing its `CREATE VECTOR INDEX`. The metric names are MariaDB's own
67
+ * (`euclidean`, not `l2`), and an unsupported one throws rather than being dropped, which would
68
+ * silently build the index on euclidean - its default - instead of what the entity asked for.
69
+ */
70
+ indexTuning(index) {
71
+ let tuning = index.m === undefined ? '' : ` M=${index.m}`;
72
+ if (index.distance) {
73
+ const metric = MARIA_VECTOR_METRICS.get(index.distance);
74
+ if (!metric) {
75
+ throw unsupportedVectorMetric(this.dialect.dialectName, index.distance, index.name);
76
+ }
77
+ tuning += ` DISTANCE=${metric}`;
78
+ }
79
+ return tuning;
80
+ }
81
+ /** `vector` is its own keyword above; pgvector's names are not access methods it has. */
82
+ vectorIndexHint = "; declare type: 'vector' instead";
83
+ }
84
+ /**
85
+ * MySQL's `CAST(... AS <type> ARRAY)` targets, the closed list its multi-valued index takes: no
86
+ * `FLOAT`, no `BOOLEAN`, no `JSON`. A `DECIMAL` element is the way to index a fractional one.
87
+ */
88
+ const ARRAY_CASTS = new Map([
89
+ [String, 'CHAR'],
90
+ [Number, 'SIGNED'],
91
+ [BigInt, 'SIGNED'],
92
+ [Date, 'DATETIME'],
93
+ ['char', 'CHAR'],
94
+ ['varchar', 'CHAR'],
95
+ ['text', 'CHAR'],
96
+ ['uuid', 'CHAR'],
97
+ ['int', 'SIGNED'],
98
+ ['integer', 'SIGNED'],
99
+ ['tinyint', 'SIGNED'],
100
+ ['smallint', 'SIGNED'],
101
+ ['bigint', 'SIGNED'],
102
+ ['decimal', 'DECIMAL'],
103
+ ['numeric', 'DECIMAL'],
104
+ ['date', 'DATE'],
105
+ ['time', 'TIME'],
106
+ ['datetime', 'DATETIME'],
107
+ ['timestamp', 'DATETIME'],
108
+ ['blob', 'BINARY'],
109
+ ['bytea', 'BINARY'],
110
+ ]);
111
+ /** The cast an element type compiles to; the sized ones need their length, since it sizes the key. */
112
+ function arrayCastType(json) {
113
+ const type = json.type;
114
+ const cast = ARRAY_CASTS.get(typeof type === 'string' ? type.toLowerCase() : type);
115
+ if (!cast) {
116
+ throw new TypeError(`mysql has no array cast for ${typeof type === 'string' ? type : type.name} elements`);
117
+ }
118
+ if (cast !== 'CHAR' && cast !== 'BINARY') {
119
+ return cast;
120
+ }
121
+ if (!json.length) {
122
+ throw new TypeError(`a multi-valued index over ${cast === 'CHAR' ? 'string' : 'binary'} elements needs a length`);
123
+ }
124
+ return `${cast}(${json.length})`;
125
+ }
@@ -0,0 +1,36 @@
1
+ import type { PgLikeSqlDialect } from '../../dialect/pgLikeSqlDialect.js';
2
+ import type { IndexColumnSchema, IndexSchema } from '../../type/index.js';
3
+ import { IndexDdl } from './indexDdl.js';
4
+ /** `CREATE INDEX ... USING hnsw ("embedding" vector_cosine_ops) WITH (m = ...)`, pgvector's form. */
5
+ export declare class PgIndexDdl extends IndexDdl<PgLikeSqlDialect> {
6
+ protected readonly indexFeatures: Set<"expression" | "include" | "jsonArray" | "jsonPath" | "nullsOrder" | "opsClass" | "partial" | "prefixLength">;
7
+ /** pgvector's own index types; CockroachDB's native one widens this. */
8
+ protected isVectorIndex(index: IndexSchema): boolean;
9
+ protected indexAccessMethod(index: IndexSchema): string;
10
+ /**
11
+ * A vector index's operator class is named `{type}_{metric}_ops`: an index on a `halfvec` column
12
+ * needs `halfvec_cosine_ops`, and `vector_cosine_ops` there is rejected outright. An unsupported
13
+ * distance throws rather than being omitted, since a bare `USING hnsw ("embedding")` would build
14
+ * with the dialect's default metric instead of the one requested, with nothing signalling it.
15
+ * Everything else takes the operator class the entry declares, e.g. `jsonb_path_ops` for GIN.
16
+ */
17
+ protected indexColumnOpsClass(entry: IndexColumnSchema, index: IndexSchema): string;
18
+ protected indexInclude(index: IndexSchema): string;
19
+ protected indexTuning(index: IndexSchema): string;
20
+ }
21
+ /**
22
+ * CockroachDB's vector index is native and has its own syntax: `CREATE VECTOR INDEX ... ("col"
23
+ * vector_cosine_ops)`, with no access-method keyword, and tuning knobs of its own names that UQL
24
+ * does not map. `type: 'vector'` is its trigger, the same generic value MariaDB's index uses.
25
+ *
26
+ * `NULLS FIRST/LAST` answers "unimplemented: this syntax" and `jsonb_path_ops` "operator class is
27
+ * not supported" (both verified on v26.2), so neither is offered here.
28
+ */
29
+ export declare class CockroachIndexDdl extends PgIndexDdl {
30
+ protected readonly indexFeatures: Set<"expression" | "include" | "jsonArray" | "jsonPath" | "nullsOrder" | "opsClass" | "partial" | "prefixLength">;
31
+ private isNativeVectorIndex;
32
+ protected isVectorIndex(index: IndexSchema): boolean;
33
+ protected indexKeyword(index: IndexSchema): string;
34
+ protected indexAccessMethod(index: IndexSchema): string;
35
+ protected indexTuning(index: IndexSchema): string;
36
+ }
@@ -0,0 +1,87 @@
1
+ import { unsupportedVectorMetric } from '../../type/vector.js';
2
+ import { IndexDdl } from './indexDdl.js';
3
+ /** `CREATE INDEX ... USING hnsw ("embedding" vector_cosine_ops) WITH (m = ...)`, pgvector's form. */
4
+ export class PgIndexDdl extends IndexDdl {
5
+ indexFeatures = new Set([
6
+ 'expression',
7
+ 'partial',
8
+ 'nullsOrder',
9
+ 'opsClass',
10
+ 'include',
11
+ 'jsonPath',
12
+ ]);
13
+ /** pgvector's own index types; CockroachDB's native one widens this. */
14
+ isVectorIndex(index) {
15
+ return index.type === 'hnsw' || index.type === 'ivfflat';
16
+ }
17
+ indexAccessMethod(index) {
18
+ return index.type ? ` USING ${index.type}` : '';
19
+ }
20
+ /**
21
+ * A vector index's operator class is named `{type}_{metric}_ops`: an index on a `halfvec` column
22
+ * needs `halfvec_cosine_ops`, and `vector_cosine_ops` there is rejected outright. An unsupported
23
+ * distance throws rather than being omitted, since a bare `USING hnsw ("embedding")` would build
24
+ * with the dialect's default metric instead of the one requested, with nothing signalling it.
25
+ * Everything else takes the operator class the entry declares, e.g. `jsonb_path_ops` for GIN.
26
+ */
27
+ indexColumnOpsClass(entry, index) {
28
+ if (!this.isVectorIndex(index) || !index.distance) {
29
+ return entry.opsClass ? ` ${entry.opsClass}` : '';
30
+ }
31
+ const metric = this.dialect.vectorMetrics.get(index.distance);
32
+ if (!metric) {
33
+ throw unsupportedVectorMetric(this.dialect.dialectName, index.distance, index.name);
34
+ }
35
+ const vectorType = this.dialect.supportedVectorType(index.vectorType ?? 'vector');
36
+ const opsClass = `${vectorType}_${metric.opsSuffix}_ops`;
37
+ // IVFFlat has neither a sparsevec nor an L1 operator class; HNSW has all of them (pgvector 0.8.2).
38
+ if (index.type === 'ivfflat' && (vectorType === 'sparsevec' || index.distance === 'l1')) {
39
+ throw new TypeError(`ivfflat has no ${opsClass} operator class (index "${index.name}"); use hnsw`);
40
+ }
41
+ return ` ${opsClass}`;
42
+ }
43
+ indexInclude(index) {
44
+ return index.include?.length
45
+ ? ` INCLUDE (${index.include.map((column) => this.dialect.escapeId(column)).join(', ')})`
46
+ : '';
47
+ }
48
+ indexTuning(index) {
49
+ if (!this.isVectorIndex(index)) {
50
+ return '';
51
+ }
52
+ const params = [];
53
+ if (index.m !== undefined)
54
+ params.push(`m = ${index.m}`);
55
+ if (index.efConstruction !== undefined)
56
+ params.push(`ef_construction = ${index.efConstruction}`);
57
+ if (index.lists !== undefined)
58
+ params.push(`lists = ${index.lists}`);
59
+ return params.length > 0 ? ` WITH (${params.join(', ')})` : '';
60
+ }
61
+ }
62
+ /**
63
+ * CockroachDB's vector index is native and has its own syntax: `CREATE VECTOR INDEX ... ("col"
64
+ * vector_cosine_ops)`, with no access-method keyword, and tuning knobs of its own names that UQL
65
+ * does not map. `type: 'vector'` is its trigger, the same generic value MariaDB's index uses.
66
+ *
67
+ * `NULLS FIRST/LAST` answers "unimplemented: this syntax" and `jsonb_path_ops` "operator class is
68
+ * not supported" (both verified on v26.2), so neither is offered here.
69
+ */
70
+ export class CockroachIndexDdl extends PgIndexDdl {
71
+ indexFeatures = new Set(['expression', 'partial', 'include', 'jsonPath']);
72
+ isNativeVectorIndex(index) {
73
+ return index.type === 'vector';
74
+ }
75
+ isVectorIndex(index) {
76
+ return this.isNativeVectorIndex(index) || super.isVectorIndex(index);
77
+ }
78
+ indexKeyword(index) {
79
+ return this.isNativeVectorIndex(index) ? 'VECTOR INDEX' : super.indexKeyword(index);
80
+ }
81
+ indexAccessMethod(index) {
82
+ return this.isNativeVectorIndex(index) ? '' : super.indexAccessMethod(index);
83
+ }
84
+ indexTuning(index) {
85
+ return this.isNativeVectorIndex(index) ? '' : super.indexTuning(index);
86
+ }
87
+ }
@@ -108,7 +108,12 @@ function addAlterColumnDrifts(colDiff, drifts, opts) {
108
108
  if (!colDiff.expected || !colDiff.actual) {
109
109
  return;
110
110
  }
111
- if (opts.checkTypes) {
111
+ // An auto-increment key is created through the dialect's `serialPrimaryKey`, whose spelling the
112
+ // entity never states - `BIGINT UNSIGNED AUTO_INCREMENT` on the MySQL family, where the column then
113
+ // reads back as `BIGINT UNSIGNED` against an entity that can only say `BIGINT`. Comparing the two
114
+ // reported every table uql created itself as drifting on its own id.
115
+ const dialectOwnsType = colDiff.expected.isPrimaryKey && colDiff.expected.isAutoIncrement;
116
+ if (opts.checkTypes && !dialectOwnsType) {
112
117
  const expectedType = formatType(colDiff.expected.type, opts.dialect);
113
118
  const actualType = formatType(colDiff.actual.type, opts.dialect);
114
119
  if (expectedType !== actualType) {
@@ -87,7 +87,7 @@ export class MongoSchemaGenerator extends AbstractDialect {
87
87
  generateCreateIndex(tableName, index) {
88
88
  const key = {};
89
89
  for (const entry of index.entries) {
90
- if (entry.expression || entry.length !== undefined || entry.nulls || entry.opsClass) {
90
+ if (entry.expression || entry.jsonArray || entry.length !== undefined || entry.nulls || entry.opsClass) {
91
91
  throw new TypeError(`mongodb does not support that index column option (index "${index.name}")`);
92
92
  }
93
93
  key[entry.column] = index.type === 'fulltext' ? 'text' : entry.order === 'desc' ? -1 : 1;
@@ -5,6 +5,7 @@ export { assertCliConfig } from './assertCliConfig.js';
5
5
  export * from './builder/index.js';
6
6
  export { loadConfig } from './cli-config.js';
7
7
  export * from './codegen/index.js';
8
+ export * from './ddl/index.js';
8
9
  export * from './drift/index.js';
9
10
  export * from './introspection/index.js';
10
11
  export { type BuilderMigrationDefinition, defineBuilderMigration, defineMigration, Migrator } from './migrator.js';
@@ -6,6 +6,8 @@ export * from './builder/index.js';
6
6
  export { loadConfig } from './cli-config.js';
7
7
  // Entity code generation
8
8
  export * from './codegen/index.js';
9
+ // `CREATE INDEX`, per dialect family
10
+ export * from './ddl/index.js';
9
11
  // Drift detection
10
12
  export * from './drift/index.js';
11
13
  // Schema introspection
@@ -44,8 +44,14 @@ type MysqlColumnRow = {
44
44
  column_comment: string | null;
45
45
  };
46
46
  /**
47
- * Alias for MysqlSchemaIntrospector.
48
- * MariaDB uses the same information_schema structure as MySQL.
47
+ * MariaDB reads out of the same `information_schema` as MySQL, save for one column type it does not
48
+ * have: `JSON` there is an alias for `LONGTEXT` plus a `json_valid()` check constraint named after
49
+ * the column, and the catalogue reports the column as `longtext`. That check is the only thing
50
+ * telling one from a column somebody really declared `LONGTEXT`, so it is what the type is read back
51
+ * through - without it every JSON column drifts against the entity that declared it ("expected
52
+ * JSON, got LONGTEXT", flagged as data loss) on a table uql created itself.
49
53
  */
50
- export declare const MariadbSchemaIntrospector: typeof MysqlSchemaIntrospector;
54
+ export declare class MariadbSchemaIntrospector extends MysqlSchemaIntrospector {
55
+ protected mapColumnsResult(read: TableRowReader, tableName: string, results: MysqlColumnRow[]): Promise<ColumnSchema[]>;
56
+ }
51
57
  export {};
@@ -54,7 +54,7 @@ export class MysqlSchemaIntrospector extends AbstractSqlSchemaIntrospector {
54
54
  return /*sql*/ `
55
55
  SELECT
56
56
  INDEX_NAME as index_name,
57
- GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) as columns,
57
+ GROUP_CONCAT(COALESCE(COLUMN_NAME, '') ORDER BY SEQ_IN_INDEX) as columns,
58
58
  NOT NON_UNIQUE as is_unique
59
59
  FROM information_schema.STATISTICS
60
60
  WHERE TABLE_SCHEMA = ${this.schemaExpr}
@@ -112,7 +112,10 @@ export class MysqlSchemaIntrospector extends AbstractSqlSchemaIntrospector {
112
112
  async mapIndexesResult(_read, _tableName, results) {
113
113
  return results.map((row) => ({
114
114
  name: row.index_name,
115
- entries: (row.columns || '').split(',').map((column) => ({ column })),
115
+ // A functional or multi-valued key part has no `COLUMN_NAME` - the `COALESCE` above keeps its
116
+ // place in the list, and it is reported as the expression it is, which is what stops diffing
117
+ // from comparing an entry list the server cannot state against the entity's own.
118
+ entries: (row.columns ?? '').split(',').map((column) => (column ? { column } : { column, expression: true })),
116
119
  unique: Boolean(row.is_unique),
117
120
  }));
118
121
  }
@@ -150,7 +153,26 @@ export class MysqlSchemaIntrospector extends AbstractSqlSchemaIntrospector {
150
153
  }
151
154
  }
152
155
  /**
153
- * Alias for MysqlSchemaIntrospector.
154
- * MariaDB uses the same information_schema structure as MySQL.
156
+ * MariaDB reads out of the same `information_schema` as MySQL, save for one column type it does not
157
+ * have: `JSON` there is an alias for `LONGTEXT` plus a `json_valid()` check constraint named after
158
+ * the column, and the catalogue reports the column as `longtext`. That check is the only thing
159
+ * telling one from a column somebody really declared `LONGTEXT`, so it is what the type is read back
160
+ * through - without it every JSON column drifts against the entity that declared it ("expected
161
+ * JSON, got LONGTEXT", flagged as data loss) on a table uql created itself.
155
162
  */
156
- export const MariadbSchemaIntrospector = MysqlSchemaIntrospector;
163
+ export class MariadbSchemaIntrospector extends MysqlSchemaIntrospector {
164
+ async mapColumnsResult(read, tableName, results) {
165
+ const columns = await super.mapColumnsResult(read, tableName, results);
166
+ const checks = await read(
167
+ /*sql*/ `
168
+ SELECT CONSTRAINT_NAME as column_name
169
+ FROM information_schema.CHECK_CONSTRAINTS
170
+ WHERE CONSTRAINT_SCHEMA = ${this.schemaExpr}
171
+ AND TABLE_NAME = ?
172
+ AND CHECK_CLAUSE = CONCAT('json_valid(\`', CONSTRAINT_NAME, '\`)')
173
+ `, [tableName]);
174
+ const jsonColumns = new Set(checks.map((row) => row.column_name));
175
+ // The reported `LONGTEXT` length is that type's maximum, which means nothing for a JSON column.
176
+ return columns.map((column) => jsonColumns.has(column.name) ? { ...column, type: 'JSON', length: undefined } : column);
177
+ }
178
+ }
@@ -8,7 +8,7 @@ import { LoggerWrapper } from '../util/index.js';
8
8
  import { withQuerierForMigrations, withSqlQuerierForMigrations } from './acquireQuerierForMigrations.js';
9
9
  import { buildSqlQuerierMigrationModule, EMPTY_MANUAL_MIGRATION_DOWN_INNER, EMPTY_MANUAL_MIGRATION_UP_INNER, emitSqlRunCalls, } from './codegen/migrationFile.js';
10
10
  import { runMongoCommand } from './generator/mongoCommand.js';
11
- import { CockroachSchemaIntrospector, MongoSchemaIntrospector, MysqlSchemaIntrospector, PostgresSchemaIntrospector, SqliteSchemaIntrospector, } from './introspection/index.js';
11
+ import { CockroachSchemaIntrospector, MariadbSchemaIntrospector, MongoSchemaIntrospector, MysqlSchemaIntrospector, PostgresSchemaIntrospector, SqliteSchemaIntrospector, } from './introspection/index.js';
12
12
  import { createSchemaGenerator } from './schemaGenerator.js';
13
13
  import { createSchemaGeneratorAsync } from './schemaGeneratorAsync.js';
14
14
  import { DatabaseMigrationStorage } from './storage/databaseStorage.js';
@@ -83,8 +83,9 @@ export class Migrator {
83
83
  case 'cockroachdb':
84
84
  return new CockroachSchemaIntrospector(this.pool, schema);
85
85
  case 'mysql':
86
- case 'mariadb':
87
86
  return new MysqlSchemaIntrospector(this.pool, schema);
87
+ case 'mariadb':
88
+ return new MariadbSchemaIntrospector(this.pool, schema);
88
89
  case 'sqlite':
89
90
  return new SqliteSchemaIntrospector(this.pool);
90
91
  case 'mongodb':
@@ -3,6 +3,7 @@ import type { SchemaAST } from '../schema/schemaAST.js';
3
3
  import type { CanonicalType, ColumnNode, ForeignKeyAction, IndexNode, TableNode } from '../schema/types.js';
4
4
  import type { ColumnSchema, CreateSchemaOptions, DialectFeatures, DropSchemaOptions, EntityMeta, FieldOptions, IndexSchema, NamingStrategy, SchemaDiff, SchemaGenerator, SqlDdlGenerator, Type } from '../type/index.js';
5
5
  import type { FullColumnDefinition, TableDefinition, TableForeignKeyDefinition } from './builder/types.js';
6
+ import { type IndexDdl } from './ddl/index.js';
6
7
  /**
7
8
  * Unified SQL schema generator.
8
9
  * Parameterized by dialect to handle Postgres, MySQL, MariaDB, and SQLite.
@@ -10,6 +11,8 @@ import type { FullColumnDefinition, TableDefinition, TableForeignKeyDefinition }
10
11
  export declare class SqlSchemaGenerator implements SqlDdlGenerator {
11
12
  protected readonly dialect: AbstractSqlDialect;
12
13
  protected readonly defaultForeignKeyAction: ForeignKeyAction;
14
+ /** `CREATE INDEX` for this dialect: the migrator's, so a runtime import carries none of it. */
15
+ protected readonly indexDdl: IndexDdl;
13
16
  constructor(dialect: AbstractSqlDialect, defaultForeignKeyAction?: ForeignKeyAction);
14
17
  get namingStrategy(): NamingStrategy | undefined;
15
18
  get features(): DialectFeatures;
@@ -99,14 +102,11 @@ export declare class SqlSchemaGenerator implements SqlDdlGenerator {
99
102
  *
100
103
  * Only ever additive. An index the entity does not name is left alone: it may well have been
101
104
  * created deliberately outside the ORM, and dropping it is a decision for a reviewed migration.
105
+ *
106
+ * A vector index is one of these like any other: MariaDB's `CREATE VECTOR INDEX ... ON t (col)`
107
+ * adds one to a table that already exists, which the inline `CREATE TABLE` form it also has cannot.
102
108
  */
103
109
  private missingIndexes;
104
- /**
105
- * A vector index this dialect declares inside `CREATE TABLE` rather than as a statement of its own,
106
- * which MariaDB is alone in doing. It has no `CREATE INDEX` form, so it can only ever be created
107
- * with its table, never added to one.
108
- */
109
- private isInlineVectorIndex;
110
110
  private columnNodeToSchema;
111
111
  /**
112
112
  * Convert field options to ColumnSchema. Both sides of a diff are the engine's SQL spelling: what it