From d8a8d3281974da821c61cc37ea0037292b518086 Mon Sep 17 00:00:00 2001 From: Nico Ritschel Date: Wed, 21 Oct 2026 07:23:52 -0700 Subject: [PATCH 1/2] Prepare Yardstick for DuckDB 2.0 --- .../workflows/MainDistributionPipeline.yml | 15 ++-- CMakeLists.txt | 8 ++ README.md | 2 +- duckdb | 2 +- extension-ci-tools | 2 +- src/aggregate_decorations.cpp | 3 +- src/aggregate_state.cpp | 3 +- src/frontend_peg.cpp | 83 +++++++++++++------ src/include/frontend_peg.hpp | 1 + src/include/yardstick_compat.hpp.in | 1 + src/include/yardstick_parser_compat.hpp | 26 ++++++ src/measure_windows.cpp | 3 +- src/yardstick_extension.cpp | 7 +- src/yardstick_parser_ffi.cpp | 27 +++--- test/extension/yardstick_test_grammar.cpp | 8 +- test/sql/measures.test | 15 ++++ test/sql/native_aggregate_decorations.test | 7 -- test/sql/native_aggregate_detection.test | 7 -- test/sql/native_aggregate_errors.test | 8 +- test/sql/native_aggregate_statements.test | 7 -- test/sql/native_aggregate_windows.test | 10 --- test/sql/native_combined_grammar.test | 6 -- test/sql/native_current.test | 12 +-- test/sql/native_implicit_measures.test | 7 -- test/sql/native_measure_declarations.test | 7 -- test/sql/native_mixed_aggregate.test | 6 -- test/sql/native_modifier_dimensions.test | 6 -- test/sql/native_parser_settings.test | 54 ++++++++++++ test/sql/native_peg.test | 10 +-- test/sql/native_query_scopes.test | 7 -- test/sql/native_star_view_aliases.test | 7 -- test/sql/native_subquery_dimensions.test | 7 -- test/sql/native_view_column_aliases.test | 7 -- yardstick-rs/src/sql/measures.rs | 8 +- 34 files changed, 214 insertions(+), 175 deletions(-) create mode 100644 src/include/yardstick_parser_compat.hpp create mode 100644 test/sql/native_parser_settings.test diff --git a/.github/workflows/MainDistributionPipeline.yml b/.github/workflows/MainDistributionPipeline.yml index ca5963c..8a5afa6 100644 --- a/.github/workflows/MainDistributionPipeline.yml +++ b/.github/workflows/MainDistributionPipeline.yml @@ -1,6 +1,6 @@ # # Build and test the extension using the DuckDB reusable CI workflow. -# Require the stable release and the upcoming release branch to build and test. +# Require the legacy release and the pinned DuckDB 2.0 release to build and test. # DuckDB main is checked separately by the scheduled canary workflow. # name: Main Extension Distribution Pipeline @@ -26,13 +26,16 @@ jobs: extra_toolchains: rust exclude_archs: "wasm_mvp;wasm_eh;wasm_threads;linux_amd64_gcc4;windows_amd64_rtools;windows_amd64_mingw" - duckdb-next-build: - name: Build DuckDB v2.0-cyanoptera release branch extension binaries - uses: duckdb/extension-ci-tools/.github/workflows/_extension_distribution.yml@main + duckdb-release-build: + name: Build DuckDB v2.0.0 extension binaries + uses: duckdb/extension-ci-tools/.github/workflows/_extension_distribution.yml@fe955ed3a046b0324cac2c691e73d8d776a7d5cc with: - duckdb_version: v2.0-cyanoptera - ci_tools_version: main + duckdb_version: v2.0.0 + duckdb_commit: d54cbbe3df17e8e5ce68312f483b4155836112aa + ci_tools_version: fe955ed3a046b0324cac2c691e73d8d776a7d5cc test_config: '{"test_env_variables":{"YARDSTICK_NATIVE_PEG":"1"}}' extension_name: yardstick extra_toolchains: rust exclude_archs: "wasm_mvp;wasm_eh;wasm_threads;linux_amd64_gcc4;windows_amd64_rtools;windows_amd64_mingw" + opt_in_archs: osx_amd64 + reduced_ci_mode: disabled diff --git a/CMakeLists.txt b/CMakeLists.txt index 9cc6b47..334c8a1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,6 +30,7 @@ function(yardstick_detect_apis) unset(YARDSTICK_NEW_EXPR_API CACHE) unset(YARDSTICK_RESULT_ACCESSORS CACHE) unset(YARDSTICK_GRAMMAR_EXTENSION CACHE) + unset(YARDSTICK_PARSER_INSTANCE_API CACHE) check_cxx_source_compiles(" #include #include @@ -45,6 +46,13 @@ function(yardstick_detect_apis) #include void probe(duckdb::QueryResult &result) { result.GetTypes(); } " YARDSTICK_RESULT_ACCESSORS) + check_cxx_source_compiles(" + #include + void probe() { + duckdb::Parser parser(duckdb::ParserOptions::Builtin()); + parser.ParseExpressionList(\"1\"); + } + " YARDSTICK_PARSER_INSTANCE_API) check_cxx_source_compiles(" #include #include diff --git a/README.md b/README.md index 2089aee..d3a4992 100644 --- a/README.md +++ b/README.md @@ -194,7 +194,7 @@ This is a staged frontend migration: forms the native grammar cannot parse retai For the combined-grammar regression suite, configure with `-DYARDSTICK_BUILD_GRAMMAR_TEST_EXTENSION=ON`, build `yardstick_test_grammar_loadable_extension`, and run the SQL tests with `YARDSTICK_NATIVE_PEG=1` and `YARDSTICK_GRAMMAR_TEST_EXTENSION` set to the fixture extension's absolute path. The fixture is excluded from normal builds. -Supported build targets are DuckDB **v1.5.5** and the upcoming **v2.0-cyanoptera release branch**. The latter is a moving branch, not a released version. CI requires both targets to build and pass tests; DuckDB `main` runs separately as a scheduled, advisory canary. +Supported build targets are DuckDB **v2.0.0** (pinned to `d54cbbe3df17e8e5ce68312f483b4155836112aa`) and legacy **v1.5.5**. The submodule and release CI use that exact 2.0 engine commit. CI requires both targets to build and pass tests; DuckDB `main` runs separately as a scheduled, advisory canary. Prerequisites: - CMake 3.12+ diff --git a/duckdb b/duckdb index d8cdaa3..d54cbbe 160000 --- a/duckdb +++ b/duckdb @@ -1 +1 @@ -Subproject commit d8cdaa33fda8df955cc76ef58a280f68f4cd43fa +Subproject commit d54cbbe3df17e8e5ce68312f483b4155836112aa diff --git a/extension-ci-tools b/extension-ci-tools index 72e76e9..fe955ed 160000 --- a/extension-ci-tools +++ b/extension-ci-tools @@ -1 +1 @@ -Subproject commit 72e76e99cd7fee45a99739cd118ec2db64e034ec +Subproject commit fe955ed3a046b0324cac2c691e73d8d776a7d5cc diff --git a/src/aggregate_decorations.cpp b/src/aggregate_decorations.cpp index 086e676..212d16e 100644 --- a/src/aggregate_decorations.cpp +++ b/src/aggregate_decorations.cpp @@ -1,4 +1,5 @@ #include "aggregate_decorations.hpp" +#include "yardstick_parser_compat.hpp" #if YARDSTICK_GRAMMAR_EXTENSION #include "duckdb/catalog/catalog.hpp" @@ -32,7 +33,7 @@ namespace { using Names = std::unordered_set; unique_ptr ParseOne(const string &sql, const ParserOptions &options) { - auto expressions = Parser::ParseExpressionList(sql, options); + auto expressions = YardstickParseExpressionList(sql, options); if (expressions.size() != 1) { throw ParserException("Expected one expression in decorated AGGREGATE"); } diff --git a/src/aggregate_state.cpp b/src/aggregate_state.cpp index 7fa6a72..e4df5b2 100644 --- a/src/aggregate_state.cpp +++ b/src/aggregate_state.cpp @@ -1,4 +1,5 @@ #include "aggregate_state.hpp" +#include "yardstick_parser_compat.hpp" #if YARDSTICK_GRAMMAR_EXTENSION #include "duckdb/common/exception.hpp" @@ -102,7 +103,7 @@ class StateFormulaBinder : public ConstantBinder { }; unique_ptr ParseFormula(const string &formula) { - auto expressions = Parser::ParseExpressionList(formula); + auto expressions = YardstickParseExpressionList(formula); if (expressions.size() != 1 || !expressions[0]->GetAlias().empty()) { throw BinderException("yardstick_state requires exactly one scalar finalization expression"); } diff --git a/src/frontend_peg.cpp b/src/frontend_peg.cpp index 0eb4100..acb70ec 100644 --- a/src/frontend_peg.cpp +++ b/src/frontend_peg.cpp @@ -1,9 +1,11 @@ #include "frontend_peg.hpp" +#include "yardstick_parser_compat.hpp" #if YARDSTICK_GRAMMAR_EXTENSION #include "native_statement_traversal.hpp" #include "duckdb/main/client_context.hpp" #include "duckdb/main/database.hpp" +#include "duckdb/main/settings.hpp" #include "duckdb/parser/expression/function_expression.hpp" #include "duckdb/parser/expression/columnref_expression.hpp" #include "duckdb/parser/expression/constant_expression.hpp" @@ -296,7 +298,7 @@ void ReadModifier(PEGTransformer &transformer, ParseResult &result, vector TransformAt(PEGTransformer &transformer, ParseResult &result) { +arena_ptr TransformAt(PEGTransformer &transformer, ParseResult &result) { CaptureSpan(result, result); // BaseExpression inserts the left operand into this marker. Keep its // identity so aggregate traversal associates suffixes through AST parents. @@ -311,10 +313,10 @@ unique_ptr TransformAt(PEGTransformer &transformer, ParseR } active_capture->clauses.push_back(std::move(clause)); } - return make_uniq>>(std::move(expression)); + return transformer.MakeResult>(std::move(expression)); } -unique_ptr TransformMeasure(PEGTransformer &transformer, ParseResult &result) { +arena_ptr TransformMeasure(PEGTransformer &transformer, ParseResult &result) { auto &list = result.Cast(); vector> children; children.push_back(transformer.Transform>(list.GetChild(0))); @@ -333,14 +335,14 @@ unique_ptr TransformMeasure(PEGTransformer &transformer, P expression.get()}); } } - return make_uniq>>(std::move(expression)); + return transformer.MakeResult>(std::move(expression)); } -unique_ptr StartAt(PEGTransformer &transformer, ParseResult &result) { - return make_uniq(transformer, result, TransformAt); +arena_ptr StartAt(PEGTransformer &transformer, ParseResult &result) { + return transformer.MakeProcess(transformer, result, TransformAt); } -unique_ptr TransformCurrentDimension(PEGTransformer &transformer, ParseResult &result, +arena_ptr TransformCurrentDimension(PEGTransformer &transformer, ParseResult &result, idx_t dimension_index) { auto &list = result.Cast(); vector> children; @@ -350,25 +352,25 @@ unique_ptr TransformCurrentDimension(PEGTransformer &trans result.offset.GetIndex() + result.length.GetIndex()}); } unique_ptr expression = make_uniq("current", std::move(children)); - return make_uniq>>(std::move(expression)); + return transformer.MakeResult>(std::move(expression)); } -unique_ptr StartCurrentReference(PEGTransformer &transformer, ParseResult &result) { - return make_uniq(transformer, result, +arena_ptr StartCurrentReference(PEGTransformer &transformer, ParseResult &result) { + return transformer.MakeProcess(transformer, result, [](PEGTransformer &transformer, ParseResult &result) { return TransformCurrentDimension(transformer, result, 1); }); } -unique_ptr StartCurrentCall(PEGTransformer &transformer, ParseResult &result) { - return make_uniq(transformer, result, +arena_ptr StartCurrentCall(PEGTransformer &transformer, ParseResult &result) { + return transformer.MakeProcess(transformer, result, [](PEGTransformer &transformer, ParseResult &result) { return TransformCurrentDimension(transformer, result, 2); }); } -unique_ptr StartMeasure(PEGTransformer &transformer, ParseResult &result) { - return make_uniq(transformer, result, TransformMeasure); +arena_ptr StartMeasure(PEGTransformer &transformer, ParseResult &result) { + return transformer.MakeProcess(transformer, result, TransformMeasure); } bool ExpandDeclarationReferences(unique_ptr &expression, @@ -496,6 +498,24 @@ NativeYardstickParseScope::~NativeYardstickParseScope() { active_parse_scope = previous; } +ParserOptions YardstickContextParserOptions(ClientContext &context) { +#if YARDSTICK_PARSER_INSTANCE_API + // GetParserOptions is private in 2.0. Snapshot the same public settings for + // deferred binding; NativeYardstickParseScope disables override recursion. + auto options = ParserOptions::Builtin(); + options.identifier_case_mode = Settings::Get(context); + options.integer_division = Settings::Get(context); + options.regex_match_operator_semantics = Settings::Get(context); + options.max_expression_depth = Settings::Get(context); + options.parser_override_setting = Settings::Get(context); + options.compiled_grammar = CompiledGrammar::Get(context); + options.context = context; + return options; +#else + return context.GetParserOptions(); +#endif +} + const ParserOptions *CurrentNativeYardstickParserOptions() { return active_parse_scope ? &active_parse_scope->ParserConfig() : nullptr; } @@ -821,10 +841,20 @@ YardstickCreateViewInfo *FindNativeYardstickMeasures(const char *sql_p) { namespace { vector> QuerySyntaxChildren(ParseResult &result) { switch (result.type) { - case ParseResultType::LIST: - return result.Cast().GetChildren(); - case ParseResultType::REPEAT: - return result.Cast().GetChildren(); + case ParseResultType::LIST: { + vector> children; + for (auto &child : result.Cast().GetChildren()) { + children.push_back(child); + } + return children; + } + case ParseResultType::REPEAT: { + vector> children; + for (auto &child : result.Cast().GetChildren()) { + children.push_back(child); + } + return children; + } case ParseResultType::CHOICE: return {result.Cast().GetResult()}; case ParseResultType::OPTIONAL: @@ -861,11 +891,11 @@ bool CollectNativeQueryScopes(ParseResult &root, PEGTransformer &transformer, auto work = std::move(pending.back()); pending.pop_back(); auto &node = *work.node; - if (node.name == "YardstickMeasureAlias") { + if (node.Name() == "YardstickMeasureAlias") { return false; // Declaration registration owns this statement. } auto children = QuerySyntaxChildren(node); - if (node.type == ParseResultType::LIST && node.name == "SimpleSelect") { + if (node.type == ParseResultType::LIST && node.Name() == "SimpleSelect") { auto location = node.GetLocation(); if (!location.IsValid() || location.Start() == location.End()) { return false; @@ -884,7 +914,7 @@ bool CollectNativeQueryScopes(ParseResult &root, PEGTransformer &transformer, if (node.type == ParseResultType::LIST && !children.empty() && children[0].get().type == ParseResultType::OPTIONAL) { auto &optional_with = children[0].get().Cast(); - if (optional_with.HasResult() && optional_with.GetResult().name == "WithClause") { + if (optional_with.HasResult() && optional_with.GetResult().Name() == "WithClause") { auto &with = optional_with.GetResult().Cast(); bool recursive = with.Child(1).HasResult(); vector declarations; @@ -892,7 +922,7 @@ bool CollectNativeQueryScopes(ParseResult &root, PEGTransformer &transformer, while (!search.empty()) { auto *candidate = search.back(); search.pop_back(); - if (candidate->type == ParseResultType::LIST && candidate->name == "WithStatement") { + if (candidate->type == ParseResultType::LIST && candidate->Name() == "WithStatement") { declarations.push_back(candidate); continue; } @@ -922,7 +952,7 @@ bool CollectNativeQueryScopes(ParseResult &root, PEGTransformer &transformer, first_child = 1; } } - if (node.type == ParseResultType::LIST && node.name == "SelectStatementInternal") { + if (node.type == ParseResultType::LIST && node.Name() == "SelectStatementInternal") { work.main_query = node.Cast().GetChild(1).GetLocation(); work.modifier_end = node.GetLocation().End(); } @@ -954,11 +984,14 @@ YardstickQueryScopeList *FindNativeYardstickQueryScopes(const char *sql_p) { while (iterator.HasMoreStatements()) { vector suggestions; ParseResultAllocator results; - ParserPackratCache packrat; idx_t max_token = iterator.Position(); ArenaAllocator process_allocator(Allocator::DefaultAllocator()); + // Match processes rewind their arena. Cached parse results must + // outlive those frames, as in DuckDB's own top-level matcher. + ArenaAllocator packrat_allocator(Allocator::DefaultAllocator()); + ParserPackratCache packrat(packrat_allocator); MatchContext context(suggestions, results, process_allocator, max_token, MatchMode::BUILD_PARSE_RESULT, - options.identifier_case_mode, options.heap_based_parser, &packrat); + options.identifier_case_mode, &packrat); MatchState state(iterator, context); auto match = grammar.TopLevelStatementMatcher().MatchParseResult(state); if (!match.IsSuccess() || !match.HasParseResult() || state.token_iterator.Position() <= iterator.Position()) { diff --git a/src/include/frontend_peg.hpp b/src/include/frontend_peg.hpp index b7b3c13..18fd0f5 100644 --- a/src/include/frontend_peg.hpp +++ b/src/include/frontend_peg.hpp @@ -12,6 +12,7 @@ class Parser; #if YARDSTICK_GRAMMAR_EXTENSION shared_ptr RegisterYardstickGrammar(DatabaseInstance &db); +ParserOptions YardstickContextParserOptions(ClientContext &context); // Keep native parsing scoped to the override's database, active grammar, and // parser settings. Nested overrides restore the previous scope on return. diff --git a/src/include/yardstick_compat.hpp.in b/src/include/yardstick_compat.hpp.in index 5cce892..51e4543 100644 --- a/src/include/yardstick_compat.hpp.in +++ b/src/include/yardstick_compat.hpp.in @@ -5,3 +5,4 @@ #cmakedefine01 YARDSTICK_NEW_EXPR_API #cmakedefine01 YARDSTICK_RESULT_ACCESSORS #cmakedefine01 YARDSTICK_GRAMMAR_EXTENSION +#cmakedefine01 YARDSTICK_PARSER_INSTANCE_API diff --git a/src/include/yardstick_parser_compat.hpp b/src/include/yardstick_parser_compat.hpp new file mode 100644 index 0000000..d47ae37 --- /dev/null +++ b/src/include/yardstick_parser_compat.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include "yardstick_compat.hpp" +#include "duckdb/parser/parser.hpp" + +namespace duckdb { + +inline ParserOptions YardstickBuiltinParserOptions() { +#if YARDSTICK_PARSER_INSTANCE_API + return ParserOptions::Builtin(); +#else + return ParserOptions(); +#endif +} + +inline vector> YardstickParseExpressionList( + const string &sql, const ParserOptions &options = YardstickBuiltinParserOptions()) { +#if YARDSTICK_PARSER_INSTANCE_API + Parser parser(options); + return parser.ParseExpressionList(sql); +#else + return Parser::ParseExpressionList(sql, options); +#endif +} + +} // namespace duckdb diff --git a/src/measure_windows.cpp b/src/measure_windows.cpp index ddc92a1..53505a9 100644 --- a/src/measure_windows.cpp +++ b/src/measure_windows.cpp @@ -1,4 +1,5 @@ #include "measure_windows.hpp" +#include "yardstick_parser_compat.hpp" #if YARDSTICK_GRAMMAR_EXTENSION #include "frontend_peg.hpp" @@ -46,7 +47,7 @@ string Quote(const string &text) { } unique_ptr Expression(const string &sql, const ParserOptions &options) { - auto expressions = Parser::ParseExpressionList(sql, options); + auto expressions = YardstickParseExpressionList(sql, options); if (expressions.size() != 1) { throw ParserException("Expected one expression while rewriting a measure window"); } diff --git a/src/yardstick_extension.cpp b/src/yardstick_extension.cpp index 8de3f2b..92d2769 100644 --- a/src/yardstick_extension.cpp +++ b/src/yardstick_extension.cpp @@ -1,6 +1,7 @@ #define DUCKDB_EXTENSION_MAIN #include "yardstick_extension.hpp" +#include "yardstick_parser_compat.hpp" #include "yardstick_parser_extension.hpp" #include "frontend_peg.hpp" #include "aggregate_state.hpp" @@ -120,7 +121,7 @@ static ParserOptions YardstickParserOptions() { return *options; } #endif - return ParserOptions(); + return YardstickBuiltinParserOptions(); } static std::string RewritePercentileWithinGroup(const std::string &sql) { @@ -168,7 +169,7 @@ static unique_ptr YardstickQueryBind(ClientContext &context, #if YARDSTICK_GRAMMAR_EXTENSION NativeYardstickBindScope bind_scope(context); auto &info = input.info->Cast(); - NativeYardstickParseScope native_scope(info.parser_info.get(), context.GetParserOptions()); + NativeYardstickParseScope native_scope(info.parser_info.get(), YardstickContextParserOptions(context)); #endif auto data = make_uniq(); data->original_sql = input.inputs[0].GetValue(); @@ -2283,7 +2284,7 @@ static BoundStatement BindDeferredMeasureStatement(ClientContext &context, Binde static unique_ptr DeferredMeasureSelectBindReplace(ClientContext &context, TableFunctionBindInput &input) { auto &info = input.info->Cast(); NativeYardstickBindScope bind_scope(context); - NativeYardstickParseScope parse_scope(info.parser_info.get(), context.GetParserOptions()); + NativeYardstickParseScope parse_scope(info.parser_info.get(), YardstickContextParserOptions(context)); auto state = context.registered_state->GetOrCreate("yardstick_deferred_catalog"); DeferredTemporaryMeasureScope temporary_scope(*state); string sql = input.inputs[0].GetValue(); diff --git a/src/yardstick_parser_ffi.cpp b/src/yardstick_parser_ffi.cpp index 06d5d2b..c542c42 100644 --- a/src/yardstick_parser_ffi.cpp +++ b/src/yardstick_parser_ffi.cpp @@ -8,6 +8,7 @@ */ #include "yardstick_ffi.h" +#include "yardstick_parser_compat.hpp" #include "yardstick_compat.hpp" #include "frontend_peg.hpp" #include "aggregate_decorations.hpp" @@ -80,7 +81,7 @@ static ParserOptions YardstickParserOptions() { return *options; } #endif - return ParserOptions(); + return YardstickBuiltinParserOptions(); } // Identifier/string -> raw string (case preserved). The Identifier overload only @@ -1296,8 +1297,8 @@ extern "C" int32_t yardstick_current_where_is_single_valued(const char* predicat return -1; } try { - auto predicates = Parser::ParseExpressionList(predicate, *options); - auto dimensions = Parser::ParseExpressionList(dimension, *options); + auto predicates = YardstickParseExpressionList(predicate, *options); + auto dimensions = YardstickParseExpressionList(dimension, *options); if (predicates.size() != 1 || dimensions.size() != 1 || dimensions[0]->GetExpressionClass() != ExpressionClass::COLUMN_REF) { return 0; @@ -1488,7 +1489,7 @@ extern "C" char* yardstick_decorate_measure( vector cte_queries; for (size_t i = 0; i < binding_cte_count; i++) cte_queries.emplace_back(binding_ctes[i]); NativeYardstickCteBindScope binding_scope(cte_queries, options); - auto calls = Parser::ParseExpressionList(call_sql, options); + auto calls = YardstickParseExpressionList(call_sql, options); if (calls.size() != 1) throw ParserException("Expected one AGGREGATE call"); // Window FILTER selects input rows in the lineage stage. DISTINCT and // argument ordering apply to the aggregate leaves being recomputed. @@ -1517,7 +1518,7 @@ extern "C" char* yardstick_window_marker(const char* call_sql, const char* marke if (error) *error = nullptr; #if YARDSTICK_GRAMMAR_EXTENSION try { - auto expressions = Parser::ParseExpressionList(call_sql, YardstickParserOptions()); + auto expressions = YardstickParseExpressionList(call_sql, YardstickParserOptions()); if (expressions.size() != 1 || expressions[0]->GetExpressionClass() != ExpressionClass::WINDOW) { throw ParserException("Expected one windowed AGGREGATE call"); } @@ -2275,7 +2276,7 @@ extern "C" YardstickExpressionInfo* yardstick_parse_expression(const char* expr_ } try { - auto expressions = Parser::ParseExpressionList(expr_str, YardstickParserOptions()); + auto expressions = YardstickParseExpressionList(expr_str, YardstickParserOptions()); if (expressions.empty()) { result->error = safe_strdup("No expressions parsed"); @@ -2314,8 +2315,8 @@ extern "C" int32_t yardstick_expressions_equal(const char* left, const char* rig } try { auto options = YardstickParserOptions(); - auto left_expressions = Parser::ParseExpressionList(left, options); - auto right_expressions = Parser::ParseExpressionList(right, options); + auto left_expressions = YardstickParseExpressionList(left, options); + auto right_expressions = YardstickParseExpressionList(right, options); if (left_expressions.size() != 1 || right_expressions.size() != 1) { return 0; } @@ -2400,7 +2401,7 @@ class VisibleFilterRewriter { } void AddDimension(const char *name, const char *sql) { - auto expressions = Parser::ParseExpressionList(sql, options); + auto expressions = YardstickParseExpressionList(sql, options); if (expressions.size() != 1 || expressions[0]->HasSubquery()) { throw ParserException("Unsupported dimension in visible filter"); } @@ -2533,7 +2534,7 @@ extern "C" char* yardstick_rewrite_visible_filter(const char* expression, const auto *options = CurrentNativeYardstickParserOptions(); if (!options || !expression) return nullptr; try { - auto expressions = Parser::ParseExpressionList(expression, *options); + auto expressions = YardstickParseExpressionList(expression, *options); if (expressions.size() != 1) { throw ParserException("Expected one expression in visible filter"); } @@ -2717,12 +2718,12 @@ extern "C" char* yardstick_qualify_expression(const char* expr_str, const char* #if YARDSTICK_GRAMMAR_EXTENSION if (!CurrentNativeYardstickParserOptions()) return nullptr; auto options = YardstickParserOptions(); - auto dimensions = Parser::ParseExpressionList(dimension, options); + auto dimensions = YardstickParseExpressionList(dimension, options); if (dimensions.size() != 1 || dimensions[0]->GetExpressionClass() != ExpressionClass::COLUMN_REF) { return nullptr; } auto column = dimensions[0]->Cast().GetColumnName(); - auto qualified = Parser::ParseExpressionList(string(qualifier) + "." + dimension, options); + auto qualified = YardstickParseExpressionList(string(qualifier) + "." + dimension, options); if (qualified.size() != 1 || qualified[0]->GetExpressionClass() != ExpressionClass::COLUMN_REF) { return nullptr; } @@ -2746,7 +2747,7 @@ extern "C" char* yardstick_qualify_expression(const char* expr_str, const char* return nullptr; #endif } - auto expressions = Parser::ParseExpressionList(expr_str, YardstickParserOptions()); + auto expressions = YardstickParseExpressionList(expr_str, YardstickParserOptions()); if (expressions.empty()) { return safe_strdup(expr_str); } diff --git a/test/extension/yardstick_test_grammar.cpp b/test/extension/yardstick_test_grammar.cpp index e51b90f..1741d71 100644 --- a/test/extension/yardstick_test_grammar.cpp +++ b/test/extension/yardstick_test_grammar.cpp @@ -8,16 +8,16 @@ using namespace duckdb; namespace { -unique_ptr TransformTestYear(PEGTransformer &, ParseResult &) { +arena_ptr TransformTestYear(PEGTransformer &transformer, ParseResult &) { // Keep this an expression when the lowerer emits it in GROUP BY; a bare // integer literal would be interpreted as a select-list ordinal. unique_ptr expression = make_uniq(LogicalType::INTEGER, ConstantExpression::Integer(2023)); - return make_uniq>>(std::move(expression)); + return transformer.MakeResult>(std::move(expression)); } -unique_ptr StartTestYear(PEGTransformer &transformer, ParseResult &result) { - return make_uniq(transformer, result, TransformTestYear); +arena_ptr StartTestYear(PEGTransformer &transformer, ParseResult &result) { + return transformer.MakeProcess(transformer, result, TransformTestYear); } class TestGrammar final : public GrammarExtension { diff --git a/test/sql/measures.test b/test/sql/measures.test index 50e22d0..b2a5ebd 100644 --- a/test/sql/measures.test +++ b/test/sql/measures.test @@ -2544,6 +2544,21 @@ JOIN fanout_orders o ON c.cust_id = o.cust_id; ---- [25, 30, 35, 40] +# Global AT recomputation also collapses repeated LIST values without an +# ordering aggregate around the scalar subquery (DuckDB 2.0 cannot bind MAX). +query T +SEMANTIC SELECT AGGREGATE(age_list) AT (ALL) +FROM fanout_customers_list_v c +JOIN fanout_orders o ON c.cust_id = o.cust_id; +---- +[25, 30, 35, 40] + +query T +SEMANTIC SELECT AGGREGATE(age_list) +FROM fanout_customers_list_v WHERE false; +---- +NULL + # -- Test 20: BOOL_AND / BOOL_OR measures immune to fan-out -- # Dave (cust_id=4) has no orders, so the join excludes him. # He is the only non-premium and the only trial user. Without fan-out diff --git a/test/sql/native_aggregate_decorations.test b/test/sql/native_aggregate_decorations.test index a1bc29f..ab9ccd8 100644 --- a/test/sql/native_aggregate_decorations.test +++ b/test/sql/native_aggregate_decorations.test @@ -38,11 +38,6 @@ statement ok CREATE VIEW decoration_scope_v AS SELECT id, g AS area, x, SUM(x) AS MEASURE total FROM decoration_scope_rows; -foreach parser_mode true false - -statement ok -SET heap_based_parser=${parser_mode}; - query RRR SELECT AGGREGATE(DISTINCT revenue), AGGREGATE(DISTINCT average_amount), AGGREGATE(DISTINCT ratio) FROM decoration_v; @@ -235,5 +230,3 @@ SELECT yardstick_finalize(yardstick_combine(a.state, b.state)) FROM states a, states b WHERE a.area = 'a' AND b.area = 'b'; ---- 70.0 - -endloop diff --git a/test/sql/native_aggregate_detection.test b/test/sql/native_aggregate_detection.test index f81b05f..e5cc4a6 100644 --- a/test/sql/native_aggregate_detection.test +++ b/test/sql/native_aggregate_detection.test @@ -16,11 +16,6 @@ statement ok CREATE VIEW aggregate_detection_v AS SELECT year, SUM(amount) AS MEASURE revenue FROM aggregate_detection_sales; -foreach parser_mode true false - -statement ok -SET heap_based_parser=${parser_mode}; - # Quoted and qualified names own the full function span. Unicode before the # call must not shift byte offsets, and comments cannot manufacture calls. query TR @@ -90,5 +85,3 @@ query R SELECT aggregate([1, AGGREGATE(revenue) AT (ALL)], 'sum') FROM aggregate_detection_v; ---- 71.0 - -endloop diff --git a/test/sql/native_aggregate_errors.test b/test/sql/native_aggregate_errors.test index fe6f6f8..4352213 100644 --- a/test/sql/native_aggregate_errors.test +++ b/test/sql/native_aggregate_errors.test @@ -21,11 +21,6 @@ FROM aggregate_error_sales; statement ok CREATE TABLE aggregate_error_results(total INTEGER); -foreach parser_mode true false - -statement ok -SET heap_based_parser=${parser_mode}; - query R SELECT AGGREGATE(DISTINCT revenue) FROM aggregate_error_v; ---- @@ -198,9 +193,8 @@ SELECT aggregate([1, 2], 'sum') OVER (); ---- aggregate is not an aggregate function -endloop query I SELECT COUNT(*) FROM aggregate_error_results; ---- -2 +1 diff --git a/test/sql/native_aggregate_statements.test b/test/sql/native_aggregate_statements.test index 0671886..d69b9df 100644 --- a/test/sql/native_aggregate_statements.test +++ b/test/sql/native_aggregate_statements.test @@ -16,11 +16,6 @@ statement ok CREATE VIEW native_statement_v AS SELECT year, SUM(amount) AS MEASURE revenue FROM native_statement_sales; -foreach parser_mode true false - -statement ok -SET heap_based_parser=${parser_mode}; - statement ok CREATE OR REPLACE TABLE native_statement_output AS SELECT year, AGGREGATE(revenue) AT (ALL) AS total FROM native_statement_v GROUP BY year; @@ -100,5 +95,3 @@ query R SELECT total FROM read_csv('__TEST_DIR__/native_statement_totals.csv'); ---- 80.0 - -endloop diff --git a/test/sql/native_aggregate_windows.test b/test/sql/native_aggregate_windows.test index 965fa3a..05eee88 100644 --- a/test/sql/native_aggregate_windows.test +++ b/test/sql/native_aggregate_windows.test @@ -27,11 +27,6 @@ CREATE TABLE measure_window_right(id INTEGER); statement ok INSERT INTO measure_window_right VALUES (10), (20); -foreach parser_mode true false - -statement ok -SET heap_based_parser=${parser_mode}; - statement ok CREATE OR REPLACE VIEW measure_window_raw AS SELECT id, yr, region, @@ -705,8 +700,3 @@ query I SELECT nextval('measure_window_cte_sequence'); ---- 2 - -endloop - -statement ok -SET heap_based_parser=true; diff --git a/test/sql/native_combined_grammar.test b/test/sql/native_combined_grammar.test index 53c9baa..1189452 100644 --- a/test/sql/native_combined_grammar.test +++ b/test/sql/native_combined_grammar.test @@ -24,11 +24,6 @@ INSERT INTO combined_sales VALUES (2022, 100), (2023, 225); statement ok CREATE VIEW combined_plain_v AS SELECT SUM(amount) AS MEASURE revenue FROM combined_sales; -foreach parser_mode true false - -statement ok -SET heap_based_parser=${parser_mode}; - # A selected grammar without Yardstick rules must not be replaced by the # extension's private grammar merely because Yardstick is loaded. statement ok @@ -75,7 +70,6 @@ SELECT * FROM yardstick('SELECT AGGREGATE(revenue) AT (WHERE year = yardstick_te ---- 225.0 -endloop # Grammar activation is connection-local, even after private connections parsed # the extension syntax. Registration alone must not make this expression bind. diff --git a/test/sql/native_current.test b/test/sql/native_current.test index c66131d..3a6f723 100644 --- a/test/sql/native_current.test +++ b/test/sql/native_current.test @@ -15,11 +15,6 @@ INSERT INTO current_sales VALUES (2022, 'US', 100), (2022, 'EU', 50), (2023, 'US statement ok CREATE VIEW current_sales_v AS SELECT year, region, SUM(amount) AS MEASURE current_revenue FROM current_sales; -foreach parser_mode true false - -statement ok -SET heap_based_parser=${parser_mode}; - # The quoted AGGREGATE function and trivia require the native frontend. query IR rowsort SELECT year, "AGGREGATE"(current_revenue) AT (SET year = CURRENT /* dimension */ year - 1) @@ -137,7 +132,7 @@ SELECT year, "AGGREGATE"(current_revenue) AT ( query IR rowsort SELECT year, "AGGREGATE"(current_revenue) AT ( - SET year = (SELECT current year FROM (SELECT 2022 AS current)) + SET year = (SELECT current AS year FROM (SELECT 2022 AS current)) ) FROM current_sales_v; ---- 2022 150.0 @@ -147,12 +142,12 @@ statement ok SET active_grammar_extensions=['yardstick']; query I -SELECT current year FROM (SELECT 2023 AS current); +SELECT current AS year FROM (SELECT 2023 AS current); ---- 2023 query I -SELECT "current" year FROM (SELECT 2023 AS current); +SELECT "current" AS year FROM (SELECT 2023 AS current); ---- 2023 @@ -181,7 +176,6 @@ DROP MACRO "current"; statement ok SET active_grammar_extensions=[]; -endloop statement ok DROP VIEW current_sales_v; diff --git a/test/sql/native_implicit_measures.test b/test/sql/native_implicit_measures.test index e28a8a4..982030e 100644 --- a/test/sql/native_implicit_measures.test +++ b/test/sql/native_implicit_measures.test @@ -24,11 +24,6 @@ statement ok CREATE VIEW native_implicit_amount_v AS SELECT amount, SUM(amount) AS MEASURE amount_revenue FROM native_implicit_sales; -foreach parser_mode true false - -statement ok -SET heap_based_parser=${parser_mode}; - # INSERT's input SELECT retains its own projection and relation scope. statement ok INSERT INTO native_implicit_inserted @@ -140,5 +135,3 @@ query RR SELECT MEDIAN(amount), amount_revenue FROM native_implicit_amount_v; ---- 100.0 375.0 - -endloop diff --git a/test/sql/native_measure_declarations.test b/test/sql/native_measure_declarations.test index 8cd5015..476e9ee 100644 --- a/test/sql/native_measure_declarations.test +++ b/test/sql/native_measure_declarations.test @@ -14,11 +14,6 @@ INSERT INTO native_decl_sales VALUES (2022, 'US', 100), (2022, 'EU', 50), (2023, 'US', 150), (2023, 'EU', 75); -foreach parser_mode true false - -statement ok -SET heap_based_parser=${parser_mode}; - # These are ordinary views. Marker text must not become a declaration merely # because it occurs in CREATE VIEW source rather than a standalone SELECT. statement ok @@ -250,5 +245,3 @@ AS MEASURE declarations must be in the top-level CREATE VIEW projection statement ok SET allow_parser_override_extension='fallback'; - -endloop diff --git a/test/sql/native_mixed_aggregate.test b/test/sql/native_mixed_aggregate.test index efe25a3..8a7095d 100644 --- a/test/sql/native_mixed_aggregate.test +++ b/test/sql/native_mixed_aggregate.test @@ -15,11 +15,6 @@ INSERT INTO mixed_aggregate_sales VALUES (10), (20); statement ok CREATE VIEW mixed_aggregate_v AS SELECT SUM(amount) AS MEASURE revenue FROM mixed_aggregate_sales; -foreach parser_mode true false - -statement ok -SET heap_based_parser=${parser_mode}; - query IR SELECT aggregate([1,2], 'sum'), "AGGREGATE"/*c*/(revenue) FROM mixed_aggregate_v; ---- @@ -45,7 +40,6 @@ SELECT aggregate(["AGGREGATE"/* nested */(revenue), 1], 'sum') FROM mixed_aggreg ---- 31.0 -endloop # The list aggregate is a scalar dimension, not a Yardstick measure. A # row-dependent input must remain correlated to the selected dimension. diff --git a/test/sql/native_modifier_dimensions.test b/test/sql/native_modifier_dimensions.test index 924e9bb..142e226 100644 --- a/test/sql/native_modifier_dimensions.test +++ b/test/sql/native_modifier_dimensions.test @@ -15,11 +15,6 @@ INSERT INTO modifier_dates VALUES ('2023-01-01', 'US', 10), ('2023-02-01', 'us', statement ok CREATE VIEW modifier_dates_v AS SELECT day, label, SUM(amount) AS MEASURE modifier_revenue FROM modifier_dates; -foreach parser_mode true false - -statement ok -SET heap_based_parser=${parser_mode}; - query IR rowsort SELECT MONTH(DAY), "AGGREGATE"(modifier_revenue) AT (SET month(day) = 2) FROM modifier_dates_v; ---- @@ -69,7 +64,6 @@ SELECT MONTH(day), "AGGREGATE"(modifier_revenue) AT (SET MONTH(day) = 2 ALL "mon 1 30.0 2 30.0 -endloop statement ok DROP VIEW modifier_dates_v; diff --git a/test/sql/native_parser_settings.test b/test/sql/native_parser_settings.test new file mode 100644 index 0000000..552ca89 --- /dev/null +++ b/test/sql/native_parser_settings.test @@ -0,0 +1,54 @@ +# name: test/sql/native_parser_settings.test +# description: Session parser settings survive deferred binding and expression reparsing. +# group: [yardstick] + +require-env YARDSTICK_NATIVE_PEG 1 + +require yardstick + +statement ok +CREATE TABLE parser_settings_sales(year INTEGER, amount INTEGER); + +statement ok +INSERT INTO parser_settings_sales VALUES (2022, 100), (2023, 225); + +# A column list exercises deferred view binding; the wrapper reparses its SQL +# under the originating session before executing on its private connection. +statement ok +CREATE VIEW parser_settings_v(fiscal_year, revenue) AS +SELECT year, SUM(amount) AS MEASURE total FROM parser_settings_sales; + +statement ok +SET integer_division=true; + +query I +SELECT * FROM yardstick('SELECT AGGREGATE(revenue) AT (WHERE fiscal_year = 4045 / 2) FROM parser_settings_v'); +---- +100 + +statement ok +SET integer_division=false; + +query I +SELECT * FROM yardstick('SELECT AGGREGATE(revenue) AT (WHERE fiscal_year = 4045 / 2) FROM parser_settings_v'); +---- +NULL + +statement ok +SET regex_match_operator_semantics='partial'; + +query I +SELECT * FROM yardstick('SELECT AGGREGATE(revenue) AT (WHERE ''x2022y'' ~ ''2022'') FROM parser_settings_v'); +---- +325 + +statement ok +SET regex_match_operator_semantics='full'; + +query I +SELECT * FROM yardstick('SELECT AGGREGATE(revenue) AT (WHERE ''x2022y'' ~ ''2022'') FROM parser_settings_v'); +---- +NULL + +statement ok +RESET regex_match_operator_semantics; diff --git a/test/sql/native_peg.test b/test/sql/native_peg.test index 9680691..07e5fa9 100644 --- a/test/sql/native_peg.test +++ b/test/sql/native_peg.test @@ -12,13 +12,8 @@ CREATE TABLE native_sales (year INTEGER, region VARCHAR, amount INTEGER); statement ok INSERT INTO native_sales VALUES (2022, 'US', 100), (2022, 'EU', 50), (2023, 'US', 150), (2023, 'EU', 75); -# Exercise both transformer modes. The connection's grammar setting stays empty: +# The connection's grammar setting stays empty: # LOAD alone must enable Yardstick's native adapter. -foreach parser_mode true false - -statement ok -SET heap_based_parser=${parser_mode}; - statement ok CREATE OR REPLACE VIEW native_sales_v AS SELECT year, region, SUM(amount)AS/* measure trivia */MEASURE native_revenue @@ -158,7 +153,6 @@ SELECT 'AS MEASURE fake', $$AGGREGATE(fake) AT (ALL)$$, 1 AS measure ---- AS MEASURE fake AGGREGATE(fake) AT (ALL) 1 -endloop # A newly opened connection inherits the registered adapter without a SET. query R second_connection @@ -173,7 +167,7 @@ SET active_grammar_extensions=['yardstick']; # CURRENT remains an ordinary identifier outside AT expressions. query I -SELECT current year FROM (SELECT 2023 AS current); +SELECT current AS year FROM (SELECT 2023 AS current); ---- 2023 diff --git a/test/sql/native_query_scopes.test b/test/sql/native_query_scopes.test index feb6c28..585c79e 100644 --- a/test/sql/native_query_scopes.test +++ b/test/sql/native_query_scopes.test @@ -18,11 +18,6 @@ CREATE VIEW query_scope_v AS SELECT year, SUM(amount) AS MEASURE revenue FROM qu statement ok CREATE VIEW query_scope_ten AS SELECT year, SUM(amount * 10) AS MEASURE revenue FROM query_scope_sales; -foreach parser_mode true false - -statement ok -SET heap_based_parser=${parser_mode}; - # Schema-qualified references still name the local measure relation. query R SELECT AGGREGATE(revenue) FROM main.query_scope_v WHERE main.query_scope_v.year = 2023; @@ -206,5 +201,3 @@ SELECT AGGREGATE(revenue) FROM query_scope_v v WHERE EXISTS (SELECT 1 FROM query_scope_sales _inner WHERE _inner.year = 2023); ---- 80.0 - -endloop diff --git a/test/sql/native_star_view_aliases.test b/test/sql/native_star_view_aliases.test index 3d86bb5..f5068d7 100644 --- a/test/sql/native_star_view_aliases.test +++ b/test/sql/native_star_view_aliases.test @@ -12,11 +12,6 @@ CREATE TABLE native_star_data(year INTEGER, amount INTEGER); statement ok INSERT INTO native_star_data VALUES (2022, 100), (2022, 50), (2023, 225); -foreach parser_mode true false - -statement ok -SET heap_based_parser=${parser_mode}; - statement ok CREATE OR REPLACE VIEW native_star_view(fiscal_year, value, total) AS SELECT *, SUM(amount) AS MEASURE revenue FROM native_star_data; @@ -206,5 +201,3 @@ ROLLBACK; statement ok SET allow_parser_override_extension='fallback'; - -endloop diff --git a/test/sql/native_subquery_dimensions.test b/test/sql/native_subquery_dimensions.test index ea714cb..f5ca19f 100644 --- a/test/sql/native_subquery_dimensions.test +++ b/test/sql/native_subquery_dimensions.test @@ -18,11 +18,6 @@ SELECT year, SUM(amount) AS MEASURE revenue, SUM(amount) AS MEASURE "Net ""revenue" FROM subquery_dimension_sales; -foreach parser_mode true false - -statement ok -SET heap_based_parser=${parser_mode}; - # Multipart outer references contribute grouping dependencies, including # references reached through a subquery with its own unrelated relation. query IR rowsort @@ -106,5 +101,3 @@ WITH totals AS (SELECT "Net ""revenue" FROM subquery_dimension_v) SELECT "Net ""revenue" FROM totals; ---- 70.0 - -endloop diff --git a/test/sql/native_view_column_aliases.test b/test/sql/native_view_column_aliases.test index f9722e2..7d70e8d 100644 --- a/test/sql/native_view_column_aliases.test +++ b/test/sql/native_view_column_aliases.test @@ -18,11 +18,6 @@ CREATE TABLE native_column_overlap(amount INTEGER, revenue INTEGER); statement ok INSERT INTO native_column_overlap VALUES (10, 1), (20, 2); -foreach parser_mode true false - -statement ok -SET heap_based_parser=${parser_mode}; - statement ok CREATE OR REPLACE VIEW native_column_full(fiscal_year, total) AS SELECT year, SUM(amount) AS MEASURE revenue FROM native_column_sales; @@ -217,5 +212,3 @@ SELECT fiscal_year, total FROM native_column_ordinary ORDER BY fiscal_year, tota statement ok SET allow_parser_override_extension='fallback'; - -endloop diff --git a/yardstick-rs/src/sql/measures.rs b/yardstick-rs/src/sql/measures.rs index 11c5c38..326dd15 100644 --- a/yardstick-rs/src/sql/measures.rs +++ b/yardstick-rs/src/sql/measures.rs @@ -8046,7 +8046,9 @@ fn expand_aggregate_query_impl(sql: &str, native_scope: bool) -> AggregateExpand if decorated_call.is_some() { wrap_decorated_scalar_recomputation(&eval_sql) } else { - format!("MAX({eval_sql})") + // Collapse repeated copies of one scalar context value without + // MAX's comparison binding (which fails for LIST subqueries on 2.0). + format!("ANY_VALUE({eval_sql})") } } else { eval_sql @@ -8068,7 +8070,7 @@ fn expand_aggregate_query_impl(sql: &str, native_scope: bool) -> AggregateExpand if decorated_call.is_some() { wrap_decorated_scalar_recomputation(&eval_sql) } else { - format!("MAX({eval_sql})") + format!("ANY_VALUE({eval_sql})") } } else { eval_sql @@ -8224,7 +8226,7 @@ fn expand_aggregate_query_impl(sql: &str, native_scope: bool) -> AggregateExpand if decorated_call.is_some() { wrap_decorated_scalar_recomputation(&eval_sql) } else { - format!("MAX({eval_sql})") + format!("ANY_VALUE({eval_sql})") } } else { eval_sql From 6b5e9ead9dd69ee1f79323407c2469d5f1619481 Mon Sep 17 00:00:00 2001 From: Nico Ritschel Date: Wed, 21 Oct 2026 07:38:36 -0700 Subject: [PATCH 2/2] Preserve session settings and scalar context maxima --- src/yardstick_extension.cpp | 10 +++++ test/sql/native_parser_settings.test | 23 ++++++++++ test/sql/scalar_context_collapse.test | 62 +++++++++++++++++++++++++++ yardstick-rs/src/sql/measures.rs | 10 ++--- 4 files changed, 100 insertions(+), 5 deletions(-) create mode 100644 test/sql/scalar_context_collapse.test diff --git a/src/yardstick_extension.cpp b/src/yardstick_extension.cpp index 92d2769..531164a 100644 --- a/src/yardstick_extension.cpp +++ b/src/yardstick_extension.cpp @@ -2,6 +2,9 @@ #include "yardstick_extension.hpp" #include "yardstick_parser_compat.hpp" +#if YARDSTICK_PARSER_INSTANCE_API +#include "duckdb/main/settings.hpp" +#endif #include "yardstick_parser_extension.hpp" #include "frontend_peg.hpp" #include "aggregate_state.hpp" @@ -205,6 +208,13 @@ static unique_ptr YardstickQueryBind(ClientContext &context, auto &execution_config = ClientConfig::GetConfig(*con.context); execution_config.active_grammar_extensions = source_config.active_grammar_extensions; execution_config.cached_grammar = source_config.cached_grammar; +#if YARDSTICK_PARSER_INSTANCE_API + // This is a binder setting, not a ParserOptions field. The private + // execution connection must allow the same ORDER BY syntax as its caller. + Settings::Set( + *con.context, SetScope::SESSION, + Value::BOOLEAN(Settings::Get(context))); +#endif #endif auto query_result = con.Query(data->rewritten_sql); diff --git a/test/sql/native_parser_settings.test b/test/sql/native_parser_settings.test index 552ca89..cfcdea5 100644 --- a/test/sql/native_parser_settings.test +++ b/test/sql/native_parser_settings.test @@ -52,3 +52,26 @@ NULL statement ok RESET regex_match_operator_semantics; + +# ORDER BY literal acceptance is a binder setting. Preserve it on the +# wrapper's execution connection, while deferred SELECTs bind on the caller. +statement ok +SET order_by_non_integer_literal=true; + +query I +SELECT AGGREGATE(revenue) FROM parser_settings_v ORDER BY 'ignored'; +---- +325 + +query I +SELECT * FROM yardstick('SELECT AGGREGATE(revenue) FROM parser_settings_v ORDER BY ''ignored'''); +---- +325 + +statement ok +SET order_by_non_integer_literal=false; + +statement error +SELECT * FROM yardstick('SELECT AGGREGATE(revenue) FROM parser_settings_v ORDER BY ''ignored'''); +---- +ORDER BY non-integer literal diff --git a/test/sql/scalar_context_collapse.test b/test/sql/scalar_context_collapse.test new file mode 100644 index 0000000..a5ca513 --- /dev/null +++ b/test/sql/scalar_context_collapse.test @@ -0,0 +1,62 @@ +# name: test/sql/scalar_context_collapse.test +# description: Ungrouped correlated contexts retain deterministic maximum semantics. +# group: [yardstick] + +require yardstick + +statement ok +CREATE TABLE collapse_sales(year INTEGER, amount INTEGER); + +statement ok +INSERT INTO collapse_sales VALUES (2020, 10), (2021, 30), (2022, 20); + +statement ok +CREATE VIEW collapse_v AS +SELECT year, year - 1 AS prior_year, SUM(amount) AS MEASURE revenue, + LIST(amount ORDER BY amount) AS MEASURE amounts +FROM collapse_sales; + +# Direct self-reference already requires grouping; preserve that validation. +statement error +SELECT AGGREGATE(revenue) AT (SET year = year - 1) FROM collapse_v; +---- +does not group by year + +# A different exposed dimension can supply a correlated SET value. Prior-year +# values differ by source row: NULL, 10, 30. Preserve the maximum, including LISTs. +query IT +SELECT AGGREGATE(revenue) AT (SET year = v.prior_year), + AGGREGATE(amounts) AT (SET year = v.prior_year) +FROM collapse_v v; +---- +30 [30] + +statement ok +CREATE TABLE collapse_reversed AS SELECT * FROM collapse_sales ORDER BY year DESC; + +statement ok +CREATE VIEW collapse_reversed_v AS +SELECT year, year - 1 AS prior_year, SUM(amount) AS MEASURE revenue, + LIST(amount ORDER BY amount) AS MEASURE amounts +FROM collapse_reversed; + +query IT +SELECT AGGREGATE(revenue) AT (SET year = v.prior_year), + AGGREGATE(amounts) AT (SET year = v.prior_year) +FROM collapse_reversed_v v; +---- +30 [30] + +query IT +SELECT AGGREGATE(revenue) AT (SET year = v.prior_year), + AGGREGATE(amounts) AT (SET year = v.prior_year) +FROM collapse_v v WHERE false; +---- +NULL NULL + +query IT +SELECT AGGREGATE(revenue) AT (SET year = 1900), + AGGREGATE(amounts) AT (SET year = 1900) +FROM collapse_v; +---- +NULL NULL diff --git a/yardstick-rs/src/sql/measures.rs b/yardstick-rs/src/sql/measures.rs index 326dd15..8880003 100644 --- a/yardstick-rs/src/sql/measures.rs +++ b/yardstick-rs/src/sql/measures.rs @@ -8046,9 +8046,9 @@ fn expand_aggregate_query_impl(sql: &str, native_scope: bool) -> AggregateExpand if decorated_call.is_some() { wrap_decorated_scalar_recomputation(&eval_sql) } else { - // Collapse repeated copies of one scalar context value without - // MAX's comparison binding (which fails for LIST subqueries on 2.0). - format!("ANY_VALUE({eval_sql})") + // Correlated contexts can produce different values. Retain + // MAX semantics without 2.0's broken MAX(LIST subquery) binding. + format!("LIST_MAX(LIST({eval_sql}))") } } else { eval_sql @@ -8070,7 +8070,7 @@ fn expand_aggregate_query_impl(sql: &str, native_scope: bool) -> AggregateExpand if decorated_call.is_some() { wrap_decorated_scalar_recomputation(&eval_sql) } else { - format!("ANY_VALUE({eval_sql})") + format!("LIST_MAX(LIST({eval_sql}))") } } else { eval_sql @@ -8226,7 +8226,7 @@ fn expand_aggregate_query_impl(sql: &str, native_scope: bool) -> AggregateExpand if decorated_call.is_some() { wrap_decorated_scalar_recomputation(&eval_sql) } else { - format!("ANY_VALUE({eval_sql})") + format!("LIST_MAX(LIST({eval_sql}))") } } else { eval_sql