The only migration tool built for all six Spanner paradigms. Manage relational tables, full-text search, vector/AI embeddings, property graphs, and analytics TTL policies — all from versioned SQL scripts. Enterprise tier adds policy-as-code contracts, live schema analysis, database diff, capacity projections, and CI/CD dashboards. 19 CLI commands. No JVM.
Every deploy follows a deterministic 9-step sequence. Nothing executes without validation. Nothing executes twice.
Verifies the target Spanner database is reachable. Exits immediately with a clear error if the database or instance is unavailable.
Creates spanchange_history, spanchange_lock, and spanchange_audit if they don't exist. No manual setup required.
Uses a Spanner READ_WRITE transaction to atomically claim the lock row. Concurrent deploys are blocked at the database level.
Recursively walks the migrations folder, classifies by V/U/R/A prefix, and sorts globally by semver — regardless of subdirectory layout.
If --tag is set, only scripts whose tag is ≤ the fence version are applied. Scripts with no tag are always included.
Each script's DDL is submitted via UpdateDatabaseDdl. spanchange polls until the long-running operation completes — handling Spanner's async DDL correctly.
Repeatable scripts rerun when checksum changes. Always scripts run unconditionally — designed for IAM grants and permission refreshes.
A row is written for every script: status, checksum, execution time, tag, run_id, and operator identity. The audit log captures every event append-only.
The lock is released via the same Spanner transaction used to acquire it. Only the process that acquired it can release — no orphaned locks possible.
The filename is the contract. No XML changelogs, no YAML config — just well-named .sql files in any folder structure you choose.
Runs once in ascending semver order. Never re-runs. The workhorse of schema evolution — CREATE TABLE, ALTER TABLE, search/vector/graph DDL.
Paired rollback for a versioned script. Only executes on spanchange rollback. Rollback is interleave-aware — child tables always dropped before parents automatically.
Reruns on deploy when its MD5 checksum changes. Ideal for views, UDFs, and lookup data that must stay in sync with the codebase.
Executes on every deploy — no conditions. Use for IAM grants and permission refreshes that must always be current.
Scripts are discovered recursively. Organise by paradigm, team, or release. Version ordering is always global across all subdirectories.
Deploy, rollback, validate, lint, drift detect, advise, shadow test, pipeline orchestrate, diff, baseline, generate, repair — everything in one binary.
Offline DDL analysis catches MySQL/Postgres idioms Spanner rejects, Spanner-specific correctness issues, and paradigm-specific hazards — entirely without a database connection.
| Rule | Sev | What it catches |
|---|---|---|
| L002 | ERROR | BEGIN TRANSACTION wrapping DDL — not transactional in Spanner |
| L003 | ERROR | AUTO_INCREMENT / SERIAL / IDENTITY — not supported |
| L007 | ERROR | INT / INTEGER type — use INT64 |
| L008 | ERROR | FLOAT / DOUBLE / REAL type — use FLOAT64 |
| L013 | ERROR | RENAME COLUMN — not supported in GoogleSQL |
| Rule | Sev | What it catches |
|---|---|---|
| L001 | ERROR | NOT NULL column via ALTER TABLE with no DEFAULT value |
| L004 | WARN | STRING column with no length — use STRING(N) or STRING(MAX) |
| L005 | WARN | BYTES column with no length |
| L006 | ERROR | DROP COLUMN with dependent index in same script |
| L009 | ERROR | CREATE TABLE with no PRIMARY KEY clause |
| L010 | INFO | TIMESTAMP column without allow_commit_timestamp=true |
| L011 | WARN | INTERLEAVE IN PARENT references unknown table |
| L012 | ERROR | DROP TABLE without dropping dependent indexes first |
| Rule | Sev | What it catches |
|---|---|---|
| L014 | ERROR | TOKENLIST column without HIDDEN attribute — always required |
| L015 | WARN | CREATE SEARCH INDEX with no TOKENLIST column in scope |
| L016 | ERROR | LIKE on a TOKENLIST column — use SEARCH() instead |
| Rule | Sev | What it catches |
|---|---|---|
| L017 | ERROR | FLOAT32 ARRAY column with no VECTOR LENGTH declared |
| L018 | WARN | CREATE VECTOR INDEX with no distance_type in OPTIONS |
| L019 | ERROR | ARRAY<FLOAT64> used — vector indexes require FLOAT32 |
| Rule | Sev | What it catches |
|---|---|---|
| L020 | WARN | NODE TABLE references table not found in migrations |
| L021 | ERROR | EDGE TABLE without SOURCE KEY or DESTINATION KEY |
| L022 | ERROR | CREATE PROPERTY GRAPH with no NODE TABLES clause |
| L023 | ERROR | ROW DELETION POLICY on a non-TIMESTAMP column |
| L024 | WARN | ROW DELETION POLICY references column not in this script |
Drift detection compares migration history against the live database across all six paradigms. The zero-downtime advisor tells you what your pending scripts may do to production.
Every paradigm has its own drift detection path. Relational tables check INFORMATION_SCHEMA.TABLES and COLUMNS. Search indexes check SEARCH_INDEXES. Vector indexes check VECTOR_INDEXES. Property graphs check PROPERTY_GRAPHS.
All compared against what migration history says should exist. ERROR items mean a resource declared in migrations is absent from the live database — your application may already be broken.
Where drift tells you what changed without your knowledge, the advisor tells you what your pending scripts might do to a live system. Runs offline — no database connection needed. Never blocks a deploy; always informs.
| Rule | Level | Risk |
|---|---|---|
| A001 | INFO | Index creation — async backfill in progress |
| A002 | WARN | DROP COLUMN — app may still reference it |
| A003 | WARN | DROP TABLE — data permanently deleted |
| A005 | WARN | Column type change — may break existing readers |
| A009 | WARN | 3+ indexes in one script — concurrent backfill load |
| A011 | WARN | SEARCH INDEX — heavier backfill than regular index |
| A014 | WARN | FLOAT32 ARRAY alter — all vector indexes must be rebuilt |
| A017 | WARN | ROW DELETION POLICY on table with already-expired data |
spanchange shadow replays every migration against a fresh Spanner emulator — lint, deploy, drift check — and tears it down without touching production.
Launches the Spanner emulator via Docker, gcloud, or connects to an existing instance. Automatically creates a throw-away database.
Every linter rule executes against the full script set. Any ERROR-level finding stops the shadow run before touching the emulator.
Every V__ script is applied in order from the beginning of history — not just pending scripts. The complete chain must succeed cleanly.
After replay, drift detection runs across all six paradigms against the emulator. History says X exists — does it?
The throw-away database and emulator are destroyed. No trace remains. Production is untouched throughout the entire process.
Deploy to multiple Spanner databases in dependency order. Define a directed acyclic graph of stages — spanchange resolves the order, checks version constraints, and marks downstream stages blocked on failure.
After all stages complete, spanchange prints a structured summary. Blocked stages clearly reference the failing dependency. Use --continue-on-failure to proceed through blocked stages for partial deployments.
All six Spanner paradigms are first-class. Each has dedicated linter rules, advisor rules, drift detection, and baseline capture.
| Paradigm | DDL Managed | Linter | Advisor | Drift | Baseline |
|---|---|---|---|---|---|
| 🗄️ Relational | Tables, columns, indexes | L001–L013 | A001–A010 | Tables, columns, indexes | ✓ |
| 🔑 Key / Value | Simple primary-key tables | L001–L013 | A001–A010 | Tables, columns | ✓ |
| 🔍 Full-text Search | TOKENLIST + SEARCH INDEX | L014–L016 | A011–A012 | SEARCH_INDEXES | ✓ |
| 🧠 Vector / AI | FLOAT32 ARRAY + VECTOR INDEX | L017–L019 | A013–A014 | VECTOR_INDEXES | ✓ |
| 🕸️ Graph | PROPERTY GRAPH definition | L020–L022 | A015–A016 | PROPERTY_GRAPHS | ✓ |
| 📊 Analytics | ROW DELETION POLICY (TTL) | L023–L024 | A017 | TABLE_OPTIONS | ✓ |
Policy-as-code governance, live schema health scoring, database comparison, capacity projections, monitoring snapshots, synthetic data seeding, and CI/CD analytics dashboards.
spanchange_monitor_snapshots for capacity projections.A single profile can target multiple Spanner databases with independent migration paths. Every command — deploy, rollback, status, drift, analyse, monitor — runs against each database in sequence automatically.
Form A (original): database: spandemo — single database, unchanged behaviour.
Form B (new): database as a mapping — each key is a database name with its own root_folder, history_table, and timeout override. Each database has an independent history table and tracks applied versions separately.
You can be at V1.3.0 on the app database and V1.0.0 on the analytics database simultaneously. The pipeline respects multi-database profiles — each stage deploys all databases in that profile.
spanchange pipeline — each stage deploys all databases in the profile. Combine with per-stage tag fences and pause_before gates for a complete multi-tenant release workflow.
From PR check to production deploy — a complete GitHub Actions workflow with Workload Identity Federation, contract checks, shadow testing, and post-deploy verification.
Offline checks on every PR. Naming, version ordering, 24 DDL rules. Zero network calls — fast, always safe.
auto on PRPolicy-as-code gate against the live database. Naming conventions, NULL constraints, index limits. GitHub PR annotations. Exits 2 on failures — blocks merge.
auto on PRFull emulator replay + drift check. Compare staging schema against prod before promoting — exits 2 on BREAKING differences.
auto on mainDeploy with Workload Identity. Post-deploy drift check confirms every resource from applied scripts exists in the live database.
auto on mainSnapshot database health metrics after every deploy. Update the CI/CD analytics dashboard for team visibility.
environment gateThe only tool built for Spanner's multi-paradigm nature. 19 commands spanning all six paradigms, policy-as-code contracts, live schema analysis, capacity projections, and multi-database pipelines — no JVM, no external database.
| Capability | spanchange | Generic Tool A | Generic Tool B | Terraform |
|---|---|---|---|---|
| Spanner GoogleSQL native | ✓ Native | ⚠ Generic SQL | ⚠ Generic SQL | ✓ |
| SEARCH INDEX lifecycle | ✓ | — | — | — |
| VECTOR INDEX lifecycle | ✓ | — | — | — |
| PROPERTY GRAPH lifecycle | ✓ | — | — | — |
| ROW DELETION POLICY (TTL) | ✓ | — | — | ✓ |
| Interleave-aware rollback | ✓ auto | — | — | — |
| 24-rule DDL linter | ✓ | — | — | — |
| Zero-downtime advisor | ✓ 17 rules | — | — | — |
| Shadow / emulator testing | ✓ | — | — | — |
| Multi-DB DAG pipeline | ✓ | — | ⚠ manual | ⚠ modules |
| Multi-database profiles | ✓ Form B | — | — | ⚠ workspaces |
| Deploy history + audit log | ✓ | ✓ | ✓ | — |
| Distributed deploy lock | ✓ Spanner TX | ⚠ DB-dependent | ⚠ DB-dependent | — |
| Policy-as-code contracts | ✓ Enterprise | — | — | — |
| Live schema health scoring | ✓ 0–100/table | — | — | — |
| Schema diff (DB compare) | ✓ BREAKING/WARN | — | — | ✓ plan |
| Capacity projections (OLS) | ✓ Enterprise | — | — | — |
| Interleave-aware seeder | ✓ Enterprise | — | — | — |
| CI/CD analytics dashboard | ✓ HTML | — | — | — |
| No JVM required | ✓ Python | JVM required | JVM required | ✓ HCL |
Python 3.8+. No JVM, no containers required. Authentication via Application Default Credentials or a service account key.
pip or from package
spanchange.yml
V1.0.0__create_users.sql
validate → contract → deploy → monitor