Dia 9 — Alterar a estrutura depois de criada

Informatica · Conteudo · publicado em 05/10/2026
Dia 9 de 15

Alterar a estrutura depois de criada

Aula 1

ALTER TABLE e a coluna nova

ALTER TABLE e a coluna nova

A tabela que o aplicativo cria na versão 1 do código vai precisar de uma coluna que ninguém previu. ALTER TABLE é o comando que muda a estrutura depois que a tabela existe — e ele tem um limite importante: no SQLite, uma operação por comando.

ALTER TABLE tarefas ADD COLUMN prioridade INTEGER NOT NULL DEFAULT 0;

O ADD COLUMN acrescenta a coluna ao fim da tabela e preenche as linhas existentes com o DEFAULT. Sem DEFAULT e sem NOT NULL, a coluna nova entra preenchida com null.

É o ALTER TABLE que faz a alterar estrutura que o dia 9 chama de migração de banco: o código mudou, e o arquivo gravado no aparelho não mudou junto. Sem o comando que leva um ao outro, o aplicativo de hoje abre um arquivo de ontem e falha na consulta.

O DEFAULT não é detalhe de estilo aqui, e sim a diferença entre a coluna nova utilizável e a coluna nova que quebra a consulta. Uma coluna INTEGER NOT NULL sem DEFAULT, acrescentada em uma tabela com mil linhas, entra preenchida com null em todas elas, e qualquer WHERE prioridade = 0 ignora as mil. Com DEFAULT 0, a linha antiga entra no mesmo estado da linha nova, e o aplicativo não precisa distinguir as duas.

O limite de uma operação por comando aparece em código como este, que tentaria fazer duas coisas de uma vez e não é aceito:

ALTER TABLE tarefas DROP COLUMN prazo, ADD COLUMN prioridade INTEGER;

Os outros dois comandos:

ALTER TABLE tarefas RENAME TO lista;   -- muda o nome da tabela
ALTER TABLE tarefas DROP COLUMN prazo; -- remove a coluna

O RENAME TO é seguro quando a tabela tem chave estrangeira apontando para ela, porque o SQLite reescreve a referência junto. O DROP COLUMN é o perigoso: o dado some do arquivo sem backup, e se a tabela for a mesma que a consulta do aplicativo espera, o erro aparece na próxima leitura, com mensagem de coluna inexistente.

A coluna antiga é o assunto real do DROP COLUMN: ela some do arquivo, e o código que ainda a lê quebra na consulta. O sintoma é o mesmo da coluna escrita errada no UPDATE do dia 6, visto do outro lado — lá o dado foi para uma coluna que ninguém lê, aqui a coluna some e o código continua lendo.

PRAGMA table_info confere a estrutura real

O ALTER TABLE que roda no aparelho é o que vale; a definição no código é o que o aplicativo acha que existe. A diferença entre as duas é o schema gravado, e é a coluna antiga que existe em um e não no outro.

PRAGMA table_info(tarefas);

Cada linha devolvida traz cid, name, type, notnull, dflt_value e pk. Comparar essa lista com a lista que o código espera é o teste que diz, em tempo de execução, se o arquivo em disco é a versão que o aplicativo sabe ler.

PerguntaComando
A coluna nova já existe?PRAGMA table_info
O que tem gravado na tabela?SELECT * FROM tarefas LIMIT 1
Quantas linhas tem?SELECT COUNT(*) FROM tarefas

O versionamento do schema é o que impede essa divergência de virar erro em campo: o código declara a estrutura que espera, e o PRAGMA diz qual estrutura está no arquivo. A aula seguinte transforma essa comparação em procedimento — comparar, decidir o que falta, e gravar que foi resolvido.

O exemplo desta página faz a sequência inteira e imprime cada etapa: o CREATE TABLE inicial, o ALTER TABLE com o ADD COLUMN, a lista de colunas do arquivo depois da alteração com a nova no fim, e o valor que as linhas antigas ganharam. Fecha com o DROP COLUMN, que reduz a lista de colunas e deixa a linha sem o campo — o caminho de volta, que é irreversível.

Exemplo

// `ALTER TABLE` muda a estrutura depois que a tabela existe. No SQLite e
// uma operacao por comando, e coluna nova entra sempre com `DEFAULT`.
const estrutura = [
  { nome: 'id', tipo: 'INTEGER', obrigatoria: true, padrao: null, chave: true },
  { nome: 'titulo', tipo: 'TEXT', obrigatoria: true, padrao: null, chave: false },
  { nome: 'prazo', tipo: 'TEXT', obrigatoria: true, padrao: null, chave: false },
  { nome: 'feita', tipo: 'INTEGER', obrigatoria: false, padrao: 0, chave: false },
];

