uql-orm 0.67.0 → 0.68.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/browser/uql-browser.min.js +2 -2
  2. package/dist/browser/uql-browser.min.js.map +3 -3
  3. package/dist/cockroachdb/cockroachDialect.d.ts +6 -4
  4. package/dist/cockroachdb/cockroachDialect.js +7 -3
  5. package/dist/d1/d1SqliteDialect.d.ts +1 -1
  6. package/dist/d1/d1SqliteDialect.js +1 -1
  7. package/dist/dialect/abstractSqlDialect.d.ts +107 -97
  8. package/dist/dialect/abstractSqlDialect.js +250 -293
  9. package/dist/dialect/hydrateColumn.js +33 -30
  10. package/dist/dialect/jsonSql.d.ts +32 -23
  11. package/dist/dialect/jsonSql.js +42 -31
  12. package/dist/dialect/mysqlLikeSqlDialect.d.ts +19 -23
  13. package/dist/dialect/mysqlLikeSqlDialect.js +34 -51
  14. package/dist/dialect/pgLikeSqlDialect.d.ts +24 -17
  15. package/dist/dialect/pgLikeSqlDialect.js +53 -26
  16. package/dist/dialect/vectorCast.d.ts +2 -0
  17. package/dist/dialect/vectorCast.js +25 -5
  18. package/dist/dialect/vectorSqlDialect.d.ts +8 -8
  19. package/dist/dialect/vectorSqlDialect.js +11 -11
  20. package/dist/index.d.ts +1 -0
  21. package/dist/maria/mariaDialect.d.ts +16 -11
  22. package/dist/maria/mariaDialect.js +23 -19
  23. package/dist/migrate/ddl/indexDdl.d.ts +3 -1
  24. package/dist/migrate/ddl/indexDdl.js +5 -1
  25. package/dist/migrate/ddl/mysqlIndexDdl.d.ts +7 -2
  26. package/dist/migrate/ddl/mysqlIndexDdl.js +28 -6
  27. package/dist/migrate/ddl/pgIndexDdl.d.ts +0 -9
  28. package/dist/migrate/ddl/pgIndexDdl.js +2 -6
  29. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.d.ts +12 -1
  30. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +13 -3
  31. package/dist/migrate/introspection/mssqlIntrospector.d.ts +2 -9
  32. package/dist/migrate/introspection/mssqlIntrospector.js +2 -9
  33. package/dist/migrate/introspection/mysqlIntrospector.d.ts +2 -9
  34. package/dist/migrate/introspection/mysqlIntrospector.js +2 -8
  35. package/dist/mongo/mongoDialect.d.ts +22 -10
  36. package/dist/mongo/mongoDialect.js +86 -38
  37. package/dist/mssql/mssqlDialect.d.ts +22 -18
  38. package/dist/mssql/mssqlDialect.js +54 -43
  39. package/dist/mysql/mysqlDialect.d.ts +16 -1
  40. package/dist/mysql/mysqlDialect.js +18 -2
  41. package/dist/sqlite/sqliteDialect.d.ts +17 -16
  42. package/dist/sqlite/sqliteDialect.js +35 -39
  43. package/dist/type/dialect.d.ts +0 -4
  44. package/dist/type/entity.d.ts +8 -3
  45. package/dist/type/vector.d.ts +5 -7
  46. package/dist/util/dialect.util.d.ts +2 -0
  47. package/dist/util/dialect.util.js +6 -2
  48. package/dist/util/object.util.d.ts +2 -4
  49. package/dist/util/object.util.js +4 -9
  50. package/package.json +1 -1
  51. package/dist/dialect/jsonArrayElemMatchUtils.d.ts +0 -2
  52. package/dist/dialect/jsonArrayElemMatchUtils.js +0 -7
  53. package/dist/dialect/pgVectorMetrics.d.ts +0 -13
  54. package/dist/dialect/pgVectorMetrics.js +0 -17
  55. package/dist/maria/mariaVectorMetrics.d.ts +0 -8
  56. package/dist/maria/mariaVectorMetrics.js +0 -10
