Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions benchmarks/duckdb-bench/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ impl DuckClient {
for stmt in &statements {
self.connection().query(stmt)?;
}
// After `LOAD spatial`, shadow `ST_DWithin` so radius filters push. No-op without it.
self.db

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to register geo-overrides only for the bench or for users in general?
If it's the latter, we should move the initialization out of benchmark

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make it internal for benchmark only for now, later I will systematically applied into vortex-duckdb.

.as_ref()
.vortex_expect("DuckClient database accessed after close")
.register_st_dwithin_override()?;
self.init_sql = statements;
Ok(())
}
Expand Down Expand Up @@ -127,6 +132,11 @@ impl DuckClient {
.vortex_expect("connection just opened")
.query(stmt)?;
}
// Re-shadow `ST_DWithin` against the fresh instance.
self.db
.as_ref()
.vortex_expect("database just opened")
.register_st_dwithin_override()?;

Ok(())
}
Expand Down
59 changes: 59 additions & 0 deletions vortex-duckdb/cpp/expr.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,19 @@
#include "duckdb/planner/expression/bound_operator_expression.hpp"
#include "duckdb/planner/expression/bound_conjunction_expression.hpp"

#include "duckdb/catalog/catalog.hpp"
#include "duckdb/catalog/catalog_entry/scalar_function_catalog_entry.hpp"
#include "duckdb/common/error_data.hpp"
#include "duckdb/logging/logger.hpp"
#include "duckdb/main/capi/capi_internal.hpp"
#include "duckdb/main/client_context.hpp"
#include "duckdb/main/connection.hpp"
#include "duckdb/main/database_manager.hpp"
#include "duckdb/parser/parsed_data/create_scalar_function_info.hpp"
#include "duckdb/transaction/meta_transaction.hpp"

#include <exception>

using namespace duckdb;

