uql-orm 0.56.0 → 0.58.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/README.md +7 -9
  2. package/dist/browser/uql-browser.min.js +2 -2
  3. package/dist/browser/uql-browser.min.js.map +4 -4
  4. package/dist/cockroachdb/cockroachDialect.d.ts +5 -2
  5. package/dist/cockroachdb/cockroachDialect.js +2 -10
  6. package/dist/d1/d1SqliteDialect.d.ts +1 -0
  7. package/dist/d1/d1SqliteDialect.js +2 -0
  8. package/dist/dialect/abstractSqlDialect.d.ts +196 -33
  9. package/dist/dialect/abstractSqlDialect.js +410 -203
  10. package/dist/dialect/aliases.d.ts +10 -7
  11. package/dist/dialect/aliases.js +12 -7
  12. package/dist/dialect/hydrateColumn.d.ts +8 -2
  13. package/dist/dialect/hydrateColumn.js +33 -1
  14. package/dist/dialect/jsonSql.d.ts +13 -5
  15. package/dist/dialect/jsonSql.js +24 -7
  16. package/dist/dialect/mysqlLikeSqlDialect.d.ts +31 -3
  17. package/dist/dialect/mysqlLikeSqlDialect.js +57 -5
  18. package/dist/dialect/pgLikeSqlDialect.d.ts +20 -20
  19. package/dist/dialect/pgLikeSqlDialect.js +23 -48
  20. package/dist/dialect/pgVectorMetrics.d.ts +13 -0
  21. package/dist/dialect/pgVectorMetrics.js +17 -0
  22. package/dist/dialect/queryContext.d.ts +3 -7
  23. package/dist/dialect/queryContext.js +13 -8
  24. package/dist/dialect/queryJoins.d.ts +8 -4
  25. package/dist/dialect/queryJoins.js +26 -11
  26. package/dist/dialect/vectorSqlDialect.d.ts +2 -2
  27. package/dist/dialect/vectorSqlDialect.js +2 -3
  28. package/dist/entity/decorator/bag.d.ts +2 -2
  29. package/dist/entity/decorator/entity.d.ts +8 -9
  30. package/dist/entity/decorator/entity.js +6 -7
  31. package/dist/entity/decorator/members.d.ts +7 -6
  32. package/dist/entity/decorator/members.js +2 -1
  33. package/dist/entity/metadata/definition.d.ts +16 -11
  34. package/dist/entity/metadata/definition.js +54 -42
  35. package/dist/http/handler.d.ts +2 -2
  36. package/dist/http/handler.js +0 -1
  37. package/dist/maria/mariaDialect.d.ts +13 -6
  38. package/dist/maria/mariaDialect.js +29 -9
  39. package/dist/migrate/cli.d.ts +2 -3
  40. package/dist/migrate/cli.js +2 -2
  41. package/dist/migrate/codegen/entityCodeGenerator.js +6 -4
  42. package/dist/migrate/codegen/entityTypes.d.ts +1 -1
  43. package/dist/migrate/codegen/entityTypes.js +4 -3
  44. package/dist/migrate/codegen/indexDecoratorSource.d.ts +5 -4
  45. package/dist/migrate/codegen/indexDecoratorSource.js +17 -13
  46. package/dist/migrate/codegen/sourceLiteral.d.ts +2 -0
  47. package/dist/migrate/codegen/sourceLiteral.js +4 -0
  48. package/dist/migrate/ddl/index.d.ts +1 -5
  49. package/dist/migrate/ddl/index.js +14 -25
  50. package/dist/migrate/ddl/indexDdl.d.ts +11 -2
  51. package/dist/migrate/ddl/indexDdl.js +17 -1
  52. package/dist/migrate/ddl/mssqlIndexDdl.d.ts +10 -0
  53. package/dist/migrate/ddl/mssqlIndexDdl.js +10 -0
  54. package/dist/migrate/ddl/mysqlIndexDdl.d.ts +10 -17
  55. package/dist/migrate/ddl/mysqlIndexDdl.js +16 -27
  56. package/dist/migrate/ddl/pgIndexDdl.d.ts +18 -8
  57. package/dist/migrate/ddl/pgIndexDdl.js +29 -12
  58. package/dist/migrate/generator/mongoSchemaGenerator.d.ts +3 -3
  59. package/dist/migrate/migrator.d.ts +4 -4
  60. package/dist/migrate/schemaGenerator.d.ts +8 -8
  61. package/dist/migrate/schemaGenerator.js +5 -7
  62. package/dist/migrate/schemaGeneratorAsync.d.ts +2 -3
  63. package/dist/mongo/mongoDialect.d.ts +31 -18
  64. package/dist/mongo/mongoDialect.js +146 -104
  65. package/dist/mongo/mongodbQuerier.d.ts +10 -17
  66. package/dist/mongo/mongodbQuerier.js +31 -106
  67. package/dist/mssql/mssqlDialect.d.ts +16 -0
  68. package/dist/mssql/mssqlDialect.js +26 -4
  69. package/dist/mysql/mysqlDialect.d.ts +2 -0
  70. package/dist/mysql/mysqlDialect.js +4 -0
  71. package/dist/querier/abstractQuerier.d.ts +20 -36
  72. package/dist/querier/abstractQuerier.js +35 -129
  73. package/dist/querier/abstractQuerierPool.d.ts +2 -2
  74. package/dist/querier/abstractSqlQuerier.d.ts +4 -17
  75. package/dist/querier/abstractSqlQuerier.js +40 -50
  76. package/dist/schema/canonicalType.js +4 -4
  77. package/dist/schema/indexDifferences.js +4 -4
  78. package/dist/schema/schemaASTBuilder.d.ts +3 -3
  79. package/dist/schema/schemaASTBuilder.js +32 -3
  80. package/dist/schema/schemaASTDiffer.js +5 -5
  81. package/dist/sqlite/sqliteDialect.d.ts +20 -1
  82. package/dist/sqlite/sqliteDialect.js +38 -7
  83. package/dist/turso/tursoDialect.d.ts +2 -0
  84. package/dist/turso/tursoDialect.js +2 -0
  85. package/dist/type/config.d.ts +3 -3
  86. package/dist/type/dialect.d.ts +4 -5
  87. package/dist/type/entity.d.ts +110 -69
  88. package/dist/type/migration.d.ts +7 -7
  89. package/dist/type/migratorDialect.d.ts +4 -0
  90. package/dist/type/querier.d.ts +6 -6
  91. package/dist/type/querierPool.d.ts +2 -2
  92. package/dist/type/query.d.ts +41 -72
  93. package/dist/type/query.js +10 -5
  94. package/dist/type/queryAggregate.d.ts +43 -34
  95. package/dist/type/queryAggregate.js +1 -1
  96. package/dist/type/queryWhere.d.ts +12 -9
  97. package/dist/type/universalQuerier.d.ts +4 -4
  98. package/dist/util/dialect.util.d.ts +4 -4
  99. package/dist/util/dialect.util.js +24 -15
  100. package/dist/util/field.util.d.ts +5 -0
  101. package/dist/util/field.util.js +19 -0
  102. package/dist/util/object.util.d.ts +2 -0
  103. package/dist/util/object.util.js +4 -0
  104. package/dist/util/relationQuery.util.d.ts +12 -65
  105. package/dist/util/relationQuery.util.js +27 -81
  106. package/dist/util/rowKey.util.d.ts +1 -11
  107. package/dist/util/rowKey.util.js +1 -13
  108. package/package.json +1 -1
  109. package/dist/querier/relationCount.d.ts +0 -16
  110. package/dist/querier/relationCount.js +0 -121
