Write strategies and SCD2
How rows are applied to Delta Lake and Apache Iceberg tables: merge, append, replace, truncate+insert, delete+insert and opt-in SCD Type 2, with global defaults and per-table overrides.
A write strategy defines how a batch of extracted rows is applied to the destination table. It only matters for delta and iceberg output; CSV and Parquet always produce new files.
Resolution order
data_retrieval:
write_strategy: merge # optional global default for every table
databases:
- name: SalesDb
sync_mode: ct
tables:
- name: dbo.*
all_columns: true
- name: dbo.Customer
all_columns: true
write_strategy: scd2 # per-table overridewrite_strategyon the table entry, when present.data_retrieval.write_strategy, when present.merge, the behaviour the agent has always had.
Both settings are optional. A configuration that never mentions write_strategy behaves exactly as before the setting existed: each table is upserted by primary key. Setting the global value to scd2 turns on history tracking for every table in every database at once, while individual tables can still opt back to merge or another strategy.
Strategies
| Strategy | Sync modes | Primary key | What happens on each run |
|---|---|---|---|
merge (default) | full, ct, cdc | Uses PK when present | CT/CDC: deletes are applied and inserted or updated rows are upserted by PK. Full: the table is overwritten with the new snapshot. Without a PK, full mode overwrites and incremental modes append. |
append | full, ct, cdc | Not required | Every batch is appended as new rows. Nothing is updated or deleted; duplicates accumulate and are left to the consumer. |
replace | full recommended | Not required | The table is overwritten with the batch. Same effect as truncate+insert. |
truncate+insert | full recommended | Not required | The table is overwritten with the batch. Kept as a separate name for readability in configs migrated from other tools. |
delete+insert | full, ct, cdc | Required for the delete step | Rows whose keys appear in the batch are deleted from the target, then the batch is appended. When incremental_key is set on the table, the key plus the incremental column bound the delete. Behaves like append when no PK is present. |
scd2 | full, ct, cdc | Required (falls back to merge without one) | Slowly Changing Dimension Type 2: history is preserved, see below. |
Strategy names are case-insensitive. Unknown values fail validation at startup and in --config-check.
Deletes and delete modes
With merge, ct_cdc_delete_mode (per table, default physical) decides whether CT/CDC deletes remove the row or keep it with _cdc_deleted = true (logical). With scd2 a source delete never removes a row; it closes the current version instead.
SCD Type 2
write_strategy: scd2 keeps every version of every row rather than overwriting it. Three metadata columns are added to the Delta or Iceberg table:
| Column | Type | Meaning |
|---|---|---|
_scd_valid_from | timestamp (UTC) | When this version became current: the batch timestamp of the run that wrote it. |
_scd_valid_to | timestamp (UTC), nullable | When this version stopped being current. NULL for the current version. |
_scd_is_current | boolean | true for exactly one version per primary key while the key exists in the source. |
Per run, for each primary key in the batch:
- New key: one row is inserted with
_scd_is_current = trueand_scd_valid_to = NULL. - Changed values: the current version is closed (
_scd_valid_toset to the batch timestamp,_scd_is_current = false) and a new current version is inserted. - Unchanged values: nothing is written, so a full extract that returns identical rows does not inflate history.
- Deleted in source (CT/CDC delete, or missing from a
fullsnapshot): the current version is closed and no new version is inserted. The key has no current row until it reappears.
All versions written in one run share one _scd_valid_from timestamp, which makes it easy to reconstruct the state of the table after a given run.
Requirements and fallback
- Output format must be
deltaoriceberg. - The table must have a primary key (or
primary_key_columnsdeclared on the entry). Ifscd2is requested for a table without one, the agent logs a warning and usesmergefor that table. This keeps a globalwrite_strategy: scd2safe for databases that contain heaps or keyless tables. - Column-level masks are applied before change detection, so masked values are compared consistently between runs.
- Existing tables cannot be switched to
scd2in place. If the target table exists without the_scd_*columns the table is reported as failed with a message asking for a one-time reload. Rename or remove the target table (and, for Iceberg, the catalog entry) and let the agent recreate it with a full load; every row of that first load becomes the initial current version.
Querying SCD2 tables
Current state:
SELECT * FROM sales_customer WHERE _scd_is_current = true;As of a point in time:
SELECT * FROM sales_customer
WHERE _scd_valid_from <= '2026-08-01 00:00:00'
AND (_scd_valid_to IS NULL OR _scd_valid_to > '2026-08-01 00:00:00');Keys deleted in the source have no current row:
SELECT CustomerID FROM sales_customer
GROUP BY CustomerID
HAVING SUM(CASE WHEN _scd_is_current THEN 1 ELSE 0 END) = 0;Choosing a strategy
| Scenario | Recommended |
|---|---|
| Operational replica of source tables for reporting | merge (default) |
| Audit or compliance history of dimension tables (customers, products, drivers, locations) | scd2 on those tables, merge elsewhere |
| Append-only event or log tables with no updates | append (cheapest write path) |
| Small lookup tables reloaded every run | sync_mode: full with replace |
| Fact tables partitioned by a date column and reloaded per period | delete+insert with incremental_key |
| Tables without a primary key | merge with no_pk_strategy: full, or append if the source is insert-only |
Write strategies interact with Schema drift handling in the same way: the drift policy is evaluated on every Delta or Iceberg write regardless of strategy.