The tables Ingest creates
What lands in your warehouse (data tables, child tables, quarantine, the three _ingest_ control tables) and the semantics of each.
Ingest writes everything into one dataset (schema) in your destination. Your warehouse is the only durable home of your data: it holds the rows, the pipeline's saved position, and the schema history, and losing every byte on Ingest's side would lose none of it. During a load, staging copies can exist briefly under the staging prefix you configured (warehouses whose bulk load reads from object storage, like Redshift's COPY); Ingest removes them when the load completes. Five kinds of tables land in your dataset.
Data tables
One per resource: orders, contacts, deals. Typed columns, deduplicated rows, maintained
incrementally by the resource's write disposition: append (every run adds rows), replace (every
run rebuilds the table), or merge (rows are upserted by primary key, so the table tracks the
source's current state).
Every row carries three lineage columns: _ingest_ingested_at (when it landed), _ingest_run_id
(which run), and _ingest_request_id (which API request). Any row can be traced to the exact
request that produced it.
Child tables
When a record contains a list of objects (an order's line items), the list becomes its own table
instead of a JSON blob, named after its path: orders__line_items. Nesting continues the same way
(orders__line_items__discounts). Plain nested objects, by contrast, flatten into prefixed columns
on their parent (customer__address__city).
Link columns tie a child to its parents: _ingest_parent_id points one level up,
_ingest_root_id points at the top-level record, and _ingest_list_idx preserves list order.
select o.id, li.sku, li.quantity
from orders o
join orders__line_items li on li._ingest_root_id = o._ingest_id
The subtree stays consistent: when a record is updated at the source, all of its child rows are replaced together with it. A line item removed upstream disappears here; it cannot linger as a stale orphan.
Quarantine tables
Created only when needed. If a value arrives that cannot fit the existing column type (a string in
what has always been a number, beyond safe widening), those rows land in <table>__quarantine as
raw JSON with a reason, instead of corrupting the table or being dropped silently. The run is
flagged so you see it, and the rows stay queryable in your warehouse. Rows never vanish, and a
column never mutates into a lowest-common-denominator type underneath your queries.
Control tables
Three small tables make the pipeline stateless on our side and durable on yours:
_ingest_state: where the pipeline left off. Cursors, watermarks, per-resource positions. Restored at the start of every run; written back only after data lands. If it cannot be read, the run fails rather than silently starting from scratch._ingest_loads: a receipt per table per load. This is what makes retries safe: a crashed run that re-runs skips everything already receipted, so rows are never loaded twice._ingest_schema_versions: the schema's change history, one row per version. Tables added, columns added, types widened. The version only advances when something actually changed.
State and schema are written in the same step as the final load receipt, so the saved position is never ahead of the data it describes.
Staging tables
Transient working tables (_stg_ prefix) used during a load: new data lands there, is
deduplicated, then swapped or merged into the real table in one atomic step, so a query never sees
a half-loaded table. They are dropped when the load finishes. One left behind by an interrupted run
is inert and safe to remove.
Semantics worth knowing
Tables are current-state. On a merge resource, an update overwrites the previous values: the
table answers "what does this look like now", not "what did it look like in March". Resources
whose data is naturally event-like (transactions, log entries) are authored as append and keep
every record.
Deletions at the source do not propagate to merge tables. An incremental sync only sees records that exist; a record hard-deleted upstream stays in your warehouse, indistinguishable from a live one. Where a vendor reports deletions in its API, the connector surfaces that as an ordinary column you can filter on. Where it does not, a Full refresh (on the pipeline page; it re-extracts everything and rebuilds the tables) is the way to true up a table where deletion accuracy matters.
Lake destinations differ slightly. In plain-file mode there are no tables: data is written as
Parquet/JSONL files under per-load prefixes with a current-pointer for atomic replace, and merge
is unsupported. In Iceberg mode, data and control tables are real Iceberg tables in your own
catalog, and merge works.