@@ -14,13 +14,11 @@ export declare const COUNT_ALIAS = "_uql_count";
14
14
  export declare const TOTAL_ALIAS = "_uql_total";
15
15
  /** The derived table a `$distinct` count wraps its deduplicated set in. MySQL requires the alias. */
16
16
  export declare const DISTINCT_DERIVED_ALIAS = "_uql_distinct";
17
- /** Prefix for the derived table each branch of a per-parent bounded read is wrapped in. */
18
- export declare const PER_PARENT_BRANCH_ALIAS = "_uql_p";
19
- /** The row source a `LATERAL` per-parent read correlates each of its branches against. */
20
- export declare const PER_PARENT_KEYS_ALIAS = "_uql_keys";
21
- /** Prefix for the alias an exploded JSON array element is read through. */
22
- export declare const JSON_ELEM_ALIAS_PREFIX = "_uql_elem";
23
- /** The alias a `$pull` reads its surviving elements through, kept distinct from {@link JSON_ELEM_ALIAS_PREFIX}. */
17
+ /** The row a Postgres relation aggregates whole: a LATERAL projection of the columns it answers under. */
18
+ export declare const RELATION_ROW_ALIAS = "_uql_row";
19
+ /** The alias an exploded JSON array element is read through, `_uql_elem_2` and on where one nests in another. */
20
+ export declare const JSON_ELEM_ALIAS = "_uql_elem";
21
+ /** The alias a `$pull` reads its surviving elements through, kept distinct from {@link JSON_ELEM_ALIAS}. */
24
22
  export declare const JSON_PULL_ALIAS = "_uql_pull";
25
23
  /** Prefix for the field a MongoDB relation lookup parks its result on, one per condition. */
26
24
  export declare const REL_TEMP_PREFIX = "_uql_rel_";
@@ -40,3 +38,8 @@ export declare const UPSERT_SOURCE_ALIAS = "_uql_src";
40
38
  * ranks a field that is not there as all-equal rather than failing, so a drift would go unnoticed.
41
39
  */
42
40
  export declare function sortCountField(relKey: string): string;
41
+ /**
42
+ * The column a relation's rows carry one sort term out in, beside the columns they answer under, for
43
+ * the aggregate reading them to order by: `_uql_sort_createdAt`.
44
+ */
45
+ export declare function relationSortColumn(path: string): string;
@@ -14,13 +14,11 @@ export const COUNT_ALIAS = '_uql_count';
14
14
  export const TOTAL_ALIAS = '_uql_total';
15
15
  /** The derived table a `$distinct` count wraps its deduplicated set in. MySQL requires the alias. */
16
16
  export const DISTINCT_DERIVED_ALIAS = '_uql_distinct';
17
- /** Prefix for the derived table each branch of a per-parent bounded read is wrapped in. */
18
- export const PER_PARENT_BRANCH_ALIAS = '_uql_p';
19
- /** The row source a `LATERAL` per-parent read correlates each of its branches against. */
20
- export const PER_PARENT_KEYS_ALIAS = '_uql_keys';
21
- /** Prefix for the alias an exploded JSON array element is read through. */
22
- export const JSON_ELEM_ALIAS_PREFIX = '_uql_elem';
23
- /** The alias a `$pull` reads its surviving elements through, kept distinct from {@link JSON_ELEM_ALIAS_PREFIX}. */
17
+ /** The row a Postgres relation aggregates whole: a LATERAL projection of the columns it answers under. */
18
+ export const RELATION_ROW_ALIAS = '_uql_row';
19
+ /** The alias an exploded JSON array element is read through, `_uql_elem_2` and on where one nests in another. */
20
+ export const JSON_ELEM_ALIAS = '_uql_elem';
21
+ /** The alias a `$pull` reads its surviving elements through, kept distinct from {@link JSON_ELEM_ALIAS}. */
24
22
  export const JSON_PULL_ALIAS = '_uql_pull';
25
23
  /** Prefix for the field a MongoDB relation lookup parks its result on, one per condition. */
26
24
  export const REL_TEMP_PREFIX = '_uql_rel_';
@@ -42,3 +40,10 @@ export const UPSERT_SOURCE_ALIAS = '_uql_src';
42
40
  export function sortCountField(relKey) {
43
41
  return `_uql_sort_count_${relKey}`;
44
42
  }
43
+ /**
44
+ * The column a relation's rows carry one sort term out in, beside the columns they answer under, for
45
+ * the aggregate reading them to order by: `_uql_sort_createdAt`.
46
+ */
47
+ export function relationSortColumn(path) {
48
+ return `_uql_sort_${path}`;
49
+ }
@@ -2,9 +2,10 @@ import { type VectorCast } from './vectorCast.js';
2
2
  /**
3
3
  * How a stored column is decoded on read: the inverse of `AbstractSqlDialect.persistKind`. `json`
4
4
  * parses; a {@link VectorCast} says which literal; `boolean` undoes an engine with no boolean type,
5
- * `number` and `bigint` a driver that hands a wide integer or a decimal back as text.
5
+ * `number` and `bigint` a driver that hands a wide integer or a decimal back as text, and `date` and
6
+ * `bytes` a row that crossed JSON inside its parent's statement, which spells both as text.
6
7
  */
