Celerity
Applications

SQL Database Schema Management

Managing SQL database schemas with Celerity

Celerity makes database schemas a first-class concern, bridging the gap between development and data teams by providing a single source of truth for database structure. Instead of managing migrations separately from your application infrastructure, Celerity integrates schema management directly into the development and deployment lifecycle.

NoSQL Datastores

This page covers schema management for SQL databases (celerity/sqlDatabase). For NoSQL data stores (celerity/datastore), see NoSQL Datastore Schema Management.

Feature Availability

  • Available in v0 - Features currently supported
  • 🔄 Planned for v0 - Features coming in future v0 evolution
  • 🚀 Planned for v1 - Features coming in v1 release

How It Works

Schema definitions are declarative YAML files that describe the desired state of your database. When the YAML changes, celerity schema diff generates a versioned SQL migration file capturing the transition, which you review, adjust if needed, and commit alongside your code. Deployments then apply pending migrations in order.

Schema YAML (desired state) ──► celerity schema diff ──► Migration file (SQL) ──► PR review ──► celerity deploy / schema apply ──► Database

                                Current state replayed from
                                the migration directory

This approach gives you the benefits of declarative schema definition (readable, diffable, version-controlled) with the safety and transparency of versioned migrations: every change to the database is an ordered, reviewable SQL file in your repository. Most files are generated for you, and you write them by hand when you need full control.

Diffing and migration execution are powered by the open-source core of Atlas, a production-grade schema migration engine. See Powered by Atlas for details.

Schema Definition Format

Project File Structure

Available in v0

A typical Celerity project with schema management follows this structure:

my-app/
├── .celerity/                       # Generated only (merged blueprint, compose, logs)
├── app.bp                           # Main blueprint — references schema files (.bp, .yaml, or .jsonc)
├── app.deploy.jsonc                  # Deploy target config
├── config/
│   ├── local/                       # Plaintext app config for local development
│   └── test/                        # Plaintext app config for testing
├── secrets/
│   ├── local/                       # Secrets for local development
│   └── test/                        # Secrets for testing
├── seed/
│   ├── local/                       # Seed data for local development
│   └── test/                        # Seed data for testing
├── schema-contracts.yaml            # Data team dependency contracts (optional)
├── schemas/
│   ├── orders.yaml                  # SQL schema for ordersDb resource
│   ├── analytics.yaml               # SQL schema for analyticsDb resource
│   └── user-store.yaml              # NoSQL schema for userStore resource (if applicable)
├── sql/
│   └── orders-db/                   # Migration directory (per SQL database resource)
│       ├── 20260601120000_init.sql                      # Generated from schema YAML
│       ├── 20260610093000_add_audit_trigger.sql         # Hand-written
│       ├── 20260628141500_add_customer_tier.sql         # Generated from schema YAML
│       └── atlas.sum                                    # Directory integrity file
├── scripts/
│   └── user-store/                  # Escape hatch data scripts (per NoSQL datastore, if applicable)
│       └── V001__backfill_status.py
├── src/
│   └── ...
└── generated/                       # Optional: codegen output
    └── ...

SQL and NoSQL schemas coexist

A single project can contain both SQL database and NoSQL datastore schema files. They share the same schemas/ directory and the same schema-contracts.yaml file. See NoSQL Datastore Schema Management for the NoSQL schema format.

The blueprint references schema files via the schemaPath field on celerity/sqlDatabase resources:

Blueprint Language

version "2025-11-02"
transform "celerity-2026-02-27-draft"

resource ordersDb: celerity/sqlDatabase {
    metadata {
        displayName = "Orders Database"
        labels = {
            application = "orders"
        }
    }

    spec {
        engine = "postgres"
        name = "orders"
        schemaPath = "./schemas/orders.yaml"
        migrationsPath = "./sql/orders-db"
    }
}

YAML

version: 2025-11-02
transform: celerity-2026-02-27-draft

resources:
  ordersDb:
    type: "celerity/sqlDatabase"
    metadata:
      displayName: "Orders Database"
      labels:
        application: "orders"
    spec:
      engine: "postgres"
      name: "orders"
      schemaPath: "./schemas/orders.yaml"
      migrationsPath: "./sql/orders-db"

Schema YAML Format

Available in v0

Schema files define the desired state of a database using engine-native column types. One schema file per celerity/sqlDatabase resource. The example below uses PostgreSQL types and functions; a MySQL schema would use MySQL-native equivalents (e.g. char(36) instead of uuid, json instead of jsonb, datetime instead of timestamptz, uuid() instead of gen_random_uuid()).

