DVT SYNC

dvt sync sets up everything DVT needs on this machine — the execution engine, an adapter and native driver for every warehouse in your profiles.yml, and the Sling engine that moves data between systems — all into the one Python environment you're already standing in. Run it once after install, and again whenever anything changes: it only ever does what's missing.

YOUR FIRST SYNC

From inside a project, no flags needed. dvt sync reads dbt_project.ymlfor the profile name, reads that profile's outputs from profiles.yml, and works out the rest itself. Here it is on a project with a Postgres output and a DuckDB output:

$ dvt sync

Sync plan (execution follows — no prompt; the invocation is the consent):
  → env: install adapters[postgres,duckdb]
  → sling: install wrapper (sling==1.5.23) + fetch engine (~100 MB)
  → duckdb: ensure extensions (httpfs, json, postgres_scanner)
  → project: write macros/dvt_federation_materializations.sql

      ⏳ fetching the Sling engine (~100 MB, one time — slow networks take minutes)...

============================================================
DVT Environment Sync
============================================================
✅ Detect environment: Darwin arm64, Python 3.11.9 (venv)
  • Project: /Users/you/analytics
  • Profile: analytics
  • Profiles dir: /Users/you/.dbt
✅ Create directories: Directories ready: /Users/you/.dvt
✅ Install adapters: Installed: adapters[postgres,duckdb]
✅ Install bucket packages: No bucket packages to install
✅ Install Python model packages: No Python models in project
✅ Download Sling: Sling ready: /Users/you/analytics/.venv/bin/sling
✅ Install DuckDB extensions: DuckDB extensions: httpfs, json, postgres_scanner
✅ Install compatibility macros: Installed macros/dvt_federation_materializations.sql
✅ Validate credentials: Warnings: AWS credentials not found; GCP credentials not found; Azure credentials not found
✅ Test connections: All output configs valid
============================================================
Plan reconciliation:
  ✓ as planned: env: install adapters[postgres,duckdb]
  ✓ as planned: sling: install wrapper (sling==1.5.23) + fetch engine (~100 MB)
  ✓ as planned: duckdb: ensure extensions (httpfs, json, postgres_scanner)
  ✓ as planned: project: write macros/dvt_federation_materializations.sql
============================================================

✅ Environment is ready for DVT

One command, one environment: adapters for both engines, their native drivers, the DuckDB extensions federation needs, and the Sling engine. The credential lines are warnings, never failures — you only need cloud credentials when your project actually reads cloud storage.

IN PLAIN DBT

You install one adapter package per warehouse yourself — pip install dbt-postgres, then pip install dbt-snowflake — and then discover they disagree about dbt-core versions and native drivers, and the conflicts are yours to resolve. The usual endgame is one virtualenv per warehouse and a Makefile to remember which is which. dvt sync reads profiles.yml and installs a set that already agrees with itself, into one environment.

THE PLAN COMES FIRST

sync never mutates your environment quietly. Before the step loop touches anything, it prints its full plan — every install, every removal, with the exact version specs it will pass. There is no prompt: running the command is the consent. What keeps the plan honest is the second half — after the steps run, the summary reconciles what was done against what was planned, line for line. A mutation that was planned but didn't happen, or happened but was never planned, gets a loud warning line either way:

Plan reconciliation:
  ✓ as planned: env: install adapters[postgres,duckdb]
  ⚠ planned but NOT done: sling: fetch engine (~100 MB) (see the failed step above)

So the transcript of any sync answers both questions that matter afterwards: what did it intend to change, and did it change exactly that.

RUN IT AGAIN ANY TIME

sync is idempotent. Every step checks before acting — adapters are probed by importing them (the exact test the engine performs at run time), the Sling engine is probed by executing it, the compatibility macros are compared byte-for-byte. A re-run on a healthy environment plans almost nothing and mutates nothing:

$ dvt sync

Sync plan (execution follows — no prompt; the invocation is the consent):
  → duckdb: ensure extensions (httpfs, json, postgres_scanner)

============================================================
DVT Environment Sync
============================================================
✅ Detect environment: Darwin arm64, Python 3.11.9 (venv)
  • Project: /Users/you/analytics
  • Profile: analytics
  • Profiles dir: /Users/you/.dbt
