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,21 +1,17 @@
1
1
  import { fieldOf, getMeta, relationOf, soleIdOf } from '../entity/index.js';
2
2
  import { COUNT_RESULT_KEY, parseQueryLock, QueryRaw, RAW_ALIAS, VECTOR_QUERY_KEYS, } from '../type/index.js';
3
3
  import { isInlinedExpression } from '../util/field.util.js';
4
- import { asSelectMap, assertNonNegativeInteger, escapeSqlId, fillOnFields, filterFieldKeys, getInsertFieldKeys, getKeys, getRelationRequestSummary, getSoftDeleteValue, hasKeys, idOnlyQuery, columnFamily, countedRelations, isJsonUpdateOp, isOperatorMap, isOperatorObject, isOperatorOnlyObject, isVectorSearch, normalizeScalarFieldSelection, parentJoins, targetKeyColumns, parseGroupMap, parseRelationAtKey, parseRelationSize, parseSortByCount, populatesRelations, raw, someValue, throwUnknownAggregateColumn, withoutSoftDeleteFilter, } from '../util/index.js';
5
- import { escapeAnsiSqlLiteral, escapeSingleQuotes } from '../util/sqlLiteral.js';
6
- import { COUNT_ALIAS, COUNTED_ROWS_ALIAS, JSON_ELEM_ALIAS, relationSortColumn } from './aliases.js';
7
- import { buildElemMatchConditions } from './jsonArrayElemMatchUtils.js';
8
- import { isJsonbOp, jsonCompareMode, jsonElemExists } from './jsonSql.js';
4
+ import { asSelectMap, assertNonNegativeInteger, escapeSqlId, fillOnFields, filterFieldKeys, getInsertFieldKeys, getKeys, getRelationRequestSummary, getSoftDeleteValue, hasKeys, idOnlyQuery, columnFamily, countedRelations, isJsonObject, isJsonUpdateOp, isOperatorMap, isOperatorKey, isVectorSearch, normalizeScalarFieldSelection, parentJoins, targetKeyColumns, parseGroupMap, parseRelationAtKey, parseRelationSize, parseSortByCount, populatesRelations, raw, throwUnknownAggregateColumn, withoutSoftDeleteFilter, } from '../util/index.js';
5
+ import { escapeAnsiSqlLiteral } from '../util/sqlLiteral.js';
6
+ import { COUNT_ALIAS, COUNTED_ROWS_ALIAS, JSON_ELEM_ALIAS, JSON_PULL_ALIAS, relationSortColumn } from './aliases.js';
7
+ import { holdsOperator, isJsonScalar, jsonCompareMode, jsonElemExists, jsonPath, } from './jsonSql.js';
9
8
  import { SqlQueryContext } from './queryContext.js';
10
9
  import { NO_JOINS, resolveQueryJoins, resolveSortableJoin, } from './queryJoins.js';
11
10
  import { resolveVectorCast } from './vectorCast.js';
12
11
  import { VectorSqlDialect } from './vectorSqlDialect.js';
13
12
  /** The key a term answers under in a populated relation's row, which a raw expression has only once aliased. */
14
13
  export function relationTermKey({ sql, key }) {
15
- if (key === undefined) {
16
- throw new TypeError(`a raw $select in a populated relation needs an alias, the key its value lands under: ${sql}`);
17
- }
18
- return key;
14
+ return orRefuse(key, `a raw $select in a populated relation needs an alias, the key its value lands under: ${sql}`);
19
15
  }
20
16
  /**
21
17
  * What a projection reads: its raw expressions, or its fields past any `$exclude`, and every field where
@@ -27,6 +23,13 @@ function projectedKeys(meta, select, exclude, json) {
27
23
  : normalizeScalarFieldSelection(meta, asSelectMap(select), exclude);
28
24
  return selected.length || !json ? selected : normalizeScalarFieldSelection(meta);
29
25
  }
26
+ /** `value`, where there is one; a `TypeError` saying `refusal` where there is none. */
27
+ function orRefuse(value, refusal) {
28
+ if (value === undefined) {
29
+ throw TypeError(refusal);
30
+ }
31
+ return value;
32
+ }
30
33
  /** An `$in`/`$nin` operand, which the types require to be an array but `/http` hands over untyped. */
