Skip to content

Commit 002e40a

Browse files
ST_Intersects pushdown: vortex.geo.intersects kernel, DuckDB lowering, spatial function overrides (#8704)
## Rationale for this change `ST_Intersects` over native geometry columns currently evaluates inside DuckDB, exporting every row as `GEOMETRY` just to be tested. This adds a native `vortex.geo.intersects` kernel and pushes the filter into the scan: on SpatialBench SF1 (6M points vs a literal polygon), 406ms (DuckDB filter) / 36.6ms (SPATIAL_JOIN) -> 12.4ms pushed. Part of the native geospatial lane, following the `ST_Distance`/`ST_DWithin` pushdown. ## What changes are included in this PR? - `vortex-geo`: `GeoIntersects` kernel. - `vortex-duckdb`: lower `st_intersects(native column, GEOMETRY literal)` like `st_distance`. - `vortex-duckdb`: shadow spatial's `ST_Intersects` with a non-fallible copy — DuckDB won't push can-throw filters through the projection every view-registered table has. The `ST_DWithin` override generalizes into a table-driven registry (`spatial_overrides.hpp`) shared by registration and the join-condition restore pass. Note: drop `GeoDistance`'s columnar point fast paths. `geo` is row-oriented, so kernels materialize rows regardless; hand-rolled columnar paths belong in a future columnar geometry compute library (pushed Q1: 5.4ms -> 12.6ms, still far ahead of unpushed). --------- Signed-off-by: Nemo Yu <zyu379@wisc.edu>
1 parent 3d78175 commit 002e40a

21 files changed

Lines changed: 1146 additions & 259 deletions

File tree

benchmarks/duckdb-bench/src/lib.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,12 @@ impl DuckClient {
7878
for stmt in &statements {
7979
self.connection().query(stmt)?;
8080
}
81-
// After `LOAD spatial`, shadow `ST_DWithin` so radius filters push. No-op without it.
81+
// After `LOAD spatial`, shadow the overridden spatial functions so that their filters
82+
// push down. No-op without it.
8283
self.db
8384
.as_ref()
8485
.vortex_expect("DuckClient database accessed after close")
85-
.register_st_dwithin_override()?;
86+
.register_spatial_overrides()?;
8687
self.init_sql = statements;
8788
Ok(())
8889
}
@@ -132,11 +133,11 @@ impl DuckClient {
132133
.vortex_expect("connection just opened")
133134
.query(stmt)?;
134135
}
135-
// Re-shadow `ST_DWithin` against the fresh instance.
136+
// Re-shadow the overridden spatial functions against the fresh instance.
136137
self.db
137138
.as_ref()
138139
.vortex_expect("database just opened")
139-
.register_st_dwithin_override()?;
140+
.register_spatial_overrides()?;
140141

141142
Ok(())
142143
}

vortex-duckdb/build.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,13 @@ const DEFAULT_DUCKDB_VERSION: &str = "1.5.3";
2727

2828
const BUILD_ARTIFACTS: [&str; 3] = ["libduckdb.dylib", "libduckdb.so", "libduckdb_static.a"];
2929

