pgmi Session API Reference#
Core model: pgmi doesn’t interpret your SQL — it loads your files into PostgreSQL session tables and hands control to
deploy.sql, which decides what to do with them.
How pgmi Actually Works#
When you run pgmi deploy ./myproject, pgmi connects, prepares the session objects shown in the diagram above (with --verbose it also runs SET client_min_messages = 'debug', enabling RAISE DEBUG output), and then executes your deploy.sql — which queries the views and runs files directly:
FOR v_file IN (SELECT * FROM pgmi_plan_view ORDER BY execution_order)
LOOP
EXECUTE v_file.content;
END LOOP;The key insight: deploy.sql is the deployment script. It queries pgmi_plan_view and uses EXECUTE to run files directly. You control the deployment logic — transactions, ordering, conditionals, error handling.
Connection requirement: Because everything depends on pg_temp tables surviving for the entire session, pgmi requires a direct connection or a pooler in session mode. Transaction-mode poolers (PgBouncer, RDS Proxy, etc.) will silently break deployments by reassigning the backend connection. See Production Guide — Connection Requirements
.
Two-Tier API Design#
pgmi uses a two-tier naming convention for session objects:
| Tier | Naming | Purpose | Example |
|---|---|---|---|
| Internal | _pgmi_* prefix | Used by pgmi Go code | _pgmi_source, _pgmi_parameter |
| Public | *_view suffix | Stable API for deploy.sql | pgmi_source_view, pgmi_plan_view |
Why this matters:
- Internal tables may change between versions
- Public views provide a stable contract for deploy.sql
- Always use views (
pgmi_source_view,pgmi_plan_view) in your SQL
Public Interface (Stable API)#
These views and functions are the stable API for deploy.sql. Use these instead of querying internal tables directly.
Which View Should I Use?#
| Use Case | View | Why |
|---|---|---|
| Deploying files | pgmi_plan_view | Pre-sorted by execution order, includes metadata |
| Introspection/debugging | pgmi_source_view | Raw file access, all columns available |
| Custom ordering | pgmi_source_view | Apply your own ORDER BY logic |
| Metadata-driven deployment | pgmi_plan_view | Respects <pgmi-meta> sort keys |
Rule of thumb: Use pgmi_plan_view for deployment loops. Use pgmi_source_view when you need raw access or custom filtering beyond what the plan provides.
File Access#
pgmi_source_view#
All project source files (excludes the root deploy.sql and __test__/ files).
This view provides direct access to all discovered files. For most use cases, prefer pgmi_plan_view which adds execution ordering via metadata.
Only the root
deploy.sqlis excluded. pgmi executes that one itself, so it never appears here. Adeploy.sqlanywhere else —examples/deploy.sql,sub-project/deploy.sql— loads like any other file, withis_sql_file = true, and a plan loop will execute it. If you keep nested projects inside a project, filter them out by path. See what gets loaded .
| Column | Type | Description |
|---|---|---|
path | text | Normalized path (always starts with ./) |
name | text | Filename without directory |
directory | text | Directory path ending with / |
extension | text | File extension (e.g., .sql) |
depth | integer | Nesting level (0 = root) |
content | text | Full file content |
size_bytes | bigint | File size in bytes |
checksum | text | SHA-256 of original content |
pgmi_checksum | text | SHA-256 of normalized content |
path_parts | text[] | Path split by / |
is_sql_file | boolean | True for .sql, .ddl, .dml, .dql, .dcl, .psql, .pgsql, .plpgsql (case-insensitive). The extension decides, not the directory — 001.sql.bak is false |
is_test_file | boolean | Always false in this view — files matching __test__/ are routed to pgmi_test_source_view instead |
parent_folder_name | text | Immediate parent directory name |
-- List all SQL files in migrations/
SELECT path, name FROM pg_temp.pgmi_source_view
WHERE directory = './migrations/' AND is_sql_file
ORDER BY path;pgmi_plan_view#
Pre-computed execution plan with metadata.
This view joins _pgmi_source with _pgmi_source_metadata and provides a clean interface for file access:
| Column | Type | Description |
|---|---|---|
path | text | File path |
content | text | File content |
checksum | text | Normalized checksum (≠ pgmi_source_view.checksum which is raw) — see Two checksums |
generic_id | uuid | Auto-generated UUID from path |
id | uuid | Explicit ID from <pgmi-meta>
(NULL if none) |
idempotent | boolean | Whether file can be re-executed (defaults to true for files without metadata) |
description | text | From <pgmi-meta> (defaults to '' for files without metadata, never NULL) |
sort_key | text | Execution ordering key |
execution_order | bigint | Sequential execution number |
This view holds every loaded file, not only SQL. README.md, pgmi.yaml
and editor leftovers (001.sql~, .bak, .orig) are all in it, and a
directory filter does not exclude them — a backup saved beside the migration
you were editing matches './migrations/%' and executes as a migration, with
the deploy still exiting 0. is_sql_file lives on pgmi_source_view, so every
execute loop below joins back to it. That join is the guard, not decoration.
Recommended usage:
-- Deploy files in metadata-driven order using direct execution
DO $$
DECLARE v_file RECORD;
BEGIN
FOR v_file IN (
SELECT p.path, p.content
FROM pg_temp.pgmi_plan_view p
JOIN pg_temp.pgmi_source_view s ON s.path = p.path
WHERE s.is_sql_file AND p.path LIKE './migrations/%'
ORDER BY p.execution_order
) LOOP
RAISE NOTICE 'Executing: %', v_file.path;
EXECUTE v_file.content;
END LOOP;
END $$;Two checksums, and which to track against#
pgmi_source_view carries two checksum columns per file:
| Column | Algorithm |
|---|---|
checksum | SHA-256 of the raw file bytes |
pgmi_checksum | SHA-256 after normalization: comments stripped, whitespace collapsed, and case folded only where SQL ignores it — keywords and unquoted identifiers. Literals, quoted identifiers and dollar-quoted bodies keep their case, so "Users"/"users" and 'Production'/'production' stay distinct |
The naming trap: pgmi_plan_view.checksum is the normalized checksum, not the raw one. The column name is the same, the value is different. Code that tracks pgmi_source_view.checksum into a history table and later compares against pgmi_plan_view.checksum will never match.
Which to use:
- Track against
pgmi_checksum(orpgmi_plan_view.checksum) when you want reformatting, re-indentation, and comment edits to be free — a touched-up file won’t re-execute. - Track against
pgmi_source_view.checksumwhen you need byte-exact provenance — any edit, including a comment, triggers re-execution.
Honest cost: a change that touches only comments is invisible to pgmi_checksum. If your comments carry operational meaning (e.g., planner hints via pg_hint_plan, or <pgmi-meta> blocks that don’t affect the normalized body), the normalized checksum won’t catch the edit.
See Highlights §5 and Checksum-based change detection for usage examples.
Parameters#
CLI parameters (passed via --param key=value) are accessible in multiple ways. This section consolidates all parameter access patterns.
Method 1: Session Variables (Recommended)#
pgmi automatically sets session variables with the pgmi. prefix. This is the simplest and most common approach:
-- Get parameter with default (the true argument prevents errors if not set)
v_env := COALESCE(current_setting('pgmi.env', true), 'development');
-- In conditional logic
IF COALESCE(current_setting('pgmi.env', true), 'dev') = 'production' THEN
-- Production-specific logic
END IF;
-- Check if parameter was provided
IF current_setting('pgmi.feature_flag', true) IS NOT NULL THEN
-- Parameter was explicitly set
END IF;Important: Always pass true as the second argument to current_setting(). This returns NULL instead of raising an error when the variable is not set.
Method 2: pgmi_parameter_view (Introspection)#
For iterating over parameters or building dynamic logic:
| Column | Type | Description |
|---|---|---|
key | text | Parameter name, always lower-cased on load — --param apiVersion=2 is stored as apiversion |
value | text | Parameter value (always text, cast as needed) |
type | text | Declared type hint (text, int, boolean, etc.) |
required | boolean | Whether parameter was marked required |
default_value | text | Default value if not provided |
description | text | Human-readable description |
Compare lower-cased. key mirrors the session variable pgmi set, and
PostgreSQL’s GUC namespace is case-insensitive — so the two access methods
disagree for a mixed-case parameter, and the view disagrees silently:
-- with --param apiVersion=2
current_setting('pgmi.apiVersion', true) -- '2'
SELECT value FROM pgmi_parameter_view WHERE key = 'apiVersion' -- no row
SELECT value FROM pgmi_parameter_view WHERE key = 'apiversion' -- '2'
SELECT value FROM pgmi_parameter_view WHERE key = lower('apiVersion') -- '2', safe either wayStoring the original case instead would make key name something the session
variable does not, which trades a visible mismatch for an invisible one.
-- List all parameters
SELECT key, value, description FROM pg_temp.pgmi_parameter_view;
-- Iterate over parameters dynamically.
-- Redact secret-like keys by default — values may be passwords, tokens, etc.
DO $$
DECLARE
v_param RECORD;
BEGIN
FOR v_param IN SELECT key, value FROM pg_temp.pgmi_parameter_view LOOP
RAISE NOTICE 'Parameter: % = %',
v_param.key,
CASE WHEN lower(v_param.key) ~ '(password|secret|token|key|credential|auth)'
THEN '********' ELSE v_param.value END;
END LOOP;
END $$;Method 3: deployment_setting() Helper (Advanced Template Only)#
Scope: advanced template only. This is SQL that
pgmi init --template advancedcopied into your project, not behaviour of the pgmi binary.
The advanced template provides a helper function with error handling:
-- Get required parameter (raises exception if missing)
v_admin_role := pg_temp.deployment_setting('database_admin_role');
-- Get optional parameter (returns NULL if missing)
v_optional := pg_temp.deployment_setting('optional_key', false);Note: This function uses a deployment. prefix internally and normalizes key names. It’s defined in the advanced template’s deploy.sql, not in pgmi core.
Parameter Precedence#
Parameters merge from multiple sources (later wins):
pgmi.yaml params < --params-file < --param CLI flagSee Configuration Reference for details.
Type Coercion#
All parameter values are stored as text. Cast them as needed:
-- Boolean
v_enabled := COALESCE(current_setting('pgmi.feature_enabled', true), 'false')::boolean;
-- Integer
v_limit := COALESCE(current_setting('pgmi.max_rows', true), '100')::int;
-- Timestamp
v_cutoff := COALESCE(current_setting('pgmi.since', true), '2024-01-01')::timestamp;Template Responsibility#
pgmi core provides raw parameter storage. Templates handle:
- Declaring expected parameters (advanced template uses
session.xml) - Validating required parameters
- Providing default values
- Type validation and coercion
Direct Execution Pattern#
pgmi uses a direct execution model: your deploy.sql queries pgmi_plan_view and uses EXECUTE to run files. This gives you full control over transaction boundaries, execution order, and conditional logic.
Basic pattern:
DO $$
DECLARE v_file RECORD;
BEGIN
-- Transaction control is in your hands
FOR v_file IN (
SELECT p.path, p.content
FROM pg_temp.pgmi_plan_view p
JOIN pg_temp.pgmi_source_view s ON s.path = p.path
WHERE s.is_sql_file AND p.path LIKE './schemas/%'
ORDER BY p.execution_order
) LOOP
RAISE NOTICE 'Executing: %', v_file.path;
EXECUTE v_file.content;
END LOOP;
END $$;With explicit transaction boundaries:
DO $$
DECLARE v_file RECORD;
BEGIN
-- Phase 1: Schema changes in one transaction
BEGIN
FOR v_file IN (
SELECT p.path, p.content
FROM pg_temp.pgmi_plan_view p
JOIN pg_temp.pgmi_source_view s ON s.path = p.path
WHERE s.is_sql_file AND p.path LIKE './schemas/%'
ORDER BY p.execution_order
) LOOP
EXECUTE v_file.content;
END LOOP;
EXCEPTION WHEN OTHERS THEN
RAISE EXCEPTION 'Schema phase failed: %', SQLERRM;
END;
-- Phase 2: Migrations
FOR v_file IN (
SELECT p.path, p.content
FROM pg_temp.pgmi_plan_view p
JOIN pg_temp.pgmi_source_view s ON s.path = p.path
WHERE s.is_sql_file AND p.path LIKE './migrations/%'
ORDER BY p.execution_order
) LOOP
EXECUTE v_file.content;
END LOOP;
END $$;Conditional execution:
DO $$
DECLARE
v_file RECORD;
v_env TEXT := COALESCE(current_setting('pgmi.env', true), 'development');
BEGIN
FOR v_file IN (
SELECT p.path, p.content
FROM pg_temp.pgmi_plan_view p
JOIN pg_temp.pgmi_source_view s ON s.path = p.path
WHERE s.is_sql_file AND p.path LIKE './migrations/%'
ORDER BY p.execution_order
) LOOP
EXECUTE v_file.content;
END LOOP;
-- Only seed data in development
IF v_env = 'development' THEN
FOR v_file IN (
SELECT p.path, p.content
FROM pg_temp.pgmi_plan_view p
JOIN pg_temp.pgmi_source_view s ON s.path = p.path
WHERE s.is_sql_file AND p.path LIKE './seeds/%'
ORDER BY p.execution_order
) LOOP
EXECUTE v_file.content;
END LOOP;
END IF;
END $$;Metadata#
pgmi_source_metadata_view#
Parsed <pgmi-meta> XML blocks from SQL files.
Files without metadata are not in this view (use pgmi_plan_view which handles fallbacks).
| Column | Type | Description |
|---|---|---|
path | text | File path (references pgmi_source_view.path) |
id | uuid | Explicit script UUID from metadata |
idempotent | boolean | Whether script can be re-executed safely |
sort_keys | text[] | Array of execution ordering keys |
description | text | Human-readable description |
-- List files with metadata
SELECT path, id, idempotent, sort_keys
FROM pg_temp.pgmi_source_metadata_view;
-- Find non-idempotent migrations
SELECT path FROM pg_temp.pgmi_source_metadata_view
WHERE NOT idempotent;See Metadata Guide for syntax and usage patterns.
Test Views#
pgmi_test_source_view#
Test file content from __test__/ or __tests__/ directories.
| Column | Type | Description |
|---|---|---|
path | text | Full path to test file |
directory | text | Parent test directory (ending with /) |
filename | text | Filename without directory |
content | text | Full file content |
is_fixture | boolean | True for _setup.sql files |
-- List all test files
SELECT path, is_fixture FROM pg_temp.pgmi_test_source_view
ORDER BY directory, filename;
-- Get fixture files only
SELECT path FROM pg_temp.pgmi_test_source_view
WHERE is_fixture;pgmi_test_directory_view#
Hierarchical test directory structure.
| Column | Type | Description |
|---|---|---|
path | text | Directory path (ending with /) |
parent_path | text | Parent directory path (NULL for root) |
depth | integer | Nesting level (0 = root __test__/) |
-- See test directory hierarchy
SELECT path, parent_path, depth
FROM pg_temp.pgmi_test_directory_view
ORDER BY depth, path;
-- Find nested test directories
SELECT path FROM pg_temp.pgmi_test_directory_view
WHERE depth > 0;Testing#
CALL pgmi_test() Preprocessor Macro#
Executes tests with automatic savepoint isolation.
The CALL pgmi_test() is a preprocessor macro that Go expands before sending SQL to PostgreSQL:
-- Run all tests with default callback
CALL pgmi_test();
-- Run tests matching a pattern (POSIX regex)
CALL pgmi_test('.*/integration/.*');
CALL pgmi_test('.*_critical\.sql$');
-- Run tests with custom callback function
CALL pgmi_test('.*/auth/.*', 'pg_temp.my_custom_callback');Automatic behavior:
- Creates SAVEPOINTs before each
_setup.sql - Executes tests in lexicographic order
- Rolls back transactional test changes to the SAVEPOINT; sequence advances and external effects are not rolled back
- Includes ancestor
_setup.sqlfiles needed by matching tests - Calls
pgmi_test_generate()internally to produce inline SQL
pgmi_test_plan(pattern) Function#
Returns the test execution plan as a table (for introspection).
This is a TABLE-returning function (not a view). Files from __test__/ or __tests__/ directories are automatically organized into a depth-first execution plan with fixture/test/teardown lifecycle.
| Column | Type | Description |
|---|---|---|
ordinal | integer | Sequential execution order (1-based) |
step_type | text | 'fixture', 'test', or 'teardown' |
script_path | text | Path to test file (NULL for teardown) |
directory | text | Test directory containing the script |
depth | integer | Nesting level (0 = root __test__/) |
-- See what tests would run
SELECT * FROM pg_temp.pgmi_test_plan();
-- Filter by pattern (POSIX regex on script_path)
SELECT * FROM pg_temp.pgmi_test_plan('.*/auth/.*');Test execution emits notices:
NOTICE: [pgmi] Test suite startedNOTICE: [pgmi] Fixture: ./path/to/_setup.sqlNOTICE: [pgmi] Test: ./path/to/test_example.sqlNOTICE: [pgmi] Test suite completed (N steps)
With --verbose, DEBUG messages show rollback and teardown events ([pgmi] Rollback: ..., [pgmi] Teardown: ...).
pgmi_test_generate(pattern, callback) Function#
Generates the SQL code for pgmi_test() macro expansion.
This is an internal function called by the Go preprocessor. It returns the complete SQL text that replaces the CALL pgmi_test() macro.
-- See what SQL the macro generates (for debugging)
SELECT pg_temp.pgmi_test_generate();
SELECT pg_temp.pgmi_test_generate('.*/auth/.*', 'pg_temp.my_callback');Critical implementation detail: The generated SQL uses top-level SAVEPOINT commands, not PL/pgSQL savepoints. PostgreSQL’s PL/pgSQL does not support SAVEPOINT, ROLLBACK TO SAVEPOINT, or RELEASE SAVEPOINT commands directly — they must be issued as top-level SQL statements.
The generated structure looks like:
SAVEPOINT __pgmi_d1__; -- Directory savepoint (top-level SQL)
DO $$ ... EXECUTE fixture ... $$; -- Fixture content via EXECUTE
SAVEPOINT __pgmi_t2__; -- Per-directory test savepoint (top-level SQL)
DO $$ ... EXECUTE test ... $$; -- Test content via EXECUTE
ROLLBACK TO SAVEPOINT __pgmi_t2__; -- Undoes test side effects
DO $$ ... EXECUTE test2 ... $$; -- Next test in same directory
ROLLBACK TO SAVEPOINT __pgmi_t2__; -- Undoes test2 side effects
ROLLBACK TO SAVEPOINT __pgmi_d1__; -- Teardown: undoes fixture
RELEASE SAVEPOINT __pgmi_d1__; -- Clean up savepointThis is why CALL pgmi_test() must appear at the top level of your deploy.sql, not inside a DO block.
Custom Callbacks#
The second argument to pgmi_test() is the name of a callback function that receives lifecycle events. Your function must accept a single pg_temp.pgmi_test_event argument and return void:
The name must be schema-qualified. pgmi interpolates it verbatim into
SELECT <name>(ROW(...)), and PostgreSQL never searches pg_temp for function
names however the search_path is set — so 'my_callback' fails with
ERROR: function my_callback(pgmi_test_event) does not exist (SQLSTATE 42883)
while 'pg_temp.my_callback' resolves.
CREATE FUNCTION pg_temp.my_callback(e pg_temp.pgmi_test_event)
RETURNS void LANGUAGE plpgsql AS $$
BEGIN
CASE e.event
WHEN 'suite_start' THEN RAISE NOTICE 'Starting test suite';
WHEN 'suite_end' THEN RAISE NOTICE 'Completed % steps', e.ordinal;
WHEN 'test_start' THEN RAISE NOTICE 'Running: %', e.path;
WHEN 'test_end' THEN RAISE NOTICE 'Passed: %', e.path;
ELSE NULL;
END CASE;
END $$;pgmi_test_event fields:
| Field | Type | Description |
|---|---|---|
event | text | Event name (see below) |
path | text | Script path (NULL for suite and teardown events) |
directory | text | Test directory containing the script |
depth | integer | Nesting level (0 = root __test__/) |
ordinal | integer | Execution order (1-based) |
context | jsonb | Extensible payload for custom data |
Event types: suite_start, fixture_start, fixture_end, test_start, test_end, rollback, teardown_start, teardown_end, suite_end.
The default callback (pgmi_test_callback) emits NOTICE for fixtures and tests, DEBUG for rollback and teardown. You can call it from your custom callback for events you don’t want to handle specially.
pgmi_persist_test_plan(schema, pattern) Function#
Exports the test plan to a permanent table for external tooling.
-- Create a permanent copy of the test plan
SELECT pg_temp.pgmi_persist_test_plan('public', NULL);
-- Creates: public.pgmi_test_plan| Parameter | Type | Description |
|---|---|---|
schema | text | Target schema for the snapshot table |
pattern | text | Optional POSIX regex filter (NULL = all tests) |
This is useful for CI/CD pipelines that need to inspect the test plan before running, or for generating test reports.
The Direct Execution Model (Critical Concept)#
This is the most important thing to understand about pgmi: pgmi’s setup phase creates the temp tables holding your files; the execution phase is entirely your deploy.sql querying those views and running files with EXECUTE.
Your deploy.sql has full control. You query pgmi_plan_view, loop through files, and use EXECUTE to run them. Transaction boundaries, error handling, execution order—all in your hands.
The Basic Pattern#
DO $$
DECLARE v_file RECORD;
BEGIN
FOR v_file IN (
SELECT p.path, p.content
FROM pg_temp.pgmi_plan_view p
JOIN pg_temp.pgmi_source_view s ON s.path = p.path
WHERE s.is_sql_file AND p.path LIKE './migrations/%'
ORDER BY p.execution_order
) LOOP
RAISE NOTICE 'Executing: %', v_file.path;
EXECUTE v_file.content;
END LOOP;
END $$;What happens:
- pgmi loads your files into internal tables and creates public views
- Your deploy.sql queries
pgmi_plan_view(orpgmi_source_view) and executes files directly withEXECUTE
Common Patterns#
Phased Deployment#
DO $$
DECLARE v_file RECORD;
BEGIN
-- Phase 1: Schema changes
RAISE NOTICE '=== Phase 1: Schema ===';
FOR v_file IN (
SELECT p.path, p.content
FROM pg_temp.pgmi_plan_view p
JOIN pg_temp.pgmi_source_view s ON s.path = p.path
WHERE s.is_sql_file AND p.path LIKE './schemas/%'
ORDER BY p.execution_order
) LOOP
RAISE NOTICE 'Executing: %', v_file.path;
EXECUTE v_file.content;
END LOOP;
-- Phase 2: Migrations
RAISE NOTICE '=== Phase 2: Migrations ===';
FOR v_file IN (
SELECT p.path, p.content
FROM pg_temp.pgmi_plan_view p
JOIN pg_temp.pgmi_source_view s ON s.path = p.path
WHERE s.is_sql_file AND p.path LIKE './migrations/%'
ORDER BY p.execution_order
) LOOP
RAISE NOTICE 'Executing: %', v_file.path;
EXECUTE v_file.content;
END LOOP;
END $$;Conditional Deployment#
CALL pgmi_test() is a preprocessor macro that expands to top-level SQL (including SAVEPOINT commands), so it must appear at the top level of deploy.sql — never inside a DO block. Structure your deploy.sql with the DO block for migrations and CALL pgmi_test() as a separate top-level statement:
BEGIN;
-- Phase 1: Migrations and seeds (inside DO block)
DO $$
DECLARE
v_file RECORD;
v_env TEXT := COALESCE(current_setting('pgmi.env', true), 'development');
BEGIN
-- Always run migrations
FOR v_file IN (
SELECT p.path, p.content
FROM pg_temp.pgmi_plan_view p
JOIN pg_temp.pgmi_source_view s ON s.path = p.path
WHERE s.is_sql_file AND p.path LIKE './migrations/%'
ORDER BY p.execution_order
) LOOP
EXECUTE v_file.content;
END LOOP;
-- Only seed data in development
IF v_env = 'development' THEN
FOR v_file IN (
SELECT p.path, p.content
FROM pg_temp.pgmi_plan_view p
JOIN pg_temp.pgmi_source_view s ON s.path = p.path
WHERE s.is_sql_file AND p.path LIKE './seeds/%'
ORDER BY p.execution_order
) LOOP
EXECUTE v_file.content;
END LOOP;
END IF;
END $$;
-- Phase 2: Tests (top-level — expands to SAVEPOINT commands)
CALL pgmi_test();
COMMIT;Dynamic File Selection#
-- Deploy SQL files from a specific directory
DO $$
DECLARE v_file RECORD;
BEGIN
FOR v_file IN (
SELECT p.path, p.content
FROM pg_temp.pgmi_plan_view p
JOIN pg_temp.pgmi_source_view s ON s.path = p.path
WHERE s.is_sql_file AND p.path LIKE './migrations/v2/%'
ORDER BY p.execution_order
) LOOP
EXECUTE v_file.content;
END LOOP;
END $$;
-- Or use POSIX regex for complex patterns
DO $$
DECLARE v_file RECORD;
BEGIN
FOR v_file IN (
SELECT path, content FROM pg_temp.pgmi_source_view
WHERE path ~ '.*/v2/.*' AND is_sql_file
ORDER BY path
) LOOP
EXECUTE v_file.content;
END LOOP;
END $$;Test Isolation with Savepoints#
BEGIN;
DO $$
DECLARE v_file RECORD;
BEGIN
-- Deploy your schema
FOR v_file IN (
SELECT p.path, p.content
FROM pg_temp.pgmi_plan_view p
JOIN pg_temp.pgmi_source_view s ON s.path = p.path
WHERE s.is_sql_file AND p.path LIKE './schemas/%'
ORDER BY p.execution_order
) LOOP
EXECUTE v_file.content;
END LOOP;
END $$;
-- Run tests (preprocessor macro handles savepoint isolation)
CALL pgmi_test();
COMMIT;Internal Tables (Implementation Details)#
These tables are the underlying storage for the session API. Users should use the public views above rather than querying these internal tables directly.
Naming convention: Internal tables use underscore prefix (_pgmi_*)
pg_temp._pgmi_source#
Raw file storage.
| Column | Type | Description |
|---|---|---|
path | text | Normalized path (always starts with ./) |
name | text | Filename without directory |
directory | text | Directory path ending with / |
extension | text | File extension (e.g., .sql) |
depth | integer | Nesting level (0 = root) |
content | text | Full file content |
size_bytes | bigint | File size |
checksum | text | SHA-256 of original content |
pgmi_checksum | text | SHA-256 of normalized content (for idempotency) |
path_parts | text[] | Path split by / |
is_sql_file | boolean | True for SQL file extensions |
is_test_file | boolean | Always false — a CHECK constraint routes test files to _pgmi_test_source instead |
parent_folder_name | text | Immediate parent directory name |
pg_temp._pgmi_parameter#
Raw parameter storage.
| Column | Type | Description |
|---|---|---|
key | text | Parameter name |
value | text | Parameter value |
type | text | Declared type hint |
required | boolean | Whether parameter is required |
default_value | text | Default if not provided |
description | text | Human-readable description |
pg_temp._pgmi_source_metadata#
Parsed XML metadata from <pgmi-meta> blocks.
| Column | Type | Description |
|---|---|---|
path | text | File path (FK to _pgmi_source.path) |
id | uuid | Explicit script UUID |
idempotent | boolean | Whether script can be re-executed |
sort_keys | text[] | Execution ordering keys (defaults to {}) |
description | text | Human-readable description |
pg_temp._pgmi_test_directory#
Test directory hierarchy.
| Column | Type | Description |
|---|---|---|
path | text | Directory path (ending with /) |
parent_path | text | Parent directory (NULL for root) |
depth | integer | Nesting level |
pg_temp._pgmi_test_source#
Test file content.
| Column | Type | Description |
|---|---|---|
path | text | Full file path |
directory | text | Parent test directory (FK to _pgmi_test_directory.path) |
filename | text | Filename only |
content | text | Full file content |
is_fixture | boolean | True for _setup.sql files |
Introspection Examples#
See What Files Are Loaded#
-- Use the view for clean access
SELECT path, execution_order, idempotent
FROM pg_temp.pgmi_plan_view
ORDER BY execution_order;See What Parameters Are Available#
SELECT key, value, type, required, default_value, description
FROM pg_temp.pgmi_parameter_view;Preview the Execution Plan#
-- See files in execution order
SELECT execution_order, path, left(content, 80) AS preview
FROM pg_temp.pgmi_plan_view
ORDER BY execution_order;See Available Tests#
SELECT step_type, script_path
FROM pg_temp.pgmi_test_plan()
ORDER BY ordinal;Philosophy: Why This Design?#
pgmi is not a migration framework. It’s an execution fabric.
| Traditional Migration Tool | pgmi |
|---|---|
| Decides execution order | Your SQL queries and filters the plan |
| Controls transactions | Your SQL controls transactions |
| Provides retry logic | Your SQL provides retry logic (EXCEPTION blocks) |
| Has migration history table | You implement tracking however you want |
| Black box | Transparent — session state is queryable |
pgmi’s job is to:
- Connect to PostgreSQL
- Load your files into session tables
- Run your deploy.sql
Your job is to:
- Query
pgmi_plan_viewto find your files - Use
EXECUTEto run them directly - Control transaction boundaries and error handling
- Decide which files run in what order
The result: Full control over deployment logic. pgmi handles infrastructure (file loading, metadata parsing, preprocessing); your SQL handles everything else. PostgreSQL is the deployment engine.
See Also#
- Testing Guide — Database testing with savepoint isolation and deploy gates
- Metadata Guide — Script tracking and execution ordering
- MCP Integration — Model Context Protocol for AI assistants (advanced template)