Skip to content

Commit 676d0e6

Browse files
committed
Add Yardstick upstream parity CI
1 parent 7d58ee9 commit 676d0e6

8 files changed

Lines changed: 892 additions & 39 deletions

File tree

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
name: Yardstick Upstream Parity
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
yardstick_ref:
7+
description: "Yardstick ref to test against"
8+
required: true
9+
default: "main"
10+
schedule:
11+
- cron: "17 10 * * *"
12+
pull_request:
13+
paths:
14+
- ".github/workflows/yardstick-upstream.yml"
15+
- "docs/compatibility/yardstick.md"
16+
- "sidemantic/adapters/yardstick.py"
17+
- "sidemantic/sql/aggregation_detection.py"
18+
- "sidemantic/sql/query_rewriter.py"
19+
- "tests/queries/test_yardstick_measures_replay.py"
20+
- "tests/queries/test_yardstick_query_rewriter.py"
21+
22+
permissions:
23+
contents: read
24+
25+
concurrency:
26+
group: yardstick-upstream-${{ github.workflow }}-${{ github.ref }}
27+
cancel-in-progress: true
28+
29+
jobs:
30+
upstream-parity:
31+
name: Live upstream SQL replay
32+
runs-on: ubuntu-latest
33+
timeout-minutes: 15
34+
env:
35+
SIDEMANTIC_YARDSTICK_UPSTREAM_TESTS: "1"
36+
YARDSTICK_UPSTREAM_REF: ${{ github.event.inputs.yardstick_ref || 'main' }}
37+
38+
steps:
39+
- uses: actions/checkout@v4
40+
41+
- name: Install uv
42+
uses: astral-sh/setup-uv@v5
43+
with:
44+
enable-cache: true
45+
46+
- name: Set up Python
47+
run: uv python install 3.12
48+
49+
- name: Install dependencies
50+
run: uv sync --extra dev
51+
52+
- name: Replay upstream Yardstick tests
53+
run: uv run pytest -q tests/queries/test_yardstick_measures_replay.py -m yardstick_upstream

docs/compatibility/yardstick.md

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Yardstick Compatibility
22