# schemas/orders.yaml
tables:
  customers:
    description: "Customer accounts. One row per registered customer."
    owner: "orders-team"
    tags: ["pii", "core-entity"]
    columns:
      id:
        type: "uuid"
        primaryKey: true
        default: "gen_random_uuid()"
        description: "Unique customer identifier"
      email:
        type: "varchar(255)"
        nullable: false
        unique: true
        description: "Customer email. Used for login and notifications."
        classification: "pii"
      name:
        type: "varchar(255)"
        nullable: false
        description: "Display name"
        classification: "pii"
      tier:
        type: "varchar(20)"
        nullable: false
        default: "'free'"
        description: "Subscription tier. One of: free, pro, enterprise."
      created_at:
        type: "timestamptz"
        nullable: false
        default: "now()"
        description: "Account creation timestamp (UTC)"
    indexes:
      - name: "idx_customers_email"
        columns: ["email"]
        unique: true
      - name: "idx_customers_tier"
        columns: ["tier"]

  orders:
    description: "Customer orders. Immutable after creation — updates go to order_events."
    owner: "orders-team"
    tags: ["financial", "core-entity"]
    columns:
      id:
        type: "uuid"
        primaryKey: true
        default: "gen_random_uuid()"
        description: "Unique order identifier"
      customer_id:
        type: "uuid"
        nullable: false
        description: "Purchasing customer"
        references:
          table: "customers"
          column: "id"
          onDelete: "cascade"
      status:
        type: "varchar(50)"
        nullable: false
        default: "'pending'"
        description: "Order lifecycle status: pending → confirmed → shipped → delivered | cancelled"
      total_cents:
        type: "integer"
        nullable: false
        description: "Order total in USD cents. Always >= 0."
        tags: ["financial-metric"]
      line_items:
        type: "jsonb"
        nullable: false
        default: "'[]'::jsonb"
        description: "Array of {product_id, quantity, unit_price_cents}"
      created_at:
        type: "timestamptz"
        nullable: false
        default: "now()"
      updated_at:
        type: "timestamptz"
        nullable: false
        default: "now()"
    indexes:
      - name: "idx_orders_customer"
        columns: ["customer_id"]
      - name: "idx_orders_status_created"
        columns: ["status", "created_at"]
      - name: "idx_orders_line_items"
        columns: ["line_items"]
        type: "gin"
    constraints:
      - type: "check"
        name: "chk_orders_total_positive"
        expression: "total_cents >= 0"

extensions:
  - "uuid-ossp"

Column Types

Column types are engine-native strings with no abstraction layer across PostgreSQL and MySQL. The engine field in the blueprint locks the dialect, so you use the exact types your database supports.

For PostgreSQL, common types include: varchar(n), text, integer, bigint, boolean, uuid, jsonb, timestamptz, numeric(p,s), bytea, interval, cidr, inet.

For MySQL, common types include: varchar(n), text, int, bigint, boolean, char(36), json, datetime, timestamp, decimal(p,s), blob, enum(...), set(...). 🚀 MySQL support is planned for v1.

Indexes

Indexes are defined at the table level with a name, columns list and optional type and unique fields.

FieldTypeDescription
namestringIndex name (must be unique within the database)
columnsarray[string]Columns included in the index
typestringIndex type. Defaults to btree. PostgreSQL supports btree, gin, gist, hash. MySQL supports btree, hash, fulltext, spatial.
uniquebooleanWhether the index enforces uniqueness. Defaults to false.

Foreign Keys

Foreign keys are defined inline on columns using the references field:

customer_id:
  type: "uuid"
  nullable: false
  references:
    table: "customers"
    column: "id"
    onDelete: "cascade"   # cascade | restrict | set null | set default | no action

Constraints

Table-level constraints beyond foreign keys:

constraints:
  - type: "check"
    name: "chk_orders_total_positive"
    expression: "total_cents >= 0"

Extensions

Extensions that should be enabled in the database. This is primarily a PostgreSQL feature. PostgreSQL supports extensions like uuid-ossp, pg_trgm, pgcrypto, etc. MySQL does not have an equivalent extension system; this field is ignored for MySQL databases.

# PostgreSQL example
extensions:
  - "uuid-ossp"
  - "pg_trgm"

When you add an extension to the schema YAML, Celerity emits the corresponding CREATE EXTENSION IF NOT EXISTS statement into the next generated migration file.

Rich Metadata

The following fields have no effect on DDL. They exist for documentation, data governance and tooling:

FieldApplies toDescription
descriptionTables, ColumnsHuman-readable description of the table or column
ownerTablesTeam or individual that owns the table
tagsTables, ColumnsArbitrary tags for categorisation (e.g. pii, financial, core-entity)
classificationColumnsData classification label (e.g. pii, sensitive, public)
typeNameTablesOverrides the generated entity type name for codegen (e.g. Person for a people table) when singularisation gets it wrong

These fields make the schema YAML self-documenting for data team consumption. They are included in schema exports and used by schema contracts for dependency tracking.

Tags vs Classification

tags and classification serve different purposes and work best together:

  • classification is a formal sensitivity label. It takes a single value per column from a small, controlled vocabulary agreed with your data team (e.g. public, internal, sensitive, pii). It answers "how sensitive is the data in this exact column?" and is what compliance and security tooling consumes: masking policies, access reviews, and the dedicated classification fields in data catalogs.
  • tags are free-form categorisation. A table or column can have as many as are useful (financial, core-entity, deprecated, financial-metric). They answer "what kind of thing is this?" and are what people and pipelines use for discovery, filtering exports, and grouping tables by domain.

The two are complementary rather than redundant. A pii tag on a table is a coarse discovery signal saying the table contains PII somewhere, while a pii classification on a column pinpoints exactly which columns carry that sensitivity, which is what an access policy or column-masking rule actually needs:

