Extend default operations with hooks #1750
Replies: 3 comments 3 replies
-
|
hi -
So to set up assumptions, I'm going to restate the use case as:
So if the use case is not exactly that, if something else is going on as well, that might impact some of these suggestions, but I'm going to go with that. Approach one (most boring) - just run a statement"extend alembic" is a way to go here but let's start with the most obvious approach which is to just write the commands in your migration script separately. Boring, but here we mean: I know this is not what we want but we should start with the above as what we'd do if we had five minutes to get data into our special table. Approach two - autogenerate running the statement (still very boring)The above (as boring as it is) can also be automated. Assuming autogenerate is in use, you can use a rewriter where you iterate through the migration structure, locate all the Approach three - augment CREATE TABLE with execution eventsThe next way to do this which requires no special alembic tinkering is to use an execution event to run the INSERT after each CREATE TABLE. You can put this right into your env.py to intercept all def run_migrations_online() -> None:
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
connectable = engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
from sqlalchemy.schema import CreateTable
from sqlalchemy import event, text
@event.listens_for(connectable, "after_execute")
def receive_after_execute(
conn, clauseelement, multiparams, params, execution_options, result
):
if isinstance(clauseelement, CreateTable):
the_table = clauseelement.element
conn.execute(
text(
"INSERT INTO my_special_table (table_name) values (:table_name)"
),
{"table_name": the_table.name},
)
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata,
#
process_revision_directives=process_revision_directives,
)
with context.begin_transaction():
context.run_migrations()Approach four - same thing but use DDL.after_create (this is pretty much @post_hook. this is the most respectable way to go here for solving the specific problem of operation-on-table-create. but not generalizable to other kinds of ops for other scenarios)There's an even better event than the above which is DDLEvent.after_create - alembic makes sure this is called correctly so you could use this too: from sqlalchemy.schema import CreateTable
from sqlalchemy import event, text
@event.listens_for(Table, "after_create")
def receive_after_create(target, connection, **kw):
the_table = target
connection.execute(
text(
"INSERT INTO my_special_table (table_name) values (:table_name)"
),
{"table_name": the_table.name},
)This is basically the same as the Approach five - use a new implementation for CreateTableOp (exciting!)OK let's look at this one. I'm not sure if you saw we have a public API for these implementations, but it's public. so you could write this (CAVEAT: almost): from alembic.operations.toimpl import create_table as _create_table_impl
from alembic.operations import Operations
from alembic.operations.ops import CreateTableOp
@Operations.implementation_for(CreateTableOp)
def create_table(operations, operation):
_create_table_impl(operations, operation)
operations.execute(
text(
"INSERT INTO my_special_table (table_name) values (:table_name)"
),
{"table_name": the_table.name},
)that is, there's What's holding us back there? This pesky assert: alembic/alembic/util/langhelpers.py Line 279 in dcba644 which of course you can do your private API thing to work around. But IMO it should be possible to provide an alternate impl for CreateTableOp. In the interests of exciting APIs I would accept a PR with tests for this new feature: diff --git a/alembic/operations/base.py b/alembic/operations/base.py
index 26c3272..3081393 100644
--- a/alembic/operations/base.py
+++ b/alembic/operations/base.py
@@ -202,7 +202,7 @@ class AbstractOperations(util.ModuleClsProxy):
return register
@classmethod
- def implementation_for(cls, op_cls: Any) -> Callable[[_C], _C]:
+ def implementation_for(cls, op_cls: Any, replace: bool=False) -> Callable[[_C], _C]:
"""Register an implementation for a given :class:`.MigrateOperation`.
This is part of the operation extensibility API.
@@ -214,7 +214,7 @@ class AbstractOperations(util.ModuleClsProxy):
"""
def decorate(fn: _C) -> _C:
- cls._to_impl.dispatch_for(op_cls)(fn)
+ cls._to_impl.dispatch_for(op_cls, replace=replace)(fn)
return fn
return decorate
diff --git a/alembic/util/langhelpers.py b/alembic/util/langhelpers.py
index 80d88cb..5408ae4 100644
--- a/alembic/util/langhelpers.py
+++ b/alembic/util/langhelpers.py
@@ -270,13 +270,14 @@ class Dispatcher:
self.uselist = uselist
def dispatch_for(
- self, target: Any, qualifier: str = "default"
+ self, target: Any, qualifier: str = "default", replace: bool =False
) -> Callable[[_C], _C]:
def decorate(fn: _C) -> _C:
if self.uselist:
self._registry.setdefault((target, qualifier), []).append(fn)
else:
- assert (target, qualifier) not in self._registry
+ if (target, qualifier) in self._registry and not replace:
+ raise ValueError("key already exists") # obviously something nicer than this
self._registry[(target, qualifier)] = fn
return fn
then you can write the code as: from alembic.operations.toimpl import create_table as _create_table_impl
from alembic.operations import Operations
from alembic.operations.ops import CreateTableOp
@Operations.implementation_for(CreateTableOp, replace=True)
def create_table(operations, operation):
_create_table_impl(operations, operation)
operations.execute("SELECT 'my_special_thing'")Overall if it were me I think intercepting |
Beta Was this translation helpful? Give feedback.
-
|
Thanks for suggesting simpler ways, I really appreciate that :) However, what you suggested as approach five seems to be the best way for me (as I need to be able to decide whether to run or not to run a default implementation), and I didn't consider i could import default implementations from I will look into writing tests for these changes and try to get something done. If there are any contribution guidelines (besides that of sqlalchemy), please point me to them. Also, should I open an issue even if I really try to explain everything inside pull request? Thanks in advance. |
Beta Was this translation helpful? Give feedback.
-
|
Don't worry about AIs, I don't like using them. And if you want further clarification on what in the world I am trying to do (it's not really related to anything, just in case you're curious): We have custom operations that manage groups, rights and encryption for tables and schemas (postgres-exclusive). To mark table as 'sensitive' or 'encrypted' we have a decorator that inserts some keys into Also, the idea with encrypted tables is that each encrypted table has an associated key table, and we want to "abstract away" these table's existence from our fellow developers (so that they don't have to think about them). To do that, it would be very nice to intercept Wondering "why make this all so difficult"? Mostly |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Our use case is the following: we need to store additional metadata for each created table inside a special table (similar to alembic_version in function) inside the database. This could be easily achieved if we could extend CreateTableOp with an additional action.
Currently, this is achievable by something like:
But this requires use of private members and does not look great.
I see 3 ways of implementing this on the library side:
AbstractOperations._to_impl = util.Dispatcher(uselist=True), so that@implementation_forcan be used several times, similar to@comparators.dispatch_for.@pre_hookand@post_hooktoAbstractOperationsthat accept a class; modifyAbstractOperations.invoketo account for hooks.@Operations.overwrite_impland document/add a way to access default implementation to allow custom implementation to call the default at any point (or not call it whatsoever).In my opinion, the most extensible way would be to allow complete overwrite of the operation, with an ability to call default implementation.
Use cases for this behaviour:
Beta Was this translation helpful? Give feedback.
All reactions