uql-orm 0.67.1 → 0.68.1
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/http/http.js +5 -8
- package/dist/browser/querier/httpQuerier.d.ts +9 -9
- package/dist/browser/uql-browser.min.js +2 -2
- package/dist/browser/uql-browser.min.js.map +9 -8
- package/dist/cockroachdb/cockroachDialect.d.ts +6 -4
- package/dist/cockroachdb/cockroachDialect.js +7 -3
- package/dist/d1/d1Querier.d.ts +5 -5
- package/dist/d1/d1QuerierPool.d.ts +3 -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/http/query.d.ts +10 -2
- package/dist/http/query.js +26 -1
- 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 +12 -7
- package/dist/type/query.d.ts +29 -24
- package/dist/type/queryWhere.d.ts +20 -20
- package/dist/type/universalQuerier.d.ts +10 -10
- package/dist/type/vector.d.ts +5 -7
- package/dist/type/wire.d.ts +9 -0
- 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 +2 -2
- 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/http/query.d.ts
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { WireQuery } from '../type/index.js';
|
|
2
2
|
/**
|
|
3
3
|
* Parse raw query-string entries (with JSON-stringified values) into a UQL query object.
|
|
4
4
|
* Symmetric counterpart of {@link stringifyQuery}. Only {@link ALLOWED_QUERY_KEYS} are honored.
|
|
5
5
|
*/
|
|
6
|
-
export declare function parseQueryParams(params?: Record<string, unknown>):
|
|
6
|
+
export declare function parseQueryParams(params?: Record<string, unknown>): WireQuery<unknown>;
|
|
7
7
|
/**
|
|
8
8
|
* Serialize a UQL query object into a percent-encoded query string where object values
|
|
9
9
|
* are JSON-stringified. Symmetric counterpart of {@link parseQueryParams}.
|
|
10
10
|
*/
|
|
11
11
|
export declare function stringifyQuery(query?: Record<string, unknown>): string;
|
|
12
|
+
/**
|
|
13
|
+
* What leaves the browser, as JSON, refusing what JSON keeps nothing of rather than letting the server
|
|
14
|
+
* build a statement around the remains. A `raw` fragment renders SQL against a dialect the client does not
|
|
15
|
+
* have and arrives as `{}`; binary arrives as an object keyed by index. A `Date` is not among them - it
|
|
16
|
+
* serializes to ISO 8601, which is what a date column reads. This is what a cast, or a JavaScript caller,
|
|
17
|
+
* hits where the client's types already refuse a fragment.
|
|
18
|
+
*/
|
|
19
|
+
export declare function wireJson(value: unknown): string;
|
package/dist/http/query.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
// the clause lists themselves, not the barrel: this module is in the browser bundle's graph
|
|
2
2
|
import { QUERY_BOOLEAN_CLAUSES, QUERY_NUMBER_CLAUSES, QUERY_OBJECT_CLAUSES, QUERY_ROOT_NUMBER_CLAUSES, QUERY_ROOT_OBJECT_CLAUSES, } from '../type/query.js';
|
|
3
|
+
// the brand alone, not the class: importing `QueryRaw` for an `instanceof` kept it, and `ColumnRef`
|
|
4
|
+
// with it, in the browser bundle, which is on a size budget
|
|
5
|
+
import { RAW_VALUE } from '../type/queryRaw.js';
|
|
3
6
|
// the specific util module, not the barrel, so the browser bundle does not pull in entity metadata
|
|
4
7
|
import { getKeys, isWhereMap } from '../util/object.util.js';
|
|
5
8
|
/**
|
|
@@ -82,8 +85,30 @@ export function stringifyQuery(query) {
|
|
|
82
85
|
if (value === undefined) {
|
|
83
86
|
continue;
|
|
84
87
|
}
|
|
85
|
-
params.append(key, typeof value === 'object' && value !== null ?
|
|
88
|
+
params.append(key, typeof value === 'object' && value !== null ? wireJson(value) : String(value));
|
|
86
89
|
}
|
|
87
90
|
const qs = params.toString();
|
|
88
91
|
return qs ? `?${qs}` : '';
|
|
89
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* What leaves the browser, as JSON, refusing what JSON keeps nothing of rather than letting the server
|
|
95
|
+
* build a statement around the remains. A `raw` fragment renders SQL against a dialect the client does not
|
|
96
|
+
* have and arrives as `{}`; binary arrives as an object keyed by index. A `Date` is not among them - it
|
|
97
|
+
* serializes to ISO 8601, which is what a date column reads. This is what a cast, or a JavaScript caller,
|
|
98
|
+
* hits where the client's types already refuse a fragment.
|
|
99
|
+
*/
|
|
100
|
+
export function wireJson(value) {
|
|
101
|
+
return JSON.stringify(value, (_key, held) => {
|
|
102
|
+
if (typeof held !== 'object' || held === null) {
|
|
103
|
+
return held;
|
|
104
|
+
}
|
|
105
|
+
if (RAW_VALUE in held) {
|
|
106
|
+
throw new TypeError('raw SQL cannot travel over HTTP: what leaves the browser is JSON');
|
|
107
|
+
}
|
|
108
|
+
// A blob is a field value, so no type parameter reaches it: this is the only place it is caught.
|
|
109
|
+
if (held instanceof ArrayBuffer || ArrayBuffer.isView(held)) {
|
|
110
|
+
throw new TypeError('binary cannot travel over HTTP: what leaves the browser is JSON');
|
|
111
|
+
}
|
|
112
|
+
return held;
|
|
113
|
+
});
|
|
114
|
+
}
|
|
@@ -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;
|