@@ -1,4 +1,6 @@
1
+ import type { JsonSlot } from '../dialect/jsonSql.js';
1
2
  import { MysqlLikeSqlDialect } from '../dialect/mysqlLikeSqlDialect.js';
3
+ import type { QueryContext } from '../type/index.js';
2
4
  export declare class MySqlDialect extends MysqlLikeSqlDialect {
3
5
  readonly dialectName = "mysql";
4
6
  /**
@@ -6,6 +8,19 @@ export declare class MySqlDialect extends MysqlLikeSqlDialect {
6
8
  * "subject to removal in a future version"; aliasing the inserted row (8.0.19+) is its replacement.
7
9
  */
8
10
  protected readonly upsertNewRowAlias = "_uql_new";
9
- /** A `SET_VAR` hint, which MySQL reads only in a statement's first `SELECT`. */
11
+ /**
12
+ * 26.7 may plan the correlated `JSON_TABLE` as a semijoin materialized once for the whole table, which
13
+ * answers every row with one row's elements. Verified in `mysqlJsonElemMatch.test.ts`.
14
+ */
15
+ protected readonly jsonElemHint = "/*+ NO_SEMIJOIN() */";
16
+ /**
17
+ * `JSON_OVERLAPS`, which a multi-valued index serves: verified in `mysqlJsonArrayIndex.test.ts`. It reads
18
+ * a scalar as an array of one, so the value must be an array.
19
+ */
20
+ protected jsonAny(ctx: QueryContext, slot: JsonSlot, values: readonly unknown[]): string;
21
+ /**
22
+ * A `SET_VAR` hint, which MySQL reads only right after the statement's own `SELECT` keyword - before
23
+ * `DISTINCT`, and never in a subquery - so it is anchored to the start rather than found.
24
+ */
10
25
  protected applySettings(sql: string, settings: readonly string[]): string;
11
26
  }
@@ -7,8 +7,24 @@ export class MySqlDialect extends MysqlLikeSqlDialect {
7
7
  * "subject to removal in a future version"; aliasing the inserted row (8.0.19+) is its replacement.
8
8
  */
9
9
  upsertNewRowAlias = UPSERT_NEW_ROW_ALIAS;
10
- /** A `SET_VAR` hint, which MySQL reads only in a statement's first `SELECT`. */
10
+ /**
11
+ * 26.7 may plan the correlated `JSON_TABLE` as a semijoin materialized once for the whole table, which
12
+ * answers every row with one row's elements. Verified in `mysqlJsonElemMatch.test.ts`.
13
+ */
14
+ jsonElemHint = '/*+ NO_SEMIJOIN() */';
15
+ /**
16
+ * `JSON_OVERLAPS`, which a multi-valued index serves: verified in `mysqlJsonArrayIndex.test.ts`. It reads
17
+ * a scalar as an array of one, so the value must be an array.
18
+ */
19
+ jsonAny(ctx, slot, values) {
20
+ const overlaps = `JSON_OVERLAPS(${this.jsonValue(slot)}, ${this.addValue(ctx, JSON.stringify(values))})`;
21
+ return `(${this.jsonIsArray(slot)} AND ${overlaps})`;
22
+ }
23
+ /**
24
+ * A `SET_VAR` hint, which MySQL reads only right after the statement's own `SELECT` keyword - before
25
+ * `DISTINCT`, and never in a subquery - so it is anchored to the start rather than found.
26
+ */
11
27
  applySettings(sql, settings) {
12
- return sql.replace('SELECT ', `SELECT /*+ ${settings.map((setting) => `SET_VAR(${setting})`).join(' ')} */ `);
28
+ return sql.replace(/^SELECT /, `SELECT /*+ ${settings.map((setting) => `SET_VAR(${setting})`).join(' ')} */ `);
13
29
  }
14
30
  }
@@ -1,5 +1,6 @@
1
1
  import { AbstractSqlDialect, type DerivedRelation, type HydrateKind, type RelationRows } from '../dialect/abstractSqlDialect.js';
