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
|
@@ -1,54 +1,57 @@
|
|
|
1
1
|
import { decodeWideNumber } from '../util/wideNumber.js';
|
|
2
|
-
import { parseVectorLiteral } from './vectorCast.js';
|
|
2
|
+
import { decodeFloat32s, parseVectorLiteral } from './vectorCast.js';
|
|
3
3
|
/**
|
|
4
4
|
* Decodes one non-null cell. A no-op where the driver already decoded it, since that varies per driver,
|
|
5
5
|
* and untouched where it does not match its column's format.
|
|
6
6
|
*/
|
|
7
7
|
export function decodeColumn(value, kind) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
8
|
+
return DECODERS[kind](value);
|
|
9
|
+
}
|
|
10
|
+
/** A decoder of the text a driver returned; anything else it already decoded, and is kept. */
|
|
11
|
+
function fromText(decode) {
|
|
12
|
+
return (value) => {
|
|
13
|
+
const text = asText(value);
|
|
14
|
+
return text === undefined ? value : decode(text, value);
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/** A vector's text in the literal its cast writes, or its packed float32s in hex, which MariaDB reads. */
|
|
18
|
+
function vectorDecoder(cast) {
|
|
19
|
+
return fromText((text, value) => text.startsWith(BYTES_PREFIX)
|
|
20
|
+
? decodeFloat32s(hexBytes(text.slice(BYTES_PREFIX.length)))
|
|
21
|
+
: (parseVectorLiteral(text, cast) ?? value));
|
|
22
|
+
}
|
|
23
|
+
const DECODERS = {
|
|
24
|
+
// 0/1 from SQLite's INTEGER or MySQL's TINYINT(1). Already a boolean on Postgres.
|
|
25
|
+
boolean: (value) => (typeof value === 'boolean' ? value : Boolean(value)),
|
|
26
|
+
date: (value) => (typeof value === 'string' ? (parseDate(value) ?? value) : value),
|
|
27
|
+
// Only a string can be bytes that crossed JSON: bytes a driver already decoded stay as they are.
|
|
28
|
+
bytes: (value) => typeof value === 'string' && value.startsWith(BYTES_PREFIX) ? hexBytes(value.slice(BYTES_PREFIX.length)) : value,
|
|
29
|
+
// A number too, not just text: `type: BigInt` is BIGINT, which the pg pools decode at the wire.
|
|
30
|
+
bigint: (value) => {
|
|
22
31
|
if (typeof value === 'bigint') {
|
|
23
32
|
return value;
|
|
24
33
|
}
|
|
25
34
|
try {
|
|
26
|
-
|
|
27
|
-
return BigInt(text ?? value);
|
|
35
|
+
return BigInt(asText(value) ?? Number(value));
|
|
28
36
|
}
|
|
29
37
|
catch {
|
|
30
38
|
// Not an integer after all (a fractional column declared `bigint`); keep what the driver gave.
|
|
31
39
|
return value;
|
|
32
40
|
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return value;
|
|
37
|
-
}
|
|
38
|
-
if (kind === 'number') {
|
|
39
|
-
return decodeWideNumber(text);
|
|
40
|
-
}
|
|
41
|
-
if (kind === 'json') {
|
|
41
|
+
},
|
|
42
|
+
number: fromText(decodeWideNumber),
|
|
43
|
+
json: fromText((text, value) => {
|
|
42
44
|
try {
|
|
43
45
|
return JSON.parse(text);
|
|
44
46
|
}
|
|
45
47
|
catch {
|
|
46
|
-
// Keep the original value when the driver returns non-JSON text.
|
|
47
48
|
return value;
|
|
48
49
|
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
}),
|
|
51
|
+
vector: vectorDecoder('vector'),
|
|
52
|
+
halfvec: vectorDecoder('halfvec'),
|
|
53
|
+
sparsevec: vectorDecoder('sparsevec'),
|
|
54
|
+
};
|
|
52
55
|
/**
|
|
53
56
|
* An ISO 8601 timestamp as a `Date`, its fraction cut to the milliseconds one holds, and a bare date at
|
|
54
57
|
* local midnight, which is how `pg` reads a `date`. `undefined` for text that is neither.
|
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
import type { FieldOptions, FieldType } from '../type/index.js';
|
|
2
2
|
/**
|
|
3
|
-
* A `'$.a.b'` JSON path literal, each dot-separated segment escaped
|
|
4
|
-
* such as `[#]` or `[*]`. Shared across dialects
|
|
5
|
-
* differently from an ANSI string literal.
|
|
3
|
+
* A `'$.a.b'` JSON path literal, each dot-separated segment escaped, and `'$'` for an empty path, the
|
|
4
|
+
* document itself. `suffix` appends an accessor such as `[#]` or `[*]`. Shared across dialects
|
|
5
|
+
* unchanged: no dialect escapes a JSON path key differently from an ANSI string literal.
|
|
6
6
|
*/
|
|
7
7
|
export declare function jsonPath(path: string, suffix?: string): string;
|
|
8
|
+
/** A JSON value a condition reads: `path` of the JSON in `base`, `''` for all of it. */
|
|
9
|
+
export type JsonSlot = {
|
|
10
|
+
readonly base: string;
|
|
11
|
+
readonly path: string;
|
|
12
|
+
};
|
|
13
|
+
/** `base, '$.a.b'`, how `JSON_EACH`, `OPENJSON` and the like take the value at `slot`: no path for the whole document. */
|
|
14
|
+
export declare function jsonSlotArgs({ base, path }: JsonSlot): string;
|
|
15
|
+
/**
|
|
16
|
+
* {@link jsonSlotArgs} reading a NULL document where `isArray` does not hold, for a function that would
|
|
17
|
+
* otherwise walk a scalar, or an object's values, as if they were elements.
|
|
18
|
+
*/
|
|
19
|
+
export declare function jsonArraySlotArgs({ base, path }: JsonSlot, isArray: string): string;
|
|
8
20
|
/** How many argument groups of `size` fit in one call beside its target, under `maxArgs`. */
|
|
9
21
|
export declare function groupsPerCall(maxArgs: number, size: number): number;
|
|
10
22
|
/**
|
|
@@ -14,37 +26,34 @@ export declare function groupsPerCall(maxArgs: number, size: number): number;
|
|
|
14
26
|
*/
|
|
15
27
|
export declare function chainedCall(fn: string, target: string, groups: readonly string[], size: number, maxArgs: number): string;
|
|
16
28
|
/**
|
|
17
|
-
* `
|
|
18
|
-
*
|
|
19
|
-
* accessor per key (SQLite's `[#]` append). Values bind in key order through `bindValue`, the caller's
|
|
20
|
-
* `jsonScalarParam` bound to its `QueryContext`.
|
|
29
|
+
* `JSON_SET(target, path, value, ...)`, which MySQL and SQLite share, nested past `maxArgs`. `pathSuffix`
|
|
30
|
+
* appends an accessor per key (SQLite's `[#]` append). Values bind in key order through `bindValue`.
|
|
21
31
|
*/
|
|
22
|
-
export declare function
|
|
32
|
+
export declare function jsonSetCall(bindValue: (value: unknown) => string, target: string, entries: Record<string, unknown>, maxArgs: number, pathSuffix?: string): string;
|
|
23
33
|
/** The `$set` target: a nullable column needs a `COALESCE` fallback to build on. */
|
|
24
34
|
export declare function jsonSetTarget(expr: string, field: FieldOptions | undefined, empty: string): string;
|
|
35
|
+
/** `JSON_REMOVE(expr, path, ...)`, which MySQL and SQLite share, nested past `maxArgs`. */
|
|
36
|
+
export declare function jsonRemoveCall(expr: string, keys: readonly string[], maxArgs: number): string;
|
|
25
37
|
/**
|
|
26
|
-
* `
|
|
27
|
-
*
|
|
38
|
+
* `WHERE` is omitted for an empty `$elemMatch`, which asks only that the array has an element. `hint` is
|
|
39
|
+
* an optimizer hint the subquery opens with, if any.
|
|
28
40
|
*/
|
|
29
|
-
export declare function
|
|
30
|
-
/** `WHERE` is omitted for an empty `$elemMatch`, which asks only that the array has an element. */
|
|
31
|
-
export declare function jsonElemExists(from: string, conditions: readonly string[]): string;
|
|
41
|
+
export declare function jsonElemExists(from: string, conditions: readonly string[], hint: string): string;
|
|
32
42
|
/**
|
|
33
43
|
* How a JSON value is read out of its document: as the JSON value itself, as a number to compare
|
|
34
|
-
* against, or as the text a path yields. One vocabulary for
|
|
35
|
-
*
|
|
44
|
+
* against, or as the text a path yields. One vocabulary for both sides of a comparison, so a path is
|
|
45
|
+
* read the way its operand is compared - see {@link jsonCompareMode}.
|
|
36
46
|
*/
|
|
37
47
|
export type JsonAccessMode = 'json' | 'numeric' | 'text';
|
|
38
48
|
/**
|
|
39
|
-
* How a JSON
|
|
40
|
-
*
|
|
41
|
-
*
|
|
49
|
+
* How a JSON path compares for equality against `value`: `numeric` casts (so `1` equals `1.0`), `json`
|
|
50
|
+
* compares JSON values (the only portable boolean), and `text` compares as extracted, also for mixed
|
|
51
|
+
* operands.
|
|
42
52
|
*/
|
|
43
53
|
export declare function jsonCompareMode(value: unknown): JsonAccessMode;
|
|
44
54
|
/** The mode a declared type asks for, {@link jsonCompareMode}'s twin: an index over a path is reached only by a comparison extracting it alike. */
|
|
45
55
|
export declare function jsonTypeMode(type: FieldType): JsonAccessMode;
|
|
46
|
-
/**
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
export declare function isJsonbOp(op: string, value?: unknown): boolean;
|
|
56
|
+
/** Whether `value` asks for more than JSON containment: an operator map anywhere in it. */
|
|
57
|
+
export declare function holdsOperator(value: unknown): boolean;
|
|
58
|
+
/** A value an array element is matched to by JSON containment: a string, a finite number or a boolean. */
|
|
59
|
+
export declare function isJsonScalar(value: unknown): value is string | number | boolean;
|
package/dist/dialect/jsonSql.js
CHANGED
|
@@ -1,13 +1,26 @@
|
|
|
1
|
+
import { isOperatorMap } from '../util/dialect.util.js';
|
|
1
2
|
import { columnFamily } from '../util/field.util.js';
|
|
3
|
+
import { isOperatorKey, someKey } from '../util/object.util.js';
|
|
2
4
|
import { escapeSingleQuotes } from '../util/sqlLiteral.js';
|
|
3
5
|
/**
|
|
4
|
-
* A `'$.a.b'` JSON path literal, each dot-separated segment escaped
|
|
5
|
-
* such as `[#]` or `[*]`. Shared across dialects
|
|
6
|
-
* differently from an ANSI string literal.
|
|
6
|
+
* A `'$.a.b'` JSON path literal, each dot-separated segment escaped, and `'$'` for an empty path, the
|
|
7
|
+
* document itself. `suffix` appends an accessor such as `[#]` or `[*]`. Shared across dialects
|
|
8
|
+
* unchanged: no dialect escapes a JSON path key differently from an ANSI string literal.
|
|
7
9
|
*/
|
|
8
10
|
export function jsonPath(path, suffix = '') {
|
|
9
|
-
const segments = path.split('.').map(escapeSingleQuotes).join('.')
|
|
10
|
-
return `'
|
|
11
|
+
const segments = path && `.${path.split('.').map(escapeSingleQuotes).join('.')}`;
|
|
12
|
+
return `'$${segments}${suffix}'`;
|
|
13
|
+
}
|
|
14
|
+
/** `base, '$.a.b'`, how `JSON_EACH`, `OPENJSON` and the like take the value at `slot`: no path for the whole document. */
|
|
15
|
+
export function jsonSlotArgs({ base, path }) {
|
|
16
|
+
return path ? `${base}, ${jsonPath(path)}` : base;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* {@link jsonSlotArgs} reading a NULL document where `isArray` does not hold, for a function that would
|
|
20
|
+
* otherwise walk a scalar, or an object's values, as if they were elements.
|
|
21
|
+
*/
|
|
22
|
+
export function jsonArraySlotArgs({ base, path }, isArray) {
|
|
23
|
+
return jsonSlotArgs({ base: `CASE WHEN ${isArray} THEN ${base} END`, path });
|
|
11
24
|
}
|
|
12
25
|
/** How many argument groups of `size` fit in one call beside its target, under `maxArgs`. */
|
|
13
26
|
export function groupsPerCall(maxArgs, size) {
|
|
@@ -27,35 +40,33 @@ export function chainedCall(fn, target, groups, size, maxArgs) {
|
|
|
27
40
|
return call;
|
|
28
41
|
}
|
|
29
42
|
/**
|
|
30
|
-
* `
|
|
31
|
-
*
|
|
32
|
-
* accessor per key (SQLite's `[#]` append). Values bind in key order through `bindValue`, the caller's
|
|
33
|
-
* `jsonScalarParam` bound to its `QueryContext`.
|
|
43
|
+
* `JSON_SET(target, path, value, ...)`, which MySQL and SQLite share, nested past `maxArgs`. `pathSuffix`
|
|
44
|
+
* appends an accessor per key (SQLite's `[#]` append). Values bind in key order through `bindValue`.
|
|
34
45
|
*/
|
|
35
|
-
export function
|
|
46
|
+
export function jsonSetCall(bindValue, target, entries, maxArgs, pathSuffix = '') {
|
|
36
47
|
const pairs = Object.entries(entries).map(([key, value]) => `${jsonPath(key, pathSuffix)}, ${bindValue(value)}`);
|
|
37
|
-
return chainedCall(
|
|
48
|
+
return chainedCall('JSON_SET', target, pairs, 2, maxArgs);
|
|
38
49
|
}
|
|
39
50
|
/** The `$set` target: a nullable column needs a `COALESCE` fallback to build on. */
|
|
40
51
|
export function jsonSetTarget(expr, field, empty) {
|
|
41
52
|
return field?.nullable === false ? expr : `COALESCE(${expr}, ${empty})`;
|
|
42
53
|
}
|
|
54
|
+
/** `JSON_REMOVE(expr, path, ...)`, which MySQL and SQLite share, nested past `maxArgs`. */
|
|
55
|
+
export function jsonRemoveCall(expr, keys, maxArgs) {
|
|
56
|
+
return chainedCall('JSON_REMOVE', expr, keys.map((key) => jsonPath(key)), 1, maxArgs);
|
|
57
|
+
}
|
|
43
58
|
/**
|
|
44
|
-
* `
|
|
45
|
-
*
|
|
59
|
+
* `WHERE` is omitted for an empty `$elemMatch`, which asks only that the array has an element. `hint` is
|
|
60
|
+
* an optimizer hint the subquery opens with, if any.
|
|
46
61
|
*/
|
|
47
|
-
export function
|
|
48
|
-
return chainedCall(fn, expr, keys.map((key) => jsonPath(key)), 1, maxArgs);
|
|
49
|
-
}
|
|
50
|
-
/** `WHERE` is omitted for an empty `$elemMatch`, which asks only that the array has an element. */
|
|
51
|
-
export function jsonElemExists(from, conditions) {
|
|
62
|
+
export function jsonElemExists(from, conditions, hint) {
|
|
52
63
|
const where = conditions.length ? ` WHERE ${conditions.join(' AND ')}` : '';
|
|
53
|
-
return `EXISTS (SELECT 1 FROM ${from}${where})`;
|
|
64
|
+
return `EXISTS (SELECT ${hint && `${hint} `}1 FROM ${from}${where})`;
|
|
54
65
|
}
|
|
55
66
|
/**
|
|
56
|
-
* How a JSON
|
|
57
|
-
*
|
|
58
|
-
*
|
|
67
|
+
* How a JSON path compares for equality against `value`: `numeric` casts (so `1` equals `1.0`), `json`
|
|
68
|
+
* compares JSON values (the only portable boolean), and `text` compares as extracted, also for mixed
|
|
69
|
+
* operands.
|
|
59
70
|
*/
|
|
60
71
|
export function jsonCompareMode(value) {
|
|
61
72
|
const operands = Array.isArray(value) ? value : [value];
|
|
@@ -75,14 +86,14 @@ export function jsonTypeMode(type) {
|
|
|
75
86
|
}
|
|
76
87
|
return family === 'boolean' ? 'json' : 'text';
|
|
77
88
|
}
|
|
78
|
-
/**
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
export function isJsonbOp(op, value) {
|
|
83
|
-
if (op === '$all' || op === '$size' || op === '$elemMatch') {
|
|
84
|
-
return true;
|
|
89
|
+
/** Whether `value` asks for more than JSON containment: an operator map anywhere in it. */
|
|
90
|
+
export function holdsOperator(value) {
|
|
91
|
+
if (Array.isArray(value)) {
|
|
92
|
+
return value.some(holdsOperator);
|
|
85
93
|
}
|
|
86
|
-
|
|
87
|
-
|
|
94
|
+
return isOperatorMap(value) && someKey(value, (key) => isOperatorKey(key) || holdsOperator(value[key]));
|
|
95
|
+
}
|
|
96
|
+
/** A value an array element is matched to by JSON containment: a string, a finite number or a boolean. */
|
|
97
|
+
export function isJsonScalar(value) {
|
|
98
|
+
return typeof value === 'string' || typeof value === 'boolean' || Number.isFinite(value);
|
|
88
99
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { EntityMeta, FieldOptions, InsertIdSource, Query, QueryConflictPaths, QueryContext, QueryOptions, QueryPager,
|
|
1
|
+
import type { EntityMeta, FieldOptions, InsertIdSource, Query, QueryConflictPaths, QueryContext, QueryOptions, QueryPager, QueryTextSearchOptions, SqlDialectFeatures, Type } from '../type/index.js';
|
|
2
2
|
import { AbstractSqlDialect, type DerivedRelation, type RelationRows } from './abstractSqlDialect.js';
|
|
3
|
+
import { type JsonSlot } from './jsonSql.js';
|
|
3
4
|
/** What the MySQL-family engines have. */
|
|
4
5
|
export declare const MYSQL_FEATURES: SqlDialectFeatures;
|
|
5
6
|
/** What MySQL and MariaDB share, their JSON functions above all: `JSON_LENGTH`, `JSON_CONTAINS`, `JSON_TABLE`, `JSON_SET`. */
|
|
@@ -63,13 +64,15 @@ export declare abstract class MysqlLikeSqlDialect extends AbstractSqlDialect {
|
|
|
63
64
|
protected jsonObject(pairs: DerivedRelation['pairs']): string;
|
|
64
65
|
/**
|
|
65
66
|
* A number and bytes cross JSON as text, where JSON would round the one and spell the other as base64,
|
|
66
|
-
* and a vector as the engine reads one back:
|
|
67
|
+
* and a vector as the engine reads one back: its packed bytes on MariaDB.
|
|
67
68
|
*/
|
|
68
69
|
protected readonly carriedFields: {
|
|
69
70
|
numeric: (expr: string) => string;
|
|
70
71
|
blob: (expr: string) => string;
|
|
71
72
|
vector: (expr: string, field: FieldOptions) => string;
|
|
72
73
|
};
|
|
74
|
+
/** Bytes as the hex text `decodeColumn` reads back, whole. */
|
|
75
|
+
protected bytesAsText(expr: string): string;
|
|
73
76
|
escape(value: unknown): string;
|
|
74
77
|
/**
|
|
75
78
|
* `MATCH(cols) AGAINST(?)`, which needs a `FULLTEXT` index over exactly those columns: without one
|
|
@@ -77,17 +80,9 @@ export declare abstract class MysqlLikeSqlDialect extends AbstractSqlDialect {
|
|
|
77
80
|
* `@Index((post) => [...], { type: 'fulltext' })`.
|
|
78
81
|
*/
|
|
79
82
|
protected appendTextSearch<E>(ctx: QueryContext, _entity: Type<E>, meta: EntityMeta<E>, search: QueryTextSearchOptions<E>): void;
|
|
83
|
+
/** `DOUBLE`, never a bare `DECIMAL`, which is `DECIMAL(10,0)` and rounds `1.4` to `1`. */
|
|
80
84
|
protected numericCast(expr: string): string;
|
|
81
85
|
protected neExpr(field: string, ph: string): string;
|
|
82
|
-
/** How a surviving element is fed back into the array a `$pull` rebuilds. */
|
|
83
|
-
protected jsonPullElem(alias: string): string;
|
|
84
|
-
/** Condition keeping the elements a `$pull` does *not* remove, given the bound pulled value. */
|
|
85
|
-
protected jsonPullKeep(alias: string, operand: string): string;
|
|
86
|
-
/**
|
|
87
|
-
* `JSON_REPLACE` leaves an absent key (and a NULL column) untouched, which is what makes `$pull`
|
|
88
|
-
* a no-op there. The subquery reads the column, so its value binds exactly once.
|
|
89
|
-
*/
|
|
90
|
-
protected jsonPullKey(ctx: QueryContext, expr: string, escapedCol: string, key: string, value: unknown): string;
|
|
91
86
|
/**
|
|
92
87
|
* Omitting the `COALESCE` on a NOT NULL column keeps MySQL's partial in-place JSON update
|
|
93
88
|
* applicable: it requires the target column as the direct `JSON_SET` input.
|
|
@@ -99,20 +94,21 @@ export declare abstract class MysqlLikeSqlDialect extends AbstractSqlDialect {
|
|
|
99
94
|
* second reference for the array source and returns NULL on MariaDB for an absent key.
|
|
100
95
|
*/
|
|
101
96
|
protected jsonPush(ctx: QueryContext, expr: string, push: Record<string, unknown>): string;
|
|
102
|
-
protected jsonUnset(_ctx: QueryContext, expr: string, unset: readonly string[]): string;
|
|
103
97
|
/**
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
* chained `col->'a'->>'b'`, which the server rejects with "Invalid JSON path expression".
|
|
98
|
+
* `->` for the value and `->>` for its text, each taking the whole path (`'$.a.b'`): a bare key, as
|
|
99
|
+
* Postgres chains them, is "Invalid JSON path expression" here.
|
|
107
100
|
*/
|
|
108
|
-
protected
|
|
109
|
-
|
|
110
|
-
protected
|
|
111
|
-
protected
|
|
101
|
+
protected jsonPathReading(escapedColumn: string, path: string, mode: 'json' | 'text'): string;
|
|
102
|
+
/** `JSON_CONTAINS` of the array as the path reads it, which is what a multi-valued index is matched by. */
|
|
103
|
+
protected jsonContains(ctx: QueryContext, slot: JsonSlot, values: readonly unknown[]): string;
|
|
104
|
+
protected jsonUnset(_ctx: QueryContext, expr: string, unset: readonly string[]): string;
|
|
105
|
+
/** Only an array's, where `JSON_LENGTH` counts a scalar as 1 and an object by its keys. */
|
|
106
|
+
protected jsonLength(slot: JsonSlot): string;
|
|
107
|
+
protected jsonIsArray(slot: JsonSlot): string;
|
|
112
108
|
/**
|
|
113
|
-
*
|
|
114
|
-
*
|
|
109
|
+
* Each element of the array at the path as one `JSON` column, which any path then reads the way it reads
|
|
110
|
+
* a column's document.
|
|
115
111
|
*/
|
|
116
|
-
protected jsonElemFrom(
|
|
117
|
-
protected
|
|
112
|
+
protected jsonElemFrom(slot: JsonSlot, alias: string): string;
|
|
113
|
+
protected jsonElemDoc(alias: string): string;
|
|
118
114
|
}
|
|
@@ -2,9 +2,9 @@ import { getMeta } from '../entity/index.js';
|
|
|
2
2
|
import { textSearchFields } from '../util/index.js';
|
|
3
3
|
import { escapeMysqlSqlLiteral, escapeSingleQuotes } from '../util/sqlLiteral.js';
|
|
4
4
|
import { AbstractSqlDialect, } from './abstractSqlDialect.js';
|
|
5
|
-
import { COUNT_ALIAS
|
|
5
|
+
import { COUNT_ALIAS } from './aliases.js';
|
|
6
6
|
import { BYTES_PREFIX } from './hydrateColumn.js';
|
|
7
|
-
import {
|
|
7
|
+
import { jsonSetCall, jsonPath, jsonRemoveCall, jsonSetTarget } from './jsonSql.js';
|
|
8
8
|
import { aggregatesRelations } from './queryJoins.js';
|
|
9
9
|
/**
|
|
10
10
|
* The largest `BIGINT UNSIGNED`: the row count MySQL's manual gives for "all rows from the offset on",
|
|
@@ -32,12 +32,12 @@ export const MYSQL_FEATURES = {
|
|
|
32
32
|
rowLockOf: true,
|
|
33
33
|
orderedUpsertReturning: true,
|
|
34
34
|
orderedJsonAggregates: true,
|
|
35
|
-
partialJsonContainment: true,
|
|
36
|
-
typedJsonElements: false,
|
|
37
35
|
narrowVectorTypes: false,
|
|
38
36
|
vectorTuningNeedsTransaction: false,
|
|
39
37
|
serialDeclaresPrimaryKey: false,
|
|
40
38
|
};
|
|
39
|
+
/** The one `JSON_TABLE` column an exploded array reads each element through, as a JSON document. */
|
|
40
|
+
const ELEM_COLUMN = 'v';
|
|
41
41
|
/** What MySQL and MariaDB share, their JSON functions above all: `JSON_LENGTH`, `JSON_CONTAINS`, `JSON_TABLE`, `JSON_SET`. */
|
|
42
42
|
export class MysqlLikeSqlDialect extends AbstractSqlDialect {
|
|
43
43
|
features = MYSQL_FEATURES;
|
|
@@ -156,13 +156,17 @@ export class MysqlLikeSqlDialect extends AbstractSqlDialect {
|
|
|
156
156
|
}
|
|
157
157
|
/**
|
|
158
158
|
* A number and bytes cross JSON as text, where JSON would round the one and spell the other as base64,
|
|
159
|
-
* and a vector as the engine reads one back:
|
|
159
|
+
* and a vector as the engine reads one back: its packed bytes on MariaDB.
|
|
160
160
|
*/
|
|
161
161
|
carriedFields = {
|
|
162
162
|
numeric: (expr) => `CAST(${expr} AS CHAR)`,
|
|
163
|
-
blob: (expr) =>
|
|
163
|
+
blob: (expr) => this.bytesAsText(expr),
|
|
164
164
|
vector: (expr, field) => this.selectFieldExpr(expr, field),
|
|
165
165
|
};
|
|
166
|
+
/** Bytes as the hex text `decodeColumn` reads back, whole. */
|
|
167
|
+
bytesAsText(expr) {
|
|
168
|
+
return `CONCAT(${this.escape(BYTES_PREFIX)}, HEX(${expr}))`;
|
|
169
|
+
}
|
|
166
170
|
escape(value) {
|
|
167
171
|
return escapeMysqlSqlLiteral(value);
|
|
168
172
|
}
|
|
@@ -177,36 +181,20 @@ export class MysqlLikeSqlDialect extends AbstractSqlDialect {
|
|
|
177
181
|
ctx.addValue(search.$value);
|
|
178
182
|
ctx.append(')');
|
|
179
183
|
}
|
|
184
|
+
/** `DOUBLE`, never a bare `DECIMAL`, which is `DECIMAL(10,0)` and rounds `1.4` to `1`. */
|
|
180
185
|
numericCast(expr) {
|
|
181
|
-
return `CAST(${expr} AS
|
|
186
|
+
return `CAST(${expr} AS DOUBLE)`;
|
|
182
187
|
}
|
|
183
188
|
neExpr(field, ph) {
|
|
184
189
|
// MySQL/MariaDB null-safe inequality: true when values differ or one side is NULL.
|
|
185
190
|
return `NOT (${field} <=> ${ph})`;
|
|
186
191
|
}
|
|
187
|
-
/** How a surviving element is fed back into the array a `$pull` rebuilds. */
|
|
188
|
-
jsonPullElem(alias) {
|
|
189
|
-
return `${alias}.v`;
|
|
190
|
-
}
|
|
191
|
-
/** Condition keeping the elements a `$pull` does *not* remove, given the bound pulled value. */
|
|
192
|
-
jsonPullKeep(alias, operand) {
|
|
193
|
-
return `${alias}.v <> ${operand}`;
|
|
194
|
-
}
|
|
195
|
-
/**
|
|
196
|
-
* `JSON_REPLACE` leaves an absent key (and a NULL column) untouched, which is what makes `$pull`
|
|
197
|
-
* a no-op there. The subquery reads the column, so its value binds exactly once.
|
|
198
|
-
*/
|
|
199
|
-
jsonPullKey(ctx, expr, escapedCol, key, value) {
|
|
200
|
-
const elements = `JSON_TABLE(${escapedCol}, ${jsonPath(key, '[*]')} COLUMNS (v JSON PATH '$')) ${JSON_PULL_ALIAS}`;
|
|
201
|
-
const kept = `SELECT COALESCE(JSON_ARRAYAGG(${this.jsonPullElem(JSON_PULL_ALIAS)}), JSON_ARRAY()) FROM ${elements} WHERE ${this.jsonPullKeep(JSON_PULL_ALIAS, this.jsonScalarParam(ctx, value))}`;
|
|
202
|
-
return `JSON_REPLACE(${expr}, ${jsonPath(key)}, (${kept}))`;
|
|
203
|
-
}
|
|
204
192
|
/**
|
|
205
193
|
* Omitting the `COALESCE` on a NOT NULL column keeps MySQL's partial in-place JSON update
|
|
206
194
|
* applicable: it requires the target column as the direct `JSON_SET` input.
|
|
207
195
|
*/
|
|
208
196
|
jsonSet(ctx, expr, set, field) {
|
|
209
|
-
return
|
|
197
|
+
return jsonSetCall((value) => this.jsonScalarParam(ctx, value), jsonSetTarget(expr, field, `'{}'`), set, this.maxFunctionArgs);
|
|
210
198
|
}
|
|
211
199
|
/**
|
|
212
200
|
* `JSON_MERGE_PRESERVE` concatenates arrays and creates absent keys, so every pushed key is
|
|
@@ -217,40 +205,35 @@ export class MysqlLikeSqlDialect extends AbstractSqlDialect {
|
|
|
217
205
|
const entries = Object.entries(push).map(([key, value]) => `'${escapeSingleQuotes(key)}', JSON_ARRAY(${this.jsonScalarParam(ctx, value)})`);
|
|
218
206
|
return `JSON_MERGE_PRESERVE(${expr}, JSON_OBJECT(${entries.join(', ')}))`;
|
|
219
207
|
}
|
|
220
|
-
jsonUnset(_ctx, expr, unset) {
|
|
221
|
-
return jsonRemoveCall('JSON_REMOVE', expr, unset, this.maxFunctionArgs);
|
|
222
|
-
}
|
|
223
208
|
/**
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
* chained `col->'a'->>'b'`, which the server rejects with "Invalid JSON path expression".
|
|
209
|
+
* `->` for the value and `->>` for its text, each taking the whole path (`'$.a.b'`): a bare key, as
|
|
210
|
+
* Postgres chains them, is "Invalid JSON path expression" here.
|
|
227
211
|
*/
|
|
228
|
-
|
|
229
|
-
return `(${escapedColumn}->>${jsonPath(
|
|
212
|
+
jsonPathReading(escapedColumn, path, mode) {
|
|
213
|
+
return mode === 'json' ? `${escapedColumn}->${jsonPath(path)}` : `(${escapedColumn}->>${jsonPath(path)})`;
|
|
230
214
|
}
|
|
231
|
-
|
|
232
|
-
|
|
215
|
+
/** `JSON_CONTAINS` of the array as the path reads it, which is what a multi-valued index is matched by. */
|
|
216
|
+
jsonContains(ctx, slot, values) {
|
|
217
|
+
return `JSON_CONTAINS(${this.jsonValue(slot)}, ${this.addValue(ctx, JSON.stringify(values))})`;
|
|
218
|
+
}
|
|
219
|
+
jsonUnset(_ctx, expr, unset) {
|
|
220
|
+
return jsonRemoveCall(expr, unset, this.maxFunctionArgs);
|
|
233
221
|
}
|
|
234
|
-
|
|
235
|
-
|
|
222
|
+
/** Only an array's, where `JSON_LENGTH` counts a scalar as 1 and an object by its keys. */
|
|
223
|
+
jsonLength(slot) {
|
|
224
|
+
return `CASE WHEN ${this.jsonIsArray(slot)} THEN JSON_LENGTH(${this.jsonValue(slot)}) END`;
|
|
236
225
|
}
|
|
237
|
-
|
|
238
|
-
return
|
|
226
|
+
jsonIsArray(slot) {
|
|
227
|
+
return `JSON_TYPE(${this.jsonValue(slot)}) = 'ARRAY'`;
|
|
239
228
|
}
|
|
240
229
|
/**
|
|
241
|
-
*
|
|
242
|
-
*
|
|
230
|
+
* Each element of the array at the path as one `JSON` column, which any path then reads the way it reads
|
|
231
|
+
* a column's document.
|
|
243
232
|
*/
|
|
244
|
-
jsonElemFrom(
|
|
245
|
-
|
|
246
|
-
? fields.map((field) => `${this.escapeId(field, true)} TEXT PATH ${jsonPath(field)}`).join(', ')
|
|
247
|
-
: `elem_text ${asJson ? 'JSON' : 'TEXT'} PATH '$'`;
|
|
248
|
-
return `JSON_TABLE(${jsonField}, '$[*]' COLUMNS (${columns})) AS ${alias}`;
|
|
233
|
+
jsonElemFrom(slot, alias) {
|
|
234
|
+
return `JSON_TABLE(${slot.base}, ${jsonPath(slot.path, '[*]')} COLUMNS (${ELEM_COLUMN} JSON PATH '$')) AS ${alias}`;
|
|
249
235
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
// `JSON_TABLE` columns stay `TEXT` so the string operators keep working; the JSON form reads
|
|
253
|
-
// that text back as JSON.
|
|
254
|
-
return asJson ? this.jsonCast(ref) : ref;
|
|
236
|
+
jsonElemDoc(alias) {
|
|
237
|
+
return `${alias}.${ELEM_COLUMN}`;
|
|
255
238
|
}
|
|
256
239
|
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import { type DriverCapabilities, type EntityMeta, type FieldOptions, type JsonColumnType, type Query, type QueryContext, type
|
|
1
|
+
import { type DriverCapabilities, type EntityMeta, type FieldOptions, type JsonColumnType, type Query, type QueryContext, type QueryTextSearchOptions, type SqlDialectFeatures, type Type, type VectorDistance, type VectorMetric } from '../type/index.js';
|
|
2
2
|
import type { DialectOptions } from './abstractDialect.js';
|
|
3
3
|
import { AbstractSqlDialect, type RelationRows } from './abstractSqlDialect.js';
|
|
4
|
+
import { type JsonAccessMode, type JsonSlot } from './jsonSql.js';
|
|
4
5
|
/** A Postgres-wire dialect's options: the base's, and how its driver binds a parameter. */
|
|
5
6
|
export type PgLikeDialectOptions = DialectOptions & {
|
|
6
7
|
readonly driverCapabilities?: Partial<DriverCapabilities>;
|
|
7
8
|
};
|
|
9
|
+
/** Each metric's pgvector distance operator, and the infix of the operator class its index is built with. */
|
|
10
|
+
export declare const PG_VECTOR_METRICS: ReadonlyMap<VectorDistance, VectorMetric>;
|
|
8
11
|
/** What the Postgres-wire engines have. */
|
|
9
12
|
export declare const PG_FEATURES: SqlDialectFeatures;
|
|
10
13
|
/** What Postgres and CockroachDB share: JSONB, full-text search, pgvector's operators, and the upsert. */
|
|
@@ -40,10 +43,7 @@ export declare abstract class PgLikeSqlDialect extends AbstractSqlDialect {
|
|
|
40
43
|
protected readonly upsertUpdateBindsInPlace = true;
|
|
41
44
|
readonly insertIdSource = "returning";
|
|
42
45
|
readonly maxBindValues: number;
|
|
43
|
-
readonly vectorMetrics: ReadonlyMap<
|
|
44
|
-
readonly op: string;
|
|
45
|
-
readonly opsSuffix: string;
|
|
46
|
-
}>;
|
|
46
|
+
readonly vectorMetrics: ReadonlyMap<VectorDistance, VectorMetric>;
|
|
47
47
|
/**
|
|
48
48
|
* The GUC each pgvector index type reads for "how much of the index to explore". They are not the
|
|
49
49
|
* same quantity - `ef_search` is a candidate-list size, `probes` a count of lists - which is why
|
|
@@ -63,19 +63,26 @@ export declare abstract class PgLikeSqlDialect extends AbstractSqlDialect {
|
|
|
63
63
|
* rejects anything unparseable - including a plain two-word search.
|
|
64
64
|
*/
|
|
65
65
|
protected appendTextSearch<E>(ctx: QueryContext, _entity: Type<E>, meta: EntityMeta<E>, search: QueryTextSearchOptions<E>): void;
|
|
66
|
-
protected
|
|
67
|
-
protected
|
|
68
|
-
/**
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
protected
|
|
66
|
+
protected jsonContains(ctx: QueryContext, slot: JsonSlot, values: readonly unknown[]): string;
|
|
67
|
+
protected jsonLength(slot: JsonSlot): string;
|
|
68
|
+
/** Each element stays `jsonb`, so it is read as any path is: `->` for the value, `->>` for its text. */
|
|
69
|
+
protected jsonElemFrom(slot: JsonSlot, alias: string): string;
|
|
70
|
+
protected jsonIsArray(slot: JsonSlot): string;
|
|
71
|
+
/** The array at `slot`, NULL where the value there is none: the array functions fail the whole read on one. */
|
|
72
|
+
private jsonArray;
|
|
73
|
+
protected jsonElemDoc(alias: string): string;
|
|
74
|
+
/** `->` down the path and `->>` for its text, the document's own text being `#>> '{}'`. */
|
|
75
|
+
protected jsonPathReading(escapedColumn: string, path: string, mode: 'json' | 'text'): string;
|
|
76
|
+
/** A number only where the value is one: the cast throws on any other text, failing the whole read. */
|
|
77
|
+
jsonPathExpr(escapedColumn: string, path: string, mode: JsonAccessMode): string;
|
|
74
78
|
protected get regexpOp(): string;
|
|
75
79
|
protected readonly caseInsensitiveMatch = "ilike";
|
|
76
80
|
protected get neOp(): string;
|
|
77
|
-
/**
|
|
78
|
-
|
|
81
|
+
/**
|
|
82
|
+
* One array parameter, which a context that inlines values has none of: it lists them instead. The
|
|
83
|
+
* array takes its type from `operand`, so it needs none of the casts `bind` would give each value.
|
|
84
|
+
*/
|
|
85
|
+
protected formatIn(ctx: QueryContext, operand: string, values: unknown[], negate: boolean, bind: (value: unknown) => string): string;
|
|
79
86
|
protected numericCast(expr: string): string;
|
|
80
87
|
protected appendJsonValue(ctx: QueryContext, value: unknown, type: JsonColumnType): void;
|
|
81
88
|
/**
|
|
@@ -84,8 +91,8 @@ export declare abstract class PgLikeSqlDialect extends AbstractSqlDialect {
|
|
|
84
91
|
*/
|
|
85
92
|
protected appendVectorValue(ctx: QueryContext, value: readonly unknown[], field?: FieldOptions): void;
|
|
86
93
|
/**
|
|
87
|
-
* `create_if_missing => false` keeps an absent key (and a NULL column) untouched
|
|
88
|
-
* ORDINALITY` keeps the surviving elements in their original order.
|
|
94
|
+
* `create_if_missing => false` keeps an absent key (and a NULL column) untouched, and a value that is no
|
|
95
|
+
* array is set back as it is; `WITH ORDINALITY` keeps the surviving elements in their original order.
|
|
89
96
|
*/
|
|
90
97
|
protected jsonPullKey(ctx: QueryContext, expr: string, escapedCol: string, key: string, value: unknown): string;
|
|
91
98
|
/**
|