Skip to content

Violation Types

Every violation carries a type, a human-readable message, a remediation (a concrete next step, not just a restatement of the problem), and — where relevant — the column, location, expected, and actual values involved. These appear in contractVerification.violations in a run’s report (see View Verification Results) and in the four-part explanation ContractEnforcementRule prints when it aborts a write.

rejectUndeclaredInputs/rejectUndeclaredFields below are VerificationOptions flags, off by default — set explicitly in code or, to attach either one to a job without changing its source, via spark-submit --conf spark.invaract.rejectUndeclaredInputs=true/ spark.invaract.rejectUndeclaredFields=true. See Fingerprint a Transformation’s Business Logic for both mechanisms worked through in full (it documents computeFingerprint, the third flag in the same case class, the identical way).

column, location, expected, actual and rule mean the same thing on every violation that sets them, so a consumer can group or route findings without special-casing each type:

  • location — the dataset the finding is about. For a missing input or output it is the location the contract declares; for everything the plan really did (an output’s location, format, save mode, catalog or schema, and every rule, data-quality and unverifiable-operation finding) it is the write’s actual location, so a contract with several outputs can tell which write a finding belongs to. Input-schema and input-catalog findings carry the input’s declared location; an organizational-policy finding carries the declared location of the dataset it faulted, when it names one.
  • column — the field path, dotted for a nested field (address.zip).
  • expected / actual — each is set when the finding has that value to report. A comparison sets both (declared type vs. actual type, declared format vs. actual format). A missing field reports only the expected type; an undeclared column only its actual type; a data-quality violation the declared constraint as expected; an unverifiable operation the kind of operation or command as actual.
  • rule — the contract rule type (required_group_by, forbid_cross_join, …) or the organizational policy id that raised the finding.
Kindlocationcolumnexpected / actualrule
MISSING_INPUT, MISSING_OUTPUTdeclared———
UNDECLARED_INPUTthe read———
OUTPUT_LOCATION_MISMATCH / _FORMAT_ / _SAVE_MODE_write—declared / actual—
*_CATALOG_REGISTRATION, *_CATALOG_MISMATCHdataset—catalog (mismatch only)—
MISSING_*_FIELDdatasetpathdeclared type / ——
UNDECLARED_*_COLUMNdatasetpath— / actual type—
*_FIELD_TYPE_MISMATCH, *_FIELD_NULLABILITY_MISMATCHdatasetpathdeclared / actual—
ROLE_CONSISTENCY_VIOLATIONthe input———
DATA_QUALITY_VIOLATIONwritefielddeclared constraint / ——
RULE_*write—the rule’s own columns / what the plan hasrule type
RULE_UNVERIFIABLE_DMLwrite—— / operation kind—
ORG_POLICY_VIOLATIONdataset, when named——policy id
UNVERIFIABLE_WRITE——— / command class—
UNSUPPORTED_CONTRACT_FEATURE——capability / unsupported by <adapter>—
INVALID_CONTRACT————
TypeMeaning
MISSING_INPUTA dataset the contract declares as an input was never read by the plan. A .checkpoint() between the read and the write does not cause this: Invaract sees through it, so an input read before a checkpoint counts as read (see Notification sinks). It is also not reported when the plan contains a lineage boundary Invaract could not see through (for example a checkpoint created before the enforcement rule was installed) — the input is then reported as unverifiable instead, since its absence isn’t proven. An unrelated checkpoint elsewhere in the job never excuses a genuinely missing input. See unverifiableInputs in the notification sinks guide for that report-only alternative. When the output the write lands on declares derivedFrom, only those inputs are expected of that write.
UNDECLARED_INPUTThe plan read a dataset the contract doesn’t declare as an input. Only checked when rejectUndeclaredInputs is enabled. The table a row-level DELETE/UPDATE/MERGE changes is not an input, even though the command reads it. Also raised, under the same option, for a read of an input the contract does declare but not as a source of the output being written (via that output’s derivedFrom).
MISSING_INPUT_FIELDA required input field is absent from the actual input schema.
UNDECLARED_INPUT_COLUMNThe actual input schema has a column the contract doesn’t declare. Only checked when rejectUndeclaredFields is enabled.
INPUT_FIELD_TYPE_MISMATCHAn input field’s actual type doesn’t match the contract’s declared type — including the contents of an array<...>/map<...>/struct<...> type or a struct’s properties (the violation’s column is then a dotted path such as address.zip).
INPUT_FIELD_NULLABILITY_MISMATCHThe contract requires an input field to be non-null, but the actual schema permits nulls.
MISSING_INPUT_CATALOG_REGISTRATIONThe contract’s input declares catalog: { required: true }, but the actual read has no catalog registration at all.
INPUT_CATALOG_MISMATCHThe input is catalog-registered, but a declared catalog sub-field (technology/catalogName/location/namespace/table) disagrees with the actual registration.
TypeMeaning
MISSING_OUTPUTA declared output was never written. One violation per declared output with no write landing on it. An adapter that sees a whole job before it runs (Beam) refuses a job that never writes one of its contract’s outputs, before anything starts. Spark checks one write at a time, so it finds this when the application ends: it reports it then, after the job’s other writes, and with spark.invaract.failOnMissingOutput=true the driver exits with status 1 (see Install the Enforcement Rule).
OUTPUT_LOCATION_MISMATCHThe write’s actual location doesn’t match any of the contract’s declared output locations.
OUTPUT_FORMAT_MISMATCHThe write’s actual format doesn’t match the contract’s declared format. Only checked when both are known.
OUTPUT_SAVE_MODE_MISMATCHThe write’s actual save mode doesn’t match the contract’s declared saveMode. Only checked when both are known.
MISSING_OUTPUT_FIELDA required output field is absent from the actual output schema.
UNDECLARED_OUTPUT_COLUMNThe actual output schema has a column the contract doesn’t declare. Only checked when rejectUndeclaredFields is enabled.
OUTPUT_FIELD_TYPE_MISMATCHAn output field’s actual type doesn’t match the contract’s declared type — including the contents of an array<...>/map<...>/struct<...> type or a struct’s properties (the violation’s column is then a dotted path such as address.zip).
OUTPUT_FIELD_NULLABILITY_MISMATCHThe contract requires an output field to be non-null, but the actual schema permits nulls.
MISSING_OUTPUT_CATALOG_REGISTRATIONThe contract’s output declares catalog: { required: true }, but the actual write has no catalog registration at all.
OUTPUT_CATALOG_MISMATCHThe output is catalog-registered, but a declared catalog sub-field (technology/catalogName/location/namespace/table) disagrees with the actual registration — e.g. the write went through a different Hive metastore than the contract declares.

