Skip to content

feat: support dataset-scoped metrics - #343

Open
jklahr wants to merge 10 commits into
apache:mainfrom
jklahr:add-data-set-scoped-metrics
Open

feat: support dataset-scoped metrics#343
jklahr wants to merge 10 commits into
apache:mainfrom
jklahr:add-data-set-scoped-metrics

Conversation

@jklahr

@jklahr jklahr commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Summary

A metric may now be declared on a dataset (datasets[].metrics) as well as on the model (semantic_model.metrics). Both placements use the same Metric structure; only name resolution differs.

datasets[].metrics is optional and absent from every existing model, and semantic_model.metrics is unchanged. Nothing breaks.

Discussed on dev@ossie.apache.org as [DISCUSS] Dataset-scoped metrics. Related: #287, #342.

What this adds

Today every metric lives at the model level, so an aggregation over a single dataset has to be written as though it could span several, and named uniquely across the entire model.

Declaring it on the dataset gives three things the current spec cannot express:

  • A dataset that carries its own aggregations. It can be authored, reviewed and reused across models without each model restating its metrics.
  • Scoped metric names. Two datasets may each declare revenue. Today the second one has to be renamed.
  • Ownership without parsing. A consumer can tell which dataset an aggregation belongs to from where it is declared, instead of inspecting the expression.

Scoping rules

Model-scoped (semantic_model.metrics) Dataset-scoped (datasets[].metrics)
Expression references Fields of any dataset, qualified: dataset.field Fields of its own dataset, qualified: dataset.field; columns of its source, unqualified: column
Name uniqueness Unique across the model Unique within its dataset, and distinct from that dataset's field names
Referenced as metric_name dataset_name.metric_name
  1. A dataset-scoped metric's expression MAY reference the declared fields of its dataset and the columns of its source. A declared field is written dataset_name.field_name; a source column is written unqualified. A qualified reference MUST name a declared field of the declaring dataset. An expression that references another dataset MUST be declared in semantic_model.metrics.
  2. Dataset-scoped metric names MUST be unique within their dataset. Two datasets MAY each declare a metric with the same name.
  3. A dataset-scoped metric name MUST NOT collide with a field name of the same dataset.
  4. A model-scoped metric SHOULD NOT reuse the name of a dataset-scoped metric in the same model. Warning, not error.
  5. An unqualified metric reference resolves to a model-scoped metric. A dataset-scoped metric is referenced as dataset_name.metric_name.

Two spellings, one per namespace. SUM(orders.net_amount) references the declared field, which is how a metric reuses a field's expression instead of repeating it. SUM(tax) references a column of the source that is not declared as a field. Because the spellings differ, a field and a column may share a name without ambiguity, so declaring a field named after an existing column does not change the meaning of an expression already using the bare name.

Placement constrains the expression, not the query. A dataset-scoped metric is joined and grouped like any other, using the model's relationships, so it can be sliced by dimensions of any dataset the model connects.

The spec defines structure and name resolution only. It says nothing about evaluation, grain or traversal, and this proposal does not change that.

Errors, warnings, and what is left alone

Hard error, where one string resolves two ways or a reference cannot resolve at all:

Condition Why
Duplicate metric name within a dataset dataset.name resolves two ways
Metric name equals a field name of the same dataset Same qualified namespace, so orders.amount resolves two ways
Expression references another dataset Belongs in semantic_model.metrics
Qualified reference does not name a declared field orders.tax asks for a field the model does not declare

Warning, exit code 0: a model-scoped metric reusing a dataset-scoped metric's name. References stay unambiguous, and a dataset may be authored independently and reused across models, so it must not become invalid because of a name the surrounding model introduces. The warning is attributed to the model and names the model-scoped metric as the thing to rename.

Not checked, and stated in spec.md so implementations do not add divergent constraints: a model-scoped metric sharing a name with a field or a dataset, since those are spelled differently; whether a bare name is a real column, since that needs catalog metadata the model does not carry; and a qualifier naming neither a dataset nor a field, since it is a local alias, CTE or subquery source.

Open questions

  1. Is the qualified-reference check right as a hard error? SUM(orders.tax) is valid SQL if tax is a column, so erroring enforces Ossie's reading of the qualifier over SQL's.
  2. Is name shadowing right as a warning? Should it block validation instead?
  3. Should a model-scoped metric sharing a field name warn? Currently silent, because it would fire on most real models.
  4. Metrics referencing other metrics is deliberately out of scope, since it affects the expression language rather than placement. Noted so the two stay compatible: under rule 5, a model-scoped metric could reference any metric, and a dataset-scoped metric only metrics of its own dataset.

Prior art

Most semantic layers already distinguish the two placements. Listed alphabetically.

System Single-entity metric Cross-entity Name uniqueness Reference form
AtScale SML metric bound to one dataset metric_calc Global Bare
Cube Measures within cubes Calculated measures Per cube cube.member
Databricks UC metric views Measures in the view Joins inside the view Per view MEASURE(name)
dbt MetricFlow Metrics in a semantic model Top-level metrics Global Bare
Snowflake semantic views tables[].metrics Top-level metrics Per table table.metric

Scoped uniqueness with qualified references, rather than the flat global namespace AtScale SML and dbt MetricFlow use, follows Ossie's existing convention that field names are scoped to a dataset.

Whether some metrics belong at the dataset level rather than above datasets and relationships is an open question in the Ossie Metrics Working Group as well, argued there from the evaluation side: a metric over one dataset has a fixed dimension domain, while one spanning datasets is evaluated on different join paths depending on the filters and dimensions in play. That reasoning lands on the same boundary this proposal draws structurally.

Not included

The TPC-DS example is unchanged. No converter reads datasets[].metrics yet, so moving the example's metrics would silently drop three of them on conversion. Shipping a lossy flagship example is worse than shipping the placement without one; the worked examples live in the Metric Scoping section of spec.md meanwhile. Proposed as follow-ups: a shared iter_metrics(model) helper so converters get both placements right by default, then updating the converters, after which the example can move. converters/polaris carries the same gap the Pydantic model did: its Dataset class has no metrics field.

