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.
Files changed (65) hide show
  1. package/dist/browser/http/http.js +5 -8
  2. package/dist/browser/querier/httpQuerier.d.ts +9 -9
  3. package/dist/browser/uql-browser.min.js +2 -2
  4. package/dist/browser/uql-browser.min.js.map +9 -8
  5. package/dist/cockroachdb/cockroachDialect.d.ts +6 -4
  6. package/dist/cockroachdb/cockroachDialect.js +7 -3
  7. package/dist/d1/d1Querier.d.ts +5 -5
  8. package/dist/d1/d1QuerierPool.d.ts +3 -3
  9. package/dist/d1/d1SqliteDialect.d.ts +1 -1
  10. package/dist/d1/d1SqliteDialect.js +1 -1
  11. package/dist/dialect/abstractSqlDialect.d.ts +107 -97
  12. package/dist/dialect/abstractSqlDialect.js +250 -293
  13. package/dist/dialect/hydrateColumn.js +33 -30
  14. package/dist/dialect/jsonSql.d.ts +32 -23
  15. package/dist/dialect/jsonSql.js +42 -31
  16. package/dist/dialect/mysqlLikeSqlDialect.d.ts +19 -23
  17. package/dist/dialect/mysqlLikeSqlDialect.js +34 -51
  18. package/dist/dialect/pgLikeSqlDialect.d.ts +24 -17
  19. package/dist/dialect/pgLikeSqlDialect.js +53 -26
  20. package/dist/dialect/vectorCast.d.ts +2 -0
  21. package/dist/dialect/vectorCast.js +25 -5
  22. package/dist/dialect/vectorSqlDialect.d.ts +8 -8
  23. package/dist/dialect/vectorSqlDialect.js +11 -11
  24. package/dist/http/query.d.ts +10 -2
  25. package/dist/http/query.js +26 -1
  26. package/dist/maria/mariaDialect.d.ts +16 -11
  27. package/dist/maria/mariaDialect.js +23 -19
  28. package/dist/migrate/ddl/indexDdl.d.ts +3 -1
  29. package/dist/migrate/ddl/indexDdl.js +5 -1
  30. package/dist/migrate/ddl/mysqlIndexDdl.d.ts +7 -2
  31. package/dist/migrate/ddl/mysqlIndexDdl.js +28 -6
  32. package/dist/migrate/ddl/pgIndexDdl.d.ts +0 -9
  33. package/dist/migrate/ddl/pgIndexDdl.js +2 -6
  34. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.d.ts +12 -1
  35. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +13 -3
  36. package/dist/migrate/introspection/mssqlIntrospector.d.ts +2 -9
  37. package/dist/migrate/introspection/mssqlIntrospector.js +2 -9
  38. package/dist/migrate/introspection/mysqlIntrospector.d.ts +2 -9
  39. package/dist/migrate/introspection/mysqlIntrospector.js +2 -8
  40. package/dist/mongo/mongoDialect.d.ts +22 -10
  41. package/dist/mongo/mongoDialect.js +86 -38
  42. package/dist/mssql/mssqlDialect.d.ts +22 -18
  43. package/dist/mssql/mssqlDialect.js +54 -43
  44. package/dist/mysql/mysqlDialect.d.ts +16 -1
  45. package/dist/mysql/mysqlDialect.js +18 -2
  46. package/dist/sqlite/sqliteDialect.d.ts +17 -16
  47. package/dist/sqlite/sqliteDialect.js +35 -39
  48. package/dist/type/dialect.d.ts +0 -4
  49. package/dist/type/entity.d.ts +12 -7
  50. package/dist/type/query.d.ts +29 -24
  51. package/dist/type/queryWhere.d.ts +20 -20
  52. package/dist/type/universalQuerier.d.ts +10 -10
  53. package/dist/type/vector.d.ts +5 -7
  54. package/dist/type/wire.d.ts +9 -0
  55. package/dist/util/dialect.util.d.ts +2 -0
  56. package/dist/util/dialect.util.js +6 -2
  57. package/dist/util/object.util.d.ts +2 -4
  58. package/dist/util/object.util.js +4 -9
  59. package/package.json +2 -2
  60. package/dist/dialect/jsonArrayElemMatchUtils.d.ts +0 -2
  61. package/dist/dialect/jsonArrayElemMatchUtils.js +0 -7
  62. package/dist/dialect/pgVectorMetrics.d.ts +0 -13
  63. package/dist/dialect/pgVectorMetrics.js +0 -17
  64. package/dist/maria/mariaVectorMetrics.d.ts +0 -8
  65. 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