7
- export type HydrateKind = 'json' | 'boolean' | 'number' | 'bigint' | VectorCast;
8
+ export type HydrateKind = 'json' | 'boolean' | 'number' | 'bigint' | 'date' | 'bytes' | VectorCast;
8
9
  /**
9
10
  * Decode one non-null cell. Kept beside {@link HydrateKind} rather than inlined into the querier's
10
11
  * row walk, so classifying a column and decoding it stay one subject in one file.
@@ -14,3 +15,8 @@ export type HydrateKind = 'json' | 'boolean' | 'number' | 'bigint' | VectorCast;
14
15
  * that does not match its column's format is returned untouched rather than replaced by a guess.
15
16
  */
16
17
  export declare function decodeColumn(value: unknown, kind: HydrateKind): unknown;
18
+ /**
19
+ * What bytes crossing JSON start with, before two hex digits per byte: Postgres's own text for `bytea`,
20
+ * which every dialect spells, so a string a driver reads from a column on its own is never mistaken.
21
+ */
22
+ export declare const BYTES_PREFIX = "\\x";
@@ -13,6 +13,14 @@ export function decodeColumn(value, kind) {
13
13
  // 0/1 from SQLite's INTEGER or MySQL's TINYINT(1). Already a boolean on Postgres.
14
14
  return typeof value === 'boolean' ? value : Boolean(value);
15
15
  }
16
+ if (kind === 'date') {
17
+ return typeof value === 'string' ? (parseDate(value) ?? value) : value;
18
+ }
19
+ if (kind === 'bytes') {
20
+ return typeof value === 'string' && value.startsWith(BYTES_PREFIX)
21
+ ? hexBytes(value.slice(BYTES_PREFIX.length))
22
+ : value;
23
+ }
16
24
  const text = asText(value);
