DEV Community

Cover image for Reduzindo as chances de errar um squash de migrações no Hasura
Fernando Karchiloff
Fernando Karchiloff

Posted on

Reduzindo as chances de errar um squash de migrações no Hasura

Sumário

  1. Versão das tecnologias
  2. Requisitos mínimos de conhecimento para entender o artigo
  3. Por quê deste artigo
  4. Para os apressados
  5. Exemplo de base de dados
  6. Processo de fazer um squash
  7. Conclusão

Versão das tecnologias

  • Hasura v2.0.9
  • Hasura CLI v2.0.9
  • PostgreSQL 12

Requisitos mínimos de conhecimento para entender o artigo

Por quê deste artigo

Durante o desenvolvimento de um dos projetos de uma empresa de e-commerce, tivemos uma tarefa que seria necessário mudar a estrutura do banco de dados, esta alteração mexeria com diversas tabelas e colunas para que o objetivo da tarefa fosse concluído.

Como o Hasura GraphQL Engine oferece uma interface para fazer as mudanças na estrutura do banco de dados, modifiquei por meio desta e executei as queries manuais que se mostraram necessárias, todas essas modificações criavam migrações no repositório do projeto, assim como esperado para um projeto com migrações habilitadas no Hasura.

Após as modificações, era notável a quantidade de pastas com modificações que foram sendo gravadas no formato de migrações, decidi então usar o comando para comprimir todas elas em um único arquivo, o que seria mais legível, e minha falha se concretizou no momento que executei o comando.

Sem ter feito validações básicas antes, executei o comando e excluí as migrações anteriores. E assim passei mais de um dia tentando corrigir o erro que cometi por não fazer uma lista de verificação básica antes de usar o comando. Como evitar isso? Vou te explicar no decorrer do artigo.

Para os apressados

  1. Faça as suas modificações usando migrações.
  2. Usando Git ou outro versionador, faça um commit para salvar as migrações.
  3. Garanta que os arquivos de up.sql e down.sql executam como deveriam usando hasura migrate apply e hasura migrate apply --down <N>. Correções manuais (caso erros aconteçam), faça pela aba SQL sem usar migrações.
  4. Salve as migrações completas no versionador.
  5. Use o comando hasura migrate squash --from <num_migracao> --name <name> para executar o squash, remova as migrações anteriores (pois você já salvou no versionador).
  6. Marque a migração que foi criada como preenchida no banco, usando hasura migrate apply --skip-execution (se as que serão removidas não haviam sido aplicadas, não use a flag).
  7. Use novamente o hasura migrate apply --down <N> para retirar, e sem a flag para colocar as migrações, garantindo que sua migração funciona.
  8. Grave as alterações no versionador.
  9. (Opcional) Faça otimizações de comandos SQL se achar necessário, exemplo: remover ALTER TABLE caso exista um DROP TABLE para uma tabela (sem perda de dados).

Exemplo de base de dados

Para demonstrar como fazer um squash corretamente, irei utilizar uma base de dados de exemplo simples com uma única tabela, mas no caso real você pode ter diversas operações entre N tabelas, o que pode tornar o squash maior e mais complexo.

A nossa base contém a tabela students definida por meio desta estrutura:

CREATE TABLE "public"."students" (
    "id" serial NOT NULL,
    "name" Text NOT NULL,
    "score" text NOT NULL,
    PRIMARY KEY ("id") 
);
Enter fullscreen mode Exit fullscreen mode

Ela foi criada depois que já executamos a conexão com a nossa base de dados criada no PostgreSQL, desativamos o console e iniciamos as migrações no projeto. Você pode ver mais detalhes de como ativar migrações nesta seção de Migrations & Metadata (CI/CD) na documentação do Hasura GraphQL Engine.

Digamos então que esta tabela foi preenchida com alguns dados de estudantes:
Tabela com três colunas chamadas: "id", "name", "score" e alguns dados preenchidos

No meio do caminho, percebemos que os dados das notas localizados na coluna score, não poderiam ser textos, pois gostaríamos de fazer algumas aritméticas com eles. Entretanto, não podemos mudar estes dados caso estejam em uso no ambiente de produção! (Suponha que você tenha milhares de dados importantes e não pode perdê-los)

