Pular para o conteúdo

Receitas

Os seis problemas que todo sistema com banco resolve — com o código que funciona e o que dá errado na versão ingênua.

Cada uma destas tem uma versão ingênua que funciona no teste e falha com concorrência, com volume, ou com um link antigo.

1. Inserir sem duplicar#

dataforge
adopt Arcane.Database as Db

db := Db.connect(":memory:")
Db.execute(db, "create table produto (sku text primary key, nome text, preco real)")

// 'upsert': insere, ou atualiza se a chave ja existe. A versao
// ingenua — buscar, e inserir se nao achou — tem uma janela entre a
// busca e a insercao, e duas importacoes ao mesmo tempo duplicam.
Db.upsert(db, "produto", {"sku": "A1", "nome": "Cafe", "preco": 32.0}, ["sku"])
Db.upsert(db, "produto", {"sku": "A1", "nome": "Cafe", "preco": 35.0}, ["sku"])

linhas := Db.query(db, "select * from produto")
assert len(linhas) is 1
out $"uma linha, preco atualizado: {linhas[0]['preco']}"

Db.close(db)

2. Contador que não perde#

dataforge
adopt Arcane.Database as Db

db := Db.connect(":memory:")
Db.execute(db, "create table estoque (id integer primary key, qtd integer)")
Db.execute(db, "insert into estoque (id, qtd) values (1, 100)")

// 'increment' faz a conta NO BANCO. A versao ingenua — ler, somar,
// gravar — perde atualizacoes: duas vendas ao mesmo tempo leem 100,
// as duas gravam 99, e uma some.
Db.increment(db, "estoque", "qtd", -1, {"id": 1})
Db.increment(db, "estoque", "qtd", -1, {"id": 1})

assert Db.query(db, "select qtd from estoque")[0]["qtd"] is 98
out "duas baixas, duas contadas"

Db.close(db)

3. Listagem paginada#

dataforge
adopt Arcane.Database as Db

db := Db.connect(":memory:")
Db.execute(db, "create table item (id integer primary key, nome text)")
cycle i from 1 to 40:
    Db.execute(db, "insert into item (nome) values (?)", [$"Item {i}"])

// 'paginate' devolve o total junto — sem ele a tela nao sabe
// quantos botoes desenhar.
p := Db.paginate(db, "item", 2, 15)
out $"pagina {p['pagina']} de {p['paginas']}: {len(p['itens'])} de {p['total']}"
assert p["total"] is 40
assert p["tem_anterior"]

Db.close(db)

4. Busca textual#

dataforge
adopt Arcane.Database as Db

db := Db.connect(":memory:")
Db.execute(db, "create table artigo (id integer primary key, titulo text, corpo text)")
Db.execute(db, "insert into artigo (titulo, corpo) values (?, ?)",
    ["Cafe especial", "sobre torra e moagem"])
Db.execute(db, "insert into artigo (titulo, corpo) values (?, ?)",
    ["Cha verde", "sobre temperatura da agua"])

// FTS5: um indice de verdade. 'like %termo%' varre a tabela
// inteira, e o custo cresce com o tamanho.
Db.create_search(db, "artigo", ["titulo", "corpo"])

r := Db.search(db, "artigo", "torra")
out $"achou: {len(r)}"
assert len(r) is 1

Db.close(db)

5. Exportar sem carregar tudo#

dataforge
adopt Arcane.Forge as Forge
adopt Arcane.OS as OS
adopt Arcane.IO as IO

db := Forge.memoria()
Forge.executar(db, "create table venda (id integer primary key, valor real)")
cycle i from 1 to 100:
    Forge.executar(db, "insert into venda (valor) values (?)", [i * 1.5])

caminho := $"{OS.temp_dir()}/df-vendas-{randint(100000, 999999)}.csv"

// 'para_csv' recebe as LINHAS, e nao a tabela: assim ele exporta o
// resultado de qualquer consulta, e nao so uma tabela inteira.
Forge.para_csv(Forge.consultar(db, "select id, valor from venda"), caminho)

conteudo := IO.read(caminho)
// O CSV exportado tem cabecalho mais uma linha por venda.
assert len(conteudo) bigger 100
assert "valor" in conteudo
out $"{len(conteudo)} bytes exportados, com cabecalho"

IO.delete(caminho)
Forge.fechar(db)

6. Migração que desfaz#

dataforge
adopt Arcane.Forge as Forge

db := Forge.memoria()
m := Forge.migracoes(db)

// O passo recebe ACOES, e nao texto de SQL. A diferenca importa:
// uma migracao de verdade quase nunca e um comando so — ela cria a
// tabela, preenche a coluna nova a partir da antiga, e so entao
// derruba a antiga. Com texto, isso viraria uma lista de strings e
// uma regra sobre a ordem delas.
action criar(c):
    Forge.executar(c, "create table cliente (id integer primary key, nome text)")

action derrubar(c):
    Forge.executar(c, "drop table cliente")

m.passo("001_cria_cliente", criar, derrubar)

m.subir()
assert "cliente" in Forge.tabelas(db)
out "aplicada"

// O 'down' e o que torna o erro reversivel. Uma migracao sem ele
// exige editar o banco a mao — as tres da manha, com pressa.
m.descer()
assert "cliente" not in Forge.tabelas(db)
out "desfeita"

Forge.fechar(db)

Continue em Em produção e O banco em contêiner.