sql: Remove duplicate constraints
A primary key is automatically unique, therefore if one or columns is included in a primary key constraint there is no need to add a separate unique constraint for these columns. Remove it. Note that this only affects MySQL. Both SQLite and PostgreSQL appear to ignore the duplicate unique constraint. As a result, it was necessary to run auto-generation against MySQL instead of the default SQLite. The actual command used was similar to what we normally do, however. $ python keystone/common/sql/migrations/manage.py revision \ --autogenerate --message 'Remove duplicate constraints' As always, the resulting schema migrations then needed some manual tweaks to remove "please adjust!" comments and unnecessary imports but they are correct. Change-Id: I64252086f994901a5ebe05afec37a6afd3a192ee Signed-off-by: Stephen Finucane <sfinucan@redhat.com>
This commit is contained in:
parent
56c47d0a39
commit
535bc8e22e
|
@ -50,8 +50,6 @@ def include_object(object, name, type_, reflected, compare_to):
|
||||||
)
|
)
|
||||||
|
|
||||||
BORKED_UNIQUE_CONSTRAINTS = (
|
BORKED_UNIQUE_CONSTRAINTS = (
|
||||||
# duplicate constraints on primary key columns
|
|
||||||
('project_tag', ['project_id', 'name']),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
BORKED_FK_CONSTRAINTS = (
|
BORKED_FK_CONSTRAINTS = (
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
99de3849d860
|
c88cdce8f248
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
"""Remove duplicate constraints.
|
||||||
|
|
||||||
|
Revision ID: c88cdce8f248
|
||||||
|
Revises: 99de3849d860
|
||||||
|
Create Date: 2023-03-15 13:17:44.060715
|
||||||
|
"""
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
from sqlalchemy.engine import reflection
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'c88cdce8f248'
|
||||||
|
down_revision = '99de3849d860'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
bind = op.get_bind()
|
||||||
|
|
||||||
|
# This only affects MySQL - PostgreSQL and SQLite were smart enough to
|
||||||
|
# ignore the duplicate constraints
|
||||||
|
if bind.engine.name != 'mysql':
|
||||||
|
return
|
||||||
|
|
||||||
|
# We want to drop a duplicate index on the 'project_tag' table. To drop an
|
||||||
|
# index, we would normally just use drop_index like so:
|
||||||
|
#
|
||||||
|
# with op.batch_alter_table('project_tag', schema=None) as batch_op:
|
||||||
|
# batch_op.drop_index(foo)
|
||||||
|
#
|
||||||
|
# However, the index wasn't explicitly named so we're not sure what 'foo'
|
||||||
|
# is. It has two potential names:
|
||||||
|
#
|
||||||
|
# - If it was created by alembic, alembic will have left things up to the
|
||||||
|
# backend, which on MySQL means the index will have the same name as the
|
||||||
|
# first column the index covers [1]. Alternatively, ...
|
||||||
|
# - If it was created by sqlalchemy-migrate then it will be called
|
||||||
|
# '{table_name}_{first_column_name}_key' [2]
|
||||||
|
#
|
||||||
|
# We need to handle both, so we need to first inspect the table to find
|
||||||
|
# which it is.
|
||||||
|
#
|
||||||
|
# Note that unlike MariaDB [3], MySQL [4] doesn't support the 'ALTER TABLE
|
||||||
|
# tbl_name DROP CONSTRAINT IF EXISTS constraint_name' syntax, which would
|
||||||
|
# have allowed us to avoid the inspection. Boo.
|
||||||
|
#
|
||||||
|
# [1] https://dba.stackexchange.com/a/160712
|
||||||
|
# [2] https://opendev.org/x/sqlalchemy-migrate/src/commit/5d1f322542cd8eb42381612765be4ed9ca8105ec/migrate/changeset/constraint.py#L199 # noqa: E501
|
||||||
|
# [3] https://mariadb.com/kb/en/alter-table/
|
||||||
|
# [4] https://dev.mysql.com/doc/refman/8.0/en/alter-table.html
|
||||||
|
|
||||||
|
inspector = reflection.Inspector.from_engine(bind)
|
||||||
|
indexes = inspector.get_indexes('project_tag')
|
||||||
|
|
||||||
|
index_name = None
|
||||||
|
for index in indexes:
|
||||||
|
if index['column_names'] == ['project_id', 'name']:
|
||||||
|
index_name = index['name']
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# This should never happen *but* we silently ignore it since there's no
|
||||||
|
# need to break user's upgrade flow, even if they've borked something
|
||||||
|
return
|
||||||
|
|
||||||
|
with op.batch_alter_table('project_tag', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index(index_name)
|
|
@ -118,7 +118,6 @@ class ProjectTag(sql.ModelBase, sql.ModelDictMixin):
|
||||||
sql.String(64), sql.ForeignKey('project.id', ondelete='CASCADE'),
|
sql.String(64), sql.ForeignKey('project.id', ondelete='CASCADE'),
|
||||||
nullable=False, primary_key=True)
|
nullable=False, primary_key=True)
|
||||||
name = sql.Column(sql.Unicode(255), nullable=False, primary_key=True)
|
name = sql.Column(sql.Unicode(255), nullable=False, primary_key=True)
|
||||||
__table_args__ = (sql.UniqueConstraint('project_id', 'name'),)
|
|
||||||
|
|
||||||
|
|
||||||
class ProjectOption(sql.ModelBase):
|
class ProjectOption(sql.ModelBase):
|
||||||
|
|
|
@ -147,18 +147,6 @@ class KeystoneModelsMigrationsSync(test_migrations.ModelsMigrationsSync):
|
||||||
):
|
):
|
||||||
continue # skip
|
continue # skip
|
||||||
else:
|
else:
|
||||||
# FIXME(stephenfin): These unique constraint are unecessary
|
|
||||||
# since the columns are already included in a primary key
|
|
||||||
# constraint. The constraints are ignored in PostgreSQL.
|
|
||||||
if element[0] == 'add_constraint':
|
|
||||||
if (
|
|
||||||
element[1].table.name,
|
|
||||||
[x.name for x in element[1].columns],
|
|
||||||
) in (
|
|
||||||
('project_tag', ['project_id', 'name']),
|
|
||||||
):
|
|
||||||
continue # skip
|
|
||||||
|
|
||||||
# FIXME(stephenfin): These indexes are present in the
|
# FIXME(stephenfin): These indexes are present in the
|
||||||
# migrations but not on the equivalent models. Resolve by
|
# migrations but not on the equivalent models. Resolve by
|
||||||
# updating the models.
|
# updating the models.
|
||||||
|
|
|
@ -254,6 +254,27 @@ class KeystoneMigrationsWalk(
|
||||||
constraint['column_names'],
|
constraint['column_names'],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _pre_upgrade_c88cdce8f248(self, connection):
|
||||||
|
if connection.engine.name != 'mysql':
|
||||||
|
return
|
||||||
|
|
||||||
|
# NOTE(stephenfin): Even though the migration is written to handle
|
||||||
|
# names generated by both alembic and sqlalchemy-migrate, we only check
|
||||||
|
# for the former here since we don't apply sqlalchemy-migrate
|
||||||
|
# migrations anymore
|
||||||
|
inspector = sqlalchemy.inspect(connection)
|
||||||
|
indexes = inspector.get_indexes('project_tag')
|
||||||
|
self.assertIn('project_id', {x['name'] for x in indexes})
|
||||||
|
|
||||||
|
def _check_c88cdce8f248(self, connection):
|
||||||
|
# This migration only applies to MySQL
|
||||||
|
if connection.engine.name != 'mysql':
|
||||||
|
return
|
||||||
|
|
||||||
|
inspector = sqlalchemy.inspect(connection)
|
||||||
|
indexes = inspector.get_indexes('project_tag')
|
||||||
|
self.assertNotIn('project_id', {x['name'] for x in indexes})
|
||||||
|
|
||||||
def test_single_base_revision(self):
|
def test_single_base_revision(self):
|
||||||
"""Ensure we only have a single base revision.
|
"""Ensure we only have a single base revision.
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue