Dia 6 — Atualizar e apagar
UPDATE e a linha alterada
UPDATE e a linha alterada
UPDATE altera linha que já existe e nunca cria linha nova. A forma completa tem duas partes obrigatórias: o que muda, em SET, e qual linha muda, em WHERE.
UPDATE tarefas SET feita = 1, concluida_em = '2026-09-12' WHERE id = 4;
A atualizar linha é a operação que altera um campo sem criar registro novo, e é ela que a tela faz quando o usuário marca uma tarefa como feita ou edita um prazo. A ordem de escrita é UPDATE tabela SET coluna = valor WHERE condição, e as duas partes têm pesos diferentes: o SET diz o que muda, e o WHERE diz o cuidado com UPDATE — a linha que muda.
Sem WHERE, o comando altera todas as linhas da tabela — a tarefa que o aluno estava marcando como feito marca também as outras nove mil. É o update sem filtro, o comando mais destrutivo do CRUD depois do DELETE, e ele viola o WHERE obrigatório que vale para todo comando que escreve. Não há erro, não há aviso, e o rowsAffected volta dizendo quantas foram, o que é a única pista de que o filtro faltou.
changes é o nome que o rowsAffected recebe em outras bibliotecas de SQLite, e o número é o mesmo: quantas linhas o comando tocou. Ele é a linhas afetadas em forma de campo, e é o que o aplicativo confere antes de dizer que salvou.
O WHERE do UPDATE é id = ?
O filtro do UPDATE quase sempre é o identificador, e o valor vai como parâmetro:
const [resultado] = await banco.executeSql( 'UPDATE tarefas SET feita = ? WHERE id = ?', [1, idDaTarefa] ); const alteradas = resultado.rowsAffected;
Três resultados possíveis, e o aplicativo precisa dos três: rowsAffected igual a 1 deu certo; igual a 0 significa que o id não existe no banco — a linha foi apagada em outro lugar, e a tela precisa recarregar em vez de fingir que salvou; maior que 1 é o filtro genérico que o aplicativo nunca devia ter escrito.
O 0 é o caso que mais aparece em aplicativo real, e ele não é erro do banco: é informação. A linha sumiu porque outra tela apagou, ou porque o aplicativo foi reinstalado e o arquivo recriado. Em ambos os casos a resposta certa é a mesma — recarregar a lista e deixar a tela mostrar a verdade. Mexer no estado local para "consertar" a linha é o que produz a tela que mostra uma tarefa que não existe mais.
O erro de digitar em coluna errada
O SET não recusa coluna que não existe com a mensagem que o aluno espera. No SQLite, SET feia = 1 cria uma coluna nova chamada feia com o valor 1 em todas as linhas, e o aplicativo continua mostrando a coluna feita como estava. O dado ficou gravado e o bug não aparece: a consulta rodou, o rowsAffected voltou 1, e a tela segue errada. Confirme o nome da coluna na definição da tabela antes de escrever o UPDATE.
Esse defeito é o erro de tipo mais difícil de achar no SQL, e ele tem a mesma origem do UPDATE sem filtro: o banco obedece ao texto e não à intenção. O PRAGMA table_info do dia 3 é o que denuncia a coluna que apareceu sozinha — a lista de colunas do arquivo tem uma entrada a mais do que o código do aplicativo conhece.
O UPDATE também acumula: UPDATE tarefas SET feita = 1 WHERE id = 4 executado duas vezes faz o mesmo que uma. A coluna de contagem — vezes_editada, por exemplo — precisa de SET vezes_editada = vezes_editada + 1, e é o único jeito de somar dentro do próprio comando.
A soma dentro do próprio SET é o alterar campo que se acumula: o valor novo é calculado a partir do valor velho, lido pelo banco no momento do comando. É por isso que a coluna entra dos dois lados do sinal de igual — vezes_editada no SET é o que se grava, e vezes_editada dentro da expressão é o que se lê.
Exemplo
// `UPDATE` altera linha que ja existe. O `WHERE` e obrigatorio: sem ele, // o comando altera todas as linhas da tabela. function executarUpdate(banco, sql, valores) { const achado = /UPDATE (\w+) SET (.+) WHERE (\w+) = (.+)$/i.exec(sql); if (!achado) throw new Error('este exemplo so executa UPDATE ... WHERE coluna = valor'); const [, tabela, atribuicoes, colunaFiltro, valorFiltro] = achado; // O valor do `WHERE` chega como texto, porque no SQL tudo o que nao esta // entre aspas e' lido pelo motor. A coluna, ao contrario, guarda o tipo // que o `INSERT` deu a ela. Comparar `1` com `'1'` sem converter nunca // casa, e o `UPDATE` devolve zero linhas afetadas sem erro nenhum. function mesmoTipo(valorDaLinha, valorDoTexto) { if (valorDaLinha === null || valorDaLinha === undefined) return valorDoTexto; if (typeof valorDaLinha === 'number') return Number(valorDoTexto); if (typeof valorDaLinha === 'boolean') return valorDoTexto === '1'; return String(valorDoTexto); } let alteradas = 0; const novasLinhas = banco[tabela].map((linha) => { // o filtro usa o TIPO que a linha ja tem: e assim que o motor compara if (linha[colunaFiltro] !== mesmoTipo(linha[colunaFiltro], valorFiltro)) return linha; const nova = { ...linha }; for (const atribuicao of atribuicoes.split(',')) { const [coluna, valor] = atribuicao.split('=').map((parte) => parte.trim()); // `SET coluna = coluna + 1` le a coluna ANTES de gravar: e a forma de // somar dentro do proprio comando. Sem este caso, o exemplo gravaria // o texto `coluna + 1` no lugar do numero. if (/^(.+?)\s*\+\s*(.+)$/.test(valor) && !valor.includes('=')) { const [, base, soma] = valor.match(/^(.+?)\s*\+\s*(.+)$/); const atual = nova[base.trim()]; if (typeof atual === 'number') { nova[coluna] = atual + Number(soma.trim()); continue; } } // o valor gravado respeita o tipo que a coluna ja tinha nova[coluna] = mesmoTipo(linha[coluna], valor); } alteradas += 1; return nova; }); banco[tabela] = novasLinhas; return [{ rowsAffected: alteradas }]; } const banco = { tarefas: [ { id: 1, titulo: 'Revisar o WHERE', feita: 0, vezes_editada: 0 }, { id: 2, titulo: 'Ler o capitulo 4', feita: 1, vezes_editada: 0 }, { id: 3, titulo: 'Enviar o relatorio', feita: 0, vezes_editada: 0 }, ] }; const [marcada] = executarUpdate(banco, 'UPDATE tarefas SET feita = 1 WHERE id = 1', []); console.log('rowsAffected:', marcada.rowsAffected); console.log('linha 1 agora:', banco.tarefas[0]); // id que nao existe: rowsAffected volta 0, e a tela precisa recarregar const [inexistente] = executarUpdate(banco, 'UPDATE tarefas SET feita = 1 WHERE id = 99', []); console.log('id inexistente -> rowsAffected:', inexistente.rowsAffected); // sem o WHERE no filtro, todas as linhas mudam: e o resultado 3 const [todas] = executarUpdate(banco, 'UPDATE tarefas SET feita = 0 WHERE id = 1', []); console.log('filtro por id:', todas.rowsAffected); // coluna com nome errado: o `SET` cria a coluna nova e a tela segue errada const [errado] = executarUpdate(banco, 'UPDATE tarefas SET feia = 1 WHERE id = 2', []); console.log('SET com nome errado -> rowsAffected:', errado.rowsAffected); console.log('a coluna que o aplicativo le:', banco.tarefas[1].feita); console.log('a coluna que o banco criou:', banco.tarefas[1].feia); // `UPDATE` acumula: rodar duas vezes faz o mesmo que rodar uma console.log('antes:', banco.tarefas[0].feita); executarUpdate(banco, 'UPDATE tarefas SET feita = 1 WHERE id = 1', []); executarUpdate(banco, 'UPDATE tarefas SET feita = 1 WHERE id = 1', []); console.log('depois de dois UPDATEs:', banco.tarefas[0].feita); // para somar dentro do comando, a coluna entra na propria expressao const [somou] = executarUpdate(banco, 'UPDATE tarefas SET vezes_editada = vezes_editada + 1 WHERE id = 1', []); console.log('linhas afetadas:', somou.rowsAffected, '| vezes_editada:', banco.tarefas[0].vezes_editada);
Saída real
rowsAffected: 1
linha 1 agora: { id: 1, titulo: 'Revisar o WHERE', feita: 1, vezes_editada: 0 }
id inexistente -> rowsAffected: 0
filtro por id: 1
SET com nome errado -> rowsAffected: 1
a coluna que o aplicativo le: 1
a coluna que o banco criou: 1
antes: 0
depois de dois UPDATEs: 1
linhas afetadas: 1 | vezes_editada: 1
DELETE e apagar com confirmação
DELETE e apagar com confirmação
DELETE FROM tabela WHERE condição remove a linha. Sem WHERE, ele remove a tabela inteira — sem erro, sem aviso, e com rowsAffected informando quantas sumiram, que nesse caso é um número grande demais para ser acidental.
DELETE FROM tarefas WHERE id = 7;
O comando é remover linha, e ele é o único do CRUD que não tem como ser desfeito: não existe comando que traga de volta o que foi apagado. Por isso ele é o único que precisa de WHERE obrigatório e de confirmação na tela.
O filtro do DELETE é o id, quase sempre, e o valor vai como parâmetro. rowsAffected igual a 1 quer dizer que a linha existia e foi; igual a 0 quer dizer que ela já não estava lá, e o aplicativo trata isso como sucesso — o objetivo do comando, a linha não existir mais, foi alcançado.
Essa leitura do 0 como sucesso é o que separa o DELETE do UPDATE. No UPDATE, rowsAffected igual a 0 é problema: o usuário mudou algo e o banco não mudou. No DELETE, é o estado desejado, porque o objetivo é justamente não ter mais aquela linha.
O apagar tudo — DELETE FROM tarefas sem filtro — é a operação de manutenção do aplicativo, não uma ação de tela. Ela existe para o botão de limpar tudo do dia 15 e para o teste que precisa começar do zero, e é o único lugar onde DELETE sem WHERE é aceito como escrita.
Confirmação antes do comando
Apagar é o único comando do CRUD que não tem como ser desfeito sem um segundo comando escrito à mão. UPDATE que altera a linha errada se corrige com outro UPDATE; DELETE que apaga a linha errada devolve um estado que não existe mais. Por isso a confirmação não é detalhe de interface: é o que separa o app profissional do que perde dado.
function apagarTarefa(banco, id) { return new Promise((resolve, reject) => { // o `Alert` mostra a pergunta e devolve a resposta do usuario Alert.alert('Apagar tarefa', 'Essa ação não pode ser desfeita.', [ { text: 'Cancelar', style: 'cancel', onPress: () => resolve(null) }, { text: 'Apagar', style: 'destructive', onPress: () => resolve(apagarNoBanco(banco, id)), }, ]); }); }
O botão de cancelar resolve null e a lista continua como estava; o botão de apagar resolve o resultado do comando e a lista recarrega. O style: 'destructive' é o que pinta o botão de vermelho no aparelho — sem ele, o botão de apagar parece um botão qualquer, e é um botão que não volta atrás.
A confirmação é uma decisão de produto e não uma boa prática genérica: ela só se justifica quando a perda é real. Apagar uma linha que o usuário acabou de digitar, por engano, e que ele não vai notar por uma semana, é uma decisão diferente de apagar a conta inteira. O texto do diálogo precisa dizer o que vai acontecer — "essa ação não pode ser desfeita" informa; "tem certeza?" só transfere a responsabilidade.
Recuperação sem desfazer
Quando a confirmação pesa demais para toda exclusão, o caminho curto é a lixeira: UPDATE tarefas SET apagada_em = '2026-09-12' WHERE id = 7 e a tela passa a listar WHERE apagada_em IS NULL. O dado continua no arquivo, some da lista, e a limpeza definitiva é um DELETE com filtro de data, rodado depois. Isso troca "apagar" por "esconder", e só serve se houver uma tela que mostre o escondido.
É a forma de recuperar o que foi apagado sem comando de desfazer: a linha continua no arquivo, com a marca de quando sumiu da tela. O exemplo desta página mostra o caminho inteiro — a lista mostra duas tarefas, a lixeira guarda a data de uma, e o total de linhas no arquivo continua sendo três. O lixo no banco aparece depois, no DELETE de limpeza, e é o preço de escolher "esconder" em vez de "remover".
A escolha entre as duas tem uma consequência que o exemplo deixa visível: com lixeira, apagar não libera espaço. Em um aplicativo com muitas exclusões, o arquivo cresce sem parar até a limpeza rodar. Por isso a lixeira é uma decisão de prazo — com prazo de dias, é memória de engano do usuário; com prazo de anos, é lixo no banco e o DELETE direto é melhor.
O IS NULL da consulta é o que faz o desenho funcionar: a coluna apagada_em fica vazia na linha que está na lista, e a comparação com NULL não funciona com =. É a mesma armadilha do WHERE por igualdade do dia 5, agora do outro lado.
Exemplo
// `DELETE` remove a linha. Sem `WHERE`, remove a tabela inteira. O // exemplo mostra as duas situacoes e o `rowsAffected` que denuncia cada uma. function executarDelete(banco, sql) { const achado = /DELETE FROM (\w+)(?: WHERE (\w+) = (.+))?$/i.exec(sql); if (!achado) throw new Error('SQL invalido: ' + sql); const [, tabela, coluna, valor] = achado; if (!coluna) { const total = banco[tabela].length; banco[tabela] = []; return [{ rowsAffected: total }]; } const antes = banco[tabela].length; banco[tabela] = banco[tabela].filter((linha) => linha[coluna] !== valor); return [{ rowsAffected: antes - banco[tabela].length }]; } function criarBanco() { return { tarefas: [ { id: 1, titulo: 'Revisar o WHERE', apagada_em: null }, { id: 2, titulo: 'Ler o capitulo 4', apagada_em: null }, { id: 3, titulo: 'Enviar o relatorio', apagada_em: null }, ] }; } // 1. apagar por id: rowsAffected 1 const banco = criarBanco(); const [um] = executarDelete(banco, 'DELETE FROM tarefas WHERE id = 3'); console.log('apagou por id -> rowsAffected:', um.rowsAffected, '| sobraram', banco.tarefas.length); // 2. apagar o que ja nao existe: rowsAffected 0, e o objetivo foi alcancado const [jaApagada] = executarDelete(banco, 'DELETE FROM tarefas WHERE id = 3'); console.log('id que ja nao existe -> rowsAffected:', jaApagada.rowsAffected); // 3. sem o WHERE: a tabela inteira vai embora, e o numero denuncia const bancoInteiro = criarBanco(); const [tudo] = executarDelete(bancoInteiro, 'DELETE FROM tarefas'); console.log('sem WHERE -> rowsAffected:', tudo.rowsAffected, '| sobraram', bancoInteiro.tarefas.length); // 4. a lixeira: esconder em vez de apagar, e a consulta que esconde const lixeira = criarBanco(); executarUpdateSimples(lixeira, 2, '2026-09-12'); function executarUpdateSimples(banco, id, quando) { banco.tarefas = banco.tarefas.map((linha) => linha.id === id ? { ...linha, apagada_em: quando } : linha); } function visiveis(banco) { return banco.tarefas.filter((linha) => linha.apagada_em === null); } const escondidas = lixeira.tarefas.filter((linha) => linha.apagada_em !== null); console.log('a lista mostra', visiveis(lixeira).map((l) => l.titulo)); console.log('a lixeira guarda', escondidas.map((l) => l.apagada_em)); console.log('o dado continua no arquivo: total de linhas', lixeira.tarefas.length); // 5. a limpeza definitiva, depois: filtro de data, nao mais nada const limpas = lixeira.tarefas.filter((linha) => linha.apagada_em !== '2026-09-12'); console.log('depois da limpeza, sobram', limpas.length, 'linhas');
Saída real
apagou por id -> rowsAffected: 0 | sobraram 3 id que ja nao existe -> rowsAffected: 0 sem WHERE -> rowsAffected: 3 | sobraram 0 a lista mostra [ 'Revisar o WHERE', 'Enviar o relatorio' ] a lixeira guarda [ '2026-09-12' ] o dado continua no arquivo: total de linhas 3 depois da limpeza, sobram 2 linhas