diff --git a/sapl/sessao/migrations/0071_votoparlamentar_unique_constraint.py b/sapl/sessao/migrations/0071_votoparlamentar_unique_constraint.py index f23ff2cea..dbddafda8 100644 --- a/sapl/sessao/migrations/0071_votoparlamentar_unique_constraint.py +++ b/sapl/sessao/migrations/0071_votoparlamentar_unique_constraint.py @@ -3,6 +3,23 @@ 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)) + def remove_votos_duplicados(apps, schema_editor): """Preserva o voto canônico antes de criar os índices únicos.""" @@ -43,12 +60,24 @@ class Migration(migrations.Migration): operations = [ migrations.RunPython( remove_votos_duplicados, migrations.RunPython.noop), - 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'), + 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'), + ), + ], ), ] diff --git a/sapl/sessao/migrations/0072_votacao_aberta_unique_constraint.py b/sapl/sessao/migrations/0072_votacao_aberta_unique_constraint.py index 0fc5e8d48..e65c25772 100644 --- a/sapl/sessao/migrations/0072_votacao_aberta_unique_constraint.py +++ b/sapl/sessao/migrations/0072_votacao_aberta_unique_constraint.py @@ -2,6 +2,21 @@ 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)) + def fecha_materias_abertas_duplicadas(apps, schema_editor): """ @@ -34,12 +49,22 @@ class Migration(migrations.Migration): operations = [ migrations.RunPython( fecha_materias_abertas_duplicadas, migrations.RunPython.noop), - 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'), + 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'), + ), + ], ), ] diff --git a/sapl/sessao/tests/test_votacao_nominal.py b/sapl/sessao/tests/test_votacao_nominal.py index 950ce62fe..886a0dd9a 100644 --- a/sapl/sessao/tests/test_votacao_nominal.py +++ b/sapl/sessao/tests/test_votacao_nominal.py @@ -667,6 +667,30 @@ 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', [ + ('0070_presenca_unica_por_sessao', + '0071_votoparlamentar_unique_constraint'), + ('0071_votoparlamentar_unique_constraint', + '0072_votacao_aberta_unique_constraint'), +]) +def test_migracao_tolera_indices_ja_existentes(anterior, migracao): + """ + 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. + """ + 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) + + @pytest.mark.django_db(transaction=False) def test_api_nao_permite_abrir_votacao_via_patch(admin_client): """