@mindstudio-ai/agent 0.1.161 → 0.1.163

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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) {
1356
+ if (!field || !this.eat("rparen")) {
1324
1357
  return null;
1325
1358
  }
1326
- if (!this.eat("rparen")) {
1327
- return null;
1328
- }
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,144 @@ 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 compileNode(node, negated, columns, params) {
1598
+ const column = (field) => field.nested ? void 0 : columns.find((c) => c.name === field.column);
1599
+ const stored = (field, value) => field.nested ? serializeColumnParam(value, "", []) : serializeColumnParam(value, field.column, columns);
1600
+ switch (node.kind) {
1601
+ case "logical": {
1602
+ const operator = negated ? node.operator === "AND" ? "OR" : "AND" : node.operator;
1603
+ const left = compileNode(node.left, negated, columns, params);
1604
+ const right = left && compileNode(node.right, negated, columns, params);
1605
+ return left && right ? `(${left} ${operator} ${right})` : null;
1606
+ }
1607
+ case "not":
1608
+ return compileNode(node.operand, !negated, columns, params);
1609
+ case "comparison": {
1610
+ if (node.field.nested) {
1611
+ return nestedComparison(node, negated, params);
1612
+ }
1613
+ const type = column(node.field)?.type;
1614
+ if (!type || typeof node.value !== VALUE_KIND[type]) {
1615
+ return null;
1616
+ }
1617
+ const op = negated ? NEGATED[node.op] : node.op;
1618
+ const nullsMatch = negated ? !jsCompare(null, node.op, node.value) : jsCompare(null, node.op, node.value);
1619
+ params.push(stored(node.field, node.value));
1620
+ if (SQL_OP[op] === "!=" && nullsMatch) {
1621
+ return `${node.field.sql} IS NOT ?`;
1622
+ }
1623
+ return withNulls(
1624
+ `${node.field.sql} ${SQL_OP[op]} ?`,
1625
+ node.field,
1626
+ nullsMatch
1627
+ );
1628
+ }
1629
+ case "nullCheck": {
1630
+ if (node.strict && node.field.nested) {
1631
+ return null;
1632
+ }
1633
+ const isNull = negated ? !node.isNull : node.isNull;
1634
+ return `${node.field.sql} ${isNull ? "IS NULL" : "IS NOT NULL"}`;
1635
+ }
1636
+ case "in": {
1637
+ const type = column(node.field)?.type;
1638
+ if (!type || node.values.some(
1639
+ (v) => v !== null && v !== void 0 && typeof v !== VALUE_KIND[type]
1640
+ )) {
1641
+ return null;
1642
+ }
1643
+ const listed = node.values.filter((v) => v !== null && v !== void 0);
1644
+ const nullsMatch = negated ? !node.values.includes(null) : node.values.includes(null);
1645
+ let core;
1646
+ if (listed.length === 0) {
1647
+ core = negated ? `${node.field.sql} IS NOT NULL` : "0";
1648
+ } else {
1649
+ params.push(JSON.stringify(listed.map((v) => stored(node.field, v))));
1650
+ core = `${node.field.sql} ${negated ? "NOT IN" : "IN"} (SELECT value FROM json_each(?))`;
1651
+ }
1652
+ return withNulls(core, node.field, nullsMatch);
1653
+ }
1654
+ case "includes": {
1655
+ const type = column(node.field)?.type;
1656
+ if (type === "json") {
1657
+ let element;
1658
+ if (typeof node.needle === "boolean") {
1659
+ element = `type = '${node.needle}'`;
1660
+ } else {
1661
+ params.push(node.needle);
1662
+ element = `type IN ${JSON_TYPES[typeof node.needle]} AND value = ?`;
1663
+ }
1664
+ return `(json_type(${node.field.sql}) = 'array' AND ${negated ? "NOT EXISTS" : "EXISTS"} (SELECT 1 FROM json_each(${node.field.sql}) WHERE ${element}))`;
1665
+ }
1666
+ if ((type === "text" || type === "user") && typeof node.needle === "string") {
1667
+ params.push(node.needle);
1668
+ return `instr(${node.field.sql}, ?) ${negated ? "=" : ">"} 0`;
1669
+ }
1670
+ return null;
1671
+ }
1672
+ case "truthy": {
1673
+ const type = column(node.field)?.type;
1674
+ if (node.field.nested) {
1675
+ return negated ? `(${node.field.sql} IN (0, '') OR ${node.field.sql} IS NULL)` : `${node.field.sql} NOT IN (0, '')`;
1676
+ }
1677
+ switch (type) {
1678
+ case "boolean":
1679
+ return negated ? `${node.field.sql} IS NOT 1` : `${node.field.sql} = 1`;
1680
+ case "number":
1681
+ return negated ? `(${node.field.sql} = 0 OR ${node.field.sql} IS NULL)` : `${node.field.sql} <> 0`;
1682
+ case "text":
1683
+ case "user":
1684
+ return negated ? `(${node.field.sql} = '' OR ${node.field.sql} IS NULL)` : `${node.field.sql} <> ''`;
1685
+ default:
1686
+ return null;
1687
+ }
1688
+ }
1689
+ }
1524
1690
  }
1525
1691
 
1526
1692
  // src/db/aggregate.ts
@@ -1631,24 +1797,6 @@ function aggregateValues(fn, values) {
1631
1797
  return present.length === 0 ? null : present.reduce((m, v) => v > m ? v : m);
1632
1798
  }
1633
1799
  }
