diff --git a/src/include/dbconnector/optimizer/optimizer_util.hpp b/src/include/dbconnector/optimizer/optimizer_util.hpp index 167e097..a9ab673 100644 --- a/src/include/dbconnector/optimizer/optimizer_util.hpp +++ b/src/include/dbconnector/optimizer/optimizer_util.hpp @@ -15,6 +15,7 @@ namespace optimizer { struct TracedBindingColumn { std::string col_name; duckdb::LogicalType col_type; + duckdb::idx_t col_idx = 0; bool Found() { return !col_name.empty(); diff --git a/src/include/dbconnector/optimizer/order_by_and_limit_optimizer.hpp b/src/include/dbconnector/optimizer/order_by_and_limit_optimizer.hpp index fafeff9..6e51d39 100644 --- a/src/include/dbconnector/optimizer/order_by_and_limit_optimizer.hpp +++ b/src/include/dbconnector/optimizer/order_by_and_limit_optimizer.hpp @@ -17,10 +17,16 @@ class OrderByAndLimitOptimizer { char identifier_quote = '"'; query::QuoteEscapeStyle escape_style = query::QuoteEscapeStyle::DOUBLE_QUOTE; std::string table_scan_name; + //! Selects the per-type ORDER-key rewrites that make the remote sort + //! reproduce DuckDB's ordering (see TryBuildOrderByClause), and whether + //! LIMIT-bearing folds are allowed over filtered scans (see + //! LimitFoldUnsafe). + query::Dialect dialect = query::Dialect::Postgres; }; static Config CreateConfig(duckdb::ClientContext &ctx, const std::string &enabled_option, char identifier_quote, - query::QuoteEscapeStyle escape_style, std::string table_scan_name); + query::QuoteEscapeStyle escape_style, std::string table_scan_name, + query::Dialect dialect); static void Optimize(const Config &config, duckdb::OptimizerExtensionInput &input, duckdb::unique_ptr &op); diff --git a/src/include/dbconnector/query/query_writer.hpp b/src/include/dbconnector/query/query_writer.hpp index 42ccb66..1b443cf 100644 --- a/src/include/dbconnector/query/query_writer.hpp +++ b/src/include/dbconnector/query/query_writer.hpp @@ -11,6 +11,11 @@ namespace query { enum class QuoteEscapeStyle { BACKSLASH, DOUBLE_QUOTE }; +// The remote SQL dialect a pushdown targets. Selects the dialect-specific bits +// that are not expressible through the quote/escape/blob knobs alone: string +// constant escaping, VARCHAR collation, and struct/tuple field access. +enum class Dialect { Postgres, ClickHouse }; + class QueryWriter { public: struct Config { @@ -18,11 +23,13 @@ class QueryWriter { QuoteEscapeStyle escape_style = QuoteEscapeStyle::DOUBLE_QUOTE; std::string blob_literal_prefix; std::string blob_literal_suffix; + Dialect dialect = Dialect::Postgres; }; static Config CreateConfig(char quote, QuoteEscapeStyle escape_style, const std::string &blob_literal_prefix = std::string(), - const std::string &blob_literal_suffix = std::string()); + const std::string &blob_literal_suffix = std::string(), + Dialect dialect = Dialect::Postgres); static std::string WriteQuotedAndEscaped(const QueryWriter::Config &config, const std::string &text); static std::string WriteConstant(const QueryWriter::Config &config, const duckdb::Value &val); diff --git a/src/include/dbconnector/table_scan/filter_pushdown.hpp b/src/include/dbconnector/table_scan/filter_pushdown.hpp index 5e7efaf..c7d899e 100644 --- a/src/include/dbconnector/table_scan/filter_pushdown.hpp +++ b/src/include/dbconnector/table_scan/filter_pushdown.hpp @@ -12,18 +12,19 @@ namespace table_scan { class FilterPushdown { struct Config { - char identifier_quote = '"'; - char constant_quote = '\''; - query::QuoteEscapeStyle escape_style = query::QuoteEscapeStyle::DOUBLE_QUOTE; - std::string blob_literal_prefix; - std::string blob_literal_suffix; + query::QueryWriter::Config identifier; + query::QueryWriter::Config constant; }; public: static Config CreateConfig(char identifier_quote, char constant_quote, query::QuoteEscapeStyle escape_style, - const std::string &blob_literal_prefix = std::string(), + query::Dialect dialect, const std::string &blob_literal_prefix = std::string(), const std::string &blob_literal_suffix = std::string()); + //! All-or-nothing: returns SQL equivalent to the filter, or an empty string + //! when any required piece cannot be rendered (the filter must then be + //! applied locally). Only optional (advisory) pieces may be dropped from + //! the rendered SQL -- correctness never depends on them. static std::string TransformFilter(const Config &config, const std::string &column_name, const duckdb::TableFilter &filter, duckdb::column_t column_id); diff --git a/src/optimizer/aggregate_optimizer.cpp b/src/optimizer/aggregate_optimizer.cpp index 394f8e4..0d01a29 100644 --- a/src/optimizer/aggregate_optimizer.cpp +++ b/src/optimizer/aggregate_optimizer.cpp @@ -184,6 +184,8 @@ static PushedAggregate TryPushAggregateToMySQL(const AggregateOptimizer::Config if (get.table_filters.HasFilters()) { string where_clause; + auto scan_config = table_scan::FilterPushdown::CreateConfig('`', '\'', config.escape_style, + query::Dialect::Postgres); for (auto &entry : get.table_filters) { ProjectionIndex proj_idx = entry.GetIndex(); ColumnIndex col_idx = get.GetColumnIndex(proj_idx); @@ -192,10 +194,11 @@ static PushedAggregate TryPushAggregateToMySQL(const AggregateOptimizer::Config return res; } auto column_name = get.names[table_col_idx]; - auto scan_config = table_scan::FilterPushdown::CreateConfig('`', '\'', config.escape_style); auto new_filter = table_scan::FilterPushdown::TransformFilter(scan_config, column_name.GetIdentifierName(), entry.Filter(), table_col_idx); if (new_filter.empty()) { + // A partial WHERE under a remote aggregate returns wrong numbers; + // there is no local re-check once the rows are aggregated away. return res; } if (!where_clause.empty()) { diff --git a/src/optimizer/optimizer_util.cpp b/src/optimizer/optimizer_util.cpp index 19368cf..0e005cc 100644 --- a/src/optimizer/optimizer_util.cpp +++ b/src/optimizer/optimizer_util.cpp @@ -70,6 +70,7 @@ TracedBindingColumn OptimizerUtil::TraceBindingToColumn(ColumnBinding binding, L } res.col_name = get.names[actual_col_idx].GetIdentifierName(); res.col_type = get.returned_types[actual_col_idx]; + res.col_idx = actual_col_idx; return res; } diff --git a/src/optimizer/order_by_and_limit_optimizer.cpp b/src/optimizer/order_by_and_limit_optimizer.cpp index ee0d925..2dd171d 100644 --- a/src/optimizer/order_by_and_limit_optimizer.cpp +++ b/src/optimizer/order_by_and_limit_optimizer.cpp @@ -8,6 +8,7 @@ #include "duckdb/planner/operator/logical_projection.hpp" #include "duckdb/planner/expression/bound_columnref_expression.hpp" #include "duckdb/planner/expression_iterator.hpp" +#include "duckdb/planner/filter/expression_filter.hpp" #include "dbconnector/bind_data.hpp" #include "dbconnector/optimizer/optimizer_util.hpp" @@ -20,7 +21,8 @@ using namespace duckdb; OrderByAndLimitOptimizer::Config OrderByAndLimitOptimizer::CreateConfig(ClientContext &ctx, const std::string &enabled_option, char identifier_quote, - query::QuoteEscapeStyle escape_style, std::string table_scan_name) { + query::QuoteEscapeStyle escape_style, std::string table_scan_name, + query::Dialect dialect) { Config res; res.enabled = false; @@ -32,68 +34,89 @@ OrderByAndLimitOptimizer::CreateConfig(ClientContext &ctx, const std::string &en res.identifier_quote = identifier_quote; res.escape_style = escape_style; res.table_scan_name = std::move(table_scan_name); + res.dialect = dialect; return res; } -static string TraceColumnToGet(const OrderByAndLimitOptimizer::Config &config, Expression &expr, LogicalOperator &child, - LogicalGet &get) { +// Traces an ORDER BY key expression to a scan column; false when the key is +// not a plain scan column. +static bool TraceOrderKey(const OrderByAndLimitOptimizer::Config &config, Expression &expr, LogicalOperator &child, + LogicalGet &get, string "ed, LogicalType &type) { if (expr.GetExpressionClass() != ExpressionClass::BOUND_COLUMN_REF) { - return std::string(); + return false; } auto &col_ref = expr.Cast(); if (col_ref.Depth() > 0) { - return std::string(); + return false; } - auto binding = col_ref.BindingMutable(); - - reference current = child; - while (current.get().type == LogicalOperatorType::LOGICAL_PROJECTION) { - auto &proj = current.get().Cast(); - if (binding.table_index != proj.table_index) { - break; - } - if (binding.column_index >= proj.expressions.size()) { - return std::string(); - } - auto &proj_expr = *proj.expressions[binding.column_index]; - if (proj_expr.GetExpressionClass() != ExpressionClass::BOUND_COLUMN_REF) { - return std::string(); - } - auto &inner_ref = proj_expr.Cast(); - if (inner_ref.Depth() > 0) { - return std::string(); - } - binding = inner_ref.BindingMutable(); - current = *current.get().children[0]; + auto traced = OptimizerUtil::TraceBindingToColumn(col_ref.BindingMutable(), child, get); + if (!traced.Found()) { + return false; } + auto query_config = query::QueryWriter::CreateConfig(config.identifier_quote, config.escape_style); + quoted = query::QueryWriter::WriteQuotedAndEscaped(query_config, traced.col_name); + type = traced.col_type; + return true; +} - if (binding.table_index != get.table_index) { - return std::string(); - } - auto &column_ids = get.GetColumnIds(); - if (binding.column_index >= column_ids.size()) { - return std::string(); +// True when `type` nests a scalar whose ClickHouse ordering diverges from +// DuckDB's: a divergent field cannot be rewritten inside a whole-value +// comparison, so such keys stay local. VARCHAR is included because an Enum +// (ordinal order remotely) is indistinguishable from a plain String here. +static bool CompoundContainsDivergent(const LogicalType &type) { + switch (type.id()) { + case LogicalTypeId::FLOAT: + case LogicalTypeId::DOUBLE: + case LogicalTypeId::UUID: + case LogicalTypeId::VARCHAR: + return true; + case LogicalTypeId::STRUCT: { + for (auto &child : StructType::GetChildTypes(type)) { + if (CompoundContainsDivergent(child.second)) { + return true; + } + } + return false; } - auto &col_index = column_ids[binding.column_index]; - if (col_index.IsRowIdColumn()) { - return std::string(); + case LogicalTypeId::LIST: + return CompoundContainsDivergent(ListType::GetChildType(type)); + case LogicalTypeId::ARRAY: + return CompoundContainsDivergent(ArrayType::GetChildType(type)); + case LogicalTypeId::MAP: + return CompoundContainsDivergent(MapType::KeyType(type)) || + CompoundContainsDivergent(MapType::ValueType(type)); + default: + return false; } +} - auto actual_col_idx = col_index.GetPrimaryIndex(); - if (actual_col_idx >= get.names.size()) { - return std::string(); +static bool LimitFoldUnsafe(const OrderByAndLimitOptimizer::Config &config, const LogicalGet &get) { + // Postgres filter pushdown is exact-or-error: every required filter runs in + // the remote statement, WHERE before LIMIT, so folding is always safe. Other + // engines' comparison semantics (ClickHouse: NaN, Enum ordinals, literals + // parsed in the server time zone) force connectors to keep some required + // filters local and re-apply them AFTER the fetch -- a folded LIMIT would + // truncate the stream before that re-check and drop qualifying rows. + // Optional (advisory) filters never change the row set and do not block. + if (config.dialect == query::Dialect::Postgres) { + return false; } - auto query_config = query::QueryWriter::CreateConfig(config.identifier_quote, config.escape_style); - return query::QueryWriter::WriteQuotedAndEscaped(query_config, get.names[actual_col_idx].GetIdentifierName()); + for (auto &entry : get.table_filters) { + if (!ExpressionFilter::IsOptionalFilter(entry.Filter())) { + return true; + } + } + return false; } static string TryBuildOrderByClause(const OrderByAndLimitOptimizer::Config &config, vector &orders, LogicalOperator &child, LogicalGet &get) { vector fragments; for (auto &order : orders) { - string col_name = TraceColumnToGet(config, *order.expression, child, get); - if (col_name.empty()) { + string quoted; + LogicalType key_type; + if (!TraceOrderKey(config, *order.expression, child, get, quoted, key_type)) { return std::string(); } @@ -108,19 +131,53 @@ static string TryBuildOrderByClause(const OrderByAndLimitOptimizer::Config &conf (direction == OrderType::ASCENDING) ? OrderByNullType::NULLS_LAST : OrderByNullType::NULLS_FIRST; } - if (direction == OrderType::ASCENDING) { - if (null_order == OrderByNullType::NULLS_FIRST) { - fragments.push_back(col_name + " ASC"); - } else { - fragments.push_back(col_name + " IS NULL, " + col_name + " ASC"); + // Emit an explicit IS [NOT] NULL prefix key for BOTH null placements instead + // of relying on the remote engine's default, which differs per engine (MySQL + // sorts NULLs first on ASC; Postgres and ClickHouse sort them last). A bare + // "col ASC" for NULLS FIRST returns the wrong rows on the latter two -- and + // since the fold removes the local sort node, nothing downstream corrects it. + const char *null_key = (null_order == OrderByNullType::NULLS_FIRST) ? " IS NOT NULL, " : " IS NULL, "; + const char *dir = (direction == OrderType::ASCENDING) ? " ASC" : " DESC"; + + // Rewrite the value key per dialect so the remote reproduces DuckDB's + // ordering: an isNaN() prefix pins NaN to the greatest position (CH + // compares IEEE); toString() restores byte-wise text order for CH's + // text-backed columns and half-swapped UUIDs (the connector surfaces + // those AS their toString() text, so the orders match by construction); + // COLLATE "C" replaces postgres' locale collation with byte order. + string value_key = quoted; + string nan_key; + switch (config.dialect) { + case query::Dialect::ClickHouse: + switch (key_type.id()) { + case LogicalTypeId::FLOAT: + case LogicalTypeId::DOUBLE: + nan_key = "isNaN(" + quoted + ")" + dir + ", "; + break; + case LogicalTypeId::VARCHAR: + case LogicalTypeId::UUID: + value_key = "toString(" + quoted + ")"; + break; + case LogicalTypeId::STRUCT: + case LogicalTypeId::LIST: + case LogicalTypeId::ARRAY: + case LogicalTypeId::MAP: + if (CompoundContainsDivergent(key_type)) { + return std::string(); + } + break; + default: + break; } - } else { - if (null_order == OrderByNullType::NULLS_FIRST) { - fragments.push_back(col_name + " IS NOT NULL, " + col_name + " DESC"); - } else { - fragments.push_back(col_name + " DESC"); + break; + case query::Dialect::Postgres: + if (key_type.id() == LogicalTypeId::VARCHAR) { + value_key = quoted + " COLLATE \"C\""; } + break; } + + fragments.push_back(quoted + null_key + nan_key + value_key + dir); } return " ORDER BY " + StringUtil::Join(fragments, ", "); } @@ -170,12 +227,15 @@ static void PruneProjectionLayer(LogicalProjection &proj, const unordered_set &projection_map) { + const vector &projection_map) { if (child.type == LogicalOperatorType::LOGICAL_GET) { - auto &column_ids = get.GetColumnIds(); + // projection_map entries are positions in the child's OUTPUT; resolve them + // through the bindings (which honor projection_ids) instead of indexing + // column_ids positionally -- the two spaces differ on non-identity scans. + auto bindings = get.GetColumnBindings(); vector new_ids; - for (auto idx : projection_map) { - new_ids.push_back(column_ids[idx]); + for (auto pos : projection_map) { + new_ids.push_back(get.GetColumnIndex(bindings[pos])); } get.SetColumnIds(std::move(new_ids)); get.projection_ids.clear(); @@ -256,7 +316,8 @@ void OrderByAndLimitOptimizer::Optimize(const OrderByAndLimitOptimizer::Config & auto &topn = op->Cast(); LogicalGet *get = nullptr; dbconnector::BindData *bind_data = nullptr; - if (OptimizerUtil::FindExtensionGet(config.table_scan_name, *op->children[0], get, bind_data)) { + if (OptimizerUtil::FindExtensionGet(config.table_scan_name, *op->children[0], get, bind_data) && + !LimitFoldUnsafe(config, *get)) { string order_clause = TryBuildOrderByClause(config, topn.orders, *op->children[0], *get); if (!order_clause.empty()) { auto &order_by_and_limit_bind_data = bind_data->GetOrderByAndLimitBindData(); @@ -284,14 +345,7 @@ void OrderByAndLimitOptimizer::Optimize(const OrderByAndLimitOptimizer::Config & auto &order_by_and_limit_bind_data = bind_data->GetOrderByAndLimitBindData(); order_by_and_limit_bind_data.order_by_clause = order_clause; if (!order.projection_map.empty()) { - vector indices; - indices.reserve(order.projection_map.size()); - for (auto proj_idx : order.projection_map) { - ColumnIndex col_idx = get->GetColumnIndex(proj_idx); - column_t table_col_idx = col_idx.GetPrimaryIndex(); - indices.emplace_back(table_col_idx); - } - PruneColumnsAfterOrderByRemoval(*op->children[0], *get, indices); + PruneColumnsAfterOrderByRemoval(*op->children[0], *get, order.projection_map); } op = std::move(op->children[0]); return; @@ -304,15 +358,10 @@ void OrderByAndLimitOptimizer::Optimize(const OrderByAndLimitOptimizer::Config & } if (op->type == LogicalOperatorType::LOGICAL_LIMIT) { auto &limit = op->Cast(); - reference child = *op->children[0]; - while (child.get().type == LogicalOperatorType::LOGICAL_PROJECTION) { - child = *child.get().children[0]; - } - if (child.get().type != LogicalOperatorType::LOGICAL_GET) { - return; - } - auto &get = child.get().Cast(); - if (get.function.name != config.table_scan_name) { + LogicalGet *get = nullptr; + dbconnector::BindData *bind_data = nullptr; + if (!OptimizerUtil::FindExtensionGet(config.table_scan_name, *op->children[0], get, bind_data) || + LimitFoldUnsafe(config, *get)) { return; } switch (limit.limit_val.Type()) { @@ -329,8 +378,7 @@ void OrderByAndLimitOptimizer::Optimize(const OrderByAndLimitOptimizer::Config & default: return; } - auto &bind_data = get.bind_data->Cast(); - auto &order_by_and_limit_bind_data = bind_data.GetOrderByAndLimitBindData(); + auto &order_by_and_limit_bind_data = bind_data->GetOrderByAndLimitBindData(); if (!order_by_and_limit_bind_data.limit_clause.empty()) { return; } diff --git a/src/query/query_writer.cpp b/src/query/query_writer.cpp index b2d9d1d..056a8ed 100644 --- a/src/query/query_writer.cpp +++ b/src/query/query_writer.cpp @@ -7,12 +7,13 @@ namespace query { QueryWriter::Config QueryWriter::CreateConfig(char quote, QuoteEscapeStyle escape_style, const std::string &blob_literal_prefix, - const std::string &blob_literal_suffix) { + const std::string &blob_literal_suffix, Dialect dialect) { Config res; res.quote = quote; res.escape_style = escape_style; res.blob_literal_prefix = blob_literal_prefix; res.blob_literal_suffix = blob_literal_suffix; + res.dialect = dialect; return res; } @@ -60,16 +61,45 @@ std::string QueryWriter::EncodeBlob(const QueryWriter::Config &config, const std } std::string QueryWriter::WriteConstant(const QueryWriter::Config &config, const duckdb::Value &val) { + // Every typed branch below assumes a non-NULL payload (StringValue::Get + // throws InternalException on NULL; ToString renders the word NULL inside + // a typed cast). + if (val.IsNull()) { + return "NULL"; + } + // ClickHouse parses a bare literal wider than (U)Int64 as Float64, losing + // precision; render (U)HugeInt and Decimal via exact typed casts instead. + if (config.dialect == Dialect::ClickHouse) { + switch (val.type().id()) { + case duckdb::LogicalTypeId::HUGEINT: + return "toInt128('" + val.ToString() + "')"; + case duckdb::LogicalTypeId::UHUGEINT: + return "toUInt128('" + val.ToString() + "')"; + case duckdb::LogicalTypeId::DECIMAL: + return "toDecimal128('" + val.ToString() + "', " + + std::to_string(duckdb::DecimalType::GetScale(val.type())) + ")"; + default: + break; + } + } if (val.type().IsNumeric() || val.type().id() == duckdb::LogicalTypeId::BOOLEAN) { return val.ToSQLString(); } if (val.type().id() == duckdb::LogicalTypeId::BLOB) { return EncodeBlob(config, duckdb::StringValue::Get(val)); } - if (val.type().id() == duckdb::LogicalTypeId::TIMESTAMP_TZ) { - return val.DefaultCastAs(duckdb::LogicalType::TIMESTAMP) - .DefaultCastAs(duckdb::LogicalType::VARCHAR) - .ToSQLString(); + // TIMESTAMP_TZ deliberately falls through to the VARCHAR cast below: it + // renders WITH the UTC offset ('... 12:00:00+00'), which the remote parses + // as the correct instant regardless of its session time zone. Casting to + // naive TIMESTAMP first (the old behaviour) dropped the offset, so a + // non-UTC remote session re-interpreted the wall time in its own zone -- + // silently shifted comparisons. + // A plain string constant: quote + escape per the config's escape_style so the + // literal is valid in the target dialect (ClickHouse treats backslash as an + // escape inside '...', so its BACKSLASH style escapes both ' and \; postgres' + // DOUBLE_QUOTE style doubles ' and leaves \ literal -- matching ToSQLString). + if (val.type().id() == duckdb::LogicalTypeId::VARCHAR) { + return WriteQuotedAndEscaped(config, duckdb::StringValue::Get(val)); } return val.DefaultCastAs(duckdb::LogicalType::VARCHAR).ToSQLString(); } diff --git a/src/table_scan/filter_pushdown.cpp b/src/table_scan/filter_pushdown.cpp index 563b3cc..5725991 100644 --- a/src/table_scan/filter_pushdown.cpp +++ b/src/table_scan/filter_pushdown.cpp @@ -6,6 +6,7 @@ #include "duckdb/planner/expression/bound_constant_expression.hpp" #include "duckdb/planner/expression/bound_function_expression.hpp" #include "duckdb/planner/expression/bound_operator_expression.hpp" +#include "duckdb/planner/filter/expression_filter.hpp" #include "duckdb/planner/filter/table_filter_functions.hpp" #include "duckdb/common/enum_util.hpp" #include "duckdb/common/string_util.hpp" @@ -21,15 +22,14 @@ namespace table_scan { using namespace duckdb; FilterPushdown::Config FilterPushdown::CreateConfig(char identifier_quote, char constant_quote, - query::QuoteEscapeStyle escape_style, + query::QuoteEscapeStyle escape_style, query::Dialect dialect, const std::string &blob_literal_prefix, const std::string &blob_literal_suffix) { Config res; - res.identifier_quote = identifier_quote; - res.constant_quote = constant_quote; - res.escape_style = escape_style; - res.blob_literal_prefix = blob_literal_prefix; - res.blob_literal_suffix = blob_literal_suffix; + res.identifier = + query::QueryWriter::CreateConfig(identifier_quote, escape_style, std::string(), std::string(), dialect); + res.constant = + query::QueryWriter::CreateConfig(constant_quote, escape_style, blob_literal_prefix, blob_literal_suffix, dialect); return res; } @@ -38,11 +38,20 @@ std::string FilterPushdown::CreateExpression(const query::QueryWriter::Config &i const std::string &column_name, const vector> &filters, const std::string &op, column_t column_id) { + const bool is_or = op == "OR"; vector filter_entries; for (auto &filter : filters) { auto new_filter = TransformExpression(identifier_config, constant_config, column_name, *filter, column_id); if (new_filter.empty()) { - continue; + // An optional (advisory) filter wrapper may be dropped from an AND: it + // widens the SQL, never the result (the real predicate is enforced above + // the scan). Dropping anything from an OR narrows the result, and dropping + // a real AND conjunct widens the SQL past the filter -- both make the SQL + // lie, so the whole filter renders empty and stays local. + if (!is_or && ExpressionFilter::IsRootOptionalExpression(*filter)) { + continue; + } + return std::string(); } filter_entries.push_back(std::move(new_filter)); } @@ -84,15 +93,18 @@ static bool IsDirectReference(const Expression &expr) { string FilterPushdown::TransformConstantFilter(const query::QueryWriter::Config &constant_config, const string &column_name, ExpressionType comparison_type, const Value &constant, column_t column_id) { - string constant_string; if (IsVirtualColumn(column_id)) { - return "FALSE"; - } else { - constant_string = query::QueryWriter::WriteConstant(constant_config, constant); + // A rowid has no remote SQL identity; rendering anything (the old FALSE) + // makes the SQL narrower than the filter. Unrenderable -> stays local. + return string(); } + string constant_string = query::QueryWriter::WriteConstant(constant_config, constant); auto operator_string = TransformComparison(comparison_type); string comparison = StringUtil::Format("%s %s %s", column_name, operator_string, constant_string); - if (constant.type().id() == LogicalTypeId::VARCHAR) { + // Postgres forces byte-wise comparison to match DuckDB; ClickHouse's String + // comparison is already byte-wise and rejects the COLLATE clause. + if (constant.type().id() == LogicalTypeId::VARCHAR && + constant_config.dialect == query::Dialect::Postgres) { comparison += " COLLATE \"C\""; } return comparison; @@ -118,8 +130,14 @@ string FilterPushdown::TransformExpressionSubject(const query::QueryWriter::Conf if (struct_type.id() != LogicalTypeId::STRUCT || StructType::IsUnnamed(struct_type)) { return string(); } - auto child_name = query::QueryWriter::WriteQuotedAndEscaped( - identifier_config, StructType::GetChildName(struct_type, child_idx).GetIdentifierName()); + auto field = StructType::GetChildName(struct_type, child_idx).GetIdentifierName(); + if (identifier_config.dialect == query::Dialect::ClickHouse) { + // ClickHouse addresses a Tuple field as tupleElement(col, 'name'). + auto constant_config = query::QueryWriter::CreateConfig('\'', identifier_config.escape_style); + return "tupleElement(" + parent_name + ", " + + query::QueryWriter::WriteQuotedAndEscaped(constant_config, field) + ")"; + } + auto child_name = query::QueryWriter::WriteQuotedAndEscaped(identifier_config, field); return "(" + parent_name + ")." + child_name; } default: @@ -184,7 +202,7 @@ std::string FilterPushdown::TransformExpression(const query::QueryWriter::Config } return std::string(); case ExpressionType::COMPARE_IN: { - if (subject.empty()) { + if (subject.empty() || IsVirtualColumn(column_id)) { return string(); } std::string in_list; @@ -195,14 +213,10 @@ std::string FilterPushdown::TransformExpression(const query::QueryWriter::Config if (!in_list.empty()) { in_list += ", "; } - if (IsVirtualColumn(column_id)) { - in_list += "FALSE"; - } else { - in_list += query::QueryWriter::WriteConstant( - identifier_config, op.GetChildren()[i]->Cast().GetValue()); - } + in_list += query::QueryWriter::WriteConstant( + constant_config, op.GetChildren()[i]->Cast().GetValue()); } - return IsVirtualColumn(column_id) ? "FALSE" : subject + " IN (" + in_list + ")"; + return subject + " IN (" + in_list + ")"; } default: return std::string(); @@ -210,22 +224,17 @@ std::string FilterPushdown::TransformExpression(const query::QueryWriter::Config } case ExpressionClass::BOUND_FUNCTION: { auto &func = expr.Cast(); - if (func.Function().GetName() == OptionalFilterScalarFun::NAME && func.BindInfo()) { - auto &data = func.BindInfo()->Cast(); - return data.child_filter_expr ? TransformExpression(identifier_config, constant_config, column_name, - *data.child_filter_expr, column_id) - : std::string(); - } - if (func.Function().GetName() == SelectivityOptionalFilterScalarFun::NAME && func.BindInfo()) { - auto &data = func.BindInfo()->Cast(); - return data.child_filter_expr ? TransformExpression(identifier_config, constant_config, column_name, - *data.child_filter_expr, column_id) - : std::string(); + auto &name = func.Function().GetName(); + optional_ptr child; + if (func.BindInfo() && name == OptionalFilterScalarFun::NAME) { + child = func.BindInfo()->Cast().child_filter_expr.get(); + } else if (func.BindInfo() && name == SelectivityOptionalFilterScalarFun::NAME) { + child = func.BindInfo()->Cast().child_filter_expr.get(); } - if (func.Function().GetName() == DynamicFilterScalarFun::NAME) { + if (!child) { return std::string(); } - return std::string(); + return TransformExpression(identifier_config, constant_config, column_name, *child, column_id); } default: return std::string(); @@ -234,12 +243,9 @@ std::string FilterPushdown::TransformExpression(const query::QueryWriter::Config std::string FilterPushdown::TransformFilter(const FilterPushdown::Config &config, const std::string &column_name, const TableFilter &filter, column_t column_id) { - auto identifier_config = query::QueryWriter::CreateConfig(config.identifier_quote, config.escape_style); - auto constant_config = query::QueryWriter::CreateConfig(config.constant_quote, config.escape_style, - config.blob_literal_prefix, config.blob_literal_suffix); - std::string column_name_quoted = query::QueryWriter::WriteQuotedAndEscaped(identifier_config, column_name); + std::string column_name_quoted = query::QueryWriter::WriteQuotedAndEscaped(config.identifier, column_name); auto &expr = FilterUtil::GetExpression(filter, "FilterPushdown::TransformFilter"); - return TransformExpression(identifier_config, constant_config, column_name_quoted, expr, column_id); + return TransformExpression(config.identifier, config.constant, column_name_quoted, expr, column_id); } } // namespace table_scan