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.
master
kaiyou 6 years ago
parent e022513a94
commit b8282b1d46

@ -24,7 +24,7 @@ class IdnaDomain(db.TypeDecorator):
""" Stores a Unicode string in it's IDNA representation (ASCII only) """ 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): def process_bind_param(self, value, dialect):
return idna.encode(value).decode("ascii").lower() 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) """ 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): def process_bind_param(self, value, dialect):
try: try:
@ -88,32 +88,39 @@ class JSONEncoded(db.TypeDecorator):
return json.loads(value) if value else None 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): class Base(db.Model):
""" Base class for all models """ Base class for all models
""" """
__abstract__ = True __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) created_at = db.Column(db.Date, nullable=False, default=datetime.now)
updated_at = db.Column(db.Date, nullable=True, onupdate=datetime.now) updated_at = db.Column(db.Date, nullable=True, onupdate=datetime.now)
comment = db.Column(db.String(255), nullable=True) 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): class Domain(Base):
""" A DNS domain that has mail addresses associated to it. """ A DNS domain that has mail addresses associated to it.
""" """
@ -309,7 +316,7 @@ class User(Base, Email):
# Settings # Settings
displayed_name = db.Column(db.String(160), nullable=False, default="") displayed_name = db.Column(db.String(160), nullable=False, default="")
spam_enabled = db.Column(db.Boolean(), nullable=False, default=True) 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 # Flask-login attributes
is_authenticated = True is_authenticated = True

@ -22,7 +22,9 @@ logger = logging.getLogger('alembic.env')
from flask import current_app from flask import current_app
config.set_main_option('sqlalchemy.url', config.set_main_option('sqlalchemy.url',
current_app.config.get('SQLALCHEMY_DATABASE_URI')) 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, # other values from the config, defined by the needs of env.py,
# can be acquired: # can be acquired:

@ -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
Loading…
Cancel
Save