function emSql(colunas) {
  return colunas
    .map((c) => c.nome + ' ' + c.tipo + (c.chave ? ' PRIMARY KEY AUTOINCREMENT' : '')
      + (c.obrigatoria ? ' NOT NULL' : '') + (c.padrao !== null ? ' DEFAULT ' + c.padrao : ''))
    .join(', ');
}

console.log('CREATE TABLE tarefas (' + emSql(estrutura) + ');');

// o arquivo do aparelho tem a versao 1 do schema
const arquivo = { colunas: estrutura.map((c) => ({ ...c })), linhas: [
  { id: 1, titulo: 'Revisar o WHERE', prazo: '2026-09-10', feita: 0 },
  { id: 2, titulo: 'Ler o capitulo 4', prazo: '2026-09-12', feita: 1 },
] };

function tableInfo(colunas) {
  return colunas.map((coluna, posicao) => ({
    cid: posicao,
    name: coluna.nome,
    type: coluna.tipo,
    notnull: coluna.obrigatoria ? 1 : 0,
    dflt_value: coluna.padrao,
    pk: coluna.chave ? 1 : 0,
  }));
}

// 1. a coluna nova: entra ao fim e preenche com o DEFAULT
const nova = { nome: 'prioridade', tipo: 'INTEGER', obrigatoria: true, padrao: 0, chave: false };
console.log('ALTER TABLE tarefas ADD COLUMN prioridade INTEGER NOT NULL DEFAULT 0;');
arquivo.colunas.push(nova);
arquivo.linhas = arquivo.linhas.map((linha) => ({ ...linha, prioridade: nova.padrao }));
console.log('colunas do arquivo depois do ADD COLUMN:', tableInfo(arquivo.colunas).map((c) => c.name).join(', '));
console.log('as linhas antigas ganharam prioridade =', arquivo.linhas.map((l) => l.prioridade));

// 2. sem DEFAULT e sem NOT NULL, a coluna entra com null
const semPadrao = { nome: 'anotacao', tipo: 'TEXT', obrigatoria: false, padrao: null, chave: false };
const comNull = arquivo.linhas.map((linha) => ({ ...linha, anotacao: null }));
console.log('coluna nova sem DEFAULT ->', comNull[0].anotacao);

// 3. `PRAGMA table_info` diz o que o arquivo tem de verdade
console.log('PRAGMA table_info(tarefas):');
for (const coluna of tableInfo(arquivo.colunas)) {
  console.log('  cid', coluna.cid, '|', coluna.name, '|', coluna.type,
    '| notnull', coluna.notnull, '| dflt', coluna.dflt_value, '| pk', coluna.pk);
}

// 4. comparar o arquivo com o que o codigo espera
const esperadas = ['id', 'titulo', 'prazo', 'feita', 'prioridade'];
const existentes = tableInfo(arquivo.colunas).map((c) => c.name);
const faltando = esperadas.filter((nome) => !existentes.includes(nome));
console.log('colunas que o codigo espera:', esperadas.length);
console.log('faltando no arquivo:', faltando.length === 0 ? 'nenhuma' : faltando.join(', '));

// 5. `DROP COLUMN`: o dado some do arquivo, sem backup
const antesDoDrop = arquivo.colunas.length;
arquivo.colunas = arquivo.colunas.filter((c) => c.nome !== 'prioridade');
arquivo.linhas = arquivo.linhas.map(({ prioridade, ...resto }) => resto);
console.log('DROP COLUMN prioridade: colunas', antesDoDrop, '->', arquivo.colunas.length);
console.log('a linha ficou sem a coluna:', Object.keys(arquivo.linhas[0]).join(', '));

Saída real

CREATE TABLE tarefas (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, titulo TEXT NOT NULL, prazo TEXT NOT NULL, feita INTEGER DEFAULT 0);
ALTER TABLE tarefas ADD COLUMN prioridade INTEGER NOT NULL DEFAULT 0;
colunas do arquivo depois do ADD COLUMN: id, titulo, prazo, feita, prioridade
as linhas antigas ganharam prioridade = [ 0, 0 ]
coluna nova sem DEFAULT -> null
PRAGMA table_info(tarefas):
  cid 0 | id | INTEGER | notnull 1 | dflt null | pk 1
  cid 1 | titulo | TEXT | notnull 1 | dflt null | pk 0
  cid 2 | prazo | TEXT | notnull 1 | dflt null | pk 0
  cid 3 | feita | INTEGER | notnull 0 | dflt 0 | pk 0
  cid 4 | prioridade | INTEGER | notnull 1 | dflt 0 | pk 0
colunas que o codigo espera: 5
faltando no arquivo: nenhuma
DROP COLUMN prioridade: colunas 5 -> 4
a linha ficou sem a coluna: id, titulo, prazo, feita
Aula 2

A migração que roda ao abrir o banco

A migração que roda ao abrir o banco

