@atscript/db-sql-tools 0.1.161 → 0.1.162
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 +90 -31
- package/dist/index.d.cts +71 -14
- package/dist/index.d.mts +71 -14
- package/dist/index.mjs +87 -32
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -213,11 +213,12 @@ function orderKeySql(dialect, expr, desc, nulls, nullTest) {
|
|
|
213
213
|
* reads the same on every dialect (SQLite's quoted path labels have no escape
|
|
214
214
|
* syntax), so it is rejected rather than escaped.
|
|
215
215
|
*
|
|
216
|
+
* An empty path (the JSON root — a primitive JSON column's value) has no segments.
|
|
217
|
+
*
|
|
216
218
|
* @since 0.1.136
|
|
217
|
-
* @throws for
|
|
219
|
+
* @throws for a segment that is empty or holds such a character.
|
|
218
220
|
*/
|
|
219
221
|
function quotedJsonPathSegments(path) {
|
|
220
|
-
if (path.length === 0) throw new Error("JSON extraction needs a path below the JSON column");
|
|
221
222
|
return path.map((seg) => {
|
|
222
223
|
let ok = seg.length > 0;
|
|
223
224
|
for (let i = 0; ok && i < seg.length; i++) {
|
|
@@ -522,15 +523,24 @@ function sqlStringLiteral(value) {
|
|
|
522
523
|
return `'${value.replace(/'/g, "''")}'`;
|
|
523
524
|
}
|
|
524
525
|
/**
|
|
526
|
+
* The value of a single-quoted SQL string literal (`'it''s'` → `it's`) — as
|
|
527
|
+
* engines report a column default — or `undefined` when `literal` is not one.
|
|
528
|
+
* @since 0.1.162
|
|
529
|
+
*/
|
|
530
|
+
function unquoteSqlStringLiteral(literal) {
|
|
531
|
+
if (literal.length < 2 || !literal.startsWith("'") || !literal.endsWith("'")) return void 0;
|
|
532
|
+
return literal.slice(1, -1).replace(/''/g, "'");
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
525
535
|
* The SQL string literal of a `$`-rooted JSON path with every segment quoted
|
|
526
536
|
* (`'$."a"."b"'`) — the path argument of SQLite's and MySQL's `json_extract` /
|
|
527
|
-
* `json_type
|
|
537
|
+
* `json_type`; an empty path is the JSON root (`'$'`).
|
|
528
538
|
*
|
|
529
539
|
* @since 0.1.136
|
|
530
540
|
* @throws as {@link quotedJsonPathSegments}.
|
|
531
541
|
*/
|
|
532
|
-
function jsonDollarPath(path) {
|
|
533
|
-
return
|
|
542
|
+
function jsonDollarPath(path, stringLiteral = sqlStringLiteral) {
|
|
543
|
+
return stringLiteral(["$", ...quotedJsonPathSegments(path)].join("."));
|
|
534
544
|
}
|
|
535
545
|
/**
|
|
536
546
|
* A calendar bucket's time zone as an inlined SQL string literal (`'Europe/Berlin'`).
|
|
@@ -596,7 +606,7 @@ function defaultValueForType(designType) {
|
|
|
596
606
|
* respecting the field's designType. Booleans become 0/1, numbers stay unquoted,
|
|
597
607
|
* strings are single-quote-escaped.
|
|
598
608
|
*/
|
|
599
|
-
function defaultValueToSqlLiteral(designType, value) {
|
|
609
|
+
function defaultValueToSqlLiteral(designType, value, stringLiteral = sqlStringLiteral) {
|
|
600
610
|
switch (designType) {
|
|
601
611
|
case "boolean": return value === "true" || value === "1" ? "1" : "0";
|
|
602
612
|
case "number":
|
|
@@ -605,7 +615,7 @@ function defaultValueToSqlLiteral(designType, value) {
|
|
|
605
615
|
const n = Number(value);
|
|
606
616
|
return Number.isFinite(n) ? String(n) : "0";
|
|
607
617
|
}
|
|
608
|
-
default: return
|
|
618
|
+
default: return stringLiteral(value);
|
|
609
619
|
}
|
|
610
620
|
}
|
|
611
621
|
const queryOpToSql = {
|
|
@@ -617,9 +627,9 @@ const queryOpToSql = {
|
|
|
617
627
|
$lte: "<="
|
|
618
628
|
};
|
|
619
629
|
/** An inlined SQL literal for a view-predicate value (DDL — no parameters). */
|
|
620
|
-
function predicateLiteral(value) {
|
|
630
|
+
function predicateLiteral(value, stringLiteral) {
|
|
621
631
|
if (value === null || value === void 0) return "NULL";
|
|
622
|
-
if (typeof value === "string") return
|
|
632
|
+
if (typeof value === "string") return stringLiteral(value);
|
|
623
633
|
if (typeof value === "number") {
|
|
624
634
|
if (!Number.isFinite(value)) throw new Error(`Non-finite number ${value} in a view predicate`);
|
|
625
635
|
return String(value);
|
|
@@ -637,18 +647,18 @@ function predicateLiteral(value) {
|
|
|
637
647
|
* @throws for `matches` (`$regex`) and any other operator — a view predicate
|
|
638
648
|
* that cannot be rendered fails at sync instead of rendering wrong SQL.
|
|
639
649
|
*/
|
|
640
|
-
function queryNodeToSql(node, resolveFieldRef) {
|
|
650
|
+
function queryNodeToSql(node, resolveFieldRef, stringLiteral = sqlStringLiteral) {
|
|
641
651
|
if ("$and" in node) {
|
|
642
652
|
const children = node.$and;
|
|
643
653
|
if (children.length === 0) return EMPTY_AND.sql;
|
|
644
|
-
return children.map((n) => queryNodeToSql(n, resolveFieldRef)).join(" AND ");
|
|
654
|
+
return children.map((n) => queryNodeToSql(n, resolveFieldRef, stringLiteral)).join(" AND ");
|
|
645
655
|
}
|
|
646
656
|
if ("$or" in node) {
|
|
647
657
|
const children = node.$or;
|
|
648
658
|
if (children.length === 0) return EMPTY_OR.sql;
|
|
649
|
-
return `(${children.map((n) => queryNodeToSql(n, resolveFieldRef)).join(" OR ")})`;
|
|
659
|
+
return `(${children.map((n) => queryNodeToSql(n, resolveFieldRef, stringLiteral)).join(" OR ")})`;
|
|
650
660
|
}
|
|
651
|
-
if ("$not" in node) return `NOT (${queryNodeToSql(node.$not, resolveFieldRef)})`;
|
|
661
|
+
if ("$not" in node) return `NOT (${queryNodeToSql(node.$not, resolveFieldRef, stringLiteral)})`;
|
|
652
662
|
const comp = node;
|
|
653
663
|
const leftSql = resolveFieldRef(comp.left);
|
|
654
664
|
switch (comp.op) {
|
|
@@ -657,7 +667,7 @@ function queryNodeToSql(node, resolveFieldRef) {
|
|
|
657
667
|
case "$nin": {
|
|
658
668
|
const values = Array.isArray(comp.right) ? comp.right : [comp.right];
|
|
659
669
|
if (values.length === 0) return comp.op === "$in" ? EMPTY_OR.sql : EMPTY_AND.sql;
|
|
660
|
-
const list = values.map((v) => predicateLiteral(v)).join(", ");
|
|
670
|
+
const list = values.map((v) => predicateLiteral(v, stringLiteral)).join(", ");
|
|
661
671
|
return `${leftSql} ${comp.op === "$in" ? "IN" : "NOT IN"} (${list})`;
|
|
662
672
|
}
|
|
663
673
|
case "$regex": throw new Error("matches is not supported in view predicates");
|
|
@@ -667,7 +677,7 @@ function queryNodeToSql(node, resolveFieldRef) {
|
|
|
667
677
|
if (!sqlOp) throw new Error(`Operator "${comp.op}" is not supported in view predicates`);
|
|
668
678
|
if ((0, _atscript_db.isFieldRef)(comp.right)) return `${leftSql} ${sqlOp} ${resolveFieldRef(comp.right)}`;
|
|
669
679
|
if (comp.right === null || comp.right === void 0) return comp.op === "$ne" ? `${leftSql} IS NOT NULL` : `${leftSql} IS NULL`;
|
|
670
|
-
return `${leftSql} ${sqlOp} ${predicateLiteral(comp.right)}`;
|
|
680
|
+
return `${leftSql} ${sqlOp} ${predicateLiteral(comp.right, stringLiteral)}`;
|
|
671
681
|
}
|
|
672
682
|
//#endregion
|
|
673
683
|
//#region src/agg.ts
|
|
@@ -1001,7 +1011,7 @@ function viewAggExpr(dialect, c, resolveFieldRef) {
|
|
|
1001
1011
|
const star = c.aggField === "*";
|
|
1002
1012
|
const src = star ? "*" : viewSourceExpr(dialect, c);
|
|
1003
1013
|
if (!c.aggFilter) return renderAggCall(c.aggFn, src, c.viewColumn);
|
|
1004
|
-
const predicate = queryNodeToSql(c.aggFilter, resolveFieldRef);
|
|
1014
|
+
const predicate = queryNodeToSql(c.aggFilter, resolveFieldRef, dialect.stringLiteral);
|
|
1005
1015
|
const call = renderAggCall(c.aggFn, `CASE WHEN ${predicate} THEN ${star ? "1" : src} END`, c.viewColumn);
|
|
1006
1016
|
return c.aggFn === "sum" ? `COALESCE(${call}, 0)` : call;
|
|
1007
1017
|
}
|
|
@@ -1092,14 +1102,14 @@ function buildViewSelect(dialect, plan, columns, resolveFieldRef) {
|
|
|
1092
1102
|
const columnExpr = (c) => c.expr !== void 0 ? viewComputeExpr(dialect, c, columnByPath, resolveFieldRef, hasAggregates, computedSql) : c.aggFn ? viewAggExpr(dialect, c, resolveFieldRef) : viewSourceExpr(dialect, c);
|
|
1093
1103
|
let sql = `SELECT ${columns.map((c) => `${columnExpr(c)} AS ${dialect.quoteIdentifier(c.viewColumn)}`).join(", ")} FROM ${dialect.quoteIdentifier(plan.entryTable)}`;
|
|
1094
1104
|
for (const join of plan.joins) {
|
|
1095
|
-
const condition = queryNodeToSql(join.condition, resolveFieldRef);
|
|
1105
|
+
const condition = queryNodeToSql(join.condition, resolveFieldRef, dialect.stringLiteral);
|
|
1096
1106
|
const keyword = join.kind === "left" ? "LEFT JOIN" : "JOIN";
|
|
1097
1107
|
const target = join.scope === join.targetTable ? dialect.quoteIdentifier(join.targetTable) : `${dialect.quoteIdentifier(join.targetTable)} AS ${dialect.quoteIdentifier(join.scope)}`;
|
|
1098
1108
|
const onClause = join.first ? firstRowOn(dialect, join, target, condition, resolveFieldRef) : condition;
|
|
1099
1109
|
sql += ` ${keyword} ${target} ON ${onClause}`;
|
|
1100
1110
|
}
|
|
1101
1111
|
if (plan.filter) {
|
|
1102
|
-
const whereClause = queryNodeToSql(plan.filter, resolveFieldRef);
|
|
1112
|
+
const whereClause = queryNodeToSql(plan.filter, resolveFieldRef, dialect.stringLiteral);
|
|
1103
1113
|
sql += ` WHERE ${whereClause}`;
|
|
1104
1114
|
}
|
|
1105
1115
|
if (hasAggregates) {
|
|
@@ -1120,7 +1130,7 @@ function buildViewSelect(dialect, plan, columns, resolveFieldRef) {
|
|
|
1120
1130
|
if (!col.json) return expr;
|
|
1121
1131
|
return dialect.bucketAliasInHaving && groupedColumns().has(col.viewColumn.toLowerCase()) ? `MIN(${expr})` : havingGroupRef(dialect, expr, col.viewColumn);
|
|
1122
1132
|
};
|
|
1123
|
-
const havingClause = queryNodeToSql(plan.having, havingResolver);
|
|
1133
|
+
const havingClause = queryNodeToSql(plan.having, havingResolver, dialect.stringLiteral);
|
|
1124
1134
|
sql += ` HAVING ${havingClause}`;
|
|
1125
1135
|
}
|
|
1126
1136
|
}
|
|
@@ -1244,8 +1254,9 @@ function buildInsertMany(dialect, table, rows, columns = insertManyColumns(rows)
|
|
|
1244
1254
|
/**
|
|
1245
1255
|
* Builds a SELECT statement with optional sort, limit, offset, projection.
|
|
1246
1256
|
* `table` may be a derived source (a pruned view read, since 0.1.153).
|
|
1257
|
+
* `lock` (since 0.1.162) appends a row-locking clause — see {@link readLockSql}.
|
|
1247
1258
|
*/
|
|
1248
|
-
function buildSelect(dialect, table, where, controls) {
|
|
1259
|
+
function buildSelect(dialect, table, where, controls, lock) {
|
|
1249
1260
|
const cols = buildProjection(dialect, controls?.$select);
|
|
1250
1261
|
let sql = `SELECT ${fromSourceHint(table)}${cols} FROM ${fromSourceSql(dialect, table)} WHERE ${where.sql}`;
|
|
1251
1262
|
const params = [...where.params];
|
|
@@ -1260,12 +1271,26 @@ function buildSelect(dialect, table, where, controls) {
|
|
|
1260
1271
|
sql += ` OFFSET ?`;
|
|
1261
1272
|
params.push(controls.$skip);
|
|
1262
1273
|
}
|
|
1274
|
+
if (lock) sql += readLockSql(lock);
|
|
1263
1275
|
return finalizeParams(dialect, {
|
|
1264
1276
|
sql,
|
|
1265
1277
|
params
|
|
1266
1278
|
});
|
|
1267
1279
|
}
|
|
1268
1280
|
/**
|
|
1281
|
+
* The row-locking clause of a locking read — `" FOR UPDATE"`,
|
|
1282
|
+
* `" FOR SHARE NOWAIT"`, `" FOR UPDATE SKIP LOCKED"`, … (PostgreSQL and
|
|
1283
|
+
* MySQL 8 share the syntax); `""` without a lock. Goes after `LIMIT` /
|
|
1284
|
+
* `OFFSET`. @since 0.1.162
|
|
1285
|
+
*/
|
|
1286
|
+
function readLockSql(lock) {
|
|
1287
|
+
if (!lock) return "";
|
|
1288
|
+
const mode = lock.mode === "share" ? " FOR SHARE" : " FOR UPDATE";
|
|
1289
|
+
if (lock.wait === "nowait") return `${mode} NOWAIT`;
|
|
1290
|
+
if (lock.wait === "skipLocked") return `${mode} SKIP LOCKED`;
|
|
1291
|
+
return mode;
|
|
1292
|
+
}
|
|
1293
|
+
/**
|
|
1269
1294
|
* `"col" ASC, "other" DESC` of a physical `$sort` — `""` when it orders
|
|
1270
1295
|
* nothing. A key with a `$nulls` entry places NULL as asked
|
|
1271
1296
|
* ({@link nullsOrderSql}); `prefix` is prepended verbatim to each quoted
|
|
@@ -1352,13 +1377,14 @@ function replaceColumnsFor(fields, nativeFns) {
|
|
|
1352
1377
|
* when the engine owns its function default — so an UPDATE-based replace never
|
|
1353
1378
|
* retains a value the caller left out. This is the SQL counterpart of the
|
|
1354
1379
|
* whole-document replace the memory and MongoDB adapters do natively. The
|
|
1355
|
-
* version column is excluded (`buildUpdate` appends the OCC bump itself)
|
|
1356
|
-
*
|
|
1380
|
+
* version column is excluded (`buildUpdate` appends the OCC bump itself), and
|
|
1381
|
+
* so are `skip` columns (`@db.onUpdate.inc` counters, since 0.1.162 — the
|
|
1382
|
+
* statement increments them). Returns a new object; `data` is not mutated.
|
|
1357
1383
|
*/
|
|
1358
|
-
function fillReplacePayload(data, columns, versionColumn) {
|
|
1384
|
+
function fillReplacePayload(data, columns, versionColumn, skip) {
|
|
1359
1385
|
const full = { ...data };
|
|
1360
1386
|
for (const col of columns) {
|
|
1361
|
-
if (col.name === versionColumn || col.name in full) continue;
|
|
1387
|
+
if (col.name === versionColumn || col.name in full || skip?.includes(col.name)) continue;
|
|
1362
1388
|
full[col.name] = col.useDefault ? SQL_DEFAULT : null;
|
|
1363
1389
|
}
|
|
1364
1390
|
return full;
|
|
@@ -1381,6 +1407,9 @@ function fillReplacePayload(data, columns, versionColumn) {
|
|
|
1381
1407
|
* `AND <col> = ?` to the WHERE clause and pushes the value. Requires
|
|
1382
1408
|
* `versionColumn` (CAS targets that column); supplying `expectedVersion`
|
|
1383
1409
|
* without `versionColumn` is a programmer error and throws.
|
|
1410
|
+
*
|
|
1411
|
+
* `limit` renders `LIMIT n`; as `{ count, orderBy }` (since 0.1.162) also
|
|
1412
|
+
* `ORDER BY <orderBy>` before it (MySQL's bounded `UPDATE`).
|
|
1384
1413
|
*/
|
|
1385
1414
|
function buildUpdate(dialect, table, data, where, limit, ops, versionColumn, expectedVersion) {
|
|
1386
1415
|
const setClauses = [];
|
|
@@ -1414,10 +1443,8 @@ function buildUpdate(dialect, table, data, where, limit, ops, versionColumn, exp
|
|
|
1414
1443
|
whereParams.push(expectedVersion);
|
|
1415
1444
|
}
|
|
1416
1445
|
}
|
|
1417
|
-
let sql = `UPDATE ${dialect.quoteTable(table)} SET ${setClauses.join(", ")} WHERE ${whereSql}`;
|
|
1418
|
-
if (limit !== void 0) sql += ` LIMIT ${limit}`;
|
|
1419
1446
|
return finalizeParams(dialect, {
|
|
1420
|
-
sql,
|
|
1447
|
+
sql: `UPDATE ${dialect.quoteTable(table)} SET ${setClauses.join(", ")} WHERE ${whereSql}` + limitSql(limit),
|
|
1421
1448
|
params: [
|
|
1422
1449
|
...params,
|
|
1423
1450
|
...where.params,
|
|
@@ -1426,16 +1453,21 @@ function buildUpdate(dialect, table, data, where, limit, ops, versionColumn, exp
|
|
|
1426
1453
|
});
|
|
1427
1454
|
}
|
|
1428
1455
|
/**
|
|
1429
|
-
* Builds a DELETE ... WHERE statement with optional LIMIT
|
|
1456
|
+
* Builds a DELETE ... WHERE statement with an optional LIMIT — as
|
|
1457
|
+
* `{ count, orderBy }` (since 0.1.162) with an ORDER BY before it.
|
|
1430
1458
|
*/
|
|
1431
1459
|
function buildDelete(dialect, table, where, limit) {
|
|
1432
|
-
let sql = `DELETE FROM ${dialect.quoteTable(table)} WHERE ${where.sql}`;
|
|
1433
|
-
if (limit !== void 0) sql += ` LIMIT ${limit}`;
|
|
1434
1460
|
return finalizeParams(dialect, {
|
|
1435
|
-
sql,
|
|
1461
|
+
sql: `DELETE FROM ${dialect.quoteTable(table)} WHERE ${where.sql}` + limitSql(limit),
|
|
1436
1462
|
params: where.params
|
|
1437
1463
|
});
|
|
1438
1464
|
}
|
|
1465
|
+
/** ` [ORDER BY …] LIMIT n` of an UPDATE / DELETE, or `""` when unbounded. */
|
|
1466
|
+
function limitSql(limit) {
|
|
1467
|
+
if (limit === void 0) return "";
|
|
1468
|
+
if (typeof limit === "number") return ` LIMIT ${limit}`;
|
|
1469
|
+
return (limit.orderBy ? ` ORDER BY ${limit.orderBy}` : "") + ` LIMIT ${limit.count}`;
|
|
1470
|
+
}
|
|
1439
1471
|
/**
|
|
1440
1472
|
* The expression a `@db.column.derived` column is generated from: the
|
|
1441
1473
|
* dialect's typed {@link SqlDialect.jsonExtract} over the (unqualified,
|
|
@@ -1483,6 +1515,29 @@ function buildKeyViolationCount(dialect, quotedTable, columns) {
|
|
|
1483
1515
|
const noNull = cols.map((c) => `${c} IS NOT NULL`).join(" AND ");
|
|
1484
1516
|
return `SELECT (SELECT COUNT(*) FROM ${quotedTable} WHERE ${anyNull}) + (SELECT COALESCE(SUM(${dialect.quoteIdentifier("n")}), 0) FROM (SELECT COUNT(*) AS ${dialect.quoteIdentifier("n")} FROM ${quotedTable} WHERE ${noNull} GROUP BY ${cols.join(", ")} HAVING COUNT(*) > 1) ${dialect.quoteIdentifier("d")}) AS ` + dialect.quoteIdentifier("violations");
|
|
1485
1517
|
}
|
|
1518
|
+
/**
|
|
1519
|
+
* Narrows an UPDATE / DELETE to the first rows of a bounded write
|
|
1520
|
+
* ({@link TDbWriteLimit}, since 0.1.162) on engines without
|
|
1521
|
+
* `UPDATE/DELETE … ORDER BY … LIMIT`: `where AND <key> IN (SELECT <key> FROM
|
|
1522
|
+
* <table> WHERE where ORDER BY … LIMIT n)`. `keyCols` are already-quoted key
|
|
1523
|
+
* expressions (the primary key columns, or SQLite's `rowid`). The filter is
|
|
1524
|
+
* repeated on the outer statement so a row changed out of it meanwhile is
|
|
1525
|
+
* left alone. Unfinalized (`?` placeholders).
|
|
1526
|
+
*/
|
|
1527
|
+
function writeLimitWhere(dialect, quotedTable, keyCols, where, bound) {
|
|
1528
|
+
const cols = keyCols.join(", ");
|
|
1529
|
+
const key = keyCols.length === 1 ? cols : `(${cols})`;
|
|
1530
|
+
const order = orderByList(dialect, bound.sort, bound.nulls);
|
|
1531
|
+
return {
|
|
1532
|
+
sql: `${where.sql} AND ${key} IN (SELECT ${cols} FROM ${quotedTable} WHERE ${where.sql} ORDER BY ${order} LIMIT ${writeLimitCount(bound)})`,
|
|
1533
|
+
params: [...where.params, ...where.params]
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1536
|
+
/** The validated row count of a bounded write — a positive safe integer, rendered inline. */
|
|
1537
|
+
function writeLimitCount(bound) {
|
|
1538
|
+
if (!Number.isSafeInteger(bound.limit) || bound.limit < 1) throw new Error(`Invalid write limit: ${String(bound.limit)}`);
|
|
1539
|
+
return bound.limit;
|
|
1540
|
+
}
|
|
1486
1541
|
//#endregion
|
|
1487
1542
|
//#region src/geo.ts
|
|
1488
1543
|
/**
|
|
@@ -1851,6 +1906,7 @@ exports.parseRegexString = parseRegexString;
|
|
|
1851
1906
|
exports.queryNodeToSql = queryNodeToSql;
|
|
1852
1907
|
exports.queryOpToSql = queryOpToSql;
|
|
1853
1908
|
exports.quotedJsonPathSegments = quotedJsonPathSegments;
|
|
1909
|
+
exports.readLockSql = readLockSql;
|
|
1854
1910
|
exports.refActionToSql = refActionToSql;
|
|
1855
1911
|
exports.renameGeoDistance = renameGeoDistance;
|
|
1856
1912
|
exports.renderArith = renderArith;
|
|
@@ -1859,5 +1915,8 @@ exports.sqlStringLiteral = sqlStringLiteral;
|
|
|
1859
1915
|
exports.sqlTimeZoneLiteral = sqlTimeZoneLiteral;
|
|
1860
1916
|
exports.stripPartitionRowNumber = stripPartitionRowNumber;
|
|
1861
1917
|
exports.toSqlValue = toSqlValue;
|
|
1918
|
+
exports.unquoteSqlStringLiteral = unquoteSqlStringLiteral;
|
|
1862
1919
|
exports.vectorDistanceSource = vectorDistanceSource;
|
|
1863
1920
|
exports.viewReadSource = viewReadSource;
|
|
1921
|
+
exports.writeLimitCount = writeLimitCount;
|
|
1922
|
+
exports.writeLimitWhere = writeLimitWhere;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AtscriptExprNode, AtscriptQueryFieldRef, AtscriptQueryNode, DbControls, DbError, DbQuery, TDbDefaultFn, TDbFieldMeta, TDbForeignKey, TDbReferentialAction, TFieldOps, TJsonCopyTarget, TReadColumnsKind, TResolvedBucket, TViewColumnMapping, TViewJsonType, TViewPlan, UniquSelect } from "@atscript/db";
|
|
1
|
+
import { AtscriptExprNode, AtscriptQueryFieldRef, AtscriptQueryNode, DbControls, DbError, DbQuery, TDbDefaultFn, TDbFieldMeta, TDbForeignKey, TDbReadLock, TDbReferentialAction, TDbWriteLimit, TFieldOps, TJsonCopyTarget, TReadColumnsKind, TResolvedBucket, TViewColumnMapping, TViewJsonType, TViewPlan, UniquSelect } from "@atscript/db";
|
|
2
2
|
import { FilterExpr, FilterVisitor, NullsPlacement } from "@uniqu/core";
|
|
3
3
|
import { TDbAggregateFn } from "@atscript/db/agg";
|
|
4
4
|
|
|
@@ -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
|
+
* Renders a string as an SQL string literal inlined into SQL text (view
|
|
24
|
+
* predicates, JSON paths, defaults): `sqlStringLiteral` (quotes doubled),
|
|
25
|
+
* or — for a dialect whose literals treat `\` as an escape (MySQL) — one
|
|
26
|
+
* that escapes it too.
|
|
27
|
+
* @since 0.1.162
|
|
28
|
+
*/
|
|
29
|
+
stringLiteral: (value: string) => string;
|
|
22
30
|
/**
|
|
23
31
|
* The optimizer hint asking the engine to MERGE the derived table `alias`
|
|
24
32
|
* into the SELECT reading it (MySQL: `/*+ MERGE(\`alias\`) *\/`) — carried
|
|
@@ -251,8 +259,10 @@ declare function orderKeySql(dialect: SqlDialect, expr: string, desc: boolean, n
|
|
|
251
259
|
* reads the same on every dialect (SQLite's quoted path labels have no escape
|
|
252
260
|
* syntax), so it is rejected rather than escaped.
|
|
253
261
|
*
|
|
262
|
+
* An empty path (the JSON root — a primitive JSON column's value) has no segments.
|
|
263
|
+
*
|
|
254
264
|
* @since 0.1.136
|
|
255
|
-
* @throws for
|
|
265
|
+
* @throws for a segment that is empty or holds such a character.
|
|
256
266
|
*/
|
|
257
267
|
declare function quotedJsonPathSegments(path: readonly string[]): string[];
|
|
258
268
|
declare const EMPTY_AND: TSqlFragment;
|
|
@@ -555,8 +565,16 @@ declare function buildInsertMany(dialect: SqlDialect, table: string, rows: reado
|
|
|
555
565
|
/**
|
|
556
566
|
* Builds a SELECT statement with optional sort, limit, offset, projection.
|
|
557
567
|
* `table` may be a derived source (a pruned view read, since 0.1.153).
|
|
568
|
+
* `lock` (since 0.1.162) appends a row-locking clause — see {@link readLockSql}.
|
|
569
|
+
*/
|
|
570
|
+
declare function buildSelect(dialect: SqlDialect, table: TSqlFromSource, where: TSqlFragment, controls?: DbControls, lock?: TDbReadLock): TSqlFragment;
|
|
571
|
+
/**
|
|
572
|
+
* The row-locking clause of a locking read — `" FOR UPDATE"`,
|
|
573
|
+
* `" FOR SHARE NOWAIT"`, `" FOR UPDATE SKIP LOCKED"`, … (PostgreSQL and
|
|
574
|
+
* MySQL 8 share the syntax); `""` without a lock. Goes after `LIMIT` /
|
|
575
|
+
* `OFFSET`. @since 0.1.162
|
|
558
576
|
*/
|
|
559
|
-
declare function
|
|
577
|
+
declare function readLockSql(lock: TDbReadLock | undefined): string;
|
|
560
578
|
/**
|
|
561
579
|
* `"col" ASC, "other" DESC` of a physical `$sort` — `""` when it orders
|
|
562
580
|
* nothing. A key with a `$nulls` entry places NULL as asked
|
|
@@ -618,10 +636,11 @@ declare function replaceColumnsFor(fields: readonly TDbFieldMeta[], nativeFns: R
|
|
|
618
636
|
* when the engine owns its function default — so an UPDATE-based replace never
|
|
619
637
|
* retains a value the caller left out. This is the SQL counterpart of the
|
|
620
638
|
* whole-document replace the memory and MongoDB adapters do natively. The
|
|
621
|
-
* version column is excluded (`buildUpdate` appends the OCC bump itself)
|
|
622
|
-
*
|
|
639
|
+
* version column is excluded (`buildUpdate` appends the OCC bump itself), and
|
|
640
|
+
* so are `skip` columns (`@db.onUpdate.inc` counters, since 0.1.162 — the
|
|
641
|
+
* statement increments them). Returns a new object; `data` is not mutated.
|
|
623
642
|
*/
|
|
624
|
-
declare function fillReplacePayload(data: Record<string, unknown>, columns: readonly TReplaceColumn[], versionColumn?: string): Record<string, unknown>;
|
|
643
|
+
declare function fillReplacePayload(data: Record<string, unknown>, columns: readonly TReplaceColumn[], versionColumn?: string, skip?: readonly string[]): Record<string, unknown>;
|
|
625
644
|
/**
|
|
626
645
|
* Builds an UPDATE ... SET ... WHERE statement with optional LIMIT.
|
|
627
646
|
*
|
|
@@ -640,12 +659,26 @@ declare function fillReplacePayload(data: Record<string, unknown>, columns: read
|
|
|
640
659
|
* `AND <col> = ?` to the WHERE clause and pushes the value. Requires
|
|
641
660
|
* `versionColumn` (CAS targets that column); supplying `expectedVersion`
|
|
642
661
|
* without `versionColumn` is a programmer error and throws.
|
|
662
|
+
*
|
|
663
|
+
* `limit` renders `LIMIT n`; as `{ count, orderBy }` (since 0.1.162) also
|
|
664
|
+
* `ORDER BY <orderBy>` before it (MySQL's bounded `UPDATE`).
|
|
643
665
|
*/
|
|
644
|
-
declare function buildUpdate(dialect: SqlDialect, table: string, data: Record<string, unknown>, where: TSqlFragment, limit?: number, ops?: TFieldOps, versionColumn?: string, expectedVersion?: number): TSqlFragment;
|
|
666
|
+
declare function buildUpdate(dialect: SqlDialect, table: string, data: Record<string, unknown>, where: TSqlFragment, limit?: number | TSqlWriteLimit, ops?: TFieldOps, versionColumn?: string, expectedVersion?: number): TSqlFragment;
|
|
645
667
|
/**
|
|
646
|
-
* Builds a DELETE ... WHERE statement with optional LIMIT
|
|
668
|
+
* Builds a DELETE ... WHERE statement with an optional LIMIT — as
|
|
669
|
+
* `{ count, orderBy }` (since 0.1.162) with an ORDER BY before it.
|
|
647
670
|
*/
|
|
648
|
-
declare function buildDelete(dialect: SqlDialect, table: string, where: TSqlFragment, limit?: number): TSqlFragment;
|
|
671
|
+
declare function buildDelete(dialect: SqlDialect, table: string, where: TSqlFragment, limit?: number | TSqlWriteLimit): TSqlFragment;
|
|
672
|
+
/**
|
|
673
|
+
* The row bound of an `UPDATE` / `DELETE` on engines that render
|
|
674
|
+
* `ORDER BY … LIMIT n` on writes (MySQL; since 0.1.162).
|
|
675
|
+
*/
|
|
676
|
+
interface TSqlWriteLimit {
|
|
677
|
+
/** Rows the statement may touch (rendered inline). */
|
|
678
|
+
count: number;
|
|
679
|
+
/** Rendered `ORDER BY` list (see {@link orderByList}). */
|
|
680
|
+
orderBy?: string;
|
|
681
|
+
}
|
|
649
682
|
/**
|
|
650
683
|
* The expression a `@db.column.derived` column is generated from: the
|
|
651
684
|
* dialect's typed {@link SqlDialect.jsonExtract} over the (unqualified,
|
|
@@ -673,19 +706,39 @@ declare function buildProjection(dialect: SqlDialect, select?: UniquSelect, qual
|
|
|
673
706
|
* @since 0.1.155
|
|
674
707
|
*/
|
|
675
708
|
declare function buildKeyViolationCount(dialect: SqlDialect, quotedTable: string, columns: readonly string[]): string;
|
|
709
|
+
/**
|
|
710
|
+
* Narrows an UPDATE / DELETE to the first rows of a bounded write
|
|
711
|
+
* ({@link TDbWriteLimit}, since 0.1.162) on engines without
|
|
712
|
+
* `UPDATE/DELETE … ORDER BY … LIMIT`: `where AND <key> IN (SELECT <key> FROM
|
|
713
|
+
* <table> WHERE where ORDER BY … LIMIT n)`. `keyCols` are already-quoted key
|
|
714
|
+
* expressions (the primary key columns, or SQLite's `rowid`). The filter is
|
|
715
|
+
* repeated on the outer statement so a row changed out of it meanwhile is
|
|
716
|
+
* left alone. Unfinalized (`?` placeholders).
|
|
717
|
+
*/
|
|
718
|
+
declare function writeLimitWhere(dialect: SqlDialect, quotedTable: string, keyCols: readonly string[], where: TSqlFragment, bound: TDbWriteLimit): TSqlFragment;
|
|
719
|
+
/** The validated row count of a bounded write — a positive safe integer, rendered inline. */
|
|
720
|
+
declare function writeLimitCount(bound: TDbWriteLimit): number;
|
|
676
721
|
//#endregion
|
|
677
722
|
//#region src/common.d.ts
|
|
678
723
|
/** Formats a string value as a SQL literal with single-quote escaping. */
|
|
679
724
|
declare function sqlStringLiteral(value: string): string;
|
|
725
|
+
/**
|
|
726
|
+
* The value of a single-quoted SQL string literal (`'it''s'` → `it's`) — as
|
|
727
|
+
* engines report a column default — or `undefined` when `literal` is not one.
|
|
728
|
+
* @since 0.1.162
|
|
729
|
+
*/
|
|
730
|
+
declare function unquoteSqlStringLiteral(literal: string): string | undefined;
|
|
680
731
|
/**
|
|
681
732
|
* The SQL string literal of a `$`-rooted JSON path with every segment quoted
|
|
682
733
|
* (`'$."a"."b"'`) — the path argument of SQLite's and MySQL's `json_extract` /
|
|
683
|
-
* `json_type
|
|
734
|
+
* `json_type`; an empty path is the JSON root (`'$'`).
|
|
684
735
|
*
|
|
685
736
|
* @since 0.1.136
|
|
686
737
|
* @throws as {@link quotedJsonPathSegments}.
|
|
687
738
|
*/
|
|
688
|
-
declare function jsonDollarPath(path: readonly string[])
|
|
739
|
+
declare function jsonDollarPath(path: readonly string[], /** The dialect's string literal (`SqlDialect.stringLiteral`). @since 0.1.162 */
|
|
740
|
+
|
|
741
|
+
stringLiteral?: (value: string) => string): string;
|
|
689
742
|
/**
|
|
690
743
|
* A calendar bucket's time zone as an inlined SQL string literal (`'Europe/Berlin'`).
|
|
691
744
|
*
|
|
@@ -714,7 +767,9 @@ declare function defaultValueForType(designType: string): string;
|
|
|
714
767
|
* respecting the field's designType. Booleans become 0/1, numbers stay unquoted,
|
|
715
768
|
* strings are single-quote-escaped.
|
|
716
769
|
*/
|
|
717
|
-
declare function defaultValueToSqlLiteral(designType: string, value: string)
|
|
770
|
+
declare function defaultValueToSqlLiteral(designType: string, value: string, /** The dialect's string literal (`SqlDialect.stringLiteral`). @since 0.1.162 */
|
|
771
|
+
|
|
772
|
+
stringLiteral?: (value: string) => string): string;
|
|
718
773
|
declare const queryOpToSql: Record<string, string>;
|
|
719
774
|
/**
|
|
720
775
|
* Renders an AtscriptQueryNode tree to raw SQL (no parameters -- for DDL use only).
|
|
@@ -726,7 +781,9 @@ declare const queryOpToSql: Record<string, string>;
|
|
|
726
781
|
* @throws for `matches` (`$regex`) and any other operator — a view predicate
|
|
727
782
|
* that cannot be rendered fails at sync instead of rendering wrong SQL.
|
|
728
783
|
*/
|
|
729
|
-
declare function queryNodeToSql(node: AtscriptQueryNode, resolveFieldRef: (ref: AtscriptQueryFieldRef) => string)
|
|
784
|
+
declare function queryNodeToSql(node: AtscriptQueryNode, resolveFieldRef: (ref: AtscriptQueryFieldRef) => string, /** The dialect's string literal (`SqlDialect.stringLiteral`). @since 0.1.162 */
|
|
785
|
+
|
|
786
|
+
stringLiteral?: (value: string) => string): string;
|
|
730
787
|
//#endregion
|
|
731
788
|
//#region src/agg.d.ts
|
|
732
789
|
/**
|
|
@@ -825,4 +882,4 @@ declare function buildJsonColumnCopy(dialect: SqlDialect, table: string, source:
|
|
|
825
882
|
*/
|
|
826
883
|
declare function buildJsonifyText(dialect: SqlDialect, table: string, column: string): string;
|
|
827
884
|
//#endregion
|
|
828
|
-
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, buildJsonColumnCopy, buildJsonifyText, buildKeyViolationCount, 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 };
|
|
885
|
+
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, type TSqlWriteLimit, VECTOR_DISTANCE_ALIAS, arithOverflowError, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildJsonColumnCopy, buildJsonifyText, buildKeyViolationCount, 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, readLockSql, refActionToSql, renameGeoDistance, renderArith, replaceColumnsFor, sqlStringLiteral, sqlTimeZoneLiteral, stripPartitionRowNumber, toSqlValue, unquoteSqlStringLiteral, vectorDistanceSource, viewReadSource, writeLimitCount, writeLimitWhere };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AtscriptExprNode, AtscriptQueryFieldRef, AtscriptQueryNode, DbControls, DbError, DbQuery, TDbDefaultFn, TDbFieldMeta, TDbForeignKey, TDbReferentialAction, TFieldOps, TJsonCopyTarget, TReadColumnsKind, TResolvedBucket, TViewColumnMapping, TViewJsonType, TViewPlan, UniquSelect } from "@atscript/db";
|
|
1
|
+
import { AtscriptExprNode, AtscriptQueryFieldRef, AtscriptQueryNode, DbControls, DbError, DbQuery, TDbDefaultFn, TDbFieldMeta, TDbForeignKey, TDbReadLock, TDbReferentialAction, TDbWriteLimit, TFieldOps, TJsonCopyTarget, TReadColumnsKind, TResolvedBucket, TViewColumnMapping, TViewJsonType, TViewPlan, UniquSelect } from "@atscript/db";
|
|
2
2
|
import { FilterExpr, FilterVisitor, NullsPlacement } from "@uniqu/core";
|
|
3
3
|
import { TDbAggregateFn } from "@atscript/db/agg";
|
|
4
4
|
|
|
@@ -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
|
+
* Renders a string as an SQL string literal inlined into SQL text (view
|
|
24
|
+
* predicates, JSON paths, defaults): `sqlStringLiteral` (quotes doubled),
|
|
25
|
+
* or — for a dialect whose literals treat `\` as an escape (MySQL) — one
|
|
26
|
+
* that escapes it too.
|
|
27
|
+
* @since 0.1.162
|
|
28
|
+
*/
|
|
29
|
+
stringLiteral: (value: string) => string;
|
|
22
30
|
/**
|
|
23
31
|
* The optimizer hint asking the engine to MERGE the derived table `alias`
|
|
24
32
|
* into the SELECT reading it (MySQL: `/*+ MERGE(\`alias\`) *\/`) — carried
|
|
@@ -251,8 +259,10 @@ declare function orderKeySql(dialect: SqlDialect, expr: string, desc: boolean, n
|
|
|
251
259
|
* reads the same on every dialect (SQLite's quoted path labels have no escape
|
|
252
260
|
* syntax), so it is rejected rather than escaped.
|
|
253
261
|
*
|
|
262
|
+
* An empty path (the JSON root — a primitive JSON column's value) has no segments.
|
|
263
|
+
*
|
|
254
264
|
* @since 0.1.136
|
|
255
|
-
* @throws for
|
|
265
|
+
* @throws for a segment that is empty or holds such a character.
|
|
256
266
|
*/
|
|
257
267
|
declare function quotedJsonPathSegments(path: readonly string[]): string[];
|
|
258
268
|
declare const EMPTY_AND: TSqlFragment;
|
|
@@ -555,8 +565,16 @@ declare function buildInsertMany(dialect: SqlDialect, table: string, rows: reado
|
|
|
555
565
|
/**
|
|
556
566
|
* Builds a SELECT statement with optional sort, limit, offset, projection.
|
|
557
567
|
* `table` may be a derived source (a pruned view read, since 0.1.153).
|
|
568
|
+
* `lock` (since 0.1.162) appends a row-locking clause — see {@link readLockSql}.
|
|
569
|
+
*/
|
|
570
|
+
declare function buildSelect(dialect: SqlDialect, table: TSqlFromSource, where: TSqlFragment, controls?: DbControls, lock?: TDbReadLock): TSqlFragment;
|
|
571
|
+
/**
|
|
572
|
+
* The row-locking clause of a locking read — `" FOR UPDATE"`,
|
|
573
|
+
* `" FOR SHARE NOWAIT"`, `" FOR UPDATE SKIP LOCKED"`, … (PostgreSQL and
|
|
574
|
+
* MySQL 8 share the syntax); `""` without a lock. Goes after `LIMIT` /
|
|
575
|
+
* `OFFSET`. @since 0.1.162
|
|
558
576
|
*/
|
|
559
|
-
declare function
|
|
577
|
+
declare function readLockSql(lock: TDbReadLock | undefined): string;
|
|
560
578
|
/**
|
|
561
579
|
* `"col" ASC, "other" DESC` of a physical `$sort` — `""` when it orders
|
|
562
580
|
* nothing. A key with a `$nulls` entry places NULL as asked
|
|
@@ -618,10 +636,11 @@ declare function replaceColumnsFor(fields: readonly TDbFieldMeta[], nativeFns: R
|
|
|
618
636
|
* when the engine owns its function default — so an UPDATE-based replace never
|
|
619
637
|
* retains a value the caller left out. This is the SQL counterpart of the
|
|
620
638
|
* whole-document replace the memory and MongoDB adapters do natively. The
|
|
621
|
-
* version column is excluded (`buildUpdate` appends the OCC bump itself)
|
|
622
|
-
*
|
|
639
|
+
* version column is excluded (`buildUpdate` appends the OCC bump itself), and
|
|
640
|
+
* so are `skip` columns (`@db.onUpdate.inc` counters, since 0.1.162 — the
|
|
641
|
+
* statement increments them). Returns a new object; `data` is not mutated.
|
|
623
642
|
*/
|
|
624
|
-
declare function fillReplacePayload(data: Record<string, unknown>, columns: readonly TReplaceColumn[], versionColumn?: string): Record<string, unknown>;
|
|
643
|
+
declare function fillReplacePayload(data: Record<string, unknown>, columns: readonly TReplaceColumn[], versionColumn?: string, skip?: readonly string[]): Record<string, unknown>;
|
|
625
644
|
/**
|
|
626
645
|
* Builds an UPDATE ... SET ... WHERE statement with optional LIMIT.
|
|
627
646
|
*
|
|
@@ -640,12 +659,26 @@ declare function fillReplacePayload(data: Record<string, unknown>, columns: read
|
|
|
640
659
|
* `AND <col> = ?` to the WHERE clause and pushes the value. Requires
|
|
641
660
|
* `versionColumn` (CAS targets that column); supplying `expectedVersion`
|
|
642
661
|
* without `versionColumn` is a programmer error and throws.
|
|
662
|
+
*
|
|
663
|
+
* `limit` renders `LIMIT n`; as `{ count, orderBy }` (since 0.1.162) also
|
|
664
|
+
* `ORDER BY <orderBy>` before it (MySQL's bounded `UPDATE`).
|
|
643
665
|
*/
|
|
644
|
-
declare function buildUpdate(dialect: SqlDialect, table: string, data: Record<string, unknown>, where: TSqlFragment, limit?: number, ops?: TFieldOps, versionColumn?: string, expectedVersion?: number): TSqlFragment;
|
|
666
|
+
declare function buildUpdate(dialect: SqlDialect, table: string, data: Record<string, unknown>, where: TSqlFragment, limit?: number | TSqlWriteLimit, ops?: TFieldOps, versionColumn?: string, expectedVersion?: number): TSqlFragment;
|
|
645
667
|
/**
|
|
646
|
-
* Builds a DELETE ... WHERE statement with optional LIMIT
|
|
668
|
+
* Builds a DELETE ... WHERE statement with an optional LIMIT — as
|
|
669
|
+
* `{ count, orderBy }` (since 0.1.162) with an ORDER BY before it.
|
|
647
670
|
*/
|
|
648
|
-
declare function buildDelete(dialect: SqlDialect, table: string, where: TSqlFragment, limit?: number): TSqlFragment;
|
|
671
|
+
declare function buildDelete(dialect: SqlDialect, table: string, where: TSqlFragment, limit?: number | TSqlWriteLimit): TSqlFragment;
|
|
672
|
+
/**
|
|
673
|
+
* The row bound of an `UPDATE` / `DELETE` on engines that render
|
|
674
|
+
* `ORDER BY … LIMIT n` on writes (MySQL; since 0.1.162).
|
|
675
|
+
*/
|
|
676
|
+
interface TSqlWriteLimit {
|
|
677
|
+
/** Rows the statement may touch (rendered inline). */
|
|
678
|
+
count: number;
|
|
679
|
+
/** Rendered `ORDER BY` list (see {@link orderByList}). */
|
|
680
|
+
orderBy?: string;
|
|
681
|
+
}
|
|
649
682
|
/**
|
|
650
683
|
* The expression a `@db.column.derived` column is generated from: the
|
|
651
684
|
* dialect's typed {@link SqlDialect.jsonExtract} over the (unqualified,
|
|
@@ -673,19 +706,39 @@ declare function buildProjection(dialect: SqlDialect, select?: UniquSelect, qual
|
|
|
673
706
|
* @since 0.1.155
|
|
674
707
|
*/
|
|
675
708
|
declare function buildKeyViolationCount(dialect: SqlDialect, quotedTable: string, columns: readonly string[]): string;
|
|
709
|
+
/**
|
|
710
|
+
* Narrows an UPDATE / DELETE to the first rows of a bounded write
|
|
711
|
+
* ({@link TDbWriteLimit}, since 0.1.162) on engines without
|
|
712
|
+
* `UPDATE/DELETE … ORDER BY … LIMIT`: `where AND <key> IN (SELECT <key> FROM
|
|
713
|
+
* <table> WHERE where ORDER BY … LIMIT n)`. `keyCols` are already-quoted key
|
|
714
|
+
* expressions (the primary key columns, or SQLite's `rowid`). The filter is
|
|
715
|
+
* repeated on the outer statement so a row changed out of it meanwhile is
|
|
716
|
+
* left alone. Unfinalized (`?` placeholders).
|
|
717
|
+
*/
|
|
718
|
+
declare function writeLimitWhere(dialect: SqlDialect, quotedTable: string, keyCols: readonly string[], where: TSqlFragment, bound: TDbWriteLimit): TSqlFragment;
|
|
719
|
+
/** The validated row count of a bounded write — a positive safe integer, rendered inline. */
|
|
720
|
+
declare function writeLimitCount(bound: TDbWriteLimit): number;
|
|
676
721
|
//#endregion
|
|
677
722
|
//#region src/common.d.ts
|
|
678
723
|
/** Formats a string value as a SQL literal with single-quote escaping. */
|
|
679
724
|
declare function sqlStringLiteral(value: string): string;
|
|
725
|
+
/**
|
|
726
|
+
* The value of a single-quoted SQL string literal (`'it''s'` → `it's`) — as
|
|
727
|
+
* engines report a column default — or `undefined` when `literal` is not one.
|
|
728
|
+
* @since 0.1.162
|
|
729
|
+
*/
|
|
730
|
+
declare function unquoteSqlStringLiteral(literal: string): string | undefined;
|
|
680
731
|
/**
|
|
681
732
|
* The SQL string literal of a `$`-rooted JSON path with every segment quoted
|
|
682
733
|
* (`'$."a"."b"'`) — the path argument of SQLite's and MySQL's `json_extract` /
|
|
683
|
-
* `json_type
|
|
734
|
+
* `json_type`; an empty path is the JSON root (`'$'`).
|
|
684
735
|
*
|
|
685
736
|
* @since 0.1.136
|
|
686
737
|
* @throws as {@link quotedJsonPathSegments}.
|
|
687
738
|
*/
|
|
688
|
-
declare function jsonDollarPath(path: readonly string[])
|
|
739
|
+
declare function jsonDollarPath(path: readonly string[], /** The dialect's string literal (`SqlDialect.stringLiteral`). @since 0.1.162 */
|
|
740
|
+
|
|
741
|
+
stringLiteral?: (value: string) => string): string;
|
|
689
742
|
/**
|
|
690
743
|
* A calendar bucket's time zone as an inlined SQL string literal (`'Europe/Berlin'`).
|
|
691
744
|
*
|
|
@@ -714,7 +767,9 @@ declare function defaultValueForType(designType: string): string;
|
|
|
714
767
|
* respecting the field's designType. Booleans become 0/1, numbers stay unquoted,
|
|
715
768
|
* strings are single-quote-escaped.
|
|
716
769
|
*/
|
|
717
|
-
declare function defaultValueToSqlLiteral(designType: string, value: string)
|
|
770
|
+
declare function defaultValueToSqlLiteral(designType: string, value: string, /** The dialect's string literal (`SqlDialect.stringLiteral`). @since 0.1.162 */
|
|
771
|
+
|
|
772
|
+
stringLiteral?: (value: string) => string): string;
|
|
718
773
|
declare const queryOpToSql: Record<string, string>;
|
|
719
774
|
/**
|
|
720
775
|
* Renders an AtscriptQueryNode tree to raw SQL (no parameters -- for DDL use only).
|
|
@@ -726,7 +781,9 @@ declare const queryOpToSql: Record<string, string>;
|
|
|
726
781
|
* @throws for `matches` (`$regex`) and any other operator — a view predicate
|
|
727
782
|
* that cannot be rendered fails at sync instead of rendering wrong SQL.
|
|
728
783
|
*/
|
|
729
|
-
declare function queryNodeToSql(node: AtscriptQueryNode, resolveFieldRef: (ref: AtscriptQueryFieldRef) => string)
|
|
784
|
+
declare function queryNodeToSql(node: AtscriptQueryNode, resolveFieldRef: (ref: AtscriptQueryFieldRef) => string, /** The dialect's string literal (`SqlDialect.stringLiteral`). @since 0.1.162 */
|
|
785
|
+
|
|
786
|
+
stringLiteral?: (value: string) => string): string;
|
|
730
787
|
//#endregion
|
|
731
788
|
//#region src/agg.d.ts
|
|
732
789
|
/**
|
|
@@ -825,4 +882,4 @@ declare function buildJsonColumnCopy(dialect: SqlDialect, table: string, source:
|
|
|
825
882
|
*/
|
|
826
883
|
declare function buildJsonifyText(dialect: SqlDialect, table: string, column: string): string;
|
|
827
884
|
//#endregion
|
|
828
|
-
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, buildJsonColumnCopy, buildJsonifyText, buildKeyViolationCount, 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 };
|
|
885
|
+
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, type TSqlWriteLimit, VECTOR_DISTANCE_ALIAS, arithOverflowError, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildJsonColumnCopy, buildJsonifyText, buildKeyViolationCount, 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, readLockSql, refActionToSql, renameGeoDistance, renderArith, replaceColumnsFor, sqlStringLiteral, sqlTimeZoneLiteral, stripPartitionRowNumber, toSqlValue, unquoteSqlStringLiteral, vectorDistanceSource, viewReadSource, writeLimitCount, writeLimitWhere };
|
package/dist/index.mjs
CHANGED
|
@@ -212,11 +212,12 @@ function orderKeySql(dialect, expr, desc, nulls, nullTest) {
|
|
|
212
212
|
* reads the same on every dialect (SQLite's quoted path labels have no escape
|
|
213
213
|
* syntax), so it is rejected rather than escaped.
|
|
214
214
|
*
|
|
215
|
+
* An empty path (the JSON root — a primitive JSON column's value) has no segments.
|
|
216
|
+
*
|
|
215
217
|
* @since 0.1.136
|
|
216
|
-
* @throws for
|
|
218
|
+
* @throws for a segment that is empty or holds such a character.
|
|
217
219
|
*/
|
|
218
220
|
function quotedJsonPathSegments(path) {
|
|
219
|
-
if (path.length === 0) throw new Error("JSON extraction needs a path below the JSON column");
|
|
220
221
|
return path.map((seg) => {
|
|
221
222
|
let ok = seg.length > 0;
|
|
222
223
|
for (let i = 0; ok && i < seg.length; i++) {
|
|
@@ -521,15 +522,24 @@ function sqlStringLiteral(value) {
|
|
|
521
522
|
return `'${value.replace(/'/g, "''")}'`;
|
|
522
523
|
}
|
|
523
524
|
/**
|
|
525
|
+
* The value of a single-quoted SQL string literal (`'it''s'` → `it's`) — as
|
|
526
|
+
* engines report a column default — or `undefined` when `literal` is not one.
|
|
527
|
+
* @since 0.1.162
|
|
528
|
+
*/
|
|
529
|
+
function unquoteSqlStringLiteral(literal) {
|
|
530
|
+
if (literal.length < 2 || !literal.startsWith("'") || !literal.endsWith("'")) return void 0;
|
|
531
|
+
return literal.slice(1, -1).replace(/''/g, "'");
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
524
534
|
* The SQL string literal of a `$`-rooted JSON path with every segment quoted
|
|
525
535
|
* (`'$."a"."b"'`) — the path argument of SQLite's and MySQL's `json_extract` /
|
|
526
|
-
* `json_type
|
|
536
|
+
* `json_type`; an empty path is the JSON root (`'$'`).
|
|
527
537
|
*
|
|
528
538
|
* @since 0.1.136
|
|
529
539
|
* @throws as {@link quotedJsonPathSegments}.
|
|
530
540
|
*/
|
|
531
|
-
function jsonDollarPath(path) {
|
|
532
|
-
return
|
|
541
|
+
function jsonDollarPath(path, stringLiteral = sqlStringLiteral) {
|
|
542
|
+
return stringLiteral(["$", ...quotedJsonPathSegments(path)].join("."));
|
|
533
543
|
}
|
|
534
544
|
/**
|
|
535
545
|
* A calendar bucket's time zone as an inlined SQL string literal (`'Europe/Berlin'`).
|
|
@@ -595,7 +605,7 @@ function defaultValueForType(designType) {
|
|
|
595
605
|
* respecting the field's designType. Booleans become 0/1, numbers stay unquoted,
|
|
596
606
|
* strings are single-quote-escaped.
|
|
597
607
|
*/
|
|
598
|
-
function defaultValueToSqlLiteral(designType, value) {
|
|
608
|
+
function defaultValueToSqlLiteral(designType, value, stringLiteral = sqlStringLiteral) {
|
|
599
609
|
switch (designType) {
|
|
600
610
|
case "boolean": return value === "true" || value === "1" ? "1" : "0";
|
|
601
611
|
case "number":
|
|
@@ -604,7 +614,7 @@ function defaultValueToSqlLiteral(designType, value) {
|
|
|
604
614
|
const n = Number(value);
|
|
605
615
|
return Number.isFinite(n) ? String(n) : "0";
|
|
606
616
|
}
|
|
607
|
-
default: return
|
|
617
|
+
default: return stringLiteral(value);
|
|
608
618
|
}
|
|
609
619
|
}
|
|
610
620
|
const queryOpToSql = {
|
|
@@ -616,9 +626,9 @@ const queryOpToSql = {
|
|
|
616
626
|
$lte: "<="
|
|
617
627
|
};
|
|
618
628
|
/** An inlined SQL literal for a view-predicate value (DDL — no parameters). */
|
|
619
|
-
function predicateLiteral(value) {
|
|
629
|
+
function predicateLiteral(value, stringLiteral) {
|
|
620
630
|
if (value === null || value === void 0) return "NULL";
|
|
621
|
-
if (typeof value === "string") return
|
|
631
|
+
if (typeof value === "string") return stringLiteral(value);
|
|
622
632
|
if (typeof value === "number") {
|
|
623
633
|
if (!Number.isFinite(value)) throw new Error(`Non-finite number ${value} in a view predicate`);
|
|
624
634
|
return String(value);
|
|
@@ -636,18 +646,18 @@ function predicateLiteral(value) {
|
|
|
636
646
|
* @throws for `matches` (`$regex`) and any other operator — a view predicate
|
|
637
647
|
* that cannot be rendered fails at sync instead of rendering wrong SQL.
|
|
638
648
|
*/
|
|
639
|
-
function queryNodeToSql(node, resolveFieldRef) {
|
|
649
|
+
function queryNodeToSql(node, resolveFieldRef, stringLiteral = sqlStringLiteral) {
|
|
640
650
|
if ("$and" in node) {
|
|
641
651
|
const children = node.$and;
|
|
642
652
|
if (children.length === 0) return EMPTY_AND.sql;
|
|
643
|
-
return children.map((n) => queryNodeToSql(n, resolveFieldRef)).join(" AND ");
|
|
653
|
+
return children.map((n) => queryNodeToSql(n, resolveFieldRef, stringLiteral)).join(" AND ");
|
|
644
654
|
}
|
|
645
655
|
if ("$or" in node) {
|
|
646
656
|
const children = node.$or;
|
|
647
657
|
if (children.length === 0) return EMPTY_OR.sql;
|
|
648
|
-
return `(${children.map((n) => queryNodeToSql(n, resolveFieldRef)).join(" OR ")})`;
|
|
658
|
+
return `(${children.map((n) => queryNodeToSql(n, resolveFieldRef, stringLiteral)).join(" OR ")})`;
|
|
649
659
|
}
|
|
650
|
-
if ("$not" in node) return `NOT (${queryNodeToSql(node.$not, resolveFieldRef)})`;
|
|
660
|
+
if ("$not" in node) return `NOT (${queryNodeToSql(node.$not, resolveFieldRef, stringLiteral)})`;
|
|
651
661
|
const comp = node;
|
|
652
662
|
const leftSql = resolveFieldRef(comp.left);
|
|
653
663
|
switch (comp.op) {
|
|
@@ -656,7 +666,7 @@ function queryNodeToSql(node, resolveFieldRef) {
|
|
|
656
666
|
case "$nin": {
|
|
657
667
|
const values = Array.isArray(comp.right) ? comp.right : [comp.right];
|
|
658
668
|
if (values.length === 0) return comp.op === "$in" ? EMPTY_OR.sql : EMPTY_AND.sql;
|
|
659
|
-
const list = values.map((v) => predicateLiteral(v)).join(", ");
|
|
669
|
+
const list = values.map((v) => predicateLiteral(v, stringLiteral)).join(", ");
|
|
660
670
|
return `${leftSql} ${comp.op === "$in" ? "IN" : "NOT IN"} (${list})`;
|
|
661
671
|
}
|
|
662
672
|
case "$regex": throw new Error("matches is not supported in view predicates");
|
|
@@ -666,7 +676,7 @@ function queryNodeToSql(node, resolveFieldRef) {
|
|
|
666
676
|
if (!sqlOp) throw new Error(`Operator "${comp.op}" is not supported in view predicates`);
|
|
667
677
|
if (isFieldRef(comp.right)) return `${leftSql} ${sqlOp} ${resolveFieldRef(comp.right)}`;
|
|
668
678
|
if (comp.right === null || comp.right === void 0) return comp.op === "$ne" ? `${leftSql} IS NOT NULL` : `${leftSql} IS NULL`;
|
|
669
|
-
return `${leftSql} ${sqlOp} ${predicateLiteral(comp.right)}`;
|
|
679
|
+
return `${leftSql} ${sqlOp} ${predicateLiteral(comp.right, stringLiteral)}`;
|
|
670
680
|
}
|
|
671
681
|
//#endregion
|
|
672
682
|
//#region src/agg.ts
|
|
@@ -1000,7 +1010,7 @@ function viewAggExpr(dialect, c, resolveFieldRef) {
|
|
|
1000
1010
|
const star = c.aggField === "*";
|
|
1001
1011
|
const src = star ? "*" : viewSourceExpr(dialect, c);
|
|
1002
1012
|
if (!c.aggFilter) return renderAggCall(c.aggFn, src, c.viewColumn);
|
|
1003
|
-
const predicate = queryNodeToSql(c.aggFilter, resolveFieldRef);
|
|
1013
|
+
const predicate = queryNodeToSql(c.aggFilter, resolveFieldRef, dialect.stringLiteral);
|
|
1004
1014
|
const call = renderAggCall(c.aggFn, `CASE WHEN ${predicate} THEN ${star ? "1" : src} END`, c.viewColumn);
|
|
1005
1015
|
return c.aggFn === "sum" ? `COALESCE(${call}, 0)` : call;
|
|
1006
1016
|
}
|
|
@@ -1091,14 +1101,14 @@ function buildViewSelect(dialect, plan, columns, resolveFieldRef) {
|
|
|
1091
1101
|
const columnExpr = (c) => c.expr !== void 0 ? viewComputeExpr(dialect, c, columnByPath, resolveFieldRef, hasAggregates, computedSql) : c.aggFn ? viewAggExpr(dialect, c, resolveFieldRef) : viewSourceExpr(dialect, c);
|
|
1092
1102
|
let sql = `SELECT ${columns.map((c) => `${columnExpr(c)} AS ${dialect.quoteIdentifier(c.viewColumn)}`).join(", ")} FROM ${dialect.quoteIdentifier(plan.entryTable)}`;
|
|
1093
1103
|
for (const join of plan.joins) {
|
|
1094
|
-
const condition = queryNodeToSql(join.condition, resolveFieldRef);
|
|
1104
|
+
const condition = queryNodeToSql(join.condition, resolveFieldRef, dialect.stringLiteral);
|
|
1095
1105
|
const keyword = join.kind === "left" ? "LEFT JOIN" : "JOIN";
|
|
1096
1106
|
const target = join.scope === join.targetTable ? dialect.quoteIdentifier(join.targetTable) : `${dialect.quoteIdentifier(join.targetTable)} AS ${dialect.quoteIdentifier(join.scope)}`;
|
|
1097
1107
|
const onClause = join.first ? firstRowOn(dialect, join, target, condition, resolveFieldRef) : condition;
|
|
1098
1108
|
sql += ` ${keyword} ${target} ON ${onClause}`;
|
|
1099
1109
|
}
|
|
1100
1110
|
if (plan.filter) {
|
|
1101
|
-
const whereClause = queryNodeToSql(plan.filter, resolveFieldRef);
|
|
1111
|
+
const whereClause = queryNodeToSql(plan.filter, resolveFieldRef, dialect.stringLiteral);
|
|
1102
1112
|
sql += ` WHERE ${whereClause}`;
|
|
1103
1113
|
}
|
|
1104
1114
|
if (hasAggregates) {
|
|
@@ -1119,7 +1129,7 @@ function buildViewSelect(dialect, plan, columns, resolveFieldRef) {
|
|
|
1119
1129
|
if (!col.json) return expr;
|
|
1120
1130
|
return dialect.bucketAliasInHaving && groupedColumns().has(col.viewColumn.toLowerCase()) ? `MIN(${expr})` : havingGroupRef(dialect, expr, col.viewColumn);
|
|
1121
1131
|
};
|
|
1122
|
-
const havingClause = queryNodeToSql(plan.having, havingResolver);
|
|
1132
|
+
const havingClause = queryNodeToSql(plan.having, havingResolver, dialect.stringLiteral);
|
|
1123
1133
|
sql += ` HAVING ${havingClause}`;
|
|
1124
1134
|
}
|
|
1125
1135
|
}
|
|
@@ -1243,8 +1253,9 @@ function buildInsertMany(dialect, table, rows, columns = insertManyColumns(rows)
|
|
|
1243
1253
|
/**
|
|
1244
1254
|
* Builds a SELECT statement with optional sort, limit, offset, projection.
|
|
1245
1255
|
* `table` may be a derived source (a pruned view read, since 0.1.153).
|
|
1256
|
+
* `lock` (since 0.1.162) appends a row-locking clause — see {@link readLockSql}.
|
|
1246
1257
|
*/
|
|
1247
|
-
function buildSelect(dialect, table, where, controls) {
|
|
1258
|
+
function buildSelect(dialect, table, where, controls, lock) {
|
|
1248
1259
|
const cols = buildProjection(dialect, controls?.$select);
|
|
1249
1260
|
let sql = `SELECT ${fromSourceHint(table)}${cols} FROM ${fromSourceSql(dialect, table)} WHERE ${where.sql}`;
|
|
1250
1261
|
const params = [...where.params];
|
|
@@ -1259,12 +1270,26 @@ function buildSelect(dialect, table, where, controls) {
|
|
|
1259
1270
|
sql += ` OFFSET ?`;
|
|
1260
1271
|
params.push(controls.$skip);
|
|
1261
1272
|
}
|
|
1273
|
+
if (lock) sql += readLockSql(lock);
|
|
1262
1274
|
return finalizeParams(dialect, {
|
|
1263
1275
|
sql,
|
|
1264
1276
|
params
|
|
1265
1277
|
});
|
|
1266
1278
|
}
|
|
1267
1279
|
/**
|
|
1280
|
+
* The row-locking clause of a locking read — `" FOR UPDATE"`,
|
|
1281
|
+
* `" FOR SHARE NOWAIT"`, `" FOR UPDATE SKIP LOCKED"`, … (PostgreSQL and
|
|
1282
|
+
* MySQL 8 share the syntax); `""` without a lock. Goes after `LIMIT` /
|
|
1283
|
+
* `OFFSET`. @since 0.1.162
|
|
1284
|
+
*/
|
|
1285
|
+
function readLockSql(lock) {
|
|
1286
|
+
if (!lock) return "";
|
|
1287
|
+
const mode = lock.mode === "share" ? " FOR SHARE" : " FOR UPDATE";
|
|
1288
|
+
if (lock.wait === "nowait") return `${mode} NOWAIT`;
|
|
1289
|
+
if (lock.wait === "skipLocked") return `${mode} SKIP LOCKED`;
|
|
1290
|
+
return mode;
|
|
1291
|
+
}
|
|
1292
|
+
/**
|
|
1268
1293
|
* `"col" ASC, "other" DESC` of a physical `$sort` — `""` when it orders
|
|
1269
1294
|
* nothing. A key with a `$nulls` entry places NULL as asked
|
|
1270
1295
|
* ({@link nullsOrderSql}); `prefix` is prepended verbatim to each quoted
|
|
@@ -1351,13 +1376,14 @@ function replaceColumnsFor(fields, nativeFns) {
|
|
|
1351
1376
|
* when the engine owns its function default — so an UPDATE-based replace never
|
|
1352
1377
|
* retains a value the caller left out. This is the SQL counterpart of the
|
|
1353
1378
|
* whole-document replace the memory and MongoDB adapters do natively. The
|
|
1354
|
-
* version column is excluded (`buildUpdate` appends the OCC bump itself)
|
|
1355
|
-
*
|
|
1379
|
+
* version column is excluded (`buildUpdate` appends the OCC bump itself), and
|
|
1380
|
+
* so are `skip` columns (`@db.onUpdate.inc` counters, since 0.1.162 — the
|
|
1381
|
+
* statement increments them). Returns a new object; `data` is not mutated.
|
|
1356
1382
|
*/
|
|
1357
|
-
function fillReplacePayload(data, columns, versionColumn) {
|
|
1383
|
+
function fillReplacePayload(data, columns, versionColumn, skip) {
|
|
1358
1384
|
const full = { ...data };
|
|
1359
1385
|
for (const col of columns) {
|
|
1360
|
-
if (col.name === versionColumn || col.name in full) continue;
|
|
1386
|
+
if (col.name === versionColumn || col.name in full || skip?.includes(col.name)) continue;
|
|
1361
1387
|
full[col.name] = col.useDefault ? SQL_DEFAULT : null;
|
|
1362
1388
|
}
|
|
1363
1389
|
return full;
|
|
@@ -1380,6 +1406,9 @@ function fillReplacePayload(data, columns, versionColumn) {
|
|
|
1380
1406
|
* `AND <col> = ?` to the WHERE clause and pushes the value. Requires
|
|
1381
1407
|
* `versionColumn` (CAS targets that column); supplying `expectedVersion`
|
|
1382
1408
|
* without `versionColumn` is a programmer error and throws.
|
|
1409
|
+
*
|
|
1410
|
+
* `limit` renders `LIMIT n`; as `{ count, orderBy }` (since 0.1.162) also
|
|
1411
|
+
* `ORDER BY <orderBy>` before it (MySQL's bounded `UPDATE`).
|
|
1383
1412
|
*/
|
|
1384
1413
|
function buildUpdate(dialect, table, data, where, limit, ops, versionColumn, expectedVersion) {
|
|
1385
1414
|
const setClauses = [];
|
|
@@ -1413,10 +1442,8 @@ function buildUpdate(dialect, table, data, where, limit, ops, versionColumn, exp
|
|
|
1413
1442
|
whereParams.push(expectedVersion);
|
|
1414
1443
|
}
|
|
1415
1444
|
}
|
|
1416
|
-
let sql = `UPDATE ${dialect.quoteTable(table)} SET ${setClauses.join(", ")} WHERE ${whereSql}`;
|
|
1417
|
-
if (limit !== void 0) sql += ` LIMIT ${limit}`;
|
|
1418
1445
|
return finalizeParams(dialect, {
|
|
1419
|
-
sql,
|
|
1446
|
+
sql: `UPDATE ${dialect.quoteTable(table)} SET ${setClauses.join(", ")} WHERE ${whereSql}` + limitSql(limit),
|
|
1420
1447
|
params: [
|
|
1421
1448
|
...params,
|
|
1422
1449
|
...where.params,
|
|
@@ -1425,16 +1452,21 @@ function buildUpdate(dialect, table, data, where, limit, ops, versionColumn, exp
|
|
|
1425
1452
|
});
|
|
1426
1453
|
}
|
|
1427
1454
|
/**
|
|
1428
|
-
* Builds a DELETE ... WHERE statement with optional LIMIT
|
|
1455
|
+
* Builds a DELETE ... WHERE statement with an optional LIMIT — as
|
|
1456
|
+
* `{ count, orderBy }` (since 0.1.162) with an ORDER BY before it.
|
|
1429
1457
|
*/
|
|
1430
1458
|
function buildDelete(dialect, table, where, limit) {
|
|
1431
|
-
let sql = `DELETE FROM ${dialect.quoteTable(table)} WHERE ${where.sql}`;
|
|
1432
|
-
if (limit !== void 0) sql += ` LIMIT ${limit}`;
|
|
1433
1459
|
return finalizeParams(dialect, {
|
|
1434
|
-
sql,
|
|
1460
|
+
sql: `DELETE FROM ${dialect.quoteTable(table)} WHERE ${where.sql}` + limitSql(limit),
|
|
1435
1461
|
params: where.params
|
|
1436
1462
|
});
|
|
1437
1463
|
}
|
|
1464
|
+
/** ` [ORDER BY …] LIMIT n` of an UPDATE / DELETE, or `""` when unbounded. */
|
|
1465
|
+
function limitSql(limit) {
|
|
1466
|
+
if (limit === void 0) return "";
|
|
1467
|
+
if (typeof limit === "number") return ` LIMIT ${limit}`;
|
|
1468
|
+
return (limit.orderBy ? ` ORDER BY ${limit.orderBy}` : "") + ` LIMIT ${limit.count}`;
|
|
1469
|
+
}
|
|
1438
1470
|
/**
|
|
1439
1471
|
* The expression a `@db.column.derived` column is generated from: the
|
|
1440
1472
|
* dialect's typed {@link SqlDialect.jsonExtract} over the (unqualified,
|
|
@@ -1482,6 +1514,29 @@ function buildKeyViolationCount(dialect, quotedTable, columns) {
|
|
|
1482
1514
|
const noNull = cols.map((c) => `${c} IS NOT NULL`).join(" AND ");
|
|
1483
1515
|
return `SELECT (SELECT COUNT(*) FROM ${quotedTable} WHERE ${anyNull}) + (SELECT COALESCE(SUM(${dialect.quoteIdentifier("n")}), 0) FROM (SELECT COUNT(*) AS ${dialect.quoteIdentifier("n")} FROM ${quotedTable} WHERE ${noNull} GROUP BY ${cols.join(", ")} HAVING COUNT(*) > 1) ${dialect.quoteIdentifier("d")}) AS ` + dialect.quoteIdentifier("violations");
|
|
1484
1516
|
}
|
|
1517
|
+
/**
|
|
1518
|
+
* Narrows an UPDATE / DELETE to the first rows of a bounded write
|
|
1519
|
+
* ({@link TDbWriteLimit}, since 0.1.162) on engines without
|
|
1520
|
+
* `UPDATE/DELETE … ORDER BY … LIMIT`: `where AND <key> IN (SELECT <key> FROM
|
|
1521
|
+
* <table> WHERE where ORDER BY … LIMIT n)`. `keyCols` are already-quoted key
|
|
1522
|
+
* expressions (the primary key columns, or SQLite's `rowid`). The filter is
|
|
1523
|
+
* repeated on the outer statement so a row changed out of it meanwhile is
|
|
1524
|
+
* left alone. Unfinalized (`?` placeholders).
|
|
1525
|
+
*/
|
|
1526
|
+
function writeLimitWhere(dialect, quotedTable, keyCols, where, bound) {
|
|
1527
|
+
const cols = keyCols.join(", ");
|
|
1528
|
+
const key = keyCols.length === 1 ? cols : `(${cols})`;
|
|
1529
|
+
const order = orderByList(dialect, bound.sort, bound.nulls);
|
|
1530
|
+
return {
|
|
1531
|
+
sql: `${where.sql} AND ${key} IN (SELECT ${cols} FROM ${quotedTable} WHERE ${where.sql} ORDER BY ${order} LIMIT ${writeLimitCount(bound)})`,
|
|
1532
|
+
params: [...where.params, ...where.params]
|
|
1533
|
+
};
|
|
1534
|
+
}
|
|
1535
|
+
/** The validated row count of a bounded write — a positive safe integer, rendered inline. */
|
|
1536
|
+
function writeLimitCount(bound) {
|
|
1537
|
+
if (!Number.isSafeInteger(bound.limit) || bound.limit < 1) throw new Error(`Invalid write limit: ${String(bound.limit)}`);
|
|
1538
|
+
return bound.limit;
|
|
1539
|
+
}
|
|
1485
1540
|
//#endregion
|
|
1486
1541
|
//#region src/geo.ts
|
|
1487
1542
|
/**
|
|
@@ -1795,4 +1850,4 @@ function buildJsonifyText(dialect, table, column) {
|
|
|
1795
1850
|
return `UPDATE ${dialect.quoteTable(table)} SET ${col} = ${dialect.jsonFromText(col)} WHERE ${col} IS NOT NULL`;
|
|
1796
1851
|
}
|
|
1797
1852
|
//#endregion
|
|
1798
|
-
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, buildJsonColumnCopy, buildJsonifyText, buildKeyViolationCount, 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 };
|
|
1853
|
+
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, buildJsonColumnCopy, buildJsonifyText, buildKeyViolationCount, 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, readLockSql, refActionToSql, renameGeoDistance, renderArith, replaceColumnsFor, sqlStringLiteral, sqlTimeZoneLiteral, stripPartitionRowNumber, toSqlValue, unquoteSqlStringLiteral, vectorDistanceSource, viewReadSource, writeLimitCount, writeLimitWhere };
|
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.162",
|
|
4
4
|
"description": "Shared SQL builder utilities for @atscript database adapters.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"atscript",
|
|
@@ -38,11 +38,11 @@
|
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@uniqu/core": "^0.1.15",
|
|
41
|
-
"unplugin-atscript": "^0.1.
|
|
41
|
+
"unplugin-atscript": "^0.1.109"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
44
|
"@uniqu/core": "^0.1.15",
|
|
45
|
-
"@atscript/db": "^0.1.
|
|
45
|
+
"@atscript/db": "^0.1.162"
|
|
46
46
|
},
|
|
47
47
|
"scripts": {
|
|
48
48
|
"build": "vp pack",
|