Rules¶
49 rules, generated from the registry. varda rules prints the same list.
Severity is what the rule ships with. A repository overrides it in varda.toml, and an extension may propose a different default — see Extending.
The annotations themselves¶
Whether a model's annotations are ones a profile declares. Without these, the difference between a constraint that is enforced and one that is not is a typo nobody can see.
V001¶
error — Annotations are declared in a profile
V002¶
error — Annotation values come from the declared enum
V003¶
warning — Annotation prefixes belong to something
Flag an annotation whose prefix names no active extension.
A warning rather than an error, and the distinction is the whole point of the rule. A model annotated by a team whose extension is not installed here is still a legal model — it is simply being read somewhere that cannot interpret part of it. Erroring would make models unportable; saying nothing would let a typo'd prefix hide a constraint that never applies.
V004¶
error — Structured annotations use declared fields
Flag a field name inside a structured annotation that is not declared.
V001 catches a misspelled annotation name, and stops there. One level
down the same typo is just as silent and reads worse: levles: for
levels: was reported as a hierarchy with zero levels, and colunm: for
column: as level '' is not a column. Neither message names the
mistake, and both describe a model nobody wrote.
Checked against the profile that declares the range, so an extension
introducing its own structured annotation is checked on the same terms as
varda:hierarchies.
V005¶
error — A declared metaclass governs what declares it
LinkML's instantiates, checked against the profile it names.
A model may say which profile class governs an element — the standard way to state a relationship Varda otherwise keeps to itself. Optional, because it restates what the annotations already imply. Checked when written, because a declaration nobody verifies is worth less than none: the two failures are naming a class no active extension declares, and naming the wrong one, and the second is the one that reads correctly.
Roles, and where each one is legal¶
What a table is, what a column is, and the placements that make no sense. Every other band reads the roles these establish.
V101¶
error — Every table declares a role
V102¶
error — Every column declares a role
V103¶
error — Roles sit on the right kind of table
V104¶
error — Table annotations sit on the right role
Flag a table annotation on a kind of table it does not describe.
varda:fact_type is the temporal shape of a fact; varda:scd is how a
dimension answers a change. Neither means anything on the other kind of
table, and neither is inert when misplaced: generators read both, so an
scd on a fact emits DDL commented as keeping history the fact does not
keep.
V103 is this check for column roles. This is the same one a level up.
Grain¶
What one row of a table is — declared as a column set a validator can test, and as a sentence it cannot.
V201¶
error — Every fact declares its grain
Flag a fact whose row identity is undeclared.
An error rather than a warning, on the same reasoning that makes additivity required: a fact whose grain is unknown is one whose measures cannot be safely aggregated through any join, so accepting it silently postpones the failure to whoever queries it.
V202¶
warning — Grain is stated as a sentence
Flag a missing or too-short grain sentence.
The length threshold is crude on purpose. It cannot tell a good sentence
from a bad one, and pretending otherwise would make this an argument
rather than a check. What it can catch is grain_statement: daily — a
word where a sentence belongs, which is the form the failure almost
always takes.
V203¶
error — Grain columns are real and distinct
Flag a grain naming a column the table lacks, or naming one twice.
Both failures are silent by construction, in the same way V703's is. An unknown name is a claim about row identity that can never be checked against anything and looks exactly like one that can — and because the generator resolves the grain to columns it can find, the emitted constraint quietly covers fewer columns than were declared, which rejects legitimate rows and reads like broken source data.
A repeated name is the same mistake wearing a different hat: it adds nothing to the constraint and means the modeler listed something twice without noticing.
Checked on any table that declares a grain rather than on facts alone.
Requiring one is a fact's business — V201 — but a grain that is wrong is
wrong wherever it appears, and examples/retail.yaml puts one on a
bridge.
V204¶
error — Grain columns locate a row
Flag a grain built from columns that cannot identify a row.
A grain is what a row is, not what it records. Only foreign keys and degenerate dimensions place a row in the model's dimensional space, so only those can compose a grain. A measure in a grain is the diagnostic form of a real confusion — a fact whose identity is defined by one of its own measurements is one where a second measurement of the same event silently becomes a second row.
Identity¶
What makes two rows the same thing: the surrogate key facts join to, the natural key a loader matches on, and the uniqueness a model declares for itself.
V301¶
error — Every dimension has exactly one surrogate key
V302¶
error — Every dimension has a natural key
Flag a dimension with no business identity.
Without a natural key there is nothing for a loader to match on, so every load either creates duplicate rows or has the matching rule written somewhere the model cannot see.
V303¶
error — Unique keys name real columns
Flag a unique_keys entry naming a column the table does not have.
LinkML accepts one without complaint — a key over a misspelled slot loads clean and constrains nothing — so this is the only thing standing between a declared uniqueness claim and no constraint at all.
V304¶
error — A type-2 business key includes its version
Flag a business unique key on a type-2 dimension with no version.
A type-2 dimension keeps a row per change, so its business key repeats once per version. A unique key over business columns alone says it does not, which is false about the table and would reject the second version of every row.
Only keys carrying a natural key are checked: one over the surrogate key is unique already and needs nothing added.
V305¶
warning — Natural keys are covered by a unique key
Flag a natural key no declared unique key covers.
Only where a table declares its own unique_keys, because a declared
key replaces the derived one: a natural key named in none of them is a
business identity nothing enforces. Where nothing is declared, one
natural key derives its own constraint and several are V306's, which is
an error rather than this warning — there the answer is not missing, it
is unknowable from the roles.
V306¶
error — A dimension with several natural keys declares them
Flag several natural keys with no unique_keys to disambiguate them.
Two columns marked NATURAL_KEY mean one of two things, and a role cannot
say which. Either they are one compound identity — a store known by its
chain code and its store number — or they are two alternative ones, a
product carrying a barcode from one source and a supplier's part number
from the other. The two want opposite constraints: UNIQUE (a, b) for
the first, UNIQUE (a) and UNIQUE (b) for the second.
Varda used to pick the first, silently, for both. On a table that meant
the second that constraint is weaker than either key alone — two rows may
share a barcode as long as their part numbers differ — and a NULL on
either side leaves the row unconstrained altogether, which is the normal
state of a table loaded from two sources that each fill one column. The
model passed --strict with nothing to say.
An error rather than a warning because both silent outcomes are wrong:
the merged constraint enforces something nobody meant, and deriving
nothing leaves a dimension whose identity the database does not hold.
The model has to say which, and unique_keys is where LinkML already
says it.
V307¶
warning — A lone natural key is required
Flag a dimension whose only business identity may be absent.
SQL counts NULLs as distinct, so UNIQUE (gtin) over a nullable column
admits any number of rows that have no gtin at all — the identity is
enforced for every row except the ones that do not have it, which are the
rows a duplicate load produces.
Only where there is one natural key, and that scoping is the rule. Where a dimension has several, being absent is usually the point: a product read from a barcode feed has no supplier part number and one read from a supplier catalog has no barcode, and each row fills the column its source knows. Warning there would fire on every well-formed table of that shape and be switched off, taking this with it.
References between tables¶
Foreign keys and what they may point at. A star becomes a normalized schema one wrong reference at a time.
V401¶
error — Every fact has at least one foreign key
V402¶
error — Every foreign key names its target
V403¶
error — Foreign key targets exist
V404¶
error — Foreign keys point at dimensions or bridges
Flag a foreign key aimed at a fact.
A fact referenced by another fact is the most common way a star quietly becomes a normalized schema: the join is now fact-to-fact, the grain of the result is neither table's, and no aggregate over it is safe.
V405¶
error — A bridge references something
Flag a bridge with no foreign key.
A bridge exists to resolve a many-to-many. One referencing nothing relates nothing, and V401 makes the same demand of a fact for the same reason: a table whose whole purpose is to connect others must name at least one.
One rather than two, deliberately. The obvious reading of a bridge is two keys and a weight, but Kimball's group-key form carries only one — the fact points at a group, and the bridge maps that group to a dimension — so requiring a pair would refuse a standard design.
Time¶
How a table behaves as the data behind it changes: a fact's temporal shape, and a dimension's answer to a source that has been updated.
V501¶
warning — Every fact declares its temporal shape
V502¶
warning — Slowly-changing type is declared
Flag a dimension that does not say what happens when a value changes.
A warning rather than an error because a great many dimensions are genuinely type 1 and saying so feels like ceremony. It stays a rule because "we never decided" and "we decided overwrite" look identical in the model and cost very differently two years later.
V503¶
error — Versioning columns belong to a type-2 dimension
Flag a version period on a dimension that keeps no versions.
Type 0 retains the original value and type 1 overwrites it. Neither produces a second row, so a column bounding "this version" describes something the declared type says does not exist. One of the two is wrong, and which one is not for a validator to guess.
V504¶
error — A version period that ends also starts
Flag a version end with no corresponding start.
An end alone bounds nothing. The reverse is not a finding: storing only the start and deriving the end from the next version is a normal design, and Data Vault virtualizes the end column outright.
V505¶
error — At most one column per versioning role
Flag a repeated versioning role on one table.
Two starts is two answers to when a version began, and every consumer picks one — the generator by declaration order, the reader by whichever name looks more official. They will not always pick the same one.
V506¶
warning — A type-2 dimension says how it versions
Flag a type-2 dimension nothing can tell the versions of apart.
Type 2 keeps a row per change, so something must distinguish those rows. Varda does not insist on a mechanism — the field uses a period, a flag and a counter, and calling any one mandatory would reject working designs. It insists that a discriminator be named, because a dimension versioning by a mechanism nobody declared cannot have its uniqueness generated or its current row found by anything but guesswork.
A start instant and a counter discriminate; IS_CURRENT does not. It is
true of exactly one version, so a key carrying it permits every superseded
row to repeat — the constraint would be there and mean nothing. All three
strategies Varda documents pair the flag with a start for this reason.
Hierarchies¶
The named paths a dimension is drilled down. The largest band, and the one whose central claim is least checkable: that a level is a real column is a question about the schema, and that each rolls up into exactly one parent is a question about the data.
V601¶
error — Hierarchy levels name real columns
Flag a level naming a column that does not exist.
The same check V203 makes of the grain, extended to the reference form. A level that names nothing is silently dropped by every generator, so the path a reader is offered is shorter than the one the model claims.
country_key.country_name has three ways to be wrong and each gets its
own message, because "not a column" would send a reader looking in the
wrong table.
V602¶
error — Hierarchy levels are distinct
Flag a column appearing twice in one hierarchy.
A level that is its own ancestor is not a drill path. Usually a copy and paste, and always meaningless.
V603¶
error — A hierarchy has at least two levels
Flag a hierarchy of one level, or none.
One level is a column, not a path. Nothing rolls up into anything, so every consumer that offers a drill-down offers a single step to nowhere.
V604¶
error — Hierarchy names are unique within a table
Flag two hierarchies on one table sharing a name.
A dimension carrying several paths is the normal case — a date dimension has a calendar path and a week path — and the name is the only thing telling a reader which one they are drilling. Two of them answering to the same name makes the choice unresolvable.
V605¶
error — Hierarchy levels are the kind of column a level can be
Flag a level named by a column a reader cannot drill.
A bare foreign key gets its own message, because it is the near-miss a snowflake invites: the coarser levels are their own tables, so the only thing to hand is the key, and a path of keys renders as integers. The reference form reaches past it to something readable.
V606¶
error — Hierarchies belong to dimensions
Flag a hierarchy on a fact or a bridge.
A drill path describes descriptive context, which is what a dimension is. A fact is drilled through its dimensions, and a bridge exists to resolve a many-to-many rather than to be navigated.
V104 is this check for the other table annotations.
V607¶
error — A declared level key identifies
Flag a level key naming a column that cannot identify one.
The key answers "which member", where the level's column answers "what is it called". A key column that does not exist identifies nothing, and one holding a measure or a version marker identifies the wrong thing.
A key is looked for in whichever table supplied the level's column, so a level reached through a foreign key is keyed in the dimension it names rather than in the near table.
Whether the columns are jointly unique is not checked, for the same reason the grain sentence is not: it is a claim about data.
Measures¶
The most expensive class of error a dimensional model produces. A structural mistake usually breaks a query; an additivity mistake returns a number that looks entirely reasonable and is wrong, to someone who will act on it.
V701¶
error — Every measure declares its additivity
V702¶
error — Semi-additive measures name their exception
V703¶
error — The semi-additive exception is a real key
Flag a semi-additive exception naming a column the fact does not have.
The failure this catches is silent by construction: a constraint that names a non-existent dimension is a constraint that never fires, and it looks exactly like one that does.
V704¶
error — Measures live on facts and bridges
Flag a measure on a dimension.
A numeric column on a dimension is usually an attribute — a size, a band, a credit limit. When it genuinely is a measure, the dimension is doing a fact's job and the grain of any aggregate over it is undefined.
V705¶
warning — Every measure declares its unit
Flag a measure with no unit.
Units are LinkML's own unit, so this rule reads a native rather than
an annotation. A unit naming the measure in none of the ways a reader
would recognize counts as undeclared.
V706¶
warning — A fact carries measures, or says it does not
V707¶
warning — Decimal measures declare precision and scale
Flag a decimal measure that does not say what it keeps.
A warning, and scoped to measures on purpose. Every string column in a model could be asked for a width on the same reasoning, and asking would fire a dozen times on a small star and be switched off. This fires rarely and covers the case that costs money: a measure is the number somebody acts on, and a decimal one silently rounded is wrong in the way that still looks like an answer.
Physical naming and types¶
What the generators emit. A physical name identifies one table or one column, or it identifies nothing; a type facet parameterizes the emitted type, or it parameterizes nothing.
V801¶
error — Physical table names are unique
Flag two classes that emit one table.
Everything is generated into one schema, so a physical name identifies a
table or it identifies nothing. Two classes sharing one emit two
CREATE TABLE statements for the same name: the database refuses the
second, and the model quietly describes a table the warehouse does not
have.
It happens without anybody writing the same name twice. DimCustomer
and Dim_Customer both derive dim_customer.
V802¶
error — Physical column names are unique in a table
Flag two columns of one table that emit one column.
The same mistake a level down, and the same silence: the emitted
CREATE TABLE carries the name twice and no database accepts it.
Checked over the induced columns, so a slot inherited from a parent collides with a slot declared here exactly as it would if both were written in the same class.
V803¶
error — Type facets are well formed
Flag a type facet that cannot be emitted as written.
A facet parameterizes the SQL type — the 80 in VARCHAR(80), the 18 and
the 2 in NUMERIC(18, 2) — so it belongs in the physical band with the
names, and it fails in the same silent way they do. varda:max_length
on a date parameterizes nothing and is dropped; a scale with no precision
has no DDL to become. Both leave a model that reads as though it stated
a width and generates output that never had one.
V804¶
error — Every range names a type the schema knows
Flag a range that is not a type this schema can resolve.
LinkML does not object to a range naming nothing, and neither did
anything here: range: intger and range: uuid both passed varda
check with no findings and then stopped varda generate with a
GenerationError. A model that validates and cannot be generated from is
the worst of the two answers, because the first one is the one people
trust.
Checked against the schema's own types rather than against the SQL
generator's table, and the difference is deliberate. curie is a real
LinkML type that Varda has no column type for; that is one generator's
limit, reported by that generator, and an extension emitting something
other than DDL may well handle it. A range naming nothing is wrong for
everybody.