Browse Source

Aproveita índices existentes só se forem os esperados pela constraint

O CREATE UNIQUE INDEX IF NOT EXISTS aceitava qualquer relação com o mesmo
nome, inclusive um índice não único ou inválido, e registrava a
constraint no estado do Django sem que o banco a aplicasse. Também
duplicava em SQL a definição já presente no UniqueConstraint.

AddConstraintSeAusente substitui o AddConstraint nas migrations 0071 e
0072. Se não houver índice com o nome da constraint, cria pelo próprio
Django. Se houver um índice único e válido sobre as mesmas colunas, ele
é aproveitado. Em qualquer outro caso, a migration falha com uma
mensagem clara.

Os testes conferem os índices antes e depois de aplicar as migrations,
nos casos em que eles já existem e em que estão ausentes, e verificam a
falha com um índice não único.
fix/migrations-votacao-indices-existentes
João Moraes 2 weeks ago
parent
commit
83e5cd2c60
  1. 43
      sapl/sessao/migrations/0071_votoparlamentar_unique_constraint.py
  2. 39
      sapl/sessao/migrations/0072_votacao_aberta_unique_constraint.py
  3. 43
      sapl/sessao/migrations/_operacoes.py
  4. 88
      sapl/sessao/tests/test_votacao_nominal.py

43
sapl/sessao/migrations/0071_votoparlamentar_unique_constraint.py