tables:
  customers:
    tags: ["pii", "core-entity"]      # Coarse signal: this table contains PII somewhere
    columns:
      id:
        type: "uuid"
        primaryKey: true              # No classification: an opaque ID isn't sensitive
      email:
        type: "varchar(255)"
        classification: "pii"         # Precise: THIS column is the PII
      tier:
        type: "varchar(20)"
        classification: "public"      # Explicitly safe to expose
        tags: ["segmentation"]        # Also useful for analytics grouping

A rule of thumb: if a compliance or security tool would consume it, it belongs in classification; if a human or pipeline is searching, filtering or grouping, use tags. Treat classification values as an enum and apply them consistently, since a single-valued field with a mixed vocabulary loses most of its value to downstream tooling. Tags can stay loose.

Migrations

Available in v0 (PostgreSQL) · 🚀 MySQL support planned for v1

Powered by Atlas

Celerity does not implement its own SQL diffing engine. Migration generation and execution are built on the open-source (Apache 2.0) core of Atlas, a schema migration engine that is embedded directly in the Celerity CLI, so there is nothing extra to install. Atlas handles what it is best at: computing correct, dialect-aware DDL transitions and executing versioned migrations safely. Celerity layers its own capabilities on top: schema YAML as the desired-state format, data-aware backfills, destructive-change gating, schema contracts, drift detection, type generation and data team exports.

Because the migration directory uses the Atlas format (timestamped migration files plus an atlas.sum integrity file), teams that separately license Atlas Pro can point its migration linting and monitoring tooling directly at the same directory. This is entirely optional; everything documented on this page works without an Atlas account or license.

Generating Migrations

The schema YAML is declarative: it describes the desired end state. Running celerity schema diff computes the transition from the current state to the desired state and writes it as a new SQL migration file in the migration directory:

  1. The desired state is built from the schema YAML
  2. The current state is computed by replaying the existing migration directory on a temporary local database (using the same local container tooling as celerity dev run)
  3. The difference becomes a new timestamped migration file in migrationsPath, ready for review

You review the generated SQL, edit it if needed, and commit it with your schema YAML change. The migration plan is part of the pull request, so both application developers and data teams see exactly what will run against the database before it is approved.

Example: making a column NOT NULL with a default

Schema change:

# Before
status:
  type: "varchar(50)"
  nullable: true

# After
status:
  type: "varchar(50)"
  nullable: false
  default: "'active'"

Generated migration file (sql/orders-db/20260628141500_make_status_not_null.sql):

-- Step 1: Set default for new rows
ALTER TABLE orders ALTER COLUMN status SET DEFAULT 'active';

