Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/47.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added named-migration CLI subcommand with --async support and PostgreSQL-specific example migrations for the PostgreSQL backend.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@

COMMAND_NAMES = [
'info', 'query', 'introspect', 'status',
'named-expression', 'named-procedure', 'named-procedure-graph', 'named-connection',
'named-expression', 'named-procedure', 'named-procedure-graph',
'named-migration', 'named-connection',
]


Expand All @@ -18,6 +19,7 @@ def register_commands(subparsers):
from .named_expression import create_parser as nq_parser
from .named_procedure import create_parser as np_parser
from .named_procedure_graph import create_parser as npg_parser
from .named_migration import create_parser as nm_parser
from .named_connection import create_parser as nc_parser
info_parser(subparsers)
query_parser(subparsers)
Expand All @@ -26,6 +28,7 @@ def register_commands(subparsers):
nq_parser(subparsers)
np_parser(subparsers)
npg_parser(subparsers)
nm_parser(subparsers)
nc_parser(subparsers)


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# src/rhosocial/activerecord/backend/impl/postgres/cli/named_migration.py
"""named-migration subcommand — PostgreSQL adapter for the shared CLI helper."""

from __future__ import annotations

from rhosocial.activerecord.backend.impl.postgres import PostgresBackend, AsyncPostgresBackend

from .connection import create_connection_parent_parser, resolve_connection_config_from_args
from .output import create_provider


def create_parser(subparsers):
"""Create the named-migration subcommand parser.

Provides connection and output arguments via a parent parser.
"""
from rhosocial.activerecord.backend.migration.cli import create_named_migration_parser

local_parent = create_connection_parent_parser()
nm_epilog = """Examples:
# Apply a migration
%(prog)s myapp.migrations.v001.CreateUsersTable --host localhost --database mydb --direction up

# Rollback
%(prog)s myapp.migrations.v001.CreateUsersTable --host localhost --database mydb --direction down

# Apply with record store
%(prog)s myapp.migrations.v001.CreateUsersTable --database mydb --record-store ./migrations.json

# Dry-run
%(prog)s myapp.migrations.v001.CreateUsersTable --database mydb --direction up --dry-run

# Apply asynchronously
%(prog)s myapp.migrations.v001.CreateUsersTable --database mydb --direction up --async

# Pass parameters to the migration
%(prog)s myapp.migrations.v001.CreateUsersTable --param table_name=custom_table

# List available migrations
%(prog)s myapp.migrations --list

# Describe a migration
%(prog)s myapp.migrations.v001.CreateUsersTable --describe

# Batch: apply all pending migrations in a module
%(prog)s myapp.migrations --all --record-store ./migrations.json

# Batch: dry-run all pending migrations
%(prog)s myapp.migrations --all --record-store ./migrations.json --dry-run

# Batch: rollback all applied migrations
%(prog)s myapp.migrations --all --record-store ./migrations.json --direction down

# Batch: wrap all migrations in a single transaction
%(prog)s myapp.migrations --all --record-store ./migrations.json --single-transaction

End-to-end demo scripts with UP/DOWN round-trip verification:
https://github.com/rhosocial/python-activerecord-postgres/tree/main/src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations
PYTHONPATH=src bash demo_basic.sh
PYTHONPATH=src bash demo_chain.sh
PYTHONPATH=src bash demo_all.sh
"""
return create_named_migration_parser(subparsers, local_parent, epilog=nm_epilog)


def handle(args):
"""Handle the named-migration subcommand."""
from rhosocial.activerecord.backend.migration.cli import handle_named_migration as handle_nm

provider = create_provider(args.output, ascii_borders=args.rich_ascii)

backend = None

def backend_factory():
nonlocal backend
config = resolve_connection_config_from_args(args)
backend = PostgresBackend(connection_config=config)
backend.connect()
backend.introspect_and_adapt()
return backend

def disconnect():
if backend and getattr(backend, "_connection", None):
backend.disconnect()

is_async = getattr(args, "is_async", False)
if is_async:
async_backend = None

def backend_async_factory():
nonlocal async_backend
config = resolve_connection_config_from_args(args)
async_backend = AsyncPostgresBackend(connection_config=config)
return async_backend

async def disconnect_async():
if async_backend and getattr(async_backend, "_connection", None):
await async_backend.disconnect()

handle_nm(
args,
provider,
backend_factory=backend_factory,
disconnect=disconnect,
backend_async_factory=backend_async_factory,
disconnect_async=disconnect_async,
)
return

