LEAST-PRIVILEGE PREREQUISITES

This is the page to hand your DBA before granting DVT anything. It answers one question per engine — what is the smallest set of privileges that makes this work? — and it answers it four times, because a read-only federation source and a default target are not the same ask, and neither is the write surface you may never turn on.

Everything below is grounded in what DVT actually issues — the catalog queries source discovery runs, the SQL the load lane sends, the drops retract performs — or in the engine's own published permission model. Where we could verify the exact statement, it is here as a statement you can paste. Where we could not, the cell says unverified and explains what specifically is unproven, rather than inventing grant syntax that reads authoritative and is wrong.

THE FOUR TIERS

READ-ONLY FEDERATION

Introspect the catalog and extract rows. This is what a federation source needs and nothing else — DVT writes nothing to a source engine.

DEFAULT TARGET

Create, load, replace and drop the tables DVT lands. Everything here happens inside one schema you nominate.

DML SURFACE

Hooks, dvt exec, the Explorer grid editor and the api-portal write endpoints. Opt-in, and the one tier where the credential is the only limit.

CLOUD STAGING

What the load lane needs on the engine's object area. Only cloud warehouses and buckets have anything in this column, and on the warehouses it is a recommendation DVT degrades around — not a wall.

They stack in the obvious way. An engine you only read from needs the first column and nothing else. An engine that is also your default target needs the second. The third is opt-in — turn it on and the credential itself becomes the only limit, which is the whole reason it has its own column.

WHAT DVT NEVER NEEDS

The list below is not a policy promise; it is a description of the commands DVT contains. None of these appear in any lane:

  • superuser, sysadmin, ACCOUNTADMIN, db_owner, DBA, metastore admin — no lane needs an administrative role on any engine. Every grant on this page is object- or schema-scoped.
  • CREATE DATABASE, DROP DATABASE, DROP SCHEMA — DVT issues none of them, anywhere. Retract drops the tables and views it recorded as its own and stops there — a project leaves no footprint, but it also never takes a schema with it.
  • any write against a federation source — the extraction lane emits a single SELECT of the pruned columns, with pushed-down predicates. No temp tables, no watermark tables, no bookkeeping on the source. Incremental state lives in the local DuckDB cache inside your project.
  • STORAGE CREDENTIAL or EXTERNAL LOCATION on Unity Catalog, storage integrations or external stages on Snowflake — the staging area DVT uses is a managed volume or an internal stage. The highest-privilege objects on both platforms stay out of the conversation.
  • account-, project- or organisation-level IAM — on the cloud platforms every role on this page binds to one dataset, one bucket or one prefix.
  • SELECT ANY TABLE and its equivalents — source discovery reads ALL_TABLES on Oracle, information_schema elsewhere — never DBA_* views, never SNOWFLAKE.ACCOUNT_USAGE, never Databricks system tables. Per-object grants are enough because nothing reads above them.

One honest asterisk: DVT issues no create schema, but the dbt lane underneath it will create the target schema if it is missing. Pre-create that schema yourself and the requirement disappears entirely — which is the recommended setup anyway, since it is the schema you are scoping every other grant to.

THE MATRIX

One row per connector, one column per tier. The detail — the actual statements — is in the per-engine sections below.

ENGINESREADTARGETDMLSTAGING
PostgreSQLCONNECT + USAGE + SELECT ✓CREATE on schema + owns what it creates ✓INSERT / UPDATE / DELETE on the target rows ✓none
SnowflakeUSAGE on warehouse + db + schema, SELECT ✓CREATE TABLE / CREATE VIEW on the schema ✓INSERT / UPDATE / DELETE / TRUNCATE ✓CREATE STAGE on the schema — recommended ⚠ NEXT RELEASE
BigQuerydataViewer on the dataset + jobUser ✓dataEditor on the target dataset + jobUser ✓dataEditor (DML is table-data writes) ✓unverified — DVT configures no staging bucket ⚠
RedshiftUSAGE on schema + SELECT ✓CREATE on schema, DROP + TRUNCATE on tables ✓INSERT / UPDATE / DELETE ✓none — no S3 COPY on this lane ✓
DatabricksUSE CATALOG + USE SCHEMA + SELECT ✓CREATE TABLE + MODIFY on the schema ✓MODIFY ✓CREATE VOLUME on the schema — recommended ⚠ NEXT RELEASE
BUCKETS & FILE LANESREADTARGETDMLSTAGING