extern "C" const char *duckdb_vx_sfunc_name(duckdb_vx_sfunc ffi_func) {
Expand All @@ -21,6 +34,52 @@ extern "C" const char *duckdb_vx_sfunc_name(duckdb_vx_sfunc ffi_func) {
return func->name.c_str();
}

extern "C" duckdb_state duckdb_vx_register_st_dwithin_override(duckdb_database ffi_db) {
if (!ffi_db) {
return DuckDBError;
}
const DatabaseWrapper &wrapper = *reinterpret_cast<DatabaseWrapper *>(ffi_db);
DatabaseInstance &db = *wrapper.database->instance;
try {
Connection conn(db);
ClientContext &context = *conn.context;
context.RunFunctionInTransaction([&]() {
auto &system = Catalog::GetSystemCatalog(context);
auto entry = system.GetEntry<ScalarFunctionCatalogEntry>(context,
DEFAULT_SCHEMA,
"st_dwithin",
OnEntryNotFound::RETURN_NULL);
if (!entry) {
// No `spatial` loaded, so there is no `ST_DWithin` to override.
return;
}
ScalarFunctionSet set("st_dwithin");
for (const auto &overload : entry->functions.functions) {
ScalarFunction copy = overload;
// Keep the radius as children[2]; spatial's bind folds it into private bind data.
copy.bind = nullptr;
set.AddFunction(copy);
}
CreateScalarFunctionInfo info(std::move(set));
info.on_conflict = OnCreateConflict::REPLACE_ON_CONFLICT;
// `internal` entries are only accepted by the system catalog.
info.internal = false;
// The user catalog binds ahead of the system catalog, shadowing spatial's entry;
// `RestoreStDWithin` rebinds unpushed calls through the original.
auto &catalog = Catalog::GetCatalog(context, DatabaseManager::GetDefaultDatabase(context));
// Durable catalogs require the modified mark; scalar function entries are never
// persisted, so this is metadata-only.
MetaTransaction::Get(context).ModifyDatabase(catalog.GetAttached(), DatabaseModificationType());
catalog.CreateFunction(context, info);
});
} catch (const std::exception &e) {
ErrorData data(e);
DUCKDB_LOG_ERROR(db, "Failed to register the ST_DWithin override:\t" + data.Message());
return DuckDBError;
}
return DuckDBSuccess;
}

extern "C" const char *duckdb_vx_expr_to_string(duckdb_vx_expr ffi_expr) {
if (!ffi_expr) {
return nullptr;
Expand Down
5 changes: 5 additions & 0 deletions vortex-duckdb/cpp/include/expr.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ typedef struct duckdb_vx_sfunc_ *duckdb_vx_sfunc;

const char *duckdb_vx_sfunc_name(duckdb_vx_sfunc ffi_func);

/// Shadow `ST_DWithin` with a copy that keeps the radius as the third argument, so
/// radius filters can push into Vortex scans. See `RestoreStDWithin` in
/// scalar_fn_pushdown.hpp for the override/restore example.
duckdb_state duckdb_vx_register_st_dwithin_override(duckdb_database ffi_db);
Comment thread
myrrc marked this conversation as resolved.

typedef struct duckdb_vx_expr_ *duckdb_vx_expr;

/// Return the string representation of the expression. Must be freed with `duckdb_free`.
Expand Down
15 changes: 15 additions & 0 deletions vortex-duckdb/cpp/include/scalar_fn_pushdown.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,21 @@ using Projections = unordered_map<TableIndex, LogicalProjection &>;

LogicalOperatorPtr TryPushdownScalarFunctions(ClientContext &context, LogicalOperatorPtr plan);

/*
* We override spatial's `ST_DWithin` with a copy that keeps the radius as a plain third
* argument (see expr.h); the original folds it into bind data at bind time. We need the
* radius visible to push the filter into a Vortex scan, but spatial's join optimizer only
* recognizes the folded 2-argument form. So this pass rebinds join conditions through
* spatial's original function and leaves filters alone:
*
* FILTER st_dwithin(t.geom, 'POINT(0 0)', 10.0) -- untouched, pushed into the scan
* JOIN ON st_dwithin(a.geom, b.geom, 10.0) -- rebound: st_dwithin(a.geom, b.geom)
* with the radius in bind data
*
* Runs in the pre-optimize hook, before any extension's optimizer pass.
*/
void RestoreStDWithin(ClientContext &context, LogicalOperator &plan);

/**
* Collect fn(col) expressions i.e. expressions where a single function (not
* a function chain) wraps a single bound column. If "col" is used without
Expand Down
66 changes: 66 additions & 0 deletions vortex-duckdb/cpp/scalar_fn_pushdown.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright the Vortex contributors
#include "duckdb/catalog/catalog.hpp"
#include "duckdb/catalog/catalog_entry/scalar_function_catalog_entry.hpp"
#include "duckdb/function/function_binder.hpp"
#include "duckdb/planner/operator/logical_projection.hpp"
#include "scalar_fn_pushdown.hpp"
#include "table_function.hpp"
Expand Down Expand Up @@ -231,3 +233,67 @@ ScalarFnReplace::ScalarFnReplace(Analyses &analyses, const Projections &projecti
TableColumnStorageIndex GetAnalysis::StorageIndex(TableColumnScanIndex idx) const {
return get.GetColumnIds()[idx].GetPrimaryIndex();
}

namespace {

// See RestoreStDWithin: rebinding through spatial's own entry lets spatial build its own bind
// data, so nothing here depends on its internals.
class StDWithinRestore final : public LogicalOperatorVisitor {
public:
explicit StDWithinRestore(ClientContext &context) : context(context) {
}

// Restore join conditions, filters must keep the radius visible so
// DuckDB's filter pushdown can offer them to Vortex scans.
void VisitOperator(LogicalOperator &op) override {
using enum LogicalOperatorType;
switch (op.type) {
case LOGICAL_COMPARISON_JOIN:
case LOGICAL_ANY_JOIN:
case LOGICAL_DELIM_JOIN:
case LOGICAL_ASOF_JOIN:
VisitOperatorExpressions(op);
break;
default:
break;
}
VisitOperatorChildren(op);
}

ExpressionPtr VisitReplace(BoundFunctionExpression &expr, ExpressionPtr *) override {
if (expr.children.size() != 3 || expr.function.name != "st_dwithin") {
return nullptr; // Not the override's shape: keep it and descend into its children.
}
// The system catalog holds spatial's original; the user-catalog override cannot shadow
// this lookup.
auto original = Catalog::GetSystemCatalog(context).GetEntry<ScalarFunctionCatalogEntry>(
context,
DEFAULT_SCHEMA,
"st_dwithin",
OnEntryNotFound::RETURN_NULL);
if (!original) {
return nullptr;
}
vector<ExpressionPtr> children;
children.reserve(expr.children.size());
for (const auto &child : expr.children) {
children.push_back(child->Copy());
}
ErrorData error;
FunctionBinder binder(context);
auto bound = binder.BindScalarFunction(*original, std::move(children), error);
if (!bound) {
return nullptr; // No matching overload: keep the executable 3-argument form.
}
return bound;
}

private:
ClientContext &context;
};

} // namespace

void RestoreStDWithin(ClientContext &context, LogicalOperator &plan) {
StDWithinRestore(context).VisitOperator(plan);
}
7 changes: 6 additions & 1 deletion vortex-duckdb/cpp/vortex_duckdb.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -271,8 +271,13 @@ static void VortexOptimizeFunction(OptimizerExtensionInput &input, unique_ptr<Lo
plan = TryPushdownScalarFunctions(input.context, std::move(plan));
}

static void VortexPreOptimizeFunction(OptimizerExtensionInput &input, unique_ptr<LogicalOperator> &plan) {
RestoreStDWithin(input.context, *plan);
}

struct VortexOptimizerExtension final : OptimizerExtension {
inline VortexOptimizerExtension() : OptimizerExtension(VortexOptimizeFunction, nullptr, {}) {
inline VortexOptimizerExtension()
: OptimizerExtension(VortexOptimizeFunction, VortexPreOptimizeFunction, {}) {
}
};

Expand Down
Loading
Loading