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:
Stephen Finucane 2023-03-15 13:12:06 +00:00
parent 56c47d0a39
commit 535bc8e22e
6 changed files with 102 additions and 16 deletions

View File

@ -50,8 +50,6 @@ def include_object(object, name, type_, reflected, compare_to):
)
BORKED_UNIQUE_CONSTRAINTS = (
# duplicate constraints on primary key columns
('project_tag', ['project_id', 'name']),
)
BORKED_FK_CONSTRAINTS = (

View File

@ -1 +1 @@
99de3849d860
c88cdce8f248

View File

@ -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)

View File

@ -118,7 +118,6 @@ class ProjectTag(sql.ModelBase, sql.ModelDictMixin):
sql.String(64), sql.ForeignKey('project.id', ondelete='CASCADE'),
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):

View File

@ -147,18 +147,6 @@ class KeystoneModelsMigrationsSync(test_migrations.ModelsMigrationsSync):
):
continue # skip
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
# migrations but not on the equivalent models. Resolve by
# updating the models.

View File

@ -254,6 +254,27 @@ class KeystoneMigrationsWalk(
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):
"""Ensure we only have a single base revision.