✓verified — the statement was read from the code path that issues it and the privilege from the engine's own reference. ⚠unverified — one half is unproven; the engine's section says which. Blank cells mean the tier does not exist for that connector. NEXT RELEASEnext release — the cell describes behaviour that is not in 0.2.58; the engine's section says what 0.2.58 does instead.

CLOUD STAGING: THE TWO HALVES, AND THE HONEST DEGRADATION

NEXT RELEASE — NOT IN 0.2.58

What follows is built and documented ahead of the release that carries it. pip install dvt-core gives you 0.2.58, and 0.2.58 does not do it yet.

On 0.2.58, today: the per-run staging root is the part that has not shipped. The published load lane issues no CREATE STAGE and no CREATE VOLUME — on Snowflake and Databricks alike the load stages into the loader's own default area, which is precisely the degraded path this section describes. So the honest reading for a DBA scoping a role today is: skip the DDL grant, grant the file half, and expect staging residue to live in that shared area rather than leave with the run. Everything else on this page — READ, TARGET and DML — is shipped behaviour and stands as written.

On Snowflake and Databricks the load travels through the engine's own object area — an internal stage, a Unity Catalog volume. Given the grant, DVT mints its own staging root, per run, and drops it wholesale when the run ends. That is what makes cleanup a single DROP instead of a hunt through a shared directory that another session might be using.

Two things about this column matter more than the grant syntax, and both are places DBAs get caught:

  • the DDL grant and the file permission are different grants — being allowed to create a stage or a volume is not being allowed to write files into it. On Databricks the file cleanup does not even go through SQL — it calls the workspace Files API with the connection's token, so that token needs file write and delete on the volume on top of the Unity Catalog grant. A principal with immaculate SQL grants and no file access fails halfway through a load.
  • these grants are recommended, not required — withhold them and the load still runs. DVT logs the missing grant by name, once, and falls back to the loader's shared default staging root. What you lose is the wholesale drop: staging residue then accumulates in that shared location instead of leaving with the run. Nothing fails, nothing is silent, and the choice stays yours.

Verification status:the two staging cells are read from the loader's source and from each platform's own privilege reference — they are not measured against a live warehouse under exactly that grant set. They are marked unverified in the matrix for that reason, and they will stay marked until a run proves them. Everything else in the staging column is a plain statement that there is no staging area at all.

THE DML SURFACE, STATED PLAINLY

Four features write rows: pre- and post-hooks on federated models, dvt exec, the Explorer grid editor, and the API portal's write endpoints. They share one execution path, and the honest description of that path is: it sends the statement as written. DVT does not parse it, narrow it, or wrap it in a transaction.

Which leads to the single most important sentence on this page: if least privilege matters to you, enforce it at the engine — a hook may name any connection in your profile, including one you think of as a read-only source. A source credential that is read-only at the engine cannot be talked into a write by anything in DVT. A source credential that happens to be an admin account can.

The guardrails that do exist, stated exactly:

  • the Explorer's raw SQL box is SELECT-only, enforced by parsing — not by convention. Reads also get an engine-side row limit injected.
  • the grid editor refuses keyless updates and deletes — an edit with no key would touch every row, so it is rejected server-side rather than executed. Applies run one statement at a time and stop at the first failure — there is no cross-engine transaction to roll back into.
  • API-portal writes are off until you turn them on — and they need key enforcement enabled, a valid key, the model enabled, a separate per-model writes toggle, an allow-listed column set and a non-empty where clause. Every executed statement is written to an audit log inside the project.
  • buckets and file connections refuse the DML lane outright — a bucket is rewritten, never updated in place. Hooks, exec and the write endpoints cannot address one.

PER-ENGINE DETAIL

Jump to yours. Each block gives the four tiers, with statements where the syntax is verified and plain English where it is not.

READ-ONLY FEDERATION