O banco que está no aparelho foi criado por uma versão anterior do aplicativo. A migração é o caminho que leva esse arquivo até a estrutura que o código de hoje espera, sem perder o dado que o usuário gravou.

O SQLite guarda um número inteiro dentro do próprio arquivo para dizer qual versão do schema está gravada: PRAGMA user_version. Ele começa em 0 num arquivo novo, e cada migração concluída grava o próximo número.

PRAGMA user_version;                  -- le a versao gravada
PRAGMA user_version = 2;              -- grava a versao

O número fica dentro do arquivo, e não no código nem em um arquivo do lado — essa é a propriedade que faz o mecanismo funcionar. O aplicativo não precisa adivinhar a versão: ele lê, e o arquivo responde.

O algoritmo completo cabe em dez linhas: ler a versão, rodar as migrações que faltam, gravar a nova versão.

async function migrar(banco) {
  const [versao] = await banco.executeSql('PRAGMA user_version');
  const atual = versao.rows[0].user_version;

  if (atual < 1) {
    await banco.executeSql('CREATE TABLE IF NOT EXISTS tarefas (...)');
    await banco.executeSql('PRAGMA user_version = 1');
  }
  if (atual < 2) {
    await banco.executeSql('ALTER TABLE tarefas ADD COLUMN prioridade INTEGER DEFAULT 0');
    await banco.executeSql('PRAGMA user_version = 2');
  }
  return atual;
}

O desenho é uma lista de migrações numeradas, e cada uma roda quando o arquivo está abaixo da versão que ela aplica. É assim que se executar migração: a migrar na abertura acontece uma vez, logo depois do openDatabase, antes de qualquer tela consultar o banco. Com o número gravado no arquivo, a primeira execução e a décima separem pelo mesmo código.

O detalhe da ordem é o que impede a migração de correr duas vezes: o if compara a versão do arquivo com a versão da migração, e a gravação da versão é a confirmação de que ela rodou. Como o número só anda para a frente, a única situação em que a migração 2 roda de novo é se ela não conseguiu gravar a versão 2.

Por que a versão se grava no fim

A ordem importa. Se o PRAGMA user_version = 2 for gravado antes do ALTER TABLE e o aparelho morrer no meio, o arquivo fica marcado como versão 2 sem ter a coluna — e a próxima abertura pula a migração, e o aplicativo quebra com "no such column" para sempre. Gravando a versão por último, a falha deixa o arquivo marcado como versão 1, e a próxima abertura tenta de novo.

É o que transforma a falha em migração pendente em vez de dano permanente. O aparelho morrer no meio de uma migração é raro, e o custo de proteger esse caso é uma linha na ordem certa.

A mesma razão exige que a migração inteira rode dentro de uma transação: ou o ALTER TABLE e a gravação da versão acontecem, ou nenhum dos dois. Com a transação, o arquivo não fica no estado intermediário em que a coluna não existe mas a versão já diz que existe.

O exemplo desta página mostra os quatro estados do arquivo. Na primeira execução, o arquivo nasce na versão 0 e as três migrações rodam em ordem, terminando na versão 3. Com o aplicativo reaberto já na versão 3, nada pendente roda e a tarefa do usuário continua como estava. Com o arquivo parado na versão 1, a segunda e a terceira migração rodam — que é o abrir e migrar depois de uma falha.

A alternativa: tabela de migrações

PRAGMA user_version guarda um número só. Quando a migração precisa de nome — "adicionei a coluna prioridade", "criei o índice por prazo" — a forma comum é uma tabela própria:

CREATE TABLE IF NOT EXISTS migracoes (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  nome TEXT NOT NULL UNIQUE,
  aplicada_em TEXT NOT NULL
);

A consulta que decide o que falta é um SELECT nome FROM migracoes, e o que falta é tudo que está no código e não está nessa lista. Custa uma tabela a mais e dá nome, histórico e data — o que só importa depois da terceira migração.

O schema_version é o nome que a tabela costuma receber quando ela existe no lugar do PRAGMA, e a escolha entre os dois é de granularidade: o user_version sabe "a versão 3 está aplicada", e não sabe o que a versão 3 fez. Quando a pergunta passa a ser "por que meu aparelho está com a coluna e o do colega não", a tabela de migrações é a que responde — e o INSERT com nome e aplicada_em é o registro que responde.

O exemplo fecha mostrando esse registro: o nome de cada migração e a data em que foi aplicada, e o INSERT que grava uma delas.

Exemplo

// A migracao roda ao abrir o banco. `PRAGMA user_version` guarda, dentro
// do proprio arquivo, qual versao do schema esta gravada.
const BANCO_NOVO = 0;

function abrirBanco(nomeArquivo) {
  // um "arquivo": o schema gravado e as linhas
  return {
    arquivo: nomeArquivo,
    userVersion: 0,
    colunas: [],
    linhas: [],
    registro: [],
  };
}

