prisma-sql 1.48.1 → 1.49.0
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/generator.cjs +458 -262
- package/dist/generator.cjs.map +1 -1
- package/dist/generator.js +458 -262
- package/dist/generator.js.map +1 -1
- package/dist/index.cjs +460 -263
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +460 -263
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/generator.cjs
CHANGED
|
@@ -56,7 +56,7 @@ var require_package = __commonJS({
|
|
|
56
56
|
"package.json"(exports$1, module) {
|
|
57
57
|
module.exports = {
|
|
58
58
|
name: "prisma-sql",
|
|
59
|
-
version: "1.
|
|
59
|
+
version: "1.49.0",
|
|
60
60
|
description: "Convert Prisma queries to optimized SQL with type safety. 2-7x faster than Prisma Client.",
|
|
61
61
|
main: "dist/index.cjs",
|
|
62
62
|
module: "dist/index.js",
|
|
@@ -95,7 +95,7 @@ var require_package = __commonJS({
|
|
|
95
95
|
"prisma:reset": "npx prisma db push --force-reset --skip-generate --schema=tests/prisma/schema.prisma",
|
|
96
96
|
prepublishOnly: "npm run build",
|
|
97
97
|
"sonar-cli": "sonar-scanner -Dsonar.projectKey=prisma-sql -Dsonar.sources=./src -Dsonar.host.url=http://localhost:9000 -Dsonar.login=sqp_9fe07460d0aa83f711d0edf4f317f05019d0613b",
|
|
98
|
-
sonar: "
|
|
98
|
+
sonar: "npm run sonar-cli && npx tsx scripts/sonar.ts"
|
|
99
99
|
},
|
|
100
100
|
keywords: [
|
|
101
101
|
"prisma",
|
|
@@ -153,6 +153,7 @@ var require_package = __commonJS({
|
|
|
153
153
|
});
|
|
154
154
|
|
|
155
155
|
// src/builder/shared/constants.ts
|
|
156
|
+
var IS_PRODUCTION = process.env.NODE_ENV === "production";
|
|
156
157
|
var SQL_SEPARATORS = Object.freeze({
|
|
157
158
|
FIELD_LIST: ", ",
|
|
158
159
|
CONDITION_AND: " AND ",
|
|
@@ -595,6 +596,15 @@ function createError(message, ctx, code = "VALIDATION_ERROR") {
|
|
|
595
596
|
|
|
596
597
|
// src/builder/shared/model-field-cache.ts
|
|
597
598
|
var MODEL_CACHE = /* @__PURE__ */ new WeakMap();
|
|
599
|
+
function quote(id) {
|
|
600
|
+
const needsQuoting2 = !/^[a-z_][a-z0-9_]*$/.test(id) || /^(select|from|where|having|order|group|limit|offset|join|inner|left|right|outer|cross|full|and|or|not|by|as|on|union|intersect|except|case|when|then|else|end|user|users|table|column|index|values|in|like|between|is|exists|null|true|false|all|any|some|update|insert|delete|create|drop|alter|truncate|grant|revoke|exec|execute)$/i.test(
|
|
601
|
+
id
|
|
602
|
+
);
|
|
603
|
+
if (needsQuoting2) {
|
|
604
|
+
return `"${id.replace(/"/g, '""')}"`;
|
|
605
|
+
}
|
|
606
|
+
return id;
|
|
607
|
+
}
|
|
598
608
|
function ensureFullCache(model) {
|
|
599
609
|
let cache = MODEL_CACHE.get(model);
|
|
600
610
|
if (!cache) {
|
|
@@ -602,6 +612,8 @@ function ensureFullCache(model) {
|
|
|
602
612
|
const scalarFields = /* @__PURE__ */ new Set();
|
|
603
613
|
const relationFields = /* @__PURE__ */ new Set();
|
|
604
614
|
const columnMap = /* @__PURE__ */ new Map();
|
|
615
|
+
const fieldByName = /* @__PURE__ */ new Map();
|
|
616
|
+
const quotedColumns = /* @__PURE__ */ new Map();
|
|
605
617
|
for (const f of model.fields) {
|
|
606
618
|
const info = {
|
|
607
619
|
name: f.name,
|
|
@@ -611,14 +623,24 @@ function ensureFullCache(model) {
|
|
|
611
623
|
isRequired: !!f.isRequired
|
|
612
624
|
};
|
|
613
625
|
fieldInfo.set(f.name, info);
|
|
626
|
+
fieldByName.set(f.name, f);
|
|
614
627
|
if (info.isRelation) {
|
|
615
628
|
relationFields.add(f.name);
|
|
616
629
|
} else {
|
|
617
630
|
scalarFields.add(f.name);
|
|
618
|
-
|
|
631
|
+
const dbName = info.dbName;
|
|
632
|
+
columnMap.set(f.name, dbName);
|
|
633
|
+
quotedColumns.set(f.name, quote(dbName));
|
|
619
634
|
}
|
|
620
635
|
}
|
|
621
|
-
cache = {
|
|
636
|
+
cache = {
|
|
637
|
+
fieldInfo,
|
|
638
|
+
scalarFields,
|
|
639
|
+
relationFields,
|
|
640
|
+
columnMap,
|
|
641
|
+
fieldByName,
|
|
642
|
+
quotedColumns
|
|
643
|
+
};
|
|
622
644
|
MODEL_CACHE.set(model, cache);
|
|
623
645
|
}
|
|
624
646
|
return cache;
|
|
@@ -635,6 +657,9 @@ function getRelationFieldSet(model) {
|
|
|
635
657
|
function getColumnMap(model) {
|
|
636
658
|
return ensureFullCache(model).columnMap;
|
|
637
659
|
}
|
|
660
|
+
function getQuotedColumn(model, fieldName) {
|
|
661
|
+
return ensureFullCache(model).quotedColumns.get(fieldName);
|
|
662
|
+
}
|
|
638
663
|
|
|
639
664
|
// src/builder/shared/validators/sql-validators.ts
|
|
640
665
|
function isValidWhereClause(clause) {
|
|
@@ -650,6 +675,7 @@ function sqlPreview(sql) {
|
|
|
650
675
|
return `${s.slice(0, 160)}...`;
|
|
651
676
|
}
|
|
652
677
|
function validateSelectQuery(sql) {
|
|
678
|
+
if (IS_PRODUCTION) return;
|
|
653
679
|
if (!hasValidContent(sql)) {
|
|
654
680
|
throw new Error("CRITICAL: Generated empty SQL query");
|
|
655
681
|
}
|
|
@@ -708,6 +734,7 @@ function assertNoGapsDollar(scan, rangeMin, rangeMax, sql) {
|
|
|
708
734
|
}
|
|
709
735
|
}
|
|
710
736
|
function validateParamConsistency(sql, params) {
|
|
737
|
+
if (IS_PRODUCTION) return;
|
|
711
738
|
const paramLen = params.length;
|
|
712
739
|
const scan = scanDollarPlaceholders(sql, paramLen);
|
|
713
740
|
if (paramLen === 0) {
|
|
@@ -744,6 +771,7 @@ function countQuestionMarkPlaceholders(sql) {
|
|
|
744
771
|
return count;
|
|
745
772
|
}
|
|
746
773
|
function validateQuestionMarkConsistency(sql, params) {
|
|
774
|
+
if (IS_PRODUCTION) return;
|
|
747
775
|
const expected = params.length;
|
|
748
776
|
const found = countQuestionMarkPlaceholders(sql);
|
|
749
777
|
if (expected !== found) {
|
|
@@ -753,6 +781,7 @@ function validateQuestionMarkConsistency(sql, params) {
|
|
|
753
781
|
}
|
|
754
782
|
}
|
|
755
783
|
function validateParamConsistencyByDialect(sql, params, dialect) {
|
|
784
|
+
if (IS_PRODUCTION) return;
|
|
756
785
|
if (dialect === "postgres") {
|
|
757
786
|
validateParamConsistency(sql, params);
|
|
758
787
|
return;
|
|
@@ -914,7 +943,7 @@ function assertSafeQualifiedName(tableRef) {
|
|
|
914
943
|
}
|
|
915
944
|
}
|
|
916
945
|
}
|
|
917
|
-
function
|
|
946
|
+
function quote2(id) {
|
|
918
947
|
if (isEmptyString(id)) {
|
|
919
948
|
throw new Error("quote: identifier is required and cannot be empty");
|
|
920
949
|
}
|
|
@@ -934,17 +963,14 @@ function resolveColumnName(model, fieldName) {
|
|
|
934
963
|
return columnMap.get(fieldName) || fieldName;
|
|
935
964
|
}
|
|
936
965
|
function quoteColumn(model, fieldName) {
|
|
937
|
-
|
|
966
|
+
if (!model) return quote2(fieldName);
|
|
967
|
+
const cached = getQuotedColumn(model, fieldName);
|
|
968
|
+
return cached || quote2(fieldName);
|
|
938
969
|
}
|
|
939
970
|
function col(alias, field, model) {
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
if (isEmptyString(field)) {
|
|
944
|
-
throw new Error("col: field is required and cannot be empty");
|
|
945
|
-
}
|
|
946
|
-
const columnName = resolveColumnName(model, field);
|
|
947
|
-
return `${alias}.${quote(columnName)}`;
|
|
971
|
+
const columnName = model ? getColumnMap(model).get(field) || field : field;
|
|
972
|
+
const quoted = model ? getQuotedColumn(model, field) || quote2(columnName) : quote2(columnName);
|
|
973
|
+
return alias + "." + quoted;
|
|
948
974
|
}
|
|
949
975
|
function colWithAlias(alias, field, model) {
|
|
950
976
|
if (isEmptyString(alias)) {
|
|
@@ -954,9 +980,9 @@ function colWithAlias(alias, field, model) {
|
|
|
954
980
|
throw new Error("colWithAlias: field is required and cannot be empty");
|
|
955
981
|
}
|
|
956
982
|
const columnName = resolveColumnName(model, field);
|
|
957
|
-
const columnRef =
|
|
983
|
+
const columnRef = alias + "." + (model ? getQuotedColumn(model, field) || quote2(columnName) : quote2(columnName));
|
|
958
984
|
if (columnName !== field) {
|
|
959
|
-
return
|
|
985
|
+
return columnRef + " AS " + quote2(field);
|
|
960
986
|
}
|
|
961
987
|
return columnRef;
|
|
962
988
|
}
|
|
@@ -979,7 +1005,7 @@ function buildTableReference(schemaName, tableName, dialect) {
|
|
|
979
1005
|
}
|
|
980
1006
|
const d = dialect != null ? dialect : "postgres";
|
|
981
1007
|
if (d === "sqlite") {
|
|
982
|
-
return
|
|
1008
|
+
return quote2(tableName);
|
|
983
1009
|
}
|
|
984
1010
|
if (isEmptyString(schemaName)) {
|
|
985
1011
|
throw new Error(
|
|
@@ -1307,6 +1333,7 @@ function assertNumericField(model, fieldName, context) {
|
|
|
1307
1333
|
|
|
1308
1334
|
// src/builder/pagination.ts
|
|
1309
1335
|
var MAX_LIMIT_OFFSET = 2147483647;
|
|
1336
|
+
var ORDER_BY_ALLOWED_KEYS = /* @__PURE__ */ new Set(["sort", "nulls"]);
|
|
1310
1337
|
function parseDirectionRaw(raw, errorLabel) {
|
|
1311
1338
|
const s = String(raw).toLowerCase();
|
|
1312
1339
|
if (s === "asc" || s === "desc") return s;
|
|
@@ -1325,9 +1352,8 @@ function requireOrderByObject(v, errorPrefix) {
|
|
|
1325
1352
|
return v;
|
|
1326
1353
|
}
|
|
1327
1354
|
function assertAllowedOrderByKeys(obj, fieldName) {
|
|
1328
|
-
const allowed = /* @__PURE__ */ new Set(["sort", "nulls"]);
|
|
1329
1355
|
for (const k of Object.keys(obj)) {
|
|
1330
|
-
if (!
|
|
1356
|
+
if (!ORDER_BY_ALLOWED_KEYS.has(k)) {
|
|
1331
1357
|
throw new Error(
|
|
1332
1358
|
fieldName ? `Unsupported orderBy key '${k}' for field '${fieldName}'` : `Unsupported orderBy key '${k}'`
|
|
1333
1359
|
);
|
|
@@ -1394,17 +1420,17 @@ function buildOrderByFragment(entries, alias, dialect, model) {
|
|
|
1394
1420
|
const c = col(alias, e.field, model);
|
|
1395
1421
|
if (dialect === "postgres") {
|
|
1396
1422
|
const nulls = isNotNullish(e.nulls) ? ` NULLS ${e.nulls.toUpperCase()}` : "";
|
|
1397
|
-
out.push(
|
|
1423
|
+
out.push(c + " " + dir + nulls);
|
|
1398
1424
|
continue;
|
|
1399
1425
|
}
|
|
1400
1426
|
if (isNotNullish(e.nulls)) {
|
|
1401
1427
|
const isNullExpr = `(${c} IS NULL)`;
|
|
1402
1428
|
const nullRankDir = e.nulls === "first" ? "DESC" : "ASC";
|
|
1403
|
-
out.push(
|
|
1404
|
-
out.push(
|
|
1429
|
+
out.push(isNullExpr + " " + nullRankDir);
|
|
1430
|
+
out.push(c + " " + dir);
|
|
1405
1431
|
continue;
|
|
1406
1432
|
}
|
|
1407
|
-
out.push(
|
|
1433
|
+
out.push(c + " " + dir);
|
|
1408
1434
|
}
|
|
1409
1435
|
return out.join(SQL_SEPARATORS.ORDER_BY);
|
|
1410
1436
|
}
|
|
@@ -1413,79 +1439,47 @@ function defaultNullsFor(dialect, direction) {
|
|
|
1413
1439
|
return direction === "asc" ? "first" : "last";
|
|
1414
1440
|
}
|
|
1415
1441
|
function ensureCursorFieldsInOrder(orderEntries, cursorEntries) {
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1442
|
+
if (cursorEntries.length === 0) return orderEntries;
|
|
1443
|
+
const existing = /* @__PURE__ */ new Set();
|
|
1444
|
+
for (let i = 0; i < orderEntries.length; i++)
|
|
1445
|
+
existing.add(orderEntries[i].field);
|
|
1446
|
+
let out = null;
|
|
1447
|
+
for (let i = 0; i < cursorEntries.length; i++) {
|
|
1448
|
+
const field = cursorEntries[i][0];
|
|
1420
1449
|
if (!existing.has(field)) {
|
|
1450
|
+
if (!out) out = orderEntries.slice();
|
|
1421
1451
|
out.push({ field, direction: "asc" });
|
|
1422
|
-
existing.
|
|
1452
|
+
existing.add(field);
|
|
1423
1453
|
}
|
|
1424
1454
|
}
|
|
1425
|
-
return out;
|
|
1455
|
+
return out != null ? out : orderEntries;
|
|
1426
1456
|
}
|
|
1427
1457
|
function buildCursorFilterParts(cursor, cursorAlias, params, model) {
|
|
1428
|
-
const entries = Object.entries(cursor);
|
|
1429
|
-
if (entries.length === 0)
|
|
1430
|
-
throw new Error("cursor must have at least one field");
|
|
1431
|
-
const placeholdersByField = /* @__PURE__ */ new Map();
|
|
1432
1458
|
const parts = [];
|
|
1433
|
-
for (const
|
|
1434
|
-
|
|
1459
|
+
for (const field in cursor) {
|
|
1460
|
+
if (!Object.prototype.hasOwnProperty.call(cursor, field)) continue;
|
|
1461
|
+
const value = cursor[field];
|
|
1462
|
+
const c = cursorAlias + "." + quoteColumn(model, field);
|
|
1435
1463
|
if (value === null) {
|
|
1436
|
-
parts.push(
|
|
1464
|
+
parts.push(c + " IS NULL");
|
|
1437
1465
|
continue;
|
|
1438
1466
|
}
|
|
1439
1467
|
const ph = addAutoScoped(params, value, `cursor.filter.${field}`);
|
|
1440
|
-
|
|
1441
|
-
parts.push(`${c} = ${ph}`);
|
|
1468
|
+
parts.push(c + " = " + ph);
|
|
1442
1469
|
}
|
|
1443
1470
|
return {
|
|
1444
|
-
whereSql: parts.length === 1 ? parts[0] :
|
|
1445
|
-
placeholdersByField
|
|
1471
|
+
whereSql: parts.length === 1 ? parts[0] : "(" + parts.join(" AND ") + ")"
|
|
1446
1472
|
};
|
|
1447
1473
|
}
|
|
1448
|
-
function buildCursorEqualityExpr(columnExpr,
|
|
1449
|
-
return `((${
|
|
1474
|
+
function buildCursorEqualityExpr(columnExpr, cursorField) {
|
|
1475
|
+
return `((${cursorField} IS NULL AND ${columnExpr} IS NULL) OR (${cursorField} IS NOT NULL AND ${columnExpr} = ${cursorField}))`;
|
|
1450
1476
|
}
|
|
1451
|
-
function buildCursorInequalityExpr(columnExpr, direction, nulls,
|
|
1477
|
+
function buildCursorInequalityExpr(columnExpr, direction, nulls, cursorField) {
|
|
1452
1478
|
const op = direction === "asc" ? ">" : "<";
|
|
1453
1479
|
if (nulls === "first") {
|
|
1454
|
-
return `(CASE WHEN ${
|
|
1455
|
-
}
|
|
1456
|
-
return `(CASE WHEN ${valueExpr} IS NULL THEN 0=1 ELSE ((${columnExpr} ${op} ${valueExpr}) OR (${columnExpr} IS NULL)) END)`;
|
|
1457
|
-
}
|
|
1458
|
-
function buildOuterCursorMatch(cursor, outerAlias, placeholdersByField, params, model) {
|
|
1459
|
-
const parts = [];
|
|
1460
|
-
for (const [field, value] of Object.entries(cursor)) {
|
|
1461
|
-
const c = col(outerAlias, field, model);
|
|
1462
|
-
if (value === null) {
|
|
1463
|
-
parts.push(`${c} IS NULL`);
|
|
1464
|
-
continue;
|
|
1465
|
-
}
|
|
1466
|
-
const existing = placeholdersByField.get(field);
|
|
1467
|
-
if (typeof existing === "string" && existing.length > 0) {
|
|
1468
|
-
parts.push(`${c} = ${existing}`);
|
|
1469
|
-
continue;
|
|
1470
|
-
}
|
|
1471
|
-
const ph = addAutoScoped(params, value, `cursor.outerMatch.${field}`);
|
|
1472
|
-
parts.push(`${c} = ${ph}`);
|
|
1473
|
-
}
|
|
1474
|
-
return parts.length === 1 ? parts[0] : `(${parts.join(" AND ")})`;
|
|
1475
|
-
}
|
|
1476
|
-
function buildOrderEntries(orderBy) {
|
|
1477
|
-
const normalized = normalizeOrderByInput(orderBy, parseOrderByValue);
|
|
1478
|
-
const entries = [];
|
|
1479
|
-
for (const item of normalized) {
|
|
1480
|
-
for (const [field, value] of Object.entries(item)) {
|
|
1481
|
-
if (typeof value === "string") {
|
|
1482
|
-
entries.push({ field, direction: value });
|
|
1483
|
-
} else {
|
|
1484
|
-
entries.push({ field, direction: value.direction, nulls: value.nulls });
|
|
1485
|
-
}
|
|
1486
|
-
}
|
|
1480
|
+
return `(CASE WHEN ${cursorField} IS NULL THEN (${columnExpr} IS NOT NULL) ELSE (${columnExpr} ${op} ${cursorField}) END)`;
|
|
1487
1481
|
}
|
|
1488
|
-
return
|
|
1482
|
+
return `(CASE WHEN ${cursorField} IS NULL THEN 0=1 ELSE ((${columnExpr} ${op} ${cursorField}) OR (${columnExpr} IS NULL)) END)`;
|
|
1489
1483
|
}
|
|
1490
1484
|
function buildCursorCteSelectList(cursorEntries, orderEntries, model) {
|
|
1491
1485
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -1507,8 +1501,7 @@ function buildCursorCteSelectList(cursorEntries, orderEntries, model) {
|
|
|
1507
1501
|
}
|
|
1508
1502
|
function truncateIdent(name, maxLen) {
|
|
1509
1503
|
const s = String(name);
|
|
1510
|
-
|
|
1511
|
-
return s.slice(0, maxLen);
|
|
1504
|
+
return s.length <= maxLen ? s : s.slice(0, maxLen);
|
|
1512
1505
|
}
|
|
1513
1506
|
function buildCursorNames(outerAlias) {
|
|
1514
1507
|
const maxLen = 63;
|
|
@@ -1525,15 +1518,25 @@ function buildCursorNames(outerAlias) {
|
|
|
1525
1518
|
}
|
|
1526
1519
|
function assertCursorAndOrderFieldsScalar(model, cursor, orderEntries) {
|
|
1527
1520
|
if (!model) return;
|
|
1528
|
-
for (const k
|
|
1529
|
-
|
|
1521
|
+
for (const k in cursor) {
|
|
1522
|
+
if (!Object.prototype.hasOwnProperty.call(cursor, k)) continue;
|
|
1523
|
+
assertScalarField(model, k, "cursor");
|
|
1524
|
+
}
|
|
1525
|
+
for (const e of orderEntries) {
|
|
1526
|
+
assertScalarField(model, e.field, "orderBy");
|
|
1527
|
+
}
|
|
1530
1528
|
}
|
|
1531
1529
|
function buildCursorCondition(cursor, orderBy, tableName, alias, params, dialect, model) {
|
|
1532
1530
|
var _a;
|
|
1533
1531
|
assertSafeTableRef(tableName);
|
|
1534
1532
|
assertSafeAlias(alias);
|
|
1535
1533
|
const d = dialect != null ? dialect : getGlobalDialect();
|
|
1536
|
-
const cursorEntries =
|
|
1534
|
+
const cursorEntries = [];
|
|
1535
|
+
for (const k in cursor) {
|
|
1536
|
+
if (Object.prototype.hasOwnProperty.call(cursor, k)) {
|
|
1537
|
+
cursorEntries.push([k, cursor[k]]);
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1537
1540
|
if (cursorEntries.length === 0)
|
|
1538
1541
|
throw new Error("cursor must have at least one field");
|
|
1539
1542
|
const { cteName, srcAlias } = buildCursorNames(alias);
|
|
@@ -1554,48 +1557,51 @@ function buildCursorCondition(cursor, orderBy, tableName, alias, params, dialect
|
|
|
1554
1557
|
orderEntries = ensureCursorFieldsInOrder(orderEntries, cursorEntries);
|
|
1555
1558
|
}
|
|
1556
1559
|
assertCursorAndOrderFieldsScalar(model, cursor, orderEntries);
|
|
1557
|
-
const { whereSql: cursorWhereSql
|
|
1560
|
+
const { whereSql: cursorWhereSql } = buildCursorFilterParts(
|
|
1561
|
+
cursor,
|
|
1562
|
+
srcAlias,
|
|
1563
|
+
params,
|
|
1564
|
+
model
|
|
1565
|
+
);
|
|
1558
1566
|
const cursorOrderBy = orderEntries.map(
|
|
1559
|
-
(e) =>
|
|
1567
|
+
(e) => srcAlias + "." + quoteColumn(model, e.field) + " " + e.direction.toUpperCase()
|
|
1560
1568
|
).join(", ");
|
|
1561
1569
|
const selectList = buildCursorCteSelectList(
|
|
1562
1570
|
cursorEntries,
|
|
1563
1571
|
orderEntries,
|
|
1564
1572
|
model
|
|
1565
1573
|
);
|
|
1566
|
-
const cte =
|
|
1567
|
-
|
|
1568
|
-
WHERE ${cursorWhereSql}
|
|
1569
|
-
ORDER BY ${cursorOrderBy}
|
|
1570
|
-
LIMIT 1
|
|
1571
|
-
)`;
|
|
1572
|
-
const existsExpr = `EXISTS (SELECT 1 FROM ${cteName})`;
|
|
1573
|
-
const outerCursorMatch = buildOuterCursorMatch(
|
|
1574
|
-
cursor,
|
|
1575
|
-
alias,
|
|
1576
|
-
placeholdersByField,
|
|
1577
|
-
params,
|
|
1578
|
-
model
|
|
1579
|
-
);
|
|
1580
|
-
const getValueExpr = (field) => `(SELECT ${quoteColumn(model, field)} FROM ${cteName})`;
|
|
1574
|
+
const cte = cteName + " AS (\n SELECT " + selectList + " FROM " + tableName + " " + srcAlias + "\n WHERE " + cursorWhereSql + "\n ORDER BY " + cursorOrderBy + "\n LIMIT 1\n )";
|
|
1575
|
+
const existsExpr = "EXISTS (SELECT 1 FROM " + cteName + ")";
|
|
1581
1576
|
const orClauses = [];
|
|
1582
1577
|
for (let level = 0; level < orderEntries.length; level++) {
|
|
1583
1578
|
const andParts = [];
|
|
1584
1579
|
for (let i = 0; i < level; i++) {
|
|
1585
1580
|
const e2 = orderEntries[i];
|
|
1586
1581
|
const c2 = col(alias, e2.field, model);
|
|
1587
|
-
const
|
|
1588
|
-
andParts.push(buildCursorEqualityExpr(c2,
|
|
1582
|
+
const cursorField2 = cteName + "." + quoteColumn(model, e2.field);
|
|
1583
|
+
andParts.push(buildCursorEqualityExpr(c2, cursorField2));
|
|
1589
1584
|
}
|
|
1590
1585
|
const e = orderEntries[level];
|
|
1591
1586
|
const c = col(alias, e.field, model);
|
|
1592
|
-
const
|
|
1587
|
+
const cursorField = cteName + "." + quoteColumn(model, e.field);
|
|
1593
1588
|
const nulls = (_a = e.nulls) != null ? _a : defaultNullsFor(d, e.direction);
|
|
1594
|
-
andParts.push(buildCursorInequalityExpr(c, e.direction, nulls,
|
|
1595
|
-
orClauses.push(
|
|
1589
|
+
andParts.push(buildCursorInequalityExpr(c, e.direction, nulls, cursorField));
|
|
1590
|
+
orClauses.push("(" + andParts.join(SQL_SEPARATORS.CONDITION_AND) + ")");
|
|
1596
1591
|
}
|
|
1597
1592
|
const exclusive = orClauses.join(SQL_SEPARATORS.CONDITION_OR);
|
|
1598
|
-
const
|
|
1593
|
+
const outerMatchParts = [];
|
|
1594
|
+
for (const [field, value] of cursorEntries) {
|
|
1595
|
+
const c = col(alias, field, model);
|
|
1596
|
+
if (value === null) {
|
|
1597
|
+
outerMatchParts.push(c + " IS NULL");
|
|
1598
|
+
continue;
|
|
1599
|
+
}
|
|
1600
|
+
const ph = addAutoScoped(params, value, `cursor.outerMatch.${field}`);
|
|
1601
|
+
outerMatchParts.push(c + " = " + ph);
|
|
1602
|
+
}
|
|
1603
|
+
const outerCursorMatch = outerMatchParts.length === 1 ? outerMatchParts[0] : "(" + outerMatchParts.join(SQL_SEPARATORS.CONDITION_AND) + ")";
|
|
1604
|
+
const condition = "(" + existsExpr + SQL_SEPARATORS.CONDITION_AND + "((" + exclusive + ")" + SQL_SEPARATORS.CONDITION_OR + "(" + outerCursorMatch + ")))";
|
|
1599
1605
|
return { cte, condition };
|
|
1600
1606
|
}
|
|
1601
1607
|
function buildOrderBy(orderBy, alias, dialect, model) {
|
|
@@ -1648,6 +1654,22 @@ function getPaginationParams(method, args) {
|
|
|
1648
1654
|
}
|
|
1649
1655
|
return {};
|
|
1650
1656
|
}
|
|
1657
|
+
function buildOrderEntries(orderBy) {
|
|
1658
|
+
const normalized = normalizeOrderByInput(orderBy, parseOrderByValue);
|
|
1659
|
+
const entries = [];
|
|
1660
|
+
for (const item of normalized) {
|
|
1661
|
+
for (const field in item) {
|
|
1662
|
+
if (!Object.prototype.hasOwnProperty.call(item, field)) continue;
|
|
1663
|
+
const value = item[field];
|
|
1664
|
+
if (typeof value === "string") {
|
|
1665
|
+
entries.push({ field, direction: value });
|
|
1666
|
+
} else {
|
|
1667
|
+
entries.push({ field, direction: value.direction, nulls: value.nulls });
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
return entries;
|
|
1672
|
+
}
|
|
1651
1673
|
function buildNotComposite(expr, val, params, dialect, buildOp, separator) {
|
|
1652
1674
|
const entries = Object.entries(val).filter(
|
|
1653
1675
|
([k, v]) => k !== "mode" && v !== void 0
|
|
@@ -2118,7 +2140,7 @@ function buildListRelationFilters(args) {
|
|
|
2118
2140
|
) || relModel.fields.find((f) => !f.isRelation && f.name === "id");
|
|
2119
2141
|
if (checkField) {
|
|
2120
2142
|
const leftJoinSql = `LEFT JOIN ${relTable} ${relAlias} ON ${join3}`;
|
|
2121
|
-
const whereClause = `${relAlias}.${
|
|
2143
|
+
const whereClause = `${relAlias}.${quote2(checkField.name)} IS NULL`;
|
|
2122
2144
|
return Object.freeze({
|
|
2123
2145
|
clause: whereClause,
|
|
2124
2146
|
joins: freezeJoins([leftJoinSql])
|
|
@@ -2569,11 +2591,24 @@ function buildOperator(expr, op, val, ctx, mode, fieldType) {
|
|
|
2569
2591
|
// src/builder/shared/alias-generator.ts
|
|
2570
2592
|
function toSafeSqlIdentifier(input) {
|
|
2571
2593
|
const raw = String(input);
|
|
2572
|
-
const
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2594
|
+
const n = raw.length;
|
|
2595
|
+
let out = "";
|
|
2596
|
+
for (let i = 0; i < n; i++) {
|
|
2597
|
+
const c = raw.charCodeAt(i);
|
|
2598
|
+
const isAZ = c >= 65 && c <= 90 || c >= 97 && c <= 122;
|
|
2599
|
+
const is09 = c >= 48 && c <= 57;
|
|
2600
|
+
const isUnderscore = c === 95;
|
|
2601
|
+
if (isAZ || is09 || isUnderscore) {
|
|
2602
|
+
out += raw[i];
|
|
2603
|
+
} else {
|
|
2604
|
+
out += "_";
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
if (out.length === 0) out = "_t";
|
|
2608
|
+
const c0 = out.charCodeAt(0);
|
|
2609
|
+
const startsOk = c0 >= 65 && c0 <= 90 || c0 >= 97 && c0 <= 122 || c0 === 95;
|
|
2610
|
+
if (!startsOk) out = `_${out}`;
|
|
2611
|
+
const lowered = out.toLowerCase();
|
|
2577
2612
|
return ALIAS_FORBIDDEN_KEYWORDS.has(lowered) ? `_${lowered}` : lowered;
|
|
2578
2613
|
}
|
|
2579
2614
|
function createAliasGenerator(maxAliases = 1e4) {
|
|
@@ -2676,14 +2711,16 @@ function validateState(params, mappings, index) {
|
|
|
2676
2711
|
}
|
|
2677
2712
|
function createStoreInternal(startIndex, initialParams = [], initialMappings = []) {
|
|
2678
2713
|
let index = startIndex;
|
|
2679
|
-
const params = [...initialParams];
|
|
2680
|
-
const mappings = [...initialMappings];
|
|
2714
|
+
const params = initialParams.length > 0 ? [...initialParams] : [];
|
|
2715
|
+
const mappings = initialMappings.length > 0 ? [...initialMappings] : [];
|
|
2681
2716
|
const dynamicNameToIndex = /* @__PURE__ */ new Map();
|
|
2682
|
-
for (const m of
|
|
2717
|
+
for (const m of mappings) {
|
|
2683
2718
|
if (typeof m.dynamicName === "string") {
|
|
2684
2719
|
dynamicNameToIndex.set(m.dynamicName.trim(), m.index);
|
|
2685
2720
|
}
|
|
2686
2721
|
}
|
|
2722
|
+
let dirty = true;
|
|
2723
|
+
let cachedSnapshot = null;
|
|
2687
2724
|
function assertCanAdd() {
|
|
2688
2725
|
if (index > MAX_PARAM_INDEX) {
|
|
2689
2726
|
throw new Error(
|
|
@@ -2691,6 +2728,9 @@ function createStoreInternal(startIndex, initialParams = [], initialMappings = [
|
|
|
2691
2728
|
);
|
|
2692
2729
|
}
|
|
2693
2730
|
}
|
|
2731
|
+
function format(position) {
|
|
2732
|
+
return `$${position}`;
|
|
2733
|
+
}
|
|
2694
2734
|
function normalizeDynamicName(dynamicName) {
|
|
2695
2735
|
const dn = dynamicName.trim();
|
|
2696
2736
|
if (dn.length === 0) {
|
|
@@ -2698,20 +2738,16 @@ function createStoreInternal(startIndex, initialParams = [], initialMappings = [
|
|
|
2698
2738
|
}
|
|
2699
2739
|
return dn;
|
|
2700
2740
|
}
|
|
2701
|
-
function format(position) {
|
|
2702
|
-
return `$${position}`;
|
|
2703
|
-
}
|
|
2704
2741
|
function addDynamic(dynamicName) {
|
|
2705
2742
|
const dn = normalizeDynamicName(dynamicName);
|
|
2706
2743
|
const existing = dynamicNameToIndex.get(dn);
|
|
2707
|
-
if (existing !== void 0)
|
|
2708
|
-
return format(existing);
|
|
2709
|
-
}
|
|
2744
|
+
if (existing !== void 0) return format(existing);
|
|
2710
2745
|
const position = index;
|
|
2711
2746
|
dynamicNameToIndex.set(dn, position);
|
|
2712
2747
|
params.push(void 0);
|
|
2713
2748
|
mappings.push({ index: position, dynamicName: dn });
|
|
2714
2749
|
index++;
|
|
2750
|
+
dirty = true;
|
|
2715
2751
|
return format(position);
|
|
2716
2752
|
}
|
|
2717
2753
|
function addStatic(value) {
|
|
@@ -2720,6 +2756,7 @@ function createStoreInternal(startIndex, initialParams = [], initialMappings = [
|
|
|
2720
2756
|
params.push(normalizedValue);
|
|
2721
2757
|
mappings.push({ index: position, value: normalizedValue });
|
|
2722
2758
|
index++;
|
|
2759
|
+
dirty = true;
|
|
2723
2760
|
return format(position);
|
|
2724
2761
|
}
|
|
2725
2762
|
function add(value, dynamicName) {
|
|
@@ -2734,11 +2771,15 @@ function createStoreInternal(startIndex, initialParams = [], initialMappings = [
|
|
|
2734
2771
|
return add(value);
|
|
2735
2772
|
}
|
|
2736
2773
|
function snapshot() {
|
|
2737
|
-
return
|
|
2774
|
+
if (!dirty && cachedSnapshot) return cachedSnapshot;
|
|
2775
|
+
const snap = {
|
|
2738
2776
|
index,
|
|
2739
|
-
params
|
|
2740
|
-
mappings
|
|
2741
|
-
}
|
|
2777
|
+
params,
|
|
2778
|
+
mappings
|
|
2779
|
+
};
|
|
2780
|
+
cachedSnapshot = snap;
|
|
2781
|
+
dirty = false;
|
|
2782
|
+
return snap;
|
|
2742
2783
|
}
|
|
2743
2784
|
return {
|
|
2744
2785
|
add,
|
|
@@ -2947,7 +2988,7 @@ function getRelationTableReference(relModel, dialect) {
|
|
|
2947
2988
|
dialect
|
|
2948
2989
|
);
|
|
2949
2990
|
}
|
|
2950
|
-
function resolveRelationOrThrow(model, schemas, relName) {
|
|
2991
|
+
function resolveRelationOrThrow(model, schemas, schemaByName, relName) {
|
|
2951
2992
|
const field = model.fields.find((f) => f.name === relName);
|
|
2952
2993
|
if (!isNotNullish(field)) {
|
|
2953
2994
|
throw new Error(
|
|
@@ -2959,10 +3000,16 @@ function resolveRelationOrThrow(model, schemas, relName) {
|
|
|
2959
3000
|
`Invalid relation metadata for '${relName}' on model ${model.name}. This usually indicates a schema parsing error (missing foreignKey/references).`
|
|
2960
3001
|
);
|
|
2961
3002
|
}
|
|
2962
|
-
const
|
|
3003
|
+
const relatedModelName = field.relatedModel;
|
|
3004
|
+
if (!isNotNullish(relatedModelName) || String(relatedModelName).trim().length === 0) {
|
|
3005
|
+
throw new Error(
|
|
3006
|
+
`Relation '${relName}' on model ${model.name} is missing relatedModel metadata.`
|
|
3007
|
+
);
|
|
3008
|
+
}
|
|
3009
|
+
const relModel = schemaByName.get(relatedModelName);
|
|
2963
3010
|
if (!isNotNullish(relModel)) {
|
|
2964
3011
|
throw new Error(
|
|
2965
|
-
`Relation '${relName}' on model ${model.name} references missing model '${
|
|
3012
|
+
`Relation '${relName}' on model ${model.name} references missing model '${relatedModelName}'.`
|
|
2966
3013
|
);
|
|
2967
3014
|
}
|
|
2968
3015
|
return { field, relModel };
|
|
@@ -3081,6 +3128,7 @@ function buildSelectWithNestedIncludes(relArgs, relModel, relAlias, ctx) {
|
|
|
3081
3128
|
relArgs,
|
|
3082
3129
|
relModel,
|
|
3083
3130
|
ctx.schemas,
|
|
3131
|
+
ctx.schemaByName,
|
|
3084
3132
|
relAlias,
|
|
3085
3133
|
ctx.aliasGen,
|
|
3086
3134
|
ctx.params,
|
|
@@ -3269,7 +3317,7 @@ function buildSingleInclude(relName, relArgs, field, relModel, ctx) {
|
|
|
3269
3317
|
ctx
|
|
3270
3318
|
});
|
|
3271
3319
|
}
|
|
3272
|
-
function buildIncludeSqlInternal(args, model, schemas, parentAlias, aliasGen, params, dialect, visitPath = [], depth = 0, stats) {
|
|
3320
|
+
function buildIncludeSqlInternal(args, model, schemas, schemaByName, parentAlias, aliasGen, params, dialect, visitPath = [], depth = 0, stats) {
|
|
3273
3321
|
if (!stats) stats = { totalIncludes: 0, totalSubqueries: 0, maxDepth: 0 };
|
|
3274
3322
|
if (depth > MAX_INCLUDE_DEPTH) {
|
|
3275
3323
|
throw new Error(
|
|
@@ -3293,7 +3341,12 @@ function buildIncludeSqlInternal(args, model, schemas, parentAlias, aliasGen, pa
|
|
|
3293
3341
|
`Query complexity limit exceeded: ${stats.totalSubqueries} subqueries generated. Maximum allowed: ${MAX_TOTAL_SUBQUERIES}. This indicates exponential include nesting. Stats: depth=${stats.maxDepth}, includes=${stats.totalIncludes}. Path: ${visitPath.join(" -> ")}. Simplify your include structure or split into multiple queries.`
|
|
3294
3342
|
);
|
|
3295
3343
|
}
|
|
3296
|
-
const resolved = resolveRelationOrThrow(
|
|
3344
|
+
const resolved = resolveRelationOrThrow(
|
|
3345
|
+
model,
|
|
3346
|
+
schemas,
|
|
3347
|
+
schemaByName,
|
|
3348
|
+
relName
|
|
3349
|
+
);
|
|
3297
3350
|
const relationPath = `${model.name}.${relName}`;
|
|
3298
3351
|
const currentPath = [...visitPath, relationPath];
|
|
3299
3352
|
if (visitPath.includes(relationPath)) {
|
|
@@ -3313,6 +3366,7 @@ function buildIncludeSqlInternal(args, model, schemas, parentAlias, aliasGen, pa
|
|
|
3313
3366
|
buildSingleInclude(relName, relArgs, resolved.field, resolved.relModel, {
|
|
3314
3367
|
model,
|
|
3315
3368
|
schemas,
|
|
3369
|
+
schemaByName,
|
|
3316
3370
|
parentAlias,
|
|
3317
3371
|
aliasGen,
|
|
3318
3372
|
dialect,
|
|
@@ -3332,10 +3386,13 @@ function buildIncludeSql(args, model, schemas, parentAlias, params, dialect) {
|
|
|
3332
3386
|
totalSubqueries: 0,
|
|
3333
3387
|
maxDepth: 0
|
|
3334
3388
|
};
|
|
3389
|
+
const schemaByName = /* @__PURE__ */ new Map();
|
|
3390
|
+
for (const m of schemas) schemaByName.set(m.name, m);
|
|
3335
3391
|
return buildIncludeSqlInternal(
|
|
3336
3392
|
args,
|
|
3337
3393
|
model,
|
|
3338
3394
|
schemas,
|
|
3395
|
+
schemaByName,
|
|
3339
3396
|
parentAlias,
|
|
3340
3397
|
aliasGen,
|
|
3341
3398
|
params,
|
|
@@ -3345,7 +3402,7 @@ function buildIncludeSql(args, model, schemas, parentAlias, params, dialect) {
|
|
|
3345
3402
|
stats
|
|
3346
3403
|
);
|
|
3347
3404
|
}
|
|
3348
|
-
function resolveCountRelationOrThrow(relName, model, schemas) {
|
|
3405
|
+
function resolveCountRelationOrThrow(relName, model, schemas, schemaByName) {
|
|
3349
3406
|
const relationSet = getRelationFieldSet(model);
|
|
3350
3407
|
if (!relationSet.has(relName)) {
|
|
3351
3408
|
throw new Error(
|
|
@@ -3362,10 +3419,16 @@ function resolveCountRelationOrThrow(relName, model, schemas) {
|
|
|
3362
3419
|
`_count.${relName} has invalid relation metadata on model ${model.name}`
|
|
3363
3420
|
);
|
|
3364
3421
|
}
|
|
3365
|
-
const
|
|
3422
|
+
const relatedModelName = field.relatedModel;
|
|
3423
|
+
if (!isNotNullish(relatedModelName) || String(relatedModelName).trim().length === 0) {
|
|
3424
|
+
throw new Error(
|
|
3425
|
+
`_count.${relName} is missing relatedModel metadata on model ${model.name}`
|
|
3426
|
+
);
|
|
3427
|
+
}
|
|
3428
|
+
const relModel = schemaByName.get(relatedModelName);
|
|
3366
3429
|
if (!relModel) {
|
|
3367
3430
|
throw new Error(
|
|
3368
|
-
`Related model '${
|
|
3431
|
+
`Related model '${relatedModelName}' not found for _count.${relName}`
|
|
3369
3432
|
);
|
|
3370
3433
|
}
|
|
3371
3434
|
return { field, relModel };
|
|
@@ -3451,9 +3514,16 @@ function buildRelationCountSql(countSelect, model, schemas, parentAlias, _params
|
|
|
3451
3514
|
const joins = [];
|
|
3452
3515
|
const pairs = [];
|
|
3453
3516
|
const aliasGen = createAliasGenerator();
|
|
3517
|
+
const schemaByName = /* @__PURE__ */ new Map();
|
|
3518
|
+
for (const m of schemas) schemaByName.set(m.name, m);
|
|
3454
3519
|
for (const [relName, shouldCount] of Object.entries(countSelect)) {
|
|
3455
3520
|
if (!shouldCount) continue;
|
|
3456
|
-
const resolved = resolveCountRelationOrThrow(
|
|
3521
|
+
const resolved = resolveCountRelationOrThrow(
|
|
3522
|
+
relName,
|
|
3523
|
+
model,
|
|
3524
|
+
schemas,
|
|
3525
|
+
schemaByName
|
|
3526
|
+
);
|
|
3457
3527
|
const built = buildCountJoinAndPair({
|
|
3458
3528
|
relName,
|
|
3459
3529
|
field: resolved.field,
|
|
@@ -3469,35 +3539,36 @@ function buildRelationCountSql(countSelect, model, schemas, parentAlias, _params
|
|
|
3469
3539
|
return { joins, jsonPairs: pairs.join(SQL_SEPARATORS.FIELD_LIST) };
|
|
3470
3540
|
}
|
|
3471
3541
|
|
|
3472
|
-
// src/builder/
|
|
3473
|
-
var ALIAS_CAPTURE = "([A-Za-z_][A-Za-z0-9_]*)";
|
|
3474
|
-
var COLUMN_PART = '(?:"([^"]+)"|([a-z_][a-z0-9_]*))';
|
|
3475
|
-
var AS_PART = `(?:\\s+AS\\s+${COLUMN_PART})?`;
|
|
3476
|
-
var SIMPLE_COLUMN_PATTERN = `^${ALIAS_CAPTURE}\\.${COLUMN_PART}${AS_PART}$`;
|
|
3477
|
-
var SIMPLE_COLUMN_RE = new RegExp(SIMPLE_COLUMN_PATTERN, "i");
|
|
3542
|
+
// src/builder/shared/string-builder.ts
|
|
3478
3543
|
function joinNonEmpty(parts, sep) {
|
|
3479
|
-
|
|
3544
|
+
let result = "";
|
|
3545
|
+
for (const p of parts) {
|
|
3546
|
+
if (p) {
|
|
3547
|
+
if (result) result += sep;
|
|
3548
|
+
result += p;
|
|
3549
|
+
}
|
|
3550
|
+
}
|
|
3551
|
+
return result;
|
|
3480
3552
|
}
|
|
3481
3553
|
function buildWhereSql(conditions) {
|
|
3482
3554
|
if (!isNonEmptyArray(conditions)) return "";
|
|
3483
|
-
|
|
3484
|
-
SQL_TEMPLATES.WHERE,
|
|
3485
|
-
conditions.join(SQL_SEPARATORS.CONDITION_AND)
|
|
3486
|
-
];
|
|
3487
|
-
return ` ${parts.join(" ")}`;
|
|
3555
|
+
return " " + SQL_TEMPLATES.WHERE + " " + conditions.join(SQL_SEPARATORS.CONDITION_AND);
|
|
3488
3556
|
}
|
|
3489
3557
|
function buildJoinsSql(...joinGroups) {
|
|
3490
3558
|
const all = [];
|
|
3491
3559
|
for (const g of joinGroups) {
|
|
3492
|
-
if (isNonEmptyArray(g))
|
|
3560
|
+
if (isNonEmptyArray(g)) {
|
|
3561
|
+
for (const j of g) all.push(j);
|
|
3562
|
+
}
|
|
3493
3563
|
}
|
|
3494
|
-
return all.length > 0 ?
|
|
3564
|
+
return all.length > 0 ? " " + all.join(" ") : "";
|
|
3495
3565
|
}
|
|
3496
3566
|
function buildSelectList(baseSelect, extraCols) {
|
|
3497
3567
|
const base = baseSelect.trim();
|
|
3498
3568
|
const extra = extraCols.trim();
|
|
3499
|
-
if (base
|
|
3500
|
-
return base
|
|
3569
|
+
if (!base) return extra;
|
|
3570
|
+
if (!extra) return base;
|
|
3571
|
+
return base + SQL_SEPARATORS.FIELD_LIST + extra;
|
|
3501
3572
|
}
|
|
3502
3573
|
function finalizeSql(sql, params, dialect) {
|
|
3503
3574
|
const snapshot = params.snapshot();
|
|
@@ -3507,38 +3578,128 @@ function finalizeSql(sql, params, dialect) {
|
|
|
3507
3578
|
snapshot.params,
|
|
3508
3579
|
dialect === "sqlite" ? "postgres" : dialect
|
|
3509
3580
|
);
|
|
3510
|
-
return
|
|
3581
|
+
return {
|
|
3511
3582
|
sql,
|
|
3512
3583
|
params: snapshot.params,
|
|
3513
|
-
paramMappings:
|
|
3514
|
-
}
|
|
3584
|
+
paramMappings: snapshot.mappings
|
|
3585
|
+
};
|
|
3515
3586
|
}
|
|
3516
3587
|
function parseSimpleScalarSelect(select, fromAlias) {
|
|
3517
|
-
var _a, _b, _c, _d;
|
|
3518
3588
|
const raw = select.trim();
|
|
3519
3589
|
if (raw.length === 0) return [];
|
|
3590
|
+
const fromLower = fromAlias.toLowerCase();
|
|
3520
3591
|
const parts = raw.split(SQL_SEPARATORS.FIELD_LIST);
|
|
3521
3592
|
const names = [];
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
const
|
|
3525
|
-
if (
|
|
3593
|
+
const isIdent = (s) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(s);
|
|
3594
|
+
const readIdentOrQuoted = (s, start) => {
|
|
3595
|
+
const n = s.length;
|
|
3596
|
+
if (start >= n) return { text: "", next: start, quoted: false };
|
|
3597
|
+
if (s.charCodeAt(start) === 34) {
|
|
3598
|
+
let i2 = start + 1;
|
|
3599
|
+
let out = "";
|
|
3600
|
+
let saw = false;
|
|
3601
|
+
while (i2 < n) {
|
|
3602
|
+
const c = s.charCodeAt(i2);
|
|
3603
|
+
if (c === 34) {
|
|
3604
|
+
const next = i2 + 1;
|
|
3605
|
+
if (next < n && s.charCodeAt(next) === 34) {
|
|
3606
|
+
out += '"';
|
|
3607
|
+
saw = true;
|
|
3608
|
+
i2 += 2;
|
|
3609
|
+
continue;
|
|
3610
|
+
}
|
|
3611
|
+
if (!saw)
|
|
3612
|
+
throw new Error(
|
|
3613
|
+
`sqlite distinct emulation: empty quoted identifier in: ${s}`
|
|
3614
|
+
);
|
|
3615
|
+
return { text: out, next: i2 + 1, quoted: true };
|
|
3616
|
+
}
|
|
3617
|
+
out += s[i2];
|
|
3618
|
+
saw = true;
|
|
3619
|
+
i2++;
|
|
3620
|
+
}
|
|
3526
3621
|
throw new Error(
|
|
3527
|
-
`sqlite distinct emulation
|
|
3622
|
+
`sqlite distinct emulation: unterminated quoted identifier in: ${s}`
|
|
3528
3623
|
);
|
|
3529
3624
|
}
|
|
3530
|
-
|
|
3531
|
-
|
|
3625
|
+
let i = start;
|
|
3626
|
+
while (i < n) {
|
|
3627
|
+
const c = s.charCodeAt(i);
|
|
3628
|
+
if (c === 32 || c === 9) break;
|
|
3629
|
+
if (c === 46) break;
|
|
3630
|
+
i++;
|
|
3631
|
+
}
|
|
3632
|
+
return { text: s.slice(start, i), next: i, quoted: false };
|
|
3633
|
+
};
|
|
3634
|
+
const skipSpaces = (s, i) => {
|
|
3635
|
+
while (i < s.length) {
|
|
3636
|
+
const c = s.charCodeAt(i);
|
|
3637
|
+
if (c !== 32 && c !== 9) break;
|
|
3638
|
+
i++;
|
|
3639
|
+
}
|
|
3640
|
+
return i;
|
|
3641
|
+
};
|
|
3642
|
+
for (let idx = 0; idx < parts.length; idx++) {
|
|
3643
|
+
const p = parts[idx].trim();
|
|
3644
|
+
if (p.length === 0) continue;
|
|
3645
|
+
let i = 0;
|
|
3646
|
+
i = skipSpaces(p, i);
|
|
3647
|
+
const a = readIdentOrQuoted(p, i);
|
|
3648
|
+
const actualAlias = a.text.toLowerCase();
|
|
3649
|
+
if (!isIdent(a.text)) {
|
|
3532
3650
|
throw new Error(
|
|
3533
|
-
`
|
|
3651
|
+
`sqlite distinct emulation requires scalar select fields to be simple columns (alias.column). Got: ${p}`
|
|
3534
3652
|
);
|
|
3535
3653
|
}
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3654
|
+
if (actualAlias !== fromLower) {
|
|
3655
|
+
throw new Error(`Expected alias '${fromAlias}', got '${a.text}' in: ${p}`);
|
|
3656
|
+
}
|
|
3657
|
+
i = a.next;
|
|
3658
|
+
if (i >= p.length || p.charCodeAt(i) !== 46) {
|
|
3659
|
+
throw new Error(
|
|
3660
|
+
`sqlite distinct emulation requires scalar select fields to be simple columns (alias.column). Got: ${p}`
|
|
3661
|
+
);
|
|
3662
|
+
}
|
|
3663
|
+
i++;
|
|
3664
|
+
i = skipSpaces(p, i);
|
|
3665
|
+
const colPart = readIdentOrQuoted(p, i);
|
|
3666
|
+
const columnName = colPart.text.trim();
|
|
3667
|
+
if (columnName.length === 0) {
|
|
3540
3668
|
throw new Error(`Failed to parse selected column name from: ${p}`);
|
|
3541
3669
|
}
|
|
3670
|
+
i = colPart.next;
|
|
3671
|
+
i = skipSpaces(p, i);
|
|
3672
|
+
let outAlias = "";
|
|
3673
|
+
if (i < p.length) {
|
|
3674
|
+
const rest = p.slice(i).trim();
|
|
3675
|
+
if (rest.length > 0) {
|
|
3676
|
+
const m = rest.match(/^AS\s+/i);
|
|
3677
|
+
if (!m) {
|
|
3678
|
+
throw new Error(
|
|
3679
|
+
`sqlite distinct emulation requires scalar select fields to be simple columns (optionally with AS). Got: ${p}`
|
|
3680
|
+
);
|
|
3681
|
+
}
|
|
3682
|
+
let j = i;
|
|
3683
|
+
j = skipSpaces(p, j);
|
|
3684
|
+
if (!/^AS\b/i.test(p.slice(j))) {
|
|
3685
|
+
throw new Error(`Failed to parse AS in: ${p}`);
|
|
3686
|
+
}
|
|
3687
|
+
j += 2;
|
|
3688
|
+
j = skipSpaces(p, j);
|
|
3689
|
+
const out = readIdentOrQuoted(p, j);
|
|
3690
|
+
outAlias = out.text.trim();
|
|
3691
|
+
if (outAlias.length === 0) {
|
|
3692
|
+
throw new Error(`Failed to parse output alias from: ${p}`);
|
|
3693
|
+
}
|
|
3694
|
+
j = skipSpaces(p, out.next);
|
|
3695
|
+
if (j !== p.length) {
|
|
3696
|
+
throw new Error(
|
|
3697
|
+
`sqlite distinct emulation requires scalar select fields to be simple columns (optionally with AS). Got: ${p}`
|
|
3698
|
+
);
|
|
3699
|
+
}
|
|
3700
|
+
}
|
|
3701
|
+
}
|
|
3702
|
+
const name = outAlias.length > 0 ? outAlias : columnName;
|
|
3542
3703
|
names.push(name);
|
|
3543
3704
|
}
|
|
3544
3705
|
return names;
|
|
@@ -3548,15 +3709,14 @@ function replaceOrderByAlias(orderBy, fromAlias, outerAlias) {
|
|
|
3548
3709
|
if (src.length === 0) return orderBy;
|
|
3549
3710
|
const escaped = src.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3550
3711
|
const re = new RegExp(`\\b${escaped}\\.`, "gi");
|
|
3551
|
-
return orderBy.replace(re,
|
|
3712
|
+
return orderBy.replace(re, outerAlias + ".");
|
|
3552
3713
|
}
|
|
3553
3714
|
function buildDistinctColumns(distinct, fromAlias, model) {
|
|
3554
3715
|
return distinct.map((f) => col(fromAlias, f, model)).join(SQL_SEPARATORS.FIELD_LIST);
|
|
3555
3716
|
}
|
|
3556
3717
|
function buildOutputColumns(scalarNames, includeNames, hasCount) {
|
|
3557
|
-
const outputCols = [...scalarNames, ...includeNames];
|
|
3558
|
-
|
|
3559
|
-
const formatted = outputCols.map((n) => quote(n)).join(SQL_SEPARATORS.FIELD_LIST);
|
|
3718
|
+
const outputCols = hasCount ? [...scalarNames, ...includeNames, "_count"] : [...scalarNames, ...includeNames];
|
|
3719
|
+
const formatted = outputCols.map((n) => quote2(n)).join(SQL_SEPARATORS.FIELD_LIST);
|
|
3560
3720
|
if (!isNonEmptyString(formatted)) {
|
|
3561
3721
|
throw new Error("distinct emulation requires at least one output column");
|
|
3562
3722
|
}
|
|
@@ -3567,11 +3727,11 @@ function buildWindowOrder(args) {
|
|
|
3567
3727
|
const fromLower = String(fromAlias).toLowerCase();
|
|
3568
3728
|
const orderFields = baseOrder.split(SQL_SEPARATORS.ORDER_BY).map((s) => s.trim().toLowerCase());
|
|
3569
3729
|
const hasIdInOrder = orderFields.some(
|
|
3570
|
-
(f) => f.startsWith(
|
|
3730
|
+
(f) => f.startsWith(fromLower + ".id ") || f.startsWith(fromLower + '."id" ')
|
|
3571
3731
|
);
|
|
3572
3732
|
if (hasIdInOrder) return baseOrder;
|
|
3573
|
-
const idTiebreaker = idField ?
|
|
3574
|
-
return
|
|
3733
|
+
const idTiebreaker = idField ? ", " + col(fromAlias, "id", model) + " ASC" : "";
|
|
3734
|
+
return baseOrder + idTiebreaker;
|
|
3575
3735
|
}
|
|
3576
3736
|
function buildSqliteDistinctQuery(spec, selectWithIncludes, countJoins) {
|
|
3577
3737
|
var _a, _b;
|
|
@@ -3588,7 +3748,7 @@ function buildSqliteDistinctQuery(spec, selectWithIncludes, countJoins) {
|
|
|
3588
3748
|
hasCount
|
|
3589
3749
|
);
|
|
3590
3750
|
const distinctCols = buildDistinctColumns([...distinct], from.alias, model);
|
|
3591
|
-
const fallbackOrder = [...distinct].map((f) =>
|
|
3751
|
+
const fallbackOrder = [...distinct].map((f) => col(from.alias, f, model) + " ASC").join(SQL_SEPARATORS.FIELD_LIST);
|
|
3592
3752
|
const idField = model.fields.find(
|
|
3593
3753
|
(f) => f.name === "id" && !f.isRelation
|
|
3594
3754
|
);
|
|
@@ -3599,7 +3759,7 @@ function buildSqliteDistinctQuery(spec, selectWithIncludes, countJoins) {
|
|
|
3599
3759
|
fromAlias: from.alias,
|
|
3600
3760
|
model
|
|
3601
3761
|
});
|
|
3602
|
-
const outerOrder = isNonEmptyString(orderBy) ? replaceOrderByAlias(orderBy, from.alias,
|
|
3762
|
+
const outerOrder = isNonEmptyString(orderBy) ? replaceOrderByAlias(orderBy, from.alias, '"__tp_distinct"') : replaceOrderByAlias(fallbackOrder, from.alias, '"__tp_distinct"');
|
|
3603
3763
|
const joins = buildJoinsSql(whereJoins, countJoins);
|
|
3604
3764
|
const conditions = [];
|
|
3605
3765
|
if (whereClause && whereClause !== "1=1") conditions.push(whereClause);
|
|
@@ -3609,7 +3769,7 @@ function buildSqliteDistinctQuery(spec, selectWithIncludes, countJoins) {
|
|
|
3609
3769
|
const innerParts = [
|
|
3610
3770
|
SQL_TEMPLATES.SELECT,
|
|
3611
3771
|
innerSelectList + innerComma,
|
|
3612
|
-
|
|
3772
|
+
"ROW_NUMBER() OVER (PARTITION BY " + distinctCols + " ORDER BY " + windowOrder + ")",
|
|
3613
3773
|
SQL_TEMPLATES.AS,
|
|
3614
3774
|
'"__tp_rn"',
|
|
3615
3775
|
SQL_TEMPLATES.FROM,
|
|
@@ -3618,12 +3778,12 @@ function buildSqliteDistinctQuery(spec, selectWithIncludes, countJoins) {
|
|
|
3618
3778
|
];
|
|
3619
3779
|
if (joins) innerParts.push(joins);
|
|
3620
3780
|
if (whereSql) innerParts.push(whereSql);
|
|
3621
|
-
const inner = innerParts.
|
|
3781
|
+
const inner = innerParts.join(" ");
|
|
3622
3782
|
const outerParts = [
|
|
3623
3783
|
SQL_TEMPLATES.SELECT,
|
|
3624
3784
|
outerSelectCols,
|
|
3625
3785
|
SQL_TEMPLATES.FROM,
|
|
3626
|
-
|
|
3786
|
+
"(" + inner + ")",
|
|
3627
3787
|
SQL_TEMPLATES.AS,
|
|
3628
3788
|
'"__tp_distinct"',
|
|
3629
3789
|
SQL_TEMPLATES.WHERE,
|
|
@@ -3632,7 +3792,7 @@ function buildSqliteDistinctQuery(spec, selectWithIncludes, countJoins) {
|
|
|
3632
3792
|
if (isNonEmptyString(outerOrder)) {
|
|
3633
3793
|
outerParts.push(SQL_TEMPLATES.ORDER_BY, outerOrder);
|
|
3634
3794
|
}
|
|
3635
|
-
return outerParts.
|
|
3795
|
+
return outerParts.join(" ");
|
|
3636
3796
|
}
|
|
3637
3797
|
function buildIncludeColumns(spec) {
|
|
3638
3798
|
var _a, _b;
|
|
@@ -3652,7 +3812,7 @@ function buildIncludeColumns(spec) {
|
|
|
3652
3812
|
dialect
|
|
3653
3813
|
);
|
|
3654
3814
|
if (countBuild.jsonPairs) {
|
|
3655
|
-
countCols =
|
|
3815
|
+
countCols = jsonBuildObject(countBuild.jsonPairs, dialect) + " " + SQL_TEMPLATES.AS + " " + quote2("_count");
|
|
3656
3816
|
}
|
|
3657
3817
|
countJoins = countBuild.joins;
|
|
3658
3818
|
}
|
|
@@ -3664,8 +3824,8 @@ function buildIncludeColumns(spec) {
|
|
|
3664
3824
|
}
|
|
3665
3825
|
const emptyJson = dialect === "postgres" ? `'[]'::json` : `json('[]')`;
|
|
3666
3826
|
const includeCols = hasIncludes ? includes.map((inc) => {
|
|
3667
|
-
const expr = inc.isOneToOne ?
|
|
3668
|
-
return
|
|
3827
|
+
const expr = inc.isOneToOne ? "(" + inc.sql + ")" : "COALESCE((" + inc.sql + "), " + emptyJson + ")";
|
|
3828
|
+
return expr + " " + SQL_TEMPLATES.AS + " " + quote2(inc.name);
|
|
3669
3829
|
}).join(SQL_SEPARATORS.FIELD_LIST) : "";
|
|
3670
3830
|
const allCols = joinNonEmpty(
|
|
3671
3831
|
[includeCols, countCols],
|
|
@@ -3733,7 +3893,7 @@ function withCountJoins(spec, countJoins, whereJoins) {
|
|
|
3733
3893
|
function buildPostgresDistinctOnClause(fromAlias, distinct, model) {
|
|
3734
3894
|
if (!isNonEmptyArray(distinct)) return null;
|
|
3735
3895
|
const distinctCols = buildDistinctColumns([...distinct], fromAlias, model);
|
|
3736
|
-
return
|
|
3896
|
+
return SQL_TEMPLATES.DISTINCT_ON + " (" + distinctCols + ")";
|
|
3737
3897
|
}
|
|
3738
3898
|
function pushJoinGroups(parts, ...groups) {
|
|
3739
3899
|
for (const g of groups) {
|
|
@@ -3778,9 +3938,7 @@ function constructFinalSql(spec) {
|
|
|
3778
3938
|
return finalizeSql(sql2, params, dialect);
|
|
3779
3939
|
}
|
|
3780
3940
|
const parts = [];
|
|
3781
|
-
if (cursorCte)
|
|
3782
|
-
parts.push(`WITH ${cursorCte}`);
|
|
3783
|
-
}
|
|
3941
|
+
if (cursorCte) parts.push("WITH " + cursorCte);
|
|
3784
3942
|
parts.push(SQL_TEMPLATES.SELECT);
|
|
3785
3943
|
const distinctOn = dialect === "postgres" ? buildPostgresDistinctOnClause(from.alias, distinct, model) : null;
|
|
3786
3944
|
if (distinctOn) parts.push(distinctOn);
|
|
@@ -3791,6 +3949,10 @@ function constructFinalSql(spec) {
|
|
|
3791
3949
|
}
|
|
3792
3950
|
parts.push(fullSelectList);
|
|
3793
3951
|
parts.push(SQL_TEMPLATES.FROM, from.table, from.alias);
|
|
3952
|
+
if (cursorCte) {
|
|
3953
|
+
const cteName = cursorCte.split(" AS ")[0].trim();
|
|
3954
|
+
parts.push("CROSS JOIN", cteName);
|
|
3955
|
+
}
|
|
3794
3956
|
pushJoinGroups(parts, whereJoins, countJoins);
|
|
3795
3957
|
const conditions = buildConditions(whereClause, cursorClause);
|
|
3796
3958
|
pushWhere(parts, conditions);
|
|
@@ -4013,14 +4175,16 @@ function buildSelectSql(input) {
|
|
|
4013
4175
|
}
|
|
4014
4176
|
|
|
4015
4177
|
// src/builder/shared/comparison-builder.ts
|
|
4016
|
-
|
|
4178
|
+
var DEFAULT_EXCLUDE_KEYS = /* @__PURE__ */ new Set(["mode"]);
|
|
4179
|
+
function buildComparisons(expr, filter, params, dialect, builder, excludeKeys = DEFAULT_EXCLUDE_KEYS) {
|
|
4017
4180
|
const out = [];
|
|
4018
|
-
for (const
|
|
4019
|
-
if (
|
|
4181
|
+
for (const op in filter) {
|
|
4182
|
+
if (!Object.prototype.hasOwnProperty.call(filter, op)) continue;
|
|
4183
|
+
if (excludeKeys.has(op)) continue;
|
|
4184
|
+
const val = filter[op];
|
|
4185
|
+
if (val === void 0) continue;
|
|
4020
4186
|
const built = builder(expr, op, val, params, dialect);
|
|
4021
|
-
if (built && built.trim().length > 0)
|
|
4022
|
-
out.push(built);
|
|
4023
|
-
}
|
|
4187
|
+
if (built && built.trim().length > 0) out.push(built);
|
|
4024
4188
|
}
|
|
4025
4189
|
return out;
|
|
4026
4190
|
}
|
|
@@ -4050,6 +4214,13 @@ var HAVING_ALLOWED_OPS = /* @__PURE__ */ new Set([
|
|
|
4050
4214
|
Ops.IN,
|
|
4051
4215
|
Ops.NOT_IN
|
|
4052
4216
|
]);
|
|
4217
|
+
var HAVING_FIELD_FIRST_AGG_KEYS = Object.freeze([
|
|
4218
|
+
"_count",
|
|
4219
|
+
"_sum",
|
|
4220
|
+
"_avg",
|
|
4221
|
+
"_min",
|
|
4222
|
+
"_max"
|
|
4223
|
+
]);
|
|
4053
4224
|
function isTruthySelection(v) {
|
|
4054
4225
|
return v === true;
|
|
4055
4226
|
}
|
|
@@ -4068,15 +4239,15 @@ function assertHavingOp(op) {
|
|
|
4068
4239
|
}
|
|
4069
4240
|
function aggExprForField(aggKey, field, alias, model) {
|
|
4070
4241
|
if (aggKey === "_count") {
|
|
4071
|
-
return field === "_all" ?
|
|
4242
|
+
return field === "_all" ? "COUNT(*)" : "COUNT(" + col(alias, field, model) + ")";
|
|
4072
4243
|
}
|
|
4073
4244
|
if (field === "_all") {
|
|
4074
4245
|
throw new Error(`'${aggKey}' does not support '_all'`);
|
|
4075
4246
|
}
|
|
4076
|
-
if (aggKey === "_sum") return
|
|
4077
|
-
if (aggKey === "_avg") return
|
|
4078
|
-
if (aggKey === "_min") return
|
|
4079
|
-
return
|
|
4247
|
+
if (aggKey === "_sum") return "SUM(" + col(alias, field, model) + ")";
|
|
4248
|
+
if (aggKey === "_avg") return "AVG(" + col(alias, field, model) + ")";
|
|
4249
|
+
if (aggKey === "_min") return "MIN(" + col(alias, field, model) + ")";
|
|
4250
|
+
return "MAX(" + col(alias, field, model) + ")";
|
|
4080
4251
|
}
|
|
4081
4252
|
function buildComparisonOp(op) {
|
|
4082
4253
|
const sqlOp = COMPARISON_OPS[op];
|
|
@@ -4103,8 +4274,8 @@ function normalizeLogicalValue2(operator, value) {
|
|
|
4103
4274
|
throw new Error(`${operator} must be an object or array of objects in HAVING`);
|
|
4104
4275
|
}
|
|
4105
4276
|
function buildNullComparison(expr, op) {
|
|
4106
|
-
if (op === Ops.EQUALS) return
|
|
4107
|
-
if (op === Ops.NOT) return
|
|
4277
|
+
if (op === Ops.EQUALS) return expr + " " + SQL_TEMPLATES.IS_NULL;
|
|
4278
|
+
if (op === Ops.NOT) return expr + " " + SQL_TEMPLATES.IS_NOT_NULL;
|
|
4108
4279
|
throw new Error(`Operator '${op}' doesn't support null in HAVING`);
|
|
4109
4280
|
}
|
|
4110
4281
|
function buildInComparison(expr, op, val, params, dialect) {
|
|
@@ -4125,7 +4296,7 @@ function buildInComparison(expr, op, val, params, dialect) {
|
|
|
4125
4296
|
function buildBinaryComparison(expr, op, val, params) {
|
|
4126
4297
|
const sqlOp = buildComparisonOp(op);
|
|
4127
4298
|
const placeholder = addHavingParam(params, op, val);
|
|
4128
|
-
return
|
|
4299
|
+
return expr + " " + sqlOp + " " + placeholder;
|
|
4129
4300
|
}
|
|
4130
4301
|
function buildSimpleComparison(expr, op, val, params, dialect) {
|
|
4131
4302
|
assertHavingOp(op);
|
|
@@ -4146,20 +4317,21 @@ function buildSimpleComparison(expr, op, val, params, dialect) {
|
|
|
4146
4317
|
return buildBinaryComparison(expr, op, val, params);
|
|
4147
4318
|
}
|
|
4148
4319
|
function negateClauses(subClauses) {
|
|
4149
|
-
if (subClauses.length === 1) return
|
|
4150
|
-
return
|
|
4320
|
+
if (subClauses.length === 1) return SQL_TEMPLATES.NOT + " " + subClauses[0];
|
|
4321
|
+
return SQL_TEMPLATES.NOT + " (" + subClauses.join(SQL_SEPARATORS.CONDITION_AND) + ")";
|
|
4151
4322
|
}
|
|
4152
4323
|
function combineLogical(key, subClauses) {
|
|
4153
4324
|
if (key === LogicalOps.NOT) return negateClauses(subClauses);
|
|
4154
|
-
return subClauses.join(
|
|
4325
|
+
return subClauses.join(" " + key + " ");
|
|
4155
4326
|
}
|
|
4156
4327
|
function buildHavingNode(node, alias, params, dialect, model) {
|
|
4157
4328
|
const clauses = [];
|
|
4158
|
-
const
|
|
4159
|
-
|
|
4329
|
+
for (const key in node) {
|
|
4330
|
+
if (!Object.prototype.hasOwnProperty.call(node, key)) continue;
|
|
4331
|
+
const value = node[key];
|
|
4160
4332
|
const built = buildHavingEntry(key, value, alias, params, dialect, model);
|
|
4161
4333
|
for (const c of built) {
|
|
4162
|
-
if (c && c.
|
|
4334
|
+
if (c && c.length > 0) clauses.push(c);
|
|
4163
4335
|
}
|
|
4164
4336
|
}
|
|
4165
4337
|
return clauses.join(SQL_SEPARATORS.CONDITION_AND);
|
|
@@ -4169,7 +4341,7 @@ function buildLogicalClause2(key, value, alias, params, dialect, model) {
|
|
|
4169
4341
|
const subClauses = [];
|
|
4170
4342
|
for (const it of items) {
|
|
4171
4343
|
const c = buildHavingNode(it, alias, params, dialect, model);
|
|
4172
|
-
if (c && c.
|
|
4344
|
+
if (c && c.length > 0) subClauses.push("(" + c + ")");
|
|
4173
4345
|
}
|
|
4174
4346
|
if (subClauses.length === 0) return "";
|
|
4175
4347
|
return combineLogical(key, subClauses);
|
|
@@ -4194,11 +4366,23 @@ function buildHavingForAggregateFirstShape(aggKey, target, alias, params, dialec
|
|
|
4194
4366
|
throw new Error(`HAVING '${aggKey}' must be an object`);
|
|
4195
4367
|
}
|
|
4196
4368
|
const out = [];
|
|
4197
|
-
|
|
4369
|
+
const targetObj = target;
|
|
4370
|
+
for (const field in targetObj) {
|
|
4371
|
+
if (!Object.prototype.hasOwnProperty.call(targetObj, field)) continue;
|
|
4198
4372
|
assertHavingAggTarget(aggKey, field, model);
|
|
4199
|
-
|
|
4373
|
+
const filter = targetObj[field];
|
|
4374
|
+
if (!isPlainObject(filter)) continue;
|
|
4375
|
+
const filterObj = filter;
|
|
4376
|
+
let hasAny = false;
|
|
4377
|
+
for (const k in filterObj) {
|
|
4378
|
+
if (Object.prototype.hasOwnProperty.call(filterObj, k)) {
|
|
4379
|
+
hasAny = true;
|
|
4380
|
+
break;
|
|
4381
|
+
}
|
|
4382
|
+
}
|
|
4383
|
+
if (!hasAny) continue;
|
|
4200
4384
|
const expr = aggExprForField(aggKey, field, alias, model);
|
|
4201
|
-
out.push(...buildHavingOpsForExpr(expr,
|
|
4385
|
+
out.push(...buildHavingOpsForExpr(expr, filterObj, params, dialect));
|
|
4202
4386
|
}
|
|
4203
4387
|
return out;
|
|
4204
4388
|
}
|
|
@@ -4209,16 +4393,23 @@ function buildHavingForFieldFirstShape(fieldName, target, alias, params, dialect
|
|
|
4209
4393
|
assertScalarField(model, fieldName, "HAVING");
|
|
4210
4394
|
const out = [];
|
|
4211
4395
|
const obj = target;
|
|
4212
|
-
const
|
|
4213
|
-
for (const aggKey of keys) {
|
|
4396
|
+
for (const aggKey of HAVING_FIELD_FIRST_AGG_KEYS) {
|
|
4214
4397
|
const aggFilter = obj[aggKey];
|
|
4215
4398
|
if (!isPlainObject(aggFilter)) continue;
|
|
4216
|
-
|
|
4399
|
+
const aggFilterObj = aggFilter;
|
|
4400
|
+
let hasAny = false;
|
|
4401
|
+
for (const k in aggFilterObj) {
|
|
4402
|
+
if (Object.prototype.hasOwnProperty.call(aggFilterObj, k)) {
|
|
4403
|
+
hasAny = true;
|
|
4404
|
+
break;
|
|
4405
|
+
}
|
|
4406
|
+
}
|
|
4407
|
+
if (!hasAny) continue;
|
|
4217
4408
|
if (aggKey === "_sum" || aggKey === "_avg") {
|
|
4218
4409
|
assertNumericField(model, fieldName, "HAVING");
|
|
4219
4410
|
}
|
|
4220
4411
|
const expr = aggExprForField(aggKey, fieldName, alias, model);
|
|
4221
|
-
out.push(...buildHavingOpsForExpr(expr,
|
|
4412
|
+
out.push(...buildHavingOpsForExpr(expr, aggFilterObj, params, dialect));
|
|
4222
4413
|
}
|
|
4223
4414
|
return out;
|
|
4224
4415
|
}
|
|
@@ -4267,13 +4458,13 @@ function normalizeCountArg(v) {
|
|
|
4267
4458
|
}
|
|
4268
4459
|
function pushCountAllField(fields) {
|
|
4269
4460
|
fields.push(
|
|
4270
|
-
|
|
4461
|
+
SQL_TEMPLATES.COUNT_ALL + " " + SQL_TEMPLATES.AS + " " + quote2("_count._all")
|
|
4271
4462
|
);
|
|
4272
4463
|
}
|
|
4273
4464
|
function pushCountField(fields, alias, fieldName, model) {
|
|
4274
|
-
const outAlias =
|
|
4465
|
+
const outAlias = "_count." + fieldName;
|
|
4275
4466
|
fields.push(
|
|
4276
|
-
|
|
4467
|
+
"COUNT(" + col(alias, fieldName, model) + ") " + SQL_TEMPLATES.AS + " " + quote2(outAlias)
|
|
4277
4468
|
);
|
|
4278
4469
|
}
|
|
4279
4470
|
function addCountFields(fields, countArg, alias, model) {
|
|
@@ -4286,12 +4477,14 @@ function addCountFields(fields, countArg, alias, model) {
|
|
|
4286
4477
|
if (countArg._all === true) {
|
|
4287
4478
|
pushCountAllField(fields);
|
|
4288
4479
|
}
|
|
4289
|
-
const
|
|
4290
|
-
(
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4480
|
+
for (const f in countArg) {
|
|
4481
|
+
if (!Object.prototype.hasOwnProperty.call(countArg, f)) continue;
|
|
4482
|
+
if (f === "_all") continue;
|
|
4483
|
+
const v = countArg[f];
|
|
4484
|
+
if (isTruthySelection(v)) {
|
|
4485
|
+
assertScalarField(model, f, "_count");
|
|
4486
|
+
pushCountField(fields, alias, f, model);
|
|
4487
|
+
}
|
|
4295
4488
|
}
|
|
4296
4489
|
}
|
|
4297
4490
|
function getAggregateSelectionObject(args, agg) {
|
|
@@ -4306,16 +4499,18 @@ function assertAggregatableScalarField(model, agg, fieldName) {
|
|
|
4306
4499
|
}
|
|
4307
4500
|
}
|
|
4308
4501
|
function pushAggregateFieldSql(fields, aggFn, alias, agg, fieldName, model) {
|
|
4309
|
-
const outAlias =
|
|
4502
|
+
const outAlias = agg + "." + fieldName;
|
|
4310
4503
|
fields.push(
|
|
4311
|
-
|
|
4504
|
+
aggFn + "(" + col(alias, fieldName, model) + ") " + SQL_TEMPLATES.AS + " " + quote2(outAlias)
|
|
4312
4505
|
);
|
|
4313
4506
|
}
|
|
4314
4507
|
function addAggregateFields(fields, args, alias, model) {
|
|
4315
4508
|
for (const [agg, aggFn] of AGGREGATES) {
|
|
4316
4509
|
const obj = getAggregateSelectionObject(args, agg);
|
|
4317
4510
|
if (!obj) continue;
|
|
4318
|
-
for (const
|
|
4511
|
+
for (const fieldName in obj) {
|
|
4512
|
+
if (!Object.prototype.hasOwnProperty.call(obj, fieldName)) continue;
|
|
4513
|
+
const selection = obj[fieldName];
|
|
4319
4514
|
if (fieldName === "_all")
|
|
4320
4515
|
throw new Error(`'${agg}' does not support '_all'`);
|
|
4321
4516
|
if (!isTruthySelection(selection)) continue;
|
|
@@ -4339,22 +4534,23 @@ function buildAggregateSql(args, whereResult, tableName, alias, model) {
|
|
|
4339
4534
|
throw new Error("buildAggregateSql requires at least one aggregate field");
|
|
4340
4535
|
}
|
|
4341
4536
|
const selectClause = aggFields.join(SQL_SEPARATORS.FIELD_LIST);
|
|
4342
|
-
const whereClause = isValidWhereClause(whereResult.clause) ?
|
|
4343
|
-
const
|
|
4537
|
+
const whereClause = isValidWhereClause(whereResult.clause) ? SQL_TEMPLATES.WHERE + " " + whereResult.clause : "";
|
|
4538
|
+
const parts = [
|
|
4344
4539
|
SQL_TEMPLATES.SELECT,
|
|
4345
4540
|
selectClause,
|
|
4346
4541
|
SQL_TEMPLATES.FROM,
|
|
4347
4542
|
tableName,
|
|
4348
|
-
alias
|
|
4349
|
-
|
|
4350
|
-
|
|
4543
|
+
alias
|
|
4544
|
+
];
|
|
4545
|
+
if (whereClause) parts.push(whereClause);
|
|
4546
|
+
const sql = parts.join(" ").trim();
|
|
4351
4547
|
validateSelectQuery(sql);
|
|
4352
4548
|
validateParamConsistency(sql, whereResult.params);
|
|
4353
|
-
return
|
|
4549
|
+
return {
|
|
4354
4550
|
sql,
|
|
4355
|
-
params:
|
|
4356
|
-
paramMappings:
|
|
4357
|
-
}
|
|
4551
|
+
params: whereResult.params,
|
|
4552
|
+
paramMappings: whereResult.paramMappings
|
|
4553
|
+
};
|
|
4358
4554
|
}
|
|
4359
4555
|
function assertGroupByBy(args, model) {
|
|
4360
4556
|
if (!isNotNullish(args.by) || !isNonEmptyArray(args.by)) {
|
|
@@ -4382,8 +4578,8 @@ function buildGroupByHaving(args, alias, params, model, dialect) {
|
|
|
4382
4578
|
if (!isNotNullish(args.having)) return "";
|
|
4383
4579
|
if (!isPlainObject(args.having)) throw new Error("having must be an object");
|
|
4384
4580
|
const h = buildHavingClause(args.having, alias, params, model, dialect);
|
|
4385
|
-
if (!h || h.
|
|
4386
|
-
return
|
|
4581
|
+
if (!h || h.length === 0) return "";
|
|
4582
|
+
return SQL_TEMPLATES.HAVING + " " + h;
|
|
4387
4583
|
}
|
|
4388
4584
|
function buildGroupBySql(args, whereResult, tableName, alias, model, dialect) {
|
|
4389
4585
|
assertSafeAlias(alias);
|
|
@@ -4398,29 +4594,28 @@ function buildGroupBySql(args, whereResult, tableName, alias, model, dialect) {
|
|
|
4398
4594
|
byFields
|
|
4399
4595
|
);
|
|
4400
4596
|
const havingClause = buildGroupByHaving(args, alias, params, model, d);
|
|
4401
|
-
const whereClause = isValidWhereClause(whereResult.clause) ?
|
|
4402
|
-
const
|
|
4597
|
+
const whereClause = isValidWhereClause(whereResult.clause) ? SQL_TEMPLATES.WHERE + " " + whereResult.clause : "";
|
|
4598
|
+
const parts = [
|
|
4403
4599
|
SQL_TEMPLATES.SELECT,
|
|
4404
4600
|
selectFields,
|
|
4405
4601
|
SQL_TEMPLATES.FROM,
|
|
4406
4602
|
tableName,
|
|
4407
|
-
alias
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4603
|
+
alias
|
|
4604
|
+
];
|
|
4605
|
+
if (whereClause) parts.push(whereClause);
|
|
4606
|
+
parts.push(SQL_TEMPLATES.GROUP_BY, groupFields);
|
|
4607
|
+
if (havingClause) parts.push(havingClause);
|
|
4608
|
+
const sql = parts.join(" ").trim();
|
|
4413
4609
|
const snapshot = params.snapshot();
|
|
4610
|
+
const allParams = [...whereResult.params, ...snapshot.params];
|
|
4611
|
+
const allMappings = [...whereResult.paramMappings, ...snapshot.mappings];
|
|
4414
4612
|
validateSelectQuery(sql);
|
|
4415
|
-
validateParamConsistency(sql,
|
|
4416
|
-
return
|
|
4613
|
+
validateParamConsistency(sql, allParams);
|
|
4614
|
+
return {
|
|
4417
4615
|
sql,
|
|
4418
|
-
params:
|
|
4419
|
-
paramMappings:
|
|
4420
|
-
|
|
4421
|
-
...snapshot.mappings
|
|
4422
|
-
])
|
|
4423
|
-
});
|
|
4616
|
+
params: allParams,
|
|
4617
|
+
paramMappings: allMappings
|
|
4618
|
+
};
|
|
4424
4619
|
}
|
|
4425
4620
|
function buildCountSql(whereResult, tableName, alias, skip, _dialect) {
|
|
4426
4621
|
assertSafeAlias(alias);
|
|
@@ -4448,24 +4643,25 @@ function buildCountSql(whereResult, tableName, alias, skip, _dialect) {
|
|
|
4448
4643
|
);
|
|
4449
4644
|
}
|
|
4450
4645
|
}
|
|
4451
|
-
const whereClause = isValidWhereClause(whereResult.clause) ?
|
|
4452
|
-
const
|
|
4646
|
+
const whereClause = isValidWhereClause(whereResult.clause) ? SQL_TEMPLATES.WHERE + " " + whereResult.clause : "";
|
|
4647
|
+
const parts = [
|
|
4453
4648
|
SQL_TEMPLATES.SELECT,
|
|
4454
4649
|
SQL_TEMPLATES.COUNT_ALL,
|
|
4455
4650
|
SQL_TEMPLATES.AS,
|
|
4456
|
-
|
|
4651
|
+
quote2("_count._all"),
|
|
4457
4652
|
SQL_TEMPLATES.FROM,
|
|
4458
4653
|
tableName,
|
|
4459
|
-
alias
|
|
4460
|
-
|
|
4461
|
-
|
|
4654
|
+
alias
|
|
4655
|
+
];
|
|
4656
|
+
if (whereClause) parts.push(whereClause);
|
|
4657
|
+
const sql = parts.join(" ").trim();
|
|
4462
4658
|
validateSelectQuery(sql);
|
|
4463
4659
|
validateParamConsistency(sql, whereResult.params);
|
|
4464
|
-
return
|
|
4660
|
+
return {
|
|
4465
4661
|
sql,
|
|
4466
|
-
params:
|
|
4467
|
-
paramMappings:
|
|
4468
|
-
}
|
|
4662
|
+
params: whereResult.params,
|
|
4663
|
+
paramMappings: whereResult.paramMappings
|
|
4664
|
+
};
|
|
4469
4665
|
}
|
|
4470
4666
|
function safeAlias(input) {
|
|
4471
4667
|
const raw = String(input).toLowerCase();
|