Buscar/Desenvolva uma extensão de índice de banco de dados que passe nos testes e resista a falhas

Desenvolva uma extensão de índice de banco de dados que passe nos testes e resista a falhas

Use quando uma IA criar ou alterar uma extensão de índice de banco de dados. Ela trata toda falha de teste como real, segue uma ordem fixa antes do commit e registra cada mudança de página para que a recuperação e o standby funcionem.

Para usar estas regras de IA
## Reinicie o servidor após reinstalar e leia primeiro a especificação de armazenamento Você contribui para uma extensão de banco de dados que implementa um método de acesso a índice apoiado por armazenamento em disco em camadas, dentro dos próprios arquivos de relação do banco de dados. O trabalho está próximo da durabilidade, da recuperação e da concorrência, portanto a correção supera a velocidade de entrega em toda decisão de julgamento. Premissas operacionais a manter o tempo todo: - A extensão é carregada na inicialização do servidor. É necessário adicioná-la à configuração de pré-carregamento do servidor e reiniciá-lo antes que a extensão possa ser criada, e uma nova reinicialização é necessária após reinstalar o binário durante o desenvolvimento. Um binário carregado desatualizado produz resultados de teste enganosos; quando o comportamento contradiz o código-fonte que você acabou de compilar, suspeite primeiro de uma reinicialização ausente. - Instale os objetos da extensão em um schema dedicado, em vez do namespace público padrão, sempre que o projeto permitir, para manter o namespace limpo. - Prefira a menor alteração que resolva completamente o problema. Não refatore código adjacente que a tarefa não exigiu. - As especificações de design no repositório são vinculantes. Leia a especificação de armazenamento antes de mexer no caminho de escrita, leitura ou transbordamento (spill), e atualize a especificação na mesma alteração quando o próprio design mudar. ## Investigue cada falha de teste em vez de chamá-la de não relacionada O branch principal permanece continuamente verde. Toda falha de teste importa. - Nunca descarte uma falha como preexistente ou não relacionada. Na prática, quase nunca é o caso. - Se você genuinamente acredita que uma falha não está relacionada à sua alteração, verifique isso: faça checkout do branch principal e execute o mesmo teste lá. - Reproduzir a falha no branch principal não a desculpa. Ela ainda precisa ser investigada e corrigida, não arquivada ou anotada como conhecida. - Nunca marque um teste como ignorado, relaxe uma asserção ou edite um arquivo de saída esperada apenas para transformar uma execução vermelha em verde. Altere uma expectativa somente depois de conseguir afirmar precisamente por que a nova saída está correta. - Relate falhas com honestidade e o quanto antes, incluindo falhas que você mesmo causou. Ocultar uma execução vermelha é pior do que o próprio defeito. ## Rode os testes de regressão e leia o arquivo de diferenças antes de cada commit Complete cada etapa abaixo antes de fazer commit de uma alteração. Não reordene nem omita etapas porque uma alteração "parece trivial". 1. Compile a extensão. 2. Execute a suíte de regressão declarativa contra uma build instalada. 3. Inspecione o arquivo de diferenças de regressão produzido pela execução, não apenas o resumo de aprovação ou falha. Uma suíte pode reportar sucesso enquanto mascara uma diferença que você introduziu. 4. Se você modificou alguma mensagem visível ao usuário, atualize os arquivos de saída esperada correspondentes para corresponder a ela, e declare no commit por que a mensagem mudou. 5. Execute a verificação de formatação. 6. Reexecute a suíte de regressão depois de atualizar qualquer arquivo de expectativa, para que a execução final registrada seja aquela que reflete a árvore commitada. Além da suíte rotineira, execute os níveis de concorrência, recuperação de falhas, múltiplos backends e estresse sempre que uma alteração tocar em bloqueio, no caminho de escrita, na recuperação ou no ciclo de vida dos segmentos. Diga claramente quais níveis você executou e quais não executou. ## Coloque cada mudança na camada de código mais baixa que a resolva O código-fonte é organizado em camadas, e cada alteração pertence à camada mais baixa que a resolve completamente. Não vaze detalhes de armazenamento para cima, na camada de interface do banco de dados, e não alcance para baixo além de uma abstração para atalhar uma correção. - Camada um, interface do banco de dados: o manipulador do método de acesso, construção de índice, varredura e pontos de entrada de vacuum; tipos de dados personalizados; hooks do planejador e estimativa de custo. - Camada dois, coordenação de índice: ranqueamento e pontuação, otimização top-k, ciclo de vida do estado do índice, registro, tratamento de metapágina e a abstração sobre fontes de postings. - Camada três, armazenamento: o buffer de escrita de nível zero, segmentos imutáveis, fusão (merge) e compressão. - Transversal: utilitários de depuração e dump, e inicialização do módulo. Quando uma alteração atravessa camadas, descreva explicitamente a travessia de fronteira na descrição da alteração. Uma correção que silenciosamente amplia uma interface é uma mudança de design e deve ser revisada como tal. ## Entenda o buffer de escrita e os segmentos imutáveis antes de mexer no armazenamento O índice segue uma abordagem de armazenamento estruturado em log e em camadas. Compreenda o modelo antes de alterar qualquer parte dele. - O nível zero é um buffer de escrita: uma cadeia de páginas orientada a anexação (append), armazenada dentro da própria relação de índice e ancorada em uma metapágina. As escritas anexam registros que carregam o identificador da linha, um comprimento e um payload compactado à página final. Todas as mutações ocorrem sob bloqueios de buffer padrão e são registradas em write-ahead log. - Segmentos são estruturas imutáveis, estruturadas em blocos, em disco, que carregam entradas de salto (skip entries) para que uma varredura possa saltar diretamente a um bloco sem percorrer cada posting. - O transbordamento (spill) do buffer de escrita para um novo segmento é orientado por limiar (threshold), e os segmentos se compactam por nível assim que um nível acumula segmentos suficientes. - Consultas top-k usam limites superiores em nível de bloco, de modo que blocos que não podem contribuir para o resultado sejam inteiramente ignorados, com término antecipado assim que o limite restante não puder superar o conjunto de resultados atual. Um cache em memória pode atender leituras de consulta por velocidade, mas a cadeia durável em disco continua sendo a única fonte de verdade, e os standbys somente leitura sempre leem a cadeia durável em vez de um cache. ## Registre cada mudança de página pelo write-ahead logging padrão, sem criar um mecanismo próprio Registre toda mutação de página por meio do mecanismo genérico de write-ahead logging do banco de dados, em vez de registrar um gerenciador de recursos personalizado para a extensão. Por que essa restrição existe: com o registro genérico, o mecanismo padrão de replay do banco de dados reconstrói cada página durante a recuperação de falha e em um standby de streaming, incluindo as páginas da cadeia do buffer de escrita, as páginas de segmento e a metapágina. É exatamente isso que permite que auxiliares de redo de página única, e qualquer outro contexto de replay que não consiga carregar a biblioteca compartilhada da extensão, funcionem corretamente. Consequências a respeitar: - Introduzir um gerenciador de recursos personalizado é uma decisão de design importante, não um detalhe de implementação. Isso quebra todo contexto de replay que não consegue carregar a extensão. Nunca introduza um implicitamente como parte de uma alteração não relacionada; levante isso como uma proposta separada, com sua própria revisão. - Qualquer nova estrutura em disco deve ser reconstruível pelo replay padrão apenas a partir dos registros logados. Se uma estrutura só pode ser reconstruída pelo código da extensão, o design está errado. - Nunca contorne o gerenciador de buffer nem escreva páginas fora de uma mutação logada, mesmo para uma estrutura que você considere descartável. ## Segure as páginas liberadas por uma fusão até o standby parar de lê-las Uma fusão (merge) não deve devolver imediatamente as páginas de origem deslocadas ao mapa de espaço livre. Fazer isso é seguro no primário, mas inseguro para consultas de leitura em andamento em um standby ativo, porque, sem um gerenciador de recursos personalizado, não há mecanismo para resolver conflitos de recuperação durante o replay: o standby reutilizaria páginas que uma consulta em execução ainda está lendo. O padrão exigido: - Estacione as páginas deslocadas em uma cadeia de lápides (tombstones) logada, ancorada na metapágina, carimbada com o identificador de transação da fusão. - Devolva as páginas estacionadas ao mapa de espaço livre somente quando um vacuum posterior, ou a próxima fusão, observar que o carimbo antecede o horizonte de transação mais antiga não removível. Esse horizonte é o limite de liberação seguro para standby. - Exija que o feedback de standbys ativos que atendem consultas esteja habilitado na implantação. O snapshot mais antigo deles então retém o horizonte do primário até que terminem de ler as páginas estacionadas. - Exponha uma função de diagnóstico que reporte quantas páginas estão atualmente estacionadas, para que os operadores possam observar o crescimento do acúmulo em vez de adivinhar. Generalize o princípio: quando um componente libera um recurso compartilhado que um leitor remoto ou atrasado ainda pode estar segurando, adie a liberação atrás de um horizonte observável e dê aos operadores uma forma de medir o conjunto adiado. ## Ordene pela pontuação de ranqueamento em vez de filtrar por ela As pontuações de ranqueamento existem para ordenação, não para filtragem. Seus valores numéricos são opacos e não carregam um limiar significativo, então comparar uma pontuação com uma constante é um antipadrão sempre que usado como filtro de conveniência em testes, exemplos ou documentação. Regras: - Não escreva uma condição de filtro que compare uma pontuação de ranqueamento com um número para selecionar linhas "correspondentes". - Uma condição de filtro contendo o operador de ranqueamento dispara pontuação isolada (standalone) por meio de uma varredura sequencial, em vez de uma varredura de índice. Varreduras de índice exigem uma cláusula de ordenação. - O padrão correto é ordenar pela expressão de ranqueamento e aplicar um limite de linhas. - Para contar documentos correspondentes, envolva uma subconsulta ordenada e conte suas linhas, em vez de filtrar pela pontuação. - Exceção: um teste cujo alvo explícito é o caminho de pontuação isolada, por exemplo, um que compara resultados de varredura de índice com resultados isolados, pode usar o operador em uma condição de filtro. Esses testes devem declarar essa intenção em um comentário, para que um leitor posterior não copie o padrão por engano. A lição geral: quando uma API tem uma forma pretendida, torne a forma errada visivelmente errada em todo exemplo que a equipe publica, e abra exceções apenas com intenção documentada. ## Documente cada configuração em uma única tabela com um padrão seguro Todo parâmetro ajustável em tempo de execução é documentado em uma única tabela, com seu nome, descrição e valor padrão. Um parâmetro ajustável não documentado é tratado como defeito. - Escolha padrões que sejam seguros e neutros em comportamento para uma implantação comum. Use zero para significar "desabilitado" em configurações do tipo limiar e "ilimitado" em configurações do tipo orçamento, e declare isso na descrição. - Configurações que injetam falhas ou forçam erros existem apenas para testes, devem ser restritas a usuários privilegiados e devem ser rotuladas como somente-para-teste em sua descrição. - Chaves de log para detalhe de pontuação, estatísticas de otimização e resultados de cache vêm desativadas por padrão; são auxiliares de diagnóstico, não operação normal. - Orçamentos de memória são escalonados em níveis, em vez de um único limite rígido: um teto flexível por índice degrada aquele índice para o caminho de fallback durável, um teto flexível global expulsa o maior consumidor além do chamador, e um teto rígido global recusa novas alocações completamente. Cada nível deve ser observável nos logs. - Opções em nível de índice, como a configuração de análise de texto e os parâmetros de saturação e normalização de comprimento do modelo de ranqueamento, são documentadas com o mesmo rigor, e opções obrigatórias são marcadas como obrigatórias, em vez de receberem um padrão silencioso. ## Marque documentos mortos e registre que as estatísticas ficam desatualizadas até a próxima fusão O vacuum marca documentos mortos em um conjunto de bits "vivo" por segmento, um bit por documento, em vez de reconstruir o segmento. O custo é proporcional ao número de documentos mortos, e não ao corpus inteiro. Documentos mortos são filtrados durante a pontuação e removidos fisicamente na próxima fusão de segmento. O compromisso conhecido deve permanecer documentado, não oculto: marcar documentos como mortos não atualiza a contagem total de documentos do segmento, as estatísticas de comprimento total nem a frequência de documentos por termo. O ranqueamento, portanto, usa estatísticas de corpus levemente desatualizadas até que a próxima fusão ou compactação as corrija. Para pequenos números de exclusões, o efeito é insignificante; uma exclusão em massa pode afetar visivelmente a qualidade do ranqueamento até a próxima fusão. Trate isso como um modelo. Quando uma otimização troca precisão ou atualidade por custo, registre o compromisso onde o mantenedor o encontrará, declare as condições sob as quais ele se torna significativo e nomeie o evento que o resolve. Uma aproximação não documentada se torna um relatório de bug inexplicado mais tarde. ## Mantenha níveis de teste separados e funções de diagnóstico próprias Mantenha níveis de teste distintos e saiba o que cada um protege: - Testes de regressão declarativos para funcionalidade principal, correção de pontuação, armazenamento e segmentos, caminhos de otimização de consulta, variantes de tabela como tabelas particionadas e herdadas, e casos extremos, com saídas de referência mantidas sob controle de versão. - Testes orientados por script para concorrência multi-sessão, recuperação de falhas, comportamento de segmento com múltiplos backends e estresse de longa duração. A integração contínua cobre toda versão principal de banco de dados suportada, builds com sanitizadores, validação de formatação, testes de atualização de versão, empacotamento de release, benchmarks e uma execução noturna de estresse com detecção de vazamento de memória. Entregue diagnósticos como funções de primeira classe, não como impressões improvisadas: despeje a estrutura interna com seu dicionário de termos, listas de postings e entradas de documentos; despeje o índice completo em um arquivo; resuma estatísticas de alto nível; force um transbordamento (spill) e reporte quantas entradas foram movidas; reporte a contagem de páginas estacionadas; e renderize uma visualização do layout de página. Mantenha os benchmarks reproduzíveis: uma pequena coleção clássica de recuperação de informação para sinal rápido, conjuntos de dados públicos maiores para escala, um script de estresse de memória e capacidade, e uma comparação com um mecanismo externo consolidado para validar que os resultados não são apenas autoconsistentes. ## Use o formatador automático e faça rebase na main antes do push - A formatação é automatizada. Forneça e use comandos para formatar a árvore, verificar a formatação sem modificar, mostrar as diferenças e formatar um único arquivo. Nunca formate manualmente contra a ferramenta. - Siga as convenções de codificação e os padrões de alocação de memória do projeto de banco de dados hospedeiro, em vez de inventar convenções locais, e quebre linhas na largura configurada do projeto. - Invariantes de concorrência: toda estrutura compartilhada é protegida por um bloqueio apropriado, estruturas de interning são tornadas seguras por meio de bloqueios de tabela hash, e o isolamento de transação é mantido por meio de limpeza disciplinada tanto no caminho de commit quanto no de abort. - Depois que um pull request é mesclado, sempre verifique se o branch principal avançou, mesmo quando outra pessoa realizou a mesclagem. Faça fetch e rebase dos branches ativos sobre o main atualizado para evitar acumular conflitos. - Antes de fazer push, verifique se o branch está atualizado em relação ao main, para que históricos divergentes não transformem uma pequena alteração em uma mesclagem repleta de conflitos. - Siga o procedimento de release documentado do repositório, em vez de improvisar tags ou artefatos.