@query-farm/vgi 0.35.0 → 0.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/filter-pushdown/evaluate.d.ts +6 -0
- package/dist/index.js +21 -4
- package/dist/index.js.map +4 -4
- package/dist/worker-cf-entry.js +21 -4
- package/dist/worker-cf-entry.js.map +4 -4
- package/package.json +1 -1
- package/src/filter-pushdown/deserialize.ts +19 -1
- package/src/filter-pushdown/evaluate.ts +51 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@query-farm/vgi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.36.0",
|
|
4
4
|
"description": "TypeScript framework for extending DuckDB with user-defined functions via the Vector Gateway Interface (VGI) over Arrow IPC.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"author": "Query Farm LLC (https://query.farm)",
|
|
@@ -633,11 +633,29 @@ function validateStandardCall(fn: StandardFilterFunction, args: FilterExpression
|
|
|
633
633
|
if (!matches) throw new FilterV2Error(`${fn} arguments do not bind under vgi.duckdb.standard.v1`);
|
|
634
634
|
}
|
|
635
635
|
|
|
636
|
+
/**
|
|
637
|
+
* Whether `expression` resolves to BOOLEAN.
|
|
638
|
+
*
|
|
639
|
+
* Ask the type, never the node kind. The two agree for every node whose type
|
|
640
|
+
* is structurally fixed — a comparison is BOOLEAN whatever its operands — and
|
|
641
|
+
* that is exactly why enumerating kinds looked sufficient. It is not:
|
|
642
|
+
* `column_ref`, `field_ref` and `cast` are BOOLEAN or not depending on a type
|
|
643
|
+
* resolved from elsewhere (the bind output schema, or a parent expression),
|
|
644
|
+
* and the schema admits all of them as a predicate root — `coreExpression`
|
|
645
|
+
* lists `columnRef` first.
|
|
646
|
+
*
|
|
647
|
+
* Enumerating kinds rejected the lot, so the idiomatic `WHERE flag` /
|
|
648
|
+
* `WHERE NOT flag` — which DuckDB pushes down as a bare column reference
|
|
649
|
+
* rather than rewriting to `flag = true` — was refused as "predicate root
|
|
650
|
+
* must resolve to BOOLEAN", on a node whose type the parser had already
|
|
651
|
+
* resolved to BOOLEAN and stored. See vgi-python 0.36.2.
|
|
652
|
+
*/
|
|
636
653
|
function isBooleanExpression(expression: FilterExpression): boolean {
|
|
637
654
|
if (["comparison", "and", "or", "not", "is_null", "in", "runtime_filter", "call"].includes(expression.node)) {
|
|
638
655
|
return true;
|
|
639
656
|
}
|
|
640
|
-
|
|
657
|
+
const type = expressionType(expression);
|
|
658
|
+
return type !== undefined && isBool(type);
|
|
641
659
|
}
|
|
642
660
|
|
|
643
661
|
function requiresSessionContext(expression: FilterExpression): boolean {
|
|
@@ -388,7 +388,46 @@ function expressionColumns(expression: FilterExpression, output = new Map<number
|
|
|
388
388
|
return output;
|
|
389
389
|
}
|
|
390
390
|
|
|
391
|
+
/**
|
|
392
|
+
* `WHERE flag` / `WHERE NOT flag` — a BOOLEAN column is a predicate on its own.
|
|
393
|
+
*
|
|
394
|
+
* DuckDB pushes such a predicate down as a bare `column_ref` rather than
|
|
395
|
+
* rewriting it to `flag = true`, and the schema admits that: `coreExpression`
|
|
396
|
+
* lists `columnRef` first. `flag` on its own is legal SQL, so this is not
|
|
397
|
+
* about the fragment being invalid — it is that the shape is invisible to
|
|
398
|
+
* everything that looks for a column/constant pair: `getColumnValues` returns
|
|
399
|
+
* nothing for it, and the rendering disagrees with every other VGI SDK, all
|
|
400
|
+
* of which spell it `flag = true`.
|
|
401
|
+
*
|
|
402
|
+
* The projection is exact rather than approximate, including under NULLs:
|
|
403
|
+
* `WHERE flag` keeps only TRUE (a NULL predicate is not satisfied) and so does
|
|
404
|
+
* `flag = true`; `WHERE NOT flag` keeps only FALSE (`NOT NULL` is NULL) and so
|
|
405
|
+
* does `flag = false`. Three-valued logic makes both pairs agree on every
|
|
406
|
+
* input, which is what lets this be a projection and not a change of meaning.
|
|
407
|
+
*
|
|
408
|
+
* A column that is not BOOLEAN is left alone rather than guessed at.
|
|
409
|
+
*
|
|
410
|
+
* @returns the column name and the constant it compares equal to, or
|
|
411
|
+
* undefined when `expression` is neither shape.
|
|
412
|
+
*/
|
|
413
|
+
function booleanColumnLeaf(expression: FilterExpression): { columnName: string; value: boolean } | undefined {
|
|
414
|
+
const column = (node: FilterExpression) =>
|
|
415
|
+
node.node === "column_ref" && node.dataType !== undefined && isBool(node.dataType)
|
|
416
|
+
? node.columnName
|
|
417
|
+
: undefined;
|
|
418
|
+
if (expression.node === "not") {
|
|
419
|
+
const name = column(expression.expression);
|
|
420
|
+
return name === undefined ? undefined : { columnName: name, value: false };
|
|
421
|
+
}
|
|
422
|
+
const name = column(expression);
|
|
423
|
+
return name === undefined ? undefined : { columnName: name, value: true };
|
|
424
|
+
}
|
|
425
|
+
|
|
391
426
|
function discreteValues(expression: FilterExpression, columnName: string): unknown[] | null {
|
|
427
|
+
// `WHERE flag` pins `flag` to TRUE exactly as `flag = true` does, so a
|
|
428
|
+
// value-pruning caller sees the constant either way round.
|
|
429
|
+
const leaf = booleanColumnLeaf(expression);
|
|
430
|
+
if (leaf && leaf.columnName === columnName) return [leaf.value];
|
|
392
431
|
if (expression.node === "comparison" && expression.op === ComparisonOp.EQ) {
|
|
393
432
|
if (expression.left.node === "column_ref" && expression.left.columnName === columnName && expression.right.node === "literal") return [expression.right.value];
|
|
394
433
|
if (expression.right.node === "column_ref" && expression.right.columnName === columnName && expression.left.node === "literal") return [expression.left.value];
|
|
@@ -502,7 +541,7 @@ export class PushdownFilters {
|
|
|
502
541
|
}
|
|
503
542
|
|
|
504
543
|
toSql(): string {
|
|
505
|
-
return this.predicates.map((predicate) =>
|
|
544
|
+
return this.predicates.map((predicate) => predicateToSql(predicate.expression)).join(" AND ");
|
|
506
545
|
}
|
|
507
546
|
}
|
|
508
547
|
|
|
@@ -514,6 +553,16 @@ function quote(value: unknown): string {
|
|
|
514
553
|
return String(value);
|
|
515
554
|
}
|
|
516
555
|
|
|
556
|
+
/**
|
|
557
|
+
* Render one predicate, projecting a bare boolean column onto its equality
|
|
558
|
+
* form first. Predicate positions are the root and the children of `and`/`or`
|
|
559
|
+
* — the shape that actually turns up is `other = x AND NOT flag`.
|
|
560
|
+
*/
|
|
561
|
+
export function predicateToSql(expression: FilterExpression): string {
|
|
562
|
+
const leaf = booleanColumnLeaf(expression);
|
|
563
|
+
return leaf ? `${leaf.columnName} = ${quote(leaf.value)}` : expressionToSql(expression);
|
|
564
|
+
}
|
|
565
|
+
|
|
517
566
|
export function expressionToSql(expression: FilterExpression): string {
|
|
518
567
|
switch (expression.node) {
|
|
519
568
|
case "column_ref": return expression.columnName;
|
|
@@ -527,7 +576,7 @@ export function expressionToSql(expression: FilterExpression): string {
|
|
|
527
576
|
};
|
|
528
577
|
return `${expressionToSql(expression.left)} ${symbols[expression.op]} ${expressionToSql(expression.right)}`;
|
|
529
578
|
}
|
|
530
|
-
case "and": case "or": return `(${expression.children.map(
|
|
579
|
+
case "and": case "or": return `(${expression.children.map(predicateToSql).join(` ${expression.node.toUpperCase()} `)})`;
|
|
531
580
|
case "not": return `(NOT ${expressionToSql(expression.expression)})`;
|
|
532
581
|
case "is_null": return `${expressionToSql(expression.expression)} IS ${expression.negated ? "NOT " : ""}NULL`;
|
|
533
582
|
case "in": return `${expressionToSql(expression.expression)} ${expression.negated ? "NOT " : ""}IN (${expression.set.values.map(quote).join(", ")})`;
|