Skip to content

Commit c29981c

Browse files
committed
Warn when AT ALL drops ungrouped WHERE filters
1 parent 3eb6e0f commit c29981c

5 files changed

Lines changed: 366 additions & 35 deletions

File tree

include/yardstick_ffi.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,7 @@ struct YardstickAggregateResult {
313313
bool had_aggregate;
314314
char* expanded_sql;
315315
char* error;
316+
char* warnings;
316317
};
317318

318319
struct YardstickMeasureAggResult {

src/yardstick_extension.cpp

Lines changed: 58 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#include "duckdb/parser/statement/extension_statement.hpp"
77
#include "duckdb/function/table_function.hpp"
88
#include "duckdb/main/connection.hpp"
9+
#include "duckdb/logging/logger.hpp"
910

1011
// Include FFI types header
1112
#include "yardstick_ffi.h"
@@ -222,6 +223,8 @@ static std::string RewritePercentileWithinGroup(const std::string &sql) {
222223
// TABLE FUNCTION: yardstick(sql) - Execute SQL with AGGREGATE() expansion
223224
//=============================================================================
224225

226+
static void HandleAggregateWarnings(ClientContext &context, const string &warnings);
227+
225228
struct YardstickQueryData : public TableFunctionData {
226229
string original_sql;
227230
string rewritten_sql;
@@ -235,6 +238,9 @@ static unique_ptr<FunctionData> YardstickQueryBind(ClientContext &context,
235238
vector<string> &names) {
236239
auto data = make_uniq<YardstickQueryData>();
237240
data->original_sql = input.inputs[0].GetValue<string>();
241+
if (input.inputs.size() > 1) {
242+
HandleAggregateWarnings(context, input.inputs[1].GetValue<string>());
243+
}
238244

239245
// Rewrite the SQL using our Rust code
240246
if (yardstick_has_aggregate(data->original_sql.c_str())) {
@@ -330,6 +336,49 @@ static bool StartsWithSemantic(const std::string &query, std::string &stripped_q
330336
return true;
331337
}
332338

339+
static std::string EscapeSqlStringLiteral(const std::string &value) {
340+
string escaped;
341+
for (char c : value) {
342+
if (c == '\'') {
343+
escaped += "''";
344+
} else {
345+
escaped += c;
346+
}
347+
}
348+
return escaped;
349+
}
350+
351+
static std::string AggregateWarnings(YardstickAggregateResult &result) {
352+
return result.warnings ? string(result.warnings) : string();
353+
}
354+
355+
static bool WarningsAsErrors(ClientContext &context) {
356+
Value setting;
357+
if (!context.TryGetCurrentSetting("warnings_as_errors", setting) || setting.IsNull()) {
358+
return false;
359+
}
360+
return setting.GetValue<bool>();
361+
}
362+
363+
static void HandleAggregateWarnings(ClientContext &context, const string &warnings) {
364+
if (warnings.empty()) {
365+
return;
366+
}
367+
if (WarningsAsErrors(context)) {
368+
throw InvalidInputException("%s", warnings);
369+
}
370+
DUCKDB_LOG_WARNING(context, "%s", warnings.c_str());
371+
}
372+
373+
static std::string YardstickWrapperSql(const std::string &expanded_sql, const std::string &warnings) {
374+
string wrapper_sql = "SELECT * FROM yardstick('" + EscapeSqlStringLiteral(expanded_sql) + "'";
375+
if (!warnings.empty()) {
376+
wrapper_sql += ", '" + EscapeSqlStringLiteral(warnings) + "'";
377+
}
378+
wrapper_sql += ")";
379+
return wrapper_sql;
380+
}
381+
333382
#if YARDSTICK_TOKEN_PARSE_FN
334383
// DuckDB main: parse_function receives the post-PEG-failure token tail rather
335384
// than the query string. Yardstick rewrites queries earlier, in
@@ -367,20 +416,10 @@ ParserExtensionParseResult yardstick_parse(ParserExtensionInfo *,
367416

368417
if (result.had_aggregate) {
369418
string expanded_sql(result.expanded_sql);
419+
string warnings = AggregateWarnings(result);
370420
yardstick_free_aggregate_result(result);
371421

372-
// Escape single quotes for embedding in string literal
373-
string escaped_sql;
374-
for (char c : expanded_sql) {
375-
if (c == '\'') {
376-
escaped_sql += "''";
377-
} else {
378-
escaped_sql += c;
379-
}
380-
}
381-
382-
// Wrap in table function call
383-
string wrapper_sql = "SELECT * FROM yardstick('" + escaped_sql + "')";
422+
string wrapper_sql = YardstickWrapperSql(expanded_sql, warnings);
384423

385424
Parser parser;
386425
parser.ParseQuery(wrapper_sql);
@@ -469,6 +508,7 @@ ParserOverrideResult yardstick_parser_override(ParserExtensionInfo *,
469508

470509
if (result.had_aggregate) {
471510
string expanded_sql(result.expanded_sql);
511+
string warnings = AggregateWarnings(result);
472512
yardstick_free_aggregate_result(result);
473513

474514
// Validate the expanded SQL parses. If expansion produced garbage
@@ -489,16 +529,7 @@ ParserOverrideResult yardstick_parser_override(ParserExtensionInfo *,
489529
validation_parser.statements[0]->type == StatementType::SELECT_STATEMENT;
490530

491531
if (is_select) {
492-
string escaped_sql;
493-
for (char c : expanded_sql) {
494-
if (c == '\'') {
495-
escaped_sql += "''";
496-
} else {
497-
escaped_sql += c;
498-
}
499-
}
500-
501-
string wrapper_sql = "SELECT * FROM yardstick('" + escaped_sql + "')";
532+
string wrapper_sql = YardstickWrapperSql(expanded_sql, warnings);
502533
Parser parser;
503534
parser.ParseQuery(wrapper_sql);
504535
return ParserOverrideResult(std::move(parser.statements));
@@ -594,20 +625,11 @@ BoundStatement yardstick_bind(ClientContext &context, Binder &binder,
594625

595626
if (result.had_aggregate) {
596627
string expanded_sql(result.expanded_sql);
628+
string warnings = AggregateWarnings(result);
597629
yardstick_free_aggregate_result(result);
598630

599-
// Escape single quotes for embedding in string literal
600-
string escaped_sql;
601-
for (char c : expanded_sql) {
602-
if (c == '\'') {
603-
escaped_sql += "''";
604-
} else {
605-
escaped_sql += c;
606-
}
607-
}
608-
609631
// Rebind through table function so rewritten SQL executes with normal planning
610-
string wrapper_sql = "SELECT * FROM yardstick('" + escaped_sql + "')";
632+
string wrapper_sql = YardstickWrapperSql(expanded_sql, warnings);
611633
Parser parser;
612634
parser.ParseQuery(wrapper_sql);
613635
auto statements = std::move(parser.statements);
@@ -678,6 +700,9 @@ static void LoadInternal(ExtensionLoader &loader) {
678700
TableFunction query_func("yardstick", {LogicalType::VARCHAR},
679701
YardstickQueryFunction, YardstickQueryBind);
680702
loader.RegisterFunction(query_func);
703+
TableFunction query_func_with_warnings("yardstick", {LogicalType::VARCHAR, LogicalType::VARCHAR},
704+
YardstickQueryFunction, YardstickQueryBind);
705+
loader.RegisterFunction(query_func_with_warnings);
681706
}
682707

683708
void YardstickExtension::Load(ExtensionLoader &loader) {

test/sql/measures.test

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,23 @@ FROM sales_v;
114114
2023 EU 225.0
115115
2023 US 225.0
116116

117+
# AT (ALL dim) drops outer WHERE filters on dimensions that are not grouped.
118+
# Surface a warning through DuckDB; warnings_as_errors makes this test assert it is emitted.
119+
statement ok
120+
SET enable_logging = true;
121+
122+
statement ok
123+
SET warnings_as_errors = true;
124+
125+
statement error
126+
SEMANTIC SELECT region, 100.0 * AGGREGATE(revenue) / AGGREGATE(revenue) AT (ALL region) AS pct_of_total
127+
FROM sales_v
128+
WHERE year = 2023;
129+
----
130+
131+
statement ok
132+
SET warnings_as_errors = false;
133+
117134
# ORDER BY expression referencing a named aggregate that expands to a subquery (#28)
118135
query IIRR
119136
SEMANTIC SELECT

yardstick-rs/src/ffi.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@ use std::ptr;
1313
use crate::sql::{
1414
drop_measure_view_from_sql, expand_aggregate_with_at, expand_curly_braces,
1515
get_measure_aggregation, has_aggregate_function, has_as_measure, has_at_syntax,
16-
has_curly_brace_measure, process_create_view,
17-
has_implicit_measure_refs, has_measure_at_refs,
16+
has_curly_brace_measure, has_implicit_measure_refs, has_measure_at_refs, process_create_view,
1817
};
1918

2019
/// Result from processing CREATE VIEW with AS MEASURE
@@ -39,6 +38,8 @@ pub struct YardstickAggregateResult {
3938
pub expanded_sql: *mut c_char,
4039
/// Error message (null if success)
4140
pub error: *mut c_char,
41+
/// Warning messages separated by newlines (null if none)
42+
pub warnings: *mut c_char,
4243
}
4344

4445
/// Check if SQL contains "AS MEASURE" pattern
@@ -179,6 +180,7 @@ pub extern "C" fn yardstick_expand_aggregate(sql: *const c_char) -> YardstickAgg
179180
had_aggregate: false,
180181
expanded_sql: ptr::null_mut(),
181182
error: to_c_string("Error: null sql pointer"),
183+
warnings: ptr::null_mut(),
182184
};
183185
}
184186

@@ -190,6 +192,7 @@ pub extern "C" fn yardstick_expand_aggregate(sql: *const c_char) -> YardstickAgg
190192
had_aggregate: false,
191193
expanded_sql: ptr::null_mut(),
192194
error: to_c_string(&format!("Error: invalid UTF-8: {e}")),
195+
warnings: ptr::null_mut(),
193196
};
194197
}
195198
}
@@ -204,6 +207,11 @@ pub extern "C" fn yardstick_expand_aggregate(sql: *const c_char) -> YardstickAgg
204207
.error
205208
.map(|s| to_c_string(&s))
206209
.unwrap_or(ptr::null_mut()),
210+
warnings: if result.warnings.is_empty() {
211+
ptr::null_mut()
212+
} else {
213+
to_c_string(&result.warnings.join("\n"))
214+
},
207215
}
208216
}
209217

@@ -299,6 +307,7 @@ pub extern "C" fn yardstick_free_create_view_result(result: YardstickCreateViewR
299307
pub extern "C" fn yardstick_free_aggregate_result(result: YardstickAggregateResult) {
300308
yardstick_free(result.expanded_sql);
301309
yardstick_free(result.error);
310+
yardstick_free(result.warnings);
302311
}
303312

304313
// Helper: convert Rust string to C string

0 commit comments

Comments
 (0)