Skip to content

feat: FromPartiQL escape hatch for raw PartiQL SELECT queries #176

Description

@j-d-ha

Summary

When LINQ translation doesn't cover a query shape, users need an escape hatch to write PartiQL directly. This story adds FromPartiQL on DbSet<T> for raw SELECT queries that return entity results, and ExecutePartiQLAsync on DatabaseFacade for raw DML (INSERT/UPDATE/DELETE). This follows the pattern established by EF Core's FromSqlRaw / ExecuteSqlRawAsync in relational providers and FromSqlRaw in the Cosmos provider.

API Surface

Entity SELECT (composable)

// Raw PartiQL SELECT — returns IQueryable<T>, supports LINQ composition
var orders = await context.Orders
    .FromPartiQL("SELECT * FROM Orders WHERE pk = ?", new AttributeValue { S = "user-1" })
    .Where(o => o.Status == "active")   // client-side composition
    .ToListAsync();

FromPartiQL is defined as an extension method on DbSet<TEntity>. It returns IQueryable<TEntity> so further LINQ operators (.Where, .Select, .Skip, .Take) can be appended as client-side evaluation.

DML / non-query execution

await context.Database.ExecutePartiQLAsync(
    "UPDATE Orders SET status = ? WHERE pk = ?",
    [new AttributeValue { S = "closed" }, new AttributeValue { S = "order-1" }]);

Signatures

// Extensions/DynamoDbSetExtensions.cs (or DynamoQueryableExtensions.cs)
public static IQueryable<TEntity> FromPartiQL<TEntity>(
    this DbSet<TEntity> source,
    [NotParameterized] string partiql,
    params AttributeValue[] parameters)
    where TEntity : class;

// Extensions/DynamoDatabaseFacadeExtensions.cs
public static Task ExecutePartiQLAsync(
    this DatabaseFacade database,
    string partiql,
    IEnumerable<AttributeValue> parameters,
    CancellationToken cancellationToken = default);

Parameters are AttributeValue (AWS SDK type) rather than object? — DynamoDB has no generic CLR-to-AttributeValue inference, so requiring the provider-native type is more honest and avoids a lossy conversion layer.

Implementation sketch

FromPartiQL (SELECT path)

Follows the same pipeline as the Cosmos FromSqlRaw implementation:

  1. New expression nodeFromPartiQLQueryRootExpression : EntityQueryRootExpression carrying the raw PartiQL string and AttributeValue[] parameters
  2. DynamoQueryRootProcessor — detect the new root, pass through to translator
  3. DynamoQueryableMethodTranslatingExpressionVisitor — handle FromPartiQLQueryRootExpression, build a SelectExpression that wraps a raw PartiQL source node
  4. DynamoQuerySqlGenerator — emit the literal PartiQL string unchanged; bind parameters into the ExecuteStatementRequest
  5. Execution — routes through the existing DynamoClientWrapper.ExecutePartiQl() path

The existing ExecutePartiQl() already accepts an ExecuteStatementRequest with a Parameters list of AttributeValue — the translation pipeline just needs to thread the user-supplied parameters through correctly.

ExecutePartiQLAsync (DML path)

Thin wrapper over the existing DynamoClientWrapper.ExecuteWriteAsync(string statement, List<AttributeValue> parameters, ...). Just expose it through DatabaseFacade so users don't need to resolve the internal service.

Design constraints

  • Async only — no synchronous FromPartiQL or ExecutePartiQL overloads; the provider is async-only
  • No server-side composition — LINQ operators appended after FromPartiQL are client-evaluated; document this clearly. Unlike relational providers there is no way to wrap raw PartiQL in an outer query
  • Parameters are positional — PartiQL uses ? placeholders; parameters bind left-to-right in the supplied array
  • No interpolated string overloadAttributeValue parameters can't be naturally embedded in a FormattableString; raw string + explicit parameter list is the only form

What is NOT in scope

Acceptance criteria

  • FromPartiQL returns entities for a valid PartiQL SELECT with positional parameters
  • LINQ operators appended after FromPartiQL evaluate client-side (not sent to DynamoDB as additional predicates)
  • ExecutePartiQLAsync executes DML statements and does not throw for valid PartiQL
  • Parameters bind positionally in supplied order
  • Integration tests: single-param SELECT, multi-param SELECT, DML execution
  • Invalid PartiQL surfaces a DynamoException (not a null ref or silent failure)

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions