@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/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.find((c) => c.name === columnName);
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
- function escapeValue(val) {
582
- if (val === null || val === void 0) {
583
- return "NULL";
584
- }
585
- if (typeof val === "boolean") {
586
- return val ? "1" : "0";
587
- }
588
- if (typeof val === "number") {
589
- return String(val);
590
- }
591
- if (typeof val === "string") {
592
- return `'${val.replace(/'/g, "''")}'`;
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
- const json = JSON.stringify(val);
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 = columns.find((c) => c.name === key);
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
- let sql = `SELECT * FROM ${table}`;
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 += ` LIMIT ${options.limit}`;
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 += ` OFFSET ${options.offset}`;
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 += ` LIMIT ${options.limit}`;
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: "empty token stream" };
857
+ return { type: "js", fn, reason: "unsupported syntax" };
806
858
  }
807
- const parser = new Parser(tokens, names.row, names.bindings, bindings, fn);
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 where = compileNode(ast, columns);
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
- while (i < expr.length) {
903
- const ch = expr[i];
904
- if (/\s/.test(ch)) {
905
- i++;
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 (i >= expr.length) {
924
- return [];
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
- tokens.push({ type: "string", value: str });
928
- continue;
929
- }
930
- if (ch === "`") {
931
- return [];
932
- }
933
- if (/[0-9]/.test(ch) || ch === "-" && i + 1 < expr.length && /[0-9]/.test(expr[i + 1])) {
934
- let num = ch;
935
- i++;
936
- while (i < expr.length && /[0-9.]/.test(expr[i])) {
937
- num += expr[i];
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
- tokens.push({ type: "number", value: num });
941
- continue;
942
- }
943
- if (expr.slice(i, i + 3) === "===" || expr.slice(i, i + 3) === "!==") {
944
- tokens.push({ type: "operator", value: expr.slice(i, i + 3) });
945
- i += 3;
946
- continue;
947
- }
948
- if (expr.slice(i, i + 2) === "==" || expr.slice(i, i + 2) === "!=" || expr.slice(i, i + 2) === "<=" || expr.slice(i, i + 2) === ">=" || expr.slice(i, i + 2) === "&&" || expr.slice(i, i + 2) === "||") {
949
- tokens.push({ type: "operator", value: expr.slice(i, i + 2) });
950
- i += 2;
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
- if (ch === ")") {
969
- tokens.push({ type: "rparen", value: ")" });
970
- i++;
971
- continue;
1025
+ } catch (err) {
1026
+ if (err instanceof Unreadable) {
1027
+ return [];
972
1028
  }
973
- if (ch === "[") {
974
- tokens.push({ type: "lbracket", value: "[" });
975
- i++;
976
- continue;
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
- tokens.push({ type: "rbracket", value: "]" });
1042
+ if (ch !== "\\") {
1043
+ value += ch;
980
1044
  i++;
981
1045
  continue;
982
1046
  }
983
- if (ch === ",") {
984
- tokens.push({ type: "comma", value: "," });
985
- i++;
986
- continue;
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
- if (/[a-zA-Z_$]/.test(ch)) {
989
- let ident = ch;
990
- i++;
991
- while (i < expr.length && /[a-zA-Z0-9_$]/.test(expr[i])) {
992
- ident += expr[i];
993
- i++;
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
- tokens.push({ type: "identifier", value: ident });
996
- continue;
1093
+ return { value: String.fromCodePoint(code), end: close + 1 };
997
1094
  }
998
- return [];
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
- return tokens;
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, originalFn) {
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
- /** Peek at the current token without consuming it. */
1017
- peek() {
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
- /** Check if the current token matches an expected type and value. */
1025
- match(type, value) {
1026
- const t = this.peek();
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.match("operator", "||")) {
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.match("operator", "&&")) {
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.match("operator", "!")) {
1086
- this.advance();
1087
- if (this.match("lparen")) {
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.match("lparen")) {
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
- if (this.bindingsName && this.match("identifier", this.bindingsName)) {
1134
- return this.parseBindingsArrayIncludes();
1224
+ const membership = this.tryParseMembership();
1225
+ if (membership) {
1226
+ return membership;
1135
1227
  }
1136
- if (this.match("identifier")) {
1137
- return this.parseNonParamExpression();
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
- * Parse an expression that starts with the parameter name (e.g. `o.field`).
1143
- *
1144
- * Could be:
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
- const next = this.peek();
1157
- if (next?.type === "dot" && this.lookAheadForIncludes()) {
1255
+ if (this.lookAheadForIncludes()) {
1158
1256
  return this.parseFieldIncludes(field);
1159
1257
  }
1160
- if (next?.type === "operator" && isComparisonOp(next.value)) {
1161
- return this.parseComparison(field);
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: "booleanField", field, negated: false };
1267
+ return { kind: "truthy", field };
1164
1268
  }
1165
1269
  /**
1166
- * Parse a dot-separated field path after the parameter name.
1167
- * `o.status` → `"status"`, `o.address.city` → `"json_extract(address, '$.city')"`.
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.tokens[this.pos + 1]?.type === "identifier") {
1178
- if (this.tokens[this.pos + 2]?.type === "lparen") {
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.length === 1) {
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 sqlOp = JS_TO_SQL_OP[jsOp];
1212
- if (!sqlOp) {
1213
- return null;
1290
+ const column = parts[0];
1291
+ if (parts.length === 1) {
1292
+ return { sql: column, column, nested: false };
1214
1293
  }
1215
- return { kind: "comparison", field, operator: sqlOp, value };
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 value = this.parseValue();
1228
- if (value === PARSE_FAILED || typeof value !== "string") {
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
- const escaped = value.replace(/%/g, "\\%").replace(/_/g, "\\_");
1235
- return { kind: "like", field, pattern: `%${escaped}%` };
1316
+ return { kind: "includes", field, needle };
1236
1317
  }
1237
1318
  /**
1238
- * Parse `['a', 'b', 'c'].includes(o.field)` → IN expression.
1239
- * The opening bracket has been peeked but not consumed.
1319
+ * `[...].includes(o.field)` or `$.path.includes(o.field)`, when that's what
1320
+ * follows; null (position restored) otherwise.
1240
1321
  */
1241
- parseArrayIncludes() {
1242
- this.advance();
1243
- const values = [];
1244
- while (!this.match("rbracket")) {
1245
- if (values.length > 0) {
1246
- if (!this.eat("comma")) {
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
- const val = this.parseValue();
1251
- if (val === PARSE_FAILED) {
1252
- return null;
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", "includes")) {
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: bound };
1359
+ return { kind: "in", field, values };
1330
1360
  }
1331
1361
  /**
1332
- * If the current token is the bindings parameter name, walk a dotted path
1333
- * (`$.foo.bar`) and resolve the value from the bindings object. Returns
1334
- * the resolved value or `PARSE_FAILED`.
1335
- *
1336
- * Stops walking before a method call (e.g. doesn't consume `.includes` in
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.tokens[this.pos + 1]?.type === "identifier") {
1355
- if (this.tokens[this.pos + 2]?.type === "lparen") {
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
- this.pos = startPos;
1369
- return PARSE_FAILED;
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
- * Parse a literal value or closure variable reference.
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 Number(t.value);
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
- if (t.type === "identifier") {
1399
- if (t.value === "true") {
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
- if (this.bindingsName && t.value === this.bindingsName) {
1416
- const bound = this.tryResolveBindingsValue();
1417
- if (bound !== PARSE_FAILED) {
1418
- return bound;
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 (t.type === "operator" && t.value === "-") {
1424
- this.advance();
1425
- const next = this.peek();
1426
- if (next?.type === "number") {
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
- return -Number(next.value);
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.tokens[this.pos]?.type === "dot" && this.tokens[this.pos + 1]?.type === "identifier" && this.tokens[this.pos + 1]?.value === "includes" && this.tokens[this.pos + 2]?.type === "lparen";
1506
+ return this.match("dot") && this.match("identifier", "includes", 1) && this.match("lparen", void 0, 2);
1472
1507
  }
1473
1508
  };
1474
- function compileNode(node, columns) {
1475
- const stored = (field, value) => escapeValue(serializeColumnParam(value, field, columns));
1476
- switch (node.kind) {
1477
- case "comparison":
1478
- return `${node.field} ${node.operator} ${stored(node.field, node.value)}`;
1479
- case "nullCheck":
1480
- return `${node.field} ${node.isNull ? "IS NULL" : "IS NOT NULL"}`;
1481
- case "in": {
1482
- if (node.values.length === 0) {
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
- default:
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 JS_TO_SQL_OP = {
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
- var PARSE_FAILED = /* @__PURE__ */ Symbol("PARSE_FAILED");
1522
- function isComparisonOp(value) {
1523
- return value in JS_TO_SQL_OP;
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) => compareForOrder(a[key], b[key]));
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
- void 0,
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 a SqlQuery for batch execution.
1910
- *
1911
- * Returns the compiled SQL query (if all predicates compile to SQL),
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
- fallbackQuery: null,
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
- const sortField = this._sortAccessor ? extractFieldName(this._sortAccessor) : void 0;
1935
- if (compiled.allSql) {
1936
- const agg = this._compileAggregateSql(compiled.sqlWhere);
1937
- if (agg) {
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
- });
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
- fallbackQuery: null,
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: null,
1965
- fallbackQuery,
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
- * For SQL-compiled queries: just deserialize the rows.
1980
- * For JS-fallback queries: filter, sort, and slice in JS.
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 rows = result.rows.map(
1987
- (row) => deserializeRow(
1988
- row,
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
- if (compiled.query) {
1993
- return compiled.postProcess ? compiled.postProcess(rows) : rows;
1994
- }
1995
- let filtered = compiled.predicates ? rows.filter(
1996
- (row) => compiled.predicates.every((p) => p.fn(row, p.bindings))
1997
- ) : rows;
1998
- if (compiled.sortAccessor) {
1999
- const accessor = compiled.sortAccessor;
2000
- const reversed = compiled.reversed ?? false;
2001
- filtered.sort((a, b) => {
2002
- const aVal = accessor(a);
2003
- const bVal = accessor(b);
2004
- if (aVal < bVal) {
2005
- return reversed ? 1 : -1;
2006
- }
2007
- if (aVal > bVal) {
2008
- return reversed ? -1 : 1;
2009
- }
2010
- return 0;
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
- if (compiled.offset != null || compiled.limit != null) {
2014
- const start = compiled.offset ?? 0;
2015
- const end = compiled.limit != null ? start + compiled.limit : void 0;
2016
- filtered = filtered.slice(start, end);
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
- return compiled.postProcess ? compiled.postProcess(filtered) : filtered;
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
- if (this._aggregate && !this._rawWhere) {
2029
- const compiled = this._compilePredicates();
2030
- if (compiled.allSql) {
2031
- const agg = this._compileAggregateSql(compiled.sqlWhere);
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, this._config.columns)
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 _Mutation {
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 Non-batchable executor for complex mutations (e.g. removeAll JS fallback). */
2216
- _executor;
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._executor = void 0;
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 this is a non-batchable mutation (created via fromExecutor).
2401
+ * Throws if it can't be expressed as SQL (its builder returned null).
2258
2402
  */
2259
2403
  _compile() {
2260
- if (this._executor) {
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: this._build(),
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
- if (this._executor) {
2287
- return this._executor();
2431
+ const queries = this._build();
2432
+ if (!queries) {
2433
+ return this._fallback();
2288
2434
  }
2289
- const results = await this._config.executeBatch(this._build());
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
- const sqlWhere = () => {
2464
- const compiled = compilePredicate(
2465
- predicate,
2466
- bindings,
2467
- this._config.columns
2468
- );
2469
- return compiled.type === "sql" ? compiled.where : null;
2470
- };
2471
- if (sqlWhere() !== null) {
2472
- return new Mutation(
2473
- this._config,
2474
- () => [buildDelete(this._config.tableName, sqlWhere())],
2475
- (results) => results[0].changes
2476
- );
2477
- }
2478
- return Mutation.fromExecutor(this._config, async () => {
2479
- console.warn(
2480
- `[mindstudio] removeAll predicate on ${this._config.tableName} could not be compiled to SQL \u2014 fetching all rows first`
2481
- );
2482
- const allQuery = buildSelect(this._config.tableName);
2483
- const allResults = await this._config.executeBatch([allQuery]);
2484
- const allRows = allResults[0].rows.map(
2485
- (r) => deserializeRow(r, this._config.columns)
2486
- );
2487
- const matching = allRows.filter((row) => predicate(row, bindings));
2488
- if (matching.length === 0) {
2489
- return 0;
2490
- }
2491
- const deleteQueries = matching.filter((row) => row.id).map(
2492
- (row) => buildDelete(this._config.tableName, `id = ?`, [row.id])
2493
- );
2494
- if (deleteQueries.length > 0) {
2495
- await this._config.executeBatch(deleteQueries);
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
- return matching.length;
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
- now: () => Date.now(),
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
- const sqlQuery = c.query ?? c.fallbackQuery;
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
- if (!c.query && c.predicates?.length) {
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
- now: () => Date.now(),
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