31
34
  function inOperands(op, value) {
32
35
  if (!Array.isArray(value)) {
@@ -206,8 +209,8 @@ export class AbstractSqlDialect extends VectorSqlDialect {
206
209
  }
207
210
  /**
208
211
  * The expression a scalar field is read through in the statement's own rows, the plain column by
209
- * default. MariaDB reads a vector column back with `VEC_ToText`, since selecting it raw yields its
210
- * binary form. A related row's column crosses JSON through {@link carriedFields} instead.
212
+ * default. MariaDB reads a vector column back as hex, since selecting it raw yields its binary form.
213
+ * A related row's column crosses JSON through {@link carriedFields} instead.
211
214
  */
212
215
  selectFieldExpr(escapedColumn, _field) {
213
216
  return escapedColumn;
@@ -413,7 +416,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
413
416
  // Detect JSONB dot-notation: 'column.path' where column is a registered JSON/JSONB field
414
417
  const jsonDot = this.resolveJsonDotPath(meta, key, opts.prefix);
415
418
  if (jsonDot) {
416
- this.compareJsonPath(ctx, jsonDot, val);
419
+ ctx.append(this.jsonConditions(ctx, this.jsonPathTarget(jsonDot), val));
417
420
  return;
418
421
  }
419
422
  if (key.includes('.')) {
@@ -430,19 +433,12 @@ export class AbstractSqlDialect extends VectorSqlDialect {
430
433
  return;
431
434
  }
432
435
  const value = this.normalizeWhereValue(val);
433
- const operators = getKeys(value);
434
- if (operators.length > 1) {
435
- ctx.append('(');
436
- }
437
- operators.forEach((op, index) => {
438
- if (index > 0) {
439
- ctx.append(' AND ');
440
- }
441
- this.compareFieldOperator(ctx, entity, key, op, value[op], opts);
442
- });
443
- if (operators.length > 1) {
444
- ctx.append(')');
445
- }
436
+ const parts = getKeys(value).map((op) => this.fieldCondition(ctx, entity, key, op, value[op], opts));
437
+ ctx.append(AbstractSqlDialect.conjunction(parts));
438
+ }
439
+ /** Conditions joined by `AND`, parenthesized where there is more than one. */
440
+ static conjunction(parts) {
441
+ return parts.length > 1 ? `(${parts.join(' AND ')})` : parts.join('');
446
442
  }
447
443
  compareLogicalOperator(ctx, entity, key, val, opts) {
448
444
  const { join, negate } = AbstractSqlDialect.GROUP_OPS[key];
@@ -477,14 +473,16 @@ export class AbstractSqlDialect extends VectorSqlDialect {
477
473
  /** What a `$near` says about the search itself; everything else in it is a bound. */
478
474
  static VECTOR_QUERY_KEYS = new Set(VECTOR_QUERY_KEYS);
479
475
  /**
480
- * The runtime half of {@link QueryVectorNear}'s bounds, derived from the map above rather than
481
- * spelled again: both are `QueryOrderedOp`, so `$near` can never accept a comparison the renderer
482
- * below has no operator for.
476
+ * The ordered comparisons, `QueryOrderedOp` at runtime, derived from the map above rather than spelled
477
+ * again: {@link QueryVectorNear}'s bounds, so `$near` never accepts one the renderer has no operator
478
+ * for, and the operators that read a JSON path as a number.
483
479
  */
484
- static NEAR_BOUND_OPS = new Set([
480
+ static ORDERED_OPS = new Set([
485
481
  ...AbstractSqlDialect.COMPARE_OP_MAP.keys(),
486
482
  '$between',
487
483
  ]);
484
+ /** The operators an equality compares by value, which a JSON path reads the way that value compares. */
485
+ static EQUALITY_OPS = new Set(['$eq', '$ne', '$in', '$nin']);
488
486
  /**
489
487
  * Every `$like`-family operator: the pattern it wraps its value in, and whether it ignores case.
490
488
  * Each case-sensitive operator is paired here with the `$i` twin that shares its pattern, so the
@@ -546,42 +544,31 @@ export class AbstractSqlDialect extends VectorSqlDialect {
546
544
  const inlined = field && isInlinedExpression(field) ? field.computed : undefined;
547
545
  return inlined ? this.rawFragment(ctx, inlined, prefix, entity) : undefined;
548
546
  }
549
- /** One operator of a field's condition. Both come from the query as data, so neither is trusted. */
547
+ /** {@link fieldCondition}, appended. */
550
548
  compareFieldOperator(ctx, entity, key, op, val, opts = {}) {
551
- const field = this.resolveOperandField(ctx, entity, key, opts);
552
- if (this.appendOperatorCondition(ctx, field, op, val)) {
553
- return;
549
+ ctx.append(this.fieldCondition(ctx, entity, key, op, val, opts));
550
+ }
551
+ /** One operator of a field's condition. Both come from the query as data, so neither is trusted. */
552
+ fieldCondition(ctx, entity, key, op, val, opts) {
553
+ if (op === '$not') {
554
+ return `NOT (${this.buildFragment(ctx, (fragmentCtx) => this.compare(fragmentCtx, entity, key, val, opts))})`;
554
555
  }
555
- switch (op) {
556
- case '$not':
557
- ctx.append('NOT (');
558
- this.compare(ctx, entity, key, val, opts);
559
- ctx.append(')');
560
- break;
561
- case '$all':
562
- ctx.append(this.jsonAll(ctx, field, val));
563
- break;
564
- case '$size':
565
- ctx.append(this.jsonSize(ctx, field, val));
566
- break;
567
- case '$elemMatch':
568
- ctx.append(this.jsonElemMatch(ctx, field, val));
569
- break;
570
- case '$near':
571
- this.compareVectorNear(ctx, getMeta(entity), key, val);
572
- break;
573
- default:
574
- throw TypeError(`unknown operator: ${op}`);
556
+ if (op === '$near') {
557
+ return this.vectorNearCondition(ctx, getMeta(entity), key, val);
575
558
  }
559
+ const field = this.resolveOperandField(ctx, entity, key, opts);
560
+ const condition = this.operatorCondition(ctx, field, op, val) ?? this.jsonArrayCondition(ctx, { base: field, path: '' }, op, val);
561
+ return orRefuse(condition, `unknown operator: ${op}`);
576
562
  }
577
563
  /**
578
564
  * `<operand> <op> <value>` for every operator that needs only its left-hand SQL, shared by a column, a
579
- * `HAVING` expression and a `$size` count; `undefined` for the rest.
565
+ * JSON path, a `HAVING` expression, a count and a distance; `undefined` for the rest. `bind` renders
566
+ * each compared value, a plain placeholder unless a JSON path reads it otherwise.
580
567
  */
581
- operatorCondition(ctx, operand, op, val) {
568
+ operatorCondition(ctx, operand, op, val, bind = (value) => this.addValue(ctx, value)) {
582
569
  const compareOp = AbstractSqlDialect.COMPARE_OP_MAP.get(op);
583
570
  if (compareOp) {
584
- return `${operand}${compareOp}${this.addValue(ctx, val)}`;
571
+ return `${operand}${compareOp}${bind(val)}`;
585
572
  }
586
573
  const like = this.likeCondition(ctx, operand, op, val);
587
574
  if (like) {
@@ -589,17 +576,17 @@ export class AbstractSqlDialect extends VectorSqlDialect {
589
576
  }
590
577
  switch (op) {
591
578
  case '$eq':
592
- return val === null ? `${operand} IS NULL` : `${operand} = ${this.addValue(ctx, val)}`;
579
+ return val === null ? `${operand} IS NULL` : `${operand} = ${bind(val)}`;
593
580
  case '$ne':
594
- return val === null ? `${operand} IS NOT NULL` : this.neExpr(operand, this.addValue(ctx, val));
581
+ return val === null ? `${operand} IS NOT NULL` : this.neExpr(operand, bind(val));
595
582
  case '$regex':
596
583
  return this.regexCondition(operand, this.addValue(ctx, val));
597
584
  case '$in':
598
585
  case '$nin':
599
- return this.formatIn(ctx, operand, inOperands(op, val), op === '$nin');
586
+ return this.formatIn(ctx, operand, inOperands(op, val), op === '$nin', bind);
600
587
  case '$between': {
601
588
  const [min, max] = val;
602
- return `${operand} BETWEEN ${this.addValue(ctx, min)} AND ${this.addValue(ctx, max)}`;
589
+ return `${operand} BETWEEN ${bind(min)} AND ${bind(max)}`;
603
590
  }
604
591
  case '$isNull':
605
592
  return operand + (val ? ' IS NULL' : ' IS NOT NULL');
@@ -609,114 +596,147 @@ export class AbstractSqlDialect extends VectorSqlDialect {
609
596
  return undefined;
610
597
  }
611
598
  }
612
- /** {@link operatorCondition}, appended; `false` when `op` needs more than an operand. */
613
- appendOperatorCondition(ctx, operand, op, val) {
614
- const condition = this.operatorCondition(ctx, operand, op, val);
615
- if (condition === undefined) {
616
- return false;
617
- }
618
- ctx.append(condition);
619
- return true;
620
- }
621
- /**
622
- * Build a comparison condition for a JSON field.
623
- * Used by both `$elemMatch` and dot-notation paths.
624
- * All dialect-specific behavior comes from overridable methods on `this`.
625
- */
626
- buildJsonFieldCondition(ctx, fieldAccessor, jsonPath, op, value, asJson = false) {
627
- const jsonField = fieldAccessor(jsonPath, asJson ? 'json' : 'text');
628
- // The left side of a comparison reads the path the way its operand is compared: a numeric one
629
- // cast to a number, a boolean as the JSON value.
630
- const comparand = (val) => fieldAccessor(jsonPath, jsonCompareMode(val));
631
- // The `$like` family reads a JSON path exactly as it reads a column, case folding included.
632
- const like = this.likeCondition(ctx, jsonField, op, value);
633
- if (like) {
634
- return like;
635
- }
636
- // The ordered comparisons read the path as a number whatever the operand is, and spell their
637
- // operator out of the same table a comparison against a column does.
638
- const compareOp = AbstractSqlDialect.COMPARE_OP_MAP.get(op);
639
- if (compareOp) {
640
- return `${fieldAccessor(jsonPath, 'numeric')}${compareOp}${this.addValue(ctx, value)}`;
641
- }
599
+ /** `$all`, `$size` and `$elemMatch`, which read the JSON array at `slot`; `undefined` for the rest. */
600
+ jsonArrayCondition(ctx, slot, op, val) {
642
601
  switch (op) {
643
- case '$eq':
644
- if (value === null)
645
- return `${jsonField} IS NULL`;
646
- return `${comparand(value)} = ${this.jsonOperand(ctx, value, asJson)}`;
647
- case '$ne':
648
- if (value === null)
649
- return `${jsonField} IS NOT NULL`;
650
- return this.neExpr(comparand(value), this.jsonOperand(ctx, value, asJson));
651
- case '$regex':
652
- return this.regexCondition(jsonField, this.addValue(ctx, value));
653
- case '$in':
654
- case '$nin':
655
- return this.jsonInNin(ctx, jsonField, comparand, op, value, asJson);
656
602
  case '$all':
657
- return this.jsonAll(ctx, jsonField, value);
603
+ return this.jsonAll(ctx, slot, val);
658
604
  case '$size':
659
- return this.jsonSize(ctx, jsonField, value);
605
+ return this.sizeCondition(ctx, (fragmentCtx) => fragmentCtx.append(this.jsonLength(slot)), val);
660
606
  case '$elemMatch':
661
- return this.jsonElemMatch(ctx, jsonField, value);
607
+ return this.jsonElemMatch(ctx, slot, val);
662
608
  default:
663
- throw TypeError(`unknown operator: ${op}`);
609
+ return undefined;
664
610
  }
665
611
  }
666
- jsonInNin(ctx, jsonField, comparand, op, value, asJson) {
667
- const values = inOperands(op, value);
668
- const negate = op === '$nin';
669
- if (!asJson) {
670
- return this.formatIn(ctx, comparand(values), values, negate);
671
- }
672
- // JSON values have no portable array literal, so the set expands into explicit comparisons.
673
- const comparisons = values.map((val) => `${jsonField} ${negate ? '<>' : '='} ${this.jsonScalarParam(ctx, val)}`);
674
- return `(${comparisons.join(negate ? ' AND ' : ' OR ')})`;
612
+ /** A path of a JSON document, read the way each operator reads it. */
613
+ jsonPathTarget(slot) {
614
+ return { slot, read: (mode) => this.jsonPathExpr(slot.base, slot.path, mode) };
615
+ }
616
+ /** Every operator `target` is compared with, `AND`-joined. */
617
+ jsonConditions(ctx, target, val) {
618
+ const value = this.normalizeWhereValue(val);
619
+ return AbstractSqlDialect.conjunction(getKeys(value).map((op) => this.jsonCondition(ctx, target, op, value[op])));
675
620
  }
676
- /** The bound operand of a JSON comparison: JSON-encoded when comparing against the JSON value. */
677
- jsonOperand(ctx, value, asJson) {
678
- return asJson ? this.jsonScalarParam(ctx, value) : this.addValue(ctx, value);
621
+ jsonCondition(ctx, target, op, value) {
622
+ if (op === '$not') {
623
+ return `NOT (${this.jsonConditions(ctx, target, value)})`;
624
+ }
625
+ const array = this.jsonArrayCondition(ctx, target.slot, op, value);
626
+ if (array !== undefined) {
627
+ return array;
628
+ }
629
+ const mode = AbstractSqlDialect.jsonOperatorMode(op, value);
630
+ const operand = target.read(mode);
631
+ // Only a boolean compares as a JSON value, so the set holds two at most, and MySQL documents `IN()`
632
+ // as unsupported on JSON values: the comparisons are spelled out.
633
+ if (mode === 'json' && (op === '$in' || op === '$nin')) {
634
+ const negate = op === '$nin';
635
+ const comparisons = inOperands(op, value).map((val) => `${operand} ${negate ? '<>' : '='} ${this.jsonScalarParam(ctx, val)}`);
636
+ return `(${comparisons.join(negate ? ' AND ' : ' OR ')})`;
637
+ }
638
+ const condition = this.operatorCondition(ctx, operand, op, value, (val) => this.jsonOperand(ctx, val, mode));
639
+ return orRefuse(condition, `unknown operator: ${op}`);
679
640
  }
680
641
  /**
681
- * `$elemMatch`: an element satisfies `match`. Operator keys test a scalar element; a plain object is
682
- * containment, which an index can serve; anything else tests each exploded object's fields.
642
+ * How `op` reads a JSON value: an ordered comparison as a number, an equality as its operand compares,
643
+ * and a pattern or a null check as text.
683
644
  */
684
- jsonElemMatch(ctx, jsonField, match) {
685
- // Conditions on the element itself. One `FROM` serves them all, so the element is read as JSON
686
- // only when *every* operand needs it - the same all-operands rule the comparison classifier uses.
687
- if (isOperatorOnlyObject(match)) {
688
- const entries = Object.entries(match);
689
- const asJson = !this.features.typedJsonElements && entries.every(([op, val]) => isJsonbOp(op, val));
690
- const alias = ctx.claimAlias(JSON_ELEM_ALIAS);
691
- const conditions = entries.map(([op, val]) => this.buildJsonFieldCondition(ctx, this.elemAccessor(alias, asJson), '', op, val, asJson));
692
- return jsonElemExists(this.jsonElemFrom(jsonField, [], alias, asJson), conditions);
693
- }
694
- if (isOperatorObject(match)) {
695
- throw TypeError(`$elemMatch cannot mix operators with field names: ${Object.keys(match).join(', ')}`);
645
+ static jsonOperatorMode(op, value) {
646
+ if (AbstractSqlDialect.ORDERED_OPS.has(op)) {
647
+ return 'numeric';
696
648
  }
697
- // A plain object with no nested operators is containment, which is also the only form an index
698
- // can serve. SQLite compares elements exactly, so it always expands the per-field form below.
699
- if (this.features.partialJsonContainment && !someValue(match, isOperatorObject)) {
700
- return this.jsonAll(ctx, jsonField, [match]);
649
+ return AbstractSqlDialect.EQUALITY_OPS.has(op) ? jsonCompareMode(value) : 'text';
650
+ }
651
+ /** A bound operand of a JSON comparison, read the way `mode` reads the value it is compared with. */
652
+ jsonOperand(ctx, value, mode) {
653
+ if (mode === 'json') {
654
+ return this.jsonScalarParam(ctx, value);
701
655
  }
656
+ const placeholder = this.addValue(ctx, value);
657
+ return mode === 'numeric' ? this.numericCast(placeholder) : placeholder;
658
+ }
659
+ /** The JSON value at `slot`, as an array operator reads it. */
660
+ jsonValue(slot) {
661
+ return slot.path ? this.jsonPathExpr(slot.base, slot.path, 'json') : slot.base;
662
+ }
663
+ /**
664
+ * `$all`: the JSON array at `slot` has an element holding each value, as Postgres's `@>` and MySQL's
665
+ * `JSON_CONTAINS` read one: a scalar equal, an array each of its elements, an object each of its keys.
666
+ * Plain JSON goes to {@link jsonContains}; an operator anywhere in it is matched element by element.
667
+ */
668
+ jsonAll(ctx, slot, values) {
669
+ return holdsOperator(values) ? this.jsonElemsHold(ctx, slot, values) : this.jsonContains(ctx, slot, values);
670
+ }
671
+ /** `$all` over plain JSON, which an engine with containment of its own spells natively, for an index to serve. */
672
+ jsonContains(ctx, slot, values) {
673
+ return this.jsonElemsHold(ctx, slot, values);
674
+ }
675
+ /** One `EXISTS` per value, over the elements of the array at `slot`: a scalar equal, anything else held. */
676
+ jsonElemsHold(ctx, slot, values) {
702
677
  const alias = ctx.claimAlias(JSON_ELEM_ALIAS);
703
- const conditions = buildElemMatchConditions(match, (field, op, opVal) => {
704
- const asJson = isJsonbOp(op, opVal);
705
- return this.buildJsonFieldCondition(ctx, this.elemAccessor(alias, asJson), field, op, opVal, asJson);
678
+ const from = this.jsonElemFrom(slot, alias);
679
+ const element = {
680
+ slot: { base: this.jsonElemDoc(alias), path: '' },
681
+ read: (mode) => this.jsonElemValue(slot, alias, mode),
682
+ };
683
+ const conditions = values.map((value) => {
684
+ const holds = isOperatorMap(value) || Array.isArray(value)
685
+ ? this.jsonHolds(ctx, element, value)
686
+ : [this.jsonElemEquals(ctx, slot, alias, value)];
687
+ return jsonElemExists(from, holds, this.jsonElemHint);
706
688
  });
707
- return jsonElemExists(this.jsonElemFrom(jsonField, Object.keys(match), alias), conditions);
689
+ return AbstractSqlDialect.conjunction(conditions);
708
690
  }
709
691
  /**
710
- * How a `$elemMatch` reads one exploded element: {@link jsonPathExpr}'s counterpart, over a column
711
- * of the derived table rather than a path of the document. An empty field names the element itself,
712
- * which is what the operator-only form matches on.
692
+ * What the JSON `target` reads satisfies to hold `value`: an array each of its elements, an object each
693
+ * of its keys as a path, and an operator map or a scalar compared as a path is.
713
694
  */
714
- elemAccessor(alias, asJson) {
715
- return (field, mode) => {
716
- const ref = this.jsonElemRef(alias, field || undefined, asJson);
717
- return mode === 'numeric' ? this.numericCast(ref) : ref;
718
- };
695
+ jsonHolds(ctx, target, value) {
696
+ if (Array.isArray(value)) {
697
+ return [this.jsonAll(ctx, target.slot, value)];
698
+ }
699
+ if (!isJsonObject(value)) {
700
+ return [this.jsonConditions(ctx, target, value)];
701
+ }
702
+ const { base, path } = target.slot;
703
+ return Object.entries(value).flatMap(([key, item]) => this.jsonHolds(ctx, this.jsonPathTarget({ base, path: path ? `${path}.${key}` : key }), item));
704
+ }
705
+ /** An element of the array at `slot` equal to `value`, both read as JSON. */
706
+ jsonElemEquals(ctx, slot, alias, value) {
707
+ return `${this.jsonElemValue(slot, alias, 'json')} = ${this.jsonScalarParam(ctx, value)}`;
708
+ }
709
+ /** The JSON array at `slot` contains at least one of `values`, each as `$all` reads it. */
710
+ jsonAny(ctx, slot, values) {
711
+ return `(${values.map((value) => this.jsonAll(ctx, slot, [value])).join(' OR ')})`;
712
+ }
713
+ /** An element of the array at `slot` itself, read the way `mode` reads a path: its document, or its root. */
714
+ jsonElemValue(_slot, alias, mode) {
715
+ const doc = this.jsonElemDoc(alias);
716
+ return mode === 'json' ? doc : this.jsonPathExpr(doc, '', mode);
719
717
  }
718
+ /**
719
+ * `$elemMatch`: an element holds `match`, as `$all` reads one value, its operators testing the element
720
+ * itself. An element equal to one value, or to one of several, is containment whatever the operator
721
+ * says, which compares by JSON type and is what an array index serves.
722
+ */
723
+ jsonElemMatch(ctx, slot, match) {
724
+ const keys = Object.keys(match);
725
+ if (keys.some(isOperatorKey) && !keys.every(isOperatorKey)) {
726
+ throw TypeError(`$elemMatch cannot mix operators with field names: ${keys.join(', ')}`);
727
+ }
728
+ const single = keys.length === 1;
729
+ const { $eq: equal, $in: within } = match;
730
+ if (single && isJsonScalar(equal)) {
731
+ return this.jsonAll(ctx, slot, [equal]);
732
+ }
733
+ if (single && Array.isArray(within) && within.length > 0 && within.every(isJsonScalar)) {
734
+ return this.jsonAny(ctx, slot, within);
735
+ }
736
+ return this.jsonAll(ctx, slot, [match]);
737
+ }
738
+ /** The optimizer hint a `$elemMatch` subquery opens with, where the engine plans one wrong without it. */
739
+ jsonElemHint = '';
720
740
  /**
721
741
  * A JSON-encoded bound parameter, cast to the dialect's JSON type. Only the positional-placeholder
722
742
  * dialects use this - PostgreSQL binds JSON through {@link PgLikeSqlDialect.jsonScalarParam} instead.
@@ -799,35 +819,45 @@ export class AbstractSqlDialect extends VectorSqlDialect {
799
819
  ? { key: keyPath, expr: this.escapeId(value.$project), direction: '', output: true }
800
820
  : {
801
821
  key: keyPath,
802
- expr: this.buildFragment(ctx, (fragmentCtx) => this.appendVectorSort(fragmentCtx, meta, key, value)),
822
+ expr: this.buildFragment(ctx, (fragmentCtx) => this.appendVectorDistance(fragmentCtx, meta, key, value)),
803
823
  direction: '',
804
824
  output: false,
805
825
  });
806
826
  continue;
807
827
  }
808
- columns.push({
809
- key: keyPath,
810
- ...this.sortColumn(ctx, meta, key, prefix),
811
- direction: this.resolveSortDirection(value),
828
+ const direction = this.resolveSortDirection(value);
829
+ // A JSON path can sort by more than one reading, each carried under a name of its own.
830
+ this.sortColumns(ctx, meta, key, prefix).forEach((column, index) => {
831
+ columns.push({ key: index ? `${keyPath}:${index}` : keyPath, ...column, direction });
812
832
  });
813
833
  }
814
834
  }
815
835
  /**
816
- * The `ORDER BY` operand for one key. A key that is not a field of `meta` - a `raw()` projection, a
817
- * `$select` alias - is an output alias, which is never table-qualified and needs no resolving.
836
+ * The `ORDER BY` operands for one key: a JSON path's in each of {@link jsonSortModes}. A key that is
837
+ * not a field of `meta` - a `raw()` projection, a `$select` alias - is an output alias, which is never
838
+ * table-qualified and needs no resolving.
818
839
  */
819
- sortColumn(ctx, meta, key, prefix) {
840
+ sortColumns(ctx, meta, key, prefix) {
820
841
  const field = meta.fields[key];
821
842
  if (field) {
822
843
  const expr = this.inlinedOperand(ctx, field, prefix ?? this.resolveTableAlias(meta), meta.entity) ??
823
844
  this.columnWithPrefix(key, field, prefix);
824
- return { expr, output: false };
845
+ return [{ expr, output: false }];
825
846
  }
826
847
  const json = this.resolveJsonDotPath(meta, key, prefix);
827
- return json
828
- ? { expr: this.jsonPathExpr(json.column, json.jsonPath, 'text'), output: false }
829
- : { expr: this.escapeId(key), output: true };
848
+ if (!json) {
849
+ return [{ expr: this.escapeId(key), output: true }];
850
+ }
851
+ return this.jsonSortModes.map((mode) => ({
852
+ expr: this.jsonPathExpr(json.base, json.path, mode),
853
+ output: false,
854
+ }));
830
855
  }
856
+ /**
857
+ * How a JSON path is sorted by: as the JSON value, which the engine orders by type and a number by its
858
+ * value. An engine that orders JSON as text reads a number first, then the text.
859
+ */
860
+ jsonSortModes = ['json'];
831
861
  /**
832
862
  * `LIMIT`/`OFFSET`. `sorted` says whether an `ORDER BY` was emitted just before, which
833
863
  * {@link MergeSqlDialect} needs: SQL Server refuses to page a statement that has none.
@@ -1007,21 +1037,13 @@ export class AbstractSqlDialect extends VectorSqlDialect {
1007
1037
  ]);
1008
1038
  resolveSortDirection(sort) {
1009
1039
  const direction = AbstractSqlDialect.SORT_DIRECTION_MAP.get(sort);
1010
- if (direction === undefined) {
1011
- throw TypeError(`unknown sort direction: ${sort}`);
1012
- }
1013
- return direction;
1040
+ return orRefuse(direction, `unknown sort direction: ${sort}`);
1014
1041
  }
1015
- /** Scalar comparison operators shared by `HAVING` conditions and `$size` comparisons. */
1042
+ /** Every operator of one `HAVING` condition, `AND`-joined. */
1016
1043
  havingCondition(ctx, expr, condition) {
1017
1044
  const ops = this.normalizeWhereValue(condition);
1018
- getKeys(ops).forEach((op, i) => {
1019
- if (i > 0)
1020
- ctx.append(' AND ');
1021
- if (!this.appendOperatorCondition(ctx, expr, op, ops[op])) {
1022
- throw TypeError(`unsupported HAVING operator: ${op}`);
1023
- }
1024
- });
1045
+ const parts = getKeys(ops).map((op) => orRefuse(this.operatorCondition(ctx, expr, op, ops[op]), `unsupported HAVING operator: ${op}`));
1046
+ ctx.append(parts.join(' AND '));
1025
1047
  }
1026
1048
  /**
1027
1049
  * How many rows the filter matched, on every row of the page. A window function runs before
@@ -1386,6 +1408,27 @@ export class AbstractSqlDialect extends VectorSqlDialect {
1386
1408
  jsonPull(ctx, expr, escapedCol, pull) {
1387
1409
  return Object.entries(pull).reduce((acc, [key, value]) => this.jsonPullKey(ctx, acc, escapedCol, key, value), expr);
1388
1410
  }
1411
+ /**
1412
+ * Wrap `expr` so the array at `key` no longer contains `value`: the array rebuilt from the elements that
1413
+ * differ from it, and any other value put back as it is. `JSON_REPLACE` leaves an absent key, and a NULL
1414
+ * column, untouched.
1415
+ */
1416
+ jsonPullKey(ctx, expr, escapedCol, key, value) {
1417
+ const slot = { base: escapedCol, path: key };
1418
+ const elem = this.jsonElemValue(slot, JSON_PULL_ALIAS, 'json');
1419
+ const differs = this.jsonDiffers(elem, this.jsonScalarParam(ctx, value));
1420
+ const kept = `SELECT ${this.jsonArrayOf(elem)} FROM ${this.jsonElemFrom(slot, JSON_PULL_ALIAS)} WHERE ${differs}`;
1421
+ const pulled = `CASE WHEN ${this.jsonIsArray(slot)} THEN (${kept}) ELSE ${this.jsonValue(slot)} END`;
1422
+ return `JSON_REPLACE(${expr}, ${jsonPath(key)}, ${pulled})`;
1423
+ }
1424
+ /** The elements a `$pull` keeps, back in one array, and an empty one where it keeps none. */
1425
+ jsonArrayOf(elem) {
1426
+ return `COALESCE(JSON_ARRAYAGG(${elem}), JSON_ARRAY())`;
1427
+ }
1428
+ /** Whether an element is not the pulled value, both read as JSON. */
1429
+ jsonDiffers(elem, operand) {
1430
+ return `${elem} <> ${operand}`;
1431
+ }
1389
1432
  compileDdl(sql, entity) {
1390
1433
  const ctx = this.createContext({ inlineValues: true });
1391
1434
  if (sql instanceof QueryRaw) {
@@ -1412,12 +1455,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
1412
1455
  escapedPrefix: escapedPrefix ?? this.escapeId(prefix, true, true),
1413
1456
  });
1414
1457
  }
1415
- /**
1416
- * Resolves a dot-notation key to its JSON field metadata.
1417
- * Shared by `where()` and `sort()` to detect 'column.path' keys where 'column' is a JSON/JSONB field.
1418
- *
1419
- * @returns resolved metadata or `undefined` if the key is not a JSON dot-notation path
1420
- */
1458
+ /** A `column.path` key of `meta`'s JSON field as the path it names, shared by `where` and `sort`; else `undefined`. */
1421
1459
  resolveJsonDotPath(meta, key, prefix) {
1422
1460
  const dotIndex = key.indexOf('.');
1423
1461
  if (dotIndex <= 0) {
@@ -1430,66 +1468,17 @@ export class AbstractSqlDialect extends VectorSqlDialect {
1430
1468
  }
1431
1469
  const colName = this.resolveColumnName(root, field);
1432
1470
  const prefixed = (prefix ? this.escapeId(prefix, true, true) : '') + this.escapeId(colName);
1433
- return { jsonPath: key.slice(dotIndex + 1), column: prefixed };
1471
+ return { base: prefixed, path: key.slice(dotIndex + 1) };
1434
1472
  }
1435
1473
  /**
1436
1474
  * One JSON path, read the way `mode` asks for: the one place the three readings are chosen between,
1437
1475
  * so `$where`, `$sort` and every operator reach a path the same way. Public because a JSON index
1438
1476
  * is matched back by its own text, so the migrator's `CREATE INDEX` has to spell it from here too.
1439
1477
  */
1440
- jsonPathExpr(escapedColumn, jsonPath, mode) {
1441
- if (mode === 'json') {
1442
- return this.getJsonPathJsonbExpr(escapedColumn, jsonPath);
1443
- }
1444
- const scalar = this.getJsonPathScalarExpr(escapedColumn, jsonPath);
1445
- return mode === 'numeric' ? this.numericCast(scalar) : scalar;
1446
- }
1447
- /**
1448
- * Compare a JSONB dot-notation path, e.g. `'settings.isArchived': { $ne: true }`.
1449
- * Receives a pre-resolved `resolveJsonDotPath` result to avoid redundant computation.
1450
- */
1451
- compareJsonPath(ctx, resolved, val) {
1452
- const { jsonPath, column } = resolved;
1453
- const accessor = (path, mode) => this.jsonPathExpr(column, path, mode);
1454
- const value = this.normalizeWhereValue(val);
1455
- const operators = getKeys(value);
1456
- if (operators.length > 1) {
1457
- ctx.append('(');
1458
- }
1459
- operators.forEach((op, index) => {
1460
- if (index > 0)
1461
- ctx.append(' AND ');
1462
- const asJson = isJsonbOp(op, value[op]);
1463
- const sql = this.buildJsonFieldCondition(ctx, accessor, jsonPath, op, value[op], asJson);
1464
- if (sql) {
1465
- ctx.append(sql);
1466
- }
1467
- });
1468
- if (operators.length > 1) {
1469
- ctx.append(')');
1470
- }
1471
- }
1472
- /**
1473
- * Returns SQL that extracts a scalar value from a JSON path.
1474
- * Dialects can override this to customize path access syntax while preserving
1475
- * the shared comparison/operator pipeline.
1476
- */
1477
- getJsonPathScalarExpr(escapedColumn, jsonPath) {
1478
- const segments = jsonPath.split('.');
1479
- let expr = escapedColumn;
1480
- for (let i = 0; i < segments.length; i++) {
1481
- const op = i === segments.length - 1 ? '->>' : '->';
1482
- expr = `(${expr}${op}'${escapeSingleQuotes(segments[i])}')`;
1483
- }
1484
- return expr;
1485
- }
1486
- getJsonPathJsonbExpr(escapedColumn, jsonPath) {
1487
- const segments = jsonPath.split('.');
1488
- let expr = escapedColumn;
1489
- for (const segment of segments) {
1490
- expr = `(${expr}->'${escapeSingleQuotes(segment)}')`;
1491
- }
1492
- return expr;
1478
+ jsonPathExpr(escapedColumn, path, mode) {
1479
+ return mode === 'numeric'
1480
+ ? this.numericCast(this.jsonPathReading(escapedColumn, path, 'text'))
1481
+ : this.jsonPathReading(escapedColumn, path, mode);
1493
1482
  }
1494
1483
  /**
1495
1484
  * Normalizes a raw WHERE value into an operator map.
@@ -1685,57 +1674,47 @@ export class AbstractSqlDialect extends VectorSqlDialect {
1685
1674
  }
1686
1675
  /** Filter by relation size: the same subquery, counting instead of testing for existence. */
1687
1676
  compareRelationSize(ctx, entity, relKey, sizeVal, rel, opts) {
1688
- this.buildSizeComparison(ctx, () => this.appendRelationSubquery(ctx, getMeta(entity), relKey, rel, opts, 'COUNT(*)', {}), sizeVal);
1677
+ const count = (fragmentCtx) => this.appendRelationSubquery(fragmentCtx, getMeta(entity), relKey, rel, opts, 'COUNT(*)', {});
1678
+ ctx.append(this.sizeCondition(ctx, count, sizeVal));
1689
1679
  }
1690
1680
  /**
1691
- * `<expr> <op> <value>` for each operator, AND-joined and parenthesized when there is more than
1692
- * one. `exprFn` is re-run per operator because what it appends is an expression, not a column:
1693
- * a `WHERE` has no output alias to refer back to, so the only way to compare it twice is to spell
1694
- * it twice. Shared by `$size`, which counts, and `$near`, which measures a distance.
1681
+ * `<expr> <op> <value>` for each bound, `AND`-joined. `expr` is spelled once per bound because it is an
1682
+ * expression, not a column: a `WHERE` has no output alias to refer back to. Shared by `$size`, which
1683
+ * counts, and `$near`, which measures a distance.
1695
1684
  */
1696
- buildExprComparison(ctx, exprFn, ops, appendOp) {
1697
- const entries = Object.entries(ops).filter(([, v]) => v !== undefined);
1698
- if (entries.length > 1) {
1699
- ctx.append('(');
1700
- }
1701
- entries.forEach(([op, val], index) => {
1702
- if (index > 0) {
1703
- ctx.append(' AND ');
1704
- }
1705
- exprFn();
1706
- appendOp(op, val);
1707
- });
1708
- if (entries.length > 1) {
1709
- ctx.append(')');
1710
- }
1685
+ boundConditions(ctx, expr, bounds, condition, refusal) {
1686
+ const parts = Object.entries(bounds)
1687
+ .filter(([, val]) => val !== undefined)
1688
+ .map(([op, val]) => orRefuse(condition(this.buildFragment(ctx, expr), op, val), `${refusal}: ${op}`));
1689
+ return AbstractSqlDialect.conjunction(parts);
1711
1690
  }
1712
1691
  /**
1713
- * Build a complete `$size` comparison expression.
1714
- * @param sizeExprFn - function that appends the size expression to ctx (e.g. `JSONB_ARRAY_LENGTH("col")`)
1692
+ * A count compared with `size`, a number or its bounds. A count is never NULL, so its equality stays
1693
+ * plain rather than the null-safe `$ne` (`IS DISTINCT FROM`, `IS NOT`): same rows, shorter SQL.
1715
1694
  */
1716
- buildSizeComparison(ctx, sizeExprFn, sizeVal) {
1717
- if (typeof sizeVal === 'number') {
1718
- sizeExprFn();
1719
- ctx.append(' = ');
1720
- ctx.addValue(sizeVal);
1721
- return;
1722
- }
1723
- this.buildExprComparison(ctx, sizeExprFn, sizeVal, (op, val) => this.appendSizeOp(ctx, op, val));
1695
+ sizeCondition(ctx, count, size) {
1696
+ const bounds = typeof size === 'number' ? { $eq: size } : size;
1697
+ return this.boundConditions(ctx, count, bounds, (operand, op, val) => {
1698
+ if (op === '$eq' || op === '$ne') {
1699
+ return `${operand} ${op === '$eq' ? '=' : '<>'} ${this.addValue(ctx, val)}`;
1700
+ }
1701
+ return AbstractSqlDialect.ORDERED_OPS.has(op) ? this.operatorCondition(ctx, operand, op, val) : undefined;
1702
+ }, 'unsupported $size comparison operator');
1724
1703
  }
1725
1704
  /** `<distance> <op> ?`, the `$where` half of a vector search, its bounds checked here since `/http` input is untyped. */
1726
- compareVectorNear(ctx, meta, key, near) {
1705
+ vectorNearCondition(ctx, meta, key, near) {
1727
1706
  const bounds = {};
1728
1707
  for (const [op, val] of Object.entries(near)) {
1729
1708
  if (AbstractSqlDialect.VECTOR_QUERY_KEYS.has(op) || val === undefined) {
1730
1709
  continue;
1731
1710
  }
1732
- if (!AbstractSqlDialect.NEAR_BOUND_OPS.has(op)) {
1711
+ if (!AbstractSqlDialect.ORDERED_OPS.has(op)) {
1733
1712
  throw TypeError(`unsupported $near bound: ${op}`);
1734
1713
  }
1735
1714
  bounds[op] = val;
1736
1715
  }
1737
1716
  if (!hasKeys(bounds)) {
1738
- const boundOps = [...AbstractSqlDialect.NEAR_BOUND_OPS].join(', ');
1717
+ const boundOps = [...AbstractSqlDialect.ORDERED_OPS].join(', ');
1739
1718
  throw TypeError(`$near on '${key}' needs a bound (${boundOps}); without one it filters nothing`);
1740
1719
  }
1741
1720
  // Required by the type, so this only fires for a query that never met it: `/http` casts client
@@ -1746,30 +1725,8 @@ export class AbstractSqlDialect extends VectorSqlDialect {
1746
1725
  throw TypeError(`$near on '${key}' needs its own $vector`);
1747
1726
  }
1748
1727
  const search = { $vector, $distance };
1749
- this.buildExprComparison(ctx, () => this.appendVectorSort(ctx, meta, key, search), bounds, (op, val) => this.appendOperatorCondition(ctx, '', op, val));
1750
- }
1751
- /** The runtime half of {@link QuerySizeComparisonOps}: what a count can sensibly be compared with. */
1752
- static SIZE_COMPARE_OPS = new Set([
1753
- '$eq',
1754
- '$ne',
1755
- '$gt',
1756
- '$gte',
1757
- '$lt',
1758
- '$lte',
1759
- '$between',
1760
- ]);
1761
- /** ` <op> <value>` after a count already written, refusing any operator a count cannot be compared with. */
1762
- appendSizeOp(ctx, op, val) {
1763
- if (!AbstractSqlDialect.SIZE_COMPARE_OPS.has(op)) {
1764
- throw TypeError(`unsupported $size comparison operator: ${op}`);
1765
- }
1766
- // A COUNT is never NULL, so equality stays plain here instead of taking the shared renderer's
1767
- // null-safe `$ne` (`IS DISTINCT FROM` on Postgres, `IS NOT` on SQLite). Same rows, shorter SQL.
1768
- if (op === '$eq' || op === '$ne') {
1769
- ctx.append(` ${op === '$eq' ? '=' : '<>'} ${this.addValue(ctx, val)}`);
1770
- return;
1771
- }
1772
- this.appendOperatorCondition(ctx, '', op, val);
1728
+ const distance = (fragmentCtx) => this.appendVectorDistance(fragmentCtx, meta, key, search);
1729
+ return this.boundConditions(ctx, distance, bounds, (operand, op, val) => this.operatorCondition(ctx, operand, op, val), 'unsupported $near bound');
1773
1730
  }
1774
1731
  /** ANSI-style single-quote escaping. MySQL-family dialects override this for backslash escaping. */
1775
1732
  escape(value) {
@@ -1800,12 +1757,12 @@ export class AbstractSqlDialect extends VectorSqlDialect {
1800
1757
  neExpr(field, ph) {
1801
1758
  return `${field} ${this.neOp} ${ph}`;
1802
1759
  }
1803
- /** `operand IN (...)` binding each value, or the constant an empty set reduces to: no value is in it. */
1804
- formatIn(ctx, operand, values, negate) {
1760
+ /** `operand IN (...)` of each value as `bind` renders it, or the constant an empty set reduces to: no value is in it. */
1761
+ formatIn(_ctx, operand, values, negate, bind) {
1805
1762
  if (!values.length) {
1806
1763
  return negate ? '1 = 1' : '1 = 0';
1807
1764
  }
1808
- const phs = values.map((v) => this.addValue(ctx, v)).join(', ');
1765
+ const phs = values.map(bind).join(', ');
1809
1766
  return `${operand} ${negate ? 'NOT IN' : 'IN'} (${phs})`;
1810
1767
  }
1811
1768
  toString() {