17
25
  if (kind === 'bigint') {
18
26
  if (typeof value === 'bigint') {
@@ -32,7 +40,7 @@ export function decodeColumn(value, kind) {
32
40
  return value;
33
41
  }
34
42
  if (kind === 'number') {
35
- return Number.isNaN(Number(text)) ? value : decodeWideNumber(text);
43
+ return decodeWideNumber(text);
36
44
  }
37
45
  if (kind === 'json') {
38
46
  try {
@@ -45,6 +53,30 @@ export function decodeColumn(value, kind) {
45
53
  }
46
54
  return parseVectorLiteral(text, kind) ?? value;
47
55
  }
56
+ /**
57
+ * An ISO 8601 timestamp as a `Date`, its fraction cut to the milliseconds one holds, and a bare date at
58
+ * local midnight, which is how `pg` reads a `date`. `undefined` for text that is neither.
59
+ */
60
+ function parseDate(text) {
61
+ const day = /^(\d{4})-(\d{2})-(\d{2})$/.exec(text);
62
+ const date = day
63
+ ? new Date(Number(day[1]), Number(day[2]) - 1, Number(day[3]))
64
+ : new Date(text.replace(/(\.\d{3})\d+/, '$1'));
65
+ return Number.isNaN(date.getTime()) ? undefined : date;
66
+ }
67
+ /**
68
+ * What bytes crossing JSON start with, before two hex digits per byte: Postgres's own text for `bytea`,
69
+ * which every dialect spells, so a string a driver reads from a column on its own is never mistaken.
70
+ */
71
+ export const BYTES_PREFIX = '\\x';
72
+ /** Two hex digits per byte, back to bytes. */
73
+ function hexBytes(hex) {
74
+ const bytes = new Uint8Array(hex.length / 2);
75
+ for (let at = 0; at < bytes.length; at++) {
76
+ bytes[at] = Number.parseInt(hex.slice(at * 2, at * 2 + 2), 16);
77
+ }
78
+ return bytes;
79
+ }
48
80
  /** Lazy so a consumer that never reads an encoded column never constructs one. */
49
81
  let decoder;
50
82
  /**
@@ -5,20 +5,28 @@ import type { FieldOptions, FieldType } from '../type/index.js';
5
5
  * differently from an ANSI string literal.
6
6
  */
7
7
  export declare function jsonPath(path: string, suffix?: string): string;
8
+ /** How many argument groups of `size` fit in one call beside its target, under `maxArgs`. */
9
+ export declare function groupsPerCall(maxArgs: number, size: number): number;
10
+ /**
11
+ * `fn(target, ...groups)`, nested wherever one call would take more than `maxArgs` arguments: each call
12
+ * applies its share to what the call inside it returned, as `JSON_SET`, `JSON_REMOVE` and `json_insert`
13
+ * do. A group, such as a path and its value, stays in one call.
14
+ */
15
+ export declare function chainedCall(fn: string, target: string, groups: readonly string[], size: number, maxArgs: number): string;
8
16
  /**
9
17
  * `FN(target, path, value, ...)` - the multi-pair JSON assignment shape shared by MySQL's
10
- * `JSON_SET` and SQLite's `JSON_SET`/`JSON_INSERT`. `pathSuffix` appends an accessor per key
11
- * (SQLite's `[#]` append). Values bind in key order through `bindValue`, the caller'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
12
20
  * `jsonScalarParam` bound to its `QueryContext`.
13
21
  */
14
- export declare function jsonAssignCall(bindValue: (value: unknown) => string, fn: string, target: string, entries: Record<string, unknown>, pathSuffix?: string): string;
22
+ export declare function jsonAssignCall(bindValue: (value: unknown) => string, fn: string, target: string, entries: Record<string, unknown>, maxArgs: number, pathSuffix?: string): string;
15
23
  /** The `$set` target: a nullable column needs a `COALESCE` fallback to build on. */
16
24
  export declare function jsonSetTarget(expr: string, field: FieldOptions | undefined, empty: string): string;
17
25
  /**
18
26
  * `FN(expr, path, ...)` - the multi-path JSON removal shape shared by MySQL's `JSON_REMOVE` and
19
- * SQLite's `json_remove`, both of which take every path in a single call.
27
+ * SQLite's `json_remove`, nested past `maxArgs`.
20
28
  */
21
- export declare function jsonRemoveCall(fn: string, expr: string, keys: readonly string[]): string;
29
+ export declare function jsonRemoveCall(fn: string, expr: string, keys: readonly string[], maxArgs: number): string;
22
30
  /** `WHERE` is omitted for an empty `$elemMatch`, which asks only that the array has an element. */
23
31
  export declare function jsonElemExists(from: string, conditions: readonly string[]): string;
24
32
  /**
@@ -9,15 +9,32 @@ export function jsonPath(path, suffix = '') {
9
9
  const segments = path.split('.').map(escapeSingleQuotes).join('.');
10
10
  return `'$.${segments}${suffix}'`;
11
11
  }
12
+ /** How many argument groups of `size` fit in one call beside its target, under `maxArgs`. */
13
+ export function groupsPerCall(maxArgs, size) {
14
+ return Math.max(1, Math.floor((maxArgs - 1) / size));
15
+ }
16
+ /**
17
+ * `fn(target, ...groups)`, nested wherever one call would take more than `maxArgs` arguments: each call
18
+ * applies its share to what the call inside it returned, as `JSON_SET`, `JSON_REMOVE` and `json_insert`
19
+ * do. A group, such as a path and its value, stays in one call.
20
+ */
21
+ export function chainedCall(fn, target, groups, size, maxArgs) {
22
+ const perCall = groupsPerCall(maxArgs, size);
23
+ let call = target;
24
+ for (let at = 0; at < groups.length; at += perCall) {
25
+ call = `${fn}(${call}, ${groups.slice(at, at + perCall).join(', ')})`;
26
+ }
27
+ return call;
28
+ }
12
29
  /**
13
30
  * `FN(target, path, value, ...)` - the multi-pair JSON assignment shape shared by MySQL's
14
- * `JSON_SET` and SQLite's `JSON_SET`/`JSON_INSERT`. `pathSuffix` appends an accessor per key
15
- * (SQLite's `[#]` append). Values bind in key order through `bindValue`, the caller'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
16
33
  * `jsonScalarParam` bound to its `QueryContext`.
17
34
  */
18
- export function jsonAssignCall(bindValue, fn, target, entries, pathSuffix = '') {
35
+ export function jsonAssignCall(bindValue, fn, target, entries, maxArgs, pathSuffix = '') {
19
36
  const pairs = Object.entries(entries).map(([key, value]) => `${jsonPath(key, pathSuffix)}, ${bindValue(value)}`);
20
- return `${fn}(${target}, ${pairs.join(', ')})`;
37
+ return chainedCall(fn, target, pairs, 2, maxArgs);
21
38
  }
22
39
  /** The `$set` target: a nullable column needs a `COALESCE` fallback to build on. */
23
40
  export function jsonSetTarget(expr, field, empty) {
@@ -25,10 +42,10 @@ export function jsonSetTarget(expr, field, empty) {
25
42
  }
26
43
  /**
27
44
  * `FN(expr, path, ...)` - the multi-path JSON removal shape shared by MySQL's `JSON_REMOVE` and
28
- * SQLite's `json_remove`, both of which take every path in a single call.
45
+ * SQLite's `json_remove`, nested past `maxArgs`.
29
46
  */
30
- export function jsonRemoveCall(fn, expr, keys) {
31
- return `${fn}(${expr}, ${keys.map((key) => jsonPath(key)).join(', ')})`;
47
+ export function jsonRemoveCall(fn, expr, keys, maxArgs) {
48
+ return chainedCall(fn, expr, keys.map((key) => jsonPath(key)), 1, maxArgs);
32
49
  }
33
50
  /** `WHERE` is omitted for an empty `$elemMatch`, which asks only that the array has an element. */
34
51
  export function jsonElemExists(from, conditions) {
@@ -1,5 +1,5 @@
1
- import type { DialectFeatures, EntityMeta, FieldOptions, InsertIdSource, QueryConflictPaths, QueryContext, QueryPager, QuerySizeComparisonOps, QueryTextSearchOptions, Type } from '../type/index.js';
2
- import { AbstractSqlDialect } from './abstractSqlDialect.js';
1
+ import type { DialectFeatures, EntityMeta, FieldOptions, InsertIdSource, Query, QueryConflictPaths, QueryContext, QueryOptions, QueryPager, QuerySizeComparisonOps, QueryTextSearchOptions, Type } from '../type/index.js';
2
+ import { AbstractSqlDialect, type DerivedRelation, type RelationRows } from './abstractSqlDialect.js';
3
3
  /**
4
4
  * Shared JSON-array / JSON-object operator implementation between MySQL and MariaDB.
5
5
  *
@@ -64,11 +64,39 @@ export declare abstract class MysqlLikeSqlDialect extends AbstractSqlDialect {
64
64
  */
65
65
  protected readonly upsertNewRowAlias: string | undefined;
66
66
  readonly maxBindValues: number;
67
+ /**
68
+ * An ordered `GROUP_CONCAT` of each row's object, which reads as a JSON array: MySQL's `JSON_ARRAYAGG`
69
+ * takes no `ORDER BY`, and as a window over the rows it rebuilds the array for every one of them.
70
+ */
71
+ protected appendRelationArray(ctx: QueryContext, rows: RelationRows): void;
72
+ /** A read's statement, with the settings it needs applied to it alone. */
73
+ find<E>(ctx: QueryContext, entity: Type<E>, q?: Query<E>, opts?: QueryOptions, totalAlias?: string): void;
74
+ /** A `$distinct` read's count, which reads the relations the read does. */
75
+ countDistinct<E>(ctx: QueryContext, entity: Type<E>, q: Query<E>, opts?: QueryOptions): void;
76
+ private settled;
77
+ /**
78
+ * `name=value` for each variable a read's statement sets for itself alone. `GROUP_CONCAT`, and
79
+ * MariaDB's `JSON_ARRAYAGG` built on it, cut a relation's array at `group_concat_max_len`.
80
+ */
81
+ protected statementSettings<E>(entity: Type<E>, q: Query<E>): string[];
82
+ /** `sql` with `settings` scoped to it, in this engine's spelling. */
83
+ protected abstract applySettings(sql: string, settings: readonly string[]): string;
84
+ /** `JSON_OBJECT` of each key and its value. */
85
+ protected jsonObject(pairs: DerivedRelation['pairs']): string;
86
+ /**
87
+ * A number and bytes cross JSON as text, where JSON would round the one and spell the other as base64,
88
+ * and a vector as the engine reads one back: text on MariaDB, which stores it packed.
89
+ */
90
+ protected readonly carriedFields: {
91
+ numeric: (expr: string) => string;
92
+ blob: (expr: string) => string;
93
+ vector: (expr: string, field: FieldOptions) => string;
94
+ };
67
95
  escape(value: unknown): string;
68
96
  /**
69
97
  * `MATCH(cols) AGAINST(?)`, which needs a `FULLTEXT` index over exactly those columns: without one
70
98
  * the server answers "Can't find FULLTEXT index matching the column list". Declare it with
71
- * `@Index([...], { type: 'fulltext' })`.
99
+ * `@Index((post) => [...], { type: 'fulltext' })`.
72
100
  */
73
101
  protected appendTextSearch<E>(ctx: QueryContext, _entity: Type<E>, meta: EntityMeta<E>, search: QueryTextSearchOptions<E>): void;
74
102
  protected numericCast(expr: string): string;
@@ -1,10 +1,15 @@
1
1
  import { getMeta } from '../entity/index.js';
2
2
  import { textSearchFields } from '../util/index.js';
3
3
  import { escapeMysqlSqlLiteral, escapeSingleQuotes } from '../util/sqlLiteral.js';
4
- import { AbstractSqlDialect } from './abstractSqlDialect.js';
4
+ import { AbstractSqlDialect, } from './abstractSqlDialect.js';
5
5
  import { COUNT_ALIAS, JSON_PULL_ALIAS } from './aliases.js';
6
+ import { BYTES_PREFIX } from './hydrateColumn.js';
6
7
  import { jsonAssignCall, jsonPath, jsonRemoveCall, jsonSetTarget } from './jsonSql.js';
7
- /** The row count MySQL's manual gives for "all rows from the offset on": the largest `BIGINT UNSIGNED`. */
8
+ import { aggregatesRelations } from './queryJoins.js';
9
+ /**
10
+ * The largest `BIGINT UNSIGNED`: the row count MySQL's manual gives for "all rows from the offset on",
11
+ * and the largest `group_concat_max_len` either engine takes.
12
+ */
8
13
  const MAX_LIMIT = BigInt.asUintN(64, -1n);
9
14
  /**
10
15
  * Shared JSON-array / JSON-object operator implementation between MySQL and MariaDB.
@@ -126,13 +131,60 @@ export class MysqlLikeSqlDialect extends AbstractSqlDialect {
126
131
  */
127
132
  upsertNewRowAlias = undefined;
128
133
  maxBindValues = 65535;
134
+ /**
135
+ * An ordered `GROUP_CONCAT` of each row's object, which reads as a JSON array: MySQL's `JSON_ARRAYAGG`
136
+ * takes no `ORDER BY`, and as a window over the rows it rebuilds the array for every one of them.
137
+ */
138
+ appendRelationArray(ctx, rows) {
139
+ const { from, pairs, order } = this.derivedRelation(ctx, rows);
140
+ const objects = `${this.jsonObject(pairs)}${order ? ` ORDER BY ${order}` : ''} SEPARATOR ','`;
141
+ ctx.append(`(SELECT COALESCE(CONCAT('[', GROUP_CONCAT(${objects}), ']'), '[]') FROM ${from})`);
142
+ }
143
+ /** A read's statement, with the settings it needs applied to it alone. */
144
+ find(ctx, entity, q = {}, opts, totalAlias) {
145
+ this.settled(ctx, entity, q, (statement) => super.find(statement, entity, q, opts, totalAlias));
146
+ }
147
+ /** A `$distinct` read's count, which reads the relations the read does. */
148
+ countDistinct(ctx, entity, q, opts) {
149
+ this.settled(ctx, entity, q, (statement) => super.countDistinct(statement, entity, q, opts));
150
+ }
151
+ settled(ctx, entity, q, build) {
152
+ const settings = this.statementSettings(entity, q);
153
+ if (!settings.length) {
154
+ build(ctx);
155
+ return;
156
+ }
157
+ const statement = ctx.createFragment();
158
+ build(statement);
159
+ ctx.append(this.applySettings(statement.sql, settings));
160
+ }
161
+ /**
162
+ * `name=value` for each variable a read's statement sets for itself alone. `GROUP_CONCAT`, and
163
+ * MariaDB's `JSON_ARRAYAGG` built on it, cut a relation's array at `group_concat_max_len`.
164
+ */
165
+ statementSettings(entity, q) {
166
+ return aggregatesRelations(getMeta(entity), q) ? [`group_concat_max_len=${MAX_LIMIT}`] : [];
167
+ }
168
+ /** `JSON_OBJECT` of each key and its value. */
169
+ jsonObject(pairs) {
170
+ return `JSON_OBJECT(${this.jsonObjectArgs(pairs)})`;
171
+ }
172
+ /**
173
+ * A number and bytes cross JSON as text, where JSON would round the one and spell the other as base64,
174
+ * and a vector as the engine reads one back: text on MariaDB, which stores it packed.
175
+ */
176
+ carriedFields = {
177
+ numeric: (expr) => `CAST(${expr} AS CHAR)`,
178
+ blob: (expr) => `CONCAT(${this.escape(BYTES_PREFIX)}, HEX(${expr}))`,
179
+ vector: (expr, field) => this.selectFieldExpr(expr, field),
180
+ };
129
181
  escape(value) {
130
182
  return escapeMysqlSqlLiteral(value);
131
183
  }
132
184
  /**
133
185
  * `MATCH(cols) AGAINST(?)`, which needs a `FULLTEXT` index over exactly those columns: without one
134
186
  * the server answers "Can't find FULLTEXT index matching the column list". Declare it with
135
- * `@Index([...], { type: 'fulltext' })`.
187
+ * `@Index((post) => [...], { type: 'fulltext' })`.
136
188
  */
137
189
  appendTextSearch(ctx, _entity, meta, search) {
138
190
  const columns = textSearchFields(meta, search).map((key) => this.escapeId(this.resolveColumnName(key, meta.fields[key])));
@@ -169,7 +221,7 @@ export class MysqlLikeSqlDialect extends AbstractSqlDialect {
169
221
  * applicable: it requires the target column as the direct `JSON_SET` input.
170
222
  */
171
223
  jsonSet(ctx, expr, set, field) {
172
- return jsonAssignCall((value) => this.jsonScalarParam(ctx, value), 'JSON_SET', jsonSetTarget(expr, field, `'{}'`), set);
224
+ return jsonAssignCall((value) => this.jsonScalarParam(ctx, value), 'JSON_SET', jsonSetTarget(expr, field, `'{}'`), set, this.maxFunctionArgs);
173
225
  }
174
226
  /**
175
227
  * `JSON_MERGE_PRESERVE` concatenates arrays and creates absent keys, so every pushed key is
@@ -181,7 +233,7 @@ export class MysqlLikeSqlDialect extends AbstractSqlDialect {
181
233
  return `JSON_MERGE_PRESERVE(${expr}, JSON_OBJECT(${entries.join(', ')}))`;
182
234
  }
183
235
  jsonUnset(_ctx, expr, unset) {
184
- return jsonRemoveCall('JSON_REMOVE', expr, unset);
236
+ return jsonRemoveCall('JSON_REMOVE', expr, unset, this.maxFunctionArgs);
185
237
  }
186
238
  /**
187
239
  * MySQL's `->`/`->>` take a full JSON path (`'$.a.b'`, never a bare key) and only apply to a
@@ -1,6 +1,5 @@
1
- import { type DialectFeatures, type EntityMeta, type FieldOptions, type JsonColumnType, type Query, type QueryContext, type QuerySizeComparisonOps, type QueryTextSearchOptions, type Type, type VectorDistance, type VectorOperatorMetric } from '../type/index.js';
2
- import { type ParentPartition } from '../util/relationQuery.util.js';
3
- import { AbstractSqlDialect } from './abstractSqlDialect.js';
1
+ import { type DialectFeatures, type EntityMeta, type FieldOptions, type JsonColumnType, type Query, type QueryContext, type QuerySizeComparisonOps, type QueryTextSearchOptions, type Type } from '../type/index.js';
2
+ import { AbstractSqlDialect, type RelationRows } from './abstractSqlDialect.js';
4
3
  /**
5
4
  * Shared AST/quoting/JSONB/full-text-search/vector-search implementation between Postgres and
6
5
  * CockroachDB (wire- and SQL-compatible for everything below, including `TO_TSVECTOR`/`TO_TSQUERY`
@@ -23,28 +22,29 @@ export declare abstract class PgLikeSqlDialect extends AbstractSqlDialect {
23
22
  readonly rollbackTransactionCommand = "ROLLBACK";
24
23
  readonly alterColumnStrategy = "separate-clauses";
25
24
  /**
26
- * One `LATERAL` branch correlated against an array of the parent keys, in place of the base
27
- * `UNION ALL` of a subquery per parent. Same rows and the same `parents x (skip + limit)` read, but
28
- * the planner sees one correlated index loop rather than N branches to plan: flat in page size where
29
- * `UNION ALL` is linear, and the statement's text stops changing with the number of parents, so one
30
- * prepared statement serves every page.
31
- *
32
- * Postgres, CockroachDB, PGlite, Neon and bun-sql inherit it together. **MySQL has `LATERAL` and must
33
- * not use it** - it does not plan this as a correlated index loop and measured slower than both its
34
- * own `UNION ALL` and a query per parent, which is why this is an override rather than a capability
35
- * flag. [The design](../../../../architecture/populate-limits.md).
25
+ * `JSON_AGG` of each row whole, read through a LATERAL projection of the columns it answers under:
26
+ * the sort terms carried out beside them order it without joining the object, and no function call
27
+ * builds it, so a wide row meets no argument limit. `json` has no equality, so under a parent's
28
+ * `DISTINCT` the array is compared as `jsonb`.
36
29
  */
37
- protected appendPerParent<E extends object>(ctx: QueryContext, entity: Type<E>, q: Query<E>, { joins, parents, parentFields }: ParentPartition): void;
30
+ protected appendRelationArray(ctx: QueryContext, rows: RelationRows): void;
31
+ /**
32
+ * What JSON would round or cannot spell crosses it as text, for the field's own decode to read back:
33
+ * a column declared numeric, whatever it decodes as, a vector, and bytes as hex.
34
+ */
35
+ protected readonly carriedFields: {
36
+ numeric: (expr: string) => string;
37
+ vector: (expr: string) => string;
38
+ blob: (expr: string) => string;
39
+ };
38
40
  /** `$N` placeholders carry their own index, so the upsert's assignments need no scratch context. */
39
41
  protected readonly upsertUpdateBindsInPlace = true;
40
42
  readonly insertIdSource = "returning";
41
43
  readonly maxBindValues: number;
42
- /**
43
- * Each metric's pgvector distance operator and the operator-class suffix its index takes, in one
44
- * place so a dialect cannot end up with the operator but not the opclass. The key set is the single
45
- * source of truth for which metrics the dialect supports at all: CockroachDB narrows it to three.
46
- */
47
- readonly vectorMetrics: ReadonlyMap<VectorDistance, VectorOperatorMetric>;
44
+ readonly vectorMetrics: ReadonlyMap<import("../type/vector.js").VectorDistance, {
45
+ readonly op: string;
46
+ readonly opsSuffix: string;
47
+ }>;
48
48
  /** `SET LOCAL` applies to the enclosing transaction and to nothing at all without one. */
49
49
  readonly vectorTuningNeedsTransaction = true;
50
50
  /**
@@ -1,12 +1,11 @@
1
- import { canonicalToSql, fieldOptionsToCanonical } from '../schema/canonicalType.js';
2
1
  import { QueryRaw, } from '../type/index.js';
3
2
  import { hasVectorNear, textSearchFields } from '../util/dialect.util.js';
4
- import { raw } from '../util/raw.js';
5
- import { queryNarrowedTo } from '../util/relationQuery.util.js';
6
3
  import { escapeSingleQuotes } from '../util/sqlLiteral.js';
7
4
  import { AbstractSqlDialect } from './abstractSqlDialect.js';
8
- import { JSON_PULL_ALIAS, PER_PARENT_BRANCH_ALIAS, PER_PARENT_KEYS_ALIAS } from './aliases.js';
5
+ import { JSON_PULL_ALIAS, RELATION_ROW_ALIAS } from './aliases.js';
6
+ import { BYTES_PREFIX } from './hydrateColumn.js';
9
7
  import { jsonSetTarget } from './jsonSql.js';
8
+ import { PG_VECTOR_METRICS } from './pgVectorMetrics.js';
10
9
  import { resolveVectorCast, toSparsevecLiteral } from './vectorCast.js';
11
10
  /**
12
11
  * Shared AST/quoting/JSONB/full-text-search/vector-search implementation between Postgres and
@@ -52,56 +51,32 @@ export class PgLikeSqlDialect extends AbstractSqlDialect {
52
51
  rollbackTransactionCommand = 'ROLLBACK';
53
52
  alterColumnStrategy = 'separate-clauses';
54
53
  /**
55
- * One `LATERAL` branch correlated against an array of the parent keys, in place of the base
56
- * `UNION ALL` of a subquery per parent. Same rows and the same `parents x (skip + limit)` read, but
57
- * the planner sees one correlated index loop rather than N branches to plan: flat in page size where
58
- * `UNION ALL` is linear, and the statement's text stops changing with the number of parents, so one
59
- * prepared statement serves every page.
60
- *
61
- * Postgres, CockroachDB, PGlite, Neon and bun-sql inherit it together. **MySQL has `LATERAL` and must
62
- * not use it** - it does not plan this as a correlated index loop and measured slower than both its
63
- * own `UNION ALL` and a query per parent, which is why this is an override rather than a capability
64
- * flag. [The design](../../../../architecture/populate-limits.md).
54
+ * `JSON_AGG` of each row whole, read through a LATERAL projection of the columns it answers under:
55
+ * the sort terms carried out beside them order it without joining the object, and no function call
56
+ * builds it, so a wide row meets no argument limit. `json` has no equality, so under a parent's
57
+ * `DISTINCT` the array is compared as `jsonb`.
65
58
  */
66
- appendPerParent(ctx, entity, q, { joins, parents, parentFields }) {
67
- const keys = this.escapeId(ctx.nextAlias(PER_PARENT_KEYS_ALIAS));
68
- const branch = this.escapeId(ctx.nextAlias(PER_PARENT_BRANCH_ALIAS));
69
- const column = (index) => `${keys}.k${index}`;
70
- // `UNNEST` resolves an uncast parameter to `unknown` and refuses it ("function unnest(unknown) is
71
- // not unique"), so the array says its type. It comes from the parent's key column, which always
72
- // declares one, rather than the child's foreign key, which would have to be resolved through the
73
- // reference it takes its own type from.
74
- const sources = joins.map(({ parent }) => {
75
- const field = parentFields[parent];
76
- if (!field) {
77
- throw new TypeError(`cannot page a relation per parent: '${parent}' is not a field of the parent entity`);
78
- }
79
- const values = parents.map((it) => it[parent]);
80
- return `${this.addValue(ctx.values, values)}::${canonicalToSql(fieldOptionsToCanonical(field), this)}[]`;
81
- });
82
- const rowSource = `UNNEST(${sources.join(', ')}) AS ${keys}(${joins.map((_, index) => `k${index}`).join(', ')})`;
83
- // The keys come from the row source rather than as values, which is the whole point of correlating:
84
- // one branch, planned once, instead of one per parent.
85
- const correlated = Object.fromEntries(joins.map(({ joined }, index) => [joined, raw(({ ctx: inner }) => inner.append(column(index)))]));
86
- ctx.append(`SELECT ${branch}.* FROM ${rowSource} JOIN LATERAL (`);
87
- this.find(ctx, entity, queryNarrowedTo(q, correlated));
88
- ctx.append(`) ${branch} ON TRUE`);
59
+ appendRelationArray(ctx, rows) {
60
+ const { from, pairs, order } = this.derivedRelation(ctx, rows);
61
+ const row = this.escapeId(RELATION_ROW_ALIAS, true);
62
+ const columns = pairs.map(([, column]) => column).join(', ');
63
+ const array = /*sql*/ `(SELECT COALESCE(JSON_AGG(${row}${order ? ` ORDER BY ${order}` : ''}), '[]'::json) FROM ${from} CROSS JOIN LATERAL (SELECT ${columns}) ${row})`;
64
+ ctx.append(rows.distinct ? `${array}::jsonb` : array);
89
65
  }
66
+ /**
67
+ * What JSON would round or cannot spell crosses it as text, for the field's own decode to read back:
68
+ * a column declared numeric, whatever it decodes as, a vector, and bytes as hex.
69
+ */
70
+ carriedFields = {
71
+ numeric: (expr) => `${expr}::text`,
72
+ vector: (expr) => `${expr}::text`,
73
+ blob: (expr) => `${this.escape(BYTES_PREFIX)} || ENCODE(${expr}, 'hex')`,
74
+ };
90
75
  /** `$N` placeholders carry their own index, so the upsert's assignments need no scratch context. */
91
76
  upsertUpdateBindsInPlace = true;
92
77
  insertIdSource = 'returning';
93
78
  maxBindValues = 65535;
94
- /**
95
- * Each metric's pgvector distance operator and the operator-class suffix its index takes, in one
96
- * place so a dialect cannot end up with the operator but not the opclass. The key set is the single
97
- * source of truth for which metrics the dialect supports at all: CockroachDB narrows it to three.
98
- */
99
- vectorMetrics = new Map([
100
- ['cosine', { op: '<=>', opsSuffix: 'cosine' }],
101
- ['l2', { op: '<->', opsSuffix: 'l2' }],
102
- ['inner', { op: '<#>', opsSuffix: 'ip' }],
103
- ['l1', { op: '<+>', opsSuffix: 'l1' }],
104
- ]);
79
+ vectorMetrics = PG_VECTOR_METRICS;
105
80
  /** `SET LOCAL` applies to the enclosing transaction and to nothing at all without one. */
106
81
  vectorTuningNeedsTransaction = true;
107
82
  /**
@@ -0,0 +1,13 @@
1
+ import type { VectorDistance, VectorOperatorMetric } from '../type/index.js';
2
+ /**
3
+ * Each metric's pgvector distance operator and the operator-class suffix its index takes, in one list
4
+ * so a dialect cannot have the operator but not the opclass. Its own module because the two ends live
5
+ * apart: the operator on the dialect, the opclass in the migrator's index DDL.
6
+ */
7
+ export declare const PG_VECTOR_METRICS: ReadonlyMap<VectorDistance, VectorOperatorMetric>;
8
+ /**
9
+ * CockroachDB's three: `<+>` and `vector_l1_ops` answer "unimplemented: operator class ... is not
10
+ * supported" (verified live on v26.2), tracked at https://github.com/cockroachdb/cockroach/issues/147839.
11
+ * Re-check that issue before adding `l1`; it is omitted on purpose.
12
+ */
13
+ export declare const COCKROACH_VECTOR_METRICS: ReadonlyMap<VectorDistance, VectorOperatorMetric>;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Each metric's pgvector distance operator and the operator-class suffix its index takes, in one list
3
+ * so a dialect cannot have the operator but not the opclass. Its own module because the two ends live
4
+ * apart: the operator on the dialect, the opclass in the migrator's index DDL.
5
+ */
6
+ export const PG_VECTOR_METRICS = new Map([
7
+ ['cosine', { op: '<=>', opsSuffix: 'cosine' }],
8
+ ['l2', { op: '<->', opsSuffix: 'l2' }],
9
+ ['inner', { op: '<#>', opsSuffix: 'ip' }],
10
+ ['l1', { op: '<+>', opsSuffix: 'l1' }],
11
+ ]);
12
+ /**
13
+ * CockroachDB's three: `<+>` and `vector_l1_ops` answer "unimplemented: operator class ... is not
14
+ * supported" (verified live on v26.2), tracked at https://github.com/cockroachdb/cockroach/issues/147839.
15
+ * Re-check that issue before adding `l1`; it is omitted on purpose.
16
+ */
17
+ export const COCKROACH_VECTOR_METRICS = new Map([...PG_VECTOR_METRICS].filter(([metric]) => metric !== 'l1'));
@@ -11,14 +11,14 @@ export declare class SqlQueryContext implements QueryContext {
11
11
  private readonly statement?;
12
12
  private readonly sqlChunks;
13
13
  private readonly params;
14
- private aliasCounter;
14
+ private readonly tableAliases;
15
15
  /**
16
16
  * @param dialect The SQL dialect used to determine how values should be formatted as placeholders.
17
17
  * @param params An existing values array to bind into instead of a fresh one - shared by a
18
18
  * fragment context built via {@link AbstractSqlDialect.buildFragment}, so a bound value's
19
19
  * placeholder is numbered correctly against the real query from the moment it's added, rather
20
20
  * than needing to be reconciled after the fact.
21
- * @param statement The context this one renders a fragment of, which owns the alias counter: a
21
+ * @param statement The context this one renders a fragment of, which owns the claimed aliases: a
22
22
  * fragment is part of one statement, so its aliases have to be unique across the whole of it.
23
23
  */
24
24
  constructor(dialect: QueryDialect, params?: unknown[], statement?: SqlQueryContext | undefined);
@@ -46,11 +46,7 @@ export declare class SqlQueryContext implements QueryContext {
46
46
  * @returns The current context instance for method chaining.
47
47
  */
48
48
  pushValue(...values: unknown[]): this;
49
- /**
50
- * A fresh alias unique within the statement being built, e.g. `nextAlias('_uql_elem')` ->
51
- * `'_uql_elem_1'`, `'_uql_elem_2'`, ...
52
- */
53
- nextAlias(prefix: string): string;
49
+ claimAlias(name: string, parent?: string): string;
54
50
  /**
55
51
  * Returns the complete SQL query string by joining all accumulated chunks.
56
52
  */
@@ -10,14 +10,14 @@ export class SqlQueryContext {
10
10
  statement;
11
11
  sqlChunks = [];
12
12
  params;
13
- aliasCounter = 0;
13
+ tableAliases = new Set();
14
14
  /**
15
15
  * @param dialect The SQL dialect used to determine how values should be formatted as placeholders.
16
16
  * @param params An existing values array to bind into instead of a fresh one - shared by a
17
17
  * fragment context built via {@link AbstractSqlDialect.buildFragment}, so a bound value's
18
18
  * placeholder is numbered correctly against the real query from the moment it's added, rather
19
19
  * than needing to be reconciled after the fact.
20
- * @param statement The context this one renders a fragment of, which owns the alias counter: a
20
+ * @param statement The context this one renders a fragment of, which owns the claimed aliases: a
21
21
  * fragment is part of one statement, so its aliases have to be unique across the whole of it.
22
22
  */
23
23
  constructor(dialect, params = [], statement) {
@@ -62,12 +62,17 @@ export class SqlQueryContext {
62
62
  this.params.push(...values.map((v) => this.dialect.normalizeValue(v)));
63
63
  return this;
64
64
  }
65
- /**
66
- * A fresh alias unique within the statement being built, e.g. `nextAlias('_uql_elem')` ->
67
- * `'_uql_elem_1'`, `'_uql_elem_2'`, ...
68
- */
69
- nextAlias(prefix) {
70
- return this.statement ? this.statement.nextAlias(prefix) : `${prefix}_${++this.aliasCounter}`;
65
+ claimAlias(name, parent) {
66
+ if (this.statement) {
67
+ return this.statement.claimAlias(name, parent);
68
+ }
69
+ const reserved = parent?.toLowerCase();
70
+ let alias = name;
71
+ for (let n = 2; this.tableAliases.has(alias.toLowerCase()) || alias.toLowerCase() === reserved; n++) {
72
+ alias = `${name}_${n}`;
73
+ }
74
+ this.tableAliases.add(alias.toLowerCase());
75
+ return alias;
71
76
  }
72
77
  /**
73
78
  * Returns the complete SQL query string by joining all accumulated chunks.