uql-orm 0.67.0 → 0.68.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/browser/uql-browser.min.js +2 -2
  2. package/dist/browser/uql-browser.min.js.map +3 -3
  3. package/dist/cockroachdb/cockroachDialect.d.ts +6 -4
  4. package/dist/cockroachdb/cockroachDialect.js +7 -3
  5. package/dist/d1/d1SqliteDialect.d.ts +1 -1
  6. package/dist/d1/d1SqliteDialect.js +1 -1
  7. package/dist/dialect/abstractSqlDialect.d.ts +107 -97
  8. package/dist/dialect/abstractSqlDialect.js +250 -293
  9. package/dist/dialect/hydrateColumn.js +33 -30
  10. package/dist/dialect/jsonSql.d.ts +32 -23
  11. package/dist/dialect/jsonSql.js +42 -31
  12. package/dist/dialect/mysqlLikeSqlDialect.d.ts +19 -23
  13. package/dist/dialect/mysqlLikeSqlDialect.js +34 -51
  14. package/dist/dialect/pgLikeSqlDialect.d.ts +24 -17
  15. package/dist/dialect/pgLikeSqlDialect.js +53 -26
  16. package/dist/dialect/vectorCast.d.ts +2 -0
  17. package/dist/dialect/vectorCast.js +25 -5
  18. package/dist/dialect/vectorSqlDialect.d.ts +8 -8
  19. package/dist/dialect/vectorSqlDialect.js +11 -11
  20. package/dist/index.d.ts +1 -0
  21. package/dist/maria/mariaDialect.d.ts +16 -11
  22. package/dist/maria/mariaDialect.js +23 -19
  23. package/dist/migrate/ddl/indexDdl.d.ts +3 -1
  24. package/dist/migrate/ddl/indexDdl.js +5 -1
  25. package/dist/migrate/ddl/mysqlIndexDdl.d.ts +7 -2
  26. package/dist/migrate/ddl/mysqlIndexDdl.js +28 -6
  27. package/dist/migrate/ddl/pgIndexDdl.d.ts +0 -9
  28. package/dist/migrate/ddl/pgIndexDdl.js +2 -6
  29. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.d.ts +12 -1
  30. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +13 -3
  31. package/dist/migrate/introspection/mssqlIntrospector.d.ts +2 -9
  32. package/dist/migrate/introspection/mssqlIntrospector.js +2 -9
  33. package/dist/migrate/introspection/mysqlIntrospector.d.ts +2 -9
  34. package/dist/migrate/introspection/mysqlIntrospector.js +2 -8
  35. package/dist/mongo/mongoDialect.d.ts +22 -10
  36. package/dist/mongo/mongoDialect.js +86 -38
  37. package/dist/mssql/mssqlDialect.d.ts +22 -18
  38. package/dist/mssql/mssqlDialect.js +54 -43
  39. package/dist/mysql/mysqlDialect.d.ts +16 -1
  40. package/dist/mysql/mysqlDialect.js +18 -2
  41. package/dist/sqlite/sqliteDialect.d.ts +17 -16
  42. package/dist/sqlite/sqliteDialect.js +35 -39
  43. package/dist/type/dialect.d.ts +0 -4
  44. package/dist/type/entity.d.ts +8 -3
  45. package/dist/type/vector.d.ts +5 -7
  46. package/dist/util/dialect.util.d.ts +2 -0
  47. package/dist/util/dialect.util.js +6 -2
  48. package/dist/util/object.util.d.ts +2 -4
  49. package/dist/util/object.util.js +4 -9
  50. package/package.json +1 -1
  51. package/dist/dialect/jsonArrayElemMatchUtils.d.ts +0 -2
  52. package/dist/dialect/jsonArrayElemMatchUtils.js +0 -7
  53. package/dist/dialect/pgVectorMetrics.d.ts +0 -13
  54. package/dist/dialect/pgVectorMetrics.js +0 -17
  55. package/dist/maria/mariaVectorMetrics.d.ts +0 -8
  56. package/dist/maria/mariaVectorMetrics.js +0 -10
@@ -5,8 +5,14 @@ import { AbstractSqlDialect } from './abstractSqlDialect.js';
5
5
  import { JSON_PULL_ALIAS, RELATION_ROW_ALIAS } from './aliases.js';
6
6
  import { BYTES_PREFIX } from './hydrateColumn.js';
7
7
  import { jsonSetTarget } from './jsonSql.js';
8
- import { PG_VECTOR_METRICS } from './pgVectorMetrics.js';
9
8
  import { resolveVectorCast, toSparsevecLiteral } from './vectorCast.js';
9
+ /** Each metric's pgvector distance operator, and the infix of the operator class its index is built with. */
10
+ export const PG_VECTOR_METRICS = new Map([
11
+ ['cosine', { op: '<=>', index: 'cosine' }],
12
+ ['l2', { op: '<->', index: 'l2' }],
13
+ ['inner', { op: '<#>', index: 'ip' }],
14
+ ['l1', { op: '<+>', index: 'l1' }],
15
+ ]);
10
16
  /** What the Postgres-wire engines have. */