Source discovery reads information_schema.tables; extraction is a plain SELECT of the pruned columns. information_schema is readable by every role and already filters itself to objects the role can see, so no extra metadata grant is needed.

GRANT CONNECT ON DATABASE analytics TO dvt_reader;
GRANT USAGE ON SCHEMA public TO dvt_reader;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO dvt_reader;

-- so tomorrow's tables are covered too
ALTER DEFAULT PRIVILEGES IN SCHEMA public
  GRANT SELECT ON TABLES TO dvt_reader;

DEFAULT TARGET

Sling creates and loads the landed table; DVT sends a `drop table if exists … cascade` before a full refresh and `drop table` / `drop view` on retract. CREATE on the schema is enough because the role then owns everything DVT made — dropping is an ownership right, not a grant. TRUNCATE is what a plain `dvt seed` uses.

GRANT CONNECT ON DATABASE analytics TO dvt_target;
GRANT USAGE, CREATE ON SCHEMA analytics TO dvt_target;

-- only if DVT must load into tables it did not create
GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE
  ON ALL TABLES IN SCHEMA analytics TO dvt_target;

DML SURFACE

Whatever the statement itself requires on the object it touches — on whichever connection the hook, the exec or the write endpoint names. DVT does not narrow it in code.

GRANT INSERT, UPDATE, DELETE ON analytics.orders TO dvt_target;

CLOUD STAGING

There is no warehouse-side staging area. Rows land through the connection itself; the only staging DVT owns is local Parquet under the project's .dvt/tmp.

  • ▸The full-refresh pre-drop is CASCADE. It will take down dependent views the role owns — give DVT its own schema if that matters.

READ-ONLY FEDERATION

Discovery reads information_schema.tables — never SHOW, never SNOWFLAKE.ACCOUNT_USAGE, so no ACCOUNTADMIN-adjacent grant appears. A warehouse is a required connection field even for pure reads, which makes USAGE ON WAREHOUSE a genuine read-tier prerequisite.

GRANT USAGE ON WAREHOUSE dvt_wh TO ROLE dvt_reader;
GRANT USAGE ON DATABASE analytics TO ROLE dvt_reader;
GRANT USAGE ON SCHEMA analytics.raw TO ROLE dvt_reader;
GRANT SELECT ON ALL TABLES IN SCHEMA analytics.raw TO ROLE dvt_reader;
GRANT SELECT ON FUTURE TABLES IN SCHEMA analytics.raw TO ROLE dvt_reader;

DEFAULT TARGET

The role creates the landed table and therefore owns it, which covers the insert, the replace and the retract drop. Nothing at database or account level is required beyond USAGE.

GRANT USAGE ON WAREHOUSE dvt_wh TO ROLE dvt_target;
GRANT USAGE ON DATABASE analytics TO ROLE dvt_target;
GRANT USAGE, CREATE TABLE, CREATE VIEW
  ON SCHEMA analytics.marts TO ROLE dvt_target;

DML SURFACE

Whatever the statement itself requires on the object it touches — on whichever connection the hook, the exec or the write endpoint names. DVT does not narrow it in code.

GRANT INSERT, UPDATE, DELETE, TRUNCATE
  ON TABLE analytics.marts.orders TO ROLE dvt_target;

CLOUD STAGING

RECOMMENDED, NOT REQUIREDUNVERIFIEDNEXT RELEASE

Bulk loads travel through a Snowflake internal stage. Given CREATE STAGE on the schema, DVT mints its own per-run stage — `create stage if not exists <db>.<schema>.dvt_staging_<12 hex>` — and drops it wholesale when the run ends, so the staging tree is DVT's by construction and leaves nothing behind. Reading and writing the files in that stage is a separate permission from the DDL grant: a role that may create a stage but not write to it will fail at the first file.

-- let DVT mint and drop its own per-run stage
GRANT CREATE STAGE ON SCHEMA analytics.marts TO ROLE dvt_target;

-- if CREATE STAGE is off the table, point DVT at one you own:
GRANT USAGE ON STAGE analytics.marts.dvt_staging TO ROLE dvt_target;
GRANT READ, WRITE ON STAGE analytics.marts.dvt_staging TO ROLE dvt_target;

