From bbec3354bdee247f8e643aa6b15abfaff60d0bb6 Mon Sep 17 00:00:00 2001 From: Patrick Ribbsaeter Date: Sun, 4 Oct 2026 19:34:32 +0200 Subject: [PATCH] Fix #26362: Null dereference in PIVOT IN list with zero-argument row() In TransformPivotInList (src/parser/peg/transformer/transform_select.cpp), when row() was passed with zero arguments (an empty tuple), the argument iteration loop did not run and the function returned true. As a consequence, TransformPivotTargetList treated the entry as fully transformed into values and skipped setting pivot_entry.expr = std::move(target). This left pivot_entry with an empty values vector and a null expr. When Binder::BindPivot subsequently processed pivot_entry.values.empty(), it forwarded pivot_entry.expr to BindPivotInList, which immediately dereferenced expr->GetExpressionType(), triggering an internal assertion crash (INTERNAL Error: Attempted to dereference unique_ptr that is NULL!). This fix: 1. In TransformPivotInList, returns false if function.FunctionName() != 'row' || function.GetArgumentsMutable().empty(), ensuring a zero-argument row() is preserved as an expression rather than falsely reporting success without values. 2. In TransformPivotTargetList, ensures pivot_entry.expr is retained if !transformed || pivot_entry.values.empty(), upholding the invariant that entries with empty values always preserve their parsed expression. 3. In TryExtractUnpivotList, mirrors the empty argument check for consistency. 4. In Binder::BindPivot, adds a defensive check asserting pivot_entry.expr != nullptr before calling BindPivotInList. 5. Adds regression tests in test/sql/pivot/pivot_errors.test covering row() and nested row(row()) in PIVOT IN clauses. --- src/parser/peg/transformer/transform_select.cpp | 4 ++-- src/planner/binder/tableref/bind_pivot.cpp | 5 ++++- test/sql/pivot/pivot_errors.test | 11 +++++++++++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/parser/peg/transformer/transform_select.cpp b/src/parser/peg/transformer/transform_select.cpp index fb00b65f..bbdfc2d8 100644 --- a/src/parser/peg/transformer/transform_select.cpp +++ b/src/parser/peg/transformer/transform_select.cpp @@ -479,7 +479,7 @@ bool PEGTransformerFactory::TransformPivotInList(unique_ptr &e } case ExpressionType::FUNCTION: { auto &function = expr->Cast(); - if (function.FunctionName() != "row") { + if (function.FunctionName() != "row" || function.GetArgumentsMutable().empty()) { return false; } for (auto &child : function.GetArgumentsMutable()) { @@ -1465,7 +1465,7 @@ PEGTransformerFactory::TransformPivotTargetList(PEGTransformer &transformer, PivotColumnEntry pivot_entry; pivot_entry.alias = target->GetAlias(); bool transformed = TransformPivotInList(target, pivot_entry); - if (!transformed) { + if (!transformed || pivot_entry.values.empty()) { pivot_entry.expr = std::move(target); } result.push_back(std::move(pivot_entry)); diff --git a/src/planner/binder/tableref/bind_pivot.cpp b/src/planner/binder/tableref/bind_pivot.cpp index 1cc7997c..151cf742 100644 --- a/src/planner/binder/tableref/bind_pivot.cpp +++ b/src/planner/binder/tableref/bind_pivot.cpp @@ -541,7 +541,7 @@ static bool TryExtractUnpivotList(ParsedExpression &expr, vector &co } case ExpressionType::FUNCTION: { auto &function = expr.Cast(); - if (function.FunctionName() != "row") { + if (function.FunctionName() != "row" || function.GetArgumentsMutable().empty()) { return false; } for (auto &child : function.GetArgumentsMutable()) { @@ -618,6 +618,9 @@ unique_ptr Binder::BindPivot(PivotRef &ref, vector