Implementation:MaterializeInc Materialize Deploy Promote Pattern
| Knowledge Sources | misc/dbt-materialize/dbt/include/materialize/macros/deploy/deploy_init.sql, misc/dbt-materialize/dbt/include/materialize/macros/deploy/deploy_promote.sql, misc/dbt-materialize/dbt/include/materialize/macros/deploy/deploy_cleanup.sql, misc/dbt-materialize/dbt/include/materialize/macros/deploy/deploy_await.sql, misc/dbt-materialize/tests/adapter/test_deploy.py
|
|---|---|
| Domains | Deployment Strategy, Zero-Downtime Releases, Database Schema Management |
| Last Updated | 2026-02-08 |
Overview
Pattern documentation for the blue-green deployment workflow in dbt-materialize, covering the deploy_init, deploy_await, deploy_promote, and deploy_cleanup macros that orchestrate zero-downtime deployments using ALTER SCHEMA SWAP and ALTER CLUSTER SWAP.
Description
The dbt-materialize blue-green deployment pattern is implemented as a set of four coordinated dbt macros, invoked via dbt run-operation. Together they provide a complete lifecycle for zero-downtime schema and cluster deployments.
deploy_init(ignore_existing_objects=False) -- Prepares the deployment environment:
- Reads the
deploymentvariable fromdbt_project.ymlto get the list of clusters and schemas for the current target. - Validates that all production schemas and clusters exist and do not contain sinks.
- Validates permissions via
deploy_validate_permissions. - For each schema, creates
{schema}_dbt_deploy(or validates it if it already exists). Copies grants and default privileges from the production schema. - For each cluster, reads the production cluster's configuration (size, replication factor, schedule type, hydration time estimate) and creates
{cluster}_dbt_deploywith identical settings. Copies cluster grants.
deploy_await(poll_interval=15, lag_threshold='1s') -- Waits for hydration:
- For each deployment cluster, calls
await_cluster_readywhich polls the cluster's replication lag atpoll_intervalsecond intervals. - Blocks until all objects in all deployment clusters are within the
lag_threshold.
deploy_promote(wait=False, poll_interval=15, lag_threshold='1s', dry_run=False) -- Atomically promotes the deployment:
- Validates that all production and deployment schemas and clusters exist.
- Optionally calls
deploy_awaitifwait=True. - In a single
BEGIN/COMMITtransaction, executes:ALTER SCHEMA "public" SWAP WITH "public_dbt_deploy"for each schema pair.ALTER CLUSTER "prod" SWAP WITH "prod_dbt_deploy"for each cluster pair.
- After the swap, discovers sinks whose upstream objects were in swapped schemas and runs
ALTER SINK ... SET FROM ...to update their references. - Tags deployed schemas with a comment including the deploying user, timestamp, and git commit SHA.
- In
dry_runmode, logs the commands without executing them.
deploy_cleanup() -- Tears down the old deployment environment:
- Drops all
{schema}_dbt_deployschemas withCASCADE. - Drops all
{cluster}_dbt_deployclusters withCASCADE.
Usage
Use this pattern when:
- Deploying new or updated dbt models to Materialize production without downtime.
- Setting up CI/CD pipelines for data model releases.
- Needing to validate a deployment before promoting (using
deploy_awaitanddry_run). - Managing grants and privileges across blue-green environments.
Code Reference
Source Location
| File | Description |
|---|---|
misc/dbt-materialize/dbt/include/materialize/macros/deploy/deploy_init.sql
|
deploy_init macro (154 lines) -- environment preparation with grant copying
|
misc/dbt-materialize/dbt/include/materialize/macros/deploy/deploy_await.sql
|
deploy_await macro (48 lines) -- hydration polling
|
misc/dbt-materialize/dbt/include/materialize/macros/deploy/deploy_promote.sql
|
deploy_promote macro (235 lines) -- atomic swap, sink discovery, schema tagging
|
misc/dbt-materialize/dbt/include/materialize/macros/deploy/deploy_cleanup.sql
|
deploy_cleanup macro (51 lines) -- environment teardown
|
misc/dbt-materialize/tests/adapter/test_deploy.py
|
Integration tests: TestTargetDeploy (L407-500), TestEndToEndDeployment, TestLagTolerance
|
Signature
-- deploy_init: create deployment schemas and clusters
{% macro deploy_init(ignore_existing_objects=False) %}
-- deploy_await: poll for hydration readiness
{% macro deploy_await(poll_interval=15, lag_threshold='1s') %}
-- deploy_promote: atomically swap environments
{% macro deploy_promote(wait=False, poll_interval=15, lag_threshold='1s', dry_run=False) %}
-- deploy_cleanup: drop deployment environment
{% macro deploy_cleanup() %}
Import
These are dbt macros invoked via dbt run-operation, not Python imports. They require the deployment variable to be configured in dbt_project.yml.
# Required dbt_project.yml configuration
vars:
deployment:
default:
clusters:
- quickstart
schemas:
- public
I/O Contract
Inputs
| Macro | Parameter | Type | Description |
|---|---|---|---|
deploy_init
|
ignore_existing_objects
|
bool
|
When True, allows deployment schemas/clusters to contain pre-existing objects without raising an error. Default: False.
|
deploy_await
|
poll_interval
|
int
|
Seconds between readiness checks. Default: 15.
|
deploy_await
|
lag_threshold
|
str
|
Maximum acceptable replication lag (e.g., '1s', '5s', '1h'). Default: '1s'.
|
deploy_promote
|
wait
|
bool
|
If True, calls deploy_await before promoting. Default: False.
|
deploy_promote
|
dry_run
|
bool
|
If True, logs swap commands without executing them. Default: False.
|
deploy_promote
|
poll_interval
|
int
|
Passed to deploy_await if wait=True. Default: 15.
|
deploy_promote
|
lag_threshold
|
str
|
Passed to deploy_await if wait=True. Default: '1s'.
|
Outputs
| Macro | Output | Description |
|---|---|---|
deploy_init
|
Side effects | Creates deployment schemas and clusters, copies grants and default privileges. |
deploy_await
|
Blocks until ready | No return value; execution halts until all deployment clusters are hydrated within the lag threshold. |
deploy_promote
|
Side effects | Atomically swaps schemas and clusters, updates sinks, tags schemas with deployment metadata. |
deploy_cleanup
|
Side effects | Drops deployment schemas and clusters with CASCADE.
|
Usage Examples
# Full deployment lifecycle
# 1. Initialize the deployment environment
dbt run-operation deploy_init \
--vars '{deployment: {default: {clusters: [quickstart], schemas: [public]}}}'
# 2. Build models in the deployment environment
dbt run --vars '{deployment: {default: {clusters: [quickstart], schemas: [public]}}, deploy: True}'
# 3. Wait for hydration (optional, can be done separately)
dbt run-operation deploy_await \
--args '{poll_interval: 10, lag_threshold: "5s"}' \
--vars '{deployment: {default: {clusters: [quickstart], schemas: [public]}}}'
# 4a. Dry run to validate (optional)
dbt run-operation deploy_promote \
--args '{dry_run: true}' \
--vars '{deployment: {default: {clusters: [quickstart], schemas: [public]}}}'
# 4b. Promote (with optional wait)
dbt run-operation deploy_promote \
--args '{wait: true}' \
--vars '{deployment: {default: {clusters: [quickstart], schemas: [public]}}}'
# 5. Clean up the old environment
dbt run-operation deploy_cleanup \
--vars '{deployment: {default: {clusters: [quickstart], schemas: [public]}}}'
-- The atomic swap executed by deploy_promote (inside a transaction):
BEGIN;
ALTER SCHEMA "public" SWAP WITH "public_dbt_deploy";
ALTER SCHEMA "staging" SWAP WITH "staging_dbt_deploy";
ALTER CLUSTER "prod" SWAP WITH "prod_dbt_deploy";
COMMIT;
-- After swap, sinks are updated:
ALTER SINK "sinks_schema"."my_sink" SET FROM "materialize"."public"."my_view";
# Integration test pattern (from test_deploy.py TestTargetDeploy.test_dbt_deploy)
# Demonstrates ID swapping verification:
# Before promote: record IDs
before_clusters = dict(
project.run_sql(
"SELECT name, id FROM mz_clusters WHERE name IN ('prod', 'prod_dbt_deploy')",
fetch="all",
)
)
# Execute promotion
run_dbt(["run-operation", "deploy_promote"])
# After promote: verify IDs swapped
after_clusters = dict(
project.run_sql(
"SELECT name, id FROM mz_clusters WHERE name IN ('prod', 'prod_dbt_deploy')",
fetch="all",
)
)
# The ID that was under 'prod' is now under 'prod_dbt_deploy' and vice versa
assert before_clusters["prod"] == after_clusters["prod_dbt_deploy"]
assert before_clusters["prod_dbt_deploy"] == after_clusters["prod"]