SEEDS
Seeds are data files that live in your project's seeds/ folder and load into your target as tables — reference data like country codes, category mappings, or plan features that belongs in version control. Drop a file in, run dvt seed, and your models can ref() it like any other table.
QUICK START
Put a file in seeds/. The filename stem becomes the table name:
# seeds/country_codes.csv country_code,country_name,region US,United States,Americas DE,Germany,EMEA JP,Japan,APAC BR,Brazil,Americas
Run dvt seed:
$ dvt seed 14:02:11 Concurrency: 4 threads (target='pg_dev') 14:02:11 1 of 1 START seed file public.country_codes ..................... [RUN] 14:02:12 1 of 1 OK loaded seed file public.country_codes ................. [INSERT 4 in 0.84s] 14:02:12 Finished running 1 seeds in 0 hours 0 minutes and 0.91 seconds (0.91s). 14:02:12 Completed successfully 14:02:12 Done. PASS=1 WARN=0 ERROR=0 SKIP=0 TOTAL=1
The seed landed as public.country_codeson your default target — the target schema, the filename stem. From here it's a normal node in the DAG:
-- models/staging/stg_orders.sql
select o.order_id, o.amount, c.region
from {{ source('shop', 'orders') }} o
join {{ ref('country_codes') }} c on o.country = c.country_codedvt seedparses your project first, exactly like every graph-bearing command — a project that doesn't parse is refused before a single seed loads.
WHEN TO USE SEEDS
Seeds are for small, slowly-changing data that should live in git next to the models that use it: lookup tables, mappings, test fixtures, plan matrices. When the data changes, you edit the file, commit, and re-run — the table always mirrors the file.
They are not for raw production data — that's what sources declare. dbt's classic advice to keep seeds tiny is partly about its loader being slow on big files; DVT removes the loader penalty (next section), but git history is still the honest limit on how big a seed should get.
ENHANCEMENT 1: BULK LOADING VIA SLING
Seeds behave exactly like dbt seeds — same semantics, same place in the DAG, same seed phase in dvt build. DVT changes two things, and the first is how the bytes move.
In plain dbt: dbt seed parses the CSV in Python and pushes rows in batches of INSERT statements — a million-row seed becomes thousands of round trips to the warehouse.
In DVT: the file goes to Sling, which uses each engine's COPY-class bulk ingestion path, and seeds load in parallel up to your profile's threads. 10-100x faster on large files — 2.1M rows / 138 MB landed in ~10s in our benchmark.
Parallelism is automatic and honest: the concurrency line at the top of every run tells you how many loaders are active. Single-writer file targets (DuckDB, SQLite) take one loader at a time — running four threads against a file database isn't a slow path, it's lock failures — and DVT says so instead of silently doing less:
14:05:03 Concurrency: 1 threads (target='duck_local')
14:05:03 Serialized: 'duck_local' is a duckdb file database — one writer
at a time, so the profile's threads:4 would collide on its lock.ENHANCEMENT 2: FOUR FILE FORMATS
In plain dbt: seeds are CSV-only. Reference data you already have as parquet or JSON must be converted before it can be a seed.
In DVT: a seed is any of the four DVT file formats — csv, parquet, json, jsonl (ndjson is an accepted spelling of jsonl). Drop the file in seeds/ and run; the same file yields the same columns, types, and row count as it would through every other DVT file lane.
seeds/ ├── country_codes.csv → public.country_codes ├── fx_rates_2025.parquet → public.fx_rates_2025 ├── plan_features.json → public.plan_features └── event_samples.jsonl → public.event_samples
Anything outside the four is refused by name, before anything loads — never silently skipped, never a half-seeded project. An .xlsx in seeds/ gets:
DVT065: seed: seed file 'regions.xlsx' asks for 'xlsx', which DVT does not support. 'xlsx' was removed in 0.2.50: DVT's file surface is four formats that behave identically in every lane, not ten that behave differently per lane. DVT supports exactly four file formats: csv, parquet, json, jsonl (ndjson is an accepted spelling of jsonl).
SELECTING SEEDS
--select (-s) and --exclude take bare seed names or full dbt selection syntax. Bare names resolve instantly; anything richer — graph operators, wildcards, tag:, path:— is answered by the engine's own selector, so a graph selection includes exactly the seeds a native build would:
dvt seed --select country_codes # one seed, by name dvt seed -s country_codes -s fx_rates_2025 # several (repeatable) dvt seed --exclude legacy_map # everything except one dvt seed --select +fct_orders # the seeds fct_orders depends on dvt seed --select tag:reference # everything tagged reference
The +fct_orders form is the one to remember: dvt build --select +my_modeluses the same resolution, so the seed phase of a build loads a model's seed ancestors automatically. And if a seed fails during dvt build, its dependents are marked SKIPPED — dbt's own semantics — instead of dying downstream on a raw engine error.
STOPPING EARLY: --FAIL-FAST
By default every seed gets its chance and failures are tallied at the end. --fail-fast (-x) stops at the first failure: whatever is already loading finishes, nothing new starts, and the tally counts what was skipped:
$ dvt seed --fail-fast 14:07:29 Concurrency: 4 threads (target='pg_dev') 14:07:29 1 of 6 START seed file public.country_codes ..................... [RUN] 14:07:29 2 of 6 START seed file public.fx_rates_2025 ..................... [RUN] 14:07:30 1 of 6 OK loaded seed file public.country_codes ................. [INSERT 249 in 0.71s] 14:07:30 3 of 6 START seed file public.bad_rows .......................... [RUN] 14:07:31 3 of 6 ERROR loading seed file public.bad_rows .................. [ERROR in 0.42s] 14:07:31 Aborting --fail-fast: a seed failed ............................. [SKIP 3] 14:07:31 2 of 6 OK loaded seed file public.fx_rates_2025 ................. [INSERT 8760 in 1.63s] 14:07:31 Finished running 6 seeds in 0 hours 0 minutes and 2.24 seconds (2.24s). 14:07:31 Completed with 1 error(s): 14:07:31 Failure in seed bad_rows 14:07:31 Done. PASS=2 WARN=0 ERROR=1 SKIP=3 TOTAL=6
RELOAD SEMANTICS: TRUNCATE VS --FULL-REFRESH
A plain dvt seed truncates each seed table and reloads it — the table object survives, so dependent views and grants survive with it. --full-refresh drops and recreates the table instead:
dvt seed # truncate + reload (dependent views survive) dvt seed --full-refresh # drop + recreate (use when columns changed)
Either way the table ends up matching the file — a seed is its file, so full-refresh is the only sensible seed semantic and the default mode replaces the data too. The difference is DDL: reach for --full-refresh when you added, removed, or retyped columns and the existing table shape no longer fits.
COLUMN NAMES: SNAKE_CASE BY DEFAULT
Seed column names are normalized to snake_case on load — "SKU Code" becomes sku_code, deterministically, on every engine. Spaced or mixed-case headers break Oracle's insert path and Databricks-class metastores outright; snake_case is the one casing every engine agrees on, and it's what makes the same seed load identically everywhere.
Override it with --column-casing (sourcekeeps the file's headers as-is, snake / upper / lower normalize):
dvt seed --column-casing source # keep "SKU Code" exactly as written dvt seed --column-casing upper # SKU_CODE
Oracle gets special handling automatically: columns land UPPERCASE (Oracle's native case fold, so unquoted SQL resolves them) and strings land as sized VARCHAR2 rather than CLOB — so seed-fed models can GROUP BY and join on them without ORA-00932 surprises.
COLUMN TYPES: +COLUMN_TYPES
Types are inferred from the file, and inference is usually right. When an engine's strict casting rejects an inferred type — Databricks ANSI mode meeting 1-Mar-15-style dates, say — pin the column with dbt's native +column_types config in dbt_project.yml. DVT honors it and passes it through to the loader:
# dbt_project.yml
seeds:
my_project:
ordering_history:
+column_types:
ordering_date: string
order_id: bigintWHERE SEEDS LAND
Seeds load into the target's schema as <schema>.<filename stem>, and DVT creates that schema if it doesn't exist yet — a fresh project's first dvt buildnever dies on "schema does not exist". The one exception is Oracle, where schemas are users: DVT won't create a user for you, so the schema must already exist there.
--target loads seeds to any output in your profile — including bucket storages. On a bucket target (S3, GCS, Azure, SFTP) there are no tables, so each seed lands as a parquet object named after the seed, whatever format the file started as:
dvt seed --target dbx_dev # seeds → another engine dvt seed --target s3_lake # seeds → a bucket # seeds/country_codes.csv → <object_base>/country_codes.parquet
Every loaded seed is also recorded in DVT's materialization ledger, which is why dvt generate-sources never rediscovers your own seed tables as sources.
FLAG REFERENCE
| FLAG | WHAT IT DOES |
|---|---|
| -s, --select | Seeds to load — bare names or full selection syntax (graph operators, wildcards, tag:, path:). Repeatable. Default: every seed. |
| --exclude | Seeds to leave out — same syntax as --select. Repeatable. |
| -t, --target | Profile output to load into, including bucket storages (seeds land as parquet objects). Default: the profile's default target. |
| --threads | Parallel loads. Default: the target's threads setting. Single-writer file targets (duckdb, sqlite) serialize to 1 with a printed notice. |
| --full-refresh | Drop and recreate each seed table. Default: truncate + reload, which keeps dependent views alive. |
| --column-casing | Column-name normalization: source | snake | upper | lower. Default: snake. |
| -x, --fail-fast | Stop at the first failed seed — in-flight loads finish, nothing new starts, skips are tallied. |
| --project-dir | Project directory. Default: the current directory. |
| --profiles-dir | Profiles directory. Default: ~/.dbt (or $DBT_PROFILES_DIR). |
| --debug | Debug logging. |