Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,9 @@ set(EXTENSION_SOURCES
src/yardstick_extension.cpp
src/yardstick_parser_ffi.cpp
src/frontend_peg.cpp
src/aggregate_decorations.cpp
src/aggregate_state.cpp
src/measure_windows.cpp
)

if(NOT COMMAND build_static_extension)
Expand Down
28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,33 @@ On DuckDB builds with grammar-extension support, Yardstick recognizes `AS MEASUR

Native query traversal lowers CTE bodies, subqueries, and set-operation operands independently. Aggregate calls are discovered from expression nodes, including parenthesized `AT` operands and queries inside INSERT, UPDATE, DELETE, CREATE VIEW, CREATE TABLE AS, EXPLAIN, and COPY statements. Visible filters retain outer query correlations. Subquery projections group their outer column dependencies, while implicit measure projections preserve their column names. DuckDB 1.5.5 retains the compatibility parser; native forms whose source spans cannot be represented also use that path.

The native frontend rejects `DISTINCT`, `FILTER`, `ORDER BY`, `OVER`, and `EXPORT_STATE` directly on one-argument `AGGREGATE()` calls. Their semantics are not implemented, and compatibility lowering could produce incorrect results. Define aggregation behavior in the `AS MEASURE` expression or use supported `AT` modifiers instead. DuckDB's multiargument `aggregate(list, function_name)` remains ordinary DuckDB syntax.
The native frontend supports `DISTINCT`, `FILTER`, argument `ORDER BY`, `OVER`, and `EXPORT_STATE` on one-argument `AGGREGATE()` calls. DuckDB 1.5.5 retains its compatibility frontend; these call decorations require the native frontend. DuckDB's multiargument `aggregate(list, function_name)` remains ordinary DuckDB syntax.

```sql
SELECT AGGREGATE(DISTINCT revenue),
AGGREGATE(revenue) FILTER (WHERE region = 'US')
FROM sales;

