Coverage for app / backend / src / couchers / migrations / env.py: 44%
53 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-19 14:14 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-19 14:14 +0000
1import re
2from logging.config import fileConfig
3from pathlib import Path
4from typing import Any
6from alembic import context
7from alembic.config import Config
8from sqlalchemy import engine_from_config, pool
9from sqlalchemy.schema import MetaData
11from couchers import models
12from couchers.config import config as couchers_config
14# this is the Alembic Config object, which provides
15# access to the values within the .ini file in use.
16config: Config = context.config
18config.set_main_option("sqlalchemy.url", couchers_config["DATABASE_CONNECTION_STRING"])
21# Interpret the config file for Python logging.
22# This line sets up loggers basically.
23if config.get_main_option("dont_mess_up_logging", "dont_care") == "dont_care": 23 ↛ 24line 23 didn't jump to line 24 because the condition on line 23 was never true
24 if not config.config_file_name:
25 raise RuntimeError(config.config_file_name)
26 fileConfig(config.config_file_name)
28# add your model's MetaData object here
29# for 'autogenerate' support
30# from myapp import mymodel
31# target_metadata = mymodel.Base.metadata
32target_metadata: MetaData = models.Base.metadata
34# other values from the config, defined by the needs of env.py,
35# can be acquired:
36# my_important_option = config.get_main_option("my_important_option")
37# ... etc.
40exclude_tables = config.get_section("alembic:exclude", {}).get("tables", "").split(",")
42VERSIONS_DIR = Path(__file__).parent / "versions"
45def _next_ordinal() -> str:
46 """Compute the next ordinal migration number (e.g. "0134") by scanning existing filenames."""
47 max_ordinal = 0
48 for path in VERSIONS_DIR.glob("*.py"):
49 match = re.match(r"^(\d+)_", path.name)
50 if match:
51 max_ordinal = max(max_ordinal, int(match.group(1)))
52 return f"{max_ordinal + 1:04d}"
55def process_revision_directives(context: Any, revision: Any, directives: Any) -> None:
56 """Set the revision ID to the next ordinal number instead of a random hex string."""
57 if directives:
58 directives[0].rev_id = _next_ordinal()
61def include_name(name: str | None, type_: str, parent_names: Any) -> bool:
62 if type_ == "schema":
63 return name in [None, "logging"]
64 if type_ == "table":
65 return name not in exclude_tables
66 return True
69def include_object(obj: Any, name: str | None, type_: str, reflected: bool, compare_to: Any) -> bool:
70 """
71 Filter objects during autogenerate comparison.
73 Unlike include_name (which only filters database reflection), this hook
74 filters BOTH database objects AND model metadata objects, which is needed
75 to properly ignore GeoAlchemy2's auto-generated spatial indexes.
76 """
77 # Filter out GeoAlchemy2 auto-generated spatial indexes (from both DB and metadata)
78 # These indexes are named like: idx_<table>_<column> and use GIST
79 if type_ == "index" and name is not None and name.startswith("idx_") and name.endswith("_geom"):
80 return False
81 return True
84def run_migrations_offline() -> None:
85 """Run migrations in 'offline' mode.
87 This configures the context with just a URL
88 and not an Engine, though an Engine is acceptable
89 here as well. By skipping the Engine creation
90 we don't even need a DBAPI to be available.
92 Calls to context.execute() here emit the given string to the
93 script output.
95 """
96 url = config.get_main_option("sqlalchemy.url")
97 context.configure(
98 url=url,
99 target_metadata=target_metadata,
100 literal_binds=True,
101 dialect_opts={"paramstyle": "named"},
102 include_schemas=True,
103 include_name=include_name,
104 include_object=include_object,
105 compare_type=True,
106 process_revision_directives=process_revision_directives,
107 )
109 with context.begin_transaction():
110 context.run_migrations()
113def run_migrations_online() -> None:
114 """Run migrations in 'online' mode.
116 In this scenario we need to create an Engine
117 and associate a connection with the context.
119 """
120 connectable = engine_from_config(
121 config.get_section(config.config_ini_section, {}),
122 prefix="sqlalchemy.",
123 poolclass=pool.NullPool,
124 )
126 with connectable.connect() as connection:
127 context.configure(
128 connection=connection,
129 target_metadata=target_metadata,
130 include_schemas=True,
131 include_name=include_name,
132 include_object=include_object,
133 process_revision_directives=process_revision_directives,
134 )
136 with context.begin_transaction():
137 context.run_migrations()
140if context.is_offline_mode(): 140 ↛ 141line 140 didn't jump to line 141 because the condition on line 140 was never true
141 run_migrations_offline()
142else:
143 run_migrations_online()