Where A Model Actually Lands
new code, production data. The selector draws a line through the DAG. Right of the line, your code builds. Left of the line, production’s tables answer. A slim CI is only as correct as where that line falls.
Say it once, out loud: CI is a process, not a node. This chapter opens on the exact census Chapter 13 closed on — 23 graph nodes, 20 non-exposure, 15 buildable, 24 manifest nodes — and closes on the same four numbers. Nothing here adds a model, a seed or a test.
What changes is where a model lands, and which of your fifteen buildable nodes a pull request actually has to rebuild before anyone can trust it. Chapter 12 handed +schema and generate_schema_name to this chapter on purpose — start there.
- target.schema = 'analytics' (target='prod')
- custom_schema_name = 'marts'
- generate_schema_name_for_env: target.name == 'prod' and custom_schema_name is not none → return custom_schema_name, bare
{% macro generate_schema_name(custom_schema_name=none, node=none) -%}
{{ return(adapter.dispatch('generate_schema_name', 'dbt')(custom_schema_name, node)) }}
{% endmacro %}
{% macro default__generate_schema_name(custom_schema_name, node) -%}
{%- set default_schema = target.schema -%}
{%- if custom_schema_name is none -%}
{{ default_schema }}
{%- else -%}
{{ default_schema }}_{{ custom_schema_name | trim }}
{%- endif -%}
{%- endmacro %}
{% macro generate_schema_name_for_env(custom_schema_name, node) -%}
{%- set default_schema = target.schema -%}
{%- if target.name == 'prod' and custom_schema_name is not none -%}
{{ custom_schema_name | trim }}
{%- else -%}
{{ default_schema }}
{%- endif -%}
{%- endmacro %}models: wavelength: marts: +schema: marts, nothing else customised. generate_database_name and generate_alias_name exist too (alias handles versioned models as {{ node.name }}_v{{ version }}) — Chapter 18 owns them as reusable package code.CI is the only environment where the code and the data deliberately disagree: new code, production data. The selector draws a line through the DAG. Right of the line, your code builds. Left of the line, production’s tables answer. A slim CI is only as correct as where that line falls.
Every "why is my table in the wrong schema" incident and every "why did CI just rebuild the whole warehouse" incident traces back to the same two mechanisms: which schema a target resolves a model into, and which nodes a selector decides belong to a given run. Get the first wrong and two developers silently overwrite each other, or a badly-written override sends a laptop straight at production. Get the second wrong and slim CI either misses a real change or burns twenty minutes proving nothing new.
- target
- A named connection profile — dev, ci, prod — each with its own schema, and nothing else. The SQL never changes; only the destination does.
- generate_schema_name
- The macro dbt calls to turn a model’s +schema config into a real schema. dbt’s own default CONCATENATES target.schema and the custom name; it does not use the custom name bare.
- this chapter’s schema decision
- From here forward, only models/marts/ carries +schema: marts (config in dbt_project.yml), using generate_schema_name_for_env. Staging, intermediate, the snapshot and both seeds keep no custom schema at all — Chapter 12 explicitly deferred this decision to this chapter.
- PR schema
- analytics.dbt_ci_pr_412 — Chapter 2’s own worked example, reused verbatim here. A throwaway schema, one per pull request, that a plain dbt build in CI writes into.
| Who is running | target | schema | fct_listens resolves to |
|---|---|---|---|
| Vijay, locally | dev | dbt_vijay | analytics.dbt_vijay.fct_listens |
| A pull request in CI | ci | dbt_ci_pr_412 | analytics.dbt_ci_pr_412.fct_listens |
| The production job | prod | analytics | analytics.marts.fct_listens |
Every one of these three addresses has already appeared in this KB — Chapter 2 published the first two, Chapter 13 and Chapter 16 both print the third. This widget is the macro that makes all three true at once.
Assuming a model configured with +schema: marts lands in a schema literally called marts. Under dbt’s unoverridden default macro it does not — it concatenates onto your target schema, landing in dbt_vijay_marts in dev. This is exactly the surprise Chapter 2 flagged and handed forward here.
Writing a custom generate_schema_name override that returns the custom schema name unconditionally, with no target check. Every environment, including a developer’s own laptop, now resolves +schema: marts to the same bare marts schema production uses. A routine local dbt build can silently overwrite a production mart.
Before trusting a printed table address, name which macro produced it — dbt’s default concatenates; generate_schema_name_for_env only branches on target.name == 'prod'.
Scope +schema configuration to the layer that needs it (marts, here) rather than sprinkling it project-wide — every additional custom schema is one more combination to reason about per target.
Treat a schema-name override as production code: it runs on every single build, in every environment, and a mistake in it is invisible until two things collide.
This chapter runs at 2026-08-04 09:00 UTC, four days after Chapter 13’s 2026-07-31 09:00 UTC handoff. The graph opens at 23 nodes, 20 non-exposure, 15 buildable, 24 manifest, and closes at exactly the same four numbers.
The weak answer blames the config. The strong answer names the macro: dbt’s default generate_schema_name concatenates target.schema and the custom name — it never uses the custom name alone unless something overrides that macro. Ask which macro is in play before touching a single YAML file.
Every table address in this KB is the output of one small macro, running once per model, per build — know which implementation is in play before you trust the address it prints.