SELECT year, AGGREGATE(revenue) OVER (
ORDER BY year ROWS BETWEEN 1 PRECEDING AND CURRENT ROW
) AS rolling_revenue
FROM sales_by_year;
```

Call decorations operate on the aggregate functions in the measure definition. For a derived measure such as `SUM(amount) / COUNT(amount)`, `DISTINCT` applies separately to both aggregate inputs, and `FILTER` restricts the base rows for both sides. Scalar arithmetic, casts, and wrappers remain intact. A call filter is combined with an existing definition filter using `AND`. Argument ordering, as in `AGGREGATE(labels ORDER BY priority DESC NULLS LAST)`, takes precedence over definition ordering; the definition's ordering remains as tie breakers. Filter and ordering expressions can reference exposed dimension aliases, including computed dimensions.

Window partitions and `ROWS`, `RANGE`, or `GROUPS` frames select visible rows, then recompute the measure from their original base rows. This preserves averages and derived ratios when visible rows represent groups of unequal size. A window call's `FILTER` selects frame input rows, so it can reference joined relations; filters in the measure definition still apply to aggregate leaves. Frames retain DuckDB's peer, exclusion, and empty-frame behavior; repeated references introduced by joins do not duplicate the same base row. Window calls also support `AT` context modifiers, which transform the context selected by the filtered frame.

Window argument ordering can mix source and joined input fields. When a join repeats a base row, its first occurrence under the caller's ordering supplies the joined keys; definition ordering still breaks ties. If `AT` expands the context to a base row absent from the frame, its joined keys are `NULL`, while its source keys are recomputed.

`AGGREGATE(measure) EXPORT_STATE` exports sufficient aggregate state for later finalization. A single aggregate produces DuckDB's native state; a derived measure produces a composite state carrying its aggregate leaves and scalar formula. `yardstick_finalize(state)` accepts either form. `yardstick_combine(left, right)` combines two states with matching formulas and aggregate types, then `yardstick_finalize` evaluates the result. DuckDB's own aggregate-specific export restrictions still apply.

```sql
SELECT yardstick_finalize(state)
FROM (SELECT AGGREGATE(revenue) EXPORT_STATE AS state FROM sales);
```

Combining exported states follows DuckDB's native state semantics: it does not retain a cross-shard set of distinct input values. Combining states exported with `DISTINCT` therefore does not remove duplicates shared by different shards. Recompute from the combined base rows when global distinctness is required.

The native frontend also supports full and partial `CREATE VIEW` column lists for measure views. Header names apply to both dimensions and measures, while derived measures retain their declaration dependencies. Star projections are expanded against the originating session before header positions are mapped. Temporary definitions stay session-local; a single statement cannot combine temporary and permanent measure views with the same name.

Expand Down
46 changes: 45 additions & 1 deletion include/yardstick_ffi.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ typedef struct {
/* AT modifier chain (supports multiple: AGGREGATE(x) AT (...) AT (...)) */
YardstickAtModifier* modifiers;
size_t modifier_count;
const char* call_sql; /* Native call AST, without enclosing AT suffixes */
bool is_window;
bool has_decorations;
} YardstickAggregateCall;

/* List of all AGGREGATE() calls found in SQL */
Expand All @@ -59,6 +62,38 @@ typedef struct {
bool native_parsed; /* Complete grammar-validated source spans */
} YardstickAggregateCallList;

/* Complete source definitions used for query-local window lineage. */
typedef struct {
const char* key;
const char* relation_name;
const char* alias;
const char* clean_select_sql;
bool grouped;
const char* const* dimension_names;
const char* const* dimension_expressions;
size_t dimension_count;
} YardstickWindowSource;

typedef struct {
const char* marker_name;
const char* source_key;
const char* expression_sql;
const YardstickAtModifier* modifiers;
size_t modifier_count;
} YardstickWindowCall;

char* yardstick_decorate_measure(
const char* expression, const char* call_sql,
const char* const* dimension_names, const char* const* dimension_expressions, size_t dimension_count,
const char* const* qualifiers, size_t qualifier_count,
const char* const* binding_ctes, size_t binding_cte_count, char** error);
char* yardstick_window_marker(const char* call_sql, const char* marker_name, char** error);
char* yardstick_rewrite_measure_windows(
const char* sql, const YardstickWindowSource* sources, size_t source_count,
const YardstickWindowCall* calls, size_t call_count,
const char* const* visible_ctes, size_t visible_cte_count,
const char* const* binding_ctes, size_t binding_cte_count, char** error);

/* Grammar-owned CURRENT references, relative to the supplied expression. */
typedef struct {
const char* dimension;
Expand Down Expand Up @@ -87,11 +122,18 @@ char* yardstick_rewrite_visible_filter(const char* expression, const char* local
char** error);

/* Complete native SELECT scopes, including nested queries and set operands. */
typedef struct {
uint32_t start_pos;
uint32_t end_pos;
bool recursive;
} YardstickCteDefinition;

typedef struct {
uint32_t start_pos;
uint32_t end_pos;
const char** visible_ctes;
size_t visible_cte_count;
YardstickCteDefinition* cte_definitions;
} YardstickQueryScope;

typedef struct {
Expand All @@ -116,6 +158,7 @@ typedef struct {
bool is_star; /* True if SELECT * or table.* */
bool is_measure_ref; /* True if references AGGREGATE() */
bool contains_subquery; /* Group outer dependencies instead of the subquery expression */
bool contains_window; /* Window expressions are not implicit grouping keys */
const char* reference_column; /* Native direct column reference, decoded */
const char* reference_qualifier; /* Native direct qualifier, decoded or NULL */
const char** subquery_dimensions; /* Outer column references required by this projection */
Expand Down Expand Up @@ -327,7 +370,8 @@ char* yardstick_replace_range(
const char* replacement
);

char* yardstick_qualify_expression(const char* expr, const char* qualifier);
/* A non-NULL dimension selects only outer references to that dimension. */
char* yardstick_qualify_expression(const char* expr, const char* qualifier, const char* dimension);

/**
* Free a string allocated by yardstick functions.
Expand Down
Loading
Loading