@mindstudio-ai/agent 0.1.162 → 0.1.163
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/cli.js +1139 -1278
- package/dist/index.d.ts +91 -67
- package/dist/index.js +919 -723
- package/dist/index.js.map +1 -1
- package/dist/postinstall.js +1138 -1277
- package/package.json +7 -2
package/dist/index.js
CHANGED
|
@@ -572,36 +572,35 @@ function serializeParam(val) {
|
|
|
572
572
|
return JSON.stringify(val);
|
|
573
573
|
}
|
|
574
574
|
function serializeColumnParam(val, columnName, columns) {
|
|
575
|
-
const col = columns.
|
|
575
|
+
const col = columnsByName(columns).get(columnName);
|
|
576
576
|
if (col?.type === "user" && typeof val === "string") {
|
|
577
577
|
return val.startsWith(USER_PREFIX) ? val : `${USER_PREFIX}${val}`;
|
|
578
578
|
}
|
|
579
579
|
return serializeParam(val);
|
|
580
580
|
}
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
if (
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
581
|
+
var USER_PREFIX = "@@user@@";
|
|
582
|
+
var byNameCache = /* @__PURE__ */ new WeakMap();
|
|
583
|
+
function columnsByName(columns) {
|
|
584
|
+
let byName = byNameCache.get(columns);
|
|
585
|
+
if (!byName) {
|
|
586
|
+
byName = /* @__PURE__ */ new Map();
|
|
587
|
+
for (const column of columns) {
|
|
588
|
+
if (!byName.has(column.name)) {
|
|
589
|
+
byName.set(column.name, column);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
byNameCache.set(columns, byName);
|
|
593
593
|
}
|
|
594
|
-
|
|
595
|
-
return `'${json.replace(/'/g, "''")}'`;
|
|
594
|
+
return byName;
|
|
596
595
|
}
|
|
597
|
-
var USER_PREFIX = "@@user@@";
|
|
598
596
|
function deserializeRow(row, columns) {
|
|
599
597
|
if (row == null) {
|
|
600
598
|
return row;
|
|
601
599
|
}
|
|
602
600
|
const result = {};
|
|
601
|
+
const byName = columnsByName(columns);
|
|
603
602
|
for (const [key, value] of Object.entries(row)) {
|
|
604
|
-
const col =
|
|
603
|
+
const col = byName.get(key);
|
|
605
604
|
if (typeof value === "string" && value.startsWith(USER_PREFIX)) {
|
|
606
605
|
result[key] = value.slice(USER_PREFIX.length);
|
|
607
606
|
} else if (col?.type === "json" && typeof value === "string") {
|
|
@@ -627,8 +626,50 @@ function deserializeRow(row, columns) {
|
|
|
627
626
|
}
|
|
628
627
|
return result;
|
|
629
628
|
}
|
|
629
|
+
function storageClass(value) {
|
|
630
|
+
if (value === null || value === void 0) {
|
|
631
|
+
return 0;
|
|
632
|
+
}
|
|
633
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
634
|
+
return 1;
|
|
635
|
+
}
|
|
636
|
+
if (value instanceof Uint8Array) {
|
|
637
|
+
return 3;
|
|
638
|
+
}
|
|
639
|
+
return 2;
|
|
640
|
+
}
|
|
641
|
+
function compareAsSqlite(a, b) {
|
|
642
|
+
const classA = storageClass(a);
|
|
643
|
+
const classB = storageClass(b);
|
|
644
|
+
if (classA !== classB) {
|
|
645
|
+
return classA - classB;
|
|
646
|
+
}
|
|
647
|
+
if (classA === 1) {
|
|
648
|
+
return Number(a) - Number(b);
|
|
649
|
+
}
|
|
650
|
+
if (classA === 2) {
|
|
651
|
+
const textA = typeof a === "string" ? a : JSON.stringify(a);
|
|
652
|
+
const textB = typeof b === "string" ? b : JSON.stringify(b);
|
|
653
|
+
return textA < textB ? -1 : textA > textB ? 1 : 0;
|
|
654
|
+
}
|
|
655
|
+
if (classA === 3) {
|
|
656
|
+
const bytesA = a;
|
|
657
|
+
const bytesB = b;
|
|
658
|
+
for (let i = 0; i < Math.min(bytesA.length, bytesB.length); i++) {
|
|
659
|
+
if (bytesA[i] !== bytesB[i]) {
|
|
660
|
+
return bytesA[i] - bytesB[i];
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
return bytesA.length - bytesB.length;
|
|
664
|
+
}
|
|
665
|
+
return 0;
|
|
666
|
+
}
|
|
630
667
|
function buildSelect(table, options = {}) {
|
|
631
|
-
|
|
668
|
+
for (const column of options.columns ?? []) {
|
|
669
|
+
assertSqlIdentifier(column, "selected column");
|
|
670
|
+
}
|
|
671
|
+
const selected = options.columns?.length ? options.columns.join(", ") : "*";
|
|
672
|
+
let sql = `SELECT ${selected} FROM ${table}`;
|
|
632
673
|
const params = [];
|
|
633
674
|
if (options.where) {
|
|
634
675
|
sql += ` WHERE ${options.where}`;
|
|
@@ -639,11 +680,13 @@ function buildSelect(table, options = {}) {
|
|
|
639
680
|
if (options.orderBy) {
|
|
640
681
|
sql += ` ORDER BY ${options.orderBy}${options.desc ? " DESC" : " ASC"}`;
|
|
641
682
|
}
|
|
642
|
-
if (options.limit != null) {
|
|
643
|
-
sql +=
|
|
683
|
+
if (options.limit != null || options.offset != null) {
|
|
684
|
+
sql += " LIMIT ?";
|
|
685
|
+
params.push(options.limit ?? -1);
|
|
644
686
|
}
|
|
645
687
|
if (options.offset != null) {
|
|
646
|
-
sql +=
|
|
688
|
+
sql += " OFFSET ?";
|
|
689
|
+
params.push(options.offset);
|
|
647
690
|
}
|
|
648
691
|
return { sql, params: params.length > 0 ? params : void 0 };
|
|
649
692
|
}
|
|
@@ -712,13 +755,12 @@ function buildGroupedAggregate(table, options) {
|
|
|
712
755
|
if (options.orderBy) {
|
|
713
756
|
sql += ` ORDER BY ${options.orderBy}${options.desc ? " DESC" : " ASC"}`;
|
|
714
757
|
}
|
|
758
|
+
const params = [...options.whereParams ?? []];
|
|
715
759
|
if (options.limit != null) {
|
|
716
|
-
sql +=
|
|
760
|
+
sql += " LIMIT ?";
|
|
761
|
+
params.push(options.limit);
|
|
717
762
|
}
|
|
718
|
-
return {
|
|
719
|
-
sql,
|
|
720
|
-
params: options.whereParams?.length ? options.whereParams : void 0
|
|
721
|
-
};
|
|
763
|
+
return { sql, params: params.length > 0 ? params : void 0 };
|
|
722
764
|
}
|
|
723
765
|
function buildExists(table, where, whereParams, negate) {
|
|
724
766
|
const inner = where ? `SELECT 1 FROM ${table} WHERE ${where}` : `SELECT 1 FROM ${table}`;
|
|
@@ -788,6 +830,16 @@ function stripSystemColumns(data) {
|
|
|
788
830
|
return result;
|
|
789
831
|
}
|
|
790
832
|
|
|
833
|
+
// src/db/time.ts
|
|
834
|
+
var timeHelpers = {
|
|
835
|
+
now: () => Date.now(),
|
|
836
|
+
days: (n) => n * 864e5,
|
|
837
|
+
hours: (n) => n * 36e5,
|
|
838
|
+
minutes: (n) => n * 6e4,
|
|
839
|
+
ago: (ms) => Date.now() - ms,
|
|
840
|
+
fromNow: (ms) => Date.now() + ms
|
|
841
|
+
};
|
|
842
|
+
|
|
791
843
|
// src/db/predicate.ts
|
|
792
844
|
function compilePredicate(fn, bindings, columns = []) {
|
|
793
845
|
try {
|
|
@@ -802,9 +854,9 @@ function compilePredicate(fn, bindings, columns = []) {
|
|
|
802
854
|
}
|
|
803
855
|
const tokens = tokenize(body);
|
|
804
856
|
if (tokens.length === 0) {
|
|
805
|
-
return { type: "js", fn, reason: "
|
|
857
|
+
return { type: "js", fn, reason: "unsupported syntax" };
|
|
806
858
|
}
|
|
807
|
-
const parser = new Parser(tokens, names.row, names.bindings, bindings
|
|
859
|
+
const parser = new Parser(tokens, names.row, names.bindings, bindings);
|
|
808
860
|
const ast = parser.parseExpression();
|
|
809
861
|
if (!ast) {
|
|
810
862
|
return { type: "js", fn, reason: "could not parse expression" };
|
|
@@ -812,11 +864,12 @@ function compilePredicate(fn, bindings, columns = []) {
|
|
|
812
864
|
if (parser.pos < tokens.length) {
|
|
813
865
|
return { type: "js", fn, reason: "unexpected tokens after expression" };
|
|
814
866
|
}
|
|
815
|
-
const
|
|
867
|
+
const params = [];
|
|
868
|
+
const where = compileNode(ast, false, columns, params);
|
|
816
869
|
if (!where) {
|
|
817
870
|
return { type: "js", fn, reason: "could not compile to SQL" };
|
|
818
871
|
}
|
|
819
|
-
return { type: "sql", where };
|
|
872
|
+
return { type: "sql", where, params };
|
|
820
873
|
} catch (err) {
|
|
821
874
|
return {
|
|
822
875
|
type: "js",
|
|
@@ -896,146 +949,221 @@ function extractBody(source) {
|
|
|
896
949
|
}
|
|
897
950
|
return body.trim() || null;
|
|
898
951
|
}
|
|
952
|
+
var OPERATORS = [
|
|
953
|
+
"===",
|
|
954
|
+
"!==",
|
|
955
|
+
"==",
|
|
956
|
+
"!=",
|
|
957
|
+
"<=",
|
|
958
|
+
">=",
|
|
959
|
+
"&&",
|
|
960
|
+
"||",
|
|
961
|
+
"!",
|
|
962
|
+
"<",
|
|
963
|
+
">",
|
|
964
|
+
"+",
|
|
965
|
+
"-",
|
|
966
|
+
"*",
|
|
967
|
+
"/",
|
|
968
|
+
"%"
|
|
969
|
+
];
|
|
970
|
+
var PUNCTUATION = {
|
|
971
|
+
".": "dot",
|
|
972
|
+
"(": "lparen",
|
|
973
|
+
")": "rparen",
|
|
974
|
+
"[": "lbracket",
|
|
975
|
+
"]": "rbracket",
|
|
976
|
+
",": "comma"
|
|
977
|
+
};
|
|
978
|
+
var Unreadable = class extends Error {
|
|
979
|
+
};
|
|
899
980
|
function tokenize(expr) {
|
|
900
981
|
const tokens = [];
|
|
901
982
|
let i = 0;
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
continue;
|
|
907
|
-
}
|
|
908
|
-
if (ch === "'" || ch === '"') {
|
|
909
|
-
const quote = ch;
|
|
910
|
-
let str = "";
|
|
911
|
-
i++;
|
|
912
|
-
while (i < expr.length && expr[i] !== quote) {
|
|
913
|
-
if (expr[i] === "\\") {
|
|
914
|
-
i++;
|
|
915
|
-
if (i < expr.length) {
|
|
916
|
-
str += expr[i];
|
|
917
|
-
}
|
|
918
|
-
} else {
|
|
919
|
-
str += expr[i];
|
|
920
|
-
}
|
|
983
|
+
try {
|
|
984
|
+
while (i < expr.length) {
|
|
985
|
+
const ch = expr[i];
|
|
986
|
+
if (/\s/.test(ch)) {
|
|
921
987
|
i++;
|
|
988
|
+
continue;
|
|
922
989
|
}
|
|
923
|
-
if (
|
|
924
|
-
|
|
990
|
+
if (ch === "'" || ch === '"' || ch === "`") {
|
|
991
|
+
const read = readString(expr, i);
|
|
992
|
+
tokens.push({ type: "string", value: read.value });
|
|
993
|
+
i = read.end;
|
|
994
|
+
continue;
|
|
925
995
|
}
|
|
926
|
-
i
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
996
|
+
if (/[0-9]/.test(ch) || ch === "." && /[0-9]/.test(expr[i + 1] ?? "")) {
|
|
997
|
+
const read = readNumber(expr, i);
|
|
998
|
+
tokens.push({ type: "number", value: read.text, number: read.value });
|
|
999
|
+
i = read.end;
|
|
1000
|
+
continue;
|
|
1001
|
+
}
|
|
1002
|
+
const op = OPERATORS.find((o) => expr.startsWith(o, i));
|
|
1003
|
+
if (op) {
|
|
1004
|
+
tokens.push({ type: "operator", value: op });
|
|
1005
|
+
i += op.length;
|
|
1006
|
+
continue;
|
|
1007
|
+
}
|
|
1008
|
+
if (PUNCTUATION[ch]) {
|
|
1009
|
+
tokens.push({ type: PUNCTUATION[ch], value: ch });
|
|
938
1010
|
i++;
|
|
1011
|
+
continue;
|
|
939
1012
|
}
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
continue;
|
|
952
|
-
}
|
|
953
|
-
if (ch === "!" || ch === "<" || ch === ">") {
|
|
954
|
-
tokens.push({ type: "operator", value: ch });
|
|
955
|
-
i++;
|
|
956
|
-
continue;
|
|
957
|
-
}
|
|
958
|
-
if (ch === ".") {
|
|
959
|
-
tokens.push({ type: "dot", value: "." });
|
|
960
|
-
i++;
|
|
961
|
-
continue;
|
|
962
|
-
}
|
|
963
|
-
if (ch === "(") {
|
|
964
|
-
tokens.push({ type: "lparen", value: "(" });
|
|
965
|
-
i++;
|
|
966
|
-
continue;
|
|
1013
|
+
if (/[a-zA-Z_$]/.test(ch)) {
|
|
1014
|
+
let ident = ch;
|
|
1015
|
+
i++;
|
|
1016
|
+
while (i < expr.length && /[a-zA-Z0-9_$]/.test(expr[i])) {
|
|
1017
|
+
ident += expr[i];
|
|
1018
|
+
i++;
|
|
1019
|
+
}
|
|
1020
|
+
tokens.push({ type: "identifier", value: ident });
|
|
1021
|
+
continue;
|
|
1022
|
+
}
|
|
1023
|
+
return [];
|
|
967
1024
|
}
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
continue;
|
|
1025
|
+
} catch (err) {
|
|
1026
|
+
if (err instanceof Unreadable) {
|
|
1027
|
+
return [];
|
|
972
1028
|
}
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
1029
|
+
throw err;
|
|
1030
|
+
}
|
|
1031
|
+
return tokens;
|
|
1032
|
+
}
|
|
1033
|
+
function readString(expr, start) {
|
|
1034
|
+
const quote = expr[start];
|
|
1035
|
+
let value = "";
|
|
1036
|
+
let i = start + 1;
|
|
1037
|
+
while (i < expr.length && expr[i] !== quote) {
|
|
1038
|
+
const ch = expr[i];
|
|
1039
|
+
if (quote === "`" && ch === "$" && expr[i + 1] === "{") {
|
|
1040
|
+
throw new Unreadable("template substitution");
|
|
977
1041
|
}
|
|
978
|
-
if (ch
|
|
979
|
-
|
|
1042
|
+
if (ch !== "\\") {
|
|
1043
|
+
value += ch;
|
|
980
1044
|
i++;
|
|
981
1045
|
continue;
|
|
982
1046
|
}
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1047
|
+
const escaped = readEscape(expr, i + 1);
|
|
1048
|
+
value += escaped.value;
|
|
1049
|
+
i = escaped.end;
|
|
1050
|
+
}
|
|
1051
|
+
if (i >= expr.length) {
|
|
1052
|
+
throw new Unreadable("unterminated string");
|
|
1053
|
+
}
|
|
1054
|
+
return { value, end: i + 1 };
|
|
1055
|
+
}
|
|
1056
|
+
var SINGLE_ESCAPES = {
|
|
1057
|
+
n: "\n",
|
|
1058
|
+
r: "\r",
|
|
1059
|
+
t: " ",
|
|
1060
|
+
b: "\b",
|
|
1061
|
+
f: "\f",
|
|
1062
|
+
v: "\v"
|
|
1063
|
+
};
|
|
1064
|
+
function readEscape(expr, start) {
|
|
1065
|
+
const ch = expr[start];
|
|
1066
|
+
if (ch === void 0) {
|
|
1067
|
+
throw new Unreadable("unterminated escape");
|
|
1068
|
+
}
|
|
1069
|
+
if (ch in SINGLE_ESCAPES) {
|
|
1070
|
+
return { value: SINGLE_ESCAPES[ch], end: start + 1 };
|
|
1071
|
+
}
|
|
1072
|
+
if (ch === "0" && !/[0-9]/.test(expr[start + 1] ?? "")) {
|
|
1073
|
+
return { value: "\0", end: start + 1 };
|
|
1074
|
+
}
|
|
1075
|
+
if (/[0-9]/.test(ch)) {
|
|
1076
|
+
throw new Unreadable("octal escape");
|
|
1077
|
+
}
|
|
1078
|
+
if (ch === "x") {
|
|
1079
|
+
const hex = expr.slice(start + 1, start + 3);
|
|
1080
|
+
if (!/^[0-9a-fA-F]{2}$/.test(hex)) {
|
|
1081
|
+
throw new Unreadable("bad \\x escape");
|
|
987
1082
|
}
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
1083
|
+
return { value: String.fromCharCode(parseInt(hex, 16)), end: start + 3 };
|
|
1084
|
+
}
|
|
1085
|
+
if (ch === "u") {
|
|
1086
|
+
if (expr[start + 1] === "{") {
|
|
1087
|
+
const close = expr.indexOf("}", start + 2);
|
|
1088
|
+
const hex2 = close === -1 ? "" : expr.slice(start + 2, close);
|
|
1089
|
+
const code = parseInt(hex2, 16);
|
|
1090
|
+
if (!/^[0-9a-fA-F]{1,6}$/.test(hex2) || code > 1114111) {
|
|
1091
|
+
throw new Unreadable("bad \\u{} escape");
|
|
994
1092
|
}
|
|
995
|
-
|
|
996
|
-
continue;
|
|
1093
|
+
return { value: String.fromCodePoint(code), end: close + 1 };
|
|
997
1094
|
}
|
|
998
|
-
|
|
1095
|
+
const hex = expr.slice(start + 1, start + 5);
|
|
1096
|
+
if (!/^[0-9a-fA-F]{4}$/.test(hex)) {
|
|
1097
|
+
throw new Unreadable("bad \\u escape");
|
|
1098
|
+
}
|
|
1099
|
+
return { value: String.fromCharCode(parseInt(hex, 16)), end: start + 5 };
|
|
999
1100
|
}
|
|
1000
|
-
|
|
1101
|
+
if (ch === "\r" && expr[start + 1] === "\n") {
|
|
1102
|
+
return { value: "", end: start + 2 };
|
|
1103
|
+
}
|
|
1104
|
+
if (ch === "\n" || ch === "\r" || ch === "\u2028" || ch === "\u2029") {
|
|
1105
|
+
return { value: "", end: start + 1 };
|
|
1106
|
+
}
|
|
1107
|
+
return { value: ch, end: start + 1 };
|
|
1108
|
+
}
|
|
1109
|
+
function readNumber(expr, start) {
|
|
1110
|
+
const match = /^0[xX][0-9a-fA-F_]+|^0[oO][0-7_]+|^0[bB][01_]+|^(?:[0-9][0-9_]*)?\.?[0-9_]*(?:[eE][+-]?[0-9_]+)?/.exec(
|
|
1111
|
+
expr.slice(start)
|
|
1112
|
+
);
|
|
1113
|
+
const text = match?.[0] ?? "";
|
|
1114
|
+
const end = start + text.length;
|
|
1115
|
+
if (!text || /[a-zA-Z0-9_$]/.test(expr[end] ?? "")) {
|
|
1116
|
+
throw new Unreadable("unsupported number");
|
|
1117
|
+
}
|
|
1118
|
+
const value = Number(text.replace(/_/g, ""));
|
|
1119
|
+
if (!Number.isFinite(value)) {
|
|
1120
|
+
throw new Unreadable("non-finite number");
|
|
1121
|
+
}
|
|
1122
|
+
return { text, value, end };
|
|
1001
1123
|
}
|
|
1124
|
+
var PARSE_FAILED = /* @__PURE__ */ Symbol("PARSE_FAILED");
|
|
1125
|
+
var COMPARISON_OPS = /* @__PURE__ */ new Set([
|
|
1126
|
+
"===",
|
|
1127
|
+
"!==",
|
|
1128
|
+
"==",
|
|
1129
|
+
"!=",
|
|
1130
|
+
"<",
|
|
1131
|
+
">",
|
|
1132
|
+
"<=",
|
|
1133
|
+
">="
|
|
1134
|
+
]);
|
|
1135
|
+
var FLIPPED = {
|
|
1136
|
+
"===": "===",
|
|
1137
|
+
"!==": "!==",
|
|
1138
|
+
"==": "==",
|
|
1139
|
+
"!=": "!=",
|
|
1140
|
+
"<": ">",
|
|
1141
|
+
">": "<",
|
|
1142
|
+
"<=": ">=",
|
|
1143
|
+
">=": "<="
|
|
1144
|
+
};
|
|
1002
1145
|
var Parser = class {
|
|
1003
|
-
constructor(tokens, paramName, bindingsName, bindingsValue
|
|
1146
|
+
constructor(tokens, paramName, bindingsName, bindingsValue) {
|
|
1004
1147
|
this.tokens = tokens;
|
|
1005
1148
|
this.paramName = paramName;
|
|
1006
1149
|
this.bindingsName = bindingsName;
|
|
1007
1150
|
this.bindingsValue = bindingsValue;
|
|
1008
|
-
this.originalFn = originalFn;
|
|
1009
1151
|
}
|
|
1010
1152
|
tokens;
|
|
1011
1153
|
paramName;
|
|
1012
1154
|
bindingsName;
|
|
1013
1155
|
bindingsValue;
|
|
1014
|
-
originalFn;
|
|
1015
1156
|
pos = 0;
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
return this.tokens[this.pos];
|
|
1157
|
+
peek(offset = 0) {
|
|
1158
|
+
return this.tokens[this.pos + offset];
|
|
1019
1159
|
}
|
|
1020
|
-
/** Consume the current token and advance. */
|
|
1021
1160
|
advance() {
|
|
1022
1161
|
return this.tokens[this.pos++];
|
|
1023
1162
|
}
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
if (!t) {
|
|
1028
|
-
return false;
|
|
1029
|
-
}
|
|
1030
|
-
if (t.type !== type) {
|
|
1031
|
-
return false;
|
|
1032
|
-
}
|
|
1033
|
-
if (value !== void 0 && t.value !== value) {
|
|
1034
|
-
return false;
|
|
1035
|
-
}
|
|
1036
|
-
return true;
|
|
1163
|
+
match(type, value, offset = 0) {
|
|
1164
|
+
const t = this.peek(offset);
|
|
1165
|
+
return !!t && t.type === type && (value === void 0 || t.value === value);
|
|
1037
1166
|
}
|
|
1038
|
-
/** Consume a token if it matches, otherwise return false. */
|
|
1039
1167
|
eat(type, value) {
|
|
1040
1168
|
if (this.match(type, value)) {
|
|
1041
1169
|
this.advance();
|
|
@@ -1044,18 +1172,15 @@ var Parser = class {
|
|
|
1044
1172
|
return false;
|
|
1045
1173
|
}
|
|
1046
1174
|
// --- Grammar rules ---
|
|
1047
|
-
/** Entry point: parse a full expression. */
|
|
1048
1175
|
parseExpression() {
|
|
1049
1176
|
return this.parseOr();
|
|
1050
1177
|
}
|
|
1051
|
-
/** or_expr → and_expr ( '||' and_expr )* */
|
|
1052
1178
|
parseOr() {
|
|
1053
1179
|
let left = this.parseAnd();
|
|
1054
1180
|
if (!left) {
|
|
1055
1181
|
return null;
|
|
1056
1182
|
}
|
|
1057
|
-
while (this.
|
|
1058
|
-
this.advance();
|
|
1183
|
+
while (this.eat("operator", "||")) {
|
|
1059
1184
|
const right = this.parseAnd();
|
|
1060
1185
|
if (!right) {
|
|
1061
1186
|
return null;
|
|
@@ -1064,14 +1189,12 @@ var Parser = class {
|
|
|
1064
1189
|
}
|
|
1065
1190
|
return left;
|
|
1066
1191
|
}
|
|
1067
|
-
/** and_expr → not_expr ( '&&' not_expr )* */
|
|
1068
1192
|
parseAnd() {
|
|
1069
1193
|
let left = this.parseNot();
|
|
1070
1194
|
if (!left) {
|
|
1071
1195
|
return null;
|
|
1072
1196
|
}
|
|
1073
|
-
while (this.
|
|
1074
|
-
this.advance();
|
|
1197
|
+
while (this.eat("operator", "&&")) {
|
|
1075
1198
|
const right = this.parseNot();
|
|
1076
1199
|
if (!right) {
|
|
1077
1200
|
return null;
|
|
@@ -1080,72 +1203,48 @@ var Parser = class {
|
|
|
1080
1203
|
}
|
|
1081
1204
|
return left;
|
|
1082
1205
|
}
|
|
1083
|
-
/** not_expr → '!' not_expr | primary */
|
|
1084
1206
|
parseNot() {
|
|
1085
|
-
if (this.
|
|
1086
|
-
this.
|
|
1087
|
-
|
|
1088
|
-
this.advance();
|
|
1089
|
-
const inner2 = this.parseExpression();
|
|
1090
|
-
if (!inner2) {
|
|
1091
|
-
return null;
|
|
1092
|
-
}
|
|
1093
|
-
if (!this.eat("rparen")) {
|
|
1094
|
-
return null;
|
|
1095
|
-
}
|
|
1096
|
-
return { kind: "not", operand: inner2 };
|
|
1097
|
-
}
|
|
1098
|
-
const inner = this.parsePrimary();
|
|
1099
|
-
if (!inner) {
|
|
1100
|
-
return null;
|
|
1101
|
-
}
|
|
1102
|
-
if (inner.kind === "booleanField") {
|
|
1103
|
-
return { ...inner, negated: !inner.negated };
|
|
1104
|
-
}
|
|
1105
|
-
return { kind: "not", operand: inner };
|
|
1207
|
+
if (this.eat("operator", "!")) {
|
|
1208
|
+
const operand = this.parseNot();
|
|
1209
|
+
return operand ? { kind: "not", operand } : null;
|
|
1106
1210
|
}
|
|
1107
1211
|
return this.parsePrimary();
|
|
1108
1212
|
}
|
|
1109
|
-
/**
|
|
1110
|
-
* primary → field_comparison | null_check | includes_expr | paren_expr | boolean_field
|
|
1111
|
-
*
|
|
1112
|
-
* This is the workhorse — handles the different patterns that can appear
|
|
1113
|
-
* as atomic expressions within a larger &&/|| combination.
|
|
1114
|
-
*/
|
|
1115
1213
|
parsePrimary() {
|
|
1116
|
-
if (this.
|
|
1117
|
-
this.advance();
|
|
1214
|
+
if (this.eat("lparen")) {
|
|
1118
1215
|
const inner = this.parseExpression();
|
|
1119
|
-
if (!inner) {
|
|
1120
|
-
return null;
|
|
1121
|
-
}
|
|
1122
|
-
if (!this.eat("rparen")) {
|
|
1216
|
+
if (!inner || !this.eat("rparen")) {
|
|
1123
1217
|
return null;
|
|
1124
1218
|
}
|
|
1125
1219
|
return inner;
|
|
1126
1220
|
}
|
|
1127
|
-
if (this.match("lbracket")) {
|
|
1128
|
-
return this.parseArrayIncludes();
|
|
1129
|
-
}
|
|
1130
1221
|
if (this.match("identifier", this.paramName)) {
|
|
1131
1222
|
return this.parseFieldExpression();
|
|
1132
1223
|
}
|
|
1133
|
-
|
|
1134
|
-
|
|
1224
|
+
const membership = this.tryParseMembership();
|
|
1225
|
+
if (membership) {
|
|
1226
|
+
return membership;
|
|
1135
1227
|
}
|
|
1136
|
-
|
|
1137
|
-
|
|
1228
|
+
const start = this.pos;
|
|
1229
|
+
const value = this.parseValue();
|
|
1230
|
+
if (value !== PARSE_FAILED && this.match("operator")) {
|
|
1231
|
+
const op = this.peek().value;
|
|
1232
|
+
if (COMPARISON_OPS.has(op) && this.match("identifier", this.paramName, 1)) {
|
|
1233
|
+
this.advance();
|
|
1234
|
+
this.advance();
|
|
1235
|
+
const field = this.parseFieldPath();
|
|
1236
|
+
if (field && !this.lookAheadForIncludes()) {
|
|
1237
|
+
return comparisonNode(field, FLIPPED[op], value);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1138
1240
|
}
|
|
1241
|
+
this.pos = start;
|
|
1139
1242
|
return null;
|
|
1140
1243
|
}
|
|
1141
1244
|
/**
|
|
1142
|
-
*
|
|
1143
|
-
*
|
|
1144
|
-
*
|
|
1145
|
-
* - `o.field === value` (comparison)
|
|
1146
|
-
* - `o.field != null` (null check)
|
|
1147
|
-
* - `o.field.includes('text')` (LIKE)
|
|
1148
|
-
* - `o.field` alone (boolean field check)
|
|
1245
|
+
* An expression starting with the row parameter: a comparison
|
|
1246
|
+
* (`o.field === value`), `o.field.includes(x)`, or the field alone, read
|
|
1247
|
+
* for its truthiness.
|
|
1149
1248
|
*/
|
|
1150
1249
|
parseFieldExpression() {
|
|
1151
1250
|
this.advance();
|
|
@@ -1153,236 +1252,185 @@ var Parser = class {
|
|
|
1153
1252
|
if (!field) {
|
|
1154
1253
|
return null;
|
|
1155
1254
|
}
|
|
1156
|
-
|
|
1157
|
-
if (next?.type === "dot" && this.lookAheadForIncludes()) {
|
|
1255
|
+
if (this.lookAheadForIncludes()) {
|
|
1158
1256
|
return this.parseFieldIncludes(field);
|
|
1159
1257
|
}
|
|
1160
|
-
|
|
1161
|
-
|
|
1258
|
+
const next = this.peek();
|
|
1259
|
+
if (next?.type === "operator" && COMPARISON_OPS.has(next.value)) {
|
|
1260
|
+
this.advance();
|
|
1261
|
+
const value = this.parseValue();
|
|
1262
|
+
if (value === PARSE_FAILED) {
|
|
1263
|
+
return null;
|
|
1264
|
+
}
|
|
1265
|
+
return comparisonNode(field, next.value, value);
|
|
1162
1266
|
}
|
|
1163
|
-
return { kind: "
|
|
1267
|
+
return { kind: "truthy", field };
|
|
1164
1268
|
}
|
|
1165
1269
|
/**
|
|
1166
|
-
*
|
|
1167
|
-
*
|
|
1270
|
+
* The field path after the row parameter: `.status` → the column,
|
|
1271
|
+
* `.address.city` → `json_extract(address, '$.city')`. Stops before a
|
|
1272
|
+
* method call. `.length` isn't a field (it's a string's or array's length,
|
|
1273
|
+
* which `json_extract` can't read), so a path holding it fails.
|
|
1168
1274
|
*/
|
|
1169
1275
|
parseFieldPath() {
|
|
1170
|
-
if (!this.eat("dot")) {
|
|
1171
|
-
return null;
|
|
1172
|
-
}
|
|
1173
|
-
if (!this.match("identifier")) {
|
|
1276
|
+
if (!this.eat("dot") || !this.match("identifier")) {
|
|
1174
1277
|
return null;
|
|
1175
1278
|
}
|
|
1176
1279
|
const parts = [this.advance().value];
|
|
1177
|
-
while (this.match("dot") && this.
|
|
1178
|
-
if (this.
|
|
1280
|
+
while (this.match("dot") && this.match("identifier", void 0, 1)) {
|
|
1281
|
+
if (this.match("lparen", void 0, 2)) {
|
|
1179
1282
|
break;
|
|
1180
1283
|
}
|
|
1181
1284
|
this.advance();
|
|
1182
1285
|
parts.push(this.advance().value);
|
|
1183
1286
|
}
|
|
1184
|
-
if (parts.
|
|
1185
|
-
return parts[0];
|
|
1186
|
-
}
|
|
1187
|
-
const root = parts[0];
|
|
1188
|
-
const jsonPath = "$." + parts.slice(1).join(".");
|
|
1189
|
-
return `json_extract(${root}, '${jsonPath}')`;
|
|
1190
|
-
}
|
|
1191
|
-
/**
|
|
1192
|
-
* Parse a comparison: `field OP value`.
|
|
1193
|
-
* The field has already been parsed; we need the operator and right-hand value.
|
|
1194
|
-
*/
|
|
1195
|
-
parseComparison(field) {
|
|
1196
|
-
const opToken = this.advance();
|
|
1197
|
-
const jsOp = opToken.value;
|
|
1198
|
-
const value = this.parseValue();
|
|
1199
|
-
if (value === PARSE_FAILED) {
|
|
1200
|
-
return null;
|
|
1201
|
-
}
|
|
1202
|
-
if (value === null || value === void 0) {
|
|
1203
|
-
if (jsOp === "===" || jsOp === "==") {
|
|
1204
|
-
return { kind: "nullCheck", field, isNull: true };
|
|
1205
|
-
}
|
|
1206
|
-
if (jsOp === "!==" || jsOp === "!=") {
|
|
1207
|
-
return { kind: "nullCheck", field, isNull: false };
|
|
1208
|
-
}
|
|
1287
|
+
if (parts.slice(1).includes("length")) {
|
|
1209
1288
|
return null;
|
|
1210
1289
|
}
|
|
1211
|
-
const
|
|
1212
|
-
if (
|
|
1213
|
-
return
|
|
1290
|
+
const column = parts[0];
|
|
1291
|
+
if (parts.length === 1) {
|
|
1292
|
+
return { sql: column, column, nested: false };
|
|
1214
1293
|
}
|
|
1215
|
-
|
|
1294
|
+
const path = `$.${parts.slice(1).join(".")}`;
|
|
1295
|
+
return {
|
|
1296
|
+
sql: `json_extract(${column}, '${path}')`,
|
|
1297
|
+
column,
|
|
1298
|
+
nested: true,
|
|
1299
|
+
path
|
|
1300
|
+
};
|
|
1216
1301
|
}
|
|
1217
|
-
/**
|
|
1218
|
-
* Parse `o.field.includes('text')` → LIKE expression.
|
|
1219
|
-
* The field name has already been parsed.
|
|
1220
|
-
*/
|
|
1302
|
+
/** `o.field.includes(needle)`, the field already read. */
|
|
1221
1303
|
parseFieldIncludes(field) {
|
|
1222
1304
|
this.advance();
|
|
1223
1305
|
this.advance();
|
|
1224
1306
|
if (!this.eat("lparen")) {
|
|
1225
1307
|
return null;
|
|
1226
1308
|
}
|
|
1227
|
-
const
|
|
1228
|
-
if (
|
|
1309
|
+
const needle = this.parseValue();
|
|
1310
|
+
if (typeof needle !== "string" && typeof needle !== "number" && typeof needle !== "boolean") {
|
|
1229
1311
|
return null;
|
|
1230
1312
|
}
|
|
1231
1313
|
if (!this.eat("rparen")) {
|
|
1232
1314
|
return null;
|
|
1233
1315
|
}
|
|
1234
|
-
|
|
1235
|
-
return { kind: "like", field, pattern: `%${escaped}%` };
|
|
1316
|
+
return { kind: "includes", field, needle };
|
|
1236
1317
|
}
|
|
1237
1318
|
/**
|
|
1238
|
-
*
|
|
1239
|
-
*
|
|
1319
|
+
* `[...].includes(o.field)` or `$.path.includes(o.field)`, when that's what
|
|
1320
|
+
* follows; null (position restored) otherwise.
|
|
1240
1321
|
*/
|
|
1241
|
-
|
|
1242
|
-
this.
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1322
|
+
tryParseMembership() {
|
|
1323
|
+
const start = this.pos;
|
|
1324
|
+
let values = null;
|
|
1325
|
+
if (this.eat("lbracket")) {
|
|
1326
|
+
const items = [];
|
|
1327
|
+
while (!this.match("rbracket")) {
|
|
1328
|
+
if (items.length > 0 && !this.eat("comma")) {
|
|
1329
|
+
this.pos = start;
|
|
1330
|
+
return null;
|
|
1331
|
+
}
|
|
1332
|
+
const item = this.parseValue();
|
|
1333
|
+
if (item === PARSE_FAILED) {
|
|
1334
|
+
this.pos = start;
|
|
1247
1335
|
return null;
|
|
1248
1336
|
}
|
|
1337
|
+
items.push(item);
|
|
1249
1338
|
}
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1339
|
+
this.advance();
|
|
1340
|
+
values = items;
|
|
1341
|
+
} else if (this.bindingsName && this.match("identifier", this.bindingsName)) {
|
|
1342
|
+
const bound = this.tryResolveBindingsValue();
|
|
1343
|
+
if (Array.isArray(bound)) {
|
|
1344
|
+
values = bound;
|
|
1253
1345
|
}
|
|
1254
|
-
values.push(val);
|
|
1255
|
-
}
|
|
1256
|
-
this.advance();
|
|
1257
|
-
if (!this.eat("dot")) {
|
|
1258
|
-
return null;
|
|
1259
1346
|
}
|
|
1260
|
-
if (!this.match("identifier",
|
|
1347
|
+
if (!values || !this.lookAheadForIncludes() || !this.match("identifier", this.paramName, 3)) {
|
|
1348
|
+
this.pos = start;
|
|
1261
1349
|
return null;
|
|
1262
1350
|
}
|
|
1263
1351
|
this.advance();
|
|
1264
|
-
if (!this.eat("lparen")) {
|
|
1265
|
-
return null;
|
|
1266
|
-
}
|
|
1267
|
-
if (!this.match("identifier", this.paramName)) {
|
|
1268
|
-
return null;
|
|
1269
|
-
}
|
|
1270
1352
|
this.advance();
|
|
1271
|
-
const field = this.parseFieldPath();
|
|
1272
|
-
if (!field) {
|
|
1273
|
-
return null;
|
|
1274
|
-
}
|
|
1275
|
-
if (!this.eat("rparen")) {
|
|
1276
|
-
return null;
|
|
1277
|
-
}
|
|
1278
|
-
return { kind: "in", field, values };
|
|
1279
|
-
}
|
|
1280
|
-
/**
|
|
1281
|
-
* Parse an expression that starts with an identifier that is NOT the
|
|
1282
|
-
* parameter name. This could be:
|
|
1283
|
-
* - A keyword literal: `true`, `false`, `null`, `undefined`
|
|
1284
|
-
* - A closure variable used in a comparison (handled by backtracking)
|
|
1285
|
-
*/
|
|
1286
|
-
parseNonParamExpression() {
|
|
1287
|
-
const ident = this.peek().value;
|
|
1288
|
-
if (ident === "true" || ident === "false") {
|
|
1289
|
-
return null;
|
|
1290
|
-
}
|
|
1291
|
-
return null;
|
|
1292
|
-
}
|
|
1293
|
-
/**
|
|
1294
|
-
* Parse `$.ids.includes(o.field)` → IN expression with resolved values.
|
|
1295
|
-
* The bindings identifier has been peeked but not consumed.
|
|
1296
|
-
*
|
|
1297
|
-
* Falls back (returns null) if the resolved bindings value isn't an array,
|
|
1298
|
-
* or if the path doesn't exist on the bindings object.
|
|
1299
|
-
*/
|
|
1300
|
-
parseBindingsArrayIncludes() {
|
|
1301
|
-
const bound = this.tryResolveBindingsValue();
|
|
1302
|
-
if (bound === PARSE_FAILED) {
|
|
1303
|
-
return null;
|
|
1304
|
-
}
|
|
1305
|
-
if (!Array.isArray(bound)) {
|
|
1306
|
-
return null;
|
|
1307
|
-
}
|
|
1308
|
-
if (!this.eat("dot")) {
|
|
1309
|
-
return null;
|
|
1310
|
-
}
|
|
1311
|
-
if (!this.match("identifier", "includes")) {
|
|
1312
|
-
return null;
|
|
1313
|
-
}
|
|
1314
1353
|
this.advance();
|
|
1315
|
-
if (!this.eat("lparen")) {
|
|
1316
|
-
return null;
|
|
1317
|
-
}
|
|
1318
|
-
if (!this.match("identifier", this.paramName)) {
|
|
1319
|
-
return null;
|
|
1320
|
-
}
|
|
1321
1354
|
this.advance();
|
|
1322
1355
|
const field = this.parseFieldPath();
|
|
1323
|
-
if (!field) {
|
|
1356
|
+
if (!field || !this.eat("rparen")) {
|
|
1324
1357
|
return null;
|
|
1325
1358
|
}
|
|
1326
|
-
|
|
1327
|
-
return null;
|
|
1328
|
-
}
|
|
1329
|
-
return { kind: "in", field, values: bound };
|
|
1359
|
+
return { kind: "in", field, values };
|
|
1330
1360
|
}
|
|
1331
1361
|
/**
|
|
1332
|
-
*
|
|
1333
|
-
*
|
|
1334
|
-
*
|
|
1335
|
-
*
|
|
1336
|
-
*
|
|
1337
|
-
* `$.ids.includes(...)`) so the caller can dispatch on what follows.
|
|
1338
|
-
*
|
|
1339
|
-
* Restores `pos` on failure so callers can fall through cleanly.
|
|
1362
|
+
* Walk a dotted bindings path (`$.foo.bar`) and resolve it from the
|
|
1363
|
+
* bindings object. Stops before a method call (`.includes(`), so the caller
|
|
1364
|
+
* can dispatch on what follows. Restores `pos` and returns `PARSE_FAILED`
|
|
1365
|
+
* when the path doesn't resolve: a missing key or `undefined` value falls
|
|
1366
|
+
* back rather than being compared as NULL.
|
|
1340
1367
|
*/
|
|
1341
1368
|
tryResolveBindingsValue() {
|
|
1342
|
-
if (!this.bindingsName) {
|
|
1343
|
-
return PARSE_FAILED;
|
|
1344
|
-
}
|
|
1345
|
-
if (!this.match("identifier", this.bindingsName)) {
|
|
1346
|
-
return PARSE_FAILED;
|
|
1347
|
-
}
|
|
1348
|
-
if (this.bindingsValue == null) {
|
|
1369
|
+
if (!this.bindingsName || !this.match("identifier", this.bindingsName) || this.bindingsValue == null) {
|
|
1349
1370
|
return PARSE_FAILED;
|
|
1350
1371
|
}
|
|
1351
1372
|
const startPos = this.pos;
|
|
1352
1373
|
this.advance();
|
|
1353
1374
|
const path = [];
|
|
1354
|
-
while (this.match("dot") && this.
|
|
1355
|
-
if (this.
|
|
1375
|
+
while (this.match("dot") && this.match("identifier", void 0, 1)) {
|
|
1376
|
+
if (this.match("lparen", void 0, 2)) {
|
|
1356
1377
|
break;
|
|
1357
1378
|
}
|
|
1358
1379
|
this.advance();
|
|
1359
1380
|
path.push(this.advance().value);
|
|
1360
1381
|
}
|
|
1361
|
-
if (path.length === 0) {
|
|
1362
|
-
this.pos = startPos;
|
|
1363
|
-
return PARSE_FAILED;
|
|
1364
|
-
}
|
|
1365
1382
|
let value = this.bindingsValue;
|
|
1366
1383
|
for (const key of path) {
|
|
1367
1384
|
if (value == null || typeof value !== "object") {
|
|
1368
|
-
|
|
1369
|
-
|
|
1385
|
+
value = void 0;
|
|
1386
|
+
break;
|
|
1370
1387
|
}
|
|
1371
1388
|
value = value[key];
|
|
1372
1389
|
}
|
|
1373
|
-
if (value === void 0) {
|
|
1390
|
+
if (path.length === 0 || value === void 0) {
|
|
1374
1391
|
this.pos = startPos;
|
|
1375
1392
|
return PARSE_FAILED;
|
|
1376
1393
|
}
|
|
1377
1394
|
return value;
|
|
1378
1395
|
}
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
*
|
|
1382
|
-
* Returns the parsed value, or PARSE_FAILED if parsing fails.
|
|
1383
|
-
* Returns `null` or `undefined` for those keyword literals.
|
|
1384
|
-
*/
|
|
1396
|
+
// --- Values: constant expressions, evaluated here ---
|
|
1397
|
+
/** A value: a literal, binding or helper call, or arithmetic over numbers. */
|
|
1385
1398
|
parseValue() {
|
|
1399
|
+
let left = this.parseTerm();
|
|
1400
|
+
while (this.match("operator", "+") || this.match("operator", "-")) {
|
|
1401
|
+
const op = this.advance().value;
|
|
1402
|
+
const right = this.parseTerm();
|
|
1403
|
+
if (typeof left !== "number" || typeof right !== "number") {
|
|
1404
|
+
return PARSE_FAILED;
|
|
1405
|
+
}
|
|
1406
|
+
left = op === "+" ? left + right : left - right;
|
|
1407
|
+
}
|
|
1408
|
+
return left;
|
|
1409
|
+
}
|
|
1410
|
+
parseTerm() {
|
|
1411
|
+
let left = this.parseUnary();
|
|
1412
|
+
while (this.match("operator", "*") || this.match("operator", "/") || this.match("operator", "%")) {
|
|
1413
|
+
const op = this.advance().value;
|
|
1414
|
+
const right = this.parseUnary();
|
|
1415
|
+
if (typeof left !== "number" || typeof right !== "number") {
|
|
1416
|
+
return PARSE_FAILED;
|
|
1417
|
+
}
|
|
1418
|
+
left = op === "*" ? left * right : op === "/" ? left / right : left % right;
|
|
1419
|
+
}
|
|
1420
|
+
return left;
|
|
1421
|
+
}
|
|
1422
|
+
parseUnary() {
|
|
1423
|
+
if (this.match("operator", "-") || this.match("operator", "+")) {
|
|
1424
|
+
const op = this.advance().value;
|
|
1425
|
+
const operand = this.parseUnary();
|
|
1426
|
+
if (typeof operand !== "number") {
|
|
1427
|
+
return PARSE_FAILED;
|
|
1428
|
+
}
|
|
1429
|
+
return op === "-" ? -operand : operand;
|
|
1430
|
+
}
|
|
1431
|
+
return this.parseAtom();
|
|
1432
|
+
}
|
|
1433
|
+
parseAtom() {
|
|
1386
1434
|
const t = this.peek();
|
|
1387
1435
|
if (!t) {
|
|
1388
1436
|
return PARSE_FAILED;
|
|
@@ -1393,122 +1441,105 @@ var Parser = class {
|
|
|
1393
1441
|
}
|
|
1394
1442
|
if (t.type === "number") {
|
|
1395
1443
|
this.advance();
|
|
1396
|
-
return
|
|
1444
|
+
return t.number;
|
|
1445
|
+
}
|
|
1446
|
+
if (t.type === "lparen") {
|
|
1447
|
+
this.advance();
|
|
1448
|
+
const inner = this.parseValue();
|
|
1449
|
+
return this.eat("rparen") ? inner : PARSE_FAILED;
|
|
1450
|
+
}
|
|
1451
|
+
if (t.type !== "identifier") {
|
|
1452
|
+
return PARSE_FAILED;
|
|
1397
1453
|
}
|
|
1398
|
-
|
|
1399
|
-
|
|
1454
|
+
switch (t.value) {
|
|
1455
|
+
case "true":
|
|
1400
1456
|
this.advance();
|
|
1401
1457
|
return true;
|
|
1402
|
-
|
|
1403
|
-
if (t.value === "false") {
|
|
1458
|
+
case "false":
|
|
1404
1459
|
this.advance();
|
|
1405
1460
|
return false;
|
|
1406
|
-
|
|
1407
|
-
if (t.value === "null") {
|
|
1461
|
+
case "null":
|
|
1408
1462
|
this.advance();
|
|
1409
1463
|
return null;
|
|
1410
|
-
|
|
1411
|
-
if (t.value === "undefined") {
|
|
1464
|
+
case "undefined":
|
|
1412
1465
|
this.advance();
|
|
1413
1466
|
return void 0;
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
return
|
|
1467
|
+
case "void":
|
|
1468
|
+
if (this.match("number", void 0, 1)) {
|
|
1469
|
+
this.advance();
|
|
1470
|
+
this.advance();
|
|
1471
|
+
return void 0;
|
|
1419
1472
|
}
|
|
1420
|
-
|
|
1421
|
-
return this.resolveClosureVariable();
|
|
1473
|
+
return PARSE_FAILED;
|
|
1422
1474
|
}
|
|
1423
|
-
if (
|
|
1424
|
-
this.
|
|
1425
|
-
|
|
1426
|
-
|
|
1475
|
+
if (this.bindingsName && t.value === this.bindingsName) {
|
|
1476
|
+
return this.tryResolveBindingsValue();
|
|
1477
|
+
}
|
|
1478
|
+
if (t.value === "Date" && this.match("dot", void 0, 1) && this.match("identifier", "now", 2) && this.match("lparen", void 0, 3) && this.match("rparen", void 0, 4)) {
|
|
1479
|
+
this.pos += 5;
|
|
1480
|
+
return Date.now();
|
|
1481
|
+
}
|
|
1482
|
+
if (t.value === "db" && this.match("dot", void 0, 1) && this.match("lparen", void 0, 3)) {
|
|
1483
|
+
const name = this.peek(2).value;
|
|
1484
|
+
if (Object.hasOwn(timeHelpers, name)) {
|
|
1485
|
+
this.pos += 4;
|
|
1486
|
+
const args = [];
|
|
1487
|
+
while (!this.match("rparen")) {
|
|
1488
|
+
if (args.length > 0 && !this.eat("comma")) {
|
|
1489
|
+
return PARSE_FAILED;
|
|
1490
|
+
}
|
|
1491
|
+
const arg = this.parseValue();
|
|
1492
|
+
if (typeof arg !== "number") {
|
|
1493
|
+
return PARSE_FAILED;
|
|
1494
|
+
}
|
|
1495
|
+
args.push(arg);
|
|
1496
|
+
}
|
|
1427
1497
|
this.advance();
|
|
1428
|
-
|
|
1498
|
+
const helper = timeHelpers[name];
|
|
1499
|
+
return helper(...args);
|
|
1429
1500
|
}
|
|
1430
|
-
return PARSE_FAILED;
|
|
1431
|
-
}
|
|
1432
|
-
return PARSE_FAILED;
|
|
1433
|
-
}
|
|
1434
|
-
/**
|
|
1435
|
-
* Attempt to resolve a closure variable's value.
|
|
1436
|
-
*
|
|
1437
|
-
* This handles the common pattern:
|
|
1438
|
-
* ```ts
|
|
1439
|
-
* const userId = auth.userId;
|
|
1440
|
-
* orders.filter(o => o.requestedBy === userId)
|
|
1441
|
-
* ```
|
|
1442
|
-
*
|
|
1443
|
-
* Closure variable resolution is fundamentally limited in JavaScript —
|
|
1444
|
-
* we can't access another function's closure scope from outside without
|
|
1445
|
-
* `eval`. The `===` operator can't be overridden via Proxy or
|
|
1446
|
-
* Symbol.toPrimitive, so we can't intercept comparisons.
|
|
1447
|
-
*
|
|
1448
|
-
* For now, this falls back to JS execution. The predicate still works
|
|
1449
|
-
* correctly — it just scans all rows instead of generating SQL.
|
|
1450
|
-
* This is the most common reason for JS fallback in practice, since
|
|
1451
|
-
* almost every real-world filter references a variable like `userId`.
|
|
1452
|
-
*
|
|
1453
|
-
* A future improvement could accept an explicit `vars` argument:
|
|
1454
|
-
* ```ts
|
|
1455
|
-
* orders.filter(o => o.requestedBy === $userId, { $userId: auth.userId })
|
|
1456
|
-
* ```
|
|
1457
|
-
*/
|
|
1458
|
-
resolveClosureVariable() {
|
|
1459
|
-
this.advance();
|
|
1460
|
-
while (this.match("dot") && this.tokens[this.pos + 1]?.type === "identifier") {
|
|
1461
|
-
this.advance();
|
|
1462
|
-
this.advance();
|
|
1463
1501
|
}
|
|
1464
1502
|
return PARSE_FAILED;
|
|
1465
1503
|
}
|
|
1466
|
-
/**
|
|
1467
|
-
* Look ahead to check if the next tokens form `.includes(`.
|
|
1468
|
-
* Used to disambiguate `o.field.includes(...)` from `o.field.nested`.
|
|
1469
|
-
*/
|
|
1504
|
+
/** Whether the next tokens are `.includes(`. */
|
|
1470
1505
|
lookAheadForIncludes() {
|
|
1471
|
-
return this.
|
|
1506
|
+
return this.match("dot") && this.match("identifier", "includes", 1) && this.match("lparen", void 0, 2);
|
|
1472
1507
|
}
|
|
1473
1508
|
};
|
|
1474
|
-
function
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
return "0";
|
|
1484
|
-
}
|
|
1485
|
-
const vals = node.values.map((v) => stored(node.field, v)).join(", ");
|
|
1486
|
-
return `${node.field} IN (${vals})`;
|
|
1487
|
-
}
|
|
1488
|
-
case "like":
|
|
1489
|
-
return `${node.field} LIKE ${escapeValue(node.pattern)}`;
|
|
1490
|
-
case "booleanField":
|
|
1491
|
-
return node.negated ? `${node.field} = 0` : `${node.field} = 1`;
|
|
1492
|
-
case "logical": {
|
|
1493
|
-
const left = compileNode(node.left, columns);
|
|
1494
|
-
const right = compileNode(node.right, columns);
|
|
1495
|
-
if (!left || !right) {
|
|
1496
|
-
return null;
|
|
1497
|
-
}
|
|
1498
|
-
return `(${left} ${node.operator} ${right})`;
|
|
1499
|
-
}
|
|
1500
|
-
case "not": {
|
|
1501
|
-
const inner = compileNode(node.operand, columns);
|
|
1502
|
-
if (!inner) {
|
|
1503
|
-
return null;
|
|
1504
|
-
}
|
|
1505
|
-
return `NOT (${inner})`;
|
|
1509
|
+
function comparisonNode(field, op, value) {
|
|
1510
|
+
if (value === null || value === void 0) {
|
|
1511
|
+
if (op === "===" || op === "==" || op === "!==" || op === "!=") {
|
|
1512
|
+
return {
|
|
1513
|
+
kind: "nullCheck",
|
|
1514
|
+
field,
|
|
1515
|
+
strict: op === "===" || op === "!==",
|
|
1516
|
+
isNull: op === "===" || op === "=="
|
|
1517
|
+
};
|
|
1506
1518
|
}
|
|
1507
|
-
|
|
1519
|
+
return null;
|
|
1520
|
+
}
|
|
1521
|
+
if (value instanceof Date) {
|
|
1522
|
+
if (op === "===" || op === "==" || op === "!==" || op === "!=") {
|
|
1508
1523
|
return null;
|
|
1524
|
+
}
|
|
1525
|
+
return comparisonNode(field, op, value.getTime());
|
|
1509
1526
|
}
|
|
1527
|
+
if (typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value)) {
|
|
1528
|
+
return { kind: "comparison", field, op, value };
|
|
1529
|
+
}
|
|
1530
|
+
return null;
|
|
1510
1531
|
}
|
|
1511
|
-
var
|
|
1532
|
+
var NEGATED = {
|
|
1533
|
+
"===": "!==",
|
|
1534
|
+
"!==": "===",
|
|
1535
|
+
"==": "!=",
|
|
1536
|
+
"!=": "==",
|
|
1537
|
+
"<": ">=",
|
|
1538
|
+
">=": "<",
|
|
1539
|
+
">": "<=",
|
|
1540
|
+
"<=": ">"
|
|
1541
|
+
};
|
|
1542
|
+
var SQL_OP = {
|
|
1512
1543
|
"===": "=",
|
|
1513
1544
|
"==": "=",
|
|
1514
1545
|
"!==": "!=",
|
|
@@ -1518,9 +1549,144 @@ var JS_TO_SQL_OP = {
|
|
|
1518
1549
|
"<=": "<=",
|
|
1519
1550
|
">=": ">="
|
|
1520
1551
|
};
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1552
|
+
function jsCompare(a, op, b) {
|
|
1553
|
+
const x = a;
|
|
1554
|
+
const y = b;
|
|
1555
|
+
switch (op) {
|
|
1556
|
+
case "===":
|
|
1557
|
+
return x === y;
|
|
1558
|
+
case "!==":
|
|
1559
|
+
return x !== y;
|
|
1560
|
+
case "==":
|
|
1561
|
+
return x == y;
|
|
1562
|
+
case "!=":
|
|
1563
|
+
return x != y;
|
|
1564
|
+
case "<":
|
|
1565
|
+
return x < y;
|
|
1566
|
+
case ">":
|
|
1567
|
+
return x > y;
|
|
1568
|
+
case "<=":
|
|
1569
|
+
return x <= y;
|
|
1570
|
+
case ">=":
|
|
1571
|
+
return x >= y;
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
var VALUE_KIND = {
|
|
1575
|
+
text: "string",
|
|
1576
|
+
user: "string",
|
|
1577
|
+
number: "number",
|
|
1578
|
+
boolean: "boolean"
|
|
1579
|
+
};
|
|
1580
|
+
var JSON_TYPES = {
|
|
1581
|
+
string: "('text')",
|
|
1582
|
+
number: "('integer', 'real')"
|
|
1583
|
+
};
|
|
1584
|
+
function nestedComparison(node, negated, params) {
|
|
1585
|
+
const strictEquals = node.op === "===" ? !negated : node.op === "!==" ? negated : null;
|
|
1586
|
+
const types = JSON_TYPES[typeof node.value];
|
|
1587
|
+
if (strictEquals === null || !types) {
|
|
1588
|
+
return null;
|
|
1589
|
+
}
|
|
1590
|
+
params.push(node.value);
|
|
1591
|
+
const equals = `(json_type(${node.field.column}, '${node.field.path}') IN ${types} AND ${node.field.sql} = ?)`;
|
|
1592
|
+
return strictEquals ? equals : `NOT coalesce(${equals}, 0)`;
|
|
1593
|
+
}
|
|
1594
|
+
function withNulls(core, field, nullsMatch) {
|
|
1595
|
+
return nullsMatch ? `(${core} OR ${field.sql} IS NULL)` : core;
|
|
1596
|
+
}
|
|
1597
|
+
function compileNode(node, negated, columns, params) {
|
|
1598
|
+
const column = (field) => field.nested ? void 0 : columns.find((c) => c.name === field.column);
|
|
1599
|
+
const stored = (field, value) => field.nested ? serializeColumnParam(value, "", []) : serializeColumnParam(value, field.column, columns);
|
|
1600
|
+
switch (node.kind) {
|
|
1601
|
+
case "logical": {
|
|
1602
|
+
const operator = negated ? node.operator === "AND" ? "OR" : "AND" : node.operator;
|
|
1603
|
+
const left = compileNode(node.left, negated, columns, params);
|
|
1604
|
+
const right = left && compileNode(node.right, negated, columns, params);
|
|
1605
|
+
return left && right ? `(${left} ${operator} ${right})` : null;
|
|
1606
|
+
}
|
|
1607
|
+
case "not":
|
|
1608
|
+
return compileNode(node.operand, !negated, columns, params);
|
|
1609
|
+
case "comparison": {
|
|
1610
|
+
if (node.field.nested) {
|
|
1611
|
+
return nestedComparison(node, negated, params);
|
|
1612
|
+
}
|
|
1613
|
+
const type = column(node.field)?.type;
|
|
1614
|
+
if (!type || typeof node.value !== VALUE_KIND[type]) {
|
|
1615
|
+
return null;
|
|
1616
|
+
}
|
|
1617
|
+
const op = negated ? NEGATED[node.op] : node.op;
|
|
1618
|
+
const nullsMatch = negated ? !jsCompare(null, node.op, node.value) : jsCompare(null, node.op, node.value);
|
|
1619
|
+
params.push(stored(node.field, node.value));
|
|
1620
|
+
if (SQL_OP[op] === "!=" && nullsMatch) {
|
|
1621
|
+
return `${node.field.sql} IS NOT ?`;
|
|
1622
|
+
}
|
|
1623
|
+
return withNulls(
|
|
1624
|
+
`${node.field.sql} ${SQL_OP[op]} ?`,
|
|
1625
|
+
node.field,
|
|
1626
|
+
nullsMatch
|
|
1627
|
+
);
|
|
1628
|
+
}
|
|
1629
|
+
case "nullCheck": {
|
|
1630
|
+
if (node.strict && node.field.nested) {
|
|
1631
|
+
return null;
|
|
1632
|
+
}
|
|
1633
|
+
const isNull = negated ? !node.isNull : node.isNull;
|
|
1634
|
+
return `${node.field.sql} ${isNull ? "IS NULL" : "IS NOT NULL"}`;
|
|
1635
|
+
}
|
|
1636
|
+
case "in": {
|
|
1637
|
+
const type = column(node.field)?.type;
|
|
1638
|
+
if (!type || node.values.some(
|
|
1639
|
+
(v) => v !== null && v !== void 0 && typeof v !== VALUE_KIND[type]
|
|
1640
|
+
)) {
|
|
1641
|
+
return null;
|
|
1642
|
+
}
|
|
1643
|
+
const listed = node.values.filter((v) => v !== null && v !== void 0);
|
|
1644
|
+
const nullsMatch = negated ? !node.values.includes(null) : node.values.includes(null);
|
|
1645
|
+
let core;
|
|
1646
|
+
if (listed.length === 0) {
|
|
1647
|
+
core = negated ? `${node.field.sql} IS NOT NULL` : "0";
|
|
1648
|
+
} else {
|
|
1649
|
+
params.push(JSON.stringify(listed.map((v) => stored(node.field, v))));
|
|
1650
|
+
core = `${node.field.sql} ${negated ? "NOT IN" : "IN"} (SELECT value FROM json_each(?))`;
|
|
1651
|
+
}
|
|
1652
|
+
return withNulls(core, node.field, nullsMatch);
|
|
1653
|
+
}
|
|
1654
|
+
case "includes": {
|
|
1655
|
+
const type = column(node.field)?.type;
|
|
1656
|
+
if (type === "json") {
|
|
1657
|
+
let element;
|
|
1658
|
+
if (typeof node.needle === "boolean") {
|
|
1659
|
+
element = `type = '${node.needle}'`;
|
|
1660
|
+
} else {
|
|
1661
|
+
params.push(node.needle);
|
|
1662
|
+
element = `type IN ${JSON_TYPES[typeof node.needle]} AND value = ?`;
|
|
1663
|
+
}
|
|
1664
|
+
return `(json_type(${node.field.sql}) = 'array' AND ${negated ? "NOT EXISTS" : "EXISTS"} (SELECT 1 FROM json_each(${node.field.sql}) WHERE ${element}))`;
|
|
1665
|
+
}
|
|
1666
|
+
if ((type === "text" || type === "user") && typeof node.needle === "string") {
|
|
1667
|
+
params.push(node.needle);
|
|
1668
|
+
return `instr(${node.field.sql}, ?) ${negated ? "=" : ">"} 0`;
|
|
1669
|
+
}
|
|
1670
|
+
return null;
|
|
1671
|
+
}
|
|
1672
|
+
case "truthy": {
|
|
1673
|
+
const type = column(node.field)?.type;
|
|
1674
|
+
if (node.field.nested) {
|
|
1675
|
+
return negated ? `(${node.field.sql} IN (0, '') OR ${node.field.sql} IS NULL)` : `${node.field.sql} NOT IN (0, '')`;
|
|
1676
|
+
}
|
|
1677
|
+
switch (type) {
|
|
1678
|
+
case "boolean":
|
|
1679
|
+
return negated ? `${node.field.sql} IS NOT 1` : `${node.field.sql} = 1`;
|
|
1680
|
+
case "number":
|
|
1681
|
+
return negated ? `(${node.field.sql} = 0 OR ${node.field.sql} IS NULL)` : `${node.field.sql} <> 0`;
|
|
1682
|
+
case "text":
|
|
1683
|
+
case "user":
|
|
1684
|
+
return negated ? `(${node.field.sql} = '' OR ${node.field.sql} IS NULL)` : `${node.field.sql} <> ''`;
|
|
1685
|
+
default:
|
|
1686
|
+
return null;
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1524
1690
|
}
|
|
1525
1691
|
|
|
1526
1692
|
// src/db/aggregate.ts
|
|
@@ -1631,24 +1797,6 @@ function aggregateValues(fn, values) {
|
|
|
1631
1797
|
return present.length === 0 ? null : present.reduce((m, v) => v > m ? v : m);
|
|
1632
1798
|
}
|
|
1633
1799
|
}
|
|
1634
|
-
function compareForOrder(a, b) {
|
|
1635
|
-
if (a == null && b == null) {
|
|
1636
|
-
return 0;
|
|
1637
|
-
}
|
|
1638
|
-
if (a == null) {
|
|
1639
|
-
return -1;
|
|
1640
|
-
}
|
|
1641
|
-
if (b == null) {
|
|
1642
|
-
return 1;
|
|
1643
|
-
}
|
|
1644
|
-
if (a < b) {
|
|
1645
|
-
return -1;
|
|
1646
|
-
}
|
|
1647
|
-
if (a > b) {
|
|
1648
|
-
return 1;
|
|
1649
|
-
}
|
|
1650
|
-
return 0;
|
|
1651
|
-
}
|
|
1652
1800
|
function computeAggregateInJs(rows, spec) {
|
|
1653
1801
|
const groups = /* @__PURE__ */ new Map();
|
|
1654
1802
|
for (const row of rows) {
|
|
@@ -1678,7 +1826,7 @@ function computeAggregateInJs(rows, spec) {
|
|
|
1678
1826
|
});
|
|
1679
1827
|
if (spec.orderBy) {
|
|
1680
1828
|
const key = spec.orderBy;
|
|
1681
|
-
out.sort((a, b) =>
|
|
1829
|
+
out.sort((a, b) => compareAsSqlite(a[key], b[key]));
|
|
1682
1830
|
if (spec.desc) {
|
|
1683
1831
|
out.reverse();
|
|
1684
1832
|
}
|
|
@@ -1732,6 +1880,8 @@ var Query = class _Query {
|
|
|
1732
1880
|
_rawWhereParams;
|
|
1733
1881
|
/** @internal See AggregateState — set by aggregate terminals. */
|
|
1734
1882
|
_aggregate;
|
|
1883
|
+
/** @internal The columns `select()` named; every column when unset. */
|
|
1884
|
+
_columns;
|
|
1735
1885
|
/** @internal Post-process transform applied after row deserialization. */
|
|
1736
1886
|
_postProcess;
|
|
1737
1887
|
constructor(config, options) {
|
|
@@ -1745,6 +1895,7 @@ var Query = class _Query {
|
|
|
1745
1895
|
this._rawWhere = options?.rawWhere;
|
|
1746
1896
|
this._rawWhereParams = options?.rawWhereParams;
|
|
1747
1897
|
this._aggregate = options?.aggregate;
|
|
1898
|
+
this._columns = options?.columns;
|
|
1748
1899
|
}
|
|
1749
1900
|
_clone(overrides) {
|
|
1750
1901
|
return new _Query(this._config, {
|
|
@@ -1756,7 +1907,8 @@ var Query = class _Query {
|
|
|
1756
1907
|
postProcess: overrides.postProcess,
|
|
1757
1908
|
rawWhere: this._rawWhere,
|
|
1758
1909
|
rawWhereParams: this._rawWhereParams,
|
|
1759
|
-
aggregate: overrides.aggregate ?? this._aggregate
|
|
1910
|
+
aggregate: overrides.aggregate ?? this._aggregate,
|
|
1911
|
+
columns: overrides.columns ?? this._columns
|
|
1760
1912
|
});
|
|
1761
1913
|
}
|
|
1762
1914
|
filter(predicate, bindings) {
|
|
@@ -1776,6 +1928,21 @@ var Query = class _Query {
|
|
|
1776
1928
|
skip(n) {
|
|
1777
1929
|
return this._clone({ offset: n });
|
|
1778
1930
|
}
|
|
1931
|
+
/**
|
|
1932
|
+
* Return only these columns: `SELECT id, name` rather than every column of
|
|
1933
|
+
* a wide row. Chain it before a terminal (`first()`, `take()`); filters and
|
|
1934
|
+
* sorts still see the whole row.
|
|
1935
|
+
*
|
|
1936
|
+
* ```ts
|
|
1937
|
+
* const names = await Companies
|
|
1938
|
+
* .filter((c, $) => c.firmId === $.firmId, { firmId })
|
|
1939
|
+
* .select('id', 'name');
|
|
1940
|
+
* // Array<{ id: string; name: string }>
|
|
1941
|
+
* ```
|
|
1942
|
+
*/
|
|
1943
|
+
select(...columns) {
|
|
1944
|
+
return this._clone({ columns });
|
|
1945
|
+
}
|
|
1779
1946
|
// -------------------------------------------------------------------------
|
|
1780
1947
|
// Terminal methods
|
|
1781
1948
|
// -------------------------------------------------------------------------
|
|
@@ -1863,8 +2030,8 @@ var Query = class _Query {
|
|
|
1863
2030
|
if (compiled.allSql && compiled.sqlWhere) {
|
|
1864
2031
|
const query = buildExists(
|
|
1865
2032
|
this._config.tableName,
|
|
1866
|
-
`NOT (${compiled.sqlWhere})`,
|
|
1867
|
-
|
|
2033
|
+
`NOT coalesce(${compiled.sqlWhere}, 0)`,
|
|
2034
|
+
compiled.params,
|
|
1868
2035
|
true
|
|
1869
2036
|
);
|
|
1870
2037
|
const results = await this._config.executeBatch([query]);
|
|
@@ -1906,116 +2073,152 @@ var Query = class _Query {
|
|
|
1906
2073
|
// Batch compilation — used by db.batch() to extract SQL without executing
|
|
1907
2074
|
// -------------------------------------------------------------------------
|
|
1908
2075
|
/**
|
|
1909
|
-
* @internal Compile this query into
|
|
1910
|
-
*
|
|
1911
|
-
*
|
|
1912
|
-
* or null (if JS fallback is needed). In the fallback case, a bare
|
|
1913
|
-
* `SELECT *` is returned as `fallbackQuery` so the batch can fetch
|
|
1914
|
-
* all rows and this query can filter them in JS post-fetch.
|
|
2076
|
+
* @internal Compile this query into the statement to run, plus whatever
|
|
2077
|
+
* JS has to do with its rows (see the module header's three cases). Used by
|
|
2078
|
+
* db.batch() and by a standalone await alike.
|
|
1915
2079
|
*/
|
|
1916
2080
|
_compile() {
|
|
2081
|
+
const table = this._config.tableName;
|
|
1917
2082
|
if (this._rawWhere) {
|
|
1918
|
-
const query = buildSelect(this._config.tableName, {
|
|
1919
|
-
where: this._rawWhere,
|
|
1920
|
-
whereParams: this._rawWhereParams,
|
|
1921
|
-
orderBy: void 0,
|
|
1922
|
-
limit: this._limit,
|
|
1923
|
-
offset: this._offset
|
|
1924
|
-
});
|
|
1925
2083
|
return {
|
|
1926
2084
|
type: "query",
|
|
1927
|
-
query,
|
|
1928
|
-
|
|
2085
|
+
query: buildSelect(table, {
|
|
2086
|
+
columns: this._columns,
|
|
2087
|
+
where: this._rawWhere,
|
|
2088
|
+
whereParams: this._rawWhereParams,
|
|
2089
|
+
limit: this._limit,
|
|
2090
|
+
offset: this._offset
|
|
2091
|
+
}),
|
|
1929
2092
|
config: this._config,
|
|
1930
2093
|
postProcess: this._postProcess
|
|
1931
2094
|
};
|
|
1932
2095
|
}
|
|
1933
2096
|
const compiled = this._compilePredicates();
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
return {
|
|
1939
|
-
type: "query",
|
|
1940
|
-
query: agg.query,
|
|
1941
|
-
fallbackQuery: null,
|
|
1942
|
-
config: this._config,
|
|
1943
|
-
processRaw: agg.processRaw
|
|
1944
|
-
};
|
|
1945
|
-
}
|
|
1946
|
-
const query = buildSelect(this._config.tableName, {
|
|
1947
|
-
where: compiled.sqlWhere || void 0,
|
|
1948
|
-
orderBy: sortField ?? void 0,
|
|
1949
|
-
desc: this._reversed,
|
|
1950
|
-
limit: this._limit,
|
|
1951
|
-
offset: this._offset
|
|
1952
|
-
});
|
|
2097
|
+
if (!compiled.allSql) {
|
|
2098
|
+
const reasons = compiled.compiled.flatMap(
|
|
2099
|
+
(c) => c.type === "js" && c.reason ? [c.reason] : []
|
|
2100
|
+
);
|
|
1953
2101
|
return {
|
|
1954
2102
|
type: "query",
|
|
1955
|
-
query,
|
|
1956
|
-
|
|
2103
|
+
query: buildSelect(table, {
|
|
2104
|
+
orderBy: !this._sortAccessor && this._reversed ? "rowid" : void 0
|
|
2105
|
+
}),
|
|
1957
2106
|
config: this._config,
|
|
2107
|
+
inJs: {
|
|
2108
|
+
why: `filter could not be compiled to SQL${reasons.length ? ` (${reasons.join("; ")})` : ""}`,
|
|
2109
|
+
predicates: this._predicates,
|
|
2110
|
+
sortAccessor: this._sortAccessor,
|
|
2111
|
+
reversed: this._reversed,
|
|
2112
|
+
limit: this._limit,
|
|
2113
|
+
offset: this._offset,
|
|
2114
|
+
columns: this._columns
|
|
2115
|
+
},
|
|
2116
|
+
postProcess: this._postProcess
|
|
2117
|
+
};
|
|
2118
|
+
}
|
|
2119
|
+
const agg = this._compileAggregateSql(compiled.sqlWhere, compiled.params);
|
|
2120
|
+
if (agg) {
|
|
2121
|
+
return {
|
|
2122
|
+
type: "query",
|
|
2123
|
+
query: agg.query,
|
|
2124
|
+
config: this._config,
|
|
2125
|
+
processRaw: agg.processRaw
|
|
2126
|
+
};
|
|
2127
|
+
}
|
|
2128
|
+
const sortField = this._sortAccessor ? extractFieldName(this._sortAccessor) : null;
|
|
2129
|
+
if (this._sortAccessor && sortField == null) {
|
|
2130
|
+
return {
|
|
2131
|
+
type: "query",
|
|
2132
|
+
query: buildSelect(table, {
|
|
2133
|
+
where: compiled.sqlWhere || void 0,
|
|
2134
|
+
whereParams: compiled.params
|
|
2135
|
+
}),
|
|
2136
|
+
config: this._config,
|
|
2137
|
+
inJs: {
|
|
2138
|
+
why: "sortBy is computed, not a column, so it sorts in JS",
|
|
2139
|
+
sortAccessor: this._sortAccessor,
|
|
2140
|
+
reversed: this._reversed,
|
|
2141
|
+
limit: this._limit,
|
|
2142
|
+
offset: this._offset,
|
|
2143
|
+
columns: this._columns
|
|
2144
|
+
},
|
|
1958
2145
|
postProcess: this._postProcess
|
|
1959
2146
|
};
|
|
1960
2147
|
}
|
|
1961
|
-
const fallbackQuery = buildSelect(this._config.tableName);
|
|
1962
2148
|
return {
|
|
1963
2149
|
type: "query",
|
|
1964
|
-
query:
|
|
1965
|
-
|
|
2150
|
+
query: buildSelect(table, {
|
|
2151
|
+
columns: this._columns,
|
|
2152
|
+
where: compiled.sqlWhere || void 0,
|
|
2153
|
+
whereParams: compiled.params,
|
|
2154
|
+
orderBy: sortField ?? (this._reversed ? "rowid" : void 0),
|
|
2155
|
+
desc: this._reversed,
|
|
2156
|
+
limit: this._limit,
|
|
2157
|
+
offset: this._offset
|
|
2158
|
+
}),
|
|
1966
2159
|
config: this._config,
|
|
1967
|
-
predicates: this._predicates,
|
|
1968
|
-
sortAccessor: this._sortAccessor,
|
|
1969
|
-
reversed: this._reversed,
|
|
1970
|
-
limit: this._limit,
|
|
1971
|
-
offset: this._offset,
|
|
1972
2160
|
postProcess: this._postProcess
|
|
1973
2161
|
};
|
|
1974
2162
|
}
|
|
1975
2163
|
/**
|
|
1976
2164
|
* @internal Process raw SQL results into typed rows. Used by db.batch()
|
|
1977
|
-
* after executing the compiled query.
|
|
2165
|
+
* after executing the compiled query, and by a standalone await.
|
|
1978
2166
|
*
|
|
1979
|
-
*
|
|
1980
|
-
*
|
|
2167
|
+
* Deserializes the rows, then does in JS whatever the SQL couldn't
|
|
2168
|
+
* (`inJs`): the filter, the sort, the page, the projection.
|
|
1981
2169
|
*/
|
|
1982
2170
|
static _processResults(result, compiled) {
|
|
1983
2171
|
if (compiled.processRaw) {
|
|
1984
2172
|
return compiled.processRaw(result);
|
|
1985
2173
|
}
|
|
1986
|
-
const
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
compiled.config.columns
|
|
1990
|
-
)
|
|
2174
|
+
const columns = compiled.config.columns;
|
|
2175
|
+
let rows = result.rows.map(
|
|
2176
|
+
(row) => deserializeRow(row, columns)
|
|
1991
2177
|
);
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2178
|
+
const inJs = compiled.inJs;
|
|
2179
|
+
if (inJs) {
|
|
2180
|
+
if (inJs.predicates?.length) {
|
|
2181
|
+
rows = rows.filter(
|
|
2182
|
+
(row) => inJs.predicates.every((p) => p.fn(row, p.bindings))
|
|
2183
|
+
);
|
|
2184
|
+
}
|
|
2185
|
+
if (inJs.sortAccessor) {
|
|
2186
|
+
const accessor = inJs.sortAccessor;
|
|
2187
|
+
const direction = inJs.reversed ? -1 : 1;
|
|
2188
|
+
rows.sort(
|
|
2189
|
+
(a, b) => direction * compareAsSqlite(accessor(a), accessor(b))
|
|
2190
|
+
);
|
|
2191
|
+
} else if (inJs.reversed) {
|
|
2192
|
+
rows.reverse();
|
|
2193
|
+
}
|
|
2194
|
+
if (inJs.offset != null || inJs.limit != null) {
|
|
2195
|
+
const start = inJs.offset ?? 0;
|
|
2196
|
+
const end = inJs.limit != null ? start + inJs.limit : void 0;
|
|
2197
|
+
rows = rows.slice(start, end);
|
|
2198
|
+
}
|
|
2199
|
+
if (inJs.columns?.length) {
|
|
2200
|
+
const keep = inJs.columns;
|
|
2201
|
+
rows = rows.map(
|
|
2202
|
+
(row) => Object.fromEntries(
|
|
2203
|
+
keep.map((column) => [
|
|
2204
|
+
column,
|
|
2205
|
+
row[column]
|
|
2206
|
+
])
|
|
2207
|
+
)
|
|
2208
|
+
);
|
|
2209
|
+
}
|
|
2012
2210
|
}
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2211
|
+
return compiled.postProcess ? compiled.postProcess(rows) : rows;
|
|
2212
|
+
}
|
|
2213
|
+
/** @internal Say that part of a query ran in JS, and why. */
|
|
2214
|
+
static _warnInJs(compiled, rowsRead) {
|
|
2215
|
+
if (!compiled.inJs) {
|
|
2216
|
+
return;
|
|
2017
2217
|
}
|
|
2018
|
-
|
|
2218
|
+
const read = rowsRead == null ? "" : ` (${rowsRead} rows read)`;
|
|
2219
|
+
console.warn(
|
|
2220
|
+
`[mindstudio] Query on '${compiled.config.tableName}': ${compiled.inJs.why} \u2014 processing in JS${read}`
|
|
2221
|
+
);
|
|
2019
2222
|
}
|
|
2020
2223
|
// -------------------------------------------------------------------------
|
|
2021
2224
|
// PromiseLike
|
|
@@ -2025,18 +2228,10 @@ var Query = class _Query {
|
|
|
2025
2228
|
}
|
|
2026
2229
|
async _run() {
|
|
2027
2230
|
await this._config.ready?.();
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
if (agg) {
|
|
2033
|
-
const results = await this._config.executeBatch([agg.query]);
|
|
2034
|
-
return agg.processRaw(results[0]);
|
|
2035
|
-
}
|
|
2036
|
-
}
|
|
2037
|
-
}
|
|
2038
|
-
const rows = await this._execute();
|
|
2039
|
-
return this._postProcess ? this._postProcess(rows) : rows;
|
|
2231
|
+
const compiled = this._compile();
|
|
2232
|
+
const results = await this._config.executeBatch([compiled.query]);
|
|
2233
|
+
_Query._warnInJs(compiled, results[0]?.rows.length);
|
|
2234
|
+
return _Query._processResults(results[0], compiled);
|
|
2040
2235
|
}
|
|
2041
2236
|
/**
|
|
2042
2237
|
* @internal Compile the pending aggregate into a single SQL statement +
|
|
@@ -2047,7 +2242,7 @@ var Query = class _Query {
|
|
|
2047
2242
|
* when a scalar accessor doesn't extract to a column (computed accessors
|
|
2048
2243
|
* stay correct via JS, with a warning to prompt optimization).
|
|
2049
2244
|
*/
|
|
2050
|
-
_compileAggregateSql(sqlWhere) {
|
|
2245
|
+
_compileAggregateSql(sqlWhere, whereParams) {
|
|
2051
2246
|
const agg = this._aggregate;
|
|
2052
2247
|
if (!agg || this._rawWhere || this._limit != null || this._offset != null) {
|
|
2053
2248
|
return null;
|
|
@@ -2068,7 +2263,8 @@ var Query = class _Query {
|
|
|
2068
2263
|
this._config.tableName,
|
|
2069
2264
|
agg.fn,
|
|
2070
2265
|
column,
|
|
2071
|
-
where
|
|
2266
|
+
where,
|
|
2267
|
+
whereParams
|
|
2072
2268
|
);
|
|
2073
2269
|
const processRaw2 = (result) => {
|
|
2074
2270
|
const value = result.rows[0]?.value;
|
|
@@ -2084,6 +2280,7 @@ var Query = class _Query {
|
|
|
2084
2280
|
by: spec.by,
|
|
2085
2281
|
select: spec.select,
|
|
2086
2282
|
where,
|
|
2283
|
+
whereParams,
|
|
2087
2284
|
orderBy: spec.orderBy,
|
|
2088
2285
|
desc: spec.desc,
|
|
2089
2286
|
limit: spec.limit
|
|
@@ -2104,89 +2301,22 @@ var Query = class _Query {
|
|
|
2104
2301
|
// -------------------------------------------------------------------------
|
|
2105
2302
|
// Execution internals
|
|
2106
2303
|
// -------------------------------------------------------------------------
|
|
2107
|
-
async _execute() {
|
|
2108
|
-
if (this._rawWhere) {
|
|
2109
|
-
const query = buildSelect(this._config.tableName, {
|
|
2110
|
-
where: this._rawWhere,
|
|
2111
|
-
whereParams: this._rawWhereParams,
|
|
2112
|
-
limit: this._limit,
|
|
2113
|
-
offset: this._offset
|
|
2114
|
-
});
|
|
2115
|
-
const results = await this._config.executeBatch([query]);
|
|
2116
|
-
return results[0].rows.map(
|
|
2117
|
-
(row) => deserializeRow(
|
|
2118
|
-
row,
|
|
2119
|
-
this._config.columns
|
|
2120
|
-
)
|
|
2121
|
-
);
|
|
2122
|
-
}
|
|
2123
|
-
const compiled = this._compilePredicates();
|
|
2124
|
-
if (compiled.allSql) {
|
|
2125
|
-
const sortField = this._sortAccessor ? extractFieldName(this._sortAccessor) : void 0;
|
|
2126
|
-
const query = buildSelect(this._config.tableName, {
|
|
2127
|
-
where: compiled.sqlWhere || void 0,
|
|
2128
|
-
orderBy: sortField ?? void 0,
|
|
2129
|
-
desc: this._reversed,
|
|
2130
|
-
limit: this._limit,
|
|
2131
|
-
offset: this._offset
|
|
2132
|
-
});
|
|
2133
|
-
const results = await this._config.executeBatch([query]);
|
|
2134
|
-
return results[0].rows.map(
|
|
2135
|
-
(row) => deserializeRow(
|
|
2136
|
-
row,
|
|
2137
|
-
this._config.columns
|
|
2138
|
-
)
|
|
2139
|
-
);
|
|
2140
|
-
}
|
|
2141
|
-
let rows = await this._fetchAndFilterInJs(compiled);
|
|
2142
|
-
if (this._sortAccessor) {
|
|
2143
|
-
const accessor = this._sortAccessor;
|
|
2144
|
-
rows.sort((a, b) => {
|
|
2145
|
-
const aVal = accessor(a);
|
|
2146
|
-
const bVal = accessor(b);
|
|
2147
|
-
if (aVal < bVal) {
|
|
2148
|
-
return this._reversed ? 1 : -1;
|
|
2149
|
-
}
|
|
2150
|
-
if (aVal > bVal) {
|
|
2151
|
-
return this._reversed ? -1 : 1;
|
|
2152
|
-
}
|
|
2153
|
-
return 0;
|
|
2154
|
-
});
|
|
2155
|
-
}
|
|
2156
|
-
if (this._offset != null || this._limit != null) {
|
|
2157
|
-
const start = this._offset ?? 0;
|
|
2158
|
-
const end = this._limit != null ? start + this._limit : void 0;
|
|
2159
|
-
rows = rows.slice(start, end);
|
|
2160
|
-
}
|
|
2161
|
-
return rows;
|
|
2162
|
-
}
|
|
2163
2304
|
_compilePredicates() {
|
|
2164
2305
|
if (this._predicates.length === 0) {
|
|
2165
|
-
return { allSql: true, sqlWhere: "", compiled: [] };
|
|
2306
|
+
return { allSql: true, sqlWhere: "", params: [], compiled: [] };
|
|
2166
2307
|
}
|
|
2308
|
+
const columns = this._config.columns;
|
|
2167
2309
|
const compiled = this._predicates.map(
|
|
2168
|
-
(p) => compilePredicate(p.fn, p.bindings,
|
|
2169
|
-
);
|
|
2170
|
-
const allSql = compiled.every((c) => c.type === "sql");
|
|
2171
|
-
let sqlWhere = "";
|
|
2172
|
-
if (allSql) {
|
|
2173
|
-
sqlWhere = compiled.map((c) => c.where).join(" AND ");
|
|
2174
|
-
}
|
|
2175
|
-
return { allSql, sqlWhere, compiled };
|
|
2176
|
-
}
|
|
2177
|
-
async _fetchAndFilterInJs(compiled) {
|
|
2178
|
-
const allRows = await this._fetchAllRows();
|
|
2179
|
-
const jsFallbacks = compiled.compiled.filter((c) => c.type === "js");
|
|
2180
|
-
if (jsFallbacks.length > 0) {
|
|
2181
|
-
const reasons = jsFallbacks.map((c) => c.type === "js" ? c.reason : void 0).filter(Boolean);
|
|
2182
|
-
const reasonSuffix = reasons.length > 0 ? ` (${reasons.join("; ")})` : "";
|
|
2183
|
-
console.warn(
|
|
2184
|
-
`[mindstudio] Filter on '${this._config.tableName}' could not be compiled to SQL${reasonSuffix} \u2014 scanning ${allRows.length} rows in JS`
|
|
2185
|
-
);
|
|
2186
|
-
}
|
|
2187
|
-
return allRows.filter(
|
|
2188
|
-
(row) => this._predicates.every((p) => p.fn(row, p.bindings))
|
|
2310
|
+
(p) => compilePredicate(p.fn, p.bindings, columns)
|
|
2189
2311
|
);
|
|
2312
|
+
const sql = compiled.flatMap((c) => c.type === "sql" ? [c] : []);
|
|
2313
|
+
const allSql = sql.length === compiled.length;
|
|
2314
|
+
return {
|
|
2315
|
+
allSql,
|
|
2316
|
+
sqlWhere: allSql ? sql.map((c) => c.where).join(" AND ") : "",
|
|
2317
|
+
params: allSql ? sql.flatMap((c) => c.params) : [],
|
|
2318
|
+
compiled
|
|
2319
|
+
};
|
|
2190
2320
|
}
|
|
2191
2321
|
async _fetchAllRows() {
|
|
2192
2322
|
const query = buildSelect(this._config.tableName);
|
|
@@ -2205,38 +2335,20 @@ function extractFieldName(accessor) {
|
|
|
2205
2335
|
}
|
|
2206
2336
|
|
|
2207
2337
|
// src/db/mutation.ts
|
|
2208
|
-
var Mutation = class
|
|
2338
|
+
var Mutation = class {
|
|
2209
2339
|
/** @internal */
|
|
2210
2340
|
_config;
|
|
2211
|
-
/** @internal */
|
|
2341
|
+
/** @internal The statements, or null when the write can't be one batch. */
|
|
2212
2342
|
_build;
|
|
2213
2343
|
/** @internal */
|
|
2214
2344
|
_processResult;
|
|
2215
|
-
/** @internal
|
|
2216
|
-
|
|
2217
|
-
constructor(config, build, processResult) {
|
|
2345
|
+
/** @internal What runs when `_build` returns null (e.g. removeAll's JS fallback). */
|
|
2346
|
+
_fallback;
|
|
2347
|
+
constructor(config, build, processResult, fallback) {
|
|
2218
2348
|
this._config = config;
|
|
2219
2349
|
this._build = build;
|
|
2220
2350
|
this._processResult = processResult;
|
|
2221
|
-
this.
|
|
2222
|
-
}
|
|
2223
|
-
/**
|
|
2224
|
-
* Create a non-batchable mutation that wraps an async executor.
|
|
2225
|
-
* Used for operations that require multi-step execution (e.g. removeAll
|
|
2226
|
-
* with a JS-fallback predicate: fetch all rows → filter → delete).
|
|
2227
|
-
*
|
|
2228
|
-
* Works fine when awaited standalone. Throws if passed to db.batch().
|
|
2229
|
-
*
|
|
2230
|
-
* @internal
|
|
2231
|
-
*/
|
|
2232
|
-
static fromExecutor(config, executor) {
|
|
2233
|
-
const m = new _Mutation(
|
|
2234
|
-
config,
|
|
2235
|
-
() => [],
|
|
2236
|
-
() => void 0
|
|
2237
|
-
);
|
|
2238
|
-
Object.defineProperty(m, "_executor", { value: executor });
|
|
2239
|
-
return m;
|
|
2351
|
+
this._fallback = fallback;
|
|
2240
2352
|
}
|
|
2241
2353
|
// -------------------------------------------------------------------------
|
|
2242
2354
|
// PromiseLike — executes on await
|
|
@@ -2254,10 +2366,11 @@ var Mutation = class _Mutation {
|
|
|
2254
2366
|
* @internal Compile this mutation into SQL for batch execution.
|
|
2255
2367
|
* Returns the queries and a result processor.
|
|
2256
2368
|
*
|
|
2257
|
-
* Throws if
|
|
2369
|
+
* Throws if it can't be expressed as SQL (its builder returned null).
|
|
2258
2370
|
*/
|
|
2259
2371
|
_compile() {
|
|
2260
|
-
|
|
2372
|
+
const queries = this._build();
|
|
2373
|
+
if (!queries) {
|
|
2261
2374
|
throw new MindStudioError(
|
|
2262
2375
|
"This operation cannot be batched (e.g. removeAll with a JS-fallback predicate). Await it separately instead of passing to db.batch().",
|
|
2263
2376
|
"not_batchable",
|
|
@@ -2266,7 +2379,7 @@ var Mutation = class _Mutation {
|
|
|
2266
2379
|
}
|
|
2267
2380
|
return {
|
|
2268
2381
|
type: "mutation",
|
|
2269
|
-
queries
|
|
2382
|
+
queries,
|
|
2270
2383
|
config: this._config,
|
|
2271
2384
|
processResult: this._processResult
|
|
2272
2385
|
};
|
|
@@ -2283,10 +2396,11 @@ var Mutation = class _Mutation {
|
|
|
2283
2396
|
// -------------------------------------------------------------------------
|
|
2284
2397
|
async _execute() {
|
|
2285
2398
|
await this._config.ready?.();
|
|
2286
|
-
|
|
2287
|
-
|
|
2399
|
+
const queries = this._build();
|
|
2400
|
+
if (!queries) {
|
|
2401
|
+
return this._fallback();
|
|
2288
2402
|
}
|
|
2289
|
-
const results = await this._config.executeBatch(
|
|
2403
|
+
const results = await this._config.executeBatch(queries);
|
|
2290
2404
|
return this._processResult(results);
|
|
2291
2405
|
}
|
|
2292
2406
|
};
|
|
@@ -2390,6 +2504,10 @@ var Table = class {
|
|
|
2390
2504
|
sortBy(accessor) {
|
|
2391
2505
|
return new Query(this._config).sortBy(accessor);
|
|
2392
2506
|
}
|
|
2507
|
+
/** Every row, with only these columns. See Query.select(). */
|
|
2508
|
+
select(...columns) {
|
|
2509
|
+
return new Query(this._config).select(...columns);
|
|
2510
|
+
}
|
|
2393
2511
|
push(data) {
|
|
2394
2512
|
const isArray = Array.isArray(data);
|
|
2395
2513
|
const items = (isArray ? data : [data]).map(
|
|
@@ -2460,42 +2578,47 @@ var Table = class {
|
|
|
2460
2578
|
);
|
|
2461
2579
|
}
|
|
2462
2580
|
removeAll(predicate, bindings) {
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
)
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2581
|
+
return new Mutation(
|
|
2582
|
+
this._config,
|
|
2583
|
+
() => {
|
|
2584
|
+
const compiled = compilePredicate(
|
|
2585
|
+
predicate,
|
|
2586
|
+
bindings,
|
|
2587
|
+
this._config.columns
|
|
2588
|
+
);
|
|
2589
|
+
return compiled.type === "sql" ? [
|
|
2590
|
+
buildDelete(
|
|
2591
|
+
this._config.tableName,
|
|
2592
|
+
compiled.where,
|
|
2593
|
+
compiled.params
|
|
2594
|
+
)
|
|
2595
|
+
] : null;
|
|
2596
|
+
},
|
|
2597
|
+
(results) => results[0].changes,
|
|
2598
|
+
// Fallback: multi-step execution — not batchable
|
|
2599
|
+
async () => {
|
|
2600
|
+
console.warn(
|
|
2601
|
+
`[mindstudio] removeAll predicate on ${this._config.tableName} could not be compiled to SQL \u2014 fetching all rows first`
|
|
2602
|
+
);
|
|
2603
|
+
const allQuery = buildSelect(this._config.tableName);
|
|
2604
|
+
const allResults = await this._config.executeBatch([allQuery]);
|
|
2605
|
+
const columns = this._config.columns;
|
|
2606
|
+
const allRows = allResults[0].rows.map(
|
|
2607
|
+
(r) => deserializeRow(r, columns)
|
|
2608
|
+
);
|
|
2609
|
+
const matching = allRows.filter((row) => predicate(row, bindings));
|
|
2610
|
+
if (matching.length === 0) {
|
|
2611
|
+
return 0;
|
|
2612
|
+
}
|
|
2613
|
+
const deleteQueries = matching.filter((row) => row.id).map(
|
|
2614
|
+
(row) => buildDelete(this._config.tableName, `id = ?`, [row.id])
|
|
2615
|
+
);
|
|
2616
|
+
if (deleteQueries.length > 0) {
|
|
2617
|
+
await this._config.executeBatch(deleteQueries);
|
|
2618
|
+
}
|
|
2619
|
+
return matching.length;
|
|
2496
2620
|
}
|
|
2497
|
-
|
|
2498
|
-
});
|
|
2621
|
+
);
|
|
2499
2622
|
}
|
|
2500
2623
|
clear() {
|
|
2501
2624
|
const query = buildDelete(this._config.tableName);
|
|
@@ -2700,12 +2823,7 @@ function createDb(databases, executeBatch, authConfig, syncRoles) {
|
|
|
2700
2823
|
},
|
|
2701
2824
|
// --- Time helpers ---
|
|
2702
2825
|
// Pure JS, no platform dependency. All timestamps are unix ms.
|
|
2703
|
-
|
|
2704
|
-
days: (n) => n * 864e5,
|
|
2705
|
-
hours: (n) => n * 36e5,
|
|
2706
|
-
minutes: (n) => n * 6e4,
|
|
2707
|
-
ago: (ms) => Date.now() - ms,
|
|
2708
|
-
fromNow: (ms) => Date.now() + ms,
|
|
2826
|
+
...timeHelpers,
|
|
2709
2827
|
// --- User references ---
|
|
2710
2828
|
userRef: (id) => id.startsWith(USER_PREFIX) ? id.slice(USER_PREFIX.length) : id,
|
|
2711
2829
|
// --- Raw SQL (read-only) ---
|
|
@@ -2745,8 +2863,7 @@ function createDb(databases, executeBatch, authConfig, syncRoles) {
|
|
|
2745
2863
|
groups.set(dbId, []);
|
|
2746
2864
|
}
|
|
2747
2865
|
if (c.type === "query") {
|
|
2748
|
-
|
|
2749
|
-
groups.get(dbId).push({ opIndex: i, sqlQueries: [sqlQuery] });
|
|
2866
|
+
groups.get(dbId).push({ opIndex: i, sqlQueries: [c.query] });
|
|
2750
2867
|
} else if (c.type === "raw") {
|
|
2751
2868
|
groups.get(dbId).push({ opIndex: i, sqlQueries: [c.query] });
|
|
2752
2869
|
} else {
|
|
@@ -2775,11 +2892,7 @@ function createDb(databases, executeBatch, authConfig, syncRoles) {
|
|
|
2775
2892
|
return compiled.map((c, i) => {
|
|
2776
2893
|
const results = opResults.get(i);
|
|
2777
2894
|
if (c.type === "query") {
|
|
2778
|
-
|
|
2779
|
-
console.warn(
|
|
2780
|
-
`[mindstudio] db.batch(): filter on '${c.config.tableName}' could not be compiled to SQL \u2014 processing in JS`
|
|
2781
|
-
);
|
|
2782
|
-
}
|
|
2895
|
+
Query._warnInJs(c, results[0]?.rows.length);
|
|
2783
2896
|
return Query._processResults(results[0], c);
|
|
2784
2897
|
} else if (c.type === "raw") {
|
|
2785
2898
|
return RawQuery._processResults(results[0], c);
|
|
@@ -2875,6 +2988,84 @@ function resolveTable(databases, tableName, databaseHint) {
|
|
|
2875
2988
|
);
|
|
2876
2989
|
}
|
|
2877
2990
|
|
|
2991
|
+
// src/db/readBatching.ts
|
|
2992
|
+
var nextTick = typeof setImmediate === "function" ? (fn) => setImmediate(fn) : (fn) => setTimeout(fn, 0);
|
|
2993
|
+
var DbReadBatching = class {
|
|
2994
|
+
constructor(send) {
|
|
2995
|
+
this.send = send;
|
|
2996
|
+
}
|
|
2997
|
+
send;
|
|
2998
|
+
pending = /* @__PURE__ */ new Map();
|
|
2999
|
+
inFlight = /* @__PURE__ */ new Map();
|
|
3000
|
+
/** Run `queries` for the request holding `token`, sharing a round trip where it can. */
|
|
3001
|
+
async run(token, databaseId, queries) {
|
|
3002
|
+
const batchKey = `${token}\0${databaseId}`;
|
|
3003
|
+
if (queries.length === 0 || !queries.every((q) => isReadOnlySql(q.sql))) {
|
|
3004
|
+
this.flush(batchKey);
|
|
3005
|
+
return this.send(databaseId, queries);
|
|
3006
|
+
}
|
|
3007
|
+
const readKey = `${batchKey}\0${JSON.stringify(queries)}`;
|
|
3008
|
+
const shared = this.inFlight.get(readKey);
|
|
3009
|
+
if (shared) {
|
|
3010
|
+
return structuredClone(await shared);
|
|
3011
|
+
}
|
|
3012
|
+
const read = this.queue(batchKey, databaseId, queries);
|
|
3013
|
+
this.inFlight.set(readKey, read);
|
|
3014
|
+
try {
|
|
3015
|
+
return await read;
|
|
3016
|
+
} finally {
|
|
3017
|
+
this.inFlight.delete(readKey);
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
queue(batchKey, databaseId, queries) {
|
|
3021
|
+
let pending = this.pending.get(batchKey);
|
|
3022
|
+
if (!pending) {
|
|
3023
|
+
pending = { databaseId, queries: [], parts: [] };
|
|
3024
|
+
this.pending.set(batchKey, pending);
|
|
3025
|
+
nextTick(() => this.flush(batchKey));
|
|
3026
|
+
}
|
|
3027
|
+
const batch = pending;
|
|
3028
|
+
return new Promise((resolve, reject) => {
|
|
3029
|
+
batch.parts.push({
|
|
3030
|
+
queries,
|
|
3031
|
+
start: batch.queries.length,
|
|
3032
|
+
resolve,
|
|
3033
|
+
reject
|
|
3034
|
+
});
|
|
3035
|
+
batch.queries.push(...queries);
|
|
3036
|
+
});
|
|
3037
|
+
}
|
|
3038
|
+
flush(batchKey) {
|
|
3039
|
+
const pending = this.pending.get(batchKey);
|
|
3040
|
+
if (!pending) {
|
|
3041
|
+
return;
|
|
3042
|
+
}
|
|
3043
|
+
this.pending.delete(batchKey);
|
|
3044
|
+
const { databaseId, parts } = pending;
|
|
3045
|
+
if (parts.length === 1) {
|
|
3046
|
+
this.send(databaseId, parts[0].queries).then(
|
|
3047
|
+
parts[0].resolve,
|
|
3048
|
+
parts[0].reject
|
|
3049
|
+
);
|
|
3050
|
+
return;
|
|
3051
|
+
}
|
|
3052
|
+
this.send(databaseId, pending.queries).then(
|
|
3053
|
+
(results) => {
|
|
3054
|
+
for (const part of parts) {
|
|
3055
|
+
part.resolve(
|
|
3056
|
+
results.slice(part.start, part.start + part.queries.length)
|
|
3057
|
+
);
|
|
3058
|
+
}
|
|
3059
|
+
},
|
|
3060
|
+
() => {
|
|
3061
|
+
for (const part of parts) {
|
|
3062
|
+
this.send(databaseId, part.queries).then(part.resolve, part.reject);
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
3065
|
+
);
|
|
3066
|
+
}
|
|
3067
|
+
};
|
|
3068
|
+
|
|
2878
3069
|
// src/files/store.ts
|
|
2879
3070
|
import { createHash, randomUUID } from "crypto";
|
|
2880
3071
|
import { createReadStream, createWriteStream } from "fs";
|
|
@@ -7879,15 +8070,25 @@ var MindStudioAgent = class {
|
|
|
7879
8070
|
}
|
|
7880
8071
|
return json;
|
|
7881
8072
|
}
|
|
8073
|
+
/** @internal Reads started together share a round trip (`db/readBatching.ts`). */
|
|
8074
|
+
_dbReads = new DbReadBatching(
|
|
8075
|
+
(databaseId, queries) => this._sendDbBatch(databaseId, queries)
|
|
8076
|
+
);
|
|
7882
8077
|
/**
|
|
7883
8078
|
* @internal Execute a batch of SQL queries against a managed database.
|
|
7884
|
-
* Used as the `executeBatch` callback for Table/Query instances.
|
|
7885
|
-
*
|
|
7886
|
-
* Calls `POST /_internal/v2/db/query` directly with the hook token
|
|
7887
|
-
* (raw, no Bearer prefix). All queries run on a single SQLite connection,
|
|
7888
|
-
* enabling RETURNING clauses and multi-statement batches.
|
|
8079
|
+
* Used as the `executeBatch` callback for Table/Query instances. Reads one
|
|
8080
|
+
* request starts in the same tick go to the platform as one batch.
|
|
7889
8081
|
*/
|
|
7890
8082
|
async _executeDbBatch(databaseId, queries) {
|
|
8083
|
+
return this._dbReads.run(this._token, databaseId, queries);
|
|
8084
|
+
}
|
|
8085
|
+
/**
|
|
8086
|
+
* @internal Send one batch: `POST /_internal/v2/db/query` directly with the
|
|
8087
|
+
* hook token (raw, no Bearer prefix), or its DB WebSocket. All queries run
|
|
8088
|
+
* on a single SQLite connection, enabling RETURNING clauses and
|
|
8089
|
+
* multi-statement batches.
|
|
8090
|
+
*/
|
|
8091
|
+
async _sendDbBatch(databaseId, queries) {
|
|
7891
8092
|
const dbWsUrl = typeof process !== "undefined" ? process.env?.DB_WS_URL : void 0;
|
|
7892
8093
|
if (dbWsUrl) {
|
|
7893
8094
|
try {
|
|
@@ -8033,12 +8234,7 @@ var MindStudioAgent = class {
|
|
|
8033
8234
|
return new Table(tableConfig);
|
|
8034
8235
|
},
|
|
8035
8236
|
// Time helpers work without context
|
|
8036
|
-
|
|
8037
|
-
days: (n) => n * 864e5,
|
|
8038
|
-
hours: (n) => n * 36e5,
|
|
8039
|
-
minutes: (n) => n * 6e4,
|
|
8040
|
-
ago: (ms) => Date.now() - ms,
|
|
8041
|
-
fromNow: (ms) => Date.now() + ms,
|
|
8237
|
+
...timeHelpers,
|
|
8042
8238
|
userRef: (id) => id.startsWith("@@user@@") ? id.slice("@@user@@".length) : id,
|
|
8043
8239
|
// Raw SQL — validate synchronously; database resolution needs context,
|
|
8044
8240
|
// so it happens inside the executeBatch closure at execution time
|