Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Alembic: How to migrate custom type in a model?

My User model is

class User(UserMixin, db.Model):
    __tablename__ = 'users'
    # noinspection PyShadowingBuiltins
    uuid = Column('uuid', GUID(), default=uuid.uuid4, primary_key=True,
                  unique=True)
    email = Column('email', String, nullable=False, unique=True)
    _password = Column('password', String, nullable=False)
    created_on = Column('created_on', sa.types.DateTime(timezone=True),
                        default=datetime.utcnow())
    last_login = Column('last_login', sa.types.DateTime(timezone=True),
                        onupdate=datetime.utcnow())

where GUID is a custom type as described in sqlalchemy docs (Exactly same)

Now when I run

alembic revision --autogenerate -m "Added initial table"

I get my upgrade() as

def upgrade():
    ### commands auto generated by Alembic - please adjust! ###
    op.create_table('users',
    sa.Column('uuid', sa.GUID(), nullable=False),
    sa.Column('email', sa.String(), nullable=False),
    sa.Column('password', sa.String(), nullable=False),
    sa.Column('created_on', sa.DateTime(timezone=True), nullable=True),
    sa.Column('last_login', sa.DateTime(timezone=True), nullable=True),
    sa.PrimaryKeyConstraint('uuid'),
    sa.UniqueConstraint('email'),
    sa.UniqueConstraint('uuid')
    )
    ### end Alembic commands ###

but during applying upgrade -> alembic upgrade head, I see

File "alembic/versions/49cc74d0da9d_added_initial_table.py", line 20, in upgrade
    sa.Column('uuid', sa.GUID(), nullable=False),
AttributeError: 'module' object has no attribute 'GUID'

How can I make it work with GUID/custom type here?

like image 972
daydreamer Avatar asked Mar 27 '13 19:03

daydreamer


People also ask

How do you make migrations with alembic?

Alembic can view the status of the database and compare against the table metadata in the application, generating the “obvious” migrations based on a comparison. This is achieved using the --autogenerate option to the alembic revision command, which places so-called candidate migrations into our new migrations file.

How does alembic work in Python?

alembic - this directory lives within your application's source tree and is the home of the migration environment. It can be named anything, and a project that uses multiple databases may even have more than one. env.py - This is a Python script that is run whenever the alembic migration tool is invoked.

What does alembic stamp head do?

The head will automatically revert to the most recent remaining migration. Using stamp will set the db version value to the specified revision; not alter the head revision number. Based on your question, the original answer will resolve your issue.


2 Answers

Short answer (using sqlalchemy version 1.4.25):

From the documentation:

For user-defined types, that is, any custom type that is not within the sqlalchemy. module namespace, by default Alembic will use the value of __module__ for the custom type:

Column("my_column", myapp.models.utils.types.MyCustomType())

The imports for the above type again must be made present within the migration, either manually, or by adding it to script.py.mako.

So, import your my_module in script.py.mako (and you probably need to define your custom type in a file other than models.py):

...
from alembic import op
import sqlalchemy as sa
import my_module
${imports if imports else ""}
...

Long answer:

I have my custom type BinaryUUID defined in uuid_type_mysql.py and I'm importing it in models.py and using it there:

models.py:

...
from .uuid_type_mysql import BinaryUUID
...

After generating the migrations with flask db migrate I would get this on the migration file:

...
sa.Column('public_id', my_module.uuid_type_mysql.BinaryUUID(length=16), nullable=False),
...

And the problem is that the migration does not know my_module because it is not imported.

After adding import my_module to script.py.mako as suggested in the documentation, now the module is imported in the migration file:

...
from alembic import op
import sqlalchemy as sa
import my_module
...

After this, everything work fine for me with the bonus that it is not needed to manually edit the generated migration.

like image 104
David Avatar answered Sep 28 '22 05:09

David


I had a similar problem and solved it like follows:

Let's assume you have the following module my_guid, containing (from the page you already cited, with minor naming modifications):

import uuid as uuid_package
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
from sqlalchemy import TypeDecorator, CHAR

class GUID(TypeDecorator):
    impl = CHAR

    def load_dialect_impl(self, dialect):
        if dialect.name == 'postgresql':
            return dialect.type_descriptor(PG_UUID())
        else:
            return dialect.type_descriptor(CHAR(32))

    def process_bind_param(self, value, dialect):
        if value is None:
            return value
        elif dialect.name == 'postgresql':
            return str(value)
        else:
            if not isinstance(value, uuid_package.UUID):
                return "%.32x" % uuid_package.UUID(value)
            else:
                # hexstring
                return "%.32x" % value

    def process_result_value(self, value, dialect):
        if value is None:
            return value
        else:
            return uuid_package.UUID(value)

If you use this GUID in your models, you just need to add three lines at alembic/env.py:

from my_guid import GUID
import sqlalchemy as sa
sa.GUID = GUID

That worked for me. Hope that helps!

like image 36
Philipp der Rautenberg Avatar answered Sep 28 '22 05:09

Philipp der Rautenberg