""" Remoção de linhas duplicadas deixadas por execuções não idempotentes. Antes de ``substituir_evidencias_visuais`` e ``substituir_cenas`` existirem, o Scanner acrescentava evidências e cenas a cada execução em vez de substituir as do clipe. Bancos criados nesse período acumularam cópias exatas da mesma observação, o que infla contagens e médias calculadas sobre essas tabelas. Este módulo apaga essas cópias mantendo sempre a linha de menor ``id`` de cada grupo idêntico. Só remove duplicata exata — linhas que diferem em qualquer campo são observações distintas e são preservadas. """ from __future__ import annotations import sqlite3 from dataclasses import dataclass @dataclass(frozen=True) class ResultadoDaLimpeza: """Quantidade de linhas removidas por tabela.""" evidencias_visuais: int cenas: int @property def total(self) -> int: """Total de linhas removidas em todas as tabelas.""" return self.evidencias_visuais + self.cenas class LimpadorDeDuplicatas: """ Remove duplicatas exatas de evidências visuais e cenas. Atributos: conexao: Conexão SQLite já aberta e com o esquema aplicado. """ def __init__(self, conexao: sqlite3.Connection) -> None: """ Inicializa o limpador sobre uma conexão existente. Parâmetros: conexao: Conexão SQLite já aberta e com o esquema aplicado. """ self.conexao = conexao def contar_duplicatas(self) -> ResultadoDaLimpeza: """ Conta quantas linhas seriam removidas, sem remover nada. Retorna: As contagens de duplicatas por tabela. """ return ResultadoDaLimpeza( evidencias_visuais=self._contar( "evidencias_visuais", ("video_id", "clipe_id", "tipo", "inicio", "fim", "valor"), ), cenas=self._contar("cenas", ("video_id", "clipe_id", "inicio", "fim")), ) def limpar(self) -> ResultadoDaLimpeza: """ Remove as duplicatas, mantendo a linha de menor ``id`` de cada grupo. Retorna: As contagens de linhas efetivamente removidas por tabela. """ with self.conexao: evidencias = self._remover( "evidencias_visuais", ("video_id", "clipe_id", "tipo", "inicio", "fim", "valor"), ) cenas = self._remover("cenas", ("video_id", "clipe_id", "inicio", "fim")) return ResultadoDaLimpeza(evidencias_visuais=evidencias, cenas=cenas) def _contar(self, tabela: str, chave: tuple[str, ...]) -> int: """Conta linhas que não são a primeira ocorrência do seu grupo.""" colunas = ", ".join(chave) return int(self.conexao.execute( f"""SELECT count(*) FROM {tabela} WHERE id NOT IN (SELECT min(id) FROM {tabela} GROUP BY {colunas})""" ).fetchone()[0]) def _remover(self, tabela: str, chave: tuple[str, ...]) -> int: """Apaga as linhas que não são a primeira ocorrência do seu grupo.""" colunas = ", ".join(chave) cursor = self.conexao.execute( f"""DELETE FROM {tabela} WHERE id NOT IN (SELECT min(id) FROM {tabela} GROUP BY {colunas})""" ) return cursor.rowcount