Browse Source

Merge 83e5cd2c60 into c46a5a0c7c

pull/3868/merge
joaohortsenado 2 weeks ago
committed by GitHub
parent
commit
fd02040384
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 6
      sapl/sessao/migrations/0071_votoparlamentar_unique_constraint.py
  2. 6
      sapl/sessao/migrations/0072_votacao_aberta_unique_constraint.py
  3. 43
      sapl/sessao/migrations/_operacoes.py
  4. 86
      sapl/sessao/tests/test_votacao_nominal.py

6
sapl/sessao/migrations/0071_votoparlamentar_unique_constraint.py

@ -3,6 +3,8 @@
from django.db import migrations, models from django.db import migrations, models
from django.db.models import Count from django.db.models import Count
from sapl.sessao.migrations._operacoes import AddConstraintSeAusente
def remove_votos_duplicados(apps, schema_editor): def remove_votos_duplicados(apps, schema_editor):
"""Preserva o voto canônico antes de criar os índices únicos.""" """Preserva o voto canônico antes de criar os índices únicos."""
@ -43,11 +45,11 @@ class Migration(migrations.Migration):
operations = [ operations = [
migrations.RunPython( migrations.RunPython(
remove_votos_duplicados, migrations.RunPython.noop), remove_votos_duplicados, migrations.RunPython.noop),
migrations.AddConstraint( AddConstraintSeAusente(
model_name='votoparlamentar', model_name='votoparlamentar',
constraint=models.UniqueConstraint(condition=models.Q(ordem__isnull=False), fields=('parlamentar', 'ordem'), name='sessao_votoparlamentar_unique_parlamentar_ordem'), constraint=models.UniqueConstraint(condition=models.Q(ordem__isnull=False), fields=('parlamentar', 'ordem'), name='sessao_votoparlamentar_unique_parlamentar_ordem'),
), ),
migrations.AddConstraint( AddConstraintSeAusente(
model_name='votoparlamentar', model_name='votoparlamentar',
constraint=models.UniqueConstraint(condition=models.Q(expediente__isnull=False), fields=('parlamentar', 'expediente'), name='sessao_votoparlamentar_unique_parlamentar_expediente'), constraint=models.UniqueConstraint(condition=models.Q(expediente__isnull=False), fields=('parlamentar', 'expediente'), name='sessao_votoparlamentar_unique_parlamentar_expediente'),
), ),

6
sapl/sessao/migrations/0072_votacao_aberta_unique_constraint.py

@ -2,6 +2,8 @@
from django.db import migrations, models from django.db import migrations, models
from sapl.sessao.migrations._operacoes import AddConstraintSeAusente
def fecha_materias_abertas_duplicadas(apps, schema_editor): def fecha_materias_abertas_duplicadas(apps, schema_editor):
""" """
@ -34,11 +36,11 @@ class Migration(migrations.Migration):
operations = [ operations = [
migrations.RunPython( migrations.RunPython(
fecha_materias_abertas_duplicadas, migrations.RunPython.noop), fecha_materias_abertas_duplicadas, migrations.RunPython.noop),
migrations.AddConstraint( AddConstraintSeAusente(
model_name='expedientemateria', model_name='expedientemateria',
constraint=models.UniqueConstraint(condition=models.Q(votacao_aberta=True), fields=('votacao_aberta',), name='sessao_expedientemateria_unique_votacao_aberta'), constraint=models.UniqueConstraint(condition=models.Q(votacao_aberta=True), fields=('votacao_aberta',), name='sessao_expedientemateria_unique_votacao_aberta'),
), ),
migrations.AddConstraint( AddConstraintSeAusente(
model_name='ordemdia', model_name='ordemdia',
constraint=models.UniqueConstraint(condition=models.Q(votacao_aberta=True), fields=('votacao_aberta',), name='sessao_ordemdia_unique_votacao_aberta'), 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)))

86
sapl/sessao/tests/test_votacao_nominal.py

@ -667,6 +667,92 @@ def test_migracao_0072_fecha_duplicatas_antes_da_constraint():
assert encerrada.registro_aberto is False assert encerrada.registro_aberto is False
MIGRACOES_COM_INDICES_UNICOS = [
('0070_presenca_unica_por_sessao',
'0071_votoparlamentar_unique_constraint',
('sessao_votoparlamentar_unique_parlamentar_ordem',
'sessao_votoparlamentar_unique_parlamentar_expediente')),
('0071_votoparlamentar_unique_constraint',
'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 deve aproveitar os índices em vez
de falhar com DuplicateTable; num banco sem eles, devem criá-los iguais.
"""
from django.db import connection
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) @pytest.mark.django_db(transaction=False)
def test_api_nao_permite_abrir_votacao_via_patch(admin_client): def test_api_nao_permite_abrir_votacao_via_patch(admin_client):
""" """

Loading…
Cancel
Save