@query-farm/vgi 0.29.0 → 0.30.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/README.md +26 -1
- package/dist/arguments/argument-spec.d.ts +4 -2
- package/dist/catalog/composite.d.ts +13 -13
- package/dist/catalog/descriptors.d.ts +7 -3
- package/dist/catalog/interface.d.ts +67 -40
- package/dist/catalog/read-only.d.ts +32 -15
- package/dist/client/client.d.ts +36 -29
- package/dist/client/types.d.ts +2 -0
- package/dist/client-entry.d.ts +1 -1
- package/dist/client-entry.js +442 -283
- package/dist/client-entry.js.map +12 -11
- package/dist/filter-pushdown/capabilities.d.ts +3 -0
- package/dist/filter-pushdown/deserialize.d.ts +14 -15
- package/dist/filter-pushdown/evaluate.d.ts +14 -44
- package/dist/filter-pushdown/index.d.ts +2 -2
- package/dist/filter-pushdown/types.d.ts +120 -68
- package/dist/functions/aggregate.d.ts +4 -0
- package/dist/functions/registry.d.ts +5 -5
- package/dist/functions/scalar.d.ts +6 -1
- package/dist/functions/table-buffering.d.ts +9 -0
- package/dist/functions/table-in-out.d.ts +13 -0
- package/dist/functions/table.d.ts +9 -2
- package/dist/functions/types.d.ts +22 -2
- package/dist/generated/vgi-client.d.ts +57 -31
- package/dist/generated/vgi-protocol-schemas.d.ts +15 -0
- package/dist/http/fetch.d.ts +7 -1
- package/dist/index.core.d.ts +2 -1
- package/dist/index.js +2320 -1363
- package/dist/index.js.map +44 -41
- package/dist/metadata/types.d.ts +15 -2
- package/dist/protocol/handlers/shared.d.ts +3 -1
- package/dist/protocol/serializers/splits.d.ts +1 -1
- package/dist/protocol/types.d.ts +9 -3
- package/dist/schema-path.d.ts +7 -0
- package/dist/serve-entry.d.ts +22 -1
- package/dist/serve-entry.js +414 -243
- package/dist/serve-entry.js.map +28 -27
- package/dist/split-token.d.ts +1 -1
- package/dist/types.d.ts +8 -0
- package/dist/worker-cf-entry.js +2033 -1210
- package/dist/worker-cf-entry.js.map +41 -37
- package/dist/worker.d.ts +1 -1
- package/package.json +3 -3
- package/src/arguments/argument-spec.ts +12 -4
- package/src/catalog/composite.ts +26 -26
- package/src/catalog/descriptors.ts +12 -3
- package/src/catalog/interface.ts +98 -48
- package/src/catalog/read-only.ts +125 -71
- package/src/client/client.ts +92 -72
- package/src/client/types.ts +2 -0
- package/src/client-entry.ts +2 -1
- package/src/filter-pushdown/capabilities.ts +13 -0
- package/src/filter-pushdown/collector.ts +22 -35
- package/src/filter-pushdown/deserialize.ts +657 -221
- package/src/filter-pushdown/evaluate.ts +410 -499
- package/src/filter-pushdown/index.ts +16 -2
- package/src/filter-pushdown/types.ts +88 -71
- package/src/functions/aggregate.ts +9 -1
- package/src/functions/registry.ts +24 -22
- package/src/functions/scalar.ts +26 -0
- package/src/functions/table-buffering.ts +46 -7
- package/src/functions/table-in-out.ts +187 -30
- package/src/functions/table.ts +58 -20
- package/src/functions/types.ts +24 -1
- package/src/generated/vgi-client.ts +62 -33
- package/src/generated/vgi-protocol-schemas.ts +226 -58
- package/src/http/client-bundle.ts +1 -1
- package/src/http/fetch.ts +12 -0
- package/src/index.core.ts +2 -0
- package/src/metadata/resolve.ts +43 -1
- package/src/metadata/serialize.ts +43 -5
- package/src/metadata/types.ts +17 -1
- package/src/protocol/dispatch.ts +1 -1
- package/src/protocol/handlers/aggregate.ts +13 -7
- package/src/protocol/handlers/catalog/admin.ts +6 -6
- package/src/protocol/handlers/catalog/index_methods.ts +2 -2
- package/src/protocol/handlers/catalog/macro.ts +4 -4
- package/src/protocol/handlers/catalog/shared.ts +2 -0
- package/src/protocol/handlers/catalog/table.ts +16 -16
- package/src/protocol/handlers/catalog/view.ts +5 -5
- package/src/protocol/handlers/function.ts +2 -2
- package/src/protocol/handlers/shared.ts +12 -3
- package/src/protocol/handlers/table-buffering.ts +4 -4
- package/src/protocol/serializers/bind.ts +15 -6
- package/src/protocol/serializers/splits.ts +4 -2
- package/src/protocol/types.ts +9 -3
- package/src/schema-path.ts +25 -0
- package/src/serve-entry.ts +65 -3
- package/src/split-token.ts +2 -2
- package/src/types.ts +9 -0
- package/src/worker.ts +49 -4
|
@@ -1,591 +1,502 @@
|
|
|
1
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
2
|
|
|
6
3
|
import {
|
|
7
4
|
type VgiBatch,
|
|
8
|
-
type VgiSchema,
|
|
9
5
|
type VgiDataType,
|
|
10
|
-
|
|
6
|
+
isBool,
|
|
7
|
+
isFloat,
|
|
8
|
+
isInt,
|
|
9
|
+
isUtf8,
|
|
11
10
|
readCanonicalValue,
|
|
12
11
|
} from "../arrow/index.js";
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
12
|
+
import { filterBatch } from "../util/arrow/index.js";
|
|
13
|
+
import { applyFilterDelta, type DeserializeFilterOptions, FilterV2Error } from "./deserialize.js";
|
|
14
|
+
import { filterFunctionIdentityKey } from "./capabilities.js";
|
|
15
15
|
import {
|
|
16
16
|
ComparisonOp,
|
|
17
|
-
type
|
|
18
|
-
type
|
|
19
|
-
type
|
|
20
|
-
type
|
|
21
|
-
type Filter,
|
|
22
|
-
type InFilter,
|
|
23
|
-
type IsNotNullFilter,
|
|
24
|
-
type IsNullFilter,
|
|
25
|
-
type OrFilter,
|
|
26
|
-
type StructFilter,
|
|
17
|
+
type EvaluationContext,
|
|
18
|
+
type FilterExpression,
|
|
19
|
+
type FilterPredicate,
|
|
20
|
+
type FunctionIdentity,
|
|
27
21
|
} from "./types.js";
|
|
28
22
|
|
|
29
|
-
|
|
30
|
-
// Comparison helpers
|
|
31
|
-
// ============================================================================
|
|
23
|
+
type SqlBoolean = boolean | null;
|
|
32
24
|
|
|
33
|
-
function
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
}
|
|
25
|
+
function readCell(batch: VgiBatch, index: number, row: number): unknown {
|
|
26
|
+
const column = batch.getChildAt(index);
|
|
27
|
+
return column ? readCanonicalValue(batch.schema.fields[index].type, column, row) : null;
|
|
42
28
|
}
|
|
43
29
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
30
|
+
function deepEqual(left: unknown, right: unknown): boolean {
|
|
31
|
+
if (typeof left === "number" && typeof right === "number") {
|
|
32
|
+
return left === right || (Number.isNaN(left) && Number.isNaN(right));
|
|
33
|
+
}
|
|
34
|
+
if (left instanceof Uint8Array && right instanceof Uint8Array) {
|
|
35
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
36
|
+
}
|
|
37
|
+
if (Array.isArray(left) && Array.isArray(right)) {
|
|
38
|
+
return left.length === right.length && left.every((value, index) => deepEqual(value, right[index]));
|
|
39
|
+
}
|
|
40
|
+
if (left && right && typeof left === "object" && typeof right === "object") {
|
|
41
|
+
const entries = Object.entries(left as Record<string, unknown>);
|
|
42
|
+
const rightObject = right as Record<string, unknown>;
|
|
43
|
+
return entries.length === Object.keys(rightObject).length &&
|
|
44
|
+
entries.every(([key, value]) => key in rightObject && deepEqual(value, rightObject[key]));
|
|
45
|
+
}
|
|
46
|
+
return left === right;
|
|
58
47
|
}
|
|
59
48
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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;
|
|
49
|
+
function orderedCompare(left: unknown, right: unknown): number {
|
|
50
|
+
if (typeof left === "number" && typeof right === "number") {
|
|
51
|
+
const leftNan = Number.isNaN(left);
|
|
52
|
+
const rightNan = Number.isNaN(right);
|
|
53
|
+
if (leftNan || rightNan) return leftNan === rightNan ? 0 : leftNan ? 1 : -1;
|
|
94
54
|
}
|
|
55
|
+
if (deepEqual(left, right)) return 0;
|
|
56
|
+
return (left as any) < (right as any) ? -1 : 1;
|
|
95
57
|
}
|
|
96
58
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
case
|
|
111
|
-
|
|
112
|
-
case
|
|
113
|
-
|
|
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
|
-
}
|
|
59
|
+
function compare(left: unknown, right: unknown, op: ComparisonOp): SqlBoolean {
|
|
60
|
+
if (op === ComparisonOp.DISTINCT_FROM) {
|
|
61
|
+
return left === null || right === null ? left !== right : !deepEqual(left, right);
|
|
62
|
+
}
|
|
63
|
+
if (op === ComparisonOp.NOT_DISTINCT_FROM) {
|
|
64
|
+
return left === null || right === null ? left === right : deepEqual(left, right);
|
|
65
|
+
}
|
|
66
|
+
if (left === null || right === null) return null;
|
|
67
|
+
const order = orderedCompare(left, right);
|
|
68
|
+
switch (op) {
|
|
69
|
+
case ComparisonOp.EQ: return order === 0;
|
|
70
|
+
case ComparisonOp.NE: return order !== 0;
|
|
71
|
+
case ComparisonOp.LT: return order < 0;
|
|
72
|
+
case ComparisonOp.LE: return order <= 0;
|
|
73
|
+
case ComparisonOp.GT: return order > 0;
|
|
74
|
+
case ComparisonOp.GE: return order >= 0;
|
|
75
|
+
default: throw new FilterV2Error(`unknown comparison operator ${op}`);
|
|
141
76
|
}
|
|
142
77
|
}
|
|
143
78
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
79
|
+
function andKleene(values: SqlBoolean[]): SqlBoolean {
|
|
80
|
+
if (values.some((value) => value === false)) return false;
|
|
81
|
+
return values.some((value) => value === null) ? null : true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function orKleene(values: SqlBoolean[]): SqlBoolean {
|
|
85
|
+
if (values.some((value) => value === true)) return true;
|
|
86
|
+
return values.some((value) => value === null) ? null : false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function integerBounds(type: VgiDataType): [bigint, bigint] | null {
|
|
90
|
+
if (!isInt(type)) return null;
|
|
91
|
+
const width = BigInt((type as any).bitWidth ?? 32);
|
|
92
|
+
const signed = (type as any).isSigned ?? (type as any).signed ?? true;
|
|
93
|
+
return signed
|
|
94
|
+
? [-(1n << (width - 1n)), (1n << (width - 1n)) - 1n]
|
|
95
|
+
: [0n, (1n << width) - 1n];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function castValue(value: unknown, type: VgiDataType): unknown {
|
|
99
|
+
if (value === null) return null;
|
|
100
|
+
if (isUtf8(type)) return String(value);
|
|
101
|
+
if (isBool(type)) {
|
|
102
|
+
if (typeof value === "boolean") return value;
|
|
103
|
+
if (value === 0 || value === 0n || value === "false") return false;
|
|
104
|
+
if (value === 1 || value === 1n || value === "true") return true;
|
|
105
|
+
throw new FilterV2Error(`cannot cast ${String(value)} to BOOLEAN`);
|
|
106
|
+
}
|
|
107
|
+
if (isFloat(type)) {
|
|
108
|
+
const result = Number(value);
|
|
109
|
+
if (Number.isNaN(result) && String(value).toLowerCase() !== "nan") {
|
|
110
|
+
throw new FilterV2Error(`cannot cast ${String(value)} to floating point`);
|
|
169
111
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
ba.maxY < bb.minY || ba.minY > bb.maxY);
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
const bounds = integerBounds(type);
|
|
115
|
+
if (bounds) {
|
|
116
|
+
let result: bigint;
|
|
117
|
+
try {
|
|
118
|
+
result = typeof value === "bigint" ? value : BigInt(typeof value === "number" ? Math.trunc(value) : String(value));
|
|
119
|
+
} catch {
|
|
120
|
+
throw new FilterV2Error(`cannot cast ${String(value)} to integer`);
|
|
180
121
|
}
|
|
181
|
-
|
|
182
|
-
|
|
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;
|
|
122
|
+
if (result < bounds[0] || result > bounds[1]) throw new FilterV2Error("integer cast overflow");
|
|
123
|
+
return (type as any).bitWidth === 64 ? result : Number(result);
|
|
187
124
|
}
|
|
125
|
+
throw new FilterV2Error(`no vgi.duckdb.standard.v1 cast evaluator for Arrow type ${type.typeId}`);
|
|
188
126
|
}
|
|
189
127
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const x = view.getFloat64(5, le);
|
|
201
|
-
const y = view.getFloat64(13, le);
|
|
202
|
-
return { minX: x, minY: y, maxX: x, maxY: y };
|
|
128
|
+
function numericBinary(
|
|
129
|
+
op: "add" | "subtract" | "multiply" | "divide" | "modulo",
|
|
130
|
+
left: unknown,
|
|
131
|
+
right: unknown,
|
|
132
|
+
context: EvaluationContext,
|
|
133
|
+
): unknown {
|
|
134
|
+
if (left === null || right === null) return null;
|
|
135
|
+
if ((typeof left !== "number" && typeof left !== "bigint") ||
|
|
136
|
+
(typeof right !== "number" && typeof right !== "bigint")) {
|
|
137
|
+
throw new FilterV2Error("arithmetic operands must be numeric");
|
|
203
138
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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;
|
|
139
|
+
if (typeof left === "bigint" && typeof right === "bigint") {
|
|
140
|
+
if ((op === "divide" || op === "modulo") && right === 0n) throw new FilterV2Error("division by zero");
|
|
141
|
+
switch (op) {
|
|
142
|
+
case "add": return left + right;
|
|
143
|
+
case "subtract": return left - right;
|
|
144
|
+
case "multiply": return left * right;
|
|
145
|
+
case "divide": return context.integerDivision ? left / right : Number(left) / Number(right);
|
|
146
|
+
case "modulo": return left % right;
|
|
219
147
|
}
|
|
220
|
-
if (minX === Infinity) return null;
|
|
221
|
-
return { minX, minY, maxX, maxY };
|
|
222
148
|
}
|
|
223
|
-
|
|
149
|
+
const a = Number(left);
|
|
150
|
+
const b = Number(right);
|
|
151
|
+
if ((op === "divide" || op === "modulo") && b === 0 && context.ieeeFloatingPointOps === false) {
|
|
152
|
+
throw new FilterV2Error("division by zero");
|
|
153
|
+
}
|
|
154
|
+
switch (op) {
|
|
155
|
+
case "add": return a + b;
|
|
156
|
+
case "subtract": return a - b;
|
|
157
|
+
case "multiply": return a * b;
|
|
158
|
+
case "divide": return a / b;
|
|
159
|
+
case "modulo": return a % b;
|
|
160
|
+
}
|
|
224
161
|
}
|
|
225
162
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
}
|
|
163
|
+
interface Bounds {
|
|
164
|
+
minX: number;
|
|
165
|
+
minY: number;
|
|
166
|
+
maxX: number;
|
|
167
|
+
maxY: number;
|
|
248
168
|
}
|
|
249
169
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
}
|
|
170
|
+
class WkbBoundsReader {
|
|
171
|
+
private readonly view: DataView;
|
|
172
|
+
private offset = 0;
|
|
173
|
+
|
|
174
|
+
constructor(bytes: Uint8Array) {
|
|
175
|
+
this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
264
176
|
}
|
|
265
|
-
}
|
|
266
177
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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;
|
|
178
|
+
read(): Bounds | null {
|
|
179
|
+
const bounds = this.geometry();
|
|
180
|
+
if (this.offset !== this.view.byteLength) throw new FilterV2Error("WKB has trailing bytes");
|
|
181
|
+
return bounds;
|
|
276
182
|
}
|
|
277
|
-
}
|
|
278
183
|
|
|
279
|
-
|
|
280
|
-
|
|
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;
|
|
184
|
+
private require(length: number): void {
|
|
185
|
+
if (length < 0 || this.offset + length > this.view.byteLength) throw new FilterV2Error("truncated WKB geometry");
|
|
288
186
|
}
|
|
289
|
-
}
|
|
290
187
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
}
|
|
188
|
+
private byte(): number {
|
|
189
|
+
this.require(1);
|
|
190
|
+
return this.view.getUint8(this.offset++);
|
|
305
191
|
}
|
|
306
|
-
}
|
|
307
192
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
193
|
+
private uint32(littleEndian: boolean): number {
|
|
194
|
+
this.require(4);
|
|
195
|
+
const value = this.view.getUint32(this.offset, littleEndian);
|
|
196
|
+
this.offset += 4;
|
|
197
|
+
return value;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private float64(littleEndian: boolean): number {
|
|
201
|
+
this.require(8);
|
|
202
|
+
const value = this.view.getFloat64(this.offset, littleEndian);
|
|
203
|
+
this.offset += 8;
|
|
204
|
+
return value;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private point(littleEndian: boolean, dimensions: number): Bounds | null {
|
|
208
|
+
const x = this.float64(littleEndian);
|
|
209
|
+
const y = this.float64(littleEndian);
|
|
210
|
+
for (let dimension = 2; dimension < dimensions; dimension++) this.float64(littleEndian);
|
|
211
|
+
if (Number.isNaN(x) && Number.isNaN(y)) return null;
|
|
212
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) throw new FilterV2Error("WKB coordinate must be finite");
|
|
213
|
+
return { minX: x, minY: y, maxX: x, maxY: y };
|
|
323
214
|
}
|
|
324
|
-
}
|
|
325
215
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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];
|
|
216
|
+
private count(littleEndian: boolean): number {
|
|
217
|
+
const count = this.uint32(littleEndian);
|
|
218
|
+
if (count > Math.floor((this.view.byteLength - this.offset) / 4)) {
|
|
219
|
+
throw new FilterV2Error("WKB element count exceeds payload size");
|
|
341
220
|
}
|
|
221
|
+
return count;
|
|
342
222
|
}
|
|
343
223
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
224
|
+
private merge(left: Bounds | null, right: Bounds | null): Bounds | null {
|
|
225
|
+
if (!left) return right;
|
|
226
|
+
if (!right) return left;
|
|
227
|
+
return {
|
|
228
|
+
minX: Math.min(left.minX, right.minX),
|
|
229
|
+
minY: Math.min(left.minY, right.minY),
|
|
230
|
+
maxX: Math.max(left.maxX, right.maxX),
|
|
231
|
+
maxY: Math.max(left.maxY, right.maxY),
|
|
232
|
+
};
|
|
347
233
|
}
|
|
348
|
-
}
|
|
349
234
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
235
|
+
private points(littleEndian: boolean, dimensions: number): Bounds | null {
|
|
236
|
+
let bounds: Bounds | null = null;
|
|
237
|
+
const count = this.count(littleEndian);
|
|
238
|
+
for (let index = 0; index < count; index++) bounds = this.merge(bounds, this.point(littleEndian, dimensions));
|
|
239
|
+
return bounds;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private geometry(): Bounds | null {
|
|
243
|
+
const byteOrder = this.byte();
|
|
244
|
+
if (byteOrder !== 0 && byteOrder !== 1) throw new FilterV2Error("invalid WKB byte order");
|
|
245
|
+
const littleEndian = byteOrder === 1;
|
|
246
|
+
const encodedType = this.uint32(littleEndian);
|
|
247
|
+
const hasZ = (encodedType & 0x80000000) !== 0;
|
|
248
|
+
const hasM = (encodedType & 0x40000000) !== 0;
|
|
249
|
+
const hasSrid = (encodedType & 0x20000000) !== 0;
|
|
250
|
+
let type = encodedType & 0x0fffffff;
|
|
251
|
+
let dimensions = 2 + Number(hasZ) + Number(hasM);
|
|
252
|
+
if (type >= 3000) {
|
|
253
|
+
type -= 3000;
|
|
254
|
+
dimensions = 4;
|
|
255
|
+
} else if (type >= 2000) {
|
|
256
|
+
type -= 2000;
|
|
257
|
+
dimensions = 3;
|
|
258
|
+
} else if (type >= 1000) {
|
|
259
|
+
type -= 1000;
|
|
260
|
+
dimensions = 3;
|
|
261
|
+
}
|
|
262
|
+
if (hasSrid) this.uint32(littleEndian);
|
|
263
|
+
|
|
264
|
+
if (type === 1) return this.point(littleEndian, dimensions);
|
|
265
|
+
if (type === 2) return this.points(littleEndian, dimensions);
|
|
266
|
+
if (type === 3) {
|
|
267
|
+
let bounds: Bounds | null = null;
|
|
268
|
+
const rings = this.count(littleEndian);
|
|
269
|
+
for (let ring = 0; ring < rings; ring++) bounds = this.merge(bounds, this.points(littleEndian, dimensions));
|
|
270
|
+
return bounds;
|
|
271
|
+
}
|
|
272
|
+
if (type >= 4 && type <= 7) {
|
|
273
|
+
let bounds: Bounds | null = null;
|
|
274
|
+
const geometries = this.count(littleEndian);
|
|
275
|
+
for (let index = 0; index < geometries; index++) bounds = this.merge(bounds, this.geometry());
|
|
276
|
+
return bounds;
|
|
277
|
+
}
|
|
278
|
+
throw new FilterV2Error(`unsupported WKB geometry type ${type}`);
|
|
279
|
+
}
|
|
388
280
|
}
|
|
389
281
|
|
|
390
|
-
|
|
391
|
-
|
|
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);
|
|
282
|
+
function wkbBBox(bytes: unknown): Bounds | null {
|
|
283
|
+
if (!(bytes instanceof Uint8Array)) throw new FilterV2Error("spatial extent arguments must be WKB binary values");
|
|
284
|
+
return new WkbBoundsReader(bytes).read();
|
|
407
285
|
}
|
|
408
286
|
|
|
409
|
-
function
|
|
410
|
-
|
|
287
|
+
function evaluateCall(expression: Extract<FilterExpression, { node: "call" }>, args: unknown[]): unknown {
|
|
288
|
+
if (args.some((value) => value === null)) return null;
|
|
289
|
+
if (typeof expression.function === "string") {
|
|
290
|
+
const [left, right] = args;
|
|
291
|
+
switch (expression.function) {
|
|
292
|
+
case "starts_with": return String(left).startsWith(String(right));
|
|
293
|
+
case "ends_with": return String(left).endsWith(String(right));
|
|
294
|
+
case "contains": return String(left).includes(String(right));
|
|
295
|
+
case "list_contains": return Array.isArray(left) && left.some((value) => value !== null && deepEqual(value, right));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (filterFunctionIdentityKey(expression.function as FunctionIdentity) === "duckdb.spatial/intersects_extent@1") {
|
|
299
|
+
const left = wkbBBox(args[0]);
|
|
300
|
+
const right = wkbBBox(args[1]);
|
|
301
|
+
return !!left && !!right && !(left.maxX < right.minX || left.minX > right.maxX ||
|
|
302
|
+
left.maxY < right.minY || left.minY > right.maxY);
|
|
303
|
+
}
|
|
304
|
+
throw new FilterV2Error("no evaluator for extension filter function");
|
|
305
|
+
}
|
|
411
306
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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)`;
|
|
307
|
+
function evaluateExpression(expression: FilterExpression, batch: VgiBatch, row: number, context: EvaluationContext): unknown {
|
|
308
|
+
switch (expression.node) {
|
|
309
|
+
case "column_ref": {
|
|
310
|
+
const projectedIndex = batch.schema.fields.findIndex((field) => field.name === expression.columnName);
|
|
311
|
+
if (projectedIndex < 0) {
|
|
312
|
+
throw new FilterV2Error(`filter column ${expression.columnName} is unavailable in emitted batch`);
|
|
425
313
|
}
|
|
426
|
-
|
|
427
|
-
|
|
314
|
+
return readCell(batch, projectedIndex, row);
|
|
315
|
+
}
|
|
316
|
+
case "field_ref": {
|
|
317
|
+
const parent = evaluateExpression(expression.expression, batch, row, context);
|
|
318
|
+
if (parent === null) return null;
|
|
319
|
+
if (Array.isArray(parent)) return parent[expression.fieldIndex] ?? null;
|
|
320
|
+
return (parent as Record<string, unknown>)[expression.fieldName] ?? null;
|
|
321
|
+
}
|
|
322
|
+
case "literal": return expression.value;
|
|
323
|
+
case "comparison": return compare(evaluateExpression(expression.left, batch, row, context),
|
|
324
|
+
evaluateExpression(expression.right, batch, row, context), expression.op);
|
|
325
|
+
case "and": return andKleene(expression.children.map((child) => evaluateExpression(child, batch, row, context) as SqlBoolean));
|
|
326
|
+
case "or": return orKleene(expression.children.map((child) => evaluateExpression(child, batch, row, context) as SqlBoolean));
|
|
327
|
+
case "not": {
|
|
328
|
+
const value = evaluateExpression(expression.expression, batch, row, context) as SqlBoolean;
|
|
329
|
+
return value === null ? null : !value;
|
|
428
330
|
}
|
|
429
|
-
case "
|
|
430
|
-
const
|
|
431
|
-
return
|
|
331
|
+
case "is_null": {
|
|
332
|
+
const result = evaluateExpression(expression.expression, batch, row, context) === null;
|
|
333
|
+
return expression.negated ? !result : result;
|
|
432
334
|
}
|
|
433
|
-
case "
|
|
434
|
-
const
|
|
435
|
-
|
|
335
|
+
case "in": {
|
|
336
|
+
const input = evaluateExpression(expression.expression, batch, row, context);
|
|
337
|
+
if (input === null) return null;
|
|
338
|
+
const matched = expression.set.values.some((value) => value !== null && deepEqual(input, value));
|
|
339
|
+
let result: SqlBoolean = matched ? true : expression.set.values.some((value) => value === null) ? null : false;
|
|
340
|
+
if (expression.negated && result !== null) result = !result;
|
|
341
|
+
return result;
|
|
436
342
|
}
|
|
437
|
-
case "
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
343
|
+
case "cast": return castValue(evaluateExpression(expression.expression, batch, row, context), expression.field.type);
|
|
344
|
+
case "arithmetic": return numericBinary(expression.op, evaluateExpression(expression.left, batch, row, context),
|
|
345
|
+
evaluateExpression(expression.right, batch, row, context), context);
|
|
346
|
+
case "negate": {
|
|
347
|
+
const value = evaluateExpression(expression.expression, batch, row, context);
|
|
348
|
+
return value === null ? null : typeof value === "bigint" ? -value : -Number(value);
|
|
441
349
|
}
|
|
442
|
-
case "
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
350
|
+
case "call": return evaluateCall(expression, expression.arguments.map((argument) => evaluateExpression(argument, batch, row, context)));
|
|
351
|
+
case "runtime_filter": throw new FilterV2Error("runtime-filter artifact evaluator is unavailable");
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function expressionColumns(expression: FilterExpression, output = new Map<number, string>()): Map<number, string> {
|
|
356
|
+
switch (expression.node) {
|
|
357
|
+
case "column_ref": output.set(expression.columnIndex, expression.columnName); break;
|
|
358
|
+
case "field_ref": case "not": case "is_null": case "cast": case "negate":
|
|
359
|
+
expressionColumns(expression.expression, output); break;
|
|
360
|
+
case "comparison": case "arithmetic":
|
|
361
|
+
expressionColumns(expression.left, output); expressionColumns(expression.right, output); break;
|
|
362
|
+
case "and": case "or": expression.children.forEach((child) => expressionColumns(child, output)); break;
|
|
363
|
+
case "in": expressionColumns(expression.expression, output); break;
|
|
364
|
+
case "call": expression.arguments.forEach((argument) => expressionColumns(argument, output)); break;
|
|
365
|
+
case "runtime_filter": expressionColumns(expression.input, output); break;
|
|
447
366
|
}
|
|
367
|
+
return output;
|
|
448
368
|
}
|
|
449
369
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
370
|
+
function discreteValues(expression: FilterExpression, columnName: string): unknown[] | null {
|
|
371
|
+
if (expression.node === "comparison" && expression.op === ComparisonOp.EQ) {
|
|
372
|
+
if (expression.left.node === "column_ref" && expression.left.columnName === columnName && expression.right.node === "literal") return [expression.right.value];
|
|
373
|
+
if (expression.right.node === "column_ref" && expression.right.columnName === columnName && expression.left.node === "literal") return [expression.left.value];
|
|
374
|
+
}
|
|
375
|
+
if (expression.node === "in" && expression.expression.node === "column_ref" &&
|
|
376
|
+
expression.expression.columnName === columnName && !expression.negated) return expression.set.values;
|
|
377
|
+
if (expression.node === "and") {
|
|
378
|
+
for (const child of expression.children) {
|
|
379
|
+
const values = discreteValues(child, columnName);
|
|
380
|
+
if (values) return values;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (expression.node === "or") {
|
|
384
|
+
const branches = expression.children.map((child) => discreteValues(child, columnName));
|
|
385
|
+
if (branches.some((values) => values === null)) return null;
|
|
386
|
+
const result: unknown[] = [];
|
|
387
|
+
for (const values of branches as unknown[][]) {
|
|
388
|
+
for (const value of values) if (!result.some((known) => deepEqual(known, value))) result.push(value);
|
|
389
|
+
}
|
|
390
|
+
return result;
|
|
391
|
+
}
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
453
394
|
|
|
454
395
|
export class PushdownFilters {
|
|
396
|
+
readonly version = "2";
|
|
397
|
+
|
|
455
398
|
constructor(
|
|
456
|
-
readonly
|
|
457
|
-
readonly
|
|
399
|
+
readonly predicates: FilterPredicate[],
|
|
400
|
+
readonly evaluationContext: EvaluationContext,
|
|
401
|
+
readonly options: DeserializeFilterOptions,
|
|
402
|
+
readonly revisions = new Map<string, number>(),
|
|
403
|
+
readonly requiredIds = new Set<string>(),
|
|
458
404
|
) {}
|
|
459
405
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
406
|
+
get filters(): FilterExpression[] {
|
|
407
|
+
return this.predicates.map((predicate) => predicate.expression);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
applyDelta(batch: VgiBatch): PushdownFilters {
|
|
411
|
+
return applyFilterDelta(this, batch);
|
|
464
412
|
}
|
|
465
413
|
|
|
466
|
-
/** Evaluate filters against a batch, returning a Uint8Array mask (0=fail, 1=pass). */
|
|
467
414
|
evaluate(batch: VgiBatch): Uint8Array {
|
|
468
|
-
const
|
|
469
|
-
const mask = new Uint8Array(n);
|
|
415
|
+
const mask = new Uint8Array(batch.numRows);
|
|
470
416
|
mask.fill(1);
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
417
|
+
for (const predicate of this.predicates) {
|
|
418
|
+
if (predicate.expression.node === "runtime_filter" && !predicate.expression.supported) continue;
|
|
419
|
+
let values: unknown[];
|
|
420
|
+
try {
|
|
421
|
+
values = Array.from({ length: batch.numRows }, (_, row) => evaluateExpression(
|
|
422
|
+
predicate.expression, batch, row, this.evaluationContext));
|
|
423
|
+
} catch (error) {
|
|
424
|
+
if (predicate.mode === "advisory") continue;
|
|
425
|
+
throw error;
|
|
426
|
+
}
|
|
427
|
+
for (let row = 0; row < batch.numRows; row++) mask[row] &= values[row] === true ? 1 : 0;
|
|
474
428
|
}
|
|
475
|
-
|
|
476
429
|
return mask;
|
|
477
430
|
}
|
|
478
431
|
|
|
479
|
-
/** Apply filters to a batch, returning only passing rows. */
|
|
480
432
|
apply(batch: VgiBatch): VgiBatch {
|
|
481
|
-
if (batch.numRows
|
|
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);
|
|
433
|
+
if (!batch.numRows || !this.predicates.length) return batch;
|
|
434
|
+
return filterBatch(batch, this.evaluate(batch));
|
|
487
435
|
}
|
|
488
436
|
|
|
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
437
|
filteredColumns(): string[] {
|
|
497
|
-
const
|
|
498
|
-
for (const
|
|
499
|
-
|
|
500
|
-
for (const c of f.children) cols.add(c.columnName);
|
|
501
|
-
} else {
|
|
502
|
-
cols.add(f.columnName);
|
|
503
|
-
}
|
|
438
|
+
const columns = new Set<string>();
|
|
439
|
+
for (const predicate of this.predicates) {
|
|
440
|
+
for (const name of expressionColumns(predicate.expression).values()) columns.add(name);
|
|
504
441
|
}
|
|
505
|
-
return
|
|
442
|
+
return [...columns].sort();
|
|
506
443
|
}
|
|
507
444
|
|
|
508
|
-
/**
|
|
509
|
-
* Whether any pushed-down filter references `columnName`. Mirrors the
|
|
510
|
-
* cross-language `has_filter_for_column` accessor.
|
|
511
|
-
*/
|
|
512
445
|
hasFilterForColumn(columnName: string): boolean {
|
|
513
|
-
return this.
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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
|
-
}
|
|
446
|
+
return this.filteredColumns().includes(columnName);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
getColumnValues(columnName: string): unknown[] | null {
|
|
450
|
+
for (const predicate of this.predicates) {
|
|
451
|
+
const values = discreteValues(predicate.expression, columnName);
|
|
452
|
+
if (values) return values;
|
|
537
453
|
}
|
|
538
454
|
return null;
|
|
539
455
|
}
|
|
540
456
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
457
|
+
toSql(): string {
|
|
458
|
+
return this.predicates.map((predicate) => expressionToSql(predicate.expression)).join(" AND ");
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function quote(value: unknown): string {
|
|
463
|
+
if (value === null) return "NULL";
|
|
464
|
+
if (typeof value === "string") return `'${value.replaceAll("'", "''")}'`;
|
|
465
|
+
if (value instanceof Uint8Array) return `X'${[...value].map((byte) => byte.toString(16).padStart(2, "0")).join("")}'`;
|
|
466
|
+
if (Array.isArray(value)) return `[${value.map(quote).join(", ")}]`;
|
|
467
|
+
return String(value);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
export function expressionToSql(expression: FilterExpression): string {
|
|
471
|
+
switch (expression.node) {
|
|
472
|
+
case "column_ref": return expression.columnName;
|
|
473
|
+
case "field_ref": return `${expressionToSql(expression.expression)}.${expression.fieldName}`;
|
|
474
|
+
case "literal": return quote(expression.value);
|
|
475
|
+
case "comparison": {
|
|
476
|
+
const symbols: Record<ComparisonOp, string> = {
|
|
477
|
+
[ComparisonOp.EQ]: "=", [ComparisonOp.NE]: "!=", [ComparisonOp.LT]: "<", [ComparisonOp.LE]: "<=",
|
|
478
|
+
[ComparisonOp.GT]: ">", [ComparisonOp.GE]: ">=", [ComparisonOp.DISTINCT_FROM]: "IS DISTINCT FROM",
|
|
479
|
+
[ComparisonOp.NOT_DISTINCT_FROM]: "IS NOT DISTINCT FROM",
|
|
480
|
+
};
|
|
481
|
+
return `${expressionToSql(expression.left)} ${symbols[expression.op]} ${expressionToSql(expression.right)}`;
|
|
558
482
|
}
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
}
|
|
483
|
+
case "and": case "or": return `(${expression.children.map(expressionToSql).join(` ${expression.node.toUpperCase()} `)})`;
|
|
484
|
+
case "not": return `(NOT ${expressionToSql(expression.expression)})`;
|
|
485
|
+
case "is_null": return `${expressionToSql(expression.expression)} IS ${expression.negated ? "NOT " : ""}NULL`;
|
|
486
|
+
case "in": return `${expressionToSql(expression.expression)} ${expression.negated ? "NOT " : ""}IN (${expression.set.values.map(quote).join(", ")})`;
|
|
487
|
+
case "cast": return `CAST(${expressionToSql(expression.expression)} AS type_${expression.typeRef})`;
|
|
488
|
+
case "arithmetic": {
|
|
489
|
+
const symbols = { add: "+", subtract: "-", multiply: "*", divide: "/", modulo: "%" } as const;
|
|
490
|
+
return `(${expressionToSql(expression.left)} ${symbols[expression.op]} ${expressionToSql(expression.right)})`;
|
|
568
491
|
}
|
|
569
|
-
return
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
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
|
-
}
|
|
492
|
+
case "negate": return `(-${expressionToSql(expression.expression)})`;
|
|
493
|
+
case "call": {
|
|
494
|
+
const name = typeof expression.function === "string"
|
|
495
|
+
? expression.function
|
|
496
|
+
: filterFunctionIdentityKey(expression.function);
|
|
497
|
+
return `${name}(${expression.arguments.map(expressionToSql).join(", ")})`;
|
|
588
498
|
}
|
|
589
|
-
|
|
499
|
+
case "runtime_filter":
|
|
500
|
+
return `${filterFunctionIdentityKey(expression.algorithm)}(${expressionToSql(expression.input)})`;
|
|
590
501
|
}
|
|
591
502
|
}
|