Guia — IDataProvider

Um banco de dados novo, ativado por provider: num spec.dataSources. QuerySpec carrega filtros/ordenação/paginação numa forma neutra — não há obrigação de suportar SQL.

Exemplo mínimo

using Pipevine.Abstractions;
using Pipevine.Core.Configuration;
using Pipevine.Data.Abstractions;
using Pipevine.Model.Resources;

// A chave usada no YAML: `provider: acmedb`
[Provider("acmedb", ProviderKind.DataSource)]
public sealed class AcmeDataProvider : IDataProvider
{
    public IDataSession CreateSession(DataSourceDefinition definition)
    {
        var config = PipevineNodeJsonConverter.ToJsonNode(definition.Config);
        var connectionString = config?["connectionString"]?.GetValue<string>()
            ?? throw new DataProviderConfigurationException($"Data source '{definition.Id}' requer 'connectionString'.");

        return new AcmeDataSession(connectionString);
    }
}

IDataSession : IPipelineTransaction — é literalmente a transação que o executor do pipeline comita/reverte quando a rota declara transaction: required (o motor de pipeline só conhece o contrato genérico, nunca que é especificamente um banco de dados):

internal sealed class AcmeDataSession(string connectionString) : IDataSession
{
    public IEntityStore GetStore(string entityName) => new AcmeEntityStore(entityName /* ... */);

    public ValueTask<int> ExecuteSqlAsync(string sql, IReadOnlyDictionary<string, JsonNode?> parameters, CancellationToken ct) =>
        throw new NotSupportedException("acmedb não tem dialeto SQL — use os steps db.* tipados.");

    public ValueTask CommitAsync(CancellationToken ct) { /* ... */ }
    public ValueTask RollbackAsync(CancellationToken ct) { /* ... */ }
    public ValueTask DisposeAsync() { /* ... */ }
}

db.sql é o escape hatch declarado no vocabulário do YAML — um provider não relacional não precisa implementá-lo de verdade; lançar NotSupportedException é aceitável quando esse caminho realmente não existe no banco de destino.

IEntityStore é onde QuerySpec entra:

internal sealed class AcmeEntityStore(string entityName) : IEntityStore
{
    public ValueTask<JsonNode?> FindAsync(QuerySpec spec, CancellationToken ct) { /* traduza spec.Filters */ }
    public ValueTask<bool> ExistsAsync(QuerySpec spec, CancellationToken ct) { /* ... */ }
    public ValueTask<IReadOnlyList<JsonNode>> QueryAsync(QuerySpec spec, CancellationToken ct) { /* spec.Sort/Skip/Take */ }
    public ValueTask PersistAsync(JsonNode data, OnConflictSpec? onConflict, CancellationToken ct) { /* ... */ }
    public ValueTask UpdateAsync(QuerySpec spec, JsonNode patch, CancellationToken ct) { /* ... */ }
    public ValueTask DeleteAsync(QuerySpec spec, CancellationToken ct) { /* ... */ }
}

QuerySpec.Filters cobre oito operadores (Eq/Ne/Gt/Gte/Lt/Lte/Contains/In) — se o seu banco não conseguir expressar algum deles, isso é sinal de que vale revisitar como a abstração está sendo usada, não que você deva forçar um NotSupportedException silencioso num caso comum.

Ativação

spec:
  dataSources:
    - { id: main, provider: acmedb, connectionString: "${env:ACME_DB_CONNECTION}" }
  entities:
    - { name: item, table: items, dataSource: main, key: id, properties: [...] }
services.AddKeyedSingleton<IDataProvider, AcmeDataProvider>("acmedb");
// mais services.AddPipevineData() do core, que registra os steps genéricos db.persist/find/...

Se provider: acmedb aparecer no YAML sem essa chave registrada, o build falha com um diagnóstico claro:

orders.pipevine.yaml(14,20): error PV3010: nenhum provider de dados registrado
para a chave 'acmedb'. Referencie o pacote que a fornece.

Outbox transacional (opcional)

Um provider que também quer participar do outbox transacional (usado por publish com mode: outbox) implementa IOutboxStore separadamente.

Para onde ir daqui