@atscript/db-sql-tools 0.1.152 → 0.1.154
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 +157 -28
- package/dist/index.d.cts +125 -15
- package/dist/index.d.mts +125 -15
- package/dist/index.mjs +152 -30
- package/package.json +5 -5
package/dist/index.cjs
CHANGED
|
@@ -157,15 +157,53 @@ async function mapQueryErrors(dialect, fn, controls) {
|
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
159
|
/**
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
|
|
164
|
-
|
|
160
|
+
* The `$nulls` entry of one `ORDER BY` key (`controls.$nulls` is keyed like
|
|
161
|
+
* `controls.$sort`), or `undefined`.
|
|
162
|
+
* @since 0.1.153
|
|
163
|
+
*/
|
|
164
|
+
function nullsPlacementOf(nulls, key) {
|
|
165
|
+
if (!nulls) return void 0;
|
|
166
|
+
const map = nulls;
|
|
167
|
+
return Object.hasOwn(map, key) ? map[key] : void 0;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* One `ORDER BY` key with a requested NULL placement. Without `nulls`, the
|
|
171
|
+
* plain `<expr> ASC|DESC` (the engine's native placement). With it:
|
|
172
|
+
*
|
|
173
|
+
* - the engine's native placement already matches (NULL is the smallest
|
|
174
|
+
* value — `ASC` → first, `DESC` → last — unless
|
|
175
|
+
* {@link SqlDialect.nullsSortLargest}, where it is the opposite) → the plain
|
|
176
|
+
* key, so an index on `expr` still serves the order;
|
|
177
|
+
* - else `<expr> ASC|DESC NULLS FIRST|LAST` on a dialect with
|
|
178
|
+
* {@link SqlDialect.nullsPlacementSyntax} (or `nullsSortLargest`);
|
|
179
|
+
* - else (MySQL) a leading NULL-ness key: `(<expr> IS NULL) DESC, <expr> DIR`
|
|
180
|
+
* for `first`, `(<expr> IS NULL) ASC, <expr> DIR` for `last`.
|
|
181
|
+
*
|
|
182
|
+
* May render two comma-separated keys — only valid inside an `ORDER BY` list.
|
|
183
|
+
* `nullTest` renders the `IS NULL` operand when it must differ from `expr`
|
|
184
|
+
* (MySQL resolves a name inside an expression to a table column before a
|
|
185
|
+
* SELECT alias, so a grouped `ORDER BY` tests the alias's own expression).
|
|
186
|
+
* @since 0.1.153
|
|
187
|
+
*/
|
|
188
|
+
function nullsOrderSql(dialect, expr, desc, nulls, nullTest) {
|
|
189
|
+
const key = `${expr} ${desc ? "DESC" : "ASC"}`;
|
|
190
|
+
if (!nulls) return key;
|
|
191
|
+
const nativeFirst = desc === (dialect.nullsSortLargest === true);
|
|
192
|
+
if (nulls === "first" === nativeFirst) return key;
|
|
193
|
+
if (dialect.nullsPlacementSyntax || dialect.nullsSortLargest) return `${key} NULLS ${nulls === "first" ? "FIRST" : "LAST"}`;
|
|
194
|
+
return `(${nullTest ? nullTest() : expr} IS NULL) ${nulls === "first" ? "DESC" : "ASC"}, ${key}`;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* One `ORDER BY` key with the uniform "NULL is the smallest value" ordering
|
|
198
|
+
* (`ASC` → NULL first, `DESC` → NULL last) unless `nulls` asks for another
|
|
199
|
+
* placement — {@link nullsOrderSql}. Without `nulls`: `<expr> ASC` /
|
|
200
|
+
* `<expr> DESC`, plus `NULLS FIRST` / `NULLS LAST` on a dialect where NULL
|
|
201
|
+
* sorts largest ({@link SqlDialect.nullsSortLargest}). Shared by first-row
|
|
202
|
+
* joins, `first` / `last` aggregates and the grouped `ORDER BY`.
|
|
203
|
+
* @since 0.1.148 (`nulls` since 0.1.153)
|
|
165
204
|
*/
|
|
166
|
-
function orderKeySql(dialect, expr, desc) {
|
|
167
|
-
|
|
168
|
-
return `${expr} ${desc ? "DESC" : "ASC"}${nulls}`;
|
|
205
|
+
function orderKeySql(dialect, expr, desc, nulls, nullTest) {
|
|
206
|
+
return nullsOrderSql(dialect, expr, desc, nulls ?? (desc ? "last" : "first"), nullTest);
|
|
169
207
|
}
|
|
170
208
|
/**
|
|
171
209
|
* Each JSON path segment wrapped in double quotes (`"a"`), for the dialects'
|
|
@@ -468,6 +506,16 @@ function buildWhere(dialect, filter, opts) {
|
|
|
468
506
|
}) : getVisitor(dialect)) ?? EMPTY_AND;
|
|
469
507
|
}
|
|
470
508
|
//#endregion
|
|
509
|
+
//#region src/from-source.ts
|
|
510
|
+
/** The FROM text of `source`: the quoted table name, or the derived table. */
|
|
511
|
+
function fromSourceSql(dialect, source) {
|
|
512
|
+
return typeof source === "string" ? dialect.quoteTable(source) : source.sql;
|
|
513
|
+
}
|
|
514
|
+
/** The hint the SELECT directly reading `source` carries (`""` for a table). */
|
|
515
|
+
function fromSourceHint(source) {
|
|
516
|
+
return typeof source === "string" ? "" : source.hint;
|
|
517
|
+
}
|
|
518
|
+
//#endregion
|
|
471
519
|
//#region src/common.ts
|
|
472
520
|
/** Formats a string value as a SQL literal with single-quote escaping. */
|
|
473
521
|
function sqlStringLiteral(value) {
|
|
@@ -815,17 +863,24 @@ function rowColumns(controls) {
|
|
|
815
863
|
* WHERE moves inside unchanged, so the bind parameters keep their order.
|
|
816
864
|
*/
|
|
817
865
|
function aggSource(dialect, table, where, controls, withRows) {
|
|
818
|
-
const quotedTable = dialect
|
|
866
|
+
const quotedTable = fromSourceSql(dialect, table);
|
|
819
867
|
const firstLast = controls.$select?.firstLast;
|
|
820
868
|
const rowOrder = controls.$select?.rowOrder;
|
|
821
|
-
if (!withRows || !firstLast?.length || !rowOrder?.length) return
|
|
869
|
+
if (!withRows || !firstLast?.length || !rowOrder?.length) return {
|
|
870
|
+
hint: fromSourceHint(table),
|
|
871
|
+
sql: `FROM ${quotedTable} WHERE ${where.sql}`
|
|
872
|
+
};
|
|
822
873
|
const groupBy = controls.$groupBy;
|
|
823
874
|
const partition = groupBy?.length ? `PARTITION BY ${groupBy.map((key) => groupKeySql(dialect, controls, key)).join(", ")} ` : "";
|
|
824
|
-
const order = (reverse) => rowOrder.map((k) => orderKeySql(dialect, dialect.quoteIdentifier(k.column), k.desc !== reverse)).join(", ");
|
|
825
|
-
|
|
875
|
+
const order = (reverse) => rowOrder.map((k) => orderKeySql(dialect, dialect.quoteIdentifier(k.column), k.desc !== reverse, k.nulls && reverse ? k.nulls === "first" ? "last" : "first" : k.nulls)).join(", ");
|
|
876
|
+
const columns = [...rowColumns(controls).map((column) => dialect.quoteIdentifier(column)), ...firstLast.map((fl, i) => {
|
|
826
877
|
const col = dialect.quoteIdentifier(firstLastColumn(i));
|
|
827
878
|
return `FIRST_VALUE(${dialect.quoteIdentifier(fl.column)}) OVER (${partition}ORDER BY ${order(fl.fn === "last")}) AS ${col}`;
|
|
828
|
-
})]
|
|
879
|
+
})];
|
|
880
|
+
return {
|
|
881
|
+
hint: "",
|
|
882
|
+
sql: `FROM (SELECT ${fromSourceHint(table)}${columns.join(", ")} FROM ${quotedTable} WHERE ${where.sql}) AS ${dialect.quoteIdentifier(ROWS_ALIAS)}`
|
|
883
|
+
};
|
|
829
884
|
}
|
|
830
885
|
/**
|
|
831
886
|
* Builds a SELECT ... GROUP BY statement with aggregate functions.
|
|
@@ -847,7 +902,9 @@ function buildAggregateSelect(dialect, table, where, controls) {
|
|
|
847
902
|
aliasSql = aliasSqlMap(dialect, controls);
|
|
848
903
|
for (const alias of select.computedAliases) selectParts.push(`${aliasSql.get(alias)} AS ${dialect.quoteIdentifier(alias)}`);
|
|
849
904
|
} else for (const expr of select?.aggregates ?? []) selectParts.push(`${aggFnSql(dialect, select, expr)} AS ${dialect.quoteIdentifier((0, _atscript_db_agg.resolveAlias)(expr))}`);
|
|
850
|
-
|
|
905
|
+
const cols = selectParts.length > 0 ? selectParts.join(", ") : "*";
|
|
906
|
+
const source = aggSource(dialect, table, where, controls, true);
|
|
907
|
+
let sql = `SELECT ${source.hint}${cols} ${source.sql}${groupByClause(dialect, controls)}`;
|
|
851
908
|
const params = [...where.params];
|
|
852
909
|
const having = havingClause(dialect, controls, aliasSql);
|
|
853
910
|
if (having) {
|
|
@@ -855,8 +912,12 @@ function buildAggregateSelect(dialect, table, where, controls) {
|
|
|
855
912
|
params.push(...having.params);
|
|
856
913
|
}
|
|
857
914
|
if (controls.$sort) {
|
|
915
|
+
const nullTest = (col) => () => {
|
|
916
|
+
aliasSql ??= aliasSqlMap(dialect, controls);
|
|
917
|
+
return aliasSql.get(col) ?? dialect.quoteIdentifier(col);
|
|
918
|
+
};
|
|
858
919
|
const orderParts = [];
|
|
859
|
-
for (const [col, dir] of Object.entries(controls.$sort)) orderParts.push(orderKeySql(dialect, dialect.quoteIdentifier(col), dir === -1));
|
|
920
|
+
for (const [col, dir] of Object.entries(controls.$sort)) orderParts.push(orderKeySql(dialect, dialect.quoteIdentifier(col), dir === -1, nullsPlacementOf(controls.$nulls, col), nullTest(col)));
|
|
860
921
|
if (orderParts.length > 0) sql += ` ORDER BY ${orderParts.join(", ")}`;
|
|
861
922
|
}
|
|
862
923
|
if (controls.$limit !== void 0) {
|
|
@@ -891,7 +952,7 @@ function buildAggregateCount(dialect, table, where, controls) {
|
|
|
891
952
|
params: []
|
|
892
953
|
});
|
|
893
954
|
return finalizeParams(dialect, {
|
|
894
|
-
sql: `SELECT ${countCol} FROM ${dialect
|
|
955
|
+
sql: `SELECT ${fromSourceHint(table)}${countCol} FROM ${fromSourceSql(dialect, table)} WHERE ${where.sql}`,
|
|
895
956
|
params: where.params
|
|
896
957
|
});
|
|
897
958
|
}
|
|
@@ -900,7 +961,7 @@ function buildAggregateCount(dialect, table, where, controls) {
|
|
|
900
961
|
let inner = "COUNT(*)";
|
|
901
962
|
if (groupBy) inner = bucketSelectParts(dialect, controls).join(", ") || "1";
|
|
902
963
|
return finalizeParams(dialect, {
|
|
903
|
-
sql: `SELECT ${countCol} FROM (SELECT ${inner} ${from}${groupBy}${having?.sql ?? ""}) AS ${dialect.quoteIdentifier("_groups")}`,
|
|
964
|
+
sql: `SELECT ${countCol} FROM (SELECT ${from.hint}${inner} ${from.sql}${groupBy}${having?.sql ?? ""}) AS ${dialect.quoteIdentifier("_groups")}`,
|
|
904
965
|
params: [...where.params, ...having?.params ?? []]
|
|
905
966
|
});
|
|
906
967
|
}
|
|
@@ -1002,7 +1063,17 @@ function firstRowOn(dialect, join, target, condition, resolveFieldRef) {
|
|
|
1002
1063
|
return `${key} = (SELECT ${key} FROM ${target} WHERE ${condition} ORDER BY ${first.order.map(({ ref, desc }) => orderKeySql(dialect, resolveFieldRef(ref), desc)).join(", ")} LIMIT 1)`;
|
|
1003
1064
|
}
|
|
1004
1065
|
/**
|
|
1005
|
-
* Builds a CREATE VIEW statement from a view plan and column mappings
|
|
1066
|
+
* Builds a CREATE VIEW statement from a view plan and column mappings:
|
|
1067
|
+
* `<dialect.createViewPrefix> <view> AS <buildViewSelect(…)>`.
|
|
1068
|
+
*/
|
|
1069
|
+
function buildCreateView(dialect, viewName, plan, columns, resolveFieldRef) {
|
|
1070
|
+
return `${dialect.createViewPrefix} ${dialect.quoteTable(viewName)} AS ${buildViewSelect(dialect, plan, columns, resolveFieldRef)}`;
|
|
1071
|
+
}
|
|
1072
|
+
/**
|
|
1073
|
+
* The SELECT of a managed view — the body of its CREATE VIEW, and (since
|
|
1074
|
+
* 0.1.153) the inline definition a pruned view read selects from (with a
|
|
1075
|
+
* `AtscriptDbView.readPlan` variant's plan and columns), so the two can never
|
|
1076
|
+
* drift. Parameter-free: predicate literals are inlined.
|
|
1006
1077
|
*
|
|
1007
1078
|
* Joins render in declaration order — `JOIN` (inner, the default) or
|
|
1008
1079
|
* `LEFT JOIN` for `kind: "left"`; a `@db.alias` target renders as
|
|
@@ -1011,15 +1082,15 @@ function firstRowOn(dialect, join, target, condition, resolveFieldRef) {
|
|
|
1011
1082
|
* name for an aliased join); `resolveFieldRef` renders predicate refs (join
|
|
1012
1083
|
* ON, WHERE, HAVING fallbacks) as `"table"."column"`. The entry table and a
|
|
1013
1084
|
* join target may be views.
|
|
1085
|
+
* @since 0.1.153
|
|
1014
1086
|
*/
|
|
1015
|
-
function
|
|
1087
|
+
function buildViewSelect(dialect, plan, columns, resolveFieldRef) {
|
|
1016
1088
|
const columnByPath = new Map(columns.map((c) => [c.viewPath, c]));
|
|
1017
1089
|
const hasAggregates = columns.some((c) => c.aggFn);
|
|
1018
1090
|
const computedSql = /* @__PURE__ */ new Map();
|
|
1019
1091
|
/** A column's SELECT expression: computed, aggregate or plain source. */
|
|
1020
1092
|
const columnExpr = (c) => c.expr !== void 0 ? viewComputeExpr(dialect, c, columnByPath, resolveFieldRef, hasAggregates, computedSql) : c.aggFn ? viewAggExpr(dialect, c, resolveFieldRef) : viewSourceExpr(dialect, c);
|
|
1021
|
-
|
|
1022
|
-
let sql = `${dialect.createViewPrefix} ${dialect.quoteTable(viewName)} AS SELECT ${selectCols} FROM ${dialect.quoteIdentifier(plan.entryTable)}`;
|
|
1093
|
+
let sql = `SELECT ${columns.map((c) => `${columnExpr(c)} AS ${dialect.quoteIdentifier(c.viewColumn)}`).join(", ")} FROM ${dialect.quoteIdentifier(plan.entryTable)}`;
|
|
1023
1094
|
for (const join of plan.joins) {
|
|
1024
1095
|
const condition = queryNodeToSql(join.condition, resolveFieldRef);
|
|
1025
1096
|
const keyword = join.kind === "left" ? "LEFT JOIN" : "JOIN";
|
|
@@ -1172,11 +1243,13 @@ function buildInsertMany(dialect, table, rows, columns = insertManyColumns(rows)
|
|
|
1172
1243
|
}
|
|
1173
1244
|
/**
|
|
1174
1245
|
* Builds a SELECT statement with optional sort, limit, offset, projection.
|
|
1246
|
+
* `table` may be a derived source (a pruned view read, since 0.1.153).
|
|
1175
1247
|
*/
|
|
1176
1248
|
function buildSelect(dialect, table, where, controls) {
|
|
1177
|
-
|
|
1249
|
+
const cols = buildProjection(dialect, controls?.$select);
|
|
1250
|
+
let sql = `SELECT ${fromSourceHint(table)}${cols} FROM ${fromSourceSql(dialect, table)} WHERE ${where.sql}`;
|
|
1178
1251
|
const params = [...where.params];
|
|
1179
|
-
const orderBy = orderByList(dialect, controls?.$sort);
|
|
1252
|
+
const orderBy = orderByList(dialect, controls?.$sort, controls?.$nulls);
|
|
1180
1253
|
if (orderBy) sql += ` ORDER BY ${orderBy}`;
|
|
1181
1254
|
if (controls?.$limit !== void 0) {
|
|
1182
1255
|
sql += ` LIMIT ?`;
|
|
@@ -1192,11 +1265,17 @@ function buildSelect(dialect, table, where, controls) {
|
|
|
1192
1265
|
params
|
|
1193
1266
|
});
|
|
1194
1267
|
}
|
|
1195
|
-
/**
|
|
1196
|
-
|
|
1268
|
+
/**
|
|
1269
|
+
* `"col" ASC, "other" DESC` of a physical `$sort` — `""` when it orders
|
|
1270
|
+
* nothing. A key with a `$nulls` entry places NULL as asked
|
|
1271
|
+
* ({@link nullsOrderSql}); `prefix` is prepended verbatim to each quoted
|
|
1272
|
+
* column (`t.` → `t."col"`).
|
|
1273
|
+
* @since 0.1.153 (exported)
|
|
1274
|
+
*/
|
|
1275
|
+
function orderByList(dialect, sort, nulls, prefix = "") {
|
|
1197
1276
|
if (!sort) return "";
|
|
1198
1277
|
const parts = [];
|
|
1199
|
-
for (const [col, dir] of Object.entries(sort)) parts.push(
|
|
1278
|
+
for (const [col, dir] of Object.entries(sort)) parts.push(nullsOrderSql(dialect, prefix + dialect.quoteIdentifier(col), dir === -1, nullsPlacementOf(nulls, col)));
|
|
1200
1279
|
return parts.join(", ");
|
|
1201
1280
|
}
|
|
1202
1281
|
/**
|
|
@@ -1217,9 +1296,9 @@ const PARTITION_ROW_NUMBER_ALIAS = "__atscript_rn";
|
|
|
1217
1296
|
*/
|
|
1218
1297
|
function buildPartitionedSelect(dialect, table, where, controls, partitionBy) {
|
|
1219
1298
|
const rn = dialect.quoteIdentifier(PARTITION_ROW_NUMBER_ALIAS);
|
|
1220
|
-
const orderBy = orderByList(dialect, controls.$sort);
|
|
1299
|
+
const orderBy = orderByList(dialect, controls.$sort, controls.$nulls);
|
|
1221
1300
|
const window = `PARTITION BY ${partitionBy.map((col) => dialect.quoteIdentifier(col)).join(", ")}` + (orderBy ? ` ORDER BY ${orderBy}` : "");
|
|
1222
|
-
const inner = `SELECT ${buildProjection(dialect, controls.$select)}, ROW_NUMBER() OVER (${window}) AS ${rn} FROM ${dialect
|
|
1301
|
+
const inner = `SELECT ${fromSourceHint(table)}${buildProjection(dialect, controls.$select)}, ROW_NUMBER() OVER (${window}) AS ${rn} FROM ${fromSourceSql(dialect, table)} WHERE ${where.sql}`;
|
|
1223
1302
|
const skip = controls.$skip ?? 0;
|
|
1224
1303
|
const limit = controls.$limit;
|
|
1225
1304
|
const params = [...where.params, skip];
|
|
@@ -1626,6 +1705,49 @@ function parseRegexString(value) {
|
|
|
1626
1705
|
};
|
|
1627
1706
|
}
|
|
1628
1707
|
//#endregion
|
|
1708
|
+
//#region src/view-read.ts
|
|
1709
|
+
/** Rendered pruned definitions per read variant (variants are memoised by their view). */
|
|
1710
|
+
const rendered = /* @__PURE__ */ new WeakMap();
|
|
1711
|
+
/**
|
|
1712
|
+
* The FROM source of a read of `readable` (the adapter's registered table or
|
|
1713
|
+
* view): `tableName` unchanged, except for a managed view whose read does
|
|
1714
|
+
* not need some of its LEFT joins (`AtscriptDbView.readPlan` over the
|
|
1715
|
+
* columns `query` reads — {@link queryReadColumns}): then the view's own
|
|
1716
|
+
* definition without those joins ({@link buildViewSelect}), aliased as the
|
|
1717
|
+
* bare view name so every column reference — and the correlation of a
|
|
1718
|
+
* relational predicate — stays valid. A column of a dropped join is absent
|
|
1719
|
+
* from the derived table, so a reference the collector missed fails with
|
|
1720
|
+
* "unknown column" rather than reading wrong data.
|
|
1721
|
+
*
|
|
1722
|
+
* Never pruned: a table, an external view, a view read under another name
|
|
1723
|
+
* than its own (`tableName` schema-qualified — its definition's unqualified
|
|
1724
|
+
* names resolve against that schema, the derived table's against the
|
|
1725
|
+
* connection's — or renamed by an adapter subclass), or anything `readPlan`
|
|
1726
|
+
* keeps whole.
|
|
1727
|
+
* @since 0.1.153
|
|
1728
|
+
*/
|
|
1729
|
+
function viewReadSource(dialect, readable, tableName, query, kind, partitionBy) {
|
|
1730
|
+
if (!readable.isView || tableName !== readable.tableName) return tableName;
|
|
1731
|
+
const view = readable;
|
|
1732
|
+
const variant = view.readPlan((0, _atscript_db.queryReadColumns)(query, kind, partitionBy));
|
|
1733
|
+
if (!variant) return tableName;
|
|
1734
|
+
let byDialect = rendered.get(variant);
|
|
1735
|
+
if (!byDialect) {
|
|
1736
|
+
byDialect = /* @__PURE__ */ new Map();
|
|
1737
|
+
rendered.set(variant, byDialect);
|
|
1738
|
+
}
|
|
1739
|
+
let source = byDialect.get(dialect);
|
|
1740
|
+
if (!source) {
|
|
1741
|
+
const qi = (name) => dialect.quoteIdentifier(name);
|
|
1742
|
+
source = {
|
|
1743
|
+
sql: `(${buildViewSelect(dialect, variant.plan, variant.columns, (ref) => view.resolveFieldRef(ref, qi))}) AS ${qi(readable.tableName)}`,
|
|
1744
|
+
hint: dialect.derivedMergeHint ? `${dialect.derivedMergeHint(readable.tableName)} ` : ""
|
|
1745
|
+
};
|
|
1746
|
+
byDialect.set(dialect, source);
|
|
1747
|
+
}
|
|
1748
|
+
return source;
|
|
1749
|
+
}
|
|
1750
|
+
//#endregion
|
|
1629
1751
|
exports.AGG_FN_SQL = AGG_FN_SQL;
|
|
1630
1752
|
exports.EMPTY_AND = EMPTY_AND;
|
|
1631
1753
|
exports.EMPTY_OR = EMPTY_OR;
|
|
@@ -1650,6 +1772,7 @@ exports.buildSelect = buildSelect;
|
|
|
1650
1772
|
exports.buildUpdate = buildUpdate;
|
|
1651
1773
|
exports.buildVectorSearchCount = buildVectorSearchCount;
|
|
1652
1774
|
exports.buildVectorSearchSelect = buildVectorSearchSelect;
|
|
1775
|
+
exports.buildViewSelect = buildViewSelect;
|
|
1653
1776
|
exports.buildWhere = buildWhere;
|
|
1654
1777
|
exports.chunkInsertRows = chunkInsertRows;
|
|
1655
1778
|
exports.createFilterVisitor = createFilterVisitor;
|
|
@@ -1659,14 +1782,19 @@ exports.derivedColumnExpr = derivedColumnExpr;
|
|
|
1659
1782
|
exports.fillReplacePayload = fillReplacePayload;
|
|
1660
1783
|
exports.finalizeParams = finalizeParams;
|
|
1661
1784
|
exports.foreignKeySql = foreignKeySql;
|
|
1785
|
+
exports.fromSourceHint = fromSourceHint;
|
|
1786
|
+
exports.fromSourceSql = fromSourceSql;
|
|
1662
1787
|
exports.geoWindowFromControls = geoWindowFromControls;
|
|
1663
1788
|
exports.groupKeySql = groupKeySql;
|
|
1664
1789
|
exports.insertManyColumns = insertManyColumns;
|
|
1665
1790
|
exports.jsonDollarPath = jsonDollarPath;
|
|
1666
1791
|
exports.mapQueryErrors = mapQueryErrors;
|
|
1667
1792
|
exports.normalizeGeoPointValue = normalizeGeoPointValue;
|
|
1793
|
+
exports.nullsOrderSql = nullsOrderSql;
|
|
1794
|
+
exports.nullsPlacementOf = nullsPlacementOf;
|
|
1668
1795
|
exports.numericOutOfRangeError = numericOutOfRangeError;
|
|
1669
1796
|
exports.orFragment = orFragment;
|
|
1797
|
+
exports.orderByList = orderByList;
|
|
1670
1798
|
exports.orderKeySql = orderKeySql;
|
|
1671
1799
|
exports.parseRegexString = parseRegexString;
|
|
1672
1800
|
exports.queryNodeToSql = queryNodeToSql;
|
|
@@ -1681,3 +1809,4 @@ exports.sqlTimeZoneLiteral = sqlTimeZoneLiteral;
|
|
|
1681
1809
|
exports.stripPartitionRowNumber = stripPartitionRowNumber;
|
|
1682
1810
|
exports.toSqlValue = toSqlValue;
|
|
1683
1811
|
exports.vectorDistanceSource = vectorDistanceSource;
|
|
1812
|
+
exports.viewReadSource = viewReadSource;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { AtscriptExprNode, AtscriptQueryFieldRef, AtscriptQueryNode, DbControls, DbError, TDbDefaultFn, TDbFieldMeta, TDbForeignKey, TDbReferentialAction, TFieldOps, TResolvedBucket, TViewColumnMapping, TViewJsonType, TViewPlan, UniquSelect } from "@atscript/db";
|
|
2
|
-
import { FilterExpr, FilterVisitor } from "@uniqu/core";
|
|
1
|
+
import { AtscriptExprNode, AtscriptQueryFieldRef, AtscriptQueryNode, DbControls, DbError, DbQuery, TDbDefaultFn, TDbFieldMeta, TDbForeignKey, TDbReferentialAction, TFieldOps, TReadColumnsKind, TResolvedBucket, TViewColumnMapping, TViewJsonType, TViewPlan, UniquSelect } from "@atscript/db";
|
|
2
|
+
import { FilterExpr, FilterVisitor, NullsPlacement } from "@uniqu/core";
|
|
3
3
|
import { TDbAggregateFn } from "@atscript/db/agg";
|
|
4
4
|
|
|
5
5
|
//#region src/dialect.d.ts
|
|
@@ -19,6 +19,14 @@ interface SqlDialect {
|
|
|
19
19
|
quoteTable(name: string): string;
|
|
20
20
|
/** SQL literal for unlimited LIMIT (SQLite: '-1', MySQL: '18446744073709551615') */
|
|
21
21
|
unlimitedLimit: string;
|
|
22
|
+
/**
|
|
23
|
+
* The optimizer hint asking the engine to MERGE the derived table `alias`
|
|
24
|
+
* into the SELECT reading it (MySQL: `/*+ MERGE(\`alias\`) *\/`) — carried
|
|
25
|
+
* by pruned view reads (`viewReadSource`), so the inline definition merges
|
|
26
|
+
* even in a session with `derived_merge=off`. Omitted: no hint.
|
|
27
|
+
* @since 0.1.153
|
|
28
|
+
*/
|
|
29
|
+
derivedMergeHint?(alias: string): string;
|
|
22
30
|
/** Convert JS value to SQL-bindable param for DML */
|
|
23
31
|
toValue(value: unknown): unknown;
|
|
24
32
|
/** Convert JS value to SQL-bindable param for filters (lighter) */
|
|
@@ -142,6 +150,14 @@ interface SqlDialect {
|
|
|
142
150
|
* @since 0.1.147
|
|
143
151
|
*/
|
|
144
152
|
nullsSortLargest?: boolean;
|
|
153
|
+
/**
|
|
154
|
+
* `true` when the engine accepts `NULLS FIRST` / `NULLS LAST` on an
|
|
155
|
+
* `ORDER BY` key (PostgreSQL, SQLite ≥ 3.30). Without it a requested NULL
|
|
156
|
+
* placement that differs from the native one renders as an extra
|
|
157
|
+
* `(<expr> IS NULL)` key ({@link nullsOrderSql}).
|
|
158
|
+
* @since 0.1.153
|
|
159
|
+
*/
|
|
160
|
+
nullsPlacementSyntax?: boolean;
|
|
145
161
|
/** e.g. 'CREATE VIEW IF NOT EXISTS' or 'CREATE OR REPLACE VIEW' */
|
|
146
162
|
createViewPrefix: string;
|
|
147
163
|
/** Returns a parameter placeholder for the given 1-based index. When absent, '?' is used. */
|
|
@@ -169,13 +185,41 @@ declare function finalizeParams(dialect: SqlDialect, fragment: TSqlFragment): TS
|
|
|
169
185
|
*/
|
|
170
186
|
declare function mapQueryErrors<R>(dialect: SqlDialect, fn: () => Promise<R>, controls?: DbControls): Promise<R>;
|
|
171
187
|
/**
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
|
|
176
|
-
|
|
188
|
+
* The `$nulls` entry of one `ORDER BY` key (`controls.$nulls` is keyed like
|
|
189
|
+
* `controls.$sort`), or `undefined`.
|
|
190
|
+
* @since 0.1.153
|
|
191
|
+
*/
|
|
192
|
+
declare function nullsPlacementOf(nulls: DbControls["$nulls"], key: string): NullsPlacement | undefined;
|
|
193
|
+
/**
|
|
194
|
+
* One `ORDER BY` key with a requested NULL placement. Without `nulls`, the
|
|
195
|
+
* plain `<expr> ASC|DESC` (the engine's native placement). With it:
|
|
196
|
+
*
|
|
197
|
+
* - the engine's native placement already matches (NULL is the smallest
|
|
198
|
+
* value — `ASC` → first, `DESC` → last — unless
|
|
199
|
+
* {@link SqlDialect.nullsSortLargest}, where it is the opposite) → the plain
|
|
200
|
+
* key, so an index on `expr` still serves the order;
|
|
201
|
+
* - else `<expr> ASC|DESC NULLS FIRST|LAST` on a dialect with
|
|
202
|
+
* {@link SqlDialect.nullsPlacementSyntax} (or `nullsSortLargest`);
|
|
203
|
+
* - else (MySQL) a leading NULL-ness key: `(<expr> IS NULL) DESC, <expr> DIR`
|
|
204
|
+
* for `first`, `(<expr> IS NULL) ASC, <expr> DIR` for `last`.
|
|
205
|
+
*
|
|
206
|
+
* May render two comma-separated keys — only valid inside an `ORDER BY` list.
|
|
207
|
+
* `nullTest` renders the `IS NULL` operand when it must differ from `expr`
|
|
208
|
+
* (MySQL resolves a name inside an expression to a table column before a
|
|
209
|
+
* SELECT alias, so a grouped `ORDER BY` tests the alias's own expression).
|
|
210
|
+
* @since 0.1.153
|
|
211
|
+
*/
|
|
212
|
+
declare function nullsOrderSql(dialect: SqlDialect, expr: string, desc: boolean, nulls?: NullsPlacement, nullTest?: () => string): string;
|
|
213
|
+
/**
|
|
214
|
+
* One `ORDER BY` key with the uniform "NULL is the smallest value" ordering
|
|
215
|
+
* (`ASC` → NULL first, `DESC` → NULL last) unless `nulls` asks for another
|
|
216
|
+
* placement — {@link nullsOrderSql}. Without `nulls`: `<expr> ASC` /
|
|
217
|
+
* `<expr> DESC`, plus `NULLS FIRST` / `NULLS LAST` on a dialect where NULL
|
|
218
|
+
* sorts largest ({@link SqlDialect.nullsSortLargest}). Shared by first-row
|
|
219
|
+
* joins, `first` / `last` aggregates and the grouped `ORDER BY`.
|
|
220
|
+
* @since 0.1.148 (`nulls` since 0.1.153)
|
|
177
221
|
*/
|
|
178
|
-
declare function orderKeySql(dialect: SqlDialect, expr: string, desc: boolean): string;
|
|
222
|
+
declare function orderKeySql(dialect: SqlDialect, expr: string, desc: boolean, nulls?: NullsPlacement, nullTest?: () => string): string;
|
|
179
223
|
/**
|
|
180
224
|
* Each JSON path segment wrapped in double quotes (`"a"`), for the dialects'
|
|
181
225
|
* {@link SqlDialect.jsonExtract} path literals (`'$."a"."b"'`, `'{"a","b"}'`).
|
|
@@ -394,9 +438,39 @@ declare function buildVectorSearchCount(dialect: SqlDialect, source: TSqlFragmen
|
|
|
394
438
|
residual?: TSqlFragment;
|
|
395
439
|
}): TSqlFragment;
|
|
396
440
|
//#endregion
|
|
441
|
+
//#region src/from-source.d.ts
|
|
442
|
+
/**
|
|
443
|
+
* A pre-rendered derived table — `(<select>) AS "alias"` — read in place of
|
|
444
|
+
* a table name, plus the optimizer hint the SELECT that reads it carries
|
|
445
|
+
* (`/*+ MERGE(…) *\/ `, or `""`).
|
|
446
|
+
* @since 0.1.153
|
|
447
|
+
*/
|
|
448
|
+
interface TSqlDerivedSource {
|
|
449
|
+
readonly sql: string;
|
|
450
|
+
readonly hint: string;
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* The FROM source of a read builder: a table / view name (quoted by the
|
|
454
|
+
* dialect) or a {@link TSqlDerivedSource}.
|
|
455
|
+
* @since 0.1.153
|
|
456
|
+
*/
|
|
457
|
+
type TSqlFromSource = string | TSqlDerivedSource;
|
|
458
|
+
/** The FROM text of `source`: the quoted table name, or the derived table. */
|
|
459
|
+
declare function fromSourceSql(dialect: SqlDialect, source: TSqlFromSource): string;
|
|
460
|
+
/** The hint the SELECT directly reading `source` carries (`""` for a table). */
|
|
461
|
+
declare function fromSourceHint(source: TSqlFromSource): string;
|
|
462
|
+
//#endregion
|
|
397
463
|
//#region src/view-builder.d.ts
|
|
398
464
|
/**
|
|
399
|
-
* Builds a CREATE VIEW statement from a view plan and column mappings
|
|
465
|
+
* Builds a CREATE VIEW statement from a view plan and column mappings:
|
|
466
|
+
* `<dialect.createViewPrefix> <view> AS <buildViewSelect(…)>`.
|
|
467
|
+
*/
|
|
468
|
+
declare function buildCreateView(dialect: SqlDialect, viewName: string, plan: TViewPlan, columns: TViewColumnMapping[], resolveFieldRef: (ref: AtscriptQueryFieldRef) => string): string;
|
|
469
|
+
/**
|
|
470
|
+
* The SELECT of a managed view — the body of its CREATE VIEW, and (since
|
|
471
|
+
* 0.1.153) the inline definition a pruned view read selects from (with a
|
|
472
|
+
* `AtscriptDbView.readPlan` variant's plan and columns), so the two can never
|
|
473
|
+
* drift. Parameter-free: predicate literals are inlined.
|
|
400
474
|
*
|
|
401
475
|
* Joins render in declaration order — `JOIN` (inner, the default) or
|
|
402
476
|
* `LEFT JOIN` for `kind: "left"`; a `@db.alias` target renders as
|
|
@@ -405,8 +479,9 @@ declare function buildVectorSearchCount(dialect: SqlDialect, source: TSqlFragmen
|
|
|
405
479
|
* name for an aliased join); `resolveFieldRef` renders predicate refs (join
|
|
406
480
|
* ON, WHERE, HAVING fallbacks) as `"table"."column"`. The entry table and a
|
|
407
481
|
* join target may be views.
|
|
482
|
+
* @since 0.1.153
|
|
408
483
|
*/
|
|
409
|
-
declare function
|
|
484
|
+
declare function buildViewSelect(dialect: SqlDialect, plan: TViewPlan, columns: TViewColumnMapping[], resolveFieldRef: (ref: AtscriptQueryFieldRef) => string): string;
|
|
410
485
|
//#endregion
|
|
411
486
|
//#region src/sql-builder.d.ts
|
|
412
487
|
/**
|
|
@@ -456,8 +531,17 @@ declare function chunkInsertRows(rows: readonly Record<string, unknown>[], maxPa
|
|
|
456
531
|
declare function buildInsertMany(dialect: SqlDialect, table: string, rows: readonly Record<string, unknown>[], columns?: readonly string[]): TSqlFragment;
|
|
457
532
|
/**
|
|
458
533
|
* Builds a SELECT statement with optional sort, limit, offset, projection.
|
|
534
|
+
* `table` may be a derived source (a pruned view read, since 0.1.153).
|
|
459
535
|
*/
|
|
460
|
-
declare function buildSelect(dialect: SqlDialect, table:
|
|
536
|
+
declare function buildSelect(dialect: SqlDialect, table: TSqlFromSource, where: TSqlFragment, controls?: DbControls): TSqlFragment;
|
|
537
|
+
/**
|
|
538
|
+
* `"col" ASC, "other" DESC` of a physical `$sort` — `""` when it orders
|
|
539
|
+
* nothing. A key with a `$nulls` entry places NULL as asked
|
|
540
|
+
* ({@link nullsOrderSql}); `prefix` is prepended verbatim to each quoted
|
|
541
|
+
* column (`t.` → `t."col"`).
|
|
542
|
+
* @since 0.1.153 (exported)
|
|
543
|
+
*/
|
|
544
|
+
declare function orderByList(dialect: SqlDialect, sort: DbControls["$sort"], nulls?: DbControls["$nulls"], prefix?: string): string;
|
|
461
545
|
/**
|
|
462
546
|
* The row-number column {@link buildPartitionedSelect} adds to every row;
|
|
463
547
|
* {@link stripPartitionRowNumber} removes it from the result rows.
|
|
@@ -474,7 +558,7 @@ declare const PARTITION_ROW_NUMBER_ALIAS = "__atscript_rn";
|
|
|
474
558
|
* SQLite ≥ 3.25, MySQL ≥ 8.0 or MariaDB ≥ 10.2.
|
|
475
559
|
* @since 0.1.147
|
|
476
560
|
*/
|
|
477
|
-
declare function buildPartitionedSelect(dialect: SqlDialect, table:
|
|
561
|
+
declare function buildPartitionedSelect(dialect: SqlDialect, table: TSqlFromSource, where: TSqlFragment, controls: DbControls, partitionBy: readonly string[]): TSqlFragment;
|
|
478
562
|
/** Removes {@link PARTITION_ROW_NUMBER_ALIAS} from rows read by {@link buildPartitionedSelect}. */
|
|
479
563
|
declare function stripPartitionRowNumber<R extends Record<string, unknown>>(rows: R[]): R[];
|
|
480
564
|
/**
|
|
@@ -641,7 +725,7 @@ declare function groupKeySql(dialect: SqlDialect, controls: DbControls, key: str
|
|
|
641
725
|
* parameter-free, so the bind parameters are exactly those of the same query
|
|
642
726
|
* without buckets (WHERE, HAVING, LIMIT, OFFSET).
|
|
643
727
|
*/
|
|
644
|
-
declare function buildAggregateSelect(dialect: SqlDialect, table:
|
|
728
|
+
declare function buildAggregateSelect(dialect: SqlDialect, table: TSqlFromSource, where: TSqlFragment, controls: DbControls): TSqlFragment;
|
|
645
729
|
/**
|
|
646
730
|
* Builds a COUNT query for the number of distinct groups — the groups that
|
|
647
731
|
* survive `$having` when one is given (the same predicate the row query
|
|
@@ -650,7 +734,7 @@ declare function buildAggregateSelect(dialect: SqlDialect, table: string, where:
|
|
|
650
734
|
* `first` / `last` value (or an expression over one): only then the window
|
|
651
735
|
* derived table is built.
|
|
652
736
|
*/
|
|
653
|
-
declare function buildAggregateCount(dialect: SqlDialect, table:
|
|
737
|
+
declare function buildAggregateCount(dialect: SqlDialect, table: TSqlFromSource, where: TSqlFragment, controls: DbControls): TSqlFragment;
|
|
654
738
|
//#endregion
|
|
655
739
|
//#region src/regex.d.ts
|
|
656
740
|
/**
|
|
@@ -665,4 +749,30 @@ declare function parseRegexString(value: unknown): {
|
|
|
665
749
|
flags: string;
|
|
666
750
|
};
|
|
667
751
|
//#endregion
|
|
668
|
-
|
|
752
|
+
//#region src/view-read.d.ts
|
|
753
|
+
/** The structural view surface {@link viewReadSource} needs from an adapter's readable. */
|
|
754
|
+
interface TReadableLike {
|
|
755
|
+
readonly isView: boolean;
|
|
756
|
+
readonly tableName: string;
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* The FROM source of a read of `readable` (the adapter's registered table or
|
|
760
|
+
* view): `tableName` unchanged, except for a managed view whose read does
|
|
761
|
+
* not need some of its LEFT joins (`AtscriptDbView.readPlan` over the
|
|
762
|
+
* columns `query` reads — {@link queryReadColumns}): then the view's own
|
|
763
|
+
* definition without those joins ({@link buildViewSelect}), aliased as the
|
|
764
|
+
* bare view name so every column reference — and the correlation of a
|
|
765
|
+
* relational predicate — stays valid. A column of a dropped join is absent
|
|
766
|
+
* from the derived table, so a reference the collector missed fails with
|
|
767
|
+
* "unknown column" rather than reading wrong data.
|
|
768
|
+
*
|
|
769
|
+
* Never pruned: a table, an external view, a view read under another name
|
|
770
|
+
* than its own (`tableName` schema-qualified — its definition's unqualified
|
|
771
|
+
* names resolve against that schema, the derived table's against the
|
|
772
|
+
* connection's — or renamed by an adapter subclass), or anything `readPlan`
|
|
773
|
+
* keeps whole.
|
|
774
|
+
* @since 0.1.153
|
|
775
|
+
*/
|
|
776
|
+
declare function viewReadSource(dialect: SqlDialect, readable: TReadableLike, tableName: string, query: DbQuery, kind: TReadColumnsKind, partitionBy?: readonly string[]): TSqlFromSource;
|
|
777
|
+
//#endregion
|
|
778
|
+
export { AGG_FN_SQL, EMPTY_AND, EMPTY_OR, GEO_DISTANCE_ALIAS, InsertSqlCache, PARTITION_ROW_NUMBER_ALIAS, SEARCH_SOURCE_ALIAS, SQL_DEFAULT, type SqlDialect, type TArithFailure, type TFilterVisitorOptions, type TGeoCircle, type TGeoSearchControls, type TGeoWindow, type TReplaceColumn, type TSqlDerivedSource, type TSqlFragment, type TSqlFromSource, VECTOR_DISTANCE_ALIAS, arithOverflowError, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildPartitionedSelect, buildProjection, buildSelect, buildUpdate, buildVectorSearchCount, buildVectorSearchSelect, buildViewSelect, buildWhere, chunkInsertRows, createFilterVisitor, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, finalizeParams, foreignKeySql, fromSourceHint, fromSourceSql, geoWindowFromControls, groupKeySql, insertManyColumns, jsonDollarPath, mapQueryErrors, normalizeGeoPointValue, nullsOrderSql, nullsPlacementOf, numericOutOfRangeError, orFragment, orderByList, orderKeySql, parseRegexString, queryNodeToSql, queryOpToSql, quotedJsonPathSegments, refActionToSql, renameGeoDistance, renderArith, replaceColumnsFor, sqlStringLiteral, sqlTimeZoneLiteral, stripPartitionRowNumber, toSqlValue, vectorDistanceSource, viewReadSource };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { AtscriptExprNode, AtscriptQueryFieldRef, AtscriptQueryNode, DbControls, DbError, TDbDefaultFn, TDbFieldMeta, TDbForeignKey, TDbReferentialAction, TFieldOps, TResolvedBucket, TViewColumnMapping, TViewJsonType, TViewPlan, UniquSelect } from "@atscript/db";
|
|
2
|
-
import { FilterExpr, FilterVisitor } from "@uniqu/core";
|
|
1
|
+
import { AtscriptExprNode, AtscriptQueryFieldRef, AtscriptQueryNode, DbControls, DbError, DbQuery, TDbDefaultFn, TDbFieldMeta, TDbForeignKey, TDbReferentialAction, TFieldOps, TReadColumnsKind, TResolvedBucket, TViewColumnMapping, TViewJsonType, TViewPlan, UniquSelect } from "@atscript/db";
|
|
2
|
+
import { FilterExpr, FilterVisitor, NullsPlacement } from "@uniqu/core";
|
|
3
3
|
import { TDbAggregateFn } from "@atscript/db/agg";
|
|
4
4
|
|
|
5
5
|
//#region src/dialect.d.ts
|
|
@@ -19,6 +19,14 @@ interface SqlDialect {
|
|
|
19
19
|
quoteTable(name: string): string;
|
|
20
20
|
/** SQL literal for unlimited LIMIT (SQLite: '-1', MySQL: '18446744073709551615') */
|
|
21
21
|
unlimitedLimit: string;
|
|
22
|
+
/**
|
|
23
|
+
* The optimizer hint asking the engine to MERGE the derived table `alias`
|
|
24
|
+
* into the SELECT reading it (MySQL: `/*+ MERGE(\`alias\`) *\/`) — carried
|
|
25
|
+
* by pruned view reads (`viewReadSource`), so the inline definition merges
|
|
26
|
+
* even in a session with `derived_merge=off`. Omitted: no hint.
|
|
27
|
+
* @since 0.1.153
|
|
28
|
+
*/
|
|
29
|
+
derivedMergeHint?(alias: string): string;
|
|
22
30
|
/** Convert JS value to SQL-bindable param for DML */
|
|
23
31
|
toValue(value: unknown): unknown;
|
|
24
32
|
/** Convert JS value to SQL-bindable param for filters (lighter) */
|
|
@@ -142,6 +150,14 @@ interface SqlDialect {
|
|
|
142
150
|
* @since 0.1.147
|
|
143
151
|
*/
|
|
144
152
|
nullsSortLargest?: boolean;
|
|
153
|
+
/**
|
|
154
|
+
* `true` when the engine accepts `NULLS FIRST` / `NULLS LAST` on an
|
|
155
|
+
* `ORDER BY` key (PostgreSQL, SQLite ≥ 3.30). Without it a requested NULL
|
|
156
|
+
* placement that differs from the native one renders as an extra
|
|
157
|
+
* `(<expr> IS NULL)` key ({@link nullsOrderSql}).
|
|
158
|
+
* @since 0.1.153
|
|
159
|
+
*/
|
|
160
|
+
nullsPlacementSyntax?: boolean;
|
|
145
161
|
/** e.g. 'CREATE VIEW IF NOT EXISTS' or 'CREATE OR REPLACE VIEW' */
|
|
146
162
|
createViewPrefix: string;
|
|
147
163
|
/** Returns a parameter placeholder for the given 1-based index. When absent, '?' is used. */
|
|
@@ -169,13 +185,41 @@ declare function finalizeParams(dialect: SqlDialect, fragment: TSqlFragment): TS
|
|
|
169
185
|
*/
|
|
170
186
|
declare function mapQueryErrors<R>(dialect: SqlDialect, fn: () => Promise<R>, controls?: DbControls): Promise<R>;
|
|
171
187
|
/**
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
|
|
176
|
-
|
|
188
|
+
* The `$nulls` entry of one `ORDER BY` key (`controls.$nulls` is keyed like
|
|
189
|
+
* `controls.$sort`), or `undefined`.
|
|
190
|
+
* @since 0.1.153
|
|
191
|
+
*/
|
|
192
|
+
declare function nullsPlacementOf(nulls: DbControls["$nulls"], key: string): NullsPlacement | undefined;
|
|
193
|
+
/**
|
|
194
|
+
* One `ORDER BY` key with a requested NULL placement. Without `nulls`, the
|
|
195
|
+
* plain `<expr> ASC|DESC` (the engine's native placement). With it:
|
|
196
|
+
*
|
|
197
|
+
* - the engine's native placement already matches (NULL is the smallest
|
|
198
|
+
* value — `ASC` → first, `DESC` → last — unless
|
|
199
|
+
* {@link SqlDialect.nullsSortLargest}, where it is the opposite) → the plain
|
|
200
|
+
* key, so an index on `expr` still serves the order;
|
|
201
|
+
* - else `<expr> ASC|DESC NULLS FIRST|LAST` on a dialect with
|
|
202
|
+
* {@link SqlDialect.nullsPlacementSyntax} (or `nullsSortLargest`);
|
|
203
|
+
* - else (MySQL) a leading NULL-ness key: `(<expr> IS NULL) DESC, <expr> DIR`
|
|
204
|
+
* for `first`, `(<expr> IS NULL) ASC, <expr> DIR` for `last`.
|
|
205
|
+
*
|
|
206
|
+
* May render two comma-separated keys — only valid inside an `ORDER BY` list.
|
|
207
|
+
* `nullTest` renders the `IS NULL` operand when it must differ from `expr`
|
|
208
|
+
* (MySQL resolves a name inside an expression to a table column before a
|
|
209
|
+
* SELECT alias, so a grouped `ORDER BY` tests the alias's own expression).
|
|
210
|
+
* @since 0.1.153
|
|
211
|
+
*/
|
|
212
|
+
declare function nullsOrderSql(dialect: SqlDialect, expr: string, desc: boolean, nulls?: NullsPlacement, nullTest?: () => string): string;
|
|
213
|
+
/**
|
|
214
|
+
* One `ORDER BY` key with the uniform "NULL is the smallest value" ordering
|
|
215
|
+
* (`ASC` → NULL first, `DESC` → NULL last) unless `nulls` asks for another
|
|
216
|
+
* placement — {@link nullsOrderSql}. Without `nulls`: `<expr> ASC` /
|
|
217
|
+
* `<expr> DESC`, plus `NULLS FIRST` / `NULLS LAST` on a dialect where NULL
|
|
218
|
+
* sorts largest ({@link SqlDialect.nullsSortLargest}). Shared by first-row
|
|
219
|
+
* joins, `first` / `last` aggregates and the grouped `ORDER BY`.
|
|
220
|
+
* @since 0.1.148 (`nulls` since 0.1.153)
|
|
177
221
|
*/
|
|
178
|
-
declare function orderKeySql(dialect: SqlDialect, expr: string, desc: boolean): string;
|
|
222
|
+
declare function orderKeySql(dialect: SqlDialect, expr: string, desc: boolean, nulls?: NullsPlacement, nullTest?: () => string): string;
|
|
179
223
|
/**
|
|
180
224
|
* Each JSON path segment wrapped in double quotes (`"a"`), for the dialects'
|
|
181
225
|
* {@link SqlDialect.jsonExtract} path literals (`'$."a"."b"'`, `'{"a","b"}'`).
|
|
@@ -394,9 +438,39 @@ declare function buildVectorSearchCount(dialect: SqlDialect, source: TSqlFragmen
|
|
|
394
438
|
residual?: TSqlFragment;
|
|
395
439
|
}): TSqlFragment;
|
|
396
440
|
//#endregion
|
|
441
|
+
//#region src/from-source.d.ts
|
|
442
|
+
/**
|
|
443
|
+
* A pre-rendered derived table — `(<select>) AS "alias"` — read in place of
|
|
444
|
+
* a table name, plus the optimizer hint the SELECT that reads it carries
|
|
445
|
+
* (`/*+ MERGE(…) *\/ `, or `""`).
|
|
446
|
+
* @since 0.1.153
|
|
447
|
+
*/
|
|
448
|
+
interface TSqlDerivedSource {
|
|
449
|
+
readonly sql: string;
|
|
450
|
+
readonly hint: string;
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* The FROM source of a read builder: a table / view name (quoted by the
|
|
454
|
+
* dialect) or a {@link TSqlDerivedSource}.
|
|
455
|
+
* @since 0.1.153
|
|
456
|
+
*/
|
|
457
|
+
type TSqlFromSource = string | TSqlDerivedSource;
|
|
458
|
+
/** The FROM text of `source`: the quoted table name, or the derived table. */
|
|
459
|
+
declare function fromSourceSql(dialect: SqlDialect, source: TSqlFromSource): string;
|
|
460
|
+
/** The hint the SELECT directly reading `source` carries (`""` for a table). */
|
|
461
|
+
declare function fromSourceHint(source: TSqlFromSource): string;
|
|
462
|
+
//#endregion
|
|
397
463
|
//#region src/view-builder.d.ts
|
|
398
464
|
/**
|
|
399
|
-
* Builds a CREATE VIEW statement from a view plan and column mappings
|
|
465
|
+
* Builds a CREATE VIEW statement from a view plan and column mappings:
|
|
466
|
+
* `<dialect.createViewPrefix> <view> AS <buildViewSelect(…)>`.
|
|
467
|
+
*/
|
|
468
|
+
declare function buildCreateView(dialect: SqlDialect, viewName: string, plan: TViewPlan, columns: TViewColumnMapping[], resolveFieldRef: (ref: AtscriptQueryFieldRef) => string): string;
|
|
469
|
+
/**
|
|
470
|
+
* The SELECT of a managed view — the body of its CREATE VIEW, and (since
|
|
471
|
+
* 0.1.153) the inline definition a pruned view read selects from (with a
|
|
472
|
+
* `AtscriptDbView.readPlan` variant's plan and columns), so the two can never
|
|
473
|
+
* drift. Parameter-free: predicate literals are inlined.
|
|
400
474
|
*
|
|
401
475
|
* Joins render in declaration order — `JOIN` (inner, the default) or
|
|
402
476
|
* `LEFT JOIN` for `kind: "left"`; a `@db.alias` target renders as
|
|
@@ -405,8 +479,9 @@ declare function buildVectorSearchCount(dialect: SqlDialect, source: TSqlFragmen
|
|
|
405
479
|
* name for an aliased join); `resolveFieldRef` renders predicate refs (join
|
|
406
480
|
* ON, WHERE, HAVING fallbacks) as `"table"."column"`. The entry table and a
|
|
407
481
|
* join target may be views.
|
|
482
|
+
* @since 0.1.153
|
|
408
483
|
*/
|
|
409
|
-
declare function
|
|
484
|
+
declare function buildViewSelect(dialect: SqlDialect, plan: TViewPlan, columns: TViewColumnMapping[], resolveFieldRef: (ref: AtscriptQueryFieldRef) => string): string;
|
|
410
485
|
//#endregion
|
|
411
486
|
//#region src/sql-builder.d.ts
|
|
412
487
|
/**
|
|
@@ -456,8 +531,17 @@ declare function chunkInsertRows(rows: readonly Record<string, unknown>[], maxPa
|
|
|
456
531
|
declare function buildInsertMany(dialect: SqlDialect, table: string, rows: readonly Record<string, unknown>[], columns?: readonly string[]): TSqlFragment;
|
|
457
532
|
/**
|
|
458
533
|
* Builds a SELECT statement with optional sort, limit, offset, projection.
|
|
534
|
+
* `table` may be a derived source (a pruned view read, since 0.1.153).
|
|
459
535
|
*/
|
|
460
|
-
declare function buildSelect(dialect: SqlDialect, table:
|
|
536
|
+
declare function buildSelect(dialect: SqlDialect, table: TSqlFromSource, where: TSqlFragment, controls?: DbControls): TSqlFragment;
|
|
537
|
+
/**
|
|
538
|
+
* `"col" ASC, "other" DESC` of a physical `$sort` — `""` when it orders
|
|
539
|
+
* nothing. A key with a `$nulls` entry places NULL as asked
|
|
540
|
+
* ({@link nullsOrderSql}); `prefix` is prepended verbatim to each quoted
|
|
541
|
+
* column (`t.` → `t."col"`).
|
|
542
|
+
* @since 0.1.153 (exported)
|
|
543
|
+
*/
|
|
544
|
+
declare function orderByList(dialect: SqlDialect, sort: DbControls["$sort"], nulls?: DbControls["$nulls"], prefix?: string): string;
|
|
461
545
|
/**
|
|
462
546
|
* The row-number column {@link buildPartitionedSelect} adds to every row;
|
|
463
547
|
* {@link stripPartitionRowNumber} removes it from the result rows.
|
|
@@ -474,7 +558,7 @@ declare const PARTITION_ROW_NUMBER_ALIAS = "__atscript_rn";
|
|
|
474
558
|
* SQLite ≥ 3.25, MySQL ≥ 8.0 or MariaDB ≥ 10.2.
|
|
475
559
|
* @since 0.1.147
|
|
476
560
|
*/
|
|
477
|
-
declare function buildPartitionedSelect(dialect: SqlDialect, table:
|
|
561
|
+
declare function buildPartitionedSelect(dialect: SqlDialect, table: TSqlFromSource, where: TSqlFragment, controls: DbControls, partitionBy: readonly string[]): TSqlFragment;
|
|
478
562
|
/** Removes {@link PARTITION_ROW_NUMBER_ALIAS} from rows read by {@link buildPartitionedSelect}. */
|
|
479
563
|
declare function stripPartitionRowNumber<R extends Record<string, unknown>>(rows: R[]): R[];
|
|
480
564
|
/**
|
|
@@ -641,7 +725,7 @@ declare function groupKeySql(dialect: SqlDialect, controls: DbControls, key: str
|
|
|
641
725
|
* parameter-free, so the bind parameters are exactly those of the same query
|
|
642
726
|
* without buckets (WHERE, HAVING, LIMIT, OFFSET).
|
|
643
727
|
*/
|
|
644
|
-
declare function buildAggregateSelect(dialect: SqlDialect, table:
|
|
728
|
+
declare function buildAggregateSelect(dialect: SqlDialect, table: TSqlFromSource, where: TSqlFragment, controls: DbControls): TSqlFragment;
|
|
645
729
|
/**
|
|
646
730
|
* Builds a COUNT query for the number of distinct groups — the groups that
|
|
647
731
|
* survive `$having` when one is given (the same predicate the row query
|
|
@@ -650,7 +734,7 @@ declare function buildAggregateSelect(dialect: SqlDialect, table: string, where:
|
|
|
650
734
|
* `first` / `last` value (or an expression over one): only then the window
|
|
651
735
|
* derived table is built.
|
|
652
736
|
*/
|
|
653
|
-
declare function buildAggregateCount(dialect: SqlDialect, table:
|
|
737
|
+
declare function buildAggregateCount(dialect: SqlDialect, table: TSqlFromSource, where: TSqlFragment, controls: DbControls): TSqlFragment;
|
|
654
738
|
//#endregion
|
|
655
739
|
//#region src/regex.d.ts
|
|
656
740
|
/**
|
|
@@ -665,4 +749,30 @@ declare function parseRegexString(value: unknown): {
|
|
|
665
749
|
flags: string;
|
|
666
750
|
};
|
|
667
751
|
//#endregion
|
|
668
|
-
|
|
752
|
+
//#region src/view-read.d.ts
|
|
753
|
+
/** The structural view surface {@link viewReadSource} needs from an adapter's readable. */
|
|
754
|
+
interface TReadableLike {
|
|
755
|
+
readonly isView: boolean;
|
|
756
|
+
readonly tableName: string;
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* The FROM source of a read of `readable` (the adapter's registered table or
|
|
760
|
+
* view): `tableName` unchanged, except for a managed view whose read does
|
|
761
|
+
* not need some of its LEFT joins (`AtscriptDbView.readPlan` over the
|
|
762
|
+
* columns `query` reads — {@link queryReadColumns}): then the view's own
|
|
763
|
+
* definition without those joins ({@link buildViewSelect}), aliased as the
|
|
764
|
+
* bare view name so every column reference — and the correlation of a
|
|
765
|
+
* relational predicate — stays valid. A column of a dropped join is absent
|
|
766
|
+
* from the derived table, so a reference the collector missed fails with
|
|
767
|
+
* "unknown column" rather than reading wrong data.
|
|
768
|
+
*
|
|
769
|
+
* Never pruned: a table, an external view, a view read under another name
|
|
770
|
+
* than its own (`tableName` schema-qualified — its definition's unqualified
|
|
771
|
+
* names resolve against that schema, the derived table's against the
|
|
772
|
+
* connection's — or renamed by an adapter subclass), or anything `readPlan`
|
|
773
|
+
* keeps whole.
|
|
774
|
+
* @since 0.1.153
|
|
775
|
+
*/
|
|
776
|
+
declare function viewReadSource(dialect: SqlDialect, readable: TReadableLike, tableName: string, query: DbQuery, kind: TReadColumnsKind, partitionBy?: readonly string[]): TSqlFromSource;
|
|
777
|
+
//#endregion
|
|
778
|
+
export { AGG_FN_SQL, EMPTY_AND, EMPTY_OR, GEO_DISTANCE_ALIAS, InsertSqlCache, PARTITION_ROW_NUMBER_ALIAS, SEARCH_SOURCE_ALIAS, SQL_DEFAULT, type SqlDialect, type TArithFailure, type TFilterVisitorOptions, type TGeoCircle, type TGeoSearchControls, type TGeoWindow, type TReplaceColumn, type TSqlDerivedSource, type TSqlFragment, type TSqlFromSource, VECTOR_DISTANCE_ALIAS, arithOverflowError, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildPartitionedSelect, buildProjection, buildSelect, buildUpdate, buildVectorSearchCount, buildVectorSearchSelect, buildViewSelect, buildWhere, chunkInsertRows, createFilterVisitor, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, finalizeParams, foreignKeySql, fromSourceHint, fromSourceSql, geoWindowFromControls, groupKeySql, insertManyColumns, jsonDollarPath, mapQueryErrors, normalizeGeoPointValue, nullsOrderSql, nullsPlacementOf, numericOutOfRangeError, orFragment, orderByList, orderKeySql, parseRegexString, queryNodeToSql, queryOpToSql, quotedJsonPathSegments, refActionToSql, renameGeoDistance, renderArith, replaceColumnsFor, sqlStringLiteral, sqlTimeZoneLiteral, stripPartitionRowNumber, toSqlValue, vectorDistanceSource, viewReadSource };
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DbError, INTEGER_REGEX_OP, aggregateExpressionsNotSupported, containsRelationFilter, fkColumns, isFieldRef, isResolvedRelationFilter } from "@atscript/db";
|
|
1
|
+
import { DbError, INTEGER_REGEX_OP, aggregateExpressionsNotSupported, containsRelationFilter, fkColumns, isFieldRef, isResolvedRelationFilter, queryReadColumns } from "@atscript/db";
|
|
2
2
|
import { BUCKET_UNITS, TIME_ZONE_NAME_RE, WEEK_STARTS, walkFilter } from "@uniqu/core";
|
|
3
3
|
import { assertAggregateFn, resolveAlias } from "@atscript/db/agg";
|
|
4
4
|
//#region src/dialect.ts
|
|
@@ -156,15 +156,53 @@ async function mapQueryErrors(dialect, fn, controls) {
|
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
158
|
/**
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
|
|
163
|
-
|
|
159
|
+
* The `$nulls` entry of one `ORDER BY` key (`controls.$nulls` is keyed like
|
|
160
|
+
* `controls.$sort`), or `undefined`.
|
|
161
|
+
* @since 0.1.153
|
|
162
|
+
*/
|
|
163
|
+
function nullsPlacementOf(nulls, key) {
|
|
164
|
+
if (!nulls) return void 0;
|
|
165
|
+
const map = nulls;
|
|
166
|
+
return Object.hasOwn(map, key) ? map[key] : void 0;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* One `ORDER BY` key with a requested NULL placement. Without `nulls`, the
|
|
170
|
+
* plain `<expr> ASC|DESC` (the engine's native placement). With it:
|
|
171
|
+
*
|
|
172
|
+
* - the engine's native placement already matches (NULL is the smallest
|
|
173
|
+
* value — `ASC` → first, `DESC` → last — unless
|
|
174
|
+
* {@link SqlDialect.nullsSortLargest}, where it is the opposite) → the plain
|
|
175
|
+
* key, so an index on `expr` still serves the order;
|
|
176
|
+
* - else `<expr> ASC|DESC NULLS FIRST|LAST` on a dialect with
|
|
177
|
+
* {@link SqlDialect.nullsPlacementSyntax} (or `nullsSortLargest`);
|
|
178
|
+
* - else (MySQL) a leading NULL-ness key: `(<expr> IS NULL) DESC, <expr> DIR`
|
|
179
|
+
* for `first`, `(<expr> IS NULL) ASC, <expr> DIR` for `last`.
|
|
180
|
+
*
|
|
181
|
+
* May render two comma-separated keys — only valid inside an `ORDER BY` list.
|
|
182
|
+
* `nullTest` renders the `IS NULL` operand when it must differ from `expr`
|
|
183
|
+
* (MySQL resolves a name inside an expression to a table column before a
|
|
184
|
+
* SELECT alias, so a grouped `ORDER BY` tests the alias's own expression).
|
|
185
|
+
* @since 0.1.153
|
|
186
|
+
*/
|
|
187
|
+
function nullsOrderSql(dialect, expr, desc, nulls, nullTest) {
|
|
188
|
+
const key = `${expr} ${desc ? "DESC" : "ASC"}`;
|
|
189
|
+
if (!nulls) return key;
|
|
190
|
+
const nativeFirst = desc === (dialect.nullsSortLargest === true);
|
|
191
|
+
if (nulls === "first" === nativeFirst) return key;
|
|
192
|
+
if (dialect.nullsPlacementSyntax || dialect.nullsSortLargest) return `${key} NULLS ${nulls === "first" ? "FIRST" : "LAST"}`;
|
|
193
|
+
return `(${nullTest ? nullTest() : expr} IS NULL) ${nulls === "first" ? "DESC" : "ASC"}, ${key}`;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* One `ORDER BY` key with the uniform "NULL is the smallest value" ordering
|
|
197
|
+
* (`ASC` → NULL first, `DESC` → NULL last) unless `nulls` asks for another
|
|
198
|
+
* placement — {@link nullsOrderSql}. Without `nulls`: `<expr> ASC` /
|
|
199
|
+
* `<expr> DESC`, plus `NULLS FIRST` / `NULLS LAST` on a dialect where NULL
|
|
200
|
+
* sorts largest ({@link SqlDialect.nullsSortLargest}). Shared by first-row
|
|
201
|
+
* joins, `first` / `last` aggregates and the grouped `ORDER BY`.
|
|
202
|
+
* @since 0.1.148 (`nulls` since 0.1.153)
|
|
164
203
|
*/
|
|
165
|
-
function orderKeySql(dialect, expr, desc) {
|
|
166
|
-
|
|
167
|
-
return `${expr} ${desc ? "DESC" : "ASC"}${nulls}`;
|
|
204
|
+
function orderKeySql(dialect, expr, desc, nulls, nullTest) {
|
|
205
|
+
return nullsOrderSql(dialect, expr, desc, nulls ?? (desc ? "last" : "first"), nullTest);
|
|
168
206
|
}
|
|
169
207
|
/**
|
|
170
208
|
* Each JSON path segment wrapped in double quotes (`"a"`), for the dialects'
|
|
@@ -467,6 +505,16 @@ function buildWhere(dialect, filter, opts) {
|
|
|
467
505
|
}) : getVisitor(dialect)) ?? EMPTY_AND;
|
|
468
506
|
}
|
|
469
507
|
//#endregion
|
|
508
|
+
//#region src/from-source.ts
|
|
509
|
+
/** The FROM text of `source`: the quoted table name, or the derived table. */
|
|
510
|
+
function fromSourceSql(dialect, source) {
|
|
511
|
+
return typeof source === "string" ? dialect.quoteTable(source) : source.sql;
|
|
512
|
+
}
|
|
513
|
+
/** The hint the SELECT directly reading `source` carries (`""` for a table). */
|
|
514
|
+
function fromSourceHint(source) {
|
|
515
|
+
return typeof source === "string" ? "" : source.hint;
|
|
516
|
+
}
|
|
517
|
+
//#endregion
|
|
470
518
|
//#region src/common.ts
|
|
471
519
|
/** Formats a string value as a SQL literal with single-quote escaping. */
|
|
472
520
|
function sqlStringLiteral(value) {
|
|
@@ -814,17 +862,24 @@ function rowColumns(controls) {
|
|
|
814
862
|
* WHERE moves inside unchanged, so the bind parameters keep their order.
|
|
815
863
|
*/
|
|
816
864
|
function aggSource(dialect, table, where, controls, withRows) {
|
|
817
|
-
const quotedTable = dialect
|
|
865
|
+
const quotedTable = fromSourceSql(dialect, table);
|
|
818
866
|
const firstLast = controls.$select?.firstLast;
|
|
819
867
|
const rowOrder = controls.$select?.rowOrder;
|
|
820
|
-
if (!withRows || !firstLast?.length || !rowOrder?.length) return
|
|
868
|
+
if (!withRows || !firstLast?.length || !rowOrder?.length) return {
|
|
869
|
+
hint: fromSourceHint(table),
|
|
870
|
+
sql: `FROM ${quotedTable} WHERE ${where.sql}`
|
|
871
|
+
};
|
|
821
872
|
const groupBy = controls.$groupBy;
|
|
822
873
|
const partition = groupBy?.length ? `PARTITION BY ${groupBy.map((key) => groupKeySql(dialect, controls, key)).join(", ")} ` : "";
|
|
823
|
-
const order = (reverse) => rowOrder.map((k) => orderKeySql(dialect, dialect.quoteIdentifier(k.column), k.desc !== reverse)).join(", ");
|
|
824
|
-
|
|
874
|
+
const order = (reverse) => rowOrder.map((k) => orderKeySql(dialect, dialect.quoteIdentifier(k.column), k.desc !== reverse, k.nulls && reverse ? k.nulls === "first" ? "last" : "first" : k.nulls)).join(", ");
|
|
875
|
+
const columns = [...rowColumns(controls).map((column) => dialect.quoteIdentifier(column)), ...firstLast.map((fl, i) => {
|
|
825
876
|
const col = dialect.quoteIdentifier(firstLastColumn(i));
|
|
826
877
|
return `FIRST_VALUE(${dialect.quoteIdentifier(fl.column)}) OVER (${partition}ORDER BY ${order(fl.fn === "last")}) AS ${col}`;
|
|
827
|
-
})]
|
|
878
|
+
})];
|
|
879
|
+
return {
|
|
880
|
+
hint: "",
|
|
881
|
+
sql: `FROM (SELECT ${fromSourceHint(table)}${columns.join(", ")} FROM ${quotedTable} WHERE ${where.sql}) AS ${dialect.quoteIdentifier(ROWS_ALIAS)}`
|
|
882
|
+
};
|
|
828
883
|
}
|
|
829
884
|
/**
|
|
830
885
|
* Builds a SELECT ... GROUP BY statement with aggregate functions.
|
|
@@ -846,7 +901,9 @@ function buildAggregateSelect(dialect, table, where, controls) {
|
|
|
846
901
|
aliasSql = aliasSqlMap(dialect, controls);
|
|
847
902
|
for (const alias of select.computedAliases) selectParts.push(`${aliasSql.get(alias)} AS ${dialect.quoteIdentifier(alias)}`);
|
|
848
903
|
} else for (const expr of select?.aggregates ?? []) selectParts.push(`${aggFnSql(dialect, select, expr)} AS ${dialect.quoteIdentifier(resolveAlias(expr))}`);
|
|
849
|
-
|
|
904
|
+
const cols = selectParts.length > 0 ? selectParts.join(", ") : "*";
|
|
905
|
+
const source = aggSource(dialect, table, where, controls, true);
|
|
906
|
+
let sql = `SELECT ${source.hint}${cols} ${source.sql}${groupByClause(dialect, controls)}`;
|
|
850
907
|
const params = [...where.params];
|
|
851
908
|
const having = havingClause(dialect, controls, aliasSql);
|
|
852
909
|
if (having) {
|
|
@@ -854,8 +911,12 @@ function buildAggregateSelect(dialect, table, where, controls) {
|
|
|
854
911
|
params.push(...having.params);
|
|
855
912
|
}
|
|
856
913
|
if (controls.$sort) {
|
|
914
|
+
const nullTest = (col) => () => {
|
|
915
|
+
aliasSql ??= aliasSqlMap(dialect, controls);
|
|
916
|
+
return aliasSql.get(col) ?? dialect.quoteIdentifier(col);
|
|
917
|
+
};
|
|
857
918
|
const orderParts = [];
|
|
858
|
-
for (const [col, dir] of Object.entries(controls.$sort)) orderParts.push(orderKeySql(dialect, dialect.quoteIdentifier(col), dir === -1));
|
|
919
|
+
for (const [col, dir] of Object.entries(controls.$sort)) orderParts.push(orderKeySql(dialect, dialect.quoteIdentifier(col), dir === -1, nullsPlacementOf(controls.$nulls, col), nullTest(col)));
|
|
859
920
|
if (orderParts.length > 0) sql += ` ORDER BY ${orderParts.join(", ")}`;
|
|
860
921
|
}
|
|
861
922
|
if (controls.$limit !== void 0) {
|
|
@@ -890,7 +951,7 @@ function buildAggregateCount(dialect, table, where, controls) {
|
|
|
890
951
|
params: []
|
|
891
952
|
});
|
|
892
953
|
return finalizeParams(dialect, {
|
|
893
|
-
sql: `SELECT ${countCol} FROM ${dialect
|
|
954
|
+
sql: `SELECT ${fromSourceHint(table)}${countCol} FROM ${fromSourceSql(dialect, table)} WHERE ${where.sql}`,
|
|
894
955
|
params: where.params
|
|
895
956
|
});
|
|
896
957
|
}
|
|
@@ -899,7 +960,7 @@ function buildAggregateCount(dialect, table, where, controls) {
|
|
|
899
960
|
let inner = "COUNT(*)";
|
|
900
961
|
if (groupBy) inner = bucketSelectParts(dialect, controls).join(", ") || "1";
|
|
901
962
|
return finalizeParams(dialect, {
|
|
902
|
-
sql: `SELECT ${countCol} FROM (SELECT ${inner} ${from}${groupBy}${having?.sql ?? ""}) AS ${dialect.quoteIdentifier("_groups")}`,
|
|
963
|
+
sql: `SELECT ${countCol} FROM (SELECT ${from.hint}${inner} ${from.sql}${groupBy}${having?.sql ?? ""}) AS ${dialect.quoteIdentifier("_groups")}`,
|
|
903
964
|
params: [...where.params, ...having?.params ?? []]
|
|
904
965
|
});
|
|
905
966
|
}
|
|
@@ -1001,7 +1062,17 @@ function firstRowOn(dialect, join, target, condition, resolveFieldRef) {
|
|
|
1001
1062
|
return `${key} = (SELECT ${key} FROM ${target} WHERE ${condition} ORDER BY ${first.order.map(({ ref, desc }) => orderKeySql(dialect, resolveFieldRef(ref), desc)).join(", ")} LIMIT 1)`;
|
|
1002
1063
|
}
|
|
1003
1064
|
/**
|
|
1004
|
-
* Builds a CREATE VIEW statement from a view plan and column mappings
|
|
1065
|
+
* Builds a CREATE VIEW statement from a view plan and column mappings:
|
|
1066
|
+
* `<dialect.createViewPrefix> <view> AS <buildViewSelect(…)>`.
|
|
1067
|
+
*/
|
|
1068
|
+
function buildCreateView(dialect, viewName, plan, columns, resolveFieldRef) {
|
|
1069
|
+
return `${dialect.createViewPrefix} ${dialect.quoteTable(viewName)} AS ${buildViewSelect(dialect, plan, columns, resolveFieldRef)}`;
|
|
1070
|
+
}
|
|
1071
|
+
/**
|
|
1072
|
+
* The SELECT of a managed view — the body of its CREATE VIEW, and (since
|
|
1073
|
+
* 0.1.153) the inline definition a pruned view read selects from (with a
|
|
1074
|
+
* `AtscriptDbView.readPlan` variant's plan and columns), so the two can never
|
|
1075
|
+
* drift. Parameter-free: predicate literals are inlined.
|
|
1005
1076
|
*
|
|
1006
1077
|
* Joins render in declaration order — `JOIN` (inner, the default) or
|
|
1007
1078
|
* `LEFT JOIN` for `kind: "left"`; a `@db.alias` target renders as
|
|
@@ -1010,15 +1081,15 @@ function firstRowOn(dialect, join, target, condition, resolveFieldRef) {
|
|
|
1010
1081
|
* name for an aliased join); `resolveFieldRef` renders predicate refs (join
|
|
1011
1082
|
* ON, WHERE, HAVING fallbacks) as `"table"."column"`. The entry table and a
|
|
1012
1083
|
* join target may be views.
|
|
1084
|
+
* @since 0.1.153
|
|
1013
1085
|
*/
|
|
1014
|
-
function
|
|
1086
|
+
function buildViewSelect(dialect, plan, columns, resolveFieldRef) {
|
|
1015
1087
|
const columnByPath = new Map(columns.map((c) => [c.viewPath, c]));
|
|
1016
1088
|
const hasAggregates = columns.some((c) => c.aggFn);
|
|
1017
1089
|
const computedSql = /* @__PURE__ */ new Map();
|
|
1018
1090
|
/** A column's SELECT expression: computed, aggregate or plain source. */
|
|
1019
1091
|
const columnExpr = (c) => c.expr !== void 0 ? viewComputeExpr(dialect, c, columnByPath, resolveFieldRef, hasAggregates, computedSql) : c.aggFn ? viewAggExpr(dialect, c, resolveFieldRef) : viewSourceExpr(dialect, c);
|
|
1020
|
-
|
|
1021
|
-
let sql = `${dialect.createViewPrefix} ${dialect.quoteTable(viewName)} AS SELECT ${selectCols} FROM ${dialect.quoteIdentifier(plan.entryTable)}`;
|
|
1092
|
+
let sql = `SELECT ${columns.map((c) => `${columnExpr(c)} AS ${dialect.quoteIdentifier(c.viewColumn)}`).join(", ")} FROM ${dialect.quoteIdentifier(plan.entryTable)}`;
|
|
1022
1093
|
for (const join of plan.joins) {
|
|
1023
1094
|
const condition = queryNodeToSql(join.condition, resolveFieldRef);
|
|
1024
1095
|
const keyword = join.kind === "left" ? "LEFT JOIN" : "JOIN";
|
|
@@ -1171,11 +1242,13 @@ function buildInsertMany(dialect, table, rows, columns = insertManyColumns(rows)
|
|
|
1171
1242
|
}
|
|
1172
1243
|
/**
|
|
1173
1244
|
* Builds a SELECT statement with optional sort, limit, offset, projection.
|
|
1245
|
+
* `table` may be a derived source (a pruned view read, since 0.1.153).
|
|
1174
1246
|
*/
|
|
1175
1247
|
function buildSelect(dialect, table, where, controls) {
|
|
1176
|
-
|
|
1248
|
+
const cols = buildProjection(dialect, controls?.$select);
|
|
1249
|
+
let sql = `SELECT ${fromSourceHint(table)}${cols} FROM ${fromSourceSql(dialect, table)} WHERE ${where.sql}`;
|
|
1177
1250
|
const params = [...where.params];
|
|
1178
|
-
const orderBy = orderByList(dialect, controls?.$sort);
|
|
1251
|
+
const orderBy = orderByList(dialect, controls?.$sort, controls?.$nulls);
|
|
1179
1252
|
if (orderBy) sql += ` ORDER BY ${orderBy}`;
|
|
1180
1253
|
if (controls?.$limit !== void 0) {
|
|
1181
1254
|
sql += ` LIMIT ?`;
|
|
@@ -1191,11 +1264,17 @@ function buildSelect(dialect, table, where, controls) {
|
|
|
1191
1264
|
params
|
|
1192
1265
|
});
|
|
1193
1266
|
}
|
|
1194
|
-
/**
|
|
1195
|
-
|
|
1267
|
+
/**
|
|
1268
|
+
* `"col" ASC, "other" DESC` of a physical `$sort` — `""` when it orders
|
|
1269
|
+
* nothing. A key with a `$nulls` entry places NULL as asked
|
|
1270
|
+
* ({@link nullsOrderSql}); `prefix` is prepended verbatim to each quoted
|
|
1271
|
+
* column (`t.` → `t."col"`).
|
|
1272
|
+
* @since 0.1.153 (exported)
|
|
1273
|
+
*/
|
|
1274
|
+
function orderByList(dialect, sort, nulls, prefix = "") {
|
|
1196
1275
|
if (!sort) return "";
|
|
1197
1276
|
const parts = [];
|
|
1198
|
-
for (const [col, dir] of Object.entries(sort)) parts.push(
|
|
1277
|
+
for (const [col, dir] of Object.entries(sort)) parts.push(nullsOrderSql(dialect, prefix + dialect.quoteIdentifier(col), dir === -1, nullsPlacementOf(nulls, col)));
|
|
1199
1278
|
return parts.join(", ");
|
|
1200
1279
|
}
|
|
1201
1280
|
/**
|
|
@@ -1216,9 +1295,9 @@ const PARTITION_ROW_NUMBER_ALIAS = "__atscript_rn";
|
|
|
1216
1295
|
*/
|
|
1217
1296
|
function buildPartitionedSelect(dialect, table, where, controls, partitionBy) {
|
|
1218
1297
|
const rn = dialect.quoteIdentifier(PARTITION_ROW_NUMBER_ALIAS);
|
|
1219
|
-
const orderBy = orderByList(dialect, controls.$sort);
|
|
1298
|
+
const orderBy = orderByList(dialect, controls.$sort, controls.$nulls);
|
|
1220
1299
|
const window = `PARTITION BY ${partitionBy.map((col) => dialect.quoteIdentifier(col)).join(", ")}` + (orderBy ? ` ORDER BY ${orderBy}` : "");
|
|
1221
|
-
const inner = `SELECT ${buildProjection(dialect, controls.$select)}, ROW_NUMBER() OVER (${window}) AS ${rn} FROM ${dialect
|
|
1300
|
+
const inner = `SELECT ${fromSourceHint(table)}${buildProjection(dialect, controls.$select)}, ROW_NUMBER() OVER (${window}) AS ${rn} FROM ${fromSourceSql(dialect, table)} WHERE ${where.sql}`;
|
|
1222
1301
|
const skip = controls.$skip ?? 0;
|
|
1223
1302
|
const limit = controls.$limit;
|
|
1224
1303
|
const params = [...where.params, skip];
|
|
@@ -1625,4 +1704,47 @@ function parseRegexString(value) {
|
|
|
1625
1704
|
};
|
|
1626
1705
|
}
|
|
1627
1706
|
//#endregion
|
|
1628
|
-
|
|
1707
|
+
//#region src/view-read.ts
|
|
1708
|
+
/** Rendered pruned definitions per read variant (variants are memoised by their view). */
|
|
1709
|
+
const rendered = /* @__PURE__ */ new WeakMap();
|
|
1710
|
+
/**
|
|
1711
|
+
* The FROM source of a read of `readable` (the adapter's registered table or
|
|
1712
|
+
* view): `tableName` unchanged, except for a managed view whose read does
|
|
1713
|
+
* not need some of its LEFT joins (`AtscriptDbView.readPlan` over the
|
|
1714
|
+
* columns `query` reads — {@link queryReadColumns}): then the view's own
|
|
1715
|
+
* definition without those joins ({@link buildViewSelect}), aliased as the
|
|
1716
|
+
* bare view name so every column reference — and the correlation of a
|
|
1717
|
+
* relational predicate — stays valid. A column of a dropped join is absent
|
|
1718
|
+
* from the derived table, so a reference the collector missed fails with
|
|
1719
|
+
* "unknown column" rather than reading wrong data.
|
|
1720
|
+
*
|
|
1721
|
+
* Never pruned: a table, an external view, a view read under another name
|
|
1722
|
+
* than its own (`tableName` schema-qualified — its definition's unqualified
|
|
1723
|
+
* names resolve against that schema, the derived table's against the
|
|
1724
|
+
* connection's — or renamed by an adapter subclass), or anything `readPlan`
|
|
1725
|
+
* keeps whole.
|
|
1726
|
+
* @since 0.1.153
|
|
1727
|
+
*/
|
|
1728
|
+
function viewReadSource(dialect, readable, tableName, query, kind, partitionBy) {
|
|
1729
|
+
if (!readable.isView || tableName !== readable.tableName) return tableName;
|
|
1730
|
+
const view = readable;
|
|
1731
|
+
const variant = view.readPlan(queryReadColumns(query, kind, partitionBy));
|
|
1732
|
+
if (!variant) return tableName;
|
|
1733
|
+
let byDialect = rendered.get(variant);
|
|
1734
|
+
if (!byDialect) {
|
|
1735
|
+
byDialect = /* @__PURE__ */ new Map();
|
|
1736
|
+
rendered.set(variant, byDialect);
|
|
1737
|
+
}
|
|
1738
|
+
let source = byDialect.get(dialect);
|
|
1739
|
+
if (!source) {
|
|
1740
|
+
const qi = (name) => dialect.quoteIdentifier(name);
|
|
1741
|
+
source = {
|
|
1742
|
+
sql: `(${buildViewSelect(dialect, variant.plan, variant.columns, (ref) => view.resolveFieldRef(ref, qi))}) AS ${qi(readable.tableName)}`,
|
|
1743
|
+
hint: dialect.derivedMergeHint ? `${dialect.derivedMergeHint(readable.tableName)} ` : ""
|
|
1744
|
+
};
|
|
1745
|
+
byDialect.set(dialect, source);
|
|
1746
|
+
}
|
|
1747
|
+
return source;
|
|
1748
|
+
}
|
|
1749
|
+
//#endregion
|
|
1750
|
+
export { AGG_FN_SQL, EMPTY_AND, EMPTY_OR, GEO_DISTANCE_ALIAS, InsertSqlCache, PARTITION_ROW_NUMBER_ALIAS, SEARCH_SOURCE_ALIAS, SQL_DEFAULT, VECTOR_DISTANCE_ALIAS, arithOverflowError, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildPartitionedSelect, buildProjection, buildSelect, buildUpdate, buildVectorSearchCount, buildVectorSearchSelect, buildViewSelect, buildWhere, chunkInsertRows, createFilterVisitor, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, finalizeParams, foreignKeySql, fromSourceHint, fromSourceSql, geoWindowFromControls, groupKeySql, insertManyColumns, jsonDollarPath, mapQueryErrors, normalizeGeoPointValue, nullsOrderSql, nullsPlacementOf, numericOutOfRangeError, orFragment, orderByList, orderKeySql, parseRegexString, queryNodeToSql, queryOpToSql, quotedJsonPathSegments, refActionToSql, renameGeoDistance, renderArith, replaceColumnsFor, sqlStringLiteral, sqlTimeZoneLiteral, stripPartitionRowNumber, toSqlValue, vectorDistanceSource, viewReadSource };
|
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.154",
|
|
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.
|
|
41
|
-
"unplugin-atscript": "^0.1.
|
|
40
|
+
"@uniqu/core": "^0.1.15",
|
|
41
|
+
"unplugin-atscript": "^0.1.103"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
|
-
"@uniqu/core": "^0.1.
|
|
45
|
-
"@atscript/db": "^0.1.
|
|
44
|
+
"@uniqu/core": "^0.1.15",
|
|
45
|
+
"@atscript/db": "^0.1.154"
|
|
46
46
|
},
|
|
47
47
|
"scripts": {
|
|
48
48
|
"build": "vp pack",
|