handle_nm(
args,
provider,
backend_factory=backend_factory,
disconnect=disconnect,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/__init__.py
"""Named migration examples for PostgreSQL backend."""
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#!/usr/bin/env bash
# ===========================================================================
# demo_all.sh — batch migration (--all) for PostgreSQL
#
# Scenarios:
# - --all runs all pending migrations in dependency order
# - --all --dry-run preview without changes
# - --all --direction down rolls back everything
#
# Usage:
# cd python-activerecord-postgres
# DEMO_VENV_PYTHON=.venv3.14-ubuntu26.04/bin/python \
# PYTHONPATH=src \
# bash src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/demo_all.sh
# ===========================================================================
set -euo pipefail

if [ -d "./src" ]; then
export PYTHONPATH="${PYTHONPATH:-}:$(pwd)/src"
fi

MODULE="rhosocial.activerecord.backend.impl.postgres.examples.named_migrations"
STORE="./demo_pg_all_mig.json"
VENV_PYTHON="${DEMO_VENV_PYTHON:-python3}"
PYTHON="$VENV_PYTHON -m rhosocial.activerecord.backend.impl.postgres"

rm -f "$STORE"
echo "=== Batch Migration (--all) for PostgreSQL ==="
echo

echo "[1] --all without --record-store (should error):"
$PYTHON named-migration "${MODULE}.migrations" --all --host localhost --database test 2>&1 || true
echo

echo "[2] --all --dry-run (preview SQL, no changes):"
$PYTHON named-migration "${MODULE}.migrations" --all --host localhost --database test --dry-run --record-store "$STORE"
echo

echo "[3] --all apply all pending migrations:"
$PYTHON named-migration "${MODULE}.migrations" --all --host localhost --database test --record-store "$STORE"
echo

echo "[4] Re-run --all (all applied, should be no-op):"
$PYTHON named-migration "${MODULE}.migrations" --all --host localhost --database test --record-store "$STORE" 2>&1 || true
echo

echo "[5] --all --single-transaction (rollback in one transaction):"
$PYTHON named-migration "${MODULE}.migrations" --all --host localhost --database test \
--direction down --record-store "$STORE" --single-transaction
echo

echo "[6] --all --single-transaction (re-apply UP):"
$PYTHON named-migration "${MODULE}.migrations" --all --host localhost --database test \
--direction up --record-store "$STORE" --single-transaction
echo

echo "[7] --all --direction down rollback everything:"
$PYTHON named-migration "${MODULE}.migrations" --all --host localhost --database test \
--direction down --record-store "$STORE"
echo

echo "[8] Record store final state:"
cat "$STORE"
echo

rm -f "$STORE"
echo "=== Batch Migration (--all) Complete for PostgreSQL ==="
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#!/usr/bin/env bash
# ===========================================================================
# demo_async.sh — async migration execution (--async) for PostgreSQL
#
# Scenarios:
# - apply UP with --async
# - dry-run with --async
# - rollback DOWN with --async
#
# Requires: asyncpg
#
# Usage:
# cd python-activerecord-postgres
# PYTHONPATH=src \
# bash src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/demo_async.sh
# ===========================================================================
set -euo pipefail

if [ -d "./src" ]; then
export PYTHONPATH="${PYTHONPATH:-}:$(pwd)/src"
fi

MODULE="rhosocial.activerecord.backend.impl.postgres.examples.named_migrations"
FQN="${MODULE}.migrations.V001CreateUsers"
STORE="./demo_pg_async_mig.json"
VENV_PYTHON="${DEMO_VENV_PYTHON:-python3}"
PYTHON="$VENV_PYTHON -m rhosocial.activerecord.backend.impl.postgres"

rm -f "$STORE"
echo "=== Async Migration (--async) for PostgreSQL ==="
echo

echo "[1] Async dry-run (preview SQL, no changes):"
$PYTHON named-migration "$FQN" --host localhost --database test \
--direction up --dry-run --async
echo

echo "[2] Async apply UP:"
$PYTHON named-migration "$FQN" --host localhost --database test \
--direction up --async --record-store "$STORE"
echo

echo "[3] Async rollback DOWN:"
$PYTHON named-migration "$FQN" --host localhost --database test \
--direction down --async --record-store "$STORE"
echo

rm -f "$STORE"
echo "=== Async Migration Demo Complete for PostgreSQL ==="
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#!/usr/bin/env bash
# ===========================================================================
# demo_basic.sh — single migration basic operations (PostgreSQL)
#
# Scenarios:
# - apply / rollback a single migration
# - dry-run preview
# - duplicate execution protection
#
# Usage:
# cd python-activerecord-postgres
# DEMO_VENV_PYTHON=.venv3.14-ubuntu26.04/bin/python \
# PYTHONPATH=src \
# bash src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/demo_basic.sh
# ===========================================================================
set -euo pipefail

if [ -d "./src" ]; then
export PYTHONPATH="${PYTHONPATH:-}:$(pwd)/src"
fi

MODULE="rhosocial.activerecord.backend.impl.postgres.examples.named_migrations"
FQN="${MODULE}.migrations.V001CreateUsers"
STORE="./demo_pg_basic_mig.json"
VENV_PYTHON="${DEMO_VENV_PYTHON:-python3}"
PYTHON="$VENV_PYTHON -m rhosocial.activerecord.backend.impl.postgres"

rm -f "$STORE"
echo "=== Single Migration Basic Operations (PostgreSQL) ==="
echo

echo "[1] List all migrations in the module:"
$PYTHON named-migration "${MODULE}.migrations" --list -o table
echo

echo "[2] Describe V001CreateUsers (--describe):"
$PYTHON named-migration "$FQN" --describe
echo

echo "[3] Dry-run preview (no actual changes):"
$PYTHON named-migration "$FQN" --host localhost --database test --direction up --dry-run
echo

echo "[4] Apply UP (create users table):"
$PYTHON named-migration "$FQN" --host localhost --database test --direction up --record-store "$STORE"
echo

echo "[5] Record store contents:"
cat "$STORE"
echo

echo
echo "[6] Duplicate UP (should be rejected):"
$PYTHON named-migration "$FQN" --host localhost --database test --direction up --record-store "$STORE" 2>&1 || true
echo

echo "[7] Apply DOWN (drop users table):"
$PYTHON named-migration "$FQN" --host localhost --database test --direction down --record-store "$STORE"
echo

echo "[8] Record store after rollback:"
cat "$STORE"
echo

rm -f "$STORE"
echo "=== Single Migration Basic Operations Complete (PostgreSQL) ==="
Loading
Loading