Metagration is a PostgreSQL-native database migration framework that uses logical replication to enable zero-downtime schema migrations. This is a mature, production-ready tool (v2.0.0) distributed as a Trusted Language Extension (TLE) for PostgreSQL 18+.
The core philosophy: migrations are PostgreSQL functions, executed in dependency order via topological sort, with full ACID guarantees.
Metagration uses a single-file architecture where sql/metagration.sql (~470 lines) is the source of truth. For v2.0+, this source is built into a TLE installer using a Python build script.
Key Components:
- Migration scripts stored in
metagration.scripttable - Execution log in
metagration.logtable - Topological sort for dependency resolution
- Transaction-wrapped execution with rollback support
sql/metagration.sql: Single source file (~470 lines) - source of truthinstall-tle.sql.template: Template with {{VERSION}} and {{SOURCE}} markersbuild-tle.py: Python script that generates TLE installerinstall-tle.sql: Generated TLE installer (git-ignored)Makefile: Simple build targets (tle, test, clean)
test/test.sql: Main test runner using pgTAP (plans 100 tests)test/core.sql: Core functionality teststest/verify.sql: Verification teststest.sh: Docker-based test runnerDockerfile: PostgreSQL 18 + pg_tle test image
README.md: User-facing installation and usageCLAUDE.md: This file - developer guidancedocs/plans/: Design documents and implementation plans
# Generate install-tle.sql from source
make tle
# This runs: python3 build-tle.py
# Reads: sql/metagration.sql, install-tle.sql.template
# Generates: install-tle.sql# Full test suite using Docker
make test
# This will:
# 1. Build install-tle.sql
# 2. Build metagration/test Docker image (PostgreSQL 18 + pg_tle)
# 3. Run PostgreSQL container with mounted volume
# 4. Install pg_tle extension
# 5. Install metagration TLE
# 6. Execute test/test.sql with pgTAP tests# In a PostgreSQL 18 database with pg_tle:
CREATE EXTENSION pg_tle;
\i install-tle.sql
CREATE EXTENSION metagration;
# Uninstall:
DROP EXTENSION metagration CASCADE;
SELECT pgtle.uninstall_extension('metagration');- Single Source File: All functionality in
sql/metagration.sql- do not split into multiple files - No External Dependencies: Pure PostgreSQL - no external libraries or languages (except build tools)
- Backward Compatibility: Migration format and API must remain stable
- Test Coverage: All changes must pass 100 pgTAP tests
- TLE Installation Required: Must install via
pg_tle.install_extension()- traditionalCREATE EXTENSIONloading from filesystem not supported in v2.0+
Metagration implements defense-in-depth security:
All functions/procedures set search_path = metagration, pg_catalog, pg_temp to prevent injection attacks. This is critical - do not remove these settings.
The script_schema column has a CHECK constraint requiring valid PostgreSQL identifiers. This prevents malicious schema injection when executing migration procedures.
- All procedures use
SECURITY INVOKER(run with caller's privileges) - Use
metagration.setup_permissions()to configure role-based access - Only trusted users should have migration permissions
- Migration scripts execute arbitrary SQL by design - this requires trust
When modifying code:
- Use
%Ifor identifier quoting (schema/table/column names) - Use
%Lfor literal quoting (string values) - Use
USINGclauses for parameterized queries - Never use
%sfor user-controlled identifiers - Always set
search_pathin function definitions
Migrations are stored in metagration.script table:
name: Unique identifier (e.g., "001_create_users")script: SQL code to executereverse: Rollback SQL (optional)requires: Array of dependency names
- User calls
metagration.apply()ormetagration.apply(target_revision) - System performs topological sort of unapplied scripts
- Each script executed in transaction
- Success/failure logged to
metagration.log - Current revision tracked via
metagration.current_revision()
metagration.apply(integer): Apply migrations up to target revisionmetagration.current_revision(): Get current revision numbermetagration.export(): Export all scripts as SQLmetagration.rollback(integer): Roll back to target revisionmetagration.plan(integer): Show execution plan without applying
- 100 pgTAP tests covering core functionality
- Tests run in Docker with PostgreSQL 18 + pg_tle
- Test data is deterministic and isolated
- Each test verifies specific behavior with clear assertions
- Edit
sql/metagration.sqldirectly - Run
make testto verify changes - Update tests if adding/changing functionality
- Update README.md if user-facing changes
- Rebuild TLE installer:
make tle
- Add implementation to
sql/metagration.sql - Add pgTAP tests to appropriate test file
- Run
make testto verify - Update documentation
- Add failing test that reproduces bug
- Fix in
sql/metagration.sql - Verify test now passes
- Check all 100 tests still pass
- Run
make testto establish baseline - Make changes to
sql/metagration.sql - Run
make testto verify no regressions - All tests must still pass
Metagration includes a hierarchical set of introspection views in the metagration schema.
Hierarchical design:
- Base
relationsview: common attributes for all table-like objects - Detail views: type-specific attributes (tables_detail, views_detail, etc.)
- Supporting views: columns, constraints, statistics
Security model:
- All views use
SECURITY INVOKER - Permission filtering via
has_table_privilege() - Only shows objects user can SELECT
Data sources:
- Primary: pg_catalog (pg_class, pg_attribute, pg_constraint, etc.)
- Secondary: information_schema (for standardized column metadata)
- Statistics: pg_stats (automatically permission-filtered)
relations (base)
├── tables_detail
├── views_detail
├── materialized_views_detail
├── foreign_tables_detail
├── partitions_detail
├── columns (also uses information_schema.columns)
└── constraints (also uses information_schema.table_constraints)
column_statistics (independent, uses pg_stats)
When adding new introspection views:
- Follow TDD: write test first in
test/introspection.sql - Add view to
sql/metagration.sqlwithSECURITY INVOKER - Use fully-qualified names (pg_catalog.pg_class)
- Filter by
has_table_privilege()or join to existing filtered view - Update test plan count in
test/test.sql - Run
make testto verify
- v2.0.0: TLE release for PostgreSQL 18+
- v1.0.5: Final PGXN release supporting PostgreSQL 11+