Incremental Loading¶
Every dlt source in Databox writes a Polaris-managed Iceberg table in S3 using a declared primary key and write disposition. Local DuckDB consumes those raw tables through the attached Polaris catalog. This page documents, per resource:
- write disposition — how new rows land:
merge,replace, orappend - primary / merge key — how dlt deduplicates on re-run
- watermark — how the extract window is bounded (date-based lookback, or none)
- idempotency guarantee — what re-running the same load against the same upstream data does to the final row set
- backfill command — exact command to reload a wider window
Summary¶
| Source | Resource | Disposition | Primary Key | Watermark |
|---|---|---|---|---|
| ebird | recent_observations |
merge | (subId, speciesCode) |
rolling days_back window |
| ebird | notable_observations |
merge | (subId, speciesCode) |
rolling days_back window |
| ebird | species_list |
replace | speciesCode |
none — full snapshot |
| ebird | hotspots |
merge | locId |
none — full snapshot |
| ebird | taxonomy |
replace | sciName |
none — full snapshot |
| ebird | region_stats |
merge | (regionCode, year, month, day) |
rolling days_back window |
| noaa | daily_weather |
merge | (date, datatype, station) |
rolling days_back window, chunked to 365-day API calls |
| noaa | stations |
merge | id |
none — full snapshot |
| noaa | datasets |
replace | id |
none — full snapshot |
| usgs | daily_values |
merge | (site_no, parameter_cd, observation_date) |
rolling days_back window, chunked to 90-day API calls |
| usgs | sites |
merge | site_no |
none — full snapshot |
| avonet | species_traits |
Iceberg replace | avibase_id plus unique source scientific name |
none — pinned full snapshot |
Idempotency model¶
No resource uses dlt's dlt.sources.incremental cursor. Instead, every
resource re-fetches a bounded window on each run, and dlt commits declared
merge or replacement semantics through Iceberg. Consequences:
- merge-disposition resources are fully idempotent: re-running with the same API responses leaves the final table untouched.
- replace-disposition resources (
species_list,taxonomy,datasets) drop and reload the table every run; idempotency is trivial because the row set is always "whatever the API returned this time." - AVONET complete snapshot validates its pinned file identity, row count, unique identifiers/names, columns, provenance, and dlt metadata before direct replacement. The committed Iceberg snapshot is the atomic boundary; failed validation cannot publish, and a failed commit leaves the prior snapshot authoritative.
The merge-disposition guarantee is validated in CI by
packages/databox-sources/tests/<source>/test_idempotency.py: each test
runs the merge-backed resource twice against the same VCR cassette and
asserts the primary-key set and row count are identical. The Iceberg merge uses
the same declared primary keys for the current source set.
Backfill procedure¶
All three sources read their extract window from a per-source env-var
override (default 30) in
packages/databox/databox/config/settings.py:
DATABOX_EBIRD_DAYS_BACKDATABOX_NOAA_DAYS_BACKDATABOX_USGS_DAYS_BACK
Set a supported window before launching the Dagster ingest job or
task full-refresh:
# Full-year NOAA backfill (hits multiple 365-day chunks; expect 5-10 min)
DATABOX_NOAA_DAYS_BACK=365 task full-refresh
# USGS daily values for the last year (chunked to 90-day API calls)
DATABOX_USGS_DAYS_BACK=365 task full-refresh
The eBird recent-observation endpoints accept only 1–30 days, so
DATABOX_EBIRD_DAYS_BACK is validated to that range and defaults to the maximum
30. Historical eBird backfill requires a different data product and is not wired
into this repository.
The merge disposition means a backfill never duplicates rows already present at the narrower window — it only fills in older dates. If the API has retroactively revised a row (which NOAA does for GHCND), the merge updates in place.
Blast radius¶
- merge resources: only the rows whose primary keys appear in the new window are touched. Existing rows outside the window are untouched.
- replace resources: the entire table is dropped and reloaded on every
run, so a backfill on
days_backhas no effect on these (they always reflect the current full snapshot). AVONET reaches the same authoritative result through its separately validated Iceberg replacement. - downstream SQLMesh CDM models: all are declarative views/tables over the
raw layer. A backfill at the source layer is picked up on the next
task full-refresh(or native SQLMesh restatement).
Dagster backfill¶
A partitioned Dagster backfill is not wired yet, but each default registered
source has an independent dlt ingest asset job. task full-refresh validates
Polaris/S3 configuration, runs sources marked parallel_refresh=True
concurrently, verifies registry-declared Iceberg tables and explicit load
status, then runs project-wide SQLMesh followed by every Soda contract. This is
the sole routine schedule; source ingest jobs remain available for manual repair
and backfill. Static AVONET remains an explicit avonet_ingest bootstrap job;
explicit-target USFWS has no unconfigured ingest job.
When to rely on merge vs replace¶
- merge: the source API returns a durable, point-in-time row identified
by a stable key (
(subId, speciesCode),(date, datatype, station),(site_no, parameter_cd, observation_date)). Revisions are in-place updates. - replace: the source API returns a catalog or reference list that should exactly mirror upstream (taxonomy, dataset manifest, species list for a region). Drift between a stale merge table and a current catalog would be worse than the cost of a full reload.