@ -3,22 +3,7 @@
from django.db import migrations, models
from django.db.models import Count
# Antes de ser renumerada no #3855, esta migration se chamava
# 0070_votoparlamentar_unique_constraint e chegou a ser aplicada em alguns
# bancos de produção. Eles já têm os índices, mas não registram a migration
# com o nome atual: o IF NOT EXISTS evita o DuplicateTable neles.
# Nos demais, o SQL é o mesmo que o AddConstraint geraria.
INDICE_UNICO_SQL = """
CREATE UNIQUE INDEX IF NOT EXISTS "{nome}"
ON "sessao_votoparlamentar" ("parlamentar_id", "{coluna}")
WHERE "{coluna}" IS NOT NULL;
"""
def indice_unico(nome, coluna):
return migrations.RunSQL(
INDICE_UNICO_SQL.format(nome=nome, coluna=coluna),
'DROP INDEX IF EXISTS "{}";'.format(nome))
from sapl.sessao.migrations._operacoes import AddConstraintSeAusente
def remove_votos_duplicados(apps, schema_editor):
@ -60,24 +45,12 @@ class Migration(migrations.Migration):
operations = [
migrations.RunPython(
remove_votos_duplicados, migrations.RunPython.noop),
migrations.SeparateDatabaseAndState(
database_operations=[
indice_unico(
'sessao_votoparlamentar_unique_parlamentar_ordem',
'ordem_id'),
indice_unico(
'sessao_votoparlamentar_unique_parlamentar_expediente',
'expediente_id'),
],
state_operations=[
migrations.AddConstraint(
model_name='votoparlamentar',
constraint=models.UniqueConstraint(condition=models.Q(ordem__isnull=False), fields=('parlamentar', 'ordem'), name='sessao_votoparlamentar_unique_parlamentar_ordem'),
),
migrations.AddConstraint(
model_name='votoparlamentar',
constraint=models.UniqueConstraint(condition=models.Q(expediente__isnull=False), fields=('parlamentar', 'expediente'), name='sessao_votoparlamentar_unique_parlamentar_expediente'),
),
],
AddConstraintSeAusente(
model_name='votoparlamentar',
constraint=models.UniqueConstraint(condition=models.Q(ordem__isnull=False), fields=('parlamentar', 'ordem'), name='sessao_votoparlamentar_unique_parlamentar_ordem'),
),
AddConstraintSeAusente(
model_name='votoparlamentar',
constraint=models.UniqueConstraint(condition=models.Q(expediente__isnull=False), fields=('parlamentar', 'expediente'), name='sessao_votoparlamentar_unique_parlamentar_expediente'),
),
]

39
sapl/sessao/migrations/0072_votacao_aberta_unique_constraint.py

@ -2,20 +2,7 @@
from django.db import migrations, models
# Mesmo caso da 0071: com o nome antigo 0071_votacao_aberta_unique_constraint
# esta migration já foi aplicada em alguns bancos de produção, que têm os
# índices sem registrar a migration com o nome atual.
INDICE_UNICO_SQL = """
CREATE UNIQUE INDEX IF NOT EXISTS "{nome}"
ON "{tabela}" ("votacao_aberta")
WHERE "votacao_aberta" = true;
"""
def indice_unico(nome, tabela):
return migrations.RunSQL(
INDICE_UNICO_SQL.format(nome=nome, tabela=tabela),
'DROP INDEX IF EXISTS "{}";'.format(nome))
from sapl.sessao.migrations._operacoes import AddConstraintSeAusente
def fecha_materias_abertas_duplicadas(apps, schema_editor):
@ -49,22 +36,12 @@ class Migration(migrations.Migration):
operations = [
migrations.RunPython(
fecha_materias_abertas_duplicadas, migrations.RunPython.noop),
migrations.SeparateDatabaseAndState(
database_operations=[
indice_unico('sessao_expedientemateria_unique_votacao_aberta',
'sessao_expedientemateria'),
indice_unico('sessao_ordemdia_unique_votacao_aberta',
'sessao_ordemdia'),
],
state_operations=[
migrations.AddConstraint(
model_name='expedientemateria',
constraint=models.UniqueConstraint(condition=models.Q(votacao_aberta=True), fields=('votacao_aberta',), name='sessao_expedientemateria_unique_votacao_aberta'),
),
migrations.AddConstraint(
model_name='ordemdia',
constraint=models.UniqueConstraint(condition=models.Q(votacao_aberta=True), fields=('votacao_aberta',), name='sessao_ordemdia_unique_votacao_aberta'),
),
],
AddConstraintSeAusente(
model_name='expedientemateria',
constraint=models.UniqueConstraint(condition=models.Q(votacao_aberta=True), fields=('votacao_aberta',), name='sessao_expedientemateria_unique_votacao_aberta'),
),
AddConstraintSeAusente(
model_name='ordemdia',
constraint=models.UniqueConstraint(condition=models.Q(votacao_aberta=True), fields=('votacao_aberta',), name='sessao_ordemdia_unique_votacao_aberta'),
),
]

43
sapl/sessao/migrations/_operacoes.py

@ -0,0 +1,43 @@
from django.db import migrations
class AddConstraintSeAusente(migrations.AddConstraint):
"""
AddConstraint que aproveita um índice já existente com o mesmo nome, desde
que ele seja único, válido e sobre as mesmas colunas. Se não for, falha em
vez de registrar no estado do Django uma constraint que o banco não aplica.
Antes de serem renumeradas no #3855, as migrations 0071 e 0072 se chamavam
0070_votoparlamentar_unique_constraint e
0071_votacao_aberta_unique_constraint e chegaram a ser aplicadas em alguns
bancos de produção. Eles já têm os índices, mas não registram as migrations
com os nomes atuais, e o AddConstraint puro falharia com DuplicateTable.
"""
def database_forwards(self, app_label, schema_editor, from_state,
to_state):
model = to_state.apps.get_model(app_label, self.model_name)
nome = self.constraint.name
connection = schema_editor.connection
with connection.cursor() as cursor:
existente = connection.introspection.get_constraints(
cursor, model._meta.db_table).get(nome)
if existente is not None:
cursor.execute(
'SELECT indisvalid FROM pg_index '
'WHERE indexrelid = %s::regclass', [nome])
existente['valid'] = cursor.fetchone()[0]
if existente is None:
super().database_forwards(
app_label, schema_editor, from_state, to_state)
return
colunas = [model._meta.get_field(campo).column
for campo in self.constraint.fields]
if not (existente['unique'] and existente['valid'] and
existente['columns'] == colunas):
raise ValueError(
'O índice "{}" já existe, mas não é o índice único e válido '
'sobre ({}) esperado pela constraint. Remova-o e rode o '
'migrate de novo.'.format(nome, ', '.join(colunas)))

88
sapl/sessao/tests/test_votacao_nominal.py

@ -667,28 +667,90 @@ def test_migracao_0072_fecha_duplicatas_antes_da_constraint():
assert encerrada.registro_aberto is False
@pytest.mark.django_db(transaction=False)
@pytest.mark.parametrize('anterior, migracao', [
MIGRACOES_COM_INDICES_UNICOS = [
('0070_presenca_unica_por_sessao',
'0071_votoparlamentar_unique_constraint'),
'0071_votoparlamentar_unique_constraint',
('sessao_votoparlamentar_unique_parlamentar_ordem',
'sessao_votoparlamentar_unique_parlamentar_expediente')),
('0071_votoparlamentar_unique_constraint',
'0072_votacao_aberta_unique_constraint'),
])
def test_migracao_tolera_indices_ja_existentes(anterior, migracao):
'0072_votacao_aberta_unique_constraint',
('sessao_expedientemateria_unique_votacao_aberta',
'sessao_ordemdia_unique_votacao_aberta')),
]
@pytest.fixture(scope='module')
def migration_loader():
from django.db.migrations.loader import MigrationLoader
# Sem conexão, só lê o grafo de migrations do disco, uma vez por módulo.
return MigrationLoader(None)
def _definicao_indice(nome):
from django.db import connection
with connection.cursor() as cursor:
cursor.execute(
'SELECT indexdef FROM pg_indexes WHERE indexname = %s', [nome])
linha = cursor.fetchone()
return linha[0] if linha else None
def _aplica_migracao(loader, anterior, migracao):
from django.db import connection
estado = loader.project_state(('sessao', anterior))
with connection.schema_editor() as schema_editor:
loader.get_migration('sessao', migracao).apply(estado, schema_editor)
@pytest.mark.django_db(transaction=False)
@pytest.mark.parametrize('indices_existentes', [True, False],
ids=['indices_existentes', 'indices_ausentes'])
@pytest.mark.parametrize('anterior, migracao, indices',
MIGRACOES_COM_INDICES_UNICOS, ids=['0071', '0072'])
def test_migracao_cria_ou_aproveita_indices_unicos(
migration_loader, anterior, migracao, indices, indices_existentes):
"""
Bancos que aplicaram estas migrations com a numeração antiga
(0070_votoparlamentar_unique_constraint e
0071_votacao_aberta_unique_constraint) já têm os índices, mas não as
registram com o nome atual. Reaplicá-las não pode falhar com
DuplicateTable — o banco de teste já tem os índices, como eles.
registram com o nome atual. Reaplicá-las deve aproveitar os índices em vez
de falhar com DuplicateTable; num banco sem eles, devem criá-los iguais.
"""
from django.db import connection
from django.db.migrations.loader import MigrationLoader
loader = MigrationLoader(connection)
estado = loader.project_state(('sessao', anterior))
with connection.schema_editor() as schema_editor:
loader.get_migration('sessao', migracao).apply(estado, schema_editor)
definicoes = {nome: _definicao_indice(nome) for nome in indices}
for definicao in definicoes.values():
assert definicao.startswith('CREATE UNIQUE INDEX')
assert ' WHERE ' in definicao
if not indices_existentes:
with connection.cursor() as cursor:
for nome in indices:
cursor.execute('DROP INDEX {}'.format(nome))
_aplica_migracao(migration_loader, anterior, migracao)
assert {nome: _definicao_indice(nome) for nome in indices} == definicoes
@pytest.mark.django_db(transaction=False)
def test_migracao_falha_se_indice_existente_nao_for_unico(migration_loader):
from django.db import connection
with connection.cursor() as cursor:
cursor.execute('DROP INDEX sessao_ordemdia_unique_votacao_aberta')
cursor.execute('CREATE INDEX sessao_ordemdia_unique_votacao_aberta '
'ON sessao_ordemdia (votacao_aberta)')
with pytest.raises(
ValueError,
match='"sessao_ordemdia_unique_votacao_aberta" já existe'):
_aplica_migracao(migration_loader,
'0071_votoparlamentar_unique_constraint',
'0072_votacao_aberta_unique_constraint')
@pytest.mark.django_db(transaction=False)

Loading…
Cancel
Save