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,4 +1,3 @@
1
- import { COCKROACH_VECTOR_METRICS, PG_VECTOR_METRICS } from '../../dialect/pgVectorMetrics.js';
2
1
  import { unsupportedVectorMetric } from '../../type/vector.js';
3
2
  import { IndexDdl } from './indexDdl.js';
4
3
  /** `$text` computes its `TO_TSVECTOR` per row, which no index over the raw columns serves. */
@@ -26,8 +25,6 @@ export class PgIndexDdl extends IndexDdl {
26
25
  'include',
27
26
  'jsonPath',
28
27
  ]);
29
- /** The metrics its vector index takes, each naming the operator class it is built with. */
30
- vectorMetrics = PG_VECTOR_METRICS;
31
28
  /** pgvector's own index types; CockroachDB's native one widens this. */
32
29
  isVectorIndex(index) {
33
30
  return index.type === 'hnsw' || index.type === 'ivfflat';
@@ -43,12 +40,12 @@ export class PgIndexDdl extends IndexDdl {
43
40
  if (!this.isVectorIndex(index) || !index.distance) {
44
41
  return entry.opsClass ? ` ${entry.opsClass}` : '';
45
42
  }
46
- const metric = this.vectorMetrics.get(index.distance);
43
+ const metric = this.dialect.vectorMetrics.get(index.distance)?.index;
47
44
  if (!metric) {
48
45
  throw unsupportedVectorMetric(this.dialect.dialectName, index.distance, index.name);
49
46
  }
50
47
  const vectorType = this.dialect.supportedVectorType(index.vectorType ?? 'vector');
51
- const opsClass = `${vectorType}_${metric.opsSuffix}_ops`;
48
+ const opsClass = `${vectorType}_${metric}_ops`;
52
49
  // IVFFlat has neither a sparsevec nor an L1 operator class; HNSW has all of them (pgvector 0.8.2).
53
50
  if (index.type === 'ivfflat' && (vectorType === 'sparsevec' || index.distance === 'l1')) {
54
51
  throw new TypeError(`ivfflat has no ${opsClass} operator class (index "${index.name}"); use hnsw`);
@@ -83,7 +80,6 @@ export class CockroachIndexDdl extends PgIndexDdl {
83
80
  ...PG_INDEX_TYPE_HINTS,
84
81
  ['ivfflat', "; declare type: 'vector' instead"],
85
82
  ]);
86
- vectorMetrics = COCKROACH_VECTOR_METRICS;
87
83
  isNativeVectorIndex(index) {
88
84
  return index.type === 'vector';
89
85
  }
@@ -12,6 +12,15 @@ import { BaseSqlIntrospector } from './baseSqlIntrospector.js';
12
12
  * it is four avoidable ones.
13
13
  */
14
14
  export type TableRowReader = <T extends RawRow>(sql: string, params?: unknown[]) => Promise<T[]>;
15
+ /** A foreign key as MySQL and SQL Server list one: a row, its column lists comma-joined. */
16
+ export type JoinedForeignKeyRow = {
17
+ readonly constraint_name: string;
18
+ readonly columns: string;
19
+ readonly referenced_table: string;
20
+ readonly referenced_columns: string;
21
+ readonly delete_rule: string;
22
+ readonly update_rule: string;
23
+ };
15
24
  /** A SQL introspector: an engine states its catalogue queries (`get*Query`) and how their rows map (`map*Result`). */
16
25
  export declare abstract class AbstractSqlSchemaIntrospector extends BaseSqlIntrospector implements SchemaIntrospector {
17
26
  protected readonly pool: QuerierPool;
@@ -48,8 +57,10 @@ export declare abstract class AbstractSqlSchemaIntrospector extends BaseSqlIntro
48
57
  protected getIndexesParams(tableName: string): unknown[];
49
58
  protected getForeignKeysParams(tableName: string): unknown[];
50
59
  protected getPrimaryKeyParams(tableName: string): unknown[];
51
- /** The {@link ForeignKeyAction} a catalogue names, whatever its case. */
60
+ /** The {@link ForeignKeyAction} a catalogue names, whatever its case, and `SET_NULL` as SQL Server spells it. */
52
61
  protected normalizeReferentialAction(action: string): ForeignKeyAction | undefined;
62
+ /** Foreign keys read one row each, as {@link JoinedForeignKeyRow} lists them. */
63
+ protected joinedForeignKeys(rows: readonly JoinedForeignKeyRow[]): ForeignKeySchema[];
53
64
  /**
54
65
  * Convert bigint/null values to number safely.
55
66
  */
@@ -100,10 +100,20 @@ export class AbstractSqlSchemaIntrospector extends BaseSqlIntrospector {
100
100
  getPrimaryKeyParams(tableName) {
101
101
  return [tableName];
102
102
  }
103
- /** The {@link ForeignKeyAction} a catalogue names, whatever its case. */
103
+ /** The {@link ForeignKeyAction} a catalogue names, whatever its case, and `SET_NULL` as SQL Server spells it. */
104
104
  normalizeReferentialAction(action) {
105
- const upper = action.toUpperCase();
106
- return FOREIGN_KEY_ACTIONS.find((known) => known === upper);
105
+ const spelled = action.toUpperCase().replaceAll('_', ' ');
106
+ return FOREIGN_KEY_ACTIONS.find((known) => known === spelled);
107
+ }
108
+ /** Foreign keys read one row each, as {@link JoinedForeignKeyRow} lists them. */
109
+ joinedForeignKeys(rows) {
110
+ return rows.map((row) => ({
111
+ name: row.constraint_name,
112
+ columns: row.columns.split(','),
113
+ references: { table: row.referenced_table, columns: row.referenced_columns.split(',') },
114
+ onDelete: this.normalizeReferentialAction(row.delete_rule),
115
+ onUpdate: this.normalizeReferentialAction(row.update_rule),
116
+ }));
107
117
  }
108
118
  /**
109
119
  * Convert bigint/null values to number safely.
@@ -1,5 +1,5 @@
1
1
  import type { ColumnSchema, ForeignKeySchema, IndexSchema } from '../../type/index.js';
2
- import { AbstractSqlSchemaIntrospector, type TableRowReader } from './abstractSqlSchemaIntrospector.js';
2
+ import { AbstractSqlSchemaIntrospector, type JoinedForeignKeyRow, type TableRowReader } from './abstractSqlSchemaIntrospector.js';
3
3
  /**
4
4
  * SQL Server schema introspector.
5
5
  *
@@ -31,14 +31,7 @@ export declare class MsSqlSchemaIntrospector extends AbstractSqlSchemaIntrospect
31
31
  columns: string;
32
32
  is_unique: boolean;
33
33
  }[]): Promise<IndexSchema[]>;
34
- protected mapForeignKeysResult(_read: TableRowReader, _tableName: string, results: {
35
- constraint_name: string;
36
- columns: string;
37
- referenced_table: string;
38
- referenced_columns: string;
39
- delete_rule: string;
40
- update_rule: string;
41
- }[]): Promise<ForeignKeySchema[]>;
34
+ protected mapForeignKeysResult(_read: TableRowReader, _tableName: string, results: JoinedForeignKeyRow[]): Promise<ForeignKeySchema[]>;
42
35
  /**
43
36
  * The engine reprints a default from its own parse tree: all of it in one pair of parentheses, and a
44
37
  * number in a second, so `((0))`, `((1)+(2))`, `(N'x')` and `(getdate())`.
@@ -1,4 +1,4 @@
1
- import { AbstractSqlSchemaIntrospector } from './abstractSqlSchemaIntrospector.js';
1
+ import { AbstractSqlSchemaIntrospector, } from './abstractSqlSchemaIntrospector.js';
2
2
  /**
3
3
  * SQL Server schema introspector.
4
4
  *
@@ -146,14 +146,7 @@ export class MsSqlSchemaIntrospector extends AbstractSqlSchemaIntrospector {
146
146
  }));
147
147
  }
148
148
  async mapForeignKeysResult(_read, _tableName, results) {
149
- return results.map((row) => ({
150
- name: row.constraint_name,
151
- columns: row.columns.split(','),
152
- references: { table: row.referenced_table, columns: row.referenced_columns.split(',') },
153
- // `sys` spells them with an underscore: `SET_NULL`, `NO_ACTION`.
154
- onDelete: this.normalizeReferentialAction(row.delete_rule.replaceAll('_', ' ')),
155
- onUpdate: this.normalizeReferentialAction(row.update_rule.replaceAll('_', ' ')),
156
- }));
149
+ return this.joinedForeignKeys(results);
157
150
  }
158
151
  /**
159
152
  * The engine reprints a default from its own parse tree: all of it in one pair of parentheses, and a
@@ -1,5 +1,5 @@
1
1
  import type { ColumnSchema, ForeignKeySchema, IndexSchema } from '../../type/index.js';
2
- import { AbstractSqlSchemaIntrospector, type TableRowReader } from './abstractSqlSchemaIntrospector.js';
2
+ import { AbstractSqlSchemaIntrospector, type JoinedForeignKeyRow, type TableRowReader } from './abstractSqlSchemaIntrospector.js';
3
3
  /**
4
4
  * MySQL/MariaDB schema introspector.
5
5
  * Works with both MySQL and MariaDB as they share the same information_schema structure.
@@ -21,14 +21,7 @@ export declare class MysqlSchemaIntrospector extends AbstractSqlSchemaIntrospect
21
21
  columns: string;
22
22
  is_unique: number;
23
23
  }[]): Promise<IndexSchema[]>;
24
- protected mapForeignKeysResult(_read: TableRowReader, _tableName: string, results: {
25
- constraint_name: string;
26
- columns: string;
27
- referenced_table: string;
28
- referenced_columns: string;
29
- delete_rule: string;
30
- update_rule: string;
31
- }[]): Promise<ForeignKeySchema[]>;
24
+ protected mapForeignKeysResult(_read: TableRowReader, _tableName: string, results: JoinedForeignKeyRow[]): Promise<ForeignKeySchema[]>;
32
25
  /**
33
26
  * MariaDB prints a string default as the literal it is (`'it''s'`). MySQL prints one bare, save an
34
27
  * expression default (`DEFAULT ('x')`, what a `TEXT` column takes), which comes with a charset
@@ -1,5 +1,5 @@
1
1
  import { unescapeMysqlString } from '../../util/sqlLiteral.js';
2
- import { AbstractSqlSchemaIntrospector } from './abstractSqlSchemaIntrospector.js';
2
+ import { AbstractSqlSchemaIntrospector, } from './abstractSqlSchemaIntrospector.js';
3
3
  /**
4
4
  * MySQL/MariaDB schema introspector.
5
5
  * Works with both MySQL and MariaDB as they share the same information_schema structure.
@@ -117,13 +117,7 @@ export class MysqlSchemaIntrospector extends AbstractSqlSchemaIntrospector {
117
117
  }));
118
118
  }
119
119
  async mapForeignKeysResult(_read, _tableName, results) {
120
- return results.map((row) => ({
121
- name: row.constraint_name,
122
- columns: row.columns.split(','),
123
- references: { table: row.referenced_table, columns: row.referenced_columns.split(',') },
124
- onDelete: this.normalizeReferentialAction(row.delete_rule),
125
- onUpdate: this.normalizeReferentialAction(row.update_rule),
126
- }));
120
+ return this.joinedForeignKeys(results);
127
121
  }
128
122
  /**
129
123
  * MariaDB prints a string default as the literal it is (`'it''s'`). MySQL prints one bare, save an
@@ -55,12 +55,20 @@ export declare class MongoDialect extends AbstractDialect {
55
55
  */
56
56
  private appendLogicalOperator;
57
57
  /**
58
- * Emits the correlated `$lookup` for one relation condition and returns the condition that tests its
59
- * result: presence of a row for a plain relation filter, a comparison against the row count for
58
+ * Emits the correlated `$lookup` for one relation condition, and adds to `filter` the condition testing
59
+ * its result: presence of a row for a plain relation filter, a comparison against the row count for
60
60
  * `$size`. The target's (and, for ManyToMany, the junction's) own filters scope the lookup, so a
61
61
  * relation subquery can no more read out-of-scope rows than a direct query on the target can.
62
62
  */
63
63
  private appendRelationLookup;
64
+ /** Adds `expr` to `filter`'s `$expr`, `AND`ed with any already there. */
65
+ private static andExpr;
66
+ /**
67
+ * `$size` against bounds, which MongoDB's own `$size` takes only as a number: the array at `path` counted
68
+ * in an `$expr`, which no other value satisfies. `$and` may evaluate every operand, so the count reads an
69
+ * empty array in place of any other value.
70
+ */
71
+ private static arraySize;
64
72
  /**
65
73
  * The correlated `$lookup` for the target rows of one relation `where` narrows, as `temp`: straight at
66
74
  * the target, or for a many-to-many from inside its junction's rows. The caller's filter bypass is not
@@ -74,11 +82,8 @@ export declare class MongoDialect extends AbstractDialect {
74
82
  * Each end is one field matched against one `_id`, so both sides must be sole-keyed.
75
83
  */
76
84
  private junctionOf;
77
- /**
78
- * Compares the looked-up row count, which is `[{ n: <count> }]` or `[]` when nothing matched - hence
79
- * the `$ifNull` fallback to 0, so `{ $size: 0 }` matches parents with no related row at all.
80
- */
81
- private compareRelationCount;
85
+ /** `count` compared with `size`, a number or its bounds, as an aggregation expression. */
86
+ private static compareCount;
82
87
  /** Whether a query subtracts `key` from the projection, via `$exclude` or a negative `$select`. */
83
88
  private subtractsKey;
84
89
  /**
@@ -104,10 +109,17 @@ export declare class MongoDialect extends AbstractDialect {
104
109
  */
105
110
  private transformOperators;
106
111
  /**
107
- * Maps the conditions inside `$elemMatch`: an operator map applies to the element itself, anything
108
- * else is a per-field map whose operator objects each need mapping.
112
+ * What a value holding `value` matches, as the SQL engines read it: an operator map tests it, an array
113
+ * holds each element by `$all`, an object each key by {@link containment}, and a scalar is equal.
114
+ */
115
+ private held;
116
+ /**
117
+ * `$all` as one `$elemMatch` per value, since native `$all` never looks into an element that is an array:
118
+ * an array element holds each of its values alike, an object or operator map as {@link held} reads it.
109
119
  */
110
- private transformElemMatch;
120
+ private allHolding;
121
+ /** An object's keys as {@link held} reads each, a nested object's by its dotted path rather than whole. */
122
+ private containment;
111
123
  select<E extends Document>(entity: Type<E>, select?: QuerySelectValue<E>, exclude?: QueryExclude<E>): Record<string, 0 | 1>;
112
124
  /**
113
125
  * The `$sort` stage. A relation key reads the document a `$lookup` unwound onto the parent, so - as
@@ -5,7 +5,7 @@ import { resolveQueryJoins, resolveSortableJoin } from '../dialect/queryJoins.js
5
5
  import { assertSoleId, fieldOf, getMeta, relationOf, soleIdOf } from '../entity/index.js';
6
6
  import { COUNT_RESULT_KEY } from '../type/query.js';
7
7
  import { QueryRaw } from '../type/queryRaw.js';
8
- import { asSelectMap, assertAggregateColumns, assertNonNegativeInteger, columnFamily, countedRelations, entityName, fillOnFields, filterFieldKeys, findVectorIndex, findVectorSort, getKeys, getRelationRequestSummary, hasKeys, isJsonUpdateOp, isOperatorMap, isOperatorObject, isVectorSearch, normalizeScalarFieldSelection, parentJoins, parseGroupMap, parseRelationAtKey, parseRelationSize, parseSortByCount, someKey, targetKeyColumns, } from '../util/index.js';
8
+ import { asSelectMap, assertAggregateColumns, assertNonNegativeInteger, columnFamily, countedRelations, entityName, fillOnFields, filterFieldKeys, findVectorIndex, findVectorSort, getKeys, getRelationRequestSummary, hasKeys, isJsonObject, isJsonUpdateOp, isOperatorMap, isOperatorObject, isRecord, isVectorSearch, normalizeScalarFieldSelection, parentJoins, parseGroupMap, parseRelationAtKey, parseRelationSize, parseSortByCount, someKey, targetKeyColumns, } from '../util/index.js';
9
9
  /** Default {@link DialectFeatures} for MongoDB. */
10
10
  export const mongoDialectFeatures = {
11
11
  ifNotExists: false,
@@ -105,7 +105,7 @@ export class MongoDialect extends AbstractDialect {
105
105
  if (!lookups) {
106
106
  throw new TypeError(`filtering by relation '${key}' is not supported here on MongoDB`);
107
107
  }
108
- Object.assign(filter, this.appendRelationLookup(meta, key, val, lookups));
108
+ this.appendRelationLookup(filter, meta, key, val, lookups);
109
109
  }
110
110
  else {
111
111
  this.assertNoRaw(val);
@@ -115,13 +115,21 @@ export class MongoDialect extends AbstractDialect {
115
115
  if ((key === MongoDialect.ID_KEY || isReference) && !isOperatorObject(val)) {
116
116
  val = this.toWireId(val);
117
117
  }
118
- if (isOperatorObject(val)) {
119
- val = this.transformOperators(val);
118
+ if (!isOperatorObject(val)) {
119
+ filter[key] = Array.isArray(val) ? { $in: val } : val;
120
+ continue;
121
+ }
122
+ // MongoDB's `$size` takes only a number, so bounds become an `$expr` beside the other operators.
123
+ const { $size: size, ...ops } = val;
124
+ if (!isRecord(size)) {
125
+ filter[key] = this.transformOperators(val);
120
126
  }
121
- else if (Array.isArray(val)) {
122
- val = { $in: val };
127
+ else {
128
+ MongoDialect.andExpr(filter, MongoDialect.arraySize(key, size));
129
+ if (hasKeys(ops)) {
130
+ filter[key] = this.transformOperators(ops);
131
+ }
123
132
  }
124
- filter[key] = val;
125
133
  }
126
134
  }
127
135
  return filter;
@@ -149,21 +157,38 @@ export class MongoDialect extends AbstractDialect {
149
157
  filter['$nor'] = [...(filter['$nor'] ?? []), ...negated];
150
158
  }
151
159
  /**
152
- * Emits the correlated `$lookup` for one relation condition and returns the condition that tests its
153
- * result: presence of a row for a plain relation filter, a comparison against the row count for
160
+ * Emits the correlated `$lookup` for one relation condition, and adds to `filter` the condition testing
161
+ * its result: presence of a row for a plain relation filter, a comparison against the row count for
154
162
  * `$size`. The target's (and, for ManyToMany, the junction's) own filters scope the lookup, so a
155
163
  * relation subquery can no more read out-of-scope rows than a direct query on the target can.
156
164
  */
157
- appendRelationLookup(meta, relKey, val, lookups) {
165
+ appendRelationLookup(filter, meta, relKey, val, lookups) {
158
166
  const temp = `${REL_TEMP_PREFIX}${lookups.temps.length}`;
159
167
  const sizeVal = parseRelationSize(val);
160
168
  const tail = sizeVal === undefined ? [{ $limit: 1 }] : [{ $count: COUNT_ALIAS }];
161
169
  const where = (sizeVal === undefined ? val : {});
162
170
  lookups.temps.push(temp);
163
171
  lookups.stages.push(this.relationLookup(meta, meta.relations[relKey], where, temp, tail));
164
- return sizeVal === undefined
165
- ? { [`${temp}.0`]: { $exists: true } }
166
- : { $expr: this.compareRelationCount(temp, sizeVal) };
172
+ if (sizeVal === undefined) {
173
+ filter[`${temp}.0`] = { $exists: true };
174
+ }
175
+ else {
176
+ MongoDialect.andExpr(filter, MongoDialect.compareCount(this.tally(temp), sizeVal));
177
+ }
178
+ }
179
+ /** Adds `expr` to `filter`'s `$expr`, `AND`ed with any already there. */
180
+ static andExpr(filter, expr) {
181
+ filter['$expr'] = filter['$expr'] ? { $and: [filter['$expr'], expr] } : expr;
182
+ }
183
+ /**
184
+ * `$size` against bounds, which MongoDB's own `$size` takes only as a number: the array at `path` counted
185
+ * in an `$expr`, which no other value satisfies. `$and` may evaluate every operand, so the count reads an
186
+ * empty array in place of any other value.
187
+ */
188
+ static arraySize(path, size) {
189
+ const value = `$${path}`;
190
+ const count = { $size: { $cond: [{ $isArray: value }, value, []] } };
191
+ return { $and: [{ $isArray: value }, MongoDialect.compareCount(count, size)] };
167
192
  }
168
193
  /**
169
194
  * The correlated `$lookup` for the target rows of one relation `where` narrows, as `temp`: straight at
@@ -222,22 +247,18 @@ export class MongoDialect extends AbstractDialect {
222
247
  target: this.columnOf(throughMeta, targetColumn),
223
248
  };
224
249
  }
225
- /**
226
- * Compares the looked-up row count, which is `[{ n: <count> }]` or `[]` when nothing matched - hence
227
- * the `$ifNull` fallback to 0, so `{ $size: 0 }` matches parents with no related row at all.
228
- */
229
- compareRelationCount(temp, sizeVal) {
230
- const count = this.tally(temp);
231
- if (typeof sizeVal === 'number') {
232
- return { $eq: [count, sizeVal] };
250
+ /** `count` compared with `size`, a number or its bounds, as an aggregation expression. */
251
+ static compareCount(count, size) {
252
+ if (typeof size === 'number') {
253
+ return { $eq: [count, size] };
233
254
  }
234
- const comparisons = Object.entries(sizeVal)
255
+ const comparisons = Object.entries(size)
235
256
  .filter(([, bound]) => bound !== undefined)
236
- .flatMap(([op, bound]) => op === '$between'
257
+ .flatMap(([op, bound]) => op === '$between' && Array.isArray(bound)
237
258
  ? [{ $gte: [count, bound[0]] }, { $lte: [count, bound[1]] }]
238
259
  : [{ [op]: [count, bound] }]);
239
260
  if (!comparisons.length) {
240
- throw new TypeError('$size on a relation needs at least one comparison');
261
+ throw new TypeError('$size needs at least one comparison');
241
262
  }
242
263
  return comparisons.length === 1 ? comparisons[0] : { $and: comparisons };
243
264
  }
@@ -311,7 +332,13 @@ export class MongoDialect extends AbstractDialect {
311
332
  // mapping - passing it through raw sends UQL-only operators (`$startsWith`, `$between`, ...)
312
333
  // straight to the server, which rejects them as unknown.
313
334
  if (op === '$elemMatch') {
314
- result[op] = this.transformElemMatch(val);
335
+ result[op] = this.held(val);
336
+ continue;
337
+ }
338
+ // An object or an array is matched by what it holds, as the SQL engines read it, where native `$all`
339
+ // compares the whole element. MongoDB takes `$elemMatch` there only when every value is one.
340
+ if (op === '$all' && Array.isArray(val) && val.some((value) => Array.isArray(value) || isOperatorMap(value))) {
341
+ result[op] = this.allHolding(val);
315
342
  continue;
316
343
  }
317
344
  // Native MongoDB operators - pass through directly
@@ -354,14 +381,36 @@ export class MongoDialect extends AbstractDialect {
354
381
  return result;
355
382
  }
356
383
  /**
357
- * Maps the conditions inside `$elemMatch`: an operator map applies to the element itself, anything
358
- * else is a per-field map whose operator objects each need mapping.
384
+ * What a value holding `value` matches, as the SQL engines read it: an operator map tests it, an array
385
+ * holds each element by `$all`, an object each key by {@link containment}, and a scalar is equal.
359
386
  */
360
- transformElemMatch(match) {
361
- if (isOperatorObject(match)) {
362
- return this.transformOperators(match);
387
+ held(value) {
388
+ if (Array.isArray(value)) {
389
+ return this.transformOperators({ $all: value });
363
390
  }
364
- return Object.fromEntries(Object.entries(match).map(([field, val]) => [field, isOperatorObject(val) ? this.transformOperators(val) : val]));
391
+ if (!isOperatorMap(value)) {
392
+ return value;
393
+ }
394
+ return isOperatorObject(value) ? this.transformOperators(value) : this.containment(value);
395
+ }
396
+ /**
397
+ * `$all` as one `$elemMatch` per value, since native `$all` never looks into an element that is an array:
398
+ * an array element holds each of its values alike, an object or operator map as {@link held} reads it.
399
+ */
400
+ allHolding(values) {
401
+ return values.map((value) => {
402
+ if (Array.isArray(value)) {
403
+ return { $elemMatch: { $all: this.allHolding(value) } };
404
+ }
405
+ return { $elemMatch: isOperatorMap(value) ? this.held(value) : { $eq: value } };
406
+ });
407
+ }
408
+ /** An object's keys as {@link held} reads each, a nested object's by its dotted path rather than whole. */
409
+ containment(object, prefix = '') {
410
+ return Object.fromEntries(Object.entries(object).flatMap(([key, value]) => {
411
+ const path = prefix ? `${prefix}.${key}` : key;
412
+ return isJsonObject(value) ? Object.entries(this.containment(value, path)) : [[path, this.held(value)]];
413
+ }));
365
414
  }
366
415
  select(entity, select, exclude) {
367
416
  const meta = getMeta(entity);
@@ -832,16 +881,15 @@ export class MongoDialect extends AbstractDialect {
832
881
  const groups = this.groupUpdateOperators(persistable);
833
882
  const { set, push, pull, unset } = groups;
834
883
  const exprKeys = [...Object.keys(pull), ...Object.keys(set), ...Object.keys(push)];
835
- // MongoDB rejects two operators targeting one path in a single update document, so any path
836
- // reached by more than one operator group forces the pipeline form.
884
+ // Native `$pull` fails on a value that is no array, and MongoDB rejects two operators targeting one
885
+ // path in a single update document: either forces the pipeline form.
837
886
  const allPaths = [...exprKeys, ...unset];
838
- if (new Set(allPaths).size < allPaths.length) {
887
+ if (hasKeys(pull) || new Set(allPaths).size < allPaths.length) {
839
888
  return this.getUpdatePipeline(groups, new Set(exprKeys));
840
889
  }
841
890
  return {
842
891
  ...(hasKeys(set) && { $set: set }),
843
892
  ...(hasKeys(push) && { $push: push }),
844
- ...(hasKeys(pull) && { $pull: pull }),
845
893
  ...(unset.size > 0 && { $unset: Object.fromEntries([...unset].map((path) => [path, ''])) }),
846
894
  };
847
895
  }
@@ -862,9 +910,9 @@ export class MongoDialect extends AbstractDialect {
862
910
  if (path in push) {
863
911
  expr = { $concatArrays: [expr, [{ $literal: push[path] }]] };
864
912
  }
865
- // Only `$set` and `$push` create a key. A `$pull` alone has to leave an absent one absent, and
866
- // `$$REMOVE` is how a pipeline `$set` skips a field - without it the filter would store `[]`.
867
- assignments[path] = path in set || path in push ? expr : { $cond: [{ $isArray: `$${path}` }, expr, '$$REMOVE'] };
913
+ // Only `$set` and `$push` create a key. A `$pull` alone leaves any value that is no array as it is,
914
+ // and an absent one absent, since a pipeline `$set` of a missing field adds none.
915
+ assignments[path] = path in set || path in push ? expr : { $cond: [{ $isArray: `$${path}` }, expr, `$${path}`] };
868
916
  }
869
917
  return [{ $set: assignments }, ...(unset.size > 0 ? [{ $unset: [...unset] }] : [])];
870
918
  }
@@ -1,6 +1,7 @@
1
1
  import { type RelationRows } from '../dialect/abstractSqlDialect.js';
2
+ import { type JsonAccessMode, type JsonSlot } from '../dialect/jsonSql.js';
2
3
  import { MergeSqlDialect } from '../dialect/mergeSqlDialect.js';
3
- import type { EntityMeta, FieldOptions, InsertIdSource, Query, QueryContext, QueryOptions, QueryPager, QuerySizeComparisonOps, SqlDialectFeatures, Type, VectorDistance, VectorMetric } from '../type/index.js';
4
+ import type { EntityMeta, FieldOptions, InsertIdSource, Query, QueryContext, QueryOptions, QueryPager, SqlDialectFeatures, Type, VectorDistance, VectorMetric } from '../type/index.js';
4
5
  /** Microsoft SQL Server 2017 and up. Identifiers are `"`-quoted, the ANSI spelling `tedious` enables. */
5
6
  export declare class MsSqlDialect extends MergeSqlDialect {
6
7
  #private;
@@ -103,18 +104,11 @@ export declare class MsSqlDialect extends MergeSqlDialect {
103
104
  estimatedCount<E>(ctx: QueryContext, entity: Type<E>): void;
104
105
  protected numericCast(expr: string): string;
105
106
  /**
106
- * `JSON_VALUE` returns `NVARCHAR(4000)` and, in the lax mode that is the default, answers NULL
107
- * rather than erroring for anything longer - so a long string read through it disappears without a
108
- * word. `OPENJSON` has no such bound, so the path is split and its last segment matched as a key.
107
+ * `OPENJSON` at the path's parent, matching its last segment as a key, in either reading: `JSON_VALUE`
108
+ * answers NULL for text past 4000 characters, and `JSON_QUERY` for a scalar. A value reads back as
109
+ * text, which {@link jsonScalarParam} binds its operand as, and an array or object as its own JSON.
109
110
  */
110
- protected getJsonPathScalarExpr(escapedColumn: string, jsonPathStr: string): string;
111
- /**
112
- * The same read as the scalar one. `JSON_QUERY` answers NULL for anything that is not an object or
113
- * an array, so it cannot serve the JSON access mode a boolean or a number operand asks for -
114
- * `OPENJSON` returns both as text, and {@link jsonScalarParam} binds the operand as the matching
115
- * text. An array or object comes back as its own JSON text, which is what `OPENJSON` takes next.
116
- */
117
- protected getJsonPathJsonbExpr(escapedColumn: string, jsonPathStr: string): string;
111
+ protected jsonPathReading(escapedColumn: string, path: string): string;
118
112
  /**
119
113
  * A value compared against a JSON path, which reads back as text, so only a boolean needs spelling as
120
114
  * `'true'`; SQL Server has no cast that parses text as JSON.
@@ -126,11 +120,20 @@ export declare class MsSqlDialect extends MergeSqlDialect {
126
120
  * flattened a boolean to 1/0 for this engine's columns, so the cast is what restores it.
127
121
  */
128
122
  protected jsonWriteParam(ctx: QueryContext, value: unknown): string;
129
- protected jsonElemFrom(jsonField: string, _fields: readonly string[], alias: string): string;
130
- /** `JSON_VALUE`'s 4000-character bound applies to an element's field, unlike a whole column. */
131
- protected jsonElemRef(alias: string, field?: string): string;
132
- protected jsonAll(ctx: QueryContext, jsonField: string, value: unknown): string;
133
- protected jsonSize(ctx: QueryContext, jsonField: string, value: number | QuerySizeComparisonOps): string;
123
+ protected jsonElemFrom(slot: JsonSlot, alias: string): string;
124
+ /** `JSON_QUERY` answers an array or an object as written, and a scalar as NULL. */
125
+ protected jsonIsArray(slot: JsonSlot): string;
126
+ /** An object element's `value` is its JSON text, which its fields are paths into. */
127
+ protected jsonElemDoc(alias: string): string;
128
+ /**
129
+ * An element of the value's own JSON type: `OPENJSON` reads a string and a number back as the same text,
130
+ * so the `type` it reports is what tells `'5'` from `5`. A number compares by value, cast on both sides:
131
+ * text against a numeric parameter converts implicitly, which throws on an element that is no number.
132
+ */
133
+ protected jsonElemEquals(ctx: QueryContext, _slot: JsonSlot, alias: string, value: unknown): string;
134
+ protected jsonLength(slot: JsonSlot): string;
135
+ /** SQL Server orders JSON as the text `OPENJSON` reads, so a number sorts by its value first. */
136
+ protected readonly jsonSortModes: readonly JsonAccessMode[];
134
137
  /** `JSON_MODIFY` takes one path per call, so several keys chain into one expression. */
135
138
  protected jsonSet(ctx: QueryContext, expr: string, set: Record<string, unknown>, _field?: FieldOptions): string;
136
139
  /** `'append '` prefixing the path extends the array there, creating it where there is none. */
@@ -139,7 +142,8 @@ export declare class MsSqlDialect extends MergeSqlDialect {
139
142
  protected jsonUnset(_ctx: QueryContext, expr: string, unset: readonly string[]): string;
140
143
  /**
141
144
  * The surviving elements are re-aggregated into an array and written back whole - there is no
142
- * remove-by-value. `JSON_QUERY` is what marks the rebuilt text as JSON rather than a string.
145
+ * remove-by-value. `JSON_QUERY` is what marks the rebuilt text as JSON rather than a string. Only an
146
+ * array is rewritten: `JSON_MODIFY` would create an absent key, and any other value stays as it is.
143
147
  */
144
148
  protected jsonPullKey(ctx: QueryContext, expr: string, escapedCol: string, key: string, value: unknown): string;
145
149
  }