@atscript/db-sql-tools 0.1.145 → 0.1.147

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.
package/dist/index.cjs CHANGED
@@ -61,6 +61,8 @@ const EMPTY_OR = {
61
61
  */
62
62
  function createFilterVisitor(dialect, options) {
63
63
  const columnRef = options?.columnRef ?? ((field) => dialect.quoteIdentifier(field));
64
+ const qualifier = options?.qualifier;
65
+ const aliasSeq = options?.aliasSeq ?? { n: 0 };
64
66
  return {
65
67
  comparison(field, op, value) {
66
68
  if (op === "$geoWithin") {
@@ -153,9 +155,75 @@ function createFilterVisitor(dialect, options) {
153
155
  sql: `NOT (${child.sql})`,
154
156
  params: child.params
155
157
  };
158
+ },
159
+ relation(field, op, operand) {
160
+ if (!(0, _atscript_db.isResolvedRelationFilter)(operand)) throw new _atscript_db.DbError("REL_FILTER_NOT_SUPPORTED", [{
161
+ path: field,
162
+ message: `Relational predicate "${op}" on "${field}" reached the SQL renderer unresolved`
163
+ }]);
164
+ return renderRelation(dialect, op, operand, qualifier ?? dialect.quoteTable(operand.source.table), aliasSeq);
156
165
  }
157
166
  };
158
167
  }
168
+ /**
169
+ * Renders one resolved relational predicate as a correlated `EXISTS`:
170
+ *
171
+ * ```sql
172
+ * -- to / from
173
+ * EXISTS (SELECT 1 FROM <target> AS "_rf1"
174
+ * WHERE "_rf1"."<pair.target>" = <outer>."<pair.source>" [AND …] [AND <inner>])
175
+ * -- via
176
+ * EXISTS (SELECT 1 FROM <junction> AS "_rf1" JOIN <target> AS "_rf2"
177
+ * ON "_rf2"."<toTarget.target>" = "_rf1"."<toTarget.junction>" [AND …]
178
+ * WHERE "_rf1"."<toSource.junction>" = <outer>."<toSource.source>" [AND …]
179
+ * [AND <junction filter on _rf1>] [AND <inner on _rf2>])
180
+ * ```
181
+ *
182
+ * `$none` is `NOT EXISTS (…)`. Outer columns are always qualified (a bare
183
+ * column would bind to the subquery's table on a self relation or a
184
+ * same-named column). A NULL foreign-key component never satisfies `=`, so
185
+ * such a row has no related row (`$some` false, `$none` true).
186
+ */
187
+ function renderRelation(dialect, op, node, outer, aliasSeq) {
188
+ const q = (name) => dialect.quoteIdentifier(name);
189
+ const nextAlias = () => q(`_rf${++aliasSeq.n}`);
190
+ const conditions = [];
191
+ const params = [];
192
+ const addFilter = (filter, alias) => {
193
+ if (!filter || Object.keys(filter).length === 0) return;
194
+ const fragment = (0, _uniqu_core.walkFilter)(filter, createFilterVisitor(dialect, {
195
+ columnRef: (field) => `${alias}.${q(field)}`,
196
+ qualifier: alias,
197
+ aliasSeq
198
+ }));
199
+ if (fragment) {
200
+ conditions.push(fragment.sql);
201
+ params.push(...fragment.params);
202
+ }
203
+ };
204
+ let from;
205
+ if (node.kind === "via") {
206
+ const junction = node.junction;
207
+ if (!junction) throw new Error(`Relational predicate on "${node.nav}": a "via" relation needs a junction`);
208
+ const j = nextAlias();
209
+ const t = nextAlias();
210
+ const on = junction.toTarget.map((p) => `${t}.${q(p.target)} = ${j}.${q(p.junction)}`);
211
+ from = `${dialect.quoteTable(junction.table)} AS ${j} JOIN ${dialect.quoteTable(node.target.table)} AS ${t} ON ${on.join(" AND ")}`;
212
+ for (const p of junction.toSource) conditions.push(`${j}.${q(p.junction)} = ${outer}.${q(p.source)}`);
213
+ addFilter(junction.filter, j);
214
+ addFilter(node.filter, t);
215
+ } else {
216
+ const t = nextAlias();
217
+ from = `${dialect.quoteTable(node.target.table)} AS ${t}`;
218
+ for (const p of node.pairs) conditions.push(`${t}.${q(p.target)} = ${outer}.${q(p.source)}`);
219
+ addFilter(node.filter, t);
220
+ }
221
+ const where = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
222
+ return {
223
+ sql: `${op === "$none" ? "NOT " : ""}EXISTS (SELECT 1 FROM ${from}${where})`,
224
+ params
225
+ };
226
+ }
159
227
  const visitorCache = /* @__PURE__ */ new WeakMap();
160
228
  function getVisitor(dialect) {
161
229
  let visitor = visitorCache.get(dialect);
@@ -167,10 +235,23 @@ function getVisitor(dialect) {
167
235
  }
168
236
  /**
169
237
  * Translates a filter expression into a parameterized SQL WHERE clause.
238
+ *
239
+ * Relational predicates (`{ nav: { $some | $none: … } }`, resolved by the
240
+ * core into `ResolvedRelationFilter` operands) render as correlated
241
+ * `[NOT] EXISTS (…)` subqueries; `opts.qualifier` names the outer table or
242
+ * alias they correlate to (default: the source table — see
243
+ * {@link TFilterVisitorOptions.qualifier}). `opts.columnRef` overrides how
244
+ * the filter's own columns render (e.g. `t."col"` for an aliased FROM).
245
+ * Placeholders stay positional `?` in textual order.
246
+ *
247
+ * @param opts - since 0.1.147
170
248
  */
171
- function buildWhere(dialect, filter) {
249
+ function buildWhere(dialect, filter, opts) {
172
250
  if (!filter || Object.keys(filter).length === 0) return EMPTY_AND;
173
- return (0, _uniqu_core.walkFilter)(filter, getVisitor(dialect)) ?? EMPTY_AND;
251
+ return (0, _uniqu_core.walkFilter)(filter, opts || (0, _atscript_db.containsRelationFilter)(filter) ? createFilterVisitor(dialect, {
252
+ ...opts,
253
+ aliasSeq: opts?.aliasSeq ?? { n: 0 }
254
+ }) : getVisitor(dialect)) ?? EMPTY_AND;
174
255
  }
175
256
  //#endregion
176
257
  //#region src/common.ts
@@ -215,6 +296,20 @@ function toSqlValue(value) {
215
296
  if (typeof value === "boolean") return value ? 1 : 0;
216
297
  return value;
217
298
  }
299
+ /**
300
+ * `FOREIGN KEY (…) REFERENCES <target> (…)[ ON DELETE …][ ON UPDATE …]` of a
301
+ * foreign key over its physical columns ({@link fkColumns}); the target is
302
+ * schema-qualified when it declares `@db.schema`. `quote` quotes one
303
+ * identifier.
304
+ */
305
+ function foreignKeySql(quote, fk) {
306
+ const { fields, targetFields } = (0, _atscript_db.fkColumns)(fk);
307
+ const target = fk.targetSchema ? `${quote(fk.targetSchema)}.${quote(fk.targetTable)}` : quote(fk.targetTable);
308
+ let sql = `FOREIGN KEY (${fields.map(quote).join(", ")}) REFERENCES ${target} (${targetFields.map(quote).join(", ")})`;
309
+ if (fk.onDelete) sql += ` ON DELETE ${refActionToSql(fk.onDelete)}`;
310
+ if (fk.onUpdate) sql += ` ON UPDATE ${refActionToSql(fk.onUpdate)}`;
311
+ return sql;
312
+ }
218
313
  function refActionToSql(action) {
219
314
  switch (action) {
220
315
  case "cascade": return "CASCADE";
@@ -532,6 +627,80 @@ function viewAggExpr(dialect, c, resolveFieldRef) {
532
627
  return c.aggFn === "sum" ? `COALESCE(${call}, 0)` : call;
533
628
  }
534
629
  /**
630
+ * The SQL expression of a computed view column (`@db.compute`), evaluated in
631
+ * IEEE double on every dialect: each field and literal leaf is cast with
632
+ * {@link SqlDialect.castDouble} (a computed leaf is already a double and is
633
+ * inlined); `+ - *` render as `(l op r)`, `/` as `(l / NULLIF(r, 0))` —
634
+ * division by zero is NULL — unary minus as `(-x)`, `coalesce` as
635
+ * `COALESCE(…)`. A leaf reads its column like the SELECT list does: an
636
+ * aggregate's call ({@link viewAggExpr}) or a dimension's source
637
+ * ({@link viewSourceExpr}). `byPath` holds the view's mappings by `viewPath`.
638
+ *
639
+ * In a `grouped` view a JSON-extracted dimension leaf reads `MIN(<extract>)`:
640
+ * the extraction is a GROUP BY key, so every row of a group holds the same
641
+ * value, but MySQL's ONLY_FULL_GROUP_BY rejects an expression over the raw
642
+ * JSON column (which is not itself grouped) — the aggregate form is accepted
643
+ * everywhere and yields that same value.
644
+ *
645
+ * @throws when the dialect has no `castDouble`, or a leaf names no column.
646
+ * @since 0.1.147
647
+ */
648
+ function viewComputeExpr(dialect, c, byPath, resolveFieldRef, grouped = false, cache = /* @__PURE__ */ new Map()) {
649
+ const cached = cache.get(c.viewPath);
650
+ if (cached !== void 0) return cached;
651
+ const cast = dialect.castDouble?.bind(dialect);
652
+ if (!cast) throw new Error(`View column "${c.viewColumn}": computed view columns are not supported by this adapter`);
653
+ const render = (e) => {
654
+ if (typeof e === "number") {
655
+ if (!Number.isFinite(e)) throw new Error(`View column "${c.viewColumn}": non-finite literal in @db.compute`);
656
+ return cast(String(e));
657
+ }
658
+ if ("field" in e) {
659
+ const leaf = byPath.get(e.field);
660
+ if (!leaf) throw new Error(`View column "${c.viewColumn}": "${e.field}" is not a column of the view`);
661
+ if (leaf.expr !== void 0) return viewComputeExpr(dialect, leaf, byPath, resolveFieldRef, grouped, cache);
662
+ if (leaf.aggFn) return cast(viewAggExpr(dialect, leaf, resolveFieldRef));
663
+ const source = viewSourceExpr(dialect, leaf);
664
+ return cast(grouped && leaf.json ? `MIN(${source})` : source);
665
+ }
666
+ const args = e.args.map(render);
667
+ switch (e.op) {
668
+ case "neg": return `(-${args[0]})`;
669
+ case "coalesce": return `COALESCE(${args.join(", ")})`;
670
+ case "/": return `(${args[0]} / NULLIF(${args[1]}, 0))`;
671
+ default: return `(${args[0]} ${e.op} ${args[1]})`;
672
+ }
673
+ };
674
+ const sql = render(c.expr);
675
+ cache.set(c.viewPath, sql);
676
+ return sql;
677
+ }
678
+ /**
679
+ * The `ON` clause of a first-row join (`join.first`): the joined row is the
680
+ * one whose primary key equals the key of the FIRST matching target row —
681
+ * a correlated scalar subquery
682
+ * `"T"."id" = (SELECT "T"."id" FROM "table" AS "T" WHERE <condition> ORDER BY … LIMIT 1)`.
683
+ * The inner `FROM` repeats the join's own target / alias text, so the
684
+ * condition's refs to the joined scope bind to the inner row (SQL name
685
+ * shadowing) while refs to the entry and earlier joins stay correlated to
686
+ * the outer query. The ordering ends with the primary key, so the pick is
687
+ * deterministic; NULL sorts first in `asc` (`nullsSortLargest` dialects
688
+ * render `NULLS FIRST` / `NULLS LAST`). No window functions — SQLite, MySQL 8
689
+ * and PostgreSQL render it alike.
690
+ * @since 0.1.147
691
+ */
692
+ function firstRowOn(dialect, join, target, condition, resolveFieldRef) {
693
+ const first = join.first;
694
+ const key = resolveFieldRef({
695
+ type: join.targetType,
696
+ field: first.key
697
+ });
698
+ return `${key} = (SELECT ${key} FROM ${target} WHERE ${condition} ORDER BY ${first.order.map(({ ref, desc }) => {
699
+ const nulls = dialect.nullsSortLargest ? desc ? " NULLS LAST" : " NULLS FIRST" : "";
700
+ return `${resolveFieldRef(ref)} ${desc ? "DESC" : "ASC"}${nulls}`;
701
+ }).join(", ")} LIMIT 1)`;
702
+ }
703
+ /**
535
704
  * Builds a CREATE VIEW statement from a view plan and column mappings.
536
705
  *
537
706
  * Joins render in declaration order — `JOIN` (inner, the default) or
@@ -543,35 +712,41 @@ function viewAggExpr(dialect, c, resolveFieldRef) {
543
712
  * join target may be views.
544
713
  */
545
714
  function buildCreateView(dialect, viewName, plan, columns, resolveFieldRef) {
546
- const selectCols = columns.map((c) => {
547
- return `${c.aggFn ? viewAggExpr(dialect, c, resolveFieldRef) : viewSourceExpr(dialect, c)} AS ${dialect.quoteIdentifier(c.viewColumn)}`;
548
- }).join(", ");
715
+ const columnByPath = new Map(columns.map((c) => [c.viewPath, c]));
716
+ const hasAggregates = columns.some((c) => c.aggFn);
717
+ const computedSql = /* @__PURE__ */ new Map();
718
+ /** A column's SELECT expression: computed, aggregate or plain source. */
719
+ const columnExpr = (c) => c.expr !== void 0 ? viewComputeExpr(dialect, c, columnByPath, resolveFieldRef, hasAggregates, computedSql) : c.aggFn ? viewAggExpr(dialect, c, resolveFieldRef) : viewSourceExpr(dialect, c);
720
+ const selectCols = columns.map((c) => `${columnExpr(c)} AS ${dialect.quoteIdentifier(c.viewColumn)}`).join(", ");
549
721
  let sql = `${dialect.createViewPrefix} ${dialect.quoteTable(viewName)} AS SELECT ${selectCols} FROM ${dialect.quoteIdentifier(plan.entryTable)}`;
550
722
  for (const join of plan.joins) {
551
- const onClause = queryNodeToSql(join.condition, resolveFieldRef);
723
+ const condition = queryNodeToSql(join.condition, resolveFieldRef);
552
724
  const keyword = join.kind === "left" ? "LEFT JOIN" : "JOIN";
553
725
  const target = join.scope === join.targetTable ? dialect.quoteIdentifier(join.targetTable) : `${dialect.quoteIdentifier(join.targetTable)} AS ${dialect.quoteIdentifier(join.scope)}`;
726
+ const onClause = join.first ? firstRowOn(dialect, join, target, condition, resolveFieldRef) : condition;
554
727
  sql += ` ${keyword} ${target} ON ${onClause}`;
555
728
  }
556
729
  if (plan.filter) {
557
730
  const whereClause = queryNodeToSql(plan.filter, resolveFieldRef);
558
731
  sql += ` WHERE ${whereClause}`;
559
732
  }
560
- if (columns.some((c) => c.aggFn)) {
561
- const dimensionCols = columns.filter((c) => !c.aggFn);
733
+ if (hasAggregates) {
734
+ const dimensionCols = columns.filter((c) => !c.aggFn && c.expr === void 0);
562
735
  if (dimensionCols.length > 0) {
563
736
  const groupByCols = dimensionCols.map((c) => viewSourceExpr(dialect, c)).join(", ");
564
737
  sql += ` GROUP BY ${groupByCols}`;
565
738
  }
566
739
  if (plan.having) {
567
- const columnByPath = /* @__PURE__ */ new Map();
568
- for (const c of columns) columnByPath.set(c.viewPath, c);
740
+ let grouped;
741
+ const groupedColumns = () => grouped ??= new Set(dimensionCols.filter((c) => !c.json).map((c) => c.sourceColumn.toLowerCase()));
569
742
  const havingResolver = (ref) => {
570
743
  const col = ref.type ? void 0 : columnByPath.get(ref.field);
571
744
  if (!col) return resolveFieldRef(ref);
745
+ if (col.expr !== void 0) return columnExpr(col);
572
746
  if (col.aggFn) return viewAggExpr(dialect, col, resolveFieldRef);
573
747
  const expr = viewSourceExpr(dialect, col);
574
- return col.json ? havingGroupRef(dialect, expr, col.viewColumn) : expr;
748
+ if (!col.json) return expr;
749
+ return dialect.bucketAliasInHaving && groupedColumns().has(col.viewColumn.toLowerCase()) ? `MIN(${expr})` : havingGroupRef(dialect, expr, col.viewColumn);
575
750
  };
576
751
  const havingClause = queryNodeToSql(plan.having, havingResolver);
577
752
  sql += ` HAVING ${havingClause}`;
@@ -632,11 +807,8 @@ function buildInsertMany(dialect, table, rows, columns = insertManyColumns(rows)
632
807
  function buildSelect(dialect, table, where, controls) {
633
808
  let sql = `SELECT ${buildProjection(dialect, controls?.$select)} FROM ${dialect.quoteTable(table)} WHERE ${where.sql}`;
634
809
  const params = [...where.params];
635
- if (controls?.$sort) {
636
- const orderParts = [];
637
- for (const [col, dir] of Object.entries(controls.$sort)) orderParts.push(`${dialect.quoteIdentifier(col)} ${dir === -1 ? "DESC" : "ASC"}`);
638
- if (orderParts.length > 0) sql += ` ORDER BY ${orderParts.join(", ")}`;
639
- }
810
+ const orderBy = orderByList(dialect, controls?.$sort);
811
+ if (orderBy) sql += ` ORDER BY ${orderBy}`;
640
812
  if (controls?.$limit !== void 0) {
641
813
  sql += ` LIMIT ?`;
642
814
  params.push(controls.$limit);
@@ -651,6 +823,53 @@ function buildSelect(dialect, table, where, controls) {
651
823
  params
652
824
  });
653
825
  }
826
+ /** `"col" ASC, "other" DESC` of a physical `$sort` — `""` when it orders nothing. */
827
+ function orderByList(dialect, sort) {
828
+ if (!sort) return "";
829
+ const parts = [];
830
+ for (const [col, dir] of Object.entries(sort)) parts.push(`${dialect.quoteIdentifier(col)} ${dir === -1 ? "DESC" : "ASC"}`);
831
+ return parts.join(", ");
832
+ }
833
+ /**
834
+ * The row-number column {@link buildPartitionedSelect} adds to every row;
835
+ * {@link stripPartitionRowNumber} removes it from the result rows.
836
+ * @since 0.1.147
837
+ */
838
+ const PARTITION_ROW_NUMBER_ALIAS = "__atscript_rn";
839
+ /**
840
+ * Builds a SELECT whose `$skip` / `$limit` apply to each partition — the rows
841
+ * sharing the values of the `partitionBy` columns (physical names) — instead
842
+ * of to the whole result: a `ROW_NUMBER() OVER (PARTITION BY … ORDER BY
843
+ * <$sort>)` window in a derived table, filtered on the row number. The rows
844
+ * of each partition come out in `$sort` order (partitions interleave); every
845
+ * row carries {@link PARTITION_ROW_NUMBER_ALIAS}. Window functions need
846
+ * SQLite ≥ 3.25, MySQL ≥ 8.0 or MariaDB ≥ 10.2.
847
+ * @since 0.1.147
848
+ */
849
+ function buildPartitionedSelect(dialect, table, where, controls, partitionBy) {
850
+ const rn = dialect.quoteIdentifier(PARTITION_ROW_NUMBER_ALIAS);
851
+ const orderBy = orderByList(dialect, controls.$sort);
852
+ const window = `PARTITION BY ${partitionBy.map((col) => dialect.quoteIdentifier(col)).join(", ")}` + (orderBy ? ` ORDER BY ${orderBy}` : "");
853
+ const inner = `SELECT ${buildProjection(dialect, controls.$select)}, ROW_NUMBER() OVER (${window}) AS ${rn} FROM ${dialect.quoteTable(table)} WHERE ${where.sql}`;
854
+ const skip = controls.$skip ?? 0;
855
+ const limit = controls.$limit;
856
+ const params = [...where.params, skip];
857
+ let sql = `SELECT * FROM (${inner}) AS ${dialect.quoteIdentifier("__atscript_p")} WHERE ${rn} > ?`;
858
+ if (limit !== void 0 && limit !== null) {
859
+ sql += ` AND ${rn} <= ?`;
860
+ params.push(skip + limit);
861
+ }
862
+ sql += ` ORDER BY ${rn}`;
863
+ return finalizeParams(dialect, {
864
+ sql,
865
+ params
866
+ });
867
+ }
868
+ /** Removes {@link PARTITION_ROW_NUMBER_ALIAS} from rows read by {@link buildPartitionedSelect}. */
869
+ function stripPartitionRowNumber(rows) {
870
+ for (const row of rows) delete row[PARTITION_ROW_NUMBER_ALIAS];
871
+ return rows;
872
+ }
654
873
  /**
655
874
  * Marker value for {@link buildUpdate}: the column is assigned its DDL
656
875
  * `DEFAULT` (`SET "col" = DEFAULT`, no bound parameter). Produced by
@@ -810,6 +1029,13 @@ function buildProjection(dialect, select, qualifier) {
810
1029
  */
811
1030
  const GEO_DISTANCE_ALIAS = "__atscript_distance";
812
1031
  /**
1032
+ * Alias of the searched table inside the geo and vector search statements —
1033
+ * a filter rendered into them correlates its relational predicates to it
1034
+ * (`buildWhere(filter, { qualifier: dialect.quoteTable(SEARCH_SOURCE_ALIAS) })`).
1035
+ * @since 0.1.147
1036
+ */
1037
+ const SEARCH_SOURCE_ALIAS = "t";
1038
+ /**
813
1039
  * Builds a distance-ranked geo search SELECT:
814
1040
  *
815
1041
  * ```sql
@@ -1032,6 +1258,8 @@ exports.AGG_FN_SQL = AGG_FN_SQL;
1032
1258
  exports.EMPTY_AND = EMPTY_AND;
1033
1259
  exports.EMPTY_OR = EMPTY_OR;
1034
1260
  exports.GEO_DISTANCE_ALIAS = GEO_DISTANCE_ALIAS;
1261
+ exports.PARTITION_ROW_NUMBER_ALIAS = PARTITION_ROW_NUMBER_ALIAS;
1262
+ exports.SEARCH_SOURCE_ALIAS = SEARCH_SOURCE_ALIAS;
1035
1263
  exports.SQL_DEFAULT = SQL_DEFAULT;
1036
1264
  exports.VECTOR_DISTANCE_ALIAS = VECTOR_DISTANCE_ALIAS;
1037
1265
  exports.buildAggregateCount = buildAggregateCount;
@@ -1042,6 +1270,7 @@ exports.buildGeoSearchCount = buildGeoSearchCount;
1042
1270
  exports.buildGeoSearchSelect = buildGeoSearchSelect;
1043
1271
  exports.buildInsert = buildInsert;
1044
1272
  exports.buildInsertMany = buildInsertMany;
1273
+ exports.buildPartitionedSelect = buildPartitionedSelect;
1045
1274
  exports.buildProjection = buildProjection;
1046
1275
  exports.buildSelect = buildSelect;
1047
1276
  exports.buildUpdate = buildUpdate;
@@ -1054,6 +1283,7 @@ exports.defaultValueToSqlLiteral = defaultValueToSqlLiteral;
1054
1283
  exports.derivedColumnExpr = derivedColumnExpr;
1055
1284
  exports.fillReplacePayload = fillReplacePayload;
1056
1285
  exports.finalizeParams = finalizeParams;
1286
+ exports.foreignKeySql = foreignKeySql;
1057
1287
  exports.geoWindowFromControls = geoWindowFromControls;
1058
1288
  exports.groupKeySql = groupKeySql;
1059
1289
  exports.insertManyColumns = insertManyColumns;
@@ -1068,5 +1298,6 @@ exports.renameGeoDistance = renameGeoDistance;
1068
1298
  exports.replaceColumnsFor = replaceColumnsFor;
1069
1299
  exports.sqlStringLiteral = sqlStringLiteral;
1070
1300
  exports.sqlTimeZoneLiteral = sqlTimeZoneLiteral;
1301
+ exports.stripPartitionRowNumber = stripPartitionRowNumber;
1071
1302
  exports.toSqlValue = toSqlValue;
1072
1303
  exports.vectorDistanceSource = vectorDistanceSource;
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { AtscriptQueryFieldRef, AtscriptQueryNode, DbControls, TDbDefaultFn, TDbFieldMeta, TDbReferentialAction, TFieldOps, TResolvedBucket, TViewColumnMapping, TViewJsonType, TViewPlan, UniquSelect } from "@atscript/db";
1
+ import { AtscriptQueryFieldRef, AtscriptQueryNode, DbControls, TDbDefaultFn, TDbFieldMeta, TDbForeignKey, TDbReferentialAction, TFieldOps, TResolvedBucket, TViewColumnMapping, TViewJsonType, TViewPlan, UniquSelect } from "@atscript/db";
2
2
  import { FilterExpr, FilterVisitor } from "@uniqu/core";
3
3
  import { TDbAggregateFn } from "@atscript/db/agg";
4
4
 
@@ -34,8 +34,10 @@ interface SqlDialect {
34
34
  geoWithin?(quotedCol: string, circle: TGeoCircle): TSqlFragment;
35
35
  /**
36
36
  * Calendar-bucket label expression over one column: TEXT `'YYYY-MM-DD'`
37
- * (the local calendar date of the bucket's first day in `b.tz`), or NULL for
38
- * a NULL source or one outside `[BUCKET_MIN_INSTANT, BUCKET_MAX_INSTANT)`.
37
+ * (the local calendar date of the bucket's first day in `b.tz`; for unit
38
+ * `hour`, `'YYYY-MM-DDTHH:00'`, the local wall-clock hour — truncate the
39
+ * zone's wall time, never the UTC instant), or NULL for a NULL source or
40
+ * one outside `[BUCKET_MIN_INSTANT, BUCKET_MAX_INSTANT)`.
39
41
  * `quotedCol` is already quoted; `b.fd` identifies the storage kind.
40
42
  *
41
43
  * The expression must be PARAMETER-FREE — inline the zone with
@@ -83,6 +85,24 @@ interface SqlDialect {
83
85
  * @since 0.1.136
84
86
  */
85
87
  jsonExtract?(quotedCol: string, path: readonly string[], type: TViewJsonType): string;
88
+ /**
89
+ * `expr` cast to an IEEE double — how a computed view column
90
+ * (`@db.compute`) evaluates every field / literal leaf, so `7 / 2 = 3.5`
91
+ * everywhere (no integer division, no DECIMAL rounding): SQLite
92
+ * `CAST(x AS REAL)`, MySQL `CAST(x AS DOUBLE)`, PostgreSQL
93
+ * `CAST(x AS DOUBLE PRECISION)`. Parameter-free. Dialects without it fail
94
+ * view sync with `computed view columns are not supported by this adapter`.
95
+ * @since 0.1.147
96
+ */
97
+ castDouble?(expr: string): string;
98
+ /**
99
+ * `true` when the database sorts NULL as the LARGEST value (PostgreSQL):
100
+ * first-row join order keys then render `ASC NULLS FIRST` /
101
+ * `DESC NULLS LAST`, keeping the uniform "NULL is the smallest value"
102
+ * ordering SQLite, MySQL and MongoDB have natively.
103
+ * @since 0.1.147
104
+ */
105
+ nullsSortLargest?: boolean;
86
106
  /** e.g. 'CREATE VIEW IF NOT EXISTS' or 'CREATE OR REPLACE VIEW' */
87
107
  createViewPrefix: string;
88
108
  /** Returns a parameter placeholder for the given 1-based index. When absent, '?' is used. */
@@ -117,6 +137,35 @@ interface TFilterVisitorOptions {
117
137
  * expression (`SUM("amount")`) — PostgreSQL rejects SELECT aliases in HAVING.
118
138
  */
119
139
  columnRef?: (field: string) => string;
140
+ /**
141
+ * The quoted outer table or alias that relational predicates
142
+ * (`{ nav: { $some | $none: … } }`) correlate to: their `EXISTS` subqueries
143
+ * compare the related rows with `<qualifier>."<column>"`. Defaults to the
144
+ * predicate's source table (`dialect.quoteTable(node.source.table)`), which
145
+ * is right for every statement whose FROM is the bare table; a statement
146
+ * that aliases its FROM (`FROM "items" AS "t"`) must pass that alias here.
147
+ *
148
+ * @since 0.1.147
149
+ */
150
+ qualifier?: string;
151
+ /**
152
+ * Alias sequence of the relational-predicate subqueries (`_rf1`, `_rf2`, …),
153
+ * shared by the visitors of one statement so nested predicates get distinct
154
+ * aliases. A visitor created without one starts its own.
155
+ *
156
+ * @internal
157
+ * @since 0.1.147
158
+ */
159
+ aliasSeq?: TRelationAliasSeq;
160
+ }
161
+ /**
162
+ * Alias counter of the relational-predicate subqueries of one statement.
163
+ *
164
+ * @internal
165
+ * @since 0.1.147
166
+ */
167
+ interface TRelationAliasSeq {
168
+ n: number;
120
169
  }
121
170
  /**
122
171
  * Creates a dialect-specific filter visitor for `walkFilter`.
@@ -124,8 +173,18 @@ interface TFilterVisitorOptions {
124
173
  declare function createFilterVisitor(dialect: SqlDialect, options?: TFilterVisitorOptions): FilterVisitor<TSqlFragment>;
125
174
  /**
126
175
  * Translates a filter expression into a parameterized SQL WHERE clause.
176
+ *
177
+ * Relational predicates (`{ nav: { $some | $none: … } }`, resolved by the
178
+ * core into `ResolvedRelationFilter` operands) render as correlated
179
+ * `[NOT] EXISTS (…)` subqueries; `opts.qualifier` names the outer table or
180
+ * alias they correlate to (default: the source table — see
181
+ * {@link TFilterVisitorOptions.qualifier}). `opts.columnRef` overrides how
182
+ * the filter's own columns render (e.g. `t."col"` for an aliased FROM).
183
+ * Placeholders stay positional `?` in textual order.
184
+ *
185
+ * @param opts - since 0.1.147
127
186
  */
128
- declare function buildWhere(dialect: SqlDialect, filter: FilterExpr): TSqlFragment;
187
+ declare function buildWhere(dialect: SqlDialect, filter: FilterExpr, opts?: TFilterVisitorOptions): TSqlFragment;
129
188
  //#endregion
130
189
  //#region src/geo.d.ts
131
190
  /**
@@ -135,6 +194,13 @@ declare function buildWhere(dialect: SqlDialect, filter: FilterExpr): TSqlFragme
135
194
  * across dialects, so the public name can't be used directly.
136
195
  */
137
196
  declare const GEO_DISTANCE_ALIAS = "__atscript_distance";
197
+ /**
198
+ * Alias of the searched table inside the geo and vector search statements —
199
+ * a filter rendered into them correlates its relational predicates to it
200
+ * (`buildWhere(filter, { qualifier: dialect.quoteTable(SEARCH_SOURCE_ALIAS) })`).
201
+ * @since 0.1.147
202
+ */
203
+ declare const SEARCH_SOURCE_ALIAS = "t";
138
204
  /** The query controls a geo search page reads — an adapter passes its `query.controls` as-is. */
139
205
  interface TGeoSearchControls {
140
206
  $limit?: number;
@@ -272,6 +338,25 @@ declare function buildInsertMany(dialect: SqlDialect, table: string, rows: reado
272
338
  * Builds a SELECT statement with optional sort, limit, offset, projection.
273
339
  */
274
340
  declare function buildSelect(dialect: SqlDialect, table: string, where: TSqlFragment, controls?: DbControls): TSqlFragment;
341
+ /**
342
+ * The row-number column {@link buildPartitionedSelect} adds to every row;
343
+ * {@link stripPartitionRowNumber} removes it from the result rows.
344
+ * @since 0.1.147
345
+ */
346
+ declare const PARTITION_ROW_NUMBER_ALIAS = "__atscript_rn";
347
+ /**
348
+ * Builds a SELECT whose `$skip` / `$limit` apply to each partition — the rows
349
+ * sharing the values of the `partitionBy` columns (physical names) — instead
350
+ * of to the whole result: a `ROW_NUMBER() OVER (PARTITION BY … ORDER BY
351
+ * <$sort>)` window in a derived table, filtered on the row number. The rows
352
+ * of each partition come out in `$sort` order (partitions interleave); every
353
+ * row carries {@link PARTITION_ROW_NUMBER_ALIAS}. Window functions need
354
+ * SQLite ≥ 3.25, MySQL ≥ 8.0 or MariaDB ≥ 10.2.
355
+ * @since 0.1.147
356
+ */
357
+ declare function buildPartitionedSelect(dialect: SqlDialect, table: string, where: TSqlFragment, controls: DbControls, partitionBy: readonly string[]): TSqlFragment;
358
+ /** Removes {@link PARTITION_ROW_NUMBER_ALIAS} from rows read by {@link buildPartitionedSelect}. */
359
+ declare function stripPartitionRowNumber<R extends Record<string, unknown>>(rows: R[]): R[];
275
360
  /**
276
361
  * Marker value for {@link buildUpdate}: the column is assigned its DDL
277
362
  * `DEFAULT` (`SET "col" = DEFAULT`, no bound parameter). Produced by
@@ -376,6 +461,13 @@ declare function jsonDollarPath(path: readonly string[]): string;
376
461
  declare function sqlTimeZoneLiteral(tz: string): string;
377
462
  /** Converts a JS value to a SQL-bindable parameter. Objects/arrays -> JSON, booleans -> 0/1. */
378
463
  declare function toSqlValue(value: unknown): unknown;
464
+ /**
465
+ * `FOREIGN KEY (…) REFERENCES <target> (…)[ ON DELETE …][ ON UPDATE …]` of a
466
+ * foreign key over its physical columns ({@link fkColumns}); the target is
467
+ * schema-qualified when it declares `@db.schema`. `quote` quotes one
468
+ * identifier.
469
+ */
470
+ declare function foreignKeySql(quote: (name: string) => string, fk: TDbForeignKey): string;
379
471
  declare function refActionToSql(action: TDbReferentialAction): string;
380
472
  /** Returns a safe SQL DEFAULT literal for a given design type. */
381
473
  declare function defaultValueForType(designType: string): string;
@@ -446,4 +538,4 @@ declare function parseRegexString(value: unknown): {
446
538
  flags: string;
447
539
  };
448
540
  //#endregion
449
- export { AGG_FN_SQL, EMPTY_AND, EMPTY_OR, GEO_DISTANCE_ALIAS, SQL_DEFAULT, type SqlDialect, type TFilterVisitorOptions, type TGeoCircle, type TGeoSearchControls, type TGeoWindow, type TReplaceColumn, type TSqlFragment, VECTOR_DISTANCE_ALIAS, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildProjection, buildSelect, buildUpdate, buildVectorSearchCount, buildVectorSearchSelect, buildWhere, createFilterVisitor, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, finalizeParams, geoWindowFromControls, groupKeySql, insertManyColumns, jsonDollarPath, normalizeGeoPointValue, parseRegexString, queryNodeToSql, queryOpToSql, quotedJsonPathSegments, refActionToSql, renameGeoDistance, replaceColumnsFor, sqlStringLiteral, sqlTimeZoneLiteral, toSqlValue, vectorDistanceSource };
541
+ export { AGG_FN_SQL, EMPTY_AND, EMPTY_OR, GEO_DISTANCE_ALIAS, PARTITION_ROW_NUMBER_ALIAS, SEARCH_SOURCE_ALIAS, SQL_DEFAULT, type SqlDialect, type TFilterVisitorOptions, type TGeoCircle, type TGeoSearchControls, type TGeoWindow, type TReplaceColumn, type TSqlFragment, VECTOR_DISTANCE_ALIAS, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildPartitionedSelect, buildProjection, buildSelect, buildUpdate, buildVectorSearchCount, buildVectorSearchSelect, buildWhere, createFilterVisitor, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, finalizeParams, foreignKeySql, geoWindowFromControls, groupKeySql, insertManyColumns, jsonDollarPath, normalizeGeoPointValue, parseRegexString, queryNodeToSql, queryOpToSql, quotedJsonPathSegments, refActionToSql, renameGeoDistance, replaceColumnsFor, sqlStringLiteral, sqlTimeZoneLiteral, stripPartitionRowNumber, toSqlValue, vectorDistanceSource };
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { FilterExpr, FilterVisitor } from "@uniqu/core";
2
- import { AtscriptQueryFieldRef, AtscriptQueryNode, DbControls, TDbDefaultFn, TDbFieldMeta, TDbReferentialAction, TFieldOps, TResolvedBucket, TViewColumnMapping, TViewJsonType, TViewPlan, UniquSelect } from "@atscript/db";
2
+ import { AtscriptQueryFieldRef, AtscriptQueryNode, DbControls, TDbDefaultFn, TDbFieldMeta, TDbForeignKey, TDbReferentialAction, TFieldOps, TResolvedBucket, TViewColumnMapping, TViewJsonType, TViewPlan, UniquSelect } from "@atscript/db";
3
3
  import { TDbAggregateFn } from "@atscript/db/agg";
4
4
 
5
5
  //#region src/dialect.d.ts
@@ -34,8 +34,10 @@ interface SqlDialect {
34
34
  geoWithin?(quotedCol: string, circle: TGeoCircle): TSqlFragment;
35
35
  /**
36
36
  * Calendar-bucket label expression over one column: TEXT `'YYYY-MM-DD'`
37
- * (the local calendar date of the bucket's first day in `b.tz`), or NULL for
38
- * a NULL source or one outside `[BUCKET_MIN_INSTANT, BUCKET_MAX_INSTANT)`.
37
+ * (the local calendar date of the bucket's first day in `b.tz`; for unit
38
+ * `hour`, `'YYYY-MM-DDTHH:00'`, the local wall-clock hour — truncate the
39
+ * zone's wall time, never the UTC instant), or NULL for a NULL source or
40
+ * one outside `[BUCKET_MIN_INSTANT, BUCKET_MAX_INSTANT)`.
39
41
  * `quotedCol` is already quoted; `b.fd` identifies the storage kind.
40
42
  *
41
43
  * The expression must be PARAMETER-FREE — inline the zone with
@@ -83,6 +85,24 @@ interface SqlDialect {
83
85
  * @since 0.1.136
84
86
  */
85
87
  jsonExtract?(quotedCol: string, path: readonly string[], type: TViewJsonType): string;
88
+ /**
89
+ * `expr` cast to an IEEE double — how a computed view column
90
+ * (`@db.compute`) evaluates every field / literal leaf, so `7 / 2 = 3.5`
91
+ * everywhere (no integer division, no DECIMAL rounding): SQLite
92
+ * `CAST(x AS REAL)`, MySQL `CAST(x AS DOUBLE)`, PostgreSQL
93
+ * `CAST(x AS DOUBLE PRECISION)`. Parameter-free. Dialects without it fail
94
+ * view sync with `computed view columns are not supported by this adapter`.
95
+ * @since 0.1.147
96
+ */
97
+ castDouble?(expr: string): string;
98
+ /**
99
+ * `true` when the database sorts NULL as the LARGEST value (PostgreSQL):
100
+ * first-row join order keys then render `ASC NULLS FIRST` /
101
+ * `DESC NULLS LAST`, keeping the uniform "NULL is the smallest value"
102
+ * ordering SQLite, MySQL and MongoDB have natively.
103
+ * @since 0.1.147
104
+ */
105
+ nullsSortLargest?: boolean;
86
106
  /** e.g. 'CREATE VIEW IF NOT EXISTS' or 'CREATE OR REPLACE VIEW' */
87
107
  createViewPrefix: string;
88
108
  /** Returns a parameter placeholder for the given 1-based index. When absent, '?' is used. */
@@ -117,6 +137,35 @@ interface TFilterVisitorOptions {
117
137
  * expression (`SUM("amount")`) — PostgreSQL rejects SELECT aliases in HAVING.
118
138
  */
119
139
  columnRef?: (field: string) => string;
140
+ /**
141
+ * The quoted outer table or alias that relational predicates
142
+ * (`{ nav: { $some | $none: … } }`) correlate to: their `EXISTS` subqueries
143
+ * compare the related rows with `<qualifier>."<column>"`. Defaults to the
144
+ * predicate's source table (`dialect.quoteTable(node.source.table)`), which
145
+ * is right for every statement whose FROM is the bare table; a statement
146
+ * that aliases its FROM (`FROM "items" AS "t"`) must pass that alias here.
147
+ *
148
+ * @since 0.1.147
149
+ */
150
+ qualifier?: string;
151
+ /**
152
+ * Alias sequence of the relational-predicate subqueries (`_rf1`, `_rf2`, …),
153
+ * shared by the visitors of one statement so nested predicates get distinct
154
+ * aliases. A visitor created without one starts its own.
155
+ *
156
+ * @internal
157
+ * @since 0.1.147
158
+ */
159
+ aliasSeq?: TRelationAliasSeq;
160
+ }
161
+ /**
162
+ * Alias counter of the relational-predicate subqueries of one statement.
163
+ *
164
+ * @internal
165
+ * @since 0.1.147
166
+ */
167
+ interface TRelationAliasSeq {
168
+ n: number;
120
169
  }
121
170
  /**
122
171
  * Creates a dialect-specific filter visitor for `walkFilter`.
@@ -124,8 +173,18 @@ interface TFilterVisitorOptions {
124
173
  declare function createFilterVisitor(dialect: SqlDialect, options?: TFilterVisitorOptions): FilterVisitor<TSqlFragment>;
125
174
  /**
126
175
  * Translates a filter expression into a parameterized SQL WHERE clause.
176
+ *
177
+ * Relational predicates (`{ nav: { $some | $none: … } }`, resolved by the
178
+ * core into `ResolvedRelationFilter` operands) render as correlated
179
+ * `[NOT] EXISTS (…)` subqueries; `opts.qualifier` names the outer table or
180
+ * alias they correlate to (default: the source table — see
181
+ * {@link TFilterVisitorOptions.qualifier}). `opts.columnRef` overrides how
182
+ * the filter's own columns render (e.g. `t."col"` for an aliased FROM).
183
+ * Placeholders stay positional `?` in textual order.
184
+ *
185
+ * @param opts - since 0.1.147
127
186
  */
128
- declare function buildWhere(dialect: SqlDialect, filter: FilterExpr): TSqlFragment;
187
+ declare function buildWhere(dialect: SqlDialect, filter: FilterExpr, opts?: TFilterVisitorOptions): TSqlFragment;
129
188
  //#endregion
130
189
  //#region src/geo.d.ts
131
190
  /**
@@ -135,6 +194,13 @@ declare function buildWhere(dialect: SqlDialect, filter: FilterExpr): TSqlFragme
135
194
  * across dialects, so the public name can't be used directly.
136
195
  */
137
196
  declare const GEO_DISTANCE_ALIAS = "__atscript_distance";
197
+ /**
198
+ * Alias of the searched table inside the geo and vector search statements —
199
+ * a filter rendered into them correlates its relational predicates to it
200
+ * (`buildWhere(filter, { qualifier: dialect.quoteTable(SEARCH_SOURCE_ALIAS) })`).
201
+ * @since 0.1.147
202
+ */
203
+ declare const SEARCH_SOURCE_ALIAS = "t";
138
204
  /** The query controls a geo search page reads — an adapter passes its `query.controls` as-is. */
139
205
  interface TGeoSearchControls {
140
206
  $limit?: number;
@@ -272,6 +338,25 @@ declare function buildInsertMany(dialect: SqlDialect, table: string, rows: reado
272
338
  * Builds a SELECT statement with optional sort, limit, offset, projection.
273
339
  */
274
340
  declare function buildSelect(dialect: SqlDialect, table: string, where: TSqlFragment, controls?: DbControls): TSqlFragment;
341
+ /**
342
+ * The row-number column {@link buildPartitionedSelect} adds to every row;
343
+ * {@link stripPartitionRowNumber} removes it from the result rows.
344
+ * @since 0.1.147
345
+ */
346
+ declare const PARTITION_ROW_NUMBER_ALIAS = "__atscript_rn";
347
+ /**
348
+ * Builds a SELECT whose `$skip` / `$limit` apply to each partition — the rows
349
+ * sharing the values of the `partitionBy` columns (physical names) — instead
350
+ * of to the whole result: a `ROW_NUMBER() OVER (PARTITION BY … ORDER BY
351
+ * <$sort>)` window in a derived table, filtered on the row number. The rows
352
+ * of each partition come out in `$sort` order (partitions interleave); every
353
+ * row carries {@link PARTITION_ROW_NUMBER_ALIAS}. Window functions need
354
+ * SQLite ≥ 3.25, MySQL ≥ 8.0 or MariaDB ≥ 10.2.
355
+ * @since 0.1.147
356
+ */
357
+ declare function buildPartitionedSelect(dialect: SqlDialect, table: string, where: TSqlFragment, controls: DbControls, partitionBy: readonly string[]): TSqlFragment;
358
+ /** Removes {@link PARTITION_ROW_NUMBER_ALIAS} from rows read by {@link buildPartitionedSelect}. */
359
+ declare function stripPartitionRowNumber<R extends Record<string, unknown>>(rows: R[]): R[];
275
360
  /**
276
361
  * Marker value for {@link buildUpdate}: the column is assigned its DDL
277
362
  * `DEFAULT` (`SET "col" = DEFAULT`, no bound parameter). Produced by
@@ -376,6 +461,13 @@ declare function jsonDollarPath(path: readonly string[]): string;
376
461
  declare function sqlTimeZoneLiteral(tz: string): string;
377
462
  /** Converts a JS value to a SQL-bindable parameter. Objects/arrays -> JSON, booleans -> 0/1. */
378
463
  declare function toSqlValue(value: unknown): unknown;
464
+ /**
465
+ * `FOREIGN KEY (…) REFERENCES <target> (…)[ ON DELETE …][ ON UPDATE …]` of a
466
+ * foreign key over its physical columns ({@link fkColumns}); the target is
467
+ * schema-qualified when it declares `@db.schema`. `quote` quotes one
468
+ * identifier.
469
+ */
470
+ declare function foreignKeySql(quote: (name: string) => string, fk: TDbForeignKey): string;
379
471
  declare function refActionToSql(action: TDbReferentialAction): string;
380
472
  /** Returns a safe SQL DEFAULT literal for a given design type. */
381
473
  declare function defaultValueForType(designType: string): string;
@@ -446,4 +538,4 @@ declare function parseRegexString(value: unknown): {
446
538
  flags: string;
447
539
  };
448
540
  //#endregion
449
- export { AGG_FN_SQL, EMPTY_AND, EMPTY_OR, GEO_DISTANCE_ALIAS, SQL_DEFAULT, type SqlDialect, type TFilterVisitorOptions, type TGeoCircle, type TGeoSearchControls, type TGeoWindow, type TReplaceColumn, type TSqlFragment, VECTOR_DISTANCE_ALIAS, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildProjection, buildSelect, buildUpdate, buildVectorSearchCount, buildVectorSearchSelect, buildWhere, createFilterVisitor, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, finalizeParams, geoWindowFromControls, groupKeySql, insertManyColumns, jsonDollarPath, normalizeGeoPointValue, parseRegexString, queryNodeToSql, queryOpToSql, quotedJsonPathSegments, refActionToSql, renameGeoDistance, replaceColumnsFor, sqlStringLiteral, sqlTimeZoneLiteral, toSqlValue, vectorDistanceSource };
541
+ export { AGG_FN_SQL, EMPTY_AND, EMPTY_OR, GEO_DISTANCE_ALIAS, PARTITION_ROW_NUMBER_ALIAS, SEARCH_SOURCE_ALIAS, SQL_DEFAULT, type SqlDialect, type TFilterVisitorOptions, type TGeoCircle, type TGeoSearchControls, type TGeoWindow, type TReplaceColumn, type TSqlFragment, VECTOR_DISTANCE_ALIAS, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildPartitionedSelect, buildProjection, buildSelect, buildUpdate, buildVectorSearchCount, buildVectorSearchSelect, buildWhere, createFilterVisitor, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, finalizeParams, foreignKeySql, geoWindowFromControls, groupKeySql, insertManyColumns, jsonDollarPath, normalizeGeoPointValue, parseRegexString, queryNodeToSql, queryOpToSql, quotedJsonPathSegments, refActionToSql, renameGeoDistance, replaceColumnsFor, sqlStringLiteral, sqlTimeZoneLiteral, stripPartitionRowNumber, toSqlValue, vectorDistanceSource };
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { BUCKET_UNITS, TIME_ZONE_NAME_RE, WEEK_STARTS, walkFilter } from "@uniqu/core";
2
- import { DbError, isFieldRef } from "@atscript/db";
2
+ import { DbError, containsRelationFilter, fkColumns, isFieldRef, isResolvedRelationFilter } from "@atscript/db";
3
3
  import { assertAggregateFn, resolveAlias } from "@atscript/db/agg";
4
4
  //#region src/dialect.ts
5
5
  /**
@@ -60,6 +60,8 @@ const EMPTY_OR = {
60
60
  */
61
61
  function createFilterVisitor(dialect, options) {
62
62
  const columnRef = options?.columnRef ?? ((field) => dialect.quoteIdentifier(field));
63
+ const qualifier = options?.qualifier;
64
+ const aliasSeq = options?.aliasSeq ?? { n: 0 };
63
65
  return {
64
66
  comparison(field, op, value) {
65
67
  if (op === "$geoWithin") {
@@ -152,9 +154,75 @@ function createFilterVisitor(dialect, options) {
152
154
  sql: `NOT (${child.sql})`,
153
155
  params: child.params
154
156
  };
157
+ },
158
+ relation(field, op, operand) {
159
+ if (!isResolvedRelationFilter(operand)) throw new DbError("REL_FILTER_NOT_SUPPORTED", [{
160
+ path: field,
161
+ message: `Relational predicate "${op}" on "${field}" reached the SQL renderer unresolved`
162
+ }]);
163
+ return renderRelation(dialect, op, operand, qualifier ?? dialect.quoteTable(operand.source.table), aliasSeq);
155
164
  }
156
165
  };
157
166
  }
167
+ /**
168
+ * Renders one resolved relational predicate as a correlated `EXISTS`:
169
+ *
170
+ * ```sql
171
+ * -- to / from
172
+ * EXISTS (SELECT 1 FROM <target> AS "_rf1"
173
+ * WHERE "_rf1"."<pair.target>" = <outer>."<pair.source>" [AND …] [AND <inner>])
174
+ * -- via
175
+ * EXISTS (SELECT 1 FROM <junction> AS "_rf1" JOIN <target> AS "_rf2"
176
+ * ON "_rf2"."<toTarget.target>" = "_rf1"."<toTarget.junction>" [AND …]
177
+ * WHERE "_rf1"."<toSource.junction>" = <outer>."<toSource.source>" [AND …]
178
+ * [AND <junction filter on _rf1>] [AND <inner on _rf2>])
179
+ * ```
180
+ *
181
+ * `$none` is `NOT EXISTS (…)`. Outer columns are always qualified (a bare
182
+ * column would bind to the subquery's table on a self relation or a
183
+ * same-named column). A NULL foreign-key component never satisfies `=`, so
184
+ * such a row has no related row (`$some` false, `$none` true).
185
+ */
186
+ function renderRelation(dialect, op, node, outer, aliasSeq) {
187
+ const q = (name) => dialect.quoteIdentifier(name);
188
+ const nextAlias = () => q(`_rf${++aliasSeq.n}`);
189
+ const conditions = [];
190
+ const params = [];
191
+ const addFilter = (filter, alias) => {
192
+ if (!filter || Object.keys(filter).length === 0) return;
193
+ const fragment = walkFilter(filter, createFilterVisitor(dialect, {
194
+ columnRef: (field) => `${alias}.${q(field)}`,
195
+ qualifier: alias,
196
+ aliasSeq
197
+ }));
198
+ if (fragment) {
199
+ conditions.push(fragment.sql);
200
+ params.push(...fragment.params);
201
+ }
202
+ };
203
+ let from;
204
+ if (node.kind === "via") {
205
+ const junction = node.junction;
206
+ if (!junction) throw new Error(`Relational predicate on "${node.nav}": a "via" relation needs a junction`);
207
+ const j = nextAlias();
208
+ const t = nextAlias();
209
+ const on = junction.toTarget.map((p) => `${t}.${q(p.target)} = ${j}.${q(p.junction)}`);
210
+ from = `${dialect.quoteTable(junction.table)} AS ${j} JOIN ${dialect.quoteTable(node.target.table)} AS ${t} ON ${on.join(" AND ")}`;
211
+ for (const p of junction.toSource) conditions.push(`${j}.${q(p.junction)} = ${outer}.${q(p.source)}`);
212
+ addFilter(junction.filter, j);
213
+ addFilter(node.filter, t);
214
+ } else {
215
+ const t = nextAlias();
216
+ from = `${dialect.quoteTable(node.target.table)} AS ${t}`;
217
+ for (const p of node.pairs) conditions.push(`${t}.${q(p.target)} = ${outer}.${q(p.source)}`);
218
+ addFilter(node.filter, t);
219
+ }
220
+ const where = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
221
+ return {
222
+ sql: `${op === "$none" ? "NOT " : ""}EXISTS (SELECT 1 FROM ${from}${where})`,
223
+ params
224
+ };
225
+ }
158
226
  const visitorCache = /* @__PURE__ */ new WeakMap();
159
227
  function getVisitor(dialect) {
160
228
  let visitor = visitorCache.get(dialect);
@@ -166,10 +234,23 @@ function getVisitor(dialect) {
166
234
  }
167
235
  /**
168
236
  * Translates a filter expression into a parameterized SQL WHERE clause.
237
+ *
238
+ * Relational predicates (`{ nav: { $some | $none: … } }`, resolved by the
239
+ * core into `ResolvedRelationFilter` operands) render as correlated
240
+ * `[NOT] EXISTS (…)` subqueries; `opts.qualifier` names the outer table or
241
+ * alias they correlate to (default: the source table — see
242
+ * {@link TFilterVisitorOptions.qualifier}). `opts.columnRef` overrides how
243
+ * the filter's own columns render (e.g. `t."col"` for an aliased FROM).
244
+ * Placeholders stay positional `?` in textual order.
245
+ *
246
+ * @param opts - since 0.1.147
169
247
  */
170
- function buildWhere(dialect, filter) {
248
+ function buildWhere(dialect, filter, opts) {
171
249
  if (!filter || Object.keys(filter).length === 0) return EMPTY_AND;
172
- return walkFilter(filter, getVisitor(dialect)) ?? EMPTY_AND;
250
+ return walkFilter(filter, opts || containsRelationFilter(filter) ? createFilterVisitor(dialect, {
251
+ ...opts,
252
+ aliasSeq: opts?.aliasSeq ?? { n: 0 }
253
+ }) : getVisitor(dialect)) ?? EMPTY_AND;
173
254
  }
174
255
  //#endregion
175
256
  //#region src/common.ts
@@ -214,6 +295,20 @@ function toSqlValue(value) {
214
295
  if (typeof value === "boolean") return value ? 1 : 0;
215
296
  return value;
216
297
  }
298
+ /**
299
+ * `FOREIGN KEY (…) REFERENCES <target> (…)[ ON DELETE …][ ON UPDATE …]` of a
300
+ * foreign key over its physical columns ({@link fkColumns}); the target is
301
+ * schema-qualified when it declares `@db.schema`. `quote` quotes one
302
+ * identifier.
303
+ */
304
+ function foreignKeySql(quote, fk) {
305
+ const { fields, targetFields } = fkColumns(fk);
306
+ const target = fk.targetSchema ? `${quote(fk.targetSchema)}.${quote(fk.targetTable)}` : quote(fk.targetTable);
307
+ let sql = `FOREIGN KEY (${fields.map(quote).join(", ")}) REFERENCES ${target} (${targetFields.map(quote).join(", ")})`;
308
+ if (fk.onDelete) sql += ` ON DELETE ${refActionToSql(fk.onDelete)}`;
309
+ if (fk.onUpdate) sql += ` ON UPDATE ${refActionToSql(fk.onUpdate)}`;
310
+ return sql;
311
+ }
217
312
  function refActionToSql(action) {
218
313
  switch (action) {
219
314
  case "cascade": return "CASCADE";
@@ -531,6 +626,80 @@ function viewAggExpr(dialect, c, resolveFieldRef) {
531
626
  return c.aggFn === "sum" ? `COALESCE(${call}, 0)` : call;
532
627
  }
533
628
  /**
629
+ * The SQL expression of a computed view column (`@db.compute`), evaluated in
630
+ * IEEE double on every dialect: each field and literal leaf is cast with
631
+ * {@link SqlDialect.castDouble} (a computed leaf is already a double and is
632
+ * inlined); `+ - *` render as `(l op r)`, `/` as `(l / NULLIF(r, 0))` —
633
+ * division by zero is NULL — unary minus as `(-x)`, `coalesce` as
634
+ * `COALESCE(…)`. A leaf reads its column like the SELECT list does: an
635
+ * aggregate's call ({@link viewAggExpr}) or a dimension's source
636
+ * ({@link viewSourceExpr}). `byPath` holds the view's mappings by `viewPath`.
637
+ *
638
+ * In a `grouped` view a JSON-extracted dimension leaf reads `MIN(<extract>)`:
639
+ * the extraction is a GROUP BY key, so every row of a group holds the same
640
+ * value, but MySQL's ONLY_FULL_GROUP_BY rejects an expression over the raw
641
+ * JSON column (which is not itself grouped) — the aggregate form is accepted
642
+ * everywhere and yields that same value.
643
+ *
644
+ * @throws when the dialect has no `castDouble`, or a leaf names no column.
645
+ * @since 0.1.147
646
+ */
647
+ function viewComputeExpr(dialect, c, byPath, resolveFieldRef, grouped = false, cache = /* @__PURE__ */ new Map()) {
648
+ const cached = cache.get(c.viewPath);
649
+ if (cached !== void 0) return cached;
650
+ const cast = dialect.castDouble?.bind(dialect);
651
+ if (!cast) throw new Error(`View column "${c.viewColumn}": computed view columns are not supported by this adapter`);
652
+ const render = (e) => {
653
+ if (typeof e === "number") {
654
+ if (!Number.isFinite(e)) throw new Error(`View column "${c.viewColumn}": non-finite literal in @db.compute`);
655
+ return cast(String(e));
656
+ }
657
+ if ("field" in e) {
658
+ const leaf = byPath.get(e.field);
659
+ if (!leaf) throw new Error(`View column "${c.viewColumn}": "${e.field}" is not a column of the view`);
660
+ if (leaf.expr !== void 0) return viewComputeExpr(dialect, leaf, byPath, resolveFieldRef, grouped, cache);
661
+ if (leaf.aggFn) return cast(viewAggExpr(dialect, leaf, resolveFieldRef));
662
+ const source = viewSourceExpr(dialect, leaf);
663
+ return cast(grouped && leaf.json ? `MIN(${source})` : source);
664
+ }
665
+ const args = e.args.map(render);
666
+ switch (e.op) {
667
+ case "neg": return `(-${args[0]})`;
668
+ case "coalesce": return `COALESCE(${args.join(", ")})`;
669
+ case "/": return `(${args[0]} / NULLIF(${args[1]}, 0))`;
670
+ default: return `(${args[0]} ${e.op} ${args[1]})`;
671
+ }
672
+ };
673
+ const sql = render(c.expr);
674
+ cache.set(c.viewPath, sql);
675
+ return sql;
676
+ }
677
+ /**
678
+ * The `ON` clause of a first-row join (`join.first`): the joined row is the
679
+ * one whose primary key equals the key of the FIRST matching target row —
680
+ * a correlated scalar subquery
681
+ * `"T"."id" = (SELECT "T"."id" FROM "table" AS "T" WHERE <condition> ORDER BY … LIMIT 1)`.
682
+ * The inner `FROM` repeats the join's own target / alias text, so the
683
+ * condition's refs to the joined scope bind to the inner row (SQL name
684
+ * shadowing) while refs to the entry and earlier joins stay correlated to
685
+ * the outer query. The ordering ends with the primary key, so the pick is
686
+ * deterministic; NULL sorts first in `asc` (`nullsSortLargest` dialects
687
+ * render `NULLS FIRST` / `NULLS LAST`). No window functions — SQLite, MySQL 8
688
+ * and PostgreSQL render it alike.
689
+ * @since 0.1.147
690
+ */
691
+ function firstRowOn(dialect, join, target, condition, resolveFieldRef) {
692
+ const first = join.first;
693
+ const key = resolveFieldRef({
694
+ type: join.targetType,
695
+ field: first.key
696
+ });
697
+ return `${key} = (SELECT ${key} FROM ${target} WHERE ${condition} ORDER BY ${first.order.map(({ ref, desc }) => {
698
+ const nulls = dialect.nullsSortLargest ? desc ? " NULLS LAST" : " NULLS FIRST" : "";
699
+ return `${resolveFieldRef(ref)} ${desc ? "DESC" : "ASC"}${nulls}`;
700
+ }).join(", ")} LIMIT 1)`;
701
+ }
702
+ /**
534
703
  * Builds a CREATE VIEW statement from a view plan and column mappings.
535
704
  *
536
705
  * Joins render in declaration order — `JOIN` (inner, the default) or
@@ -542,35 +711,41 @@ function viewAggExpr(dialect, c, resolveFieldRef) {
542
711
  * join target may be views.
543
712
  */
544
713
  function buildCreateView(dialect, viewName, plan, columns, resolveFieldRef) {
545
- const selectCols = columns.map((c) => {
546
- return `${c.aggFn ? viewAggExpr(dialect, c, resolveFieldRef) : viewSourceExpr(dialect, c)} AS ${dialect.quoteIdentifier(c.viewColumn)}`;
547
- }).join(", ");
714
+ const columnByPath = new Map(columns.map((c) => [c.viewPath, c]));
715
+ const hasAggregates = columns.some((c) => c.aggFn);
716
+ const computedSql = /* @__PURE__ */ new Map();
717
+ /** A column's SELECT expression: computed, aggregate or plain source. */
718
+ const columnExpr = (c) => c.expr !== void 0 ? viewComputeExpr(dialect, c, columnByPath, resolveFieldRef, hasAggregates, computedSql) : c.aggFn ? viewAggExpr(dialect, c, resolveFieldRef) : viewSourceExpr(dialect, c);
719
+ const selectCols = columns.map((c) => `${columnExpr(c)} AS ${dialect.quoteIdentifier(c.viewColumn)}`).join(", ");
548
720
  let sql = `${dialect.createViewPrefix} ${dialect.quoteTable(viewName)} AS SELECT ${selectCols} FROM ${dialect.quoteIdentifier(plan.entryTable)}`;
549
721
  for (const join of plan.joins) {
550
- const onClause = queryNodeToSql(join.condition, resolveFieldRef);
722
+ const condition = queryNodeToSql(join.condition, resolveFieldRef);
551
723
  const keyword = join.kind === "left" ? "LEFT JOIN" : "JOIN";
552
724
  const target = join.scope === join.targetTable ? dialect.quoteIdentifier(join.targetTable) : `${dialect.quoteIdentifier(join.targetTable)} AS ${dialect.quoteIdentifier(join.scope)}`;
725
+ const onClause = join.first ? firstRowOn(dialect, join, target, condition, resolveFieldRef) : condition;
553
726
  sql += ` ${keyword} ${target} ON ${onClause}`;
554
727
  }
555
728
  if (plan.filter) {
556
729
  const whereClause = queryNodeToSql(plan.filter, resolveFieldRef);
557
730
  sql += ` WHERE ${whereClause}`;
558
731
  }
559
- if (columns.some((c) => c.aggFn)) {
560
- const dimensionCols = columns.filter((c) => !c.aggFn);
732
+ if (hasAggregates) {
733
+ const dimensionCols = columns.filter((c) => !c.aggFn && c.expr === void 0);
561
734
  if (dimensionCols.length > 0) {
562
735
  const groupByCols = dimensionCols.map((c) => viewSourceExpr(dialect, c)).join(", ");
563
736
  sql += ` GROUP BY ${groupByCols}`;
564
737
  }
565
738
  if (plan.having) {
566
- const columnByPath = /* @__PURE__ */ new Map();
567
- for (const c of columns) columnByPath.set(c.viewPath, c);
739
+ let grouped;
740
+ const groupedColumns = () => grouped ??= new Set(dimensionCols.filter((c) => !c.json).map((c) => c.sourceColumn.toLowerCase()));
568
741
  const havingResolver = (ref) => {
569
742
  const col = ref.type ? void 0 : columnByPath.get(ref.field);
570
743
  if (!col) return resolveFieldRef(ref);
744
+ if (col.expr !== void 0) return columnExpr(col);
571
745
  if (col.aggFn) return viewAggExpr(dialect, col, resolveFieldRef);
572
746
  const expr = viewSourceExpr(dialect, col);
573
- return col.json ? havingGroupRef(dialect, expr, col.viewColumn) : expr;
747
+ if (!col.json) return expr;
748
+ return dialect.bucketAliasInHaving && groupedColumns().has(col.viewColumn.toLowerCase()) ? `MIN(${expr})` : havingGroupRef(dialect, expr, col.viewColumn);
574
749
  };
575
750
  const havingClause = queryNodeToSql(plan.having, havingResolver);
576
751
  sql += ` HAVING ${havingClause}`;
@@ -631,11 +806,8 @@ function buildInsertMany(dialect, table, rows, columns = insertManyColumns(rows)
631
806
  function buildSelect(dialect, table, where, controls) {
632
807
  let sql = `SELECT ${buildProjection(dialect, controls?.$select)} FROM ${dialect.quoteTable(table)} WHERE ${where.sql}`;
633
808
  const params = [...where.params];
634
- if (controls?.$sort) {
635
- const orderParts = [];
636
- for (const [col, dir] of Object.entries(controls.$sort)) orderParts.push(`${dialect.quoteIdentifier(col)} ${dir === -1 ? "DESC" : "ASC"}`);
637
- if (orderParts.length > 0) sql += ` ORDER BY ${orderParts.join(", ")}`;
638
- }
809
+ const orderBy = orderByList(dialect, controls?.$sort);
810
+ if (orderBy) sql += ` ORDER BY ${orderBy}`;
639
811
  if (controls?.$limit !== void 0) {
640
812
  sql += ` LIMIT ?`;
641
813
  params.push(controls.$limit);
@@ -650,6 +822,53 @@ function buildSelect(dialect, table, where, controls) {
650
822
  params
651
823
  });
652
824
  }
825
+ /** `"col" ASC, "other" DESC` of a physical `$sort` — `""` when it orders nothing. */
826
+ function orderByList(dialect, sort) {
827
+ if (!sort) return "";
828
+ const parts = [];
829
+ for (const [col, dir] of Object.entries(sort)) parts.push(`${dialect.quoteIdentifier(col)} ${dir === -1 ? "DESC" : "ASC"}`);
830
+ return parts.join(", ");
831
+ }
832
+ /**
833
+ * The row-number column {@link buildPartitionedSelect} adds to every row;
834
+ * {@link stripPartitionRowNumber} removes it from the result rows.
835
+ * @since 0.1.147
836
+ */
837
+ const PARTITION_ROW_NUMBER_ALIAS = "__atscript_rn";
838
+ /**
839
+ * Builds a SELECT whose `$skip` / `$limit` apply to each partition — the rows
840
+ * sharing the values of the `partitionBy` columns (physical names) — instead
841
+ * of to the whole result: a `ROW_NUMBER() OVER (PARTITION BY … ORDER BY
842
+ * <$sort>)` window in a derived table, filtered on the row number. The rows
843
+ * of each partition come out in `$sort` order (partitions interleave); every
844
+ * row carries {@link PARTITION_ROW_NUMBER_ALIAS}. Window functions need
845
+ * SQLite ≥ 3.25, MySQL ≥ 8.0 or MariaDB ≥ 10.2.
846
+ * @since 0.1.147
847
+ */
848
+ function buildPartitionedSelect(dialect, table, where, controls, partitionBy) {
849
+ const rn = dialect.quoteIdentifier(PARTITION_ROW_NUMBER_ALIAS);
850
+ const orderBy = orderByList(dialect, controls.$sort);
851
+ const window = `PARTITION BY ${partitionBy.map((col) => dialect.quoteIdentifier(col)).join(", ")}` + (orderBy ? ` ORDER BY ${orderBy}` : "");
852
+ const inner = `SELECT ${buildProjection(dialect, controls.$select)}, ROW_NUMBER() OVER (${window}) AS ${rn} FROM ${dialect.quoteTable(table)} WHERE ${where.sql}`;
853
+ const skip = controls.$skip ?? 0;
854
+ const limit = controls.$limit;
855
+ const params = [...where.params, skip];
856
+ let sql = `SELECT * FROM (${inner}) AS ${dialect.quoteIdentifier("__atscript_p")} WHERE ${rn} > ?`;
857
+ if (limit !== void 0 && limit !== null) {
858
+ sql += ` AND ${rn} <= ?`;
859
+ params.push(skip + limit);
860
+ }
861
+ sql += ` ORDER BY ${rn}`;
862
+ return finalizeParams(dialect, {
863
+ sql,
864
+ params
865
+ });
866
+ }
867
+ /** Removes {@link PARTITION_ROW_NUMBER_ALIAS} from rows read by {@link buildPartitionedSelect}. */
868
+ function stripPartitionRowNumber(rows) {
869
+ for (const row of rows) delete row[PARTITION_ROW_NUMBER_ALIAS];
870
+ return rows;
871
+ }
653
872
  /**
654
873
  * Marker value for {@link buildUpdate}: the column is assigned its DDL
655
874
  * `DEFAULT` (`SET "col" = DEFAULT`, no bound parameter). Produced by
@@ -809,6 +1028,13 @@ function buildProjection(dialect, select, qualifier) {
809
1028
  */
810
1029
  const GEO_DISTANCE_ALIAS = "__atscript_distance";
811
1030
  /**
1031
+ * Alias of the searched table inside the geo and vector search statements —
1032
+ * a filter rendered into them correlates its relational predicates to it
1033
+ * (`buildWhere(filter, { qualifier: dialect.quoteTable(SEARCH_SOURCE_ALIAS) })`).
1034
+ * @since 0.1.147
1035
+ */
1036
+ const SEARCH_SOURCE_ALIAS = "t";
1037
+ /**
812
1038
  * Builds a distance-ranked geo search SELECT:
813
1039
  *
814
1040
  * ```sql
@@ -1027,4 +1253,4 @@ function parseRegexString(value) {
1027
1253
  };
1028
1254
  }
1029
1255
  //#endregion
1030
- export { AGG_FN_SQL, EMPTY_AND, EMPTY_OR, GEO_DISTANCE_ALIAS, SQL_DEFAULT, VECTOR_DISTANCE_ALIAS, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildProjection, buildSelect, buildUpdate, buildVectorSearchCount, buildVectorSearchSelect, buildWhere, createFilterVisitor, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, finalizeParams, geoWindowFromControls, groupKeySql, insertManyColumns, jsonDollarPath, normalizeGeoPointValue, parseRegexString, queryNodeToSql, queryOpToSql, quotedJsonPathSegments, refActionToSql, renameGeoDistance, replaceColumnsFor, sqlStringLiteral, sqlTimeZoneLiteral, toSqlValue, vectorDistanceSource };
1256
+ export { AGG_FN_SQL, EMPTY_AND, EMPTY_OR, GEO_DISTANCE_ALIAS, PARTITION_ROW_NUMBER_ALIAS, SEARCH_SOURCE_ALIAS, SQL_DEFAULT, VECTOR_DISTANCE_ALIAS, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildPartitionedSelect, buildProjection, buildSelect, buildUpdate, buildVectorSearchCount, buildVectorSearchSelect, buildWhere, createFilterVisitor, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, finalizeParams, foreignKeySql, geoWindowFromControls, groupKeySql, insertManyColumns, jsonDollarPath, normalizeGeoPointValue, parseRegexString, queryNodeToSql, queryOpToSql, quotedJsonPathSegments, refActionToSql, renameGeoDistance, replaceColumnsFor, sqlStringLiteral, sqlTimeZoneLiteral, stripPartitionRowNumber, toSqlValue, vectorDistanceSource };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atscript/db-sql-tools",
3
- "version": "0.1.145",
3
+ "version": "0.1.147",
4
4
  "description": "Shared SQL builder utilities for @atscript database adapters.",
5
5
  "keywords": [
6
6
  "atscript",
@@ -37,12 +37,12 @@
37
37
  "access": "public"
38
38
  },
39
39
  "devDependencies": {
40
- "@uniqu/core": "^0.1.11",
41
- "unplugin-atscript": "^0.1.97"
40
+ "@uniqu/core": "^0.1.12",
41
+ "unplugin-atscript": "^0.1.99"
42
42
  },
43
43
  "peerDependencies": {
44
- "@uniqu/core": "^0.1.11",
45
- "@atscript/db": "^0.1.145"
44
+ "@uniqu/core": "^0.1.12",
45
+ "@atscript/db": "^0.1.147"
46
46
  },
47
47
  "scripts": {
48
48
  "build": "vp pack",