2
- import type { EntityMeta, FieldOptions, QueryContext, QueryPager, QuerySizeComparisonOps, QueryTextSearchOptions, SqlDialectFeatures, Type, VectorDistance, VectorMetric } from '../type/index.js';
2
+ import { type JsonAccessMode, type JsonSlot } from '../dialect/jsonSql.js';
3
+ import type { EntityMeta, FieldOptions, QueryContext, QueryPager, QueryTextSearchOptions, SqlDialectFeatures, Type, VectorDistance, VectorMetric } from '../type/index.js';
3
4
  /** What SQLite and the engines derived from it have. */
4
5
  export declare const SQLITE_FEATURES: SqlDialectFeatures;
5
6
  export declare class SqliteDialect extends AbstractSqlDialect {
@@ -58,25 +59,25 @@ export declare class SqliteDialect extends AbstractSqlDialect {
58
59
  * FTS5 virtual table (UQL does not create those; declare it outside your entities).
59
60
  */
60
61
  protected appendTextSearch<E>(ctx: QueryContext, entity: Type<E>, meta: EntityMeta<E>, search: QueryTextSearchOptions<E>): void;
62
+ protected jsonLength(slot: JsonSlot): string;
63
+ /** `JSON_EACH` walks the array at the path itself, each element's `fullkey` naming it from the column. */
64
+ protected jsonElemFrom(slot: JsonSlot, alias: string): string;
65
+ protected jsonIsArray(slot: JsonSlot): string;
66
+ /** An object element's `value` is its JSON text, which its fields are paths into. */
67
+ protected jsonElemDoc(alias: string): string;
61
68
  /**
62
- * Each element is read back as JSON text through `->` at its own `fullkey`, so it compares
63
- * correctly whatever its type. `JSON_EACH`'s `value` column would not: it unquotes strings (`a`
64
- * vs `"a"`), flattens booleans to 0/1, and stringifies objects.
69
+ * A scalar element's `value` is already typed, so a number and a string compare as such. Its JSON form
70
+ * is read back from the column at the element's own `fullkey`, since `value` flattens a boolean to 0/1.
65
71
  */
66
- protected jsonAll(ctx: QueryContext, jsonField: string, value: unknown): string;
67
- protected jsonSize(ctx: QueryContext, jsonField: string, value: number | QuerySizeComparisonOps): string;
68
- /** `JSON_EACH` yields both scalar and object elements, so one form covers each case. */
69
- protected jsonElemFrom(jsonField: string, _fields: readonly string[], alias: string): string;
70
- protected jsonElemRef(alias: string, field?: string, asJson?: boolean): string;
71
- protected getJsonPathScalarExpr(escapedColumn: string, jsonPathStr: string): string;
72
+ protected jsonElemValue(slot: JsonSlot, alias: string, mode: JsonAccessMode): string;
73
+ /** `JSON_EXTRACT` already answers a number as one, and orders it by value. */
74
+ protected readonly jsonSortModes: readonly JsonAccessMode[];
75
+ /** `->` for the JSON value, and `JSON_EXTRACT` for SQLite's own: a number as a number, a string as text. */
76
+ protected jsonPathReading(escapedColumn: string, path: string, mode: 'json' | 'text'): string;
72
77
  protected numericCast(expr: string): string;
73
78
  protected jsonCast(operand: string): string;
74
- /**
75
- * `JSON_REPLACE` leaves an absent key (and a NULL column) untouched. Elements are read back
76
- * through `->` at their own `fullkey` so each keeps its JSON type - `JSON_EACH`'s `value` would
77
- * flatten booleans to 0/1 and stringify objects.
78
- */
79
- protected jsonPullKey(ctx: QueryContext, expr: string, escapedCol: string, key: string, value: unknown): string;
79
+ /** `JSON` keeps each element's JSON type in the array, which answers `[]` for no rows. */
80
+ protected jsonArrayOf(elem: string): string;
80
81
  protected jsonSet(ctx: QueryContext, expr: string, set: Record<string, unknown>, field?: FieldOptions): string;
81
82
  /** `[#]` appends, creating the array where it is absent: `JSON_SET`, since Turso's `JSON_INSERT` will not touch an existing array. */
82
83
  protected jsonPush(ctx: QueryContext, expr: string, push: Record<string, unknown>): string;
@@ -1,7 +1,6 @@
1
1
  import { AbstractSqlDialect, } from '../dialect/abstractSqlDialect.js';
2
- import { JSON_ELEM_ALIAS, JSON_PULL_ALIAS } from '../dialect/aliases.js';
3
2
  import { BYTES_PREFIX } from '../dialect/hydrateColumn.js';
4
- import { chainedCall, groupsPerCall, jsonAssignCall, jsonElemExists, jsonPath, jsonRemoveCall, jsonSetTarget, } from '../dialect/jsonSql.js';
3
+ import { chainedCall, groupsPerCall, jsonSetCall, jsonPath, jsonArraySlotArgs, jsonSlotArgs, jsonRemoveCall, jsonSetTarget, } from '../dialect/jsonSql.js';
5
4
  import { textSearchFields } from '../util/dialect.util.js';
6
5
  import { columnFamily, isIntegerColumn } from '../util/field.util.js';
7
6
  /** What SQLite and the engines derived from it have. */
@@ -25,8 +24,6 @@ export const SQLITE_FEATURES = {
25
24
  rowLockOf: true,
26
25
  orderedUpsertReturning: true,
27
26
  orderedJsonAggregates: true,
28
- partialJsonContainment: false,
29
- typedJsonElements: true,
30
27
  narrowVectorTypes: false,
31
28
  vectorTuningNeedsTransaction: false,
32
29
  serialDeclaresPrimaryKey: true,
@@ -126,32 +123,38 @@ export class SqliteDialect extends AbstractSqlDialect {
126
123
  ctx.append(`${this.escapedTableName(meta)} MATCH {${columns.join(' ')}} : `);
127
124
  ctx.addValue(search.$value);
128
125
  }
126
+ jsonLength(slot) {
127
+ return `JSON_ARRAY_LENGTH(${jsonArraySlotArgs(slot, this.jsonIsArray(slot))})`;
128
+ }
129
+ /** `JSON_EACH` walks the array at the path itself, each element's `fullkey` naming it from the column. */
130
+ jsonElemFrom(slot, alias) {
131
+ return `JSON_EACH(${jsonArraySlotArgs(slot, this.jsonIsArray(slot))}) ${alias}`;
132
+ }
133
+ jsonIsArray(slot) {
134
+ return `JSON_TYPE(${jsonSlotArgs(slot)}) = 'array'`;
135
+ }
136
+ /** An object element's `value` is its JSON text, which its fields are paths into. */
137
+ jsonElemDoc(alias) {
138
+ return `${alias}.value`;
139
+ }
129
140
  /**
130
- * Each element is read back as JSON text through `->` at its own `fullkey`, so it compares
131
- * correctly whatever its type. `JSON_EACH`'s `value` column would not: it unquotes strings (`a`
132
- * vs `"a"`), flattens booleans to 0/1, and stringifies objects.
141
+ * A scalar element's `value` is already typed, so a number and a string compare as such. Its JSON form
142
+ * is read back from the column at the element's own `fullkey`, since `value` flattens a boolean to 0/1.
133
143
  */
134
- jsonAll(ctx, jsonField, value) {
135
- const alias = ctx.claimAlias(JSON_ELEM_ALIAS);
136
- const from = this.jsonElemFrom(jsonField, [], alias);
137
- const conditions = value.map((val) => jsonElemExists(from, [`${jsonField} -> ${alias}.fullkey = ${this.jsonScalarParam(ctx, val)}`]));
138
- return `(${conditions.join(' AND ')})`;
139
- }
140
- jsonSize(ctx, jsonField, value) {
141
- return this.buildFragment(ctx, (fragmentCtx) => this.buildSizeComparison(fragmentCtx, () => fragmentCtx.append(`JSON_ARRAY_LENGTH(${jsonField})`), value));
142
- }
143
- /** `JSON_EACH` yields both scalar and object elements, so one form covers each case. */
144
- jsonElemFrom(jsonField, _fields, alias) {
145
- return `JSON_EACH(${jsonField}) ${alias}`;
146
- }
147
- jsonElemRef(alias, field, asJson = false) {
148
- if (field === undefined) {
149
- return `${alias}.value`;
144
+ jsonElemValue(slot, alias, mode) {
145
+ if (mode === 'json') {
146
+ return `${slot.base} -> ${alias}.fullkey`;
150
147
  }
151
- return asJson ? `${alias}.value -> ${jsonPath(field)}` : `JSON_EXTRACT(${alias}.value, ${jsonPath(field)})`;
148
+ const value = this.jsonElemDoc(alias);
149
+ return mode === 'numeric' ? this.numericCast(value) : value;
152
150
  }
153
- getJsonPathScalarExpr(escapedColumn, jsonPathStr) {
154
- return `JSON_EXTRACT(${escapedColumn}, ${jsonPath(jsonPathStr)})`;
151
+ /** `JSON_EXTRACT` already answers a number as one, and orders it by value. */
152
+ jsonSortModes = ['text'];
153
+ /** `->` for the JSON value, and `JSON_EXTRACT` for SQLite's own: a number as a number, a string as text. */
154
+ jsonPathReading(escapedColumn, path, mode) {
155
+ return mode === 'json'
156
+ ? `(${escapedColumn} -> ${jsonPath(path)})`
157
+ : `JSON_EXTRACT(${escapedColumn}, ${jsonPath(path)})`;
155
158
  }
156
159
  numericCast(expr) {
157
160
  return `CAST(${expr} AS REAL)`;
@@ -159,25 +162,18 @@ export class SqliteDialect extends AbstractSqlDialect {
159
162
  jsonCast(operand) {
160
163
  return `JSON(${operand})`;
161
164
  }
162
- /**
163
- * `JSON_REPLACE` leaves an absent key (and a NULL column) untouched. Elements are read back
164
- * through `->` at their own `fullkey` so each keeps its JSON type - `JSON_EACH`'s `value` would
165
- * flatten booleans to 0/1 and stringify objects.
166
- */
167
- jsonPullKey(ctx, expr, escapedCol, key, value) {
168
- const path = jsonPath(key);
169
- const elem = `${escapedCol} -> ${JSON_PULL_ALIAS}.fullkey`;
170
- const kept = `SELECT JSON_GROUP_ARRAY(JSON(${elem})) FROM JSON_EACH(${escapedCol}, ${path}) ${JSON_PULL_ALIAS} WHERE ${elem} <> ${this.jsonScalarParam(ctx, value)}`;
171
- return `JSON_REPLACE(${expr}, ${path}, (${kept}))`;
165
+ /** `JSON` keeps each element's JSON type in the array, which answers `[]` for no rows. */
166
+ jsonArrayOf(elem) {
167
+ return `JSON_GROUP_ARRAY(JSON(${elem}))`;
172
168
  }
173
169
  jsonSet(ctx, expr, set, field) {
174
- return jsonAssignCall((value) => this.jsonScalarParam(ctx, value), 'JSON_SET', jsonSetTarget(expr, field, `'{}'`), set, this.maxFunctionArgs);
170
+ return jsonSetCall((value) => this.jsonScalarParam(ctx, value), jsonSetTarget(expr, field, `'{}'`), set, this.maxFunctionArgs);
175
171
  }
176
172
  /** `[#]` appends, creating the array where it is absent: `JSON_SET`, since Turso's `JSON_INSERT` will not touch an existing array. */
177
173
  jsonPush(ctx, expr, push) {
178
- return jsonAssignCall((value) => this.jsonScalarParam(ctx, value), 'JSON_SET', expr, push, this.maxFunctionArgs, '[#]');
174
+ return jsonSetCall((value) => this.jsonScalarParam(ctx, value), expr, push, this.maxFunctionArgs, '[#]');
179
175
  }
180
176
  jsonUnset(_ctx, expr, unset) {
181
- return jsonRemoveCall('JSON_REMOVE', expr, unset, this.maxFunctionArgs);
177
+ return jsonRemoveCall(expr, unset, this.maxFunctionArgs);
182
178
  }
183
179
  }
@@ -135,10 +135,6 @@ export interface SqlDialectFeatures extends DialectFeatures {
135
135
  readonly orderedUpsertReturning: boolean;
136
136
  /** Whether a JSON aggregate takes an `ORDER BY` of its own; where not, a relation's rows keep their derived table's order. */
137
137
  readonly orderedJsonAggregates: boolean;
138
- /** Whether JSON array containment matches an object element that merely includes the given keys, as `@>` does. */
139
- readonly partialJsonContainment: boolean;
140
- /** Whether an exploded scalar JSON element keeps its SQL type, as SQLite's `JSON_EACH` does. */
141
- readonly typedJsonElements: boolean;
142
138
  /** Whether the engine has pgvector's `halfvec` and `sparsevec`; elsewhere both map onto `vector`. */
143
139
  readonly narrowVectorTypes: boolean;
144
140
  /** Whether an ANN index's tuning `SET` applies only inside a transaction, as `SET LOCAL` does. */
@@ -181,7 +181,10 @@ export type FieldOptions<V = TsTypeOf<FieldType>, E = unknown> = {
181
181
  readonly type?: FieldType;
182
182
  /** A vector column's dimensions: `@Field({ type: 'vector', dimensions: 1536 })`. */
183
183
  readonly dimensions?: number;
184
- /** The metric a vector search on this field uses unless it names its own `$distance`; `'cosine'` by default. */
184
+ /**
185
+ * The metric a vector search on this field uses unless it names its own `$distance`; by default its
186
+ * vector index's metric, else `'cosine'`.
187
+ */
185
188
  readonly distance?: VectorDistance;
186
189
  /** The entity this column is a foreign key to. */
187
190
  readonly references?: EntityGetter;
@@ -445,10 +448,12 @@ export type IndexJsonPath = {
445
448
  readonly path: string;
446
449
  /** How the value is read, matching what the queries over it compare against. */
447
450
  readonly type: FieldType;
451
+ /** Length of a string value, which MySQL keys as `CHAR(n)` and so requires. */
452
+ readonly length?: number;
448
453
  };
449
454
  /**
450
- * MySQL's multi-valued index, one key per element of the JSON array at `path`, what `$all` and
451
- * `$elemMatch` containment use; refused on any other engine.
455
+ * MySQL's multi-valued index, one key per element of the JSON array at `path`, which `$all` and an
456
+ * `$elemMatch` on one value or several use; refused on any other engine.
452
457
  * @example `@Index((user) => [{ column: user.tags, jsonArray: { type: String, length: 64 } }])`
453
458
  */
454
459
  export type IndexJsonArray = {
@@ -28,20 +28,18 @@ export interface QueryVectorSearch extends QueryVectorQuery {
28
28
  */
29
29
  export type WithDistance<E, K extends string = '_distance'> = E & Record<K, number>;
30
30
  /**
31
- * How a dialect spells a metric: an operator with the operator class an index names from it, or a
32
- * function, `metricArg` where it takes the metric by name. One map, so its keys say which metrics exist.
31
+ * How a dialect spells a metric: an operator, or a function taking the metric by name where `metricArg`
32
+ * says so, and `index`, how the dialect's vector index names it where it builds one. One map serves the
33
+ * query and the index alike, so its keys say which metrics exist.
33
34
  */
34
35
  export type VectorMetric = {
35
36
  readonly op: string;
36
- readonly opsSuffix: string;
37
+ readonly index: string;
37
38
  } | {
38
39
  readonly fn: string;
39
40
  readonly metricArg?: string;
41
+ readonly index?: string;
40
42
  };
41
- /** The operator form, for the pgvector-family dialects whose index DDL also needs `opsSuffix`. */
42
- export type VectorOperatorMetric = Extract<VectorMetric, {
43
- op: string;
44
- }>;
45
43
  /** The error every dialect throws for a metric it lacks. */
46
44
  export declare function unsupportedVectorMetric(dialectName: string, distance: VectorDistance, indexName?: string): TypeError;
47
45
  /**
@@ -138,6 +138,8 @@ export declare function parseGroupMap<E>(group?: QueryGroupMap<E>, select?: Quer
138
138
  * indices. Shared by the SQL and MongoDB builders, whose `$where` and `$having` all face this.
139
139
  */
140
140
  export declare function isOperatorMap(value: unknown): value is Record<string, unknown>;
141
+ /** A JSON object, matched by what it holds: a plain object with no operator key. */
142
+ export declare function isJsonObject(value: unknown): value is Record<string, unknown>;
141
143
  /**
142
144
  * A page operand, checked before it reaches an engine. These arrive from page arithmetic and from
143
145
  * REST query strings (`parseQueryParams` yields `NaN` for a non-numeric one), and each engine's own
@@ -3,7 +3,7 @@ import { soleIdOf } from '../entity/metadata/definition.js';
3
3
  import { QueryRaw, resolveAggregateOp, SOFT_DELETE_FILTER, } from '../type/index.js';
4
4
  import { VECTOR_INDEX_TYPES } from '../type/vector.js';
5
5
  import { isDatabaseWritten } from './field.util.js';
6
- import { entityName, getFieldKeys, getKeys, hasKeys, isScalarId, isRecord, isWhereMap, someKey, } from './object.util.js';
6
+ import { entityName, getFieldKeys, getKeys, hasKeys, isOperatorObject, isScalarId, isRecord, isWhereMap, someKey, } from './object.util.js';
7
7
  /** The keys of `payload` a write persists as columns. */
8
8
  export function filterFieldKeys(meta, payload, callbackKey) {
9
9
  return getKeys(payload).filter((key) => {
@@ -226,7 +226,7 @@ const JSON_UPDATE_OPS = [
226
226
  ];
227
227
  /** Type guard: checks whether an update payload value is a JSON operator object. */
228
228
  export function isJsonUpdateOp(value) {
229
- return value !== null && typeof value === 'object' && someKey(value, (key) => JSON_UPDATE_OPS.includes(key));
229
+ return isRecord(value) && someKey(value, (key) => JSON_UPDATE_OPS.includes(key));
230
230
  }
231
231
  /**
232
232
  * The `$where` naming rows by key: a bare value names the one key column (refused on a composite), a
@@ -386,6 +386,10 @@ export function isOperatorMap(value) {
386
386
  !(value instanceof Uint8Array) &&
387
387
  !(value instanceof QueryRaw));
388
388
  }
389
+ /** A JSON object, matched by what it holds: a plain object with no operator key. */
390
+ export function isJsonObject(value) {
391
+ return isOperatorMap(value) && !isOperatorObject(value);
392
+ }
389
393
  /**
390
394
  * A page operand, checked before it reaches an engine. These arrive from page arithmetic and from
391
395
  * REST query strings (`parseQueryParams` yields `NaN` for a non-numeric one), and each engine's own
@@ -9,12 +9,10 @@ export declare function hasKeys<T>(obj: T): obj is NonNullable<T>;
9
9
  * without materializing a key array (unlike `Object.keys(obj).some(pred)`).
10
10
  */
11
11
  export declare function someKey<T extends object>(obj: T, pred: (key: keyof T & string) => boolean): boolean;
12
- /** Whether any enumerable value of `obj` satisfies `pred`, short-circuiting like {@link someKey}. */
13
- export declare function someValue(obj: object, pred: (value: unknown) => boolean): boolean;
12
+ /** Whether `key` names an operator (`$eq`, `$push`...) rather than a field. */
13
+ export declare function isOperatorKey(key: string): boolean;
14
14
  /** Whether `value` is a non-empty object with an operator key (`$eq`, `$push`...): the one test every dialect classifies with. */
15
15
  export declare function isOperatorObject(value: unknown): value is Record<string, unknown>;
16
- /** Whether every key of the non-empty object `value` is an operator (no plain field names mixed in). */
17
- export declare function isOperatorOnlyObject(value: unknown): value is Record<string, unknown>;
18
16
  /** Whether `value` is an object that is not an array, whose keys can be read. */
19
17
  export declare function isRecord(value: unknown): value is Record<string, unknown>;
20
18
  export declare function getKeys<T extends object>(obj: T | null | undefined): (keyof T & string)[];
@@ -32,18 +32,13 @@ export function someKey(obj, pred) {
32
32
  }
33
33
  return false;
34
34
  }
35
- /** Whether any enumerable value of `obj` satisfies `pred`, short-circuiting like {@link someKey}. */
36
- export function someValue(obj, pred) {
37
- return someKey(obj, (key) => pred(obj[key]));
35
+ /** Whether `key` names an operator (`$eq`, `$push`...) rather than a field. */
36
+ export function isOperatorKey(key) {
37
+ return key.startsWith('$');
38
38
  }
39
- const isOperatorKey = (key) => key.startsWith('$');
40
39
  /** Whether `value` is a non-empty object with an operator key (`$eq`, `$push`...): the one test every dialect classifies with. */
41
40
  export function isOperatorObject(value) {
42
- return hasKeys(value) && !Array.isArray(value) && someKey(value, isOperatorKey);
43
- }
44
- /** Whether every key of the non-empty object `value` is an operator (no plain field names mixed in). */
45
- export function isOperatorOnlyObject(value) {
46
- return hasKeys(value) && !Array.isArray(value) && !someKey(value, (key) => !isOperatorKey(key));
41
+ return isRecord(value) && someKey(value, isOperatorKey);
47
42
  }
48
43
  /** Whether `value` is an object that is not an array, whose keys can be read. */
49
44
  export function isRecord(value) {
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "homepage": "https://uql-orm.dev",
4
4
  "description": "The JSON-native TypeScript ORM for Bun, Browsers, Edge, Deno, Node, Workers. Supports PostgreSQL, PGlite, MySQL, MariaDB, SQLite, CockroachDB, SQL Server, Turso, Neon, Cloudflare D1 and MongoDB. Queries are plain JSON, typed to the leaf.",
5
5
  "license": "MIT",
6
- "version": "0.67.0",
6
+ "version": "0.68.0",
7
7
  "type": "module",
8
8
  "engines": {
9
9
  "node": ">=24"
@@ -1,2 +0,0 @@
1
- /** An `$elemMatch` as per-field conditions, a plain value as `$eq`, so both spellings emit the same SQL. */
2
- export declare function buildElemMatchConditions(match: Record<string, unknown>, onCondition: (field: string, op: string, value: unknown) => string): string[];
@@ -1,7 +0,0 @@
1
- import { isOperatorObject } from '../util/object.util.js';
2
- /** An `$elemMatch` as per-field conditions, a plain value as `$eq`, so both spellings emit the same SQL. */
3
- export function buildElemMatchConditions(match, onCondition) {
4
- return Object.entries(match).flatMap(([field, value]) => isOperatorObject(value)
5
- ? Object.entries(value).map(([op, opVal]) => onCondition(field, op, opVal))
6
- : onCondition(field, '$eq', value));
7
- }
@@ -1,13 +0,0 @@
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>;
@@ -1,17 +0,0 @@
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'));
@@ -1,8 +0,0 @@
1
- import type { VectorDistance } from '../type/index.js';
2
- /**
3
- * MariaDB's own name for each metric it supports (`euclidean`, not `l2`), which its index clause and
4
- * its distance function are both spelled from - one list, so a metric can never be searchable and
5
- * unindexable or the reverse. Its own module because the two ends now live apart: the distance
6
- * function on the dialect, the `DISTANCE=` clause in the migrator's index DDL.
7
- */
8
- export declare const MARIA_VECTOR_METRICS: Map<VectorDistance, string>;
@@ -1,10 +0,0 @@
1
- /**
2
- * MariaDB's own name for each metric it supports (`euclidean`, not `l2`), which its index clause and
3
- * its distance function are both spelled from - one list, so a metric can never be searchable and
4
- * unindexable or the reverse. Its own module because the two ends now live apart: the distance
5
- * function on the dialect, the `DISTANCE=` clause in the migrator's index DDL.
6
- */
7
- export const MARIA_VECTOR_METRICS = new Map([
8
- ['cosine', 'cosine'],
9
- ['l2', 'euclidean'],
10
- ]);