Skip to content

Commit 6eb21a3

Browse files
committed
feat(cli): add named-migration subcommand for PostgreSQL backend (#47)
* feat(cli): add named-migration subcommand (PostgreSQL adapter) Registers the 'named-migration' CLI subcommand for the PostgreSQL backend. Delegates to the shared implementation in rhosocial.activerecord.backend.migration.cli. - cli/named_migration.py: parser + handler adapter - cli/__init__.py: register in COMMAND_NAMES and register_commands() * feat(migration): add --async support, example migrations, and demo scripts for PostgreSQL - Extend named_migration.py adapter with --async support (AsyncPostgresBackend) - Add extensive CLI epilog with 12 scenario examples and demo script references - Create examples/named_migrations/ with migrations.py, expressions.py, run_basic.py, run_chain.py - Add demo_basic.sh, demo_chain.sh, demo_all.sh end-to-end shell scripts - expressions.py uses PostgresSerialType and PostgresCharacterVaryingType * chore: add changelog fragment for PR #47 * chore: add parameterized migration, async, incompatible, and single-transaction demo scripts for PostgreSQL * fix(postgres): use positional arg instead of keyword 'size' for PostgresCharacterVaryingType * ci: trigger rebuild
1 parent bdaa142 commit 6eb21a3

14 files changed

Lines changed: 838 additions & 1 deletion

File tree

‎changelog.d/47.added.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added named-migration CLI subcommand with --async support and PostgreSQL-specific example migrations for the PostgreSQL backend.

‎src/rhosocial/activerecord/backend/impl/postgres/cli/__init__.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55

66
COMMAND_NAMES = [
77
'info', 'query', 'introspect', 'status',
8-
'named-expression', 'named-procedure', 'named-procedure-graph', 'named-connection',
8+
'named-expression', 'named-procedure', 'named-procedure-graph',
9+
'named-migration', 'named-connection',
910
]
1011

1112

@@ -18,6 +19,7 @@ def register_commands(subparsers):
1819
from .named_expression import create_parser as nq_parser
1920
from .named_procedure import create_parser as np_parser
2021
from .named_procedure_graph import create_parser as npg_parser
22+
from .named_migration import create_parser as nm_parser
2123
from .named_connection import create_parser as nc_parser
2224
info_parser(subparsers)
2325
query_parser(subparsers)
@@ -26,6 +28,7 @@ def register_commands(subparsers):
2628
nq_parser(subparsers)
2729
np_parser(subparsers)
2830
npg_parser(subparsers)
31+
nm_parser(subparsers)
2932
nc_parser(subparsers)
3033

3134

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# src/rhosocial/activerecord/backend/impl/postgres/cli/named_migration.py
2+
"""named-migration subcommand — PostgreSQL adapter for the shared CLI helper."""
3+
4+
from __future__ import annotations
5+
6+
from rhosocial.activerecord.backend.impl.postgres import PostgresBackend, AsyncPostgresBackend
7+
8+
from .connection import create_connection_parent_parser, resolve_connection_config_from_args
9+
from .output import create_provider
10+
11+
12+
def create_parser(subparsers):
13+
"""Create the named-migration subcommand parser.
14+
15+
Provides connection and output arguments via a parent parser.
16+
"""
17+
from rhosocial.activerecord.backend.migration.cli import create_named_migration_parser
18+
19+
local_parent = create_connection_parent_parser()
20+
nm_epilog = """Examples:
21+
# Apply a migration
22+
%(prog)s myapp.migrations.v001.CreateUsersTable --host localhost --database mydb --direction up
23+
24+
# Rollback
25+
%(prog)s myapp.migrations.v001.CreateUsersTable --host localhost --database mydb --direction down
26+
27+
# Apply with record store
28+
%(prog)s myapp.migrations.v001.CreateUsersTable --database mydb --record-store ./migrations.json
29+
30+
# Dry-run
31+
%(prog)s myapp.migrations.v001.CreateUsersTable --database mydb --direction up --dry-run
32+
33+
# Apply asynchronously
34+
%(prog)s myapp.migrations.v001.CreateUsersTable --database mydb --direction up --async
35+
36+
# Pass parameters to the migration
37+
%(prog)s myapp.migrations.v001.CreateUsersTable --param table_name=custom_table
38+
39+
# List available migrations
40+
%(prog)s myapp.migrations --list
41+
42+
# Describe a migration
43+
%(prog)s myapp.migrations.v001.CreateUsersTable --describe
44+
45+
# Batch: apply all pending migrations in a module
46+
%(prog)s myapp.migrations --all --record-store ./migrations.json
47+
48+
# Batch: dry-run all pending migrations
49+
%(prog)s myapp.migrations --all --record-store ./migrations.json --dry-run
50+
51+
# Batch: rollback all applied migrations
52+
%(prog)s myapp.migrations --all --record-store ./migrations.json --direction down
53+
54+
# Batch: wrap all migrations in a single transaction
55+
%(prog)s myapp.migrations --all --record-store ./migrations.json --single-transaction
56+
57+
End-to-end demo scripts with UP/DOWN round-trip verification:
58+
https://github.com/rhosocial/python-activerecord-postgres/tree/main/src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations
59+
PYTHONPATH=src bash demo_basic.sh
60+
PYTHONPATH=src bash demo_chain.sh
61+
PYTHONPATH=src bash demo_all.sh
62+
"""
63+
return create_named_migration_parser(subparsers, local_parent, epilog=nm_epilog)
64+
65+
66+
def handle(args):
67+
"""Handle the named-migration subcommand."""
68+
from rhosocial.activerecord.backend.migration.cli import handle_named_migration as handle_nm
69+
70+
provider = create_provider(args.output, ascii_borders=args.rich_ascii)
71+
72+
backend = None
73+
74+
def backend_factory():
75+
nonlocal backend
76+
config = resolve_connection_config_from_args(args)
77+
backend = PostgresBackend(connection_config=config)
78+
backend.connect()
79+
backend.introspect_and_adapt()
80+
return backend
81+
82+
def disconnect():
83+
if backend and getattr(backend, "_connection", None):
84+
backend.disconnect()
85+
86+
is_async = getattr(args, "is_async", False)
87+
if is_async:
88+
async_backend = None
89+
90+
def backend_async_factory():
91+
nonlocal async_backend
92+
config = resolve_connection_config_from_args(args)
93+
async_backend = AsyncPostgresBackend(connection_config=config)
94+
return async_backend
95+
96+
async def disconnect_async():
97+
if async_backend and getattr(async_backend, "_connection", None):
98+
await async_backend.disconnect()
99+
100+
handle_nm(
101+
args,
102+
provider,
103+
backend_factory=backend_factory,
104+
disconnect=disconnect,
105+
backend_async_factory=backend_async_factory,
106+
disconnect_async=disconnect_async,
107+
)
108+
return
109+
110+
handle_nm(
111+
args,
112+
provider,
113+
backend_factory=backend_factory,
114+
disconnect=disconnect,
115+
)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/__init__.py
2+
"""Named migration examples for PostgreSQL backend."""
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
#!/usr/bin/env bash
2+
# ===========================================================================
3+
# demo_all.sh — batch migration (--all) for PostgreSQL
4+
#
5+
# Scenarios:
6+
# - --all runs all pending migrations in dependency order
7+
# - --all --dry-run preview without changes
8+
# - --all --direction down rolls back everything
9+
#
10+
# Usage:
11+
# cd python-activerecord-postgres
12+
# DEMO_VENV_PYTHON=.venv3.14-ubuntu26.04/bin/python \
13+
# PYTHONPATH=src \
14+
# bash src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/demo_all.sh
15+
# ===========================================================================
16+
set -euo pipefail
17+
18+
if [ -d "./src" ]; then
19+
export PYTHONPATH="${PYTHONPATH:-}:$(pwd)/src"
20+
fi
21+
22+
MODULE="rhosocial.activerecord.backend.impl.postgres.examples.named_migrations"
23+
STORE="./demo_pg_all_mig.json"
24+
VENV_PYTHON="${DEMO_VENV_PYTHON:-python3}"
25+
PYTHON="$VENV_PYTHON -m rhosocial.activerecord.backend.impl.postgres"
26+
27+
rm -f "$STORE"
28+
echo "=== Batch Migration (--all) for PostgreSQL ==="
29+
echo
30+
31+
echo "[1] --all without --record-store (should error):"
32+
$PYTHON named-migration "${MODULE}.migrations" --all --host localhost --database test 2>&1 || true
33+
echo
34+
35+
echo "[2] --all --dry-run (preview SQL, no changes):"
36+
$PYTHON named-migration "${MODULE}.migrations" --all --host localhost --database test --dry-run --record-store "$STORE"
37+
echo
38+
39+
echo "[3] --all apply all pending migrations:"
40+
$PYTHON named-migration "${MODULE}.migrations" --all --host localhost --database test --record-store "$STORE"
41+
echo
42+
43+
echo "[4] Re-run --all (all applied, should be no-op):"
44+
$PYTHON named-migration "${MODULE}.migrations" --all --host localhost --database test --record-store "$STORE" 2>&1 || true
45+
echo
46+
47+
echo "[5] --all --single-transaction (rollback in one transaction):"
48+
$PYTHON named-migration "${MODULE}.migrations" --all --host localhost --database test \
49+
--direction down --record-store "$STORE" --single-transaction
50+
echo
51+
52+
echo "[6] --all --single-transaction (re-apply UP):"
53+
$PYTHON named-migration "${MODULE}.migrations" --all --host localhost --database test \
54+
--direction up --record-store "$STORE" --single-transaction
55+
echo
56+
57+
echo "[7] --all --direction down rollback everything:"
58+
$PYTHON named-migration "${MODULE}.migrations" --all --host localhost --database test \
59+
--direction down --record-store "$STORE"
60+
echo
61+
62+
echo "[8] Record store final state:"
63+
cat "$STORE"
64+
echo
65+
66+
rm -f "$STORE"
67+
echo "=== Batch Migration (--all) Complete for PostgreSQL ==="
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#!/usr/bin/env bash
2+
# ===========================================================================
3+
# demo_async.sh — async migration execution (--async) for PostgreSQL
4+
#
5+
# Scenarios:
6+
# - apply UP with --async
7+
# - dry-run with --async
8+
# - rollback DOWN with --async
9+
#
10+
# Requires: asyncpg
11+
#
12+
# Usage:
13+
# cd python-activerecord-postgres
14+
# PYTHONPATH=src \
15+
# bash src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/demo_async.sh
16+
# ===========================================================================
17+
set -euo pipefail
18+
19+
if [ -d "./src" ]; then
20+
export PYTHONPATH="${PYTHONPATH:-}:$(pwd)/src"
21+
fi
22+
23+
MODULE="rhosocial.activerecord.backend.impl.postgres.examples.named_migrations"
24+
FQN="${MODULE}.migrations.V001CreateUsers"
25+
STORE="./demo_pg_async_mig.json"
26+
VENV_PYTHON="${DEMO_VENV_PYTHON:-python3}"
27+
PYTHON="$VENV_PYTHON -m rhosocial.activerecord.backend.impl.postgres"
28+
29+
rm -f "$STORE"
30+
echo "=== Async Migration (--async) for PostgreSQL ==="
31+
echo
32+
33+
echo "[1] Async dry-run (preview SQL, no changes):"
34+
$PYTHON named-migration "$FQN" --host localhost --database test \
35+
--direction up --dry-run --async
36+
echo
37+
38+
echo "[2] Async apply UP:"
39+
$PYTHON named-migration "$FQN" --host localhost --database test \
40+
--direction up --async --record-store "$STORE"
41+
echo
42+
43+
echo "[3] Async rollback DOWN:"
44+
$PYTHON named-migration "$FQN" --host localhost --database test \
45+
--direction down --async --record-store "$STORE"
46+
echo
47+
48+
rm -f "$STORE"
49+
echo "=== Async Migration Demo Complete for PostgreSQL ==="
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
#!/usr/bin/env bash
2+
# ===========================================================================
3+
# demo_basic.sh — single migration basic operations (PostgreSQL)
4+
#
5+
# Scenarios:
6+
# - apply / rollback a single migration
7+
# - dry-run preview
8+
# - duplicate execution protection
9+
#
10+
# Usage:
11+
# cd python-activerecord-postgres
12+
# DEMO_VENV_PYTHON=.venv3.14-ubuntu26.04/bin/python \
13+
# PYTHONPATH=src \
14+
# bash src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/demo_basic.sh
15+
# ===========================================================================
16+
set -euo pipefail
17+
18+
if [ -d "./src" ]; then
19+
export PYTHONPATH="${PYTHONPATH:-}:$(pwd)/src"
20+
fi
21+
22+
MODULE="rhosocial.activerecord.backend.impl.postgres.examples.named_migrations"
23+
FQN="${MODULE}.migrations.V001CreateUsers"
24+
STORE="./demo_pg_basic_mig.json"
25+
VENV_PYTHON="${DEMO_VENV_PYTHON:-python3}"
26+
PYTHON="$VENV_PYTHON -m rhosocial.activerecord.backend.impl.postgres"
27+
28+
rm -f "$STORE"
29+
echo "=== Single Migration Basic Operations (PostgreSQL) ==="
30+
echo
31+
32+
echo "[1] List all migrations in the module:"
33+
$PYTHON named-migration "${MODULE}.migrations" --list -o table
34+
echo
35+
36+
echo "[2] Describe V001CreateUsers (--describe):"
37+
$PYTHON named-migration "$FQN" --describe
38+
echo
39+
40+
echo "[3] Dry-run preview (no actual changes):"
41+
$PYTHON named-migration "$FQN" --host localhost --database test --direction up --dry-run
42+
echo
43+
44+
echo "[4] Apply UP (create users table):"
45+
$PYTHON named-migration "$FQN" --host localhost --database test --direction up --record-store "$STORE"
46+
echo
47+
48+
echo "[5] Record store contents:"
49+
cat "$STORE"
50+
echo
51+
52+
echo
53+
echo "[6] Duplicate UP (should be rejected):"
54+
$PYTHON named-migration "$FQN" --host localhost --database test --direction up --record-store "$STORE" 2>&1 || true
55+
echo
56+
57+
echo "[7] Apply DOWN (drop users table):"
58+
$PYTHON named-migration "$FQN" --host localhost --database test --direction down --record-store "$STORE"
59+
echo
60+
61+
echo "[8] Record store after rollback:"
62+
cat "$STORE"
63+
echo
64+
65+
rm -f "$STORE"
66+
echo "=== Single Migration Basic Operations Complete (PostgreSQL) ==="

0 commit comments

Comments
 (0)