Skip to content

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.

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.