-- Step 2: Backfill existing NULL values (added by Celerity's data-aware transition handling)
UPDATE orders SET status = 'active' WHERE status IS NULL;

-- Step 3: Apply NOT NULL constraint
ALTER TABLE orders ALTER COLUMN status SET NOT NULL;

For common transition patterns that need data operations to succeed safely, such as backfilling NULLs before applying a NOT NULL constraint, Celerity augments the generated DDL automatically. And because the output is a plain SQL file, anything the generator didn't anticipate can be adjusted by hand before you commit.

Transition Patterns

TransitionGenerated Migration Content
Add nullable columnALTER TABLE ADD COLUMN
Add NOT NULL column with defaultALTER TABLE ADD COLUMN ... DEFAULT ... NOT NULL
Make column NOT NULL (was nullable)UPDATE SET default WHERE NULLALTER SET NOT NULL
Make column nullable (was NOT NULL)ALTER DROP NOT NULL
Change column type (compatible)ALTER COLUMN TYPE ... USING (PostgreSQL) / MODIFY COLUMN (MySQL)
Change column type (incompatible)Warning: edit the generated migration to define the conversion
Add indexCREATE INDEX CONCURRENTLY (PostgreSQL) / CREATE INDEX (MySQL)
Drop indexDROP INDEX CONCURRENTLY (PostgreSQL) / DROP INDEX (MySQL)
Add foreign keyALTER TABLE ADD CONSTRAINT ... FOREIGN KEY
Add tableCREATE TABLE
Add extensionCREATE EXTENSION IF NOT EXISTS
Drop tableDROP TABLE — classified breaking; see Migration Risk Classification
Drop columnALTER TABLE DROP COLUMN — classified breaking; see Migration Risk Classification
Rename columnWarning: the diff sees a drop + add (ambiguous intent); edit the generated migration to use RENAME COLUMN instead

Migration Directory

Available in v0

All migrations, generated and hand-written alike, live in a single directory per database resource, specified by migrationsPath on the celerity/sqlDatabase resource. Files execute in timestamp order, forming one linear history:

sql/orders-db/
├── 20260601120000_init.sql                  # Generated from schema YAML
├── 20260610093000_add_audit_trigger.sql     # Hand-written
├── 20260628141500_make_status_not_null.sql  # Generated from schema YAML
└── atlas.sum                                # Directory integrity file

File Naming Convention

<timestamp>_<description>.sql

Generated files are named automatically (pass --name <description> to celerity schema diff to control the description). To start a hand-written migration, use celerity schema new <description>, which creates an empty timestamped file and updates the integrity file.

Directory Integrity

The atlas.sum file records a hash of every migration file. It is updated automatically when Celerity generates or creates migration files, and verified before any migration is applied. This catches accidentally edited already-applied migrations, missing files, and conflicting migration histories from parallel branches. A merge conflict in atlas.sum is a signal that two branches added migrations concurrently and the directory needs to be corrected by re-running celerity schema diff after merging.

Keeping migrations in version control

Migration files and atlas.sum are part of your source: commit the whole migrationsPath directory (including atlas.sum) alongside the schema YAML change that produced it. Two CI checks keep the committed directory honest:

  • Integritycelerity schema validate requires atlas.sum to be present and to match the committed files (no database needed). It fails if a migration was added or edited without updating the sum.
  • Completenesscelerity schema diff --check writes nothing and exits non-zero if a migration would be generated, i.e. the committed migrations do not fully cover the committed schema YAML. This catches "the schema changed but no migration was generated or committed for it."

Applying to a deployed environment additionally requires a valid committed atlas.sum (it is not auto-generated), so a live database is never migrated from an unverified directory.

Hand-Written Migrations

Available in v0

Hand-written migration files cover structural SQL that the schema YAML cannot express:

  • Custom functions, stored procedures or triggers
  • Specialised index types beyond what the schema YAML supports (e.g. partial indexes, expression indexes)
  • Extension setup beyond simple CREATE EXTENSION (e.g. extension-specific configuration)
  • Table partitioning
  • Row-level security policies
  • Custom types or domains

They are plain SQL files in the same migration directory and the same linear history, with no separate execution phase and no special file type. Objects created by hand-written migrations are left untouched by generated migrations: the diff engine only manages what the schema YAML models (tables, columns, indexes, foreign keys, constraints and extensions), so it will never generate statements that drop or modify your triggers, functions or policies.

If a generated change would break a hand-written object (e.g. dropping a column that a trigger references), the failure surfaces immediately when celerity schema diff replays the directory on the temporary local database, before anything reaches a real environment.

Hand-written migrations are for DDL and structural changes only, not for data backfills or data transformations. See Data Migrations for how data changes are handled.

Execution Model

  1. Pending migration files are applied in timestamp order, generated and hand-written files alike
  2. Each migration is tracked in a revision table (atlas_schema_revisions) that is automatically created in each managed database, and runs only once; re-running dev run, deploy or schema apply skips already-applied migrations
  3. Directory integrity (atlas.sum) is verified before applying; a tampered or out-of-sync directory blocks execution
  4. Each migration file runs in a transaction by default; migrations that cannot run inside a transaction (such as CREATE INDEX CONCURRENTLY) are marked to run non-transactionally, with statement-level tracking in the revision table so a partial failure can be resumed safely

Rollback

Schema migrations are forward-only. There are no down scripts to maintain, because reverting a schema change is itself just another schema change:

  1. Restore the previous version of the schema YAML (e.g. git revert the schema change)
  2. Run celerity schema diff to generate a new forward migration that transitions the database back
  3. Review and apply it like any other migration

This keeps the migration history append-only and truthful: the revision table always reflects what actually ran, in order, including reverts.

Reverting Hand-Written Migrations

The revert diff only covers what the schema YAML models. Since the diff engine deliberately doesn't manage triggers, functions, views or policies, it can't compute their inverse, so reverting a hand-written migration means writing the inverse as a new forward migration yourself:

  1. Run celerity schema new remove_audit_trigger to scaffold a new timestamped file
  2. Write the inverse SQL (e.g. DROP TRIGGER audit_trigger ON orders;). For a changed rather than removed object, use CREATE OR REPLACE with the previous body, which you can copy from the original migration file in Git history
  3. Review, commit and apply it like any other migration

This is not a capability the forward-only model takes away: no engine can derive the inverse of arbitrary custom SQL, so a down script for a trigger or function would have been hand-written anyway. Writing the inverse when you actually need it, rather than speculatively at creation time, means it is written with full knowledge of the situation being reverted, goes through the same review and local replay as any other migration, and avoids the classic trap of down scripts that are untested and forgotten until an emergency.

The burden of a correct inverse does sit with you, and for stateful constructs it can be non-trivial (dropping a trigger is easy; reverting a partitioning scheme is a project in its own right). This is a good reason to keep hand-written migrations small and single-purpose.

Deploy Safety

For deploy safety, schema changes should be backward-compatible with the currently running application (often called expand/contract): add new columns and tables first, deploy application code that uses them, and only remove old structures in a later change once nothing depends on them. If an application deployment fails and rolls back, the previous application version keeps working against the already-migrated schema.

Data Migrations

Data backfills, data transformations and other DML operations tied to schema evolution are handled separately from structural migration files. This separation keeps the migrationsPath directory focused on structural DDL; a data backfill that takes hours to run has no place in a migration that deployments wait on.

Automatic Data Transitions

Celerity handles common data-aware transitions automatically when generating migrations. When the schema YAML changes in ways that require data operations, the generated migration file includes the correct imperative sequence:

Schema ChangeWhat the Generated Migration Includes
Make column NOT NULL (was nullable, has default)UPDATE SET default WHERE NULLALTER SET NOT NULL
Add NOT NULL column with defaultALTER TABLE ADD COLUMN ... DEFAULT ... NOT NULL
Change column type (compatible cast)ALTER COLUMN TYPE ... USING (PostgreSQL)

These transitions are included in the generated file automatically, and they remain visible and editable there like everything else in the migration.

Complex Data Migrations

For data migrations that cannot be handled automatically (column splits, complex data transforms, populating new foreign key columns from external sources), use a two-phase deploy approach with application-level tooling between deploys:

Example: splitting a name column into first_name and last_name

Deploy 1 — add new columns, keep old column:

# schemas/orders.yaml — add new columns alongside the old one
name:
  type: "varchar(255)"
  nullable: false
first_name:
  type: "varchar(255)"
  nullable: true         # Nullable for now — will be populated
last_name:
  type: "varchar(255)"
  nullable: true         # Nullable for now — will be populated

After deploy 1: the new columns exist but are empty. The old column is still in use.

Between deploys — run the data migration:

Use application-level tooling to perform the data transform. This could be a one-off script, a CLI command, a CI job, or a post-deploy hook:

-- Run via psql, a script, or a post-deploy hook
UPDATE customers
SET first_name = split_part(name, ' ', 1),
    last_name = split_part(name, ' ', 2)
WHERE first_name IS NULL;

This is not a migration script; it runs outside of the Celerity migration lifecycle, after the structural deploy succeeds and before the next deploy.

Deploy 2 — make new columns NOT NULL, drop old column:

# schemas/orders.yaml — finalize the split
first_name:
  type: "varchar(255)"
  nullable: false        # Safe: all rows populated
last_name:
  type: "varchar(255)"
  nullable: false        # Safe: all rows populated
# 'name' column removed — the drop is classified `breaking`; apply with --allow-breaking

After deploy 2: the old column is dropped, new columns are NOT NULL.

Why not put data migrations in migration files?

  • Reversal semantics are unclear. You can't meaningfully reverse UPDATE customers SET first_name = split_part(name, ...). A revert migration can undo structure, not a data transform.
  • Data migrations are environment-specific. Production might need batched updates with progress tracking; local dev doesn't need them at all (the database starts from scratch). Migration files run identically everywhere.
  • Data migrations are transient. Once all environments have run the backfill, the script serves no purpose. Structural migrations (triggers, functions, indexes) are permanent parts of the schema.
  • Separation enables flexibility. Application-level tooling can batch large updates, add progress logging, run dry-run modes, or integrate with monitoring, none of which fit in a SQL file that deployments wait on.

Safety

  • Risk-gated apply: every migration is classified safe, risky or breaking; celerity schema apply applies only safe migrations by default and stops before a riskier one unless explicitly allowed. See Migration Risk Classification
  • Destructive changes blocked by default: dropping a table or column is classified breaking, so celerity schema apply will not apply it without --allow-breaking. This is enforced by an un-spoofable floor that scans each migration's actual SQL statements — a hand-edited safe header on a migration that drops data is still treated as breaking
  • Migrations reviewed before they run: Every migration is a committed SQL file, so the full plan is reviewed in the pull request rather than approved at deploy time
  • Directory integrity enforced: The atlas.sum check prevents applying a tampered or inconsistent migration directory
  • Pre-apply drift check: Before applying migrations, Celerity verifies the live database matches the expected starting state. See Drift Detection

Migration Risk Classification

Available in v0

Every migration is classified by how safe it is to apply against a live database, and celerity schema apply uses that classification as a safety gate. The class is recorded in a header comment at the top of the migration file (written by celerity schema diff), so it is visible in review and deterministic at apply time — no re-analysis against the target database is needed.

ClassMeaningExamples
safeAdditive, backward-compatible with running codeadd table; add a nullable column or a column with a default; add a non-unique index; drop an index or constraint
riskyData-dependent — may fail or lock depending on existing datamake a column NOT NULL; add a unique index; add a foreign key or check constraint; change a column type; add a NOT NULL column without a default
breakingDestructive / backward-incompatible with running codedrop a table, column or schema
-- celerity:risk breaking  (drops column customers.legacy_tier)
ALTER TABLE customers DROP COLUMN legacy_tier;

The apply gate. celerity schema apply applies pending migrations in order but stops before the first migration whose risk exceeds what is allowed, exiting non-zero (migrations already applied before it remain committed):

  • default — apply only safe migrations;
  • --allow-risky — also apply data-dependent migrations;
  • --allow-breaking — also apply destructive migrations (implies --allow-risky).

This is what makes an automated celerity schema apply step safe in CI: the step stays in the pipeline unconditionally, safe migrations flow straight through, and a risky or breaking change halts the pipeline — signalling that a backfill, a data script, or an expand/contract split is needed — instead of silently applying a change that could break the running application. It composes with the expand/contract discipline in Data Migrations.

The header is not the only line of defence. Because a header could be wrong — a generated safe migration hand-edited to add a destructive statement, for example — schema apply also scans each migration's actual SQL and treats any migration that drops a table, column or schema as breaking regardless of its header. The safety-critical floor is therefore derived from what the migration actually does, not only from what it claims.

Hand-written migrations (celerity schema new) default to risky, since their intent cannot be inferred from a diff; downgrade the header to safe when the migration is additive and backward-compatible.

Drift Detection

Available in v0 (PostgreSQL)

Migrations assume a known starting state. When someone changes a database out-of-band, whether through a hotfix ALTER TABLE in production, an index added by hand or a permissions script gone wrong, that assumption breaks silently and the schema YAML stops being the truth. Drift detection is what keeps the "single source of truth" claim honest, and it is the foundation the product ↔ data team sync features are built on.

celerity schema drift <resource> --env <env> checks a live database against the expected state at three layers:

  1. History drift: compares the revision table against the migration directory to find migrations applied out-of-band, missing from the directory, or applied out of order.
  2. Structural drift: inspects the live database and diffs it against the expected state for everything the schema YAML models (tables, columns, indexes, foreign keys and constraints), reported as a full object-level diff.
  3. Advanced-object drift: objects created by hand-written migrations (triggers, functions, views, row-level security policies) are fingerprinted from the database catalog. Changed, missing or unexpected objects are flagged. This layer is deliberately coarse in that it tells you that an object changed, not line-by-line what changed, but it means out-of-band changes never go unseen.

Drift output is contract-aware: drifted tables are matched against schema-contracts.yaml, so the report shows which downstream consumers are affected:

$ celerity schema drift ordersDb --env production

ordersDb (postgres: orders) — drift detected:

  Structural drift:
    ~ table orders: column discount_cents (integer, nullable) exists in database but not in schema
    ~ index idx_orders_discount exists in database but not in schema

  Contracts affected:
    ⛔ revenue-pipeline — orders table drifted (blocking)

  Resolution:
    - To adopt the change: add the column and index to schemas/orders.yaml, run
      celerity schema diff (the generated statements are guarded with IF NOT EXISTS
      so they no-op where the objects already exist), then apply with --allow-drift
    - To remove the change: create a hand-written migration dropping the column and
      index (celerity schema new remove_discount_drift), then apply with --allow-drift

The command exits non-zero when drift is found, so it can run as a scheduled CI job for continuous checking.

The same command covers NoSQL datastores, where drift is about data shape rather than structure. See NoSQL Drift Detection.

Pre-Apply Drift Check

celerity schema apply and the deploy pipeline run a structural drift check before applying pending migrations. If the live database does not match the expected starting state, the apply is blocked with a drift report (override with --allow-drift once the drift is understood). This prevents migrations from running against a database in an unexpected state, where even correct data definitions can do the wrong thing.

Scheduled Drift Monitoring

Future Capability

Hosted, scheduled drift monitoring with notifications routed to contract owners is projected as part of the paid Schema Service for a future release after v1 (post-July 2027). The v0 celerity schema drift command is the free, on-demand building block it is built on. Run it from a scheduled CI job for continuous checking today.

Deploy Pipeline Integration

Available in v0

Schema management is integrated into the celerity deploy pipeline:

celerity deploy

  ├─ Phase 1: Infrastructure
  │   Transformer converts celerity/sqlDatabase → Provider-specific database resources (e.g. AWS RDS)
  │   Provider deploys database server infrastructure
  │   Database verified as reachable

  ├─ Phase 2: Schema Migration
  │   Connect to database directly
  │   Verify directory integrity (atlas.sum) and check for drift
  │   Display pending migrations and prompt for confirmation
  │   Apply pending migration files in order (tracked in the revision table)
  │   Write updated schema state

  └─ Phase 3: Application
      Deploy handler resources with database connection configuration
      Handlers start against the migrated database

Contract validation in v0

In v0, contract validation is not built into the deploy pipeline. Use celerity schema validate in your CI pipeline to catch contract violations before deployment. See Schema Contracts for details.

Deploy-time contract enforcement (blocking deploys and dispatching webhook notifications automatically) is projected as a paid tier feature for a future release after v1 (post-July 2027).

Local Development

Available in v0

Running Locally

When celerity dev run starts an application with SQL database resources:

  1. A local PostgreSQL or MySQL container is started based on engine in the spec, only PostgreSQL is supported in v0
  2. All migration files are applied in timestamp order, skipping any already tracked in the revision table
  3. Connection environment variables are injected, pointing to the local container
  4. The runtime starts and handlers receive connection configuration

If the schema YAML has changed but no migration has been generated for it yet, dev run warns and prompts to run celerity schema diff first, so local databases always come up from the same migration history that other environments run.

For persistent local development (opt-in): diff-based migration is used instead of full recreate, preserving data between restarts.

Testing

When celerity dev test runs an application with SQL database resources:

  1. An isolated database instance is created per test suite
  2. The schema is applied from scratch
  3. Test fixtures are loaded
  4. Tests run against the fully-schema'd database
  5. The database is torn down after tests complete

CLI Commands

Available in v0

The celerity schema command group provides all the tools for working with database schemas. For full command reference including all flags and configuration options, see the CLI Reference: schema.

CommandDescription
celerity schema diffGenerate a migration file from schema YAML changes, showing the plan and contract impact (--check writes nothing and fails if a migration is missing — a CI completeness gate)
celerity schema newCreate an empty migration file for hand-written SQL
celerity schema applyApply pending migrations outside of a full deployment (only safe migrations by default — see Migration Risk Classification)
celerity schema driftCheck a live database for out-of-band changes
celerity schema validateValidate schema files, foreign keys, migration SQL, contracts and optionally codegen freshness
celerity schema exportExport schema as SQL DDL, markdown, JSON Schema or Mermaid ERD
celerity schema codegenGenerate type-safe code from schema definitions
celerity schema showInspect a live database (--url / --instance-name) and print its current schema
celerity schema historyShow the applied-migration history of a live database from its revision table

Example: celerity schema diff

The diff output displays the engine alongside the database name (e.g. postgres: orders or mysql: orders). The generated migration uses engine-appropriate syntax.

$ celerity schema diff --name add_customer_tier

ordersDb (postgres: orders):
  Generated sql/orders-db/20260713102400_add_customer_tier.sql  [risk: safe]:
    [1] ALTER TABLE customers ADD COLUMN tier varchar(20) NOT NULL DEFAULT 'free';
    [2] CREATE INDEX CONCURRENTLY idx_customers_tier ON customers (tier);

  Pending (not yet applied to any environment):
    20260710093000_add_status_audit_trigger.sql (hand-written)
    20260713102400_add_customer_tier.sql (generated above)

  Contracts:
    ⛔ revenue-pipeline — orders table changed (blocking)
    ⚠ customer-analytics — customers table changed (notify)

  Warnings:
    - Column 'users.name' removed and 'users.first_name' + 'users.last_name' added.
      This looks like a column split — use a two-phase deploy to migrate data.
      See: https://celerityframework.io/docs/framework/applications/sql-database-schema-management#data-migrations
      No DROP statement was generated for 'users.name' (ambiguous split vs drop). Once the
      data is migrated, remove the column from the schema and apply the generated drop —
      it will be classified `breaking` and needs `celerity schema apply --allow-breaking`.

  Review the generated file, then commit it with your schema change.
  Apply with: celerity schema apply

Type Generation

Available in v0 (TypeScript, Python)

Generate types from the schema YAML: a TypeScript type alias or Python Pydantic model for each table's row, plus column-name and table-name constants. There is no ORM coupling, you can combine with whatever library you prefer.

Two conventions make the output predictable:

  • Types are named as singular entities. A table is a collection; a row is one of them, so customers generates Customer, order_events generates OrderEvent. The name is the table name singularised and PascalCased. For irregular plurals the singulariser cannot infer, set typeName on the table in the schema YAML to override it.
  • Field names mirror the database columns exactly. They are kept verbatim (snake_case), not camelCased, in both languages. The generated type describes the shape your driver returns for a plain query, so keeping the keys identical to the columns means the type stays honest at runtime — no hidden translation layer. If you prefer camelCase in your application, map it in your own layer. (Teams using an ORM that already maps columns to camelCase typically will not use codegen.)

For SDK usage examples with generated types, see the Node.js SDK - SQL Database and Python SDK - SQL Database documentation.

TypeScript

celerity schema codegen --lang typescript --out ./src/generated/

Generated output:

// orders-db.ts — auto-generated by `celerity schema codegen`, do not edit.
// Field names mirror the database columns exactly (what your driver returns for a
// plain query); only type names are stylised. Map to camelCase in your own layer
// if you want it — codegen keeps the shape faithful to the database.

/** A customer account */
export type Customer = {
  id: string;
  created_at: Date;
  email: string;
  name: string;
  tier: string;
}

/** A row from the "orders" table */
export type Order = {
  id: string;
  created_at: Date;
  customer_id: string;
  line_items: unknown;
  status: string;
  total_cents: number;
  updated_at: Date;
}

export const Tables = {
  customers: "customers",
  order_events: "order_events",
  orders: "orders",
} as const;

export const OrderColumns = {
  id: "id",
  created_at: "created_at",
  customer_id: "customer_id",
  line_items: "line_items",
  status: "status",
  total_cents: "total_cents",
  updated_at: "updated_at",
} as const;

Row shapes use type rather than interface (structural data, not behaviour). Columns are ordered primary keys first, then alphabetically, so the output is deterministic — celerity schema codegen --check fails CI if the committed code no longer matches the schema.

Python

celerity schema codegen --lang python --out ./src/generated/

Generated output:

# orders_db.py — auto-generated by `celerity schema codegen`, do not edit.
# Field names mirror the database columns exactly (what your driver returns for a
# plain query); only class names are stylised.
from pydantic import BaseModel
from datetime import datetime
from typing import Any


class Customer(BaseModel):
    id: str
    created_at: datetime
    email: str
    name: str
    tier: str


class Order(BaseModel):
    id: str
    created_at: datetime
    customer_id: str
    line_items: Any
    status: str
    total_cents: int
    updated_at: datetime

Go

🚀 Planned for v1 - Go type generation is planned for a future release.

Java

🚀 Planned for v1 - Java type generation (record classes) is planned for a future release.

C#

🚀 Planned for v1 - C# type generation (record types) is planned for a future release.

Schema Contracts

Schema contracts allow data teams to declare which tables they depend on, so they are automatically informed when schema changes affect them. Instead of manually tracking column-level dependencies, contracts operate at the table level: any structural change to a watched table triggers the contract's policy.

The schema diff already knows exactly what changed (columns added, dropped, type changes, etc.), so contracts don't need to duplicate that information. They simply declare: "I care about these tables. Tell me when they change."

Contracts File Format

Available in v0

Data teams maintain a contracts file in the repository alongside the blueprint:

# schema-contracts.yaml
contracts:
  - name: "revenue-pipeline"
    owner: "data-team"
    dependencies:
      - database: "ordersDb"
        tables: ["orders"]
        policy: "blocking"       # non-zero exit code if these tables change

  - name: "customer-analytics"
    owner: "data-team"
    dependencies:
      - database: "ordersDb"
        tables: ["customers", "orders"]
        policy: "notify"         # warning output, zero exit code

Contracts can also include NoSQL datastore dependencies in the same file. Use the datastore key instead of database; no tables sub-key is needed since a datastore represents a single table:

# schema-contracts.yaml — combined SQL and NoSQL contracts
contracts:
  - name: "revenue-pipeline"
    owner: "data-team"
    dependencies:
      - database: "ordersDb"
        tables: ["orders"]
        policy: "blocking"
      - datastore: "userStore"
        policy: "notify"

  - name: "user-analytics"
    owner: "data-team"
    dependencies:
      - datastore: "userStore"
        policy: "blocking"

See NoSQL Datastore Schema Contracts for more details on NoSQL contract format.

FieldDescription
nameHuman-readable contract name
ownerTeam or individual that owns the downstream dependency
dependencies[].databaseName of the celerity/sqlDatabase resource in the blueprint (for SQL databases)
dependencies[].tablesTables this contract watches for changes (SQL databases only)
dependencies[].datastoreName of the celerity/datastore resource in the blueprint (for NoSQL datastores)
dependencies[].entitiesOptional list of entities to watch on a datastore dependency, for single-table designs
dependencies[].policyblocking (non-zero exit code) or notify (warning only, zero exit code)

When a schema change touches any table listed in a contract, the diff output includes the full details of what changed: columns added, dropped, renamed, type changes, etc. The contract itself doesn't need to enumerate these; it just identifies which tables matter.

Validation and CI Integration

Available in v0

Contract handling splits across two places, depending on whether a change set is known:

  • celerity schema validate checks the contracts file itself — that every dependency names a real celerity/sqlDatabase resource and only tables that exist in its schema, with a valid policy. This needs no database connection, so it stays a fast, no-DB CI gate alongside schema correctness, foreign-key integrity and migration SQL parsing. It does not evaluate change impact (validate has no baseline to diff against).

  • Change impact is reported wherever the change is knowncelerity schema diff (the pending change you are about to commit) and celerity schema drift (out-of-band changes to a live database). Both list the affected contracts:

    • blocking contracts affected: the command exits with a non-zero exit code, failing your CI pipeline.
    • notify contracts affected: printed as a warning; the exit code is unaffected.

A single celerity schema validate step keeps the contracts file honest:

# Example CI step (GitHub Actions)
- name: Validate schema and contracts
  run: celerity schema validate

And the diff/drift steps already in your pipeline surface who is affected by an actual change:

$ celerity schema diff

Generated db/migrations/20260721115109_add_discount.sql  [risk: safe]

  Contracts affected:
    ⛔ revenue-pipeline (data-team) — ordersDb.orders changed [blocking]
    ⚠ customer-analytics (data-team) — ordersDb.customers, orders changed [notify]

When a blocking contract fires, the output shows exactly which tables changed, giving the data team the information they need to review the PR. Use your platform's existing notification mechanisms (CODEOWNERS, required reviewers, Slack integrations on CI failure) to alert the right people.

Deploy-Time Enforcement and Webhook Notifications

Future Capability

In v0, contract validation runs via celerity schema validate as a CI gate; it does not run automatically during celerity deploy.

Deploy-time contract enforcement (automatically blocking deploys when contracts are affected) and webhook notifications (dispatching to Slack, email or custom endpoints when contracts fire) are projected as paid tier features for a future release after v1 (post-July 2027). These are future projections, not committed features.

The paid Schema Service would add a notify field to contracts for webhook configuration, integrate contract checks directly into the deploy pipeline, and dispatch notifications automatically. The service is centred on the contract layer that connects product teams and their data-team consumers, with scheduled drift monitoring as a supporting capability built on the same contracts.

Programmatic Schema API

Future Capability

A programmatic REST API for querying deployed schemas, change history and contract status is projected as a paid tier feature for a future release after v1 (post-July 2027).

Planned endpoints include:

  • GET /schemas/{instanceId}/{resourceName} — current deployed schema
  • GET /schemas/{instanceId}/{resourceName}/history — change history
  • GET /schemas/{instanceId}/{resourceName}/contracts — contract status

Pipeline tools would integrate with this API to auto-generate configs, sync data catalogs and trigger downstream updates.

Data Catalog Integrations

Future Capability

Auto-sync to data catalog services (AWS Glue Data Catalog, DataHub, Atlan, etc.) is projected as a paid tier feature for a future release after v1 (post-July 2027).

For Data Teams

The schema YAML files serve as always-accurate documentation because they are what actually drives the database. Data teams benefit from:

  • description, owner, tags, classification fields on every table and column make schemas self-documenting
  • Schema exports (celerity schema export --format markdown) generate human-readable documentation
  • Git history of schema files (git log schemas/orders.yaml) provides full change history with PR review
  • Contract definitions in schema-contracts.yaml let data teams declare and protect their dependencies
  • Drift detection (celerity schema drift) surfaces out-of-band database changes, with affected contracts called out in the report
  • Machine-readable exports (celerity schema export --format json-schema) integrate with pipeline tools
  • ERD diagrams (celerity schema export --format mermaid) visualise table relationships

Last updated on