Release v0.1.8
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, replace
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
@@ -41,6 +41,9 @@ CORE_SCOPE_TABLE = "core_scopes"
|
||||
PRE_MIGRATION_TASK_PHASES = ("pre_migration_check", "pre_migration_prepare")
|
||||
POST_MIGRATION_TASK_PHASES = ("post_migration_backfill", "post_migration_verify")
|
||||
MIGRATION_TASK_PHASES = (*PRE_MIGRATION_TASK_PHASES, *POST_MIGRATION_TASK_PHASES)
|
||||
MIGRATION_TRACK_RELEASE = "release"
|
||||
MIGRATION_TRACK_DEV = "dev"
|
||||
MIGRATION_TRACKS = (MIGRATION_TRACK_RELEASE, MIGRATION_TRACK_DEV)
|
||||
|
||||
_NAMESPACE_TABLE_RENAMES = (
|
||||
("tenants", "tenancy_tenants"),
|
||||
@@ -179,12 +182,14 @@ def registered_module_migration_plan(
|
||||
*,
|
||||
enabled_modules: tuple[str, ...] | list[str] | None = None,
|
||||
manifest_factories: tuple[ManifestFactory, ...] = (),
|
||||
migration_track: str | None = None,
|
||||
) -> MigrationMetadataPlan:
|
||||
return migration_metadata_plan(_registered_module_registry(
|
||||
plan = migration_metadata_plan(_registered_module_registry(
|
||||
database_url=database_url,
|
||||
enabled_modules=enabled_modules,
|
||||
manifest_factories=manifest_factories,
|
||||
))
|
||||
return _migration_plan_for_track(plan, _normalize_migration_track(migration_track))
|
||||
|
||||
|
||||
def _registered_module_registry(
|
||||
@@ -357,13 +362,76 @@ def _repo_root() -> Path:
|
||||
return packaged_root
|
||||
|
||||
|
||||
def _normalize_migration_track(value: str | None = None) -> str:
|
||||
raw_value = value if value is not None else settings.migration_track
|
||||
track = str(raw_value or MIGRATION_TRACK_RELEASE).strip().lower()
|
||||
aliases = {
|
||||
"baseline": MIGRATION_TRACK_RELEASE,
|
||||
"prod": MIGRATION_TRACK_RELEASE,
|
||||
"production": MIGRATION_TRACK_RELEASE,
|
||||
"releases": MIGRATION_TRACK_RELEASE,
|
||||
"detailed": MIGRATION_TRACK_DEV,
|
||||
"development": MIGRATION_TRACK_DEV,
|
||||
}
|
||||
track = aliases.get(track, track)
|
||||
if track not in MIGRATION_TRACKS:
|
||||
raise ValueError(
|
||||
"Unsupported migration track "
|
||||
f"{raw_value!r}; expected one of: {', '.join(MIGRATION_TRACKS)}"
|
||||
)
|
||||
return track
|
||||
|
||||
|
||||
def _core_version_location(root: Path, migration_track: str) -> Path:
|
||||
if migration_track == MIGRATION_TRACK_DEV:
|
||||
dev_versions = root / "alembic" / "dev_versions"
|
||||
if dev_versions.is_dir():
|
||||
return dev_versions
|
||||
return root / "alembic" / "versions"
|
||||
|
||||
|
||||
def _script_location_for_track(location: str | None, migration_track: str) -> str | None:
|
||||
if not location or migration_track != MIGRATION_TRACK_DEV:
|
||||
return location
|
||||
path = Path(location)
|
||||
if path.name != "versions":
|
||||
return location
|
||||
dev_location = path.with_name("dev_versions")
|
||||
if dev_location.is_dir():
|
||||
return str(dev_location)
|
||||
return location
|
||||
|
||||
|
||||
def _migration_plan_for_track(plan: MigrationMetadataPlan, migration_track: str) -> MigrationMetadataPlan:
|
||||
if migration_track == MIGRATION_TRACK_RELEASE:
|
||||
return plan
|
||||
script_locations = tuple(
|
||||
location
|
||||
for location in (
|
||||
_script_location_for_track(location, migration_track)
|
||||
for location in plan.script_locations
|
||||
)
|
||||
if location
|
||||
)
|
||||
modules = tuple(
|
||||
replace(
|
||||
item,
|
||||
script_location=_script_location_for_track(item.script_location, migration_track),
|
||||
)
|
||||
for item in plan.modules
|
||||
)
|
||||
return replace(plan, script_locations=script_locations, modules=modules)
|
||||
|
||||
|
||||
def alembic_config(
|
||||
*,
|
||||
database_url: str | None = None,
|
||||
enabled_modules: tuple[str, ...] | list[str] | None = None,
|
||||
manifest_factories: tuple[ManifestFactory, ...] = (),
|
||||
migration_track: str | None = None,
|
||||
) -> Config:
|
||||
root = _repo_root()
|
||||
active_migration_track = _normalize_migration_track(migration_track)
|
||||
config = Config(str(root / "alembic.ini"))
|
||||
config.set_main_option("script_location", str(root / "alembic"))
|
||||
|
||||
@@ -372,13 +440,15 @@ def alembic_config(
|
||||
active_database_url,
|
||||
enabled_modules=enabled_modules,
|
||||
manifest_factories=manifest_factories,
|
||||
migration_track=active_migration_track,
|
||||
)
|
||||
version_locations = [str(root / "alembic" / "versions")]
|
||||
version_locations = [str(_core_version_location(root, active_migration_track))]
|
||||
version_locations.extend(location for location in plan.script_locations if location)
|
||||
config.set_main_option("version_locations", os.pathsep.join(dict.fromkeys(version_locations)))
|
||||
config.set_main_option("version_path_separator", "os")
|
||||
config.set_main_option("path_separator", "os")
|
||||
|
||||
config.attributes["database_url"] = active_database_url
|
||||
config.attributes["migration_track"] = active_migration_track
|
||||
if enabled_modules is not None:
|
||||
config.attributes["enabled_modules"] = tuple(enabled_modules)
|
||||
if manifest_factories:
|
||||
@@ -602,7 +672,67 @@ def reconcile_change_sequence_retention_floor_drift(database_url: str | None = N
|
||||
return changed
|
||||
|
||||
|
||||
def reconcile_legacy_create_all_schema(database_url: str | None = None) -> str | None:
|
||||
def reconcile_covered_alembic_dependency_heads(
|
||||
database_url: str | None = None,
|
||||
*,
|
||||
config: Config | None = None,
|
||||
) -> tuple[str, ...]:
|
||||
"""Remove stale parent/dependency rows from ``alembic_version``.
|
||||
|
||||
Alembic's version table represents current graph heads, not every owner
|
||||
head that contributed to the schema. After the v0.1.7 baseline squash,
|
||||
development databases may still carry dependency parents such as the core
|
||||
baseline beside child module heads. That shape is semantically already
|
||||
upgraded, but Alembic rejects it as an overlapping current revision set.
|
||||
"""
|
||||
|
||||
url = database_url or settings.database_url
|
||||
active_config = config or alembic_config(database_url=url)
|
||||
script = ScriptDirectory.from_config(active_config)
|
||||
engine = create_engine(url)
|
||||
removed: set[str] = set()
|
||||
try:
|
||||
with engine.begin() as connection:
|
||||
tables = set(inspect(connection).get_table_names())
|
||||
if "alembic_version" not in tables:
|
||||
return ()
|
||||
current = {
|
||||
str(row[0])
|
||||
for row in connection.execute(text("SELECT version_num FROM alembic_version")).all()
|
||||
if row[0]
|
||||
}
|
||||
if len(current) < 2:
|
||||
return ()
|
||||
covered: set[str] = set()
|
||||
for revision_id in current:
|
||||
try:
|
||||
revision = script.get_revision(revision_id)
|
||||
except Exception:
|
||||
continue
|
||||
ancestors = {
|
||||
item.revision
|
||||
for item in script.revision_map._get_ancestor_nodes( # noqa: SLF001 - Alembic has no public dependency-ancestor API.
|
||||
[revision],
|
||||
include_dependencies=True,
|
||||
)
|
||||
}
|
||||
covered.update(ancestors - {revision_id})
|
||||
removed = current.intersection(covered)
|
||||
for revision_id in sorted(removed):
|
||||
connection.execute(
|
||||
text("DELETE FROM alembic_version WHERE version_num = :revision"),
|
||||
{"revision": revision_id},
|
||||
)
|
||||
finally:
|
||||
engine.dispose()
|
||||
return tuple(sorted(removed))
|
||||
|
||||
|
||||
def reconcile_legacy_create_all_schema(
|
||||
database_url: str | None = None,
|
||||
*,
|
||||
migration_track: str | None = None,
|
||||
) -> str | None:
|
||||
"""Repair the known Alembic/create_all drift without modifying application data.
|
||||
|
||||
Returns the revision stamped during reconciliation, or ``None`` when no
|
||||
@@ -659,7 +789,7 @@ def reconcile_legacy_create_all_schema(database_url: str | None = None) -> str |
|
||||
if target is None:
|
||||
return None
|
||||
|
||||
command.stamp(alembic_config(database_url=url), target)
|
||||
command.stamp(alembic_config(database_url=url, migration_track=migration_track), target)
|
||||
return target
|
||||
|
||||
|
||||
@@ -670,23 +800,32 @@ def migrate_database(
|
||||
enabled_modules: tuple[str, ...] | list[str] | None = None,
|
||||
migration_module_order: tuple[str, ...] | list[str] | None = None,
|
||||
manifest_factories: tuple[ManifestFactory, ...] = (),
|
||||
migration_track: str | None = None,
|
||||
) -> MigrationResult:
|
||||
url = database_url or settings.database_url
|
||||
active_migration_track = _normalize_migration_track(migration_track)
|
||||
if reconcile_legacy_schema:
|
||||
reconcile_namespace_table_drift(url)
|
||||
reconcile_change_sequence_retention_floor_drift(url)
|
||||
previous = database_revision(url)
|
||||
reconciled = reconcile_legacy_create_all_schema(url) if reconcile_legacy_schema else None
|
||||
reconciled = (
|
||||
reconcile_legacy_create_all_schema(url, migration_track=active_migration_track)
|
||||
if reconcile_legacy_schema
|
||||
else None
|
||||
)
|
||||
config = alembic_config(
|
||||
database_url=url,
|
||||
enabled_modules=enabled_modules,
|
||||
manifest_factories=manifest_factories,
|
||||
migration_track=active_migration_track,
|
||||
)
|
||||
reconcile_covered_alembic_dependency_heads(url, config=config)
|
||||
if migration_module_order:
|
||||
plan = registered_module_migration_plan(
|
||||
url,
|
||||
enabled_modules=enabled_modules,
|
||||
manifest_factories=manifest_factories,
|
||||
migration_track=active_migration_track,
|
||||
)
|
||||
_upgrade_ordered_module_heads(config, plan, tuple(migration_module_order))
|
||||
command.upgrade(config, "heads")
|
||||
|
||||
Reference in New Issue
Block a user