@atscript/db-sql-tools 0.1.146 → 0.1.148
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 +481 -55
- package/dist/index.d.cts +188 -8
- package/dist/index.d.mts +188 -8
- package/dist/index.mjs +471 -56
- package/package.json +5 -5
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { DbError, aggregateExpressionsNotSupported, containsRelationFilter, fkColumns, isFieldRef, isResolvedRelationFilter } from "@atscript/db";
|
|
1
2
|
import { BUCKET_UNITS, TIME_ZONE_NAME_RE, WEEK_STARTS, walkFilter } from "@uniqu/core";
|
|
2
|
-
import { DbError, isFieldRef } from "@atscript/db";
|
|
3
3
|
import { assertAggregateFn, resolveAlias } from "@atscript/db/agg";
|
|
4
4
|
//#region src/dialect.ts
|
|
5
5
|
/**
|
|
@@ -23,6 +23,32 @@ function havingGroupRef(dialect, expr, alias) {
|
|
|
23
23
|
return dialect.bucketAliasInHaving ? dialect.quoteIdentifier(alias) : expr;
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
26
|
+
* Runs `fn`, rethrowing a driver error as the `DbError`
|
|
27
|
+
* {@link SqlDialect.mapQueryError} maps it to (any other error unchanged).
|
|
28
|
+
* `controls` of the query tell whether it has arithmetic expressions.
|
|
29
|
+
* @since 0.1.148
|
|
30
|
+
*/
|
|
31
|
+
async function mapQueryErrors(dialect, fn, controls) {
|
|
32
|
+
try {
|
|
33
|
+
return await fn();
|
|
34
|
+
} catch (error) {
|
|
35
|
+
const select = controls?.$select;
|
|
36
|
+
const arithmetic = !!(select?.exprAggregates?.length || select?.exprs?.length);
|
|
37
|
+
throw dialect.mapQueryError?.(error, arithmetic) ?? error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* One `ORDER BY` key with the uniform "NULL is the smallest value" ordering:
|
|
42
|
+
* `<expr> ASC` / `<expr> DESC`, plus `NULLS FIRST` / `NULLS LAST` on a
|
|
43
|
+
* dialect where NULL sorts largest ({@link SqlDialect.nullsSortLargest}).
|
|
44
|
+
* Shared by first-row joins and `first` / `last` aggregates.
|
|
45
|
+
* @since 0.1.148
|
|
46
|
+
*/
|
|
47
|
+
function orderKeySql(dialect, expr, desc) {
|
|
48
|
+
const nulls = dialect.nullsSortLargest ? desc ? " NULLS LAST" : " NULLS FIRST" : "";
|
|
49
|
+
return `${expr} ${desc ? "DESC" : "ASC"}${nulls}`;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
26
52
|
* Each JSON path segment wrapped in double quotes (`"a"`), for the dialects'
|
|
27
53
|
* {@link SqlDialect.jsonExtract} path literals (`'$."a"."b"'`, `'{"a","b"}'`).
|
|
28
54
|
*
|
|
@@ -54,12 +80,75 @@ const EMPTY_OR = {
|
|
|
54
80
|
params: []
|
|
55
81
|
};
|
|
56
82
|
//#endregion
|
|
83
|
+
//#region src/arith.ts
|
|
84
|
+
/**
|
|
85
|
+
* Renders a computed expression tree (`@db.compute` on a view, or a query-time
|
|
86
|
+
* arithmetic `$select` entry) as SQL, in IEEE double on every dialect: each
|
|
87
|
+
* literal is cast with {@link SqlDialect.castDouble}; `+ - *` render as
|
|
88
|
+
* `(l op r)`, `/` as `(l / NULLIF(r, 0))` (division by zero is NULL), unary
|
|
89
|
+
* minus as `(-x)` and `coalesce` as `COALESCE(…)`. `leaf` renders a field
|
|
90
|
+
* reference as raw SQL, which is cast to double here — or, as `{ double }`,
|
|
91
|
+
* SQL that already is a double (a nested computed column) and stays as is.
|
|
92
|
+
*
|
|
93
|
+
* The one renderer of both paths, so declared and query-time arithmetic
|
|
94
|
+
* cannot diverge.
|
|
95
|
+
*
|
|
96
|
+
* @param fail - the error to throw for a failure (default `DbError`:
|
|
97
|
+
* `AGG_EXPR_NOT_SUPPORTED` without `castDouble`, `INVALID_QUERY` for a
|
|
98
|
+
* non-finite literal).
|
|
99
|
+
* @since 0.1.148
|
|
100
|
+
*/
|
|
101
|
+
function renderArith(dialect, node, leaf, fail = defaultFailure) {
|
|
102
|
+
const cast = dialect.castDouble?.bind(dialect);
|
|
103
|
+
if (!cast) throw fail("no-cast");
|
|
104
|
+
const render = (e) => {
|
|
105
|
+
if (typeof e === "number") {
|
|
106
|
+
if (!Number.isFinite(e)) throw fail("non-finite");
|
|
107
|
+
return cast(String(e));
|
|
108
|
+
}
|
|
109
|
+
if ("field" in e) {
|
|
110
|
+
const sql = leaf(e.field);
|
|
111
|
+
return typeof sql === "string" ? cast(sql) : sql.double;
|
|
112
|
+
}
|
|
113
|
+
const args = e.args.map(render);
|
|
114
|
+
switch (e.op) {
|
|
115
|
+
case "neg": return `(-${args[0]})`;
|
|
116
|
+
case "coalesce": return `COALESCE(${args.join(", ")})`;
|
|
117
|
+
case "/": return `(${args[0]} / NULLIF(${args[1]}, 0))`;
|
|
118
|
+
default: return `(${args[0]} ${e.op} ${args[1]})`;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
return render(node);
|
|
122
|
+
}
|
|
123
|
+
function defaultFailure(reason) {
|
|
124
|
+
return reason === "no-cast" ? aggregateExpressionsNotSupported() : new DbError("INVALID_QUERY", [{
|
|
125
|
+
path: "$select",
|
|
126
|
+
message: "Expression literal is not finite"
|
|
127
|
+
}]);
|
|
128
|
+
}
|
|
129
|
+
/** The error of a numeric overflow that is no arithmetic expression's (`INVALID_QUERY`, `path` `""`). */
|
|
130
|
+
function numericOutOfRangeError() {
|
|
131
|
+
return new DbError("INVALID_QUERY", [{
|
|
132
|
+
path: "",
|
|
133
|
+
message: "Numeric value out of range"
|
|
134
|
+
}]);
|
|
135
|
+
}
|
|
136
|
+
/** The error of a double overflow in aggregate arithmetic (`INVALID_QUERY`, `path` `$select`). */
|
|
137
|
+
function arithOverflowError() {
|
|
138
|
+
return new DbError("INVALID_QUERY", [{
|
|
139
|
+
path: "$select",
|
|
140
|
+
message: "Arithmetic overflow"
|
|
141
|
+
}]);
|
|
142
|
+
}
|
|
143
|
+
//#endregion
|
|
57
144
|
//#region src/filter-builder.ts
|
|
58
145
|
/**
|
|
59
146
|
* Creates a dialect-specific filter visitor for `walkFilter`.
|
|
60
147
|
*/
|
|
61
148
|
function createFilterVisitor(dialect, options) {
|
|
62
149
|
const columnRef = options?.columnRef ?? ((field) => dialect.quoteIdentifier(field));
|
|
150
|
+
const qualifier = options?.qualifier;
|
|
151
|
+
const aliasSeq = options?.aliasSeq ?? { n: 0 };
|
|
63
152
|
return {
|
|
64
153
|
comparison(field, op, value) {
|
|
65
154
|
if (op === "$geoWithin") {
|
|
@@ -152,9 +241,75 @@ function createFilterVisitor(dialect, options) {
|
|
|
152
241
|
sql: `NOT (${child.sql})`,
|
|
153
242
|
params: child.params
|
|
154
243
|
};
|
|
244
|
+
},
|
|
245
|
+
relation(field, op, operand) {
|
|
246
|
+
if (!isResolvedRelationFilter(operand)) throw new DbError("REL_FILTER_NOT_SUPPORTED", [{
|
|
247
|
+
path: field,
|
|
248
|
+
message: `Relational predicate "${op}" on "${field}" reached the SQL renderer unresolved`
|
|
249
|
+
}]);
|
|
250
|
+
return renderRelation(dialect, op, operand, qualifier ?? dialect.quoteTable(operand.source.table), aliasSeq);
|
|
155
251
|
}
|
|
156
252
|
};
|
|
157
253
|
}
|
|
254
|
+
/**
|
|
255
|
+
* Renders one resolved relational predicate as a correlated `EXISTS`:
|
|
256
|
+
*
|
|
257
|
+
* ```sql
|
|
258
|
+
* -- to / from
|
|
259
|
+
* EXISTS (SELECT 1 FROM <target> AS "_rf1"
|
|
260
|
+
* WHERE "_rf1"."<pair.target>" = <outer>."<pair.source>" [AND …] [AND <inner>])
|
|
261
|
+
* -- via
|
|
262
|
+
* EXISTS (SELECT 1 FROM <junction> AS "_rf1" JOIN <target> AS "_rf2"
|
|
263
|
+
* ON "_rf2"."<toTarget.target>" = "_rf1"."<toTarget.junction>" [AND …]
|
|
264
|
+
* WHERE "_rf1"."<toSource.junction>" = <outer>."<toSource.source>" [AND …]
|
|
265
|
+
* [AND <junction filter on _rf1>] [AND <inner on _rf2>])
|
|
266
|
+
* ```
|
|
267
|
+
*
|
|
268
|
+
* `$none` is `NOT EXISTS (…)`. Outer columns are always qualified (a bare
|
|
269
|
+
* column would bind to the subquery's table on a self relation or a
|
|
270
|
+
* same-named column). A NULL foreign-key component never satisfies `=`, so
|
|
271
|
+
* such a row has no related row (`$some` false, `$none` true).
|
|
272
|
+
*/
|
|
273
|
+
function renderRelation(dialect, op, node, outer, aliasSeq) {
|
|
274
|
+
const q = (name) => dialect.quoteIdentifier(name);
|
|
275
|
+
const nextAlias = () => q(`_rf${++aliasSeq.n}`);
|
|
276
|
+
const conditions = [];
|
|
277
|
+
const params = [];
|
|
278
|
+
const addFilter = (filter, alias) => {
|
|
279
|
+
if (!filter || Object.keys(filter).length === 0) return;
|
|
280
|
+
const fragment = walkFilter(filter, createFilterVisitor(dialect, {
|
|
281
|
+
columnRef: (field) => `${alias}.${q(field)}`,
|
|
282
|
+
qualifier: alias,
|
|
283
|
+
aliasSeq
|
|
284
|
+
}));
|
|
285
|
+
if (fragment) {
|
|
286
|
+
conditions.push(fragment.sql);
|
|
287
|
+
params.push(...fragment.params);
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
let from;
|
|
291
|
+
if (node.kind === "via") {
|
|
292
|
+
const junction = node.junction;
|
|
293
|
+
if (!junction) throw new Error(`Relational predicate on "${node.nav}": a "via" relation needs a junction`);
|
|
294
|
+
const j = nextAlias();
|
|
295
|
+
const t = nextAlias();
|
|
296
|
+
const on = junction.toTarget.map((p) => `${t}.${q(p.target)} = ${j}.${q(p.junction)}`);
|
|
297
|
+
from = `${dialect.quoteTable(junction.table)} AS ${j} JOIN ${dialect.quoteTable(node.target.table)} AS ${t} ON ${on.join(" AND ")}`;
|
|
298
|
+
for (const p of junction.toSource) conditions.push(`${j}.${q(p.junction)} = ${outer}.${q(p.source)}`);
|
|
299
|
+
addFilter(junction.filter, j);
|
|
300
|
+
addFilter(node.filter, t);
|
|
301
|
+
} else {
|
|
302
|
+
const t = nextAlias();
|
|
303
|
+
from = `${dialect.quoteTable(node.target.table)} AS ${t}`;
|
|
304
|
+
for (const p of node.pairs) conditions.push(`${t}.${q(p.target)} = ${outer}.${q(p.source)}`);
|
|
305
|
+
addFilter(node.filter, t);
|
|
306
|
+
}
|
|
307
|
+
const where = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
|
|
308
|
+
return {
|
|
309
|
+
sql: `${op === "$none" ? "NOT " : ""}EXISTS (SELECT 1 FROM ${from}${where})`,
|
|
310
|
+
params
|
|
311
|
+
};
|
|
312
|
+
}
|
|
158
313
|
const visitorCache = /* @__PURE__ */ new WeakMap();
|
|
159
314
|
function getVisitor(dialect) {
|
|
160
315
|
let visitor = visitorCache.get(dialect);
|
|
@@ -166,10 +321,23 @@ function getVisitor(dialect) {
|
|
|
166
321
|
}
|
|
167
322
|
/**
|
|
168
323
|
* Translates a filter expression into a parameterized SQL WHERE clause.
|
|
324
|
+
*
|
|
325
|
+
* Relational predicates (`{ nav: { $some | $none: … } }`, resolved by the
|
|
326
|
+
* core into `ResolvedRelationFilter` operands) render as correlated
|
|
327
|
+
* `[NOT] EXISTS (…)` subqueries; `opts.qualifier` names the outer table or
|
|
328
|
+
* alias they correlate to (default: the source table — see
|
|
329
|
+
* {@link TFilterVisitorOptions.qualifier}). `opts.columnRef` overrides how
|
|
330
|
+
* the filter's own columns render (e.g. `t."col"` for an aliased FROM).
|
|
331
|
+
* Placeholders stay positional `?` in textual order.
|
|
332
|
+
*
|
|
333
|
+
* @param opts - since 0.1.147
|
|
169
334
|
*/
|
|
170
|
-
function buildWhere(dialect, filter) {
|
|
335
|
+
function buildWhere(dialect, filter, opts) {
|
|
171
336
|
if (!filter || Object.keys(filter).length === 0) return EMPTY_AND;
|
|
172
|
-
return walkFilter(filter,
|
|
337
|
+
return walkFilter(filter, opts || containsRelationFilter(filter) ? createFilterVisitor(dialect, {
|
|
338
|
+
...opts,
|
|
339
|
+
aliasSeq: opts?.aliasSeq ?? { n: 0 }
|
|
340
|
+
}) : getVisitor(dialect)) ?? EMPTY_AND;
|
|
173
341
|
}
|
|
174
342
|
//#endregion
|
|
175
343
|
//#region src/common.ts
|
|
@@ -214,6 +382,20 @@ function toSqlValue(value) {
|
|
|
214
382
|
if (typeof value === "boolean") return value ? 1 : 0;
|
|
215
383
|
return value;
|
|
216
384
|
}
|
|
385
|
+
/**
|
|
386
|
+
* `FOREIGN KEY (…) REFERENCES <target> (…)[ ON DELETE …][ ON UPDATE …]` of a
|
|
387
|
+
* foreign key over its physical columns ({@link fkColumns}); the target is
|
|
388
|
+
* schema-qualified when it declares `@db.schema`. `quote` quotes one
|
|
389
|
+
* identifier.
|
|
390
|
+
*/
|
|
391
|
+
function foreignKeySql(quote, fk) {
|
|
392
|
+
const { fields, targetFields } = fkColumns(fk);
|
|
393
|
+
const target = fk.targetSchema ? `${quote(fk.targetSchema)}.${quote(fk.targetTable)}` : quote(fk.targetTable);
|
|
394
|
+
let sql = `FOREIGN KEY (${fields.map(quote).join(", ")}) REFERENCES ${target} (${targetFields.map(quote).join(", ")})`;
|
|
395
|
+
if (fk.onDelete) sql += ` ON DELETE ${refActionToSql(fk.onDelete)}`;
|
|
396
|
+
if (fk.onUpdate) sql += ` ON UPDATE ${refActionToSql(fk.onUpdate)}`;
|
|
397
|
+
return sql;
|
|
398
|
+
}
|
|
217
399
|
function refActionToSql(action) {
|
|
218
400
|
switch (action) {
|
|
219
401
|
case "cascade": return "CASCADE";
|
|
@@ -332,15 +514,23 @@ const AGG_FN_SQL = {
|
|
|
332
514
|
*/
|
|
333
515
|
function renderAggCall(fn, arg, path) {
|
|
334
516
|
assertAggregateFn(fn, path);
|
|
335
|
-
|
|
517
|
+
if (fn === "countDistinct") return `COUNT(DISTINCT ${arg})`;
|
|
518
|
+
return `${AGG_FN_SQL[fn]}(${arg})`;
|
|
336
519
|
}
|
|
337
|
-
/**
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
520
|
+
/**
|
|
521
|
+
* `MIN` / `MAX` of a column the engine may not aggregate directly: a boolean
|
|
522
|
+
* on a dialect with {@link SqlDialect.booleanAggregates} (PostgreSQL has no
|
|
523
|
+
* `MIN(boolean)`) renders its stand-in.
|
|
524
|
+
*/
|
|
525
|
+
function pickSql(dialect, select, fn, column, sql) {
|
|
526
|
+
const bool = select?.sources.get(column)?.designType === "boolean" ? dialect.booleanAggregates?.[fn] : void 0;
|
|
527
|
+
return bool ? `${bool}(${sql})` : renderAggCall(fn, sql);
|
|
341
528
|
}
|
|
342
|
-
|
|
343
|
-
|
|
529
|
+
/** The bare aggregate call, e.g. `SUM("amount")` / `COUNT(*)` / `COUNT(DISTINCT "region")`. */
|
|
530
|
+
function aggFnSql(dialect, select, expr) {
|
|
531
|
+
if (expr.$field === "*") return renderAggCall(expr.$fn, "*");
|
|
532
|
+
const field = dialect.quoteIdentifier(expr.$field);
|
|
533
|
+
return expr.$fn === "min" || expr.$fn === "max" ? pickSql(dialect, select, expr.$fn, expr.$field, field) : renderAggCall(expr.$fn, field);
|
|
344
534
|
}
|
|
345
535
|
const BUCKET_UNIT_SET = new Set(BUCKET_UNITS);
|
|
346
536
|
const WEEK_START_SET = new Set(WEEK_STARTS);
|
|
@@ -388,25 +578,52 @@ function groupKeySql(dialect, controls, key) {
|
|
|
388
578
|
const bucket = controls.$select?.bucketByAlias(key);
|
|
389
579
|
return bucket ? bucketSql(dialect, bucket) : dialect.quoteIdentifier(key);
|
|
390
580
|
}
|
|
581
|
+
/** Alias of the derived table `first` / `last` aggregates read from. */
|
|
582
|
+
const ROWS_ALIAS = "__as_rows";
|
|
583
|
+
/** The derived-table column of the `i`-th `first` / `last` entry. */
|
|
584
|
+
const firstLastColumn = (i) => `__as_fl${i}`;
|
|
585
|
+
/**
|
|
586
|
+
* The SQL each computed alias stands for where an alias is not usable
|
|
587
|
+
* (HAVING; other expressions): an aggregate's call, a `first` / `last`
|
|
588
|
+
* derived column (aggregated: constant within its group), a row-level
|
|
589
|
+
* expression aggregate's call, and every group-level expression rendered over
|
|
590
|
+
* those (in dependency order, operands cast to double). A grouped column
|
|
591
|
+
* renders as its quoted name.
|
|
592
|
+
*/
|
|
593
|
+
function aliasSqlMap(dialect, controls) {
|
|
594
|
+
const map = /* @__PURE__ */ new Map();
|
|
595
|
+
const select = controls.$select;
|
|
596
|
+
const quote = (name) => dialect.quoteIdentifier(name);
|
|
597
|
+
for (const expr of select?.aggregates ?? []) map.set(resolveAlias(expr), aggFnSql(dialect, select, expr));
|
|
598
|
+
for (const e of select?.exprAggregates ?? []) map.set(e.alias, renderAggCall(e.fn, renderArith(dialect, e.expr, quote)));
|
|
599
|
+
(select?.firstLast ?? []).forEach((fl, i) => {
|
|
600
|
+
const col = quote(firstLastColumn(i));
|
|
601
|
+
map.set(fl.alias, dialect.anyValue ? dialect.anyValue(col, select?.sources.get(fl.column)) : renderAggCall("min", col));
|
|
602
|
+
});
|
|
603
|
+
for (const e of select?.exprs ?? []) map.set(e.alias, renderArith(dialect, e.expr, (name) => map.get(name) ?? quote(name)));
|
|
604
|
+
return map;
|
|
605
|
+
}
|
|
391
606
|
/**
|
|
392
607
|
* ` HAVING <predicate>` (leading space) + params for `controls.$having`, or
|
|
393
608
|
* `undefined` when there is nothing to render. Shared by the row and the
|
|
394
609
|
* count builders so both filter the same group set.
|
|
395
610
|
*
|
|
396
|
-
* A key that names
|
|
397
|
-
*
|
|
398
|
-
* not allow a SELECT alias in HAVING (MySQL and SQLite
|
|
399
|
-
* expression form keeps all three identical). A
|
|
400
|
-
* its bucket expression ({@link groupKeySql}),
|
|
401
|
-
* dialect sets `SqlDialect.bucketAliasInHaving`
|
|
402
|
-
* (grouped columns) render as plain columns.
|
|
611
|
+
* A key that names a computed alias (`$as`, else `fn_field`) renders what the
|
|
612
|
+
* alias stands for — `SUM("amount") > ?`, an expression's arithmetic — because
|
|
613
|
+
* PostgreSQL does not allow a SELECT alias in HAVING (MySQL and SQLite
|
|
614
|
+
* tolerate it, so the expression form keeps all three identical). A
|
|
615
|
+
* calendar-bucket alias renders its bucket expression ({@link groupKeySql}),
|
|
616
|
+
* or its quoted alias when the dialect sets `SqlDialect.bucketAliasInHaving`
|
|
617
|
+
* (`havingGroupRef`). Other keys (grouped columns) render as plain columns.
|
|
618
|
+
* `aliasSql` is the query's {@link aliasSqlMap} when the caller has built it.
|
|
403
619
|
*/
|
|
404
|
-
function havingClause(dialect, controls) {
|
|
620
|
+
function havingClause(dialect, controls, aliasSql) {
|
|
405
621
|
const having = controls.$having;
|
|
406
622
|
if (!having) return void 0;
|
|
407
|
-
const exprByAlias =
|
|
408
|
-
|
|
623
|
+
const exprByAlias = aliasSql ?? aliasSqlMap(dialect, controls);
|
|
624
|
+
const refs = /* @__PURE__ */ new Set();
|
|
409
625
|
const fragment = walkFilter(having, createFilterVisitor(dialect, { columnRef: (field) => {
|
|
626
|
+
refs.add(field);
|
|
410
627
|
const aggExpr = exprByAlias.get(field);
|
|
411
628
|
if (aggExpr) return aggExpr;
|
|
412
629
|
const bucket = controls.$select?.bucketByAlias(field);
|
|
@@ -415,43 +632,103 @@ function havingClause(dialect, controls) {
|
|
|
415
632
|
if (!fragment || fragment.sql === EMPTY_AND.sql) return void 0;
|
|
416
633
|
return {
|
|
417
634
|
sql: ` HAVING ${fragment.sql}`,
|
|
418
|
-
params: fragment.params
|
|
635
|
+
params: fragment.params,
|
|
636
|
+
refs
|
|
419
637
|
};
|
|
420
638
|
}
|
|
421
639
|
/** `<bucket expr> AS "alias"` for every calendar bucket in `$select`. */
|
|
422
640
|
function bucketSelectParts(dialect, controls) {
|
|
423
641
|
return (controls.$select?.buckets ?? []).map((bucket) => `${bucketSql(dialect, bucket)} AS ${dialect.quoteIdentifier(bucket.alias)}`);
|
|
424
642
|
}
|
|
643
|
+
/** ` GROUP BY <keys>` (leading space), or `""` for the whole table as one group. */
|
|
644
|
+
function groupByClause(dialect, controls) {
|
|
645
|
+
const keys = controls.$groupBy ?? [];
|
|
646
|
+
return keys.length ? ` GROUP BY ${keys.map((key) => groupKeySql(dialect, controls, key)).join(", ")}` : "";
|
|
647
|
+
}
|
|
648
|
+
/** Whether any of the HAVING keys is a `first` / `last` alias, or an expression over one. */
|
|
649
|
+
function readsFirstLast(select, refs) {
|
|
650
|
+
if (!select?.firstLast) return false;
|
|
651
|
+
const firstLast = new Set(select.firstLast.map((fl) => fl.alias));
|
|
652
|
+
const names = new Map((select.exprs ?? []).map((e) => [e.alias, e.names]));
|
|
653
|
+
const visit = (name, seen) => {
|
|
654
|
+
if (firstLast.has(name)) return true;
|
|
655
|
+
const deps = names.get(name);
|
|
656
|
+
if (!deps || seen.has(name)) return false;
|
|
657
|
+
seen.add(name);
|
|
658
|
+
return deps.some((dep) => visit(dep, seen));
|
|
659
|
+
};
|
|
660
|
+
return [...refs].some((key) => visit(key, /* @__PURE__ */ new Set()));
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* The columns the outer query reads from the derived table of a `first` /
|
|
664
|
+
* `last` aggregate: group keys (a bucket's source for its alias), plain
|
|
665
|
+
* `$select` fields, aggregate fields and the leaves of row-level expressions.
|
|
666
|
+
* The `$rowOrder` columns and the `first` / `last` sources stay inside the
|
|
667
|
+
* window; a group-level expression, `$having` and `$sort` read aliases or
|
|
668
|
+
* group keys only.
|
|
669
|
+
*/
|
|
670
|
+
function rowColumns(controls) {
|
|
671
|
+
const select = controls.$select;
|
|
672
|
+
const columns = /* @__PURE__ */ new Set();
|
|
673
|
+
for (const key of controls.$groupBy ?? []) if (!select?.bucketByAlias(key)) columns.add(key);
|
|
674
|
+
for (const bucket of select?.buckets ?? []) columns.add(bucket.field);
|
|
675
|
+
for (const field of select?.asArray ?? []) columns.add(field);
|
|
676
|
+
for (const expr of select?.aggregates ?? []) if (expr.$field !== "*") columns.add(expr.$field);
|
|
677
|
+
for (const e of select?.exprAggregates ?? []) for (const name of e.names) columns.add(name);
|
|
678
|
+
return [...columns];
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* The row source of an aggregate: `FROM <table> WHERE <where>`; with `first` /
|
|
682
|
+
* `last` (and `withRows`) a derived table instead — the table's rows that
|
|
683
|
+
* pass the WHERE, only the columns {@link rowColumns} lists, plus one
|
|
684
|
+
* `FIRST_VALUE(col) OVER (PARTITION BY <group keys> ORDER BY <rowOrder>)`
|
|
685
|
+
* column per entry (`last` over the reversed order). Each group then reads its
|
|
686
|
+
* representative row's value as an aggregate (constant within the group). The
|
|
687
|
+
* WHERE moves inside unchanged, so the bind parameters keep their order.
|
|
688
|
+
*/
|
|
689
|
+
function aggSource(dialect, table, where, controls, withRows) {
|
|
690
|
+
const quotedTable = dialect.quoteTable(table);
|
|
691
|
+
const firstLast = controls.$select?.firstLast;
|
|
692
|
+
const rowOrder = controls.$select?.rowOrder;
|
|
693
|
+
if (!withRows || !firstLast?.length || !rowOrder?.length) return `FROM ${quotedTable} WHERE ${where.sql}`;
|
|
694
|
+
const groupBy = controls.$groupBy;
|
|
695
|
+
const partition = groupBy?.length ? `PARTITION BY ${groupBy.map((key) => groupKeySql(dialect, controls, key)).join(", ")} ` : "";
|
|
696
|
+
const order = (reverse) => rowOrder.map((k) => orderKeySql(dialect, dialect.quoteIdentifier(k.column), k.desc !== reverse)).join(", ");
|
|
697
|
+
return `FROM (SELECT ${[...rowColumns(controls).map((column) => dialect.quoteIdentifier(column)), ...firstLast.map((fl, i) => {
|
|
698
|
+
const col = dialect.quoteIdentifier(firstLastColumn(i));
|
|
699
|
+
return `FIRST_VALUE(${dialect.quoteIdentifier(fl.column)}) OVER (${partition}ORDER BY ${order(fl.fn === "last")}) AS ${col}`;
|
|
700
|
+
})].join(", ")} FROM ${quotedTable} WHERE ${where.sql}) AS ${dialect.quoteIdentifier(ROWS_ALIAS)}`;
|
|
701
|
+
}
|
|
425
702
|
/**
|
|
426
703
|
* Builds a SELECT ... GROUP BY statement with aggregate functions.
|
|
427
704
|
*
|
|
428
705
|
* SELECT lists the plain grouped columns, then `<bucket expr> AS "alias"`
|
|
429
|
-
* per calendar bucket, then
|
|
706
|
+
* per calendar bucket, then every computed alias (aggregates, row-level
|
|
707
|
+
* expression aggregates, `first` / `last`, group-level expressions — the
|
|
708
|
+
* order of `UniquSelect.computedAliases`). Bucket expressions are
|
|
430
709
|
* parameter-free, so the bind parameters are exactly those of the same query
|
|
431
710
|
* without buckets (WHERE, HAVING, LIMIT, OFFSET).
|
|
432
711
|
*/
|
|
433
712
|
function buildAggregateSelect(dialect, table, where, controls) {
|
|
713
|
+
const select = controls.$select;
|
|
434
714
|
const selectParts = [];
|
|
435
|
-
const
|
|
436
|
-
if (plainFields) for (const f of plainFields) selectParts.push(dialect.quoteIdentifier(f));
|
|
715
|
+
for (const f of select?.asArray ?? []) selectParts.push(dialect.quoteIdentifier(f));
|
|
437
716
|
selectParts.push(...bucketSelectParts(dialect, controls));
|
|
438
|
-
|
|
439
|
-
if (
|
|
440
|
-
|
|
717
|
+
let aliasSql;
|
|
718
|
+
if (select?.exprAggregates || select?.firstLast || select?.exprs) {
|
|
719
|
+
aliasSql = aliasSqlMap(dialect, controls);
|
|
720
|
+
for (const alias of select.computedAliases) selectParts.push(`${aliasSql.get(alias)} AS ${dialect.quoteIdentifier(alias)}`);
|
|
721
|
+
} else for (const expr of select?.aggregates ?? []) selectParts.push(`${aggFnSql(dialect, select, expr)} AS ${dialect.quoteIdentifier(resolveAlias(expr))}`);
|
|
722
|
+
let sql = `SELECT ${selectParts.length > 0 ? selectParts.join(", ") : "*"} ${aggSource(dialect, table, where, controls, true)}${groupByClause(dialect, controls)}`;
|
|
441
723
|
const params = [...where.params];
|
|
442
|
-
const
|
|
443
|
-
if (groupBy?.length) {
|
|
444
|
-
const groupCols = groupBy.map((key) => groupKeySql(dialect, controls, key)).join(", ");
|
|
445
|
-
sql += ` GROUP BY ${groupCols}`;
|
|
446
|
-
}
|
|
447
|
-
const having = havingClause(dialect, controls);
|
|
724
|
+
const having = havingClause(dialect, controls, aliasSql);
|
|
448
725
|
if (having) {
|
|
449
726
|
sql += having.sql;
|
|
450
727
|
params.push(...having.params);
|
|
451
728
|
}
|
|
452
729
|
if (controls.$sort) {
|
|
453
730
|
const orderParts = [];
|
|
454
|
-
for (const [col, dir] of Object.entries(controls.$sort)) orderParts.push(
|
|
731
|
+
for (const [col, dir] of Object.entries(controls.$sort)) orderParts.push(orderKeySql(dialect, dialect.quoteIdentifier(col), dir === -1));
|
|
455
732
|
if (orderParts.length > 0) sql += ` ORDER BY ${orderParts.join(", ")}`;
|
|
456
733
|
}
|
|
457
734
|
if (controls.$limit !== void 0) {
|
|
@@ -472,21 +749,30 @@ function buildAggregateSelect(dialect, table, where, controls) {
|
|
|
472
749
|
* Builds a COUNT query for the number of distinct groups — the groups that
|
|
473
750
|
* survive `$having` when one is given (the same predicate the row query
|
|
474
751
|
* renders, so `$count` agrees with the row set). Returns `{ count: N }` when
|
|
475
|
-
* executed.
|
|
752
|
+
* executed. The rows come straight from the table, unless `$having` reads a
|
|
753
|
+
* `first` / `last` value (or an expression over one): only then the window
|
|
754
|
+
* derived table is built.
|
|
476
755
|
*/
|
|
477
756
|
function buildAggregateCount(dialect, table, where, controls) {
|
|
478
757
|
const groupFields = controls.$groupBy;
|
|
479
758
|
const having = havingClause(dialect, controls);
|
|
480
759
|
const countCol = `COUNT(*) AS ${dialect.quoteIdentifier("count")}`;
|
|
481
|
-
if (!groupFields?.length && !having)
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
760
|
+
if (!groupFields?.length && !having) {
|
|
761
|
+
if ((controls.$select?.computedAliases.length ?? 0) > 0) return finalizeParams(dialect, {
|
|
762
|
+
sql: `SELECT 1 AS ${dialect.quoteIdentifier("count")}`,
|
|
763
|
+
params: []
|
|
764
|
+
});
|
|
765
|
+
return finalizeParams(dialect, {
|
|
766
|
+
sql: `SELECT ${countCol} FROM ${dialect.quoteTable(table)} WHERE ${where.sql}`,
|
|
767
|
+
params: where.params
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
const from = aggSource(dialect, table, where, controls, !!having && readsFirstLast(controls.$select, having.refs));
|
|
771
|
+
const groupBy = groupByClause(dialect, controls);
|
|
486
772
|
let inner = "COUNT(*)";
|
|
487
773
|
if (groupBy) inner = bucketSelectParts(dialect, controls).join(", ") || "1";
|
|
488
774
|
return finalizeParams(dialect, {
|
|
489
|
-
sql: `SELECT ${countCol} FROM (SELECT ${inner}
|
|
775
|
+
sql: `SELECT ${countCol} FROM (SELECT ${inner} ${from}${groupBy}${having?.sql ?? ""}) AS ${dialect.quoteIdentifier("_groups")}`,
|
|
490
776
|
params: [...where.params, ...having?.params ?? []]
|
|
491
777
|
});
|
|
492
778
|
}
|
|
@@ -531,6 +817,63 @@ function viewAggExpr(dialect, c, resolveFieldRef) {
|
|
|
531
817
|
return c.aggFn === "sum" ? `COALESCE(${call}, 0)` : call;
|
|
532
818
|
}
|
|
533
819
|
/**
|
|
820
|
+
* The SQL expression of a computed view column (`@db.compute`), evaluated in
|
|
821
|
+
* IEEE double on every dialect: each field and literal leaf is cast with
|
|
822
|
+
* {@link SqlDialect.castDouble} (a computed leaf is already a double and is
|
|
823
|
+
* inlined); `+ - *` render as `(l op r)`, `/` as `(l / NULLIF(r, 0))` —
|
|
824
|
+
* division by zero is NULL — unary minus as `(-x)`, `coalesce` as
|
|
825
|
+
* `COALESCE(…)`. A leaf reads its column like the SELECT list does: an
|
|
826
|
+
* aggregate's call ({@link viewAggExpr}) or a dimension's source
|
|
827
|
+
* ({@link viewSourceExpr}). `byPath` holds the view's mappings by `viewPath`.
|
|
828
|
+
*
|
|
829
|
+
* In a `grouped` view a JSON-extracted dimension leaf reads `MIN(<extract>)`:
|
|
830
|
+
* the extraction is a GROUP BY key, so every row of a group holds the same
|
|
831
|
+
* value, but MySQL's ONLY_FULL_GROUP_BY rejects an expression over the raw
|
|
832
|
+
* JSON column (which is not itself grouped) — the aggregate form is accepted
|
|
833
|
+
* everywhere and yields that same value.
|
|
834
|
+
*
|
|
835
|
+
* @throws when the dialect has no `castDouble`, or a leaf names no column.
|
|
836
|
+
* @since 0.1.147
|
|
837
|
+
*/
|
|
838
|
+
function viewComputeExpr(dialect, c, byPath, resolveFieldRef, grouped = false, cache = /* @__PURE__ */ new Map()) {
|
|
839
|
+
const cached = cache.get(c.viewPath);
|
|
840
|
+
if (cached !== void 0) return cached;
|
|
841
|
+
const fail = (reason) => /* @__PURE__ */ new Error(reason === "no-cast" ? `View column "${c.viewColumn}": computed view columns are not supported by this adapter` : `View column "${c.viewColumn}": non-finite literal in @db.compute`);
|
|
842
|
+
const leaf = (field) => {
|
|
843
|
+
const mapping = byPath.get(field);
|
|
844
|
+
if (!mapping) throw new Error(`View column "${c.viewColumn}": "${field}" is not a column of the view`);
|
|
845
|
+
if (mapping.expr !== void 0) return { double: viewComputeExpr(dialect, mapping, byPath, resolveFieldRef, grouped, cache) };
|
|
846
|
+
if (mapping.aggFn) return viewAggExpr(dialect, mapping, resolveFieldRef);
|
|
847
|
+
const source = viewSourceExpr(dialect, mapping);
|
|
848
|
+
return grouped && mapping.json ? `MIN(${source})` : source;
|
|
849
|
+
};
|
|
850
|
+
const sql = renderArith(dialect, c.expr, leaf, fail);
|
|
851
|
+
cache.set(c.viewPath, sql);
|
|
852
|
+
return sql;
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* The `ON` clause of a first-row join (`join.first`): the joined row is the
|
|
856
|
+
* one whose primary key equals the key of the FIRST matching target row —
|
|
857
|
+
* a correlated scalar subquery
|
|
858
|
+
* `"T"."id" = (SELECT "T"."id" FROM "table" AS "T" WHERE <condition> ORDER BY … LIMIT 1)`.
|
|
859
|
+
* The inner `FROM` repeats the join's own target / alias text, so the
|
|
860
|
+
* condition's refs to the joined scope bind to the inner row (SQL name
|
|
861
|
+
* shadowing) while refs to the entry and earlier joins stay correlated to
|
|
862
|
+
* the outer query. The ordering ends with the primary key, so the pick is
|
|
863
|
+
* deterministic; NULL sorts first in `asc` (`nullsSortLargest` dialects
|
|
864
|
+
* render `NULLS FIRST` / `NULLS LAST`). No window functions — SQLite, MySQL 8
|
|
865
|
+
* and PostgreSQL render it alike.
|
|
866
|
+
* @since 0.1.147
|
|
867
|
+
*/
|
|
868
|
+
function firstRowOn(dialect, join, target, condition, resolveFieldRef) {
|
|
869
|
+
const first = join.first;
|
|
870
|
+
const key = resolveFieldRef({
|
|
871
|
+
type: join.targetType,
|
|
872
|
+
field: first.key
|
|
873
|
+
});
|
|
874
|
+
return `${key} = (SELECT ${key} FROM ${target} WHERE ${condition} ORDER BY ${first.order.map(({ ref, desc }) => orderKeySql(dialect, resolveFieldRef(ref), desc)).join(", ")} LIMIT 1)`;
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
534
877
|
* Builds a CREATE VIEW statement from a view plan and column mappings.
|
|
535
878
|
*
|
|
536
879
|
* Joins render in declaration order — `JOIN` (inner, the default) or
|
|
@@ -542,35 +885,41 @@ function viewAggExpr(dialect, c, resolveFieldRef) {
|
|
|
542
885
|
* join target may be views.
|
|
543
886
|
*/
|
|
544
887
|
function buildCreateView(dialect, viewName, plan, columns, resolveFieldRef) {
|
|
545
|
-
const
|
|
546
|
-
|
|
547
|
-
|
|
888
|
+
const columnByPath = new Map(columns.map((c) => [c.viewPath, c]));
|
|
889
|
+
const hasAggregates = columns.some((c) => c.aggFn);
|
|
890
|
+
const computedSql = /* @__PURE__ */ new Map();
|
|
891
|
+
/** A column's SELECT expression: computed, aggregate or plain source. */
|
|
892
|
+
const columnExpr = (c) => c.expr !== void 0 ? viewComputeExpr(dialect, c, columnByPath, resolveFieldRef, hasAggregates, computedSql) : c.aggFn ? viewAggExpr(dialect, c, resolveFieldRef) : viewSourceExpr(dialect, c);
|
|
893
|
+
const selectCols = columns.map((c) => `${columnExpr(c)} AS ${dialect.quoteIdentifier(c.viewColumn)}`).join(", ");
|
|
548
894
|
let sql = `${dialect.createViewPrefix} ${dialect.quoteTable(viewName)} AS SELECT ${selectCols} FROM ${dialect.quoteIdentifier(plan.entryTable)}`;
|
|
549
895
|
for (const join of plan.joins) {
|
|
550
|
-
const
|
|
896
|
+
const condition = queryNodeToSql(join.condition, resolveFieldRef);
|
|
551
897
|
const keyword = join.kind === "left" ? "LEFT JOIN" : "JOIN";
|
|
552
898
|
const target = join.scope === join.targetTable ? dialect.quoteIdentifier(join.targetTable) : `${dialect.quoteIdentifier(join.targetTable)} AS ${dialect.quoteIdentifier(join.scope)}`;
|
|
899
|
+
const onClause = join.first ? firstRowOn(dialect, join, target, condition, resolveFieldRef) : condition;
|
|
553
900
|
sql += ` ${keyword} ${target} ON ${onClause}`;
|
|
554
901
|
}
|
|
555
902
|
if (plan.filter) {
|
|
556
903
|
const whereClause = queryNodeToSql(plan.filter, resolveFieldRef);
|
|
557
904
|
sql += ` WHERE ${whereClause}`;
|
|
558
905
|
}
|
|
559
|
-
if (
|
|
560
|
-
const dimensionCols = columns.filter((c) => !c.aggFn);
|
|
906
|
+
if (hasAggregates) {
|
|
907
|
+
const dimensionCols = columns.filter((c) => !c.aggFn && c.expr === void 0);
|
|
561
908
|
if (dimensionCols.length > 0) {
|
|
562
909
|
const groupByCols = dimensionCols.map((c) => viewSourceExpr(dialect, c)).join(", ");
|
|
563
910
|
sql += ` GROUP BY ${groupByCols}`;
|
|
564
911
|
}
|
|
565
912
|
if (plan.having) {
|
|
566
|
-
|
|
567
|
-
|
|
913
|
+
let grouped;
|
|
914
|
+
const groupedColumns = () => grouped ??= new Set(dimensionCols.filter((c) => !c.json).map((c) => c.sourceColumn.toLowerCase()));
|
|
568
915
|
const havingResolver = (ref) => {
|
|
569
916
|
const col = ref.type ? void 0 : columnByPath.get(ref.field);
|
|
570
917
|
if (!col) return resolveFieldRef(ref);
|
|
918
|
+
if (col.expr !== void 0) return columnExpr(col);
|
|
571
919
|
if (col.aggFn) return viewAggExpr(dialect, col, resolveFieldRef);
|
|
572
920
|
const expr = viewSourceExpr(dialect, col);
|
|
573
|
-
|
|
921
|
+
if (!col.json) return expr;
|
|
922
|
+
return dialect.bucketAliasInHaving && groupedColumns().has(col.viewColumn.toLowerCase()) ? `MIN(${expr})` : havingGroupRef(dialect, expr, col.viewColumn);
|
|
574
923
|
};
|
|
575
924
|
const havingClause = queryNodeToSql(plan.having, havingResolver);
|
|
576
925
|
sql += ` HAVING ${havingClause}`;
|
|
@@ -603,6 +952,21 @@ function insertManyColumns(rows) {
|
|
|
603
952
|
return [...columns];
|
|
604
953
|
}
|
|
605
954
|
/**
|
|
955
|
+
* Splits `rows` into batches that stay under the driver's bind-parameter limit
|
|
956
|
+
* (PostgreSQL ~65535, MySQL packet size): `maxParams` (default 60000) divided
|
|
957
|
+
* by the column count. Returns the shared column union and the batches.
|
|
958
|
+
*/
|
|
959
|
+
function chunkInsertRows(rows, maxParams = 6e4) {
|
|
960
|
+
const columns = insertManyColumns(rows);
|
|
961
|
+
const size = columns.length > 0 ? Math.max(1, Math.floor(maxParams / columns.length)) : rows.length;
|
|
962
|
+
const batches = [];
|
|
963
|
+
for (let offset = 0; offset < rows.length; offset += size) batches.push(rows.slice(offset, offset + size));
|
|
964
|
+
return {
|
|
965
|
+
columns,
|
|
966
|
+
batches
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
606
970
|
* Builds a multi-row `INSERT … VALUES (…), (…)` statement over `columns`
|
|
607
971
|
* (default: {@link insertManyColumns} of `rows`). A row lacking a column gets
|
|
608
972
|
* `DEFAULT` — exactly what a single-row INSERT omitting it stores. Callers
|
|
@@ -631,11 +995,8 @@ function buildInsertMany(dialect, table, rows, columns = insertManyColumns(rows)
|
|
|
631
995
|
function buildSelect(dialect, table, where, controls) {
|
|
632
996
|
let sql = `SELECT ${buildProjection(dialect, controls?.$select)} FROM ${dialect.quoteTable(table)} WHERE ${where.sql}`;
|
|
633
997
|
const params = [...where.params];
|
|
634
|
-
|
|
635
|
-
|
|
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
|
-
}
|
|
998
|
+
const orderBy = orderByList(dialect, controls?.$sort);
|
|
999
|
+
if (orderBy) sql += ` ORDER BY ${orderBy}`;
|
|
639
1000
|
if (controls?.$limit !== void 0) {
|
|
640
1001
|
sql += ` LIMIT ?`;
|
|
641
1002
|
params.push(controls.$limit);
|
|
@@ -650,6 +1011,53 @@ function buildSelect(dialect, table, where, controls) {
|
|
|
650
1011
|
params
|
|
651
1012
|
});
|
|
652
1013
|
}
|
|
1014
|
+
/** `"col" ASC, "other" DESC` of a physical `$sort` — `""` when it orders nothing. */
|
|
1015
|
+
function orderByList(dialect, sort) {
|
|
1016
|
+
if (!sort) return "";
|
|
1017
|
+
const parts = [];
|
|
1018
|
+
for (const [col, dir] of Object.entries(sort)) parts.push(`${dialect.quoteIdentifier(col)} ${dir === -1 ? "DESC" : "ASC"}`);
|
|
1019
|
+
return parts.join(", ");
|
|
1020
|
+
}
|
|
1021
|
+
/**
|
|
1022
|
+
* The row-number column {@link buildPartitionedSelect} adds to every row;
|
|
1023
|
+
* {@link stripPartitionRowNumber} removes it from the result rows.
|
|
1024
|
+
* @since 0.1.147
|
|
1025
|
+
*/
|
|
1026
|
+
const PARTITION_ROW_NUMBER_ALIAS = "__atscript_rn";
|
|
1027
|
+
/**
|
|
1028
|
+
* Builds a SELECT whose `$skip` / `$limit` apply to each partition — the rows
|
|
1029
|
+
* sharing the values of the `partitionBy` columns (physical names) — instead
|
|
1030
|
+
* of to the whole result: a `ROW_NUMBER() OVER (PARTITION BY … ORDER BY
|
|
1031
|
+
* <$sort>)` window in a derived table, filtered on the row number. The rows
|
|
1032
|
+
* of each partition come out in `$sort` order (partitions interleave); every
|
|
1033
|
+
* row carries {@link PARTITION_ROW_NUMBER_ALIAS}. Window functions need
|
|
1034
|
+
* SQLite ≥ 3.25, MySQL ≥ 8.0 or MariaDB ≥ 10.2.
|
|
1035
|
+
* @since 0.1.147
|
|
1036
|
+
*/
|
|
1037
|
+
function buildPartitionedSelect(dialect, table, where, controls, partitionBy) {
|
|
1038
|
+
const rn = dialect.quoteIdentifier(PARTITION_ROW_NUMBER_ALIAS);
|
|
1039
|
+
const orderBy = orderByList(dialect, controls.$sort);
|
|
1040
|
+
const window = `PARTITION BY ${partitionBy.map((col) => dialect.quoteIdentifier(col)).join(", ")}` + (orderBy ? ` ORDER BY ${orderBy}` : "");
|
|
1041
|
+
const inner = `SELECT ${buildProjection(dialect, controls.$select)}, ROW_NUMBER() OVER (${window}) AS ${rn} FROM ${dialect.quoteTable(table)} WHERE ${where.sql}`;
|
|
1042
|
+
const skip = controls.$skip ?? 0;
|
|
1043
|
+
const limit = controls.$limit;
|
|
1044
|
+
const params = [...where.params, skip];
|
|
1045
|
+
let sql = `SELECT * FROM (${inner}) AS ${dialect.quoteIdentifier("__atscript_p")} WHERE ${rn} > ?`;
|
|
1046
|
+
if (limit !== void 0 && limit !== null) {
|
|
1047
|
+
sql += ` AND ${rn} <= ?`;
|
|
1048
|
+
params.push(skip + limit);
|
|
1049
|
+
}
|
|
1050
|
+
sql += ` ORDER BY ${rn}`;
|
|
1051
|
+
return finalizeParams(dialect, {
|
|
1052
|
+
sql,
|
|
1053
|
+
params
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
/** Removes {@link PARTITION_ROW_NUMBER_ALIAS} from rows read by {@link buildPartitionedSelect}. */
|
|
1057
|
+
function stripPartitionRowNumber(rows) {
|
|
1058
|
+
for (const row of rows) delete row[PARTITION_ROW_NUMBER_ALIAS];
|
|
1059
|
+
return rows;
|
|
1060
|
+
}
|
|
653
1061
|
/**
|
|
654
1062
|
* Marker value for {@link buildUpdate}: the column is assigned its DDL
|
|
655
1063
|
* `DEFAULT` (`SET "col" = DEFAULT`, no bound parameter). Produced by
|
|
@@ -809,6 +1217,13 @@ function buildProjection(dialect, select, qualifier) {
|
|
|
809
1217
|
*/
|
|
810
1218
|
const GEO_DISTANCE_ALIAS = "__atscript_distance";
|
|
811
1219
|
/**
|
|
1220
|
+
* Alias of the searched table inside the geo and vector search statements —
|
|
1221
|
+
* a filter rendered into them correlates its relational predicates to it
|
|
1222
|
+
* (`buildWhere(filter, { qualifier: dialect.quoteTable(SEARCH_SOURCE_ALIAS) })`).
|
|
1223
|
+
* @since 0.1.147
|
|
1224
|
+
*/
|
|
1225
|
+
const SEARCH_SOURCE_ALIAS = "t";
|
|
1226
|
+
/**
|
|
812
1227
|
* Builds a distance-ranked geo search SELECT:
|
|
813
1228
|
*
|
|
814
1229
|
* ```sql
|
|
@@ -1027,4 +1442,4 @@ function parseRegexString(value) {
|
|
|
1027
1442
|
};
|
|
1028
1443
|
}
|
|
1029
1444
|
//#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 };
|
|
1445
|
+
export { AGG_FN_SQL, EMPTY_AND, EMPTY_OR, GEO_DISTANCE_ALIAS, PARTITION_ROW_NUMBER_ALIAS, SEARCH_SOURCE_ALIAS, SQL_DEFAULT, VECTOR_DISTANCE_ALIAS, arithOverflowError, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildPartitionedSelect, buildProjection, buildSelect, buildUpdate, buildVectorSearchCount, buildVectorSearchSelect, buildWhere, chunkInsertRows, createFilterVisitor, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, finalizeParams, foreignKeySql, geoWindowFromControls, groupKeySql, insertManyColumns, jsonDollarPath, mapQueryErrors, normalizeGeoPointValue, numericOutOfRangeError, orderKeySql, parseRegexString, queryNodeToSql, queryOpToSql, quotedJsonPathSegments, refActionToSql, renameGeoDistance, renderArith, replaceColumnsFor, sqlStringLiteral, sqlTimeZoneLiteral, stripPartitionRowNumber, toSqlValue, vectorDistanceSource };
|