// as migracoes do codigo, em ordem. Cada uma so roda se a versao do
// arquivo for menor que a versao que ela aplica.
const migracoes = [
  {
    paraVersao: 1,
    nome: 'criar tabela tarefas',
    aplicar(banco) {
      banco.colunas = ['id', 'titulo', 'prazo', 'feita'];
      banco.linhas.push({ id: 1, titulo: 'Revisar o WHERE', prazo: '2026-09-10', feita: 0 });
      banco.linhas.push({ id: 2, titulo: 'Ler o capitulo 4', prazo: '2026-09-12', feita: 1 });
    },
  },
  {
    paraVersao: 2,
    nome: 'add column prioridade',
    aplicar(banco) {
      banco.colunas.push('prioridade');
      banco.linhas = banco.linhas.map((linha) => ({ ...linha, prioridade: 0 }));
    },
  },
  {
    paraVersao: 3,
    nome: 'criar indice por prazo',
    aplicar(banco) {
      banco.registro.push('CREATE INDEX idx_tarefas_prazo ON tarefas (prazo);');
    },
  },
];

function migrar(banco) {
  const rodada = [];
  for (const migracao of migracoes) {
    if (banco.userVersion >= migracao.paraVersao) {
      rodada.push(migracao.paraVersao + ': ja estava');
      continue;
    }
    // o passo real e a gravacao da versao; se o aparelho morrer entre os
    // dois, a proxima abertura tenta de novo
    migracao.aplicar(banco);
    banco.userVersion = migracao.paraVersao;
    rodada.push(migracao.paraVersao + ': ' + migracao.nome);
  }
  return rodada;
}

// 1. primeira execucao: o arquivo nasce na versao 0 e roda tudo
const novo = abrirBanco('app.db');
console.log('arquivo recem-criado, PRAGMA user_version =', novo.userVersion);
console.log('rodou:', migrar(novo).join(' | '));
console.log('user_version depois:', novo.userVersion);
console.log('colunas no arquivo:', novo.colunas.join(', '));
console.log('linhas preservadas:', novo.linhas.length, '| prioridade de cada uma:', novo.linhas.map((l) => l.prioridade));

// 2. segunda execucao: nada pendente, e o dado do usuario continua la
const usuario = abrirBanco('app.db');
usuario.userVersion = 3;
usuario.colunas = ['id', 'titulo', 'prazo', 'feita', 'prioridade'];
usuario.linhas = [{ id: 7, titulo: 'Tarefa do usuario', prazo: '2026-10-01', feita: 0, prioridade: 2 }];
console.log('\naplicativo reaberto na versao 3:');
console.log('rodou:', migrar(usuario).join(' | '));
console.log('a tarefa do usuario continua:', usuario.linhas[0].titulo, '| prioridade', usuario.linhas[0].prioridade);

// 3. a falha no meio: a versao antiga fica, e a proxima abertura tenta
const pelaMetade = abrirBanco('app.db');
pelaMetade.userVersion = 1;
pelaMetade.colunas = ['id', 'titulo', 'prazo', 'feita'];
console.log('\narquivo parado na versao', pelaMetade.userVersion);
console.log('rodou:', migrar(pelaMetade).join(' | '));
console.log('terminou na versao', pelaMetade.userVersion);

// 4. a alternativa: tabela de migracoes, com nome e data
const historico = [];
for (const migracao of migracoes) {
  historico.push({ nome: migracao.nome, aplicada_em: '2026-09-05' });
}
console.log('\ntabela de migracoes:');
for (const linha of historico) console.log(' ', linha.nome, '|', linha.aplicada_em);
console.log('INSERT INTO migracoes (nome, aplicada_em) VALUES (?, ?);',
  JSON.stringify(historico[0].nome), "'2026-09-05'");

Saída real

arquivo recem-criado, PRAGMA user_version = 0
rodou: 1: criar tabela tarefas | 2: add column prioridade | 3: criar indice por prazo
user_version depois: 3
colunas no arquivo: id, titulo, prazo, feita, prioridade
linhas preservadas: 2 | prioridade de cada uma: [ 0, 0 ]

aplicativo reaberto na versao 3:
rodou: 1: ja estava | 2: ja estava | 3: ja estava
a tarefa do usuario continua: Tarefa do usuario | prioridade 2

arquivo parado na versao 1
rodou: 1: ja estava | 2: add column prioridade | 3: criar indice por prazo
terminou na versao 3

tabela de migracoes:
  criar tabela tarefas | 2026-09-05
  add column prioridade | 2026-09-05
  criar indice por prazo | 2026-09-05
INSERT INTO migracoes (nome, aplicada_em) VALUES (?, ?); "criar tabela tarefas" '2026-09-05'