SKILL.md
mz-deploy
mz-deploy is declarative SQL project tooling for Materialize. A project is a git-tracked directory of .sql files describing the desired state of a Materialize environment; mz-deploy compiles it, type-checks it offline, tests it in a local container, and deploys changes through an atomic stage-then-promote lifecycle.
brew install materializeinc/materialize/mz-deploy
Discover Before You Run
The CLI is self-documenting, and its help text is the source of truth. Do not guess command names, flags, or argument order.
mz-deploy help— all commands, grouped by purpose.mz-deploy help <command>— the detailed usage guide for one command:
behavior, every flag, examples, error recovery, and exit codes.
mz-deploy help profiles— the configuration topic guide (profiles,
variables, suffixes, file overrides, TLS).
This skill covers the concepts and the shape of the workflow so you know which command to reach for. Read that command's help for the flags before running it.
Run commands from the project root, or pass -d <path>. Global options worth knowing: -p/--profile selects the connection profile, --output json for machine-readable output (supported by most commands, useful in CI), -v for verbose debugging, -q to suppress informational output.
Project Layout
project.toml # project config: mz_version, dependencies, per-profile settings
profiles.toml # connection profiles (also resolved from ~/.mz)
.mzprofile # gitignored per-checkout default profile
types.lock # cached schemas of external dependencies, for offline type checking
target/ # gitignored local build cache (safe to delete; see `clean`)
models/
<database>/
<schema>.sql # schema mod file — schema-level statements (see below)
<schema>/
<object>.sql # one view, materialized view, table, source, sink, connection, or secret
clusters/<name>.sql # cluster definitions (+ their GRANTs)
roles/<name>.sql # role definitions
network-policies/<name>.sql
The path determines the object's fully qualified name: an MV in models/materialize/catalog/clusters.sql is materialize.catalog.clusters. A schema mod file — <schema>.sql sitting beside the <schema>/ directory — holds statements that apply to the schema as a whole, such as SET api = stable.
mz-deploy new <name> scaffolds this structure in a new directory; init does the same in the current one.
Command Map
| Group | Commands |
|---|---|
| Getting started | new, init, profile (list/set/current), setup, debug |
| Develop | compile, clean, test, explain, dev, lsp, sql, mcp |
| Infrastructure | lock, apply, delete |
| Deploy | stage, wait, promote, abort, describe, list, log |
One-time bootstrap: mz-deploy setup creates the mzdeploy tracking database, its tables, the mzdeploy_server cluster, and three roles. It must be run by a superuser when RBAC is enabled, because it grants system privileges. Everything after that runs as an ordinary user.
Workflow
mz-deploy compile # parse, resolve dependencies, type-check offline
mz-deploy test # run unit tests in a local Materialize container
mz-deploy apply # converge infrastructure (see below)
mz-deploy stage # deploy changed views/MVs to suffixed staging schemas
mz-deploy wait <DEPLOY_ID> # watch clusters hydrate until ready
mz-deploy promote <DEPLOY_ID> # atomic swap into production
compile needs no database connection, so it belongs in CI on every commit. A passing compile guarantees stage and apply will not fail at the SQL parsing stage. Note that every profile variant is validated regardless of --profile, so a syntax error in foo#staging.sql still fails compile --profile production.
Alongside: list shows unpromoted deployments (like git branch), describe <ID> details one, log shows promotion history (like git log), and abort <ID> destroys a staging deployment without promoting it.
Key Concepts
apply owns infrastructure; stage owns views. apply is declarative, diff-based, and idempotent, converging clusters, roles, network policies, secrets, connections, sources, and tables in dependency order. stage handles views and materialized views. Tables and sources are never created by stage — they must already exist, so apply runs first. delete <type> <name> is the inverse of apply: it drops one object without CASCADE and removes its project file.
Deploy IDs and staging suffixes. Each deployment gets an ID — by default the first 7 characters of the current commit SHA — used to suffix its schemas and clusters (public → public_abc123). Staging clusters are cloned from the corresponding production cluster's configuration, including any auto-scaling strategy, so staged objects hydrate the way production will. Staging runs in isolation alongside production.
Change detection is hash-based. stage compares each object's SQL hash against the last promoted snapshot and deploys only what changed, plus anything downstream of a change. Unchanged objects are not recreated. Override with --redeploy-schema <db.schema> or --redeploy-all.
Promotion is atomic and resumable. promote executes ALTER … SWAP on schemas and clusters inside a single transaction, then does post-swap work: creating deferred sinks, applying replacement MVs, repointing sinks at the new production objects, and dropping the old resources. If it dies mid-flight, re-running the same command detects the post-swap state and resumes cleanup.
Sinks are deferred to promote. They must not start producing until the deployment is live, so stage records them and promote creates them.
Conflict detection works at schema and cluster granularity. Because a whole schema is swapped as a unit, two deployments touching any of the same schemas or clusters conflict — even when they modify different objects inside them. The first to promote wins; the second is rejected and must be re-staged against current production. --force skips the check, and doing so drops the other deployer's schemas, since schemas are swapped wholesale rather than merged. Only use it when clobbering that work is the intent.
Stable API schemas. By default a changed object is recreated in staging and its whole schema is swapped, which redeploys in-project dependents automatically but breaks consumers in other mz-deploy projects. Adding SET api = stable to a schema mod file marks that schema as an API boundary: changed MVs are updated in place via ALTER MATERIALIZED VIEW … APPLY REPLACEMENT, preserving object identity. Downstream consumers — in any project — need no redeployment. Constraints: stable schemas may contain only materialized views, and a changed replacement MV does not propagate dirtiness to its dependents.
Type checking is offline. compile and test read external dependency schemas from types.lock. Declare external objects in project.toml as dependencies = ["db.schema.table"] and run mz-deploy lock to refresh the file. Source tables created by CREATE TABLE FROM SOURCE are auto-discovered and need no declaration; apply tables regenerates the lock automatically.
Roles. setup creates materializedeployer (stage, promote, abort), materializedeveloper (read-only deployment state, plus dev overlays), and materialize_monitor. Each user must belong to exactly one — holding several is an error. Use separate profiles with distinct users for deploying, developing, and monitoring.
Profiles and Per-Profile Configuration
A profile is a named connection target in profiles.toml, resolved from --profiles-dir, then MZDEPLOYPROFILESDIR, then ~/.mz. The active profile resolves from --profile, then MZDEPLOY_PROFILE, then the gitignored .mzprofile in the project root (written by mz-deploy profile set). A built-in emulator profile always exists, so a local Materialize emulator works with zero configuration.
Passwords support ${VAR} substitution, overridable by MZPROFILE<NAME>_PASSWORD. sslmode follows PostgreSQL's vocabulary and defaults to prefer for loopback hosts, require otherwise — use verify-full for Materialize Cloud.
The profile does more than pick a host. It also selects, at compile time:
profile_suffix(inproject.toml) — appended to every database and
cluster name, including IN CLUSTER references. Write the delimiter yourself: "staging", not "staging". Staging suffixes stack on top (foo → foostaging → foostaginga).
- SQL variables —
[<profile>.variables]inproject.toml, referenced in
any .sql file with psql syntax: :name (raw), :'name' (quoted string), :"name" (quoted identifier). Referencing an undefined variable fails compilation.
- File overrides —
name#<profile>.sqlreplacesname.sqlwhen that
profile is active. All variants are validated at compile time regardless of which is active, and all must share the same primary statement type. Views and materialized views cannot have file overrides — use SQL variables instead.
Rollback
There is no rollback command. Reverse the change in the project and promote the result:
git revert <commit>
mz-deploy stage
mz-deploy promote <DEPLOY_ID>
Because promote swaps atomically, the rollback promotion is itself atomic — production switches back in a single transaction.
Unit Tests
mz-deploy test runs tests written inline in the same .sql file as the view they cover, using EXECUTE UNIT TEST with mocked dependencies. The syntax is specific to mz-deploy and documented nowhere else — see [references/unit-tests.md](references/unit-tests.md) for the full grammar, worked examples, and failure modes.
Gotchas
stagerequires a clean git tree. Commit, stash, or pass
--allow-dirty.
setupneeds a superuser under RBAC, one time only.testandexplainneed Docker, and share one container named
mz-deploy-sandbox across invocations on the host. Reuse is by name, not by image, so --docker-image has no effect until you docker rm -f mz-deploy-sandbox.
deletenever cascades. If dependents exist it fails and leaves the
project file in place.
devwill not target a production cluster. Provision a dedicated dev
cluster.
- A profile's
clusteroption is ignored. mz-deploy pins every connection
to its own internal mzdeploy_server cluster; resize that with a standard ALTER CLUSTER if needed.
stagefailures roll back automatically. Pass--no-rollbackto keep
the partial deployment for debugging, then clean up with abort.
- A stale
target/cache can produce confusing compile or type errors. Run
mz-deploy clean.