Skip to content

docs(core-spec): Let model-scoped metrics evaluate dataset-scoped metrics with AGG() - #703

Open
justint-db wants to merge 1 commit into
apache:mainfrom
justint-db:model-metric-compilation
Open

justint-db wants to merge 1 commit into
apache:mainfrom
justint-db:model-metric-compilation

Conversation

@justint-db

Copy link
Copy Markdown
Contributor

Summary

A model-scoped metric can now evaluate a dataset-scoped metric with AGG(dataset.metric):

AGG(orders.total_revenue) / COUNT(DISTINCT customers.customer_id)

This is the first place model declarations may use AGG, which #662 reserved for a future version.

Rules (spec.md Metric Scoping rule 6 and Name Resolution, expression_language.md Name Spaces and Measure Evaluation: AGG)

  • The argument must be a single dataset.metric reference to a dataset-scoped metric.
  • The call takes no FILTER (WHERE ...) clause.
  • These stay invalid: a bare metric reference such as orders.total_revenue, AGG of a field (fields are never measures), and AGG of a model-scoped metric.
  • AGG stays disallowed everywhere else, including field expressions and dataset-scoped metrics.

Within a dataset, field and metric names share one namespace, so orders.total_revenue always names exactly one thing.

Deferred: model-scoped metrics referring to other model-scoped metrics, FILTER on AGG, and AGG over a source measure in dataset-scoped metrics.

Layer 3 (wide_table_semantics.md §5.2): one sentence saying that an AGG(D.m) call is one component with home dataset D, evaluated as the dataset-scoped metric D.m. This matches how §5.2 already treats a dataset-scoped metric queried directly.

Validator (validation/validate.py)

  • The model-metric reference check from feat(validation): check metric references and field shadowing against Name Resolution #608 accepts AGG(dataset.metric). It warns on a bare metric reference (suggesting the AGG form), AGG of a field or a model-scoped metric, an argument that isn't a single qualified reference, an unknown dataset or metric, and AGG with FILTER. These are warnings, like the rest of that check.
  • A new check reports AGG in a field expression or a dataset-scoped metric as an error, since the spec says MUST NOT.

spec.yaml and ossie-schema.json descriptions are updated to match.

Related Issues

Follows #662 (reserve AGG), #633 (source columns), and #653 (Layer 3).

Checklist

Specification

  • Spec changes are included in core-spec/ and follow the existing structure
  • Spec changes have been discussed on the mailing list or in a linked issue
  • Breaking changes to the spec are clearly called out in the summary (none: this only allows a new reference form)

Validation

  • Validation rules in validation/ are updated if the spec changed
  • New validation cases are covered by tests

Tests

  • All existing tests pass (validation/tests/: 579 passed; python/tests/: 269 passed)

This pull request and its description were written by Isaac.

@kayemkim kayemkim left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two things from running the validator on the branch.

The header still pins sqlglot>=30.12.0, but exp.Agg only exists from sqlglot 30.19.0. With 30.18.0 installed, every document ends in AttributeError while validating …: module 'sqlglot.expressions' has no attribute 'Agg', examples/tpcds_semantic_model.yaml included, because validate_agg_placement and validate_metric_references look for exp.Agg whether or not the text calls it. CI installs the latest sqlglot, so it does not show there; bumping the header to >=30.19.0 closes it.

The checks also reach other dialects. A SNOWFLAKE field expression AGG(x) and a SNOWFLAKE dataset-scoped metric AGG(total_usd) now fail with the [Scope] error, and a SNOWFLAKE model-scoped metric gets the argument and FILTER warnings, while expression_language.md keeps "Expressions in other dialects are not affected" and the new MUST NOT is scoped to OSSIE_SQL_2026. Both places have expression.dialect in hand, so skipping the AGG-specific messages when it is not PORTABLE_DIALECT would match the text.

Two smaller notes. A dataset-scoped metric AGG(order_revenue) is reported twice, first by validate_metric_scoping as an undeclared field order_revenue and then by the new check, and the first message points the wrong way. And AGG(orders.total_usd), AGG(DISTINCT orders.order_revenue), AGG(*) and the FILTER form each get a [Reference] warning and the document passes, while AGG in a field expression is an error, although the spec says MUST NOT for both; an observation, not a request.

The accepted shapes behave as written: agg(...), AGG(Orders.ORDER_REVENUE), the quoted form, AGG inside CASE and COALESCE and the dialects entry form pass clean, and a bare orders.order_revenue gets the new "write AGG(...)" warning. The branch conflicts with #698 and #696 in validate.py, so whichever lands second needs a rebase.

@justint-db
justint-db force-pushed the model-metric-compilation branch from e19b363 to 7b26364 Compare November 9, 2026 18:24
…rics with AGG()