30-
const SOURCE_FILES: [&str; 10] = [
30+
const SOURCE_FILES: [&str; 11] = [
3131
"cpp/vortex_duckdb.cpp",
3232
"cpp/copy_function.cpp",
3333
"cpp/expr.cpp",
3434
"cpp/optimizer.cpp",
3535
"cpp/scalar_fn_pushdown.cpp",
36+
"cpp/spatial_overrides.cpp",
3637
"cpp/cast_pushdown.cpp",
3738
"cpp/aggregate_fn_pushdown.cpp",
3839
"cpp/table_filter.cpp",
@@ -178,9 +179,10 @@ const DUCKDB_C_API_FUNCTIONS: [&str; 133] = [
178179
"duckdb_vector_size",
179180
];
180181

181-
const DUCKDB_C_API_HEADERS: [&str; 6] = [
182+
const DUCKDB_C_API_HEADERS: [&str; 7] = [
182183
"cpp/include/vortex_duckdb.h",
183184
"cpp/include/expr.h",
185+
"cpp/include/spatial_overrides.h",
184186
"cpp/include/table_filter.h",
185187
"cpp/include/vector.h",
186188
"cpp/include/copy_function.h",

vortex-duckdb/cpp/expr.cpp

Lines changed: 0 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,6 @@
1414
#include "duckdb/planner/expression/bound_operator_expression.hpp"
1515
#include "duckdb/planner/expression/bound_conjunction_expression.hpp"
1616

17-
#include "duckdb/catalog/catalog.hpp"
18-
#include "duckdb/catalog/catalog_entry/scalar_function_catalog_entry.hpp"
19-
#include "duckdb/common/error_data.hpp"
20-
#include "duckdb/logging/logger.hpp"
21-
#include "duckdb/main/capi/capi_internal.hpp"
22-
#include "duckdb/main/client_context.hpp"
23-
#include "duckdb/main/connection.hpp"
24-
#include "duckdb/main/database_manager.hpp"
25-
#include "duckdb/parser/parsed_data/create_scalar_function_info.hpp"
26-
#include "duckdb/transaction/meta_transaction.hpp"
27-
28-
#include <exception>
29-
3017
using namespace duckdb;
3118

3219
extern "C" const char *duckdb_vx_sfunc_name(duckdb_vx_sfunc ffi_func) {
@@ -37,52 +24,6 @@ extern "C" const char *duckdb_vx_sfunc_name(duckdb_vx_sfunc ffi_func) {
3724
return func->name.c_str();
3825
}
3926

40-
extern "C" duckdb_state duckdb_vx_register_st_dwithin_override(duckdb_database ffi_db) {
41-
if (!ffi_db) {
42-
return DuckDBError;
43-
}
44-
const DatabaseWrapper &wrapper = *reinterpret_cast<DatabaseWrapper *>(ffi_db);
45-
DatabaseInstance &db = *wrapper.database->instance;
46-
try {
47-
Connection conn(db);
48-
ClientContext &context = *conn.context;
49-
context.RunFunctionInTransaction([&]() {
50-
auto &system = Catalog::GetSystemCatalog(context);
51-
auto entry = system.GetEntry<ScalarFunctionCatalogEntry>(context,
52-
DEFAULT_SCHEMA,
53-
"st_dwithin",
54-
OnEntryNotFound::RETURN_NULL);
55-
if (!entry) {
56-
// No `spatial` loaded, so there is no `ST_DWithin` to override.
57-
return;
58-
}
59-
ScalarFunctionSet set("st_dwithin");
60-
for (const auto &overload : entry->functions.functions) {
61-
ScalarFunction copy = overload;
62-
// Keep the radius as children[2]; spatial's bind folds it into private bind data.
63-
copy.bind = nullptr;
64-
set.AddFunction(copy);
65-
}
66-
CreateScalarFunctionInfo info(std::move(set));
67-
info.on_conflict = OnCreateConflict::REPLACE_ON_CONFLICT;
68-
// `internal` entries are only accepted by the system catalog.
69-
info.internal = false;
70-
// The user catalog binds ahead of the system catalog, shadowing spatial's entry;
71-
// `RestoreStDWithin` rebinds unpushed calls through the original.
72-
auto &catalog = Catalog::GetCatalog(context, DatabaseManager::GetDefaultDatabase(context));
73-
// Durable catalogs require the modified mark; scalar function entries are never
74-
// persisted, so this is metadata-only.
75-
MetaTransaction::Get(context).ModifyDatabase(catalog.GetAttached(), DatabaseModificationType());
76-
catalog.CreateFunction(context, info);
77-
});
78-
} catch (const std::exception &e) {
79-
ErrorData data(e);
80-
DUCKDB_LOG_ERROR(db, "Failed to register the ST_DWithin override:\t" + data.Message());
81-
return DuckDBError;
82-
}
83-
return DuckDBSuccess;
84-
}
85-
8627
extern "C" const char *duckdb_vx_agg_func_name(duckdb_vx_agg_func ffi) {
8728
D_ASSERT(ffi);
8829
return reinterpret_cast<AggregateFunction *>(ffi)->name.c_str();

vortex-duckdb/cpp/include/expr.h

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,6 @@ typedef struct duckdb_vx_agg_func_ *duckdb_vx_agg_func;
1414

1515
const char *duckdb_vx_sfunc_name(duckdb_vx_sfunc ffi_func);
1616

17-
/// Shadow `ST_DWithin` with a copy that keeps the radius as the third argument, so
18-
/// radius filters can push into Vortex scans. See `RestoreStDWithin` in
19-
/// scalar_fn_pushdown.hpp for the override/restore example.
20-
duckdb_state duckdb_vx_register_st_dwithin_override(duckdb_database ffi_db);
21-
2217
typedef struct duckdb_vx_expr_ *duckdb_vx_expr;
2318

2419
const char *duckdb_vx_agg_func_name(duckdb_vx_agg_func func);

vortex-duckdb/cpp/include/scalar_fn_pushdown.hpp

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,5 +35,3 @@ struct ScalarFnReplace final : LogicalOperatorVisitor {
3535
ExpressionPtr VisitReplace(BoundColumnRefExpression &expr, ExpressionPtr *ptr) override;
3636
ExpressionPtr VisitReplace(BoundFunctionExpression &expr, ExpressionPtr *ptr) override;
3737
};
38-
39-
void RestoreStDWithin(ClientContext &context, LogicalOperator &plan);
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
#pragma once
5+
6+
#include "duckdb.h"
7+
8+
#ifdef __cplusplus /* If compiled as C++, use C ABI */
9+
extern "C" {
10+
#endif
11+
12+
/// Shadow the spatial functions that block filter pushdown with pushable copies; see
13+
/// `SPATIAL_OVERRIDES` in spatial_overrides.cpp for the list. Call after `LOAD spatial`;
14+
/// does nothing when spatial is not loaded.
15+
duckdb_state duckdb_vx_register_spatial_overrides(duckdb_database ffi_db);
16+
17+
#ifdef __cplusplus
18+
}
19+
#endif
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
#pragma once
4+
#include "duckdb/main/client_context.hpp"
5+
#include "duckdb/planner/expression/bound_function_expression.hpp"
6+
#include "duckdb/planner/logical_operator_visitor.hpp"
7+
8+
using namespace duckdb;
9+
10+
/*
11+
* Vortex shadows some spatial (`ST_*`) scalar functions with tweaked copies, because as
12+
* spatial registers them their filters can never push into Vortex scans.
13+
*
14+
* `duckdb_vx_register_spatial_overrides` (spatial_overrides.h) installs the copies after
15+
* `LOAD spatial`. `RestoreSpatialOverrides` below hands join conditions back to spatial's
16+
* originals: Vortex never pushes join conditions, and spatial's join machinery expects its
17+
* own functions.
18+
*/
19+
20+
// Rebinds overridden spatial calls in join conditions back to spatial's original, so spatial's
21+
// own machinery handles joins. Filters are left untouched: they keep the override and push to
22+
// Vortex scans.
23+
struct SpatialOverrideRestore final : LogicalOperatorVisitor {
24+
ClientContext &context;
25+
26+
explicit SpatialOverrideRestore(ClientContext &context);
27+
void VisitOperator(LogicalOperator &op) override;
28+
unique_ptr<Expression> VisitReplace(BoundFunctionExpression &expr, unique_ptr<Expression> *ptr) override;
29+
};
30+
31+
/// Rebind overridden spatial calls in join conditions to spatial's originals.
32+
void RestoreSpatialOverrides(ClientContext &context, LogicalOperator &plan);

vortex-duckdb/cpp/scalar_fn_pushdown.cpp

Lines changed: 0 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@
22
// SPDX-FileCopyrightText: Copyright the Vortex contributors
33
#include "scalar_fn_pushdown.hpp"
44

5-
#include "duckdb/catalog/catalog.hpp"
6-
#include "duckdb/catalog/catalog_entry/scalar_function_catalog_entry.hpp"
7-
#include "duckdb/function/function_binder.hpp"
85
#include "duckdb/planner/operator/logical_projection.hpp"
96
#include "duckdb/planner/operator/logical_get.hpp"
107

@@ -122,67 +119,3 @@ ScalarFnCollect::ScalarFnCollect(Analyses &analyses, const Projections &projecti
122119
ScalarFnReplace::ScalarFnReplace(Analyses &analyses, const Projections &projections)
123120
: analyses(analyses), projections(projections) {
124121
}
125-
126-
namespace {
127-
128-
// See RestoreStDWithin: rebinding through spatial's own entry lets spatial build its own bind
129-
// data, so nothing here depends on its internals.
130-
class StDWithinRestore final : public LogicalOperatorVisitor {
131-
public:
132-
explicit StDWithinRestore(ClientContext &context) : context(context) {
133-
}
134-
135-
// Restore join conditions, filters must keep the radius visible so
136-
// DuckDB's filter pushdown can offer them to Vortex scans.
137-
void VisitOperator(LogicalOperator &op) override {
138-
using enum LogicalOperatorType;
139-
switch (op.type) {
140-
case LOGICAL_COMPARISON_JOIN:
141-
case LOGICAL_ANY_JOIN:
142-
case LOGICAL_DELIM_JOIN:
143-
case LOGICAL_ASOF_JOIN:
144-
VisitOperatorExpressions(op);
145-
break;
146-
default:
147-
break;
148-
}
149-
VisitOperatorChildren(op);
150-
}
151-
152-
ExpressionPtr VisitReplace(BoundFunctionExpression &expr, ExpressionPtr *) override {
153-
if (expr.children.size() != 3 || expr.function.name != "st_dwithin") {
154-
return nullptr; // Not the override's shape: keep it and descend into its children.
155-
}
156-
// The system catalog holds spatial's original; the user-catalog override cannot shadow
157-
// this lookup.
158-
auto original = Catalog::GetSystemCatalog(context).GetEntry<ScalarFunctionCatalogEntry>(
159-
context,
160-
DEFAULT_SCHEMA,
161-
"st_dwithin",
162-
OnEntryNotFound::RETURN_NULL);
163-
if (!original) {
164-
return nullptr;
165-
}
166-
vector<ExpressionPtr> children;
167-
children.reserve(expr.children.size());
168-
for (const auto &child : expr.children) {
169-
children.push_back(child->Copy());
170-
}
171-
ErrorData error;
172-
FunctionBinder binder(context);
173-
auto bound = binder.BindScalarFunction(*original, std::move(children), error);
174-
if (!bound) {
175-
return nullptr; // No matching overload: keep the executable 3-argument form.
176-
}
177-
return bound;
178-
}
179-
180-
private:
181-
ClientContext &context;
182-
};
183-
184-
} // namespace
185-
186-
void RestoreStDWithin(ClientContext &context, LogicalOperator &plan) {
187-
StDWithinRestore(context).VisitOperator(plan);
188-
}

0 commit comments

Comments
 (0)