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.
- package/dist/browser/uql-browser.min.js +2 -2
- package/dist/browser/uql-browser.min.js.map +3 -3
- package/dist/cockroachdb/cockroachDialect.d.ts +6 -4
- package/dist/cockroachdb/cockroachDialect.js +7 -3
- package/dist/d1/d1SqliteDialect.d.ts +1 -1
- package/dist/d1/d1SqliteDialect.js +1 -1
- package/dist/dialect/abstractSqlDialect.d.ts +107 -97
- package/dist/dialect/abstractSqlDialect.js +250 -293
- package/dist/dialect/hydrateColumn.js +33 -30
- package/dist/dialect/jsonSql.d.ts +32 -23
- package/dist/dialect/jsonSql.js +42 -31
- package/dist/dialect/mysqlLikeSqlDialect.d.ts +19 -23
- package/dist/dialect/mysqlLikeSqlDialect.js +34 -51
- package/dist/dialect/pgLikeSqlDialect.d.ts +24 -17
- package/dist/dialect/pgLikeSqlDialect.js +53 -26
- package/dist/dialect/vectorCast.d.ts +2 -0
- package/dist/dialect/vectorCast.js +25 -5
- package/dist/dialect/vectorSqlDialect.d.ts +8 -8
- package/dist/dialect/vectorSqlDialect.js +11 -11
- package/dist/index.d.ts +1 -0
- package/dist/maria/mariaDialect.d.ts +16 -11
- package/dist/maria/mariaDialect.js +23 -19
- package/dist/migrate/ddl/indexDdl.d.ts +3 -1
- package/dist/migrate/ddl/indexDdl.js +5 -1
- package/dist/migrate/ddl/mysqlIndexDdl.d.ts +7 -2
- package/dist/migrate/ddl/mysqlIndexDdl.js +28 -6
- package/dist/migrate/ddl/pgIndexDdl.d.ts +0 -9
- package/dist/migrate/ddl/pgIndexDdl.js +2 -6
- package/dist/migrate/introspection/abstractSqlSchemaIntrospector.d.ts +12 -1
- package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +13 -3
- package/dist/migrate/introspection/mssqlIntrospector.d.ts +2 -9
- package/dist/migrate/introspection/mssqlIntrospector.js +2 -9
- package/dist/migrate/introspection/mysqlIntrospector.d.ts +2 -9
- package/dist/migrate/introspection/mysqlIntrospector.js +2 -8
- package/dist/mongo/mongoDialect.d.ts +22 -10
- package/dist/mongo/mongoDialect.js +86 -38
- package/dist/mssql/mssqlDialect.d.ts +22 -18
- package/dist/mssql/mssqlDialect.js +54 -43
- package/dist/mysql/mysqlDialect.d.ts +16 -1
- package/dist/mysql/mysqlDialect.js +18 -2
- package/dist/sqlite/sqliteDialect.d.ts +17 -16
- package/dist/sqlite/sqliteDialect.js +35 -39
- package/dist/type/dialect.d.ts +0 -4
- package/dist/type/entity.d.ts +8 -3
- package/dist/type/vector.d.ts +5 -7
- package/dist/util/dialect.util.d.ts +2 -0
- package/dist/util/dialect.util.js +6 -2
- package/dist/util/object.util.d.ts +2 -4
- package/dist/util/object.util.js +4 -9
- package/package.json +1 -1
- package/dist/dialect/jsonArrayElemMatchUtils.d.ts +0 -2
- package/dist/dialect/jsonArrayElemMatchUtils.js +0 -7
- package/dist/dialect/pgVectorMetrics.d.ts +0 -13
- package/dist/dialect/pgVectorMetrics.js +0 -17
- package/dist/maria/mariaVectorMetrics.d.ts +0 -8
- 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
|
-
|
|
134
|
-
return `${
|
|
137
|
+
jsonContains(ctx, slot, values) {
|
|
138
|
+
return `${this.jsonValue(slot)} @> ${this.jsonVal(ctx, values)}`;
|
|
135
139
|
}
|
|
136
|
-
|
|
137
|
-
return
|
|
140
|
+
jsonLength(slot) {
|
|
141
|
+
return `JSONB_ARRAY_LENGTH(${this.jsonArray(slot)})`;
|
|
138
142
|
}
|
|
139
|
-
/**
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
161
|
-
|
|
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
|
|
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
|
|
189
|
-
const kept = `SELECT JSONB_AGG(${JSON_PULL_ALIAS}.val ORDER BY ${JSON_PULL_ALIAS}.ord) FROM JSONB_ARRAY_ELEMENTS(${
|
|
190
|
-
|
|
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
|
-
|
|
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
|
|
25
|
-
* all, which is what MySQL and D1 are. The key set is the single answer to "is this
|
|
26
|
-
* supported here",
|
|
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
|
-
*
|
|
33
|
-
*
|
|
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
|
|
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
|
-
* `
|
|
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
|
|
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
|
|
39
|
-
* all, which is what MySQL and D1 are. The key set is the single answer to "is this
|
|
40
|
-
* supported here",
|
|
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
|
-
*
|
|
45
|
-
*
|
|
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
|
-
|
|
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
|
-
* `
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
*
|
|
20
|
-
*
|
|
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
|
|
23
|
-
|
|
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\""]`)
|
|
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
|
|
31
|
-
/** Text-backed JSON compares as text, so
|
|
32
|
-
protected
|
|
33
|
-
/** `VEC_DISTANCE_COSINE`/`VEC_DISTANCE_EUCLIDEAN`, 11.7
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
46
|
-
*
|
|
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
|
-
|
|
49
|
-
return
|
|
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\""]`)
|
|
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
|
-
|
|
63
|
-
return `JSON_COMPACT(${
|
|
60
|
+
jsonArrayOf(elem) {
|
|
61
|
+
return super.jsonArrayOf(`JSON_COMPACT(${elem})`);
|
|
64
62
|
}
|
|
65
|
-
/** Text-backed JSON compares as text, so
|
|
66
|
-
|
|
67
|
-
return `NOT JSON_EQUALS(${
|
|
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
|
|
70
|
-
vectorMetrics = new Map([
|
|
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
|
-
/**
|
|
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' ?
|
|
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.
|
|
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 {
|
|
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
|
-
|
|
22
|
-
|
|
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 ?
|
|
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 =
|
|
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
|
|
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
|
*/
|