O que fazer então? No ambiente de desenvolvimento, iremos criar uma coluna para acomodar esses dados, vamos nomeá-la score_decimal, pois representa a nota em número decimal permitindo operações aritméticas. Vá até à aba Modify da tabela students e clique em Add new column. Vamos definir como uma coluna Numeric.

Aba de Modify selecionada e com opção de adicionar nova coluna aparecendo com dados preenchidos

Uma vez criada a nova coluna, podemos fazer a migração dos dados da coluna score para score_decimal. Para isso ser possível, vamos usar a seção de SQL que fica logo abaixo das tabelas da nossa base de dados.

Bancos de dados sendo exibidos na aba de cima e logo abaixo a aba de SQL

Nessa aba você pode rodar código SQL diretamente contra o banco de dados selecionado, podendo marcar estas consultas como migrações ou não. Dependendo do código, o Hasura talvez seja inteligente para identificar ser uma alteração estrutural ou de dados e marque como migração automaticamente, então sempre preste atenção antes de rodar a consulta para ver se a caixa de "This is a migration" está habilitada.

Colocando a consulta que converte as notas no campo de texto, podemos rodar sem adicionar como migração com intenção de verificar se a consulta está sendo executada como descrita. Após a verificação, podemos ativar a caixa de migração e escrever um nome para ela, usei um nome descritivo cast_students_score_to_decimal_migration.
Esta é a consulta feita para converter os dados:

UPDATE students
SET score_decimal=CAST(score AS DECIMAL)
WHERE students.id = students.id;
Enter fullscreen mode Exit fullscreen mode

Aba SQL com consulta preenchida no campo de texto e função de migração ativada

OBSERVAÇÃO: sempre tome cuidado para fazer alterações estruturais que mudam categorias de campos, então faça por partes e vá atualizando a camada de visualização (front-end), caso sejam utilizadas, para mudar o novo tipo sem quebrar a aplicação para os clientes.

Após rodar a atualização dos dados, podemos renomear as colunas e deletar a antiga. O nome usado para migração foi rename_columns_and_remove_old_migration.

ALTER TABLE students RENAME COLUMN "score" TO "score_text";
ALTER TABLE students RENAME COLUMN "score_decimal" TO "score";
ALTER TABLE students DROP COLUMN "score_text";
Enter fullscreen mode Exit fullscreen mode

Aba SQL com consulta preenchida no campo de texto e função de migração ativada

Com isso, finalizamos nossa modificação para executarmos nossas operações aritméticas, mas acabamos com algumas migrações separadas que podem ser unidas através de uma operação de squash, pois podem ser executadas todas juntas.

Processo de fazer um squash

Para que o processo de squash das migrações ocorra "corretamente" (sempre pode ocorrer algum erro inesperado no meio do processo), precisamos garantir que algumas etapas sejam seguidas. Vou listar algumas delas para evitar erros desse processo.

  1. Gravar as migrações feitas no repositório
  2. Garantir que as migrações podem ser feitas e desfeitas sem erros
  3. Gravar as migrações completas
  4. Fazer o squash das migrações
  5. Marcar a migração como executada
  6. Grave as alterações
  7. (Opcional) Modifique a migração comprimida como desejar (e grave as alterações)

1. Gravar as migrações feitas

Para mantermos controle das nossas migrações e não perdermos o progresso feito, gravamos as alterações em um commit do versionador, podemos então prosseguir em alterá-las sem risco de perder o que já fizemos, esse foi um dos erros que cometi anteriormente, não ter salvo no versionador.

2. Garanta que as migrações podem ser feitas e desfeitas sem erros

Com as migrações salvas no repositório, podemos começar a modificação das que possuem arquivos down.sql incompletos. Muitas das alterações que executamos no banco de dados, nem sempre refletem nos arquivos por serem modificações complexas, como a renomeação das colunas e deleção de coluna na mesma migração. Por exemplo, a última migração, o arquivo down.sql foi preenchido desta forma:

-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- ALTER TABLE students RENAME COLUMN "score" TO "score_text";
-- ALTER TABLE students RENAME COLUMN "score_decimal" TO "score";
-- ALTER TABLE students DROP COLUMN "score_text";
Enter fullscreen mode Exit fullscreen mode