✅ Create directories: Directories ready: /Users/you/.dvt
✅ Install adapters: All adapters already installed
✅ Install bucket packages: No bucket packages to install
✅ Install Python model packages: No Python models in project
✅ Download Sling: Sling ready: /Users/you/analytics/.venv/bin/sling
✅ Install DuckDB extensions: DuckDB extensions: httpfs, json, postgres_scanner
✅ Install compatibility macros: Compatibility macros already installed
✅ Validate credentials: All cloud credentials found
✅ Test connections: All output configs valid
============================================================
Plan reconciliation:
  ✓ as planned: duckdb: ensure extensions (httpfs, json, postgres_scanner)
============================================================

✅ Environment is ready for DVT

That makes sync the standing repair verb, not just the day-one one. Added a Snowflake output to profiles.yml? Re-run sync. New teammate cloned the repo? sync. A run ever tells you the environment looks damaged? sync. It converges on the same working state from any starting point.

IN PLAIN DBT

There is no converge verb. When an environment drifts — a driver upgrade, a colleague's pip install, a half-finished setup — repair is manual archaeology: read the traceback, guess the package, pin, retry. dvt sync re-derives the whole environment from profiles.yml and fixes exactly the delta.

SELF-HEALING — THE CORRUPTED DOWNLOAD

The Sling engine is a ~100 MB binary that downloads on first use — and a download that dies mid-fetch leaves a truncated binary on disk that looksinstalled. Version metadata can't tell the difference, so syncdoesn't ask metadata — it executes the binary. A corrupted engine fails that probe, gets deleted, and is fetched again, visibly:

      ⚠️ removed 1 truncated Sling engine(s) — re-fetching
      ⏳ fetching the Sling engine (~100 MB, one time — slow networks take minutes)...

This is also why sync takes the download up front instead of declaring victory early: deferred to the middle of your first real query, that fetch would be racing every connection timeout you have. sync eats the wait now, once, where you can watch it.

THE COLLISION GUARD

If an upstream per-engine dbt distribution — dbt-postgres, dbt-snowflake, and friends — is already installed in the environment, it shadows DVT's own adapters and the two fight over the same import namespace. syncdoesn't leave that landmine armed: every colliding distribution goes on the plan and is removed, always with the reason stated, never silently:

Sync plan (execution follows — no prompt; the invocation is the consent):
  → env: uninstall dbt-snowflake (upstream per-engine dist shadows DVT's adapters)

      removed dbt-snowflake: upstream per-engine dist shadows DVT's adapters

Then comes the verify pass: every adapter your profile needs must actually import — that import is exactly what the engine's factory will do at run time, so it is the only honest test. Any adapter that still fails is named, and the step (and sync's exit code) fails with it. An output type sync doesn't recognize is reported by name, never guessed at.

THE ENGINE VERSION IS PINNED — AND CORRECTED

The Sling engine sync installs is pinned to the exact version DVT's cross-engine campaign proved— not "whatever upstream released this week." A new engine version, however promising, has not run DVT's 110-transition battery, so it doesn't ship until it has. And the pin is enforced, not assumed: if the environment carries a different wrapper version — because something else upgraded it, or an older DVT installed before the pin — sync plans and applies the correction:

  → sling: correct wrapper 1.6.0 -> ==1.5.23 (the ring-proven version in service)

The pin also ships in dvt-core's own dependencies, as an exact version — so pip install -U or uv sync -U cannot move the wrapper out from under you in the first place. There is no flag to choose another version: the one in service is the one that passed.

Upstream releases are tracked and adopted deliberately — changelog review, then the candidate runs the battery, then the pin moves in an ordinary release. You get upstream's progress at the speed of proof, never at the speed of surprise.

BULK-LOAD ACCELERATORS — THE SPEED LAW

DVT's standing rule is that loads run at each engine's highest speed. Most engines carry their bulk lane natively — Postgres COPY, the MySQL family's LOAD DATA, ClickHouse's HTTP insert, Snowflake's PUT+COPY, StarRocks Stream Load, DuckDB and SQLite in-process. But a few engines gate their bulk lane behind the engine's own client tool, which has to exist on the machine where DVT runs: SQL Server, Fabric and Synapse need bcp; Oracle needs SQL*Loader (sqlldr). Without the tool, loads silently fall back to batched inserts — same green result, orders of magnitude slower.