3-
Sidemantic's Yardstick adapter parses SQL files containing `CREATE VIEW` statements that use the `AS MEASURE` syntax from Julian Hyde's ["Measures in SQL" proposal](https://arxiv.org/abs/2307.14009). It maps Yardstick concepts to Sidemantic's semantic model (Model, Dimension, Metric) and supports the `SEMANTIC SELECT`, `AGGREGATE()`, and `AT` query modifiers for measure-aware SQL queries.
3+
Sidemantic's Yardstick adapter parses SQL files containing `CREATE VIEW` statements that use the `AS MEASURE` syntax from Julian Hyde's ["Measures in SQL" proposal](https://arxiv.org/abs/2307.14009). It maps Yardstick concepts to Sidemantic's semantic model (Model, Dimension, Metric) and supports `SEMANTIC SELECT`, optional-prefix `AGGREGATE()`, and `AT` query modifiers for measure-aware SQL queries.
44

55
Features are marked **supported**, **partial support**, or **unsupported**. Partial support entries include notes explaining the limitation.
66

@@ -110,23 +110,27 @@ Derived measure detection works by scanning the expression's column references a
110110
| `MODE(expr) AS MEASURE name` | Supported (stored as raw SQL expression metric with `agg=None`) |
111111
| `PERCENTILE_CONT(n) WITHIN GROUP (ORDER BY expr) AS MEASURE name` | Supported (stored as raw SQL expression metric) |
112112
| `CASE WHEN AGG(...) THEN ... END AS MEASURE name` | Supported (detected as having aggregate semantics; stored as raw SQL expression metric) |
113-
| Other aggregate functions not in the standard list | Supported (full expression preserved as `Metric.sql`) |
113+
| `PRODUCT(expr)`, `ENTROPY(expr)`, `KURTOSIS(expr)`, `SKEWNESS(expr)`, `LIST(expr)`, and related DuckDB aggregate functions | Supported (stored as raw SQL expression metrics with aggregate semantics) |
114+
| Other aggregate functions not in the standard list | Supported when sqlglot identifies them as aggregates; otherwise preserved as raw SQL only when aggregate semantics can be detected |
114115

115-
When a measure expression contains aggregate functions (detected by walking the AST for `AggFunc` nodes or known anonymous aggregations like `mode`) but doesn't match a simple aggregation pattern, the full expression is preserved as-is for query-time evaluation.
116+
When a measure expression contains aggregate functions (detected by walking the AST for `AggFunc` nodes or known anonymous aggregations like `mode`, `product`, and `entropy`) but doesn't match a simple aggregation pattern, the full expression is preserved as-is for query-time evaluation.
116117

117118
---
118119

119120
## Query Semantics
120121

121-
The Yardstick adapter works in tandem with Sidemantic's query rewriter to support the `SEMANTIC SELECT`, `AGGREGATE()`, and `AT` modifiers described in the Measures in SQL proposal.
122+
The Yardstick adapter works in tandem with Sidemantic's query rewriter to support `SEMANTIC SELECT`, optional-prefix `AGGREGATE()`, and `AT` modifiers described in the Measures in SQL proposal.
122123

123124
### SEMANTIC Prefix
124125

125126
| Feature | Status |
126127
|---------|--------|
127128
| `SEMANTIC SELECT ...` | Supported (enables measure-aware query rewriting) |
128129
| `SEMANTIC WITH ... SELECT ...` | Supported (CTEs within semantic queries) |
129-
| Implicit measure detection without `SEMANTIC` prefix | Supported (queries containing `AT` modifiers or curly-brace measure references are auto-detected) |
130+
| `SELECT ... AGGREGATE(measure)` without `SEMANTIC` prefix | Supported |
131+
| `CREATE TABLE ... AS SELECT ... AGGREGATE(...)` | Supported |
132+
| `INSERT INTO ... SELECT ... AGGREGATE(...)` | Supported |
133+
| Implicit measure detection without `SEMANTIC` prefix | Supported (queries containing `AGGREGATE()`, `AT` modifiers, or curly-brace measure references are auto-detected) |
130134

131135
### AGGREGATE() Function
132136

@@ -139,7 +143,36 @@ The Yardstick adapter works in tandem with Sidemantic's query rewriter to suppor
139143
| `AGGREGATE()` in arithmetic expressions (`2 * AGGREGATE(revenue)`) | Supported |
140144
| `AGGREGATE(measure) / AGGREGATE(measure) AT (...)` | Supported (each AGGREGATE evaluated independently) |
141145
| Scalar `AGGREGATE()` without GROUP BY | Supported (produces a single grand-total row) |
142-
| `AGGREGATE()` without `SEMANTIC` prefix and without `AT` | Error: raises `ValueError` requiring the `SEMANTIC` prefix |
146+
| `AGGREGATE()` without `SEMANTIC` prefix and without `AT` | Supported |
147+
| Native DuckDB `aggregate(list, 'function')` | Supported (falls through to DuckDB; not treated as Yardstick syntax) |
148+
149+
### Upstream Parity Tests
150+
151+
The default test suite replays a vendored Yardstick `measures.test` fixture for stable CI coverage. To check against the live upstream Yardstick repository without copying fixtures into Sidemantic, run:
152+
153+
```bash
154+
SIDEMANTIC_YARDSTICK_UPSTREAM_TESTS=1 uv run pytest -q tests/queries/test_yardstick_measures_replay.py -m yardstick_upstream
155+
```
156+
157+
The same command runs in the `Yardstick Upstream Parity` GitHub Actions workflow. That workflow runs nightly, can be triggered manually with a Yardstick ref override, and runs on pull requests that touch Yardstick-specific code or tests.
158+
159+
The live replay fetches `https://github.com/sidequery/yardstick.git` at `main` by default, checks all upstream `test/sql/*.test` files, and validates both facets:
160+
161+
| Facet | Coverage |
162+
|-------|----------|
163+
| Model/metric definitions | Parses every upstream `CREATE VIEW ... AS MEASURE` statement and asserts model name, source table/base SQL, primary key, Yardstick metadata, dimension SQL/type/granularity, and metric `agg`/`sql`/`filters`/`type` |
164+
| Query execution | Replays every upstream query block against Sidemantic's Yardstick rewriter and compares result rows |
165+
166+
The live definition check covers the `CREATE VIEW ... AS MEASURE` definitions used by Yardstick's SQL tests. Sidemantic's native SQL definition parser owns `MODEL(...)`, `METRIC(...)`, and `DIMENSION(...)` files separately from the Yardstick adapter; the live upstream replay does not treat Yardstick's top-level `yardstick_definitions.sql` helper file as part of the SQL-test corpus.
167+
168+
Optional environment variables:
169+
170+
| Variable | Purpose |
171+
|----------|---------|
172+
| `YARDSTICK_UPSTREAM_PATH` | Use an existing local Yardstick checkout instead of fetching |
173+
| `YARDSTICK_UPSTREAM_REPO` | Override the upstream Git URL |
174+
| `YARDSTICK_UPSTREAM_REF` | Override the ref fetched from upstream |
175+
| `YARDSTICK_UPSTREAM_CACHE_DIR` | Override the temporary checkout path |
143176

144177
### AT Modifiers
145178

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ testpaths = ["tests"]
169169
pythonpath = ["."]
170170
markers = [
171171
"integration: marks tests as integration tests requiring external services (deselect with '-m \"not integration\"')",
172+
"yardstick_upstream: opt-in tests that fetch and replay live upstream Yardstick SQL tests",
172173
]
173174
addopts = "-m 'not integration' --cov=sidemantic --cov-report=term-missing" # Skip integration tests by default and show coverage
174175

sidemantic/adapters/yardstick.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,15 @@ def __init__(self, dialect: str = "duckdb"):
102102
exp.Variance: "variance",
103103
exp.VariancePop: "variance_pop",
104104
}
105-
_ANONYMOUS_AGGREGATIONS: set[str] = {"mode"}
105+
_ANONYMOUS_AGGREGATIONS: set[str] = {
106+
"entropy",
107+
"geometric_mean",
108+
"kurtosis",
109+
"mode",
110+
"product",
111+
"skewness",
112+
"weighted_avg",
113+
}
106114

107115
def parse(self, source: str | Path) -> SemanticGraph:
108116
"""Parse Yardstick SQL files into a semantic graph."""
@@ -344,6 +352,8 @@ def _has_aggregate_semantics(self, expression: exp.Expression) -> bool:
344352
return True
345353

346354
for node in expression.walk():
355+
if isinstance(node, exp.List):
356+
return True
347357
if isinstance(node, exp.Anonymous) and (node.name or "").lower() in self._ANONYMOUS_AGGREGATIONS:
348358
return True
349359
return False

sidemantic/sql/aggregation_detection.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,20 @@
1010
# SQLGlot treats some engine-specific aggregate functions as Anonymous.
1111
# Keep this focused on known aggregate forms we need to support.
1212
_ANONYMOUS_AGGREGATE_FUNCTIONS = {
13+
"entropy",
14+
"geometric_mean",
15+
"kurtosis",
1316
"mode",
17+
"product",
18+
"skewness",
19+
"weighted_avg",
1420
}
1521

1622
_AGGREGATE_REGEX = re.compile(
17-
r"\b(sum|count|avg|min|max|median|stddev|stddev_pop|variance|variance_pop|mode|quantile|percentile)\s*\(",
23+
r"\b("
24+
r"sum|count|avg|min|max|median|stddev|stddev_pop|variance|variance_pop|mode|"
25+
r"quantile|percentile|product|entropy|kurtosis|skewness|geometric_mean|weighted_avg|list"
26+
r")\s*\(",
1827
re.IGNORECASE,
1928
)
2029

@@ -25,6 +34,8 @@ def expression_has_aggregate(expression: exp.Expression) -> bool:
2534
return True
2635

2736
for node in expression.walk():
37+
if isinstance(node, exp.List):
38+
return True
2839
if isinstance(node, exp.Anonymous) and (node.name or "").lower() in _ANONYMOUS_AGGREGATE_FUNCTIONS:
2940
return True
3041

0 commit comments

Comments
 (0)