@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.
Files changed (206) hide show
  1. package/LICENSE +134 -0
  2. package/README.md +494 -0
  3. package/dist/__dbg.test-d.d.ts +1 -0
  4. package/dist/arguments/argument-spec.d.ts +51 -0
  5. package/dist/arguments/arguments.d.ts +13 -0
  6. package/dist/arrow/codec/branded.d.ts +83 -0
  7. package/dist/arrow/codec/registry.d.ts +29 -0
  8. package/dist/arrow/codec/repr.d.ts +23 -0
  9. package/dist/arrow/codec/type-descriptors.d.ts +109 -0
  10. package/dist/arrow/impl-arrowjs/build.d.ts +19 -0
  11. package/dist/arrow/impl-arrowjs/canonical.d.ts +15 -0
  12. package/dist/arrow/impl-arrowjs/empty.d.ts +7 -0
  13. package/dist/arrow/impl-arrowjs/filter.d.ts +11 -0
  14. package/dist/arrow/impl-arrowjs/index.d.ts +11 -0
  15. package/dist/arrow/impl-arrowjs/ipc.d.ts +24 -0
  16. package/dist/arrow/impl-arrowjs/iterate.d.ts +39 -0
  17. package/dist/arrow/impl-arrowjs/project.d.ts +10 -0
  18. package/dist/arrow/impl-arrowjs/schema.d.ts +62 -0
  19. package/dist/arrow/impl-arrowjs/statistics.d.ts +13 -0
  20. package/dist/arrow/impl-flechette/build.d.ts +14 -0
  21. package/dist/arrow/impl-flechette/canonical.d.ts +18 -0
  22. package/dist/arrow/impl-flechette/empty.d.ts +9 -0
  23. package/dist/arrow/impl-flechette/filter.d.ts +10 -0
  24. package/dist/arrow/impl-flechette/index.d.ts +11 -0
  25. package/dist/arrow/impl-flechette/ipc.d.ts +33 -0
  26. package/dist/arrow/impl-flechette/iterate.d.ts +29 -0
  27. package/dist/arrow/impl-flechette/message-meta.d.ts +10 -0
  28. package/dist/arrow/impl-flechette/normalize-type.d.ts +11 -0
  29. package/dist/arrow/impl-flechette/project.d.ts +3 -0
  30. package/dist/arrow/impl-flechette/schema.d.ts +47 -0
  31. package/dist/arrow/impl-flechette/statistics.d.ts +13 -0
  32. package/dist/arrow/index.d.ts +11 -0
  33. package/dist/arrow/predicates.d.ts +47 -0
  34. package/dist/arrow/repr.test-d.d.ts +52 -0
  35. package/dist/arrow/schema-types.d.ts +71 -0
  36. package/dist/arrow/types.d.ts +57 -0
  37. package/dist/catalog/attach-option.d.ts +33 -0
  38. package/dist/catalog/attach-options.d.ts +30 -0
  39. package/dist/catalog/composite.d.ts +33 -0
  40. package/dist/catalog/descriptors.d.ts +154 -0
  41. package/dist/catalog/interface.d.ts +239 -0
  42. package/dist/catalog/read-only.d.ts +33 -0
  43. package/dist/client/client.d.ts +189 -0
  44. package/dist/client/errors.d.ts +26 -0
  45. package/dist/client/helpers.d.ts +12 -0
  46. package/dist/client/protocol.d.ts +16 -0
  47. package/dist/client/types.d.ts +125 -0
  48. package/dist/client-entry.d.ts +11 -0
  49. package/dist/client-entry.js +48 -0
  50. package/dist/client-entry.js.map +9 -0
  51. package/dist/codec/asd.d.ts +25 -0
  52. package/dist/crypto.d.ts +36 -0
  53. package/dist/errors.d.ts +24 -0
  54. package/dist/filter-pushdown/collector.d.ts +32 -0
  55. package/dist/filter-pushdown/deserialize.d.ts +16 -0
  56. package/dist/filter-pushdown/evaluate.d.ts +51 -0
  57. package/dist/filter-pushdown/index.d.ts +4 -0
  58. package/dist/filter-pushdown/types.d.ts +84 -0
  59. package/dist/functions/aggregate.d.ts +129 -0
  60. package/dist/functions/copy-from.d.ts +81 -0
  61. package/dist/functions/copy-to.d.ts +121 -0
  62. package/dist/functions/registry.d.ts +16 -0
  63. package/dist/functions/scalar.d.ts +98 -0
  64. package/dist/functions/storage-cf-do.d.ts +47 -0
  65. package/dist/functions/storage.d.ts +86 -0
  66. package/dist/functions/table-buffering.d.ts +99 -0
  67. package/dist/functions/table-in-out.d.ts +64 -0
  68. package/dist/functions/table.d.ts +118 -0
  69. package/dist/functions/types.d.ts +157 -0
  70. package/dist/generated/vgi-client.d.ts +466 -0
  71. package/dist/generated/vgi-protocol-schemas.d.ts +115 -0
  72. package/dist/index.core.d.ts +42 -0
  73. package/dist/index.d.ts +2 -0
  74. package/dist/index.js +10888 -0
  75. package/dist/index.js.map +85 -0
  76. package/dist/metadata/resolve.d.ts +3 -0
  77. package/dist/metadata/serialize.d.ts +12 -0
  78. package/dist/metadata/types.d.ts +47 -0
  79. package/dist/protocol/dispatch.d.ts +23 -0
  80. package/dist/protocol/handlers/aggregate.d.ts +3 -0
  81. package/dist/protocol/handlers/catalog/admin.d.ts +3 -0
  82. package/dist/protocol/handlers/catalog/index.d.ts +3 -0
  83. package/dist/protocol/handlers/catalog/index_methods.d.ts +3 -0
  84. package/dist/protocol/handlers/catalog/macro.d.ts +3 -0
  85. package/dist/protocol/handlers/catalog/shared.d.ts +70 -0
  86. package/dist/protocol/handlers/catalog/table.d.ts +3 -0
  87. package/dist/protocol/handlers/catalog/view.d.ts +3 -0
  88. package/dist/protocol/handlers/function.d.ts +8 -0
  89. package/dist/protocol/handlers/shared.d.ts +31 -0
  90. package/dist/protocol/handlers/table-buffering.d.ts +3 -0
  91. package/dist/protocol/serialize.d.ts +4 -0
  92. package/dist/protocol/serializers/arguments.d.ts +3 -0
  93. package/dist/protocol/serializers/bind.d.ts +9 -0
  94. package/dist/protocol/serializers/cardinality.d.ts +7 -0
  95. package/dist/protocol/serializers/init.d.ts +9 -0
  96. package/dist/protocol/serializers/shared.d.ts +12 -0
  97. package/dist/protocol/state-serializer.d.ts +23 -0
  98. package/dist/protocol/types.d.ts +129 -0
  99. package/dist/secrets/helpers.d.ts +15 -0
  100. package/dist/types.d.ts +52 -0
  101. package/dist/util/arrow/index.d.ts +1 -0
  102. package/dist/util/bytes.d.ts +2 -0
  103. package/dist/util/statistics.d.ts +7 -0
  104. package/dist/worker-cf-entry.d.ts +30 -0
  105. package/dist/worker-cf-entry.js +8767 -0
  106. package/dist/worker-cf-entry.js.map +74 -0
  107. package/dist/worker.d.ts +36 -0
  108. package/package.json +96 -0
  109. package/src/arguments/argument-spec.ts +200 -0
  110. package/src/arguments/arguments.ts +81 -0
  111. package/src/arrow/codec/branded.ts +206 -0
  112. package/src/arrow/codec/registry.ts +674 -0
  113. package/src/arrow/codec/repr.ts +181 -0
  114. package/src/arrow/codec/type-descriptors.ts +137 -0
  115. package/src/arrow/impl-arrowjs/build.ts +88 -0
  116. package/src/arrow/impl-arrowjs/canonical.ts +509 -0
  117. package/src/arrow/impl-arrowjs/empty.ts +72 -0
  118. package/src/arrow/impl-arrowjs/filter.ts +53 -0
  119. package/src/arrow/impl-arrowjs/index.ts +54 -0
  120. package/src/arrow/impl-arrowjs/ipc.ts +67 -0
  121. package/src/arrow/impl-arrowjs/iterate.ts +159 -0
  122. package/src/arrow/impl-arrowjs/project.ts +73 -0
  123. package/src/arrow/impl-arrowjs/schema.ts +256 -0
  124. package/src/arrow/impl-arrowjs/statistics.ts +185 -0
  125. package/src/arrow/impl-flechette/build.ts +65 -0
  126. package/src/arrow/impl-flechette/canonical.ts +289 -0
  127. package/src/arrow/impl-flechette/empty.ts +20 -0
  128. package/src/arrow/impl-flechette/filter.ts +42 -0
  129. package/src/arrow/impl-flechette/index.ts +54 -0
  130. package/src/arrow/impl-flechette/ipc.ts +110 -0
  131. package/src/arrow/impl-flechette/iterate.ts +121 -0
  132. package/src/arrow/impl-flechette/message-meta.ts +175 -0
  133. package/src/arrow/impl-flechette/normalize-type.ts +99 -0
  134. package/src/arrow/impl-flechette/project.ts +64 -0
  135. package/src/arrow/impl-flechette/schema.ts +202 -0
  136. package/src/arrow/impl-flechette/statistics.ts +198 -0
  137. package/src/arrow/index.ts +129 -0
  138. package/src/arrow/predicates.ts +66 -0
  139. package/src/arrow/repr.test-d.ts +189 -0
  140. package/src/arrow/schema-types.ts +213 -0
  141. package/src/arrow/types.ts +74 -0
  142. package/src/catalog/attach-option.ts +107 -0
  143. package/src/catalog/attach-options.ts +79 -0
  144. package/src/catalog/composite.ts +180 -0
  145. package/src/catalog/descriptors.ts +164 -0
  146. package/src/catalog/interface.ts +721 -0
  147. package/src/catalog/read-only.ts +909 -0
  148. package/src/client/client.ts +1348 -0
  149. package/src/client/errors.ts +69 -0
  150. package/src/client/helpers.ts +56 -0
  151. package/src/client/protocol.ts +33 -0
  152. package/src/client/types.ts +140 -0
  153. package/src/client-entry.ts +112 -0
  154. package/src/codec/asd.ts +147 -0
  155. package/src/crypto.ts +143 -0
  156. package/src/errors.ts +61 -0
  157. package/src/filter-pushdown/collector.ts +147 -0
  158. package/src/filter-pushdown/deserialize.ts +253 -0
  159. package/src/filter-pushdown/evaluate.ts +591 -0
  160. package/src/filter-pushdown/index.ts +28 -0
  161. package/src/filter-pushdown/types.ts +91 -0
  162. package/src/functions/aggregate.ts +384 -0
  163. package/src/functions/copy-from.ts +282 -0
  164. package/src/functions/copy-to.ts +319 -0
  165. package/src/functions/registry.ts +258 -0
  166. package/src/functions/scalar.ts +356 -0
  167. package/src/functions/storage-cf-do.ts +303 -0
  168. package/src/functions/storage.ts +521 -0
  169. package/src/functions/table-buffering.ts +562 -0
  170. package/src/functions/table-in-out.ts +373 -0
  171. package/src/functions/table.ts +521 -0
  172. package/src/functions/types.ts +192 -0
  173. package/src/generated/vgi-client.ts +560 -0
  174. package/src/generated/vgi-protocol-schemas.ts +864 -0
  175. package/src/index.core.ts +267 -0
  176. package/src/index.ts +11 -0
  177. package/src/metadata/resolve.ts +109 -0
  178. package/src/metadata/serialize.ts +242 -0
  179. package/src/metadata/types.ts +60 -0
  180. package/src/protocol/dispatch.ts +52 -0
  181. package/src/protocol/handlers/aggregate.ts +355 -0
  182. package/src/protocol/handlers/catalog/admin.ts +368 -0
  183. package/src/protocol/handlers/catalog/index.ts +25 -0
  184. package/src/protocol/handlers/catalog/index_methods.ts +65 -0
  185. package/src/protocol/handlers/catalog/macro.ts +114 -0
  186. package/src/protocol/handlers/catalog/shared.ts +291 -0
  187. package/src/protocol/handlers/catalog/table.ts +401 -0
  188. package/src/protocol/handlers/catalog/view.ts +119 -0
  189. package/src/protocol/handlers/function.ts +302 -0
  190. package/src/protocol/handlers/shared.ts +80 -0
  191. package/src/protocol/handlers/table-buffering.ts +207 -0
  192. package/src/protocol/serialize.ts +23 -0
  193. package/src/protocol/serializers/arguments.ts +160 -0
  194. package/src/protocol/serializers/bind.ts +212 -0
  195. package/src/protocol/serializers/cardinality.ts +45 -0
  196. package/src/protocol/serializers/init.ts +247 -0
  197. package/src/protocol/serializers/shared.ts +46 -0
  198. package/src/protocol/state-serializer.ts +181 -0
  199. package/src/protocol/types.ts +153 -0
  200. package/src/secrets/helpers.ts +70 -0
  201. package/src/types.ts +71 -0
  202. package/src/util/arrow/index.ts +23 -0
  203. package/src/util/bytes.ts +14 -0
  204. package/src/util/statistics.ts +40 -0
  205. package/src/worker-cf-entry.ts +172 -0
  206. 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[] };