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