Produced when a contract declares one of the row-level DML rules and the actual MERGE/UPDATE/DELETE doesn’t satisfy it:

TypeMeaning
RULE_MERGE_CONDITION_VIOLATIONA MERGE’s ON condition doesn’t include a genuine equality match on every column a merge_condition rule declares.
RULE_UNCONDITIONAL_DELETEA DELETE (or DSv2 DeleteFromTable) removes every row it reaches, with no filtering predicate, under a forbid_unconditional_delete rule.
RULE_DISALLOWED_UPDATE_COLUMNA standalone UPDATE assigns a column outside an allowed_update_columns rule’s declared list.

Produced when a contract declares one of the transformation shape rules and the plan’s structure doesn’t satisfy it — a separate rule family from DML rules above, checked against the plan’s whole shape rather than one extracted DML operation:

TypeMeaning
RULE_REQUIRED_GROUP_BY_VIOLATIONNo aggregation anywhere in the plan groups by every column a required_group_by rule declares.
RULE_CROSS_JOIN_VIOLATIONThe plan contains a cartesian-product join (a CROSS JOIN, or any join with no condition at all) under a forbid_cross_join rule.
RULE_REQUIRED_JOIN_COLUMNS_VIOLATIONNo join’s condition anywhere in the plan establishes an equality match on every column a required_join_columns rule declares.
RULE_REQUIRED_FILTER_COLUMNS_VIOLATIONNo filter anywhere in the plan references a column a required_filter_columns rule declares.

Produced when a contract declares an output field’s nullable: false or a static data-quality constraint and the transformation’s own semantics prove it cannot hold — never merely that it might fail, which is reported (not enforced) instead. Only checked when staticDataQuality is enabled (off by default; see the guide linked above for both ways to enable it, the identical mechanism computeFingerprint uses).

TypeMeaning
DATA_QUALITY_VIOLATIONThe transformation’s own logic provably guarantees an output field can produce a value that violates its declared nullable: false/equals/oneOf/range constraint — e.g. a filter’s negation, or an arithmetic operation, that demonstrably crosses a declared bound.

Produced when a contract declares an input’s type and the transformation’s own translated plan contradicts it — currently, the one high-confidence shape checked: a CONTROL-declared input whose data reaches a produced output column. Only checked when roleConsistency is enabled (off by default; see the guide linked above for both ways to enable it, the identical mechanism staticDataQuality uses).

TypeMeaning
ROLE_CONSISTENCY_VIOLATIONA CONTROL-declared input’s data reaches a produced output column — pipeline-only/control data must not become part of the resulting business data asset it’s declared to only operate/control.

