Files
rose-ash/shared/db/alembic_env.py
giles e65bd41ebe
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m19s
Decouple per-service Alembic migrations and fix cross-DB queries
Each service (blog, market, cart, events, federation, account) now owns
its own database schema with independent Alembic migrations. Removes the
monolithic shared/alembic/ that ran all migrations against a single DB.

- Add per-service alembic.ini, env.py, and 0001_initial.py migrations
- Add shared/db/alembic_env.py helper with table-name filtering
- Fix cross-DB FK in blog/models/snippet.py (users lives in db_account)
- Fix cart_impl.py cross-DB queries: fetch products and market_places
  via internal data endpoints instead of direct SQL joins
- Fix blog ghost_sync to fetch page_configs from cart via data endpoint
- Add products-by-ids and page-config-ensure data endpoints
- Update all entrypoint.sh to create own DB and run own migrations
- Cart now uses db_cart instead of db_market
- Add docker-compose.dev.yml, dev.sh for local development
- CI deploys both rose-ash swarm stack and rose-ash-dev compose stack
- Fix Quart namespace package crash (root_path in factory.py)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 12:07:24 +00:00

103 lines
3.4 KiB
Python

"""Reusable Alembic env.py helper for per-service migrations.
Each service calls ``run_alembic(config, model_modules, table_names)``
from its own ``alembic/env.py``. The helper:
* Imports only the requested model modules (so ``Base.metadata`` sees
only the tables that belong to the service).
* Uses an ``include_name`` callback to filter ``CREATE TABLE`` to only
the service's tables (belt-and-suspenders on top of the import filter).
* Reads ``ALEMBIC_DATABASE_URL`` for the connection string.
"""
from __future__ import annotations
import importlib
import os
import sys
from typing import Sequence
from alembic import context
from sqlalchemy import engine_from_config, pool
def run_alembic(
config,
model_modules: Sequence[str],
table_names: frozenset[str],
) -> None:
"""Run Alembic migrations filtered to *table_names*.
Parameters
----------
config:
The ``alembic.config.Config`` instance (``context.config``).
model_modules:
Dotted module paths to import so that ``Base.metadata`` is
populated (e.g. ``["shared.models.user", "blog.models"]``).
table_names:
The set of table names this service owns. Only these tables
will be created / altered / dropped.
"""
# Ensure project root is importable
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))
if project_root not in sys.path:
sys.path.insert(0, project_root)
# Import models so Base.metadata sees the tables
for mod in model_modules:
try:
importlib.import_module(mod)
except ImportError:
pass # OK in Docker images that don't ship sibling apps
from shared.db.base import Base
target_metadata = Base.metadata
# ---- include_name filter ------------------------------------------------
def _include_name(name, type_, parent_names):
if type_ == "table":
return name in table_names
# Always include indexes/constraints that belong to included tables
return True
# ---- connection URL -----------------------------------------------------
def _get_url() -> str:
return os.getenv(
"ALEMBIC_DATABASE_URL",
os.getenv("DATABASE_URL", config.get_main_option("sqlalchemy.url") or ""),
)
# ---- offline / online ---------------------------------------------------
if context.is_offline_mode():
context.configure(
url=_get_url(),
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
compare_type=True,
include_name=_include_name,
)
with context.begin_transaction():
context.run_migrations()
else:
url = _get_url()
if url:
config.set_main_option("sqlalchemy.url", url)
connectable = engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata,
compare_type=True,
include_name=_include_name,
)
with context.begin_transaction():
context.run_migrations()