Pular para o conteúdo

Pipeline ETL

Extração, transformação e carga com generators encadeados — sem carregar tudo na memória.

Este é o código completo do exercício 150_projeto_etl.df, que roda e verifica a si mesmo.

150_projeto_etl.df
adopt Arcane.Serialization as Serde
adopt Arcane.Collections as Col

// ── EXTRACAO: a origem dos dados brutos ──
stream action extrair():
    linhas := [
        "  ANA SILVA ,ana@Exemplo.COM , 30, vendas , 5500",
        "bruno costa,bruno@teste.org,25,ti,7200",
        "linha malformada",
        "CARLA DIAS , carla@x.com ,41, vendas ,9100",
        "diego alves,invalido,29,ti,6300",
        "elena rocha,elena@y.com,abc,rh,4800"
    ]
    cycle l in linhas:
        emit l

// ── TRANSFORMACAO: um estagio por responsabilidade ──
stream action separar(fonte):
    cycle linha in fonte:
        campos := linha.split(",")
        given len(campos) is 5:
            emit campos
        otherwise:
            emit {"__erro__": $"campos de menos: '{linha.trim()}'"}

stream action limpar(fonte):
    cycle item in fonte:
        match item:
            point {"__erro__": e}:
                emit item
            point [nome, email, idade, setor, salario]:
                emit {
                    "nome": nome.trim().title(),
                    "email": email.trim().lower(),
                    "idade": idade.trim(),
                    "setor": setor.trim().lower(),
                    "salario": salario.trim()
                }
            default:
                emit {"__erro__": "formato inesperado"}

stream action validar(fonte):
    cycle registro in fonte:
        given "__erro__" in registro:
            emit registro
        otherwise:
            problemas := []
            given "@" not in registro["email"]:
                problemas.append("email invalido")
            given not registro["idade"].isdigit():
                problemas.append("idade nao numerica")
            given len(problemas) bigger 0:
                emit {"__erro__": $"{registro["nome"]}: {problemas.join(", ")}"}
            otherwise:
                emit {
                    "nome": registro["nome"],
                    "email": registro["email"],
                    "idade": cast registro["idade"] as Integer,
                    "setor": registro["setor"],
                    "salario": cast registro["salario"] as Integer
                }

// ── CARGA: separa o que passou do que falhou ──
validos := []
rejeitados := []

observe registro in validar(limpar(separar(extrair()))):
    given "__erro__" in registro:
        rejeitados.append(registro["__erro__"])
    otherwise:
        validos.append(registro)

out "── carregados ──"
cycle v in validos:
    out $"  {v["nome"].pad_end(12)} {v["setor"].pad_end(8)} R$ {v["salario"]}"

out ""
out "── rejeitados ──"
cycle r in rejeitados:
    out $"  {r}"

// ── RELATORIO ──
out ""
out "── por setor ──"
por_setor := Col.group_by(validos, "setor")
cycle setor in por_setor.keys():
    salarios := por_setor[setor] >> morph p: p["salario"]
    out $"  {setor.pad_end(8)} n={len(salarios)}  media=R$ {round(mean(salarios), 2)}"

folha := validos >> morph v: v["salario"] >> distill acc, s: acc + s 0
out ""
out $"folha total: R$ {folha}"
out $"aproveitamento: {len(validos)}/{len(validos) + len(rejeitados)}"

assert len(validos) is 3, "tres registros validos"
assert len(rejeitados) is 3, "tres rejeitados"
assert folha is 21800, "5500 + 9100 + 7200"
assert por_setor["vendas"].length() is 2, "duas pessoas em vendas"

A arquitetura#

text
extrair()  →  separar()  →  limpar()  →  validar()  →  observe
   ↓             ↓            ↓            ↓             ↓
 origem      estrutura    normaliza     verifica     carrega

Cada estágio é um stream action que consome o anterior. Montar a cadeia não lê nada — cada linha atravessa os quatro estágios individualmente.

Erros que atravessam o pipeline#

O ponto mais delicado: uma linha ruim não pode derrubar as outras. A solução é fazer o erro viajar como dado, com um marcador que cada estágio reconhece e repassa intacto.

Um monitor em volta de tudo não resolveria: ele abortaria o pipeline inteiro na primeira linha torta.

Normalizar na entrada#

Dados de fora chegam sujos. Limpar uma vez, na fronteira, evita ter que lembrar disso em cada consulta depois.

Converter só depois de validar#

cast "abc" as Integer dispara erro. Verificar primeiro transforma uma exceção num registro rejeitado com mensagem clara.

Mais sobre isso em Streams.