1634
- function compareForOrder(a, b) {
1635
- if (a == null && b == null) {
1636
- return 0;
1637
- }
1638
- if (a == null) {
1639
- return -1;
1640
- }
1641
- if (b == null) {
1642
- return 1;
1643
- }
1644
- if (a < b) {
1645
- return -1;
1646
- }
1647
- if (a > b) {
1648
- return 1;
1649
- }
1650
- return 0;
1651
- }
1652
1800
  function computeAggregateInJs(rows, spec) {
1653
1801
  const groups = /* @__PURE__ */ new Map();
1654
1802
  for (const row of rows) {
@@ -1678,7 +1826,7 @@ function computeAggregateInJs(rows, spec) {
1678
1826
  });
1679
1827
  if (spec.orderBy) {
1680
1828
  const key = spec.orderBy;
1681
- out.sort((a, b) => compareForOrder(a[key], b[key]));
1829
+ out.sort((a, b) => compareAsSqlite(a[key], b[key]));
1682
1830
  if (spec.desc) {
1683
1831
  out.reverse();
1684
1832
  }
@@ -1732,6 +1880,8 @@ var Query = class _Query {
1732
1880
  _rawWhereParams;
1733
1881
  /** @internal See AggregateState — set by aggregate terminals. */
1734
1882
  _aggregate;
1883
+ /** @internal The columns `select()` named; every column when unset. */
1884
+ _columns;
1735
1885
  /** @internal Post-process transform applied after row deserialization. */
1736
1886
  _postProcess;
1737
1887
  constructor(config, options) {
@@ -1745,6 +1895,7 @@ var Query = class _Query {
1745
1895
  this._rawWhere = options?.rawWhere;
1746
1896
  this._rawWhereParams = options?.rawWhereParams;
1747
1897
  this._aggregate = options?.aggregate;
1898
+ this._columns = options?.columns;
1748
1899
  }
1749
1900
  _clone(overrides) {
1750
1901
  return new _Query(this._config, {
@@ -1756,7 +1907,8 @@ var Query = class _Query {
1756
1907
  postProcess: overrides.postProcess,
1757
1908
  rawWhere: this._rawWhere,
1758
1909
  rawWhereParams: this._rawWhereParams,
1759
- aggregate: overrides.aggregate ?? this._aggregate
1910
+ aggregate: overrides.aggregate ?? this._aggregate,
1911
+ columns: overrides.columns ?? this._columns
1760
1912
  });
1761
1913
  }
1762
1914
  filter(predicate, bindings) {
@@ -1776,6 +1928,21 @@ var Query = class _Query {
1776
1928
  skip(n) {
1777
1929
  return this._clone({ offset: n });
1778
1930
  }
1931
+ /**
1932
+ * Return only these columns: `SELECT id, name` rather than every column of
1933
+ * a wide row. Chain it before a terminal (`first()`, `take()`); filters and
1934
+ * sorts still see the whole row.
1935
+ *
1936
+ * ```ts
1937
+ * const names = await Companies
1938
+ * .filter((c, $) => c.firmId === $.firmId, { firmId })
1939
+ * .select('id', 'name');
1940
+ * // Array<{ id: string; name: string }>
1941
+ * ```
1942
+ */
1943
+ select(...columns) {
1944
+ return this._clone({ columns });
1945
+ }
1779
1946
  // -------------------------------------------------------------------------
1780
1947
  // Terminal methods
1781
1948
  // -------------------------------------------------------------------------
@@ -1863,8 +2030,8 @@ var Query = class _Query {
1863
2030
  if (compiled.allSql && compiled.sqlWhere) {
1864
2031
  const query = buildExists(
1865
2032
  this._config.tableName,
1866
- `NOT (${compiled.sqlWhere})`,
1867
- void 0,
2033
+ `NOT coalesce(${compiled.sqlWhere}, 0)`,
2034
+ compiled.params,
1868
2035
  true
1869
2036
  );
1870
2037
  const results = await this._config.executeBatch([query]);
@@ -1906,116 +2073,152 @@ var Query = class _Query {
1906
2073
  // Batch compilation — used by db.batch() to extract SQL without executing
1907
2074
  // -------------------------------------------------------------------------
1908
2075
  /**
1909
- * @internal Compile this query into 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.
2076
+ * @internal Compile this query into the statement to run, plus whatever
2077
+ * JS has to do with its rows (see the module header's three cases). Used by
2078
+ * db.batch() and by a standalone await alike.
1915
2079
  */
1916
2080
  _compile() {
2081
+ const table = this._config.tableName;
1917
2082
  if (this._rawWhere) {
1918
- const query = buildSelect(this._config.tableName, {
1919
- where: this._rawWhere,
1920
- whereParams: this._rawWhereParams,
1921
- orderBy: void 0,
1922
- limit: this._limit,
1923
- offset: this._offset
1924
- });
1925
2083
  return {
1926
2084
  type: "query",
1927
- query,
1928
- fallbackQuery: null,
2085
+ query: buildSelect(table, {
2086
+ columns: this._columns,
2087
+ where: this._rawWhere,
2088
+ whereParams: this._rawWhereParams,
2089
+ limit: this._limit,
2090
+ offset: this._offset
2091
+ }),
1929
2092
  config: this._config,
1930
2093
  postProcess: this._postProcess
1931
2094
  };
1932
2095
  }
1933
2096
  const compiled = this._compilePredicates();
1934
- 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
- });
2097
+ if (!compiled.allSql) {
2098
+ const reasons = compiled.compiled.flatMap(
2099
+ (c) => c.type === "js" && c.reason ? [c.reason] : []
2100
+ );
1953
2101
  return {
1954
2102
  type: "query",
1955
- query,
1956
- fallbackQuery: null,
2103
+ query: buildSelect(table, {
2104
+ orderBy: !this._sortAccessor && this._reversed ? "rowid" : void 0
2105
+ }),
1957
2106
  config: this._config,
2107
+ inJs: {
2108
+ why: `filter could not be compiled to SQL${reasons.length ? ` (${reasons.join("; ")})` : ""}`,
2109
+ predicates: this._predicates,
2110
+ sortAccessor: this._sortAccessor,
2111
+ reversed: this._reversed,
2112
+ limit: this._limit,
2113
+ offset: this._offset,
2114
+ columns: this._columns
2115
+ },
2116
+ postProcess: this._postProcess
2117
+ };
2118
+ }
2119
+ const agg = this._compileAggregateSql(compiled.sqlWhere, compiled.params);
2120
+ if (agg) {
2121
+ return {
2122
+ type: "query",
2123
+ query: agg.query,
2124
+ config: this._config,
2125
+ processRaw: agg.processRaw
2126
+ };
2127
+ }
2128
+ const sortField = this._sortAccessor ? extractFieldName(this._sortAccessor) : null;
2129
+ if (this._sortAccessor && sortField == null) {
2130
+ return {
2131
+ type: "query",
2132
+ query: buildSelect(table, {
2133
+ where: compiled.sqlWhere || void 0,
2134
+ whereParams: compiled.params
2135
+ }),
2136
+ config: this._config,
2137
+ inJs: {
2138
+ why: "sortBy is computed, not a column, so it sorts in JS",
2139
+ sortAccessor: this._sortAccessor,
2140
+ reversed: this._reversed,
2141
+ limit: this._limit,
2142
+ offset: this._offset,
2143
+ columns: this._columns
2144
+ },
1958
2145
  postProcess: this._postProcess
1959
2146
  };
1960
2147
  }
1961
- const fallbackQuery = buildSelect(this._config.tableName);
1962
2148
  return {
1963
2149
  type: "query",
1964
- query: null,
1965
- fallbackQuery,
2150
+ query: buildSelect(table, {
2151
+ columns: this._columns,
2152
+ where: compiled.sqlWhere || void 0,
2153
+ whereParams: compiled.params,
2154
+ orderBy: sortField ?? (this._reversed ? "rowid" : void 0),
2155
+ desc: this._reversed,
2156
+ limit: this._limit,
2157
+ offset: this._offset
2158
+ }),
1966
2159
  config: this._config,
1967
- predicates: this._predicates,
1968
- sortAccessor: this._sortAccessor,
1969
- reversed: this._reversed,
1970
- limit: this._limit,
1971
- offset: this._offset,
1972
2160
  postProcess: this._postProcess
1973
2161
  };
1974
2162
  }
1975
2163
  /**
1976
2164
  * @internal Process raw SQL results into typed rows. Used by db.batch()
1977
- * after executing the compiled query.
2165
+ * after executing the compiled query, and by a standalone await.
1978
2166
  *
1979
- * For SQL-compiled queries: just deserialize the rows.
1980
- * For JS-fallback queries: filter, sort, and slice in JS.
2167
+ * Deserializes the rows, then does in JS whatever the SQL couldn't
2168
+ * (`inJs`): the filter, the sort, the page, the projection.
1981
2169
  */
1982
2170
  static _processResults(result, compiled) {
1983
2171
  if (compiled.processRaw) {
1984
2172
  return compiled.processRaw(result);
1985
2173
  }
1986
- const rows = result.rows.map(
1987
- (row) => deserializeRow(
1988
- row,
1989
- compiled.config.columns
1990
- )
2174
+ const columns = compiled.config.columns;
2175
+ let rows = result.rows.map(
2176
+ (row) => deserializeRow(row, columns)
1991
2177
  );
1992
- 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
- });
2178
+ const inJs = compiled.inJs;
2179
+ if (inJs) {
2180
+ if (inJs.predicates?.length) {
2181
+ rows = rows.filter(
2182
+ (row) => inJs.predicates.every((p) => p.fn(row, p.bindings))
2183
+ );
2184
+ }
2185
+ if (inJs.sortAccessor) {
2186
+ const accessor = inJs.sortAccessor;
2187
+ const direction = inJs.reversed ? -1 : 1;
2188
+ rows.sort(
2189
+ (a, b) => direction * compareAsSqlite(accessor(a), accessor(b))
2190
+ );
2191
+ } else if (inJs.reversed) {
2192
+ rows.reverse();
2193
+ }
2194
+ if (inJs.offset != null || inJs.limit != null) {
2195
+ const start = inJs.offset ?? 0;
2196
+ const end = inJs.limit != null ? start + inJs.limit : void 0;
2197
+ rows = rows.slice(start, end);
2198
+ }
2199
+ if (inJs.columns?.length) {
2200
+ const keep = inJs.columns;
2201
+ rows = rows.map(
2202
+ (row) => Object.fromEntries(
2203
+ keep.map((column) => [
2204
+ column,
2205
+ row[column]
2206
+ ])
2207
+ )
2208
+ );
2209
+ }
2012
2210
  }
2013
- 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);
2211
+ return compiled.postProcess ? compiled.postProcess(rows) : rows;
2212
+ }
2213
+ /** @internal Say that part of a query ran in JS, and why. */
2214
+ static _warnInJs(compiled, rowsRead) {
2215
+ if (!compiled.inJs) {
2216
+ return;
2017
2217
  }
2018
- return compiled.postProcess ? compiled.postProcess(filtered) : filtered;
2218
+ const read = rowsRead == null ? "" : ` (${rowsRead} rows read)`;
2219
+ console.warn(
2220
+ `[mindstudio] Query on '${compiled.config.tableName}': ${compiled.inJs.why} \u2014 processing in JS${read}`
2221
+ );
2019
2222
  }
2020
2223
  // -------------------------------------------------------------------------
2021
2224
  // PromiseLike
@@ -2025,18 +2228,10 @@ var Query = class _Query {
2025
2228
  }
2026
2229
  async _run() {
2027
2230
  await this._config.ready?.();
2028
- 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;
2231
+ const compiled = this._compile();
2232
+ const results = await this._config.executeBatch([compiled.query]);
2233
+ _Query._warnInJs(compiled, results[0]?.rows.length);
2234
+ return _Query._processResults(results[0], compiled);
2040
2235
  }
2041
2236
  /**
2042
2237
  * @internal Compile the pending aggregate into a single SQL statement +
@@ -2047,7 +2242,7 @@ var Query = class _Query {
2047
2242
  * when a scalar accessor doesn't extract to a column (computed accessors
2048
2243
  * stay correct via JS, with a warning to prompt optimization).
2049
2244
  */
2050
- _compileAggregateSql(sqlWhere) {
2245
+ _compileAggregateSql(sqlWhere, whereParams) {
2051
2246
  const agg = this._aggregate;
2052
2247
  if (!agg || this._rawWhere || this._limit != null || this._offset != null) {
2053
2248
  return null;
@@ -2068,7 +2263,8 @@ var Query = class _Query {
2068
2263
  this._config.tableName,
2069
2264
  agg.fn,
2070
2265
  column,
2071
- where
2266
+ where,
2267
+ whereParams
2072
2268
  );
2073
2269
  const processRaw2 = (result) => {
2074
2270
  const value = result.rows[0]?.value;
@@ -2084,6 +2280,7 @@ var Query = class _Query {
2084
2280
  by: spec.by,
2085
2281
  select: spec.select,
2086
2282
  where,
2283
+ whereParams,
2087
2284
  orderBy: spec.orderBy,
2088
2285
  desc: spec.desc,
2089
2286
  limit: spec.limit
@@ -2104,89 +2301,22 @@ var Query = class _Query {
2104
2301
  // -------------------------------------------------------------------------
2105
2302
  // Execution internals
2106
2303
  // -------------------------------------------------------------------------
2107
- async _execute() {
2108
- if (this._rawWhere) {
2109
- const query = buildSelect(this._config.tableName, {
2110
- where: this._rawWhere,
2111
- whereParams: this._rawWhereParams,
2112
- limit: this._limit,
2113
- offset: this._offset
2114
- });
2115
- const results = await this._config.executeBatch([query]);
2116
- return results[0].rows.map(
2117
- (row) => deserializeRow(
2118
- row,
2119
- this._config.columns
2120
- )
2121
- );
2122
- }
2123
- const compiled = this._compilePredicates();
2124
- if (compiled.allSql) {
2125
- const sortField = this._sortAccessor ? extractFieldName(this._sortAccessor) : void 0;
2126
- const query = buildSelect(this._config.tableName, {
2127
- where: compiled.sqlWhere || void 0,
2128
- orderBy: sortField ?? void 0,
2129
- desc: this._reversed,
2130
- limit: this._limit,
2131
- offset: this._offset
2132
- });
2133
- const results = await this._config.executeBatch([query]);
2134
- return results[0].rows.map(
2135
- (row) => deserializeRow(
2136
- row,
2137
- this._config.columns
2138
- )
2139
- );
2140
- }
2141
- let rows = await this._fetchAndFilterInJs(compiled);
2142
- if (this._sortAccessor) {
2143
- const accessor = this._sortAccessor;
2144
- rows.sort((a, b) => {
2145
- const aVal = accessor(a);
2146
- const bVal = accessor(b);
2147
- if (aVal < bVal) {
2148
- return this._reversed ? 1 : -1;
2149
- }
2150
- if (aVal > bVal) {
2151
- return this._reversed ? -1 : 1;
2152
- }
2153
- return 0;
2154
- });
2155
- }
2156
- if (this._offset != null || this._limit != null) {
2157
- const start = this._offset ?? 0;
2158
- const end = this._limit != null ? start + this._limit : void 0;
2159
- rows = rows.slice(start, end);
2160
- }
2161
- return rows;
2162
- }
2163
2304
  _compilePredicates() {
2164
2305
  if (this._predicates.length === 0) {
2165
- return { allSql: true, sqlWhere: "", compiled: [] };
2306
+ return { allSql: true, sqlWhere: "", params: [], compiled: [] };
2166
2307
  }
2308
+ const columns = this._config.columns;
2167
2309
  const compiled = this._predicates.map(
2168
- (p) => compilePredicate(p.fn, p.bindings, 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))
2310
+ (p) => compilePredicate(p.fn, p.bindings, columns)
2189
2311
  );
2312
+ const sql = compiled.flatMap((c) => c.type === "sql" ? [c] : []);
2313
+ const allSql = sql.length === compiled.length;
2314
+ return {
2315
+ allSql,
2316
+ sqlWhere: allSql ? sql.map((c) => c.where).join(" AND ") : "",
2317
+ params: allSql ? sql.flatMap((c) => c.params) : [],
2318
+ compiled
2319
+ };
2190
2320
  }
2191
2321
  async _fetchAllRows() {
2192
2322
  const query = buildSelect(this._config.tableName);
@@ -2205,38 +2335,20 @@ function extractFieldName(accessor) {
2205
2335
  }
2206
2336
 
2207
2337
  // src/db/mutation.ts
2208
- var Mutation = class _Mutation {
2338
+ var Mutation = class {
2209
2339
  /** @internal */
2210
2340
  _config;
2211
- /** @internal */
2341
+ /** @internal The statements, or null when the write can't be one batch. */
2212
2342
  _build;
2213
2343
  /** @internal */
2214
2344
  _processResult;
2215
- /** @internal Non-batchable executor for complex mutations (e.g. removeAll JS fallback). */
2216
- _executor;
2217
- constructor(config, build, processResult) {
2345
+ /** @internal What runs when `_build` returns null (e.g. removeAll's JS fallback). */
2346
+ _fallback;
2347
+ constructor(config, build, processResult, fallback) {
2218
2348
  this._config = config;
2219
2349
  this._build = build;
2220
2350
  this._processResult = processResult;
2221
- this._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;
2351
+ this._fallback = fallback;
2240
2352
  }
2241
2353
  // -------------------------------------------------------------------------
2242
2354
  // PromiseLike — executes on await
@@ -2254,10 +2366,11 @@ var Mutation = class _Mutation {
2254
2366
  * @internal Compile this mutation into SQL for batch execution.
2255
2367
  * Returns the queries and a result processor.
2256
2368
  *
2257
- * Throws if this is a non-batchable mutation (created via fromExecutor).
2369
+ * Throws if it can't be expressed as SQL (its builder returned null).
2258
2370
  */
2259
2371
  _compile() {
2260
- if (this._executor) {
2372
+ const queries = this._build();
2373
+ if (!queries) {
2261
2374
  throw new MindStudioError(
2262
2375
  "This operation cannot be batched (e.g. removeAll with a JS-fallback predicate). Await it separately instead of passing to db.batch().",
2263
2376
  "not_batchable",
@@ -2266,7 +2379,7 @@ var Mutation = class _Mutation {
2266
2379
  }
2267
2380
  return {
2268
2381
  type: "mutation",
2269
- queries: this._build(),
2382
+ queries,
2270
2383
  config: this._config,
2271
2384
  processResult: this._processResult
2272
2385
  };
@@ -2283,10 +2396,11 @@ var Mutation = class _Mutation {
2283
2396
  // -------------------------------------------------------------------------
2284
2397
  async _execute() {
2285
2398
  await this._config.ready?.();
2286
- if (this._executor) {
2287
- return this._executor();
2399
+ const queries = this._build();
2400
+ if (!queries) {
2401
+ return this._fallback();
2288
2402
  }
2289
- const results = await this._config.executeBatch(this._build());
2403
+ const results = await this._config.executeBatch(queries);
2290
2404
  return this._processResult(results);
2291
2405
  }
2292
2406
  };
@@ -2390,6 +2504,10 @@ var Table = class {
2390
2504
  sortBy(accessor) {
2391
2505
  return new Query(this._config).sortBy(accessor);
2392
2506
  }
2507
+ /** Every row, with only these columns. See Query.select(). */
2508
+ select(...columns) {
2509
+ return new Query(this._config).select(...columns);
2510
+ }
2393
2511
  push(data) {
2394
2512
  const isArray = Array.isArray(data);
2395
2513
  const items = (isArray ? data : [data]).map(
@@ -2460,42 +2578,47 @@ var Table = class {
2460
2578
  );
2461
2579
  }
2462
2580
  removeAll(predicate, bindings) {
2463
- 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);
2581
+ return new Mutation(
2582
+ this._config,
2583
+ () => {
2584
+ const compiled = compilePredicate(
2585
+ predicate,
2586
+ bindings,
2587
+ this._config.columns
2588
+ );
2589
+ return compiled.type === "sql" ? [
2590
+ buildDelete(
2591
+ this._config.tableName,
2592
+ compiled.where,
2593
+ compiled.params
2594
+ )
2595
+ ] : null;
2596
+ },
2597
+ (results) => results[0].changes,
2598
+ // Fallback: multi-step execution — not batchable
2599
+ async () => {
2600
+ console.warn(
2601
+ `[mindstudio] removeAll predicate on ${this._config.tableName} could not be compiled to SQL \u2014 fetching all rows first`
2602
+ );
2603
+ const allQuery = buildSelect(this._config.tableName);
2604
+ const allResults = await this._config.executeBatch([allQuery]);
2605
+ const columns = this._config.columns;
2606
+ const allRows = allResults[0].rows.map(
2607
+ (r) => deserializeRow(r, columns)
2608
+ );
2609
+ const matching = allRows.filter((row) => predicate(row, bindings));
2610
+ if (matching.length === 0) {
2611
+ return 0;
2612
+ }
2613
+ const deleteQueries = matching.filter((row) => row.id).map(
2614
+ (row) => buildDelete(this._config.tableName, `id = ?`, [row.id])
2615
+ );
2616
+ if (deleteQueries.length > 0) {
2617
+ await this._config.executeBatch(deleteQueries);
2618
+ }
2619
+ return matching.length;
2496
2620
  }
2497
- return matching.length;
2498
- });
2621
+ );
2499
2622
  }
2500
2623
  clear() {
2501
2624
  const query = buildDelete(this._config.tableName);
@@ -2700,12 +2823,7 @@ function createDb(databases, executeBatch, authConfig, syncRoles) {
2700
2823
  },
2701
2824
  // --- Time helpers ---
2702
2825
  // Pure JS, no platform dependency. All timestamps are unix ms.
2703
- 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,
2826
+ ...timeHelpers,
2709
2827
  // --- User references ---
2710
2828
  userRef: (id) => id.startsWith(USER_PREFIX) ? id.slice(USER_PREFIX.length) : id,
2711
2829
  // --- Raw SQL (read-only) ---
@@ -2745,8 +2863,7 @@ function createDb(databases, executeBatch, authConfig, syncRoles) {
2745
2863
  groups.set(dbId, []);
2746
2864
  }
2747
2865
  if (c.type === "query") {
2748
- const sqlQuery = c.query ?? c.fallbackQuery;
2749
- groups.get(dbId).push({ opIndex: i, sqlQueries: [sqlQuery] });
2866
+ groups.get(dbId).push({ opIndex: i, sqlQueries: [c.query] });
2750
2867
  } else if (c.type === "raw") {
2751
2868
  groups.get(dbId).push({ opIndex: i, sqlQueries: [c.query] });
2752
2869
  } else {
@@ -2775,11 +2892,7 @@ function createDb(databases, executeBatch, authConfig, syncRoles) {
2775
2892
  return compiled.map((c, i) => {
2776
2893
  const results = opResults.get(i);
2777
2894
  if (c.type === "query") {
2778
- 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
- }
2895
+ Query._warnInJs(c, results[0]?.rows.length);
2783
2896
  return Query._processResults(results[0], c);
2784
2897
  } else if (c.type === "raw") {
2785
2898
  return RawQuery._processResults(results[0], c);
@@ -2875,6 +2988,84 @@ function resolveTable(databases, tableName, databaseHint) {
2875
2988
  );
2876
2989
  }
2877
2990
 
2991
+ // src/db/readBatching.ts
2992
+ var nextTick = typeof setImmediate === "function" ? (fn) => setImmediate(fn) : (fn) => setTimeout(fn, 0);
2993
+ var DbReadBatching = class {
2994
+ constructor(send) {
2995
+ this.send = send;
2996
+ }
2997
+ send;
2998
+ pending = /* @__PURE__ */ new Map();
2999
+ inFlight = /* @__PURE__ */ new Map();
3000
+ /** Run `queries` for the request holding `token`, sharing a round trip where it can. */
3001
+ async run(token, databaseId, queries) {
3002
+ const batchKey = `${token}\0${databaseId}`;
3003
+ if (queries.length === 0 || !queries.every((q) => isReadOnlySql(q.sql))) {
3004
+ this.flush(batchKey);
3005
+ return this.send(databaseId, queries);
3006
+ }
3007
+ const readKey = `${batchKey}\0${JSON.stringify(queries)}`;
3008
+ const shared = this.inFlight.get(readKey);
3009
+ if (shared) {
3010
+ return structuredClone(await shared);
3011
+ }
3012
+ const read = this.queue(batchKey, databaseId, queries);
3013
+ this.inFlight.set(readKey, read);
3014
+ try {
3015
+ return await read;
3016
+ } finally {
3017
+ this.inFlight.delete(readKey);
3018
+ }
3019
+ }
3020
+ queue(batchKey, databaseId, queries) {
3021
+ let pending = this.pending.get(batchKey);
3022
+ if (!pending) {
3023
+ pending = { databaseId, queries: [], parts: [] };
3024
+ this.pending.set(batchKey, pending);
3025
+ nextTick(() => this.flush(batchKey));
3026
+ }
3027
+ const batch = pending;
3028
+ return new Promise((resolve, reject) => {
3029
+ batch.parts.push({
3030
+ queries,
3031
+ start: batch.queries.length,
3032
+ resolve,
3033
+ reject
3034
+ });
3035
+ batch.queries.push(...queries);
3036
+ });
3037
+ }
3038
+ flush(batchKey) {
3039
+ const pending = this.pending.get(batchKey);
3040
+ if (!pending) {
3041
+ return;
3042
+ }
3043
+ this.pending.delete(batchKey);
3044
+ const { databaseId, parts } = pending;
3045
+ if (parts.length === 1) {
3046
+ this.send(databaseId, parts[0].queries).then(
3047
+ parts[0].resolve,
3048
+ parts[0].reject
3049
+ );
3050
+ return;
3051
+ }
3052
+ this.send(databaseId, pending.queries).then(
3053
+ (results) => {
3054
+ for (const part of parts) {
3055
+ part.resolve(
3056
+ results.slice(part.start, part.start + part.queries.length)
3057
+ );
3058
+ }
3059
+ },
3060
+ () => {
3061
+ for (const part of parts) {
3062
+ this.send(databaseId, part.queries).then(part.resolve, part.reject);
3063
+ }
3064
+ }
3065
+ );
3066
+ }
3067
+ };
3068
+
2878
3069
  // src/files/store.ts
2879
3070
  import { createHash, randomUUID } from "crypto";
2880
3071
  import { createReadStream, createWriteStream } from "fs";
@@ -4879,7 +5070,7 @@ var stepMetadata = {
4879
5070
  "renderVideoFromHtmlComposition": {
4880
5071
  stepType: "renderVideoFromHtmlComposition",
4881
5072
  description: "Render an HTML composition (HTML, CSS, media and seekable animation on a timeline) to a video file",
4882
- usageNotes: "- Renders an HTML composition in HyperFrames' format (https://hyperframes.heygen.com) with HyperFrames 0.8.133, in Chrome off the app's own sandbox; author for that version\n- Pass the composition as `html`, the page itself (up to 10 MB), or as `project`, the URL of a zip of a project's folder (`index.html` at its root, or the file `entryFile` names) or of one HTML file, up to 2 GiB: an app file or any public URL\n- `variables` overrides the composition's `data-composition-variables` defaults, which it reads with `window.__hyperframes.getVariables()`\n- Whatever the composition loads itself (fonts, scripts, images, video) is fetched as it renders, so each is in the project or at a public URL; a private app file needs a signed URL\n- fps 24, 30 or 60 (default 30); quality draft, standard or high (default standard); format mp4 (default), webm or mov (both keep transparency), or gif\n- An mp4, webm or mov renders in chunks of about 10 seconds side by side, so a long video takes little longer than a short one; a call finishes within about 30 minutes, and reports its progress as it goes. A gif renders whole, within 15 minutes\n- A composition HyperFrames can't render fails with HyperFrames' own reason",
5073
+ usageNotes: "- Renders an HTML composition in HyperFrames' format (https://hyperframes.heygen.com) with HyperFrames 0.8.143, in Chrome off the app's own sandbox; author for that version\n- Pass the composition as `html`, the page itself (up to 10 MB), or as `project`, the URL of a zip of a project's folder (`index.html` at its root, or the file `entryFile` names) or of one HTML file, up to 2 GiB: an app file or any public URL\n- `variables` overrides the composition's `data-composition-variables` defaults, which it reads with `window.__hyperframes.getVariables()`\n- Whatever the composition loads itself (fonts, scripts, images, video) is fetched as it renders, so each is in the project or at a public URL; a private app file needs a signed URL\n- fps 24, 30 or 60 (default 30); quality draft, standard or high (default standard); format mp4 (default), webm or mov (both keep transparency), or gif\n- An mp4, webm or mov renders in chunks of about 10 seconds side by side, so a long video takes little longer than a short one; a call finishes within about 30 minutes, and reports its progress as it goes. A gif renders whole, within 15 minutes\n- A composition HyperFrames can't render fails with HyperFrames' own reason",
4883
5074
  inputSchema: { "type": "object", "properties": { "html": { "type": "string", "description": "The composition's HTML, up to 10 MB; give this or project" }, "project": { "type": "string", "description": "URL of a zip of the project's folder, or of one self-contained HTML file; give this or html" }, "entryFile": { "type": "string", "description": "The entry HTML inside the zip, relative to its root; index.html when omitted" }, "variables": { "type": "object", "properties": {}, "required": [], "description": "Values over the composition's declared variables" }, "fps": { "enum": [24, 30, 60], "type": "number", "description": "Frames per second: 24, 30 (default) or 60" }, "quality": { "enum": ["draft", "standard", "high"], "type": "string", "description": "Encode quality: draft, standard (default) or high" }, "format": { "enum": ["mp4", "webm", "mov", "gif"], "type": "string", "description": "mp4 (default), webm or mov (both keep transparency), or gif" }, "intermediateAsset": { "type": "boolean", "description": "When true, the asset is created but hidden from the user's gallery (tagged as intermediate)" } } },
4884
5075
  outputSchema: { "type": "object", "properties": { "videoUrl": { "type": "string", "description": "URL of the rendered video" }, "durationSec": { "type": "number", "description": "Its length in seconds" }, "frames": { "type": "number", "description": "How many frames it has" } }, "required": ["videoUrl", "durationSec", "frames"] }
4885
5076
  },
@@ -7879,15 +8070,25 @@ var MindStudioAgent = class {
7879
8070
  }
7880
8071
  return json;
7881
8072
  }
8073
+ /** @internal Reads started together share a round trip (`db/readBatching.ts`). */
8074
+ _dbReads = new DbReadBatching(
8075
+ (databaseId, queries) => this._sendDbBatch(databaseId, queries)
8076
+ );
7882
8077
  /**
7883
8078
  * @internal Execute a batch of SQL queries against a managed database.
7884
- * Used as the `executeBatch` callback for Table/Query instances.
7885
- *
7886
- * Calls `POST /_internal/v2/db/query` directly with the hook token
7887
- * (raw, no Bearer prefix). All queries run on a single SQLite connection,
7888
- * enabling RETURNING clauses and multi-statement batches.
8079
+ * Used as the `executeBatch` callback for Table/Query instances. Reads one
8080
+ * request starts in the same tick go to the platform as one batch.
7889
8081
  */
7890
8082
  async _executeDbBatch(databaseId, queries) {
8083
+ return this._dbReads.run(this._token, databaseId, queries);
8084
+ }
8085
+ /**
8086
+ * @internal Send one batch: `POST /_internal/v2/db/query` directly with the
8087
+ * hook token (raw, no Bearer prefix), or its DB WebSocket. All queries run
8088
+ * on a single SQLite connection, enabling RETURNING clauses and
8089
+ * multi-statement batches.
8090
+ */
8091
+ async _sendDbBatch(databaseId, queries) {
7891
8092
  const dbWsUrl = typeof process !== "undefined" ? process.env?.DB_WS_URL : void 0;
7892
8093
  if (dbWsUrl) {
7893
8094
  try {
@@ -8033,12 +8234,7 @@ var MindStudioAgent = class {
8033
8234
  return new Table(tableConfig);
8034
8235
  },
8035
8236
  // Time helpers work without context
8036
- 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,
8237
+ ...timeHelpers,
8042
8238
  userRef: (id) => id.startsWith("@@user@@") ? id.slice("@@user@@".length) : id,
8043
8239
  // Raw SQL — validate synchronously; database resolution needs context,
8044
8240
  // so it happens inside the executeBatch closure at execution time