Skip to main content

Atlas vs dbt: Different Jobs, Better Together

These are not competing tools. dbt transforms data inside your schemas. It compiles SQL, resolves a model DAG, and materializes the tables and views its models describe. Atlas manages the schema layer those models sit on, meaning the databases, source tables, lookup data, and roles and grants, plus migration history, linting and CI review for all of it. Most teams that need one need the other, and the two run against the same warehouse without overlapping.

What Each Owns

dbt compiles SQL models, resolves ref() and source() into a DAG, materializes tables incrementally or as snapshots, runs tests that assert on data, and generates model documentation and exposures.

Atlas plans migrations for existing objects, keeps a migration history, lints changes before they run, detects drift between the warehouse and the code, manages lookup-table rows with INSERT, UPSERT, or SYNC semantics, and applies the same migration directory across environments.

Applied to the objects in one warehouse:

ObjectOwner
Model tables and views (analytics.stg_events, analytics.daily_active_users)dbt, recreated on every run
CSV seedsdbt, loaded with dbt seed
Databases the models are built intoAtlas
Source and raw tables that ingestion loads and models readAtlas
Lookup tables that are part of the source contract, and their rowsAtlas
Roles, users, and grants, including those dbt connects withAtlas
Migration history, linting, and CI review for all of the aboveAtlas

Neither is a shortcoming of the other tool. They describe different objects.

The boundary between the two is a glob list in atlas.hcl. Atlas manages the analytics database as an object and ignores the set of objects dbt materializes inside it. See Where the Line Falls below for the exact config.

Is dbt a Migration Tool?

dbt's materializations are not migrations. They rebuild an object from its model definition. Running dbt run twice on an unchanged project drops and recreates the same objects rather than computing a change:

1 of 2 START sql view model `analytics`.`stg_events` ........................... [RUN]
1 of 2 OK created sql view model `analytics`.`stg_events` ...................... [OK in 0.11s]
2 of 2 START sql table model `analytics`.`daily_active_users` .................. [RUN]
2 of 2 OK created sql table model `analytics`.`daily_active_users` ............. [OK in 0.45s]
Finished running 1 table model, 1 view model in 0 hours 0 minutes and 4.63 seconds (4.63s).

That model is fine for something you can rebuild from a select. It does not work for a source table that ingestion writes into, which holds data no model can regenerate. For those objects someone has to plan a change to the existing structure, which is what a migration is:

migrations/20260812081822_add_session_id.sql
ALTER TABLE `raw`.`events` ADD COLUMN `session_id` String;

Atlas generates that file from a one-column edit to the checked-in schema, and it goes through atlas migrate lint and atlas migrate apply like any other migration. This is not a gap in dbt: source tables are not dbt's objects to migrate.

What Atlas Adds

On top of planning that migration, Atlas covers three things a dbt project has no mechanism for:

  • Who reviewed this DDL? A change to a source table is typed into the warehouse by whoever has access. dbt has no plan step for it, no migration file, and nothing for a reviewer to approve. Atlas generates a migration file from the schema change, so it arrives as a diff in a pull request that someone approves and merges.

  • Has the warehouse drifted? dbt reads sources at run time and fails if something is missing, but it does not compare the warehouse against an intended state. A column that changed in type or a grant someone widened will not surface at all. Atlas diffs the live warehouse against the checked-in schema, and can block a deployment when they disagree.

  • Is this change safe to apply? Atlas lints generated migrations and fails on destructive changes before they reach a warehouse.

The Same Warehouse, Four Changes

Each of the following examples was run against one ClickHouse database holding both Atlas-managed source tables and dbt-managed models.

The changedbtAtlas
A model's SQL changesdbt run replaces the objectIgnored, as the change was inside the excluded database
A source table needs a new columnNo mechanism, as the object is not dbt'smigrate diff writes ALTER TABLE raw.events ADD COLUMN session_id String
A source column that models read is droppedModels fail at run time, after the factmigrate lint fails first: Dropping non-virtual column "email" DS103
dbt needs write access to build modelsDocumented as a prerequisiteGRANT ... ON analytics.* TO dbt_runner planned and applied as a migration

The third row is the one worth dwelling on. Atlas's linter reports the destructive change before it reaches a warehouse, and exits non-zero so CI can stop it:

  -- analyzing version 20260812082333
-- destructive changes detected:
-- L1: Dropping non-virtual column "email" https://atlasgo.io/lint/analyzers#DS103
-- suggested fix:
-> Add a pre-migration check to ensure column "email" is NULL before dropping it

Where the Line Falls

One exclude list in atlas.hcl decides where the boundary that prevents the two from interfering with one another lies:

atlas.hcl
exclude = [
"analytics.*", // every object dbt builds
"atlas_schema_revisions", // Atlas migration history
"atlas_schema_revisions.*",
]

With this in place and after dbt run has materialized two models into analytics, run:

atlas schema diff --env local --from "$CLICKHOUSE_URL" --to "file://schema.ch.hcl"
Schemas are synced, no changes to be made.

Remove the single "analytics.*" line and the same command against the same warehouse plans this instead:

-- Drop "stg_events" view
DROP VIEW `analytics`.`stg_events`;
-- Drop "daily_active_users" table
DROP TABLE `analytics`.`daily_active_users`;

That is the whole integration. Name dbt's database in exclude, and the two tools stop being able to disagree. The full walkthrough builds this from an empty warehouse.

note

If Atlas were to manage all of your warehouse DDL, the two tools would be in conflict on every dbt run. Instead, this guide ensures that model DDL belongs to dbt and Atlas leaves it alone.

Adding Atlas to an Existing dbt Project

There is nothing to remove or rewrite. Point Atlas at the warehouse you already have, with dbt's database excluded, and inspect it:

atlas schema inspect --env local > schema.ch.hcl

On the warehouse from the walkthrough, which contained both layers at the time, the output holds the source tables, the roles, the user and grants, and the analytics database as an object with none of dbt's models inside it:

schema.ch.hcl (inspected)
schema "analytics" {
engine = Atomic
}
schema "raw" {
engine = Atomic
}
permission {
to = role.dbt_runner
for = schema.analytics
privileges = [ALTER, CREATE_TABLE, CREATE_VIEW, DROP_TABLE, DROP_VIEW, INSERT, OPTIMIZE, SELECT, TRUNCATE]
}
permission {
to = role.dbt_runner
for = schema.raw
privileges = [SELECT]
}
role "dbt_runner" {
}
user "dbt" {
member_of = [role.dbt_runner]
auth_type = "no_password"
}

That file is your starting point. Commit it, and from here atlas migrate diff plans changes to the schema layer while dbt run keeps building models exactly as before.

For a warehouse whose schema layer predates Atlas, see importing an existing database to baseline the migration directory against what is already deployed.

Next Steps

Have questions? Feedback? Find our team on our Discord server.