Without it: The load still runs. DVT logs the missing grant by name, once, and falls back to the loader's shared default stage — which it cannot drop wholesale. Staging residue then accumulates in that shared location instead of disappearing with the run.

Unverified: Read from the loader's source, not measured against a live Snowflake account. The privilege names are Snowflake's own and are verified; what is unproven here is the end-to-end run under exactly this grant set.

NEXT RELEASE — NOT IN 0.2.58

What follows is built and documented ahead of the release that carries it. pip install dvt-core gives you 0.2.58, and 0.2.58 does not do it yet.

On 0.2.58, today: DVT mints no stage. Nothing in the published load lane issues CREATE STAGE, so granting it buys you nothing yet — every load already travels through the loader's own default internal stage, which is exactly the fallback described under “Without it”. Grant the READ/WRITE half if you are scoping a role now; the CREATE STAGE half is the one that starts paying off next release.

  • ▸Never ACCOUNTADMIN, never SECURITYADMIN, never an external stage, never a storage integration, never a file format — the per-run stage is an internal one.
  • ▸The fallback path cleans files with a REMOVE against the shared default stage. Giving DVT its own stage is what turns cleanup from per-file into one DROP.

READ-ONLY FEDERATION

Discovery queries the dataset's own INFORMATION_SCHEMA.TABLES, and extraction runs a query — so the identity needs both the data role on the dataset and the right to run jobs in the project.

# dataset-scoped read
gcloud projects add-iam-policy-binding PROJECT \
  --member='serviceAccount:dvt@PROJECT.iam.gserviceaccount.com' \
  --role='roles/bigquery.jobUser'

bq add-iam-policy-binding \
  --member='serviceAccount:dvt@PROJECT.iam.gserviceaccount.com' \
  --role='roles/bigquery.dataViewer' \
  PROJECT:analytics

DEFAULT TARGET

dataEditor covers creating, replacing and deleting the dataset's tables plus writing their data. Nothing at organisation or folder level is involved.

bq add-iam-policy-binding \
  --member='serviceAccount:dvt@PROJECT.iam.gserviceaccount.com' \
  --role='roles/bigquery.dataEditor' \
  PROJECT:marts

DML SURFACE

Whatever the statement itself requires on the object it touches — on whichever connection the hook, the exec or the write endpoint names. DVT does not narrow it in code.

CLOUD STAGING

UNVERIFIED

DVT never sets a GCS staging location on the BigQuery connection, so the load falls through to whatever the loader does by default. If that turns out to be a GCS-staged load, the identity will also need object create and delete on the bucket it picks.

Unverified: BigQuery is a supported adapter, not one of the engines standing in DVT's live verification suite — so this cell is a reading of the code path, not an observed run. Treat it as: grant the two roles above, run a load, and read the error if there is one.

  • ▸Never roles/bigquery.admin, never a project-level owner or editor role.

READ-ONLY FEDERATION

Discovery reads information_schema.tables — no SVV_* system views, no catalog-admin grant. Redshift's own ACCESS CATALOG system permission is not needed.

GRANT USAGE ON SCHEMA public TO dvt_reader;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO dvt_reader;

DEFAULT TARGET

Same shape as PostgreSQL, with Redshift's explicit DROP privilege: the full-refresh pre-drop is a CASCADE drop and retract drops tables and views.

GRANT USAGE, CREATE ON SCHEMA analytics TO dvt_target;
GRANT SELECT, INSERT, UPDATE, DELETE, DROP, TRUNCATE
  ON ALL TABLES IN SCHEMA analytics TO dvt_target;

DML SURFACE

Whatever the statement itself requires on the object it touches — on whichever connection the hook, the exec or the write endpoint names. DVT does not narrow it in code.

GRANT INSERT, UPDATE, DELETE ON analytics.orders TO dvt_target;

CLOUD STAGING

DVT maps Redshift over the PostgreSQL wire protocol and configures no S3 bucket, so loads do not go through COPY. There is no bucket policy to write and no ASSUMEROLE grant to make — and, honestly, no COPY-speed bulk load either: large loads travel over the wire.

  • ▸IAM authentication is refused on DVT's data-movement lane — this connection takes a database user and password. Short-lived credentials are not available here.
  • ▸The full-refresh pre-drop is CASCADE, same caveat as PostgreSQL.

