Repository navigation
Conversation
…rences do not name a declared field The spec's Name Resolution section says primary_key, unique_keys, relationship from_columns/to_columns and metric expressions refer to a dataset's declared fields, not to columns of its source. validate.py did not check any of that, so a document whose keys name source columns, or whose metric reads SUM(orders.o_totalprice) against a field called total_amount, passed. Three checks report each such reference, one per rule. Keys and relationship columns have to be declared fields of their dataset. Metric expressions are parsed with sqlglot and every column has to be a declared field written as dataset.field: an unqualified name, a name that is another metric, a dataset the model does not declare and a field the dataset does not are each told apart. Dialects sqlglot cannot parse are skipped, an expression that fails to parse is left to the SQL check, and a bare date part such as the day in DATEDIFF(day, a, b), which the default parser reads as a column, is not reported. The same section makes a bare name in a field expression a source column. When another field of the same dataset carries that name, the author may have meant the field, so that is reported too, unless the other field is the same-named column passed through, read from its parsed definition, in which case both readings agree. The findings are warnings for now: at least one converter still emits a metric over a name that is not a field and runs validate.py in its tests, so an error would turn that suite red before the converter has caught up. examples/renamed_fields.yaml is a small model whose field names differ from their source columns throughout, and validation-ci validates it. The test fixtures now declare the fields their keys refer to. Generated-by: Claude Code
| anonymous = isinstance(parent, exp.Anonymous) and parent.name.upper() in DATE_PART_FUNCTIONS | ||
| if column.table or not (isinstance(parent, typed) or anonymous): | ||
| return False | ||
| units = exp.TimeUnit.UNABBREVIATED_UNIT_NAME |
There was a problem hiding this comment.
exp.TimeUnit.UNABBREVIATED_UNIT_NAME has no MONTH. Its keys are single letters (D, H, M, Q, ...) and its values are DAY, HOUR, MINUTE, QUARTER, WEEK, YEAR, etc. So DATEDIFF(month, orders.a, orders.b), DATEADD(month, 1, orders.a) and TIMESTAMPDIFF(MONTH, ...) all warn "'month' is not qualified". DATE_PART(month, ...) warns the same way on DATABRICKS, and so do dd and days.
sqlglot already has the full alias table in Dialect.DATE_PART_MAPPING:
| units = exp.TimeUnit.UNABBREVIATED_UNIT_NAME | |
| parts = Dialect.DATE_PART_MAPPING | |
| return column.name.upper() in parts or column.name.upper() in parts.values() |
This needs from sqlglot.dialects.dialect import Dialect next to the other sqlglot imports.
Could we also add tests for month, dd and days? The current tests only use day, MINUTE and year.
There was a problem hiding this comment.
Done in 8c29e26: Dialect.DATE_PART_MAPPING, and only in the unit position, since that table also has q, w and ms; the Snowflake argument order is handled too. month, days, dd and DATE_PART(month) are in the tests.
| tree = parse_sql(expr, dialect) | ||
| if tree is None: | ||
| continue | ||
| names = [c.name for c in tree.find_all(sqlglot.exp.Column) if not c.table] |
There was a problem hiding this comment.
This collects every unqualified column without the is_date_part filter that validate_metric_references applies.
Take a dataset with a field day (EXTRACT(DAY FROM order_date)) and a field days_open (DATEDIFF(day, opened_at, closed_at)). It warns "'day' is the source column, not the field 'day' ...". Date dimensions often have fields named day, month or year.
| names = [c.name for c in tree.find_all(sqlglot.exp.Column) if not c.table] | |
| names = [ | |
| c.name for c in tree.find_all(sqlglot.exp.Column) | |
| if not c.table and not is_date_part(c) | |
| ] |
For month it depends on the is_date_part fix above.
There was a problem hiding this comment.
Done in 8c29e26, with a day / days_open case.
| f"'{column.name}' is not qualified; metric expressions refer to fields " | ||
| f"as dataset.field" | ||
| ) | ||
| elif column.table not in fields: |
There was a problem hiding this comment.
Names are compared exactly here. The same happens in undeclared(), other_metrics, by_name and passes_through. But expression_language.md (in "Namespacing and Identifier Resolution") says regular, unquoted identifiers are case-insensitive: id matches Id and ID.
As written, SUM(ORDERS.TOTAL_USD) over dataset orders warns "references unknown dataset 'ORDERS'". And Total_USD * 2 next to a field total_usd misses the shadowing this check is meant to catch.
Could we compare normalized names everywhere? For example:
def fold(name: str) -> str:
"""Regular identifiers are case-insensitive (expression_language.md)."""
return name.upper()Then apply fold() to both sides of the dataset and field lookups, the metric-name set, by_name and passes_through. The spec leaves quoted versus unquoted comparison to each database, so folding everything seems the safer choice for a warning, and it keeps "amount" working as a pass-through.
There was a problem hiding this comment.
Done in 8c29e26: fold() on both sides of every lookup; messages keep the spelling as written.
| assert _field_reference_findings(document) == [] | ||
|
|
||
|
|
||
| def test_pass_through_compares_names_as_written() -> None: |
There was a problem hiding this comment.
Under the spec's case rule, a field amount defined as AMOUNT is a pass-through, so this test should expect no finding.
Could we flip it? Also we could add a case where the casing differs and a warning is still expected, such as Total_USD * 2 next to a field total_usd.
There was a problem hiding this comment.
Flipped in 8c29e26, and Total_USD * 0.9 next to total_usd is the case that still warns.
| return errors | ||
|
|
||
|
|
||
| def declared_fields(model: dict) -> dict[str, set[str]]: |
There was a problem hiding this comment.
I guess we have to merge #486 first to be consistent here.
… part as a unit The three Name Resolution checks compared names as written, while expression_language.md makes regular identifiers case-insensitive. So SUM(ORDERS.TOTAL_USD) over dataset orders warned about an unknown dataset, and Total_USD * 2 next to a field total_usd escaped the shadowing check. fold() now upper-cases both sides of every lookup: the declared fields, the metric names and the pass-through test. is_date_part read sqlglot's UNABBREVIATED_UNIT_NAME, which has no MONTH and none of the aliases, so month, dd and days in DATEDIFF, DATEADD and DATE_PART warned as unqualified columns. It now reads Dialect.DATE_PART_MAPPING, and the shadowing check applies the same filter, so a field named day next to DATEDIFF(day, ...) is quiet. Generated-by: Claude Code
|
Pushed a commit for the first four; the fifth waits for #486.
300 tests; the validation workflow passes locally on current main. |
Summary
The Name Resolution section that #486 adds to spec.md settles what a name means in each place of a model: a bare name inside a field expression is a column of the dataset's
source, whileprimary_key,unique_keys, relationshipfrom_columns/to_columnsand metric expressions name declared fields, the last qualified asdataset.field. validate.py checks none of that today, so a model whose keys name source columns, or whose metric readsSUM(orders.o_totalprice)against a field calledtotal_amount, passes. Issue #602 asks for the three checks this adds.Three checks, one per rule.
validate_key_referencesreports a key or relationship column that is not a declared field of its dataset.validate_metric_referencesreports a metric column that is unqualified, names another metric, names an unknown dataset or names an undeclared field, each told apart in the message.validate_field_shadowingreports a field expression whose bare name is also the name of another field in the same dataset, where the author may have meant the field but the spec reads the source column. That last one stays quiet when the other field is the same-named column passed through, read from its parsed definition, since both readings then agree, when it has no SQL definition to compare, and when a field names its own source column, as in the spec'samountexample. A qualifiedorders.total_usdinside a field expression is not covered, since #602 asks about bare names. Metric and field expressions are parsed with sqlglot per dialect, the dialects it cannot parse are skipped, an expression that does not parse is left to the SQL check, and a bare date part such as thedayinDATEDIFF(day, a, b), which the default parser reads as a column, is not reported. Names are compared as written, the wayvalidate_referencescompares dataset names.Every finding is a warning for now, as #602 suggests for the rollout. A sweep of the 26 semantic-model documents in this tree finds 59 keys, relationship columns and metric references the first two checks report, in 15 converter fixtures and the TPC-DS example, 7 of them metrics that refer to other metrics, and the NVIDIA converter's generated document, which its own tests run through validate.py, still carries a metric over
orders.subtotal, which is not a field. The shadowing check reports nothing in the tree. Promoting the first two to errors is a one-line change once the converters have caught up.examples/renamed_fields.yamlis the spec example as a complete model: every field name differs from its source column, and validation-ci validates it. The test fixtures now declare the fields their keys refer to, which is the validation-side instance of the fixture updates #486 anticipates. Until #486 lands,examples/tpcds_semantic_model.yamlreports itsss_ticket_numberkey as undeclared, which is the field #486 declares.Verification
validation/tests/: 285 passed, 52 of them new, also in random order, one of them through the CLI.validation/test_validate.pyOK. The validation workflow run locally against current main (53418ba) on Python 3.11 to 3.14.examples/renamed_fields.yamlandexamples/flights.semantic_model.yamlpass clean.examples/tpcds_semantic_model.yamlpasses with the twoss_ticket_numberwarnings described above.Related Issues
#602. The wording follows the Name Resolution section as it stands in #486 and will be adjusted if that text changes before it lands.
Checklist
Validation
validation/are updated if the spec changedExamples
examples/are added or updated for any new spec constructs or converter supportTests
pytest/ CI green)Compliance