uql-orm 0.79.0 → 0.81.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/dist/browser/uql-browser.min.js.map +2 -2
  2. package/dist/cockroachdb/cockroachDialect.js +5 -1
  3. package/dist/dialect/abstractDialect.d.ts +1 -31
  4. package/dist/dialect/abstractDialect.js +3 -27
  5. package/dist/dialect/abstractSqlDialect.d.ts +25 -56
  6. package/dist/dialect/abstractSqlDialect.js +78 -145
  7. package/dist/dialect/aliases.d.ts +5 -0
  8. package/dist/dialect/aliases.js +5 -0
  9. package/dist/dialect/mysqlLikeSqlDialect.d.ts +4 -2
  10. package/dist/dialect/mysqlLikeSqlDialect.js +14 -1
  11. package/dist/dialect/operators.d.ts +66 -0
  12. package/dist/dialect/operators.js +129 -0
  13. package/dist/dialect/pgLikeSqlDialect.d.ts +4 -1
  14. package/dist/dialect/pgLikeSqlDialect.js +16 -3
  15. package/dist/dialect/vectorSqlDialect.d.ts +2 -0
  16. package/dist/dialect/vectorSqlDialect.js +4 -0
  17. package/dist/entity/decorator/entity.d.ts +6 -1
  18. package/dist/entity/decorator/entity.js +12 -1
  19. package/dist/entity/index.d.ts +1 -1
  20. package/dist/entity/index.js +1 -1
  21. package/dist/entity/metadata/definition.d.ts +6 -1
  22. package/dist/entity/metadata/definition.js +19 -0
  23. package/dist/migrate/builder/expressions.d.ts +2 -0
  24. package/dist/migrate/builder/expressions.js +24 -0
  25. package/dist/migrate/cli.js +1 -1
  26. package/dist/migrate/codegen/entityCodeGenerator.js +2 -2
  27. package/dist/migrate/codegen/entityTypes.js +1 -2
  28. package/dist/migrate/codegen/indexDecoratorSource.d.ts +3 -2
  29. package/dist/migrate/codegen/indexDecoratorSource.js +5 -23
  30. package/dist/migrate/ddl/mssqlIndexDdl.d.ts +5 -0
  31. package/dist/migrate/ddl/mssqlIndexDdl.js +10 -0
  32. package/dist/migrate/ddl/mssqlTableDdl.d.ts +6 -4
  33. package/dist/migrate/ddl/mssqlTableDdl.js +25 -14
  34. package/dist/migrate/ddl/mysqlIndexDdl.d.ts +2 -2
  35. package/dist/migrate/ddl/mysqlIndexDdl.js +7 -6
  36. package/dist/migrate/ddl/pgIndexDdl.d.ts +2 -1
  37. package/dist/migrate/ddl/pgIndexDdl.js +8 -6
  38. package/dist/migrate/ddl/tableDdl.d.ts +5 -2
  39. package/dist/migrate/ddl/tableDdl.js +13 -7
  40. package/dist/migrate/drift/driftDetector.d.ts +4 -5
  41. package/dist/migrate/drift/driftDetector.js +21 -21
  42. package/dist/migrate/generator/definitionToNode.d.ts +1 -1
  43. package/dist/migrate/generator/definitionToNode.js +9 -20
  44. package/dist/migrate/generator/mongoSchemaGenerator.d.ts +5 -1
  45. package/dist/migrate/generator/mongoSchemaGenerator.js +20 -18
  46. package/dist/migrate/index.d.ts +2 -1
  47. package/dist/migrate/index.js +1 -0
  48. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.d.ts +16 -6
  49. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +18 -4
  50. package/dist/migrate/introspection/baseSqlIntrospector.js +7 -18
  51. package/dist/migrate/introspection/mongoIntrospector.d.ts +3 -1
  52. package/dist/migrate/introspection/mongoIntrospector.js +7 -3
  53. package/dist/migrate/introspection/mssqlIntrospector.d.ts +1 -0
  54. package/dist/migrate/introspection/mssqlIntrospector.js +9 -0
  55. package/dist/migrate/introspection/mysqlIntrospector.d.ts +16 -5
  56. package/dist/migrate/introspection/mysqlIntrospector.js +39 -2
  57. package/dist/migrate/introspection/postgresIntrospector.d.ts +30 -21
  58. package/dist/migrate/introspection/postgresIntrospector.js +70 -43
  59. package/dist/migrate/introspection/sqliteIntrospector.d.ts +1 -0
  60. package/dist/migrate/introspection/sqliteIntrospector.js +13 -8
  61. package/dist/migrate/migrator.d.ts +33 -1
  62. package/dist/migrate/migrator.js +111 -43
  63. package/dist/migrate/schemaChange.d.ts +18 -0
  64. package/dist/migrate/schemaChange.js +37 -0
  65. package/dist/migrate/schemaGenerator.d.ts +25 -15
  66. package/dist/migrate/schemaGenerator.js +130 -182
  67. package/dist/migrate/storage/databaseStorage.d.ts +4 -0
  68. package/dist/migrate/storage/databaseStorage.js +14 -8
  69. package/dist/migrate/triggerSql.d.ts +24 -0
  70. package/dist/migrate/triggerSql.js +229 -0
  71. package/dist/mongo/mongoDialect.d.ts +0 -21
  72. package/dist/mongo/mongoDialect.js +105 -100
  73. package/dist/mongo/mongodbQuerier.js +17 -1
  74. package/dist/mssql/mssqlDialect.d.ts +18 -7
  75. package/dist/mssql/mssqlDialect.js +77 -33
  76. package/dist/mssql/mssqlQuerier.js +2 -2
  77. package/dist/schema/canonicalType.d.ts +6 -1
  78. package/dist/schema/canonicalType.js +14 -0
  79. package/dist/schema/indexDifferences.d.ts +22 -6
  80. package/dist/schema/indexDifferences.js +23 -8
  81. package/dist/schema/matchByKey.d.ts +10 -0
  82. package/dist/schema/matchByKey.js +18 -0
  83. package/dist/schema/schemaAST.d.ts +6 -2
  84. package/dist/schema/schemaAST.js +7 -3
  85. package/dist/schema/schemaASTBuilder.d.ts +4 -8
  86. package/dist/schema/schemaASTBuilder.js +20 -29
  87. package/dist/schema/schemaASTDiffer.d.ts +2 -3
  88. package/dist/schema/schemaASTDiffer.js +15 -36
  89. package/dist/schema/types.d.ts +14 -15
  90. package/dist/sqlite/sqliteDialect.d.ts +1 -1
  91. package/dist/sqlite/sqliteDialect.js +12 -3
  92. package/dist/type/dialect.d.ts +69 -9
  93. package/dist/type/entity.d.ts +96 -3
  94. package/dist/type/migration.d.ts +43 -44
  95. package/dist/type/query.d.ts +13 -4
  96. package/dist/type/queryWhere.d.ts +4 -2
  97. package/dist/util/ddlExpression.util.d.ts +5 -1
  98. package/dist/util/ddlExpression.util.js +6 -2
  99. package/dist/util/field.util.d.ts +9 -1
  100. package/dist/util/field.util.js +14 -2
  101. package/dist/util/fieldOption.util.d.ts +2 -2
  102. package/dist/util/fieldOption.util.js +2 -2
  103. package/dist/util/raw.d.ts +9 -1
  104. package/dist/util/raw.js +36 -11
  105. package/dist/util/sql.util.d.ts +12 -0
  106. package/dist/util/sql.util.js +24 -3
  107. package/dist/util/uqlError.d.ts +2 -0
  108. package/dist/util/uqlError.js +4 -0
  109. package/package.json +4 -4
  110. package/skills/uql-orm/SKILL.md +11 -6
@@ -1,11 +1,12 @@
1
1
  import { fieldOf, getMeta, relationOf, soleIdOf } from '../entity/index.js';
2
- import { COUNT_RESULT_KEY, parseQueryLock, QueryRaw, RAW_ALIAS, VECTOR_QUERY_KEYS, } from '../type/index.js';
2
+ import { COUNT_RESULT_KEY, parseQueryLock, QueryRaw, RAW_ALIAS, } from '../type/index.js';
3
3
  import { isInlinedExpression } from '../util/field.util.js';
4
4
  import { asSelectMap, assertNonNegativeInteger, escapeSqlId, fillOnFields, filterFieldKeys, getInsertFieldKeys, getKeys, getRelationRequestSummary, getSoftDeleteValue, hasKeys, idOnlyQuery, columnFamily, countedRelations, fieldUpdateOf, fulltextIndexOver, fulltextWeights, isFieldUpdateOp, isJsonObject, isJsonUpdateOp, isOperatorMap, isOperatorKey, isVectorSearch, normalizeScalarFieldSelection, parentJoins, rankedTextSearch, targetKeyColumns, textSearchFields, textSortOf, textWeightSteps, parseGroupMap, parseRelationAtKey, parseRelationSize, populatesRelations, aggregateOf, raw, refs, throwUnknownAggregateColumn, withoutSoftDeleteFilter, } from '../util/index.js';
5
5
  import { escapeAnsiSqlLiteral } from '../util/sqlLiteral.js';
6
- import { UqlUsageError } from '../util/uqlError.js';
6
+ import { kindOf, UqlUsageError } from '../util/uqlError.js';
7
7
  import { AGGREGATE_PAGE_ALIAS, AGGREGATE_VALUE_ALIAS, ROWS_ALIAS, JSON_ELEM_ALIAS, JSON_PULL_ALIAS, relationSortColumn, } from './aliases.js';
8
8
  import { holdsOperator, isJsonScalar, jsonCompareMode, jsonElemExists, jsonPath, } from './jsonSql.js';
9
+ import { betweenBounds, COMPARE_OPS, EQUALITY_OPS, GROUP_OPS, groupClauses, inOperands, isGroupOp, isOrderedOp, isVectorQuery, LIKE_OPS, ORDERED_OPS, VECTOR_QUERY_KEY_SET, whereOperators, } from './operators.js';
9
10
  import { SqlQueryContext } from './queryContext.js';
10
11
  import { groupPathField, NO_JOINS, aggregateColumnField, resolveGroupJoins, resolveQueryJoins, relationSortTerms, resolveSortableJoin, } from './queryJoins.js';
11
12
  import { resolveVectorCast } from './vectorCast.js';
@@ -33,14 +34,43 @@ function orRefuse(value, refusal) {
33
34
  }
34
35
  return value;
35
36
  }
36
- /** An `$in`/`$nin` operand, which the types require to be an array but `/http` hands over untyped. */
37
- function inOperands(op, value) {
38
- if (!Array.isArray(value)) {
39
- throw new UqlUsageError(`${op} expects an array, got ${value === null ? 'null' : typeof value}`);
37
+ /** Conditions joined by `AND`, parenthesized where there is more than one. */
38
+ function conjunction(parts) {
39
+ return parts.length > 1 ? `(${parts.join(' AND ')})` : parts.join('');
40
+ }
41
+ /**
42
+ * How `op` reads a JSON value: an ordered comparison as a number, an equality as its operand compares,
43
+ * and a pattern or a null check as text.
44
+ */
45
+ function jsonOperatorMode(op, value) {
46
+ if (ORDERED_OPS.has(op)) {
47
+ return 'numeric';
40
48
  }
41
- return value;
49
+ return EQUALITY_OPS.has(op) ? jsonCompareMode(value) : 'text';
42
50
  }
51
+ /** `$group` aggregate operator to SQL function name, over ops `resolveAggregateOp` has already allowlisted. */
52
+ const AGGREGATE_FN = {
53
+ $count: 'COUNT',
54
+ $sum: 'SUM',
55
+ $avg: 'AVG',
56
+ $min: 'MIN',
57
+ $max: 'MAX',
58
+ };
59
+ const SORT_DIRECTION_MAP = new Map([
60
+ [1, {}],
61
+ ['asc', {}],
62
+ ['desc', { direction: ' DESC' }],
63
+ [-1, { direction: ' DESC' }],
64
+ ['ascNullsFirst', { nulls: 'first' }],
65
+ ['ascNullsLast', { nulls: 'last' }],
66
+ ['descNullsFirst', { direction: ' DESC', nulls: 'first' }],
67
+ ['descNullsLast', { direction: ' DESC', nulls: 'last' }],
68
+ ]);
43
69
  export class AbstractSqlDialect extends VectorSqlDialect {
70
+ /** Itself, unless the engine is a fork running another's SQL, which is the only case that overrides. */
71
+ get dialectFamily() {
72
+ return this.dialectName;
73
+ }
44
74
  /**
45
75
  * How this engine declares a namespace, so a generated migration creates the schemas its tables
46
76
  * need before creating them. Only reached where {@link DialectFeatures.schemas} is on. MySQL and
@@ -142,10 +172,14 @@ export class AbstractSqlDialect extends VectorSqlDialect {
142
172
  const expression = this.returningIdExpression(meta);
143
173
  return expression ? `RETURNING ${expression}` : '';
144
174
  }
175
+ /** What the returned row is read off, for an engine that names it: SQL Server's `INSERTED.`. */
176
+ returnedRowPrefix = '';
145
177
  /** `<id column> AS id` on its own, for a statement composing a `RETURNING` list of several items. */
146
178
  returningIdExpression(meta) {
147
179
  const [idKey] = meta.ids;
148
- return meta.ids.length === 1 ? `${this.escapeId(this.columnOf(meta, idKey))} ${this.escapeId('id')}` : '';
180
+ return meta.ids.length === 1
181
+ ? `${this.returnedRowPrefix}${this.escapeId(this.columnOf(meta, idKey))} ${this.escapeId('id')}`
182
+ : '';
149
183
  }
150
184
  search(ctx, entity, q = {}, opts = {}, joins = NO_JOINS, order) {
151
185
  const meta = getMeta(entity);
@@ -258,7 +292,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
258
292
  }
259
293
  /** The columns a `$text` over `keys` reads, qualified by `prefix` where the statement joins, as any column is. */
260
294
  textColumns(meta, keys, prefix) {
261
- return keys.map((key) => this.columnWithPrefix(key, meta.fields[key], prefix));
295
+ return keys.map((key) => this.columnWithPrefix(key, meta.fields[key], { prefix }));
262
296
  }
263
297
  /** Ranks by the root `$text` of `where`, which is looked up only once a `$sort` asks for it. */
264
298
  textRanker(meta, where, prefix) {
@@ -447,14 +481,14 @@ export class AbstractSqlDialect extends VectorSqlDialect {
447
481
  }
448
482
  this.getComparisonKey(ctx, entity, key, opts);
449
483
  ctx.append(' = ');
450
- this.getRawValue(ctx, { value: val, prefix: opts.prefix });
484
+ this.getRawValue(ctx, { ...opts, value: val });
451
485
  return;
452
486
  }
453
487
  if (key === '$text') {
454
488
  this.appendTextSearch(ctx, meta, val, opts.prefix);
455
489
  return;
456
490
  }
457
- if (AbstractSqlDialect.isGroupOp(key)) {
491
+ if (isGroupOp(key)) {
458
492
  this.compareLogicalOperator(ctx, entity, key, val, opts);
459
493
  return;
460
494
  }
@@ -477,27 +511,23 @@ export class AbstractSqlDialect extends VectorSqlDialect {
477
511
  this.compareRelation(ctx, entity, key, val, rel, opts);
478
512
  return;
479
513
  }
480
- const value = this.normalizeWhereValue(val);
481
- const parts = getKeys(value).map((op) => this.fieldCondition(ctx, entity, key, op, value[op], opts));
482
- ctx.append(AbstractSqlDialect.conjunction(parts));
483
- }
484
- /** Conditions joined by `AND`, parenthesized where there is more than one. */
485
- static conjunction(parts) {
486
- return parts.length > 1 ? `(${parts.join(' AND ')})` : parts.join('');
514
+ const parts = whereOperators(val, 'unknown operator').map(([op, value]) => this.fieldCondition(ctx, entity, key, op, value, opts));
515
+ ctx.append(conjunction(parts));
487
516
  }
488
517
  compareLogicalOperator(ctx, entity, key, val, opts) {
489
- const { join, negate } = AbstractSqlDialect.GROUP_OPS[key];
490
- const items = AbstractSqlDialect.groupClauses(key, val);
518
+ const { join, negate } = GROUP_OPS[key];
519
+ const items = groupClauses(key, val);
491
520
  // With more than one item each is an operand of the operator joining them, so a compound item
492
521
  // parenthesizes itself and precedence never applies; a lone item is this group verbatim, so it
493
522
  // inherits the group's own position. A negation always makes its subject an operand.
494
523
  const childOperand = items.length > 1 || negate || opts.operand;
495
524
  const parts = this.renderOperands(ctx, items, (fragmentCtx, entry) => {
525
+ // The same scope as the group, so every render option carries over: a trigger's `NEW.` included.
496
526
  if (entry instanceof QueryRaw) {
497
- this.getRawValue(fragmentCtx, { value: entry, prefix: opts.prefix });
527
+ this.getRawValue(fragmentCtx, { ...opts, value: entry });
498
528
  }
499
529
  else {
500
- this.renderWhere(fragmentCtx, entity, entry, { prefix: opts.prefix, operand: childOperand, clause: false });
530
+ this.renderWhere(fragmentCtx, entity, entry, { ...opts, operand: childOperand, clause: false });
501
531
  }
502
532
  });
503
533
  if (!parts.length) {
@@ -509,40 +539,6 @@ export class AbstractSqlDialect extends VectorSqlDialect {
509
539
  }
510
540
  /** Memoizes {@link escapedColumnName}; see there for why it is per dialect instance. */
511
541
  escapedColumns = new WeakMap();
512
- static COMPARE_OP_MAP = new Map([
513
- ['$gt', ' > '],
514
- ['$gte', ' >= '],
515
- ['$lt', ' < '],
516
- ['$lte', ' <= '],
517
- ]);
518
- /** What a `$near` says about the search itself; everything else in it is a bound. */
519
- static VECTOR_QUERY_KEYS = new Set(VECTOR_QUERY_KEYS);
520
- /**
521
- * The ordered comparisons, `QueryOrderedOp` at runtime, derived from the map above rather than spelled
522
- * again: {@link QueryVectorNear}'s bounds, so `$near` never accepts one the renderer has no operator
523
- * for, and the operators that read a JSON path as a number.
524
- */
525
- static ORDERED_OPS = new Set([
526
- ...AbstractSqlDialect.COMPARE_OP_MAP.keys(),
527
- '$between',
528
- ]);
529
- /** The operators an equality compares by value, which a JSON path reads the way that value compares. */
530
- static EQUALITY_OPS = new Set(['$eq', '$ne', '$in', '$nin']);
531
- /**
532
- * Every `$like`-family operator: the pattern it wraps its value in, and whether it ignores case.
533
- * Each case-sensitive operator is paired here with the `$i` twin that shares its pattern, so the
534
- * two can never drift apart - and neither one decides case folding, which is
535
- * {@link caseInsensitiveMatch}'s single call.
536
- */
537
- static LIKE_OPS = new Map([
538
- ['$like', '$ilike', (v) => v],
539
- ['$startsWith', '$istartsWith', (v) => `${v}%`],
540
- ['$endsWith', '$iendsWith', (v) => `%${v}`],
541
- ['$includes', '$iincludes', (v) => `%${v}%`],
542
- ].flatMap(([sensitive, insensitive, pattern]) => [
543
- [sensitive, { pattern, insensitive: false }],
544
- [insensitive, { pattern, insensitive: true }],
545
- ]));
546
542
  /**
547
543
  * How the engine matches case-insensitively: `ilike` has the operator, `native` ignores case already
548
544
  * (SQLite, where folding in JS would break non-ASCII), and `fold` lowers both sides.
@@ -554,7 +550,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
554
550
  * compared against.
555
551
  */
556
552
  likeCondition(ctx, operand, op, val) {
557
- const like = AbstractSqlDialect.LIKE_OPS.get(op);
553
+ const like = LIKE_OPS.get(op);
558
554
  if (!like) {
559
555
  return undefined;
560
556
  }
@@ -565,8 +561,8 @@ export class AbstractSqlDialect extends VectorSqlDialect {
565
561
  return `${fold ? `LOWER(${operand})` : operand} ${matchOp} ${ph}`;
566
562
  }
567
563
  /** Builds `prefix.column` from an already-resolved field, through the same memo writes use. */
568
- columnWithPrefix(key, field, prefix) {
569
- return this.escapeId(prefix, true, true) + this.escapedColumnOf(key, field);
564
+ columnWithPrefix(key, field, opts) {
565
+ return (opts.escapedPrefix ?? this.escapeId(opts.prefix, true, true)) + this.escapedColumnOf(key, field);
570
566
  }
571
567
  /**
572
568
  * The SQL a field comparison reads its left-hand side from. An inlined field builds its expression
@@ -577,7 +573,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
577
573
  const meta = getMeta(entity);
578
574
  const field = meta.fields[key];
579
575
  return (this.inlinedOperand(ctx, field, opts.prefix ?? this.resolveTableAlias(meta), entity) ??
580
- this.columnWithPrefix(key, field, opts.prefix));
576
+ this.columnWithPrefix(key, field, opts));
581
577
  }
582
578
  /**
583
579
  * The expression an inlined computed field stands for, or nothing when the field is a real column.
@@ -608,10 +604,11 @@ export class AbstractSqlDialect extends VectorSqlDialect {
608
604
  /**
609
605
  * `<operand> <op> <value>` for every operator that needs only its left-hand SQL, shared by a column, a
610
606
  * JSON path, a `HAVING` expression, a count and a distance; `undefined` for the rest. `bind` renders
611
- * each compared value, a plain placeholder unless a JSON path reads it otherwise.
607
+ * each compared value, a plain placeholder unless a JSON path reads it otherwise. NULL compares as the
608
+ * engine compares it: `<>`, `NOT IN` and `NOT` are unknown on a NULL, which SQL drops.
612
609
  */
613
610
  operatorCondition(ctx, operand, op, val, bind = (value) => this.addValue(ctx, value)) {
614
- const compareOp = AbstractSqlDialect.COMPARE_OP_MAP.get(op);
611
+ const compareOp = COMPARE_OPS.get(op);
615
612
  if (compareOp) {
616
613
  return `${operand}${compareOp}${bind(val)}`;
617
614
  }
@@ -623,14 +620,14 @@ export class AbstractSqlDialect extends VectorSqlDialect {
623
620
  case '$eq':
624
621
  return val === null ? `${operand} IS NULL` : `${operand} = ${bind(val)}`;
625
622
  case '$ne':
626
- return val === null ? `${operand} IS NOT NULL` : this.neExpr(operand, bind(val));
623
+ return val === null ? `${operand} IS NOT NULL` : `${operand} <> ${bind(val)}`;
627
624
  case '$regex':
628
625
  return this.regexCondition(operand, this.addValue(ctx, val));
629
626
  case '$in':
630
627
  case '$nin':
631
628
  return this.formatIn(ctx, operand, inOperands(op, val), op === '$nin', bind);
632
629
  case '$between': {
633
- const [min, max] = val;
630
+ const [min, max] = betweenBounds(val);
634
631
  return `${operand} BETWEEN ${bind(min)} AND ${bind(max)}`;
635
632
  }
636
633
  case '$isNull':
@@ -660,8 +657,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
660
657
  }
661
658
  /** Every operator `target` is compared with, `AND`-joined. */
662
659
  jsonConditions(ctx, target, val) {
663
- const value = this.normalizeWhereValue(val);
664
- return AbstractSqlDialect.conjunction(getKeys(value).map((op) => this.jsonCondition(ctx, target, op, value[op])));
660
+ return conjunction(whereOperators(val, 'unknown operator').map(([op, value]) => this.jsonCondition(ctx, target, op, value)));
665
661
  }
666
662
  jsonCondition(ctx, target, op, value) {
667
663
  if (op === '$not') {
@@ -671,7 +667,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
671
667
  if (array !== undefined) {
672
668
  return array;
673
669
  }
674
- const mode = AbstractSqlDialect.jsonOperatorMode(op, value);
670
+ const mode = jsonOperatorMode(op, value);
675
671
  const operand = target.read(mode);
676
672
  // Only a boolean compares as a JSON value, so the set holds two at most, and MySQL documents `IN()`
677
673
  // as unsupported on JSON values: the comparisons are spelled out.
@@ -683,16 +679,6 @@ export class AbstractSqlDialect extends VectorSqlDialect {
683
679
  const condition = this.operatorCondition(ctx, operand, op, value, (val) => this.jsonOperand(ctx, val, mode));
684
680
  return orRefuse(condition, `unknown operator: ${op}`);
685
681
  }
686
- /**
687
- * How `op` reads a JSON value: an ordered comparison as a number, an equality as its operand compares,
688
- * and a pattern or a null check as text.
689
- */
690
- static jsonOperatorMode(op, value) {
691
- if (AbstractSqlDialect.ORDERED_OPS.has(op)) {
692
- return 'numeric';
693
- }
694
- return AbstractSqlDialect.EQUALITY_OPS.has(op) ? jsonCompareMode(value) : 'text';
695
- }
696
682
  /** A bound operand of a JSON comparison, read the way `mode` reads the value it is compared with. */
697
683
  jsonOperand(ctx, value, mode) {
698
684
  if (mode === 'json') {
@@ -731,7 +717,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
731
717
  : [this.jsonElemEquals(ctx, slot, alias, value)];
732
718
  return jsonElemExists(from, holds, this.jsonElemHint);
733
719
  });
734
- return AbstractSqlDialect.conjunction(conditions);
720
+ return conjunction(conditions);
735
721
  }
736
722
  /**
737
723
  * What the JSON `target` reads satisfies to hold `value`: an array each of its elements, an object each
@@ -897,7 +883,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
897
883
  const field = meta.fields[key];
898
884
  if (field) {
899
885
  const expr = this.inlinedOperand(ctx, field, prefix ?? this.resolveTableAlias(meta), meta.entity) ??
900
- this.columnWithPrefix(key, field, prefix);
886
+ this.columnWithPrefix(key, field, { prefix });
901
887
  return [{ expr, output: false }];
902
888
  }
903
889
  const json = this.resolveJsonDotPath(meta, key, prefix);
@@ -1007,14 +993,6 @@ export class AbstractSqlDialect extends VectorSqlDialect {
1007
993
  estimatedCount(_ctx, _entity) {
1008
994
  throw new UqlUsageError(`${this.dialectName} does not support estimatedCount`);
1009
995
  }
1010
- /** `$group` aggregate operator to SQL function name, over ops `resolveAggregateOp` has already allowlisted. */
1011
- static AGGREGATE_FN = {
1012
- $count: 'COUNT',
1013
- $sum: 'SUM',
1014
- $avg: 'AVG',
1015
- $min: 'MIN',
1016
- $max: 'MAX',
1017
- };
1018
996
  aggregate(ctx, entity, q, opts = {}) {
1019
997
  const meta = getMeta(entity);
1020
998
  const entries = parseGroupMap(q.$group, q.$select);
@@ -1121,18 +1099,8 @@ export class AbstractSqlDialect extends VectorSqlDialect {
1121
1099
  this.havingCondition(ctx, this.aggregateRef(emittedColumns, alias, '$having'), condition);
1122
1100
  });
1123
1101
  }
1124
- static SORT_DIRECTION_MAP = new Map([
1125
- [1, {}],
1126
- ['asc', {}],
1127
- ['desc', { direction: ' DESC' }],
1128
- [-1, { direction: ' DESC' }],
1129
- ['ascNullsFirst', { nulls: 'first' }],
1130
- ['ascNullsLast', { nulls: 'last' }],
1131
- ['descNullsFirst', { direction: ' DESC', nulls: 'first' }],
1132
- ['descNullsLast', { direction: ' DESC', nulls: 'last' }],
1133
- ]);
1134
1102
  resolveSortDirection(sort) {
1135
- const order = AbstractSqlDialect.SORT_DIRECTION_MAP.get(sort);
1103
+ const order = SORT_DIRECTION_MAP.get(sort);
1136
1104
  return orRefuse(order, `unknown sort direction: ${sort}`);
1137
1105
  }
1138
1106
  /**
@@ -1155,8 +1123,8 @@ export class AbstractSqlDialect extends VectorSqlDialect {
1155
1123
  }
1156
1124
  /** Every operator of one `HAVING` condition, `AND`-joined. */
1157
1125
  havingCondition(ctx, expr, condition) {
1158
- const ops = this.normalizeWhereValue(condition);
1159
- const parts = getKeys(ops).map((op) => orRefuse(this.operatorCondition(ctx, expr, op, ops[op]), `unsupported HAVING operator: ${op}`));
1126
+ const refusal = 'unsupported HAVING operator';
1127
+ const parts = whereOperators(condition, refusal).map(([op, value]) => orRefuse(this.operatorCondition(ctx, expr, op, value), `${refusal}: ${op}`));
1160
1128
  ctx.append(parts.join(' AND '));
1161
1129
  }
1162
1130
  /**
@@ -1620,17 +1588,6 @@ export class AbstractSqlDialect extends VectorSqlDialect {
1620
1588
  ? this.numericCast(this.jsonPathReading(escapedColumn, path, 'text'))
1621
1589
  : this.jsonPathReading(escapedColumn, path, mode);
1622
1590
  }
1623
- /**
1624
- * Normalizes a raw WHERE value into an operator map.
1625
- * Arrays become `$in`, operator maps pass through, everything else becomes `$eq`.
1626
- */
1627
- normalizeWhereValue(val) {
1628
- if (Array.isArray(val))
1629
- return { $in: val };
1630
- if (isOperatorMap(val))
1631
- return val;
1632
- return { $eq: val };
1633
- }
1634
1591
  /**
1635
1592
  * A field key's mapped column (`@Field({ name })`), escaped, memoized per dialect instance: field
1636
1593
  * metadata is shared between dialects while this result is not, since `escapeIdChar` and the naming
@@ -1759,7 +1716,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
1759
1716
  }
1760
1717
  /** One aggregate function call, the one spelling every statement that aggregates writes. */
1761
1718
  aggregateFn(op, operand, distinct) {
1762
- return `${AbstractSqlDialect.AGGREGATE_FN[op]}(${distinct ? 'DISTINCT ' : ''}${operand})`;
1719
+ return `${AGGREGATE_FN[op]}(${distinct ? 'DISTINCT ' : ''}${operand})`;
1763
1720
  }
1764
1721
  /**
1765
1722
  * One equality per key of the parent, anded: a composite correlates on every column, and matching on
@@ -1901,45 +1858,31 @@ export class AbstractSqlDialect extends VectorSqlDialect {
1901
1858
  const parts = Object.entries(bounds)
1902
1859
  .filter(([, val]) => val !== undefined)
1903
1860
  .map(([op, val]) => orRefuse(condition(this.buildFragment(ctx, expr), op, val), `${refusal}: ${op}`));
1904
- return AbstractSqlDialect.conjunction(parts);
1861
+ return conjunction(parts);
1905
1862
  }
1906
- /**
1907
- * A count compared with `size`, a number or its bounds. A count is never NULL, so its equality stays
1908
- * plain rather than the null-safe `$ne` (`IS DISTINCT FROM`, `IS NOT`): same rows, shorter SQL.
1909
- */
1863
+ /** A count compared with `size`, a number or its bounds. */
1910
1864
  sizeCondition(ctx, count, size) {
1911
1865
  const bounds = typeof size === 'number' ? { $eq: size } : size;
1912
- return this.boundConditions(ctx, count, bounds, (operand, op, val) => {
1913
- if (op === '$eq' || op === '$ne') {
1914
- return `${operand} ${op === '$eq' ? '=' : '<>'} ${this.addValue(ctx, val)}`;
1915
- }
1916
- return AbstractSqlDialect.ORDERED_OPS.has(op) ? this.operatorCondition(ctx, operand, op, val) : undefined;
1917
- }, 'unsupported $size comparison operator');
1866
+ return this.boundConditions(ctx, count, bounds, (operand, op, val) => op === '$eq' || op === '$ne' || isOrderedOp(op) ? this.operatorCondition(ctx, operand, op, val) : undefined, 'unsupported $size comparison operator');
1918
1867
  }
1919
1868
  /** `<distance> <op> ?`, the `$where` half of a vector search, its bounds checked here since `/http` input is untyped. */
1920
1869
  vectorNearCondition(ctx, meta, key, near, prefix) {
1921
- const bounds = {};
1922
- for (const [op, val] of Object.entries(near)) {
1923
- if (AbstractSqlDialect.VECTOR_QUERY_KEYS.has(op) || val === undefined) {
1924
- continue;
1925
- }
1926
- if (!AbstractSqlDialect.ORDERED_OPS.has(op)) {
1927
- throw new UqlUsageError(`unsupported $near bound: ${op}`);
1928
- }
1929
- bounds[op] = val;
1870
+ if (!isOperatorMap(near)) {
1871
+ throw new UqlUsageError(`$near on '${key}' expects an object of its $vector and bounds, got ${kindOf(near)}`);
1930
1872
  }
1873
+ const bounds = Object.fromEntries(Object.entries(near).filter(([op, val]) => !VECTOR_QUERY_KEY_SET.has(op) && val !== undefined));
1931
1874
  if (!hasKeys(bounds)) {
1932
- const boundOps = [...AbstractSqlDialect.ORDERED_OPS].join(', ');
1875
+ const boundOps = [...ORDERED_OPS].join(', ');
1933
1876
  throw new UqlUsageError(`$near on '${key}' needs a bound (${boundOps}); without one it filters nothing`);
1934
1877
  }
1935
1878
  // Required by the type, so this only fires for a query that never met it: `/http` casts client
1936
1879
  // JSON straight to `Query`. A `$near` never borrows the `$sort`'s vector, which is what keeps the
1937
1880
  // predicate meaning the same thing in a `count`, or in an entity filter merged into a `$where`.
1938
- if (!near.$vector) {
1881
+ if (!isVectorQuery(near)) {
1939
1882
  throw new UqlUsageError(`$near on '${key}' needs its own $vector`);
1940
1883
  }
1941
1884
  const distance = (fragmentCtx) => this.appendVectorDistance(fragmentCtx, meta, key, near, prefix);
1942
- return this.boundConditions(ctx, distance, bounds, (operand, op, val) => this.operatorCondition(ctx, operand, op, val), 'unsupported $near bound');
1885
+ return this.boundConditions(ctx, distance, bounds, (operand, op, val) => (isOrderedOp(op) ? this.operatorCondition(ctx, operand, op, val) : undefined), 'unsupported $near bound');
1943
1886
  }
1944
1887
  /** ANSI-style single-quote escaping. MySQL-family dialects override this for backslash escaping. */
1945
1888
  escape(value) {
@@ -1960,16 +1903,6 @@ export class AbstractSqlDialect extends VectorSqlDialect {
1960
1903
  get likeFn() {
1961
1904
  return 'LIKE';
1962
1905
  }
1963
- /**
1964
- * Not-equal operator token for non-null comparisons.
1965
- * Postgres uses `IS DISTINCT FROM`; MySQL/Maria uses custom `neExpr`.
1966
- */
1967
- get neOp() {
1968
- return '<>';
1969
- }
1970
- neExpr(field, ph) {
1971
- return `${field} ${this.neOp} ${ph}`;
1972
- }
1973
1906
  /** `operand IN (...)` of each value as `bind` renders it, or the constant an empty set reduces to: no value is in it. */
1974
1907
  formatIn(_ctx, operand, values, negate, bind) {
1975
1908
  if (!values.length) {
@@ -1,4 +1,9 @@
1
1
  import type { RelationAggregateSpec } from '../type/index.js';
2
+ /**
3
+ * What every schema object uql creates is named from, so a hand-written trigger or function is never
4
+ * offered for dropping: ownership is the name, since no engine records who created one.
5
+ */
6
+ export declare const OWNED_PREFIX = "_uql";
2
7
  /** The column a paged read carries its own unpaged total in, from `COUNT(*) OVER ()`. */
3
8
  export declare const TOTAL_ALIAS = "_uql_total";
4
9
  /**
@@ -1,5 +1,10 @@
1
1
  // Every identifier UQL invents, `_uql`-prefixed to stay off a user's own, collected in one place:
2
2
  // the ends writing and reading one sit in different modules, and a drift between them fails silently.
3
+ /**
4
+ * What every schema object uql creates is named from, so a hand-written trigger or function is never
5
+ * offered for dropping: ownership is the name, since no engine records who created one.
6
+ */
7
+ export const OWNED_PREFIX = '_uql';
3
8
  /** The column a paged read carries its own unpaged total in, from `COUNT(*) OVER ()`. */
4
9
  export const TOTAL_ALIAS = '_uql_total';
5
10
  /**
@@ -1,4 +1,4 @@
1
- import type { EntityMeta, FieldOptions, InsertIdSource, Query, QueryConflictPaths, QueryContext, QueryOptions, QueryPager, QueryTextSearchOptions, RowLockFeatures, SqlDialectFeatures, Type } from '../type/index.js';
1
+ import type { EntityMeta, FieldOptions, InsertIdSource, Query, QueryConflictPaths, QueryContext, QueryOptions, QueryPager, QueryTextSearchOptions, RowLockFeatures, SqlDialectFeatures, SqlDialectName, Type } from '../type/index.js';
2
2
  import { AbstractSqlDialect, type DerivedRelation, type RelationRows } from './abstractSqlDialect.js';
3
3
  import { type JsonSlot } from './jsonSql.js';
4
4
  /** What the MySQL-family engines have. */
@@ -7,6 +7,8 @@ export declare const MYSQL_ROW_LOCKS: RowLockFeatures;
7
7
  export declare const MYSQL_FEATURES: SqlDialectFeatures;
8
8
  /** What MySQL and MariaDB share, their JSON functions above all: `JSON_LENGTH`, `JSON_CONTAINS`, `JSON_TABLE`, `JSON_SET`. */
9
9
  export declare abstract class MysqlLikeSqlDialect extends AbstractSqlDialect {
10
+ /** Every member of this family runs the same SQL, so a body written once serves them all. */
11
+ get dialectFamily(): SqlDialectName;
10
12
  readonly features: SqlDialectFeatures;
11
13
  /**
12
14
  * `information_schema` keeps InnoDB's own row estimate, which is live enough to answer before
@@ -89,7 +91,7 @@ export declare abstract class MysqlLikeSqlDialect extends AbstractSqlDialect {
89
91
  protected appendTextScore<E>(ctx: QueryContext, meta: EntityMeta<E>, search: QueryTextSearchOptions<E>, keys: readonly string[], prefix: string | undefined): void;
90
92
  /** `DOUBLE`, never a bare `DECIMAL`, which is `DECIMAL(10,0)` and rounds `1.4` to `1`. */
91
93
  protected numericCast(expr: string): string;
92
- protected neExpr(field: string, ph: string): string;
94
+ neExpr(field: string, ph: string): string;
93
95
  /**
94
96
  * Omitting the `COALESCE` on a NOT NULL column keeps MySQL's partial in-place JSON update
95
97
  * applicable: it requires the target column as the direct `JSON_SET` input.
@@ -15,7 +15,6 @@ const MAX_LIMIT = BigInt.asUintN(64, -1n);
15
15
  /** Declared apart so MariaDB, which has no `FOR UPDATE OF`, can restate that one part of it. */
16
16
  export const MYSQL_ROW_LOCKS = { of: true, withWindow: true, placement: 'suffix' };
17
17
  export const MYSQL_FEATURES = {
18
- ifNotExists: true,
19
18
  indexIfNotExists: false,
20
19
  schemas: true,
21
20
  dropTableCascade: false,
@@ -39,11 +38,25 @@ export const MYSQL_FEATURES = {
39
38
  narrowVectorTypes: false,
40
39
  vectorTuningNeedsTransaction: false,
41
40
  serialDeclaresPrimaryKey: false,
41
+ triggers: {
42
+ preamble: '',
43
+ assignsRow: true,
44
+ body: 'inline',
45
+ guards: 'thenEndIf',
46
+ layout: 'timingFirst',
47
+ rows: 'row',
48
+ scope: 'schema',
49
+ before: true,
50
+ },
42
51
  };
43
52
  /** The one `JSON_TABLE` column an exploded array reads each element through, as a JSON document. */
44
53
  const ELEM_COLUMN = 'v';
45
54
  /** What MySQL and MariaDB share, their JSON functions above all: `JSON_LENGTH`, `JSON_CONTAINS`, `JSON_TABLE`, `JSON_SET`. */
46
55
  export class MysqlLikeSqlDialect extends AbstractSqlDialect {
56
+ /** Every member of this family runs the same SQL, so a body written once serves them all. */
57
+ get dialectFamily() {
58
+ return 'mysql';
59
+ }
47
60
  features = MYSQL_FEATURES;
48
61
  /**
49
62
  * `information_schema` keeps InnoDB's own row estimate, which is live enough to answer before
@@ -0,0 +1,66 @@
1
+ import { type QueryGroupOp, type QueryOrderedOp, type QueryVectorQuery, type QueryWhereArray, type QueryWhereFieldOp } from '../type/index.js';
2
+ /**
3
+ * How each grouping operator renders: the operator joining its clauses, and whether the group is
4
+ * negated (`$not` is `NOT (a AND b)`). Total over {@link QueryGroupOp}.
5
+ */
6
+ export declare const GROUP_OPS: {
7
+ readonly $and: {
8
+ readonly join: '$and';
9
+ readonly negate: false;
10
+ };
11
+ readonly $or: {
12
+ readonly join: '$or';
13
+ readonly negate: false;
14
+ };
15
+ readonly $not: {
16
+ readonly join: '$and';
17
+ readonly negate: true;
18
+ };
19
+ readonly $nor: {
20
+ readonly join: '$or';
21
+ readonly negate: true;
22
+ };
23
+ };
24
+ /** Whether a `$where` key groups clauses, narrowing it for the renderers that read {@link GROUP_OPS}. */
25
+ export declare function isGroupOp(key: string): key is QueryGroupOp;
26
+ /**
27
+ * A group operator's clauses, rejecting what the types do not cover: `/http` casts client JSON
28
+ * straight to `Query`, so a scalar can arrive where an array belongs. Shared so both backends
29
+ * refuse the same payload rather than one throwing and the other failing further in.
30
+ */
31
+ export declare function groupClauses<E>(key: QueryGroupOp, val: QueryWhereArray<E> | undefined): QueryWhereArray<E>;
32
+ /**
33
+ * A WHERE value as the operators it applies: an array is `$in`, an operator map its own entries, and
34
+ * anything else `$eq`. Each key is checked here, once, refused as `refusal` where it is no operator.
35
+ */
36
+ export declare function whereOperators(val: unknown, refusal: string): [QueryWhereFieldOp, unknown][];
37
+ /** An `$in`/`$nin` operand, which the types require to be an array but `/http` hands over untyped. */
38
+ export declare function inOperands(op: string, value: unknown): unknown[];
39
+ /** A `$between`'s two bounds, which `/http` hands over untyped too; one missing would bind `undefined`. */
40
+ export declare function betweenBounds(value: unknown): readonly [unknown, unknown];
41
+ /** Whether a `$near` names the vector it measures from, which its type requires and `/http` does not. */
42
+ export declare function isVectorQuery(near: Record<string, unknown>): near is Record<string, unknown> & QueryVectorQuery;
43
+ /** One entry of {@link LIKE_OPS}: how the pattern is built, and whether it ignores case. */
44
+ export type LikeOp = {
45
+ readonly pattern: (value: string) => string;
46
+ readonly insensitive: boolean;
47
+ };
48
+ export declare const COMPARE_OPS: ReadonlyMap<QueryWhereFieldOp, string>;
49
+ /**
50
+ * The ordered comparisons, `QueryOrderedOp` at runtime, derived from {@link COMPARE_OPS} rather than spelled
51
+ * again: {@link QueryVectorNear}'s bounds, so `$near` never accepts one the renderer has no operator
52
+ * for, and the operators that read a JSON path as a number.
53
+ */
54
+ export declare const ORDERED_OPS: ReadonlySet<string>;
55
+ export declare function isOrderedOp(op: string): op is QueryOrderedOp;
56
+ /** The operators an equality compares by value, which a JSON path reads the way that value compares. */
57
+ export declare const EQUALITY_OPS: ReadonlySet<string>;
58
+ /**
59
+ * Every `$like`-family operator: the pattern it wraps its value in, and whether it ignores case.
60
+ * Each case-sensitive operator is paired here with the `$i` twin that shares its pattern, so the
61
+ * two can never drift apart - and neither one decides case folding, which is
62
+ * `AbstractSqlDialect.caseInsensitiveMatch`'s single call.
63
+ */
64
+ export declare const LIKE_OPS: ReadonlyMap<string, LikeOp>;
65
+ /** What a `$near` says about the search itself; everything else in it is a bound. */
66
+ export declare const VECTOR_QUERY_KEY_SET: ReadonlySet<string>;