So synctreats accelerators exactly like adapters and drivers: it checks for each engine's tool and provisions what it can (on macOS that includes the Oracle Instant Client dylib links SQL*Loader refuses to start without). An accelerator sync can't install itself becomes a warning naming the exact install command — never a failed sync, because a slow load is still a correct load. The difference is not subtle: the same 1-million-row Oracle seed measured 424s on the insert fallback and 125s through SQL*Loader — a 3.4× gain from one client tool.

The provisioning is self-healing, too: if a client upgrade later removes the tool out from under DVT, the load doesn't quietly degrade or die with a cryptic path error — it stops with the three fixes spelled out (relink the client, re-run dvt sync, or remove the accelerator to load slowly on purpose).

Presence is not availability. syncexecutes each accelerator it finds, so a tool that is installed but cannot start on this machine — an x86_64 SQL*Loader on Apple silicon after an OS upgrade removed Rosetta, say — is reported as missing, with the fix (Rosetta, or an arm64 build of the client tools). A load that meets such a tool at run time is re-issued once on the loader's plain lane with the same warning: slower, still landed.

UV PROJECTS — THE DRIVERS ARE RECORDED, NOT JUST INSTALLED

If your project is managed by uv — a pyproject.toml and a uv.lock beside it, and dvt running from the project's .venv — a plain install into the environment would not survive the next uv sync, which rebuilds the environment from the lock and removes anything the lock never saw. So in a uv project the adapters step is uv add: the extras are recorded in your pyproject.toml and uv.lock and installed into the project environment, and the plan says so before anything runs:

Sync plan (execution follows — no prompt; the invocation is the consent):
  → env: uv add "dvt-adapters[duckdb,oracle,snowflake]" (uv project — recorded in pyproject.toml + uv.lock so `uv sync` keeps the drivers)

Anywhere else — a plain venv, a Docker image, a CI runner — nothing changes. If uv add refuses, sync falls back to the plain install and the reconciliation at the end shows the divergence.

WHAT ELSE RIDES ALONG

Bucket packages. A profile that declares cloud-storage connectors gets their transports installed too — s3, gcs, and azure each map to their own package, installed only when your profiles.yml actually declares that connector.

Python model packages. syncscans your project's Python models — explicit dbt.config(packages=[...])declarations first, then top-level imports — and installs whatever's missing. With a guard: a requirement set that would move DVT's own foundational packages is refused with instructions to pin compatible versions in your models, never installed anyway.

Compatibility macros. sync writes macros/dvt_federation_materializations.sql into the project so that plain dbt run — an IDE extension, CI calling dbt directly — hitting an f_table model fails with a clear "run it with dvt run" instead of a baffling "materialization not found".

Connection check. Every output in your profile is validated as a well-formed connection config, and a broken one is named output-by-output — so a typo in profiles.yml surfaces here, not three steps into your first run.

Every engine in your profile gets the full treatment — Postgres, Redshift, Snowflake, BigQuery, Athena, Databricks, DuckDB (MotherDuck included), Trino, ClickHouse, Oracle, SQL Server, Fabric, SQLite, MySQL, MariaDB, StarRocks, Exasol. The per-engine details live on the adapter pages.

REFERENCE — EVERY FLAG

All flags are optional — the whole point of syncis that the bare invocation figures everything out from the project it's standing in.

FLAGDEFAULTWHAT IT DOES
--project-dir <path>current directoryThe project to sync for — where dbt_project.yml lives. --dbt-project is a shipped alias with the same meaning; scripts that type either keep working.
--profiles-dir <path>resolvedWhere profiles.yml lives. The default is the same resolver every DVT command uses — $DBT_PROFILES_DIR, then a project-local profiles.yml, then ~/.dbt — so sync installs drivers for the same file everything else reads.
--profile <name>auto-detectedWhich profile's outputs decide the adapter set. Auto-detected from dbt_project.yml's profile: key (falling back to the project name).
--python-env <path>the active environmentThe Python environment bucket and Python-model packages install into. Adapters always install into the environment dvt itself runs in — one environment is the design, not an option.