|
| 1 | +# **Alembic Env** |
| 2 | + |
| 3 | +In the generated migration template, EllarSQL adopts an async-first approach for handling migration file generation. |
| 4 | +This approach simplifies the execution of migrations for both `Session`, `Engine`, `AsyncSession`, and `AsyncEngine`, |
| 5 | +but it also introduces a certain level of complexity. |
| 6 | + |
| 7 | +```python |
| 8 | +from logging.config import fileConfig |
| 9 | + |
| 10 | +from alembic import context |
| 11 | +from ellar.app import current_injector |
| 12 | +from ellar.threading import execute_coroutine_with_sync_worker |
| 13 | + |
| 14 | +from ellar_sql.migrations import SingleDatabaseAlembicEnvMigration |
| 15 | +from ellar_sql.services import EllarSQLService |
| 16 | + |
| 17 | +# this is the Alembic Config object, which provides |
| 18 | +# access to the values within the .ini file in use. |
| 19 | +config = context.config |
| 20 | + |
| 21 | +# Interpret the config file for Python logging. |
| 22 | +# This line sets up loggers basically. |
| 23 | +fileConfig(config.config_file_name) # type:ignore[arg-type] |
| 24 | + |
| 25 | +# logger = logging.getLogger("alembic.env") |
| 26 | +# other values from the config, defined by the needs of env.py, |
| 27 | +# can be acquired: |
| 28 | +# my_important_option = config.get_main_option("my_important_option") |
| 29 | +# ... etc. |
| 30 | + |
| 31 | + |
| 32 | +async def main() -> None: |
| 33 | + db_service: EllarSQLService = current_injector.get(EllarSQLService) |
| 34 | + |
| 35 | + # initialize migration class |
| 36 | + alembic_env_migration = SingleDatabaseAlembicEnvMigration(db_service) |
| 37 | + |
| 38 | + if context.is_offline_mode(): |
| 39 | + alembic_env_migration.run_migrations_offline(context) # type:ignore[arg-type] |
| 40 | + else: |
| 41 | + await alembic_env_migration.run_migrations_online(context) # type:ignore[arg-type] |
| 42 | + |
| 43 | + |
| 44 | +execute_coroutine_with_sync_worker(main()) |
| 45 | +``` |
| 46 | + |
| 47 | +The EllarSQL migration package provides two main migration classes: |
| 48 | + |
| 49 | +- **SingleDatabaseAlembicEnvMigration**: Manages migrations for a **single** database configuration, catering to both `Engine` and `AsyncEngine`. |
| 50 | +- **MultipleDatabaseAlembicEnvMigration**: Manages migrations for **multiple** database configurations, covering both `Engine` and `AsyncEngine`. |
| 51 | + |
| 52 | +## **Customizing the Env file** |
| 53 | + |
| 54 | +To customize or edit the Env file, it is recommended to inherit from either `SingleDatabaseAlembicEnvMigration` or `MultipleDatabaseAlembicEnvMigration` based on your specific configuration. Make the necessary changes within the inherited class. |
| 55 | + |
| 56 | +If you prefer to write something from scratch, then the abstract class `AlembicEnvMigrationBase` is the starting point. This class includes three abstract methods and expects a `EllarSQLService` during initialization, as demonstrated below: |
| 57 | + |
| 58 | +```python |
| 59 | +class AlembicEnvMigrationBase: |
| 60 | + def __init__(self, db_service: EllarSQLService) -> None: |
| 61 | + self.db_service = db_service |
| 62 | + self.use_two_phase = db_service.migration_options.use_two_phase |
| 63 | + |
| 64 | + @abstractmethod |
| 65 | + def default_process_revision_directives( |
| 66 | + self, |
| 67 | + context: "MigrationContext", |
| 68 | + revision: RevisionArgs, |
| 69 | + directives: t.List["MigrationScript"], |
| 70 | + ) -> t.Any: |
| 71 | + pass |
| 72 | + |
| 73 | + @abstractmethod |
| 74 | + def run_migrations_offline(self, context: "EnvironmentContext") -> None: |
| 75 | + pass |
| 76 | + |
| 77 | + @abstractmethod |
| 78 | + async def run_migrations_online(self, context: "EnvironmentContext") -> None: |
| 79 | + pass |
| 80 | +``` |
| 81 | + |
| 82 | +The `run_migrations_online` and `run_migrations_offline` are all similar to the same function from Alembic env.py template. |
| 83 | +The `default_process_revision_directives` is a callback is used to prevent an auto-migration from being generated |
| 84 | +when there are no changes to the schema described in details [here](http://alembic.zzzcomputing.com/en/latest/cookbook.html) |
| 85 | + |
| 86 | +### Example |
| 87 | +```python |
| 88 | +import logging |
| 89 | +from logging.config import fileConfig |
| 90 | + |
| 91 | +from alembic import context |
| 92 | +from ellar_sql.migrations import AlembicEnvMigrationBase |
| 93 | +from ellar_sql.model.database_binds import get_metadata |
| 94 | +from ellar.app import current_injector |
| 95 | +from ellar.threading import execute_coroutine_with_sync_worker |
| 96 | +from ellar_sql.services import EllarSQLService |
| 97 | + |
| 98 | +# This is the Alembic Config object, which provides |
| 99 | +# access to the values within the .ini file in use. |
| 100 | +config = context.config |
| 101 | +logger = logging.getLogger("alembic.env") |
| 102 | +# Interpret the config file for Python logging. |
| 103 | +# This line sets up loggers essentially. |
| 104 | +fileConfig(config.config_file_name) # type:ignore[arg-type] |
| 105 | + |
| 106 | +class MyCustomMigrationEnv(AlembicEnvMigrationBase): |
| 107 | + def default_process_revision_directives( |
| 108 | + self, |
| 109 | + context, |
| 110 | + revision, |
| 111 | + directives, |
| 112 | + ) -> None: |
| 113 | + if getattr(context.config.cmd_opts, "autogenerate", False): |
| 114 | + script = directives[0] |
| 115 | + if script.upgrade_ops.is_empty(): |
| 116 | + directives[:] = [] |
| 117 | + logger.info("No changes in schema detected.") |
| 118 | + |
| 119 | + def run_migrations_offline(self, context: "EnvironmentContext") -> None: |
| 120 | + """Run migrations in 'offline' mode. |
| 121 | +
|
| 122 | + This configures the context with just a URL |
| 123 | + and not an Engine, though an Engine is acceptable |
| 124 | + here as well. By skipping the Engine creation |
| 125 | + we don't even need a DBAPI to be available. |
| 126 | +
|
| 127 | + Calls to context.execute() here emit the given string to the |
| 128 | + script output. |
| 129 | +
|
| 130 | + """ |
| 131 | + pass |
| 132 | + |
| 133 | + async def run_migrations_online(self, context: "EnvironmentContext") -> None: |
| 134 | + """Run migrations in 'online' mode. |
| 135 | +
|
| 136 | + In this scenario, we need to create an Engine |
| 137 | + and associate a connection with the context. |
| 138 | +
|
| 139 | + """ |
| 140 | + key, engine = self.db_service.engines.popitem() |
| 141 | + metadata = get_metadata(key, certain=True).metadata |
| 142 | + |
| 143 | + conf_args = {} |
| 144 | + conf_args.setdefault( |
| 145 | + "process_revision_directives", self.default_process_revision_directives |
| 146 | + ) |
| 147 | + |
| 148 | + with engine.connect() as connection: |
| 149 | + context.configure( |
| 150 | + connection=connection, |
| 151 | + target_metadata=metadata, |
| 152 | + **conf_args |
| 153 | + ) |
| 154 | + |
| 155 | + with context.begin_transaction(): |
| 156 | + context.run_migrations() |
| 157 | + |
| 158 | +async def main() -> None: |
| 159 | + db_service: EllarSQLService = current_injector.get(EllarSQLService) |
| 160 | + |
| 161 | + # initialize migration class |
| 162 | + alembic_env_migration = MyCustomMigrationEnv(db_service) |
| 163 | + |
| 164 | + if context.is_offline_mode(): |
| 165 | + alembic_env_migration.run_migrations_offline(context) |
| 166 | + else: |
| 167 | + await alembic_env_migration.run_migrations_online(context) |
| 168 | + |
| 169 | +execute_coroutine_with_sync_worker(main()) |
| 170 | +``` |
| 171 | + |
| 172 | +This migration environment class, `MyCustomMigrationEnv`, inherits from `AlembicEnvMigrationBase` |
| 173 | +and provides the necessary methods for offline and online migrations. |
| 174 | +It utilizes the `EllarSQLService` to obtain the database engines and metadata for the migration process. |
| 175 | +The `main` function initializes and executes the migration class, with specific handling for offline and online modes. |
0 commit comments