Skip to content

Commit 4053c9a

Browse files
Fix describe (#546)
2 parents 5829acc + c1f4763 commit 4053c9a

4 files changed

Lines changed: 63 additions & 32 deletions

File tree

external/duckdb

Submodule duckdb updated 1135 files

src/pyconnection.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#include "duckdb_python/pyconnection/pyconnection.hpp"
22

3+
#include "duckdb/catalog/catalog.hpp"
34
#include "duckdb/common/arrow/arrow.hpp"
45
#include "duckdb/common/types.hpp"
56
#include "duckdb/common/types/vector.hpp"

src/pyrelation.cpp

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -287,10 +287,12 @@ struct DescribeAggregateInfo {
287287
bool numeric_only;
288288
};
289289

290-
vector<string> CreateExpressionList(const vector<ColumnDefinition> &columns,
291-
const vector<DescribeAggregateInfo> &aggregates) {
292-
vector<string> expressions;
293-
expressions.reserve(columns.size());
290+
// Build the Describe query as two levels. The inner single-group aggregate computes one
291+
// scalar per (column, aggregate); the outer projection pivots them into rows via parallel
292+
// UNNESTs. This avoids GROUP BY ALL, which core rejects for UNNEST/UNLIST expressions.
293+
void CreateDescribeExpressions(const vector<ColumnDefinition> &columns, const vector<DescribeAggregateInfo> &aggregates,
294+
vector<string> &inner_aggregates, vector<string> &outer_projection) {
295+
outer_projection.reserve(columns.size() + 1);
294296

295297
string aggr_names = "UNNEST([";
296298
for (idx_t i = 0; i < aggregates.size(); i++) {
@@ -303,33 +305,35 @@ vector<string> CreateExpressionList(const vector<ColumnDefinition> &columns,
303305
}
304306
aggr_names += "])";
305307
aggr_names += " AS aggr";
306-
expressions.push_back(aggr_names);
308+
outer_projection.push_back(aggr_names);
309+
307310
for (idx_t c = 0; c < columns.size(); c++) {
308311
auto &col = columns[c];
309-
string expr = "UNNEST([";
312+
bool numeric = col.GetType().IsNumeric();
313+
string unnest = "UNNEST([";
310314
for (idx_t i = 0; i < aggregates.size(); i++) {
315+
auto alias = "__describe_" + std::to_string(c) + "_" + std::to_string(i);
311316
if (i > 0) {
312-
expr += ", ";
313-
}
314-
if (aggregates[i].numeric_only && !col.GetType().IsNumeric()) {
315-
expr += "NULL";
316-
continue;
317+
unnest += ", ";
317318
}
318-
expr += aggregates[i].name;
319-
expr += "(";
320-
expr += SQLIdentifier(col.GetName());
321-
expr += ")";
322-
if (col.GetType().IsNumeric()) {
323-
expr += "::DOUBLE";
319+
unnest += SQLIdentifier(alias);
320+
string stat;
321+
if (aggregates[i].numeric_only && !numeric) {
322+
stat = "NULL";
324323
} else {
325-
expr += "::VARCHAR";
324+
stat = aggregates[i].name;
325+
stat += "(";
326+
stat += SQLIdentifier(col.GetName());
327+
stat += ")";
328+
stat += numeric ? "::DOUBLE" : "::VARCHAR";
326329
}
330+
stat += " AS " + SQLIdentifier(alias);
331+
inner_aggregates.push_back(stat);
327332
}
328-
expr += "])";
329-
expr += " AS " + SQLIdentifier(col.GetName());
330-
expressions.push_back(expr);
333+
unnest += "])";
334+
unnest += " AS " + SQLIdentifier(col.GetName());
335+
outer_projection.push_back(unnest);
331336
}
332-
return expressions;
333337
}
334338

335339
std::unique_ptr<DuckDBPyRelation> DuckDBPyRelation::Describe() {
@@ -338,8 +342,11 @@ std::unique_ptr<DuckDBPyRelation> DuckDBPyRelation::Describe() {
338342
aggregates = {DescribeAggregateInfo("count"), DescribeAggregateInfo("mean", true),
339343
DescribeAggregateInfo("stddev", true), DescribeAggregateInfo("min"),
340344
DescribeAggregateInfo("max"), DescribeAggregateInfo("median", true)};
341-
auto expressions = CreateExpressionList(columns, aggregates);
342-
return DeriveRelation(rel->Aggregate(expressions));
345+
vector<string> inner_aggregates;
346+
vector<string> outer_projection;
347+
CreateDescribeExpressions(columns, aggregates, inner_aggregates, outer_projection);
348+
auto stats = rel->Aggregate(inner_aggregates);
349+
return DeriveRelation(stats->Project(outer_projection));
343350
}
344351

345352
string DuckDBPyRelation::ToSQL() {

tests/fast/arrow/test_filter_pushdown.py

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -747,35 +747,58 @@ def test_2145_parquet_glob_through_arrow(self, duckdb_cursor, tmp_path):
747747
# ===========================================================================
748748

749749

750+
# pyarrow view type filter pushdown canaries.
751+
# No released pyarrow fully executes view type filters (compare plus array_filter/take); checked
752+
# against 25.0.0. These stay xfail until one does. They run .to_table() so scanner construction
753+
# alone does not flip them: pyarrow accepts a binary_view predicate at build time but still fails
754+
# inside array_filter at execution. When one XPASSes: bump its version below, enable view type
755+
# filter pushdown, and drop the matching TestUnsupportedTypes fallback. See the view type filter
756+
# pushdown tracking issue.
757+
_PA_VERSION = Version(pa.__version__)
758+
_PYARROW_STRING_VIEW_FILTER_VERSION = Version("99")
759+
_PYARROW_BINARY_VIEW_FILTER_VERSION = Version("99")
760+
761+
750762
class TestCanaries:
751763
"""Markers for behaviours we expect to change upstream eventually."""
752764

753765
# ----- pyarrow capabilities ----------------------------------------
754766

755767
@pytest.mark.xfail(
768+
_PA_VERSION < _PYARROW_STRING_VIEW_FILTER_VERSION,
756769
raises=pa_lib.ArrowNotImplementedError,
757-
reason="pyarrow does not yet implement string_view filter compare kernels",
770+
reason="pyarrow does not execute string_view filters fully (equal kernel)",
758771
strict=True,
759772
)
760773
def test_pyarrow_gains_string_view_filter_support(self):
761-
"""When pyarrow adds string_view comparison kernels this will xpass.
774+
"""XPASSes when pyarrow executes a string_view filter fully.
762775
763-
At that point we should remove the post-scan fallback in TestUnsupportedTypes.
776+
Runs .to_table() so it reflects execution, not just scanner construction. When it
777+
XPASSes: bump the version, enable view type pushdown, drop the TestUnsupportedTypes
778+
fallback.
764779
"""
765780
filter_expr = pa_ds.field("col") == pa_ds.scalar("val1")
766781
table = pa.table({"col": pa.array(["val1", "val2"], type=pa.string_view())})
767-
pa_ds.dataset(table).scanner(columns=["col"], filter=filter_expr)
782+
result = pa_ds.dataset(table).scanner(columns=["col"], filter=filter_expr).to_table()
783+
assert result.num_rows == 1
768784

769785
@pytest.mark.xfail(
786+
_PA_VERSION < _PYARROW_BINARY_VIEW_FILTER_VERSION,
770787
raises=pa_lib.ArrowNotImplementedError,
771-
reason="pyarrow does not yet implement binary_view filter compare kernels",
788+
reason="pyarrow does not execute binary_view filters fully (array_filter kernel)",
772789
strict=True,
773790
)
774791
def test_pyarrow_gains_binary_view_filter_support(self):
775-
"""When pyarrow adds binary_view comparison kernels this will xpass."""
792+
"""XPASSes when pyarrow executes a binary_view filter fully.
793+
794+
Runs .to_table() so it reflects execution, not just scanner construction. When it
795+
XPASSes: bump the version, enable view type pushdown, drop the TestUnsupportedTypes
796+
fallback.
797+
"""
776798
filter_expr = pa_ds.field("col") == pa_ds.scalar(pa.scalar(b"bin1", pa.binary_view()))
777799
table = pa.table({"col": pa.array([b"bin1", b"bin2"], type=pa.binary_view())})
778-
pa_ds.dataset(table).scanner(columns=["col"], filter=filter_expr)
800+
result = pa_ds.dataset(table).scanner(columns=["col"], filter=filter_expr).to_table()
801+
assert result.num_rows == 1
779802

780803
# ----- DuckDB optimizer decisions we expect to change --------------
781804

0 commit comments

Comments
 (0)