A model-scoped metric may now evaluate a dataset-scoped metric with
AGG(dataset.metric), as in AGG(orders.total_revenue) /
COUNT(DISTINCT customers.customer_id). The argument must be a single
dataset.metric reference, and the call takes no FILTER clause. Bare
metric references, AGG of a field, and AGG of a model-scoped metric
remain invalid. AGG stays disallowed everywhere else.

Layer 3 lowers an AGG(D.m) call as one component with home dataset D.

The validator accepts AGG(dataset.metric) in model-scoped metrics and
reports a misused AGG call as an error, as it does AGG in a field
expression or a dataset-scoped metric. No model written before AGG was
allowed can contain one. A bare metric reference stays a warning, like
the other metric reference findings, with a hint toward AGG. These AGG
checks apply only to OSSIE_SQL_2026, as the spec does. They need sqlglot
30.19.0, the first release with exp.Agg, so the minimum version is
raised.

Co-authored-by: Isaac <no-reply@databricks.com>
@justint-db
justint-db force-pushed the model-metric-compilation branch from 7b26364 to c9844ba Compare November 9, 2026 18:31
@justint-db

Copy link
Copy Markdown
Contributor Author

Thanks @kayemkim, this was a really useful review. I've pushed c9844ba:

  1. sqlglot version: the minimum is now sqlglot>=30.19.0 in both script headers (validation/validate.py and validation/test_validate.py). I confirmed that 30.18.0 has no exp.Agg, and the tests pass on 30.19.0.
  2. Other dialects: the AGG checks now run only on OSSIE_SQL_2026, matching the spec. Your Snowflake field, dataset-scoped metric and model-scoped metric cases no longer get AGG messages. In other dialects, the reference check behaves as it did before this PR.
  3. Double report: a dataset-scoped AGG(order_revenue) now gets a single error from the placement check. validate_metric_scoping no longer reports the name inside the call as an undeclared field.
  4. Warnings vs. errors: good point. Misused AGG in a model-scoped metric is now an error too. That covers an argument that's a field, a model-scoped metric, or not a single dataset.metric reference, plus AGG with FILTER. feat(validation): check metric references and field shadowing against Name Resolution #608 used warnings to avoid failing models that already existed, but no existing model can use AGG, since docs(core-spec): Reserve AGG() in Ossie SQL #662 disallowed it until now. A bare orders.total_revenue stays a warning, because it's the reference check's existing "names another metric" finding. It now suggests the AGG form. Whether the other reference findings should become errors is still the open question you raised on feat(validation): check metric references and field shadowing against Name Resolution #608, so I've left them alone.

@kayemkim kayemkim left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All four are in as described, so the validation side is good from me. On the merged tree the suite passes on sqlglot 30.19.0, the Snowflake cases read exactly as they do on main, a dataset-scoped AGG(order_revenue) gives the one [Scope] line, and each misuse in a model-scoped metric now fails the document. Moving those to errors changes no existing document: the examples, the compliance cases and the converter fixtures all come out the same as on main.

On ordering, this conflicts with #696 and #704 in validate.py. With #696 it is the two column loops, where your id() skip has to move into the iter_columns(tree, expression.dialect) loop that PR introduces, so whichever lands second needs a small rebase.

@christianeu-db christianeu-db left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @justint-db! While the dev@ lazy consensus runs until Friday, I flagged one potential text update that would be nice to address before merging

Ossie SQL reserves the function name `AGG` for evaluating a measure: a column that holds an aggregate calculation instead of a value for each row. The [Relational Query Interface](relational_semantics.md) defines measures (Section 4) and `AGG` (Section 6), including its valid arguments. In `OSSIE_SQL_2026` expressions, an implementation MUST NOT give `AGG` any other meaning. Expressions in other dialects are not affected. Because function names should be case insensitive, the reservation covers every capitalization, such as `agg` and `Agg`.

This version reserves the name only. `OSSIE_SQL_2026` expressions MUST NOT call `AGG` until a future version defines where model declarations may use it.
A model-scoped metric may use `AGG` to evaluate a dataset-scoped metric, as in `AGG(orders.total_revenue)`. The argument is a single `dataset.metric` reference, and the call takes no `FILTER (WHERE ...)` clause (see [Metric Scoping](spec.md#metric-scoping)). `OSSIE_SQL_2026` expressions MUST NOT call `AGG` anywhere else until a future version defines it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Layer 2 Section 6 makes AGG an aggregate function, and the FILTER (WHERE ...) section above says every aggregate function supports FILTER, while rule 6 forbids FILTER on AGG. Could that section name AGG as the exception, or this paragraph say that it overrides that section? Not a blocker.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants