Skip to content

Commit f96abd1

Browse files
committed
pushdown scalar function
Signed-off-by: Mikhail Kot <mikhail@spiraldb.com>
1 parent b94291b commit f96abd1

15 files changed

Lines changed: 1049 additions & 33 deletions

File tree

vortex-duckdb/build.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const DEFAULT_DUCKDB_VERSION: &str = "1.5.3";
2424

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

27-
const SOURCE_FILES: [&str; 17] = [
27+
const SOURCE_FILES: [&str; 18] = [
2828
"cpp/client_context.cpp",
2929
"cpp/config.cpp",
3030
"cpp/copy_function.cpp",
@@ -34,6 +34,7 @@ const SOURCE_FILES: [&str; 17] = [
3434
"cpp/expr.cpp",
3535
"cpp/file_system.cpp",
3636
"cpp/logical_type.cpp",
37+
"cpp/optimizer.cpp",
3738
"cpp/replacement_scan.cpp",
3839
"cpp/reusable_dict.cpp",
3940
"cpp/scalar_function.cpp",

vortex-duckdb/cpp/include/duckdb_vx.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#pragma once
55

66
#include "duckdb_vx/client_context.h"
7+
#include "duckdb_vx/optimizer.h"
78
#include "duckdb_vx/config.h"
89
#include "duckdb_vx/copy_function.h"
910
#include "duckdb_vx/data.h"
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
#pragma once
4+
#include "duckdb.h"
5+
6+
#ifdef __cplusplus
7+
extern "C" {
8+
#endif
9+
10+
duckdb_state duckdb_vx_optimizer_extension_register(duckdb_database ffi_db);
11+
12+
#ifdef __cplusplus
13+
}
14+
#endif
15+
16+
#ifdef __cplusplus
17+
#include "duckdb/optimizer/optimizer_extension.hpp"
18+
#include "duckdb/planner/expression/bound_columnref_expression.hpp"
19+
#include "duckdb/planner/expression/bound_function_expression.hpp"
20+
#include "duckdb/planner/operator/logical_get.hpp"
21+
#include <optional>
22+
23+
// Only one consumer of this header file, so "using" is fine
24+
using namespace duckdb;
25+
26+
using ExpressionPtr = unique_ptr<Expression>;
27+
using LogicalOperatorPtr = unique_ptr<LogicalOperator>;
28+
29+
/**
30+
* Column index in requested scan. Example:
31+
*
32+
* CREATE TABLE t (a1 INTEGER, a2 INTEGER, a3 INTEGER);
33+
* SELECT a2, a3 FROM t;
34+
*
35+
* a2's TableColumnScanIndex is 0, a3's TableColumnScanIndex is 1,
36+
* index is index in SELECT clause.
37+
*/
38+
using TableColumnScanIndex = idx_t;
39+
40+
/**
41+
* Column index in table's storage. Example:
42+
*
43+
* CREATE TABLE t (a1 INTEGER, a2 INTEGER, a3 INTEGER);
44+
* SELECT a2, a3 FROM t;
45+
*
46+
* a2's TableColumnStorageIndex is 1, a3's TableColumnScanIndex is 2,
47+
* index is index of column in table storage.
48+
*
49+
* for i: TableColumnScanIndex, column_ids[i].GetPrimaryIndex() is
50+
* TableColumnStorageIndex
51+
*/
52+
using TableColumnStorageIndex = idx_t;
53+
54+
using TableIndex = idx_t;
55+
56+
struct GetAnalysis {
57+
LogicalGet &get;
58+
/**
59+
* for fn(col), mapping of "col scan index" -> "fn expression".
60+
* "fn expression" is nullptr iff column is used with a different function
61+
* or without function application in the query plan.
62+
*/
63+
unordered_map<TableColumnScanIndex, const BoundFunctionExpression *> col_to_fn;
64+
};
65+
66+
using Analyses = unordered_map<TableIndex, GetAnalysis>;
67+
68+
/*
69+
* SELECT fn(col) FROM '*.vortex' yields a PROJECTION fn(col) -> GET (vortex)
70+
* plan. PROJECTION's "col" table_index is 1, vortex GET's table_index is 0.
71+
* So we want to track original table_index for GET in case column is found
72+
* in filter we failed to push down (i.e. WHERE prefix(col, 'h')) as well as
73+
* projection's table_index.
74+
*
75+
* So we keep a mapping of
76+
*
77+
* "projection table index" to "projection operator".
78+
*
79+
* to resolve this.
80+
* For simplicity, current implementation is limited to one level i.e.
81+
* PROJECTION -> GET (i.e. read from VIEW) is pushed down but VIEW->VIEW->GET
82+
* or VIEW->CTE->GET is not.
83+
*/
84+
using Projections = unordered_map<TableIndex, const LogicalProjection &>;
85+
86+
/**
87+
* Collect fn(col) expressions i.e. expressions where a single function (not
88+
* a function chain) wraps a single bound column. If "col" is used without
89+
* function application in "plan", record in "analyses.conflicts"
90+
*/
91+
struct ScalarFnCollect final : LogicalOperatorVisitor {
92+
Analyses &analyses;
93+
const Projections &projections;
94+
95+
ScalarFnCollect(Analyses &analyses, const Projections &projections);
96+
void VisitOperator(LogicalOperator &op) override;
97+
ExpressionPtr VisitReplace(BoundColumnRefExpression &expr, ExpressionPtr *ptr) override;
98+
ExpressionPtr VisitReplace(BoundFunctionExpression &expr, ExpressionPtr *ptr) override;
99+
};
100+
101+
/*
102+
* For "col" in columns collected by ScalarFnCollect, replace fn(col) to "col"
103+
* if "col" doesn't have conflicting usage. Update return types for bound
104+
* columns and logical projections referencing this column.
105+
*/
106+
struct ScalarFnReplace final : LogicalOperatorVisitor {
107+
Analyses &analyses;
108+
const Projections &projections;
109+
110+
ScalarFnReplace(Analyses &analyses, const Projections &aliases);
111+
ExpressionPtr VisitReplace(BoundColumnRefExpression &expr, ExpressionPtr *ptr) override;
112+
ExpressionPtr VisitReplace(BoundFunctionExpression &expr, ExpressionPtr *ptr) override;
113+
};
114+
115+
void FindGetsAndProjections(LogicalOperator &op, Analyses &analyses, Projections &aliases);
116+
117+
LogicalOperatorPtr TryPushdownScalarFunctions(ClientContext &context, LogicalOperatorPtr plan);
118+
void VortexOptimizeFunction(OptimizerExtensionInput &input, LogicalOperatorPtr &plan);
119+
120+
struct VortexOptimizerExtension final : OptimizerExtension {
121+
inline VortexOptimizerExtension() : OptimizerExtension(VortexOptimizeFunction, nullptr, {}) {
122+
}
123+
};
124+
125+
struct GetBinding {
126+
GetAnalysis &analysis;
127+
TableColumnScanIndex column_index;
128+
};
129+
130+
/*
131+
* Given a column binding, resolve it to a GET and a GET's column scan index.
132+
* Returns nullopt for virtual columns and columns which are neither part of
133+
* GET nor part of PROJECTION wrapping a GET.
134+
*/
135+
std::optional<GetBinding> Resolve(ColumnBinding binding, Analyses &analyses, const Projections &projections);
136+
137+
#endif

vortex-duckdb/cpp/include/duckdb_vx/table_function.h

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,24 @@
1010

1111
#ifdef __cplusplus
1212
static_assert(sizeof(idx_t) == 8);
13+
14+
#include "duckdb/main/capi/capi_internal.hpp"
15+
16+
duckdb::unique_ptr<duckdb::FunctionData>
17+
duckdb_vx_table_function_bind(duckdb::ClientContext &context,
18+
duckdb::TableFunctionBindInput &input,
19+
duckdb::vector<duckdb::LogicalType> &return_types,
20+
duckdb::vector<duckdb::string> &names);
21+
22+
struct TableFunctionProjectionExpressionInput {
23+
const duckdb::LogicalGet &get;
24+
const duckdb::Expression &expression;
25+
idx_t projection_idx;
26+
};
27+
28+
// true if we can push down the expression, false otherwise
29+
bool projection_expression_pushdown(duckdb::ClientContext &context,
30+
const TableFunctionProjectionExpressionInput &input);
1331
#endif
1432

1533
#ifdef __cplusplus

0 commit comments

Comments
 (0)