@atscript/db-sql-tools 0.1.134 → 0.1.136
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 +192 -55
- package/dist/index.d.cts +72 -10
- package/dist/index.d.mts +72 -10
- package/dist/index.mjs +193 -58
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -15,6 +15,37 @@ function finalizeParams(dialect, fragment) {
|
|
|
15
15
|
params: fragment.params
|
|
16
16
|
};
|
|
17
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* How HAVING references a grouped NON-column expression (a calendar bucket, a
|
|
20
|
+
* view's JSON-extracted dimension): its quoted SELECT `alias` on dialects that
|
|
21
|
+
* set {@link SqlDialect.bucketAliasInHaving}, else the expression itself.
|
|
22
|
+
*/
|
|
23
|
+
function havingGroupRef(dialect, expr, alias) {
|
|
24
|
+
return dialect.bucketAliasInHaving ? dialect.quoteIdentifier(alias) : expr;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Each JSON path segment wrapped in double quotes (`"a"`), for the dialects'
|
|
28
|
+
* {@link SqlDialect.jsonExtract} path literals (`'$."a"."b"'`, `'{"a","b"}'`).
|
|
29
|
+
*
|
|
30
|
+
* A segment containing `"`, `\` or a control character has no quoting that
|
|
31
|
+
* reads the same on every dialect (SQLite's quoted path labels have no escape
|
|
32
|
+
* syntax), so it is rejected rather than escaped.
|
|
33
|
+
*
|
|
34
|
+
* @since 0.1.136
|
|
35
|
+
* @throws for an empty path or a segment that is empty or holds such a character.
|
|
36
|
+
*/
|
|
37
|
+
function quotedJsonPathSegments(path) {
|
|
38
|
+
if (path.length === 0) throw new Error("JSON extraction needs a path below the JSON column");
|
|
39
|
+
return path.map((seg) => {
|
|
40
|
+
let ok = seg.length > 0;
|
|
41
|
+
for (let i = 0; ok && i < seg.length; i++) {
|
|
42
|
+
const code = seg.charCodeAt(i);
|
|
43
|
+
ok = code >= 32 && code !== 34 && code !== 92;
|
|
44
|
+
}
|
|
45
|
+
if (!ok) throw new Error(`JSON path segment ${JSON.stringify(seg)} can't be extracted — segments must be non-empty and free of '"', '\\' and control characters`);
|
|
46
|
+
return `"${seg}"`;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
18
49
|
const EMPTY_AND = {
|
|
19
50
|
sql: "1=1",
|
|
20
51
|
params: []
|
|
@@ -255,6 +286,17 @@ function sqlStringLiteral(value) {
|
|
|
255
286
|
return `'${value.replace(/'/g, "''")}'`;
|
|
256
287
|
}
|
|
257
288
|
/**
|
|
289
|
+
* The SQL string literal of a `$`-rooted JSON path with every segment quoted
|
|
290
|
+
* (`'$."a"."b"'`) — the path argument of SQLite's and MySQL's `json_extract` /
|
|
291
|
+
* `json_type`.
|
|
292
|
+
*
|
|
293
|
+
* @since 0.1.136
|
|
294
|
+
* @throws as {@link quotedJsonPathSegments}.
|
|
295
|
+
*/
|
|
296
|
+
function jsonDollarPath(path) {
|
|
297
|
+
return sqlStringLiteral(`$.${quotedJsonPathSegments(path).join(".")}`);
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
258
300
|
* A calendar bucket's time zone as an inlined SQL string literal (`'Europe/Berlin'`).
|
|
259
301
|
*
|
|
260
302
|
* The zone is already canonical (the core normalizer ran uniqu's
|
|
@@ -324,23 +366,65 @@ const queryOpToSql = {
|
|
|
324
366
|
$lt: "<",
|
|
325
367
|
$lte: "<="
|
|
326
368
|
};
|
|
369
|
+
/** An inlined SQL literal for a view-predicate value (DDL — no parameters). */
|
|
370
|
+
function predicateLiteral(value) {
|
|
371
|
+
if (value === null || value === void 0) return "NULL";
|
|
372
|
+
if (typeof value === "string") return sqlStringLiteral(value);
|
|
373
|
+
if (typeof value === "number") {
|
|
374
|
+
if (!Number.isFinite(value)) throw new Error(`Non-finite number ${value} in a view predicate`);
|
|
375
|
+
return String(value);
|
|
376
|
+
}
|
|
377
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
378
|
+
throw new Error(`Unsupported literal ${JSON.stringify(value)} in a view predicate`);
|
|
379
|
+
}
|
|
327
380
|
/**
|
|
328
381
|
* Renders an AtscriptQueryNode tree to raw SQL (no parameters -- for DDL use only).
|
|
382
|
+
*
|
|
383
|
+
* Operators: `=`/`!=`/`<`/`<=`/`>`/`>=` (a `null` operand becomes `IS [NOT] NULL`),
|
|
384
|
+
* `in` / `not in` (inlined literals; an empty `in` is `0=1`, an empty
|
|
385
|
+
* `not in` `1=1`), `exists` → `IS NOT NULL`, `not exists` → `IS NULL`.
|
|
386
|
+
*
|
|
387
|
+
* @throws for `matches` (`$regex`) and any other operator — a view predicate
|
|
388
|
+
* that cannot be rendered fails at sync instead of rendering wrong SQL.
|
|
329
389
|
*/
|
|
330
390
|
function queryNodeToSql(node, resolveFieldRef) {
|
|
331
|
-
if ("$and" in node)
|
|
332
|
-
|
|
391
|
+
if ("$and" in node) {
|
|
392
|
+
const children = node.$and;
|
|
393
|
+
if (children.length === 0) return EMPTY_AND.sql;
|
|
394
|
+
return children.map((n) => queryNodeToSql(n, resolveFieldRef)).join(" AND ");
|
|
395
|
+
}
|
|
396
|
+
if ("$or" in node) {
|
|
397
|
+
const children = node.$or;
|
|
398
|
+
if (children.length === 0) return EMPTY_OR.sql;
|
|
399
|
+
return `(${children.map((n) => queryNodeToSql(n, resolveFieldRef)).join(" OR ")})`;
|
|
400
|
+
}
|
|
333
401
|
if ("$not" in node) return `NOT (${queryNodeToSql(node.$not, resolveFieldRef)})`;
|
|
334
402
|
const comp = node;
|
|
335
403
|
const leftSql = resolveFieldRef(comp.left);
|
|
336
|
-
|
|
337
|
-
|
|
404
|
+
switch (comp.op) {
|
|
405
|
+
case "$exists": return comp.right === false ? `${leftSql} IS NULL` : `${leftSql} IS NOT NULL`;
|
|
406
|
+
case "$in":
|
|
407
|
+
case "$nin": {
|
|
408
|
+
const values = Array.isArray(comp.right) ? comp.right : [comp.right];
|
|
409
|
+
if (values.length === 0) return comp.op === "$in" ? EMPTY_OR.sql : EMPTY_AND.sql;
|
|
410
|
+
const list = values.map((v) => predicateLiteral(v)).join(", ");
|
|
411
|
+
return `${leftSql} ${comp.op === "$in" ? "IN" : "NOT IN"} (${list})`;
|
|
412
|
+
}
|
|
413
|
+
case "$regex": throw new Error("matches is not supported in view predicates");
|
|
414
|
+
default:
|
|
415
|
+
}
|
|
416
|
+
const sqlOp = queryOpToSql[comp.op];
|
|
417
|
+
if (!sqlOp) throw new Error(`Operator "${comp.op}" is not supported in view predicates`);
|
|
418
|
+
if ((0, _atscript_db.isFieldRef)(comp.right)) return `${leftSql} ${sqlOp} ${resolveFieldRef(comp.right)}`;
|
|
338
419
|
if (comp.right === null || comp.right === void 0) return comp.op === "$ne" ? `${leftSql} IS NOT NULL` : `${leftSql} IS NULL`;
|
|
339
|
-
|
|
340
|
-
return `${leftSql} ${sqlOp} ${comp.right}`;
|
|
420
|
+
return `${leftSql} ${sqlOp} ${predicateLiteral(comp.right)}`;
|
|
341
421
|
}
|
|
342
422
|
//#endregion
|
|
343
423
|
//#region src/agg.ts
|
|
424
|
+
/**
|
|
425
|
+
* SQL function name of each single-name aggregate. `countDistinct` is not a
|
|
426
|
+
* name but a form (`COUNT(DISTINCT x)`) — see {@link renderAggCall}.
|
|
427
|
+
*/
|
|
344
428
|
const AGG_FN_SQL = {
|
|
345
429
|
sum: "SUM",
|
|
346
430
|
avg: "AVG",
|
|
@@ -348,9 +432,20 @@ const AGG_FN_SQL = {
|
|
|
348
432
|
min: "MIN",
|
|
349
433
|
max: "MAX"
|
|
350
434
|
};
|
|
351
|
-
/**
|
|
435
|
+
/**
|
|
436
|
+
* Renders one aggregate call over an already-rendered argument (`*`, a quoted
|
|
437
|
+
* column, a `CASE` expression): `SUM(x)`, `COUNT(DISTINCT x)`, …
|
|
438
|
+
* Re-asserts the name first (`INVALID_QUERY` on an unknown one), so nothing
|
|
439
|
+
* unchecked reaches SQL.
|
|
440
|
+
*/
|
|
441
|
+
function renderAggCall(fn, arg, path) {
|
|
442
|
+
(0, _atscript_db_agg.assertAggregateFn)(fn, path);
|
|
443
|
+
return fn === "countDistinct" ? `COUNT(DISTINCT ${arg})` : `${AGG_FN_SQL[fn]}(${arg})`;
|
|
444
|
+
}
|
|
445
|
+
/** The bare aggregate call, e.g. `SUM("amount")` / `COUNT(*)` / `COUNT(DISTINCT "region")`. */
|
|
352
446
|
function aggFnSql(dialect, expr) {
|
|
353
|
-
|
|
447
|
+
const field = expr.$field === "*" ? "*" : dialect.quoteIdentifier(expr.$field);
|
|
448
|
+
return renderAggCall(expr.$fn, field);
|
|
354
449
|
}
|
|
355
450
|
function buildAggExpr(dialect, expr) {
|
|
356
451
|
return `${aggFnSql(dialect, expr)} AS ${dialect.quoteIdentifier((0, _atscript_db_agg.resolveAlias)(expr))}`;
|
|
@@ -411,7 +506,7 @@ function groupKeySql(dialect, controls, key) {
|
|
|
411
506
|
* not allow a SELECT alias in HAVING (MySQL and SQLite tolerate it, so the
|
|
412
507
|
* expression form keeps all three identical). A calendar-bucket alias renders
|
|
413
508
|
* its bucket expression ({@link groupKeySql}), or its quoted alias when the
|
|
414
|
-
* dialect sets `SqlDialect.bucketAliasInHaving` (
|
|
509
|
+
* dialect sets `SqlDialect.bucketAliasInHaving` (`havingGroupRef`). Other keys
|
|
415
510
|
* (grouped columns) render as plain columns.
|
|
416
511
|
*/
|
|
417
512
|
function havingClause(dialect, controls) {
|
|
@@ -422,8 +517,8 @@ function havingClause(dialect, controls) {
|
|
|
422
517
|
const fragment = (0, _uniqu_core.walkFilter)(having, createFilterVisitor(dialect, { columnRef: (field) => {
|
|
423
518
|
const aggExpr = exprByAlias.get(field);
|
|
424
519
|
if (aggExpr) return aggExpr;
|
|
425
|
-
|
|
426
|
-
return
|
|
520
|
+
const bucket = controls.$select?.bucketByAlias(field);
|
|
521
|
+
return bucket ? havingGroupRef(dialect, bucketSql(dialect, bucket), field) : dialect.quoteIdentifier(field);
|
|
427
522
|
} }));
|
|
428
523
|
if (!fragment || fragment.sql === EMPTY_AND.sql) return void 0;
|
|
429
524
|
return {
|
|
@@ -504,6 +599,90 @@ function buildAggregateCount(dialect, table, where, controls) {
|
|
|
504
599
|
});
|
|
505
600
|
}
|
|
506
601
|
//#endregion
|
|
602
|
+
//#region src/view-builder.ts
|
|
603
|
+
/**
|
|
604
|
+
* The SQL expression a view column reads: `"table"."column"` — the physical
|
|
605
|
+
* source column resolved by `AtscriptDbView.getViewColumnMappings()` — or,
|
|
606
|
+
* for a primitive leaf inside a JSON column (`mapping.json`), the dialect's
|
|
607
|
+
* typed {@link SqlDialect.jsonExtract} over that column. Used for SELECT
|
|
608
|
+
* columns, GROUP BY / HAVING dimensions and as an aggregate's source.
|
|
609
|
+
*
|
|
610
|
+
* @throws when the column reads a JSON leaf and the dialect has no `jsonExtract`.
|
|
611
|
+
*/
|
|
612
|
+
function viewSourceExpr(dialect, mapping) {
|
|
613
|
+
const col = `${dialect.quoteIdentifier(mapping.sourceTable)}.${dialect.quoteIdentifier(mapping.sourceColumn)}`;
|
|
614
|
+
if (!mapping.json) return col;
|
|
615
|
+
if (!dialect.jsonExtract) throw new Error(`View column "${mapping.viewColumn}": JSON extraction is not supported by this adapter`);
|
|
616
|
+
return dialect.jsonExtract(col, mapping.json.path, mapping.json.type);
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* The SQL expression of one aggregate view column, e.g. `SUM("orders"."amount")`,
|
|
620
|
+
* `COUNT(*)` or `COUNT(DISTINCT "orders"."customer_id")`, over
|
|
621
|
+
* {@link viewSourceExpr}. The mapping's aggregate rules (`*` only for count, …)
|
|
622
|
+
* are validated where the core builds it.
|
|
623
|
+
*
|
|
624
|
+
* A conditional aggregate (`aggFilter`, rendered with the view's predicate
|
|
625
|
+
* resolver) aggregates `CASE WHEN <predicate> THEN <src> END` — NULL for the
|
|
626
|
+
* rows the predicate rejects, which every aggregate skips:
|
|
627
|
+
* - `COUNT(*)` → `COUNT(CASE WHEN p THEN 1 END)`;
|
|
628
|
+
* - `countDistinct` → `COUNT(DISTINCT CASE WHEN p THEN src END)`;
|
|
629
|
+
* - `sum` → `COALESCE(SUM(CASE WHEN p THEN src END), 0)` — a group with no
|
|
630
|
+
* matching row sums to 0, not NULL (MongoDB's `$sum` answer too);
|
|
631
|
+
* - `avg` / `min` / `max` stay NULL when no row matches.
|
|
632
|
+
*/
|
|
633
|
+
function viewAggExpr(dialect, c, resolveFieldRef) {
|
|
634
|
+
const star = c.aggField === "*";
|
|
635
|
+
const src = star ? "*" : viewSourceExpr(dialect, c);
|
|
636
|
+
if (!c.aggFilter) return renderAggCall(c.aggFn, src, c.viewColumn);
|
|
637
|
+
const predicate = queryNodeToSql(c.aggFilter, resolveFieldRef);
|
|
638
|
+
const call = renderAggCall(c.aggFn, `CASE WHEN ${predicate} THEN ${star ? "1" : src} END`, c.viewColumn);
|
|
639
|
+
return c.aggFn === "sum" ? `COALESCE(${call}, 0)` : call;
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Builds a CREATE VIEW statement from a view plan and column mappings.
|
|
643
|
+
*
|
|
644
|
+
* Joins render in declaration order — `JOIN` (inner, the default) or
|
|
645
|
+
* `LEFT JOIN` for `kind: "left"`. Column mappings carry PHYSICAL source
|
|
646
|
+
* names; `resolveFieldRef` renders predicate refs (join ON, WHERE, HAVING
|
|
647
|
+
* fallbacks) as `"table"."column"`.
|
|
648
|
+
*/
|
|
649
|
+
function buildCreateView(dialect, viewName, plan, columns, resolveFieldRef) {
|
|
650
|
+
const selectCols = columns.map((c) => {
|
|
651
|
+
return `${c.aggFn ? viewAggExpr(dialect, c, resolveFieldRef) : viewSourceExpr(dialect, c)} AS ${dialect.quoteIdentifier(c.viewColumn)}`;
|
|
652
|
+
}).join(", ");
|
|
653
|
+
let sql = `${dialect.createViewPrefix} ${dialect.quoteTable(viewName)} AS SELECT ${selectCols} FROM ${dialect.quoteIdentifier(plan.entryTable)}`;
|
|
654
|
+
for (const join of plan.joins) {
|
|
655
|
+
const onClause = queryNodeToSql(join.condition, resolveFieldRef);
|
|
656
|
+
const keyword = join.kind === "left" ? "LEFT JOIN" : "JOIN";
|
|
657
|
+
sql += ` ${keyword} ${dialect.quoteIdentifier(join.targetTable)} ON ${onClause}`;
|
|
658
|
+
}
|
|
659
|
+
if (plan.filter) {
|
|
660
|
+
const whereClause = queryNodeToSql(plan.filter, resolveFieldRef);
|
|
661
|
+
sql += ` WHERE ${whereClause}`;
|
|
662
|
+
}
|
|
663
|
+
if (columns.some((c) => c.aggFn)) {
|
|
664
|
+
const dimensionCols = columns.filter((c) => !c.aggFn);
|
|
665
|
+
if (dimensionCols.length > 0) {
|
|
666
|
+
const groupByCols = dimensionCols.map((c) => viewSourceExpr(dialect, c)).join(", ");
|
|
667
|
+
sql += ` GROUP BY ${groupByCols}`;
|
|
668
|
+
}
|
|
669
|
+
if (plan.having) {
|
|
670
|
+
const columnByPath = /* @__PURE__ */ new Map();
|
|
671
|
+
for (const c of columns) columnByPath.set(c.viewPath, c);
|
|
672
|
+
const havingResolver = (ref) => {
|
|
673
|
+
const col = ref.type ? void 0 : columnByPath.get(ref.field);
|
|
674
|
+
if (!col) return resolveFieldRef(ref);
|
|
675
|
+
if (col.aggFn) return viewAggExpr(dialect, col, resolveFieldRef);
|
|
676
|
+
const expr = viewSourceExpr(dialect, col);
|
|
677
|
+
return col.json ? havingGroupRef(dialect, expr, col.viewColumn) : expr;
|
|
678
|
+
};
|
|
679
|
+
const havingClause = queryNodeToSql(plan.having, havingResolver);
|
|
680
|
+
sql += ` HAVING ${havingClause}`;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
return sql;
|
|
684
|
+
}
|
|
685
|
+
//#endregion
|
|
507
686
|
//#region src/sql-builder.ts
|
|
508
687
|
/**
|
|
509
688
|
* Builds an INSERT statement.
|
|
@@ -701,50 +880,6 @@ function buildProjection(dialect, select) {
|
|
|
701
880
|
}
|
|
702
881
|
return sql || "*";
|
|
703
882
|
}
|
|
704
|
-
/** Builds the SQL expression for a single aggregate column. */
|
|
705
|
-
function buildAggColExpr(dialect, c) {
|
|
706
|
-
return `${AGG_FN_SQL[c.aggFn] ?? c.aggFn.toUpperCase()}(${c.aggField === "*" ? "*" : `${dialect.quoteIdentifier(c.sourceTable)}.${dialect.quoteIdentifier(c.sourceColumn)}`})`;
|
|
707
|
-
}
|
|
708
|
-
/**
|
|
709
|
-
* Builds a CREATE VIEW statement from a view plan and column mappings.
|
|
710
|
-
*/
|
|
711
|
-
function buildCreateView(dialect, viewName, plan, columns, resolveFieldRef) {
|
|
712
|
-
const selectCols = columns.map((c) => {
|
|
713
|
-
if (c.aggFn) return `${buildAggColExpr(dialect, c)} AS ${dialect.quoteIdentifier(c.viewColumn)}`;
|
|
714
|
-
return `${dialect.quoteIdentifier(c.sourceTable)}.${dialect.quoteIdentifier(c.sourceColumn)} AS ${dialect.quoteIdentifier(c.viewColumn)}`;
|
|
715
|
-
}).join(", ");
|
|
716
|
-
let sql = `${dialect.createViewPrefix} ${dialect.quoteTable(viewName)} AS SELECT ${selectCols} FROM ${dialect.quoteIdentifier(plan.entryTable)}`;
|
|
717
|
-
for (const join of plan.joins) {
|
|
718
|
-
const onClause = queryNodeToSql(join.condition, resolveFieldRef);
|
|
719
|
-
sql += ` JOIN ${dialect.quoteIdentifier(join.targetTable)} ON ${onClause}`;
|
|
720
|
-
}
|
|
721
|
-
if (plan.filter) {
|
|
722
|
-
const whereClause = queryNodeToSql(plan.filter, resolveFieldRef);
|
|
723
|
-
sql += ` WHERE ${whereClause}`;
|
|
724
|
-
}
|
|
725
|
-
if (columns.some((c) => c.aggFn)) {
|
|
726
|
-
const dimensionCols = columns.filter((c) => !c.aggFn);
|
|
727
|
-
if (dimensionCols.length > 0) {
|
|
728
|
-
const groupByCols = dimensionCols.map((c) => `${dialect.quoteIdentifier(c.sourceTable)}.${dialect.quoteIdentifier(c.sourceColumn)}`).join(", ");
|
|
729
|
-
sql += ` GROUP BY ${groupByCols}`;
|
|
730
|
-
}
|
|
731
|
-
if (plan.having) {
|
|
732
|
-
const columnMap = /* @__PURE__ */ new Map();
|
|
733
|
-
for (const c of columns) columnMap.set(c.viewColumn, c);
|
|
734
|
-
const havingResolver = (ref) => {
|
|
735
|
-
if (!ref.type) {
|
|
736
|
-
const col = columnMap.get(ref.field);
|
|
737
|
-
if (col?.aggFn) return buildAggColExpr(dialect, col);
|
|
738
|
-
if (col) return `${dialect.quoteIdentifier(col.sourceTable)}.${dialect.quoteIdentifier(col.sourceColumn)}`;
|
|
739
|
-
}
|
|
740
|
-
return resolveFieldRef(ref);
|
|
741
|
-
};
|
|
742
|
-
const havingClause = queryNodeToSql(plan.having, havingResolver);
|
|
743
|
-
sql += ` HAVING ${havingClause}`;
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
return sql;
|
|
747
|
-
}
|
|
748
883
|
//#endregion
|
|
749
884
|
//#region src/regex.ts
|
|
750
885
|
/**
|
|
@@ -796,10 +931,12 @@ exports.finalizeParams = finalizeParams;
|
|
|
796
931
|
exports.geoWindowFromControls = geoWindowFromControls;
|
|
797
932
|
exports.groupKeySql = groupKeySql;
|
|
798
933
|
exports.insertManyColumns = insertManyColumns;
|
|
934
|
+
exports.jsonDollarPath = jsonDollarPath;
|
|
799
935
|
exports.normalizeGeoPointValue = normalizeGeoPointValue;
|
|
800
936
|
exports.parseRegexString = parseRegexString;
|
|
801
937
|
exports.queryNodeToSql = queryNodeToSql;
|
|
802
938
|
exports.queryOpToSql = queryOpToSql;
|
|
939
|
+
exports.quotedJsonPathSegments = quotedJsonPathSegments;
|
|
803
940
|
exports.refActionToSql = refActionToSql;
|
|
804
941
|
exports.renameGeoDistance = renameGeoDistance;
|
|
805
942
|
exports.replaceColumnsFor = replaceColumnsFor;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { AtscriptQueryFieldRef, AtscriptQueryNode, DbControls, TDbDefaultFn, TDbFieldMeta, TDbReferentialAction, TFieldOps, TResolvedBucket, TViewColumnMapping, TViewPlan, UniquSelect } from "@atscript/db";
|
|
1
|
+
import { AtscriptQueryFieldRef, AtscriptQueryNode, DbControls, TDbDefaultFn, TDbFieldMeta, TDbReferentialAction, TFieldOps, TResolvedBucket, TViewColumnMapping, TViewJsonType, TViewPlan, UniquSelect } from "@atscript/db";
|
|
2
2
|
import { FilterExpr, FilterVisitor } from "@uniqu/core";
|
|
3
|
+
import { TDbAggregateFn } from "@atscript/db/agg";
|
|
3
4
|
|
|
4
5
|
//#region src/dialect.d.ts
|
|
5
6
|
interface TSqlFragment {
|
|
@@ -47,9 +48,10 @@ interface SqlDialect {
|
|
|
47
48
|
*/
|
|
48
49
|
calendarBucket?(quotedCol: string, b: TResolvedBucket): string;
|
|
49
50
|
/**
|
|
50
|
-
* HAVING references a calendar bucket
|
|
51
|
-
*
|
|
52
|
-
*
|
|
51
|
+
* HAVING references a calendar bucket — and a view's JSON-extracted
|
|
52
|
+
* dimension ({@link jsonExtract}) — by its quoted SELECT alias instead of
|
|
53
|
+
* re-rendering the expression (the aggregate count query's inner SELECT
|
|
54
|
+
* lists `<bucket expr> AS alias`, so the alias exists there too).
|
|
53
55
|
*
|
|
54
56
|
* MySQL needs this: the bucket expression reads the raw source column, which
|
|
55
57
|
* is not itself in GROUP BY (only the expression is), so HAVING rejects it
|
|
@@ -60,6 +62,27 @@ interface SqlDialect {
|
|
|
60
62
|
* Since 0.1.132.
|
|
61
63
|
*/
|
|
62
64
|
bucketAliasInHaving?: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Typed extraction of one primitive leaf inside a JSON column — how a view
|
|
67
|
+
* field that references a descendant of a `@db.json` column reads it.
|
|
68
|
+
*
|
|
69
|
+
* Semantics (identical on every dialect): the result is the declared
|
|
70
|
+
* primitive, or NULL when the path is missing, the value is JSON `null`, or
|
|
71
|
+
* the value has another JSON type — never a coercion (the JSON string
|
|
72
|
+
* `"5"` is NULL for a `number` leaf). Numbers come back as doubles (integers
|
|
73
|
+
* beyond 2^53 lose precision); booleans as the dialect's boolean
|
|
74
|
+
* representation (0/1 where booleans are integers).
|
|
75
|
+
*
|
|
76
|
+
* `quotedCol` is already quoted (`"table"."column"`); `path` holds the
|
|
77
|
+
* segments below it. The expression must be PARAMETER-FREE — it renders in
|
|
78
|
+
* CREATE VIEW DDL, repeated in SELECT, GROUP BY, HAVING and aggregate
|
|
79
|
+
* arguments. Quote every segment via {@link quotedJsonPathSegments}.
|
|
80
|
+
* Dialects without JSON extraction omit this; view sync then fails with
|
|
81
|
+
* `JSON extraction is not supported by this adapter`.
|
|
82
|
+
*
|
|
83
|
+
* @since 0.1.136
|
|
84
|
+
*/
|
|
85
|
+
jsonExtract?(quotedCol: string, path: readonly string[], type: TViewJsonType): string;
|
|
63
86
|
/** e.g. 'CREATE VIEW IF NOT EXISTS' or 'CREATE OR REPLACE VIEW' */
|
|
64
87
|
createViewPrefix: string;
|
|
65
88
|
/** Returns a parameter placeholder for the given 1-based index. When absent, '?' is used. */
|
|
@@ -70,6 +93,18 @@ interface SqlDialect {
|
|
|
70
93
|
* (e.g. `$1, $2, ...` for PostgreSQL). No-op when `dialect.paramPlaceholder` is not set.
|
|
71
94
|
*/
|
|
72
95
|
declare function finalizeParams(dialect: SqlDialect, fragment: TSqlFragment): TSqlFragment;
|
|
96
|
+
/**
|
|
97
|
+
* Each JSON path segment wrapped in double quotes (`"a"`), for the dialects'
|
|
98
|
+
* {@link SqlDialect.jsonExtract} path literals (`'$."a"."b"'`, `'{"a","b"}'`).
|
|
99
|
+
*
|
|
100
|
+
* A segment containing `"`, `\` or a control character has no quoting that
|
|
101
|
+
* reads the same on every dialect (SQLite's quoted path labels have no escape
|
|
102
|
+
* syntax), so it is rejected rather than escaped.
|
|
103
|
+
*
|
|
104
|
+
* @since 0.1.136
|
|
105
|
+
* @throws for an empty path or a segment that is empty or holds such a character.
|
|
106
|
+
*/
|
|
107
|
+
declare function quotedJsonPathSegments(path: readonly string[]): string[];
|
|
73
108
|
declare const EMPTY_AND: TSqlFragment;
|
|
74
109
|
declare const EMPTY_OR: TSqlFragment;
|
|
75
110
|
//#endregion
|
|
@@ -142,6 +177,17 @@ declare function normalizeGeoPointValue(value: unknown): [number, number] | unde
|
|
|
142
177
|
/** Renames the internal distance alias to the public `$distance` field (in place). */
|
|
143
178
|
declare function renameGeoDistance(row: Record<string, unknown>): Record<string, unknown>;
|
|
144
179
|
//#endregion
|
|
180
|
+
//#region src/view-builder.d.ts
|
|
181
|
+
/**
|
|
182
|
+
* Builds a CREATE VIEW statement from a view plan and column mappings.
|
|
183
|
+
*
|
|
184
|
+
* Joins render in declaration order — `JOIN` (inner, the default) or
|
|
185
|
+
* `LEFT JOIN` for `kind: "left"`. Column mappings carry PHYSICAL source
|
|
186
|
+
* names; `resolveFieldRef` renders predicate refs (join ON, WHERE, HAVING
|
|
187
|
+
* fallbacks) as `"table"."column"`.
|
|
188
|
+
*/
|
|
189
|
+
declare function buildCreateView(dialect: SqlDialect, viewName: string, plan: TViewPlan, columns: TViewColumnMapping[], resolveFieldRef: (ref: AtscriptQueryFieldRef) => string): string;
|
|
190
|
+
//#endregion
|
|
145
191
|
//#region src/sql-builder.d.ts
|
|
146
192
|
/**
|
|
147
193
|
* Builds an INSERT statement.
|
|
@@ -226,14 +272,19 @@ declare function buildDelete(dialect: SqlDialect, table: string, where: TSqlFrag
|
|
|
226
272
|
* Builds a column projection (SELECT clause fields).
|
|
227
273
|
*/
|
|
228
274
|
declare function buildProjection(dialect: SqlDialect, select?: UniquSelect): string;
|
|
229
|
-
/**
|
|
230
|
-
* Builds a CREATE VIEW statement from a view plan and column mappings.
|
|
231
|
-
*/
|
|
232
|
-
declare function buildCreateView(dialect: SqlDialect, viewName: string, plan: TViewPlan, columns: TViewColumnMapping[], resolveFieldRef: (ref: AtscriptQueryFieldRef) => string): string;
|
|
233
275
|
//#endregion
|
|
234
276
|
//#region src/common.d.ts
|
|
235
277
|
/** Formats a string value as a SQL literal with single-quote escaping. */
|
|
236
278
|
declare function sqlStringLiteral(value: string): string;
|
|
279
|
+
/**
|
|
280
|
+
* The SQL string literal of a `$`-rooted JSON path with every segment quoted
|
|
281
|
+
* (`'$."a"."b"'`) — the path argument of SQLite's and MySQL's `json_extract` /
|
|
282
|
+
* `json_type`.
|
|
283
|
+
*
|
|
284
|
+
* @since 0.1.136
|
|
285
|
+
* @throws as {@link quotedJsonPathSegments}.
|
|
286
|
+
*/
|
|
287
|
+
declare function jsonDollarPath(path: readonly string[]): string;
|
|
237
288
|
/**
|
|
238
289
|
* A calendar bucket's time zone as an inlined SQL string literal (`'Europe/Berlin'`).
|
|
239
290
|
*
|
|
@@ -259,11 +310,22 @@ declare function defaultValueToSqlLiteral(designType: string, value: string): st
|
|
|
259
310
|
declare const queryOpToSql: Record<string, string>;
|
|
260
311
|
/**
|
|
261
312
|
* Renders an AtscriptQueryNode tree to raw SQL (no parameters -- for DDL use only).
|
|
313
|
+
*
|
|
314
|
+
* Operators: `=`/`!=`/`<`/`<=`/`>`/`>=` (a `null` operand becomes `IS [NOT] NULL`),
|
|
315
|
+
* `in` / `not in` (inlined literals; an empty `in` is `0=1`, an empty
|
|
316
|
+
* `not in` `1=1`), `exists` → `IS NOT NULL`, `not exists` → `IS NULL`.
|
|
317
|
+
*
|
|
318
|
+
* @throws for `matches` (`$regex`) and any other operator — a view predicate
|
|
319
|
+
* that cannot be rendered fails at sync instead of rendering wrong SQL.
|
|
262
320
|
*/
|
|
263
321
|
declare function queryNodeToSql(node: AtscriptQueryNode, resolveFieldRef: (ref: AtscriptQueryFieldRef) => string): string;
|
|
264
322
|
//#endregion
|
|
265
323
|
//#region src/agg.d.ts
|
|
266
|
-
|
|
324
|
+
/**
|
|
325
|
+
* SQL function name of each single-name aggregate. `countDistinct` is not a
|
|
326
|
+
* name but a form (`COUNT(DISTINCT x)`) — see {@link renderAggCall}.
|
|
327
|
+
*/
|
|
328
|
+
declare const AGG_FN_SQL: Readonly<Record<Exclude<TDbAggregateFn, "countDistinct">, string>>;
|
|
267
329
|
/**
|
|
268
330
|
* The SQL a `$groupBy` key renders as: a calendar-bucket alias renders the
|
|
269
331
|
* bucket EXPRESSION (`dialect.calendarBucket`), anything else the quoted
|
|
@@ -306,4 +368,4 @@ declare function parseRegexString(value: unknown): {
|
|
|
306
368
|
flags: string;
|
|
307
369
|
};
|
|
308
370
|
//#endregion
|
|
309
|
-
export { AGG_FN_SQL, EMPTY_AND, EMPTY_OR, GEO_DISTANCE_ALIAS, SQL_DEFAULT, type SqlDialect, type TFilterVisitorOptions, type TGeoCircle, type TGeoWindow, type TReplaceColumn, type TSqlFragment, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildProjection, buildSelect, buildUpdate, buildWhere, createFilterVisitor, defaultValueForType, defaultValueToSqlLiteral, fillReplacePayload, finalizeParams, geoWindowFromControls, groupKeySql, insertManyColumns, normalizeGeoPointValue, parseRegexString, queryNodeToSql, queryOpToSql, refActionToSql, renameGeoDistance, replaceColumnsFor, sqlStringLiteral, sqlTimeZoneLiteral, toSqlValue };
|
|
371
|
+
export { AGG_FN_SQL, EMPTY_AND, EMPTY_OR, GEO_DISTANCE_ALIAS, SQL_DEFAULT, type SqlDialect, type TFilterVisitorOptions, type TGeoCircle, type TGeoWindow, type TReplaceColumn, type TSqlFragment, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildProjection, buildSelect, buildUpdate, buildWhere, createFilterVisitor, defaultValueForType, defaultValueToSqlLiteral, fillReplacePayload, finalizeParams, geoWindowFromControls, groupKeySql, insertManyColumns, jsonDollarPath, normalizeGeoPointValue, parseRegexString, queryNodeToSql, queryOpToSql, quotedJsonPathSegments, refActionToSql, renameGeoDistance, replaceColumnsFor, sqlStringLiteral, sqlTimeZoneLiteral, toSqlValue };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { FilterExpr, FilterVisitor } from "@uniqu/core";
|
|
2
|
-
import { AtscriptQueryFieldRef, AtscriptQueryNode, DbControls, TDbDefaultFn, TDbFieldMeta, TDbReferentialAction, TFieldOps, TResolvedBucket, TViewColumnMapping, TViewPlan, UniquSelect } from "@atscript/db";
|
|
2
|
+
import { AtscriptQueryFieldRef, AtscriptQueryNode, DbControls, TDbDefaultFn, TDbFieldMeta, TDbReferentialAction, TFieldOps, TResolvedBucket, TViewColumnMapping, TViewJsonType, TViewPlan, UniquSelect } from "@atscript/db";
|
|
3
|
+
import { TDbAggregateFn } from "@atscript/db/agg";
|
|
3
4
|
|
|
4
5
|
//#region src/dialect.d.ts
|
|
5
6
|
interface TSqlFragment {
|
|
@@ -47,9 +48,10 @@ interface SqlDialect {
|
|
|
47
48
|
*/
|
|
48
49
|
calendarBucket?(quotedCol: string, b: TResolvedBucket): string;
|
|
49
50
|
/**
|
|
50
|
-
* HAVING references a calendar bucket
|
|
51
|
-
*
|
|
52
|
-
*
|
|
51
|
+
* HAVING references a calendar bucket — and a view's JSON-extracted
|
|
52
|
+
* dimension ({@link jsonExtract}) — by its quoted SELECT alias instead of
|
|
53
|
+
* re-rendering the expression (the aggregate count query's inner SELECT
|
|
54
|
+
* lists `<bucket expr> AS alias`, so the alias exists there too).
|
|
53
55
|
*
|
|
54
56
|
* MySQL needs this: the bucket expression reads the raw source column, which
|
|
55
57
|
* is not itself in GROUP BY (only the expression is), so HAVING rejects it
|
|
@@ -60,6 +62,27 @@ interface SqlDialect {
|
|
|
60
62
|
* Since 0.1.132.
|
|
61
63
|
*/
|
|
62
64
|
bucketAliasInHaving?: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Typed extraction of one primitive leaf inside a JSON column — how a view
|
|
67
|
+
* field that references a descendant of a `@db.json` column reads it.
|
|
68
|
+
*
|
|
69
|
+
* Semantics (identical on every dialect): the result is the declared
|
|
70
|
+
* primitive, or NULL when the path is missing, the value is JSON `null`, or
|
|
71
|
+
* the value has another JSON type — never a coercion (the JSON string
|
|
72
|
+
* `"5"` is NULL for a `number` leaf). Numbers come back as doubles (integers
|
|
73
|
+
* beyond 2^53 lose precision); booleans as the dialect's boolean
|
|
74
|
+
* representation (0/1 where booleans are integers).
|
|
75
|
+
*
|
|
76
|
+
* `quotedCol` is already quoted (`"table"."column"`); `path` holds the
|
|
77
|
+
* segments below it. The expression must be PARAMETER-FREE — it renders in
|
|
78
|
+
* CREATE VIEW DDL, repeated in SELECT, GROUP BY, HAVING and aggregate
|
|
79
|
+
* arguments. Quote every segment via {@link quotedJsonPathSegments}.
|
|
80
|
+
* Dialects without JSON extraction omit this; view sync then fails with
|
|
81
|
+
* `JSON extraction is not supported by this adapter`.
|
|
82
|
+
*
|
|
83
|
+
* @since 0.1.136
|
|
84
|
+
*/
|
|
85
|
+
jsonExtract?(quotedCol: string, path: readonly string[], type: TViewJsonType): string;
|
|
63
86
|
/** e.g. 'CREATE VIEW IF NOT EXISTS' or 'CREATE OR REPLACE VIEW' */
|
|
64
87
|
createViewPrefix: string;
|
|
65
88
|
/** Returns a parameter placeholder for the given 1-based index. When absent, '?' is used. */
|
|
@@ -70,6 +93,18 @@ interface SqlDialect {
|
|
|
70
93
|
* (e.g. `$1, $2, ...` for PostgreSQL). No-op when `dialect.paramPlaceholder` is not set.
|
|
71
94
|
*/
|
|
72
95
|
declare function finalizeParams(dialect: SqlDialect, fragment: TSqlFragment): TSqlFragment;
|
|
96
|
+
/**
|
|
97
|
+
* Each JSON path segment wrapped in double quotes (`"a"`), for the dialects'
|
|
98
|
+
* {@link SqlDialect.jsonExtract} path literals (`'$."a"."b"'`, `'{"a","b"}'`).
|
|
99
|
+
*
|
|
100
|
+
* A segment containing `"`, `\` or a control character has no quoting that
|
|
101
|
+
* reads the same on every dialect (SQLite's quoted path labels have no escape
|
|
102
|
+
* syntax), so it is rejected rather than escaped.
|
|
103
|
+
*
|
|
104
|
+
* @since 0.1.136
|
|
105
|
+
* @throws for an empty path or a segment that is empty or holds such a character.
|
|
106
|
+
*/
|
|
107
|
+
declare function quotedJsonPathSegments(path: readonly string[]): string[];
|
|
73
108
|
declare const EMPTY_AND: TSqlFragment;
|
|
74
109
|
declare const EMPTY_OR: TSqlFragment;
|
|
75
110
|
//#endregion
|
|
@@ -142,6 +177,17 @@ declare function normalizeGeoPointValue(value: unknown): [number, number] | unde
|
|
|
142
177
|
/** Renames the internal distance alias to the public `$distance` field (in place). */
|
|
143
178
|
declare function renameGeoDistance(row: Record<string, unknown>): Record<string, unknown>;
|
|
144
179
|
//#endregion
|
|
180
|
+
//#region src/view-builder.d.ts
|
|
181
|
+
/**
|
|
182
|
+
* Builds a CREATE VIEW statement from a view plan and column mappings.
|
|
183
|
+
*
|
|
184
|
+
* Joins render in declaration order — `JOIN` (inner, the default) or
|
|
185
|
+
* `LEFT JOIN` for `kind: "left"`. Column mappings carry PHYSICAL source
|
|
186
|
+
* names; `resolveFieldRef` renders predicate refs (join ON, WHERE, HAVING
|
|
187
|
+
* fallbacks) as `"table"."column"`.
|
|
188
|
+
*/
|
|
189
|
+
declare function buildCreateView(dialect: SqlDialect, viewName: string, plan: TViewPlan, columns: TViewColumnMapping[], resolveFieldRef: (ref: AtscriptQueryFieldRef) => string): string;
|
|
190
|
+
//#endregion
|
|
145
191
|
//#region src/sql-builder.d.ts
|
|
146
192
|
/**
|
|
147
193
|
* Builds an INSERT statement.
|
|
@@ -226,14 +272,19 @@ declare function buildDelete(dialect: SqlDialect, table: string, where: TSqlFrag
|
|
|
226
272
|
* Builds a column projection (SELECT clause fields).
|
|
227
273
|
*/
|
|
228
274
|
declare function buildProjection(dialect: SqlDialect, select?: UniquSelect): string;
|
|
229
|
-
/**
|
|
230
|
-
* Builds a CREATE VIEW statement from a view plan and column mappings.
|
|
231
|
-
*/
|
|
232
|
-
declare function buildCreateView(dialect: SqlDialect, viewName: string, plan: TViewPlan, columns: TViewColumnMapping[], resolveFieldRef: (ref: AtscriptQueryFieldRef) => string): string;
|
|
233
275
|
//#endregion
|
|
234
276
|
//#region src/common.d.ts
|
|
235
277
|
/** Formats a string value as a SQL literal with single-quote escaping. */
|
|
236
278
|
declare function sqlStringLiteral(value: string): string;
|
|
279
|
+
/**
|
|
280
|
+
* The SQL string literal of a `$`-rooted JSON path with every segment quoted
|
|
281
|
+
* (`'$."a"."b"'`) — the path argument of SQLite's and MySQL's `json_extract` /
|
|
282
|
+
* `json_type`.
|
|
283
|
+
*
|
|
284
|
+
* @since 0.1.136
|
|
285
|
+
* @throws as {@link quotedJsonPathSegments}.
|
|
286
|
+
*/
|
|
287
|
+
declare function jsonDollarPath(path: readonly string[]): string;
|
|
237
288
|
/**
|
|
238
289
|
* A calendar bucket's time zone as an inlined SQL string literal (`'Europe/Berlin'`).
|
|
239
290
|
*
|
|
@@ -259,11 +310,22 @@ declare function defaultValueToSqlLiteral(designType: string, value: string): st
|
|
|
259
310
|
declare const queryOpToSql: Record<string, string>;
|
|
260
311
|
/**
|
|
261
312
|
* Renders an AtscriptQueryNode tree to raw SQL (no parameters -- for DDL use only).
|
|
313
|
+
*
|
|
314
|
+
* Operators: `=`/`!=`/`<`/`<=`/`>`/`>=` (a `null` operand becomes `IS [NOT] NULL`),
|
|
315
|
+
* `in` / `not in` (inlined literals; an empty `in` is `0=1`, an empty
|
|
316
|
+
* `not in` `1=1`), `exists` → `IS NOT NULL`, `not exists` → `IS NULL`.
|
|
317
|
+
*
|
|
318
|
+
* @throws for `matches` (`$regex`) and any other operator — a view predicate
|
|
319
|
+
* that cannot be rendered fails at sync instead of rendering wrong SQL.
|
|
262
320
|
*/
|
|
263
321
|
declare function queryNodeToSql(node: AtscriptQueryNode, resolveFieldRef: (ref: AtscriptQueryFieldRef) => string): string;
|
|
264
322
|
//#endregion
|
|
265
323
|
//#region src/agg.d.ts
|
|
266
|
-
|
|
324
|
+
/**
|
|
325
|
+
* SQL function name of each single-name aggregate. `countDistinct` is not a
|
|
326
|
+
* name but a form (`COUNT(DISTINCT x)`) — see {@link renderAggCall}.
|
|
327
|
+
*/
|
|
328
|
+
declare const AGG_FN_SQL: Readonly<Record<Exclude<TDbAggregateFn, "countDistinct">, string>>;
|
|
267
329
|
/**
|
|
268
330
|
* The SQL a `$groupBy` key renders as: a calendar-bucket alias renders the
|
|
269
331
|
* bucket EXPRESSION (`dialect.calendarBucket`), anything else the quoted
|
|
@@ -306,4 +368,4 @@ declare function parseRegexString(value: unknown): {
|
|
|
306
368
|
flags: string;
|
|
307
369
|
};
|
|
308
370
|
//#endregion
|
|
309
|
-
export { AGG_FN_SQL, EMPTY_AND, EMPTY_OR, GEO_DISTANCE_ALIAS, SQL_DEFAULT, type SqlDialect, type TFilterVisitorOptions, type TGeoCircle, type TGeoWindow, type TReplaceColumn, type TSqlFragment, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildProjection, buildSelect, buildUpdate, buildWhere, createFilterVisitor, defaultValueForType, defaultValueToSqlLiteral, fillReplacePayload, finalizeParams, geoWindowFromControls, groupKeySql, insertManyColumns, normalizeGeoPointValue, parseRegexString, queryNodeToSql, queryOpToSql, refActionToSql, renameGeoDistance, replaceColumnsFor, sqlStringLiteral, sqlTimeZoneLiteral, toSqlValue };
|
|
371
|
+
export { AGG_FN_SQL, EMPTY_AND, EMPTY_OR, GEO_DISTANCE_ALIAS, SQL_DEFAULT, type SqlDialect, type TFilterVisitorOptions, type TGeoCircle, type TGeoWindow, type TReplaceColumn, type TSqlFragment, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildProjection, buildSelect, buildUpdate, buildWhere, createFilterVisitor, defaultValueForType, defaultValueToSqlLiteral, fillReplacePayload, finalizeParams, geoWindowFromControls, groupKeySql, insertManyColumns, jsonDollarPath, normalizeGeoPointValue, parseRegexString, queryNodeToSql, queryOpToSql, quotedJsonPathSegments, refActionToSql, renameGeoDistance, replaceColumnsFor, sqlStringLiteral, sqlTimeZoneLiteral, toSqlValue };
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BUCKET_UNITS, TIME_ZONE_NAME_RE, WEEK_STARTS, walkFilter } from "@uniqu/core";
|
|
2
|
-
import { DbError } from "@atscript/db";
|
|
3
|
-
import { resolveAlias } from "@atscript/db/agg";
|
|
2
|
+
import { DbError, isFieldRef } from "@atscript/db";
|
|
3
|
+
import { assertAggregateFn, resolveAlias } from "@atscript/db/agg";
|
|
4
4
|
//#region src/dialect.ts
|
|
5
5
|
/**
|
|
6
6
|
* Replaces positional `?` placeholders with dialect-specific numbered placeholders
|
|
@@ -14,6 +14,37 @@ function finalizeParams(dialect, fragment) {
|
|
|
14
14
|
params: fragment.params
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* How HAVING references a grouped NON-column expression (a calendar bucket, a
|
|
19
|
+
* view's JSON-extracted dimension): its quoted SELECT `alias` on dialects that
|
|
20
|
+
* set {@link SqlDialect.bucketAliasInHaving}, else the expression itself.
|
|
21
|
+
*/
|
|
22
|
+
function havingGroupRef(dialect, expr, alias) {
|
|
23
|
+
return dialect.bucketAliasInHaving ? dialect.quoteIdentifier(alias) : expr;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Each JSON path segment wrapped in double quotes (`"a"`), for the dialects'
|
|
27
|
+
* {@link SqlDialect.jsonExtract} path literals (`'$."a"."b"'`, `'{"a","b"}'`).
|
|
28
|
+
*
|
|
29
|
+
* A segment containing `"`, `\` or a control character has no quoting that
|
|
30
|
+
* reads the same on every dialect (SQLite's quoted path labels have no escape
|
|
31
|
+
* syntax), so it is rejected rather than escaped.
|
|
32
|
+
*
|
|
33
|
+
* @since 0.1.136
|
|
34
|
+
* @throws for an empty path or a segment that is empty or holds such a character.
|
|
35
|
+
*/
|
|
36
|
+
function quotedJsonPathSegments(path) {
|
|
37
|
+
if (path.length === 0) throw new Error("JSON extraction needs a path below the JSON column");
|
|
38
|
+
return path.map((seg) => {
|
|
39
|
+
let ok = seg.length > 0;
|
|
40
|
+
for (let i = 0; ok && i < seg.length; i++) {
|
|
41
|
+
const code = seg.charCodeAt(i);
|
|
42
|
+
ok = code >= 32 && code !== 34 && code !== 92;
|
|
43
|
+
}
|
|
44
|
+
if (!ok) throw new Error(`JSON path segment ${JSON.stringify(seg)} can't be extracted — segments must be non-empty and free of '"', '\\' and control characters`);
|
|
45
|
+
return `"${seg}"`;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
17
48
|
const EMPTY_AND = {
|
|
18
49
|
sql: "1=1",
|
|
19
50
|
params: []
|
|
@@ -254,6 +285,17 @@ function sqlStringLiteral(value) {
|
|
|
254
285
|
return `'${value.replace(/'/g, "''")}'`;
|
|
255
286
|
}
|
|
256
287
|
/**
|
|
288
|
+
* The SQL string literal of a `$`-rooted JSON path with every segment quoted
|
|
289
|
+
* (`'$."a"."b"'`) — the path argument of SQLite's and MySQL's `json_extract` /
|
|
290
|
+
* `json_type`.
|
|
291
|
+
*
|
|
292
|
+
* @since 0.1.136
|
|
293
|
+
* @throws as {@link quotedJsonPathSegments}.
|
|
294
|
+
*/
|
|
295
|
+
function jsonDollarPath(path) {
|
|
296
|
+
return sqlStringLiteral(`$.${quotedJsonPathSegments(path).join(".")}`);
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
257
299
|
* A calendar bucket's time zone as an inlined SQL string literal (`'Europe/Berlin'`).
|
|
258
300
|
*
|
|
259
301
|
* The zone is already canonical (the core normalizer ran uniqu's
|
|
@@ -323,23 +365,65 @@ const queryOpToSql = {
|
|
|
323
365
|
$lt: "<",
|
|
324
366
|
$lte: "<="
|
|
325
367
|
};
|
|
368
|
+
/** An inlined SQL literal for a view-predicate value (DDL — no parameters). */
|
|
369
|
+
function predicateLiteral(value) {
|
|
370
|
+
if (value === null || value === void 0) return "NULL";
|
|
371
|
+
if (typeof value === "string") return sqlStringLiteral(value);
|
|
372
|
+
if (typeof value === "number") {
|
|
373
|
+
if (!Number.isFinite(value)) throw new Error(`Non-finite number ${value} in a view predicate`);
|
|
374
|
+
return String(value);
|
|
375
|
+
}
|
|
376
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
377
|
+
throw new Error(`Unsupported literal ${JSON.stringify(value)} in a view predicate`);
|
|
378
|
+
}
|
|
326
379
|
/**
|
|
327
380
|
* Renders an AtscriptQueryNode tree to raw SQL (no parameters -- for DDL use only).
|
|
381
|
+
*
|
|
382
|
+
* Operators: `=`/`!=`/`<`/`<=`/`>`/`>=` (a `null` operand becomes `IS [NOT] NULL`),
|
|
383
|
+
* `in` / `not in` (inlined literals; an empty `in` is `0=1`, an empty
|
|
384
|
+
* `not in` `1=1`), `exists` → `IS NOT NULL`, `not exists` → `IS NULL`.
|
|
385
|
+
*
|
|
386
|
+
* @throws for `matches` (`$regex`) and any other operator — a view predicate
|
|
387
|
+
* that cannot be rendered fails at sync instead of rendering wrong SQL.
|
|
328
388
|
*/
|
|
329
389
|
function queryNodeToSql(node, resolveFieldRef) {
|
|
330
|
-
if ("$and" in node)
|
|
331
|
-
|
|
390
|
+
if ("$and" in node) {
|
|
391
|
+
const children = node.$and;
|
|
392
|
+
if (children.length === 0) return EMPTY_AND.sql;
|
|
393
|
+
return children.map((n) => queryNodeToSql(n, resolveFieldRef)).join(" AND ");
|
|
394
|
+
}
|
|
395
|
+
if ("$or" in node) {
|
|
396
|
+
const children = node.$or;
|
|
397
|
+
if (children.length === 0) return EMPTY_OR.sql;
|
|
398
|
+
return `(${children.map((n) => queryNodeToSql(n, resolveFieldRef)).join(" OR ")})`;
|
|
399
|
+
}
|
|
332
400
|
if ("$not" in node) return `NOT (${queryNodeToSql(node.$not, resolveFieldRef)})`;
|
|
333
401
|
const comp = node;
|
|
334
402
|
const leftSql = resolveFieldRef(comp.left);
|
|
335
|
-
|
|
336
|
-
|
|
403
|
+
switch (comp.op) {
|
|
404
|
+
case "$exists": return comp.right === false ? `${leftSql} IS NULL` : `${leftSql} IS NOT NULL`;
|
|
405
|
+
case "$in":
|
|
406
|
+
case "$nin": {
|
|
407
|
+
const values = Array.isArray(comp.right) ? comp.right : [comp.right];
|
|
408
|
+
if (values.length === 0) return comp.op === "$in" ? EMPTY_OR.sql : EMPTY_AND.sql;
|
|
409
|
+
const list = values.map((v) => predicateLiteral(v)).join(", ");
|
|
410
|
+
return `${leftSql} ${comp.op === "$in" ? "IN" : "NOT IN"} (${list})`;
|
|
411
|
+
}
|
|
412
|
+
case "$regex": throw new Error("matches is not supported in view predicates");
|
|
413
|
+
default:
|
|
414
|
+
}
|
|
415
|
+
const sqlOp = queryOpToSql[comp.op];
|
|
416
|
+
if (!sqlOp) throw new Error(`Operator "${comp.op}" is not supported in view predicates`);
|
|
417
|
+
if (isFieldRef(comp.right)) return `${leftSql} ${sqlOp} ${resolveFieldRef(comp.right)}`;
|
|
337
418
|
if (comp.right === null || comp.right === void 0) return comp.op === "$ne" ? `${leftSql} IS NOT NULL` : `${leftSql} IS NULL`;
|
|
338
|
-
|
|
339
|
-
return `${leftSql} ${sqlOp} ${comp.right}`;
|
|
419
|
+
return `${leftSql} ${sqlOp} ${predicateLiteral(comp.right)}`;
|
|
340
420
|
}
|
|
341
421
|
//#endregion
|
|
342
422
|
//#region src/agg.ts
|
|
423
|
+
/**
|
|
424
|
+
* SQL function name of each single-name aggregate. `countDistinct` is not a
|
|
425
|
+
* name but a form (`COUNT(DISTINCT x)`) — see {@link renderAggCall}.
|
|
426
|
+
*/
|
|
343
427
|
const AGG_FN_SQL = {
|
|
344
428
|
sum: "SUM",
|
|
345
429
|
avg: "AVG",
|
|
@@ -347,9 +431,20 @@ const AGG_FN_SQL = {
|
|
|
347
431
|
min: "MIN",
|
|
348
432
|
max: "MAX"
|
|
349
433
|
};
|
|
350
|
-
/**
|
|
434
|
+
/**
|
|
435
|
+
* Renders one aggregate call over an already-rendered argument (`*`, a quoted
|
|
436
|
+
* column, a `CASE` expression): `SUM(x)`, `COUNT(DISTINCT x)`, …
|
|
437
|
+
* Re-asserts the name first (`INVALID_QUERY` on an unknown one), so nothing
|
|
438
|
+
* unchecked reaches SQL.
|
|
439
|
+
*/
|
|
440
|
+
function renderAggCall(fn, arg, path) {
|
|
441
|
+
assertAggregateFn(fn, path);
|
|
442
|
+
return fn === "countDistinct" ? `COUNT(DISTINCT ${arg})` : `${AGG_FN_SQL[fn]}(${arg})`;
|
|
443
|
+
}
|
|
444
|
+
/** The bare aggregate call, e.g. `SUM("amount")` / `COUNT(*)` / `COUNT(DISTINCT "region")`. */
|
|
351
445
|
function aggFnSql(dialect, expr) {
|
|
352
|
-
|
|
446
|
+
const field = expr.$field === "*" ? "*" : dialect.quoteIdentifier(expr.$field);
|
|
447
|
+
return renderAggCall(expr.$fn, field);
|
|
353
448
|
}
|
|
354
449
|
function buildAggExpr(dialect, expr) {
|
|
355
450
|
return `${aggFnSql(dialect, expr)} AS ${dialect.quoteIdentifier(resolveAlias(expr))}`;
|
|
@@ -410,7 +505,7 @@ function groupKeySql(dialect, controls, key) {
|
|
|
410
505
|
* not allow a SELECT alias in HAVING (MySQL and SQLite tolerate it, so the
|
|
411
506
|
* expression form keeps all three identical). A calendar-bucket alias renders
|
|
412
507
|
* its bucket expression ({@link groupKeySql}), or its quoted alias when the
|
|
413
|
-
* dialect sets `SqlDialect.bucketAliasInHaving` (
|
|
508
|
+
* dialect sets `SqlDialect.bucketAliasInHaving` (`havingGroupRef`). Other keys
|
|
414
509
|
* (grouped columns) render as plain columns.
|
|
415
510
|
*/
|
|
416
511
|
function havingClause(dialect, controls) {
|
|
@@ -421,8 +516,8 @@ function havingClause(dialect, controls) {
|
|
|
421
516
|
const fragment = walkFilter(having, createFilterVisitor(dialect, { columnRef: (field) => {
|
|
422
517
|
const aggExpr = exprByAlias.get(field);
|
|
423
518
|
if (aggExpr) return aggExpr;
|
|
424
|
-
|
|
425
|
-
return
|
|
519
|
+
const bucket = controls.$select?.bucketByAlias(field);
|
|
520
|
+
return bucket ? havingGroupRef(dialect, bucketSql(dialect, bucket), field) : dialect.quoteIdentifier(field);
|
|
426
521
|
} }));
|
|
427
522
|
if (!fragment || fragment.sql === EMPTY_AND.sql) return void 0;
|
|
428
523
|
return {
|
|
@@ -503,6 +598,90 @@ function buildAggregateCount(dialect, table, where, controls) {
|
|
|
503
598
|
});
|
|
504
599
|
}
|
|
505
600
|
//#endregion
|
|
601
|
+
//#region src/view-builder.ts
|
|
602
|
+
/**
|
|
603
|
+
* The SQL expression a view column reads: `"table"."column"` — the physical
|
|
604
|
+
* source column resolved by `AtscriptDbView.getViewColumnMappings()` — or,
|
|
605
|
+
* for a primitive leaf inside a JSON column (`mapping.json`), the dialect's
|
|
606
|
+
* typed {@link SqlDialect.jsonExtract} over that column. Used for SELECT
|
|
607
|
+
* columns, GROUP BY / HAVING dimensions and as an aggregate's source.
|
|
608
|
+
*
|
|
609
|
+
* @throws when the column reads a JSON leaf and the dialect has no `jsonExtract`.
|
|
610
|
+
*/
|
|
611
|
+
function viewSourceExpr(dialect, mapping) {
|
|
612
|
+
const col = `${dialect.quoteIdentifier(mapping.sourceTable)}.${dialect.quoteIdentifier(mapping.sourceColumn)}`;
|
|
613
|
+
if (!mapping.json) return col;
|
|
614
|
+
if (!dialect.jsonExtract) throw new Error(`View column "${mapping.viewColumn}": JSON extraction is not supported by this adapter`);
|
|
615
|
+
return dialect.jsonExtract(col, mapping.json.path, mapping.json.type);
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* The SQL expression of one aggregate view column, e.g. `SUM("orders"."amount")`,
|
|
619
|
+
* `COUNT(*)` or `COUNT(DISTINCT "orders"."customer_id")`, over
|
|
620
|
+
* {@link viewSourceExpr}. The mapping's aggregate rules (`*` only for count, …)
|
|
621
|
+
* are validated where the core builds it.
|
|
622
|
+
*
|
|
623
|
+
* A conditional aggregate (`aggFilter`, rendered with the view's predicate
|
|
624
|
+
* resolver) aggregates `CASE WHEN <predicate> THEN <src> END` — NULL for the
|
|
625
|
+
* rows the predicate rejects, which every aggregate skips:
|
|
626
|
+
* - `COUNT(*)` → `COUNT(CASE WHEN p THEN 1 END)`;
|
|
627
|
+
* - `countDistinct` → `COUNT(DISTINCT CASE WHEN p THEN src END)`;
|
|
628
|
+
* - `sum` → `COALESCE(SUM(CASE WHEN p THEN src END), 0)` — a group with no
|
|
629
|
+
* matching row sums to 0, not NULL (MongoDB's `$sum` answer too);
|
|
630
|
+
* - `avg` / `min` / `max` stay NULL when no row matches.
|
|
631
|
+
*/
|
|
632
|
+
function viewAggExpr(dialect, c, resolveFieldRef) {
|
|
633
|
+
const star = c.aggField === "*";
|
|
634
|
+
const src = star ? "*" : viewSourceExpr(dialect, c);
|
|
635
|
+
if (!c.aggFilter) return renderAggCall(c.aggFn, src, c.viewColumn);
|
|
636
|
+
const predicate = queryNodeToSql(c.aggFilter, resolveFieldRef);
|
|
637
|
+
const call = renderAggCall(c.aggFn, `CASE WHEN ${predicate} THEN ${star ? "1" : src} END`, c.viewColumn);
|
|
638
|
+
return c.aggFn === "sum" ? `COALESCE(${call}, 0)` : call;
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Builds a CREATE VIEW statement from a view plan and column mappings.
|
|
642
|
+
*
|
|
643
|
+
* Joins render in declaration order — `JOIN` (inner, the default) or
|
|
644
|
+
* `LEFT JOIN` for `kind: "left"`. Column mappings carry PHYSICAL source
|
|
645
|
+
* names; `resolveFieldRef` renders predicate refs (join ON, WHERE, HAVING
|
|
646
|
+
* fallbacks) as `"table"."column"`.
|
|
647
|
+
*/
|
|
648
|
+
function buildCreateView(dialect, viewName, plan, columns, resolveFieldRef) {
|
|
649
|
+
const selectCols = columns.map((c) => {
|
|
650
|
+
return `${c.aggFn ? viewAggExpr(dialect, c, resolveFieldRef) : viewSourceExpr(dialect, c)} AS ${dialect.quoteIdentifier(c.viewColumn)}`;
|
|
651
|
+
}).join(", ");
|
|
652
|
+
let sql = `${dialect.createViewPrefix} ${dialect.quoteTable(viewName)} AS SELECT ${selectCols} FROM ${dialect.quoteIdentifier(plan.entryTable)}`;
|
|
653
|
+
for (const join of plan.joins) {
|
|
654
|
+
const onClause = queryNodeToSql(join.condition, resolveFieldRef);
|
|
655
|
+
const keyword = join.kind === "left" ? "LEFT JOIN" : "JOIN";
|
|
656
|
+
sql += ` ${keyword} ${dialect.quoteIdentifier(join.targetTable)} ON ${onClause}`;
|
|
657
|
+
}
|
|
658
|
+
if (plan.filter) {
|
|
659
|
+
const whereClause = queryNodeToSql(plan.filter, resolveFieldRef);
|
|
660
|
+
sql += ` WHERE ${whereClause}`;
|
|
661
|
+
}
|
|
662
|
+
if (columns.some((c) => c.aggFn)) {
|
|
663
|
+
const dimensionCols = columns.filter((c) => !c.aggFn);
|
|
664
|
+
if (dimensionCols.length > 0) {
|
|
665
|
+
const groupByCols = dimensionCols.map((c) => viewSourceExpr(dialect, c)).join(", ");
|
|
666
|
+
sql += ` GROUP BY ${groupByCols}`;
|
|
667
|
+
}
|
|
668
|
+
if (plan.having) {
|
|
669
|
+
const columnByPath = /* @__PURE__ */ new Map();
|
|
670
|
+
for (const c of columns) columnByPath.set(c.viewPath, c);
|
|
671
|
+
const havingResolver = (ref) => {
|
|
672
|
+
const col = ref.type ? void 0 : columnByPath.get(ref.field);
|
|
673
|
+
if (!col) return resolveFieldRef(ref);
|
|
674
|
+
if (col.aggFn) return viewAggExpr(dialect, col, resolveFieldRef);
|
|
675
|
+
const expr = viewSourceExpr(dialect, col);
|
|
676
|
+
return col.json ? havingGroupRef(dialect, expr, col.viewColumn) : expr;
|
|
677
|
+
};
|
|
678
|
+
const havingClause = queryNodeToSql(plan.having, havingResolver);
|
|
679
|
+
sql += ` HAVING ${havingClause}`;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
return sql;
|
|
683
|
+
}
|
|
684
|
+
//#endregion
|
|
506
685
|
//#region src/sql-builder.ts
|
|
507
686
|
/**
|
|
508
687
|
* Builds an INSERT statement.
|
|
@@ -700,50 +879,6 @@ function buildProjection(dialect, select) {
|
|
|
700
879
|
}
|
|
701
880
|
return sql || "*";
|
|
702
881
|
}
|
|
703
|
-
/** Builds the SQL expression for a single aggregate column. */
|
|
704
|
-
function buildAggColExpr(dialect, c) {
|
|
705
|
-
return `${AGG_FN_SQL[c.aggFn] ?? c.aggFn.toUpperCase()}(${c.aggField === "*" ? "*" : `${dialect.quoteIdentifier(c.sourceTable)}.${dialect.quoteIdentifier(c.sourceColumn)}`})`;
|
|
706
|
-
}
|
|
707
|
-
/**
|
|
708
|
-
* Builds a CREATE VIEW statement from a view plan and column mappings.
|
|
709
|
-
*/
|
|
710
|
-
function buildCreateView(dialect, viewName, plan, columns, resolveFieldRef) {
|
|
711
|
-
const selectCols = columns.map((c) => {
|
|
712
|
-
if (c.aggFn) return `${buildAggColExpr(dialect, c)} AS ${dialect.quoteIdentifier(c.viewColumn)}`;
|
|
713
|
-
return `${dialect.quoteIdentifier(c.sourceTable)}.${dialect.quoteIdentifier(c.sourceColumn)} AS ${dialect.quoteIdentifier(c.viewColumn)}`;
|
|
714
|
-
}).join(", ");
|
|
715
|
-
let sql = `${dialect.createViewPrefix} ${dialect.quoteTable(viewName)} AS SELECT ${selectCols} FROM ${dialect.quoteIdentifier(plan.entryTable)}`;
|
|
716
|
-
for (const join of plan.joins) {
|
|
717
|
-
const onClause = queryNodeToSql(join.condition, resolveFieldRef);
|
|
718
|
-
sql += ` JOIN ${dialect.quoteIdentifier(join.targetTable)} ON ${onClause}`;
|
|
719
|
-
}
|
|
720
|
-
if (plan.filter) {
|
|
721
|
-
const whereClause = queryNodeToSql(plan.filter, resolveFieldRef);
|
|
722
|
-
sql += ` WHERE ${whereClause}`;
|
|
723
|
-
}
|
|
724
|
-
if (columns.some((c) => c.aggFn)) {
|
|
725
|
-
const dimensionCols = columns.filter((c) => !c.aggFn);
|
|
726
|
-
if (dimensionCols.length > 0) {
|
|
727
|
-
const groupByCols = dimensionCols.map((c) => `${dialect.quoteIdentifier(c.sourceTable)}.${dialect.quoteIdentifier(c.sourceColumn)}`).join(", ");
|
|
728
|
-
sql += ` GROUP BY ${groupByCols}`;
|
|
729
|
-
}
|
|
730
|
-
if (plan.having) {
|
|
731
|
-
const columnMap = /* @__PURE__ */ new Map();
|
|
732
|
-
for (const c of columns) columnMap.set(c.viewColumn, c);
|
|
733
|
-
const havingResolver = (ref) => {
|
|
734
|
-
if (!ref.type) {
|
|
735
|
-
const col = columnMap.get(ref.field);
|
|
736
|
-
if (col?.aggFn) return buildAggColExpr(dialect, col);
|
|
737
|
-
if (col) return `${dialect.quoteIdentifier(col.sourceTable)}.${dialect.quoteIdentifier(col.sourceColumn)}`;
|
|
738
|
-
}
|
|
739
|
-
return resolveFieldRef(ref);
|
|
740
|
-
};
|
|
741
|
-
const havingClause = queryNodeToSql(plan.having, havingResolver);
|
|
742
|
-
sql += ` HAVING ${havingClause}`;
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
return sql;
|
|
746
|
-
}
|
|
747
882
|
//#endregion
|
|
748
883
|
//#region src/regex.ts
|
|
749
884
|
/**
|
|
@@ -770,4 +905,4 @@ function parseRegexString(value) {
|
|
|
770
905
|
};
|
|
771
906
|
}
|
|
772
907
|
//#endregion
|
|
773
|
-
export { AGG_FN_SQL, EMPTY_AND, EMPTY_OR, GEO_DISTANCE_ALIAS, SQL_DEFAULT, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildProjection, buildSelect, buildUpdate, buildWhere, createFilterVisitor, defaultValueForType, defaultValueToSqlLiteral, fillReplacePayload, finalizeParams, geoWindowFromControls, groupKeySql, insertManyColumns, normalizeGeoPointValue, parseRegexString, queryNodeToSql, queryOpToSql, refActionToSql, renameGeoDistance, replaceColumnsFor, sqlStringLiteral, sqlTimeZoneLiteral, toSqlValue };
|
|
908
|
+
export { AGG_FN_SQL, EMPTY_AND, EMPTY_OR, GEO_DISTANCE_ALIAS, SQL_DEFAULT, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildProjection, buildSelect, buildUpdate, buildWhere, createFilterVisitor, defaultValueForType, defaultValueToSqlLiteral, fillReplacePayload, finalizeParams, geoWindowFromControls, groupKeySql, insertManyColumns, jsonDollarPath, normalizeGeoPointValue, parseRegexString, queryNodeToSql, queryOpToSql, quotedJsonPathSegments, refActionToSql, renameGeoDistance, replaceColumnsFor, sqlStringLiteral, sqlTimeZoneLiteral, toSqlValue };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atscript/db-sql-tools",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.136",
|
|
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.
|
|
40
|
+
"@uniqu/core": "^0.1.11",
|
|
41
41
|
"unplugin-atscript": "^0.1.92"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
|
-
"@uniqu/core": "^0.1.
|
|
45
|
-
"@atscript/db": "^0.1.
|
|
44
|
+
"@uniqu/core": "^0.1.11",
|
|
45
|
+
"@atscript/db": "^0.1.136"
|
|
46
46
|
},
|
|
47
47
|
"scripts": {
|
|
48
48
|
"build": "vp pack",
|