-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture
The migration engine processes schema changes through 8 distinct modules (7 layers + Behavioral module):
SQL ──► Introspector ──► Translator ──► Differ ──► DDL Generator ──► Planner ──► Executor
│ │ │
▼ ▼ ▼
Risk Tagger Safe Patterns Storage
│
▼
Behavioral
(Track 2)
| Module | Layer # | Purpose |
|---|---|---|
| Introspector | 1 | Capture live database schema |
| Translator | 2 | Normalize catalog rows to canonical model |
| Differ | 3 | Detect changes between snapshots |
| DDL Generator | 4 | Generate PostgreSQL DDL statements |
| Planner | 5 | Order changes by dependencies |
| Executor | 6 | Apply migration safely |
| Storage | 7 | Persist migration history |
| Behavioral | — | Handle functions, views, triggers, policies |
Purpose: Capture live database schema from PostgreSQL catalogs.
What it does:
- Queries
pg_catalogandinformation_schemafor all 50+ object types - Detects PostgreSQL version (10-18) and adapts queries accordingly
- Handles version-specific features (e.g., multiranges in PG14+, temporal tables in PG18)
Output: SchemaSnapshot — complete database state
const snapshot = await engine.introspect(pool);
// snapshot.tables, snapshot.indexes, snapshot.constraints,
// snapshot.views, snapshot.functions, snapshot.triggers, ...Purpose: Normalize raw PostgreSQL catalog rows into canonical schema model.
What it does:
- Converts snake_case catalog names to camelCase
- Resolves OID references to qualified names
- Handles inheritance, partitions, dependencies
- Merges data from multiple catalog tables
Input: Raw pg_catalog query results
Output: Canonical SchemaSnapshot object
Purpose: Detect changes between two schema snapshots.
What it does:
- Classifies changes:
CREATE,ALTER,DROP,RENAME - Property-level diff for ALTER changes
- Smart rename detection (avoids DROP+CREATE when object was renamed)
- Detects implicit changes (e.g., dropping a table drops its indexes)
Input: desired schema + current schema
Output: SchemaDiff with list of Change[]
const diff = engine.diff(desired, current);
// diff.changes = [
// { changeType: 'CREATE', objectType: 'table', objectKey: 'public.users' },
// { changeType: 'ALTER', objectType: 'column', objectKey: 'public.users.email', properties: [...] }
// ]Purpose: Generate valid PostgreSQL DDL statements.
What it does:
- Generates
CREATE,ALTER,DROP,COMMENTstatements - Applies safe patterns automatically (6+ safe patterns)
- Handles quoting, escaping, dollar-quoted strings
- Version-aware syntax (e.g.,
NULLS NOT DISTINCTfor PG15+)
Safe Patterns Applied:
| Pattern | Steps |
|---|---|
| NOT NULL column | 1) ADD DEFAULT → 2) SET NOT NULL → 3) DROP DEFAULT |
| FK constraint | 1) ADD NOT VALID → 2) VALIDATE separately |
| CREATE INDEX | Use CONCURRENTLY for zero-downtime |
| Type change | USING clause for safe cast |
| UNIQUE constraint | Create unique index, then convert to constraint |
| CHECK constraint | NOT VALID first, then validate |
| ENUM ADD VALUE | Pre-transaction (PG < 14 requires outside transaction) |
const ddl = engine.generateDDL(diff);
// CREATE TABLE "public"."users" (...);
// ALTER TABLE "public"."users" ALTER COLUMN "email" SET NOT NULL;Purpose: Order changes respecting dependencies.
What it does:
- Topological sort based on object dependencies
- Phase-based execution (Track 1 before Track 2)
- Detects circular dependencies
- Groups changes for optimal execution
Phase Order:
| Phase | Object Types |
|---|---|
| 0-4 | Schemas, Extensions |
| 5-9 | Types, Domains, Sequences |
| 10-14 | Tables, Columns |
| 15-19 | Indexes, Constraints |
| 20-24 | Functions, Triggers |
| 25-29 | Views, Materialized Views |
| 30+ | Policies, RLS |
Track Separation:
- Track 1: Structural objects (tables, indexes, constraints)
- Track 2: Behavioral objects (functions, views, triggers, policies)
const plan = engine.plan(diff);
// plan.steps are ordered by phase
// plan.blocked = true if any change exceeds risk thresholdPurpose: Apply migration safely.
What it does:
- Acquires advisory lock to prevent concurrent migrations
- Creates savepoints for partial rollback
- Drift detection (fails if schema changed since introspection)
- Progress callbacks for UI integration
- Handles non-transactional operations (e.g.,
CREATE INDEX CONCURRENTLY)
Safety Mechanisms:
| Mechanism | Purpose |
|---|---|
| Advisory Lock | Prevents concurrent migrations (key = CRC32 of connectionId) |
| Drift Detection | Fails if database changed since introspection |
| Savepoints | Partial rollback on failure |
| Fingerprints | SHA-256 of schema state for drift detection |
const result = await engine.execute(pool, plan);
// result.migrationId, result.applied, result.failedPurpose: Persist migration history.
Backends:
| Backend | Use Case |
|---|---|
| PostgreSQL | Production — migration_history table |
| Memory | Testing — in-memory store |
| File | Local development — JSON files |
| GitHub | CI/CD — commit-based history |
Schema:
CREATE TABLE migration_history (
id UUID PRIMARY KEY,
applied_at TIMESTAMPTZ DEFAULT NOW(),
connection_id UUID NOT NULL,
snapshot_before JSONB,
snapshot_after JSONB,
changes JSONB,
ddl TEXT[],
duration_ms INTEGER,
status TEXT
);Purpose: Handle behavioral objects separately from structural DDL.
What it does:
- Processes functions, views, triggers, policies, rules
- Handles RLS (Row Level Security) policies
- Manages event triggers and aggregate functions
- Processes AFTER structural DDL completes
Object Types:
- Views (regular and materialized)
- Functions (scalar, aggregate, window)
- Procedures
- Triggers (table and event)
- Policies (RLS)
- Rules
// Behavioral objects processed after structural
const result = await engine.execute(pool, plan, {
behavioral: true // Include Track 2 objects
});1. Introspect current DB
│
▼
2. Compare with desired schema
│
▼
3. Generate DDL statements
│
▼
4. Plan execution order
│
▼
5. Risk assessment
│
├── BLOCKED? → Stop, show warnings
│
└── Continue
│
▼
6. Execute with locks
│
▼
7. Apply behavioral DDL
│
▼
8. Store history
| Error Type | Recovery |
|---|---|
IntrospectionError |
Fails fast, returns partial results |
DiffError |
Returns changes + warnings |
DDLGenerationError |
Skips unsupported, continues |
PlanningError |
Circular deps → report and fail |
ExecutionError |
Rollback to savepoint, report position |
DriftDetectedError |
Fail, require re-introspection |
| Operation | Typical Time |
|---|---|
| Introspect (100 tables) | 500ms - 2s |
| Diff | < 100ms |
| DDL Generation | < 50ms |
| Plan | < 50ms |
| Execute (varies by DDL) | Depends on PostgreSQL |
- Supported-Objects — Full list of 50+ types
- API-Reference — Method signatures
- Safe-Migration-Patterns — Detailed safe pattern docs