@atscript/db-sql-tools 0.1.135 → 0.1.137
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 +190 -58
- package/dist/index.d.cts +71 -10
- package/dist/index.d.mts +71 -10
- package/dist/index.mjs +190 -60
- package/package.json +2 -2
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,14 +432,20 @@ const AGG_FN_SQL = {
|
|
|
348
432
|
min: "MIN",
|
|
349
433
|
max: "MAX"
|
|
350
434
|
};
|
|
351
|
-
/**
|
|
352
|
-
|
|
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) {
|
|
353
442
|
(0, _atscript_db_agg.assertAggregateFn)(fn, path);
|
|
354
|
-
return AGG_FN_SQL[fn]
|
|
443
|
+
return fn === "countDistinct" ? `COUNT(DISTINCT ${arg})` : `${AGG_FN_SQL[fn]}(${arg})`;
|
|
355
444
|
}
|
|
356
|
-
/** The bare aggregate call, e.g. `SUM("amount")` / `COUNT(*)`. */
|
|
445
|
+
/** The bare aggregate call, e.g. `SUM("amount")` / `COUNT(*)` / `COUNT(DISTINCT "region")`. */
|
|
357
446
|
function aggFnSql(dialect, expr) {
|
|
358
|
-
|
|
447
|
+
const field = expr.$field === "*" ? "*" : dialect.quoteIdentifier(expr.$field);
|
|
448
|
+
return renderAggCall(expr.$fn, field);
|
|
359
449
|
}
|
|
360
450
|
function buildAggExpr(dialect, expr) {
|
|
361
451
|
return `${aggFnSql(dialect, expr)} AS ${dialect.quoteIdentifier((0, _atscript_db_agg.resolveAlias)(expr))}`;
|
|
@@ -416,7 +506,7 @@ function groupKeySql(dialect, controls, key) {
|
|
|
416
506
|
* not allow a SELECT alias in HAVING (MySQL and SQLite tolerate it, so the
|
|
417
507
|
* expression form keeps all three identical). A calendar-bucket alias renders
|
|
418
508
|
* its bucket expression ({@link groupKeySql}), or its quoted alias when the
|
|
419
|
-
* dialect sets `SqlDialect.bucketAliasInHaving` (
|
|
509
|
+
* dialect sets `SqlDialect.bucketAliasInHaving` (`havingGroupRef`). Other keys
|
|
420
510
|
* (grouped columns) render as plain columns.
|
|
421
511
|
*/
|
|
422
512
|
function havingClause(dialect, controls) {
|
|
@@ -427,8 +517,8 @@ function havingClause(dialect, controls) {
|
|
|
427
517
|
const fragment = (0, _uniqu_core.walkFilter)(having, createFilterVisitor(dialect, { columnRef: (field) => {
|
|
428
518
|
const aggExpr = exprByAlias.get(field);
|
|
429
519
|
if (aggExpr) return aggExpr;
|
|
430
|
-
|
|
431
|
-
return
|
|
520
|
+
const bucket = controls.$select?.bucketByAlias(field);
|
|
521
|
+
return bucket ? havingGroupRef(dialect, bucketSql(dialect, bucket), field) : dialect.quoteIdentifier(field);
|
|
432
522
|
} }));
|
|
433
523
|
if (!fragment || fragment.sql === EMPTY_AND.sql) return void 0;
|
|
434
524
|
return {
|
|
@@ -509,6 +599,90 @@ function buildAggregateCount(dialect, table, where, controls) {
|
|
|
509
599
|
});
|
|
510
600
|
}
|
|
511
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
|
|
512
686
|
//#region src/sql-builder.ts
|
|
513
687
|
/**
|
|
514
688
|
* Builds an INSERT statement.
|
|
@@ -706,50 +880,6 @@ function buildProjection(dialect, select) {
|
|
|
706
880
|
}
|
|
707
881
|
return sql || "*";
|
|
708
882
|
}
|
|
709
|
-
/** Builds the SQL expression for a single aggregate column. */
|
|
710
|
-
function buildAggColExpr(dialect, c) {
|
|
711
|
-
return `${aggFnName(c.aggFn, c.viewColumn)}(${c.aggField === "*" ? "*" : `${dialect.quoteIdentifier(c.sourceTable)}.${dialect.quoteIdentifier(c.sourceColumn)}`})`;
|
|
712
|
-
}
|
|
713
|
-
/**
|
|
714
|
-
* Builds a CREATE VIEW statement from a view plan and column mappings.
|
|
715
|
-
*/
|
|
716
|
-
function buildCreateView(dialect, viewName, plan, columns, resolveFieldRef) {
|
|
717
|
-
const selectCols = columns.map((c) => {
|
|
718
|
-
if (c.aggFn) return `${buildAggColExpr(dialect, c)} AS ${dialect.quoteIdentifier(c.viewColumn)}`;
|
|
719
|
-
return `${dialect.quoteIdentifier(c.sourceTable)}.${dialect.quoteIdentifier(c.sourceColumn)} AS ${dialect.quoteIdentifier(c.viewColumn)}`;
|
|
720
|
-
}).join(", ");
|
|
721
|
-
let sql = `${dialect.createViewPrefix} ${dialect.quoteTable(viewName)} AS SELECT ${selectCols} FROM ${dialect.quoteIdentifier(plan.entryTable)}`;
|
|
722
|
-
for (const join of plan.joins) {
|
|
723
|
-
const onClause = queryNodeToSql(join.condition, resolveFieldRef);
|
|
724
|
-
sql += ` JOIN ${dialect.quoteIdentifier(join.targetTable)} ON ${onClause}`;
|
|
725
|
-
}
|
|
726
|
-
if (plan.filter) {
|
|
727
|
-
const whereClause = queryNodeToSql(plan.filter, resolveFieldRef);
|
|
728
|
-
sql += ` WHERE ${whereClause}`;
|
|
729
|
-
}
|
|
730
|
-
if (columns.some((c) => c.aggFn)) {
|
|
731
|
-
const dimensionCols = columns.filter((c) => !c.aggFn);
|
|
732
|
-
if (dimensionCols.length > 0) {
|
|
733
|
-
const groupByCols = dimensionCols.map((c) => `${dialect.quoteIdentifier(c.sourceTable)}.${dialect.quoteIdentifier(c.sourceColumn)}`).join(", ");
|
|
734
|
-
sql += ` GROUP BY ${groupByCols}`;
|
|
735
|
-
}
|
|
736
|
-
if (plan.having) {
|
|
737
|
-
const columnMap = /* @__PURE__ */ new Map();
|
|
738
|
-
for (const c of columns) columnMap.set(c.viewColumn, c);
|
|
739
|
-
const havingResolver = (ref) => {
|
|
740
|
-
if (!ref.type) {
|
|
741
|
-
const col = columnMap.get(ref.field);
|
|
742
|
-
if (col?.aggFn) return buildAggColExpr(dialect, col);
|
|
743
|
-
if (col) return `${dialect.quoteIdentifier(col.sourceTable)}.${dialect.quoteIdentifier(col.sourceColumn)}`;
|
|
744
|
-
}
|
|
745
|
-
return resolveFieldRef(ref);
|
|
746
|
-
};
|
|
747
|
-
const havingClause = queryNodeToSql(plan.having, havingResolver);
|
|
748
|
-
sql += ` HAVING ${havingClause}`;
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
return sql;
|
|
752
|
-
}
|
|
753
883
|
//#endregion
|
|
754
884
|
//#region src/regex.ts
|
|
755
885
|
/**
|
|
@@ -801,10 +931,12 @@ exports.finalizeParams = finalizeParams;
|
|
|
801
931
|
exports.geoWindowFromControls = geoWindowFromControls;
|
|
802
932
|
exports.groupKeySql = groupKeySql;
|
|
803
933
|
exports.insertManyColumns = insertManyColumns;
|
|
934
|
+
exports.jsonDollarPath = jsonDollarPath;
|
|
804
935
|
exports.normalizeGeoPointValue = normalizeGeoPointValue;
|
|
805
936
|
exports.parseRegexString = parseRegexString;
|
|
806
937
|
exports.queryNodeToSql = queryNodeToSql;
|
|
807
938
|
exports.queryOpToSql = queryOpToSql;
|
|
939
|
+
exports.quotedJsonPathSegments = quotedJsonPathSegments;
|
|
808
940
|
exports.refActionToSql = refActionToSql;
|
|
809
941
|
exports.renameGeoDistance = renameGeoDistance;
|
|
810
942
|
exports.replaceColumnsFor = replaceColumnsFor;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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
3
|
import { TDbAggregateFn } from "@atscript/db/agg";
|
|
4
4
|
|
|
@@ -48,9 +48,10 @@ interface SqlDialect {
|
|
|
48
48
|
*/
|
|
49
49
|
calendarBucket?(quotedCol: string, b: TResolvedBucket): string;
|
|
50
50
|
/**
|
|
51
|
-
* HAVING references a calendar bucket
|
|
52
|
-
*
|
|
53
|
-
*
|
|
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).
|
|
54
55
|
*
|
|
55
56
|
* MySQL needs this: the bucket expression reads the raw source column, which
|
|
56
57
|
* is not itself in GROUP BY (only the expression is), so HAVING rejects it
|
|
@@ -61,6 +62,27 @@ interface SqlDialect {
|
|
|
61
62
|
* Since 0.1.132.
|
|
62
63
|
*/
|
|
63
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;
|
|
64
86
|
/** e.g. 'CREATE VIEW IF NOT EXISTS' or 'CREATE OR REPLACE VIEW' */
|
|
65
87
|
createViewPrefix: string;
|
|
66
88
|
/** Returns a parameter placeholder for the given 1-based index. When absent, '?' is used. */
|
|
@@ -71,6 +93,18 @@ interface SqlDialect {
|
|
|
71
93
|
* (e.g. `$1, $2, ...` for PostgreSQL). No-op when `dialect.paramPlaceholder` is not set.
|
|
72
94
|
*/
|
|
73
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[];
|
|
74
108
|
declare const EMPTY_AND: TSqlFragment;
|
|
75
109
|
declare const EMPTY_OR: TSqlFragment;
|
|
76
110
|
//#endregion
|
|
@@ -143,6 +177,17 @@ declare function normalizeGeoPointValue(value: unknown): [number, number] | unde
|
|
|
143
177
|
/** Renames the internal distance alias to the public `$distance` field (in place). */
|
|
144
178
|
declare function renameGeoDistance(row: Record<string, unknown>): Record<string, unknown>;
|
|
145
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
|
|
146
191
|
//#region src/sql-builder.d.ts
|
|
147
192
|
/**
|
|
148
193
|
* Builds an INSERT statement.
|
|
@@ -227,14 +272,19 @@ declare function buildDelete(dialect: SqlDialect, table: string, where: TSqlFrag
|
|
|
227
272
|
* Builds a column projection (SELECT clause fields).
|
|
228
273
|
*/
|
|
229
274
|
declare function buildProjection(dialect: SqlDialect, select?: UniquSelect): string;
|
|
230
|
-
/**
|
|
231
|
-
* Builds a CREATE VIEW statement from a view plan and column mappings.
|
|
232
|
-
*/
|
|
233
|
-
declare function buildCreateView(dialect: SqlDialect, viewName: string, plan: TViewPlan, columns: TViewColumnMapping[], resolveFieldRef: (ref: AtscriptQueryFieldRef) => string): string;
|
|
234
275
|
//#endregion
|
|
235
276
|
//#region src/common.d.ts
|
|
236
277
|
/** Formats a string value as a SQL literal with single-quote escaping. */
|
|
237
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;
|
|
238
288
|
/**
|
|
239
289
|
* A calendar bucket's time zone as an inlined SQL string literal (`'Europe/Berlin'`).
|
|
240
290
|
*
|
|
@@ -260,11 +310,22 @@ declare function defaultValueToSqlLiteral(designType: string, value: string): st
|
|
|
260
310
|
declare const queryOpToSql: Record<string, string>;
|
|
261
311
|
/**
|
|
262
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.
|
|
263
320
|
*/
|
|
264
321
|
declare function queryNodeToSql(node: AtscriptQueryNode, resolveFieldRef: (ref: AtscriptQueryFieldRef) => string): string;
|
|
265
322
|
//#endregion
|
|
266
323
|
//#region src/agg.d.ts
|
|
267
|
-
|
|
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>>;
|
|
268
329
|
/**
|
|
269
330
|
* The SQL a `$groupBy` key renders as: a calendar-bucket alias renders the
|
|
270
331
|
* bucket EXPRESSION (`dialect.calendarBucket`), anything else the quoted
|
|
@@ -307,4 +368,4 @@ declare function parseRegexString(value: unknown): {
|
|
|
307
368
|
flags: string;
|
|
308
369
|
};
|
|
309
370
|
//#endregion
|
|
310
|
-
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,5 @@
|
|
|
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
3
|
import { TDbAggregateFn } from "@atscript/db/agg";
|
|
4
4
|
|
|
5
5
|
//#region src/dialect.d.ts
|
|
@@ -48,9 +48,10 @@ interface SqlDialect {
|
|
|
48
48
|
*/
|
|
49
49
|
calendarBucket?(quotedCol: string, b: TResolvedBucket): string;
|
|
50
50
|
/**
|
|
51
|
-
* HAVING references a calendar bucket
|
|
52
|
-
*
|
|
53
|
-
*
|
|
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).
|
|
54
55
|
*
|
|
55
56
|
* MySQL needs this: the bucket expression reads the raw source column, which
|
|
56
57
|
* is not itself in GROUP BY (only the expression is), so HAVING rejects it
|
|
@@ -61,6 +62,27 @@ interface SqlDialect {
|
|
|
61
62
|
* Since 0.1.132.
|
|
62
63
|
*/
|
|
63
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;
|
|
64
86
|
/** e.g. 'CREATE VIEW IF NOT EXISTS' or 'CREATE OR REPLACE VIEW' */
|
|
65
87
|
createViewPrefix: string;
|
|
66
88
|
/** Returns a parameter placeholder for the given 1-based index. When absent, '?' is used. */
|
|
@@ -71,6 +93,18 @@ interface SqlDialect {
|
|
|
71
93
|
* (e.g. `$1, $2, ...` for PostgreSQL). No-op when `dialect.paramPlaceholder` is not set.
|
|
72
94
|
*/
|
|
73
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[];
|
|
74
108
|
declare const EMPTY_AND: TSqlFragment;
|
|
75
109
|
declare const EMPTY_OR: TSqlFragment;
|
|
76
110
|
//#endregion
|
|
@@ -143,6 +177,17 @@ declare function normalizeGeoPointValue(value: unknown): [number, number] | unde
|
|
|
143
177
|
/** Renames the internal distance alias to the public `$distance` field (in place). */
|
|
144
178
|
declare function renameGeoDistance(row: Record<string, unknown>): Record<string, unknown>;
|
|
145
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
|
|
146
191
|
//#region src/sql-builder.d.ts
|
|
147
192
|
/**
|
|
148
193
|
* Builds an INSERT statement.
|
|
@@ -227,14 +272,19 @@ declare function buildDelete(dialect: SqlDialect, table: string, where: TSqlFrag
|
|
|
227
272
|
* Builds a column projection (SELECT clause fields).
|
|
228
273
|
*/
|
|
229
274
|
declare function buildProjection(dialect: SqlDialect, select?: UniquSelect): string;
|
|
230
|
-
/**
|
|
231
|
-
* Builds a CREATE VIEW statement from a view plan and column mappings.
|
|
232
|
-
*/
|
|
233
|
-
declare function buildCreateView(dialect: SqlDialect, viewName: string, plan: TViewPlan, columns: TViewColumnMapping[], resolveFieldRef: (ref: AtscriptQueryFieldRef) => string): string;
|
|
234
275
|
//#endregion
|
|
235
276
|
//#region src/common.d.ts
|
|
236
277
|
/** Formats a string value as a SQL literal with single-quote escaping. */
|
|
237
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;
|
|
238
288
|
/**
|
|
239
289
|
* A calendar bucket's time zone as an inlined SQL string literal (`'Europe/Berlin'`).
|
|
240
290
|
*
|
|
@@ -260,11 +310,22 @@ declare function defaultValueToSqlLiteral(designType: string, value: string): st
|
|
|
260
310
|
declare const queryOpToSql: Record<string, string>;
|
|
261
311
|
/**
|
|
262
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.
|
|
263
320
|
*/
|
|
264
321
|
declare function queryNodeToSql(node: AtscriptQueryNode, resolveFieldRef: (ref: AtscriptQueryFieldRef) => string): string;
|
|
265
322
|
//#endregion
|
|
266
323
|
//#region src/agg.d.ts
|
|
267
|
-
|
|
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>>;
|
|
268
329
|
/**
|
|
269
330
|
* The SQL a `$groupBy` key renders as: a calendar-bucket alias renders the
|
|
270
331
|
* bucket EXPRESSION (`dialect.calendarBucket`), anything else the quoted
|
|
@@ -307,4 +368,4 @@ declare function parseRegexString(value: unknown): {
|
|
|
307
368
|
flags: string;
|
|
308
369
|
};
|
|
309
370
|
//#endregion
|
|
310
|
-
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,5 +1,5 @@
|
|
|
1
1
|
import { BUCKET_UNITS, TIME_ZONE_NAME_RE, WEEK_STARTS, walkFilter } from "@uniqu/core";
|
|
2
|
-
import { DbError } from "@atscript/db";
|
|
2
|
+
import { DbError, isFieldRef } from "@atscript/db";
|
|
3
3
|
import { assertAggregateFn, resolveAlias } from "@atscript/db/agg";
|
|
4
4
|
//#region src/dialect.ts
|
|
5
5
|
/**
|
|
@@ -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,14 +431,20 @@ const AGG_FN_SQL = {
|
|
|
347
431
|
min: "MIN",
|
|
348
432
|
max: "MAX"
|
|
349
433
|
};
|
|
350
|
-
/**
|
|
351
|
-
|
|
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) {
|
|
352
441
|
assertAggregateFn(fn, path);
|
|
353
|
-
return AGG_FN_SQL[fn]
|
|
442
|
+
return fn === "countDistinct" ? `COUNT(DISTINCT ${arg})` : `${AGG_FN_SQL[fn]}(${arg})`;
|
|
354
443
|
}
|
|
355
|
-
/** The bare aggregate call, e.g. `SUM("amount")` / `COUNT(*)`. */
|
|
444
|
+
/** The bare aggregate call, e.g. `SUM("amount")` / `COUNT(*)` / `COUNT(DISTINCT "region")`. */
|
|
356
445
|
function aggFnSql(dialect, expr) {
|
|
357
|
-
|
|
446
|
+
const field = expr.$field === "*" ? "*" : dialect.quoteIdentifier(expr.$field);
|
|
447
|
+
return renderAggCall(expr.$fn, field);
|
|
358
448
|
}
|
|
359
449
|
function buildAggExpr(dialect, expr) {
|
|
360
450
|
return `${aggFnSql(dialect, expr)} AS ${dialect.quoteIdentifier(resolveAlias(expr))}`;
|
|
@@ -415,7 +505,7 @@ function groupKeySql(dialect, controls, key) {
|
|
|
415
505
|
* not allow a SELECT alias in HAVING (MySQL and SQLite tolerate it, so the
|
|
416
506
|
* expression form keeps all three identical). A calendar-bucket alias renders
|
|
417
507
|
* its bucket expression ({@link groupKeySql}), or its quoted alias when the
|
|
418
|
-
* dialect sets `SqlDialect.bucketAliasInHaving` (
|
|
508
|
+
* dialect sets `SqlDialect.bucketAliasInHaving` (`havingGroupRef`). Other keys
|
|
419
509
|
* (grouped columns) render as plain columns.
|
|
420
510
|
*/
|
|
421
511
|
function havingClause(dialect, controls) {
|
|
@@ -426,8 +516,8 @@ function havingClause(dialect, controls) {
|
|
|
426
516
|
const fragment = walkFilter(having, createFilterVisitor(dialect, { columnRef: (field) => {
|
|
427
517
|
const aggExpr = exprByAlias.get(field);
|
|
428
518
|
if (aggExpr) return aggExpr;
|
|
429
|
-
|
|
430
|
-
return
|
|
519
|
+
const bucket = controls.$select?.bucketByAlias(field);
|
|
520
|
+
return bucket ? havingGroupRef(dialect, bucketSql(dialect, bucket), field) : dialect.quoteIdentifier(field);
|
|
431
521
|
} }));
|
|
432
522
|
if (!fragment || fragment.sql === EMPTY_AND.sql) return void 0;
|
|
433
523
|
return {
|
|
@@ -508,6 +598,90 @@ function buildAggregateCount(dialect, table, where, controls) {
|
|
|
508
598
|
});
|
|
509
599
|
}
|
|
510
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
|
|
511
685
|
//#region src/sql-builder.ts
|
|
512
686
|
/**
|
|
513
687
|
* Builds an INSERT statement.
|
|
@@ -705,50 +879,6 @@ function buildProjection(dialect, select) {
|
|
|
705
879
|
}
|
|
706
880
|
return sql || "*";
|
|
707
881
|
}
|
|
708
|
-
/** Builds the SQL expression for a single aggregate column. */
|
|
709
|
-
function buildAggColExpr(dialect, c) {
|
|
710
|
-
return `${aggFnName(c.aggFn, c.viewColumn)}(${c.aggField === "*" ? "*" : `${dialect.quoteIdentifier(c.sourceTable)}.${dialect.quoteIdentifier(c.sourceColumn)}`})`;
|
|
711
|
-
}
|
|
712
|
-
/**
|
|
713
|
-
* Builds a CREATE VIEW statement from a view plan and column mappings.
|
|
714
|
-
*/
|
|
715
|
-
function buildCreateView(dialect, viewName, plan, columns, resolveFieldRef) {
|
|
716
|
-
const selectCols = columns.map((c) => {
|
|
717
|
-
if (c.aggFn) return `${buildAggColExpr(dialect, c)} AS ${dialect.quoteIdentifier(c.viewColumn)}`;
|
|
718
|
-
return `${dialect.quoteIdentifier(c.sourceTable)}.${dialect.quoteIdentifier(c.sourceColumn)} AS ${dialect.quoteIdentifier(c.viewColumn)}`;
|
|
719
|
-
}).join(", ");
|
|
720
|
-
let sql = `${dialect.createViewPrefix} ${dialect.quoteTable(viewName)} AS SELECT ${selectCols} FROM ${dialect.quoteIdentifier(plan.entryTable)}`;
|
|
721
|
-
for (const join of plan.joins) {
|
|
722
|
-
const onClause = queryNodeToSql(join.condition, resolveFieldRef);
|
|
723
|
-
sql += ` JOIN ${dialect.quoteIdentifier(join.targetTable)} ON ${onClause}`;
|
|
724
|
-
}
|
|
725
|
-
if (plan.filter) {
|
|
726
|
-
const whereClause = queryNodeToSql(plan.filter, resolveFieldRef);
|
|
727
|
-
sql += ` WHERE ${whereClause}`;
|
|
728
|
-
}
|
|
729
|
-
if (columns.some((c) => c.aggFn)) {
|
|
730
|
-
const dimensionCols = columns.filter((c) => !c.aggFn);
|
|
731
|
-
if (dimensionCols.length > 0) {
|
|
732
|
-
const groupByCols = dimensionCols.map((c) => `${dialect.quoteIdentifier(c.sourceTable)}.${dialect.quoteIdentifier(c.sourceColumn)}`).join(", ");
|
|
733
|
-
sql += ` GROUP BY ${groupByCols}`;
|
|
734
|
-
}
|
|
735
|
-
if (plan.having) {
|
|
736
|
-
const columnMap = /* @__PURE__ */ new Map();
|
|
737
|
-
for (const c of columns) columnMap.set(c.viewColumn, c);
|
|
738
|
-
const havingResolver = (ref) => {
|
|
739
|
-
if (!ref.type) {
|
|
740
|
-
const col = columnMap.get(ref.field);
|
|
741
|
-
if (col?.aggFn) return buildAggColExpr(dialect, col);
|
|
742
|
-
if (col) return `${dialect.quoteIdentifier(col.sourceTable)}.${dialect.quoteIdentifier(col.sourceColumn)}`;
|
|
743
|
-
}
|
|
744
|
-
return resolveFieldRef(ref);
|
|
745
|
-
};
|
|
746
|
-
const havingClause = queryNodeToSql(plan.having, havingResolver);
|
|
747
|
-
sql += ` HAVING ${havingClause}`;
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
return sql;
|
|
751
|
-
}
|
|
752
882
|
//#endregion
|
|
753
883
|
//#region src/regex.ts
|
|
754
884
|
/**
|
|
@@ -775,4 +905,4 @@ function parseRegexString(value) {
|
|
|
775
905
|
};
|
|
776
906
|
}
|
|
777
907
|
//#endregion
|
|
778
|
-
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.137",
|
|
4
4
|
"description": "Shared SQL builder utilities for @atscript database adapters.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"atscript",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
44
|
"@uniqu/core": "^0.1.11",
|
|
45
|
-
"@atscript/db": "^0.1.
|
|
45
|
+
"@atscript/db": "^0.1.137"
|
|
46
46
|
},
|
|
47
47
|
"scripts": {
|
|
48
48
|
"build": "vp pack",
|