DVT RETRACT
dvt retract is the inverse of dvt build: it removes everything your project materialized — models, seeds, snapshots, even stored test-failure tables — from every engine and bucket involved, in reverse dependency order. It works from DVT's own materialization ledger, the record of what actually landed and where, so it drops exactly what exists and nothing it can't account for.
THE MINIMAL INVOCATION
Run it with no flags and retract plans the full teardown, shows you every relation it intends to drop, and asks once. Here's a project spread across Postgres, MySQL and an S3 bucket:
$ dvt retract 14:02:11 Retracting 6 relations across 3 connections, in reverse dependency order: ⌫ pg_warehouse: analytics.customer_ltv ⌫ s3_lake: analytics.daily_revenue ⌫ pg_warehouse: analytics.orders_enriched ⌫ pg_warehouse: analytics.stg_orders ⌫ mysql_crm: crm.stg_customers ⌫ pg_warehouse: analytics.raw_payments Drop ALL of the above? [y/N] y 14:02:12 1 of 6 START retract pg_warehouse.analytics.customer_ltv ..................... [RUN] 14:02:12 1 of 6 OK retracted pg_warehouse.analytics.customer_ltv ...................... [DROP TABLE in 0.41s] 14:02:13 2 of 6 START retract s3_lake.analytics.daily_revenue ......................... [RUN] 14:02:14 2 of 6 OK retracted s3_lake.daily_revenue .................................... [DELETED in 0.63s] 14:02:14 3 of 6 START retract pg_warehouse.analytics.orders_enriched .................. [RUN] 14:02:15 3 of 6 OK retracted pg_warehouse.analytics.orders_enriched ................... [DROP TABLE in 0.38s] 14:02:15 4 of 6 START retract pg_warehouse.analytics.stg_orders ....................... [RUN] 14:02:15 4 of 6 OK retracted pg_warehouse.analytics.stg_orders ........................ [DROP VIEW in 0.35s] 14:02:16 5 of 6 START retract mysql_crm.crm.stg_customers ............................. [RUN] 14:02:16 5 of 6 OK retracted mysql_crm.crm.stg_customers .............................. [DROP VIEW in 0.52s] 14:02:16 6 of 6 START retract pg_warehouse.analytics.raw_payments ..................... [RUN] 14:02:16 6 of 6 OK retracted pg_warehouse.analytics.raw_payments ...................... [DROP TABLE in 0.36s] 14:02:16 Cleared local incremental state for 1 model(s) — next runs start fresh. 14:02:16 Removed 6 entries from the materialization ledger. 14:02:16 Finished retracting 6 relations in 0 hours 0 minutes and 4.83 seconds (4.83s). 14:02:16 Done. PASS=6 WARN=0 ERROR=0 SKIP=0 TOTAL=6
One command, three engines, every trace gone: tables and views dropped with the right DDL per engine, the Parquet file on S3 deleted as a file (buckets hold files, not tables — DELETED, not DROP TABLE), local incremental state cleared so the next run behaves like a first run, and the ledger entries removed so nothing downstream still believes those relations exist.
IN PLAIN DBT
There is no inverse of build. dbt remembers how to create everything and nothing about how to remove it — so you open a console on each warehouse and write the DROPstatements by hand, guessing which schema each model landed in, in which environment, under which alias. Miss one and it sits there for years, showing up in someone's catalog crawl. DVT keeps a ledger of every relation it lands and tears all of it down with one command.
WHEN YOU REACH FOR IT
Retract is for the moments when the project should leave no footprint: tearing down a dev schema after a feature branch merges, cleaning a demo environment between audiences, resetting a shared sandbox that three people have been experimenting in, or decommissioning a project whose outputs are scattered across a warehouse, an operational database and a bucket. In each case the hard part was never the dropping — it was knowing what to drop, and where. That knowledge is exactly what the ledger holds.
It's also a graph command like any other: retract parses your project first, and a broken project refuses cleanly before anything is dropped. You'll never demolish from a manifest DVT couldn't verify.
THE LEDGER — THE AUTHORITY ON WHAT LANDED
Retract doesn't guess from your model files. DVT records every relation it lands — both federation lanes, dbt-phase materializations, seeds, snapshots, stored test failures — in a materialization ledger at .dvt/materializations.jsonl, written at load time. When retract plans a teardown, the ledger is the authority, and it settles two things the manifest alone can't:
What actually exists.A model you declared but never ran leaves the plan — a relation that never landed can't be dropped, and pretending to drop it would be a claim, not a finding. And relations only the ledger knows about join the plan: seeds in DVT's non-CSV formats (Parquet, JSON, JSONL — DVT's seed engine goes beyond dbt's csv-only seeds) exist only in the ledger, and retract sweeps them too.
Where it actually is. The ledger records the schema each relation landed in at load time, and that recording outranks whatever the manifest says today. If a model landed in public and the config has since moved to main, retract drops it from public— where it really is — not where today's config points.
IN PLAIN DBT
The manifest tells you what the project declares, not what any run actually created. After a config change, a renamed schema, or a half-finished run, the only record of what's really on each engine is the engine itself — so cleanup means crawling information schemas and comparing by hand. DVT's ledger records every landing as it happens, and retract reads it back.
REVERSE DEPENDENCY ORDER
Build runs roots-first; retract runs leaves-first. A relation is only dropped after everything that depends on it is already gone — in the run above, the customer_ltv mart comes off before the staging views it reads from, and the raw_paymentsseed goes last. That's why views with dependents, foreign keys and downstream readers don't turn the teardown into a wall of dependency errors: by the time each relation's turn comes, nothing above it is left to object.
THE HONESTY CONTRACT — AN EMPTY RUN SAYS SO, AND SAYS WHY
Run retract a second time and nothing prints three green DROPs against an empty engine. The ledger is empty now — the first retract cleaned it — so DVT holds no record of anything landed, and it tells you exactly that:
$ dvt retract 14:12:03 Nothing to drop — this project declares nothing that lands a relation, and the materialization ledger is empty: dvt holds no record of it materializing anything, anywhere.
This is deliberate. drop table if exists succeeds whether the table exists or not, so a naive second run would report a full page of successes while removing nothing — silence on an empty run would be indistinguishable from silence on a broken one. Retract refuses that ambiguity: an empty run exits 0 (nothing to drop is a success), and the reason is always specific, because each one is something different you can act on. A selection that matched nothing is probably a typo:
$ dvt retract --select stg_order 14:15:22 Nothing to drop — no model, seed, snapshot or stored-failure table matched --select 'stg_order'.
A project that never ran has no ledger yet; ledger entries pointing at connections your current profile doesn't define suggest you meant a different profile — DVT never drops what it cannot resolve. And when a planned run turns out to have nothing real to remove — every relation positively reported absent by its engine — the tally says that too: Nothing to drop — all 6 relation(s) were already absent from their engines. Nothing dvt knows this project landed is left behind. Being already gone counts as success, never failure: retract is idempotent by design.
--SELECT — PARTIAL RETRACTS
Pass model names (space-separated, quoted) to retract just those relations. Everything else stays exactly where it is:
$ dvt retract --select stg_orders --yes 14:07:40 Retracting 1 relations across 1 connections, in reverse dependency order: ⌫ pg_warehouse: analytics.stg_orders 14:07:40 1 of 1 START retract pg_warehouse.analytics.stg_orders ....................... [RUN] 14:07:41 1 of 1 OK retracted pg_warehouse.analytics.stg_orders ........................ [DROP VIEW in 0.34s] 14:07:41 Finished retracting 1 relations in 0 hours 0 minutes and 0.35 seconds (0.35s). 14:07:41 Done. PASS=1 WARN=0 ERROR=0 SKIP=0 TOTAL=1
A selected retract is a deliberately partial one, so the ledger-leftover sweep stays off: retract touches what you named and nothing more. The typical use is re-landing one model cleanly — retract it, fix it, run it — without disturbing the rest of the project.
--YES — THE CONFIRMATION PROMPT, AND CI
Dropping is destructive, so the full plan prints first and retract asks Drop ALL of the above? [y/N] — default no. Pass --yes (or -y) to skip the prompt in scripts and CI. And if there's no terminal to ask on, retract never hangs or crashes — it refuses cleanly and tells you the fix:
No interactive terminal to confirm on — pass --yes to retract without a prompt.
CROSS-ENGINE DROPS AND BUCKET OBJECTS
No two engines take the same teardown, and retract speaks each dialect it lands on. Postgres-family engines get drop ... cascadeso dependent objects don't block the teardown; engines that fold identifier case get unquoted names so the drop finds the relation exactly as the loader named it; Oracle has no IF EXISTS, so an ORA-00942 coming back simply counts as already gone. The manifest even tells retract whether each relation landed as a table or a view, so it issues the right statement first instead of probing blindly.
Bucket connections — S3, GCS, Azure — hold files, not relations, so retract deletes the object the loader wrote: {model}.{ext}under the connection's base path, in whichever of DVT's four file formats (CSV, Parquet, JSON, JSONL) the model landed as. The output labels it DELETED, or ABSENT when the listing shows it already gone. And a connector with no safe deletion lane (SFTP today) is skipped honestly with a WARN and a stated reason — DVT will never fire DROP TABLE at a file server.
The teardown reaches your laptop too: retracted incremental models have their local bookkeeping cleared from .dvt/cache.duckdb — indexes, ran-flags, replicas — so the next dvt run starts genuinely fresh instead of resuming from state that describes tables which no longer exist.
IN PLAIN DBT
Cleanup is per-warehouse, in each warehouse's own SQL, with your own bookkeeping of which models went where — and file outputs on object storage aren't dbt's department at all, so those you chase down separately with cloud-console clicks or a second tool. One project, four cleanup procedures. Retract is all of them, in order, with a tally.
--TARGET — RETRACT WHERE YOU RAN
--target mirrors dvt run --target: models that follow the profile's default output resolve against the output you name instead, so a run made with --target staging is retracted with --target staging. Models pinned to an explicit output in their own config are unaffected either way — they retract from their pinned engine, exactly where they landed.
$ dvt retract --target staging --yes
REFERENCE — EVERY FLAG
| FLAG | DEFAULT | WHAT IT DOES |
|---|---|---|
| --select | all | Retract only the named models, seeds, snapshots or stored-failure tables (space-separated names, quoted). A partial retract by intent: the ledger-leftover sweep is skipped, and nothing you didn't name is touched. |
| --target, -t | profile default | Resolve default-target models against this output instead of the profile default — mirrors dvt run --target. Models pinned to an explicit output in their config retract from their pinned engine regardless. |
| --yes, -y | off | Skip the Drop ALL of the above? [y/N] confirmation — for scripts and CI. Without it, a session with no interactive terminal refuses cleanly instead of hanging. |
| --project-dir | . | Path to the dbt/DVT project to retract. |
| --profiles-dir | standard resolution | Where to read profiles.yml from. Resolves the same way as every other command: this flag, then $DBT_PROFILES_DIR, then the project directory, then ~/.dbt. |
Exit codes: 0 on a clean run — including an honest empty one — 1 when any drop errored or the run was aborted, 2 when the project failed to parse (nothing dropped).