DVT EXEC
dvt exec runs one SQL statement on any named connection in your profiles.yml— Postgres, Snowflake, Oracle, whichever engine that connection points at. SELECTs come back as rows right in your terminal; UPDATE, DELETE and DDL execute and report the engine's affected-row count.
FIRST RUN
Pick a connection with -t, hand it a statement, done:
$ dvt exec -t pg_prod "select id, status from stg.events order by id desc limit 3" id | status -----+-------- 9114 | queued 9113 | shipped 9112 | shipped 3 row(s) from pg_prod
That's the whole idea. pg_prod is just an output name from your profiles.yml — the same names your models already use. No new credentials, no separate client, no connection string to remember.
THE USE CASE: A QUICK FIX BEFORE A RUN
You're about to run your models and notice a bad batch in the source system — a loader double-fired and left a few hundred orders flagged wrong in Postgres. You don't want to model around bad data; you want to fix it there, now, and then run. That's an exec:
$ dvt exec -t pg_prod "update stg.orders set status = 'cancelled' where batch_id = 8841" OK on pg_prod — 214 row(s) affected $ dvt run
The affected count is the engine's own answer, so you know immediately whether the fix landed on the rows you expected. When an engine doesn't report a count for a statement (some DDL doesn't), you get a plain OK on pg_prod — success, honestly stated, with no invented number.
IN PLAIN DBT
You open a separate SQL client per engine — psql for Postgres, snowsql for Snowflake, SQL Developer for Oracle — each with its own saved credentials, each one more thing to install and keep signed in. dbt itself has no way to run a one-off statement on an arbitrary connection; the closest is writing a macro and invoking dbt run-operation, and even that only reaches your one default target. dvt exec reuses the credentials already in profiles.yml, on any connection, by name.
SELECTS PRINT ROWS — AND --LIMIT CAPS THE DISPLAY
DVT looks at the statement to decide what kind it is. Anything that returns rows — SELECT, UNION, a CTE ending in a select, SHOW, DESCRIBE — comes back as a table. Everything else takes the execute path and reports a count. You never tell exec which is which; it just does the right thing.
Row display is capped at 500 by default so a careless select * from big_tabledoesn't flood your terminal. The full count is always reported, and the cap tells you when it's in effect:
$ dvt exec -t pg_prod -l 5 "select id from stg.events" id ---- 9114 9113 9112 9111 9110 1204 row(s) from pg_prod (showing 5 of 1204)
--limit 0 removes the cap entirely — every row, no truncation.
--JSON FOR SCRIPTS
Piping into jq or a script? --json switches both paths to one-line JSON. A query gives you columns, rows and the true row_count; a write gives you ok and affected:
$ dvt exec -t pg_prod --json "select count(*) as n from stg.orders"
{"columns": ["n"], "rows": [[48210]], "row_count": 1}
$ dvt exec -t pg_prod --json "delete from stg.events where dt < '2026-01-01'"
{"ok": true, "affected": 3172}--FILE FOR STATEMENTS WORTH SAVING
A statement long enough to be annoying on the command line probably deserves a file anyway. --file reads the SQL from disk instead of the argument:
$ dvt exec -t sf_dev --file cleanup.sql OK on sf_dev — 862 row(s) affected
One statement per invocation, either way — a trailing semicolon is fine (exec strips it), but this is a single-statement runner, not a script runner.
ENGINE-NATIVE, VERBATIM
Your SQL runs verbatimon the connection's engine — its dialect, its rules. Nothing is translated, rewritten or "helpfully" normalized on the way through. Oracle wants from dual? Give it from dual:
$ dvt exec -t eska "select systimestamp from dual" systimestamp ------------------------------------ 2026-08-03 14:22:07.581404 +02:00 1 row(s) from eska
This is the same execution lane DVT's federation engine uses for its own cross-engine DDL — so if a run works against a connection, an exec against it works too, dialect quirks and all. Even a statement so engine-specific that DVT can't parse it still executes; classification falls back to a keyword sniff rather than getting in your way.
BUCKETS ARE REFUSED BY NAME
Bucket and filesystem connections — s3, gcs, azure, sftp, the local filesystem — hold files, not tables. There is no SQL engine there to run your statement on, and exec says exactly that instead of failing somewhere confusing:
$ dvt exec -t s3_landing "select 1" 's3_landing' is a s3 bucket/filesystem connection — it has no SQL engine to run statements on
Files on those connections are read as sources in your models (csv, parquet, json, jsonl) — that's their surface. SQL is for the engines.
WHERE THE CONNECTION COMES FROM
Exec resolves profiles.yml the same way every DVT command does: --profiles-dir if you pass it, then $DBT_PROFILES_DIR, then a project-local profiles.yml, then ~/.dbt. Run it from your project directory (or point --project-dirat one) — the project's profile is how the connection name is looked up, and exec tells you plainly if there's no project here:
$ dvt exec -t pg_prod "select 1" no dbt_project.yml here — run inside a DVT project (or pass --project-dir)
Exit codes follow the same honesty: 0 when the statement ran, 1 when the engine rejected it (you'll see failed on 'pg_prod': ... with the engine's message), and 2 for setup problems — an unknown connection, a bucket, a missing project, an unreadable --file, or no SQL given at all.
REFERENCE — EVERY FLAG
| FLAG | TYPE | DEFAULT | WHAT IT DOES |
|---|---|---|---|
| sql | positional | — | The SQL statement, quoted. Optional only because --file is the other way in; passing neither is an error. One statement per invocation; a trailing semicolon is stripped. |
| --target, -t | string | (required) | The connection to run on — an output name from profiles.yml. Any database connection works; bucket/filesystem connections are refused by name. |
| --file, -f | path | — | Read the SQL from a file instead of the command line. An unreadable file is reported plainly and exits 2. |
| --limit, -l | integer | 500 | Max rows to display for SELECTs. The true row count is always reported, with a '(showing X of Y)' note when the cap is in effect. 0 removes the cap. |
| --json | flag | off | One-line JSON instead of the table: {columns, rows, row_count} for queries, {ok, affected} for writes. Made for jq and scripts. |
| --project-dir | path | . | The DVT project to run from — its profile name is how the connection is looked up in profiles.yml. |
| --profiles-dir | path | (resolved) | Where to find profiles.yml. Unset, the standard resolution applies: $DBT_PROFILES_DIR, then a project-local profiles.yml, then ~/.dbt. |