@minnowdb/core 0.7.2 → 0.7.7
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/engine/client.d.ts +2 -2
- package/dist/engine/client.js +7 -2
- package/dist/engine/database.d.ts +11 -1
- package/dist/engine/database.js +642 -175
- package/dist/engine/optimizer.js +48 -26
- package/dist/engine/query.d.ts +98 -0
- package/dist/engine/query.js +1452 -165
- package/dist/engine/sql-domains.d.ts +11 -0
- package/dist/engine/sql-domains.js +56 -0
- package/dist/engine/sql-functions.js +129 -3
- package/dist/engine/sql-json.d.ts +2 -0
- package/dist/engine/sql-json.js +4 -0
- package/dist/engine/sql-semantics.d.ts +9 -1
- package/dist/engine/sql-semantics.js +19 -3
- package/dist/engine/vector.js +29 -13
- package/dist/engine/worker-server.js +4 -1
- package/dist/plan/model.d.ts +14 -2
- package/dist/storage/indexeddb.js +13 -8
- package/dist/storage/memory.d.ts +2 -1
- package/dist/storage/memory.js +29 -4
- package/dist/storage/toolkit/record-core.js +11 -6
- package/dist/storage/types.d.ts +11 -0
- package/dist/testing/sqllogictest.js +3 -1
- package/package.json +1 -1
- package/postgres-feature-profile.json +9 -4
- package/sql-feature-matrix.json +541 -4
package/dist/engine/query.js
CHANGED
|
@@ -6,12 +6,12 @@ import { SqlCompileError } from "./errors.js";
|
|
|
6
6
|
import { cachedQueryTerms, ftsBm25Row, FtsStatsAccumulator, ftsMatchTruth, renderDocumentValue, tokenize as ftsTokenize, validateFtsQuery } from "./fts.js";
|
|
7
7
|
import { QueryMemoryContext } from "./memory.js";
|
|
8
8
|
import { buildSortKeyColumn, sortKeyIndexes } from "./sort-keys.js";
|
|
9
|
-
import { coerceComparisonOperands, coercedComparable, parseSqlTimestampText, stringArgument } from "./sql-semantics.js";
|
|
9
|
+
import { coerceComparisonOperands, coercedComparable, parseSqlTimestampText, stringArgument, readUntypedText } from "./sql-semantics.js";
|
|
10
10
|
import { simpleScalarFunctions } from "./sql-functions.js";
|
|
11
|
-
import { jsonArrowStep, jsonAtPath, jsonConstructor, jsonIsValid, parseJsonPath } from "./sql-json.js";
|
|
11
|
+
import { jsonArrowStep, jsonAtPath, jsonConstructor, jsonIsValid, parseJsonPath, jsonDocumentOf } from "./sql-json.js";
|
|
12
12
|
import { optimizePlan, rewriteBoundCalendarEqualities } from "./optimizer.js";
|
|
13
13
|
import { compareSqlValues as compareValues, compileLikePattern, compileSimilarPattern, encodeSqlEqualityValue, roundSqlNumber } from "./sql-semantics.js";
|
|
14
|
-
import { arrayDomainValue, boundedJsonText, collatedDomainValue, concatenatedSqlValue, dateDomainValue, exactNumericAsNumber, exactNumericBinary, exactNumericLiteral, exactNumericValue, externalSqlDomainColumnValue, externalSqlDomainValue, intervalDomainValue, isDateDomainValue, isExactNumeric, isSqlDomainValue, jsonDomainValue, normalizeSqlDomainValue, preservedJsonDomainValue, protectedSqlTextValue, timeDomainValue, uuidDomainValue } from "./sql-domains.js";
|
|
14
|
+
import { arrayDomainValue, boundedJsonText, collatedDomainValue, concatenatedSqlValue, dateDomainValue, exactNumericAsNumber, exactNumericBinary, exactNumericLiteral, decimalScaleOfNumber, exactNumericRounded, exactNumericUnary, exactNumericValue, externalSqlDomainColumnValue, externalSqlDomainValue, intervalDomainValue, isDateDomainValue, isExactNumeric, isSqlDomainValue, jsonDomainValue, normalizeSqlDomainValue, preservedJsonDomainValue, protectedSqlTextValue, timeDomainValue, uuidDomainValue } from "./sql-domains.js";
|
|
15
15
|
import { columnarTableFromRows, prepareVectorQuery } from "./vector.js";
|
|
16
16
|
function unknownColumnDomains(columns) {
|
|
17
17
|
return columns.map(() => null);
|
|
@@ -61,6 +61,7 @@ const scalarFunctionNames = /* @__PURE__ */ new Set([
|
|
|
61
61
|
"JSON_EXISTS",
|
|
62
62
|
"JSON_OBJECT",
|
|
63
63
|
"JSON_ARRAY",
|
|
64
|
+
"TO_JSON",
|
|
64
65
|
"IS_JSON",
|
|
65
66
|
"ARRAY",
|
|
66
67
|
"MINNOW_JSON_GET",
|
|
@@ -87,13 +88,23 @@ const functionSpellings = /* @__PURE__ */ new Map([
|
|
|
87
88
|
["DATE_PART", "EXTRACT"],
|
|
88
89
|
["CEILING", "CEIL"],
|
|
89
90
|
["CHAR_LENGTH", "LENGTH"],
|
|
90
|
-
["CHARACTER_LENGTH", "LENGTH"]
|
|
91
|
+
["CHARACTER_LENGTH", "LENGTH"],
|
|
92
|
+
["POW", "POWER"],
|
|
93
|
+
["JSON_AGG", "JSON_ARRAYAGG"],
|
|
94
|
+
["JSONB_AGG", "JSON_ARRAYAGG"],
|
|
95
|
+
["JSON_BUILD_ARRAY", "JSON_ARRAY"],
|
|
96
|
+
["JSONB_BUILD_ARRAY", "JSON_ARRAY"],
|
|
97
|
+
["TO_JSONB", "TO_JSON"],
|
|
98
|
+
["ROW_TO_JSON", "TO_JSON"]
|
|
91
99
|
]);
|
|
92
100
|
const statementDatetimeAliases = /* @__PURE__ */ new Map([
|
|
93
101
|
["CURRENT_DATE", "CURRENT_DATE"],
|
|
94
102
|
["CURRENT_TIMESTAMP", "CURRENT_TIMESTAMP"],
|
|
95
103
|
["LOCALTIMESTAMP", "CURRENT_TIMESTAMP"],
|
|
96
104
|
["NOW", "CURRENT_TIMESTAMP"],
|
|
105
|
+
["TRANSACTION_TIMESTAMP", "CURRENT_TIMESTAMP"],
|
|
106
|
+
["STATEMENT_TIMESTAMP", "CURRENT_TIMESTAMP"],
|
|
107
|
+
["CLOCK_TIMESTAMP", "CURRENT_TIMESTAMP"],
|
|
97
108
|
["CURRENT_TIME", "LOCALTIME"],
|
|
98
109
|
["LOCALTIME", "LOCALTIME"]
|
|
99
110
|
]);
|
|
@@ -256,17 +267,23 @@ function buildScalarFunctionEvaluator(name) {
|
|
|
256
267
|
if (values.length > 1 && (values[1] === null || values[1] === void 0))
|
|
257
268
|
return null;
|
|
258
269
|
const digits = values.length > 1 ? numeric(values[1]) : 0;
|
|
259
|
-
if (isExactNumeric(first) && Number.isInteger(digits)
|
|
260
|
-
return
|
|
270
|
+
if (isExactNumeric(first) && Number.isInteger(digits)) {
|
|
271
|
+
return exactNumericRounded(first, digits, "round");
|
|
261
272
|
}
|
|
262
273
|
return roundSqlNumber(numeric(first), digits);
|
|
263
274
|
};
|
|
264
275
|
case "FLOOR":
|
|
265
|
-
return (values) => values[0] === null || values[0] === void 0 ? null : Math.floor(numeric(values[0]));
|
|
266
276
|
case "CEIL":
|
|
267
|
-
return (values) => values[0] === null || values[0] === void 0 ? null : Math.ceil(numeric(values[0]));
|
|
268
277
|
case "ABS":
|
|
269
|
-
return (values) =>
|
|
278
|
+
return (values) => {
|
|
279
|
+
const first = values[0];
|
|
280
|
+
if (first === null || first === void 0)
|
|
281
|
+
return null;
|
|
282
|
+
if (isExactNumeric(first))
|
|
283
|
+
return exactNumericUnary(name, first);
|
|
284
|
+
const operand = numeric(first);
|
|
285
|
+
return name === "FLOOR" ? Math.floor(operand) : name === "CEIL" ? Math.ceil(operand) : Math.abs(operand);
|
|
286
|
+
};
|
|
270
287
|
case "UPPER":
|
|
271
288
|
return (values) => {
|
|
272
289
|
const first = values[0];
|
|
@@ -355,12 +372,15 @@ function scalarFunctionValueGeneric(name, values) {
|
|
|
355
372
|
return comparable(first) === comparable(other) ? null : first;
|
|
356
373
|
}
|
|
357
374
|
case "FLOOR":
|
|
358
|
-
return Math.floor(numeric(first));
|
|
375
|
+
return isExactNumeric(first) ? exactNumericUnary("FLOOR", first) : Math.floor(numeric(first));
|
|
359
376
|
case "CEIL":
|
|
360
|
-
return Math.ceil(numeric(first));
|
|
377
|
+
return isExactNumeric(first) ? exactNumericUnary("CEIL", first) : Math.ceil(numeric(first));
|
|
361
378
|
case "MOD": {
|
|
362
379
|
if (values[1] === null || values[1] === void 0)
|
|
363
380
|
return null;
|
|
381
|
+
const exact = exactNumericBinary("%", first, values[1]);
|
|
382
|
+
if (exact !== void 0)
|
|
383
|
+
return exact;
|
|
364
384
|
const divisor = numeric(values[1]);
|
|
365
385
|
return divisor === 0 ? null : numeric(first) % divisor;
|
|
366
386
|
}
|
|
@@ -412,13 +432,13 @@ function scalarFunctionValueGeneric(name, values) {
|
|
|
412
432
|
if (values.length > 1 && (values[1] === null || values[1] === void 0))
|
|
413
433
|
return null;
|
|
414
434
|
const digits = values.length > 1 ? numeric(values[1]) : 0;
|
|
415
|
-
if (isExactNumeric(first) && Number.isInteger(digits)
|
|
416
|
-
return
|
|
435
|
+
if (isExactNumeric(first) && Number.isInteger(digits)) {
|
|
436
|
+
return exactNumericRounded(first, digits, "round");
|
|
417
437
|
}
|
|
418
438
|
return roundSqlNumber(numeric(first), digits);
|
|
419
439
|
}
|
|
420
440
|
case "ABS":
|
|
421
|
-
return Math.abs(numeric(first));
|
|
441
|
+
return isExactNumeric(first) ? exactNumericUnary("ABS", first) : Math.abs(numeric(first));
|
|
422
442
|
case "UPPER": {
|
|
423
443
|
const source = stringArgument("UPPER", first);
|
|
424
444
|
assertScalarInputLength(source, "UPPER input");
|
|
@@ -458,6 +478,8 @@ function scalarFunctionValueGeneric(name, values) {
|
|
|
458
478
|
return null;
|
|
459
479
|
return preservedJsonDomainValue(JSON.stringify(found.value));
|
|
460
480
|
}
|
|
481
|
+
case "TO_JSON":
|
|
482
|
+
return preservedJsonDomainValue(jsonDocumentOf(first));
|
|
461
483
|
case "MINNOW_JSON_GET": {
|
|
462
484
|
if (values[1] === null || values[1] === void 0)
|
|
463
485
|
return null;
|
|
@@ -767,20 +789,36 @@ const createTableTypeNames = /* @__PURE__ */ new Map([
|
|
|
767
789
|
["BOOL", "boolean"],
|
|
768
790
|
["INTEGER", "number"],
|
|
769
791
|
["INT", "number"],
|
|
792
|
+
["INT4", "number"],
|
|
770
793
|
["BIGINT", "number"],
|
|
794
|
+
["INT8", "number"],
|
|
771
795
|
["SMALLINT", "number"],
|
|
796
|
+
["INT2", "number"],
|
|
772
797
|
["REAL", "number"],
|
|
798
|
+
["FLOAT4", "number"],
|
|
773
799
|
["FLOAT", "number"],
|
|
800
|
+
["FLOAT8", "number"],
|
|
774
801
|
["TEXT", "string"],
|
|
775
802
|
["VARCHAR", "string"],
|
|
776
803
|
["CHAR", "string"],
|
|
804
|
+
["CHARACTER", "string"],
|
|
777
805
|
["STRING", "string"],
|
|
778
806
|
["TIMESTAMP", "datetime"],
|
|
779
807
|
["TIMESTAMPTZ", "datetime"],
|
|
780
808
|
["DATETIME", "datetime"]
|
|
781
809
|
]);
|
|
810
|
+
const integerTypeNames = /* @__PURE__ */ new Set([
|
|
811
|
+
"INTEGER",
|
|
812
|
+
"INT",
|
|
813
|
+
"INT4",
|
|
814
|
+
"BIGINT",
|
|
815
|
+
"INT8",
|
|
816
|
+
"SMALLINT",
|
|
817
|
+
"INT2"
|
|
818
|
+
]);
|
|
782
819
|
const clauseKeywords = /* @__PURE__ */ new Set([
|
|
783
820
|
"WHERE",
|
|
821
|
+
"FOR",
|
|
784
822
|
"NATURAL",
|
|
785
823
|
"USING",
|
|
786
824
|
"OUTER",
|
|
@@ -802,6 +840,72 @@ const clauseKeywords = /* @__PURE__ */ new Set([
|
|
|
802
840
|
"UNION",
|
|
803
841
|
"RETURNING"
|
|
804
842
|
]);
|
|
843
|
+
const bareLabelStopWords = /* @__PURE__ */ new Set([
|
|
844
|
+
...clauseKeywords,
|
|
845
|
+
"ALL",
|
|
846
|
+
"AND",
|
|
847
|
+
"ANY",
|
|
848
|
+
"ARRAY",
|
|
849
|
+
"AS",
|
|
850
|
+
"ASC",
|
|
851
|
+
"AT",
|
|
852
|
+
"BETWEEN",
|
|
853
|
+
"BOTH",
|
|
854
|
+
"CASE",
|
|
855
|
+
"CAST",
|
|
856
|
+
"CHECK",
|
|
857
|
+
"COLLATE",
|
|
858
|
+
"COLUMN",
|
|
859
|
+
"CONSTRAINT",
|
|
860
|
+
"CREATE",
|
|
861
|
+
"DEFAULT",
|
|
862
|
+
"DESC",
|
|
863
|
+
"DISTINCT",
|
|
864
|
+
"DO",
|
|
865
|
+
"ELSE",
|
|
866
|
+
"END",
|
|
867
|
+
"ESCAPE",
|
|
868
|
+
"FALSE",
|
|
869
|
+
"FILTER",
|
|
870
|
+
"FOR",
|
|
871
|
+
"FOREIGN",
|
|
872
|
+
"FROM",
|
|
873
|
+
"GRANT",
|
|
874
|
+
"ILIKE",
|
|
875
|
+
"IN",
|
|
876
|
+
"INTO",
|
|
877
|
+
"IS",
|
|
878
|
+
"ISNULL",
|
|
879
|
+
"LATERAL",
|
|
880
|
+
"LEADING",
|
|
881
|
+
"LIKE",
|
|
882
|
+
"NOT",
|
|
883
|
+
"NOTNULL",
|
|
884
|
+
"NULL",
|
|
885
|
+
"ON",
|
|
886
|
+
"ONLY",
|
|
887
|
+
"OR",
|
|
888
|
+
"OVER",
|
|
889
|
+
"OVERLAPS",
|
|
890
|
+
"PLACING",
|
|
891
|
+
"PRIMARY",
|
|
892
|
+
"REFERENCES",
|
|
893
|
+
"SELECT",
|
|
894
|
+
"SIMILAR",
|
|
895
|
+
"SOME",
|
|
896
|
+
"TABLE",
|
|
897
|
+
"THEN",
|
|
898
|
+
"TO",
|
|
899
|
+
"TRAILING",
|
|
900
|
+
"TRUE",
|
|
901
|
+
"UNIQUE",
|
|
902
|
+
"USER",
|
|
903
|
+
"VALUES",
|
|
904
|
+
"WHEN",
|
|
905
|
+
"WITH",
|
|
906
|
+
"WITHIN",
|
|
907
|
+
"WITHOUT"
|
|
908
|
+
]);
|
|
805
909
|
const aggregateNames = /* @__PURE__ */ new Set([
|
|
806
910
|
"COUNT",
|
|
807
911
|
"SUM",
|
|
@@ -830,20 +934,28 @@ function rejectSemicolons(tokens) {
|
|
|
830
934
|
function normalizeSql(sql) {
|
|
831
935
|
return { text: sql.trim().replace(/;$/, "").trim(), offset: sql.length - sql.trimStart().length };
|
|
832
936
|
}
|
|
937
|
+
function normalizeQuerySql(sql) {
|
|
938
|
+
const spelling = "SELECT * FROM ";
|
|
939
|
+
const table = /^\s*TABLE\s+(?=[A-Za-z_"])/i.exec(sql);
|
|
940
|
+
if (table === null)
|
|
941
|
+
return normalizeSql(sql);
|
|
942
|
+
const { text } = normalizeSql(spelling + sql.slice(table[0].length));
|
|
943
|
+
return { text, offset: table[0].length - spelling.length };
|
|
944
|
+
}
|
|
833
945
|
function throwLocated(error, offset, span) {
|
|
834
946
|
if (error instanceof SqlCompileError) {
|
|
835
947
|
if (offset === 0)
|
|
836
948
|
throw error;
|
|
837
|
-
throw new SqlCompileError(error.message, error.offset + offset, error.length);
|
|
949
|
+
throw new SqlCompileError(error.message, Math.max(error.offset + offset, 0), error.length);
|
|
838
950
|
}
|
|
839
951
|
if (error instanceof TypeError) {
|
|
840
|
-
throw new SqlCompileError(error.message, offset + span.start, Math.max(span.end - span.start, 0));
|
|
952
|
+
throw new SqlCompileError(error.message, Math.max(offset + span.start, 0), Math.max(span.end - span.start, 0));
|
|
841
953
|
}
|
|
842
954
|
throw error;
|
|
843
955
|
}
|
|
844
956
|
function compileQuery(sql, options = {}) {
|
|
845
957
|
validateSqlSource(sql);
|
|
846
|
-
const { text, offset } =
|
|
958
|
+
const { text, offset } = normalizeQuerySql(sql);
|
|
847
959
|
if (text.length === 0)
|
|
848
960
|
throw new SqlCompileError("Enter a SELECT query", offset, 0);
|
|
849
961
|
let parser;
|
|
@@ -858,6 +970,8 @@ function compileQuery(sql, options = {}) {
|
|
|
858
970
|
}
|
|
859
971
|
let compiled;
|
|
860
972
|
try {
|
|
973
|
+
plan = expandRowReferences(plan, () => void 0);
|
|
974
|
+
annotatePlanIntegerDivision(plan);
|
|
861
975
|
resolvePlanExactNumericConstants(plan);
|
|
862
976
|
if (options.optimize === false && planHasPendingSelectShapes(plan)) {
|
|
863
977
|
plan.preserveUnoptimizedShape = true;
|
|
@@ -896,6 +1010,94 @@ function isDefaultInsertValue(value) {
|
|
|
896
1010
|
function isDeferredInsertExpression(value) {
|
|
897
1011
|
return typeof value === "object" && value !== null && !(value instanceof Date) && "expression" in value;
|
|
898
1012
|
}
|
|
1013
|
+
function parseSettingStatement(keyword, tokens) {
|
|
1014
|
+
const words = tokens.filter((token) => token.kind !== "eof");
|
|
1015
|
+
const upperAt = (at) => {
|
|
1016
|
+
const word = words[at];
|
|
1017
|
+
return word?.kind === "identifier" && word.quoted !== true ? word.text.toUpperCase() : void 0;
|
|
1018
|
+
};
|
|
1019
|
+
if (keyword === "RESET") {
|
|
1020
|
+
const name2 = words[1];
|
|
1021
|
+
if (name2 === void 0 || name2.kind !== "identifier" && name2.kind !== "string") {
|
|
1022
|
+
throw new TypeError("RESET takes a setting name or ALL");
|
|
1023
|
+
}
|
|
1024
|
+
return { kind: "set", action: "reset", name: name2.text };
|
|
1025
|
+
}
|
|
1026
|
+
let index = 1;
|
|
1027
|
+
if (upperAt(index) === "SESSION" || upperAt(index) === "LOCAL")
|
|
1028
|
+
index += 1;
|
|
1029
|
+
if (upperAt(index) === "TIME" && upperAt(index + 1) === "ZONE") {
|
|
1030
|
+
const zone = words[index + 2];
|
|
1031
|
+
const spelled = zone?.kind === "string" ? zone.text : zone?.text.toUpperCase();
|
|
1032
|
+
const utc = spelled === void 0 || spelled.toUpperCase() === "UTC" || spelled.toUpperCase() === "DEFAULT" || spelled.toUpperCase() === "LOCAL" || spelled === "Etc/UTC" || spelled === "GMT";
|
|
1033
|
+
if (!utc) {
|
|
1034
|
+
throw new TypeError(`SET TIME ZONE ${zone?.text ?? ""} is not supported: every datetime is an instant in UTC`);
|
|
1035
|
+
}
|
|
1036
|
+
return { kind: "set", action: "set", name: "timezone" };
|
|
1037
|
+
}
|
|
1038
|
+
if (upperAt(index) === "TRANSACTION" || upperAt(index) === "CONSTRAINTS") {
|
|
1039
|
+
if (words.some((word) => word.text.toUpperCase() === "SERIALIZABLE")) {
|
|
1040
|
+
throw new TypeError("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE is not supported: the engine has one isolation level");
|
|
1041
|
+
}
|
|
1042
|
+
return { kind: "set", action: "set", name: (upperAt(index) ?? "").toLowerCase() };
|
|
1043
|
+
}
|
|
1044
|
+
const name = words[index];
|
|
1045
|
+
if (name?.kind !== "identifier") {
|
|
1046
|
+
throw new TypeError(`Expected a setting name, found ${name?.text ?? "end of input"}`);
|
|
1047
|
+
}
|
|
1048
|
+
let cursor = index + 1;
|
|
1049
|
+
while (words[cursor]?.text === ".")
|
|
1050
|
+
cursor += 2;
|
|
1051
|
+
const settingName = words.slice(index, cursor).map((token) => token.text).join("");
|
|
1052
|
+
const separator = words[cursor];
|
|
1053
|
+
if (separator === void 0 || separator.text.toUpperCase() !== "TO" && separator.text !== "=") {
|
|
1054
|
+
throw new TypeError(`SET ${settingName} takes TO or = and a value`);
|
|
1055
|
+
}
|
|
1056
|
+
if (words[cursor + 1] === void 0)
|
|
1057
|
+
throw new TypeError(`SET ${settingName} needs a value`);
|
|
1058
|
+
if (settingName.toLowerCase() === "timezone" || settingName.toLowerCase() === "time_zone") {
|
|
1059
|
+
const zone = words[cursor + 1];
|
|
1060
|
+
const spelled = zone?.kind === "string" ? zone.text : zone?.text ?? "";
|
|
1061
|
+
if (!["utc", "default", "local", "etc/utc", "gmt"].includes(spelled.toLowerCase())) {
|
|
1062
|
+
throw new TypeError(`SET ${settingName} = ${zone?.text ?? ""} is not supported: every datetime is an instant in UTC`);
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
return { kind: "set", action: "set", name: settingName };
|
|
1066
|
+
}
|
|
1067
|
+
function parseTruncateStatement(tokens) {
|
|
1068
|
+
const words = tokens.filter((token) => token.kind !== "eof");
|
|
1069
|
+
let index = 1;
|
|
1070
|
+
const upperAt = (at) => {
|
|
1071
|
+
const word = words[at];
|
|
1072
|
+
return word?.kind === "identifier" ? word.text.toUpperCase() : void 0;
|
|
1073
|
+
};
|
|
1074
|
+
if (upperAt(index) === "TABLE")
|
|
1075
|
+
index += 1;
|
|
1076
|
+
if (upperAt(index) === "ONLY")
|
|
1077
|
+
index += 1;
|
|
1078
|
+
const table = words[index];
|
|
1079
|
+
if (table?.kind !== "identifier") {
|
|
1080
|
+
throw new TypeError(`Expected table name, found ${table?.text ?? "end of input"}`);
|
|
1081
|
+
}
|
|
1082
|
+
index += 1;
|
|
1083
|
+
if (words[index]?.text === ",")
|
|
1084
|
+
throw new TypeError("TRUNCATE takes one table at a time");
|
|
1085
|
+
if (upperAt(index) === "RESTART" || upperAt(index) === "CONTINUE") {
|
|
1086
|
+
if (upperAt(index + 1) !== "IDENTITY") {
|
|
1087
|
+
throw new TypeError(`Expected IDENTITY, found ${words[index + 1]?.text ?? "end of input"}`);
|
|
1088
|
+
}
|
|
1089
|
+
index += 2;
|
|
1090
|
+
}
|
|
1091
|
+
if (upperAt(index) === "CASCADE") {
|
|
1092
|
+
throw new TypeError("TRUNCATE CASCADE is not supported; truncate dependents explicitly");
|
|
1093
|
+
}
|
|
1094
|
+
if (upperAt(index) === "RESTRICT")
|
|
1095
|
+
index += 1;
|
|
1096
|
+
if (index !== words.length) {
|
|
1097
|
+
throw new TypeError(`Unexpected input after TRUNCATE: ${words[index]?.text ?? ""}`);
|
|
1098
|
+
}
|
|
1099
|
+
return { kind: "delete", table: table.text, predicates: [] };
|
|
1100
|
+
}
|
|
899
1101
|
function parseTransactionStatement(keyword, tokens) {
|
|
900
1102
|
const identifierAt = (index) => {
|
|
901
1103
|
const token = tokens[index];
|
|
@@ -927,8 +1129,30 @@ function parseTransactionStatement(keyword, tokens) {
|
|
|
927
1129
|
return { kind: "transaction", action: "rollback-to", name };
|
|
928
1130
|
}
|
|
929
1131
|
const words = tokens.filter((token) => token.kind === "identifier").map((token) => token.text.toUpperCase());
|
|
930
|
-
const action = keyword === "START" || keyword === "BEGIN" ? "begin" : keyword.toLowerCase();
|
|
931
|
-
|
|
1132
|
+
const action = keyword === "START" || keyword === "BEGIN" ? "begin" : keyword === "END" ? "commit" : keyword === "ABORT" ? "rollback" : keyword.toLowerCase();
|
|
1133
|
+
if (words.includes("SERIALIZABLE")) {
|
|
1134
|
+
throw new TypeError("BEGIN ISOLATION LEVEL SERIALIZABLE is not supported: the engine has one isolation level");
|
|
1135
|
+
}
|
|
1136
|
+
const allowed = /* @__PURE__ */ new Set([
|
|
1137
|
+
"BEGIN",
|
|
1138
|
+
"START",
|
|
1139
|
+
"COMMIT",
|
|
1140
|
+
"END",
|
|
1141
|
+
"ROLLBACK",
|
|
1142
|
+
"ABORT",
|
|
1143
|
+
"WORK",
|
|
1144
|
+
"TRANSACTION",
|
|
1145
|
+
"READ",
|
|
1146
|
+
"ONLY",
|
|
1147
|
+
"WRITE",
|
|
1148
|
+
"NOT",
|
|
1149
|
+
"DEFERRABLE",
|
|
1150
|
+
"ISOLATION",
|
|
1151
|
+
"LEVEL",
|
|
1152
|
+
"COMMITTED",
|
|
1153
|
+
"UNCOMMITTED",
|
|
1154
|
+
"REPEATABLE"
|
|
1155
|
+
]);
|
|
932
1156
|
for (const word of words) {
|
|
933
1157
|
if (!allowed.has(word)) {
|
|
934
1158
|
throw new TypeError(`${keyword} takes no ${word}: this engine has one isolation level`);
|
|
@@ -1115,7 +1339,7 @@ function parseCreateTrigger(text, tokens) {
|
|
|
1115
1339
|
}
|
|
1116
1340
|
function compileStatement(sql) {
|
|
1117
1341
|
validateSqlSource(sql);
|
|
1118
|
-
const { text, offset } =
|
|
1342
|
+
const { text, offset } = normalizeQuerySql(sql);
|
|
1119
1343
|
if (text.length === 0)
|
|
1120
1344
|
throw new SqlCompileError("Enter a SQL statement", offset, 0);
|
|
1121
1345
|
let parser;
|
|
@@ -1123,13 +1347,25 @@ function compileStatement(sql) {
|
|
|
1123
1347
|
const tokens = tokenize(text);
|
|
1124
1348
|
const first = tokens[0];
|
|
1125
1349
|
const keyword = first?.kind === "identifier" ? first.text.toUpperCase() : "";
|
|
1126
|
-
|
|
1350
|
+
let second = tokens[1];
|
|
1127
1351
|
const isTriggerDdl = (keyword === "CREATE" || keyword === "DROP") && second?.kind === "identifier" && second.text.toUpperCase() === "TRIGGER";
|
|
1128
1352
|
if (!isTriggerDdl)
|
|
1129
1353
|
rejectSemicolons(tokens);
|
|
1130
|
-
if (keyword === "BEGIN" || keyword === "START" || keyword === "COMMIT" || keyword === "ROLLBACK" || keyword === "SAVEPOINT" || keyword === "RELEASE") {
|
|
1354
|
+
if (keyword === "BEGIN" || keyword === "START" || keyword === "COMMIT" || keyword === "END" || keyword === "ROLLBACK" || keyword === "ABORT" || keyword === "SAVEPOINT" || keyword === "RELEASE") {
|
|
1131
1355
|
return parseTransactionStatement(keyword, tokens);
|
|
1132
1356
|
}
|
|
1357
|
+
if (keyword === "TRUNCATE")
|
|
1358
|
+
return parseTruncateStatement(tokens);
|
|
1359
|
+
if (keyword === "SET" || keyword === "RESET")
|
|
1360
|
+
return parseSettingStatement(keyword, tokens);
|
|
1361
|
+
if (keyword === "SHOW") {
|
|
1362
|
+
const name = tokens[1];
|
|
1363
|
+
if (name?.kind !== "identifier" || tokens[2]?.kind !== "eof" && tokens[2]?.text !== ".") {
|
|
1364
|
+
throw new TypeError("SHOW takes one setting name");
|
|
1365
|
+
}
|
|
1366
|
+
const setting = tokens.slice(1).filter((token) => token.kind !== "eof").map((token) => token.text).join("");
|
|
1367
|
+
return { kind: "show", name: setting };
|
|
1368
|
+
}
|
|
1133
1369
|
if (keyword === "MERGE") {
|
|
1134
1370
|
parser = new Parser(tokens, text);
|
|
1135
1371
|
const statement = parser.parseMerge();
|
|
@@ -1147,6 +1383,10 @@ function compileStatement(sql) {
|
|
|
1147
1383
|
if (keyword === "CREATE") {
|
|
1148
1384
|
if (isTriggerDdl)
|
|
1149
1385
|
return parseCreateTrigger(text, tokens);
|
|
1386
|
+
while (tokens[1]?.kind === "identifier" && ["TEMP", "TEMPORARY", "UNLOGGED", "GLOBAL", "LOCAL"].includes(tokens[1].text.toUpperCase())) {
|
|
1387
|
+
tokens.splice(1, 1);
|
|
1388
|
+
}
|
|
1389
|
+
second = tokens[1];
|
|
1150
1390
|
if (second?.kind === "identifier" && second.text.toUpperCase() === "TYPE") {
|
|
1151
1391
|
const name = tokens[2];
|
|
1152
1392
|
if (name?.kind !== "identifier")
|
|
@@ -1719,15 +1959,42 @@ function inferBlockSchema(plan, schemas) {
|
|
|
1719
1959
|
return { kind: "interval" };
|
|
1720
1960
|
return void 0;
|
|
1721
1961
|
};
|
|
1962
|
+
const operandScale = (expression, domain) => {
|
|
1963
|
+
if (domain !== void 0)
|
|
1964
|
+
return domain.kind === "numeric" ? domain.scale : void 0;
|
|
1965
|
+
if (expression.kind === "literal") {
|
|
1966
|
+
return typeof expression.value === "number" && Number.isFinite(expression.value) ? decimalScaleOfNumber(expression.value) : void 0;
|
|
1967
|
+
}
|
|
1968
|
+
return expression.kind === "column" ? 0 : void 0;
|
|
1969
|
+
};
|
|
1970
|
+
const numericResultDomain = (operator, sides) => {
|
|
1971
|
+
const scales = sides.map(([expression, domain]) => operandScale(expression, domain));
|
|
1972
|
+
if (scales.some((scale) => scale === void 0))
|
|
1973
|
+
return { kind: "numeric" };
|
|
1974
|
+
const known = scales;
|
|
1975
|
+
return {
|
|
1976
|
+
kind: "numeric",
|
|
1977
|
+
scale: operator === "*" ? known.reduce((sum, scale) => sum + scale, 0) : Math.max(...known)
|
|
1978
|
+
};
|
|
1979
|
+
};
|
|
1722
1980
|
const inferDomain = (expression) => {
|
|
1723
1981
|
if (expression.kind === "literal")
|
|
1724
1982
|
return expression.sqlDomain;
|
|
1725
1983
|
if (expression.kind === "column")
|
|
1726
1984
|
return resolveColumnDomain(expression.reference);
|
|
1727
1985
|
if (expression.kind === "binary") {
|
|
1986
|
+
if (expression.operator === "||")
|
|
1987
|
+
return void 0;
|
|
1728
1988
|
const left = inferDomain(expression.left);
|
|
1729
1989
|
const right = inferDomain(expression.right);
|
|
1730
|
-
|
|
1990
|
+
if (left?.kind !== "numeric" && right?.kind !== "numeric")
|
|
1991
|
+
return void 0;
|
|
1992
|
+
if (expression.operator === "/")
|
|
1993
|
+
return { kind: "numeric" };
|
|
1994
|
+
return numericResultDomain(expression.operator, [
|
|
1995
|
+
[expression.left, left],
|
|
1996
|
+
[expression.right, right]
|
|
1997
|
+
]);
|
|
1731
1998
|
}
|
|
1732
1999
|
if (expression.kind === "case") {
|
|
1733
2000
|
const outcomes = [
|
|
@@ -1749,11 +2016,30 @@ function inferBlockSchema(plan, schemas) {
|
|
|
1749
2016
|
const target = expression.arguments[1];
|
|
1750
2017
|
return target?.kind === "literal" && typeof target.value === "string" ? castDomain(target.value) : void 0;
|
|
1751
2018
|
}
|
|
1752
|
-
if (expression.name === "JSON_ARRAYAGG" || expression.name === "JSON_QUERY" || expression.name === "JSON_OBJECT" || expression.name === "JSON_ARRAY" || expression.name === "MINNOW_JSON_GET") {
|
|
2019
|
+
if (expression.name === "JSON_ARRAYAGG" || expression.name === "JSON_QUERY" || expression.name === "JSON_OBJECT" || expression.name === "JSON_ARRAY" || expression.name === "TO_JSON" || expression.name === "MINNOW_JSON_GET") {
|
|
1753
2020
|
return { kind: "json" };
|
|
1754
2021
|
}
|
|
1755
2022
|
if (expression.name === "GEN_RANDOM_UUID")
|
|
1756
2023
|
return { kind: "uuid" };
|
|
2024
|
+
if (expression.name === "ROUND" || expression.name === "TRUNC") {
|
|
2025
|
+
const input = inferDomain(expression.arguments[0] ?? { kind: "literal", value: null });
|
|
2026
|
+
if (input?.kind !== "numeric")
|
|
2027
|
+
return void 0;
|
|
2028
|
+
const digits = expression.arguments[1];
|
|
2029
|
+
return digits?.kind === "literal" && typeof digits.value === "number" && Number.isInteger(digits.value) && digits.value > 0 ? { kind: "numeric", scale: digits.value } : { kind: "numeric" };
|
|
2030
|
+
}
|
|
2031
|
+
if (expression.name === "ABS") {
|
|
2032
|
+
const input = inferDomain(expression.arguments[0] ?? { kind: "literal", value: null });
|
|
2033
|
+
return input?.kind === "numeric" ? input : void 0;
|
|
2034
|
+
}
|
|
2035
|
+
if (expression.name === "FLOOR" || expression.name === "CEIL" || expression.name === "SIGN") {
|
|
2036
|
+
const input = inferDomain(expression.arguments[0] ?? { kind: "literal", value: null });
|
|
2037
|
+
return input?.kind === "numeric" ? { kind: "numeric" } : void 0;
|
|
2038
|
+
}
|
|
2039
|
+
if (expression.name === "MOD") {
|
|
2040
|
+
const sides = expression.arguments.map((argument) => [argument, inferDomain(argument)]);
|
|
2041
|
+
return sides.some(([, domain]) => domain?.kind === "numeric") ? numericResultDomain("%", sides) : void 0;
|
|
2042
|
+
}
|
|
1757
2043
|
if (expression.name === "DATE_ADD") {
|
|
1758
2044
|
const input = inferDomain(expression.arguments[0] ?? { kind: "literal", value: null });
|
|
1759
2045
|
const milliseconds = expression.arguments[2];
|
|
@@ -1764,6 +2050,7 @@ function inferBlockSchema(plan, schemas) {
|
|
|
1764
2050
|
}
|
|
1765
2051
|
return void 0;
|
|
1766
2052
|
};
|
|
2053
|
+
const exactNumericMix = (expression, left, right) => (left === "string" && right === "number" || left === "number" && right === "string") && inferDomain(expression)?.kind === "numeric";
|
|
1767
2054
|
const infer = (expression) => {
|
|
1768
2055
|
if (expression.kind === "subquery" || expression.kind === "list") {
|
|
1769
2056
|
throw new TypeError("Subqueries must be resolved before schema inference");
|
|
@@ -1788,6 +2075,10 @@ function inferBlockSchema(plan, schemas) {
|
|
|
1788
2075
|
if (type === "null")
|
|
1789
2076
|
continue;
|
|
1790
2077
|
if (resolved !== "null" && resolved !== type) {
|
|
2078
|
+
if (exactNumericMix(expression, resolved, type)) {
|
|
2079
|
+
resolved = "string";
|
|
2080
|
+
continue;
|
|
2081
|
+
}
|
|
1791
2082
|
throw new TypeError("CASE branches must produce one value type");
|
|
1792
2083
|
}
|
|
1793
2084
|
resolved = type;
|
|
@@ -1846,10 +2137,13 @@ function inferBlockSchema(plan, schemas) {
|
|
|
1846
2137
|
if (expression.name === "SUM" || expression.name === "AVG") {
|
|
1847
2138
|
return inferDomain(expression)?.kind === "numeric" ? "string" : "number";
|
|
1848
2139
|
}
|
|
1849
|
-
if (expression.name === "ROUND" || expression.name === "
|
|
2140
|
+
if (expression.name === "ROUND" || expression.name === "ABS" || expression.name === "FLOOR" || expression.name === "CEIL" || expression.name === "MOD") {
|
|
2141
|
+
return inferDomain(expression)?.kind === "numeric" ? "string" : "number";
|
|
2142
|
+
}
|
|
2143
|
+
if (expression.name === "LENGTH" || expression.name === "POWER" || expression.name === "SQRT" || expression.name === "INSTR" || expression.name === "EXTRACT" || expression.name === "OCTET_LENGTH" || expression.name === "GROUPING" || expression.name === "NEXTVAL" || expression.name === "CURRVAL" || expression.name === "RANDOM") {
|
|
1850
2144
|
return "number";
|
|
1851
2145
|
}
|
|
1852
|
-
if (expression.name === "JSON_ARRAYAGG" || expression.name === "STRING_AGG" || expression.name === "JSON_VALUE" || expression.name === "JSON_QUERY" || expression.name === "JSON_OBJECT" || expression.name === "JSON_ARRAY" || expression.name === "ARRAY" || expression.name === "MINNOW_JSON_GET" || expression.name === "MINNOW_JSON_GET_TEXT" || expression.name === "MINNOW_TUPLE_KEY" || expression.name === "MINNOW_COLLATE" || expression.name === "GEN_RANDOM_UUID") {
|
|
2146
|
+
if (expression.name === "JSON_ARRAYAGG" || expression.name === "STRING_AGG" || expression.name === "JSON_VALUE" || expression.name === "JSON_QUERY" || expression.name === "JSON_OBJECT" || expression.name === "JSON_ARRAY" || expression.name === "TO_JSON" || expression.name === "ARRAY" || expression.name === "MINNOW_JSON_GET" || expression.name === "MINNOW_JSON_GET_TEXT" || expression.name === "MINNOW_TUPLE_KEY" || expression.name === "MINNOW_COLLATE" || expression.name === "GEN_RANDOM_UUID") {
|
|
1853
2147
|
return "string";
|
|
1854
2148
|
}
|
|
1855
2149
|
if (expression.name === "JSON_EXISTS" || expression.name === "IS_JSON")
|
|
@@ -1860,6 +2154,9 @@ function inferBlockSchema(plan, schemas) {
|
|
|
1860
2154
|
const argument2 = expression.arguments[0];
|
|
1861
2155
|
return argument2 === void 0 ? "null" : infer(argument2);
|
|
1862
2156
|
}
|
|
2157
|
+
if (simple.returns === "number" && inferDomain(expression)?.kind === "numeric") {
|
|
2158
|
+
return "string";
|
|
2159
|
+
}
|
|
1863
2160
|
return simple.returns === "date" || simple.returns === "interval" ? "string" : simple.returns;
|
|
1864
2161
|
}
|
|
1865
2162
|
if (expression.name === "DATE_TRUNC")
|
|
@@ -1880,6 +2177,10 @@ function inferBlockSchema(plan, schemas) {
|
|
|
1880
2177
|
if (type === "null")
|
|
1881
2178
|
continue;
|
|
1882
2179
|
if (resolved !== "null" && resolved !== type) {
|
|
2180
|
+
if (exactNumericMix(expression, resolved, type)) {
|
|
2181
|
+
resolved = "string";
|
|
2182
|
+
continue;
|
|
2183
|
+
}
|
|
1883
2184
|
throw new TypeError(`${expression.name} arguments must produce one value type`);
|
|
1884
2185
|
}
|
|
1885
2186
|
resolved = type;
|
|
@@ -1915,6 +2216,10 @@ function inferBlockSchema(plan, schemas) {
|
|
|
1915
2216
|
if (type === "null")
|
|
1916
2217
|
continue;
|
|
1917
2218
|
if (resolved !== "null" && resolved !== type) {
|
|
2219
|
+
if (exactNumericMix(expression, resolved, type)) {
|
|
2220
|
+
resolved = "string";
|
|
2221
|
+
continue;
|
|
2222
|
+
}
|
|
1918
2223
|
throw new TypeError("COALESCE arguments must produce one value type");
|
|
1919
2224
|
}
|
|
1920
2225
|
resolved = type;
|
|
@@ -1936,7 +2241,7 @@ function inferBlockSchema(plan, schemas) {
|
|
|
1936
2241
|
if (type === "null") {
|
|
1937
2242
|
throw new TypeError(`Cannot infer a column type for output ${item.alias}`);
|
|
1938
2243
|
}
|
|
1939
|
-
const integer = item.expression
|
|
2244
|
+
const integer = integerTypedExpression(item.expression, resolveColumnInteger);
|
|
1940
2245
|
const sqlDomain = inferDomain(item.expression);
|
|
1941
2246
|
return [
|
|
1942
2247
|
{
|
|
@@ -1988,6 +2293,7 @@ function referencedColumns(plan, schemas) {
|
|
|
1988
2293
|
const sourceAliases = sources.map((source) => source.alias);
|
|
1989
2294
|
if (new Set(sourceAliases).size !== sourceAliases.length)
|
|
1990
2295
|
throw new TypeError("Table aliases must be unique");
|
|
2296
|
+
plan = unifyPlanGroupedReferences(plan, (tableName) => schemas.get(tableName));
|
|
1991
2297
|
validateGrouping(plan);
|
|
1992
2298
|
const aliases = new Map(sources.map((source) => [source.alias, source.table]));
|
|
1993
2299
|
const requested = new Map(sources.map((source) => [source.table, /* @__PURE__ */ new Set()]));
|
|
@@ -2108,6 +2414,7 @@ function createPreparedQuery(plan, tables, options = {}) {
|
|
|
2108
2414
|
assertTailParametersBound(plan);
|
|
2109
2415
|
plan = resolveStatementDatetimes(plan);
|
|
2110
2416
|
plan = bindPendingSelectShapes(plan, wildcardRowColumns(tables));
|
|
2417
|
+
plan = unifyPlanGroupedReferences(plan, wildcardRowColumns(tables));
|
|
2111
2418
|
validateGrouping(plan);
|
|
2112
2419
|
plan = expandSourceColumnAliases(plan, wildcardRowColumns(tables));
|
|
2113
2420
|
plan = expandNaturalJoins(plan, wildcardRowColumns(tables));
|
|
@@ -2153,6 +2460,7 @@ function createPreparedColumnarQuery(plan, tables, memory = new QueryMemoryConte
|
|
|
2153
2460
|
plan = expandNaturalJoins(plan, columnarColumns);
|
|
2154
2461
|
plan = expandQualifiedWildcards(plan, columnarColumns);
|
|
2155
2462
|
plan = expandDistinctWildcard(plan, columnarColumns);
|
|
2463
|
+
plan = unifyPlanGroupedReferences(plan, columnarColumns);
|
|
2156
2464
|
validateGrouping(plan);
|
|
2157
2465
|
let closed = false;
|
|
2158
2466
|
let prepared;
|
|
@@ -2495,6 +2803,485 @@ function forEachNestedBlock(block, visit) {
|
|
|
2495
2803
|
}
|
|
2496
2804
|
}
|
|
2497
2805
|
}
|
|
2806
|
+
function integerTypedExpression(expression, columnInteger = () => false, tableIntegerColumns) {
|
|
2807
|
+
const typed = (candidate) => integerTypedExpression(candidate, columnInteger, tableIntegerColumns);
|
|
2808
|
+
switch (expression.kind) {
|
|
2809
|
+
case "subquery": {
|
|
2810
|
+
const item = expression.block.select[0];
|
|
2811
|
+
return expression.block.select.length === 1 && item !== void 0 && integerTypedExpression(item.expression, blockColumnIntegerResolver(expression.block, tableIntegerColumns), tableIntegerColumns);
|
|
2812
|
+
}
|
|
2813
|
+
case "literal":
|
|
2814
|
+
return typeof expression.value === "number" && expression.exactText === void 0 && expression.decimal !== true && expression.internalSqlValue !== true && Number.isSafeInteger(expression.value);
|
|
2815
|
+
case "column":
|
|
2816
|
+
return columnInteger(expression.reference);
|
|
2817
|
+
case "binary": {
|
|
2818
|
+
if (expression.operator === "||")
|
|
2819
|
+
return false;
|
|
2820
|
+
const left = typed(expression.left);
|
|
2821
|
+
const right = typed(expression.right);
|
|
2822
|
+
if (left && right)
|
|
2823
|
+
return true;
|
|
2824
|
+
const untyped = (candidate) => candidate.kind === "parameter" || candidate.kind === "literal" && typeof candidate.value === "string" && candidate.internalSqlValue !== true;
|
|
2825
|
+
return left && untyped(expression.right) || right && untyped(expression.left);
|
|
2826
|
+
}
|
|
2827
|
+
case "call": {
|
|
2828
|
+
const { name, arguments: args } = expression;
|
|
2829
|
+
if (name === "COUNT")
|
|
2830
|
+
return true;
|
|
2831
|
+
if (name === "CAST") {
|
|
2832
|
+
return args[1]?.kind === "literal" && args[1].value === "number-integer";
|
|
2833
|
+
}
|
|
2834
|
+
if (name === "SUM" || name === "MIN" || name === "MAX" || name === "ABS") {
|
|
2835
|
+
return args.length === 1 && args[0] !== void 0 && typed(args[0]);
|
|
2836
|
+
}
|
|
2837
|
+
if (name === "MOD")
|
|
2838
|
+
return args.length === 2 && args.every(typed);
|
|
2839
|
+
if (name === "COALESCE" || name === "NULLIF" || name === "GREATEST" || name === "LEAST") {
|
|
2840
|
+
return args.length > 0 && args.every(typed);
|
|
2841
|
+
}
|
|
2842
|
+
return false;
|
|
2843
|
+
}
|
|
2844
|
+
case "case":
|
|
2845
|
+
return expression.branches.every((branch) => typed(branch.then)) && (expression.otherwise === void 0 || typed(expression.otherwise));
|
|
2846
|
+
default:
|
|
2847
|
+
return false;
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
function integerQuotient(left, right) {
|
|
2851
|
+
if (right === 0)
|
|
2852
|
+
return null;
|
|
2853
|
+
if (!Number.isInteger(left) || !Number.isInteger(right))
|
|
2854
|
+
return left / right;
|
|
2855
|
+
const quotient = Math.trunc(left / right);
|
|
2856
|
+
return quotient === 0 ? 0 : quotient;
|
|
2857
|
+
}
|
|
2858
|
+
function annotateIntegerDivision(expression, columnInteger = () => false, tableIntegerColumns) {
|
|
2859
|
+
if (expression.kind === "subquery" || expression.kind === "exists") {
|
|
2860
|
+
annotatePlanIntegerDivision(expression.block, tableIntegerColumns);
|
|
2861
|
+
return;
|
|
2862
|
+
}
|
|
2863
|
+
for (const child of childExpressions(expression)) {
|
|
2864
|
+
annotateIntegerDivision(child, columnInteger, tableIntegerColumns);
|
|
2865
|
+
}
|
|
2866
|
+
if (expression.kind === "binary" && expression.operator === "/" && expression.integer !== true && integerTypedExpression(expression, columnInteger, tableIntegerColumns)) {
|
|
2867
|
+
expression.integer = true;
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
function blockIntegerOutputs(block, tableIntegerColumns, columnAliases) {
|
|
2871
|
+
const resolver = blockColumnIntegerResolver(block, tableIntegerColumns);
|
|
2872
|
+
const names = [];
|
|
2873
|
+
for (const item of block.select) {
|
|
2874
|
+
if (item.expression.kind === "wildcard") {
|
|
2875
|
+
const table = item.expression.table;
|
|
2876
|
+
for (const source of [block.base, ...block.joins]) {
|
|
2877
|
+
if (table !== void 0 && source.alias !== table)
|
|
2878
|
+
continue;
|
|
2879
|
+
const columns = sourceIntegerColumns(source, tableIntegerColumns);
|
|
2880
|
+
if (columns === true)
|
|
2881
|
+
return true;
|
|
2882
|
+
for (const name of columns)
|
|
2883
|
+
if (resolver(name))
|
|
2884
|
+
names.push(name);
|
|
2885
|
+
}
|
|
2886
|
+
continue;
|
|
2887
|
+
}
|
|
2888
|
+
if (integerTypedExpression(item.expression, resolver, tableIntegerColumns)) {
|
|
2889
|
+
names.push(item.alias);
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
if (columnAliases === void 0)
|
|
2893
|
+
return new Set(names);
|
|
2894
|
+
const aliased = /* @__PURE__ */ new Set();
|
|
2895
|
+
block.select.forEach((item, index) => {
|
|
2896
|
+
const alias = columnAliases[index];
|
|
2897
|
+
if (alias !== void 0 && item.expression.kind !== "wildcard" && names.includes(item.alias)) {
|
|
2898
|
+
aliased.add(alias);
|
|
2899
|
+
}
|
|
2900
|
+
});
|
|
2901
|
+
return aliased;
|
|
2902
|
+
}
|
|
2903
|
+
function sourceIntegerColumns(source, tableIntegerColumns) {
|
|
2904
|
+
if (source.derived !== void 0) {
|
|
2905
|
+
return blockIntegerOutputs(source.derived, tableIntegerColumns, source.columnAliases);
|
|
2906
|
+
}
|
|
2907
|
+
if (source.union !== void 0) {
|
|
2908
|
+
const first = source.union.blocks[0];
|
|
2909
|
+
return first === void 0 ? /* @__PURE__ */ new Set() : blockIntegerOutputs(first, tableIntegerColumns, source.columnAliases);
|
|
2910
|
+
}
|
|
2911
|
+
if (source.windowed !== void 0) {
|
|
2912
|
+
return blockIntegerOutputs(source.windowed.block, tableIntegerColumns, source.columnAliases);
|
|
2913
|
+
}
|
|
2914
|
+
if (source.recursive !== void 0)
|
|
2915
|
+
return /* @__PURE__ */ new Set();
|
|
2916
|
+
return tableIntegerColumns?.(source.table) ?? /* @__PURE__ */ new Set();
|
|
2917
|
+
}
|
|
2918
|
+
function blockColumnIntegerResolver(block, tableIntegerColumns) {
|
|
2919
|
+
const sources = [block.base, ...block.joins];
|
|
2920
|
+
const cache = /* @__PURE__ */ new Map();
|
|
2921
|
+
const has = (columns, name) => columns === true || columns.has(name);
|
|
2922
|
+
const columnsOf = (source) => {
|
|
2923
|
+
let columns = cache.get(source);
|
|
2924
|
+
if (columns === void 0) {
|
|
2925
|
+
columns = sourceIntegerColumns(source, tableIntegerColumns);
|
|
2926
|
+
cache.set(source, columns);
|
|
2927
|
+
}
|
|
2928
|
+
return columns;
|
|
2929
|
+
};
|
|
2930
|
+
return (reference) => {
|
|
2931
|
+
const separator = reference.indexOf(".");
|
|
2932
|
+
if (separator !== -1) {
|
|
2933
|
+
const alias = reference.slice(0, separator);
|
|
2934
|
+
const name = reference.slice(separator + 1);
|
|
2935
|
+
const source = sources.find((candidate) => candidate.alias === alias);
|
|
2936
|
+
return source !== void 0 && has(columnsOf(source), name);
|
|
2937
|
+
}
|
|
2938
|
+
let found = 0;
|
|
2939
|
+
for (const source of sources)
|
|
2940
|
+
if (has(columnsOf(source), reference))
|
|
2941
|
+
found += 1;
|
|
2942
|
+
return found === 1;
|
|
2943
|
+
};
|
|
2944
|
+
}
|
|
2945
|
+
function annotatePlanIntegerDivision(plan, tableIntegerColumns) {
|
|
2946
|
+
forEachNestedBlock(plan, (nested) => annotatePlanIntegerDivision(nested, tableIntegerColumns));
|
|
2947
|
+
const resolver = blockColumnIntegerResolver(plan, tableIntegerColumns);
|
|
2948
|
+
forEachBlockExpression(plan, (expression) => annotateIntegerDivision(expression, resolver, tableIntegerColumns));
|
|
2949
|
+
}
|
|
2950
|
+
function planMayHaveIntegerDivision(plan) {
|
|
2951
|
+
const state = { found: false };
|
|
2952
|
+
const optimistic = () => true;
|
|
2953
|
+
const everyColumn = () => true;
|
|
2954
|
+
const probe = (expression) => {
|
|
2955
|
+
if (state.found)
|
|
2956
|
+
return;
|
|
2957
|
+
if (expression.kind === "subquery" || expression.kind === "exists") {
|
|
2958
|
+
if (planMayHaveIntegerDivision(expression.block))
|
|
2959
|
+
state.found = true;
|
|
2960
|
+
return;
|
|
2961
|
+
}
|
|
2962
|
+
if (expression.kind === "binary" && expression.operator === "/" && expression.integer !== true && integerTypedExpression(expression, optimistic, everyColumn)) {
|
|
2963
|
+
state.found = true;
|
|
2964
|
+
return;
|
|
2965
|
+
}
|
|
2966
|
+
childExpressions(expression).forEach(probe);
|
|
2967
|
+
};
|
|
2968
|
+
forEachNestedBlock(plan, (nested) => {
|
|
2969
|
+
if (!state.found && planMayHaveIntegerDivision(nested))
|
|
2970
|
+
state.found = true;
|
|
2971
|
+
});
|
|
2972
|
+
if (!state.found)
|
|
2973
|
+
forEachBlockExpression(plan, probe);
|
|
2974
|
+
return state.found;
|
|
2975
|
+
}
|
|
2976
|
+
function foldIdentifierCase(plan, tables) {
|
|
2977
|
+
const lowerTables = /* @__PURE__ */ new Map();
|
|
2978
|
+
for (const name of tables.keys()) {
|
|
2979
|
+
const lowered = name.toLowerCase();
|
|
2980
|
+
const bucket = lowerTables.get(lowered);
|
|
2981
|
+
if (bucket === void 0)
|
|
2982
|
+
lowerTables.set(lowered, [name]);
|
|
2983
|
+
else
|
|
2984
|
+
bucket.push(name);
|
|
2985
|
+
}
|
|
2986
|
+
const foldTable = (name) => {
|
|
2987
|
+
if (tables.has(name))
|
|
2988
|
+
return void 0;
|
|
2989
|
+
const lowered = name.toLowerCase();
|
|
2990
|
+
if (tables.has(lowered))
|
|
2991
|
+
return lowered;
|
|
2992
|
+
const candidates = lowerTables.get(lowered);
|
|
2993
|
+
return candidates?.length === 1 ? candidates[0] : void 0;
|
|
2994
|
+
};
|
|
2995
|
+
const foldColumn = (columns, name) => {
|
|
2996
|
+
if (columns.includes(name))
|
|
2997
|
+
return void 0;
|
|
2998
|
+
const lowered = name.toLowerCase();
|
|
2999
|
+
if (columns.includes(lowered))
|
|
3000
|
+
return lowered;
|
|
3001
|
+
const matches = columns.filter((column) => column.toLowerCase() === lowered);
|
|
3002
|
+
return matches.length === 1 ? matches[0] : void 0;
|
|
3003
|
+
};
|
|
3004
|
+
const pass = { probing: true, needed: false };
|
|
3005
|
+
const sourceColumns = (source) => {
|
|
3006
|
+
if (source.derived !== void 0) {
|
|
3007
|
+
return source.columnAliases ?? source.derived.select.map((item) => item.alias);
|
|
3008
|
+
}
|
|
3009
|
+
if (source.union !== void 0) {
|
|
3010
|
+
return source.columnAliases ?? source.union.blocks[0]?.select.map((item) => item.alias) ?? [];
|
|
3011
|
+
}
|
|
3012
|
+
if (source.windowed !== void 0) {
|
|
3013
|
+
return source.columnAliases ?? source.windowed.block.select.map((item) => item.alias);
|
|
3014
|
+
}
|
|
3015
|
+
if (source.recursive !== void 0)
|
|
3016
|
+
return source.columnAliases ?? [];
|
|
3017
|
+
return source.columnAliases ?? tables.get(source.table) ?? [];
|
|
3018
|
+
};
|
|
3019
|
+
const rewriteBlock = (block) => {
|
|
3020
|
+
if (pass.probing && pass.needed)
|
|
3021
|
+
return;
|
|
3022
|
+
forEachNestedBlock(block, rewriteBlock);
|
|
3023
|
+
const sources = [block.base, ...block.joins];
|
|
3024
|
+
for (const source of sources) {
|
|
3025
|
+
const plain = source.derived === void 0 && source.union === void 0 && source.windowed === void 0 && source.recursive === void 0;
|
|
3026
|
+
if (!plain)
|
|
3027
|
+
continue;
|
|
3028
|
+
const folded = foldTable(source.table);
|
|
3029
|
+
if (folded === void 0)
|
|
3030
|
+
continue;
|
|
3031
|
+
if (pass.probing) {
|
|
3032
|
+
pass.needed = true;
|
|
3033
|
+
return;
|
|
3034
|
+
}
|
|
3035
|
+
const aliasWasTable = source.alias === source.table;
|
|
3036
|
+
source.table = folded;
|
|
3037
|
+
if (aliasWasTable)
|
|
3038
|
+
source.alias = folded;
|
|
3039
|
+
}
|
|
3040
|
+
const foldReference = (reference) => {
|
|
3041
|
+
const separator = reference.indexOf(".");
|
|
3042
|
+
if (separator !== -1) {
|
|
3043
|
+
const qualifier = reference.slice(0, separator);
|
|
3044
|
+
const name = reference.slice(separator + 1);
|
|
3045
|
+
let source = sources.find((candidate) => candidate.alias === qualifier);
|
|
3046
|
+
let foldedQualifier;
|
|
3047
|
+
if (source === void 0) {
|
|
3048
|
+
const lowered = qualifier.toLowerCase();
|
|
3049
|
+
const matches = sources.filter((candidate) => candidate.alias.toLowerCase() === lowered);
|
|
3050
|
+
if (matches.length !== 1)
|
|
3051
|
+
return void 0;
|
|
3052
|
+
source = matches[0];
|
|
3053
|
+
foldedQualifier = source?.alias;
|
|
3054
|
+
}
|
|
3055
|
+
if (source === void 0)
|
|
3056
|
+
return void 0;
|
|
3057
|
+
const columns = sourceColumns(source);
|
|
3058
|
+
const foldedName = columns.includes(name) ? void 0 : foldColumn(columns, name);
|
|
3059
|
+
if (foldedQualifier === void 0 && foldedName === void 0)
|
|
3060
|
+
return void 0;
|
|
3061
|
+
return `${foldedQualifier ?? qualifier}.${foldedName ?? name}`;
|
|
3062
|
+
}
|
|
3063
|
+
if (sources.some((source) => sourceColumns(source).includes(reference)))
|
|
3064
|
+
return void 0;
|
|
3065
|
+
const folded = sources.map((source) => foldColumn(sourceColumns(source), reference)).filter((name) => name !== void 0);
|
|
3066
|
+
return folded.length === 1 ? folded[0] : void 0;
|
|
3067
|
+
};
|
|
3068
|
+
const aliases = new Set(block.select.map((item) => item.alias));
|
|
3069
|
+
const orderByAliases = new Set(block.orderBy.map((order) => order.expression).filter((expression) => expression.kind === "column" && aliases.has(expression.reference)));
|
|
3070
|
+
const writtenNames = block.select.map((item) => item.expression.kind === "column" ? bareColumnName(item.expression.reference) : void 0);
|
|
3071
|
+
const rewrite = (expression) => {
|
|
3072
|
+
if (expression.kind === "column") {
|
|
3073
|
+
if (orderByAliases.has(expression))
|
|
3074
|
+
return;
|
|
3075
|
+
const folded = foldReference(expression.reference);
|
|
3076
|
+
if (folded === void 0)
|
|
3077
|
+
return;
|
|
3078
|
+
if (pass.probing)
|
|
3079
|
+
pass.needed = true;
|
|
3080
|
+
else
|
|
3081
|
+
expression.reference = folded;
|
|
3082
|
+
return;
|
|
3083
|
+
}
|
|
3084
|
+
if (expression.kind === "wildcard") {
|
|
3085
|
+
if (expression.table === void 0)
|
|
3086
|
+
return;
|
|
3087
|
+
const exact = sources.some((source) => source.alias === expression.table);
|
|
3088
|
+
if (exact)
|
|
3089
|
+
return;
|
|
3090
|
+
const lowered = expression.table.toLowerCase();
|
|
3091
|
+
const matches = sources.filter((source) => source.alias.toLowerCase() === lowered);
|
|
3092
|
+
if (matches.length !== 1)
|
|
3093
|
+
return;
|
|
3094
|
+
if (pass.probing)
|
|
3095
|
+
pass.needed = true;
|
|
3096
|
+
else
|
|
3097
|
+
expression.table = matches[0]?.alias ?? expression.table;
|
|
3098
|
+
return;
|
|
3099
|
+
}
|
|
3100
|
+
if (expression.kind === "subquery" || expression.kind === "exists") {
|
|
3101
|
+
rewriteBlock(expression.block);
|
|
3102
|
+
return;
|
|
3103
|
+
}
|
|
3104
|
+
childExpressions(expression).forEach(rewrite);
|
|
3105
|
+
};
|
|
3106
|
+
forEachBlockExpression(block, rewrite);
|
|
3107
|
+
if (!pass.probing) {
|
|
3108
|
+
block.select.forEach((item, index) => {
|
|
3109
|
+
if (item.expression.kind !== "column")
|
|
3110
|
+
return;
|
|
3111
|
+
const name = bareColumnName(item.expression.reference);
|
|
3112
|
+
if (item.alias !== name && item.alias === writtenNames[index])
|
|
3113
|
+
item.alias = name;
|
|
3114
|
+
});
|
|
3115
|
+
}
|
|
3116
|
+
};
|
|
3117
|
+
rewriteBlock(plan);
|
|
3118
|
+
if (!pass.needed)
|
|
3119
|
+
return plan;
|
|
3120
|
+
const cloned = clonePlanTree(plan);
|
|
3121
|
+
pass.probing = false;
|
|
3122
|
+
rewriteBlock(cloned);
|
|
3123
|
+
return cloned;
|
|
3124
|
+
}
|
|
3125
|
+
function expandRowReferences(plan, tableColumns) {
|
|
3126
|
+
const pass = { probing: true, needed: false };
|
|
3127
|
+
const sourceColumns = (source) => {
|
|
3128
|
+
if (source.columnAliases !== void 0)
|
|
3129
|
+
return source.columnAliases;
|
|
3130
|
+
if (source.derived !== void 0)
|
|
3131
|
+
return source.derived.select.map((item) => item.alias);
|
|
3132
|
+
if (source.union !== void 0) {
|
|
3133
|
+
return source.union.blocks[0]?.select.map((item) => item.alias) ?? [];
|
|
3134
|
+
}
|
|
3135
|
+
if (source.windowed !== void 0)
|
|
3136
|
+
return source.windowed.block.select.map((item) => item.alias);
|
|
3137
|
+
if (source.recursive !== void 0)
|
|
3138
|
+
return [];
|
|
3139
|
+
return tableColumns(source.table) ?? [];
|
|
3140
|
+
};
|
|
3141
|
+
const rewriteBlock = (block) => {
|
|
3142
|
+
if (pass.probing && pass.needed)
|
|
3143
|
+
return;
|
|
3144
|
+
forEachNestedBlock(block, rewriteBlock);
|
|
3145
|
+
const sources = [block.base, ...block.joins];
|
|
3146
|
+
const rowObject = (reference) => {
|
|
3147
|
+
if (reference.includes("."))
|
|
3148
|
+
return void 0;
|
|
3149
|
+
const source = sources.find((candidate) => candidate.alias === reference);
|
|
3150
|
+
if (source === void 0)
|
|
3151
|
+
return void 0;
|
|
3152
|
+
if (sources.some((candidate) => sourceColumns(candidate).includes(reference))) {
|
|
3153
|
+
return void 0;
|
|
3154
|
+
}
|
|
3155
|
+
const columns = sourceColumns(source);
|
|
3156
|
+
if (columns.length === 0 || columns.some((name) => name.startsWith("\0")))
|
|
3157
|
+
return void 0;
|
|
3158
|
+
return {
|
|
3159
|
+
kind: "call",
|
|
3160
|
+
name: "JSON_OBJECT",
|
|
3161
|
+
arguments: columns.flatMap((name) => [
|
|
3162
|
+
{ kind: "literal", value: name },
|
|
3163
|
+
{ kind: "column", reference: `${source.alias}.${name}` }
|
|
3164
|
+
])
|
|
3165
|
+
};
|
|
3166
|
+
};
|
|
3167
|
+
if (pass.probing) {
|
|
3168
|
+
const probe = (expression) => {
|
|
3169
|
+
if (pass.needed)
|
|
3170
|
+
return;
|
|
3171
|
+
if (expression.kind === "column") {
|
|
3172
|
+
if (rowObject(expression.reference) !== void 0)
|
|
3173
|
+
pass.needed = true;
|
|
3174
|
+
return;
|
|
3175
|
+
}
|
|
3176
|
+
if (expression.kind === "subquery" || expression.kind === "exists") {
|
|
3177
|
+
rewriteBlock(expression.block);
|
|
3178
|
+
return;
|
|
3179
|
+
}
|
|
3180
|
+
childExpressions(expression).forEach(probe);
|
|
3181
|
+
};
|
|
3182
|
+
forEachBlockExpression(block, probe);
|
|
3183
|
+
return;
|
|
3184
|
+
}
|
|
3185
|
+
const rewrite = (expression) => {
|
|
3186
|
+
if (expression.kind === "column") {
|
|
3187
|
+
return rowObject(expression.reference) ?? expression;
|
|
3188
|
+
}
|
|
3189
|
+
if (expression.kind === "subquery" || expression.kind === "exists") {
|
|
3190
|
+
rewriteBlock(expression.block);
|
|
3191
|
+
return expression;
|
|
3192
|
+
}
|
|
3193
|
+
return mapChildExpressions(expression, rewrite);
|
|
3194
|
+
};
|
|
3195
|
+
mapBlockExpressions(block, rewrite);
|
|
3196
|
+
};
|
|
3197
|
+
rewriteBlock(plan);
|
|
3198
|
+
if (!pass.needed)
|
|
3199
|
+
return plan;
|
|
3200
|
+
const cloned = clonePlanTree(plan);
|
|
3201
|
+
pass.probing = false;
|
|
3202
|
+
rewriteBlock(cloned);
|
|
3203
|
+
return cloned;
|
|
3204
|
+
}
|
|
3205
|
+
function extendGroupByWithKeyDependents(plan, keyColumns, tableColumns = keyColumns) {
|
|
3206
|
+
const pass = { probing: true, needed: false };
|
|
3207
|
+
const rewriteBlock = (block) => {
|
|
3208
|
+
if (pass.probing && pass.needed)
|
|
3209
|
+
return;
|
|
3210
|
+
forEachNestedBlock(block, rewriteBlock);
|
|
3211
|
+
const visitSubqueries = (expression) => {
|
|
3212
|
+
if (expression.kind === "subquery" || expression.kind === "exists") {
|
|
3213
|
+
rewriteBlock(expression.block);
|
|
3214
|
+
return;
|
|
3215
|
+
}
|
|
3216
|
+
childExpressions(expression).forEach(visitSubqueries);
|
|
3217
|
+
};
|
|
3218
|
+
forEachBlockExpression(block, visitSubqueries);
|
|
3219
|
+
if (block.groupBy.length === 0)
|
|
3220
|
+
return;
|
|
3221
|
+
const sources = [block.base, ...block.joins];
|
|
3222
|
+
const multipleSources = sources.length > 1;
|
|
3223
|
+
const groupedColumns = new Set(block.groupBy.flatMap((expression) => expression.kind === "column" ? [expression.reference] : []));
|
|
3224
|
+
const additions = [];
|
|
3225
|
+
for (const source of sources) {
|
|
3226
|
+
const plain = source.derived === void 0 && source.union === void 0 && source.windowed === void 0 && source.recursive === void 0 && source.columnAliases === void 0;
|
|
3227
|
+
if (!plain)
|
|
3228
|
+
continue;
|
|
3229
|
+
const key = keyColumns(source.table);
|
|
3230
|
+
if (key === void 0 || key.length === 0)
|
|
3231
|
+
continue;
|
|
3232
|
+
const columns = tableColumns(source.table) ?? key;
|
|
3233
|
+
const namesFor = (column) => multipleSources ? [`${source.alias}.${column}`] : [column, `${source.alias}.${column}`];
|
|
3234
|
+
const keyGrouped = key.every((column) => namesFor(column).some((name) => groupedColumns.has(name)));
|
|
3235
|
+
if (!keyGrouped)
|
|
3236
|
+
continue;
|
|
3237
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
3238
|
+
const collect = (expression, insideAggregate) => {
|
|
3239
|
+
if (expression.kind === "column") {
|
|
3240
|
+
if (insideAggregate || groupedColumns.has(expression.reference))
|
|
3241
|
+
return;
|
|
3242
|
+
const separator = expression.reference.indexOf(".");
|
|
3243
|
+
const qualifier = separator === -1 ? void 0 : expression.reference.slice(0, separator);
|
|
3244
|
+
if (qualifier === void 0 ? multipleSources : qualifier !== source.alias)
|
|
3245
|
+
return;
|
|
3246
|
+
const name = separator === -1 ? expression.reference : expression.reference.slice(separator + 1);
|
|
3247
|
+
if (!columns.includes(name))
|
|
3248
|
+
return;
|
|
3249
|
+
referenced.add(expression.reference);
|
|
3250
|
+
return;
|
|
3251
|
+
}
|
|
3252
|
+
if (expression.kind === "subquery" || expression.kind === "exists")
|
|
3253
|
+
return;
|
|
3254
|
+
const aggregate = insideAggregate || expression.kind === "call" && aggregateNames.has(expression.name);
|
|
3255
|
+
childExpressions(expression).forEach((child) => collect(child, aggregate));
|
|
3256
|
+
};
|
|
3257
|
+
for (const item of block.select)
|
|
3258
|
+
collect(item.expression, false);
|
|
3259
|
+
for (const predicate of block.having) {
|
|
3260
|
+
collect(predicate.left, false);
|
|
3261
|
+
collect(predicate.right, false);
|
|
3262
|
+
}
|
|
3263
|
+
for (const order of block.orderBy)
|
|
3264
|
+
collect(order.expression, false);
|
|
3265
|
+
for (const reference of referenced) {
|
|
3266
|
+
if (pass.probing) {
|
|
3267
|
+
pass.needed = true;
|
|
3268
|
+
return;
|
|
3269
|
+
}
|
|
3270
|
+
groupedColumns.add(reference);
|
|
3271
|
+
additions.push({ kind: "column", reference });
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
3274
|
+
if (additions.length > 0)
|
|
3275
|
+
block.groupBy = [...block.groupBy, ...additions];
|
|
3276
|
+
};
|
|
3277
|
+
rewriteBlock(plan);
|
|
3278
|
+
if (!pass.needed)
|
|
3279
|
+
return plan;
|
|
3280
|
+
const cloned = clonePlanTree(plan);
|
|
3281
|
+
pass.probing = false;
|
|
3282
|
+
rewriteBlock(cloned);
|
|
3283
|
+
return cloned;
|
|
3284
|
+
}
|
|
2498
3285
|
function mapBlockExpressions(block, map) {
|
|
2499
3286
|
for (const item of block.select)
|
|
2500
3287
|
item.expression = map(item.expression);
|
|
@@ -2689,6 +3476,9 @@ function exactConstantFold(expression) {
|
|
|
2689
3476
|
const seeded = left.seeded || right.seeded;
|
|
2690
3477
|
if (left.value === null || right.value === null)
|
|
2691
3478
|
return { value: null, seeded };
|
|
3479
|
+
if (expression.integer === true && expression.operator === "/" && typeof left.value === "number" && typeof right.value === "number") {
|
|
3480
|
+
return { value: integerQuotient(left.value, right.value), seeded };
|
|
3481
|
+
}
|
|
2692
3482
|
const folded = exactNumericBinary(expression.operator, left.value, right.value, 0, false);
|
|
2693
3483
|
return folded === void 0 ? void 0 : { value: folded, seeded };
|
|
2694
3484
|
}
|
|
@@ -2701,11 +3491,13 @@ function resolveExactNumericConstants(expression) {
|
|
|
2701
3491
|
}
|
|
2702
3492
|
if (folded?.seeded === true) {
|
|
2703
3493
|
const value = folded.value;
|
|
2704
|
-
if (value === null
|
|
3494
|
+
if (value === null)
|
|
2705
3495
|
return { kind: "literal", value };
|
|
3496
|
+
if (typeof value === "number")
|
|
3497
|
+
return { kind: "literal", value, decimal: true };
|
|
2706
3498
|
const fits = exactNumericAsNumber(value);
|
|
2707
3499
|
if (fits !== void 0)
|
|
2708
|
-
return { kind: "literal", value: fits };
|
|
3500
|
+
return { kind: "literal", value: fits, decimal: true };
|
|
2709
3501
|
return { kind: "literal", value, internalSqlValue: true, sqlDomain: { kind: "numeric" } };
|
|
2710
3502
|
}
|
|
2711
3503
|
if (expression.kind === "subquery" || expression.kind === "exists") {
|
|
@@ -2729,8 +3521,12 @@ function resolvePlanExactNumericConstants(plan) {
|
|
|
2729
3521
|
mapBlockExpressions(plan, resolveExactNumericConstants);
|
|
2730
3522
|
forEachNestedBlock(plan, resolvePlanExactNumericConstants);
|
|
2731
3523
|
}
|
|
3524
|
+
function resolveStatementExpression(expression) {
|
|
3525
|
+
annotateIntegerDivision(expression);
|
|
3526
|
+
return resolveExactNumericConstants(expression);
|
|
3527
|
+
}
|
|
2732
3528
|
function resolveStatementExactNumericConstants(statement) {
|
|
2733
|
-
const resolve =
|
|
3529
|
+
const resolve = resolveStatementExpression;
|
|
2734
3530
|
if (statement.kind === "insert") {
|
|
2735
3531
|
const conflict = statement.onConflict;
|
|
2736
3532
|
if (conflict?.assignments !== void 0) {
|
|
@@ -2752,6 +3548,18 @@ function resolveStatementExactNumericConstants(statement) {
|
|
|
2752
3548
|
predicate.left = resolve(predicate.left);
|
|
2753
3549
|
predicate.right = resolve(predicate.right);
|
|
2754
3550
|
}
|
|
3551
|
+
if (statement.from !== void 0) {
|
|
3552
|
+
for (const predicate of statement.from.predicates) {
|
|
3553
|
+
predicate.left = resolve(predicate.left);
|
|
3554
|
+
predicate.right = resolve(predicate.right);
|
|
3555
|
+
}
|
|
3556
|
+
for (const join of statement.from.joins) {
|
|
3557
|
+
join.left = resolve(join.left);
|
|
3558
|
+
join.right = resolve(join.right);
|
|
3559
|
+
if (join.on !== void 0)
|
|
3560
|
+
join.on = resolve(join.on);
|
|
3561
|
+
}
|
|
3562
|
+
}
|
|
2755
3563
|
return;
|
|
2756
3564
|
}
|
|
2757
3565
|
if (statement.kind === "merge") {
|
|
@@ -3205,6 +4013,7 @@ function executeRowQueryInternal(plan, tables, memory) {
|
|
|
3205
4013
|
assertTailParametersBound(plan);
|
|
3206
4014
|
plan = resolveStatementDatetimes(plan);
|
|
3207
4015
|
plan = bindPendingSelectShapes(plan, wildcardRowColumns(tables));
|
|
4016
|
+
plan = unifyPlanGroupedReferences(plan, wildcardRowColumns(tables));
|
|
3208
4017
|
validateGrouping(plan);
|
|
3209
4018
|
plan = expandSourceColumnAliases(plan, wildcardRowColumns(tables));
|
|
3210
4019
|
plan = expandNaturalJoins(plan, wildcardRowColumns(tables));
|
|
@@ -3468,11 +4277,16 @@ function evaluate(expression, context, group) {
|
|
|
3468
4277
|
return null;
|
|
3469
4278
|
if (expression.operator === "||")
|
|
3470
4279
|
return concatenatedSqlValue(left, right);
|
|
3471
|
-
const
|
|
4280
|
+
const leftValue = typeof left === "string" && typeof right === "number" && !isSqlDomainValue(left) ? readUntypedText("number", left) : left;
|
|
4281
|
+
const rightValue = typeof right === "string" && typeof left === "number" && !isSqlDomainValue(right) ? readUntypedText("number", right) : right;
|
|
4282
|
+
if (expression.integer === true && expression.operator === "/" && typeof leftValue === "number" && typeof rightValue === "number") {
|
|
4283
|
+
return integerQuotient(leftValue, rightValue);
|
|
4284
|
+
}
|
|
4285
|
+
const exact = exactNumericBinary(expression.operator, leftValue, rightValue);
|
|
3472
4286
|
if (exact !== void 0)
|
|
3473
4287
|
return exact;
|
|
3474
|
-
const a = numeric(
|
|
3475
|
-
const b = numeric(
|
|
4288
|
+
const a = numeric(leftValue);
|
|
4289
|
+
const b = numeric(rightValue);
|
|
3476
4290
|
if (expression.operator === "+")
|
|
3477
4291
|
return a + b;
|
|
3478
4292
|
if (expression.operator === "-")
|
|
@@ -3912,10 +4726,12 @@ function evaluateBooleanExpression(expression, evaluateValue) {
|
|
|
3912
4726
|
const distinct = distinctFromComparison(evaluateValue(expression.left), evaluateValue(expression.right));
|
|
3913
4727
|
return operator === "IS DISTINCT FROM" ? distinct : !distinct;
|
|
3914
4728
|
}
|
|
3915
|
-
const
|
|
3916
|
-
const
|
|
3917
|
-
if (
|
|
4729
|
+
const evaluatedLeft = evaluateValue(expression.left);
|
|
4730
|
+
const evaluatedRight = evaluateValue(expression.right);
|
|
4731
|
+
if (evaluatedLeft === null || evaluatedLeft === void 0 || evaluatedRight === null || evaluatedRight === void 0) {
|
|
3918
4732
|
return null;
|
|
4733
|
+
}
|
|
4734
|
+
const [left, right] = coerceComparisonOperands(evaluatedLeft, evaluatedRight);
|
|
3919
4735
|
const a = comparable(left);
|
|
3920
4736
|
const b = comparable(right);
|
|
3921
4737
|
if (operator === "=")
|
|
@@ -4071,6 +4887,15 @@ function validateGrouping(plan) {
|
|
|
4071
4887
|
throw new TypeError(`Selected column must appear in GROUP BY: ${item.alias}`);
|
|
4072
4888
|
}
|
|
4073
4889
|
}
|
|
4890
|
+
for (const predicate of plan.having) {
|
|
4891
|
+
for (const side of [predicate.left, predicate.right]) {
|
|
4892
|
+
if (hasAggregate(side) || expressionColumns(side).length === 0)
|
|
4893
|
+
continue;
|
|
4894
|
+
if (!groupedExpression(side, groupExpressions)) {
|
|
4895
|
+
throw new TypeError("HAVING conditions must use aggregates, literals, or GROUP BY expressions");
|
|
4896
|
+
}
|
|
4897
|
+
}
|
|
4898
|
+
}
|
|
4074
4899
|
const forbiddenAggregates = [
|
|
4075
4900
|
...plan.joins.flatMap((join) => [
|
|
4076
4901
|
join.left,
|
|
@@ -4158,8 +4983,9 @@ function externalizeQueryResult(result) {
|
|
|
4158
4983
|
for (let position = 0; position < result.columns.length; position += 1) {
|
|
4159
4984
|
const name = result.columns[position] ?? "";
|
|
4160
4985
|
const value = row[name];
|
|
4161
|
-
if (value !== void 0 && !isSqlDomainValue(value))
|
|
4986
|
+
if (value !== void 0 && !isSqlDomainValue(value) && !(typeof value === "number" && result.columnDomains[position]?.kind === "numeric")) {
|
|
4162
4987
|
continue;
|
|
4988
|
+
}
|
|
4163
4989
|
const external = asQueryValue(externalSqlDomainColumnValue(value, result.columnDomains[position]));
|
|
4164
4990
|
if (external === value)
|
|
4165
4991
|
continue;
|
|
@@ -4242,6 +5068,12 @@ class Parser {
|
|
|
4242
5068
|
}
|
|
4243
5069
|
const columns = this.#cteColumnList();
|
|
4244
5070
|
this.#keyword("AS");
|
|
5071
|
+
if (this.#isKeyword("NOT")) {
|
|
5072
|
+
this.#keyword("NOT");
|
|
5073
|
+
this.#keyword("MATERIALIZED");
|
|
5074
|
+
} else if (this.#isKeyword("MATERIALIZED")) {
|
|
5075
|
+
this.#keyword("MATERIALIZED");
|
|
5076
|
+
}
|
|
4245
5077
|
this.#expectPunctuation("(");
|
|
4246
5078
|
if (recursive) {
|
|
4247
5079
|
this.#recursiveCte(name, columns);
|
|
@@ -4333,7 +5165,9 @@ class Parser {
|
|
|
4333
5165
|
return plan;
|
|
4334
5166
|
}
|
|
4335
5167
|
#compoundTail() {
|
|
4336
|
-
|
|
5168
|
+
const tail = { orderBy: this.#orderByClause(), ...this.#tailClauses() };
|
|
5169
|
+
this.#lockingClause();
|
|
5170
|
+
return tail;
|
|
4337
5171
|
}
|
|
4338
5172
|
#setTerm(sql, member) {
|
|
4339
5173
|
const first = this.#unionMember(sql, member);
|
|
@@ -4522,6 +5356,22 @@ class Parser {
|
|
|
4522
5356
|
nullable = false;
|
|
4523
5357
|
continue;
|
|
4524
5358
|
}
|
|
5359
|
+
if (this.#isKeyword("CONSTRAINT")) {
|
|
5360
|
+
this.#keyword("CONSTRAINT");
|
|
5361
|
+
const constraintName = this.#identifier();
|
|
5362
|
+
if (this.#isKeyword("CHECK")) {
|
|
5363
|
+
checks.push(this.#checkConstraint(constraintName));
|
|
5364
|
+
continue;
|
|
5365
|
+
}
|
|
5366
|
+
if (this.#isKeyword("REFERENCES")) {
|
|
5367
|
+
foreignKeys.push(this.#references(constraintName, [name]));
|
|
5368
|
+
continue;
|
|
5369
|
+
}
|
|
5370
|
+
if (this.#isKeyword("PRIMARY") || this.#isKeyword("UNIQUE") || this.#isKeyword("NOT") || this.#isKeyword("NULL")) {
|
|
5371
|
+
continue;
|
|
5372
|
+
}
|
|
5373
|
+
throw new TypeError(`Expected a constraint after CONSTRAINT ${constraintName}`);
|
|
5374
|
+
}
|
|
4525
5375
|
if (this.#isKeyword("CHECK")) {
|
|
4526
5376
|
checks.push(this.#checkConstraint(`${table}_${name}_check`));
|
|
4527
5377
|
continue;
|
|
@@ -4898,6 +5748,7 @@ class Parser {
|
|
|
4898
5748
|
return `numeric:${precision === void 0 ? "" : String(precision)}:${scale === void 0 ? "" : String(scale)}`;
|
|
4899
5749
|
}
|
|
4900
5750
|
if (["JSON", "JSONB", "UUID", "DATE", "TIME", "INTERVAL"].includes(word)) {
|
|
5751
|
+
this.#multiWordTypeTail(word);
|
|
4901
5752
|
return word.toLowerCase();
|
|
4902
5753
|
}
|
|
4903
5754
|
if (word === "DOUBLE") {
|
|
@@ -4907,14 +5758,27 @@ class Parser {
|
|
|
4907
5758
|
const mapped = createTableTypeNames.get(word);
|
|
4908
5759
|
if (mapped === void 0)
|
|
4909
5760
|
throw new TypeError(`Unsupported CAST target: ${word}`);
|
|
5761
|
+
this.#multiWordTypeTail(word);
|
|
4910
5762
|
if (this.#punctuation("(")) {
|
|
4911
5763
|
this.#typeWidth();
|
|
4912
5764
|
if (this.#punctuation(","))
|
|
4913
5765
|
this.#typeWidth();
|
|
4914
5766
|
this.#expectPunctuation(")");
|
|
4915
5767
|
}
|
|
4916
|
-
|
|
4917
|
-
|
|
5768
|
+
return integerTypeNames.has(word) ? "number-integer" : mapped;
|
|
5769
|
+
}
|
|
5770
|
+
#multiWordTypeTail(word) {
|
|
5771
|
+
if (word === "CHARACTER" && this.#isKeyword("VARYING")) {
|
|
5772
|
+
this.#keyword("VARYING");
|
|
5773
|
+
return true;
|
|
5774
|
+
}
|
|
5775
|
+
if ((word === "TIMESTAMP" || word === "TIME") && (this.#isKeyword("WITH") || this.#isKeyword("WITHOUT"))) {
|
|
5776
|
+
this.#keyword(this.#isKeyword("WITH") ? "WITH" : "WITHOUT");
|
|
5777
|
+
this.#keyword("TIME");
|
|
5778
|
+
this.#keyword("ZONE");
|
|
5779
|
+
return true;
|
|
5780
|
+
}
|
|
5781
|
+
return false;
|
|
4918
5782
|
}
|
|
4919
5783
|
#columnType() {
|
|
4920
5784
|
const declared = this.#identifier();
|
|
@@ -4940,6 +5804,7 @@ class Parser {
|
|
|
4940
5804
|
};
|
|
4941
5805
|
}
|
|
4942
5806
|
if (["JSON", "JSONB", "UUID", "DATE", "TIME", "INTERVAL"].includes(word)) {
|
|
5807
|
+
this.#multiWordTypeTail(word);
|
|
4943
5808
|
return { type: "string", sqlDomain: { kind: word.toLowerCase() } };
|
|
4944
5809
|
}
|
|
4945
5810
|
if (word === "DOUBLE") {
|
|
@@ -4950,6 +5815,7 @@ class Parser {
|
|
|
4950
5815
|
if (mapped === void 0) {
|
|
4951
5816
|
return { type: "string", sqlDomain: { kind: "enum", name: declared, values: [] } };
|
|
4952
5817
|
}
|
|
5818
|
+
const multiWord = this.#multiWordTypeTail(word);
|
|
4953
5819
|
if (this.#punctuation("(")) {
|
|
4954
5820
|
this.#typeWidth();
|
|
4955
5821
|
if (this.#punctuation(",")) {
|
|
@@ -4957,12 +5823,11 @@ class Parser {
|
|
|
4957
5823
|
}
|
|
4958
5824
|
this.#expectPunctuation(")");
|
|
4959
5825
|
}
|
|
4960
|
-
if (this.#punctuation("[")) {
|
|
5826
|
+
if (!multiWord && this.#punctuation("[")) {
|
|
4961
5827
|
this.#expectPunctuation("]");
|
|
4962
5828
|
return { type: "string", sqlDomain: { kind: "array", element: word } };
|
|
4963
5829
|
}
|
|
4964
|
-
|
|
4965
|
-
return { type: mapped, ...integer ? { integer: true } : {} };
|
|
5830
|
+
return { type: mapped, ...integerTypeNames.has(word) ? { integer: true } : {} };
|
|
4966
5831
|
}
|
|
4967
5832
|
parseMutation(keyword) {
|
|
4968
5833
|
const statement = keyword === "INSERT" ? this.#insertStatement() : keyword === "UPDATE" ? this.#updateStatement() : this.#deleteStatement();
|
|
@@ -5001,6 +5866,7 @@ class Parser {
|
|
|
5001
5866
|
}
|
|
5002
5867
|
if (this.#isKeyword("SELECT") || this.#isKeyword("WITH") || this.#peek().text === "(") {
|
|
5003
5868
|
const insertSource = this.#queryExpression("(insert select)");
|
|
5869
|
+
annotatePlanIntegerDivision(insertSource);
|
|
5004
5870
|
resolvePlanExactNumericConstants(insertSource);
|
|
5005
5871
|
const query = planHasPendingSelectShapes(insertSource) ? insertSource : optimizePlan(insertSource);
|
|
5006
5872
|
if (!planHasPendingSelectShapes(query) && !query.select.some((item) => item.expression.kind === "wildcard") && columns.length > 0 && query.select.length !== columns.length) {
|
|
@@ -5320,6 +6186,7 @@ class Parser {
|
|
|
5320
6186
|
if (new Set(assignments.map(({ column }) => column)).size !== assignments.length) {
|
|
5321
6187
|
throw new TypeError("UPDATE assignments must set each column once");
|
|
5322
6188
|
}
|
|
6189
|
+
const from = this.#mutationSources("FROM");
|
|
5323
6190
|
const predicates = this.#mutationPredicates();
|
|
5324
6191
|
return {
|
|
5325
6192
|
kind: "update",
|
|
@@ -5327,20 +6194,31 @@ class Parser {
|
|
|
5327
6194
|
...alias === void 0 ? {} : { alias },
|
|
5328
6195
|
assignments,
|
|
5329
6196
|
predicates,
|
|
6197
|
+
...from === void 0 ? {} : { from },
|
|
5330
6198
|
...this.#returningClause(table, alias)
|
|
5331
6199
|
};
|
|
5332
6200
|
}
|
|
6201
|
+
#mutationSources(keyword) {
|
|
6202
|
+
if (!this.#isKeyword(keyword))
|
|
6203
|
+
return void 0;
|
|
6204
|
+
this.#keyword(keyword);
|
|
6205
|
+
const groupConditions = [];
|
|
6206
|
+
const { base, joins } = this.#joinedSources([], groupConditions);
|
|
6207
|
+
return { base, joins, predicates: groupConditions.flatMap(splitCondition) };
|
|
6208
|
+
}
|
|
5333
6209
|
#deleteStatement() {
|
|
5334
6210
|
this.#keyword("DELETE");
|
|
5335
6211
|
this.#keyword("FROM");
|
|
5336
6212
|
const table = this.#identifier();
|
|
5337
|
-
const alias = this.#mutationAlias("WHERE", "RETURNING");
|
|
6213
|
+
const alias = this.#mutationAlias("WHERE", "USING", "RETURNING");
|
|
6214
|
+
const from = this.#mutationSources("USING");
|
|
5338
6215
|
const predicates = this.#mutationPredicates();
|
|
5339
6216
|
return {
|
|
5340
6217
|
kind: "delete",
|
|
5341
6218
|
table,
|
|
5342
6219
|
...alias === void 0 ? {} : { alias },
|
|
5343
6220
|
predicates,
|
|
6221
|
+
...from === void 0 ? {} : { from },
|
|
5344
6222
|
...this.#returningClause(table, alias)
|
|
5345
6223
|
};
|
|
5346
6224
|
}
|
|
@@ -5394,9 +6272,9 @@ class Parser {
|
|
|
5394
6272
|
}
|
|
5395
6273
|
const needsExecution = (value) => value.kind === "subquery" || value.kind === "exists" || value.kind === "call" && (statementDatetimeNames.has(value.name) || value.name === "NEXTVAL" || value.name === "CURRVAL" || volatileScalarFunctionNames.has(value.name)) || childExpressions(value).some(needsExecution);
|
|
5396
6274
|
if (needsExecution(expression)) {
|
|
5397
|
-
return { expression:
|
|
6275
|
+
return { expression: resolveStatementExpression(expression) };
|
|
5398
6276
|
}
|
|
5399
|
-
return asQueryValue(evaluate(
|
|
6277
|
+
return asQueryValue(evaluate(resolveStatementExpression(expression), {}));
|
|
5400
6278
|
}
|
|
5401
6279
|
#unionMember(sql, member) {
|
|
5402
6280
|
if (this.#punctuation("(")) {
|
|
@@ -5444,6 +6322,10 @@ class Parser {
|
|
|
5444
6322
|
#limitClause() {
|
|
5445
6323
|
if (this.#isKeyword("LIMIT")) {
|
|
5446
6324
|
this.#keyword("LIMIT");
|
|
6325
|
+
if (this.#isKeyword("ALL")) {
|
|
6326
|
+
this.#keyword("ALL");
|
|
6327
|
+
return {};
|
|
6328
|
+
}
|
|
5447
6329
|
if (this.#peek().kind === "parameter") {
|
|
5448
6330
|
const parameter = this.#parameterExpression();
|
|
5449
6331
|
return { limitParameter: parameter.index };
|
|
@@ -5496,6 +6378,37 @@ class Parser {
|
|
|
5496
6378
|
this.#keyword("ROW");
|
|
5497
6379
|
return result;
|
|
5498
6380
|
}
|
|
6381
|
+
#lockingClause() {
|
|
6382
|
+
while (this.#isKeyword("FOR")) {
|
|
6383
|
+
this.#keyword("FOR");
|
|
6384
|
+
if (this.#isKeyword("UPDATE"))
|
|
6385
|
+
this.#keyword("UPDATE");
|
|
6386
|
+
else if (this.#isKeyword("SHARE"))
|
|
6387
|
+
this.#keyword("SHARE");
|
|
6388
|
+
else if (this.#isKeyword("NO")) {
|
|
6389
|
+
this.#keyword("NO");
|
|
6390
|
+
this.#keyword("KEY");
|
|
6391
|
+
this.#keyword("UPDATE");
|
|
6392
|
+
} else if (this.#isKeyword("KEY")) {
|
|
6393
|
+
this.#keyword("KEY");
|
|
6394
|
+
this.#keyword("SHARE");
|
|
6395
|
+
} else {
|
|
6396
|
+
throw new TypeError(`Expected UPDATE or SHARE after FOR, found ${this.#peek().text}`);
|
|
6397
|
+
}
|
|
6398
|
+
if (this.#isKeyword("OF")) {
|
|
6399
|
+
this.#keyword("OF");
|
|
6400
|
+
this.#identifier();
|
|
6401
|
+
while (this.#punctuation(","))
|
|
6402
|
+
this.#identifier();
|
|
6403
|
+
}
|
|
6404
|
+
if (this.#isKeyword("NOWAIT"))
|
|
6405
|
+
this.#keyword("NOWAIT");
|
|
6406
|
+
else if (this.#isKeyword("SKIP")) {
|
|
6407
|
+
this.#keyword("SKIP");
|
|
6408
|
+
this.#keyword("LOCKED");
|
|
6409
|
+
}
|
|
6410
|
+
}
|
|
6411
|
+
}
|
|
5499
6412
|
#tailClauses() {
|
|
5500
6413
|
if (this.#isKeyword("LIMIT")) {
|
|
5501
6414
|
return { ...this.#limitClause(), ...this.#offsetClause() };
|
|
@@ -5569,36 +6482,219 @@ class Parser {
|
|
|
5569
6482
|
this.#compoundMember = false;
|
|
5570
6483
|
this.#keyword("SELECT");
|
|
5571
6484
|
let distinct = false;
|
|
6485
|
+
let distinctOn;
|
|
5572
6486
|
if (this.#isKeyword("DISTINCT")) {
|
|
5573
6487
|
this.#keyword("DISTINCT");
|
|
5574
|
-
|
|
6488
|
+
if (this.#isKeyword("ON")) {
|
|
6489
|
+
this.#keyword("ON");
|
|
6490
|
+
this.#expectPunctuation("(");
|
|
6491
|
+
distinctOn = this.#expressionList();
|
|
6492
|
+
this.#expectPunctuation(")");
|
|
6493
|
+
} else {
|
|
6494
|
+
distinct = true;
|
|
6495
|
+
}
|
|
6496
|
+
} else if (this.#isKeyword("ALL")) {
|
|
6497
|
+
this.#keyword("ALL");
|
|
5575
6498
|
}
|
|
5576
6499
|
const select = this.#selectList();
|
|
5577
6500
|
let base;
|
|
6501
|
+
let joins = [];
|
|
6502
|
+
const groupConditions = [];
|
|
5578
6503
|
if (this.#isKeyword("FROM")) {
|
|
5579
6504
|
this.#keyword("FROM");
|
|
5580
|
-
base = this.#
|
|
6505
|
+
({ base, joins } = this.#joinedSources(select, groupConditions));
|
|
5581
6506
|
} else {
|
|
5582
6507
|
if (select.some((item) => item.expression.kind === "wildcard")) {
|
|
5583
6508
|
throw new TypeError("SELECT * requires a FROM clause");
|
|
5584
6509
|
}
|
|
5585
6510
|
base = { table: DUAL_TABLE, alias: DUAL_TABLE };
|
|
5586
6511
|
}
|
|
5587
|
-
const
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
}
|
|
5600
|
-
|
|
5601
|
-
|
|
6512
|
+
const predicates = groupConditions.flatMap(splitCondition);
|
|
6513
|
+
if (this.#isKeyword("WHERE")) {
|
|
6514
|
+
this.#keyword("WHERE");
|
|
6515
|
+
predicates.push(...splitCondition(this.#expression()));
|
|
6516
|
+
}
|
|
6517
|
+
const groupBy = [];
|
|
6518
|
+
let groupingSets;
|
|
6519
|
+
if (this.#isKeyword("GROUP")) {
|
|
6520
|
+
this.#keyword("GROUP");
|
|
6521
|
+
this.#keyword("BY");
|
|
6522
|
+
if (this.#isKeyword("GROUPING") || this.#isKeyword("ROLLUP") || this.#isKeyword("CUBE")) {
|
|
6523
|
+
groupingSets = this.#groupingSets();
|
|
6524
|
+
} else {
|
|
6525
|
+
groupBy.push(...this.#expressionList());
|
|
6526
|
+
}
|
|
6527
|
+
}
|
|
6528
|
+
const having = [];
|
|
6529
|
+
if (this.#isKeyword("HAVING")) {
|
|
6530
|
+
this.#keyword("HAVING");
|
|
6531
|
+
having.push(...splitCondition(this.#expression()));
|
|
6532
|
+
}
|
|
6533
|
+
if (this.#isKeyword("WINDOW")) {
|
|
6534
|
+
this.#keyword("WINDOW");
|
|
6535
|
+
for (; ; ) {
|
|
6536
|
+
this.#identifier();
|
|
6537
|
+
this.#keyword("AS");
|
|
6538
|
+
const definition = this.#namedWindows.get(this.tokens[this.#index - 2]?.text ?? "");
|
|
6539
|
+
this.#index = definition?.end ?? this.#index;
|
|
6540
|
+
if (!this.#punctuation(","))
|
|
6541
|
+
break;
|
|
6542
|
+
}
|
|
6543
|
+
}
|
|
6544
|
+
const orderBy = compoundMember ? [] : this.#orderByClause();
|
|
6545
|
+
const tail = compoundMember ? {} : this.#tailClauses();
|
|
6546
|
+
if (!compoundMember)
|
|
6547
|
+
this.#lockingClause();
|
|
6548
|
+
if (distinctOn !== void 0) {
|
|
6549
|
+
return this.#distinctOnBlock(sql, distinctOn, { base, joins, select, predicates, groupBy, having, orderBy, groupingSets }, tail);
|
|
6550
|
+
}
|
|
6551
|
+
return assembleSelectBlock({
|
|
6552
|
+
sql,
|
|
6553
|
+
base,
|
|
6554
|
+
joins,
|
|
6555
|
+
select,
|
|
6556
|
+
distinct,
|
|
6557
|
+
predicates,
|
|
6558
|
+
groupBy,
|
|
6559
|
+
having,
|
|
6560
|
+
orderBy,
|
|
6561
|
+
...tail,
|
|
6562
|
+
...groupingSets === void 0 ? {} : { groupingSets }
|
|
6563
|
+
}, this.nextDerivedSequence);
|
|
6564
|
+
}
|
|
6565
|
+
#distinctOnBlock(sql, distinctOn, parts, tail) {
|
|
6566
|
+
const rowNumberAlias = "\0distinct_on";
|
|
6567
|
+
const innerSelect = [...parts.select];
|
|
6568
|
+
const aliased = new Map(parts.select.filter((item) => item.expression.kind !== "wildcard").map((item) => [item.alias, item.expression]));
|
|
6569
|
+
const windowExpression = (expression) => {
|
|
6570
|
+
if (expression.kind === "column" && !expression.reference.includes(".")) {
|
|
6571
|
+
const labelled = aliased.get(expression.reference);
|
|
6572
|
+
if (labelled !== void 0)
|
|
6573
|
+
return labelled;
|
|
6574
|
+
}
|
|
6575
|
+
return expression;
|
|
6576
|
+
};
|
|
6577
|
+
const outerOrderBy = [];
|
|
6578
|
+
parts.orderBy.forEach((order, index) => {
|
|
6579
|
+
const expression = order.expression;
|
|
6580
|
+
const output = expression.kind === "column" && !expression.reference.includes(".") ? parts.select.find((item) => item.alias === expression.reference) : parts.select.find((item) => item.expression.kind !== "wildcard" && JSON.stringify(item.expression) === JSON.stringify(expression));
|
|
6581
|
+
let alias = output?.alias;
|
|
6582
|
+
if (alias === void 0) {
|
|
6583
|
+
alias = `\0distinct_on_order_${String(index)}`;
|
|
6584
|
+
innerSelect.push({ expression, alias });
|
|
6585
|
+
}
|
|
6586
|
+
outerOrderBy.push({ ...order, expression: { kind: "column", reference: alias } });
|
|
6587
|
+
});
|
|
6588
|
+
innerSelect.push({
|
|
6589
|
+
expression: {
|
|
6590
|
+
kind: "window",
|
|
6591
|
+
name: "ROW_NUMBER",
|
|
6592
|
+
partitionBy: distinctOn.map(windowExpression),
|
|
6593
|
+
orderBy: parts.orderBy.map((order) => ({
|
|
6594
|
+
...order,
|
|
6595
|
+
expression: windowExpression(order.expression)
|
|
6596
|
+
}))
|
|
6597
|
+
},
|
|
6598
|
+
alias: rowNumberAlias
|
|
6599
|
+
});
|
|
6600
|
+
const inner = assembleSelectBlock({
|
|
6601
|
+
sql: "(distinct on)",
|
|
6602
|
+
base: parts.base,
|
|
6603
|
+
joins: parts.joins,
|
|
6604
|
+
select: innerSelect,
|
|
6605
|
+
distinct: false,
|
|
6606
|
+
predicates: parts.predicates,
|
|
6607
|
+
groupBy: parts.groupBy,
|
|
6608
|
+
having: parts.having,
|
|
6609
|
+
orderBy: [],
|
|
6610
|
+
...parts.groupingSets === void 0 ? {} : { groupingSets: parts.groupingSets }
|
|
6611
|
+
}, this.nextDerivedSequence);
|
|
6612
|
+
const outerAlias = "(distinct on)";
|
|
6613
|
+
const outerSelect = parts.select.map((item) => item.expression.kind === "wildcard" ? item : { expression: { kind: "column", reference: item.alias }, alias: item.alias });
|
|
6614
|
+
return assembleSelectBlock({
|
|
6615
|
+
sql,
|
|
6616
|
+
base: this.#derivedSource(inner, outerAlias),
|
|
6617
|
+
joins: [],
|
|
6618
|
+
select: outerSelect,
|
|
6619
|
+
distinct: false,
|
|
6620
|
+
predicates: [
|
|
6621
|
+
{
|
|
6622
|
+
left: { kind: "column", reference: rowNumberAlias },
|
|
6623
|
+
operator: "=",
|
|
6624
|
+
right: { kind: "literal", value: 1 }
|
|
6625
|
+
}
|
|
6626
|
+
],
|
|
6627
|
+
groupBy: [],
|
|
6628
|
+
having: [],
|
|
6629
|
+
orderBy: outerOrderBy,
|
|
6630
|
+
...tail
|
|
6631
|
+
}, this.nextDerivedSequence);
|
|
6632
|
+
}
|
|
6633
|
+
#groupingSets() {
|
|
6634
|
+
if (this.#isKeyword("GROUPING")) {
|
|
6635
|
+
this.#keyword("GROUPING");
|
|
6636
|
+
this.#keyword("SETS");
|
|
6637
|
+
this.#expectPunctuation("(");
|
|
6638
|
+
const sets2 = [];
|
|
6639
|
+
for (; ; ) {
|
|
6640
|
+
this.#expectPunctuation("(");
|
|
6641
|
+
const set = [];
|
|
6642
|
+
if (!this.#punctuation(")")) {
|
|
6643
|
+
set.push(...this.#expressionList());
|
|
6644
|
+
this.#expectPunctuation(")");
|
|
6645
|
+
}
|
|
6646
|
+
sets2.push(set);
|
|
6647
|
+
if (!this.#punctuation(","))
|
|
6648
|
+
break;
|
|
6649
|
+
}
|
|
6650
|
+
this.#expectPunctuation(")");
|
|
6651
|
+
return sets2;
|
|
6652
|
+
}
|
|
6653
|
+
const cube = this.#isKeyword("CUBE");
|
|
6654
|
+
this.#keyword(cube ? "CUBE" : "ROLLUP");
|
|
6655
|
+
this.#expectPunctuation("(");
|
|
6656
|
+
const columns = this.#expressionList();
|
|
6657
|
+
this.#expectPunctuation(")");
|
|
6658
|
+
if (columns.length === 0)
|
|
6659
|
+
throw new TypeError("ROLLUP/CUBE take at least one expression");
|
|
6660
|
+
if (cube) {
|
|
6661
|
+
if (columns.length > 5) {
|
|
6662
|
+
throw new TypeError("CUBE supports at most 5 expressions (32 grouping sets)");
|
|
6663
|
+
}
|
|
6664
|
+
const sets2 = [];
|
|
6665
|
+
for (let mask = (1 << columns.length) - 1; mask >= 0; mask -= 1) {
|
|
6666
|
+
sets2.push(columns.filter((_, index) => (mask & 1 << index) !== 0));
|
|
6667
|
+
}
|
|
6668
|
+
return sets2;
|
|
6669
|
+
}
|
|
6670
|
+
const sets = [];
|
|
6671
|
+
for (let length = columns.length; length >= 0; length -= 1) {
|
|
6672
|
+
sets.push(columns.slice(0, length));
|
|
6673
|
+
}
|
|
6674
|
+
return sets;
|
|
6675
|
+
}
|
|
6676
|
+
#joinedSources(select, groupConditions) {
|
|
6677
|
+
const first = this.#joinOperand(select, groupConditions);
|
|
6678
|
+
let base = first.base;
|
|
6679
|
+
const joins = [...first.joins];
|
|
6680
|
+
let rightJoins = first.rightJoins;
|
|
6681
|
+
while (this.#peek().text === "," || this.#isKeyword("NATURAL") || this.#isKeyword("JOIN") || this.#isKeyword("INNER") || this.#isKeyword("LEFT") || this.#isKeyword("RIGHT") || this.#isKeyword("FULL") || this.#isKeyword("CROSS")) {
|
|
6682
|
+
if (this.#punctuation(",")) {
|
|
6683
|
+
const operand2 = this.#joinOperand(select, groupConditions);
|
|
6684
|
+
joins.push(crossJoinPlan(operand2.base), ...operand2.joins);
|
|
6685
|
+
rightJoins += operand2.rightJoins;
|
|
6686
|
+
continue;
|
|
6687
|
+
}
|
|
6688
|
+
if (this.#isKeyword("CROSS")) {
|
|
6689
|
+
this.#keyword("CROSS");
|
|
6690
|
+
this.#keyword("JOIN");
|
|
6691
|
+
const operand2 = this.#joinOperand(select, groupConditions);
|
|
6692
|
+
joins.push(crossJoinPlan(operand2.base), ...operand2.joins);
|
|
6693
|
+
rightJoins += operand2.rightJoins;
|
|
6694
|
+
continue;
|
|
6695
|
+
}
|
|
6696
|
+
let kind = "inner";
|
|
6697
|
+
let right = false;
|
|
5602
6698
|
let full = false;
|
|
5603
6699
|
let natural = false;
|
|
5604
6700
|
if (this.#isKeyword("NATURAL")) {
|
|
@@ -5622,7 +6718,21 @@ class Parser {
|
|
|
5622
6718
|
if (this.#isKeyword("OUTER"))
|
|
5623
6719
|
this.#keyword("OUTER");
|
|
5624
6720
|
this.#keyword("JOIN");
|
|
5625
|
-
const
|
|
6721
|
+
const operand = this.#joinOperand(select, groupConditions);
|
|
6722
|
+
if (operand.group) {
|
|
6723
|
+
if (natural || right || full || kind === "left") {
|
|
6724
|
+
throw new TypeError("A parenthesized join can only follow a comma, CROSS JOIN, or INNER JOIN");
|
|
6725
|
+
}
|
|
6726
|
+
if (this.#isKeyword("USING")) {
|
|
6727
|
+
throw new TypeError("A parenthesized join takes an ON condition, not USING");
|
|
6728
|
+
}
|
|
6729
|
+
this.#keyword("ON");
|
|
6730
|
+
groupConditions.push(this.#expression());
|
|
6731
|
+
joins.push(crossJoinPlan(operand.base), ...operand.joins);
|
|
6732
|
+
rightJoins += operand.rightJoins;
|
|
6733
|
+
continue;
|
|
6734
|
+
}
|
|
6735
|
+
const source = operand.base;
|
|
5626
6736
|
if (natural) {
|
|
5627
6737
|
if (this.#isKeyword("ON") || this.#isKeyword("USING")) {
|
|
5628
6738
|
throw new TypeError("A NATURAL join takes no ON or USING clause");
|
|
@@ -5682,95 +6792,58 @@ class Parser {
|
|
|
5682
6792
|
}
|
|
5683
6793
|
}
|
|
5684
6794
|
}
|
|
5685
|
-
|
|
5686
|
-
|
|
5687
|
-
this.#keyword("WHERE");
|
|
5688
|
-
predicates.push(...splitCondition(this.#expression()));
|
|
5689
|
-
}
|
|
5690
|
-
const groupBy = [];
|
|
5691
|
-
let groupingSets;
|
|
5692
|
-
if (this.#isKeyword("GROUP")) {
|
|
5693
|
-
this.#keyword("GROUP");
|
|
5694
|
-
this.#keyword("BY");
|
|
5695
|
-
if (this.#isKeyword("GROUPING") || this.#isKeyword("ROLLUP") || this.#isKeyword("CUBE")) {
|
|
5696
|
-
groupingSets = this.#groupingSets();
|
|
5697
|
-
} else {
|
|
5698
|
-
groupBy.push(...this.#expressionList());
|
|
5699
|
-
}
|
|
5700
|
-
}
|
|
5701
|
-
const having = [];
|
|
5702
|
-
if (this.#isKeyword("HAVING")) {
|
|
5703
|
-
this.#keyword("HAVING");
|
|
5704
|
-
having.push(...splitCondition(this.#expression()));
|
|
6795
|
+
if (rightJoins > 0 && joins.length !== 1) {
|
|
6796
|
+
throw new TypeError("RIGHT JOIN is only supported as the sole join");
|
|
5705
6797
|
}
|
|
5706
|
-
|
|
5707
|
-
this.#keyword("WINDOW");
|
|
5708
|
-
for (; ; ) {
|
|
5709
|
-
this.#identifier();
|
|
5710
|
-
this.#keyword("AS");
|
|
5711
|
-
const definition = this.#namedWindows.get(this.tokens[this.#index - 2]?.text ?? "");
|
|
5712
|
-
this.#index = definition?.end ?? this.#index;
|
|
5713
|
-
if (!this.#punctuation(","))
|
|
5714
|
-
break;
|
|
5715
|
-
}
|
|
5716
|
-
}
|
|
5717
|
-
const orderBy = compoundMember ? [] : this.#orderByClause();
|
|
5718
|
-
return assembleSelectBlock({
|
|
5719
|
-
sql,
|
|
5720
|
-
base,
|
|
5721
|
-
joins,
|
|
5722
|
-
select,
|
|
5723
|
-
distinct,
|
|
5724
|
-
predicates,
|
|
5725
|
-
groupBy,
|
|
5726
|
-
having,
|
|
5727
|
-
orderBy,
|
|
5728
|
-
...compoundMember ? {} : this.#tailClauses(),
|
|
5729
|
-
...groupingSets === void 0 ? {} : { groupingSets }
|
|
5730
|
-
}, this.nextDerivedSequence);
|
|
6798
|
+
return { base, joins, rightJoins };
|
|
5731
6799
|
}
|
|
5732
|
-
#
|
|
5733
|
-
|
|
5734
|
-
|
|
5735
|
-
this.#keyword("SETS");
|
|
6800
|
+
#joinOperand(select, groupConditions) {
|
|
6801
|
+
const next = this.#peek();
|
|
6802
|
+
if (next.kind === "punctuation" && next.text === "(" && this.#parenthesizedJoinAhead()) {
|
|
5736
6803
|
this.#expectPunctuation("(");
|
|
5737
|
-
const
|
|
5738
|
-
for (; ; ) {
|
|
5739
|
-
this.#expectPunctuation("(");
|
|
5740
|
-
const set = [];
|
|
5741
|
-
if (!this.#punctuation(")")) {
|
|
5742
|
-
set.push(...this.#expressionList());
|
|
5743
|
-
this.#expectPunctuation(")");
|
|
5744
|
-
}
|
|
5745
|
-
sets2.push(set);
|
|
5746
|
-
if (!this.#punctuation(","))
|
|
5747
|
-
break;
|
|
5748
|
-
}
|
|
6804
|
+
const inner = this.#joinedSources(select, groupConditions);
|
|
5749
6805
|
this.#expectPunctuation(")");
|
|
5750
|
-
|
|
5751
|
-
|
|
5752
|
-
const cube = this.#isKeyword("CUBE");
|
|
5753
|
-
this.#keyword(cube ? "CUBE" : "ROLLUP");
|
|
5754
|
-
this.#expectPunctuation("(");
|
|
5755
|
-
const columns = this.#expressionList();
|
|
5756
|
-
this.#expectPunctuation(")");
|
|
5757
|
-
if (columns.length === 0)
|
|
5758
|
-
throw new TypeError("ROLLUP/CUBE take at least one expression");
|
|
5759
|
-
if (cube) {
|
|
5760
|
-
if (columns.length > 5) {
|
|
5761
|
-
throw new TypeError("CUBE supports at most 5 expressions (32 grouping sets)");
|
|
6806
|
+
if (this.#sourceAlias() !== void 0) {
|
|
6807
|
+
throw new TypeError("A parenthesized join cannot take an alias");
|
|
5762
6808
|
}
|
|
5763
|
-
|
|
5764
|
-
for (let mask = (1 << columns.length) - 1; mask >= 0; mask -= 1) {
|
|
5765
|
-
sets2.push(columns.filter((_, index) => (mask & 1 << index) !== 0));
|
|
5766
|
-
}
|
|
5767
|
-
return sets2;
|
|
6809
|
+
return { ...inner, group: true };
|
|
5768
6810
|
}
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
|
|
6811
|
+
return { base: this.#source(), joins: [], rightJoins: 0, group: false };
|
|
6812
|
+
}
|
|
6813
|
+
#parenthesizedJoinAhead() {
|
|
6814
|
+
let cursor = this.#index;
|
|
6815
|
+
let first = this.tokens[cursor];
|
|
6816
|
+
while (first?.kind === "punctuation" && first.text === "(") {
|
|
6817
|
+
cursor += 1;
|
|
6818
|
+
first = this.tokens[cursor];
|
|
5772
6819
|
}
|
|
5773
|
-
|
|
6820
|
+
if (first?.kind !== "identifier")
|
|
6821
|
+
return false;
|
|
6822
|
+
const keyword = first.quoted === true ? "" : first.text.toUpperCase();
|
|
6823
|
+
if (keyword !== "SELECT" && keyword !== "VALUES" && keyword !== "WITH")
|
|
6824
|
+
return true;
|
|
6825
|
+
if (cursor === this.#index + 1)
|
|
6826
|
+
return false;
|
|
6827
|
+
let depth = 0;
|
|
6828
|
+
for (let index = this.#index; index < this.tokens.length; index += 1) {
|
|
6829
|
+
const token = this.tokens[index];
|
|
6830
|
+
if (token?.kind !== "punctuation")
|
|
6831
|
+
continue;
|
|
6832
|
+
if (token.text === "(")
|
|
6833
|
+
depth += 1;
|
|
6834
|
+
else if (token.text === ")") {
|
|
6835
|
+
depth -= 1;
|
|
6836
|
+
if (depth > 0)
|
|
6837
|
+
continue;
|
|
6838
|
+
const next = this.tokens[index + 1];
|
|
6839
|
+
if (next?.kind !== "identifier")
|
|
6840
|
+
return true;
|
|
6841
|
+
if (next.quoted === true || next.text.toUpperCase() === "AS")
|
|
6842
|
+
return false;
|
|
6843
|
+
return clauseKeywords.has(next.text.toUpperCase()) || next.text.toUpperCase() === "ON";
|
|
6844
|
+
}
|
|
6845
|
+
}
|
|
6846
|
+
return false;
|
|
5774
6847
|
}
|
|
5775
6848
|
#selectList() {
|
|
5776
6849
|
const items = [];
|
|
@@ -5783,6 +6856,9 @@ class Parser {
|
|
|
5783
6856
|
this.#keyword("AS");
|
|
5784
6857
|
alias = this.#identifier();
|
|
5785
6858
|
explicitAlias = true;
|
|
6859
|
+
} else if (this.#bareLabelAhead()) {
|
|
6860
|
+
alias = this.#identifier();
|
|
6861
|
+
explicitAlias = true;
|
|
5786
6862
|
}
|
|
5787
6863
|
items.push({ expression, alias });
|
|
5788
6864
|
explicitAliases.push(explicitAlias);
|
|
@@ -6068,6 +7144,12 @@ class Parser {
|
|
|
6068
7144
|
}
|
|
6069
7145
|
return { table, alias };
|
|
6070
7146
|
}
|
|
7147
|
+
#bareLabelAhead() {
|
|
7148
|
+
const token = this.#peek();
|
|
7149
|
+
if (token.kind !== "identifier")
|
|
7150
|
+
return false;
|
|
7151
|
+
return token.quoted === true || !bareLabelStopWords.has(token.text.toUpperCase());
|
|
7152
|
+
}
|
|
6071
7153
|
#sourceAlias() {
|
|
6072
7154
|
if (this.#isKeyword("AS")) {
|
|
6073
7155
|
this.#keyword("AS");
|
|
@@ -6332,6 +7414,14 @@ class Parser {
|
|
|
6332
7414
|
}
|
|
6333
7415
|
return { kind: "condition", operator, left, right: { kind: "list", items } };
|
|
6334
7416
|
}
|
|
7417
|
+
const likeSpelling = this.#peek();
|
|
7418
|
+
if (likeSpelling.kind === "operator" && /^!?~~\*?$/.test(likeSpelling.text)) {
|
|
7419
|
+
this.#index += 1;
|
|
7420
|
+
const negatedSpelling = likeSpelling.text.startsWith("!") !== negated;
|
|
7421
|
+
const insensitiveSpelling = likeSpelling.text.endsWith("*");
|
|
7422
|
+
const operator = insensitiveSpelling ? negatedSpelling ? "NOT ILIKE" : "ILIKE" : negatedSpelling ? "NOT LIKE" : "LIKE";
|
|
7423
|
+
return { kind: "condition", operator, left, right: this.#additive() };
|
|
7424
|
+
}
|
|
6335
7425
|
if (this.#isKeyword("LIKE") || this.#isKeyword("ILIKE")) {
|
|
6336
7426
|
const insensitive = this.#isKeyword("ILIKE");
|
|
6337
7427
|
this.#keyword(insensitive ? "ILIKE" : "LIKE");
|
|
@@ -6530,8 +7620,9 @@ class Parser {
|
|
|
6530
7620
|
throw new SqlCompileError(error instanceof Error ? error.message : `Invalid number: ${token.text}`, token.start, token.end - token.start);
|
|
6531
7621
|
}
|
|
6532
7622
|
const fits = exactNumericAsNumber(tagged);
|
|
6533
|
-
if (fits !== void 0)
|
|
6534
|
-
return { kind: "literal", value: fits, exactText: token.text };
|
|
7623
|
+
if (fits !== void 0) {
|
|
7624
|
+
return { kind: "literal", value: fits, exactText: token.text, decimal: true };
|
|
7625
|
+
}
|
|
6535
7626
|
return {
|
|
6536
7627
|
kind: "literal",
|
|
6537
7628
|
value: tagged,
|
|
@@ -6696,6 +7787,10 @@ class Parser {
|
|
|
6696
7787
|
args.push(this.#expression());
|
|
6697
7788
|
}
|
|
6698
7789
|
this.#expectPunctuation(")");
|
|
7790
|
+
const pattern = args[1];
|
|
7791
|
+
if (args.length === 2 && pattern?.kind === "literal" && typeof pattern.value === "string") {
|
|
7792
|
+
return { kind: "call", name: "REGEXP_SUBSTR", arguments: args };
|
|
7793
|
+
}
|
|
6699
7794
|
return { kind: "call", name: "SUBSTR", arguments: args };
|
|
6700
7795
|
}
|
|
6701
7796
|
this.#index = restore;
|
|
@@ -6900,8 +7995,18 @@ class Parser {
|
|
|
6900
7995
|
...fallback === null ? {} : { fallback }
|
|
6901
7996
|
};
|
|
6902
7997
|
}
|
|
6903
|
-
if (upper === "JSON_OBJECT")
|
|
7998
|
+
if (upper === "JSON_OBJECT" || upper === "JSON_BUILD_OBJECT" || upper === "JSONB_BUILD_OBJECT") {
|
|
6904
7999
|
return this.#jsonObject();
|
|
8000
|
+
}
|
|
8001
|
+
if (upper === "DATE") {
|
|
8002
|
+
const operand = this.#expression();
|
|
8003
|
+
this.#expectPunctuation(")");
|
|
8004
|
+
return {
|
|
8005
|
+
kind: "call",
|
|
8006
|
+
name: "CAST",
|
|
8007
|
+
arguments: [operand, { kind: "literal", value: "date" }]
|
|
8008
|
+
};
|
|
8009
|
+
}
|
|
6905
8010
|
if (statisticalAggregates.has(upper))
|
|
6906
8011
|
return this.#statisticalAggregate(upper);
|
|
6907
8012
|
if (upper === "EVERY" || upper === "BOOL_AND" || upper === "BOOL_OR") {
|
|
@@ -7019,6 +8124,10 @@ class Parser {
|
|
|
7019
8124
|
}
|
|
7020
8125
|
if (name === "SUBSTR" && (args.length < 2 || args.length > 3))
|
|
7021
8126
|
throw new TypeError("SUBSTR requires a string, a start, and an optional length");
|
|
8127
|
+
const substringPattern = args[1];
|
|
8128
|
+
if (upper === "SUBSTRING" && args.length === 2 && substringPattern?.kind === "literal" && typeof substringPattern.value === "string") {
|
|
8129
|
+
return { kind: "call", name: "REGEXP_SUBSTR", arguments: args };
|
|
8130
|
+
}
|
|
7022
8131
|
if (name === "DATE_TRUNC") {
|
|
7023
8132
|
if (args.length !== 2)
|
|
7024
8133
|
throw new TypeError("DATE_TRUNC requires a unit and a datetime argument");
|
|
@@ -7499,6 +8608,105 @@ function resolveGroupByReferences(groupBy, select) {
|
|
|
7499
8608
|
return structuredClone(item.expression);
|
|
7500
8609
|
});
|
|
7501
8610
|
}
|
|
8611
|
+
function unifyGroupedReferences(block, resolve) {
|
|
8612
|
+
const groupingReferences = [...block.groupBy, ...(block.groupingSets ?? []).flat()].flatMap((expression) => expression.kind === "column" ? [expression.reference] : []);
|
|
8613
|
+
if (groupingReferences.length === 0)
|
|
8614
|
+
return block;
|
|
8615
|
+
const spelled = new Set(groupingReferences);
|
|
8616
|
+
const bareNames = new Set(groupingReferences.map(bareColumnName));
|
|
8617
|
+
const candidate = (expression) => expressionColumns(expression).some((reference) => !spelled.has(reference) && bareNames.has(bareColumnName(reference)));
|
|
8618
|
+
const aliases = new Set(block.select.map((item) => item.alias));
|
|
8619
|
+
const orderCandidates = block.orderBy.filter((order) => !(order.expression.kind === "column" && aliases.has(order.expression.reference)));
|
|
8620
|
+
if (!block.select.some((item) => candidate(item.expression)) && !block.having.some((predicate) => candidate(predicate.left) || candidate(predicate.right)) && !orderCandidates.some((order) => candidate(order.expression)) && !groupingReferences.some((reference) => !spelled.has(reference))) {
|
|
8621
|
+
return block;
|
|
8622
|
+
}
|
|
8623
|
+
const spellings = /* @__PURE__ */ new Map();
|
|
8624
|
+
for (const reference of groupingReferences) {
|
|
8625
|
+
const column = resolve(reference);
|
|
8626
|
+
if (column !== void 0 && !spellings.has(column))
|
|
8627
|
+
spellings.set(column, reference);
|
|
8628
|
+
}
|
|
8629
|
+
if (spellings.size === 0)
|
|
8630
|
+
return block;
|
|
8631
|
+
let respelled = 0;
|
|
8632
|
+
const respell = (expression) => {
|
|
8633
|
+
if (expression.kind === "column") {
|
|
8634
|
+
const column = resolve(expression.reference);
|
|
8635
|
+
const spelling = column === void 0 ? void 0 : spellings.get(column);
|
|
8636
|
+
if (spelling === void 0 || spelling === expression.reference)
|
|
8637
|
+
return expression;
|
|
8638
|
+
respelled += 1;
|
|
8639
|
+
return { ...expression, reference: spelling };
|
|
8640
|
+
}
|
|
8641
|
+
if (expression.kind === "subquery" || expression.kind === "exists")
|
|
8642
|
+
return expression;
|
|
8643
|
+
if (expression.kind === "window") {
|
|
8644
|
+
return {
|
|
8645
|
+
...expression,
|
|
8646
|
+
partitionBy: expression.partitionBy.map(respell),
|
|
8647
|
+
orderBy: expression.orderBy.map((order) => ({
|
|
8648
|
+
...order,
|
|
8649
|
+
expression: respell(order.expression)
|
|
8650
|
+
})),
|
|
8651
|
+
...expression.argument === void 0 ? {} : { argument: respell(expression.argument) }
|
|
8652
|
+
};
|
|
8653
|
+
}
|
|
8654
|
+
return mapChildExpressions(expression, respell);
|
|
8655
|
+
};
|
|
8656
|
+
const unified = {
|
|
8657
|
+
...block,
|
|
8658
|
+
select: block.select.map((item) => ({ ...item, expression: respell(item.expression) })),
|
|
8659
|
+
groupBy: block.groupBy.map(respell),
|
|
8660
|
+
having: block.having.map((predicate) => ({
|
|
8661
|
+
...predicate,
|
|
8662
|
+
left: respell(predicate.left),
|
|
8663
|
+
right: respell(predicate.right)
|
|
8664
|
+
})),
|
|
8665
|
+
orderBy: block.orderBy.map((order) => order.expression.kind === "column" && aliases.has(order.expression.reference) ? order : { ...order, expression: respell(order.expression) }),
|
|
8666
|
+
...block.groupingSets === void 0 ? {} : { groupingSets: block.groupingSets.map((set) => set.map(respell)) }
|
|
8667
|
+
};
|
|
8668
|
+
return respelled > 0 ? unified : block;
|
|
8669
|
+
}
|
|
8670
|
+
function bareColumnName(reference) {
|
|
8671
|
+
const separator = reference.indexOf(".");
|
|
8672
|
+
return separator === -1 ? reference : reference.slice(separator + 1);
|
|
8673
|
+
}
|
|
8674
|
+
function unifyBlockGroupedReferences(parts) {
|
|
8675
|
+
if (parts.joins.length > 0 || parts.base.table === DUAL_TABLE)
|
|
8676
|
+
return parts;
|
|
8677
|
+
const alias = parts.base.alias;
|
|
8678
|
+
return unifyGroupedReferences(parts, (reference) => {
|
|
8679
|
+
const separator = reference.indexOf(".");
|
|
8680
|
+
if (separator === -1)
|
|
8681
|
+
return `${alias}.${reference}`;
|
|
8682
|
+
return reference.slice(0, separator) === alias ? reference : void 0;
|
|
8683
|
+
});
|
|
8684
|
+
}
|
|
8685
|
+
function unifyPlanGroupedReferences(plan, columnsOf) {
|
|
8686
|
+
if (plan.joins.length === 0 || plan.groupBy.length === 0)
|
|
8687
|
+
return plan;
|
|
8688
|
+
const sources = [plan.base, ...plan.joins];
|
|
8689
|
+
const aliases = new Set(sources.map((source) => source.alias));
|
|
8690
|
+
let columnsBySource;
|
|
8691
|
+
return unifyGroupedReferences(plan, (reference) => {
|
|
8692
|
+
const separator = reference.indexOf(".");
|
|
8693
|
+
if (separator !== -1)
|
|
8694
|
+
return aliases.has(reference.slice(0, separator)) ? reference : void 0;
|
|
8695
|
+
columnsBySource ??= new Map(sources.map((source) => [
|
|
8696
|
+
source.alias,
|
|
8697
|
+
new Set(sourceWildcardColumns(source, columnsOf) ?? [])
|
|
8698
|
+
]));
|
|
8699
|
+
let owner;
|
|
8700
|
+
for (const [alias, columns] of columnsBySource) {
|
|
8701
|
+
if (!columns.has(reference))
|
|
8702
|
+
continue;
|
|
8703
|
+
if (owner !== void 0)
|
|
8704
|
+
return void 0;
|
|
8705
|
+
owner = alias;
|
|
8706
|
+
}
|
|
8707
|
+
return owner === void 0 ? void 0 : `${owner}.${reference}`;
|
|
8708
|
+
});
|
|
8709
|
+
}
|
|
7502
8710
|
function desugarFullJoin(parts, nextSequence) {
|
|
7503
8711
|
const join = parts.joins[0];
|
|
7504
8712
|
if (parts.joins.length !== 1 || join?.full !== true) {
|
|
@@ -7599,6 +8807,7 @@ function assembleSelectBlock(parts, nextSequence) {
|
|
|
7599
8807
|
groupingSets: parts.groupingSets.map((set) => resolveGroupByReferences(set, parts.select))
|
|
7600
8808
|
}
|
|
7601
8809
|
};
|
|
8810
|
+
parts = unifyBlockGroupedReferences(parts);
|
|
7602
8811
|
if (parts.joins.some((join) => join.full === true)) {
|
|
7603
8812
|
return desugarFullJoin(parts, nextSequence);
|
|
7604
8813
|
}
|
|
@@ -7654,18 +8863,6 @@ function assembleSelectBlock(parts, nextSequence) {
|
|
|
7654
8863
|
const grouped = parts.groupBy.length > 0 || select.some((item) => hasAggregate(item.expression));
|
|
7655
8864
|
if (!grouped)
|
|
7656
8865
|
throw new TypeError("HAVING requires GROUP BY or aggregate functions");
|
|
7657
|
-
const groupExpressions = new Set(groupBy.map((expression) => JSON.stringify(expression)));
|
|
7658
|
-
for (const predicate of having) {
|
|
7659
|
-
for (const side of [predicate.left, predicate.right]) {
|
|
7660
|
-
if (hasAggregate(side))
|
|
7661
|
-
continue;
|
|
7662
|
-
if (expressionColumns(side).length === 0)
|
|
7663
|
-
continue;
|
|
7664
|
-
if (groupExpressions.has(JSON.stringify(side)))
|
|
7665
|
-
continue;
|
|
7666
|
-
throw new TypeError("HAVING conditions must use aggregates, literals, or GROUP BY expressions");
|
|
7667
|
-
}
|
|
7668
|
-
}
|
|
7669
8866
|
}
|
|
7670
8867
|
const clauseExpressions = [
|
|
7671
8868
|
...predicates.flatMap((predicate) => [predicate.left, predicate.right]),
|
|
@@ -8271,8 +9468,11 @@ function assembleOrderByExpressionBlock(parts, nextSequence) {
|
|
|
8271
9468
|
orderBy: []
|
|
8272
9469
|
};
|
|
8273
9470
|
}
|
|
9471
|
+
function blockHasRowWindow(block) {
|
|
9472
|
+
return block.limit !== void 0 || block.offset !== void 0 || block.limitParameter !== void 0 || block.offsetParameter !== void 0;
|
|
9473
|
+
}
|
|
8274
9474
|
function transparentProjectionSource(plan) {
|
|
8275
|
-
if (plan.joins.length > 0 || plan.predicates.length > 0 || plan.groupBy.length > 0 || plan.having.length > 0 || plan.orderBy.length > 0 || plan
|
|
9475
|
+
if (plan.joins.length > 0 || plan.predicates.length > 0 || plan.groupBy.length > 0 || plan.having.length > 0 || plan.orderBy.length > 0 || blockHasRowWindow(plan)) {
|
|
8276
9476
|
return void 0;
|
|
8277
9477
|
}
|
|
8278
9478
|
const inner = plan.base.derived;
|
|
@@ -8568,6 +9768,59 @@ function tokenize(sql) {
|
|
|
8568
9768
|
index += 1;
|
|
8569
9769
|
continue;
|
|
8570
9770
|
}
|
|
9771
|
+
if ((character === "E" || character === "e") && sql[index + 1] === "'") {
|
|
9772
|
+
const start = index;
|
|
9773
|
+
index += 2;
|
|
9774
|
+
let value = "";
|
|
9775
|
+
let closed = false;
|
|
9776
|
+
while (index < sql.length) {
|
|
9777
|
+
const current = sql[index] ?? "";
|
|
9778
|
+
if (current === "'" && sql[index + 1] === "'") {
|
|
9779
|
+
value += "'";
|
|
9780
|
+
index += 2;
|
|
9781
|
+
} else if (current === "'") {
|
|
9782
|
+
index += 1;
|
|
9783
|
+
closed = true;
|
|
9784
|
+
break;
|
|
9785
|
+
} else if (current === "\\") {
|
|
9786
|
+
const next = sql[index + 1] ?? "";
|
|
9787
|
+
const simple = /* @__PURE__ */ new Map([
|
|
9788
|
+
["n", "\n"],
|
|
9789
|
+
["t", " "],
|
|
9790
|
+
["r", "\r"],
|
|
9791
|
+
["b", "\b"],
|
|
9792
|
+
["f", "\f"]
|
|
9793
|
+
]);
|
|
9794
|
+
const escaped = simple.get(next);
|
|
9795
|
+
if (escaped !== void 0) {
|
|
9796
|
+
value += escaped;
|
|
9797
|
+
index += 2;
|
|
9798
|
+
} else if (next === "x" && /[0-9A-Fa-f]/.test(sql[index + 2] ?? "")) {
|
|
9799
|
+
const hex = /^[0-9A-Fa-f]{1,2}/.exec(sql.slice(index + 2, index + 4))?.[0] ?? "";
|
|
9800
|
+
value += String.fromCharCode(Number.parseInt(hex, 16));
|
|
9801
|
+
index += 2 + hex.length;
|
|
9802
|
+
} else if (next === "u" && /^[0-9A-Fa-f]{4}/.test(sql.slice(index + 2, index + 6))) {
|
|
9803
|
+
value += String.fromCharCode(Number.parseInt(sql.slice(index + 2, index + 6), 16));
|
|
9804
|
+
index += 6;
|
|
9805
|
+
} else if (/[0-7]/.test(next)) {
|
|
9806
|
+
const octal = /^[0-7]{1,3}/.exec(sql.slice(index + 1, index + 4))?.[0] ?? "";
|
|
9807
|
+
value += String.fromCharCode(Number.parseInt(octal, 8));
|
|
9808
|
+
index += 1 + octal.length;
|
|
9809
|
+
} else {
|
|
9810
|
+
value += next;
|
|
9811
|
+
index += 2;
|
|
9812
|
+
}
|
|
9813
|
+
} else {
|
|
9814
|
+
value += current;
|
|
9815
|
+
index += 1;
|
|
9816
|
+
}
|
|
9817
|
+
}
|
|
9818
|
+
if (!closed) {
|
|
9819
|
+
throw new SqlCompileError("Unterminated string literal", start, sql.length - start);
|
|
9820
|
+
}
|
|
9821
|
+
push({ kind: "string", text: value, start, end: index });
|
|
9822
|
+
continue;
|
|
9823
|
+
}
|
|
8571
9824
|
if (/[A-Za-z_]/.test(character)) {
|
|
8572
9825
|
const start = index++;
|
|
8573
9826
|
while (index < sql.length && /[A-Za-z0-9_]/.test(sql[index] ?? ""))
|
|
@@ -8575,7 +9828,7 @@ function tokenize(sql) {
|
|
|
8575
9828
|
push({ kind: "identifier", text: sql.slice(start, index), start, end: index });
|
|
8576
9829
|
continue;
|
|
8577
9830
|
}
|
|
8578
|
-
if (/\d/.test(character)) {
|
|
9831
|
+
if (/\d/.test(character) || character === "." && /\d/.test(sql[index + 1] ?? "")) {
|
|
8579
9832
|
const start = index;
|
|
8580
9833
|
const radix = { x: 16, o: 8, b: 2 }[(sql[index + 1] ?? "").toLowerCase()];
|
|
8581
9834
|
if (character === "0" && radix !== void 0) {
|
|
@@ -8601,9 +9854,10 @@ function tokenize(sql) {
|
|
|
8601
9854
|
index += 1;
|
|
8602
9855
|
}
|
|
8603
9856
|
}
|
|
8604
|
-
const
|
|
9857
|
+
const spelled = sql.slice(start, index);
|
|
9858
|
+
const text = spelled.startsWith(".") ? `0${spelled}` : /^\d+\.(?:[eE][+-]?\d+)?$/.test(spelled) ? spelled.replace(".", ".0") : spelled;
|
|
8605
9859
|
if (!validNumericLiteral(text, 10))
|
|
8606
|
-
throw new SqlCompileError(`Invalid number: ${
|
|
9860
|
+
throw new SqlCompileError(`Invalid number: ${spelled}`, start, index - start);
|
|
8607
9861
|
push({ kind: "number", text: text.replaceAll("_", ""), start, end: index });
|
|
8608
9862
|
continue;
|
|
8609
9863
|
}
|
|
@@ -8658,6 +9912,18 @@ function tokenize(sql) {
|
|
|
8658
9912
|
}
|
|
8659
9913
|
if (character === "$") {
|
|
8660
9914
|
const start = index++;
|
|
9915
|
+
const tagMatch = /^\$([A-Za-z_][A-Za-z0-9_]*)?\$/.exec(sql.slice(start));
|
|
9916
|
+
if (tagMatch !== null) {
|
|
9917
|
+
const tag = tagMatch[0];
|
|
9918
|
+
const bodyStart = start + tag.length;
|
|
9919
|
+
const close = sql.indexOf(tag, bodyStart);
|
|
9920
|
+
if (close === -1) {
|
|
9921
|
+
throw new SqlCompileError("Unterminated dollar-quoted string", start, sql.length - start);
|
|
9922
|
+
}
|
|
9923
|
+
push({ kind: "string", text: sql.slice(bodyStart, close), start, end: close + tag.length });
|
|
9924
|
+
index = close + tag.length;
|
|
9925
|
+
continue;
|
|
9926
|
+
}
|
|
8661
9927
|
while (index < sql.length && /\d/.test(sql[index] ?? ""))
|
|
8662
9928
|
index += 1;
|
|
8663
9929
|
const digits = sql.slice(start + 1, index);
|
|
@@ -8691,6 +9957,17 @@ function tokenize(sql) {
|
|
|
8691
9957
|
index += 3;
|
|
8692
9958
|
continue;
|
|
8693
9959
|
}
|
|
9960
|
+
const likeOperator = /^!?~~\*?/.exec(sql.slice(index, index + 4))?.[0];
|
|
9961
|
+
if (likeOperator !== void 0) {
|
|
9962
|
+
push({
|
|
9963
|
+
kind: "operator",
|
|
9964
|
+
text: likeOperator,
|
|
9965
|
+
start: index,
|
|
9966
|
+
end: index + likeOperator.length
|
|
9967
|
+
});
|
|
9968
|
+
index += likeOperator.length;
|
|
9969
|
+
continue;
|
|
9970
|
+
}
|
|
8694
9971
|
if ([">=", "<=", "!=", "<>", "||", "->"].includes(pair)) {
|
|
8695
9972
|
push({ kind: "operator", text: pair, start: index, end: index + 2 });
|
|
8696
9973
|
index += 2;
|
|
@@ -8740,11 +10017,14 @@ function validateSqlSource(sql) {
|
|
|
8740
10017
|
export {
|
|
8741
10018
|
DUAL_TABLE,
|
|
8742
10019
|
annotateAvgArgumentScales,
|
|
10020
|
+
annotateIntegerDivision,
|
|
10021
|
+
annotatePlanIntegerDivision,
|
|
8743
10022
|
applyWindowFunctions,
|
|
8744
10023
|
assembleSelectBlock,
|
|
8745
10024
|
bindPendingSelectShapes,
|
|
8746
10025
|
bindPlanParameters,
|
|
8747
10026
|
bindStatementParameters,
|
|
10027
|
+
blockHasRowWindow,
|
|
8748
10028
|
blockHasSubqueries,
|
|
8749
10029
|
cachedListMembership,
|
|
8750
10030
|
childExpressions,
|
|
@@ -8771,17 +10051,22 @@ export {
|
|
|
8771
10051
|
executeRowQuery,
|
|
8772
10052
|
expandFtsColumns,
|
|
8773
10053
|
expandNaturalJoins,
|
|
10054
|
+
expandRowReferences,
|
|
8774
10055
|
expandSourceColumnAliases,
|
|
8775
10056
|
expandViewSources,
|
|
8776
10057
|
expressionAliases,
|
|
8777
10058
|
expressionColumnNames,
|
|
8778
10059
|
expressionColumns,
|
|
10060
|
+
extendGroupByWithKeyDependents,
|
|
8779
10061
|
externalizeQueryResult,
|
|
10062
|
+
foldIdentifierCase,
|
|
8780
10063
|
forEachBlockExpression,
|
|
8781
10064
|
forEachNestedBlock,
|
|
8782
10065
|
hasAggregate,
|
|
8783
10066
|
inferBlockSchema,
|
|
8784
10067
|
inferResultColumnDomains,
|
|
10068
|
+
integerQuotient,
|
|
10069
|
+
integerTypedExpression,
|
|
8785
10070
|
isAggregateCall,
|
|
8786
10071
|
isDefaultInsertValue,
|
|
8787
10072
|
isDeferredInsertExpression,
|
|
@@ -8796,6 +10081,7 @@ export {
|
|
|
8796
10081
|
planHasNaturalJoins,
|
|
8797
10082
|
planHasPendingSelectShapes,
|
|
8798
10083
|
planHasSourceColumnAliases,
|
|
10084
|
+
planMayHaveIntegerDivision,
|
|
8799
10085
|
planReadsBeyondSingleScan,
|
|
8800
10086
|
planReadsTable,
|
|
8801
10087
|
planReadsViews,
|
|
@@ -8812,6 +10098,7 @@ export {
|
|
|
8812
10098
|
subqueryResolutionSteps,
|
|
8813
10099
|
topLevelFtsMatchConjuncts,
|
|
8814
10100
|
transparentProjectionSource,
|
|
10101
|
+
unifyPlanGroupedReferences,
|
|
8815
10102
|
unknownColumnDomains,
|
|
8816
10103
|
validateDefaultExpression,
|
|
8817
10104
|
validateLimit,
|