- if (kind === 'boolean') {
9
- // 0/1 from SQLite's INTEGER or MySQL's TINYINT(1). Already a boolean on Postgres.
10
- return typeof value === 'boolean' ? value : Boolean(value);
11
- }
12
- if (kind === 'date') {
13
- return typeof value === 'string' ? (parseDate(value) ?? value) : value;
14
- }
15
- if (kind === 'bytes') {
16
- return typeof value === 'string' && value.startsWith(BYTES_PREFIX)
17
- ? hexBytes(value.slice(BYTES_PREFIX.length))
18
- : value;
19
- }
20
- const text = asText(value);
21
- if (kind === 'bigint') {
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
- // A number, not just text: `type: BigInt` is BIGINT, which the pg pools decode at the wire.
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
- // Everything below decodes text; anything else the driver already returned correctly.
35
- if (text === undefined) {
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
- return parseVectorLiteral(text, kind) ?? value;
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. `suffix` appends an accessor
4
- * such as `[#]` or `[*]`. Shared across dialects unchanged: no dialect escapes a JSON path key
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
- * `FN(target, path, value, ...)` - the multi-pair JSON assignment shape shared by MySQL's
18
- * `JSON_SET` and SQLite's `JSON_SET`/`JSON_INSERT`, nested past `maxArgs`. `pathSuffix` appends an
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 jsonAssignCall(bindValue: (value: unknown) => string, fn: string, target: string, entries: Record<string, unknown>, maxArgs: number, pathSuffix?: string): string;
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
- * `FN(expr, path, ...)` - the multi-path JSON removal shape shared by MySQL's `JSON_REMOVE` and
27
- * SQLite's `json_remove`, nested past `maxArgs`.
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 jsonRemoveCall(fn: string, expr: string, keys: readonly string[], maxArgs: number): string;
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 every operand of a comparison, so the
35
- * left side is read the way the right side is compared - see {@link jsonCompareMode}.
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 scalar compares against `value`, since extraction yields text: `numeric` casts (so `1` equals
40
- * `1.0`), `json` compares JSON values (the only portable boolean), and `text` compares as extracted,
41
- * also for mixed operands.
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
- * Whether the operator reads the JSON value rather than its text: the array operators, and equality
48
- * against a boolean, which no cast recovers portably from text.
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;
@@ -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. `suffix` appends an accessor
5
- * such as `[#]` or `[*]`. Shared across dialects unchanged: no dialect escapes a JSON path key
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 `'$.${segments}${suffix}'`;
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
- * `FN(target, path, value, ...)` - the multi-pair JSON assignment shape shared by MySQL's
31
- * `JSON_SET` and SQLite's `JSON_SET`/`JSON_INSERT`, nested past `maxArgs`. `pathSuffix` appends an
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 jsonAssignCall(bindValue, fn, target, entries, maxArgs, pathSuffix = '') {
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(fn, target, pairs, 2, maxArgs);
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
- * `FN(expr, path, ...)` - the multi-path JSON removal shape shared by MySQL's `JSON_REMOVE` and
45
- * SQLite's `json_remove`, nested past `maxArgs`.
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 jsonRemoveCall(fn, expr, keys, maxArgs) {
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 scalar compares against `value`, since extraction yields text: `numeric` casts (so `1` equals
57
- * `1.0`), `json` compares JSON values (the only portable boolean), and `text` compares as extracted,
58
- * also for mixed operands.
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
- * Whether the operator reads the JSON value rather than its text: the array operators, and equality
80
- * against a boolean, which no cast recovers portably from text.
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
- const comparesValue = op === '$eq' || op === '$ne' || op === '$in' || op === '$nin';
87
- return comparesValue && jsonCompareMode(value) === 'json';
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, QuerySizeComparisonOps, QueryTextSearchOptions, SqlDialectFeatures, Type } from '../type/index.js';
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: text on MariaDB, which stores it packed.
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
- * MySQL's `->`/`->>` take a full JSON path (`'$.a.b'`, never a bare key) and only apply to a
105
- * column reference, so the whole dotted path goes into a single accessor instead of the base's
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 getJsonPathScalarExpr(escapedColumn: string, jsonPathStr: string): string;
109
- protected getJsonPathJsonbExpr(escapedColumn: string, jsonPathStr: string): string;
110
- protected jsonAll(ctx: QueryContext, jsonField: string, value: unknown): string;
111
- protected jsonSize(ctx: QueryContext, jsonField: string, value: number | QuerySizeComparisonOps): string;
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
- * `JSON_TABLE` with its columns declared up front. A scalar element reads as `JSON` where `asJson`,
114
- * since `TEXT` reads a nested array as `NULL`.
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(jsonField: string, fields: readonly string[], alias: string, asJson?: boolean): string;
117
- protected jsonElemRef(alias: string, field?: string, asJson?: boolean): string;
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, JSON_PULL_ALIAS } from './aliases.js';
5
+ import { COUNT_ALIAS } from './aliases.js';
6
6
  import { BYTES_PREFIX } from './hydrateColumn.js';
7
- import { jsonAssignCall, jsonPath, jsonRemoveCall, jsonSetTarget } from './jsonSql.js';
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: text on MariaDB, which stores it packed.
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) => `CONCAT(${this.escape(BYTES_PREFIX)}, HEX(${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 DECIMAL)`;
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 jsonAssignCall((value) => this.jsonScalarParam(ctx, value), 'JSON_SET', jsonSetTarget(expr, field, `'{}'`), set, this.maxFunctionArgs);
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
- * MySQL's `->`/`->>` take a full JSON path (`'$.a.b'`, never a bare key) and only apply to a
225
- * column reference, so the whole dotted path goes into a single accessor instead of the base's
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
- getJsonPathScalarExpr(escapedColumn, jsonPathStr) {
229
- return `(${escapedColumn}->>${jsonPath(jsonPathStr)})`;
212
+ jsonPathReading(escapedColumn, path, mode) {
213
+ return mode === 'json' ? `${escapedColumn}->${jsonPath(path)}` : `(${escapedColumn}->>${jsonPath(path)})`;
230
214
  }
231
- getJsonPathJsonbExpr(escapedColumn, jsonPathStr) {
232
- return `${escapedColumn}->${jsonPath(jsonPathStr)}`;
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
- jsonAll(ctx, jsonField, value) {
235
- return `JSON_CONTAINS(${jsonField}, ${this.addValue(ctx, JSON.stringify(value))})`;
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
- jsonSize(ctx, jsonField, value) {
238
- return this.buildFragment(ctx, (fragmentCtx) => this.buildSizeComparison(fragmentCtx, () => fragmentCtx.append(`JSON_LENGTH(${jsonField})`), value));
226
+ jsonIsArray(slot) {
227
+ return `JSON_TYPE(${this.jsonValue(slot)}) = 'ARRAY'`;
239
228
  }
240
229
  /**
241
- * `JSON_TABLE` with its columns declared up front. A scalar element reads as `JSON` where `asJson`,
242
- * since `TEXT` reads a nested array as `NULL`.
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(jsonField, fields, alias, asJson = false) {
245
- const columns = fields.length
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
- jsonElemRef(alias, field, asJson = false) {
251
- const ref = field === undefined ? `${alias}.elem_text` : `${alias}.${this.escapeId(field, true)}`;
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 QuerySizeComparisonOps, type QueryTextSearchOptions, type SqlDialectFeatures, type Type } from '../type/index.js';
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<import("../type/vector.js").VectorDistance, {
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 jsonAll(ctx: QueryContext, jsonField: string, value: unknown): string;
67
- protected jsonSize(ctx: QueryContext, jsonField: string, value: number | QuerySizeComparisonOps): string;
68
- /**
69
- * Object elements stay `jsonb` so each field can pick `->` or `->>`. Scalar elements are exploded
70
- * as text unless they are compared as JSON, where `_text` would yield `text = jsonb`.
71
- */
72
- protected jsonElemFrom(jsonField: string, fields: readonly string[], alias: string, asJson?: boolean): string;
73
- protected jsonElemRef(alias: string, field?: string, asJson?: boolean): string;
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
- /** One array parameter, which a context that inlines values has none of: it lists them instead. */
78
- protected formatIn(ctx: QueryContext, operand: string, values: unknown[], negate: boolean): string;
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; `WITH
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
  /**