Para que a migração funcione na volta, ou seja, remover as mudanças, vamos inverter os comandos feitos no up.sql.

ALTER TABLE students ADD COLUMN "score_text" text;
ALTER TABLE students RENAME COLUMN "score" TO "score_decimal";
ALTER TABLE students RENAME COLUMN "score_text" TO "score";
Enter fullscreen mode Exit fullscreen mode

Faremos o mesmo com a migração que cria a nova coluna e inverteremos o comando.

ALTER TABLE "public"."students" DROP COLUMN "score_decimal";
Enter fullscreen mode Exit fullscreen mode

Com isso, podemos usar as flags de --up e --down do hasura migrate apply. O comando altera o banco de dados conforme os .sql definidos nas migrações. Se você relacionar os arquivos aos seus respectivos comandos, fará mais sentido, o comando que usa --down vai remover as alterações de migrações já aplicadas, enquanto o --up irá aplicá-las (o --up é aplicado por padrão se não for definido). Para remover somente algumas, use:

hasura migrate apply --down <N>
Enter fullscreen mode Exit fullscreen mode

No comando, N é a quantidade de migrações que serão removidas. Use hasura migrate apply --help para mais detalhes.

Observação: se você executou alguma migração e ela falhou, caso tenha mais de um comando SQL no mesmo arquivo, tente corrigir pela aba SQL (sem fazer uma nova), corrigi-la e tentar novamente.

3. Grave as migrações completas

Dado que você testou suas migrações, e elas estão funcionando, grave novamente em um commit, agora com as migrações completas.

4. Fazer o squash das migrações

Com todas as migrações completas e funcionando ida e volta, podemos agora fazer o squash delas, ou seja, comprimi-las em uma única só migração, para isso, vamos rodar o comando:

hasura migrate squash --from <num_migracao> --name <name>
Enter fullscreen mode Exit fullscreen mode

Ele executa a compressão dos arquivos das migrações para nós. No meu caso, farei dessa forma:

hasura migrate squash --from 1638852925395 --name "changing_column_type"
Enter fullscreen mode Exit fullscreen mode

Este comando irá a partir da migração de prefixo 1638852925395 juntar todas em uma única, de nome "changing_column_type". Recomendo você deletar as migrações anteriores pois já estão salvas no repositório.

5. Marcar a migração como executada

Assim que criamos a nossa nova migração comprimida como resultado da operação, caso as migrações anteriores não estivessem aplicadas no banco de dados, vamos rodar o comando para marcar como se a migração que acabamos de gerar, já tivesse sido aplicada:

hasura migrate apply --skip-execution
Enter fullscreen mode Exit fullscreen mode

Lembrando que este comando só deve ser executado se efetivamente aplicamos as alterações das outras migrações no banco de dados, caso contrário, rode o comando sem a flag --skip-execution para aplicar normalmente.

6. Grave as alterações

Feito isso, grave novamente as alterações na versão de controle do repositório para não perder suas alterações.

7. (Opcional) Modifique a migração comprimida como desejar (e grave as alterações)

Se a sua migração possui muitos comandos, avalie o arquivo e verifique se, dependendo das alterações feitas, você pode simplificar o código SQL executado. Por exemplo, você cria uma tabela, e depois em outra migração adiciona uma coluna, caso você não tenha adicionado nenhum dado no meio do caminho, ou não foi para produção ainda, coloque diretamente a coluna na criação da tabela, o que simplificará o código.

Conclusão

Espero que estas instruções sirvam de guia para que não cometa os mesmos erros que eu durante o projeto que estava desenvolvendo. Tentei colocar o máximo de detalhes no passo a passo e seus motivos.

Essas instruções podem ser replicadas em outros projetos, mesmo que não sigam a mesma tecnologia, mas se seus conceitos forem parecidos, é possível. Caso você não aplique imediatamente, ao menos espero que tenha aprendido algo com o que demonstrei neste artigo.

Qualquer crítica positiva que possa incrementar este artigo é bem-vinda! Compartilhe com outras pessoas que precisam deste conteúdo!

Obrigado por sua atenção.

Oldest comments (0)