@mindstudio-ai/agent 0.1.162 → 0.1.164
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/CHANGELOG.md +47 -0
- package/dist/cli.js +1172 -1279
- package/dist/index.d.ts +91 -67
- package/dist/index.js +951 -723
- package/dist/index.js.map +1 -1
- package/dist/postinstall.js +1171 -1278
- package/package.json +9 -3
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) {
|
|
1324
|
-
return null;
|
|
1325
|
-
}
|
|
1326
|
-
if (!this.eat("rparen")) {
|
|
1356
|
+
if (!field || !this.eat("rparen")) {
|
|
1327
1357
|
return null;
|
|
1328
1358
|
}
|
|
1329
|
-
return { kind: "in", field, values
|
|
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,176 @@ 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 readsBackAsWritten(value) {
|
|
1598
|
+
if (typeof value !== "string") {
|
|
1599
|
+
return false;
|
|
1600
|
+
}
|
|
1601
|
+
try {
|
|
1602
|
+
JSON.parse(value);
|
|
1603
|
+
return false;
|
|
1604
|
+
} catch {
|
|
1605
|
+
return true;
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
function jsonColumnIn(field, values, negated, params) {
|
|
1609
|
+
params.push(JSON.stringify(values.flatMap((v) => [v, JSON.stringify(v)])));
|
|
1610
|
+
return withNulls(
|
|
1611
|
+
`${field.sql} ${negated ? "NOT IN" : "IN"} (SELECT value FROM json_each(?))`,
|
|
1612
|
+
field,
|
|
1613
|
+
negated
|
|
1614
|
+
);
|
|
1615
|
+
}
|
|
1616
|
+
function compileNode(node, negated, columns, params) {
|
|
1617
|
+
const column = (field) => field.nested ? void 0 : columns.find((c) => c.name === field.column);
|
|
1618
|
+
const stored = (field, value) => field.nested ? serializeColumnParam(value, "", []) : serializeColumnParam(value, field.column, columns);
|
|
1619
|
+
switch (node.kind) {
|
|
1620
|
+
case "logical": {
|
|
1621
|
+
const operator = negated ? node.operator === "AND" ? "OR" : "AND" : node.operator;
|
|
1622
|
+
const left = compileNode(node.left, negated, columns, params);
|
|
1623
|
+
const right = left && compileNode(node.right, negated, columns, params);
|
|
1624
|
+
return left && right ? `(${left} ${operator} ${right})` : null;
|
|
1625
|
+
}
|
|
1626
|
+
case "not":
|
|
1627
|
+
return compileNode(node.operand, !negated, columns, params);
|
|
1628
|
+
case "comparison": {
|
|
1629
|
+
if (node.field.nested) {
|
|
1630
|
+
return nestedComparison(node, negated, params);
|
|
1631
|
+
}
|
|
1632
|
+
const type = column(node.field)?.type;
|
|
1633
|
+
if (type === "json") {
|
|
1634
|
+
const strictEquals = node.op === "===" ? !negated : node.op === "!==" ? negated : null;
|
|
1635
|
+
return strictEquals !== null && readsBackAsWritten(node.value) ? jsonColumnIn(node.field, [node.value], !strictEquals, params) : null;
|
|
1636
|
+
}
|
|
1637
|
+
if (!type || typeof node.value !== VALUE_KIND[type]) {
|
|
1638
|
+
return null;
|
|
1639
|
+
}
|
|
1640
|
+
const op = negated ? NEGATED[node.op] : node.op;
|
|
1641
|
+
const nullsMatch = negated ? !jsCompare(null, node.op, node.value) : jsCompare(null, node.op, node.value);
|
|
1642
|
+
params.push(stored(node.field, node.value));
|
|
1643
|
+
if (SQL_OP[op] === "!=" && nullsMatch) {
|
|
1644
|
+
return `${node.field.sql} IS NOT ?`;
|
|
1645
|
+
}
|
|
1646
|
+
return withNulls(
|
|
1647
|
+
`${node.field.sql} ${SQL_OP[op]} ?`,
|
|
1648
|
+
node.field,
|
|
1649
|
+
nullsMatch
|
|
1650
|
+
);
|
|
1651
|
+
}
|
|
1652
|
+
case "nullCheck": {
|
|
1653
|
+
if (node.strict && node.field.nested) {
|
|
1654
|
+
return null;
|
|
1655
|
+
}
|
|
1656
|
+
const isNull = negated ? !node.isNull : node.isNull;
|
|
1657
|
+
if (column(node.field)?.type === "json") {
|
|
1658
|
+
return isNull ? `(${node.field.sql} IS NULL OR ${node.field.sql} = 'null')` : `(${node.field.sql} IS NOT NULL AND ${node.field.sql} <> 'null')`;
|
|
1659
|
+
}
|
|
1660
|
+
return `${node.field.sql} ${isNull ? "IS NULL" : "IS NOT NULL"}`;
|
|
1661
|
+
}
|
|
1662
|
+
case "in": {
|
|
1663
|
+
const type = column(node.field)?.type;
|
|
1664
|
+
if (type === "json") {
|
|
1665
|
+
return node.values.every(readsBackAsWritten) ? jsonColumnIn(node.field, node.values, negated, params) : null;
|
|
1666
|
+
}
|
|
1667
|
+
if (!type || node.values.some(
|
|
1668
|
+
(v) => v !== null && v !== void 0 && typeof v !== VALUE_KIND[type]
|
|
1669
|
+
)) {
|
|
1670
|
+
return null;
|
|
1671
|
+
}
|
|
1672
|
+
const listed = node.values.filter((v) => v !== null && v !== void 0);
|
|
1673
|
+
const nullsMatch = negated ? !node.values.includes(null) : node.values.includes(null);
|
|
1674
|
+
let core;
|
|
1675
|
+
if (listed.length === 0) {
|
|
1676
|
+
core = negated ? `${node.field.sql} IS NOT NULL` : "0";
|
|
1677
|
+
} else {
|
|
1678
|
+
params.push(JSON.stringify(listed.map((v) => stored(node.field, v))));
|
|
1679
|
+
core = `${node.field.sql} ${negated ? "NOT IN" : "IN"} (SELECT value FROM json_each(?))`;
|
|
1680
|
+
}
|
|
1681
|
+
return withNulls(core, node.field, nullsMatch);
|
|
1682
|
+
}
|
|
1683
|
+
case "includes": {
|
|
1684
|
+
const type = column(node.field)?.type;
|
|
1685
|
+
if (type === "json") {
|
|
1686
|
+
const col = node.field.sql;
|
|
1687
|
+
let element;
|
|
1688
|
+
if (typeof node.needle === "boolean") {
|
|
1689
|
+
element = `type = '${node.needle}'`;
|
|
1690
|
+
} else {
|
|
1691
|
+
params.push(node.needle);
|
|
1692
|
+
element = `type IN ${JSON_TYPES[typeof node.needle]} AND value = ?`;
|
|
1693
|
+
}
|
|
1694
|
+
const substring = `${negated ? "=" : ">"} 0`;
|
|
1695
|
+
params.push(String(node.needle), String(node.needle));
|
|
1696
|
+
return `(CASE WHEN json_valid(${col}) THEN CASE json_type(${col}) WHEN 'array' THEN ${negated ? "NOT EXISTS" : "EXISTS"} (SELECT 1 FROM json_each(${col}) WHERE ${element}) WHEN 'text' THEN instr(${col} ->> '$', ?) ${substring} ELSE 0 END ELSE instr(${col}, ?) ${substring} END)`;
|
|
1697
|
+
}
|
|
1698
|
+
if ((type === "text" || type === "user") && typeof node.needle === "string") {
|
|
1699
|
+
params.push(node.needle);
|
|
1700
|
+
return `instr(${node.field.sql}, ?) ${negated ? "=" : ">"} 0`;
|
|
1701
|
+
}
|
|
1702
|
+
return null;
|
|
1703
|
+
}
|
|
1704
|
+
case "truthy": {
|
|
1705
|
+
const type = column(node.field)?.type;
|
|
1706
|
+
if (node.field.nested) {
|
|
1707
|
+
return negated ? `(${node.field.sql} IN (0, '') OR ${node.field.sql} IS NULL)` : `${node.field.sql} NOT IN (0, '')`;
|
|
1708
|
+
}
|
|
1709
|
+
switch (type) {
|
|
1710
|
+
case "boolean":
|
|
1711
|
+
return negated ? `${node.field.sql} IS NOT 1` : `${node.field.sql} = 1`;
|
|
1712
|
+
case "number":
|
|
1713
|
+
return negated ? `(${node.field.sql} = 0 OR ${node.field.sql} IS NULL)` : `${node.field.sql} <> 0`;
|
|
1714
|
+
case "text":
|
|
1715
|
+
case "user":
|
|
1716
|
+
return negated ? `(${node.field.sql} = '' OR ${node.field.sql} IS NULL)` : `${node.field.sql} <> ''`;
|
|
1717
|
+
default:
|
|
1718
|
+
return null;
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1524
1722
|
}
|
|
1525
1723
|
|
|
1526
1724
|
// src/db/aggregate.ts
|
|
@@ -1631,24 +1829,6 @@ function aggregateValues(fn, values) {
|
|
|
1631
1829
|
return present.length === 0 ? null : present.reduce((m, v) => v > m ? v : m);
|
|
1632
1830
|
}
|
|
1633
1831
|
}
|
|
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
1832
|
function computeAggregateInJs(rows, spec) {
|
|
1653
1833
|
const groups = /* @__PURE__ */ new Map();
|
|
1654
1834
|
for (const row of rows) {
|
|
@@ -1678,7 +1858,7 @@ function computeAggregateInJs(rows, spec) {
|
|
|
1678
1858
|
});
|
|
1679
1859
|
if (spec.orderBy) {
|
|
1680
1860
|
const key = spec.orderBy;
|
|
1681
|
-
out.sort((a, b) =>
|
|
1861
|
+
out.sort((a, b) => compareAsSqlite(a[key], b[key]));
|
|
1682
1862
|
if (spec.desc) {
|
|
1683
1863
|
out.reverse();
|
|
1684
1864
|
}
|
|
@@ -1732,6 +1912,8 @@ var Query = class _Query {
|
|
|
1732
1912
|
_rawWhereParams;
|
|
1733
1913
|
/** @internal See AggregateState — set by aggregate terminals. */
|
|
1734
1914
|
_aggregate;
|
|
1915
|
+
/** @internal The columns `select()` named; every column when unset. */
|
|
1916
|
+
_columns;
|
|
1735
1917
|
/** @internal Post-process transform applied after row deserialization. */
|
|
1736
1918
|
_postProcess;
|
|
1737
1919
|
constructor(config, options) {
|
|
@@ -1745,6 +1927,7 @@ var Query = class _Query {
|
|
|
1745
1927
|
this._rawWhere = options?.rawWhere;
|
|
1746
1928
|
this._rawWhereParams = options?.rawWhereParams;
|
|
1747
1929
|
this._aggregate = options?.aggregate;
|
|
1930
|
+
this._columns = options?.columns;
|
|
1748
1931
|
}
|
|
1749
1932
|
_clone(overrides) {
|
|
1750
1933
|
return new _Query(this._config, {
|
|
@@ -1756,7 +1939,8 @@ var Query = class _Query {
|
|
|
1756
1939
|
postProcess: overrides.postProcess,
|
|
1757
1940
|
rawWhere: this._rawWhere,
|
|
1758
1941
|
rawWhereParams: this._rawWhereParams,
|
|
1759
|
-
aggregate: overrides.aggregate ?? this._aggregate
|
|
1942
|
+
aggregate: overrides.aggregate ?? this._aggregate,
|
|
1943
|
+
columns: overrides.columns ?? this._columns
|
|
1760
1944
|
});
|
|
1761
1945
|
}
|
|
1762
1946
|
filter(predicate, bindings) {
|
|
@@ -1776,6 +1960,21 @@ var Query = class _Query {
|
|
|
1776
1960
|
skip(n) {
|
|
1777
1961
|
return this._clone({ offset: n });
|
|
1778
1962
|
}
|
|
1963
|
+
/**
|
|
1964
|
+
* Return only these columns: `SELECT id, name` rather than every column of
|
|
1965
|
+
* a wide row. Chain it before a terminal (`first()`, `take()`); filters and
|
|
1966
|
+
* sorts still see the whole row.
|
|
1967
|
+
*
|
|
1968
|
+
* ```ts
|
|
1969
|
+
* const names = await Companies
|
|
1970
|
+
* .filter((c, $) => c.firmId === $.firmId, { firmId })
|
|
1971
|
+
* .select('id', 'name');
|
|
1972
|
+
* // Array<{ id: string; name: string }>
|
|
1973
|
+
* ```
|
|
1974
|
+
*/
|
|
1975
|
+
select(...columns) {
|
|
1976
|
+
return this._clone({ columns });
|
|
1977
|
+
}
|
|
1779
1978
|
// -------------------------------------------------------------------------
|
|
1780
1979
|
// Terminal methods
|
|
1781
1980
|
// -------------------------------------------------------------------------
|
|
@@ -1863,8 +2062,8 @@ var Query = class _Query {
|
|
|
1863
2062
|
if (compiled.allSql && compiled.sqlWhere) {
|
|
1864
2063
|
const query = buildExists(
|
|
1865
2064
|
this._config.tableName,
|
|
1866
|
-
`NOT (${compiled.sqlWhere})`,
|
|
1867
|
-
|
|
2065
|
+
`NOT coalesce(${compiled.sqlWhere}, 0)`,
|
|
2066
|
+
compiled.params,
|
|
1868
2067
|
true
|
|
1869
2068
|
);
|
|
1870
2069
|
const results = await this._config.executeBatch([query]);
|
|
@@ -1906,116 +2105,152 @@ var Query = class _Query {
|
|
|
1906
2105
|
// Batch compilation — used by db.batch() to extract SQL without executing
|
|
1907
2106
|
// -------------------------------------------------------------------------
|
|
1908
2107
|
/**
|
|
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.
|
|
2108
|
+
* @internal Compile this query into the statement to run, plus whatever
|
|
2109
|
+
* JS has to do with its rows (see the module header's three cases). Used by
|
|
2110
|
+
* db.batch() and by a standalone await alike.
|
|
1915
2111
|
*/
|
|
1916
2112
|
_compile() {
|
|
2113
|
+
const table = this._config.tableName;
|
|
1917
2114
|
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
2115
|
return {
|
|
1926
2116
|
type: "query",
|
|
1927
|
-
query,
|
|
1928
|
-
|
|
2117
|
+
query: buildSelect(table, {
|
|
2118
|
+
columns: this._columns,
|
|
2119
|
+
where: this._rawWhere,
|
|
2120
|
+
whereParams: this._rawWhereParams,
|
|
2121
|
+
limit: this._limit,
|
|
2122
|
+
offset: this._offset
|
|
2123
|
+
}),
|
|
1929
2124
|
config: this._config,
|
|
1930
2125
|
postProcess: this._postProcess
|
|
1931
2126
|
};
|
|
1932
2127
|
}
|
|
1933
2128
|
const compiled = this._compilePredicates();
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
2129
|
+
if (!compiled.allSql) {
|
|
2130
|
+
const reasons = compiled.compiled.flatMap(
|
|
2131
|
+
(c) => c.type === "js" && c.reason ? [c.reason] : []
|
|
2132
|
+
);
|
|
2133
|
+
return {
|
|
2134
|
+
type: "query",
|
|
2135
|
+
query: buildSelect(table, {
|
|
2136
|
+
orderBy: !this._sortAccessor && this._reversed ? "rowid" : void 0
|
|
2137
|
+
}),
|
|
2138
|
+
config: this._config,
|
|
2139
|
+
inJs: {
|
|
2140
|
+
why: `filter could not be compiled to SQL${reasons.length ? ` (${reasons.join("; ")})` : ""}`,
|
|
2141
|
+
predicates: this._predicates,
|
|
2142
|
+
sortAccessor: this._sortAccessor,
|
|
2143
|
+
reversed: this._reversed,
|
|
2144
|
+
limit: this._limit,
|
|
2145
|
+
offset: this._offset,
|
|
2146
|
+
columns: this._columns
|
|
2147
|
+
},
|
|
2148
|
+
postProcess: this._postProcess
|
|
2149
|
+
};
|
|
2150
|
+
}
|
|
2151
|
+
const agg = this._compileAggregateSql(compiled.sqlWhere, compiled.params);
|
|
2152
|
+
if (agg) {
|
|
2153
|
+
return {
|
|
2154
|
+
type: "query",
|
|
2155
|
+
query: agg.query,
|
|
2156
|
+
config: this._config,
|
|
2157
|
+
processRaw: agg.processRaw
|
|
2158
|
+
};
|
|
2159
|
+
}
|
|
2160
|
+
const sortField = this._sortAccessor ? extractFieldName(this._sortAccessor) : null;
|
|
2161
|
+
if (this._sortAccessor && sortField == null) {
|
|
1953
2162
|
return {
|
|
1954
2163
|
type: "query",
|
|
1955
|
-
query,
|
|
1956
|
-
|
|
2164
|
+
query: buildSelect(table, {
|
|
2165
|
+
where: compiled.sqlWhere || void 0,
|
|
2166
|
+
whereParams: compiled.params
|
|
2167
|
+
}),
|
|
1957
2168
|
config: this._config,
|
|
2169
|
+
inJs: {
|
|
2170
|
+
why: "sortBy is computed, not a column, so it sorts in JS",
|
|
2171
|
+
sortAccessor: this._sortAccessor,
|
|
2172
|
+
reversed: this._reversed,
|
|
2173
|
+
limit: this._limit,
|
|
2174
|
+
offset: this._offset,
|
|
2175
|
+
columns: this._columns
|
|
2176
|
+
},
|
|
1958
2177
|
postProcess: this._postProcess
|
|
1959
2178
|
};
|
|
1960
2179
|
}
|
|
1961
|
-
const fallbackQuery = buildSelect(this._config.tableName);
|
|
1962
2180
|
return {
|
|
1963
2181
|
type: "query",
|
|
1964
|
-
query:
|
|
1965
|
-
|
|
2182
|
+
query: buildSelect(table, {
|
|
2183
|
+
columns: this._columns,
|
|
2184
|
+
where: compiled.sqlWhere || void 0,
|
|
2185
|
+
whereParams: compiled.params,
|
|
2186
|
+
orderBy: sortField ?? (this._reversed ? "rowid" : void 0),
|
|
2187
|
+
desc: this._reversed,
|
|
2188
|
+
limit: this._limit,
|
|
2189
|
+
offset: this._offset
|
|
2190
|
+
}),
|
|
1966
2191
|
config: this._config,
|
|
1967
|
-
predicates: this._predicates,
|
|
1968
|
-
sortAccessor: this._sortAccessor,
|
|
1969
|
-
reversed: this._reversed,
|
|
1970
|
-
limit: this._limit,
|
|
1971
|
-
offset: this._offset,
|
|
1972
2192
|
postProcess: this._postProcess
|
|
1973
2193
|
};
|
|
1974
2194
|
}
|
|
1975
2195
|
/**
|
|
1976
2196
|
* @internal Process raw SQL results into typed rows. Used by db.batch()
|
|
1977
|
-
* after executing the compiled query.
|
|
2197
|
+
* after executing the compiled query, and by a standalone await.
|
|
1978
2198
|
*
|
|
1979
|
-
*
|
|
1980
|
-
*
|
|
2199
|
+
* Deserializes the rows, then does in JS whatever the SQL couldn't
|
|
2200
|
+
* (`inJs`): the filter, the sort, the page, the projection.
|
|
1981
2201
|
*/
|
|
1982
2202
|
static _processResults(result, compiled) {
|
|
1983
2203
|
if (compiled.processRaw) {
|
|
1984
2204
|
return compiled.processRaw(result);
|
|
1985
2205
|
}
|
|
1986
|
-
const
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
compiled.config.columns
|
|
1990
|
-
)
|
|
2206
|
+
const columns = compiled.config.columns;
|
|
2207
|
+
let rows = result.rows.map(
|
|
2208
|
+
(row) => deserializeRow(row, columns)
|
|
1991
2209
|
);
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2210
|
+
const inJs = compiled.inJs;
|
|
2211
|
+
if (inJs) {
|
|
2212
|
+
if (inJs.predicates?.length) {
|
|
2213
|
+
rows = rows.filter(
|
|
2214
|
+
(row) => inJs.predicates.every((p) => p.fn(row, p.bindings))
|
|
2215
|
+
);
|
|
2216
|
+
}
|
|
2217
|
+
if (inJs.sortAccessor) {
|
|
2218
|
+
const accessor = inJs.sortAccessor;
|
|
2219
|
+
const direction = inJs.reversed ? -1 : 1;
|
|
2220
|
+
rows.sort(
|
|
2221
|
+
(a, b) => direction * compareAsSqlite(accessor(a), accessor(b))
|
|
2222
|
+
);
|
|
2223
|
+
} else if (inJs.reversed) {
|
|
2224
|
+
rows.reverse();
|
|
2225
|
+
}
|
|
2226
|
+
if (inJs.offset != null || inJs.limit != null) {
|
|
2227
|
+
const start = inJs.offset ?? 0;
|
|
2228
|
+
const end = inJs.limit != null ? start + inJs.limit : void 0;
|
|
2229
|
+
rows = rows.slice(start, end);
|
|
2230
|
+
}
|
|
2231
|
+
if (inJs.columns?.length) {
|
|
2232
|
+
const keep = inJs.columns;
|
|
2233
|
+
rows = rows.map(
|
|
2234
|
+
(row) => Object.fromEntries(
|
|
2235
|
+
keep.map((column) => [
|
|
2236
|
+
column,
|
|
2237
|
+
row[column]
|
|
2238
|
+
])
|
|
2239
|
+
)
|
|
2240
|
+
);
|
|
2241
|
+
}
|
|
2012
2242
|
}
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2243
|
+
return compiled.postProcess ? compiled.postProcess(rows) : rows;
|
|
2244
|
+
}
|
|
2245
|
+
/** @internal Say that part of a query ran in JS, and why. */
|
|
2246
|
+
static _warnInJs(compiled, rowsRead) {
|
|
2247
|
+
if (!compiled.inJs) {
|
|
2248
|
+
return;
|
|
2017
2249
|
}
|
|
2018
|
-
|
|
2250
|
+
const read = rowsRead == null ? "" : ` (${rowsRead} rows read)`;
|
|
2251
|
+
console.warn(
|
|
2252
|
+
`[mindstudio] Query on '${compiled.config.tableName}': ${compiled.inJs.why} \u2014 processing in JS${read}`
|
|
2253
|
+
);
|
|
2019
2254
|
}
|
|
2020
2255
|
// -------------------------------------------------------------------------
|
|
2021
2256
|
// PromiseLike
|
|
@@ -2025,18 +2260,10 @@ var Query = class _Query {
|
|
|
2025
2260
|
}
|
|
2026
2261
|
async _run() {
|
|
2027
2262
|
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;
|
|
2263
|
+
const compiled = this._compile();
|
|
2264
|
+
const results = await this._config.executeBatch([compiled.query]);
|
|
2265
|
+
_Query._warnInJs(compiled, results[0]?.rows.length);
|
|
2266
|
+
return _Query._processResults(results[0], compiled);
|
|
2040
2267
|
}
|
|
2041
2268
|
/**
|
|
2042
2269
|
* @internal Compile the pending aggregate into a single SQL statement +
|
|
@@ -2047,7 +2274,7 @@ var Query = class _Query {
|
|
|
2047
2274
|
* when a scalar accessor doesn't extract to a column (computed accessors
|
|
2048
2275
|
* stay correct via JS, with a warning to prompt optimization).
|
|
2049
2276
|
*/
|
|
2050
|
-
_compileAggregateSql(sqlWhere) {
|
|
2277
|
+
_compileAggregateSql(sqlWhere, whereParams) {
|
|
2051
2278
|
const agg = this._aggregate;
|
|
2052
2279
|
if (!agg || this._rawWhere || this._limit != null || this._offset != null) {
|
|
2053
2280
|
return null;
|
|
@@ -2068,7 +2295,8 @@ var Query = class _Query {
|
|
|
2068
2295
|
this._config.tableName,
|
|
2069
2296
|
agg.fn,
|
|
2070
2297
|
column,
|
|
2071
|
-
where
|
|
2298
|
+
where,
|
|
2299
|
+
whereParams
|
|
2072
2300
|
);
|
|
2073
2301
|
const processRaw2 = (result) => {
|
|
2074
2302
|
const value = result.rows[0]?.value;
|
|
@@ -2084,6 +2312,7 @@ var Query = class _Query {
|
|
|
2084
2312
|
by: spec.by,
|
|
2085
2313
|
select: spec.select,
|
|
2086
2314
|
where,
|
|
2315
|
+
whereParams,
|
|
2087
2316
|
orderBy: spec.orderBy,
|
|
2088
2317
|
desc: spec.desc,
|
|
2089
2318
|
limit: spec.limit
|
|
@@ -2104,89 +2333,22 @@ var Query = class _Query {
|
|
|
2104
2333
|
// -------------------------------------------------------------------------
|
|
2105
2334
|
// Execution internals
|
|
2106
2335
|
// -------------------------------------------------------------------------
|
|
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
2336
|
_compilePredicates() {
|
|
2164
2337
|
if (this._predicates.length === 0) {
|
|
2165
|
-
return { allSql: true, sqlWhere: "", compiled: [] };
|
|
2338
|
+
return { allSql: true, sqlWhere: "", params: [], compiled: [] };
|
|
2166
2339
|
}
|
|
2340
|
+
const columns = this._config.columns;
|
|
2167
2341
|
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))
|
|
2342
|
+
(p) => compilePredicate(p.fn, p.bindings, columns)
|
|
2189
2343
|
);
|
|
2344
|
+
const sql = compiled.flatMap((c) => c.type === "sql" ? [c] : []);
|
|
2345
|
+
const allSql = sql.length === compiled.length;
|
|
2346
|
+
return {
|
|
2347
|
+
allSql,
|
|
2348
|
+
sqlWhere: allSql ? sql.map((c) => c.where).join(" AND ") : "",
|
|
2349
|
+
params: allSql ? sql.flatMap((c) => c.params) : [],
|
|
2350
|
+
compiled
|
|
2351
|
+
};
|
|
2190
2352
|
}
|
|
2191
2353
|
async _fetchAllRows() {
|
|
2192
2354
|
const query = buildSelect(this._config.tableName);
|
|
@@ -2205,38 +2367,20 @@ function extractFieldName(accessor) {
|
|
|
2205
2367
|
}
|
|
2206
2368
|
|
|
2207
2369
|
// src/db/mutation.ts
|
|
2208
|
-
var Mutation = class
|
|
2370
|
+
var Mutation = class {
|
|
2209
2371
|
/** @internal */
|
|
2210
2372
|
_config;
|
|
2211
|
-
/** @internal */
|
|
2373
|
+
/** @internal The statements, or null when the write can't be one batch. */
|
|
2212
2374
|
_build;
|
|
2213
2375
|
/** @internal */
|
|
2214
2376
|
_processResult;
|
|
2215
|
-
/** @internal
|
|
2216
|
-
|
|
2217
|
-
constructor(config, build, processResult) {
|
|
2377
|
+
/** @internal What runs when `_build` returns null (e.g. removeAll's JS fallback). */
|
|
2378
|
+
_fallback;
|
|
2379
|
+
constructor(config, build, processResult, fallback) {
|
|
2218
2380
|
this._config = config;
|
|
2219
2381
|
this._build = build;
|
|
2220
2382
|
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;
|
|
2383
|
+
this._fallback = fallback;
|
|
2240
2384
|
}
|
|
2241
2385
|
// -------------------------------------------------------------------------
|
|
2242
2386
|
// PromiseLike — executes on await
|
|
@@ -2254,10 +2398,11 @@ var Mutation = class _Mutation {
|
|
|
2254
2398
|
* @internal Compile this mutation into SQL for batch execution.
|
|
2255
2399
|
* Returns the queries and a result processor.
|
|
2256
2400
|
*
|
|
2257
|
-
* Throws if
|
|
2401
|
+
* Throws if it can't be expressed as SQL (its builder returned null).
|
|
2258
2402
|
*/
|
|
2259
2403
|
_compile() {
|
|
2260
|
-
|
|
2404
|
+
const queries = this._build();
|
|
2405
|
+
if (!queries) {
|
|
2261
2406
|
throw new MindStudioError(
|
|
2262
2407
|
"This operation cannot be batched (e.g. removeAll with a JS-fallback predicate). Await it separately instead of passing to db.batch().",
|
|
2263
2408
|
"not_batchable",
|
|
@@ -2266,7 +2411,7 @@ var Mutation = class _Mutation {
|
|
|
2266
2411
|
}
|
|
2267
2412
|
return {
|
|
2268
2413
|
type: "mutation",
|
|
2269
|
-
queries
|
|
2414
|
+
queries,
|
|
2270
2415
|
config: this._config,
|
|
2271
2416
|
processResult: this._processResult
|
|
2272
2417
|
};
|
|
@@ -2283,10 +2428,11 @@ var Mutation = class _Mutation {
|
|
|
2283
2428
|
// -------------------------------------------------------------------------
|
|
2284
2429
|
async _execute() {
|
|
2285
2430
|
await this._config.ready?.();
|
|
2286
|
-
|
|
2287
|
-
|
|
2431
|
+
const queries = this._build();
|
|
2432
|
+
if (!queries) {
|
|
2433
|
+
return this._fallback();
|
|
2288
2434
|
}
|
|
2289
|
-
const results = await this._config.executeBatch(
|
|
2435
|
+
const results = await this._config.executeBatch(queries);
|
|
2290
2436
|
return this._processResult(results);
|
|
2291
2437
|
}
|
|
2292
2438
|
};
|
|
@@ -2390,6 +2536,10 @@ var Table = class {
|
|
|
2390
2536
|
sortBy(accessor) {
|
|
2391
2537
|
return new Query(this._config).sortBy(accessor);
|
|
2392
2538
|
}
|
|
2539
|
+
/** Every row, with only these columns. See Query.select(). */
|
|
2540
|
+
select(...columns) {
|
|
2541
|
+
return new Query(this._config).select(...columns);
|
|
2542
|
+
}
|
|
2393
2543
|
push(data) {
|
|
2394
2544
|
const isArray = Array.isArray(data);
|
|
2395
2545
|
const items = (isArray ? data : [data]).map(
|
|
@@ -2460,42 +2610,47 @@ var Table = class {
|
|
|
2460
2610
|
);
|
|
2461
2611
|
}
|
|
2462
2612
|
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
|
-
|
|
2613
|
+
return new Mutation(
|
|
2614
|
+
this._config,
|
|
2615
|
+
() => {
|
|
2616
|
+
const compiled = compilePredicate(
|
|
2617
|
+
predicate,
|
|
2618
|
+
bindings,
|
|
2619
|
+
this._config.columns
|
|
2620
|
+
);
|
|
2621
|
+
return compiled.type === "sql" ? [
|
|
2622
|
+
buildDelete(
|
|
2623
|
+
this._config.tableName,
|
|
2624
|
+
compiled.where,
|
|
2625
|
+
compiled.params
|
|
2626
|
+
)
|
|
2627
|
+
] : null;
|
|
2628
|
+
},
|
|
2629
|
+
(results) => results[0].changes,
|
|
2630
|
+
// Fallback: multi-step execution — not batchable
|
|
2631
|
+
async () => {
|
|
2632
|
+
console.warn(
|
|
2633
|
+
`[mindstudio] removeAll predicate on ${this._config.tableName} could not be compiled to SQL \u2014 fetching all rows first`
|
|
2634
|
+
);
|
|
2635
|
+
const allQuery = buildSelect(this._config.tableName);
|
|
2636
|
+
const allResults = await this._config.executeBatch([allQuery]);
|
|
2637
|
+
const columns = this._config.columns;
|
|
2638
|
+
const allRows = allResults[0].rows.map(
|
|
2639
|
+
(r) => deserializeRow(r, columns)
|
|
2640
|
+
);
|
|
2641
|
+
const matching = allRows.filter((row) => predicate(row, bindings));
|
|
2642
|
+
if (matching.length === 0) {
|
|
2643
|
+
return 0;
|
|
2644
|
+
}
|
|
2645
|
+
const deleteQueries = matching.filter((row) => row.id).map(
|
|
2646
|
+
(row) => buildDelete(this._config.tableName, `id = ?`, [row.id])
|
|
2647
|
+
);
|
|
2648
|
+
if (deleteQueries.length > 0) {
|
|
2649
|
+
await this._config.executeBatch(deleteQueries);
|
|
2650
|
+
}
|
|
2651
|
+
return matching.length;
|
|
2496
2652
|
}
|
|
2497
|
-
|
|
2498
|
-
});
|
|
2653
|
+
);
|
|
2499
2654
|
}
|
|
2500
2655
|
clear() {
|
|
2501
2656
|
const query = buildDelete(this._config.tableName);
|
|
@@ -2700,12 +2855,7 @@ function createDb(databases, executeBatch, authConfig, syncRoles) {
|
|
|
2700
2855
|
},
|
|
2701
2856
|
// --- Time helpers ---
|
|
2702
2857
|
// 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,
|
|
2858
|
+
...timeHelpers,
|
|
2709
2859
|
// --- User references ---
|
|
2710
2860
|
userRef: (id) => id.startsWith(USER_PREFIX) ? id.slice(USER_PREFIX.length) : id,
|
|
2711
2861
|
// --- Raw SQL (read-only) ---
|
|
@@ -2745,8 +2895,7 @@ function createDb(databases, executeBatch, authConfig, syncRoles) {
|
|
|
2745
2895
|
groups.set(dbId, []);
|
|
2746
2896
|
}
|
|
2747
2897
|
if (c.type === "query") {
|
|
2748
|
-
|
|
2749
|
-
groups.get(dbId).push({ opIndex: i, sqlQueries: [sqlQuery] });
|
|
2898
|
+
groups.get(dbId).push({ opIndex: i, sqlQueries: [c.query] });
|
|
2750
2899
|
} else if (c.type === "raw") {
|
|
2751
2900
|
groups.get(dbId).push({ opIndex: i, sqlQueries: [c.query] });
|
|
2752
2901
|
} else {
|
|
@@ -2775,11 +2924,7 @@ function createDb(databases, executeBatch, authConfig, syncRoles) {
|
|
|
2775
2924
|
return compiled.map((c, i) => {
|
|
2776
2925
|
const results = opResults.get(i);
|
|
2777
2926
|
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
|
-
}
|
|
2927
|
+
Query._warnInJs(c, results[0]?.rows.length);
|
|
2783
2928
|
return Query._processResults(results[0], c);
|
|
2784
2929
|
} else if (c.type === "raw") {
|
|
2785
2930
|
return RawQuery._processResults(results[0], c);
|
|
@@ -2875,6 +3020,84 @@ function resolveTable(databases, tableName, databaseHint) {
|
|
|
2875
3020
|
);
|
|
2876
3021
|
}
|
|
2877
3022
|
|
|
3023
|
+
// src/db/readBatching.ts
|
|
3024
|
+
var nextTick = typeof setImmediate === "function" ? (fn) => setImmediate(fn) : (fn) => setTimeout(fn, 0);
|
|
3025
|
+
var DbReadBatching = class {
|
|
3026
|
+
constructor(send) {
|
|
3027
|
+
this.send = send;
|
|
3028
|
+
}
|
|
3029
|
+
send;
|
|
3030
|
+
pending = /* @__PURE__ */ new Map();
|
|
3031
|
+
inFlight = /* @__PURE__ */ new Map();
|
|
3032
|
+
/** Run `queries` for the request holding `token`, sharing a round trip where it can. */
|
|
3033
|
+
async run(token, databaseId, queries) {
|
|
3034
|
+
const batchKey = `${token}\0${databaseId}`;
|
|
3035
|
+
if (queries.length === 0 || !queries.every((q) => isReadOnlySql(q.sql))) {
|
|
3036
|
+
this.flush(batchKey);
|
|
3037
|
+
return this.send(databaseId, queries);
|
|
3038
|
+
}
|
|
3039
|
+
const readKey = `${batchKey}\0${JSON.stringify(queries)}`;
|
|
3040
|
+
const shared = this.inFlight.get(readKey);
|
|
3041
|
+
if (shared) {
|
|
3042
|
+
return structuredClone(await shared);
|
|
3043
|
+
}
|
|
3044
|
+
const read = this.queue(batchKey, databaseId, queries);
|
|
3045
|
+
this.inFlight.set(readKey, read);
|
|
3046
|
+
try {
|
|
3047
|
+
return await read;
|
|
3048
|
+
} finally {
|
|
3049
|
+
this.inFlight.delete(readKey);
|
|
3050
|
+
}
|
|
3051
|
+
}
|
|
3052
|
+
queue(batchKey, databaseId, queries) {
|
|
3053
|
+
let pending = this.pending.get(batchKey);
|
|
3054
|
+
if (!pending) {
|
|
3055
|
+
pending = { databaseId, queries: [], parts: [] };
|
|
3056
|
+
this.pending.set(batchKey, pending);
|
|
3057
|
+
nextTick(() => this.flush(batchKey));
|
|
3058
|
+
}
|
|
3059
|
+
const batch = pending;
|
|
3060
|
+
return new Promise((resolve, reject) => {
|
|
3061
|
+
batch.parts.push({
|
|
3062
|
+
queries,
|
|
3063
|
+
start: batch.queries.length,
|
|
3064
|
+
resolve,
|
|
3065
|
+
reject
|
|
3066
|
+
});
|
|
3067
|
+
batch.queries.push(...queries);
|
|
3068
|
+
});
|
|
3069
|
+
}
|
|
3070
|
+
flush(batchKey) {
|
|
3071
|
+
const pending = this.pending.get(batchKey);
|
|
3072
|
+
if (!pending) {
|
|
3073
|
+
return;
|
|
3074
|
+
}
|
|
3075
|
+
this.pending.delete(batchKey);
|
|
3076
|
+
const { databaseId, parts } = pending;
|
|
3077
|
+
if (parts.length === 1) {
|
|
3078
|
+
this.send(databaseId, parts[0].queries).then(
|
|
3079
|
+
parts[0].resolve,
|
|
3080
|
+
parts[0].reject
|
|
3081
|
+
);
|
|
3082
|
+
return;
|
|
3083
|
+
}
|
|
3084
|
+
this.send(databaseId, pending.queries).then(
|
|
3085
|
+
(results) => {
|
|
3086
|
+
for (const part of parts) {
|
|
3087
|
+
part.resolve(
|
|
3088
|
+
results.slice(part.start, part.start + part.queries.length)
|
|
3089
|
+
);
|
|
3090
|
+
}
|
|
3091
|
+
},
|
|
3092
|
+
() => {
|
|
3093
|
+
for (const part of parts) {
|
|
3094
|
+
this.send(databaseId, part.queries).then(part.resolve, part.reject);
|
|
3095
|
+
}
|
|
3096
|
+
}
|
|
3097
|
+
);
|
|
3098
|
+
}
|
|
3099
|
+
};
|
|
3100
|
+
|
|
2878
3101
|
// src/files/store.ts
|
|
2879
3102
|
import { createHash, randomUUID } from "crypto";
|
|
2880
3103
|
import { createReadStream, createWriteStream } from "fs";
|
|
@@ -7879,15 +8102,25 @@ var MindStudioAgent = class {
|
|
|
7879
8102
|
}
|
|
7880
8103
|
return json;
|
|
7881
8104
|
}
|
|
8105
|
+
/** @internal Reads started together share a round trip (`db/readBatching.ts`). */
|
|
8106
|
+
_dbReads = new DbReadBatching(
|
|
8107
|
+
(databaseId, queries) => this._sendDbBatch(databaseId, queries)
|
|
8108
|
+
);
|
|
7882
8109
|
/**
|
|
7883
8110
|
* @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.
|
|
8111
|
+
* Used as the `executeBatch` callback for Table/Query instances. Reads one
|
|
8112
|
+
* request starts in the same tick go to the platform as one batch.
|
|
7889
8113
|
*/
|
|
7890
8114
|
async _executeDbBatch(databaseId, queries) {
|
|
8115
|
+
return this._dbReads.run(this._token, databaseId, queries);
|
|
8116
|
+
}
|
|
8117
|
+
/**
|
|
8118
|
+
* @internal Send one batch: `POST /_internal/v2/db/query` directly with the
|
|
8119
|
+
* hook token (raw, no Bearer prefix), or its DB WebSocket. All queries run
|
|
8120
|
+
* on a single SQLite connection, enabling RETURNING clauses and
|
|
8121
|
+
* multi-statement batches.
|
|
8122
|
+
*/
|
|
8123
|
+
async _sendDbBatch(databaseId, queries) {
|
|
7891
8124
|
const dbWsUrl = typeof process !== "undefined" ? process.env?.DB_WS_URL : void 0;
|
|
7892
8125
|
if (dbWsUrl) {
|
|
7893
8126
|
try {
|
|
@@ -8033,12 +8266,7 @@ var MindStudioAgent = class {
|
|
|
8033
8266
|
return new Table(tableConfig);
|
|
8034
8267
|
},
|
|
8035
8268
|
// 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,
|
|
8269
|
+
...timeHelpers,
|
|
8042
8270
|
userRef: (id) => id.startsWith("@@user@@") ? id.slice("@@user@@".length) : id,
|
|
8043
8271
|
// Raw SQL — validate synchronously; database resolution needs context,
|
|
8044
8272
|
// so it happens inside the executeBatch closure at execution time
|