Schema contracts¶
Schema contracts let users define portable logical column types before dpone
renders target-specific DDL. They are the right place for business-critical
columns, sparse columns, financial decimals, timestamps, and fields where
sampled inference is not acceptable.
Basic contract¶
schema_contract:
id: analytics.orders
version: 1.5.0
owner: data-platform
enforcement: strict
compatibility: backward
columns:
amount:
type: decimal
precision: 18
scale: 4
nullable: false
updated_at:
type: timestamp
timezone: true
nullable: false
payload:
type: json
nullable: true
Version registry¶
schema_contract.registry turns a logical schema into a versioned data API:
analytics.orders@1.5.0. The registry is provider-neutral and does not open a
target database connection. It stores immutable contract versions in
local_json or sqlite, then lets PR/MR and deploy pipelines prove that a
schema migration is compatible with the published contract and declared
consumers.
sink:
options:
schema_contract:
id: analytics.orders
version: 1.5.0
owner: data-platform
enforcement: strict
compatibility: backward
registry:
enabled: true
mode: gate
store_backend: sqlite
store_uri: .dpone/schema-contracts/registry.sqlite3
versioning:
semver: strict
allow_major: approval_required
unknown_consumer: warn
deprecation:
default_remove_after_days: 90
expired_policy: block
consumers:
discovery:
enabled: true
mode: gate
unknown_consumer: warn
required_sources: [manual]
sources:
manual: true
manifests: true
dbt_manifest: target/manifest.json
openlineage: .dpone/lineage/latest.json
manual:
- id: finance.daily_margin
type: dashboard
owner: finance-analytics
version_constraint: ">=1.4,<2.0"
reads:
columns: [amount, customer_id]
columns:
amount:
type: decimal
precision: 18
scale: 2
nullable: true
customer_id:
type: integer
nullable: true
Semver rules¶
| Change | Required bump | Default compatibility |
|---|---|---|
| Owner, docs, or deprecation metadata only | patch |
compatible |
| Add nullable column or add alias | minor |
compatible |
Drop column, narrow type, nullable -> not nullable, direct rename, expired alias removal |
major |
breaking |
Incompatible type expansion with __dpone__nc__<column> |
major unless hidden behind a compatible alias lifecycle |
breaking |
In semver: strict, the declared manifest version must satisfy the required
bump. If the registry already has analytics.orders@1.4.0, adding a nullable
column must declare at least 1.5.0; a breaking change must declare 2.0.0 or
another higher major version.
Consumers and deprecation windows¶
Manual consumers let data owners pin dashboards, jobs and downstream products to
a compatible version range. dpone schema contract gate blocks when a consumer
reads a removed column or when its version_constraint excludes the head
contract version. Unknown consumer policy is configurable:
| Policy | Behavior |
|---|---|
allow |
Do not emit consumer evidence. |
warn |
Emit warning evidence but allow local/dev flows. |
block |
Fail gate until consumers are registered. |
Deprecation windows belong in the contract registry rather than in ad hoc PR
comments. Use dpone schema contract deprecate to emit a deterministic receipt
for a column or alias removal window.
Consumer discovery and compatibility matrix¶
Manual consumers are still the safest source of ownership truth, but production review usually needs more evidence than hand-maintained YAML. Consumer discovery builds a local, provider-neutral inventory from:
| Source | Evidence |
|---|---|
manual |
schema_contract.consumers.manual owner/version/read declarations. |
manifests |
Local dpone manifests/routes that reference the same contract/table. |
dbt_manifest |
Local dbt manifest.json source/model dependency metadata. |
openlineage |
Local OpenLineage artifacts with input datasets and schema facets. |
dbt_compiled_sql |
Local dbt compiled SQL parsed with sqlglot for column-level reads. |
datahub |
Local DataHub MCP/export payloads with dataset and column mappings. |
generic_catalog |
Portable JSON/CSV-style catalog exports for BI/jobs/catalog tools. |
The resulting matrix answers:
It complements schema_impact: impact is a migration blast-radius view,
consumer matrix is a data API compatibility view. The matrix is offline and does
not query target databases or SCM APIs.
dpone schema contract consumers lineage \
--manifest manifests/orders.yaml \
--format json \
--output .dpone/schema-contracts/orders.lineage.json
dpone schema contract consumers discover \
--manifest manifests/orders.yaml \
--lineage .dpone/schema-contracts/orders.lineage.json \
--format json \
--output .dpone/schema-contracts/orders.consumers.json
dpone schema contract consumers matrix \
--manifest manifests/orders.yaml \
--against analytics.orders@1.5.0 \
--consumers .dpone/schema-contracts/orders.consumers.json \
--lineage .dpone/schema-contracts/orders.lineage.json \
--format md \
--output .dpone/schema-contracts/orders.consumer-matrix.md
dpone schema contract consumers gate \
--manifest manifests/orders.yaml \
--pack .dpone/schema-migration/orders.pack.json \
--matrix .dpone/schema-contracts/orders.consumer-matrix.json \
--format json \
--output .dpone/schema-contracts/orders.consumer-gate.json
In mode: gate, the consumer gate blocks removed column reads, expired alias
reads, incompatible version_constraint ranges and missing required discovery
sources. In mode: observe, the same evidence is emitted as warnings so teams
can adopt discovery without breaking existing pipelines.
Lineage evidence carries a confidence level:
| Confidence | Meaning |
|---|---|
explicit |
Manual declaration or catalog column mapping. |
parsed |
sqlglot parsed a local SQL artifact to column-level reads. |
inferred |
Dataset dependency is known and columns are partially inferred from facets. |
table_only |
Consumer depends on the table, but column reads are unknown. |
For major, drop, direct-rename and expired-alias changes, the default
low_confidence_major_change: block fails closed when only table_only or
inferred evidence exists. Set it to warn only for an explicit migration
window where owners accept the risk.
Consumer test kit and certification¶
The consumer matrix tells the release pipeline who is affected. The test kit turns that evidence into owner-routable checks, so PR/MR reviewers can see which consumer contract tests should run before promotion. V1 is offline and provider-neutral: dpone generates deterministic pytest/Markdown scaffolding and certifies an imported result artifact; it does not call BI tools, APIs or SCMs.
dpone schema contract consumers test-kit plan \
--manifest manifests/orders.yaml \
--matrix .dpone/schema-contracts/orders.consumer-matrix.json \
--compatibility-view-plan .dpone/schema-contracts/orders.compatibility-views.json \
--format json \
--output .dpone/schema-contracts/orders.consumer-test-kit.json
dpone schema contract consumers test-kit render \
--kit .dpone/schema-contracts/orders.consumer-test-kit.json \
--format pytest \
--output tests/generated/test_orders_consumers.py
dpone schema contract consumers test-kit certify \
--kit .dpone/schema-contracts/orders.consumer-test-kit.json \
--result passed \
--format json \
--output .dpone/schema-contracts/orders.consumer-certification.json
test-kit plan treats blocked matrix rows as required cases instead of failing
the kit. That is intentional: the kit is the handoff artifact for consumer
owners. certify is the go/no-go artifact; it returns blocked when a required
case fails or is missing from a detailed result artifact.
| Evidence | Assertion |
|---|---|
| Column reads | Required columns are available to the consumer. |
| Version constraint | The consumer pin is compatible with the head contract or a served compatibility view. |
| Compatibility view | Older pinned consumers can read the versioned view that covers their columns. |
| Owner/confidence | Review routing preserves owner and evidence confidence from discovery/lineage. |
consumer_test_kit and consumer_certification are normal migration bundle
artifacts. Production bundle policies should require consumer_certification
for major/drop/direct-rename migrations, and the evidence registry can persist
stage consumer_certified.
Compatibility views and multi-version serving¶
Consumer matrix gates intentionally fail closed for breaking changes. Contract compatibility views turn that blocker into a release path: publish the new contract version, keep older pinned consumers on a versioned view, migrate consumers during the deprecation window, then retire the view when the registry proves no consumers are pinned.
sink:
options:
schema_contract:
id: analytics.orders
version: 2.0.0
serving:
enabled: true
mode: gate
default_strategy: projection_view
unknown_mapping: block
expired_view: block
view_naming: "{schema}.{table}__contract_v{major}"
versions:
- constraint: "1.x"
source_contract: analytics.orders@1.5.0
view: analytics.orders__contract_v1
remove_after: 2026-09-01
owner: data-platform
columns:
client_id:
from: customer_id
amount:
from: amount
cast: "Decimal(18, 2)"
Planning is offline and provider-neutral:
dpone schema contract views plan \
--manifest manifests/orders.yaml \
--against analytics.orders@1.5.0 \
--consumer-matrix .dpone/schema-contracts/orders.consumer-matrix.json \
--format json \
--output .dpone/schema-contracts/orders.compatibility-views.json
dpone schema contract views gate \
--plan .dpone/schema-contracts/orders.compatibility-views.json \
--consumer-gate .dpone/schema-contracts/orders.consumer-gate.json \
--format json \
--output .dpone/schema-contracts/orders.compatibility-view-gate.json
dpone schema contract views report \
--plan .dpone/schema-contracts/orders.compatibility-views.json \
--format md
The planner maps old contract columns to current expressions by stable identity
first, then by configured column mapping. It supports direct projection, alias
projection, safe casts, configured expressions/defaults, and fail-closed blockers
for unmapped or expired views. For ClickHouse V1 it renders normal
CREATE OR REPLACE VIEW ... AS SELECT ... statements; materialized views are
intentionally out of scope because they add storage and backfill semantics.
When enabled, dpone schema migration plan embeds
compatibility_view_summary and inserts the phase order:
compatibility_views_expand
-> normal schema/physical migration phases
-> compatibility_views_validate
-> destructive contract/drop only after coverage or separate approval
compatibility_view_gate is a normal bundle/registry artifact:
dpone schema migration bundle build \
--pack .dpone/schema-migration/orders.pack.json \
--contract-gate .dpone/schema-contracts/orders.contract-gate.json \
--consumer-gate .dpone/schema-contracts/orders.consumer-gate.json \
--compatibility-view-gate .dpone/schema-contracts/orders.compatibility-view-gate.json \
--output-dir .dpone/schema-migration/review/orders
dpone schema migration registry record \
--bundle .dpone/schema-migration/review/orders/bundle.json \
--compatibility-view-gate .dpone/schema-contracts/orders.compatibility-view-gate.json \
--environment prod \
--stage compatibility_view_planned
Adoption and deprecation retirement¶
Compatibility views keep old contract ranges alive; adoption orchestration decides when those old ranges are safe to retire. The layer is fully opt-in and offline: it reads the consumer matrix, compatibility view plan, evidence registry records and optional consumer certification artifacts. It never calls a database, BI tool, catalog or SCM API.
sink:
options:
schema_contract:
adoption:
enabled: true
mode: gate
profile: prod_strict
default_migration_window_days: 90
expired_window_policy: block
require_consumer_certification: true
unknown_consumer: warn
Build the owner-routed adoption campaign:
dpone schema contract adoption plan \
--manifest manifests/orders.yaml \
--consumer-matrix .dpone/schema-contracts/orders.consumer-matrix.json \
--compatibility-view-plan .dpone/schema-contracts/orders.compatibility-views.json \
--format json \
--output .dpone/schema-contracts/orders.adoption-plan.json
Merge the campaign with registry and consumer certification evidence:
dpone schema contract adoption status \
--plan .dpone/schema-contracts/orders.adoption-plan.json \
--registry .dpone/schema-migration/registry/registry.json \
--consumer-certification .dpone/schema-contracts/orders.consumer-certification.json \
--format json \
--output .dpone/schema-contracts/orders.adoption-status.json
Gate and plan retirement:
dpone schema contract adoption gate \
--status .dpone/schema-contracts/orders.adoption-status.json \
--profile prod_strict \
--format json \
--output .dpone/schema-contracts/orders.retirement-gate.json
dpone schema contract adoption retire \
--gate .dpone/schema-contracts/orders.retirement-gate.json \
--compatibility-view-plan .dpone/schema-contracts/orders.compatibility-views.json \
--format json \
--output .dpone/schema-contracts/orders.retirement-plan.json
retire emits a deterministic plan only. Actual view deletion, alias removal or
destructive DDL still goes through dpone schema migration plan and
dpone schema migration apply --execute.
| Artifact | Purpose |
|---|---|
contract_adoption_status |
Machine-readable status for who is still pinned, certified, migrated, blocked or unknown. |
contract_retirement_gate |
Fail-closed go/no-go receipt for removing a compatibility view or expired alias. |
contract_retirement_plan |
Offline retirement plan with affected view ids, consumers and reviewer actions. |
Evidence registry stages cover the whole lifecycle: adoption_planned,
consumer_migrating, consumer_migrated, contract_retirement_ready and
contract_retired.
Compatibility views complement, not replace, other contract layers:
| Layer | Question answered |
|---|---|
schema_contract check |
Is the head contract semver-compatible with the base contract? |
consumer matrix |
Which consumers are affected and are their version pins compatible? |
compatibility views |
Can old pinned consumers continue reading a stable data API while v2 rolls out? |
adoption status |
Which consumers still depend on the old range, and is retirement safe? |
schema_impact |
What broader migration blast radius and owner approvals are needed? |
Contract CLI¶
Publish the first immutable contract version:
dpone schema contract publish \
--manifest manifests/orders.yaml \
--format json \
--output .dpone/schema-contracts/orders-1.5.0.json
Check a manifest against a published version:
dpone schema contract check \
--manifest manifests/orders.yaml \
--against analytics.orders@1.4.0 \
--compatibility backward \
--format md
Build a pack-bound consumer compatibility gate:
dpone schema contract gate \
--manifest manifests/orders.yaml \
--pack .dpone/schema-migration/orders.pack.json \
--format json \
--output .dpone/schema-contracts/orders.contract-gate.json
Query the registry:
dpone schema contract history --contract analytics.orders --format table
dpone schema contract latest --contract analytics.orders --format json
dpone schema contract consumers --contract analytics.orders --version 1.x --format table
dpone schema contract deprecate --contract analytics.orders --column client_id --remove-after 2026-09-01
Migration and CI/CD integration¶
When schema_contract.registry.enabled: true, dpone schema migration plan
embeds contract_compatibility_summary. In mode: gate, migration apply
fails closed when the pack lacks a compatible contract summary.
The PR/MR path is:
schema contract publish/check
-> schema migration plan
-> schema contract gate
-> schema contract consumers discover/matrix/gate
-> schema contract views plan/gate
-> schema contract consumers test-kit plan/render/certify
-> schema contract adoption plan/status/gate
-> bundle build --contract-gate --consumer-gate --compatibility-view-gate --consumer-certification --contract-adoption-status --contract-retirement-gate
-> bundle gate required_artifacts: [migration_pack, contract_gate, consumer_gate, compatibility_view_gate, consumer_certification, contract_retirement_gate, ...]
-> registry record --stage contract_gate_passed
-> registry record --stage consumer_gate_passed
-> registry record --stage consumer_certified
-> registry record --stage compatibility_view_planned
-> registry record --stage adoption_planned|consumer_migrating|consumer_migrated|contract_retirement_ready|contract_retired
contract_gate, consumer_gate, compatibility_view_gate,
consumer_certification, contract_adoption_status and
contract_retirement_gate are normal evidence artifacts. They can be required
by bundle gate policies, shown in bundle summaries, and stored in the
evidence registry. SCM systems remain the orchestrator; dpone owns deterministic
contract evidence and go/no-go decisions.
Architecture¶
flowchart TD
Manifest["Manifest schema_contract"] --> Builder["SchemaContractVersionBuilder"]
Identity["schema_identity ids and aliases"] --> Builder
Builder --> Version["SchemaContractVersion"]
Store["SchemaContractRegistryStore"] --> Comparator["SchemaContractComparator"]
Version --> Comparator
Comparator --> Classifier["SchemaCompatibilityClassifier"]
Consumers["SchemaConsumerProvider[]"] --> Inventory["SchemaConsumerInventory"]
Inventory --> Matrix["SchemaConsumerMatrixBuilder"]
Matrix --> ConsumerGate["SchemaConsumerMatrixGate"]
Matrix --> TestKit["SchemaConsumerTestKitBuilder"]
ViewPlanner --> TestKit
TestKit --> ConsumerCert["SchemaConsumerCertificationEvaluator"]
Comparator --> ViewPlanner["CompatibilityViewPlanner"]
Matrix --> ViewPlanner
ViewPlanner --> ViewGate["CompatibilityViewGate"]
Consumers["SchemaConsumerRegistry"] --> Gate["SchemaConsumerCompatibilityGate"]
Classifier --> Gate
Gate --> Migration["MigrationPack contract summary"]
Gate --> Bundle["contract_gate evidence artifact"]
ConsumerGate --> Bundle2["consumer_gate evidence artifact"]
ViewGate --> Bundle3["compatibility_view_gate evidence artifact"]
ConsumerCert --> Bundle4["consumer_certification evidence artifact"]
| Component | Responsibility |
|---|---|
SchemaContractVersionBuilder |
Builds immutable logical contract versions from manifest, columns and identity metadata. |
SchemaContractComparator |
Diffs base/head by stable identity first, then by name. |
SchemaCompatibilityClassifier |
Assigns required semver bump, risk tags and blockers. |
SchemaConsumerCompatibilityGate |
Checks consumer version ranges and removed column/alias reads. |
SchemaConsumerProvider |
Loads manual, manifest, dbt and OpenLineage consumer evidence from local artifacts. |
SchemaConsumerLineageProvider |
Loads column-level lineage evidence from local dbt compiled SQL, DataHub exports and generic catalog artifacts. |
SqlColumnLineageExtractor |
Uses sqlglot for SELECT/CTE/JOIN/projection parsing; no target DB connection. |
ConsumerEvidenceConfidenceClassifier |
Normalizes evidence as explicit, parsed, inferred or table_only. |
SchemaConsumerMatrixBuilder |
Builds consumer x contract-change compatibility rows with owner routing and reviewer actions. |
SchemaConsumerMatrixGate |
Emits pack-bound allowed, warning or blocked consumer evidence for CI/CD. |
SchemaConsumerTestKitBuilder |
Converts affected consumers into deterministic owner-routable test cases. |
SchemaConsumerCertificationEvaluator |
Converts consumer test results into certified, warning or blocked release evidence. |
CompatibilityViewPlanner |
Builds versioned projection view specs from contract diff, identity ids and consumer pins. |
CompatibilityViewGate |
Blocks breaking migrations when affected pinned consumers lack usable view coverage. |
SchemaContractRegistryStore |
Provider-neutral persistence port with local_json and sqlite adapters. |
SchemaContractFacade |
Thin CLI/file-IO facade; no target DB or SCM imports. |
Industrial comparison¶
| System | Pattern | dpone behavior |
|---|---|---|
| dlt | Schema contracts/evolution control whether schemas evolve, freeze or discard. | dpone keeps contract-as-code ergonomics and adds immutable semver versions plus consumer gates. |
| Airbyte | Schema changes can be reviewed, propagated or stopped before sync. | dpone makes compatibility decisions portable, pack-bound and can serve old/new contract versions in parallel. |
| Fivetran | Managed schema settings, history mode and schema logs provide platform auditability. | dpone separates data history from data API versions and emits GitOps-ready compatibility view evidence. |
| Informatica | Enterprise schema drift workflows emphasize governance and dynamic mappings. | dpone keeps governance value, but makes compatibility mappings typed, deterministic and testable. |
| Pentaho | Metadata injection and SQL scripts support dynamic metadata flows. | dpone replaces ad hoc view SQL scripts with versioned plans, gates, dialects and registry lifecycle. |
| SSIS | SSIS Catalog centralizes deployed projects, environments and executions. | dpone adds modern semver, multi-version serving and CI-verifiable contract evidence. |
Enforcement modes¶
| Mode | Behavior |
|---|---|
strict |
Fail before or during load when values do not match the contract. |
coerce |
Attempt explicit value conversion before staging. |
quarantine |
Route bad rows to __dpone__quarantine and keep the target clean. |
warn |
Continue best-effort and emit run artifact warnings. |
Production default is strict. For dirty APIs and files, use quarantine.
Runtime behavior is implemented by
Runtime data contracts. That layer decides which
rows are safe to stage, which rows go to quarantine, and whether state can
advance after the load.
Type conflict policies¶
type_inference.conflict_policy handles conflicts between source values,
contracts, and target shape:
| Policy | Behavior |
|---|---|
fail |
Stop before target writes. |
variant_column |
Route incompatible values into __dpone__nc__<column>. |
quarantine |
Keep target columns clean and store rejected rows with diagnostics. |
Example:
sink:
options:
type_inference:
conflict_policy: variant_column
schema_evolution:
data_type: variant_column
If amount was decimal(18,2) and source starts sending JSON/text payloads,
dpone creates or reuses __dpone__nc__amount. The old amount column remains
untouched.
Target-specific type override¶
Use logical contracts for portable semantics and physical overrides for one specific sink:
sink:
options:
physical_design:
columns:
amount:
target_type:
mssql: decimal(18,4)
postgres: numeric(18,4)
clickhouse: Decimal(18,4)
bigquery: NUMERIC
The override wins over inference. If it would narrow an existing target column, the online DDL governance layer blocks it unless a safe-window or manual approval flow is configured.
Decision precedence is visible in dpone schema physical-plan and
dpone schema explain:
| Winner | Decision category |
|---|---|
physical_design.columns.<column>.target_type.<sink> |
explicit_physical_override |
schema_contract.columns.<column> |
explicit_logical_contract |
| Source metadata / Schema Registry | auto_inferred |
| Sample profile | auto_inferred |
| Safe fallback, quarantine or variant-column policy | quarantine_required, variant_column_required, or incompatible_requires_policy |
Example: force a source integer to land as a string in ClickHouse:
This is allowed only because it is an explicit physical override. Without the
override, the certified MSSQL -> ClickHouse matrix maps int to Int32.
CLI¶
dpone schema infer --manifest manifests/orders.batch.yaml --format md
dpone schema physical-plan --manifest manifests/orders.batch.yaml --format json
dpone plan manifests/orders.batch.yaml --selector public.orders --format json
Runbook¶
| Change | Recommended action |
|---|---|
| Add a sparse field | Add it to schema_contract.columns so it appears before first non-null value. |
| Currency amount | Use decimal with explicit precision and scale. |
| Timestamp from API | Set type: timestamp and timezone intentionally. |
| Source type changed incompatibly | Prefer fail; use variant_column only for planned downstream migration. |
| Bad rows should not block ingestion | Use enforcement: quarantine and monitor quarantine artifacts. |