Persistência

Declarando onde e o quê

Declare dataSources e entities uma vez em spec; use os steps db.* no pipeline para ler e escrever.

spec:
  dataSources:
    - { id: main, provider: postgres, connectionString: "${env:PIPEVINE_PG_CONNECTION}" }

  entities:
    - name: widget
      table: widgets
      dataSource: main
      key: id
      properties:
        - { name: id,   type: uuid }
        - { name: name, type: string }

postgres, mysql e sqlite são os três providers relacionais do core — o mesmo pipeline, sem alterar uma linha, roda contra qualquer um dos três trocando só provider:/connectionString:

# postgres
- { id: main, provider: postgres, connectionString: "${env:PIPEVINE_PG_CONNECTION}" }
# mysql
- { id: main, provider: mysql, connectionString: "${env:PIPEVINE_MYSQL_CONNECTION}" }
# sqlite — o único que não precisa de servidor/container
- { id: main, provider: sqlite, connectionString: "Data Source=widgets.db" }

sqlite é o alvo natural para os testes da sua própria aplicação (nenhuma infra para subir) e para rodar a imagem base genérica sem nenhum container de banco.

Os providers não relacionais também são do core, não de terceiros: mongodb/documentdb (Pipevine.Data.MongoDb), cassandra (Pipevine.Data.Cassandra) e dynamodb (Pipevine.Data.DynamoDb). Eles usam os mesmos steps db.* com a mesma sintaxe, dentro do que cada um declara saber fazer — e Cassandra e DynamoDB publicam ainda steps próprios (cassandra.*, dynamo.*) para o que o db.* não expressa. Um provider para um banco ainda não coberto pode ser adicionado por um pacote de terceiro sem tocar no core; veja IDataProvider.

Tipos de coluna aceitos hoje: uuid, string, int/integer, long, decimal (com precision/scale), bool/boolean, datetime, timestamptz, jsonb/json. Nem todos são nativos nos três bancos — onde não são, o provider converte, e a tabela completa de conversões (e das poucas perdas documentadas, como o offset original de timestamptz) está em Configuração YAML § Conversão de tipos por provider relacional.

Bootstrap de tabela é uma conveniência do CLI (pipevine db script minha-api.pipevine.yaml | psql ... — ou o cliente do banco escolhido), não uma ferramenta de migração — não é seguro rodar duas vezes contra um schema que já mudou por fora.

Os steps db.*

Step Função
db.persist Insert ou upsert (onConflict.columns opcional). Não publica resultado
db.update Update parcial — faz merge do patch com o registro existente. Casa pela chave declarada da entidade, informada em key: (uma expressão), não por where:. Não publica resultado
db.delete Remoção, também por key:. Não publica resultado
db.sql Escape hatch para SQL arbitrário — exige dataSource: (não entity:). Parâmetros por expressão, nunca por interpolação de string. Não publica resultado
db.find Uma entidade (where) ou null. Publica em steps.<id>.result
db.exists Booleano (where). Publica em steps.<id>.result
db.query Lista (where, sort, skip/take). Publica em steps.<id>.result

where é sempre igualdade por campo. db.find, db.exists e db.query exigem id: — é assim que publicam o resultado em steps.<id>.result, para o resto do pipeline ler.

- type: db.persist
  entity: widget
  data: vars.widget

- { type: db.find, id: found, entity: widget, where: { id: "input.route.id" } }
- { type: fail, when: "steps.found.result = null", status: 404, code: WIDGET_NOT_FOUND }

db.sql é o escape hatch para o que os steps tipados acima não cobrem. Os parâmetros são sempre parâmetros ADO.NET reais, resolvidos por expressão e vinculados pelo provider — nunca interpolados no texto do SQL. É isso que torna um valor malicioso em um campo de entrada apenas dado comum, em vez de uma injeção: o texto do SQL em si é fixo desde o build e nunca toca os valores avaliados.

Transações

Para uma rota que faz mais de uma escrita e precisa que tudo aconteça atomicamente, declare transaction: required na rota:

- id: createOrder
  transaction: required
  pipeline:
    - { type: db.persist, entity: Order, data: vars.order }
    - { type: db.update,  entity: Inventory, key: "vars.order.sku", patch: "{ 'reserved': true }" }

Uma sessão transacional é uma única conexão — não é seguro fazer db.* concorrente dentro dela. Isso importa especificamente quando você combina transação com os steps de controle de fluxo paralelo (parallel, foreach com parallelism > 1); veja Controle de fluxo — a seção "Um detalhe que morde: transação e concorrência" — para o detalhe completo. É um erro fácil de cometer e vale a leitura antes de combinar as duas coisas.

Para onde ir daqui

  • Controle de fluxo — decisões condicionais e iteração sobre os dados que você acabou de ler ou gravar.
  • Cache — evitar ir ao banco quando o dado já foi lido recentemente.
  • Um exemplo real com transação e múltiplas escritas: samples/03-inventory-orders no repositório do Pipevine.