The pinned rules
Databricks publishes the Metric View YAML specification as prose and enforces it whenCREATE VIEW ... WITH METRICS runs in a workspace. Dex reads the committed
YAML without one, so it restates the rules it relies on as its own port, pinned
to one specification version. The catalog mapping is ported from the databricks
spoke of apache/ossie-converters,
which converts Metric View YAML to and from Apache Ossie. A Metric View and its
Ossie rendering therefore agree on what each construct means.
The fingerprint records
format_version: 1.1 and the converter commit
(vendor_release) beside every hash. A later port that reads a file differently,
or a move to another specification version, is reported as that change rather
than as every view changing.
Required extras
Reading the layer installs no Databricks package and opens no workspace
connection. The
[databricks] extra is the warehouse connector. Only a governed
query (#964) needs it.
The YAML body is read; the DDL is not
Each configured file holds one Metric View YAML body: the document thatCREATE VIEW <name> WITH METRICS LANGUAGE YAML AS $$ ... $$ holds between its
delimiters. This is also the format the Ossie converter reads and writes.
- A view is named after its file’s stem, because the YAML carries no name.
views/orders.yamlis the vieworders. The name it is deployed under in Unity Catalog is not read. - Two files with one stem are refused in configuration, case ignored. They would name one view twice.
- A
.sqlfile is refused by name. The DDL bundles deployment settings (OR REPLACE,TBLPROPERTIES, column aliases,COPY *) with the definition. Dex reads definitions and never deploys, so it does not guess the YAML out of a statement.
CREATE VIEW, deploys a materialization, or changes a Unity
Catalog privilege. Reading the layer writes no file.
What a file is checked against
YAML is read the way Databricks reads it:- YAML 1.2 booleans. A join’s bare
on:key stays the texton, where YAML 1.1 would read it astrue. - Duplicate keys. A key written twice in one mapping is refused.
version is required and must be 1.1. 0.1, or any other
version, refuses the view, and the refusal names the re-save that upgrades it
(ALTER VIEW ... AS $$...$$). Copy Unity Catalog comments into comment first,
since the upgrade wipes them.
What refuses the whole view, because Databricks would refuse to create it:
- a missing or empty
source; - neither
fieldsnormeasures; - malformed
parameters: each needs anameand adata_type, and once one parameter has adefault, every later one needs one too; - malformed
materialization:mode: relaxed, a non-emptymaterialized_viewslist with anameand atypeon each, at most oneunaggregatedentry with no dimensions or measures, anddimensionsormeasureson eachaggregatedone; materializationon a view that declaresparameters, which Databricks binds per query, so there is no one result to maintain.
- Field or measure. A non-empty
expr, and an explicitnamethat is not empty. Names are unique across fields and measures, case ignored, and the later of two is refused. display_name. At most 255 characters.synonyms. At most 10 entries of at most 255 characters each.format. Checked bytype(number,currency,percentage,byte,date,date_time) with that type’s keys:- a currency format needs an ISO-4217
currency_code; - a
dateformat needsdate_format, and adate_timeformat needs bothdate_formatandtime_format(Databricks refuses either without, checked withCREATE VIEW ... WITH METRICSin #1258); decimal_placesof typemaxorexactneedsplacesfrom 0 to 10;date_formatandtime_formatcannot both be none.
- a currency format needs an ISO-4217
- Measure
window.orderandrangeare required.rangeiscurrent,cumulative,all, ortrailing|leading <n> [<unit>] [inclusive|exclusive].semiadditiveisfirstorlast.offsetis a signed integer or<n> day|month|year[s].- The number in a
rangeor anoffsetmay be an integer parameter the view declares, by its bare name (trailing days_back day). A name that is no integer parameter of the view is refused.
- Measure
partition.includeis a list of field names, andouter_aggregateis one ofSUM,AVG,MIN,MAX,COUNTorMEDIAN. A measure has awindowor apartition, never both. - Join. A
namethat is an identifier and is notsource, and asource.- It needs
onorusing. With both,onwins, as in Databricks, and Dex warns. cardinalityismany_to_one(the default) orone_to_many, and every join below a join keeps that join’s cardinality.relytakesat_most_one_match: true|false.- Sibling joins have distinct names. A refused join takes the joins below it with it.
- It needs
- Expressions. These are read with the Databricks SQL dialect:
- every
MEASURE(name)names a measure of the view defined before it, which also rules out a cycle. A refused measure retracts the measures that read it, to a fixed point; - no field reads a
one_to_manyjoin. Only a measure can aggregate over one; - a nested join is read through its whole path:
orders.customer.c_name, nevercustomer.c_name.
- every
- An unknown key, at any level,
relyincluded. Databricks refuses it on create, but the rest of the definition is still readable. Authoring refuses it in a file the plan writes (see Authoring). onbesideusing.fieldsand its older spellingdimensionsboth written. Databricks creates the view fromdimensionsand dropsfields, and so does the Ossie converter. Dex readsdimensionstoo and names what it dropped. Authoring refuses it in a file the plan writes.- A field reading a later field’s name, or a
filterreading a field’s name. A field reads only the fields before it, and afilterreads source columns only, so Databricks reads such a name as a source column and refuses the view if there is none. Whether there is one is the warehouse’s to say, so Dex warns rather than refuses. - A field or measure with no
name. It is named by the rule Databricks applies on DBR 19 and later. A plain column keeps its final identifier, including insideCAST; any other expression keeps its text with the whitespace removed (SUM(a+b)). - A wildcard (
source.*,<join>.* EXCEPT (...)). Databricks expands it from the warehouse schema when the view is created. Dex does not expand it, and the members it would add are not listed. A wildcard carrying anameor other metadata is refused. - An expression sqlglot cannot parse. Its column is not resolved.
measures['revenue'],
joins['customer.nation'], fields['status'].format). One broken file leaves the
rest readable, and the catalog raises only when nothing at all could be read.
These rules were held to Databricks itself: each refusal above, and each
reading Dex makes of fields beside dimensions, forward references and nested
paths, was checked against CREATE VIEW ... WITH METRICS on a serverless SQL
warehouse. Where they differ, the difference is deliberate:
- Version
0.1. Databricks still creates it; Dex reads1.1only. - An unknown key. Databricks refuses the view; Dex warns while reading, and refuses it in a file a plan writes.
- A duplicate name. Databricks refuses the view; Dex refuses the later member and keeps the rest while reading. A plan that writes a duplicate name is refused, since any error in the set refuses the plan.
References and lineage
A column is addressed the way the specification addresses it:- In a field or measure,
source.<col>or a bare<col>reads the view’s source.<join>.<col>reads a join, and a nested join is read through its whole path (customer.nation.n_name). - A bare name that matches an earlier field is that field, not a source column of the same name. A field reads only the fields before it.
- In a join’s
on,source.is the view’s source, the join’s own name (or a bare column) is the joined table, and a nested join names its parent by the parent’s name.
expr only where the expression
is a single column. A computed field resolves to no column, and the PII gate is
given its SQL over the one table it reads, so every column it touches is
screened. A resolved name is folded the way the active connector folds an
unquoted identifier: on Snowflake, customer_id is CUSTOMER_ID.
A source or a join’s source depends on how it is written:
- A table name is the relation, read with the active connector’s folding. A name that is not fully qualified (Databricks completes it from the session’s catalog and schema) claims no relation, with a note. The PII gate still matches it by suffix.
- A query is a relation only as
SELECT <columns or *> FROM <one table>with no other clause, and its columns are traced through the projection. - A filtered single-table query keeps its table for the PII gate alone, with a note, because its rows are not the table’s.
- A joined, grouped or otherwise shaped query has no relation.
using, or from on when it is an equality or a
conjunction of equalities between a parent column and a joined column. Any other
condition, such as a range join, declares no pairs, and a note says so.
How a Metric View maps onto the catalog
unavailable declares what a Metric View has no field for:
- a view’s
label,model_ref,agg_time_dimensionandprimary_entity; - a metric’s
time_axisandqueryable_granularities; - a dimension’s
queryable_granularities; - a measure’s
agg_time_dimension; - every entity field.
DATE_TRUNC('MONTH', o_orderdate)).
What the declarations reach in explore
explore relationships|map|profile --use-project read the declaration channel:
- Joins. Each join with key pairs is a declared edge at confidence 1.0,
declared_by: databricks relationship '<view>.<join>'.--verifymeasures it. relykeys. Eachrely.at_most_one_matchkey is a declared unique key on the joined relation. Profiling that finds duplicates says so against “the Databricks Metric View layer”. Databricks does not checkrely, and a wrong one returns wrong numbers, which is why Dex measures it rather than trusting it.- Exposure.
explore map --use-projectmarks each relation a view sits on with that view undersemantic_models.
meta.dex) could live in,
so declaration is never carried from this layer.
query and values are not available yet
A governed Metric View query is SQL over the view with MEASURE(), executed by
Dex through the Databricks connector under the cost guard. That is #964. Until
then:
explore semantic queryrefuses by name.explore semantic valuesresolves the token and runs the values PII gate first, so a personal field is refused as PII before it is refused for want of a runtime.
What maintain makes of the layer
maintain snapshot fingerprints the layer:
- Each view is one semantic model. Its fields and aggregating measures on the source are mapped to their columns.
- Each joined table that is not another view’s source is one semantic model
of its own (
view.join.path). It holds the fields that read it and itsrelykey. - Each measure is one metric. Each join with key pairs is one relationship
oriented child to parent, so its arity is
many_to_one.
maintain semanticreports definition drift: a view, measure or join added, removed or changed. It also reports a column a field reads that the warehouse no longer has.maintain grainmeasures eachrelykey (declared_grain_not_unique) and each join’s declared arity. A contradictedrelykey is worded as the view’s claim (rely.at_most_one_matchon a join to the table), not as the project’s grain: Databricks trusts the flag and returns wrong numbers when it is false.maintain semanticreports a view re-saved in a spec version Dex does not read assemantic_format_changedfor that file, naming it and both versions: the fingerprint records each file’s declaredversionunderdefinition_versions, so the other views are still compared by content.
maintain schema holds each view to the warehouse metadata Dex captured (#1259),
with no workspace connection. Each finding names the view, the element and the
column:
A relation neither the connection nor the baseline lists, and an expression Dex
cannot read, are named in
warnings as unchecked rather than passed.
maintain reconcile cuts each removal out of the view file and stores it as a
semantic databricks update plan (data.semantic_plan_id), validated by the
reader before it is stored, so a reconciled view reads back. A removed field,
measure or join is cut by the span it occupies (a nested join goes with its
parent), a contradicted format is cut from its member, and every other byte is
kept. These stay advisory and are never written:
- a view or a join whose
sourceis gone: a vanished table is as often a rename; - a
filterreading a dropped column: the filter is the view’s population; - a query
sourcereading a dropped column: no member removal repairs it; - a removal a
materializationlists, or one that would leave the view no field and no measure; - a file already holding a key Databricks refuses.
SELECT of its measure columns,
so the Databricks connector leaves Unity Catalog objects of type METRIC_VIEW
out of its inventory. The view’s YAML in the repository is what Dex reads.
The Ossie bridge
semantic databricks define|update|plan --from-ossie <file> proposes a Metric
View from an Apache Ossie document, and
semantic ossie define|update|plan --from-metric-view <file> proposes an Ossie
document from a Metric View file (#1260). Each flag takes several files. Those
whose output is a configured file are stored as one plan, since the target layer
validates every configured file together; the rest are proposed, not stored. The layer written is always the configured semantic.vendor. The
other format is only read, from files inside the repository, and is never
configured. The proposal is a plan that transform apply writes. Nothing is
deployed, and no command opens a workspace connection.
--fact, --name, --into and --dataset-metrics are optional; each is
described below.
Two layers: the port, and what dex adds
The conversion is Dex’s Python port of the converter, at the commit the catalog mapping is pinned to. The port reproduces the converter’s output for every input it accepts and refuses every input it refuses, with the same reason (a YAML syntax error aside, whose text is the YAML library’s). It reproduces the byte-pinned JSON of thecustom_extensions[DATABRICKS] stash
too. The converter’s six fixture directions, its example suite, its
expression-dialect suite and its 300-seed round-trip property suite (with
java.util.Random ported, so seed n is the converter’s seed n) run against
the port.
Dex then changes only what it has to for each proposal to pass Dex’s own
validator for the target layer. Each change is named in a notice whose origin
is dex:
The Java converter, reading a document Dex wrote, sees a key-only field as one
more field and exports it as a dimension.
What is named rather than dropped
data.bridge.conversions[].notices lists every notice, each with its origin.
The converter’s notices keep its exact [scope] message text, including:
- on export,
datatype,dimension.is_time, the members of an objectai_contextother thansynonyms, a dataset’sdescriptionandai_context, the model’sai_context, and foreign-vendorcustom_extensions; primary_keyandunique_keys, which setrely.at_most_one_matchwhere they cover a join’s columns and are otherwise not carried;- a metric naming a dataset reached by two join paths (a diamond), which is dropped;
- a complex field on a fanned-out join, which is dropped;
- a field on a
one_to_manybranch, which cannot be a dimension and is dropped; - synonyms beyond ten, truncated;
- a column that reads one already dropped, which is dropped too;
- on import, a wildcard field or measure, and a view writing both
dimensionsandfields.
--fact. That is the
converter’s --source, renamed because source means a dataset’s relation
everywhere else in Dex.
Stored or proposed
The output goes to--into, or else beside the configured files, named after
the model: <dir>/<model>.yaml for a view and <dir>/<model>.ossie.yaml for a
document.
Several files convert together. The configured outputs are stored as one plan
and the rest are proposed in the same answer. --into and --name name one
output, so they take one file.
- A path in the configured files is validated by the target layer as a
hand-written
--edits-filewould be, with the Metric View write rules for a view, and stored as a plan.definerefuses an existing view or model;updateclassifies it. - Any other path stores no plan. The document is validated the same way and
returned under the
contentof its owndata.bridge.conversionsentry. With no configured output in the batch,plan_idisnull.nextnames thedex config setthat adopts the path and the command to run again (updatewhere every output of the rerun already exists,definewhere none does,planfor a mix).updaterefuses such a path.
The stash
A Metric-View-only construct with no Ossie field rides in the object’scustom_extensions entry for DATABRICKS, as JSON in the converter’s pinned
bytes: {"_v": 1, ...}, ": " and ", " separators, non-ASCII as lowercase
\uXXXX. That covers a view’s filter, parameters and materialization,
the fact a one_to_many import was on, a join’s rely, cardinality and
non-canonical on, a member’s format, a measure’s window, partition and
display_name, and Dex’s key_only. A stash is read strictly: the first
DATABRICKS entry wins with a notice, and data that is not one JSON object, or
that repeats a key, is refused.
Known differences from the converter
- The YAML reader. Both directions read YAML 1.2, as the converter does, so
on,off,yesandnostay text. Dex’s own Ossie reader reads YAML 1.1. A hand-written Ossie document with a bareonvalue therefore reads as text in the bridge and astruein the layer. - YAML timestamps. An unquoted timestamp stays a timestamp on the way through. The converter would write it as epoch milliseconds.
- Recursive aliases. A recursive YAML alias is refused. The converter would recurse into it without end.
- What is not ported. The converter’s serialization failures (a Java bean Jackson cannot write) have no Python counterpart, since a parsed document is always plain data.
Authoring
semantic databricks define|update|plan --edits-file <f> (#1258) authors the
configured files through a stored plan that transform apply writes. Dex never
deploys: nothing runs CREATE VIEW ... WITH METRICS, and no authoring path opens
a workspace connection.
- The write surface is exactly
semantic.databricks.files. A path off it, a.sqlfile, another suffix and a stem another file already names are refused by name, the first with thedex config setthat adopts it. - Edits are whole files, written byte-for-byte, all or none. A file changed after planning is a conflict. The set apply would leave is validated again.
- The whole prospective set is validated by the rules above. Every error
anywhere in the set refuses the plan, and so do two warnings in a file the plan
writes: an unknown key, and
fieldsbesidedimensions. - YAML 1.2, never a 1.1 round trip. A key that is not text is refused while
reading; a
true:key names theon:rewrite a YAML 1.1 writer makes. Dex’s own writers (derivation and annotation) read and write YAML 1.2, soon:is written plain. A file whose YAML aliases expand past 100,000 values, or refer to themselves, is refused too. - Derivation from a declared dbt mart, and annotation with a
declarationspayload, follow the mapping in the transform skill’sdeclarations.md. A Metric View has no metadata key, so the population is carried only as the last sentence of the view’scomment, which the catalog withholds, and the NULL rule, additivity, assumptions and grain are named as not carried. A population is never written as afilter.
references/databricks-metric-views-compatibility.md (edit on main). This page is generated; change the source, not the site.