Schemas should be first-class, versioned artifacts #15179
Replies: 0 comments 24 replies
|
I think this is a very important feature and any implementation would be a big improvement. I think the idea of schema locks is a really good system for handling this. A lot of the configs makes sense to me.
As long as I wonder how to keep these concepts intuitive for users
I think partial. In my experience new columns don't often critically require this level of enforcement |
|
As I told Anders last week, this is an extremely cool and necessary idea that I personally will never use! TL;DR:
Broadly, I see the remote source / schema cache relationship as follows:
Currently, Fusion's schema cache is a 1:1 representation of the DWH's reality at a given point in time. The ability to alter this representation into one that is ungrounded from the DWH's state is very empowering for end users, but it should not be taken lightly. Similarly, the move from Parquet to YAML seems necessary, but also introduces lots of potential for user error. Responses to posed questions
I have a strong preference for schema lockfiles to be ignored by Git by default, for two reasons:
Yes. but I do think it's important that the authoring interface for overriding the remote DWH state be independent from schema lockfiles. The proposal to define these in A question of my ownIf my understanding is correct, this architecture change is attempting to address two user archetypes:
First, I think the introduction of an explicit My question is: what is the intended path for User 1 to resolve their Are they supposed to know that they should run I think the pain points are actually best addressed through other ergonomic improvements in the CLI/VSCode Extension/dbt Studio: mainly, hinting towards refreshing the schema cache and restarting the LSP, if necessary. My concern is that without a better UX, users will end up introducing bad information into schema lockfiles that will result in confusion when attempting to run models. There's some amount of RTFM involved in Fusion already, and I'm not sure that the new YAML format will provide sufficient education on its own. Two sidebars
|
|
Definitely think this will be a very valuable feature! I agree with others that I see @sophieml 's points on defaulting to lockfiles not being version controlled, but if that's the case I almost wouldn't want to call them lockfiles (because in all other uses lock files are meant to be version-controlled to make environments reproducible). To safely have them be version-controlled, I think being able to declare source environments would be necessary (almost like deferral environments but in reverse). Then perhaps you can choose to version control specific source environments (just prod, prod+staging, whatever folks decide). This would also make it easier in the cases where there's a planned change to a source table, and you want to test the change's effects before they hit production. If you're syncing a lower-level source environment, you could temporarily switch to that and see what does / doesn't break, and update your models in preparation for the change. |
|
For me, this is a game breaking feature for the future of Fusion DBT ! I also think that the idea of schema locks is really elegant and is very well though (with the I think it will help for some DBT projects with specificities that make the hidden cache buggy (like wrongly interpret the type of a column with BigQuery TIMESTAMP vs DATETIME type for my project 😅) without breaking/changing anything for more standard DBT projects where the hidden cache works. Regarding the questions asked, here is my inputs:
Yes, I totally agreed with the reasoning describe in the proposal
I think we should go for
I think we should put it in a schema_dire path to being able to Git control it form the beginning. Because we want cache to be a real citizen of Fusion, we should not hide it from Git.
I would go for |
|
Opinions/Questions
Edit I say this because we sometimes (rarely, but it happens) only add columns that we have to test in our source yamls, but we still stage other columns. So if it were to be default local, we'd basically crash every time. |
|
Huge props, to me this is both the technical and philosophical correct direction for DBT fusion to head towards. It is undeniable, really, that the lockfiles should be version-controlled by default. It both doesn't degrade the end user experience and provides an improvement on the maintainer's experience. I will start with the end user experience portion. We should remember that although the schema-cache allows us to statically validate models without hitting the warehouse, data developers should (hopefully) actually still be testing their models by running them locally during the development process. I say this The antecedent to this being that users have access to the actual database objects that dependent sources of their particular If there is a schema lockfile, we use that metadata to statically verify the model. If the model depends on a column that does not exist in the schema lockfile, we can say this situation corresponds with the DBT fusion behavior when a column is referenced that does not exist in the schema cache. Today, fusion fails with e.g. and requires flushing and recreating the entire cache to resolve. The proposed behavior of the source schema lockfile will allow me to run If the model depends on a column that does exist in the schema lockfile, all is well in both the current day fusion allegory and fusion post-lockfile. If there is no schema lockfile, this is analogous to the cache being empty. In such a case, we should generate the lockfile. This presents no issue at all, as well, if the lockfiles are tied to an actual database object, rather than the source name. This would completely abstract away the actual environment your commands are run in and solve the problem of unfaithful lockfiles. It would be impossible for the lockfile to be unfaithful to the environment with such an implementation. If the lockfile differs, then it represents genuine drift that should be dealt with. The overall thrust of the argument on the user side is that schema lockfiles ostensibly seem to bring us closer to a reality where development on DBT just "works". For maintainers, the real value here lies in As well, consider sources whose schemas are programmatically generated. Today, if you want to check whether a schema migration on the producer side breaks your DAG, you need to manually twiddle with parquet files. With this proposed idea, the schema lockfiles as yaml would be easy enough to generate that we could just generate them with a simple Python script, then statically verify there are no structural failures in our DAG. A good architecture and implementation for schema lockfiles would honestly provide close to a step-function change for the maintainability of a DBT project. My hot take as well is that the manifest should get the same treatment and that would round out the step change. I elide that from this already large message, but can expound if anyone is curious. |
|
I've thought about this a bit longer since we met and I've swung back all the way to this engine doesn't need a To enumerate the issues with the cache that you listed above
I largely agree with all of these. I'm particularly interested in how this gets users to an offline static analysis of the sql. My gut reaction here is that it seems like this solution is to shift the burden of responsibility of maintaining the cache off the engine and to user via the I've jotted a lot of thoughts down below but if I can sum it up in a tl;dr I would say that: If its even possible I would prefer a solution that makes users think less about the cache rather than more. For users coming from dbt-core that just want it to work, it should just work even if it is more computationally expensive (more round trips to the db). For users like us with a giant project that are trying to emulate the cache by generating our own parquet files -- we want to bring our own table metadata (aka cache) so we aren't making any db round trips to get metadata. What is stopping the engine from just keeping better track of cache? Why does the cache get stale?The cache stores a list of tables and the table column names/data types. The engine today effectively does some kind of What is stopping the engine from looking at table x in the cache and comparing the cache time to the time the DDL for that table changed? I accept this could be tricky and a number of factors can get in the way. While Snowflake reliably keeps track of DDL changes with a I'm sure I missed a more sophisticated strategy -- but assuming the above is the best that can be done -- where does that leave users? You likely would find the engine fetching table metadata definitions from the database more often than is actually needed but slightly less than every table every run. My suspicion is that for a majority of projects and user profiles this is actually more acceptable than is assumed by this doc. Small to medium size projects can probably even just survive fetching the table definitions every time a command is run. How many sources does a project need to have before that actually becomes prohibitive? 100? 200? 1000? dbt-core/cloud users today accept an enormous overhead before every command even with partial parsing. How does
|
|
Here are my first round of thoughts. There may be more to some, sorry I was late to the discussion. Original questions:
Yes I think that the thing that the engine is using the validate the schema should be different from the thing that developers are using the define and configure the models. The are both models, in the simple definition of a thing that approximated reality, but they server different purposes and are interacted with in very different ways.
I would want partial by default, I am a little sensitive to dbt telling me when what I am doing is 'wrong' and I do not want to be yelled at just because a source system added a column.
While I am big on version control and 'business logic in code', this is not a time where I would want the thing version controlled. In our project these would not be a source of truth, not a definition of anything so having them as part of the version controlled code would just add process without value.
We do not currently use contracts so I do not have a strong opinion here.
Introspected schemas maybe? I agree that these are not lock files in the traditional sense so calling them such might be confusing. Current MVP statusThe outlined changes will still require manual intervention and manually overwriting files for our project. This is no different than the current schema parquet files for us we just would not have to maintain code to generate the parquet files so that would be a plus. My thoughtsThe first thing I think of is that I would have the command be
Second thing I think of is that the assumption that we would not want a stale schema is not always valid. There are times in our development process where we want the schema to remain static regardless of changes to any actual database. I think that the developers should be able to control any refresh frequency that is implemented and it should include the ability to tun it off. If I run the analysis on my code with out of date schema definitions that is on me.
I would want to be able to use |
|
Custom Materialization Feedback It would be valuable if had a way to define Columns that a materialization will add to a table, while not needing to define the columns in the model itself. An example would be: And the materialization adds a set of columns (e.g. for audit purposes) such that we have columns derived from the Model & from the Materialization.
Ideally we would have a way to define somewhere that a materialisation with a given name will generate a set of columns, and have Fusion append those to the columns it parses out of the model on its on. Maybe in an future / ideal state this is something that materialisations can inform Fusion about ahead of time in the definition. |
|
Should we have source schemas for UDFs too? There's a use case described by @eliasdefaria here:
Let's say you're using UDFs in your dbt project, but you can't use dbt-managed functions because another team manages the UDFs (and that team doesn't use dbt). So, you're referencing those UDFs with a hard-coded-reference: Could there be a way to declare your function source schema definitions to inform Fusion compiler context? And then dbt would produce a lock file like: |
|
@graciegoheen I like where your head's at. Another reason to extend this spec to support more than just source schemas is Snowflake's newly introduced user defined types. These will fully fail strict compilation, as they are custom types defined as objects in Snowflake by the user. We could extend the spec in the same way you're thinking: |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
schemas should be first-class, versioned artifacts
🧩 TL;DR
Today: dbt Fusion relies on a hidden schema cache, local Parquet files of column names and data types, to validate SQL offline.
It works, but it’s invisible, can go stale, and requires manual deletion or refreshes when schemas drift.
Problem: this cache isn’t inspectable, versioned, or easily shareable.
Teams can’t reproduce compiles, review schema changes, or work fully offline without live warehouse access or the necessary permissions to access source schemas.
Proposal: make schemas first-class, version-able artifacts, “schema locks”, stored as human-readable YAML files under a directory.
New command:
dbt sync→ fetch, validate, and update schema locks.Think pip freeze but for your data schemas.
Benefits: predictable & faster compiles, transparent schema diffs in PRs, offline-ready workflows, and drift detection integrated with
contracts.Goal: turn schema metadata from an internal cache into an artifact dbt can both trust and verify.
Ask: Give us feedback on the proposal and questions at the bottom
how we got here
The dbt Fusion engine radically improves developer experience with a new compiler toolchain that is not only faster but can understand your SQL.
This largely happens via local SQL validation, i.e. validating that any change you make to a model query does not have errors without needing to actually run that query against the data warehouse. To do this, dbt needs to know the names and data types of all your columns for all your models. How does dbt Fusion figure this out?
Enter the schema cache.
schema cache:
a local copy of all column names and types for models and sources in your dbt project. the schema cache is required to have Fusion understand your SQL
To populate the cache, Fusion makes metadata requests to your data warehouse to retrieve the column names and data types of your source tables.
💡 More on schema cache
If you understand a SQL dialect, you can derive all models’ column types from upstream.
For example, imagine the below scenario with models:
fct_ordersandreport_orders.We know that
COUNT()returns an integer, and that information can be used in downstream models likereport_orders.But the buck stops at the top of your DAG: your source tables. If we fetch the column types from the warehouse on the sources, it is enough information to understand all type of your downstream models. So one of the first things that Fusion does is download the source table schemas (read: the source table’s column names and types)
To accomplish this, the first thing Fusion does is make metadata queries to your data platform to get all the column names and datatypes of all of the relevant source tables. On most data platforms, this does not spin up a warehouse and drawdown credits.
The result is saved to
target/db/as parquet files.The parquet files have no rows, but the columns and datatypes do correspond to those of the source table in the data warehouse.
To see the information here’s two ways:
select * from parquet_metadata(filepath)(docs)Once populated, the schema cache ensures that fusion doesn't need to re-fetch this metadata every invocation, making validation faster.
original design principles
The schema cache has been a part of dbt Fusion since launch in May this year. Since then it has improved greatly, works smoothly for many, and has even been reverse-engineered by several early adopters of Fusion.
We wanted new users to try out static analysis, which is a huge part of Fusion’s promise as the new engine for dbt. To that end, we originally treated the source schema cache as an implementation detail of static analysis, rather than a user-facing feature in its own right. Accordingly, we’ve documented a lot about static analysis but very little about the source schema cache. (
static analysisConfig, New Concepts)Therefore the schema cache today is largely invisible to end users. Today, a user will only see a single line in
stdoutthat it is happening:Downloading Relations ⠖ [6s]. Hardly anyone was aware of the actual schema cache: all the parquet files intarget/db/.🚨 The Problem
While the schema cache has been a solid foundation for enabling offline SQL validation in dbt Fusion, it’s also shown clear limits in practice.
The schema cache today is a hidden implementation detail, local Parquet files of column names and data types stored under
target/db/. While it enables Fusion to understand your SQL without hitting the warehouse, it’s hard to inspect, can easily go stale, and often requires manual deletion or refreshes when things drift.For example, one of the most common issues we’ve seen (dbt-fusion#615) looks like this:
COLUMN_THAT_EXISTSis a newly added column.Fusion says it can’t find it, because it’s still looking at the outdated schema cache.
The only fix is to manually delete the local cache and re-download all your schemas.
Beyond staleness, several other pain points have emerged:
Together, these issues make the schema cache feel fragile and opaque, a powerful but “magical” mechanism that works until it doesn’t.
💡where we’re headed
👉 We think managing this metadata could look like introducing a new command —
dbt sync— to fetch, lock, and update schema metadata, turning today’s hidden cache into an explicit, reviewable “schema lock.”We want to make schema metadata a first-class, versioned artifact that dbt can both trust and verify, one that users can see, review, and manage directly, not invisible cache files that only Fusion knows about.
Still early thinking, but below is the direction we want to validate.
Does this feel like the right shape of the solution?
🧭 goal
Enable dbt projects to:
This turns schema metadata into a declarative artifact that dbt can both trust and verify.
🧱 core concept: schema locks
Schema locks evolve the existing Fusion source schema cache into something explicit and version-controlled, part of your project’s source of truth.
target/db/schemas/schemas/dbt syncWith this, dbt would:
npm install, but for schemas🧩 what this unlocks
✍️ authoring ergonomics we want feedback on
Hopefully you now have a big picture understanding of this proposal. You may already have some questions like: what will these lock file look like? where will they live? how does one create or update them?
Let’s go through them one at a time
where do these files live?
We propose that they live in their own
schemas/folder, and not inproperties.ymlfiles (more on that later).The folder would be organized in the same folder structure as your project like below
what goes in a lock file (minimal example)?
in this directory, there’s a small manifest file to indicate freshness and provenance
🧩 Managing Schema Locks with dbt sync
With a new command
dbt syncand configuration indbt_project.ymlIt fetches schema metadata, stores it locally, and verifies it against the warehouse when needed.
dbt syncdbt synctarget/schemas/.dbt sync --checkdbt sync --updatedbt sync --from-remotedbt sync --from-localIt would also support:
--select/--exclude)--schema-lockfiles-dir)configuration
There are also sync-specific configurations that, like other dbt configurations, could be set at the folder- or model-level
Note:
By default, schema locks are written to
target/schemas/, a directory typically ignored by Git.Teams that want to version-control and review these files can opt in by setting
schema_lockfiles_dirto a path inside their project (for example, schemas/). This provides flexibility between ephemeral local locks and persistent, reviewable artifacts.🧩 Workflow
Initialize:
dbt syncThis creates human-readable YAML lockfiles that mirror your project’s folder structure under schemas/.
Commit these files to your repo to make schema metadata reproducible and reviewable.
Check Drift Before Deployment
dbt sync --checkThis command is ideal for CI/CD pipelines or pre-deploy hooks.
It reports schema drift (e.g., added, removed, or type-changed columns) before merging or releasing changes.
Refresh as needed:
dbt sync --update --select sources.*Review schema changes:
If version-controlled under schemas/, PR diffs show column-level changes directly.
(Optional) Define Schemas Locally—Skip the Remote
If you’d rather declare the schema in schema.yml and have dbt build the lock from that (no warehouse calls), author columns and types in your properties:
Then:
dbt sync --select stg_ordersdbt will generate the lockfile from your declared schema, without contacting the warehouse.
If you explicitly want to ignore local declarations and pull from the warehouse instead, use:
dbt sync --from-remote --select stg_ordersHow strict are they?
What if your schemas drift over time?
Drift happens when new columns are added to your table. dbt already has a concept of model contracts to ensure the underlying table matches your expectations
We could reuse contracts to enforce schema drift:
What if you don't necessarily care about new columns, but need to ensure the columns you depend on exist?
We've introduce a new completeness mode:
dbt sync.escape hatches
Sometimes you need flexibility beyond the default behavior. We've included two escape hatches:
dbt compile --prefer-remote: Use this flag when you want to bypass the local schema locks and fetch schemas directly from the warehouse for a one-off compile.dbt compile --no-auto-sync: Use this flag to disable automatic schema syncing and fail immediately if any required schema lockfile is missing.what about Python models and unanalyzable SQL?
For nodes we can’t statically infer (Python models, or SQL with unknown functions), their schema locks are authoritative, they tell Fusion what the shape of the data is at compile time.
This means static analysis can continue downstream of those nodes, even when we can’t parse their SQL or inspect their runtime code.
At run/build time, dbt can validate those declared schemas (and any others) against the live warehouse to check for drift, using the same enforcement rules as other locks.
why do we need schema locks — aren’t schema.yml files enough?
A common question we’ve already heard:
It’s a fair question, and one we’ve debated internally. While schema.yml files already describe models and sources, they serve a very different purpose from schema locks.
Keeping these two artifacts separate is intentional and valuable:
In short, schema.yml files tell dbt what you intend, while schema locks record what actually exists, giving you the benefits of both declarative intent and verifiable reality.
questions for y’all
schemas.ymlconvention for configuring and documenting models and sources?All reactions