11
17
  export const PG_FEATURES = {
12
18
  ifNotExists: true,
@@ -28,8 +34,6 @@ export const PG_FEATURES = {
28
34
  rowLockOf: true,
29
35
  orderedUpsertReturning: true,
30
36
  orderedJsonAggregates: true,
31
- partialJsonContainment: true,
32
- typedJsonElements: false,
33
37
  narrowVectorTypes: false,
34
38
  vectorTuningNeedsTransaction: true,
35
39
  serialDeclaresPrimaryKey: false,
@@ -130,25 +134,44 @@ export class PgLikeSqlDialect extends AbstractSqlDialect {
130
134
  ctx.addValue(search.$value);
131
135
  ctx.append(')');
132
136
  }
133
- jsonAll(ctx, jsonField, value) {
134
- return `${jsonField} @> ${this.jsonVal(ctx, value)}`;
137
+ jsonContains(ctx, slot, values) {
138
+ return `${this.jsonValue(slot)} @> ${this.jsonVal(ctx, values)}`;
135
139
  }
136
- jsonSize(ctx, jsonField, value) {
137
- return this.buildFragment(ctx, (fragmentCtx) => this.buildSizeComparison(fragmentCtx, () => fragmentCtx.append(`JSONB_ARRAY_LENGTH(${jsonField})`), value));
140
+ jsonLength(slot) {
141
+ return `JSONB_ARRAY_LENGTH(${this.jsonArray(slot)})`;
138
142
  }
139
- /**
140
- * Object elements stay `jsonb` so each field can pick `->` or `->>`. Scalar elements are exploded
141
- * as text unless they are compared as JSON, where `_text` would yield `text = jsonb`.
142
- */
143
- jsonElemFrom(jsonField, fields, alias, asJson = false) {
144
- const fn = fields.length || asJson ? 'JSONB_ARRAY_ELEMENTS' : 'JSONB_ARRAY_ELEMENTS_TEXT';
145
- return `${fn}(${jsonField}) AS ${alias}`;
143
+ /** Each element stays `jsonb`, so it is read as any path is: `->` for the value, `->>` for its text. */
144
+ jsonElemFrom(slot, alias) {
145
+ return `JSONB_ARRAY_ELEMENTS(${this.jsonArray(slot)}) AS ${alias}`;
146
+ }
147
+ jsonIsArray(slot) {
148
+ return `JSONB_TYPEOF(${this.jsonValue(slot)}) = 'array'`;
149
+ }
150
+ /** The array at `slot`, NULL where the value there is none: the array functions fail the whole read on one. */
151
+ jsonArray(slot) {
152
+ return `CASE WHEN ${this.jsonIsArray(slot)} THEN ${this.jsonValue(slot)} END`;
153
+ }
154
+ jsonElemDoc(alias) {
155
+ return alias;
146
156
  }
147
- jsonElemRef(alias, field, asJson = false) {
148
- if (field === undefined) {
149
- return alias;
157
+ /** `->` down the path and `->>` for its text, the document's own text being `#>> '{}'`. */
158
+ jsonPathReading(escapedColumn, path, mode) {
159
+ if (!path) {
160
+ return mode === 'json' ? escapedColumn : `(${escapedColumn} #>> '{}')`;
150
161
  }
151
- return asJson ? `${alias}->'${escapeSingleQuotes(field)}'` : `${alias}->>'${escapeSingleQuotes(field)}'`;
162
+ const segments = path.split('.');
163
+ return segments.reduce((expr, segment, index) => {
164
+ const op = mode === 'text' && index === segments.length - 1 ? '->>' : '->';
165
+ return `(${expr}${op}'${escapeSingleQuotes(segment)}')`;
166
+ }, escapedColumn);
167
+ }
168
+ /** A number only where the value is one: the cast throws on any other text, failing the whole read. */
169
+ jsonPathExpr(escapedColumn, path, mode) {
170
+ const read = super.jsonPathExpr(escapedColumn, path, mode);
171
+ if (mode !== 'numeric') {
172
+ return read;
173
+ }
174
+ return `CASE WHEN JSONB_TYPEOF(${this.jsonValue({ base: escapedColumn, path })}) = 'number' THEN ${read} END`;
152
175
  }
153
176
  get regexpOp() {
154
177
  return '~';
@@ -157,10 +180,13 @@ export class PgLikeSqlDialect extends AbstractSqlDialect {
157
180
  get neOp() {
158
181
  return 'IS DISTINCT FROM';
159
182
  }
160
- /** One array parameter, which a context that inlines values has none of: it lists them instead. */
161
- formatIn(ctx, operand, values, negate) {
183
+ /**
184
+ * One array parameter, which a context that inlines values has none of: it lists them instead. The
185
+ * array takes its type from `operand`, so it needs none of the casts `bind` would give each value.
186
+ */
187
+ formatIn(ctx, operand, values, negate, bind) {
162
188
  if (!values.length || ctx.inlineValues) {
163
- return super.formatIn(ctx, operand, values, negate);
189
+ return super.formatIn(ctx, operand, values, negate, bind);
164
190
  }
165
191
  const ph = this.addValue(ctx, values);
166
192
  return negate ? `${operand} <> ALL(${ph})` : `${operand} = ANY(${ph})`;
@@ -181,13 +207,14 @@ export class PgLikeSqlDialect extends AbstractSqlDialect {
181
207
  ctx.append(`::${vectorType}`);
182
208
  }
183
209
  /**
184
- * `create_if_missing => false` keeps an absent key (and a NULL column) untouched; `WITH
185
- * ORDINALITY` keeps the surviving elements in their original order.
210
+ * `create_if_missing => false` keeps an absent key (and a NULL column) untouched, and a value that is no
211
+ * array is set back as it is; `WITH ORDINALITY` keeps the surviving elements in their original order.
186
212
  */
187
213
  jsonPullKey(ctx, expr, escapedCol, key, value) {
188
- const escapedKey = escapeSingleQuotes(key);
189
- const kept = `SELECT JSONB_AGG(${JSON_PULL_ALIAS}.val ORDER BY ${JSON_PULL_ALIAS}.ord) FROM JSONB_ARRAY_ELEMENTS(${escapedCol}->'${escapedKey}') WITH ORDINALITY AS ${JSON_PULL_ALIAS}(val, ord) WHERE ${JSON_PULL_ALIAS}.val <> ${this.jsonVal(ctx, value)}`;
190
- return `JSONB_SET(${expr}, '{${escapedKey}}', COALESCE((${kept}), '[]'::jsonb), false)`;
214
+ const slot = { base: escapedCol, path: key };
215
+ const kept = `SELECT JSONB_AGG(${JSON_PULL_ALIAS}.val ORDER BY ${JSON_PULL_ALIAS}.ord) FROM JSONB_ARRAY_ELEMENTS(${this.jsonValue(slot)}) WITH ORDINALITY AS ${JSON_PULL_ALIAS}(val, ord) WHERE ${JSON_PULL_ALIAS}.val <> ${this.jsonVal(ctx, value)}`;
216
+ const pulled = `CASE WHEN ${this.jsonIsArray(slot)} THEN COALESCE((${kept}), '[]'::jsonb) ELSE COALESCE(${this.jsonValue(slot)}, 'null') END`;
217
+ return `JSONB_SET(${expr}, '{${escapeSingleQuotes(key)}}', ${pulled}, false)`;
191
218
  }
192
219
  /**
193
220
  * The plain values merge as one bound object; a `raw()` one is an SQL expression rather than JSON,
@@ -20,3 +20,5 @@ export declare function toSparsevecLiteral(values: readonly unknown[]): string;
20
20
  * as it was written, `{1:1,3:2}/3` or `[1,2,3]`; `undefined` where the text matches neither.
21
21
  */
22
22
  export declare function parseVectorLiteral(raw: string, cast: VectorCast): number[] | undefined;
23
+ /** Packed little-endian float32s, each read as {@link shortestFloat32}. */
24
+ export declare function decodeFloat32s(bytes: Uint8Array): number[];
@@ -17,11 +17,8 @@ export function resolveVectorCast(field) {
17
17
  * declaring `type: 'sparsevec'` still hands UQL the dense array its field type promises.
18
18
  */
19
19
  export function toSparsevecLiteral(values) {
20
- const pairs = values
21
- .map((value, index) => `${index + 1}:${value}`)
22
- .filter((_, index) => Number(values[index]) !== 0)
23
- .join(',');
24
- return `{${pairs}}/${values.length}`;
20
+ const pairs = values.flatMap((value, index) => (Number(value) === 0 ? [] : [`${index + 1}:${value}`]));
21
+ return `{${pairs.join(',')}}/${values.length}`;
25
22
  }
26
23
  /**
27
24
  * A vector column's text as the dense array its field promises (pgvector returns text), read by `cast`
@@ -65,3 +62,26 @@ function parseDense(text) {
65
62
  return undefined;
66
63
  }
67
64
  }
65
+ /** Packed little-endian float32s, each read as {@link shortestFloat32}. */
66
+ export function decodeFloat32s(bytes) {
67
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
68
+ return Array.from({ length: bytes.byteLength / 4 }, (_, at) => shortestFloat32(view.getFloat32(at * 4, true)));
69
+ }
70
+ /**
71
+ * `value` in the fewest significant digits that read back to it as a float32, by bisection: nine always
72
+ * do, and a count past one that does nearly always does too, so at worst it keeps a digit it could drop.
73
+ */
74
+ function shortestFloat32(value) {
75
+ let low = 1;
76
+ let high = 9;
77
+ while (low < high) {
78
+ const mid = (low + high) >> 1;
79
+ if (Math.fround(Number(value.toPrecision(mid))) === value) {
80
+ high = mid;
81
+ }
82
+ else {
83
+ low = mid + 1;
84
+ }
85
+ }
86
+ return Number(value.toPrecision(low));
87
+ }
@@ -21,18 +21,18 @@ export declare abstract class VectorSqlDialect extends AbstractDialect {
21
21
  */
22
22
  protected tunedVectorIndex<E>(meta: EntityMeta<E>, q: Query<E>): EntityIndexMeta | undefined;
23
23
  /**
24
- * Every distance metric this dialect has, and how it spells each. Empty means no vector search at
25
- * all, which is what MySQL and D1 are. The key set is the single answer to "is this metric
26
- * supported here", so a metric cannot be searchable and unindexable or the reverse.
24
+ * Every distance metric this dialect has, and how a search and an index spell each. Empty means no
25
+ * vector search at all, which is what MySQL and D1 are. The key set is the single answer to "is this
26
+ * metric supported here", for a query and an index alike.
27
27
  */
28
28
  readonly vectorMetrics: ReadonlyMap<VectorDistance, VectorMetric>;
29
29
  /** Quotes an identifier; supplied by the SQL dialect built on top of this layer. */
30
30
  abstract escapeId(val: string | undefined, forbidQualified?: boolean, addDot?: boolean): string;
31
31
  /**
32
- * Resolve common parameters for a vector similarity ORDER BY expression.
33
- * Shared by all dialect overrides of `appendVectorSort`.
32
+ * What a distance expression reads, for a `$sort` and a `$near` alike. The metric falls back to the
33
+ * field's, then its index's, which serves no other, then cosine.
34
34
  */
35
- protected resolveVectorSortParams<E>(meta: EntityMeta<E>, key: string, search: QueryVectorSearch): {
35
+ protected resolveVectorDistance<E>(meta: EntityMeta<E>, key: string, search: QueryVectorSearch): {
36
36
  colName: string;
37
37
  distance: VectorDistance;
38
38
  field: FieldOptions | undefined;
@@ -49,12 +49,12 @@ export declare abstract class VectorSqlDialect extends AbstractDialect {
49
49
  supportedVectorType(cast: VectorCast): VectorCast;
50
50
  /**
51
51
  * The distance a vector `$sort` projects, which the projection names after `$project`. Delegates to
52
- * `appendVectorSort` so each dialect's distance syntax is written once.
52
+ * `appendVectorDistance` so each dialect's distance syntax is written once.
53
53
  */
54
54
  protected appendVectorProjection<E>(ctx: QueryContext, meta: EntityMeta<E>, key: string, search: QueryVectorSearch): void;
55
55
  /**
56
56
  * The distance expression, in whichever of the two shapes this dialect spells it. One method for
57
57
  * both, so the metric lookup and its refusal exist once rather than per shape.
58
58
  */
59
- protected appendVectorSort<E>(ctx: QueryContext, meta: EntityMeta<E>, key: string, search: QueryVectorSearch): void;
59
+ protected appendVectorDistance<E>(ctx: QueryContext, meta: EntityMeta<E>, key: string, search: QueryVectorSearch): void;
60
60
  }
@@ -35,19 +35,19 @@ export class VectorSqlDialect extends AbstractDialect {
35
35
  return key ? findVectorIndex(meta, key) : undefined;
36
36
  }
37
37
  /**
38
- * Every distance metric this dialect has, and how it spells each. Empty means no vector search at
39
- * all, which is what MySQL and D1 are. The key set is the single answer to "is this metric
40
- * supported here", so a metric cannot be searchable and unindexable or the reverse.
38
+ * Every distance metric this dialect has, and how a search and an index spell each. Empty means no
39
+ * vector search at all, which is what MySQL and D1 are. The key set is the single answer to "is this
40
+ * metric supported here", for a query and an index alike.
41
41
  */
42
42
  vectorMetrics = new Map();
43
43
  /**
44
- * Resolve common parameters for a vector similarity ORDER BY expression.
45
- * Shared by all dialect overrides of `appendVectorSort`.
44
+ * What a distance expression reads, for a `$sort` and a `$near` alike. The metric falls back to the
45
+ * field's, then its index's, which serves no other, then cosine.
46
46
  */
47
- resolveVectorSortParams(meta, key, search) {
47
+ resolveVectorDistance(meta, key, search) {
48
48
  const field = meta.fields[key];
49
49
  const colName = this.resolveColumnName(key, field);
50
- const distance = search.$distance ?? field?.distance ?? 'cosine';
50
+ const distance = search.$distance ?? field?.distance ?? findVectorIndex(meta, key)?.distance ?? 'cosine';
51
51
  return { colName, distance, field };
52
52
  }
53
53
  /**
@@ -66,7 +66,7 @@ export class VectorSqlDialect extends AbstractDialect {
66
66
  }
67
67
  /**
68
68
  * The distance a vector `$sort` projects, which the projection names after `$project`. Delegates to
69
- * `appendVectorSort` so each dialect's distance syntax is written once.
69
+ * `appendVectorDistance` so each dialect's distance syntax is written once.
70
70
  */
71
71
  appendVectorProjection(ctx, meta, key, search) {
72
72
  const alias = search.$project;
@@ -76,17 +76,17 @@ export class VectorSqlDialect extends AbstractDialect {
76
76
  if (meta.fields[alias]) {
77
77
  throw new TypeError(`$project '${alias}' collides with a field of '${entityName(meta)}'`);
78
78
  }
79
- this.appendVectorSort(ctx, meta, key, search);
79
+ this.appendVectorDistance(ctx, meta, key, search);
80
80
  }
81
81
  /**
82
82
  * The distance expression, in whichever of the two shapes this dialect spells it. One method for
83
83
  * both, so the metric lookup and its refusal exist once rather than per shape.
84
84
  */
85
- appendVectorSort(ctx, meta, key, search) {
85
+ appendVectorDistance(ctx, meta, key, search) {
86
86
  if (this.vectorMetrics.size === 0) {
87
87
  throw new TypeError(`${this.dialectName} does not support vector similarity search. Use raw() for vector queries.`);
88
88
  }
89
- const { colName, distance, field } = this.resolveVectorSortParams(meta, key, search);
89
+ const { colName, distance, field } = this.resolveVectorDistance(meta, key, search);
90
90
  const metric = this.vectorMetrics.get(distance);
91
91
  if (!metric) {
92
92
  throw unsupportedVectorMetric(this.dialectName, distance);
package/dist/index.d.ts CHANGED
@@ -5,5 +5,6 @@ export * from './namingStrategy/index.js';
5
5
  export * from './querier/index.js';
6
6
  export * from './type/index.js';
7
7
  export { withDeleted } from './util/filters.util.js';
8
+ export type { HookContext } from './util/hook.util.js';
8
9
  export { DefaultLogger } from './util/logger.js';
9
10
  export { raw, refs } from './util/raw.js';
@@ -1,4 +1,5 @@
1
1
  import { type RelationRows } from '../dialect/abstractSqlDialect.js';
2
+ import { type JsonAccessMode } from '../dialect/jsonSql.js';
2
3
  import { MysqlLikeSqlDialect } from '../dialect/mysqlLikeSqlDialect.js';
3
4
  import type { EntityMeta, FieldOptions, Query, QueryContext, SqlDialectFeatures, Type, VectorDistance, VectorMetric } from '../type/index.js';
4
5
  export declare class MariaDialect extends MysqlLikeSqlDialect {
@@ -16,21 +17,22 @@ export declare class MariaDialect extends MysqlLikeSqlDialect {
16
17
  protected appendRelationArray(ctx: QueryContext, { entity, query, alias, joins }: RelationRows): void;
17
18
  protected upsertReturning<E>(meta: EntityMeta<E>): string;
18
19
  /**
19
- * MariaDB supports neither MySQL's `->`/`->>` shorthand nor the base's chained form. `JSON_VALUE`
20
- * reads a scalar and `JSON_EXTRACT` the subtree that the array operators need.
20
+ * `JSON_EXTRACT` for the value and `JSON_VALUE` for its text, which every version has: MySQL's `->`/`->>`
21
+ * arrive only in 13.1.
21
22
  */
22
- protected getJsonPathScalarExpr(escapedColumn: string, jsonPathStr: string): string;
23
- protected getJsonPathJsonbExpr(escapedColumn: string, jsonPathStr: string): string;
23
+ protected jsonPathReading(escapedColumn: string, path: string, mode: 'json' | 'text'): string;
24
+ /** MariaDB orders JSON as the text it stores, so a number sorts by its value first. */
25
+ protected readonly jsonSortModes: readonly JsonAccessMode[];
24
26
  /** MariaDB has no `CAST(val AS JSON)`; `JSON_EXTRACT` at the root reads a value as JSON. */
25
27
  protected jsonCast(operand: string): string;
26
28
  /**
27
- * MariaDB stores JSON as text, so JSON_ARRAYAGG would re-quote each element into a string
28
- * (`["\"a\""]`). JSON_COMPACT marks it back as JSON, keeping element types intact.
29
+ * MariaDB stores JSON as text, so `JSON_ARRAYAGG` would re-quote each element into a string
30
+ * (`["\"a\""]`); `JSON_COMPACT` marks it back as JSON, keeping its type.
29
31
  */
30
- protected jsonPullElem(alias: string): string;
31
- /** Text-backed JSON compares as text, so use JSON_EQUALS for key-order-independent equality. */
32
- protected jsonPullKeep(alias: string, operand: string): string;
33
- /** `VEC_DISTANCE_COSINE`/`VEC_DISTANCE_EUCLIDEAN`, 11.7+: the metric's own name, uppercased. */
32
+ protected jsonArrayOf(elem: string): string;
33
+ /** Text-backed JSON compares as text, so `JSON_EQUALS` compares it as JSON, key order aside. */
34
+ protected jsonDiffers(elem: string, operand: string): string;
35
+ /** `VEC_DISTANCE_COSINE`/`VEC_DISTANCE_EUCLIDEAN`, 11.7+, which the index's `DISTANCE=` names alike. */
34
36
  readonly vectorMetrics: ReadonlyMap<VectorDistance, VectorMetric>;
35
37
  /**
36
38
  * A `VECTOR` column holds a packed little-endian float32 blob, and MariaDB refuses text where one
@@ -47,6 +49,9 @@ export declare class MariaDialect extends MysqlLikeSqlDialect {
47
49
  protected statementSettings<E>(entity: Type<E>, q: Query<E>): string[];
48
50
  /** `SET STATEMENT ... FOR`, which scopes a variable to the statement it prefixes. */
49
51
  protected applySettings(sql: string, settings: readonly string[]): string;
50
- /** The reverse: selecting a `VECTOR` column raw yields that blob, so it is read back as text. */
52
+ /**
53
+ * The reverse: a `VECTOR` column reads back as its packed bytes in hex, which every driver hands over
54
+ * alike, and never through `VEC_ToText`, which keeps six of the nine digits a float32 needs.
55
+ */
51
56
  protected selectFieldExpr(escapedColumn: string, field: FieldOptions): string;
52
57
  }
@@ -3,7 +3,6 @@ import { jsonPath } from '../dialect/jsonSql.js';
3
3
  import { MYSQL_FEATURES, MysqlLikeSqlDialect } from '../dialect/mysqlLikeSqlDialect.js';
4
4
  import { getMeta } from '../entity/index.js';
5
5
  import { columnFamily } from '../util/field.util.js';
6
- import { MARIA_VECTOR_METRICS } from './mariaVectorMetrics.js';
7
6
  export class MariaDialect extends MysqlLikeSqlDialect {
8
7
  dialectName = 'mariadb';
9
8
  // MariaDB 10.5+ has `INSERT ... RETURNING`, so ids come back exact per row - the upsert's too.
@@ -42,32 +41,34 @@ export class MariaDialect extends MysqlLikeSqlDialect {
42
41
  return returning ? ` ${returning}` : '';
43
42
  }
44
43
  /**
45
- * MariaDB supports neither MySQL's `->`/`->>` shorthand nor the base's chained form. `JSON_VALUE`
46
- * reads a scalar and `JSON_EXTRACT` the subtree that the array operators need.
44
+ * `JSON_EXTRACT` for the value and `JSON_VALUE` for its text, which every version has: MySQL's `->`/`->>`
45
+ * arrive only in 13.1.
47
46
  */
48
- getJsonPathScalarExpr(escapedColumn, jsonPathStr) {
49
- return `JSON_VALUE(${escapedColumn}, ${jsonPath(jsonPathStr)})`;
50
- }
51
- getJsonPathJsonbExpr(escapedColumn, jsonPathStr) {
52
- return `JSON_EXTRACT(${escapedColumn}, ${jsonPath(jsonPathStr)})`;
47
+ jsonPathReading(escapedColumn, path, mode) {
48
+ return `${mode === 'json' ? 'JSON_EXTRACT' : 'JSON_VALUE'}(${escapedColumn}, ${jsonPath(path)})`;
53
49
  }
50
+ /** MariaDB orders JSON as the text it stores, so a number sorts by its value first. */
51
+ jsonSortModes = ['numeric', 'text'];
54
52
  /** MariaDB has no `CAST(val AS JSON)`; `JSON_EXTRACT` at the root reads a value as JSON. */
55
53
  jsonCast(operand) {
56
54
  return `JSON_EXTRACT(${operand}, '$')`;
57
55
  }
58
56
  /**
59
- * MariaDB stores JSON as text, so JSON_ARRAYAGG would re-quote each element into a string
60
- * (`["\"a\""]`). JSON_COMPACT marks it back as JSON, keeping element types intact.
57
+ * MariaDB stores JSON as text, so `JSON_ARRAYAGG` would re-quote each element into a string
58
+ * (`["\"a\""]`); `JSON_COMPACT` marks it back as JSON, keeping its type.
61
59
  */
62
- jsonPullElem(alias) {
63
- return `JSON_COMPACT(${alias}.v)`;
60
+ jsonArrayOf(elem) {
61
+ return super.jsonArrayOf(`JSON_COMPACT(${elem})`);
64
62
  }
65
- /** Text-backed JSON compares as text, so use JSON_EQUALS for key-order-independent equality. */
66
- jsonPullKeep(alias, operand) {
67
- return `NOT JSON_EQUALS(${alias}.v, ${operand})`;
63
+ /** Text-backed JSON compares as text, so `JSON_EQUALS` compares it as JSON, key order aside. */
64
+ jsonDiffers(elem, operand) {
65
+ return `NOT JSON_EQUALS(${elem}, ${operand})`;
68
66
  }
69
- /** `VEC_DISTANCE_COSINE`/`VEC_DISTANCE_EUCLIDEAN`, 11.7+: the metric's own name, uppercased. */
70
- vectorMetrics = new Map([...MARIA_VECTOR_METRICS].map(([metric, name]) => [metric, { fn: `VEC_DISTANCE_${name.toUpperCase()}` }]));
67
+ /** `VEC_DISTANCE_COSINE`/`VEC_DISTANCE_EUCLIDEAN`, 11.7+, which the index's `DISTANCE=` names alike. */
68
+ vectorMetrics = new Map([
69
+ ['cosine', { fn: 'VEC_DISTANCE_COSINE', index: 'cosine' }],
70
+ ['l2', { fn: 'VEC_DISTANCE_EUCLIDEAN', index: 'euclidean' }],
71
+ ]);
71
72
  /**
72
73
  * A `VECTOR` column holds a packed little-endian float32 blob, and MariaDB refuses text where one
73
74
  * belongs: inserting `'[1,2,3]'` fails with `Incorrect vector value`, and passing it to
@@ -94,8 +95,11 @@ export class MariaDialect extends MysqlLikeSqlDialect {
94
95
  applySettings(sql, settings) {
95
96
  return `SET STATEMENT ${settings.join(', ')} FOR ${sql}`;
96
97
  }
97
- /** The reverse: selecting a `VECTOR` column raw yields that blob, so it is read back as text. */
98
+ /**
99
+ * The reverse: a `VECTOR` column reads back as its packed bytes in hex, which every driver hands over
100
+ * alike, and never through `VEC_ToText`, which keeps six of the nine digits a float32 needs.
101
+ */
98
102
  selectFieldExpr(escapedColumn, field) {
99
- return columnFamily(field.type) === 'vector' ? `VEC_ToText(${escapedColumn})` : escapedColumn;
103
+ return columnFamily(field.type) === 'vector' ? this.bytesAsText(escapedColumn) : escapedColumn;
100
104
  }
101
105
  }
@@ -1,6 +1,6 @@
1
1
  import type { AbstractSqlDialect } from '../../dialect/abstractSqlDialect.js';
2
2
  import { type IndexType } from '../../schema/types.js';
3
- import { type IndexColumnSchema, type IndexFeature, type IndexJsonArray, type IndexSchema } from '../../type/index.js';
3
+ import { type IndexColumnSchema, type IndexFeature, type IndexJsonArray, type IndexJsonPath, type IndexSchema } from '../../type/index.js';
4
4
  /** Refuses an index of a type `types` lacks, `hints` naming what to declare instead. */
5
5
  export declare function assertIndexType(index: IndexSchema, types: ReadonlySet<IndexType>, dialectName: string, hints?: ReadonlyMap<IndexType, string>): void;
6
6
  /** Refuses an index asking for a feature `features` lacks. */
@@ -46,6 +46,8 @@ export declare class IndexDdl<D extends AbstractSqlDialect = AbstractSqlDialect>
46
46
  * expression compiled from the path rather than written out by the caller.
47
47
  */
48
48
  protected indexColumnTarget(entry: IndexColumnSchema): string;
49
+ /** The path read the way a query comparing it reads it, which is how the planner matches the two. */
50
+ protected jsonPathIndexExpr(escapedColumn: string, json: IndexJsonPath): string;
49
51
  /**
50
52
  * One key per *element* of the JSON array, which is MySQL's multi-valued index and nothing else's -
51
53
  * every other dialect refuses `jsonArray` in {@link assertIndexFeatures} and never reaches this.
@@ -94,13 +94,17 @@ export class IndexDdl {
94
94
  }
95
95
  const column = this.dialect.escapeId(entry.column);
96
96
  if (entry.jsonPath) {
97
- return `(${this.dialect.jsonPathExpr(column, entry.jsonPath.path, jsonTypeMode(entry.jsonPath.type))})`;
97
+ return `(${this.jsonPathIndexExpr(column, entry.jsonPath)})`;
98
98
  }
99
99
  if (entry.jsonArray) {
100
100
  return `(${this.jsonArrayIndexExpr(column, entry.jsonArray)})`;
101
101
  }
102
102
  return entry.length === undefined ? column : `${column}(${entry.length})`;
103
103
  }
104
+ /** The path read the way a query comparing it reads it, which is how the planner matches the two. */
105
+ jsonPathIndexExpr(escapedColumn, json) {
106
+ return this.dialect.jsonPathExpr(escapedColumn, json.path, jsonTypeMode(json.type));
107
+ }
104
108
  /**
105
109
  * One key per *element* of the JSON array, which is MySQL's multi-valued index and nothing else's -
106
110
  * every other dialect refuses `jsonArray` in {@link assertIndexFeatures} and never reaches this.
@@ -1,5 +1,5 @@
1
1
  import type { IndexType } from '../../schema/types.js';
2
- import type { IndexJsonArray, IndexSchema } from '../../type/index.js';
2
+ import type { IndexJsonArray, IndexJsonPath, IndexSchema } from '../../type/index.js';
3
3
  import { IndexDdl } from './indexDdl.js';
4
4
  /** `CREATE INDEX ... (cols) USING btree`, plus the types this family spells as a keyword instead. */
5
5
  export declare class MysqlLikeIndexDdl extends IndexDdl {
@@ -10,8 +10,13 @@ export declare class MysqlLikeIndexDdl extends IndexDdl {
10
10
  protected indexTuning(index: IndexSchema): string;
11
11
  }
12
12
  export declare class MySqlIndexDdl extends MysqlLikeIndexDdl {
13
- /** The multi-valued index is the only JSON index MySQL has - see `IndexFeature` for why. */
14
13
  protected readonly indexFeatures: Set<"expression" | "include" | "jsonArray" | "jsonPath" | "nullsOrder" | "opsClass" | "partial" | "prefixLength">;
14
+ /**
15
+ * A number as the query reads it. A string as `CHAR(n)` in the collation `->>` returns, which the
16
+ * planner strips back to the bare `->>` a query compares; any other collation leaves it unused. A
17
+ * boolean compares as JSON, which no key part can hold.
18
+ */
19
+ protected jsonPathIndexExpr(escapedColumn: string, json: IndexJsonPath): string;
15
20
  /**
16
21
  * `CAST(col AS CHAR(64) ARRAY)`, over the column itself where the array is the whole document -
17
22
  * which is what `$all` reads, and what its `JSON_CONTAINS(col, ?)` is matched against. A `path`
@@ -1,5 +1,4 @@
1
- import { jsonPath } from '../../dialect/jsonSql.js';
2
- import { MARIA_VECTOR_METRICS } from '../../maria/mariaVectorMetrics.js';
1
+ import { jsonTypeMode } from '../../dialect/jsonSql.js';
3
2
  import { unsupportedVectorMetric, VECTOR_INDEX_TYPES } from '../../type/vector.js';
4
3
  import { IndexDdl } from './indexDdl.js';
5
4
  /**
@@ -18,15 +17,38 @@ export class MysqlLikeIndexDdl extends IndexDdl {
18
17
  }
19
18
  }
20
19
  export class MySqlIndexDdl extends MysqlLikeIndexDdl {
21
- /** The multi-valued index is the only JSON index MySQL has - see `IndexFeature` for why. */
22
- indexFeatures = new Set(['expression', 'prefixLength', 'jsonArray']);
20
+ indexFeatures = new Set([
21
+ 'expression',
22
+ 'prefixLength',
23
+ 'jsonPath',
24
+ 'jsonArray',
25
+ ]);
26
+ /**
27
+ * A number as the query reads it. A string as `CHAR(n)` in the collation `->>` returns, which the
28
+ * planner strips back to the bare `->>` a query compares; any other collation leaves it unused. A
29
+ * boolean compares as JSON, which no key part can hold.
30
+ */
31
+ jsonPathIndexExpr(escapedColumn, json) {
32
+ const mode = jsonTypeMode(json.type);
33
+ if (mode === 'json') {
34
+ throw new TypeError(`mysql cannot index the boolean JSON path '${json.path}', which compares as JSON`);
35
+ }
36
+ const expr = super.jsonPathIndexExpr(escapedColumn, json);
37
+ if (mode === 'numeric') {
38
+ return expr;
39
+ }
40
+ if (!json.length) {
41
+ throw new TypeError(`a MySQL index over the string JSON path '${json.path}' needs a length`);
42
+ }
43
+ return `CAST(${expr} AS CHAR(${json.length}) CHARACTER SET utf8mb4) COLLATE utf8mb4_bin`;
44
+ }
23
45
  /**
24
46
  * `CAST(col AS CHAR(64) ARRAY)`, over the column itself where the array is the whole document -
25
47
  * which is what `$all` reads, and what its `JSON_CONTAINS(col, ?)` is matched against. A `path`
26
48
  * indexes the array at that path instead, as `'tags.ids': { $all: [...] }` reads it.
27
49
  */
28
50
  jsonArrayIndexExpr(escapedColumn, json) {
29
- const source = json.path ? `${escapedColumn}->${jsonPath(json.path)}` : escapedColumn;
51
+ const source = json.path ? this.dialect.jsonPathExpr(escapedColumn, json.path, 'json') : escapedColumn;
30
52
  return `CAST(${source} AS ${arrayCastType(json)} ARRAY)`;
31
53
  }
32
54
  /**
@@ -61,7 +83,7 @@ export class MariaIndexDdl extends MysqlLikeIndexDdl {
61
83
  indexTuning(index) {
62
84
  let tuning = super.indexTuning(index) + (index.m === undefined ? '' : ` M=${index.m}`);
63
85
  if (index.distance) {
64
- const metric = MARIA_VECTOR_METRICS.get(index.distance);
86
+ const metric = this.dialect.vectorMetrics.get(index.distance)?.index;
65
87
  if (!metric) {
66
88
  throw unsupportedVectorMetric(this.dialect.dialectName, index.distance, index.name);
67
89
  }
@@ -6,11 +6,6 @@ export declare class PgIndexDdl extends IndexDdl {
6
6
  protected readonly indexTypes: Set<"brin" | "btree" | "fulltext" | "gin" | "gist" | "hash" | "hnsw" | "ivfflat" | "vector" | "vectorSearch">;
7
7
  protected readonly indexTypeHints: ReadonlyMap<"brin" | "btree" | "fulltext" | "gin" | "gist" | "hash" | "hnsw" | "ivfflat" | "vector" | "vectorSearch", string>;
8
8
  protected readonly indexFeatures: Set<"expression" | "include" | "jsonArray" | "jsonPath" | "nullsOrder" | "opsClass" | "partial" | "prefixLength">;
9
- /** The metrics its vector index takes, each naming the operator class it is built with. */
10
- protected readonly vectorMetrics: ReadonlyMap<import("../../type/vector.js").VectorDistance, {
11
- readonly op: string;
12
- readonly opsSuffix: string;
13
- }>;
14
9
  /** pgvector's own index types; CockroachDB's native one widens this. */
15
10
  protected isVectorIndex(index: IndexSchema): boolean;
16
11
  protected indexAccessMethod(index: IndexSchema): string;
@@ -28,10 +23,6 @@ export declare class CockroachIndexDdl extends PgIndexDdl {
28
23
  /** v26.3 answers `hash` and `brin` "unimplemented", `ivfflat` "unrecognized"; `hnsw` builds its vector index. */
29
24
  protected readonly indexTypes: Set<"brin" | "btree" | "fulltext" | "gin" | "gist" | "hash" | "hnsw" | "ivfflat" | "vector" | "vectorSearch">;
30
25
  protected readonly indexTypeHints: Map<"brin" | "btree" | "fulltext" | "gin" | "gist" | "hash" | "hnsw" | "ivfflat" | "vector" | "vectorSearch", string>;
31
- protected readonly vectorMetrics: ReadonlyMap<import("../../type/vector.js").VectorDistance, {
32
- readonly op: string;
33
- readonly opsSuffix: string;
34
- }>;
35
26
  private isNativeVectorIndex;
36
27
  protected isVectorIndex(index: IndexSchema): boolean;
37
28
  protected indexKeyword(index: IndexSchema): string;
@@ -1,4 +1,3 @@
1
- import { COCKROACH_VECTOR_METRICS, PG_VECTOR_METRICS } from '../../dialect/pgVectorMetrics.js';
2
1
  import { unsupportedVectorMetric } from '../../type/vector.js';
3
2
  import { IndexDdl } from './indexDdl.js';
4
3
  /** `$text` computes its `TO_TSVECTOR` per row, which no index over the raw columns serves. */
@@ -26,8 +25,6 @@ export class PgIndexDdl extends IndexDdl {
26
25
  'include',
27
26
  'jsonPath',
28
27
  ]);
29
- /** The metrics its vector index takes, each naming the operator class it is built with. */
30
- vectorMetrics = PG_VECTOR_METRICS;
31
28
  /** pgvector's own index types; CockroachDB's native one widens this. */
32
29
  isVectorIndex(index) {
33
30
  return index.type === 'hnsw' || index.type === 'ivfflat';
@@ -43,12 +40,12 @@ export class PgIndexDdl extends IndexDdl {
43
40
  if (!this.isVectorIndex(index) || !index.distance) {
44
41
  return entry.opsClass ? ` ${entry.opsClass}` : '';
45
42
  }
46
- const metric = this.vectorMetrics.get(index.distance);
43
+ const metric = this.dialect.vectorMetrics.get(index.distance)?.index;
47
44
  if (!metric) {
48
45
  throw unsupportedVectorMetric(this.dialect.dialectName, index.distance, index.name);
49
46
  }
50
47
  const vectorType = this.dialect.supportedVectorType(index.vectorType ?? 'vector');
51
- const opsClass = `${vectorType}_${metric.opsSuffix}_ops`;
48
+ const opsClass = `${vectorType}_${metric}_ops`;
52
49
  // IVFFlat has neither a sparsevec nor an L1 operator class; HNSW has all of them (pgvector 0.8.2).
53
50
  if (index.type === 'ivfflat' && (vectorType === 'sparsevec' || index.distance === 'l1')) {
54
51
  throw new TypeError(`ivfflat has no ${opsClass} operator class (index "${index.name}"); use hnsw`);
@@ -83,7 +80,6 @@ export class CockroachIndexDdl extends PgIndexDdl {
83
80
  ...PG_INDEX_TYPE_HINTS,
84
81
  ['ivfflat', "; declare type: 'vector' instead"],
85
82
  ]);
86
- vectorMetrics = COCKROACH_VECTOR_METRICS;
87
83
  isNativeVectorIndex(index) {
88
84
  return index.type === 'vector';
89
85
  }
@@ -12,6 +12,15 @@ import { BaseSqlIntrospector } from './baseSqlIntrospector.js';
12
12
  * it is four avoidable ones.
13
13
  */
14
14
  export type TableRowReader = <T extends RawRow>(sql: string, params?: unknown[]) => Promise<T[]>;
15
+ /** A foreign key as MySQL and SQL Server list one: a row, its column lists comma-joined. */
16
+ export type JoinedForeignKeyRow = {
17
+ readonly constraint_name: string;
18
+ readonly columns: string;
19
+ readonly referenced_table: string;
20
+ readonly referenced_columns: string;
21
+ readonly delete_rule: string;
22
+ readonly update_rule: string;
23
+ };
15
24
  /** A SQL introspector: an engine states its catalogue queries (`get*Query`) and how their rows map (`map*Result`). */
16
25
  export declare abstract class AbstractSqlSchemaIntrospector extends BaseSqlIntrospector implements SchemaIntrospector {
17
26
  protected readonly pool: QuerierPool;
@@ -48,8 +57,10 @@ export declare abstract class AbstractSqlSchemaIntrospector extends BaseSqlIntro
48
57
  protected getIndexesParams(tableName: string): unknown[];
49
58
  protected getForeignKeysParams(tableName: string): unknown[];
50
59
  protected getPrimaryKeyParams(tableName: string): unknown[];
51
- /** The {@link ForeignKeyAction} a catalogue names, whatever its case. */
60
+ /** The {@link ForeignKeyAction} a catalogue names, whatever its case, and `SET_NULL` as SQL Server spells it. */
52
61
  protected normalizeReferentialAction(action: string): ForeignKeyAction | undefined;
62
+ /** Foreign keys read one row each, as {@link JoinedForeignKeyRow} lists them. */
63
+ protected joinedForeignKeys(rows: readonly JoinedForeignKeyRow[]): ForeignKeySchema[];
53
64
  /**
54
65
  * Convert bigint/null values to number safely.
55
66
  */