Skip to content

Materialization & Storage

Materialization is declarative convergence: the contract declares the desired state of the target table, and the framework converges the actual table toward it. The materialization block (Materialization, 18 fields) chooses the write strategy, table format, partitioning, history handling, and delete semantics.

Powered by

Materialization is a Pydantic model. The write goes through the format's library: Delta via deltalake (delta-rs) on DuckDB/Polars or delta-spark on PySpark; Iceberg via pyiceberg (+ a catalog such as Glue) or the Iceberg Spark framework; DuckLake via DuckDB's ducklake extension; plain Parquet via pyarrow.

Shape

materialization:
  strategy: merge            # append | merge | scd2 | overwrite
  format: iceberg            # delta | iceberg | ducklake | parquet | native
  location: "s3://lake/silver/orders"    # or target_path
  partition_by: [order_date]
  cluster_by: [customer_id]
  merge_dedup_guard: true

Write strategies

strategy Converges the table to…
append …all rows, added.
merge …latest row per primary_key (upsert).
scd2 …full history — a slowly-changing dimension with validity windows.
overwrite …exactly the current batch (replace).
Field Purpose
strategy The convergence semantics above.
format Table format for the target.
location / target_path Where the table is written.
partition_by Physical partition columns.
cluster_by Clustering/Z-order columns for read pruning.
merge_dedup_guard Guard against duplicate keys within a merge batch.
table_properties Format-specific table properties passed through to the writer.
compaction Compaction/optimize settings for the table.

SCD2 history

strategy: scd2 keeps history with validity columns instead of overwriting. The version key is the contract's primary_key — there is no separate business-key setting inside the scd2 block:

primary_key: [customer_id]

materialization:
  strategy: scd2
  scd2:
    track_columns: [name, tier, address]   # change detection
    timestamp_field: updated_at            # SOURCE column holding the change date
    effective_from_field: effective_from   # names of the injected columns
    effective_to_field: effective_to
    current_flag_field: is_current
    version_column: _version
    effective_from_default: "1900-01-01"
    effective_to_default: "9999-12-31"

The framework injects the surrogate key + audit columns; quality rules that reference them are enforced at materialization, not validation.

scd2 keys

Every key is optional and has a default. The strict OLC v1 model rejects any key not on this list, because an undeclared key is silently ignored — and the most expensive case is a misspelled track_columns, which does not mean "no tracking" but "compare every column", cutting a new version on every load.

Injected column names — these name columns in the target dimension; they do not select source columns.

Key Default Purpose
surrogate_key _sk Name of the injected surrogate-key column.
surrogate_key_strategy hash hash (deterministic, from primary_key + version start) or uuid.
effective_from_field effective_from Version-start column.
effective_to_field effective_to Version-end column.
current_flag_field is_current Boolean column marking the live row.
version_column _version 1-based version counter per key.
change_reason_column _change_reason Holds initial_load, or the comma-joined names of the columns that changed.

Change detection

Key Default Purpose
track_columns (none) Only open a new version when one of these columns actually changed. Omit it and every incoming row for a known key is treated as a change.

Aliased keys — each group below is one setting with two accepted spellings. Both are load-bearing for contracts already in the wild; neither is deprecated.

Alias group Which wins Default Purpose
timestamp_field · change_date_field timestamp_field the value of effective_from_field The source column whose value is the change-event date.
end_date_default · effective_to_default end_date_default, by key presence (an explicit null still wins) 9999-12-31 Sentinel written to the end column of an open row.
start_date_default · effective_from_default start_date_default, by key presence 1900-01-01 Start date for an initial load, or a key's first appearance.

Other

Key Default Purpose
default_effective_from (current UTC time) Despite the name, not an alias of the start defaults: it is the value substituted for "now", used when the incoming data carries neither the effective-from column nor a usable change-date column, and to close rows superseded within a single batch. Pin it to make a load reproducible.
merge_dedup_guard false Deduplicate incoming rows by primary key (latest change-date wins) before applying SCD2. Some writer paths take this from the materialization-level merge_dedup_guard flag instead.
unknown_member (see below) Kimball unknown-member row.

scd2.unknown_member

Key Default Purpose
enabled true Inject the unknown-member row. (The Spark already-written-table path treats a missing value as false.)
surrogate_key_value -1 Surrogate-key value for the unknown row.
default_values {} Column → value for the unknown row's remaining columns.

Facts & dimensions

fact (FactConfig) marks a table as a fact and declares milestone-date semantics (e.g. accumulating snapshot facts):

materialization:
  fact:
    type: transaction        # transaction | periodic | accumulating
    milestone_dates: [requested_at, accepted_at, completed_at]

Soft deletes

Rather than physically removing rows (e.g. from a CDC delete), mark them — preserving history and enabling audit:

materialization:
  soft_delete_column: is_deleted
  soft_delete_value: true
  soft_delete_time_column: deleted_at
  soft_delete_reason_column: delete_reason
Field Purpose
soft_delete_column Column set when a row is deleted.
soft_delete_value The value written to mark a delete.
soft_delete_time_column When the delete happened.
soft_delete_reason_column Why (e.g. the CDC op or a rule).

Reprocessing

Bounded, idempotent re-runs over a date window — replace a slice without duplicating the rest:

materialization:
  reprocess_policy: window          # how a reprocess replaces existing data
  reprocess_date_column: order_date

Schema evolution

schema_policy (SchemaPolicy) governs how the target reacts when the incoming schema changes, and what to do with unexpected columns:

schema_policy:
  evolution: compatible     # strict | append | merge | overwrite | compatible | all
  unknown_fields: quarantine   # quarantine | drop | allow
Field Values Meaning
evolution strict / append / merge / overwrite / compatible / all How much schema change is tolerated on write. strict rejects any change; compatible allows safe additions; all allows anything.
unknown_fields quarantine / drop / allow What to do with columns not in the contract — route to quarantine, silently drop, or pass through.

Unknown dimension members

unknown_member configures the placeholder row used when a fact references a dimension key that doesn't exist yet (the classic -1 / "Unknown" member), so facts never lose rows to a missing lookup.