Reduzindo as chances de errar um squash de migrações no Hasura
Tags: graphql, database, hasura, migrations
Published: 2021-12-15 15:55:59

#Sumário
1. [Versão das tecnologias](#versão-das-tecnologias)
2. [Requisitos mínimos de conhecimento para entender o artigo](#requisitos-mínimos-de-conhecimento-para-entender-o-artigo)
3. [Por quê deste artigo](#por-quê-deste-artigo)
4. [Para os apressados](#para-os-apressados)
5. [Exemplo de base de dados](#exemplo-de-base-de-dados)
6. [Processo de fazer um squash](#processo-de-fazer-um-squash)
7. [Conclusão](#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

- Entender de comandos SQL
- Como migrações funcionam
- Como [Hasura GraphQL Engine](https://hasura.io/docs/latest/graphql/core/getting-started/index.html#getting-started) funciona
- Como [Git](https://git-scm.com/) funciona

##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:

```sql
CREATE TABLE "public"."students" (
    "id" serial NOT NULL,
    "name" Text NOT NULL,
    "score" text NOT NULL,
    PRIMARY KEY ("id") 
);
```

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](https://hasura.io/docs/latest/graphql/core/migrations/migrations-setup.html) 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g1goo3fgozuc3tasrzz1.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6iop7eaqomy9s2yt4u2o.png)
 
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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7euulbm2lsd9aq5v9gm2.png)

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:

```sql
UPDATE students
SET score_decimal=CAST(score AS DECIMAL)
WHERE students.id = students.id;
```

![Aba SQL com consulta preenchida no campo de texto e função de migração ativada](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7izhheuev7kw4gpd6ugo.png)

**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`.

```sql
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";
```

![Aba SQL com consulta preenchida no campo de texto e função de migração ativada](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m85kjzac5sndhxbljb4k.png)

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:

```sql
-- 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";
```

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

```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";
```

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

```sql
ALTER TABLE "public"."students" DROP COLUMN "score_decimal";
```

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:
```bash
hasura migrate apply --down <N>
```
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: 
```bash
hasura migrate squash --from <num_migracao> --name <name>
```
Ele executa a compressão dos arquivos das migrações para nós. No meu caso, farei dessa forma: 
```bash
hasura migrate squash --from 1638852925395 --name "changing_column_type"
``` 
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:
```bash
hasura migrate apply --skip-execution
```
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.