Skip to content

Commit be64a96

Browse files
authored
Add parser_override to eliminate SEMANTIC prefix requirement (#30)
* Add parser_override to eliminate SEMANTIC prefix requirement Use DuckDB's parser_override API (v1.5+) to intercept queries before the native parser, allowing AGGREGATE() to work in all statement types (SELECT, CTAS, INSERT...SELECT) without the SEMANTIC prefix. The existing parse_function fallback and yardstick_bind operator extension are kept for backwards compatibility. Also filters AGGREGATE() argument extraction to simple identifiers so DuckDB's built-in list aggregate() function is not intercepted. * Handle quoted identifiers in AGGREGATE() argument extraction Fix parse_simple_measure_ref to allow any characters inside matched quotes (double-quotes, backticks, brackets) so AGGREGATE("col_name") is correctly recognized as a measure reference. Add test for quoted measure names. * Return parsed statements directly for non-SELECT to preserve transaction context For CTAS and INSERT...SELECT, return the expanded SQL as parsed statements instead of wrapping in yardstick() table function. The table function executes via con.Query() on a fresh Connection, which runs outside the caller's transaction context. SELECT statements still use the table function wrapper for the second expansion pass. * Allow whitespace in qualified measure refs like AGGREGATE(s . revenue) parse_simple_measure_ref now accepts spaces around the dot in qualified identifiers (e.g. "s . revenue", "s. revenue") and trims parts before normalization. Add tests for spaced qualified refs.
1 parent 5105d15 commit be64a96

4 files changed

Lines changed: 2069 additions & 8 deletions

File tree

src/include/yardstick_extension.hpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ ParserExtensionParseResult yardstick_parse(ParserExtensionInfo *,
2828
ParserExtensionPlanResult yardstick_plan(ParserExtensionInfo *, ClientContext &,
2929
unique_ptr<ParserExtensionParseData>);
3030

31+
ParserOverrideResult yardstick_parser_override(ParserExtensionInfo *info,
32+
const std::string &query,
33+
ParserOptions &options);
34+
3135
// Operator extension: handles binding after parsing
3236
struct YardstickOperatorExtension : public OperatorExtension {
3337
YardstickOperatorExtension() : OperatorExtension() { Bind = yardstick_bind; }
@@ -39,10 +43,14 @@ struct YardstickOperatorExtension : public OperatorExtension {
3943
};
4044

4145
// Parser extension: intercepts query strings
46+
// parser_override runs BEFORE DuckDB's native parser, handling all statement types.
47+
// parse_function/plan_function are kept as fallback for when the native parser fails
48+
// (e.g., AT(...) syntax that is not valid SQL).
4249
struct YardstickParserExtension : public ParserExtension {
4350
YardstickParserExtension() : ParserExtension() {
4451
parse_function = yardstick_parse;
4552
plan_function = yardstick_plan;
53+
parser_override = yardstick_parser_override;
4654
}
4755
};
4856

src/yardstick_extension.cpp

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,117 @@ ParserExtensionParseResult yardstick_parse(ParserExtensionInfo *,
422422
return ParserExtensionParseResult();
423423
}
424424

425+
//=============================================================================
426+
// PARSER OVERRIDE: intercepts ALL queries before DuckDB's native parser
427+
//=============================================================================
428+
429+
ParserOverrideResult yardstick_parser_override(ParserExtensionInfo *,
430+
const std::string &query,
431+
ParserOptions &options) {
432+
// Strip SEMANTIC prefix if present (backwards compatibility)
433+
std::string sql_to_check = query;
434+
std::string semantic_stripped;
435+
bool had_semantic_prefix = StartsWithSemantic(query, semantic_stripped);
436+
if (had_semantic_prefix) {
437+
sql_to_check = semantic_stripped;
438+
}
439+
440+
// Check for DROP VIEW on measure views
441+
if (yardstick_drop_measure_view_from_sql(sql_to_check.c_str())) {
442+
// Catalog cleanup done; let DuckDB handle the actual DROP
443+
return ParserOverrideResult();
444+
}
445+
446+
// Check for AGGREGATE() function
447+
if (yardstick_has_aggregate(sql_to_check.c_str())) {
448+
YardstickAggregateResult result = yardstick_expand_aggregate(sql_to_check.c_str());
449+
450+
if (result.error) {
451+
// Expansion failed: this might not be a yardstick AGGREGATE() call
452+
// (e.g. DuckDB's built-in list aggregate function). Fall through to
453+
// the native parser in case it can handle the query.
454+
yardstick_free_aggregate_result(result);
455+
return ParserOverrideResult();
456+
}
457+
458+
if (result.had_aggregate) {
459+
string expanded_sql(result.expanded_sql);
460+
yardstick_free_aggregate_result(result);
461+
462+
// Validate the expanded SQL parses. If expansion produced garbage
463+
// (e.g. because AGGREGATE() was actually DuckDB's list aggregate
464+
// function, not a yardstick measure), fall through to the native parser.
465+
Parser validation_parser;
466+
try {
467+
validation_parser.ParseQuery(expanded_sql);
468+
} catch (...) {
469+
return ParserOverrideResult();
470+
}
471+
472+
// For SELECT statements, wrap in yardstick() table function so that
473+
// any remaining AGGREGATE() calls get a second expansion pass.
474+
// For non-SELECT (CTAS, INSERT...SELECT), return parsed statements
475+
// directly to preserve the caller's transaction context.
476+
bool is_select = !validation_parser.statements.empty() &&
477+
validation_parser.statements[0]->type == StatementType::SELECT_STATEMENT;
478+
479+
if (is_select) {
480+
string escaped_sql;
481+
for (char c : expanded_sql) {
482+
if (c == '\'') {
483+
escaped_sql += "''";
484+
} else {
485+
escaped_sql += c;
486+
}
487+
}
488+
489+
string wrapper_sql = "SELECT * FROM yardstick('" + escaped_sql + "')";
490+
Parser parser;
491+
parser.ParseQuery(wrapper_sql);
492+
return ParserOverrideResult(std::move(parser.statements));
493+
}
494+
495+
return ParserOverrideResult(std::move(validation_parser.statements));
496+
}
497+
498+
yardstick_free_aggregate_result(result);
499+
}
500+
501+
// Check for CREATE VIEW with AS MEASURE
502+
if (yardstick_has_as_measure(sql_to_check.c_str())) {
503+
std::string rewritten_query = RewritePercentileWithinGroup(query);
504+
YardstickCreateViewResult result = yardstick_process_create_view(rewritten_query.c_str());
505+
506+
if (result.error) {
507+
string error_msg(result.error);
508+
yardstick_free_create_view_result(result);
509+
try {
510+
throw ParserException(error_msg);
511+
} catch (std::exception &e) {
512+
return ParserOverrideResult(e);
513+
}
514+
}
515+
516+
if (result.is_measure_view) {
517+
string clean_sql = RewritePercentileWithinGroup(result.clean_sql);
518+
yardstick_free_create_view_result(result);
519+
520+
try {
521+
Parser parser;
522+
parser.ParseQuery(clean_sql);
523+
return ParserOverrideResult(std::move(parser.statements));
524+
} catch (std::exception &e) {
525+
return ParserOverrideResult(e);
526+
}
527+
}
528+
529+
yardstick_free_create_view_result(result);
530+
}
531+
532+
// Not a yardstick query; fall through to DuckDB's native parser
533+
return ParserOverrideResult();
534+
}
535+
425536
ParserExtensionPlanResult yardstick_plan(ParserExtensionInfo *,
426537
ClientContext &context,
427538
unique_ptr<ParserExtensionParseData> parse_data) {
@@ -531,6 +642,10 @@ static void LoadInternal(ExtensionLoader &loader) {
531642
auto &db = loader.GetDatabaseInstance();
532643
auto &config = DBConfig::GetConfig(db);
533644

645+
// Enable parser_override so yardstick intercepts queries before DuckDB's native parser.
646+
// FALLBACK mode: if our override doesn't handle the query, DuckDB's parser takes over.
647+
config.SetOptionByName("allow_parser_override_extension", Value("fallback"));
648+
534649
// Register parser extension
535650
YardstickParserExtension parser;
536651
#if __has_include("duckdb/main/extension_callback_manager.hpp")

0 commit comments

Comments
 (0)