Produced when a spark.invaract.orgPolicy-configured organizational policy rejects the contract itself, independent of any transformation — checked once, eagerly, before any plan is even analyzed, since a policy rule depends only on the contract’s own declared shape:

TypeMeaning
ORG_POLICY_VIOLATIONThe contract doesn’t satisfy an enforce-mode organizational policy rule (e.g. every output must be catalog-registered). message/remediation name the specific policy rule (by id) and dataset involved — there’s no further sub-vocabulary the way structural violations have one per check, since the policy document’s own id/description already carry that specificity.

Produced when Invaract genuinely can’t verify a write or operation, rather than when a verified write is structurally wrong. See Fail-Closed by Default for the reasoning behind both.

TypeMeaning
UNVERIFIABLE_WRITESomething may write or change data, but Invaract can’t check it, so it’s rejected rather than let through. On Spark: a command-shaped plan that isn’t on the known-safe list and doesn’t translate to a recognized write. On Beam: a step that ends the pipeline without being a recognized write, or a write that doesn’t state what the contract asks of it (its columns, format or save mode). The remediation says what to do in the engine’s own terms.
RULE_UNVERIFIABLE_DMLThe plan is genuinely row-level DML of a kind the active contract declares a rule for, but Invaract couldn’t extract the fact that rule needs (e.g. Iceberg’s merge-on-read UPDATE).
INVALID_CONTRACTThe contract itself is structurally unsound (e.g. no declared outputs) — caught before any plan is checked against it.
UNSUPPORTED_CONTRACT_FEATUREThe contract relies on something the engine adapter in use declares it cannot verify (a catalog requirement, a rule, a nested type, a format, …), so passing the write would represent an unchecked requirement as checked. expected is the capability id, e.g. check.catalogRegistration; the message gives the adapter’s own reason. Remove what needs it from the contract, or use an adapter that supports it — see Engine Capabilities. The Spark adapter declares none of the checks a contract can ask for as unsupported, so this does not occur with it today.

A job that calls .checkpoint() or .localCheckpoint() is verified as if the checkpoint weren’t there. Invaract remembers the plan each checkpoint was made from and puts it back before any check runs, so every check that looks at what a job reads or how it transforms data applies to the work done upstream of a checkpoint too:

  • the input checks — MISSING_INPUT, UNDECLARED_INPUT, the input schema checks (MISSING_INPUT_FIELD, UNDECLARED_INPUT_COLUMN, INPUT_FIELD_TYPE_MISMATCH, INPUT_FIELD_NULLABILITY_MISMATCH) and the input catalog checks;
  • the transformation-shape rules (RULE_CROSS_JOIN_VIOLATION, RULE_REQUIRED_GROUP_BY_VIOLATION, RULE_REQUIRED_JOIN_COLUMNS_VIOLATION, RULE_REQUIRED_FILTER_COLUMNS_VIOLATION), including rules an organizational policy injects;
  • ROLE_CONSISTENCY_VIOLATION and DATA_QUALITY_VIOLATION, and the sensitivity tags and fingerprint computed from the same lineage.

The verdict is the same one the same job gets with the checkpoint removed.

This can change a job’s result. A checkpoint used to hide everything before it, so a job could pass by accident — an undeclared read, a cross join, a CONTROL input flowing into the output, an input with the wrong schema, all upstream of the checkpoint, were never checked. A job could also be rejected by accident: a required_filter_columns or required_group_by rule satisfied before the checkpoint used to be reported as violated. If a checkpointed job that passed before is now rejected, the violation is real; it was there all along.

Where Invaract can’t determine what a checkpoint was made from, it leaves the checkpoint opaque rather than guess. Nothing is blocked because of that — a declared input that can’t be found is reported as unverifiable (report-only, see unverifiableInputs in the notification sinks guide) instead of MISSING_INPUT — but the checks above can’t run on what is upstream of it:

CaseWhyWhat to do
A checkpoint created before the enforcement rule was installed, in another session, or from a plan Invaract no longer remembersInvaract can only look up plans it saw being created (it keeps the most recent 256, and never remembers a plan over 5,000 nodes — a checkpoint exists to cut a lineage that long)Install the rule before the job creates its Datasets
Several plans that share their output columns but read different datasets — typically a join with a lookup followed by a select of one side’s columns, unchangedThe plan a checkpoint was made from can’t be told apart from a plain read of that sideGive the selected columns a new name or an alias (col("id").as("id")) — that makes the columns distinct and the checkpoint resolvable
A Delta MERGE whose source is the checkpointed DatasetThe command keeps its source where Invaract doesn’t look—
A cached relation (.cache()/.persist())Not a checkpoint; a plain cache never reaches the check at all—