@pythia-software/query-table-core 0.2.0 → 0.3.0

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
@@ -1,4 +1,152 @@
1
+ // src/computed.ts
2
+ var COMPUTED_PREFIX = "@computed/";
3
+ var computedFieldName = (id) => COMPUTED_PREFIX + id;
4
+ var isComputedField = (name) => name.trim().toLowerCase().startsWith(COMPUTED_PREFIX);
5
+ function validateComputedColumn(input) {
6
+ const c = input;
7
+ if (!c || typeof c.id !== "string" || !/^[a-zA-Z0-9_-]{1,128}$/.test(c.id) || typeof c.label !== "string" || !c.label.trim() || c.label.length > 200 || typeof c.revision !== "string" || !c.revision || c.revision.length > 256 || c.expression?.language !== "qt-expr" || c.expression.version !== 1 || typeof c.expression.source !== "string" || !c.expression.source.trim() || c.expression.source.length > 1e4) {
8
+ throw new Error(
9
+ "Invalid computed column definition or unsupported formula version."
10
+ );
11
+ }
12
+ return {
13
+ id: c.id,
14
+ label: c.label,
15
+ revision: c.revision,
16
+ expression: { ...c.expression }
17
+ };
18
+ }
19
+ function memoryComputedColumnStore(seed = {}) {
20
+ const data = new Map(
21
+ Object.entries(seed).map(([k, v]) => [
22
+ k,
23
+ new Map(v.map((c) => [c.id, validateComputedColumn(c)]))
24
+ ])
25
+ );
26
+ const seedRevisions = new Set(
27
+ [...data.values()].flatMap(
28
+ (entries) => [...entries.values()].map((c) => c.revision)
29
+ )
30
+ );
31
+ let revisionSequence = 0n;
32
+ function freshRevision() {
33
+ let revision;
34
+ do {
35
+ revision = `memory:${++revisionSequence}`;
36
+ } while (seedRevisions.has(revision));
37
+ return revision;
38
+ }
39
+ const listeners = /* @__PURE__ */ new Map();
40
+ return {
41
+ async list(dataset) {
42
+ return [...data.get(dataset)?.values() ?? []].map(
43
+ validateComputedColumn
44
+ );
45
+ },
46
+ async save(dataset, column, expectedRevision) {
47
+ const entries = data.get(dataset) ?? /* @__PURE__ */ new Map();
48
+ const current = entries.get(column.id);
49
+ if ((current?.revision ?? null) !== expectedRevision)
50
+ throw new Error(
51
+ "This definition changed. Reload it before saving again."
52
+ );
53
+ const next = validateComputedColumn({
54
+ ...column,
55
+ revision: freshRevision()
56
+ });
57
+ entries.set(next.id, next);
58
+ data.set(dataset, entries);
59
+ listeners.get(dataset)?.forEach((fn) => fn());
60
+ return validateComputedColumn(next);
61
+ },
62
+ subscribe(dataset, listener) {
63
+ const set = listeners.get(dataset) ?? /* @__PURE__ */ new Set();
64
+ set.add(listener);
65
+ listeners.set(dataset, set);
66
+ return () => {
67
+ set.delete(listener);
68
+ };
69
+ }
70
+ };
71
+ }
72
+ function httpComputedColumnStore(base, fetcher = fetch) {
73
+ const listeners = /* @__PURE__ */ new Map();
74
+ const url = (dataset) => `${base}${base.includes("?") ? "&" : "?"}dataset=${encodeURIComponent(dataset)}`;
75
+ async function read(response) {
76
+ if (response.status === 409)
77
+ throw new Error(
78
+ "This definition changed. Reload it before saving again."
79
+ );
80
+ if (!response.ok)
81
+ throw new Error(
82
+ `Computed column store request failed (${response.status}).`
83
+ );
84
+ return response.json();
85
+ }
86
+ return {
87
+ async list(dataset, signal) {
88
+ const raw = await read(
89
+ await fetcher(url(dataset), { ...signal ? { signal } : {} })
90
+ );
91
+ if (!Array.isArray(raw))
92
+ throw new Error("Invalid computed column catalogue.");
93
+ return raw.map(validateComputedColumn);
94
+ },
95
+ async save(dataset, column, expectedRevision) {
96
+ const result = validateComputedColumn(
97
+ await read(
98
+ await fetcher(url(dataset), {
99
+ method: "PUT",
100
+ headers: { "Content-Type": "application/json" },
101
+ body: JSON.stringify({ column, expectedRevision })
102
+ })
103
+ )
104
+ );
105
+ listeners.get(dataset)?.forEach((fn) => fn());
106
+ return result;
107
+ },
108
+ subscribe(dataset, listener) {
109
+ const set = listeners.get(dataset) ?? /* @__PURE__ */ new Set();
110
+ set.add(listener);
111
+ listeners.set(dataset, set);
112
+ return () => {
113
+ set.delete(listener);
114
+ };
115
+ }
116
+ };
117
+ }
118
+ function isComputedCellError(value) {
119
+ return value !== null && typeof value === "object" && "computedError" in value && typeof value.computedError === "string";
120
+ }
121
+ function groupPreview(dependencies, inputs, results, total) {
122
+ const groups = /* @__PURE__ */ new Map();
123
+ let nulls = 0, errors = 0;
124
+ inputs.forEach((row, i) => {
125
+ const values = dependencies.map((name) => row[name] ?? null), result = results[i] ?? { value: null, error: "No result." };
126
+ const key = JSON.stringify([values, result.value, result.error ?? null]);
127
+ const group = groups.get(key);
128
+ if (group) group.count++;
129
+ else groups.set(key, { inputs: values, result, count: 1 });
130
+ if (result.error) errors++;
131
+ else if (result.value === null) nulls++;
132
+ });
133
+ return {
134
+ dependencies,
135
+ groups: [...groups.values()].sort((a, b) => b.count - a.count),
136
+ processed: inputs.length,
137
+ total,
138
+ nulls,
139
+ errors
140
+ };
141
+ }
142
+
1
143
  // src/query.ts
