@query-farm/vgi 0.6.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/LICENSE +134 -0
- package/README.md +494 -0
- package/dist/__dbg.test-d.d.ts +1 -0
- package/dist/arguments/argument-spec.d.ts +51 -0
- package/dist/arguments/arguments.d.ts +13 -0
- package/dist/arrow/codec/branded.d.ts +83 -0
- package/dist/arrow/codec/registry.d.ts +29 -0
- package/dist/arrow/codec/repr.d.ts +23 -0
- package/dist/arrow/codec/type-descriptors.d.ts +109 -0
- package/dist/arrow/impl-arrowjs/build.d.ts +19 -0
- package/dist/arrow/impl-arrowjs/canonical.d.ts +15 -0
- package/dist/arrow/impl-arrowjs/empty.d.ts +7 -0
- package/dist/arrow/impl-arrowjs/filter.d.ts +11 -0
- package/dist/arrow/impl-arrowjs/index.d.ts +11 -0
- package/dist/arrow/impl-arrowjs/ipc.d.ts +24 -0
- package/dist/arrow/impl-arrowjs/iterate.d.ts +39 -0
- package/dist/arrow/impl-arrowjs/project.d.ts +10 -0
- package/dist/arrow/impl-arrowjs/schema.d.ts +62 -0
- package/dist/arrow/impl-arrowjs/statistics.d.ts +13 -0
- package/dist/arrow/impl-flechette/build.d.ts +14 -0
- package/dist/arrow/impl-flechette/canonical.d.ts +18 -0
- package/dist/arrow/impl-flechette/empty.d.ts +9 -0
- package/dist/arrow/impl-flechette/filter.d.ts +10 -0
- package/dist/arrow/impl-flechette/index.d.ts +11 -0
- package/dist/arrow/impl-flechette/ipc.d.ts +33 -0
- package/dist/arrow/impl-flechette/iterate.d.ts +29 -0
- package/dist/arrow/impl-flechette/message-meta.d.ts +10 -0
- package/dist/arrow/impl-flechette/normalize-type.d.ts +11 -0
- package/dist/arrow/impl-flechette/project.d.ts +3 -0
- package/dist/arrow/impl-flechette/schema.d.ts +47 -0
- package/dist/arrow/impl-flechette/statistics.d.ts +13 -0
- package/dist/arrow/index.d.ts +11 -0
- package/dist/arrow/predicates.d.ts +47 -0
- package/dist/arrow/repr.test-d.d.ts +52 -0
- package/dist/arrow/schema-types.d.ts +71 -0
- package/dist/arrow/types.d.ts +57 -0
- package/dist/catalog/attach-option.d.ts +33 -0
- package/dist/catalog/attach-options.d.ts +30 -0
- package/dist/catalog/composite.d.ts +33 -0
- package/dist/catalog/descriptors.d.ts +154 -0
- package/dist/catalog/interface.d.ts +239 -0
- package/dist/catalog/read-only.d.ts +33 -0
- package/dist/client/client.d.ts +189 -0
- package/dist/client/errors.d.ts +26 -0
- package/dist/client/helpers.d.ts +12 -0
- package/dist/client/protocol.d.ts +16 -0
- package/dist/client/types.d.ts +125 -0
- package/dist/client-entry.d.ts +11 -0
- package/dist/client-entry.js +48 -0
- package/dist/client-entry.js.map +9 -0
- package/dist/codec/asd.d.ts +25 -0
- package/dist/crypto.d.ts +36 -0
- package/dist/errors.d.ts +24 -0
- package/dist/filter-pushdown/collector.d.ts +32 -0
- package/dist/filter-pushdown/deserialize.d.ts +16 -0
- package/dist/filter-pushdown/evaluate.d.ts +51 -0
- package/dist/filter-pushdown/index.d.ts +4 -0
- package/dist/filter-pushdown/types.d.ts +84 -0
- package/dist/functions/aggregate.d.ts +129 -0
- package/dist/functions/copy-from.d.ts +81 -0
- package/dist/functions/copy-to.d.ts +121 -0
- package/dist/functions/registry.d.ts +16 -0
- package/dist/functions/scalar.d.ts +98 -0
- package/dist/functions/storage-cf-do.d.ts +47 -0
- package/dist/functions/storage.d.ts +86 -0
- package/dist/functions/table-buffering.d.ts +99 -0
- package/dist/functions/table-in-out.d.ts +64 -0
- package/dist/functions/table.d.ts +118 -0
- package/dist/functions/types.d.ts +157 -0
- package/dist/generated/vgi-client.d.ts +466 -0
- package/dist/generated/vgi-protocol-schemas.d.ts +115 -0
- package/dist/index.core.d.ts +42 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +10888 -0
- package/dist/index.js.map +85 -0
- package/dist/metadata/resolve.d.ts +3 -0
- package/dist/metadata/serialize.d.ts +12 -0
- package/dist/metadata/types.d.ts +47 -0
- package/dist/protocol/dispatch.d.ts +23 -0
- package/dist/protocol/handlers/aggregate.d.ts +3 -0
- package/dist/protocol/handlers/catalog/admin.d.ts +3 -0
- package/dist/protocol/handlers/catalog/index.d.ts +3 -0
- package/dist/protocol/handlers/catalog/index_methods.d.ts +3 -0
- package/dist/protocol/handlers/catalog/macro.d.ts +3 -0
- package/dist/protocol/handlers/catalog/shared.d.ts +70 -0
- package/dist/protocol/handlers/catalog/table.d.ts +3 -0
- package/dist/protocol/handlers/catalog/view.d.ts +3 -0
- package/dist/protocol/handlers/function.d.ts +8 -0
- package/dist/protocol/handlers/shared.d.ts +31 -0
- package/dist/protocol/handlers/table-buffering.d.ts +3 -0
- package/dist/protocol/serialize.d.ts +4 -0
- package/dist/protocol/serializers/arguments.d.ts +3 -0
- package/dist/protocol/serializers/bind.d.ts +9 -0
- package/dist/protocol/serializers/cardinality.d.ts +7 -0
- package/dist/protocol/serializers/init.d.ts +9 -0
- package/dist/protocol/serializers/shared.d.ts +12 -0
- package/dist/protocol/state-serializer.d.ts +23 -0
- package/dist/protocol/types.d.ts +129 -0
- package/dist/secrets/helpers.d.ts +15 -0
- package/dist/types.d.ts +52 -0
- package/dist/util/arrow/index.d.ts +1 -0
- package/dist/util/bytes.d.ts +2 -0
- package/dist/util/statistics.d.ts +7 -0
- package/dist/worker-cf-entry.d.ts +30 -0
- package/dist/worker-cf-entry.js +8767 -0
- package/dist/worker-cf-entry.js.map +74 -0
- package/dist/worker.d.ts +36 -0
- package/package.json +96 -0
- package/src/arguments/argument-spec.ts +200 -0
- package/src/arguments/arguments.ts +81 -0
- package/src/arrow/codec/branded.ts +206 -0
- package/src/arrow/codec/registry.ts +674 -0
- package/src/arrow/codec/repr.ts +181 -0
- package/src/arrow/codec/type-descriptors.ts +137 -0
- package/src/arrow/impl-arrowjs/build.ts +88 -0
- package/src/arrow/impl-arrowjs/canonical.ts +509 -0
- package/src/arrow/impl-arrowjs/empty.ts +72 -0
- package/src/arrow/impl-arrowjs/filter.ts +53 -0
- package/src/arrow/impl-arrowjs/index.ts +54 -0
- package/src/arrow/impl-arrowjs/ipc.ts +67 -0
- package/src/arrow/impl-arrowjs/iterate.ts +159 -0
- package/src/arrow/impl-arrowjs/project.ts +73 -0
- package/src/arrow/impl-arrowjs/schema.ts +256 -0
- package/src/arrow/impl-arrowjs/statistics.ts +185 -0
- package/src/arrow/impl-flechette/build.ts +65 -0
- package/src/arrow/impl-flechette/canonical.ts +289 -0
- package/src/arrow/impl-flechette/empty.ts +20 -0
- package/src/arrow/impl-flechette/filter.ts +42 -0
- package/src/arrow/impl-flechette/index.ts +54 -0
- package/src/arrow/impl-flechette/ipc.ts +110 -0
- package/src/arrow/impl-flechette/iterate.ts +121 -0
- package/src/arrow/impl-flechette/message-meta.ts +175 -0
- package/src/arrow/impl-flechette/normalize-type.ts +99 -0
- package/src/arrow/impl-flechette/project.ts +64 -0
- package/src/arrow/impl-flechette/schema.ts +202 -0
- package/src/arrow/impl-flechette/statistics.ts +198 -0
- package/src/arrow/index.ts +129 -0
- package/src/arrow/predicates.ts +66 -0
- package/src/arrow/repr.test-d.ts +189 -0
- package/src/arrow/schema-types.ts +213 -0
- package/src/arrow/types.ts +74 -0
- package/src/catalog/attach-option.ts +107 -0
- package/src/catalog/attach-options.ts +79 -0
- package/src/catalog/composite.ts +180 -0
- package/src/catalog/descriptors.ts +164 -0
- package/src/catalog/interface.ts +721 -0
- package/src/catalog/read-only.ts +909 -0
- package/src/client/client.ts +1348 -0
- package/src/client/errors.ts +69 -0
- package/src/client/helpers.ts +56 -0
- package/src/client/protocol.ts +33 -0
- package/src/client/types.ts +140 -0
- package/src/client-entry.ts +112 -0
- package/src/codec/asd.ts +147 -0
- package/src/crypto.ts +143 -0
- package/src/errors.ts +61 -0
- package/src/filter-pushdown/collector.ts +147 -0
- package/src/filter-pushdown/deserialize.ts +253 -0
- package/src/filter-pushdown/evaluate.ts +591 -0
- package/src/filter-pushdown/index.ts +28 -0
- package/src/filter-pushdown/types.ts +91 -0
- package/src/functions/aggregate.ts +384 -0
- package/src/functions/copy-from.ts +282 -0
- package/src/functions/copy-to.ts +319 -0
- package/src/functions/registry.ts +258 -0
- package/src/functions/scalar.ts +356 -0
- package/src/functions/storage-cf-do.ts +303 -0
- package/src/functions/storage.ts +521 -0
- package/src/functions/table-buffering.ts +562 -0
- package/src/functions/table-in-out.ts +373 -0
- package/src/functions/table.ts +521 -0
- package/src/functions/types.ts +192 -0
- package/src/generated/vgi-client.ts +560 -0
- package/src/generated/vgi-protocol-schemas.ts +864 -0
- package/src/index.core.ts +267 -0
- package/src/index.ts +11 -0
- package/src/metadata/resolve.ts +109 -0
- package/src/metadata/serialize.ts +242 -0
- package/src/metadata/types.ts +60 -0
- package/src/protocol/dispatch.ts +52 -0
- package/src/protocol/handlers/aggregate.ts +355 -0
- package/src/protocol/handlers/catalog/admin.ts +368 -0
- package/src/protocol/handlers/catalog/index.ts +25 -0
- package/src/protocol/handlers/catalog/index_methods.ts +65 -0
- package/src/protocol/handlers/catalog/macro.ts +114 -0
- package/src/protocol/handlers/catalog/shared.ts +291 -0
- package/src/protocol/handlers/catalog/table.ts +401 -0
- package/src/protocol/handlers/catalog/view.ts +119 -0
- package/src/protocol/handlers/function.ts +302 -0
- package/src/protocol/handlers/shared.ts +80 -0
- package/src/protocol/handlers/table-buffering.ts +207 -0
- package/src/protocol/serialize.ts +23 -0
- package/src/protocol/serializers/arguments.ts +160 -0
- package/src/protocol/serializers/bind.ts +212 -0
- package/src/protocol/serializers/cardinality.ts +45 -0
- package/src/protocol/serializers/init.ts +247 -0
- package/src/protocol/serializers/shared.ts +46 -0
- package/src/protocol/state-serializer.ts +181 -0
- package/src/protocol/types.ts +153 -0
- package/src/secrets/helpers.ts +70 -0
- package/src/types.ts +71 -0
- package/src/util/arrow/index.ts +23 -0
- package/src/util/bytes.ts +14 -0
- package/src/util/statistics.ts +40 -0
- package/src/worker-cf-entry.ts +172 -0
- package/src/worker.ts +212 -0
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
// Copyright 2025, 2026 Query Farm LLC - https://query.farm
|
|
2
|
+
// Filter pushdown evaluation: row-by-row predicate evaluation against
|
|
3
|
+
// Arrow RecordBatches, plus the PushdownFilters class that owns a parsed
|
|
4
|
+
// filter set and applies it to batches.
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
type VgiBatch,
|
|
8
|
+
type VgiSchema,
|
|
9
|
+
type VgiDataType,
|
|
10
|
+
schema,
|
|
11
|
+
readCanonicalValue,
|
|
12
|
+
} from "../arrow/index.js";
|
|
13
|
+
import { batchFromColumns, filterBatch } from "../util/arrow/index.js";
|
|
14
|
+
import { codecFor } from "../arrow/codec/registry.js";
|
|
15
|
+
import {
|
|
16
|
+
ComparisonOp,
|
|
17
|
+
type AndFilter,
|
|
18
|
+
type ConstantFilter,
|
|
19
|
+
type ExpressionFilter,
|
|
20
|
+
type ExprNode,
|
|
21
|
+
type Filter,
|
|
22
|
+
type InFilter,
|
|
23
|
+
type IsNotNullFilter,
|
|
24
|
+
type IsNullFilter,
|
|
25
|
+
type OrFilter,
|
|
26
|
+
type StructFilter,
|
|
27
|
+
} from "./types.js";
|
|
28
|
+
|
|
29
|
+
// ============================================================================
|
|
30
|
+
// Comparison helpers
|
|
31
|
+
// ============================================================================
|
|
32
|
+
|
|
33
|
+
function compare(a: any, b: any, op: ComparisonOp): boolean {
|
|
34
|
+
switch (op) {
|
|
35
|
+
case ComparisonOp.EQ: return a === b;
|
|
36
|
+
case ComparisonOp.NE: return a !== b;
|
|
37
|
+
case ComparisonOp.GT: return a > b;
|
|
38
|
+
case ComparisonOp.GE: return a >= b;
|
|
39
|
+
case ComparisonOp.LT: return a < b;
|
|
40
|
+
case ComparisonOp.LE: return a <= b;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Read a column cell at `index` in CANONICAL form (the same representation the
|
|
46
|
+
* filter literals are deserialized into — see deserialize.ts `getValue`). Going
|
|
47
|
+
* through the per-backend canonical reader makes this work on BOTH Arrow
|
|
48
|
+
* backends (arrow-js's `col.get` and flechette's `col.at` are not interchange-
|
|
49
|
+
* able) and guarantees temporal/decimal cells and literals compare like-for-
|
|
50
|
+
* like (e.g. timestamp -> bigint on both sides). `colIndex` resolves the field
|
|
51
|
+
* type from the batch schema.
|
|
52
|
+
*/
|
|
53
|
+
function readCell(batch: VgiBatch, colIndex: number, index: number): any {
|
|
54
|
+
const col = batch.getChildAt(colIndex);
|
|
55
|
+
if (!col) return null;
|
|
56
|
+
const type = batch.schema.fields[colIndex].type as unknown as VgiDataType;
|
|
57
|
+
return readCanonicalValue(type, col, index);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ============================================================================
|
|
61
|
+
// Filter evaluation (row-by-row)
|
|
62
|
+
// ============================================================================
|
|
63
|
+
|
|
64
|
+
function evaluateFilter(
|
|
65
|
+
filter: Filter,
|
|
66
|
+
batch: VgiBatch,
|
|
67
|
+
mask: Uint8Array,
|
|
68
|
+
): void {
|
|
69
|
+
switch (filter.type) {
|
|
70
|
+
case "constant":
|
|
71
|
+
evaluateConstant(filter, batch, mask);
|
|
72
|
+
break;
|
|
73
|
+
case "is_null":
|
|
74
|
+
evaluateIsNull(filter, batch, mask);
|
|
75
|
+
break;
|
|
76
|
+
case "is_not_null":
|
|
77
|
+
evaluateIsNotNull(filter, batch, mask);
|
|
78
|
+
break;
|
|
79
|
+
case "in":
|
|
80
|
+
evaluateIn(filter, batch, mask);
|
|
81
|
+
break;
|
|
82
|
+
case "and":
|
|
83
|
+
evaluateAnd(filter, batch, mask);
|
|
84
|
+
break;
|
|
85
|
+
case "or":
|
|
86
|
+
evaluateOr(filter, batch, mask);
|
|
87
|
+
break;
|
|
88
|
+
case "struct":
|
|
89
|
+
evaluateStruct(filter, batch, mask);
|
|
90
|
+
break;
|
|
91
|
+
case "expression":
|
|
92
|
+
evaluateExpression(filter, batch, mask);
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Recursively evaluate an expression AST against a row of the batch, returning
|
|
98
|
+
// the scalar value it produces. column_ref reads from the batch using the
|
|
99
|
+
// caller-supplied remap (DuckDB rewrites column refs inside an ExpressionFilter
|
|
100
|
+
// so index=0 means "this filter's column"); constant returns its pre-resolved
|
|
101
|
+
// JS value; function dispatches on function_name; comparison/conjunction
|
|
102
|
+
// combine children.
|
|
103
|
+
function evaluateExpr(
|
|
104
|
+
node: ExprNode,
|
|
105
|
+
batch: VgiBatch,
|
|
106
|
+
rowIndex: number,
|
|
107
|
+
colRefToBatchIndex: (index: number) => number,
|
|
108
|
+
): any {
|
|
109
|
+
switch (node.expr_type) {
|
|
110
|
+
case "column_ref":
|
|
111
|
+
return readCell(batch, colRefToBatchIndex(node.index), rowIndex);
|
|
112
|
+
case "constant":
|
|
113
|
+
return node.value;
|
|
114
|
+
case "function":
|
|
115
|
+
return evalFunction(
|
|
116
|
+
node.function_name,
|
|
117
|
+
node.children.map((c) => evaluateExpr(c, batch, rowIndex, colRefToBatchIndex)),
|
|
118
|
+
);
|
|
119
|
+
case "comparison": {
|
|
120
|
+
const left = evaluateExpr(node.left, batch, rowIndex, colRefToBatchIndex);
|
|
121
|
+
const right = evaluateExpr(node.right, batch, rowIndex, colRefToBatchIndex);
|
|
122
|
+
if (left == null || right == null) return null;
|
|
123
|
+
return compare(left, right, node.op);
|
|
124
|
+
}
|
|
125
|
+
case "conjunction": {
|
|
126
|
+
if (node.conjunction_type === "and") {
|
|
127
|
+
for (const c of node.children) {
|
|
128
|
+
const v = evaluateExpr(c, batch, rowIndex, colRefToBatchIndex);
|
|
129
|
+
if (v !== true) return v === false ? false : null;
|
|
130
|
+
}
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
let sawNull = false;
|
|
134
|
+
for (const c of node.children) {
|
|
135
|
+
const v = evaluateExpr(c, batch, rowIndex, colRefToBatchIndex);
|
|
136
|
+
if (v === true) return true;
|
|
137
|
+
if (v == null) sawNull = true;
|
|
138
|
+
}
|
|
139
|
+
return sawNull ? null : false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Dispatch DuckDB scalar-function names we support as pushdown filters.
|
|
145
|
+
// Return null for any missing argument (SQL three-valued logic).
|
|
146
|
+
function evalFunction(name: string, args: any[]): any {
|
|
147
|
+
if (args.some((a) => a == null)) return null;
|
|
148
|
+
switch (name) {
|
|
149
|
+
case "list_contains":
|
|
150
|
+
case "array_contains": {
|
|
151
|
+
// args[0] is an Arrow ListVector row (iterable), args[1] is the needle.
|
|
152
|
+
const [list, needle] = args;
|
|
153
|
+
if (!list || typeof list[Symbol.iterator] !== "function") return false;
|
|
154
|
+
for (const v of list) {
|
|
155
|
+
if (v === needle) return true;
|
|
156
|
+
}
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
case "starts_with":
|
|
160
|
+
case "prefix": {
|
|
161
|
+
const [hay, needle] = args;
|
|
162
|
+
return typeof hay === "string" && typeof needle === "string"
|
|
163
|
+
? hay.startsWith(needle) : false;
|
|
164
|
+
}
|
|
165
|
+
case "contains": {
|
|
166
|
+
const [hay, needle] = args;
|
|
167
|
+
return typeof hay === "string" && typeof needle === "string"
|
|
168
|
+
? hay.includes(needle) : false;
|
|
169
|
+
}
|
|
170
|
+
case "&&":
|
|
171
|
+
case "st_intersects_extent": {
|
|
172
|
+
// Bounding-box intersection between two WKB geometries. Parses each
|
|
173
|
+
// geometry's XY bbox and checks for overlap in both dimensions.
|
|
174
|
+
const [a, b] = args;
|
|
175
|
+
const ba = wkbBBox(a);
|
|
176
|
+
const bb = wkbBBox(b);
|
|
177
|
+
if (!ba || !bb) return false;
|
|
178
|
+
return !(ba.maxX < bb.minX || ba.minX > bb.maxX ||
|
|
179
|
+
ba.maxY < bb.minY || ba.minY > bb.maxY);
|
|
180
|
+
}
|
|
181
|
+
default:
|
|
182
|
+
// Unsupported functions shouldn't make it past the C++-side
|
|
183
|
+
// `ExpressionTreeIsSupported` gate, but be defensive: evaluate to null
|
|
184
|
+
// so the row is filtered out conservatively rather than silently
|
|
185
|
+
// producing wrong results.
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Parse the XY bounding box of a WKB geometry. Supports Point and Polygon for
|
|
191
|
+
// ST_MakeEnvelope output; extend as new geometry kinds are needed.
|
|
192
|
+
function wkbBBox(bytes: any): { minX: number; minY: number; maxX: number; maxY: number } | null {
|
|
193
|
+
if (!(bytes instanceof Uint8Array)) return null;
|
|
194
|
+
if (bytes.length < 5) return null;
|
|
195
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
196
|
+
const le = view.getUint8(0) === 1;
|
|
197
|
+
const type = view.getUint32(1, le);
|
|
198
|
+
// Point (type=1): 21 bytes total (5 header + 8+8 coords)
|
|
199
|
+
if (type === 1 && bytes.length >= 21) {
|
|
200
|
+
const x = view.getFloat64(5, le);
|
|
201
|
+
const y = view.getFloat64(13, le);
|
|
202
|
+
return { minX: x, minY: y, maxX: x, maxY: y };
|
|
203
|
+
}
|
|
204
|
+
// Polygon (type=3): used by ST_MakeEnvelope. One ring, 4 points, close.
|
|
205
|
+
// Header 5 + num_rings 4 + num_points 4 + 4*16 coord bytes = 81 bytes.
|
|
206
|
+
if (type === 3 && bytes.length >= 81) {
|
|
207
|
+
const numRings = view.getUint32(5, le);
|
|
208
|
+
if (numRings < 1) return null;
|
|
209
|
+
const numPoints = view.getUint32(9, le);
|
|
210
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
211
|
+
let off = 13;
|
|
212
|
+
for (let i = 0; i < numPoints && off + 16 <= bytes.length; i++, off += 16) {
|
|
213
|
+
const x = view.getFloat64(off, le);
|
|
214
|
+
const y = view.getFloat64(off + 8, le);
|
|
215
|
+
if (x < minX) minX = x;
|
|
216
|
+
if (x > maxX) maxX = x;
|
|
217
|
+
if (y < minY) minY = y;
|
|
218
|
+
if (y > maxY) maxY = y;
|
|
219
|
+
}
|
|
220
|
+
if (minX === Infinity) return null;
|
|
221
|
+
return { minX, minY, maxX, maxY };
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function evaluateExpression(
|
|
227
|
+
filter: ExpressionFilter,
|
|
228
|
+
batch: VgiBatch,
|
|
229
|
+
mask: Uint8Array,
|
|
230
|
+
): void {
|
|
231
|
+
const n = batch.numRows;
|
|
232
|
+
// DuckDB's FilterSerializer rewrites bound references inside an
|
|
233
|
+
// ExpressionFilter so that column_ref index 0 refers to the filter's own
|
|
234
|
+
// column (see ReplaceWithBoundReference in DuckDB's optimizer). Higher
|
|
235
|
+
// indices on the same filter aren't produced — the filter is always
|
|
236
|
+
// scoped to a single column in the outer batch — so we map every
|
|
237
|
+
// column_ref back to `filter.columnIndex` by name, resolving through the
|
|
238
|
+
// current batch in case projection reordered columns.
|
|
239
|
+
const colName = filter.columnName;
|
|
240
|
+
let targetIdx = batch.schema.fields.findIndex((f) => f.name === colName);
|
|
241
|
+
if (targetIdx < 0) targetIdx = filter.columnIndex;
|
|
242
|
+
const colRef = (_nodeIndex: number) => targetIdx;
|
|
243
|
+
for (let i = 0; i < n; i++) {
|
|
244
|
+
if (!mask[i]) continue;
|
|
245
|
+
const v = evaluateExpr(filter.expr, batch, i, colRef);
|
|
246
|
+
mask[i] = v === true ? 1 : 0;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function evaluateConstant(
|
|
251
|
+
filter: ConstantFilter,
|
|
252
|
+
batch: VgiBatch,
|
|
253
|
+
mask: Uint8Array,
|
|
254
|
+
): void {
|
|
255
|
+
const n = batch.numRows;
|
|
256
|
+
for (let i = 0; i < n; i++) {
|
|
257
|
+
if (!mask[i]) continue;
|
|
258
|
+
const cell = readCell(batch, filter.columnIndex, i);
|
|
259
|
+
if (cell === null) {
|
|
260
|
+
mask[i] = 0;
|
|
261
|
+
} else {
|
|
262
|
+
mask[i] = compare(cell, filter.value, filter.op) ? 1 : 0;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function evaluateIsNull(
|
|
268
|
+
filter: IsNullFilter,
|
|
269
|
+
batch: VgiBatch,
|
|
270
|
+
mask: Uint8Array,
|
|
271
|
+
): void {
|
|
272
|
+
const n = batch.numRows;
|
|
273
|
+
for (let i = 0; i < n; i++) {
|
|
274
|
+
if (!mask[i]) continue;
|
|
275
|
+
mask[i] = (readCell(batch, filter.columnIndex, i) !== null) ? 0 : 1;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function evaluateIsNotNull(
|
|
280
|
+
filter: IsNotNullFilter,
|
|
281
|
+
batch: VgiBatch,
|
|
282
|
+
mask: Uint8Array,
|
|
283
|
+
): void {
|
|
284
|
+
const n = batch.numRows;
|
|
285
|
+
for (let i = 0; i < n; i++) {
|
|
286
|
+
if (!mask[i]) continue;
|
|
287
|
+
mask[i] = (readCell(batch, filter.columnIndex, i) !== null) ? 1 : 0;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function evaluateIn(
|
|
292
|
+
filter: InFilter,
|
|
293
|
+
batch: VgiBatch,
|
|
294
|
+
mask: Uint8Array,
|
|
295
|
+
): void {
|
|
296
|
+
const n = batch.numRows;
|
|
297
|
+
for (let i = 0; i < n; i++) {
|
|
298
|
+
if (!mask[i]) continue;
|
|
299
|
+
const cell = readCell(batch, filter.columnIndex, i);
|
|
300
|
+
if (cell === null) {
|
|
301
|
+
mask[i] = 0;
|
|
302
|
+
} else {
|
|
303
|
+
mask[i] = filter.values.has(cell) ? 1 : 0;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function evaluateAnd(
|
|
309
|
+
filter: AndFilter,
|
|
310
|
+
batch: VgiBatch,
|
|
311
|
+
mask: Uint8Array,
|
|
312
|
+
): void {
|
|
313
|
+
// AND: start with all-pass, then narrow with each child
|
|
314
|
+
const n = batch.numRows;
|
|
315
|
+
const childMask = new Uint8Array(n);
|
|
316
|
+
|
|
317
|
+
for (const child of filter.children) {
|
|
318
|
+
childMask.fill(1);
|
|
319
|
+
evaluateFilter(child, batch, childMask);
|
|
320
|
+
for (let i = 0; i < n; i++) {
|
|
321
|
+
mask[i] &= childMask[i];
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function evaluateOr(
|
|
327
|
+
filter: OrFilter,
|
|
328
|
+
batch: VgiBatch,
|
|
329
|
+
mask: Uint8Array,
|
|
330
|
+
): void {
|
|
331
|
+
// OR: start with all-fail, then widen with each child
|
|
332
|
+
const n = batch.numRows;
|
|
333
|
+
const result = new Uint8Array(n); // all zeros
|
|
334
|
+
const childMask = new Uint8Array(n);
|
|
335
|
+
|
|
336
|
+
for (const child of filter.children) {
|
|
337
|
+
childMask.fill(1);
|
|
338
|
+
evaluateFilter(child, batch, childMask);
|
|
339
|
+
for (let i = 0; i < n; i++) {
|
|
340
|
+
result[i] |= childMask[i];
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Combine with incoming mask
|
|
345
|
+
for (let i = 0; i < n; i++) {
|
|
346
|
+
mask[i] &= result[i];
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function evaluateStruct(
|
|
351
|
+
filter: StructFilter,
|
|
352
|
+
batch: VgiBatch,
|
|
353
|
+
mask: Uint8Array,
|
|
354
|
+
): void {
|
|
355
|
+
const structType = batch.schema.fields[filter.columnIndex].type as unknown as VgiDataType;
|
|
356
|
+
const childField = (structType as any).children?.[filter.childIndex];
|
|
357
|
+
if (!childField) {
|
|
358
|
+
// Child field not found — fail all rows
|
|
359
|
+
mask.fill(0);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
const childType = childField.type as VgiDataType;
|
|
363
|
+
|
|
364
|
+
// Read the struct column in canonical form (backend-agnostic), pull out the
|
|
365
|
+
// target child's canonical value per row, and rebuild a single-column child
|
|
366
|
+
// batch via batchFromColumns (which re-runs canonical writes). Filter literals
|
|
367
|
+
// are compared in canonical form too (see deserialize.ts), so the child column
|
|
368
|
+
// cells and literals stay in matching representations across both backends.
|
|
369
|
+
const childCanonical = Array.from({ length: batch.numRows }, (_, i) => {
|
|
370
|
+
const structVal = readCell(batch, filter.columnIndex, i) as Record<string, unknown> | null;
|
|
371
|
+
return structVal == null ? null : (structVal[childField.name] ?? null);
|
|
372
|
+
});
|
|
373
|
+
// batchFromColumns expects RICH values; map canonical -> rich for the child.
|
|
374
|
+
const childCodec = codecFor(childType);
|
|
375
|
+
const childRich = childCanonical.map((v) => childCodec.canonicalToRich(v));
|
|
376
|
+
|
|
377
|
+
const childSchema = schema([
|
|
378
|
+
(structType as any).children[filter.childIndex],
|
|
379
|
+
]);
|
|
380
|
+
const childBatch = batchFromColumns(
|
|
381
|
+
{ [filter.childName]: childRich },
|
|
382
|
+
childSchema,
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
// Remap child filter's columnIndex to 0 for the wrapper batch
|
|
386
|
+
const remapped = { ...filter.childFilter, columnIndex: 0 };
|
|
387
|
+
evaluateFilter(remapped, childBatch, mask);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// ============================================================================
|
|
391
|
+
// SQL formatting
|
|
392
|
+
// ============================================================================
|
|
393
|
+
|
|
394
|
+
const OP_SYMBOLS: Record<ComparisonOp, string> = {
|
|
395
|
+
[ComparisonOp.EQ]: "=",
|
|
396
|
+
[ComparisonOp.NE]: "!=",
|
|
397
|
+
[ComparisonOp.GT]: ">",
|
|
398
|
+
[ComparisonOp.GE]: ">=",
|
|
399
|
+
[ComparisonOp.LT]: "<",
|
|
400
|
+
[ComparisonOp.LE]: "<=",
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
function formatValue(v: any): string {
|
|
404
|
+
if (typeof v === "string") return `'${v}'`;
|
|
405
|
+
if (typeof v === "bigint") return String(v);
|
|
406
|
+
return String(v);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function filterToSql(f: Filter): string {
|
|
410
|
+
const col = f.columnName;
|
|
411
|
+
|
|
412
|
+
switch (f.type) {
|
|
413
|
+
case "constant":
|
|
414
|
+
return `${col} ${OP_SYMBOLS[f.op]} ${formatValue(f.value)}`;
|
|
415
|
+
case "is_null":
|
|
416
|
+
return `${col} IS NULL`;
|
|
417
|
+
case "is_not_null":
|
|
418
|
+
return `${col} IS NOT NULL`;
|
|
419
|
+
case "in": {
|
|
420
|
+
// Match Python's repr: summarize large lists as `N values`, otherwise
|
|
421
|
+
// render each element. Threshold matches vgi-python's InFilter.__repr__.
|
|
422
|
+
const size = f.values.size;
|
|
423
|
+
if (size > 20) {
|
|
424
|
+
return `${col} IN (${size} values)`;
|
|
425
|
+
}
|
|
426
|
+
const vals = [...f.values].map(formatValue).join(", ");
|
|
427
|
+
return `${col} IN (${vals})`;
|
|
428
|
+
}
|
|
429
|
+
case "and": {
|
|
430
|
+
const parts = f.children.map(filterToSql);
|
|
431
|
+
return `(${parts.join(" AND ")})`;
|
|
432
|
+
}
|
|
433
|
+
case "or": {
|
|
434
|
+
const parts = f.children.map(filterToSql);
|
|
435
|
+
return `(${parts.join(" OR ")})`;
|
|
436
|
+
}
|
|
437
|
+
case "struct": {
|
|
438
|
+
const nested = `${f.columnName}.${f.childName}`;
|
|
439
|
+
const remapped = { ...f.childFilter, columnName: nested };
|
|
440
|
+
return filterToSql(remapped);
|
|
441
|
+
}
|
|
442
|
+
case "expression":
|
|
443
|
+
// Expression filters don't have a natural SQL rendering here — the
|
|
444
|
+
// tree came from DuckDB's bound-expression serializer. Use a generic
|
|
445
|
+
// placeholder so debug output stays readable.
|
|
446
|
+
return `${f.columnName} MATCHES (expression)`;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// ============================================================================
|
|
451
|
+
// PushdownFilters
|
|
452
|
+
// ============================================================================
|
|
453
|
+
|
|
454
|
+
export class PushdownFilters {
|
|
455
|
+
constructor(
|
|
456
|
+
readonly filters: Filter[],
|
|
457
|
+
readonly version: string,
|
|
458
|
+
) {}
|
|
459
|
+
|
|
460
|
+
/** Convert filters to a human-readable SQL-like string. */
|
|
461
|
+
toSql(): string {
|
|
462
|
+
if (this.filters.length === 0) return "";
|
|
463
|
+
return this.filters.map(filterToSql).join(" AND ");
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** Evaluate filters against a batch, returning a Uint8Array mask (0=fail, 1=pass). */
|
|
467
|
+
evaluate(batch: VgiBatch): Uint8Array {
|
|
468
|
+
const n = batch.numRows;
|
|
469
|
+
const mask = new Uint8Array(n);
|
|
470
|
+
mask.fill(1);
|
|
471
|
+
|
|
472
|
+
for (const filter of this.filters) {
|
|
473
|
+
evaluateFilter(filter, batch, mask);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return mask;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/** Apply filters to a batch, returning only passing rows. */
|
|
480
|
+
apply(batch: VgiBatch): VgiBatch {
|
|
481
|
+
if (batch.numRows === 0 || this.filters.length === 0) return batch;
|
|
482
|
+
|
|
483
|
+
const mask = this.evaluate(batch);
|
|
484
|
+
// filterBatch rebuilds via the codec/canonical path (backend-agnostic,
|
|
485
|
+
// lossless), including its own all-pass / all-fail fast paths.
|
|
486
|
+
return filterBatch(batch, mask);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* The set of column names referenced by the pushed-down filters, sorted
|
|
491
|
+
* ascending. Descends one level into AndFilter children (DuckDB pushes
|
|
492
|
+
* `col IN (...) AND col>=min` as a single AndFilter), consistent with
|
|
493
|
+
* `getColumnValues` / `hasFilterForColumn`. Mirrors the cross-language
|
|
494
|
+
* `filtered_columns` accessor (vgi-python / vgi-go / vgi-rust).
|
|
495
|
+
*/
|
|
496
|
+
filteredColumns(): string[] {
|
|
497
|
+
const cols = new Set<string>();
|
|
498
|
+
for (const f of this.filters) {
|
|
499
|
+
if (f.type === "and") {
|
|
500
|
+
for (const c of f.children) cols.add(c.columnName);
|
|
501
|
+
} else {
|
|
502
|
+
cols.add(f.columnName);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
return Array.from(cols).sort();
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Whether any pushed-down filter references `columnName`. Mirrors the
|
|
510
|
+
* cross-language `has_filter_for_column` accessor.
|
|
511
|
+
*/
|
|
512
|
+
hasFilterForColumn(columnName: string): boolean {
|
|
513
|
+
return this.collectColumnFilters(columnName).length > 0;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Discrete values a column could take, from equality (`=`) or `IN` filters.
|
|
518
|
+
* Useful for partition pruning (resolve the key set up front, fetch only
|
|
519
|
+
* those keys). Returns null when the predicate is not enumerable — no
|
|
520
|
+
* filter, a bare range, or an OR with a non-discrete branch.
|
|
521
|
+
*
|
|
522
|
+
* Mirrors vgi-python's `PushdownFilters.get_column_values`: descends one
|
|
523
|
+
* level into an AndFilter (DuckDB pushes `col IN (...) AND col>=min AND
|
|
524
|
+
* col<=max` as a single AndFilter from a semi-join), and unions OR branches
|
|
525
|
+
* only when every branch pins the column to discrete values.
|
|
526
|
+
*/
|
|
527
|
+
getColumnValues(columnName: string): any[] | null {
|
|
528
|
+
for (const f of this.collectColumnFilters(columnName)) {
|
|
529
|
+
if (f.type === "constant" && f.op === ComparisonOp.EQ) {
|
|
530
|
+
return [f.value];
|
|
531
|
+
} else if (f.type === "in") {
|
|
532
|
+
return Array.from(f.values);
|
|
533
|
+
} else if (f.type === "or") {
|
|
534
|
+
const union = this.orDiscreteValues(f, columnName);
|
|
535
|
+
if (union !== null) return union;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Union of discrete values for `columnName` across all OR branches, iff every
|
|
543
|
+
* child constrains it to discrete values (`=` or `IN`); otherwise null (the
|
|
544
|
+
* column could take any value through a non-discrete branch). Returning one
|
|
545
|
+
* branch's values would be an unsafe subset.
|
|
546
|
+
*/
|
|
547
|
+
private orDiscreteValues(orFilter: OrFilter, columnName: string): any[] | null {
|
|
548
|
+
const values: any[] = [];
|
|
549
|
+
for (const child of orFilter.children) {
|
|
550
|
+
if (child.columnName !== columnName) return null;
|
|
551
|
+
if (child.type === "constant" && child.op === ComparisonOp.EQ) {
|
|
552
|
+
values.push(child.value);
|
|
553
|
+
} else if (child.type === "in") {
|
|
554
|
+
for (const v of child.values) values.push(v);
|
|
555
|
+
} else {
|
|
556
|
+
return null;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
if (values.length === 0) return null;
|
|
560
|
+
// Deduplicate, preserving first-seen order for stable output.
|
|
561
|
+
const seen = new Set<any>();
|
|
562
|
+
const deduped: any[] = [];
|
|
563
|
+
for (const v of values) {
|
|
564
|
+
if (!seen.has(v)) {
|
|
565
|
+
seen.add(v);
|
|
566
|
+
deduped.push(v);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return deduped;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Collect filters for a column from the top level and direct AND children
|
|
574
|
+
* (one level of descent, consistent with the Python accessor). Deeply nested
|
|
575
|
+
* AND/AND is not traversed.
|
|
576
|
+
*/
|
|
577
|
+
private collectColumnFilters(columnName: string): Filter[] {
|
|
578
|
+
const result: Filter[] = [];
|
|
579
|
+
for (const f of this.filters) {
|
|
580
|
+
if (f.columnName !== columnName) continue;
|
|
581
|
+
if (f.type === "and") {
|
|
582
|
+
for (const c of f.children) {
|
|
583
|
+
if (c.columnName === columnName) result.push(c);
|
|
584
|
+
}
|
|
585
|
+
} else {
|
|
586
|
+
result.push(f);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return result;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Copyright 2025, 2026 Query Farm LLC - https://query.farm
|
|
2
|
+
// Filter pushdown — types, deserialization from the wire format, evaluation
|
|
3
|
+
// against Arrow batches, and an OutputCollector wrapper that applies filters
|
|
4
|
+
// transparently. Ported from vgi-python/vgi/table_filter_pushdown.py.
|
|
5
|
+
|
|
6
|
+
export {
|
|
7
|
+
ComparisonOp,
|
|
8
|
+
type AndFilter,
|
|
9
|
+
type ConstantFilter,
|
|
10
|
+
type ExpressionFilter,
|
|
11
|
+
type ExprNode,
|
|
12
|
+
type Filter,
|
|
13
|
+
type InFilter,
|
|
14
|
+
type IsNotNullFilter,
|
|
15
|
+
type IsNullFilter,
|
|
16
|
+
type OrFilter,
|
|
17
|
+
type StructFilter,
|
|
18
|
+
} from "./types.js";
|
|
19
|
+
|
|
20
|
+
export { PushdownFilters } from "./evaluate.js";
|
|
21
|
+
|
|
22
|
+
export { buildJoinKeysLookup, deserializeFilters } from "./deserialize.js";
|
|
23
|
+
|
|
24
|
+
export {
|
|
25
|
+
FilteringOutputCollector,
|
|
26
|
+
formatPushedFilters,
|
|
27
|
+
reprPushedFilters,
|
|
28
|
+
} from "./collector.js";
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Copyright 2025, 2026 Query Farm LLC - https://query.farm
|
|
2
|
+
// Filter pushdown type definitions.
|
|
3
|
+
|
|
4
|
+
export enum ComparisonOp {
|
|
5
|
+
EQ = "eq",
|
|
6
|
+
NE = "ne",
|
|
7
|
+
GT = "gt",
|
|
8
|
+
GE = "ge",
|
|
9
|
+
LT = "lt",
|
|
10
|
+
LE = "le",
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ConstantFilter {
|
|
14
|
+
type: "constant";
|
|
15
|
+
columnName: string;
|
|
16
|
+
columnIndex: number;
|
|
17
|
+
op: ComparisonOp;
|
|
18
|
+
value: any;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface IsNullFilter {
|
|
22
|
+
type: "is_null";
|
|
23
|
+
columnName: string;
|
|
24
|
+
columnIndex: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface IsNotNullFilter {
|
|
28
|
+
type: "is_not_null";
|
|
29
|
+
columnName: string;
|
|
30
|
+
columnIndex: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface InFilter {
|
|
34
|
+
type: "in";
|
|
35
|
+
columnName: string;
|
|
36
|
+
columnIndex: number;
|
|
37
|
+
values: Set<any>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface AndFilter {
|
|
41
|
+
type: "and";
|
|
42
|
+
columnName: string;
|
|
43
|
+
columnIndex: number;
|
|
44
|
+
children: Filter[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface OrFilter {
|
|
48
|
+
type: "or";
|
|
49
|
+
columnName: string;
|
|
50
|
+
columnIndex: number;
|
|
51
|
+
children: Filter[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface StructFilter {
|
|
55
|
+
type: "struct";
|
|
56
|
+
columnName: string;
|
|
57
|
+
columnIndex: number;
|
|
58
|
+
childIndex: number;
|
|
59
|
+
childName: string;
|
|
60
|
+
childFilter: Filter;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type Filter =
|
|
64
|
+
| ConstantFilter
|
|
65
|
+
| IsNullFilter
|
|
66
|
+
| IsNotNullFilter
|
|
67
|
+
| InFilter
|
|
68
|
+
| AndFilter
|
|
69
|
+
| OrFilter
|
|
70
|
+
| StructFilter
|
|
71
|
+
| ExpressionFilter;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Expression-tree filter. The expr is a parsed AST; evaluation matches
|
|
75
|
+
* DuckDB's semantics for the function names the table function advertises in
|
|
76
|
+
* `supportedExpressionFilters` (e.g. list_contains, starts_with, contains,
|
|
77
|
+
* &&/st_intersects_extent for spatial).
|
|
78
|
+
*/
|
|
79
|
+
export interface ExpressionFilter {
|
|
80
|
+
type: "expression";
|
|
81
|
+
columnName: string;
|
|
82
|
+
columnIndex: number;
|
|
83
|
+
expr: ExprNode;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export type ExprNode =
|
|
87
|
+
| { expr_type: "column_ref"; index: number }
|
|
88
|
+
| { expr_type: "constant"; value: any }
|
|
89
|
+
| { expr_type: "function"; function_name: string; children: ExprNode[] }
|
|
90
|
+
| { expr_type: "comparison"; op: ComparisonOp; left: ExprNode; right: ExprNode }
|
|
91
|
+
| { expr_type: "conjunction"; conjunction_type: "and" | "or"; children: ExprNode[] };
|