No CI workflow covers the paths this PR touches. Every workflow is filtered to cli/** or converters/<name>/**, so nothing runs against core-spec/, validation/, python/, docs/ or examples/, and a green check here means no jobs ran. This also affects the validator suite added in #330. Left alone so this PR stays a specification change.

Run locally instead: the validator suite (31 test functions, the 10 from #330 plus 21 added here), the python suite, and 9 of the 11 converter suites. Two Java converters need a JRE I do not have. The one databricks failure is a pre-existing Hypothesis case reproducible on unmodified main. Both examples validate, every fenced YAML block in spec.md parses, and the two snippets spec.md marks INVALID are confirmed rejected by the validator.

Changes

  • core-spec/spec.md: Metric Scoping promoted to ## so the TOC entry resolves; rules, namespace model and worked examples added
  • core-spec/spec.yaml, core-spec/ossie-schema.json: Dataset.metrics added and both metric descriptions documented
  • validation/validate.py: scoping and field/metric collision checks added; one shared expression-parsing helper with caching replaces two duplicated parse paths
  • validation/tests/test_validate.py: 21 test functions added to the suite from Warn when relationship to_columns does not cover a declared key #330
  • python/src/ossie/models.py: OssieDataset.metrics added
  • python/tests/test_models.py: test walking schema $defs to catch schema/model drift
  • docs/index.md, converters/README.md: corrected, both stated that metrics are model-level only

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 are called out in the summary. There are none; the addition is optional and backward compatible

Ontology

  • Ontology changes are consistent with spec changes. None required: ontology.json references SemanticModel by $ref, so it picks up Dataset.metrics without change
  • New or modified terms are defined and documented

Converters

  • Converter logic is updated. Deliberately not done, see "Not included". No converter reads datasets[].metrics; because the example is unchanged, none regresses
  • New converters include tests. No new converters

Validation

  • Validation rules in validation/ are updated
  • New validation cases are covered by tests

Documentation

  • docs/ is updated to reflect user-facing changes
  • New behaviours are documented with examples
  • CONTRIBUTING.md updated if the process changed. Not applicable

Examples

  • examples/ updated for new spec constructs. Deliberately not done, so the flagship example is not lossy through converters that do not yet read the new placement

Tests

  • All existing tests pass. Verified locally; note that no CI workflow covers these paths
  • New functionality is covered by tests

Compliance

  • ASF license headers present on all new source files. No new files
  • No third-party dependencies added

AI disclosure

Per the ASF Generative Tooling Guidance, this contribution was prepared with AI assistance. All specification decisions, the scoping rules, and the responses to review are mine. I have reviewed and verified every change, and the verification results were produced by running the suites rather than asserted.

Comment thread core-spec/ossie-schema.json Outdated
"items": {
"$ref": "#/$defs/Metric"
},
"description": "Dataset-scoped metrics. Expressions must resolve entirely within this dataset and must not traverse relationships or reference fields of another dataset. Names must be unique within the dataset and must not collide with any model-scoped metric name. Referenced from outside the dataset as dataset_name.metric_name. Metrics that span datasets belong in semantic_model.metrics."

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.

How about clarifying here that the dataset scoped metrics are computed at the grain of the dataset's primary key. Also why restrict relationship traversal ?

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.

  1. Agree that dataset-scoped metrics are computed at the dataset's grain. However, this grain should be safely computable without requiring a PK.
  2. Agree with the relationship traversal restriction. To follow on to the layering framing, dataset-scoped metrics belong to the relational / metric layer. In that layer, each dataset acts as a self-contained table that can be independently queried / composed in SQL. If databases supported this layer natively, Ossie datasets could be pushed down to the database layer & used by multiple Ossie models. Relationship traversal is more tied to the multi-table semantics within a Ossie model (i.e. it depends on the graph shape), which would mean that datasets now need to reason about each other in the context of some graph

Comment thread core-spec/spec.yaml
# Represents key calculations like sums, averages, ratios, etc.
#
# The same structure is used in two placements:
# - semantic_model.metrics (model-scoped): may span datasets via relationships

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.

Let's add here that semantic_model.metrics can be used to combine multiple dataset scoped metrics ?

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.

To Josh's PR review questions, this fits with the question on if / how to build metrics from other metrics. That's probably worth a followup PR / discussion since it touches on how to extend the expression language

@jbonofre
jbonofre self-requested a review August 28, 2026 05:12

@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 for putting this PR together - definitely agree with adding dataset-scoped metrics. Conceptually, this fits in nicely with the layered model from the working group.

Most of my questions are around making dataset-scoped metrics consistent with fields in terms of name-uniqueness, how they reference fields in their expressions, and the requirement to always be called with a two-level name. Another question is around what can be an input to a dataset-scoped metric.

With respect to your open questions:

  1. Relationship traversal information seems like a model-level concept since it spans across datasets.
  2. Agreed that defining metric / other reference rules should be a separate discussion since that's a pretty fundamental part of the spec

Comment thread core-spec/spec.md Outdated

**Rules**

1. A dataset-scoped metric's expression MUST only reference fields of the dataset that declares it. It MUST NOT traverse relationships or reference fields belonging to another dataset. A metric that needs to span datasets MUST be model-scoped.

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.

Does the metric's expression reference the dataset's fields, the dataset's source's columns or both (with some disambiguation mechanism)?

In the example below, COUNT(order_id) references a column in the source, not a field in the containing dataset.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Proposal for data set metrics -- SUM(col_name) references a source column. SUM(dataset.field_name) can reference a field in the same data set.

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.

Having differentiation makes a lot sense. Let's chat at the working group as well. This one is quite tricky to get right. For metric views, we have:

  1. source.field: from the table
  2. field: a dimension (if it exists), otherwise it's from source

The wrinkle is that you could have a struct-typed column called source so that source.field is ambiguous. This been a tricky area for development.

To be honest, 1 pager on the name resolution precedence rules (e.g. source, dataset, struct, model) could be valuable in of itself.

Comment thread core-spec/spec.md Outdated
**Rules**

1. A dataset-scoped metric's expression MUST only reference fields of the dataset that declares it. It MUST NOT traverse relationships or reference fields belonging to another dataset. A metric that needs to span datasets MUST be model-scoped.
2. Dataset-scoped metric names MUST be unique within their dataset. Two different datasets MAY each declare a metric with the same name (e.g. `orders.item_count` and `shipments.item_count`).

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.

Should uniqueness apply across both fields & metrics (e.g. can a metric and a field in a dataset have the same name)?

Uniqueness would guarantee that dataset.name referred to exactly one of a field or metric, not both.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

yes

Comment thread core-spec/spec.md Outdated

Rule 1 constrains only what a metric's *expression* may reference. It does not restrict how the metric may be queried. A dataset-scoped metric can still be grouped by, or filtered on, dimensions from other datasets reached through relationships — grouping dimensions are supplied by the consumer at query time and are not part of the metric definition.

For example, a metric declared on `store_sales` as `SUM(store_sales.ss_ext_sales_price)` is dataset-scoped because its expression touches only `store_sales`, yet it remains valid to group that metric by `item.i_brand` or `store.s_state` via the model's relationships. Only a metric whose own expression must reach into another dataset — such as `SUM(store_sales.amount) / COUNT(DISTINCT customer.id)` — needs to be model-scoped.

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.

Should dataset-scoped metric expressions use a single-layer namespace since they can only reference fields from the dataset and/or source (depending on this discussion)?

For example, store_sales-scoped metric would have the expression SUM(ss_ext_sales_price) instead of SUM(store_sales.ss_ext_sales_price). This would be more consistent with fields and lines up with some of the layering framing (i.e. dataset-scoped objects don't reason about other datasets)

If a single-dataset metric references the two level namespace, that seems to be relational metrics layer being aware of concepts (two-layer names) from the layer above.

Comment thread core-spec/ossie-schema.json Outdated
"items": {
"$ref": "#/$defs/Metric"
},
"description": "Dataset-scoped metrics. Expressions must resolve entirely within this dataset and must not traverse relationships or reference fields of another dataset. Names must be unique within the dataset and must not collide with any model-scoped metric name. Referenced from outside the dataset as dataset_name.metric_name. Metrics that span datasets belong in semantic_model.metrics."

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.

  1. Agree that dataset-scoped metrics are computed at the dataset's grain. However, this grain should be safely computable without requiring a PK.
  2. Agree with the relationship traversal restriction. To follow on to the layering framing, dataset-scoped metrics belong to the relational / metric layer. In that layer, each dataset acts as a self-contained table that can be independently queried / composed in SQL. If databases supported this layer natively, Ossie datasets could be pushed down to the database layer & used by multiple Ossie models. Relationship traversal is more tied to the multi-table semantics within a Ossie model (i.e. it depends on the graph shape), which would mean that datasets now need to reason about each other in the context of some graph

Comment thread core-spec/spec.yaml
# Represents key calculations like sums, averages, ratios, etc.
#
# The same structure is used in two placements:
# - semantic_model.metrics (model-scoped): may span datasets via relationships

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.

To Josh's PR review questions, this fits with the question on if / how to build metrics from other metrics. That's probably worth a followup PR / discussion since it touches on how to extend the expression language

Comment thread core-spec/spec.md Outdated

1. A dataset-scoped metric's expression MUST only reference fields of the dataset that declares it. It MUST NOT traverse relationships or reference fields belonging to another dataset. A metric that needs to span datasets MUST be model-scoped.
2. Dataset-scoped metric names MUST be unique within their dataset. Two different datasets MAY each declare a metric with the same name (e.g. `orders.item_count` and `shipments.item_count`).
3. A dataset-scoped metric name MUST NOT collide with the name of any model-scoped metric in the same semantic model. This keeps an unqualified metric reference unambiguous.

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.

Could the rule be that only model-scoped metrics can use unqualified names?

The requirement that unqualified metric references must be unambiguous seems to conflict with rule 2, which could allow for two datasets to have metrics with the same name. Rule 2 seems more consistent with field behavior / having datasets not reason about name uniqueness across each other.

Then, the model could be:

  1. Unscoped names: model-level
  2. Scoped names: dataset-level

Some tools, such as Tableau, support cross-dataset dimensions, which also live in the global namespace. In their model, they have both dataset-scoped metrics/dimensions and model-scoped metrics/dimensions

Comment thread core-spec/spec.md Outdated
| **AtScale SML** | Standalone `metric` object bound to one `dataset` + `column` | Separate `metric_calc` object type | Global across all repositories | Bare `unique_name` |
| **dbt MetricFlow** (v1.12+) | Metrics inside a semantic model | Top-level `metrics` | Global across the project | Bare name |
| **Cube** | Measures within cubes | Calculated measures referencing other measures | Per cube | Qualified — `cube.member` |
| **Databricks UC metric views** | Measures in the view (one flat scope) | Joins declared inside the view | Per metric view | `MEASURE(name)` |

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.

For metric views, it's close to named subqueries when building a view. Each source has a name so within a metric view, field names are <source_name>.<field_name> but the metric view itself acts like a table so its schema has a flat scope

Comment thread core-spec/spec.md Outdated

**Scope restricts the expression, not the query**

Rule 1 constrains only what a metric's *expression* may reference. It does not restrict how the metric may be queried. A dataset-scoped metric can still be grouped by, or filtered on, dimensions from other datasets reached through relationships — grouping dimensions are supplied by the consumer at query time and are not part of the metric definition.

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.

Probably worth clarifying that within the layering proposal, the ability to natively query across datasets from a single interface would be in what is called the dimensional layer (Will was also proposing "presentation layer" as an alternate name)

Comment thread core-spec/spec.md Outdated

**Choosing a placement**

Prefer dataset-scoped for simple aggregations that belong conceptually to one entity — they keep the metric next to the fields it depends on and make the dataset independently interpretable. Use model-scoped for anything requiring a join.

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.

(nit) The aggregations themselves don't necessarily need to be simple. More complex aggregates (e.g. later window calculation extensions) could be dataset-scoped

Comment thread examples/tpcds_semantic_model.yaml Outdated
# store_sales. These may still be grouped by dimensions of other
# datasets (for example item.i_brand or store.s_state) through the
# model's relationships. Referenced as store_sales.<metric_name>.
metrics:

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 tests read this file directly and assert on model-level metrics:

  • converters/omni/tests/test_ossie_to_omni.py test_tpcds_export_matches_expected loads this file and compare the export against tests/fixtures/tpcds_omni
  • converters/omni/tests/test_roundtrip.py test_ossie_roundtrip_up_to_documented_normalizations

Both fail due to this change (cd converters/omni && uv run pytest).

I suggest to update the omni converter to host dataset-scoped metrics (updating the fixture in this PR). That would be my preference.

"$ref": "#/$defs/Field"
}
},
"metrics": {

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.

OssieDataset in python/src/ossie/models.py (the model shipped as the apache-ossie package) has no metrics field. Pydantic's default extra='ignore' means dataset-scoped metrics are discarded on load with no error:

doc = OssieDocument.model_validate(yaml.safe_load(open('examples/tpcds_semantic_model.yaml')))
# store_sales has no `metrics` key; only customer_lifetime_value and
# store_productivity survive to_ossie_yaml(). Three metrics vanish silently.

Adding metrics: list[OssieMetric] | None = None to OssieDataset fixes it.

Worth noting the reason nothing caught this: python/tests/test_models.py:75 only cross-checks the DataType enum against the JSON Schema, so structural drift between the schema and the pydantic model is invisible. A test that walks $defs properties and asserts each one exists on the corresponding model would have failed here.

Comment thread core-spec/spec.md Outdated

**Consumer guidance: flattening to a single metric namespace**

Consumers whose native model has only model-level metrics do not need to represent the two placements separately. Because a dataset-scoped metric's expression resolves entirely within its declaring dataset, that expression is already valid as a model-scoped metric — hoisting requires no expression rewriting.

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.

Every converter reads model-level metrics only:

Converter Location
omni converters/omni/src/ossie_omni/ossie_to_omni.py:187
honeydew converters/honeydew/.../converter.py:136
snowflake converters/snowflake/.../converter.py:169
nvidia converters/nvidia/.../native_converter.py:322
orionbelt converters/orionbelt/.../ossie_to_obml.py:170
databricks converters/databricks/python/.../ossie_to_metric_view.py:168
polaris converters/polaris/.../OssieYamlGenerator.java:72

Convert the new TPC-DS example to any target today and three metrics disappear with no warning. Compare converters/honeydew, which at least warnings.warns when it has to guess.

"Consumers that read only semantic_model.metrics remain valid" is a reasonable spec position, but combined with moving the reference example it means the flagship model is lossy through the entire hub-and-spoke on day one.

Two things would help:

  • A shared hoist helper in the spec tooling (iter_metrics(model) yielding both placements with qualified names), so converters get this right by default rather than each reimplementing it.
  • Stronger wording here: a consumer that ignores datasets[].metrics silently produces an incomplete model, which is a lossy conversion, not a valid one. At minimum it SHOULD warn.

Comment thread validation/validate.py Outdated
if qualifiers is None:
continue

foreign = sorted(q for q in qualifiers if q != dataset_name)

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.

SQL identifiers are case-insensitive, and sqlglot preserves the raw text of unquoted identifiers, so q != dataset_name is comparing raw casing against the YAML name.

To reproduce, use dataset orders, dialect SNOWFLAKE, expression SUM(ORDERS.AMOUNT):

[Scope] Dataset-scoped metric 'orders.total' in model 'm' (SNOWFLAKE) references dataset(s) 'ORDERS'.

So a correct model is rejected, and the error names a dataset that does not exist. Uppercase is the natural casing for Snowflake semantic views, which the PR body names as the motivating consumer.

Fold case before comparing (q.casefold() != dataset_name.casefold()). Strictly correct handling also depends on whether the identifier was quoted, but case-folding unquoted identifiers covers the realistic cases and is a clear improvement on the current behavior.

Comment thread validation/validate.py Outdated
continue
if tree is None:
continue
return {col.table for col in tree.find_all(exp.Column) if col.table}

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.

Verified with sqlglot:

qualifiers("SUM(orders.payload.amount)")  # -> {'payload'}   (not 'orders')
qualifiers("SUM(payload.amount)")         # -> {'payload'}

For a three-part path sqlglot puts the middle part in col.table, so a dataset-scoped metric over a STRUCT/VARIANT column on orders is rejected with references dataset(s) payload'`.

There is no way to express such a metric at dataset scope at all, the only escape is moving it to model scope, where the rule does not apply.

exp.Column has catalog/db/table/this parts, for the three-part case you want to look at col.db (or col.parts[0]) as the potential dataset qualifier, and treat a bare two-part path whose first element is a declared field name as intra-dataset.

Comment thread core-spec/spec.md Outdated

1. A dataset-scoped metric's expression MUST only reference fields of the dataset that declares it. It MUST NOT traverse relationships or reference fields belonging to another dataset. A metric that needs to span datasets MUST be model-scoped.
2. Dataset-scoped metric names MUST be unique within their dataset. Two different datasets MAY each declare a metric with the same name (e.g. `orders.item_count` and `shipments.item_count`).
3. A dataset-scoped metric name MUST NOT collide with the name of any model-scoped metric in the same semantic model. This keeps an unqualified metric reference unambiguous.

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.

Rule 4 makes a dataset-scoped metric reachable as dataset_name.metric_name, which is exactly the form a field already uses. Nothing forbids a metric from taking a field's name in the same dataset, and the validator accepts it:

- name: orders
  fields:
    - name: amount        # ...
  metrics:
    - name: amount        # expression: SUM(orders.amount)

Validation PASSED.

A consumer resolving orders.amount now cannot tell whether it means the row-level field or the aggregate, and the metric's own expression SUM(orders.amount) becomes self-referential under rule 4. Suggest a rule 5: a dataset-scoped metric name MUST NOT collide with the name of any field of the same dataset: plus the corresponding check in validate_unique_names, which already has both name lists in scope.

Comment thread core-spec/spec.md
| `description` | string | No | Human-readable description |
| `ai_context` | string/object | No | Additional context for AI tools (e.g., synonyms, common terms) |
| `fields` | array | No | Row-level attributes for grouping, filtering, and metric expressions |
| `metrics` | array | No | Dataset-scoped metrics whose expressions resolve entirely within this dataset. See [Metric Scoping](#metric-scoping). |

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.

  • docs/index.md:251 (FAQ) — "Metrics are defined at the semantic model level (not within a dataset)"
  • docs/index.md:58 and docs/index.md:321 (glossary) — same claim
  • converters/README.md:169 — "Metrics are aggregate measures defined at the semantic model level"
  • converters/README.md:97 — the dataset property table omits metrics

The converter guide one matters most: an author following it writes a converter that drops dataset-scoped metrics, which is exactly the failure mode already present in all the existing converters.

Comment thread core-spec/spec.md

metrics:
- name: total_revenue
# Model-scoped: spans orders and customers via the relationship

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.

The comment says "spans orders and customers via the relationship", but it applies to a metrics: list whose second entry (customer_count, line 722) is COUNT(DISTINCT customers.id), single-dataset. By the guidance at line 453 ("prefer dataset-scoped for simple aggregations that belong conceptually to one entity") it belongs on customers.metrics.

Commit 4 moved all three single-dataset metrics in the TPC-DS example for exactly this reason, so the two examples in the PR now teach opposite things. Either move customer_count onto customers here too, or scope the comment to revenue_per_customer alone.

Comment thread core-spec/spec.md
expression:
dialects:
- dialect: ANSI_SQL
expression: COUNT(order_id)

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.

The orders dataset above declares exactly one field, amount; order_id appears only in primary_key. So the metric introduced as "unqualified reference to a field of the declaring dataset" references something that is not among the declaring dataset's fields, the example undercuts the point it is making.

Either add order_id to fields, or use COUNT(amount).

Comment thread core-spec/spec.md
5. [Fields](#fields)
6. [Metrics](#metrics)
7. [Examples](#examples)
7. [Metric Scoping](#metric-scoping)

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.

nit: every other numbered entry (1–6, 8) maps to a ## heading, but Metric Scoping is authored as ### Metric Scoping at line 427, nested under ## Metrics. Rendered docs get a top-level entry that jumps into the middle of the Metrics section.

Either promote it to ## Metric Scoping, or drop the TOC entry and keep the in-section link at line 370.

Comment thread core-spec/ossie-schema.json Outdated
"items": {
"$ref": "#/$defs/Metric"
},
"description": "Dataset-scoped metrics. Expressions must resolve entirely within this dataset and must not traverse relationships or reference fields of another dataset. Names must be unique within the dataset and must not collide with any model-scoped metric name. Referenced from outside the dataset as dataset_name.metric_name. Metrics that span datasets belong in semantic_model.metrics."

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.

Let's add here that metrics and fields in the dataset share the same namespace. So the names should be unique across fields and metrics in dataset.

@willpugh willpugh 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.

I would like us to focus on the spec, before the code changes. A few issues:
1). Why not just remove the no-aggregation restriction on fields?
2) We should probably work on the semantics first. Particularly around relationship traversal
3) I'm assuming we are going to want grain locking here, but don't think you covered it.

Josh Klahr and others added 6 commits August 29, 2026 11:22
Allow metrics to be declared on an individual dataset (datasets[].metrics)
in addition to the semantic model (semantic_model.metrics), using the same
metric structure in both placements.

Dataset-scoped metrics are for aggregations that resolve entirely within a
single dataset. They keep a metric next to the fields it depends on and make
a dataset independently interpretable. Metrics that span datasets via
relationships remain model-scoped.

Scoping rules:
- A dataset-scoped metric expression must only reference fields of its own
  dataset; it must not traverse relationships.
- Names must be unique within their dataset. Two datasets may each declare a
  metric with the same local name.
- A dataset-scoped name must not collide with any model-scoped metric name,
  keeping unqualified metric references unambiguous.
- Referenced from outside the dataset as dataset_name.metric_name, mirroring
  how a dataset's fields are already referenced in metric expressions.

Prior art: dbt MetricFlow (v1.12+) supports the same two-placement split,
reserving in-model metrics for single-semantic-model metrics and top-level
metrics for cross-model ones. Cube scopes measures to cubes with qualified
cube_name.member references. Ossie follows Cube's scoped-uniqueness and
qualified-reference convention because it matches how Ossie already treats
fields.

Dataset-scoped metrics reuse the existing Metric schema definition, so they
inherit any future additions to the metric shape automatically.

validate.py gains three checks that JSON Schema cannot express: duplicate
metric names within a dataset, collisions with model-scoped metric names, and
cross-dataset references in dataset-scoped expressions. Scope checking
degrades gracefully for dialects sqlglot cannot parse (MDX, TABLEAU, MAQL)
rather than reporting false positives.

.... Generated with [Cortex Code](https://docs.snowflake.com/en/user-guide/cortex-code/cortex-code)

Co-Authored-By: Cortex Code <noreply@snowflake.com>
Add Snowflake semantic views and Databricks Unity Catalog metric views to
the prior-art comparison, alongside dbt MetricFlow and Cube.

Snowflake is the closest analogue: table-level metrics scoped to a logical
table plus top-level derived metrics that combine metrics across tables,
with qualified table.metric references. Databricks takes a different
approach, with a single flat scope per metric view and joins declared
inside the view.

Also records that this proposal is deliberately stricter than Snowflake on
scope enforcement: Snowflake permits table-level metrics to traverse
relationships via using_relationships, whereas dataset-scoped metrics here
must resolve within their own dataset. Notes the rationale (a strict
boundary is legible and can be relaxed compatibly later) and leaves the
question open for the community.

.... Generated with [Cortex Code](https://docs.snowflake.com/en/user-guide/cortex-code/cortex-code)

Co-Authored-By: Cortex Code <noreply@snowflake.com>
Add AtScale SML as a fifth reference point. SML demonstrates a third
placement pattern: a standalone, globally-named metric object that
declares its binding by property (dataset and column are both required),
with cross-entity calculations as a separate metric_calc object type.

This positions the proposal between the extremes rather than at one end:
SML binds a plain metric to a single column with a single aggregation
method; Snowflake permits table-level metrics to traverse relationships;
Ossie permits an arbitrary expression over the declaring dataset but no
traversal.

Also adds consumer guidance on flattening to a single metric namespace.
Because a dataset-scoped expression resolves within its declaring dataset,
it is already valid as a model-scoped metric, so consumers that support
only model-level metrics can hoist without rewriting expressions. Notes
the two real caveats: flattening requires name qualification, and
consumers reading only semantic_model.metrics will not observe
dataset-scoped metrics.

.... Generated with [Cortex Code](https://docs.snowflake.com/en/user-guide/cortex-code/cortex-code)

Co-Authored-By: Cortex Code <noreply@snowflake.com>
Move the three single-dataset metrics in the TPC-DS example into
store_sales.metrics, leaving the two that genuinely span datasets at model
level. The example now demonstrates the placement decision rather than
contradicting the guidance in spec.md.

Dataset-scoped (expressions resolve within store_sales):
- total_sales, total_profit, sales_by_brand

Model-scoped (span datasets via relationships):
- customer_lifetime_value (store_sales + customer)
- store_productivity (store_sales + store)

Referencing these three changes from total_sales to store_sales.total_sales.

Also clarifies in spec.md and spec.yaml that the scoping rule constrains a
metric's expression, not how it may be queried. A dataset-scoped metric can
still be grouped by or filtered on dimensions of other datasets reached
through relationships, since grouping dimensions are supplied by the consumer
at query time. The existing sales_by_brand metric is exactly this case: its
expression touches only store_sales, but its description notes it "requires
grouping by item.i_brand". Without this clarification the rule is easy to
misread as forbidding cross-dataset grouping, which would make the feature
appear far more limited than it is.

.... Generated with [Cortex Code](https://docs.snowflake.com/en/user-guide/cortex-code/cortex-code)

Co-Authored-By: Cortex Code <noreply@snowflake.com>
Incorporates review from jbonofre, christianeu-db and khush-bhatia on apache#343.

Two substantive changes:

- Dataset-scoped metric expressions now reference fields by unqualified name
  (SUM(amount), not SUM(orders.amount)), matching how a field's own expression
  is written. Raised by christianeu-db.
- Name-collision rules are split into errors and warnings. A model-scoped
  metric reusing a dataset-scoped metric's name now warns instead of failing,
  and the warning is attributed to the model rather than the dataset, since a
  dataset may be authored independently and reused across models.

Also reframes the placement throughout: a dataset-scoped metric aggregates data
held by its dataset, and is still joined and grouped through the model's
relationships like any other metric. The earlier "no traversal" wording implied
a restriction on how the metric could be queried, which was wrong.

Validator fixes, each reported with a reproduction:

- Case-fold qualifier comparison, so SUM(ORDERS.AMOUNT) on dataset orders is no
  longer rejected against a dataset that does not exist
- Use Column.parts[0] rather than Column.table, so STRUCT and VARIANT paths are
  not read as dataset references
- Cross-check qualifiers against declared dataset names, so local aliases, CTEs
  and subquery sources are not reported as cross-dataset references
- Extract a single cached _parse_expression helper, replacing two duplicated
  parse paths that had begun to diverge
- Sort collision output for determinism under hash randomisation
- Handle an explicitly null expression without raising AttributeError

Spec and docs:

- Metric Scoping promoted to a top-level section so its TOC entry resolves
- Aggregation grain stated, and explicitly not dependent on primary_key
- New rule: a dataset-scoped metric name must not collide with a field name of
  the same dataset
- Namespace model documented, so the permitted repetitions are stated rather
  than left for implementations to constrain differently
- docs/index.md and converters/README.md corrected; both said metrics are
  model-level only
- Consumer guidance strengthened: ignoring datasets[].metrics is a lossy
  conversion, not a valid one

The TPC-DS example is reverted to its state on main. No converter reads
datasets[].metrics yet, so a flagship example using the placement would be
lossy through every converter.

Assisted-by: Cortex Code <noreply@snowflake.com>
Extends the validator test suite added in apache#330 with cases for the metric
scoping and metric name checks.

Each test under "reported in review" corresponds to a defect found in review of
apache#343 and fails against the validator as it stood before that review: raw-cased
qualifier comparison, three-part STRUCT paths read as dataset references, local
aliases and subquery sources reported as cross-dataset references, a traceback
on an explicitly null expression, non-deterministic collision output, and the
missing field/metric name collision check.

Also covers the deliberately permitted cases, so a later change does not
constrain them by accident: two datasets may reuse a metric name, and a
model-scoped metric may take the name of a field or of a dataset.

Follows the module-loading and importorskip pattern established by the existing
tests. sqlglot is skipped rather than asserted, since the scoping checks no-op
without it and would otherwise pass without asserting anything.

Assisted-by: Cortex Code <noreply@snowflake.com>
@jklahr
jklahr force-pushed the add-data-set-scoped-metrics branch from df8819d to 75118a7 Compare August 29, 2026 16:02
Josh Klahr added 4 commits August 29, 2026 18:14
Rule 1 originally read "a dataset-scoped metric's expression MUST NOT
reference a field of another dataset", justified on the grounds that
such a metric could then be exchanged without resolving the surrounding
join graph. That justification is a claim about evaluation, and this
specification does not define evaluation semantics: there is nothing in
core-spec/ about how a metric is computed, how grain is resolved, or how
fan-out is handled. Constraining authors on the strength of a benefit
the spec never specifies overreaches. Raised by willpugh in review.

Restating it as a placement rule removed the overreach but left the rule
redundant. It is entailed by the unqualified-reference rule: an
expression restricted to unqualified field names cannot reach another
dataset, because a cross-dataset reference requires a qualifier. The
rule stated a consequence and then needed two paragraphs to walk back
the evaluation reading it invited, so it is now dropped and the
consequence folded into rule 1 as a single clause. The remaining rules
are renumbered 1 to 5.

Also drops the aggregation grain paragraph and the corresponding
comparison table row, for the same reason the rule 1 rationale went:
grain is an evaluation concept, and this spec defines structure and
naming rather than evaluation semantics.

No check changes behaviour. Both previously documented INVALID snippets
are still rejected; they are now both rule 1 violations.

Assisted-by: Cortex Code <noreply@snowflake.com>
Matches the rest of spec.md, which uses one em dash in total.

Assisted-by: Cortex Code <noreply@snowflake.com>
Rule 1 said a dataset-scoped metric's expression MUST reference fields of
its declaring dataset. That forces an author to declare a field for a
column they only want to aggregate, and then hide it.

The expression is written against the dataset's source and references its
columns, so nothing needs declaring first. This is not a new rule: it is
how a field's own expression already works. The spec declares a field
customer_id with expression customer_id, which refers to the source
column rather than to the field itself, since referring to the field
would be circular.

That also settles a resolution question the old wording left open. A
declared field does not shadow a source column of the same name, so if a
dataset declares a field foo whose expression is not simply foo, then
SUM(foo) in a metric of that dataset still refers to the source column.
Reusing a declared field's expression inside a metric is the same
composability question as a metric referencing another metric, and is
left to a separate proposal.

No check changes behaviour. validate_metric_scoping keys on dataset
names and never on whether a leaf is a declared field, so permitting
undeclared columns costs nothing mechanically. Two tests lock the
documented behaviour in; both also pass against the previous validator,
because it did not check field declarations either.

Assisted-by: Cortex Code <noreply@snowflake.com>
Rule 1 said a dataset-scoped metric's expression references fields of its
dataset. That forces an author to declare a field for a column they only
want to aggregate, and then hide it. It also narrowed something the spec
already states: the Fields section describes fields as "row-level
attributes that can be used for grouping, filtering, and in metric
expressions".

The expression now reaches both namespaces, with a distinct spelling for
each. A declared field is written dataset_name.field_name, which is how a
metric reuses a field's expression instead of repeating it. A column of
the source is written unqualified.

Two spellings rather than one shared namespace avoids a shadowing rule. A
field and a source column may share a name without ambiguity, so
declaring a field named after an existing column does not change the
meaning of an expression already using the bare name.

This replaces the hard error on self-qualification, since orders.amount
now means the declared field amount, with a check that a qualified
reference names a declared field of the declaring dataset. That is
verifiable from the model, so SUM(orders.tax) is reported and points the
author at SUM(tax). Whether a bare name is a real column stays unchecked,
because it needs catalog metadata the model does not carry, and nothing
else in validate.py checks a field's expression against real columns
either.

validate.py: _leading_qualifiers becomes _qualified_references, returning
(qualifier, name) pairs so the name a qualifier introduces can be
resolved against the dataset's field list.

spec.md: the worked example now demonstrates both spellings on a derived
field rather than two identity fields, and the second INVALID case is a
qualified reference to an undeclared name instead of a self-qualifier.

Tests: 21 functions added overall, collecting 32 cases with the 10 from
 apache#330. Reverting only validate.py to 592db69 fails 11 of the added
functions and none of the 10 pre-existing ones.

Assisted-by: Cortex Code <noreply@snowflake.com>
@jklahr

jklahr commented Aug 29, 2026

Copy link
Copy Markdown
Contributor Author

Replying to each review comment against the current revision. The PR description is rewritten too.

@christianeu-db

@jbonofre

@khush-bhatia

@willpugh

Your three points are the open design questions, so they are in the comment below.

@jklahr

jklahr commented Aug 29, 2026

Copy link
Copy Markdown
Contributor Author

@willpugh, your three points.

1. Why not remove the no-aggregation restriction on fields?

Depends on whether the aggregate is marked, and I think only the marked version is viable.

Unmarked. If a field may hold SUM(amount) with nothing distinguishing it from amount, the dimension/measure split has to be recovered by parsing the expression. Ossie declares seven dialects and validate.py cannot parse three of them (MDX, TABLEAU, MAQL), so for those a consumer cannot determine aggregate-ness at all. Of the four it can parse, four of the six validator defects @jbonofre found were expression-analysis mistakes.

A metric role marker on a field, symmetric with dimension:

fields:
  - name: total_amount
    expression:
      dialects:
        - dialect: ANSI_SQL
          expression: SUM(amount)
    metric: {}

Two things favor it. It reuses the existing pattern for declaring a field's role rather than adding a node. And it makes rule 3 unnecessary, since fields and metrics would share one list and a collision becomes structurally impossible rather than something a validator catches.

Declaration and query surface are separate questions, which is a distinction the Metrics Working Group has been drawing too. A marker on a projected expression answers how a consumer queries an aggregate. A separate list answers how the model declares it. Systems that mark aggregates in SQL still keep dimensions and measures as separate lists in their metadata. Ossie's fields and metrics are declaration, so the marker argument carries over less directly than it first looks.

Two against.

semantic_model.metrics is already a list of Metric, so a dataset-scoped aggregation would be a Field with a marker while a model-scoped one is a Metric. Same concept, two representations. There is a coherent story the other way: a dataset is table-like, and the model level is where standalone metrics live.

The bigger one is the eleven existing converters, because it inverts the failure mode. With a separate metrics: list, a converter that has not been updated drops dataset-scoped metrics, which is detectable and which the spec now says is lossy and SHOULD warn. With a marker on a field, an un-updated converter maps that field to a dimension, because that is what it does with every entry in fields. converters/omni iterates fields and writes each into dimensions unconditionally, so it would publish SUM(amount) as a groupable attribute and give wrong answers rather than missing ones.

Silently wrong is worse than silently absent. That is the point I would want resolved before choosing the marker shape, and if you think the converter risk is manageable I would rather hear the argument than treat it as decisive.

2. Semantics first, particularly relationship traversal

I do not think this blocks, because the proposal adds no evaluation semantics to a spec that defines none. There is nothing in core-spec/ about how a metric is computed, how grain is resolved, or how fan-out is handled, and this does not change that.

Traversal specifically: nothing here restricts how a metric may be queried. A dataset-scoped metric is joined and grouped like any other through the model's relationships. The unsettled question is that Ossie has no mechanism for choosing between multiple relationship paths when more than one connects the same pair of datasets. That affects model-scoped expressions and consumer queries today, independently of this change, and I raised it in #342.

Where the door does close is the naming and namespace rules, since tightening a name rule later invalidates models that were valid before. That is where most of the review has gone, and rule 4 is deliberately a warning rather than an error for exactly that reason.

If you see a way this forecloses an evaluation design, that is worth blocking on and I would rather hear it now than after it merges.

3. Grain locking

Not covered. grain lock, fixed grain and semi-additive appear nowhere in core-spec/, and this PR states no grain default either, so there is nothing here for a grain-locking design to unpick.

Pinning a metric to a grain independent of the query would be a property of a metric, applying identically to both placements since they share the Metric structure. So I do not think placement is the decision that constrains it. Probably merits a separate discussion.

On your opening point about spec before code: the validate.py and models.py changes are fixes to defects @jbonofre found rather than new functionality, but that does pull against his review, so if you would rather this be spec-only I am glad to split them out.

@christianeu-db

Copy link
Copy Markdown
Contributor

@willpugh, your three points.

1. Why not remove the no-aggregation restriction on fields?

Depends on whether the aggregate is marked, and I think only the marked version is viable.

Unmarked. If a field may hold SUM(amount) with nothing distinguishing it from amount, the dimension/measure split has to be recovered by parsing the expression. Ossie declares seven dialects and validate.py cannot parse three of them (MDX, TABLEAU, MAQL), so for those a consumer cannot determine aggregate-ness at all. Of the four it can parse, four of the six validator defects @jbonofre found were expression-analysis mistakes.

A metric role marker on a field, symmetric with dimension:

fields:
  - name: total_amount
    expression:
      dialects:
        - dialect: ANSI_SQL
          expression: SUM(amount)
    metric: {}

Two things favor it. It reuses the existing pattern for declaring a field's role rather than adding a node. And it makes rule 3 unnecessary, since fields and metrics would share one list and a collision becomes structurally impossible rather than something a validator catches.

Declaration and query surface are separate questions, which is a distinction the Metrics Working Group has been drawing too. A marker on a projected expression answers how a consumer queries an aggregate. A separate list answers how the model declares it. Systems that mark aggregates in SQL still keep dimensions and measures as separate lists in their metadata. Ossie's fields and metrics are declaration, so the marker argument carries over less directly than it first looks.

Two against.

semantic_model.metrics is already a list of Metric, so a dataset-scoped aggregation would be a Field with a marker while a model-scoped one is a Metric. Same concept, two representations. There is a coherent story the other way: a dataset is table-like, and the model level is where standalone metrics live.

The bigger one is the eleven existing converters, because it inverts the failure mode. With a separate metrics: list, a converter that has not been updated drops dataset-scoped metrics, which is detectable and which the spec now says is lossy and SHOULD warn. With a marker on a field, an un-updated converter maps that field to a dimension, because that is what it does with every entry in fields. converters/omni iterates fields and writes each into dimensions unconditionally, so it would publish SUM(amount) as a groupable attribute and give wrong answers rather than missing ones.

Silently wrong is worse than silently absent. That is the point I would want resolved before choosing the marker shape, and if you think the converter risk is manageable I would rather hear the argument than treat it as decisive.

2. Semantics first, particularly relationship traversal

I do not think this blocks, because the proposal adds no evaluation semantics to a spec that defines none. There is nothing in core-spec/ about how a metric is computed, how grain is resolved, or how fan-out is handled, and this does not change that.

Traversal specifically: nothing here restricts how a metric may be queried. A dataset-scoped metric is joined and grouped like any other through the model's relationships. The unsettled question is that Ossie has no mechanism for choosing between multiple relationship paths when more than one connects the same pair of datasets. That affects model-scoped expressions and consumer queries today, independently of this change, and I raised it in #342.

Where the door does close is the naming and namespace rules, since tightening a name rule later invalidates models that were valid before. That is where most of the review has gone, and rule 4 is deliberately a warning rather than an error for exactly that reason.

If you see a way this forecloses an evaluation design, that is worth blocking on and I would rather hear it now than after it merges.

3. Grain locking

Not covered. grain lock, fixed grain and semi-additive appear nowhere in core-spec/, and this PR states no grain default either, so there is nothing here for a grain-locking design to unpick.

Pinning a metric to a grain independent of the query would be a property of a metric, applying identically to both placements since they share the Metric structure. So I do not think placement is the decision that constrains it. Probably merits a separate discussion.

On your opening point about spec before code: the validate.py and models.py changes are fixes to defects @jbonofre found rather than new functionality, but that does pull against his review, so if you would rather this be spec-only I am glad to split them out.

RE 1: Josh - those are good points around the limit of parsing for resolving types / performing validation. This could be important if we extend Ossie to consume metrics from inside a source (e.g. an Ossie dataset directly points at a semantic view which meets the single-table invariants, or a metric view). Then, an expression: measure_name could more easily differentiated as someone correctly importing in a measure v. someone accidentally trying to use a measure-typed column as a dimension.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants