-
-
Notifications
You must be signed in to change notification settings - Fork 245
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Autogenerate renders TypeDecorator instance instead of underlying impl type #1386
Comments
Hi
Well a type decorator can render a different type depending on the dialect, so this change if accepted would need to be made a config option. For example https://docs.sqlalchemy.org/en/20/core/custom_types.html#backend-agnostic-guid-type Also at the moment alembic support customization of what type is rendered, documented here: https://alembic.sqlalchemy.org/en/latest/autogenerate.html#affecting-the-rendering-of-types-themselves maybe an extension to that documentation to show an example function to render the impl of type decorator could be enough? |
we have a configuration path for this which is to add your application's standard type import paths to the
if you are refactoring your application to move where a particular symbol imports from, you necessarily have to change all the places that reference it, so I dont see why an alembic migration file is any different than any other file which refers to it.
there is, which is that the
there already is! make yourself a render_item callable that does this for all typedecorators, I will gladly accept an add for cookbook for this: def render_item(type_, obj, autogen_context):
"""Apply custom rendering for selected items."""
if type_ == 'type' and isinstance(obj, TypeDecorator):
return f"sa.{obj.impl!r}"
# default rendering for other objects
return False
def run_migrations_online():
# ...
context.configure(
connection=connection,
target_metadata=target_metadata,
render_item=render_item,
# ...
)
# ... |
Thanks @CaselIT and @zzzeek for the feedback! For some additional context on where I'm coming from, I've already implemented a monkeypatch that is applying a change similar to the one I've proposed that we've been using for a couple of years now, so that for most part developers get correctly generated migrations... but at the cost of someone maintaining ugly monkeypatch code in sync with alembic's changelog, so I was hoping to fold this into alembic source code if it would make sense. It looks like there are already cases where folks want to preserve their type decorators in the migration, so finding the right configuration instead of monkey patching / broadly making the change seems sensible to me. monkeypatch if you're curiousfrom alembic import context
from alembic.autogenerate.render import _repr_type
import sqlalchemy.types as types
migration_context = context.get_context()
old_render_type = migration_context.impl.render_type
def new_render_type(type_, autogen_context):
# If we are dealing with a custom TypeDecorator, run _repr_type on the underlying implementation.
if isinstance(type_, types.TypeDecorator) and type(type_).__module__.startswith("app."):
return _repr_type(type_.impl, autogen_context)
# Otherwise, do the default behavior.
return old_render_type(type_, autogen_context)
migration_context.impl.render_type = new_render_type But in the interest of properly communicating why this sort of recipe is useful, I'll respond to some of the things you both have mentioned 😇.
That's generally true... for our project, we've decided to stricly enfoce that
There are many ways to combat the above issues, which I concur are not universal issues, but we found this decoupling to work for us.
FWIW, where I put my proposed fix (#1387) happens after a dialect has attempted to render the type, so I think the mentioned use case still works - at least no test broke where I put my logic.
Good to know. In my monolith applicaiton, we have many places that a type decorator could live, so a standard set of imports isn't totally doable, and the overhead of custom code for adding dynamic imports sound like a heavier burden on the team, also not allowed by my teams' "
Nice! I'll try this out, and attempt to write up a cookbook recipe. |
re: alembic/alembic/autogenerate/render.py Lines 815 to 835 in abc8002
One way of implementing |
this is the thing with recipes. Think of real world use. You have an app, and you're using like two or three database-specific types, you know you're using postgresql. Realistically, I dont really think having the TypeDecorator render is really any kind of issue, I dont really feel "maintenance" or "bugs" are impacted very much at all by this rendering and certainly not "performance", database migrations can certainly afford an extra .3 seconds startup to import your application's utility module, but as stated, there's the so re: render_item's "automatic logic with imports", again, real world - your app is hardcoded to Postgresql, so OK, add "from sqlalchemy.dialects import postgresql" to your def render_item(type_, obj, autogen_context):
"""Apply custom rendering for selected items."""
if type_ == 'type' and isinstance(obj, MySpecialType):
# add import for this type
autogen_context.imports.add("from mymodel import types")
return "types.%r" % obj
# default rendering for other objects
return False I think overall the vibe I want to impart is that this is a simple thing Alembic does and it allows a hook to customize, we want to keep things simple, not overthink, not qualify all of Alembic's API with N^18 flags and switches, just imperative code where needed. |
Hello, I really think there is a possibility to ease there life even to the slightest. For instance, I took the original test case, and instead of expecting the TypeDecorator's impl to the rendering, which I understand may be complex when some type decorators may be dialect dependent, I changed the expectation to add the import automatically. else:
prefix = _user_autogenerate_prefix(autogen_context, type_)
+ autogen_context.imports.add(prefix.rstrip("."))
return "%s%r" % (prefix, type_) This single line of code may lighten 100s of repositories which would have to copy your above recipe. The good thing is that this How would you feel about this trade-off? |
we're talking about a single line of code in their
we can't assume that a user-defined type is importable based on what it says in the as mentioned above, we have a prominent documentation section which explains why Alembic works the way it does as well as exactly what someone needs to do when they are using non-SQLAlchemy types. SQLAlchemy and Alembic both subscribe to a philosophy of zero assumptions about a user environment and it's assumed that users will be reading documentation. |
Describe the bug
This isn't a bug per se, but a small improvement for autogenerate when using TypeDecorator.
When a TypeDecorator is used in a column definition, e.g.:
The auto-generated migration ends up referencing the TypeDecorator:
which is annoying for two reasons:
app
, e.g. if the app/models/foo.py is refactored, we may need to update this migration file... when that could have been avoided if instead of renderingapp.models.foo.JSONBData
, alembic directly rendered the underlying impl:postgresql.JSONB
.I'm not sure if there are any scenarios where it is actually preferable to have the TypeDecorator in the autogenerated file. If there are use cases for it, would it be sensible to make this a config option instead of unconditional?
Expected behavior
Ideally, we'd generate the same thing as when
foo = Table("foo", MetaData(), Column("data", JSONB))
is provided, i.e.:To Reproduce
Test case:
Error
When running
tox -e py-sqlalchemy -- tests/test_autogen_render.py
with the above patch:Versions.
Have a nice day!
The text was updated successfully, but these errors were encountered: