prisma-sql 1.48.1 → 1.50.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 +450 -262
- package/dist/generator.cjs.map +1 -1
- package/dist/generator.js +450 -262
- package/dist/generator.js.map +1 -1
- package/dist/index.cjs +452 -263
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +452 -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.50.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,19 @@ 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
|
+
]);
|
|
4224
|
+
function hasAnyOwnKey(obj) {
|
|
4225
|
+
for (const k in obj) {
|
|
4226
|
+
if (Object.prototype.hasOwnProperty.call(obj, k)) return true;
|
|
4227
|
+
}
|
|
4228
|
+
return false;
|
|
4229
|
+
}
|
|
4053
4230
|
function isTruthySelection(v) {
|
|
4054
4231
|
return v === true;
|
|
4055
4232
|
}
|
|
@@ -4068,15 +4245,15 @@ function assertHavingOp(op) {
|
|
|
4068
4245
|
}
|
|
4069
4246
|
function aggExprForField(aggKey, field, alias, model) {
|
|
4070
4247
|
if (aggKey === "_count") {
|
|
4071
|
-
return field === "_all" ?
|
|
4248
|
+
return field === "_all" ? "COUNT(*)" : "COUNT(" + col(alias, field, model) + ")";
|
|
4072
4249
|
}
|
|
4073
4250
|
if (field === "_all") {
|
|
4074
4251
|
throw new Error(`'${aggKey}' does not support '_all'`);
|
|
4075
4252
|
}
|
|
4076
|
-
if (aggKey === "_sum") return
|
|
4077
|
-
if (aggKey === "_avg") return
|
|
4078
|
-
if (aggKey === "_min") return
|
|
4079
|
-
return
|
|
4253
|
+
if (aggKey === "_sum") return "SUM(" + col(alias, field, model) + ")";
|
|
4254
|
+
if (aggKey === "_avg") return "AVG(" + col(alias, field, model) + ")";
|
|
4255
|
+
if (aggKey === "_min") return "MIN(" + col(alias, field, model) + ")";
|
|
4256
|
+
return "MAX(" + col(alias, field, model) + ")";
|
|
4080
4257
|
}
|
|
4081
4258
|
function buildComparisonOp(op) {
|
|
4082
4259
|
const sqlOp = COMPARISON_OPS[op];
|
|
@@ -4103,8 +4280,8 @@ function normalizeLogicalValue2(operator, value) {
|
|
|
4103
4280
|
throw new Error(`${operator} must be an object or array of objects in HAVING`);
|
|
4104
4281
|
}
|
|
4105
4282
|
function buildNullComparison(expr, op) {
|
|
4106
|
-
if (op === Ops.EQUALS) return
|
|
4107
|
-
if (op === Ops.NOT) return
|
|
4283
|
+
if (op === Ops.EQUALS) return expr + " " + SQL_TEMPLATES.IS_NULL;
|
|
4284
|
+
if (op === Ops.NOT) return expr + " " + SQL_TEMPLATES.IS_NOT_NULL;
|
|
4108
4285
|
throw new Error(`Operator '${op}' doesn't support null in HAVING`);
|
|
4109
4286
|
}
|
|
4110
4287
|
function buildInComparison(expr, op, val, params, dialect) {
|
|
@@ -4125,7 +4302,7 @@ function buildInComparison(expr, op, val, params, dialect) {
|
|
|
4125
4302
|
function buildBinaryComparison(expr, op, val, params) {
|
|
4126
4303
|
const sqlOp = buildComparisonOp(op);
|
|
4127
4304
|
const placeholder = addHavingParam(params, op, val);
|
|
4128
|
-
return
|
|
4305
|
+
return expr + " " + sqlOp + " " + placeholder;
|
|
4129
4306
|
}
|
|
4130
4307
|
function buildSimpleComparison(expr, op, val, params, dialect) {
|
|
4131
4308
|
assertHavingOp(op);
|
|
@@ -4146,20 +4323,21 @@ function buildSimpleComparison(expr, op, val, params, dialect) {
|
|
|
4146
4323
|
return buildBinaryComparison(expr, op, val, params);
|
|
4147
4324
|
}
|
|
4148
4325
|
function negateClauses(subClauses) {
|
|
4149
|
-
if (subClauses.length === 1) return
|
|
4150
|
-
return
|
|
4326
|
+
if (subClauses.length === 1) return SQL_TEMPLATES.NOT + " " + subClauses[0];
|
|
4327
|
+
return SQL_TEMPLATES.NOT + " (" + subClauses.join(SQL_SEPARATORS.CONDITION_AND) + ")";
|
|
4151
4328
|
}
|
|
4152
4329
|
function combineLogical(key, subClauses) {
|
|
4153
4330
|
if (key === LogicalOps.NOT) return negateClauses(subClauses);
|
|
4154
|
-
return subClauses.join(
|
|
4331
|
+
return subClauses.join(" " + key + " ");
|
|
4155
4332
|
}
|
|
4156
4333
|
function buildHavingNode(node, alias, params, dialect, model) {
|
|
4157
4334
|
const clauses = [];
|
|
4158
|
-
const
|
|
4159
|
-
|
|
4335
|
+
for (const key in node) {
|
|
4336
|
+
if (!Object.prototype.hasOwnProperty.call(node, key)) continue;
|
|
4337
|
+
const value = node[key];
|
|
4160
4338
|
const built = buildHavingEntry(key, value, alias, params, dialect, model);
|
|
4161
4339
|
for (const c of built) {
|
|
4162
|
-
if (c && c.
|
|
4340
|
+
if (c && c.length > 0) clauses.push(c);
|
|
4163
4341
|
}
|
|
4164
4342
|
}
|
|
4165
4343
|
return clauses.join(SQL_SEPARATORS.CONDITION_AND);
|
|
@@ -4169,7 +4347,7 @@ function buildLogicalClause2(key, value, alias, params, dialect, model) {
|
|
|
4169
4347
|
const subClauses = [];
|
|
4170
4348
|
for (const it of items) {
|
|
4171
4349
|
const c = buildHavingNode(it, alias, params, dialect, model);
|
|
4172
|
-
if (c && c.
|
|
4350
|
+
if (c && c.length > 0) subClauses.push("(" + c + ")");
|
|
4173
4351
|
}
|
|
4174
4352
|
if (subClauses.length === 0) return "";
|
|
4175
4353
|
return combineLogical(key, subClauses);
|
|
@@ -4194,11 +4372,16 @@ function buildHavingForAggregateFirstShape(aggKey, target, alias, params, dialec
|
|
|
4194
4372
|
throw new Error(`HAVING '${aggKey}' must be an object`);
|
|
4195
4373
|
}
|
|
4196
4374
|
const out = [];
|
|
4197
|
-
|
|
4375
|
+
const targetObj = target;
|
|
4376
|
+
for (const field in targetObj) {
|
|
4377
|
+
if (!Object.prototype.hasOwnProperty.call(targetObj, field)) continue;
|
|
4198
4378
|
assertHavingAggTarget(aggKey, field, model);
|
|
4199
|
-
|
|
4379
|
+
const filter = targetObj[field];
|
|
4380
|
+
if (!isPlainObject(filter)) continue;
|
|
4381
|
+
const filterObj = filter;
|
|
4382
|
+
if (!hasAnyOwnKey(filterObj)) continue;
|
|
4200
4383
|
const expr = aggExprForField(aggKey, field, alias, model);
|
|
4201
|
-
out.push(...buildHavingOpsForExpr(expr,
|
|
4384
|
+
out.push(...buildHavingOpsForExpr(expr, filterObj, params, dialect));
|
|
4202
4385
|
}
|
|
4203
4386
|
return out;
|
|
4204
4387
|
}
|
|
@@ -4209,16 +4392,16 @@ function buildHavingForFieldFirstShape(fieldName, target, alias, params, dialect
|
|
|
4209
4392
|
assertScalarField(model, fieldName, "HAVING");
|
|
4210
4393
|
const out = [];
|
|
4211
4394
|
const obj = target;
|
|
4212
|
-
const
|
|
4213
|
-
for (const aggKey of keys) {
|
|
4395
|
+
for (const aggKey of HAVING_FIELD_FIRST_AGG_KEYS) {
|
|
4214
4396
|
const aggFilter = obj[aggKey];
|
|
4215
4397
|
if (!isPlainObject(aggFilter)) continue;
|
|
4216
|
-
|
|
4398
|
+
const aggFilterObj = aggFilter;
|
|
4399
|
+
if (!hasAnyOwnKey(aggFilterObj)) continue;
|
|
4217
4400
|
if (aggKey === "_sum" || aggKey === "_avg") {
|
|
4218
4401
|
assertNumericField(model, fieldName, "HAVING");
|
|
4219
4402
|
}
|
|
4220
4403
|
const expr = aggExprForField(aggKey, fieldName, alias, model);
|
|
4221
|
-
out.push(...buildHavingOpsForExpr(expr,
|
|
4404
|
+
out.push(...buildHavingOpsForExpr(expr, aggFilterObj, params, dialect));
|
|
4222
4405
|
}
|
|
4223
4406
|
return out;
|
|
4224
4407
|
}
|
|
@@ -4267,13 +4450,13 @@ function normalizeCountArg(v) {
|
|
|
4267
4450
|
}
|
|
4268
4451
|
function pushCountAllField(fields) {
|
|
4269
4452
|
fields.push(
|
|
4270
|
-
|
|
4453
|
+
SQL_TEMPLATES.COUNT_ALL + " " + SQL_TEMPLATES.AS + " " + quote2("_count._all")
|
|
4271
4454
|
);
|
|
4272
4455
|
}
|
|
4273
4456
|
function pushCountField(fields, alias, fieldName, model) {
|
|
4274
|
-
const outAlias =
|
|
4457
|
+
const outAlias = "_count." + fieldName;
|
|
4275
4458
|
fields.push(
|
|
4276
|
-
|
|
4459
|
+
"COUNT(" + col(alias, fieldName, model) + ") " + SQL_TEMPLATES.AS + " " + quote2(outAlias)
|
|
4277
4460
|
);
|
|
4278
4461
|
}
|
|
4279
4462
|
function addCountFields(fields, countArg, alias, model) {
|
|
@@ -4286,12 +4469,14 @@ function addCountFields(fields, countArg, alias, model) {
|
|
|
4286
4469
|
if (countArg._all === true) {
|
|
4287
4470
|
pushCountAllField(fields);
|
|
4288
4471
|
}
|
|
4289
|
-
const
|
|
4290
|
-
(
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4472
|
+
for (const f in countArg) {
|
|
4473
|
+
if (!Object.prototype.hasOwnProperty.call(countArg, f)) continue;
|
|
4474
|
+
if (f === "_all") continue;
|
|
4475
|
+
const v = countArg[f];
|
|
4476
|
+
if (isTruthySelection(v)) {
|
|
4477
|
+
assertScalarField(model, f, "_count");
|
|
4478
|
+
pushCountField(fields, alias, f, model);
|
|
4479
|
+
}
|
|
4295
4480
|
}
|
|
4296
4481
|
}
|
|
4297
4482
|
function getAggregateSelectionObject(args, agg) {
|
|
@@ -4306,16 +4491,18 @@ function assertAggregatableScalarField(model, agg, fieldName) {
|
|
|
4306
4491
|
}
|
|
4307
4492
|
}
|
|
4308
4493
|
function pushAggregateFieldSql(fields, aggFn, alias, agg, fieldName, model) {
|
|
4309
|
-
const outAlias =
|
|
4494
|
+
const outAlias = agg + "." + fieldName;
|
|
4310
4495
|
fields.push(
|
|
4311
|
-
|
|
4496
|
+
aggFn + "(" + col(alias, fieldName, model) + ") " + SQL_TEMPLATES.AS + " " + quote2(outAlias)
|
|
4312
4497
|
);
|
|
4313
4498
|
}
|
|
4314
4499
|
function addAggregateFields(fields, args, alias, model) {
|
|
4315
4500
|
for (const [agg, aggFn] of AGGREGATES) {
|
|
4316
4501
|
const obj = getAggregateSelectionObject(args, agg);
|
|
4317
4502
|
if (!obj) continue;
|
|
4318
|
-
for (const
|
|
4503
|
+
for (const fieldName in obj) {
|
|
4504
|
+
if (!Object.prototype.hasOwnProperty.call(obj, fieldName)) continue;
|
|
4505
|
+
const selection = obj[fieldName];
|
|
4319
4506
|
if (fieldName === "_all")
|
|
4320
4507
|
throw new Error(`'${agg}' does not support '_all'`);
|
|
4321
4508
|
if (!isTruthySelection(selection)) continue;
|
|
@@ -4339,22 +4526,23 @@ function buildAggregateSql(args, whereResult, tableName, alias, model) {
|
|
|
4339
4526
|
throw new Error("buildAggregateSql requires at least one aggregate field");
|
|
4340
4527
|
}
|
|
4341
4528
|
const selectClause = aggFields.join(SQL_SEPARATORS.FIELD_LIST);
|
|
4342
|
-
const whereClause = isValidWhereClause(whereResult.clause) ?
|
|
4343
|
-
const
|
|
4529
|
+
const whereClause = isValidWhereClause(whereResult.clause) ? SQL_TEMPLATES.WHERE + " " + whereResult.clause : "";
|
|
4530
|
+
const parts = [
|
|
4344
4531
|
SQL_TEMPLATES.SELECT,
|
|
4345
4532
|
selectClause,
|
|
4346
4533
|
SQL_TEMPLATES.FROM,
|
|
4347
4534
|
tableName,
|
|
4348
|
-
alias
|
|
4349
|
-
|
|
4350
|
-
|
|
4535
|
+
alias
|
|
4536
|
+
];
|
|
4537
|
+
if (whereClause) parts.push(whereClause);
|
|
4538
|
+
const sql = parts.join(" ").trim();
|
|
4351
4539
|
validateSelectQuery(sql);
|
|
4352
4540
|
validateParamConsistency(sql, whereResult.params);
|
|
4353
|
-
return
|
|
4541
|
+
return {
|
|
4354
4542
|
sql,
|
|
4355
|
-
params:
|
|
4356
|
-
paramMappings:
|
|
4357
|
-
}
|
|
4543
|
+
params: whereResult.params,
|
|
4544
|
+
paramMappings: whereResult.paramMappings
|
|
4545
|
+
};
|
|
4358
4546
|
}
|
|
4359
4547
|
function assertGroupByBy(args, model) {
|
|
4360
4548
|
if (!isNotNullish(args.by) || !isNonEmptyArray(args.by)) {
|
|
@@ -4382,8 +4570,8 @@ function buildGroupByHaving(args, alias, params, model, dialect) {
|
|
|
4382
4570
|
if (!isNotNullish(args.having)) return "";
|
|
4383
4571
|
if (!isPlainObject(args.having)) throw new Error("having must be an object");
|
|
4384
4572
|
const h = buildHavingClause(args.having, alias, params, model, dialect);
|
|
4385
|
-
if (!h || h.
|
|
4386
|
-
return
|
|
4573
|
+
if (!h || h.length === 0) return "";
|
|
4574
|
+
return SQL_TEMPLATES.HAVING + " " + h;
|
|
4387
4575
|
}
|
|
4388
4576
|
function buildGroupBySql(args, whereResult, tableName, alias, model, dialect) {
|
|
4389
4577
|
assertSafeAlias(alias);
|
|
@@ -4398,29 +4586,28 @@ function buildGroupBySql(args, whereResult, tableName, alias, model, dialect) {
|
|
|
4398
4586
|
byFields
|
|
4399
4587
|
);
|
|
4400
4588
|
const havingClause = buildGroupByHaving(args, alias, params, model, d);
|
|
4401
|
-
const whereClause = isValidWhereClause(whereResult.clause) ?
|
|
4402
|
-
const
|
|
4589
|
+
const whereClause = isValidWhereClause(whereResult.clause) ? SQL_TEMPLATES.WHERE + " " + whereResult.clause : "";
|
|
4590
|
+
const parts = [
|
|
4403
4591
|
SQL_TEMPLATES.SELECT,
|
|
4404
4592
|
selectFields,
|
|
4405
4593
|
SQL_TEMPLATES.FROM,
|
|
4406
4594
|
tableName,
|
|
4407
|
-
alias
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4595
|
+
alias
|
|
4596
|
+
];
|
|
4597
|
+
if (whereClause) parts.push(whereClause);
|
|
4598
|
+
parts.push(SQL_TEMPLATES.GROUP_BY, groupFields);
|
|
4599
|
+
if (havingClause) parts.push(havingClause);
|
|
4600
|
+
const sql = parts.join(" ").trim();
|
|
4413
4601
|
const snapshot = params.snapshot();
|
|
4602
|
+
const allParams = [...whereResult.params, ...snapshot.params];
|
|
4603
|
+
const allMappings = [...whereResult.paramMappings, ...snapshot.mappings];
|
|
4414
4604
|
validateSelectQuery(sql);
|
|
4415
|
-
validateParamConsistency(sql,
|
|
4416
|
-
return
|
|
4605
|
+
validateParamConsistency(sql, allParams);
|
|
4606
|
+
return {
|
|
4417
4607
|
sql,
|
|
4418
|
-
params:
|
|
4419
|
-
paramMappings:
|
|
4420
|
-
|
|
4421
|
-
...snapshot.mappings
|
|
4422
|
-
])
|
|
4423
|
-
});
|
|
4608
|
+
params: allParams,
|
|
4609
|
+
paramMappings: allMappings
|
|
4610
|
+
};
|
|
4424
4611
|
}
|
|
4425
4612
|
function buildCountSql(whereResult, tableName, alias, skip, _dialect) {
|
|
4426
4613
|
assertSafeAlias(alias);
|
|
@@ -4448,24 +4635,25 @@ function buildCountSql(whereResult, tableName, alias, skip, _dialect) {
|
|
|
4448
4635
|
);
|
|
4449
4636
|
}
|
|
4450
4637
|
}
|
|
4451
|
-
const whereClause = isValidWhereClause(whereResult.clause) ?
|
|
4452
|
-
const
|
|
4638
|
+
const whereClause = isValidWhereClause(whereResult.clause) ? SQL_TEMPLATES.WHERE + " " + whereResult.clause : "";
|
|
4639
|
+
const parts = [
|
|
4453
4640
|
SQL_TEMPLATES.SELECT,
|
|
4454
4641
|
SQL_TEMPLATES.COUNT_ALL,
|
|
4455
4642
|
SQL_TEMPLATES.AS,
|
|
4456
|
-
|
|
4643
|
+
quote2("_count._all"),
|
|
4457
4644
|
SQL_TEMPLATES.FROM,
|
|
4458
4645
|
tableName,
|
|
4459
|
-
alias
|
|
4460
|
-
|
|
4461
|
-
|
|
4646
|
+
alias
|
|
4647
|
+
];
|
|
4648
|
+
if (whereClause) parts.push(whereClause);
|
|
4649
|
+
const sql = parts.join(" ").trim();
|
|
4462
4650
|
validateSelectQuery(sql);
|
|
4463
4651
|
validateParamConsistency(sql, whereResult.params);
|
|
4464
|
-
return
|
|
4652
|
+
return {
|
|
4465
4653
|
sql,
|
|
4466
|
-
params:
|
|
4467
|
-
paramMappings:
|
|
4468
|
-
}
|
|
4654
|
+
params: whereResult.params,
|
|
4655
|
+
paramMappings: whereResult.paramMappings
|
|
4656
|
+
};
|
|
4469
4657
|
}
|
|
4470
4658
|
function safeAlias(input) {
|
|
4471
4659
|
const raw = String(input).toLowerCase();
|