READ-ONLY FEDERATION

Discovery reads the catalog's own information_schema.tables — never the system.* tables, which is what keeps this off metastore-admin territory. USE CATALOG and USE SCHEMA grant no data by themselves; SELECT does.

GRANT USE CATALOG ON CATALOG main TO `dvt_reader`;
GRANT USE SCHEMA ON SCHEMA main.raw TO `dvt_reader`;
GRANT SELECT ON SCHEMA main.raw TO `dvt_reader`;

DEFAULT TARGET

The loader creates the landed table and merges into it; MODIFY is Unity Catalog's insert/update/delete privilege. Dropping on retract follows from owning what was created.

GRANT USE CATALOG ON CATALOG main TO `dvt_target`;
GRANT USE SCHEMA, CREATE TABLE ON SCHEMA main.marts TO `dvt_target`;
GRANT SELECT, MODIFY ON SCHEMA main.marts TO `dvt_target`;

DML SURFACE

Whatever the statement itself requires on the object it touches — on whichever connection the hook, the exec or the write endpoint names. DVT does not narrow it in code.

GRANT MODIFY ON TABLE main.marts.orders TO `dvt_target`;

CLOUD STAGING

RECOMMENDED, NOT REQUIREDUNVERIFIEDNEXT RELEASE

The load stages files in a Unity Catalog volume and then runs COPY INTO. Given CREATE VOLUME on the schema, DVT mints its own per-run volume — `create volume if not exists <catalog>.<schema>.dvt_staging_<12 hex>` — and drops it when the run ends. Watch the second half: the file cleanup does not go through SQL at all. It calls the workspace Files API with the connection's token, so that token needs write and delete on the volume's files in addition to the SQL grant. This is the trap — a principal with perfect SQL grants and no file access fails halfway through.

-- the SQL half: let DVT mint and drop its own per-run volume
GRANT CREATE VOLUME ON SCHEMA main.marts TO `dvt_target`;

-- the file half, required either way
GRANT READ VOLUME, WRITE VOLUME ON SCHEMA main.marts TO `dvt_target`;

-- if CREATE VOLUME is off the table, scope the file grants to one volume you own
GRANT READ VOLUME, WRITE VOLUME
  ON VOLUME main.marts.dvt_staging TO `dvt_target`;

Without it: The load still runs. DVT logs the missing grant by name, once, and falls back to the loader's shared default volume — which it cannot drop wholesale, so staging residue accumulates there instead of leaving with the run.

Unverified: Read from the loader's source, not measured against a live workspace. The Unity Catalog privilege names are Databricks' own and are verified; the end-to-end run under exactly this grant set is not.

NEXT RELEASE — NOT IN 0.2.58

What follows is built and documented ahead of the release that carries it. pip install dvt-core gives you 0.2.58, and 0.2.58 does not do it yet.

On 0.2.58, today: DVT mints no volume, and calls no Files API. Nothing in the published load lane issues CREATE VOLUME, so that grant buys you nothing yet — every load stages into the loader's own default volume, which is the fallback described under “Without it”. The file half is the half that matters today: READ VOLUME and WRITE VOLUME are what a load actually needs. Grant those now; CREATE VOLUME is the one that starts paying off next release.

  • ▸A managed volume needs no STORAGE CREDENTIAL and no EXTERNAL LOCATION — the two highest-privilege objects in Unity Catalog. DVT never asks for either.
  • ▸DVT's data-movement lane requires a personal access token; OAuth and service-principal browser flows are refused there. Issue the token for a dedicated principal whose grants are exactly the ones above — the token is the boundary, so it should carry nothing else.

How this page stays honest: a cell is verifiedwhen both halves check out — the statement DVT issues was read from the code path that issues it, and the privilege that covers it was read from the engine's own permission reference. It is unverifiedwhen either half is missing, most often because the engine is supported rather than standing in DVT's live verification suite. Unverified never means "probably fine": it means the note tells you exactly which half is unproven, so you can close the gap with one test run instead of trusting a docs page. Cells get promoted, never quietly reworded.