From b8282b1d468fbc98b5034b0e770e889a777ec6e2 Mon Sep 17 00:00:00 2001 From: kaiyou Date: Sun, 9 Dec 2018 16:06:53 +0100 Subject: [PATCH] Support named constraints for multiple backends Supporting multiple backends requires that specific sqlite collations are not used, thus lowercase is applied to all non case-sensitive columns. However, lowercasing the database requires temporary disabling foreign key constraints, which is not possible on SQLite and requires we specify the constraint names. This migration specific to sqlite and postgresql drops every constraint, whether it is named or not, and recreates all of them with known names so we can later disable them. --- core/admin/mailu/models.py | 43 +++++----- core/admin/migrations/env.py | 4 +- .../migrations/versions/546b04c886f0_.py | 79 +++++++++++++++++++ 3 files changed, 107 insertions(+), 19 deletions(-) create mode 100644 core/admin/migrations/versions/546b04c886f0_.py diff --git a/core/admin/mailu/models.py b/core/admin/mailu/models.py index a1c54b49..6cfe6a60 100644 --- a/core/admin/mailu/models.py +++ b/core/admin/mailu/models.py @@ -24,7 +24,7 @@ class IdnaDomain(db.TypeDecorator): """ Stores a Unicode string in it's IDNA representation (ASCII only) """ - impl = db.String(80, collation="NOCASE") + impl = db.String(80) def process_bind_param(self, value, dialect): return idna.encode(value).decode("ascii").lower() @@ -37,7 +37,7 @@ class IdnaEmail(db.TypeDecorator): """ Stores a Unicode string in it's IDNA representation (ASCII only) """ - impl = db.String(255, collation="NOCASE") + impl = db.String(255) def process_bind_param(self, value, dialect): try: @@ -88,32 +88,39 @@ class JSONEncoded(db.TypeDecorator): return json.loads(value) if value else None -class Config(db.Model): - """ In-database configuration values - """ - - name = db.Column(db.String(255), primary_key=True, nullable=False) - value = db.Column(JSONEncoded) - - -# Many-to-many association table for domain managers -managers = db.Table('manager', - db.Column('domain_name', IdnaDomain, db.ForeignKey('domain.name')), - db.Column('user_email', IdnaEmail, db.ForeignKey('user.email')) -) - - class Base(db.Model): """ Base class for all models """ __abstract__ = True + metadata = sqlalchemy.schema.MetaData( + naming_convention={ + "fk": "%(table_name)s_%(column_0_name)s_fkey", + "pk": "%(table_name)s_pkey" + } + ) + created_at = db.Column(db.Date, nullable=False, default=datetime.now) updated_at = db.Column(db.Date, nullable=True, onupdate=datetime.now) comment = db.Column(db.String(255), nullable=True) +# Many-to-many association table for domain managers +managers = db.Table('manager', Base.metadata, + db.Column('domain_name', IdnaDomain, db.ForeignKey('domain.name')), + db.Column('user_email', IdnaEmail, db.ForeignKey('user.email')) +) + + +class Config(Base): + """ In-database configuration values + """ + + name = db.Column(db.String(255), primary_key=True, nullable=False) + value = db.Column(JSONEncoded) + + class Domain(Base): """ A DNS domain that has mail addresses associated to it. """ @@ -309,7 +316,7 @@ class User(Base, Email): # Settings displayed_name = db.Column(db.String(160), nullable=False, default="") spam_enabled = db.Column(db.Boolean(), nullable=False, default=True) - spam_threshold = db.Column(db.Integer(), nullable=False, default=80.0) + spam_threshold = db.Column(db.Integer(), nullable=False, default=80) # Flask-login attributes is_authenticated = True diff --git a/core/admin/migrations/env.py b/core/admin/migrations/env.py index bb43fc55..a3f9cf18 100755 --- a/core/admin/migrations/env.py +++ b/core/admin/migrations/env.py @@ -22,7 +22,9 @@ logger = logging.getLogger('alembic.env') from flask import current_app config.set_main_option('sqlalchemy.url', current_app.config.get('SQLALCHEMY_DATABASE_URI')) -target_metadata = current_app.extensions['migrate'].db.metadata +#target_metadata = current_app.extensions['migrate'].db.metadata +from mailu import models +target_metadata = models.Base.metadata # other values from the config, defined by the needs of env.py, # can be acquired: diff --git a/core/admin/migrations/versions/546b04c886f0_.py b/core/admin/migrations/versions/546b04c886f0_.py new file mode 100644 index 00000000..35534aa0 --- /dev/null +++ b/core/admin/migrations/versions/546b04c886f0_.py @@ -0,0 +1,79 @@ +""" Fix constraint naming by addint a name to all constraints + +Revision ID: 546b04c886f0 +Revises: 5aeb5811408e +Create Date: 2018-12-08 16:33:37.757634 + +""" + +# revision identifiers, used by Alembic. +revision = '546b04c886f0' +down_revision = 'cd79ed46d9c2' + +from alembic import op, context +import sqlalchemy as sa + + +def upgrade(): + # Only run this for somehow supported data types at the date we started naming constraints + # Among others, these will probably fail on MySQL + if context.get_bind().engine.name not in ('sqlite', 'postgresql'): + return + + metadata = context.get_context().opts['target_metadata'] + + # Drop every constraint on every table + with op.batch_alter_table('alias', naming_convention=metadata.naming_convention) as batch_op: + batch_op.drop_constraint('alias_pkey', type_="primary") + batch_op.drop_constraint('alias_domain_name_fkey', type_="foreignkey") + with op.batch_alter_table('alternative', naming_convention=metadata.naming_convention) as batch_op: + batch_op.drop_constraint('alternative_pkey', type_="primary") + batch_op.drop_constraint('alternative_domain_name_fkey', type_="foreignkey") + with op.batch_alter_table('manager', naming_convention=metadata.naming_convention) as batch_op: + batch_op.drop_constraint('manager_domain_name_fkey', type_="foreignkey") + batch_op.drop_constraint('manager_user_email_fkey', type_="foreignkey") + with op.batch_alter_table('token', naming_convention=metadata.naming_convention) as batch_op: + batch_op.drop_constraint('token_pkey', type_="primary") + batch_op.drop_constraint('token_user_email_fkey', type_="foreignkey") + with op.batch_alter_table('fetch', naming_convention=metadata.naming_convention) as batch_op: + batch_op.drop_constraint('fetch_pkey', type_="primary") + batch_op.drop_constraint('fetch_user_email_fkey', type_="foreignkey") + with op.batch_alter_table('relay', naming_convention=metadata.naming_convention) as batch_op: + batch_op.drop_constraint('relay_pkey', type_="primary") + with op.batch_alter_table('config', naming_convention=metadata.naming_convention) as batch_op: + batch_op.drop_constraint('config_pkey', type_="primary") + with op.batch_alter_table('user', naming_convention=metadata.naming_convention) as batch_op: + batch_op.drop_constraint('user_pkey', type_="primary") + batch_op.drop_constraint('user_domain_name_fkey', type_="foreignkey") + with op.batch_alter_table('domain', naming_convention=metadata.naming_convention) as batch_op: + batch_op.drop_constraint('domain_pkey', type_="primary") + + # Recreate constraints with proper names + with op.batch_alter_table('domain', naming_convention=metadata.naming_convention) as batch_op: + batch_op.create_primary_key('domain_pkey', ['name']) + with op.batch_alter_table('alias', naming_convention=metadata.naming_convention) as batch_op: + batch_op.create_primary_key('alias_pkey', ['email']) + batch_op.create_foreign_key('alias_domain_name_fkey', 'domain', ['domain_name'], ['name']) + with op.batch_alter_table('user', naming_convention=metadata.naming_convention) as batch_op: + batch_op.create_primary_key('user_pkey', ['email']) + batch_op.create_foreign_key('user_domain_name_fkey', 'domain', ['domain_name'], ['name']) + with op.batch_alter_table('alternative', naming_convention=metadata.naming_convention) as batch_op: + batch_op.create_primary_key('alternative_pkey', ['name']) + batch_op.create_foreign_key('alternative_domain_name_fkey', 'domain', ['domain_name'], ['name']) + with op.batch_alter_table('manager', naming_convention=metadata.naming_convention) as batch_op: + batch_op.create_foreign_key('manager_domain_name_fkey', 'domain', ['domain_name'], ['name']) + batch_op.create_foreign_key('manager_user_email_fkey', 'user', ['user_email'], ['email']) + with op.batch_alter_table('token', naming_convention=metadata.naming_convention) as batch_op: + batch_op.create_primary_key('token_pkey', ['id']) + batch_op.create_foreign_key('token_user_email_fkey', 'user', ['user_email'], ['email']) + with op.batch_alter_table('fetch', naming_convention=metadata.naming_convention) as batch_op: + batch_op.create_primary_key('fetch_pkey', ['id']) + batch_op.create_foreign_key('fetch_user_email_fkey', 'user', ['user_email'], ['email']) + with op.batch_alter_table('relay', naming_convention=metadata.naming_convention) as batch_op: + batch_op.create_primary_key('relay_pkey', ['name']) + with op.batch_alter_table('config', naming_convention=metadata.naming_convention) as batch_op: + batch_op.create_primary_key('config_pkey', ['name']) + + +def downgrade(): + pass