144
+ function isOrGroup(term) {
145
+ return Array.isArray(term.any);
146
+ }
147
+ function predicatesOf(term) {
148
+ return isOrGroup(term) ? term.any : [term];
149
+ }
2
150
  var EMPTY_QUERY = {
3
151
  select: [],
4
152
  where: [],
@@ -29,6 +177,8 @@ var FILTER_OPS = /* @__PURE__ */ new Set([
29
177
  "contains",
30
178
  "starts_with",
31
179
  "ends_with",
180
+ "matches_regex",
181
+ "not_matches_regex",
32
182
  "includes",
33
183
  "is_null",
34
184
  "is_not_null"
@@ -44,6 +194,20 @@ function boundedInteger(value, fallback, min, max) {
44
194
  if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
45
195
  return Math.max(min, Math.min(max, Math.round(value)));
46
196
  }
197
+ function normalizeWhereClause(item) {
198
+ if (!isRecord(item)) return null;
199
+ const field = boundedString(item.field, MAX_FIELD_NAME_LENGTH);
200
+ const value = typeof item.value === "string" && item.value.length <= MAX_FILTER_VALUE_LENGTH ? item.value : null;
201
+ if (!field || isComputedField(field) || typeof item.op !== "string" || !FILTER_OPS.has(item.op) || value == null) {
202
+ return null;
203
+ }
204
+ const clause = { field, op: item.op, value };
205
+ if (item.negated === true) clause.negated = true;
206
+ return clause;
207
+ }
208
+ function cloneWhereTerm(term) {
209
+ return isOrGroup(term) ? { any: term.any.map((c) => ({ ...c })) } : { ...term };
210
+ }
47
211
  function normalizeQueryState(input, fallback = EMPTY_QUERY) {
48
212
  const raw = isRecord(input) ? input : {};
49
213
  const select = [];
@@ -63,28 +227,49 @@ function normalizeQueryState(input, fallback = EMPTY_QUERY) {
63
227
  }
64
228
  const where = [];
65
229
  if (Array.isArray(raw.where)) {
66
- for (const item of raw.where.slice(0, MAX_WHERE_CLAUSES)) {
67
- if (!isRecord(item)) continue;
68
- const field = boundedString(item.field, MAX_FIELD_NAME_LENGTH);
69
- const value = typeof item.value === "string" && item.value.length <= MAX_FILTER_VALUE_LENGTH ? item.value : null;
70
- if (!field || typeof item.op !== "string" || !FILTER_OPS.has(item.op) || value == null) continue;
71
- where.push({ field, op: item.op, value });
230
+ let literalBudget = MAX_WHERE_CLAUSES;
231
+ for (const item of raw.where) {
232
+ if (literalBudget <= 0) break;
233
+ if (isRecord(item) && Array.isArray(item.any)) {
234
+ const members = [];
235
+ for (const inner of item.any) {
236
+ if (literalBudget <= 0) break;
237
+ const clause = normalizeWhereClause(inner);
238
+ if (clause) {
239
+ members.push(clause);
240
+ literalBudget--;
241
+ }
242
+ }
243
+ if (members.length === 1) where.push(members[0]);
244
+ else if (members.length > 1) where.push({ any: members });
245
+ } else {
246
+ const clause = normalizeWhereClause(item);
247
+ if (clause) {
248
+ where.push(clause);
249
+ literalBudget--;
250
+ }
251
+ }
72
252
  }
73
253
  } else {
74
- where.push(...fallback.where.map((clause) => ({ ...clause })));
254
+ where.push(...fallback.where.map(cloneWhereTerm));
75
255
  }
76
256
  const orderBy = [];
77
257
  if (Array.isArray(raw.orderBy)) {
78
258
  for (const item of raw.orderBy.slice(0, MAX_ORDER_BY_TERMS)) {
79
259
  if (!isRecord(item)) continue;
80
260
  const field = boundedString(item.field, MAX_FIELD_NAME_LENGTH);
81
- if (!field || item.dir !== "asc" && item.dir !== "desc") continue;
261
+ if (!field || isComputedField(field) || item.dir !== "asc" && item.dir !== "desc") continue;
82
262
  const term = { field, dir: item.dir };
83
263
  if (item.nulls === "first" || item.nulls === "last") term.nulls = item.nulls;
264
+ if (isRecord(item.extract) && typeof item.extract.regex === "string" && item.extract.regex.length <= MAX_FILTER_VALUE_LENGTH) {
265
+ term.extract = { regex: item.extract.regex };
266
+ }
84
267
  orderBy.push(term);
85
268
  }
86
269
  } else {
87
- orderBy.push(...fallback.orderBy.map((term) => ({ ...term })));
270
+ orderBy.push(
271
+ ...fallback.orderBy.map((term) => term.extract ? { ...term, extract: { ...term.extract } } : { ...term })
272
+ );
88
273
  }
89
274
  const out = {
90
275
  select,
@@ -103,6 +288,7 @@ function normalizeQueryState(input, fallback = EMPTY_QUERY) {
103
288
  const aggregation = { id, op: item.op, groupBy };
104
289
  const field = boundedString(item.field, MAX_FIELD_NAME_LENGTH);
105
290
  const label = boundedString(item.label, MAX_LABEL_LENGTH);
291
+ if (field && isComputedField(field) || groupBy.some(isComputedField)) continue;
106
292
  if (field) aggregation.field = field;
107
293
  if (label) aggregation.label = label;
108
294
  aggregations.push(aggregation);
@@ -131,37 +317,39 @@ function sameArray(a, b) {
131
317
  }
132
318
 
133
319
  // src/schema.ts
134
- function isFilterable(f) {
135
- return f.filter?.enabled ?? f.source.kind === "backend";
320
+ function isFilterable(f2) {
321
+ if (f2.source.kind === "derived" && f2.source.computedId) return false;
322
+ return f2.filter?.enabled ?? f2.source.kind === "backend";
136
323
  }
137
- function isPushdownFilter(f) {
138
- return isFilterable(f) && (f.filter?.pushdown ?? f.source.kind === "backend");
324
+ function isPushdownFilter(f2) {
325
+ return isFilterable(f2) && (f2.filter?.pushdown ?? f2.source.kind === "backend");
139
326
  }
140
- function isSortable(f) {
141
- return f.sort?.enabled ?? f.source.kind === "backend";
327
+ function isSortable(f2) {
328
+ if (f2.source.kind === "derived" && f2.source.computedId) return false;
329
+ return f2.sort?.enabled ?? f2.source.kind === "backend";
142
330
  }
143
- function isSelectable(f) {
144
- return f.select?.enabled ?? true;
331
+ function isSelectable(f2) {
332
+ return f2.select?.enabled ?? true;
145
333
  }
146
- function filterValues(f) {
147
- return f.filter?.values ?? { source: "autocomplete" };
334
+ function filterValues(f2) {
335
+ return f2.filter?.values ?? { source: "autocomplete" };
148
336
  }
149
337
  function indexFields(schema) {
150
- return new Map(schema.fields.map((f) => [f.name, f]));
338
+ return new Map(schema.fields.map((f2) => [f2.name, f2]));
151
339
  }
152
340
  function selectedFields(schema, q) {
153
- const order = q.select.length > 0 ? q.select.map((c) => c.field) : (schema.defaultSelect ?? schema.fields.filter((f) => f.select?.default).map((f) => ({ field: f.name }))).map(
341
+ const order = q.select.length > 0 ? q.select.map((c) => c.field) : (schema.defaultSelect ?? schema.fields.filter((f2) => f2.select?.default).map((f2) => ({ field: f2.name }))).map(
154
342
  (c) => c.field
155
343
  );
156
344
  const seen = /* @__PURE__ */ new Set();
157
345
  const out = [];
158
346
  for (const name of order) {
159
347
  if (seen.has(name)) continue;
160
- const f = resolveField(schema, name);
161
- if (!f || !isSelectable(f)) continue;
348
+ const f2 = resolveField(schema, name);
349
+ if (!f2 || !isSelectable(f2)) continue;
162
350
  seen.add(name);
163
- seen.add(f.name);
164
- out.push(f);
351
+ seen.add(f2.name);
352
+ out.push(f2);
165
353
  }
166
354
  return out;
167
355
  }
@@ -172,9 +360,9 @@ function resolveField(schema, field) {
172
360
  const byExactName = byName.get(token);
173
361
  if (byExactName) return byExactName;
174
362
  const needle = token.toLowerCase();
175
- for (const f of schema.fields) {
176
- if (f.name.toLowerCase() === needle) return f;
177
- if (aliasMatches(f, needle)) return f;
363
+ for (const f2 of schema.fields) {
364
+ if (f2.name.toLowerCase() === needle) return f2;
365
+ if (aliasMatches(f2, needle)) return f2;
178
366
  }
179
367
  return void 0;
180
368
  }
@@ -286,8 +474,57 @@ function normalizeAliases(rawAliases, rawAlias) {
286
474
 
287
475
  // src/ops.ts
288
476
  var NULLITY = ["is_null", "is_not_null"];
477
+ var COMPLEMENT_OP = {
478
+ "=": "!=",
479
+ "!=": "=",
480
+ ">": "<=",
481
+ "<=": ">",
482
+ ">=": "<",
483
+ "<": ">=",
484
+ is_null: "is_not_null",
485
+ is_not_null: "is_null",
486
+ matches_regex: "not_matches_regex",
487
+ not_matches_regex: "matches_regex"
488
+ };
489
+ function negateClause(clause, allowedOps) {
490
+ const complement = COMPLEMENT_OP[clause.op];
491
+ if (complement && (!allowedOps || allowedOps.includes(complement))) {
492
+ const next2 = { field: clause.field, op: complement, value: clause.value };
493
+ return next2;
494
+ }
495
+ const next = { field: clause.field, op: clause.op, value: clause.value };
496
+ if (!clause.negated) next.negated = true;
497
+ return next;
498
+ }
499
+ function isNegativePredicate(clause) {
500
+ return Boolean(clause.negated) || clause.op === "!=" || clause.op === "not_matches_regex" || clause.op === "is_not_null";
501
+ }
502
+ var POSITIVE_OP_ORDER = ["=", ">", ">=", "contains", "starts_with", "ends_with", "matches_regex", "includes"];
503
+ function opPairsForField(field) {
504
+ const ops = opsForField(field);
505
+ const allow = new Set(ops);
506
+ const pairs = [];
507
+ for (const op of POSITIVE_OP_ORDER) {
508
+ if (!allow.has(op)) continue;
509
+ const neg = negateClause({ field: "", op, value: "" }, ops);
510
+ pairs.push({ keep: { op, negated: false }, exclude: { op: neg.op, negated: Boolean(neg.negated) } });
511
+ }
512
+ if (allow.has("is_null") || allow.has("is_not_null")) {
513
+ pairs.push({ keep: { op: "is_null", negated: false }, exclude: { op: "is_not_null", negated: false } });
514
+ }
515
+ return pairs;
516
+ }
289
517
  var OPS_BY_TYPE = {
290
- text: ["=", "!=", "contains", "starts_with", "ends_with", ...NULLITY],
518
+ text: [
519
+ "=",
520
+ "!=",
521
+ "contains",
522
+ "starts_with",
523
+ "ends_with",
524
+ "matches_regex",
525
+ "not_matches_regex",
526
+ ...NULLITY
527
+ ],
291
528
  enum: ["=", "!=", ...NULLITY],
292
529
  number: ["=", "!=", ">", ">=", "<", "<=", ...NULLITY],
293
530
  datetime: ["=", "!=", ">", ">=", "<", "<=", ...NULLITY],
@@ -430,42 +667,50 @@ function toServerQuery(q, schema) {
430
667
  q = normalizeQueryState(q);
431
668
  const byName = indexFields(schema);
432
669
  const resolveField2 = (name) => resolveFieldName(schema, name) ?? name;
433
- const where = q.where.filter((cl) => {
434
- const field = resolveField2(cl.field);
435
- const f = byName.get(field);
436
- return f != null && isPushdownFilter(f);
437
- });
670
+ const where = q.where.filter((term) => termIsPushdown(term, byName, resolveField2));
438
671
  const orderBy = [];
439
672
  for (const term of q.orderBy) {
440
673
  const field = resolveField2(term.field);
441
- const f = byName.get(field);
442
- if (!f || !isSortable(f) || f.source.kind !== "backend") continue;
443
- orderBy.push({ ...term, field: f.sort?.field ?? f.name });
674
+ const f2 = byName.get(field);
675
+ if (!f2 || !isSortable(f2) || f2.source.kind !== "backend") continue;
676
+ orderBy.push({ ...term, field: f2.sort?.field ?? f2.name });
444
677
  }
445
678
  const select = new Set(
446
- selectedFields(schema, q).filter((f) => f.source.kind === "backend").map((f) => f.name)
679
+ selectedFields(schema, q).filter((f2) => f2.source.kind === "backend").map((f2) => f2.name)
447
680
  );
681
+ for (const field of selectedFields(schema, q)) {
682
+ if (field.source.kind === "derived") {
683
+ for (const name of field.source.dependencies ?? []) {
684
+ if (byName.get(name)?.source.kind === "backend") select.add(name);
685
+ }
686
+ }
687
+ }
448
688
  select.add(schema.idField);
449
- for (const cl of q.where) {
450
- const field = resolveField2(cl.field);
451
- const f = byName.get(field);
452
- if (f && isFilterable(f) && !isPushdownFilter(f) && f.source.kind === "backend") select.add(field);
689
+ for (const term of q.where) {
690
+ for (const cl of predicatesOf(term)) {
691
+ const field = resolveField2(cl.field);
692
+ const f2 = byName.get(field);
693
+ if (f2 && isFilterable(f2) && !isPushdownFilter(f2) && f2.source.kind === "backend") select.add(field);
694
+ }
453
695
  }
454
- return { select: [...select], where, orderBy, limit: q.limit, offset: q.offset };
696
+ return { select: [...select].sort(), where, orderBy, limit: q.limit, offset: q.offset };
697
+ }
698
+ function termIsPushdown(term, byName, resolveField2) {
699
+ return predicatesOf(term).every((cl) => {
700
+ const f2 = byName.get(resolveField2(cl.field));
701
+ return f2 != null && isPushdownFilter(f2);
702
+ });
455
703
  }
456
704
  function toAggregationQuery(q, schema) {
457
705
  q = normalizeQueryState(q);
458
706
  const byName = indexFields(schema);
459
707
  const resolveField2 = (name) => resolveFieldName(schema, name) ?? name;
460
- const where = q.where.filter((cl) => {
461
- const f = byName.get(resolveField2(cl.field));
462
- return f != null && isPushdownFilter(f);
463
- });
708
+ const where = q.where.filter((term) => termIsPushdown(term, byName, resolveField2));
464
709
  const isBackend = (name) => {
465
710
  if (name == null) return true;
466
711
  const field = resolveField2(name);
467
- const f = byName.get(field);
468
- return f != null && f.source.kind === "backend";
712
+ const f2 = byName.get(field);
713
+ return f2 != null && f2.source.kind === "backend";
469
714
  };
470
715
  const aggregations = (q.aggregations ?? []).filter(
471
716
  (a) => isBackend(a.field) && a.groupBy.every((g) => isBackend(g))
@@ -477,16 +722,23 @@ function toAggregationQuery(q, schema) {
477
722
  function applyQuery(rows, q, schema) {
478
723
  const byName = indexFields(schema);
479
724
  const resolveField2 = (name) => resolveFieldName(schema, name) ?? name;
480
- const whereClauses = q.where.map((clause) => ({ ...clause, field: resolveField2(clause.field) }));
481
- const orderBy = q.orderBy.map((term) => ({ ...term, field: resolveField2(term.field) }));
482
- let out = rows.filter((row) => whereClauses.every((cl) => matchesWith(byName, row, cl)));
725
+ const whereTerms = q.where.filter((term) => !predicatesOf(term).some((c) => isComputedField(c.field))).map((term) => prepareWhereTerm(term, resolveField2));
726
+ const orderBy = q.orderBy.filter((term) => !isComputedField(term.field)).map((term) => ({ ...term, field: resolveField2(term.field) }));
727
+ let out = rows.filter((row) => whereTerms.every((term) => matchesTerm(byName, row, term)));
483
728
  const total = out.length;
484
729
  if (orderBy.length) {
485
- const terms = orderBy.map((t) => ({ field: byName.get(t.field), dir: t.dir, nullsLast: (t.nulls ?? "last") === "last" })).filter((t) => t.field != null);
730
+ const terms = orderBy.map((t) => ({
731
+ field: byName.get(t.field),
732
+ dir: t.dir,
733
+ nullsLast: (t.nulls ?? "last") === "last",
734
+ extract: compileRegex(t.extract?.regex)
735
+ })).filter(
736
+ (t) => t.field != null
737
+ );
486
738
  out = [...out].sort((a, b) => {
487
739
  for (const t of terms) {
488
- const av = readFieldValue(t.field, a);
489
- const bv = readFieldValue(t.field, b);
740
+ const av = extractSortValue(readFieldValue(t.field, a), t.extract);
741
+ const bv = extractSortValue(readFieldValue(t.field, b), t.extract);
490
742
  const aNull = av == null;
491
743
  const bNull = bv == null;
492
744
  if (aNull || bNull) {
@@ -504,14 +756,14 @@ function applyQuery(rows, q, schema) {
504
756
  return { rows: out.slice(start, end), total };
505
757
  }
506
758
  function matchesClause(row, clause, schema) {
507
- return matchesWith(indexFields(schema), row, clause);
759
+ return matchesWith(indexFields(schema), row, prepareWhereClause(clause));
508
760
  }
509
761
  function applyAggregations(rows, q, schema) {
510
762
  const byName = indexFields(schema);
511
763
  const resolveField2 = (name) => resolveFieldName(schema, name) ?? name;
512
- const whereClauses = q.where.map((clause) => ({ ...clause, field: resolveField2(clause.field) }));
513
- const filtered = rows.filter((row) => whereClauses.every((cl) => matchesWith(byName, row, cl)));
514
- const metrics = (q.aggregations ?? []).map((agg) => ({
764
+ const whereTerms = q.where.filter((term) => !predicatesOf(term).some((c) => isComputedField(c.field))).map((term) => prepareWhereTerm(term, resolveField2));
765
+ const filtered = rows.filter((row) => whereTerms.every((term) => matchesTerm(byName, row, term)));
766
+ const metrics = (q.aggregations ?? []).filter((a) => !isComputedField(a.field ?? "") && !a.groupBy.some(isComputedField)).map((agg) => ({
515
767
  id: agg.id,
516
768
  buckets: computeBuckets(filtered, agg, byName, resolveField2)
517
769
  }));
@@ -522,9 +774,9 @@ function computeBuckets(rows, agg, byName, resolveField2) {
522
774
  const measure = agg.field ? byName.get(resolveField2(agg.field)) : void 0;
523
775
  const groups = /* @__PURE__ */ new Map();
524
776
  for (const row of rows) {
525
- const keys = groupFields.map((f) => {
526
- if (!f) return null;
527
- const v = readFieldValue(f, row);
777
+ const keys = groupFields.map((f2) => {
778
+ if (!f2) return null;
779
+ const v = readFieldValue(f2, row);
528
780
  return v == null || v === "" ? null : String(v);
529
781
  });
530
782
  const k = JSON.stringify(keys);
@@ -593,18 +845,48 @@ function sortBuckets(buckets) {
593
845
  return JSON.stringify(a.keys).localeCompare(JSON.stringify(b.keys));
594
846
  });
595
847
  }
596
- function matchesWith(byName, row, clause) {
848
+ function prepareWhereClause(clause) {
849
+ const usesRegex = clause.op === "matches_regex" || clause.op === "not_matches_regex";
850
+ return { clause, regex: usesRegex ? compileRegex(clause.value) : void 0 };
851
+ }
852
+ function prepareWhereTerm(term, resolveField2) {
853
+ if (isOrGroup(term)) {
854
+ return { kind: "or", predicates: term.any.map((c) => prepareWhereClause({ ...c, field: resolveField2(c.field) })) };
855
+ }
856
+ return { kind: "lit", predicate: prepareWhereClause({ ...term, field: resolveField2(term.field) }) };
857
+ }
858
+ function matchesTerm(byName, row, term) {
859
+ if (term.kind === "lit") return matchesWith(byName, row, term.predicate);
860
+ return term.predicates.some((p) => matchesWith(byName, row, p));
861
+ }
862
+ function matchesWith(byName, row, prepared) {
863
+ const { clause } = prepared;
597
864
  const field = byName.get(clause.field);
598
865
  if (!field) return true;
599
866
  const v = readFieldValue(field, row);
867
+ if (clause.value === "" && clause.op !== "=" && clause.op !== "!=" && clause.op !== "is_null" && clause.op !== "is_not_null") {
868
+ return true;
869
+ }
870
+ const base = matchesBase(field, v, prepared);
871
+ if (!clause.negated) return base;
872
+ const isNull = Array.isArray(v) ? v.length === 0 : v == null || v === "";
873
+ return !isNull && !base;
874
+ }
875
+ function matchesBase(field, v, prepared) {
876
+ const { clause, regex } = prepared;
600
877
  if (clause.op === "is_null") return Array.isArray(v) ? v.length === 0 : v == null || v === "";
601
878
  if (clause.op === "is_not_null") return Array.isArray(v) ? v.length > 0 : v != null && v !== "";
602
- if (clause.value === "" && clause.op !== "=" && clause.op !== "!=") return true;
603
879
  if (clause.op === "includes") {
604
880
  const arr = Array.isArray(v) ? v : [];
605
881
  const needle = clause.value.toLowerCase();
606
882
  return arr.some((x) => String(x).toLowerCase() === needle);
607
883
  }
884
+ if (clause.op === "matches_regex" || clause.op === "not_matches_regex") {
885
+ if (v == null || Array.isArray(v)) return false;
886
+ if (!regex) return false;
887
+ const matches = regex.test(String(v));
888
+ return clause.op === "matches_regex" ? matches : !matches;
889
+ }
608
890
  if (Array.isArray(v)) {
609
891
  const needle = clause.value.toLowerCase();
610
892
  const hay = v.map((x) => String(x).toLowerCase());
@@ -642,6 +924,21 @@ function matchesWith(byName, row, clause) {
642
924
  return true;
643
925
  }
644
926
  }
927
+ function compileRegex(pattern) {
928
+ if (pattern === void 0) return void 0;
929
+ try {
930
+ return new RegExp(pattern);
931
+ } catch {
932
+ return null;
933
+ }
934
+ }
935
+ function extractSortValue(value, regex) {
936
+ if (regex === void 0) return value;
937
+ if (regex === null || value == null) return null;
938
+ const match = regex.exec(String(value));
939
+ if (!match) return null;
940
+ return match.length > 1 ? match[1] ?? null : match[0];
941
+ }
645
942
  function coerceSafe(field, raw) {
646
943
  try {
647
944
  return coerceValue(field.type, raw);
@@ -813,10 +1110,1117 @@ function localStorageAdapter() {
813
1110
  }
814
1111
  };
815
1112
  }
1113
+
1114
+ // src/formula.ts
1115
+ var f = (name, signature, description, min, max, result, args) => ({
1116
+ name,
1117
+ signature,
1118
+ description,
1119
+ min,
1120
+ max,
1121
+ result,
1122
+ args
1123
+ });
1124
+ var FORMULA_FUNCTIONS = [
1125
+ ...["LEFT", "RIGHT"].map(
1126
+ (n) => f(
1127
+ n,
1128
+ `${n}(text, count)`,
1129
+ "Take Unicode code points from the end indicated.",
1130
+ 2,
1131
+ 2,
1132
+ "text",
1133
+ ["text", "number"]
1134
+ )
1135
+ ),
1136
+ f(
1137
+ "SUBSTRING",
1138
+ "SUBSTRING(text, start, length)",
1139
+ "One-based start; length is optional.",
1140
+ 2,
1141
+ 3,
1142
+ "text",
1143
+ ["text", "number", "number"]
1144
+ ),
1145
+ f("LENGTH", "LENGTH(text)", "Count Unicode code points.", 1, 1, "number", [
1146
+ "text"
1147
+ ]),
1148
+ ...["LOWER", "UPPER", "TRIM", "LTRIM", "RTRIM"].map(
1149
+ (n) => f(
1150
+ n,
1151
+ `${n}(text)`,
1152
+ "Change case or remove surrounding whitespace.",
1153
+ 1,
1154
+ 1,
1155
+ "text",
1156
+ ["text"]
1157
+ )
1158
+ ),
1159
+ f(
1160
+ "REPLACE",
1161
+ "REPLACE(text, search, replacement)",
1162
+ "Replace all literal occurrences.",
1163
+ 3,
1164
+ 3,
1165
+ "text",
1166
+ ["text", "text", "text"]
1167
+ ),
1168
+ ...["LPAD", "RPAD"].map(
1169
+ (n) => f(
1170
+ n,
1171
+ `${n}(text, length, padding)`,
1172
+ "Pad to a code-point length; default padding is a space.",
1173
+ 2,
1174
+ 3,
1175
+ "text",
1176
+ ["text", "number", "text"]
1177
+ )
1178
+ ),
1179
+ f(
1180
+ "SPLIT_PART",
1181
+ "SPLIT_PART(text, separator, index)",
1182
+ "One-based part; missing part returns null.",
1183
+ 3,
1184
+ 3,
1185
+ "text",
1186
+ ["text", "text", "number"]
1187
+ ),
1188
+ f(
1189
+ "CONCAT",
1190
+ "CONCAT(text, ...)",
1191
+ "Combine text; null propagates.",
1192
+ 1,
1193
+ 50,
1194
+ "text",
1195
+ ["text"]
1196
+ ),
1197
+ f(
1198
+ "CONCAT_WS",
1199
+ "CONCAT_WS(separator, text, ...)",
1200
+ "Combine text, skipping null arguments.",
1201
+ 2,
1202
+ 50,
1203
+ "text",
1204
+ ["text"]
1205
+ ),
1206
+ ...["CONTAINS", "STARTS_WITH", "ENDS_WITH"].map(
1207
+ (n) => f(
1208
+ n,
1209
+ `${n}(text, search, ignoreCase)`,
1210
+ "Case-sensitive unless the optional third argument is true.",
1211
+ 2,
1212
+ 3,
1213
+ "bool",
1214
+ ["text", "text", "bool"]
1215
+ )
1216
+ ),
1217
+ f(
1218
+ "REGEX_TEST",
1219
+ "REGEX_TEST(text, pattern, flags)",
1220
+ "Test a JavaScript Unicode regex; flags: i, m, s.",
1221
+ 2,
1222
+ 3,
1223
+ "bool",
1224
+ ["text", "text", "text"]
1225
+ ),
1226
+ f(
1227
+ "REGEX_EXTRACT",
1228
+ "REGEX_EXTRACT(text, pattern, group, flags)",
1229
+ "Extract a capture (default 0: whole match); no match returns null.",
1230
+ 2,
1231
+ 4,
1232
+ "text",
1233
+ ["text", "text", "number", "text"]
1234
+ ),
1235
+ f(
1236
+ "REGEX_REPLACE",
1237
+ "REGEX_REPLACE(text, pattern, replacement, flags)",
1238
+ "Replace all matches; supports $1 capture references.",
1239
+ 3,
1240
+ 4,
1241
+ "text",
1242
+ ["text", "text", "text", "text"]
1243
+ ),
1244
+ ...["ABS", "FLOOR", "CEIL", "TRUNC", "SQRT"].map(
1245
+ (n) => f(n, `${n}(number)`, "Numeric transformation.", 1, 1, "number", ["number"])
1246
+ ),
1247
+ f(
1248
+ "ROUND",
1249
+ "ROUND(number, digits)",
1250
+ "Round to decimal places (default 0, range -15 to 15).",
1251
+ 1,
1252
+ 2,
1253
+ "number",
1254
+ ["number"]
1255
+ ),
1256
+ f(
1257
+ "POWER",
1258
+ "POWER(base, exponent)",
1259
+ "Raise a number to a power.",
1260
+ 2,
1261
+ 2,
1262
+ "number",
1263
+ ["number"]
1264
+ ),
1265
+ f(
1266
+ "CLAMP",
1267
+ "CLAMP(number, lower, upper)",
1268
+ "Constrain a number to an inclusive range.",
1269
+ 3,
1270
+ 3,
1271
+ "number",
1272
+ ["number"]
1273
+ ),
1274
+ ...["LEAST", "GREATEST"].map(
1275
+ (n) => f(
1276
+ n,
1277
+ `${n}(number, ...)`,
1278
+ "Compare numbers within this row.",
1279
+ 1,
1280
+ 50,
1281
+ "number",
1282
+ ["number"]
1283
+ )
1284
+ ),
1285
+ f(
1286
+ "IF",
1287
+ "IF(condition, then, otherwise)",
1288
+ "Evaluate only the chosen branch; null condition uses otherwise.",
1289
+ 3,
1290
+ 3,
1291
+ "branch",
1292
+ "any"
1293
+ ),
1294
+ f(
1295
+ "IFS",
1296
+ "IFS(condition, value, ..., fallback)",
1297
+ "First true condition wins; final fallback is required.",
1298
+ 3,
1299
+ 49,
1300
+ "branch",
1301
+ "any"
1302
+ ),
1303
+ f(
1304
+ "SWITCH",
1305
+ "SWITCH(value, match, result, ..., fallback)",
1306
+ "Match a value to a result; final fallback is required.",
1307
+ 4,
1308
+ 50,
1309
+ "branch",
1310
+ "any"
1311
+ ),
1312
+ f(
1313
+ "COALESCE",
1314
+ "COALESCE(value, ...)",
1315
+ "First non-null value, evaluated lazily.",
1316
+ 1,
1317
+ 50,
1318
+ "branch",
1319
+ "any"
1320
+ ),
1321
+ f(
1322
+ "NULLIF",
1323
+ "NULLIF(value, other)",
1324
+ "Return null if the values are equal.",
1325
+ 2,
1326
+ 2,
1327
+ "branch",
1328
+ "any"
1329
+ ),
1330
+ ...["IS_NULL", "IS_EMPTY"].map(
1331
+ (n) => f(
1332
+ n,
1333
+ `${n}(value)`,
1334
+ "Test null, or null/empty text/empty array.",
1335
+ 1,
1336
+ 1,
1337
+ "bool",
1338
+ "any"
1339
+ )
1340
+ ),
1341
+ f(
1342
+ "IFERROR",
1343
+ "IFERROR(value, fallback)",
1344
+ "Use fallback only when evaluating value fails.",
1345
+ 2,
1346
+ 2,
1347
+ "branch",
1348
+ "any"
1349
+ ),
1350
+ ...[
1351
+ ["TO_TEXT", "text"],
1352
+ ["TO_NUMBER", "number"],
1353
+ ["TO_BOOLEAN", "bool"],
1354
+ ["TO_DATETIME", "datetime"]
1355
+ ].map(
1356
+ ([n, t]) => f(
1357
+ n,
1358
+ `${n}(value)`,
1359
+ "Explicit conversion; invalid input produces a cell error.",
1360
+ 1,
1361
+ 1,
1362
+ t,
1363
+ "any"
1364
+ )
1365
+ ),
1366
+ f(
1367
+ "SPLIT",
1368
+ "SPLIT(text, separator)",
1369
+ "Split text into an array.",
1370
+ 2,
1371
+ 2,
1372
+ "textarray",
1373
+ ["text", "text"]
1374
+ ),
1375
+ f("JOIN", "JOIN(array, separator)", "Join an array of text.", 2, 2, "text", [
1376
+ "textarray",
1377
+ "text"
1378
+ ]),
1379
+ f(
1380
+ "ARRAY_LENGTH",
1381
+ "ARRAY_LENGTH(array)",
1382
+ "Count array elements.",
1383
+ 1,
1384
+ 1,
1385
+ "number",
1386
+ ["textarray"]
1387
+ ),
1388
+ f(
1389
+ "ARRAY_CONTAINS",
1390
+ "ARRAY_CONTAINS(array, text)",
1391
+ "Case-sensitive array membership.",
1392
+ 2,
1393
+ 2,
1394
+ "bool",
1395
+ ["textarray", "text"]
1396
+ ),
1397
+ f(
1398
+ "ARRAY_GET",
1399
+ "ARRAY_GET(array, index)",
1400
+ "One-based element; out of range returns null.",
1401
+ 2,
1402
+ 2,
1403
+ "text",
1404
+ ["textarray", "number"]
1405
+ ),
1406
+ ...["ARRAY_UNIQUE", "ARRAY_SORT"].map(
1407
+ (n) => f(
1408
+ n,
1409
+ `${n}(array)`,
1410
+ "Deduplicate or sort text elements by code-point order.",
1411
+ 1,
1412
+ 1,
1413
+ "textarray",
1414
+ ["textarray"]
1415
+ )
1416
+ ),
1417
+ ...["YEAR", "MONTH", "DAY", "HOUR", "WEEKDAY"].map(
1418
+ (n) => f(
1419
+ n,
1420
+ `${n}(datetime)`,
1421
+ "UTC date component; weekday is Monday=1 through Sunday=7.",
1422
+ 1,
1423
+ 1,
1424
+ "number",
1425
+ ["datetime"]
1426
+ )
1427
+ ),
1428
+ f(
1429
+ "DATE_TRUNC",
1430
+ "DATE_TRUNC(unit, datetime)",
1431
+ "UTC truncation: year, month, week (Monday), day, hour, minute, second.",
1432
+ 2,
1433
+ 2,
1434
+ "datetime",
1435
+ ["text", "datetime"]
1436
+ ),
1437
+ f(
1438
+ "DATE_ADD",
1439
+ "DATE_ADD(unit, amount, datetime)",
1440
+ "UTC calendar addition; month/year clamp to the last day.",
1441
+ 3,
1442
+ 3,
1443
+ "datetime",
1444
+ ["text", "number", "datetime"]
1445
+ ),
1446
+ f(
1447
+ "DATE_DIFF",
1448
+ "DATE_DIFF(unit, start, end)",
1449
+ "Elapsed whole units: week, day, hour, minute, second, millisecond.",
1450
+ 3,
1451
+ 3,
1452
+ "number",
1453
+ ["text", "datetime", "datetime"]
1454
+ ),
1455
+ f(
1456
+ "FORMAT_DATE",
1457
+ "FORMAT_DATE(datetime, pattern)",
1458
+ "UTC tokens: YYYY MM DD HH mm ss; other text is literal.",
1459
+ 2,
1460
+ 2,
1461
+ "text",
1462
+ ["datetime", "text"]
1463
+ )
1464
+ ];
1465
+ var FormulaError = class extends Error {
1466
+ constructor(message, from = 0, to = from + 1) {
1467
+ super(message);
1468
+ this.from = from;
1469
+ this.to = to;
1470
+ }
1471
+ };
1472
+ function compileFormula(source, fields, resolve) {
1473
+ if (!source.trim() || source.length > 1e4)
1474
+ throw new FormulaError("Formula must contain 1\u201310,000 characters.");
1475
+ const tokens = [];
1476
+ let pos = 0;
1477
+ while (pos < source.length) {
1478
+ if (/\s/.test(source[pos])) {
1479
+ pos++;
1480
+ continue;
1481
+ }
1482
+ const from = pos;
1483
+ let match;
1484
+ if (source[pos] === "[") {
1485
+ pos++;
1486
+ let text = "";
1487
+ let closed = false;
1488
+ while (pos < source.length) {
1489
+ if (source[pos] === "]") {
1490
+ if (source[pos + 1] === "]") {
1491
+ text += "]";
1492
+ pos += 2;
1493
+ } else {
1494
+ pos++;
1495
+ closed = true;
1496
+ break;
1497
+ }
1498
+ } else text += source[pos++];
1499
+ }
1500
+ if (!closed)
1501
+ throw new FormulaError("Unclosed field reference.", from, pos);
1502
+ tokens.push({ kind: "field", text, from, to: pos });
1503
+ } else if (source[pos] === '"') {
1504
+ pos++;
1505
+ while (pos < source.length && source[pos] !== '"') {
1506
+ if (source[pos] === "\\") pos++;
1507
+ pos++;
1508
+ }
1509
+ pos++;
1510
+ try {
1511
+ tokens.push({
1512
+ kind: "string",
1513
+ text: JSON.parse(source.slice(from, pos)),
1514
+ from,
1515
+ to: pos
1516
+ });
1517
+ } catch {
1518
+ throw new FormulaError("Invalid JSON string literal.", from, pos);
1519
+ }
1520
+ } else if (match = source.slice(pos).match(/^(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?/)) {
1521
+ pos += match[0].length;
1522
+ tokens.push({ kind: "number", text: match[0], from, to: pos });
1523
+ } else if (match = source.slice(pos).match(/^[a-zA-Z_][a-zA-Z_0-9]*/)) {
1524
+ pos += match[0].length;
1525
+ tokens.push({
1526
+ kind: "word",
1527
+ text: match[0].toUpperCase(),
1528
+ from,
1529
+ to: pos
1530
+ });
1531
+ } else if (match = source.slice(pos).match(/^(>=|<=|!=|<>|[=<>+*/%(),-])/)) {
1532
+ pos += match[0].length;
1533
+ tokens.push({ kind: "op", text: match[0], from, to: pos });
1534
+ } else throw new FormulaError(`Unexpected character ${source[pos]}.`, from);
1535
+ if (tokens.length > 2e3)
1536
+ throw new FormulaError("Formula is too complex.", from);
1537
+ }
1538
+ tokens.push({ kind: "end", text: "", from: pos, to: pos });
1539
+ let i = 0, depth = 0;
1540
+ const peek = () => tokens[i];
1541
+ const take = () => tokens[i++];
1542
+ const expect = (s) => {
1543
+ if (peek().text !== s)
1544
+ throw new FormulaError(`Expected ${s}.`, peek().from, peek().to);
1545
+ return take();
1546
+ };
1547
+ const call = (name, args, from, to) => ({ kind: "call", name, args, from, to });
1548
+ const priority = {
1549
+ OR: 1,
1550
+ AND: 2,
1551
+ "=": 3,
1552
+ "!=": 3,
1553
+ "<>": 3,
1554
+ "<": 3,
1555
+ ">": 3,
1556
+ "<=": 3,
1557
+ ">=": 3,
1558
+ IN: 3,
1559
+ BETWEEN: 3,
1560
+ "+": 4,
1561
+ "-": 4,
1562
+ "*": 5,
1563
+ "/": 5,
1564
+ "%": 5
1565
+ };
1566
+ function expression(min = 0) {
1567
+ if (++depth > 50)
1568
+ throw new FormulaError("Maximum nesting depth is 50.", peek().from);
1569
+ const t = take();
1570
+ let node;
1571
+ if (t.kind === "number") {
1572
+ if (!Number.isFinite(Number(t.text)))
1573
+ throw new FormulaError("Number must be finite.", t.from, t.to);
1574
+ node = { kind: "literal", value: Number(t.text), from: t.from, to: t.to };
1575
+ } else if (t.kind === "string")
1576
+ node = { kind: "literal", value: t.text, from: t.from, to: t.to };
1577
+ else if (t.kind === "field")
1578
+ node = { kind: "field", name: t.text, from: t.from, to: t.to };
1579
+ else if (["TRUE", "FALSE", "NULL"].includes(t.text))
1580
+ node = {
1581
+ kind: "literal",
1582
+ value: t.text === "NULL" ? null : t.text === "TRUE",
1583
+ from: t.from,
1584
+ to: t.to
1585
+ };
1586
+ else if (t.text === "(") {
1587
+ node = expression();
1588
+ expect(")");
1589
+ } else if (["NOT", "-", "+"].includes(t.text)) {
1590
+ const arg = expression(t.text === "NOT" ? 3 : 6);
1591
+ node = call(`unary:${t.text}`, [arg], t.from, arg.to);
1592
+ } else if (t.kind === "word") {
1593
+ expect("(");
1594
+ const args = [];
1595
+ if (peek().text !== ")") {
1596
+ do {
1597
+ args.push(expression());
1598
+ if (peek().text !== ",") break;
1599
+ take();
1600
+ } while (true);
1601
+ }
1602
+ node = call(t.text, args, t.from, expect(")").to);
1603
+ } else
1604
+ throw new FormulaError(
1605
+ "Expected a value, field, or function.",
1606
+ t.from,
1607
+ t.to
1608
+ );
1609
+ while (peek().kind !== "end" && (priority[peek().text] ?? -1) >= min) {
1610
+ const op = take();
1611
+ const p = priority[op.text];
1612
+ if (op.text === "IN") {
1613
+ expect("(");
1614
+ const args = [node];
1615
+ do {
1616
+ args.push(expression());
1617
+ if (peek().text !== ",") break;
1618
+ take();
1619
+ } while (true);
1620
+ node = call("op:IN", args, node.from, expect(")").to);
1621
+ } else if (op.text === "BETWEEN") {
1622
+ const lo = expression(p + 1);
1623
+ expect("AND");
1624
+ const hi = expression(p + 1);
1625
+ node = call("op:BETWEEN", [node, lo, hi], node.from, hi.to);
1626
+ } else {
1627
+ const right = expression(p + 1);
1628
+ node = call(`op:${op.text}`, [node, right], node.from, right.to);
1629
+ }
1630
+ }
1631
+ depth--;
1632
+ return node;
1633
+ }
1634
+ const ast = expression();
1635
+ if (peek().kind !== "end")
1636
+ throw new FormulaError("Unexpected token.", peek().from, peek().to);
1637
+ const deps = /* @__PURE__ */ new Set();
1638
+ const byName = new Map(fields.map((v) => [v.name, v.type]));
1639
+ const same = (ts, n) => {
1640
+ const real = [...new Set(ts.filter((t) => t !== "null"))];
1641
+ if (real.length > 1)
1642
+ throw new FormulaError(
1643
+ "Values must have compatible types; use an explicit conversion.",
1644
+ n.from,
1645
+ n.to
1646
+ );
1647
+ return real[0] ?? "null";
1648
+ };
1649
+ function check(n, level = 0) {
1650
+ if (level > 50)
1651
+ throw new FormulaError("Maximum nesting depth is 50.", n.from, n.to);
1652
+ if (n.kind === "literal")
1653
+ return n.value === null ? "null" : typeof n.value === "boolean" ? "bool" : typeof n.value === "number" ? "number" : "text";
1654
+ if (n.kind === "field") {
1655
+ const t = byName.get(n.name);
1656
+ if (!t) {
1657
+ const nested = resolve?.(n.name);
1658
+ if (nested) {
1659
+ Object.assign(n, nested.ast);
1660
+ nested.dependencies.forEach((d) => deps.add(d));
1661
+ return nested.type;
1662
+ }
1663
+ throw new FormulaError(
1664
+ `Unknown or unavailable field: ${n.name}`,
1665
+ n.from,
1666
+ n.to
1667
+ );
1668
+ }
1669
+ deps.add(n.name);
1670
+ n.valueType = t === "enum" ? "text" : t;
1671
+ return n.valueType;
1672
+ }
1673
+ const ts = n.args.map((a) => check(a, level + 1));
1674
+ const need = (index, t) => {
1675
+ if (ts[index] !== void 0 && ts[index] !== "null" && ts[index] !== t)
1676
+ throw new FormulaError(
1677
+ `Expected ${t}, received ${ts[index]}.`,
1678
+ n.args[index].from,
1679
+ n.args[index].to
1680
+ );
1681
+ };
1682
+ if (n.name.startsWith("op:") || n.name.startsWith("unary:")) {
1683
+ const op = n.name.split(":")[1];
1684
+ if (["AND", "OR", "NOT"].includes(op)) {
1685
+ ts.forEach((_, j) => need(j, "bool"));
1686
+ return "bool";
1687
+ }
1688
+ if (["+", "-", "*", "/", "%"].includes(op)) {
1689
+ ts.forEach((_, j) => need(j, "number"));
1690
+ return "number";
1691
+ }
1692
+ same(ts, n);
1693
+ return "bool";
1694
+ }
1695
+ const spec = FORMULA_FUNCTIONS.find((v) => v.name === n.name);
1696
+ if (!spec)
1697
+ throw new FormulaError(`Unknown function: ${n.name}`, n.from, n.to);
1698
+ if (ts.length < spec.min || ts.length > spec.max)
1699
+ throw new FormulaError(`Use ${spec.signature}.`, n.from, n.to);
1700
+ if (spec.args !== "any") {
1701
+ const args = spec.args;
1702
+ ts.forEach((_, j) => need(j, args[Math.min(j, args.length - 1)]));
1703
+ }
1704
+ if (n.name === "IF") {
1705
+ need(0, "bool");
1706
+ return same(ts.slice(1), n);
1707
+ }
1708
+ if (n.name === "IFS") {
1709
+ if (ts.length % 2 !== 1)
1710
+ throw new FormulaError(
1711
+ "IFS requires condition/value pairs and a fallback.",
1712
+ n.from,
1713
+ n.to
1714
+ );
1715
+ const branches = [];
1716
+ ts.forEach((t, j) => {
1717
+ if (j === ts.length - 1 || j % 2 === 1) branches.push(t);
1718
+ else need(j, "bool");
1719
+ });
1720
+ return same(branches, n);
1721
+ }
1722
+ if (n.name === "SWITCH") {
1723
+ if (ts.length % 2 !== 0)
1724
+ throw new FormulaError(
1725
+ "SWITCH requires match/result pairs and a fallback.",
1726
+ n.from,
1727
+ n.to
1728
+ );
1729
+ const branches = [];
1730
+ for (let j = 1; j < ts.length - 1; j += 2) {
1731
+ same([ts[0], ts[j]], n);
1732
+ branches.push(ts[j + 1]);
1733
+ }
1734
+ branches.push(ts[ts.length - 1]);
1735
+ return same(branches, n);
1736
+ }
1737
+ if (n.name.startsWith("REGEX_")) {
1738
+ const pattern = n.args[1];
1739
+ const flags = n.args[n.name === "REGEX_TEST" ? 2 : 3];
1740
+ if (pattern?.kind === "literal" && typeof pattern.value === "string") {
1741
+ if (pattern.value.length > 2e3)
1742
+ throw new FormulaError(
1743
+ "Regex pattern exceeds 2,000 characters.",
1744
+ pattern.from,
1745
+ pattern.to
1746
+ );
1747
+ if (!flags || flags.kind === "literal" && typeof flags.value === "string") {
1748
+ const value = flags?.value ?? "";
1749
+ if (!/^(?!.*(.).*\1)[ims]*$/.test(String(value)))
1750
+ throw new FormulaError(
1751
+ "Regex flags must be unique i, m, or s.",
1752
+ n.from,
1753
+ n.to
1754
+ );
1755
+ try {
1756
+ new RegExp(pattern.value, `${value}u`);
1757
+ } catch {
1758
+ throw new FormulaError(
1759
+ "Invalid regex pattern.",
1760
+ pattern.from,
1761
+ pattern.to
1762
+ );
1763
+ }
1764
+ }
1765
+ }
1766
+ }
1767
+ if (spec.result === "branch") return same(ts, n);
1768
+ return spec.result;
1769
+ }
1770
+ const type = check(ast);
1771
+ return { ast, dependencies: [...deps].sort(), type };
1772
+ }
1773
+ function formulaRuntime(ast, inputs) {
1774
+ const fail = (s) => {
1775
+ throw new Error(s);
1776
+ };
1777
+ const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : fail("Expected a finite number.");
1778
+ const str = (v) => typeof v === "string" ? v : fail("Expected text.");
1779
+ const arr = (v) => Array.isArray(v) && v.every((x) => typeof x === "string") ? v : fail("Expected a text array.");
1780
+ const integer = (v, min = 0, max = 1e5) => {
1781
+ const n = num(v);
1782
+ return Number.isInteger(n) && n >= min && n <= max ? n : fail(`Expected an integer between ${min} and ${max}.`);
1783
+ };
1784
+ const equal = (a, b) => JSON.stringify(a) === JSON.stringify(b);
1785
+ const date = (v) => {
1786
+ const s = str(v);
1787
+ if (!/^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?(?:Z|[+-]\d{2}:\d{2}))?$/.test(
1788
+ s
1789
+ ))
1790
+ fail("Expected an ISO date or timestamp with timezone.");
1791
+ const d = new Date(s);
1792
+ const day = Number(s.slice(8, 10)), month = Number(s.slice(5, 7));
1793
+ if (!Number.isFinite(d.getTime()) || month < 1 || month > 12 || day < 1 || day > new Date(Date.UTC(Number(s.slice(0, 4)), month, 0)).getUTCDate())
1794
+ fail("Invalid date.");
1795
+ return d;
1796
+ };
1797
+ const bound = (v) => {
1798
+ if (v !== null && typeof v !== "number" && typeof v !== "string" && typeof v !== "boolean" && !Array.isArray(v))
1799
+ fail("Unsupported input value type.");
1800
+ if (Array.isArray(v) && !v.every((x) => typeof x === "string"))
1801
+ fail("Expected a text array.");
1802
+ if (typeof v === "number" && !Number.isFinite(v))
1803
+ fail("Result is not finite.");
1804
+ if (typeof v === "string" && v.length > 1e5)
1805
+ fail("Text result exceeds 100,000 characters.");
1806
+ if (Array.isArray(v) && (v.length > 1e4 || v.reduce((s, x) => s + x.length, 0) > 1e5))
1807
+ fail("Array result is too large.");
1808
+ return v;
1809
+ };
1810
+ let inspection;
1811
+ let budget = 5e3;
1812
+ function run(n) {
1813
+ if (--budget < 0) fail("Formula operation limit exceeded.");
1814
+ if (n.kind === "literal") return n.value;
1815
+ if (n.kind === "field") {
1816
+ if (!Object.prototype.hasOwnProperty.call(inputs, n.name)) return null;
1817
+ const v = bound(inputs[n.name] ?? null);
1818
+ if (v === null) return null;
1819
+ if (n.valueType === "number") return num(v);
1820
+ if (n.valueType === "bool" && typeof v !== "boolean")
1821
+ fail("Expected a boolean.");
1822
+ if (n.valueType === "datetime") return date(v).toISOString();
1823
+ if (n.valueType === "text") return str(v);
1824
+ if (n.valueType === "textarray") return arr(v);
1825
+ return v;
1826
+ }
1827
+ const name = n.name;
1828
+ const args = n.args;
1829
+ if (name === "IF") return run(run(args[0]) === true ? args[1] : args[2]);
1830
+ if (name === "IFS") {
1831
+ for (let j = 0; j < args.length - 1; j += 2)
1832
+ if (run(args[j]) === true) return run(args[j + 1]);
1833
+ return run(args[args.length - 1]);
1834
+ }
1835
+ if (name === "SWITCH") {
1836
+ const v = run(args[0]);
1837
+ for (let j = 1; j < args.length - 1; j += 2)
1838
+ if (equal(v, run(args[j]))) return run(args[j + 1]);
1839
+ return run(args[args.length - 1]);
1840
+ }
1841
+ if (name === "COALESCE") {
1842
+ for (const arg of args) {
1843
+ const v = run(arg);
1844
+ if (v !== null) return v;
1845
+ }
1846
+ return null;
1847
+ }
1848
+ if (name === "IFERROR") {
1849
+ try {
1850
+ return run(args[0]);
1851
+ } catch {
1852
+ return run(args[1]);
1853
+ }
1854
+ }
1855
+ if (name === "op:AND" || name === "op:OR") {
1856
+ const a2 = run(args[0]);
1857
+ if (name === "op:AND" && a2 === false) return false;
1858
+ if (name === "op:OR" && a2 === true) return true;
1859
+ const b2 = run(args[1]);
1860
+ return name === "op:AND" ? b2 === false ? false : a2 === null || b2 === null ? null : true : b2 === true ? true : a2 === null || b2 === null ? null : false;
1861
+ }
1862
+ const vs = args.map(run);
1863
+ const a = vs[0] ?? null, b = vs[1] ?? null, c = vs[2] ?? null;
1864
+ if (name === "IS_NULL") return a === null;
1865
+ if (name === "IS_EMPTY")
1866
+ return a === null || a === "" || Array.isArray(a) && a.length === 0;
1867
+ if (name === "NULLIF") return equal(a, b) ? null : a;
1868
+ if (name === "CONCAT_WS")
1869
+ return a === null ? null : bound(
1870
+ vs.slice(1).filter((v) => v !== null).map(str).join(str(a))
1871
+ );
1872
+ if (name === "op:IN") {
1873
+ if (a === null) return null;
1874
+ if (vs.slice(1).some((v) => v !== null && equal(a, v))) return true;
1875
+ return vs.slice(1).includes(null) ? null : false;
1876
+ }
1877
+ if (vs.some((v) => v === null)) return null;
1878
+ let out;
1879
+ switch (name) {
1880
+ case "unary:NOT":
1881
+ out = !a;
1882
+ break;
1883
+ case "unary:-":
1884
+ out = -num(a);
1885
+ break;
1886
+ case "unary:+":
1887
+ out = num(a);
1888
+ break;
1889
+ case "op:+":
1890
+ out = num(a) + num(b);
1891
+ break;
1892
+ case "op:-":
1893
+ out = num(a) - num(b);
1894
+ break;
1895
+ case "op:*":
1896
+ out = num(a) * num(b);
1897
+ break;
1898
+ case "op:/":
1899
+ case "op:%":
1900
+ if (num(b) === 0) fail("Division by zero.");
1901
+ out = name === "op:/" ? num(a) / num(b) : num(a) % num(b);
1902
+ break;
1903
+ case "op:=":
1904
+ out = equal(a, b);
1905
+ break;
1906
+ case "op:!=":
1907
+ case "op:<>":
1908
+ out = !equal(a, b);
1909
+ break;
1910
+ case "op:<":
1911
+ out = a < b;
1912
+ break;
1913
+ case "op:>":
1914
+ out = a > b;
1915
+ break;
1916
+ case "op:<=":
1917
+ out = a <= b;
1918
+ break;
1919
+ case "op:>=":
1920
+ out = a >= b;
1921
+ break;
1922
+ case "op:BETWEEN":
1923
+ out = a >= b && a <= c;
1924
+ break;
1925
+ case "LEFT":
1926
+ out = Array.from(str(a)).slice(0, integer(b)).join("");
1927
+ break;
1928
+ case "RIGHT": {
1929
+ const n2 = integer(b);
1930
+ out = n2 ? Array.from(str(a)).slice(-n2).join("") : "";
1931
+ break;
1932
+ }
1933
+ case "SUBSTRING": {
1934
+ const start = integer(b, 1) - 1;
1935
+ out = Array.from(str(a)).slice(start, vs.length > 2 ? start + integer(c) : void 0).join("");
1936
+ break;
1937
+ }
1938
+ case "LENGTH":
1939
+ out = Array.from(str(a)).length;
1940
+ break;
1941
+ case "LOWER":
1942
+ out = str(a).toLowerCase();
1943
+ break;
1944
+ case "UPPER":
1945
+ out = str(a).toUpperCase();
1946
+ break;
1947
+ case "TRIM":
1948
+ out = str(a).trim();
1949
+ break;
1950
+ case "LTRIM":
1951
+ out = str(a).trimStart();
1952
+ break;
1953
+ case "RTRIM":
1954
+ out = str(a).trimEnd();
1955
+ break;
1956
+ case "REPLACE":
1957
+ if (b === "") fail("Search text must not be empty.");
1958
+ const parts = str(a).split(str(b));
1959
+ if (parts.length * str(c).length + str(a).length > 2e5)
1960
+ fail("Replacement result is too large.");
1961
+ out = parts.join(str(c));
1962
+ break;
1963
+ case "LPAD":
1964
+ case "RPAD": {
1965
+ const chars = Array.from(str(a)), len = integer(b), pad = Array.from(vs.length > 2 ? str(c) : " ");
1966
+ if (!pad.length) fail("Padding must not be empty.");
1967
+ const fill = Array.from(
1968
+ { length: Math.max(0, len - chars.length) },
1969
+ (_, i) => pad[i % pad.length]
1970
+ ).join("");
1971
+ out = (name === "LPAD" ? fill : "") + chars.slice(0, len).join("") + (name === "RPAD" ? fill : "");
1972
+ break;
1973
+ }
1974
+ case "SPLIT_PART":
1975
+ out = str(a).split(str(b))[integer(c, 1) - 1] ?? null;
1976
+ break;
1977
+ case "CONCAT":
1978
+ out = vs.map(str).join("");
1979
+ break;
1980
+ case "CONTAINS":
1981
+ case "STARTS_WITH":
1982
+ case "ENDS_WITH": {
1983
+ const s = c === true ? str(a).toLowerCase() : str(a), t = c === true ? str(b).toLowerCase() : str(b);
1984
+ out = name === "CONTAINS" ? s.includes(t) : name === "STARTS_WITH" ? s.startsWith(t) : s.endsWith(t);
1985
+ break;
1986
+ }
1987
+ case "REGEX_TEST":
1988
+ case "REGEX_EXTRACT":
1989
+ case "REGEX_REPLACE": {
1990
+ const flags = vs[name === "REGEX_TEST" ? 2 : 3] ?? "";
1991
+ if (!/^(?!.*(.).*\1)[ims]*$/.test(str(flags)))
1992
+ fail("Regex flags must be unique i, m, or s.");
1993
+ if (str(b).length > 2e3)
1994
+ fail("Regex pattern exceeds 2,000 characters.");
1995
+ const re = new RegExp(
1996
+ str(b),
1997
+ `${flags}u${name === "REGEX_REPLACE" ? "g" : ""}`
1998
+ );
1999
+ if (name === "REGEX_TEST") out = re.test(str(a));
2000
+ else if (name === "REGEX_REPLACE") {
2001
+ let produced = 0;
2002
+ out = str(a).replace(re, (...captures) => {
2003
+ const replacement = str(c).replace(
2004
+ /\$(\$|[0-9]{1,2}|&|`|')/g,
2005
+ (_token, key) => {
2006
+ if (key === "$") return "$";
2007
+ if (key === "&") return String(captures[0]);
2008
+ if (key === "`" || key === "'")
2009
+ return fail(
2010
+ "Regex replacement supports $$, $&, and numbered captures only."
2011
+ );
2012
+ const extra = typeof captures[captures.length - 1] === "object" ? 3 : 2;
2013
+ const i = Number(key);
2014
+ return i > 0 && i < captures.length - extra ? String(captures[i] ?? "") : fail("Invalid replacement capture.");
2015
+ }
2016
+ );
2017
+ produced += replacement.length;
2018
+ if (produced > 1e5) fail("Replacement result is too large.");
2019
+ return replacement;
2020
+ });
2021
+ } else {
2022
+ const match = re.exec(str(a));
2023
+ const group = vs.length > 2 ? integer(c, 0, 100) : 0;
2024
+ out = match?.[group] ?? null;
2025
+ if (match)
2026
+ inspection = {
2027
+ input: str(a),
2028
+ start: match.index,
2029
+ end: match.index + match[0].length,
2030
+ groups: Array.from(match, (v) => v ?? null)
2031
+ };
2032
+ }
2033
+ break;
2034
+ }
2035
+ case "ABS":
2036
+ out = Math.abs(num(a));
2037
+ break;
2038
+ case "FLOOR":
2039
+ out = Math.floor(num(a));
2040
+ break;
2041
+ case "CEIL":
2042
+ out = Math.ceil(num(a));
2043
+ break;
2044
+ case "TRUNC":
2045
+ out = Math.trunc(num(a));
2046
+ break;
2047
+ case "SQRT":
2048
+ out = Math.sqrt(num(a));
2049
+ break;
2050
+ case "ROUND": {
2051
+ const scale = 10 ** (vs.length > 1 ? integer(b, -15, 15) : 0);
2052
+ out = Math.round(num(a) * scale) / scale;
2053
+ break;
2054
+ }
2055
+ case "POWER":
2056
+ out = Math.pow(num(a), num(b));
2057
+ break;
2058
+ case "CLAMP":
2059
+ if (num(b) > num(c)) fail("Lower bound exceeds upper bound.");
2060
+ out = Math.max(num(b), Math.min(num(c), num(a)));
2061
+ break;
2062
+ case "LEAST":
2063
+ out = Math.min(...vs.map(num));
2064
+ break;
2065
+ case "GREATEST":
2066
+ out = Math.max(...vs.map(num));
2067
+ break;
2068
+ case "TO_TEXT":
2069
+ out = Array.isArray(a) ? JSON.stringify(a) : String(a);
2070
+ break;
2071
+ case "TO_NUMBER":
2072
+ if (typeof a !== "number" && (typeof a !== "string" || !/^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/.test(a.trim())))
2073
+ fail("Cannot convert to number.");
2074
+ out = Number(a);
2075
+ break;
2076
+ case "TO_BOOLEAN":
2077
+ if (a === true || a === false) out = a;
2078
+ else if (a === "true" || a === 1) out = true;
2079
+ else if (a === "false" || a === 0) out = false;
2080
+ else return fail("Cannot convert to boolean.");
2081
+ break;
2082
+ case "TO_DATETIME":
2083
+ out = date(a).toISOString();
2084
+ break;
2085
+ case "SPLIT":
2086
+ out = str(a).split(str(b));
2087
+ break;
2088
+ case "JOIN":
2089
+ out = arr(a).join(str(b));
2090
+ break;
2091
+ case "ARRAY_LENGTH":
2092
+ out = arr(a).length;
2093
+ break;
2094
+ case "ARRAY_CONTAINS":
2095
+ out = arr(a).includes(str(b));
2096
+ break;
2097
+ case "ARRAY_GET":
2098
+ out = arr(a)[integer(b, 1) - 1] ?? null;
2099
+ break;
2100
+ case "ARRAY_UNIQUE":
2101
+ out = [...new Set(arr(a))];
2102
+ break;
2103
+ case "ARRAY_SORT":
2104
+ out = [...arr(a)].sort((left, right) => {
2105
+ const l = Array.from(left), r = Array.from(right);
2106
+ for (let i = 0; i < Math.min(l.length, r.length); i++) {
2107
+ const delta = l[i].codePointAt(0) - r[i].codePointAt(0);
2108
+ if (delta) return delta;
2109
+ }
2110
+ return l.length - r.length;
2111
+ });
2112
+ break;
2113
+ case "YEAR":
2114
+ out = date(a).getUTCFullYear();
2115
+ break;
2116
+ case "MONTH":
2117
+ out = date(a).getUTCMonth() + 1;
2118
+ break;
2119
+ case "DAY":
2120
+ out = date(a).getUTCDate();
2121
+ break;
2122
+ case "HOUR":
2123
+ out = date(a).getUTCHours();
2124
+ break;
2125
+ case "WEEKDAY":
2126
+ out = (date(a).getUTCDay() + 6) % 7 + 1;
2127
+ break;
2128
+ case "DATE_TRUNC": {
2129
+ const d = date(b), unit = str(a).toLowerCase();
2130
+ if (![
2131
+ "year",
2132
+ "month",
2133
+ "week",
2134
+ "day",
2135
+ "hour",
2136
+ "minute",
2137
+ "second"
2138
+ ].includes(unit))
2139
+ fail("Unsupported date unit.");
2140
+ d.setUTCMilliseconds(0);
2141
+ if (unit !== "second") d.setUTCSeconds(0);
2142
+ if (!["second", "minute"].includes(unit)) d.setUTCMinutes(0);
2143
+ if (["year", "month", "week", "day"].includes(unit)) d.setUTCHours(0);
2144
+ if (unit === "week")
2145
+ d.setUTCDate(d.getUTCDate() - (d.getUTCDay() + 6) % 7);
2146
+ if (unit === "year" || unit === "month") d.setUTCDate(1);
2147
+ if (unit === "year") d.setUTCMonth(0);
2148
+ out = d.toISOString();
2149
+ break;
2150
+ }
2151
+ case "DATE_ADD": {
2152
+ const d = date(c), unit = str(a).toLowerCase(), amount = integer(b, -1e5, 1e5);
2153
+ if (unit === "year" || unit === "month") {
2154
+ const day = d.getUTCDate();
2155
+ d.setUTCDate(1);
2156
+ d.setUTCMonth(d.getUTCMonth() + amount * (unit === "year" ? 12 : 1));
2157
+ const last = new Date(d.getTime());
2158
+ last.setUTCMonth(last.getUTCMonth() + 1, 0);
2159
+ d.setUTCDate(Math.min(day, last.getUTCDate()));
2160
+ } else {
2161
+ const units = {
2162
+ week: 6048e5,
2163
+ day: 864e5,
2164
+ hour: 36e5,
2165
+ minute: 6e4,
2166
+ second: 1e3,
2167
+ millisecond: 1
2168
+ };
2169
+ const ms = units[unit];
2170
+ if (!ms) fail("Unsupported date unit.");
2171
+ d.setTime(d.getTime() + amount * ms);
2172
+ }
2173
+ out = d.toISOString();
2174
+ break;
2175
+ }
2176
+ case "DATE_DIFF": {
2177
+ const units = {
2178
+ week: 6048e5,
2179
+ day: 864e5,
2180
+ hour: 36e5,
2181
+ minute: 6e4,
2182
+ second: 1e3,
2183
+ millisecond: 1
2184
+ };
2185
+ const ms = units[str(a).toLowerCase()];
2186
+ if (!ms) fail("Unsupported elapsed date unit.");
2187
+ out = Math.trunc((date(c).getTime() - date(b).getTime()) / ms);
2188
+ break;
2189
+ }
2190
+ case "FORMAT_DATE": {
2191
+ const d = date(a), pad = (n2) => String(n2).padStart(2, "0"), parts2 = {
2192
+ YYYY: String(d.getUTCFullYear()).padStart(4, "0"),
2193
+ MM: pad(d.getUTCMonth() + 1),
2194
+ DD: pad(d.getUTCDate()),
2195
+ HH: pad(d.getUTCHours()),
2196
+ mm: pad(d.getUTCMinutes()),
2197
+ ss: pad(d.getUTCSeconds())
2198
+ };
2199
+ out = str(b).replace(/YYYY|MM|DD|HH|mm|ss/g, (t) => parts2[t]);
2200
+ break;
2201
+ }
2202
+ default:
2203
+ return fail(`Unknown operation: ${name}`);
2204
+ }
2205
+ return bound(out);
2206
+ }
2207
+ try {
2208
+ const value = run(ast);
2209
+ return inspection ? { value, regex: inspection } : { value };
2210
+ } catch (e) {
2211
+ return {
2212
+ value: null,
2213
+ error: e instanceof Error ? e.message : "Formula evaluation failed."
2214
+ };
2215
+ }
2216
+ }
816
2217
  export {
817
2218
  AGG_OPS_BY_TYPE,
818
2219
  AGG_OPS_NEEDING_FIELD,
2220
+ COMPUTED_PREFIX,
819
2221
  EMPTY_QUERY,
2222
+ FORMULA_FUNCTIONS,
2223
+ FormulaError,
820
2224
  MAX_AGGREGATIONS,
821
2225
  MAX_GROUP_BY_FIELDS,
822
2226
  MAX_ORDER_BY_TERMS,
@@ -833,27 +2237,41 @@ export {
833
2237
  applyAggregations,
834
2238
  applyQuery,
835
2239
  coerceValue,
2240
+ compileFormula,
2241
+ computedFieldName,
836
2242
  decodeQuery,
837
2243
  encodeQuery,
838
2244
  filterValues,
2245
+ formulaRuntime,
2246
+ groupPreview,
2247
+ httpComputedColumnStore,
839
2248
  indexFields,
2249
+ isComputedCellError,
2250
+ isComputedField,
840
2251
  isFilterable,
841
2252
  isGroupable,
842
2253
  isMeasurable,
2254
+ isNegativePredicate,
2255
+ isOrGroup,
843
2256
  isPushdownFilter,
844
2257
  isSelectable,
845
2258
  isSortable,
846
2259
  loadSchema,
847
2260
  localStorageAdapter,
848
2261
  matchesClause,
2262
+ memoryComputedColumnStore,
849
2263
  memoryStorageAdapter,
2264
+ negateClause,
850
2265
  normalizeQueryState,
851
2266
  opAllowedForType,
2267
+ opPairsForField,
852
2268
  opsForField,
2269
+ predicatesOf,
853
2270
  queriesEqual,
854
2271
  readFieldValue,
855
2272
  selectedFields,
856
2273
  toAggregationQuery,
857
- toServerQuery
2274
+ toServerQuery,
2275
+ validateComputedColumn
858
2276
  };
859
2277
  //# sourceMappingURL=index.js.map