Skip to main content

PartiQL Support

SQL-compatible query language for DynamoDB that allows you to query, insert, update, and delete data using familiar SQL syntax.

Overview

PartiQL support in Oproto.FluentDynamoDb follows the same request builder pattern as other operations:

  • PartiQLRequestBuilder<TEntity> for building and executing statements
  • Format string placeholders for parameter substitution
  • Entity hydration for SELECT results
  • Response metadata access after execution
  • Batch PartiQL via DynamoDbBatch.PartiQL

Basic Usage

SELECT Queries

// Simple SELECT with parameter
var users = await table.ExecutePartiQL<User>(
"SELECT * FROM Users WHERE pk = {0}",
"USER#123")
.ToListAsync();

// SELECT with multiple parameters
var orders = await table.ExecutePartiQL<Order>(
"SELECT * FROM Orders WHERE pk = {0} AND sk BETWEEN {1} AND {2}",
"TENANT#A", "ORDER#2024-01", "ORDER#2024-12")
.ToListAsync();

// SELECT specific attributes
var names = await table.ExecutePartiQL<User>(
"SELECT name, email FROM Users WHERE pk = {0}",
"USER#123")
.ToListAsync();

INSERT Statements

await table.ExecutePartiQL<User>(
"INSERT INTO Users VALUE {'pk': {0}, 'sk': {1}, 'name': {2}, 'email': {3}}",
"USER#456", "PROFILE", "Jane Doe", "jane@example.com")
.ExecuteAsync();

UPDATE Statements

await table.ExecutePartiQL<User>(
"UPDATE Users SET name = {0}, updated_at = {1:o} WHERE pk = {2} AND sk = {3}",
"Jane Smith", DateTime.UtcNow, "USER#456", "PROFILE")
.ExecuteAsync();

DELETE Statements

await table.ExecutePartiQL<User>(
"DELETE FROM Users WHERE pk = {0} AND sk = {1}",
"USER#456", "PROFILE")
.ExecuteAsync();

Format String Placeholders

PartiQL statements support format string placeholders with optional format specifiers:

Basic Placeholders

// {0}, {1}, {2} - positional parameters
var users = await table.ExecutePartiQL<User>(
"SELECT * FROM Users WHERE pk = {0} AND status = {1}",
"USER#123", "active")
.ToListAsync();

DateTime Formatting

// {0:o} - ISO 8601 format (recommended for DynamoDB)
var recentOrders = await table.ExecutePartiQL<Order>(
"SELECT * FROM Orders WHERE pk = {0} AND created_at > {1:o}",
"TENANT#A", DateTime.UtcNow.AddDays(-7))
.ToListAsync();

// Other DateTime formats
// {0:s} - Sortable format
// {0:u} - Universal sortable format
// {0:yyyy-MM-dd} - Custom format

Numeric Formatting

// {0:F2} - Fixed-point with 2 decimal places
await table.ExecutePartiQL<Product>(
"UPDATE Products SET price = {0:F2} WHERE pk = {1}",
19.99m, "PROD#123")
.ExecuteAsync();

PartiQLRequestBuilder

Builder Methods

var builder = table.ExecutePartiQL<User>(
"SELECT * FROM Users WHERE pk = {0}",
"USER#123");

// Execute SELECT and get list
var users = await builder.ToListAsync();

// Execute SELECT for compound entity tables
var result = await builder.ToCompoundEntityAsync();
var users = result.GetEntities<User>();
var profiles = result.GetEntities<UserProfile>();

// Execute INSERT/UPDATE/DELETE
await builder.ExecuteAsync();

// Get underlying SDK request
ExecuteStatementRequest request = builder.ToRequest();

Response Metadata

Access response metadata after execution:

var builder = table.ExecutePartiQL<User>(
"SELECT * FROM Users WHERE pk = {0}",
"USER#123");

var users = await builder.ToListAsync();

// Access metadata after execution via .Response property
ResponseMetadata? metadata = builder.Response?.ResponseMetadata;
ConsumedCapacity? capacity = builder.Response?.ConsumedCapacity;

if (capacity != null)
{
Console.WriteLine($"Read capacity consumed: {capacity.ReadCapacityUnits}");
}

Batch PartiQL

Execute multiple PartiQL statements in a single batch using DynamoDbBatch.PartiQL:

Basic Batch

var response = await DynamoDbBatch.PartiQL
.Add(table.ExecutePartiQL<User>(
"SELECT * FROM Users WHERE pk = {0}",
"USER#123"))
.Add(table.ExecutePartiQL<Order>(
"SELECT * FROM Orders WHERE pk = {0}",
"ORDER#456"))
.ExecuteAsync();

// Access results by index
var user = response.GetItem<User>(0);
var order = response.GetItem<Order>(1);

Tuple Convenience Methods

// Single result
var user = await DynamoDbBatch.PartiQL
.Add(table.ExecutePartiQL<User>("SELECT * FROM Users WHERE pk = {0}", "USER#123"))
.ExecuteAndMapAsync<User>();

// Two results
var (user, order) = await DynamoDbBatch.PartiQL
.Add(table.ExecutePartiQL<User>("SELECT * FROM Users WHERE pk = {0}", "USER#123"))
.Add(table.ExecutePartiQL<Order>("SELECT * FROM Orders WHERE pk = {0}", "ORDER#456"))
.ExecuteAndMapAsync<User, Order>();

// Up to 8 results supported
var (a, b, c, d) = await DynamoDbBatch.PartiQL
.Add(table.ExecutePartiQL<EntityA>("SELECT * FROM TableA WHERE pk = {0}", "A"))
.Add(table.ExecutePartiQL<EntityB>("SELECT * FROM TableB WHERE pk = {0}", "B"))
.Add(table.ExecutePartiQL<EntityC>("SELECT * FROM TableC WHERE pk = {0}", "C"))
.Add(table.ExecutePartiQL<EntityD>("SELECT * FROM TableD WHERE pk = {0}", "D"))
.ExecuteAndMapAsync<EntityA, EntityB, EntityC, EntityD>();

Mixed Operations

Unlike BatchGetItem/BatchWriteItem, PartiQL batch can mix SELECT, INSERT, UPDATE, and DELETE:

await DynamoDbBatch.PartiQL
.Add(table.ExecutePartiQL<User>(
"UPDATE Users SET last_login = {0:o} WHERE pk = {1}",
DateTime.UtcNow, "USER#123"))
.Add(table.ExecutePartiQL<AuditLog>(
"INSERT INTO AuditLogs VALUE {'pk': {0}, 'sk': {1}, 'action': {2}}",
"AUDIT#123", DateTime.UtcNow.ToString("O"), "LOGIN"))
.ExecuteAsync();

Explicit Client Configuration

// Client inferred from first builder added
var response = await DynamoDbBatch.PartiQL
.Add(table.ExecutePartiQL<User>("SELECT * FROM Users WHERE pk = {0}", "USER#123"))
.ExecuteAsync();

// Or explicitly set client
var response = await DynamoDbBatch.PartiQL
.WithClient(customClient)
.Add(table.ExecutePartiQL<User>("SELECT * FROM Users WHERE pk = {0}", "USER#123"))
.ExecuteAsync();

// Or pass client to ExecuteAsync
var response = await DynamoDbBatch.PartiQL
.Add(table.ExecutePartiQL<User>("SELECT * FROM Users WHERE pk = {0}", "USER#123"))
.ExecuteAsync(customClient);

DynamicTable PartiQL

Use PartiQL with DynamicTable for schema-less queries:

var dynamicTable = new DynamicTable(client, "my-table");

// SELECT returns DynamicEntity instances
var items = await dynamicTable.ExecutePartiQL(
"SELECT * FROM \"my-table\" WHERE pk = {0}",
"ITEM#123")
.ToListAsync();

foreach (var item in items)
{
var name = item.DynamicFields.GetString("name");
var age = item.DynamicFields.GetInt("age");
}

Compound Entity Tables

For tables with multiple entity types, use ToCompoundEntityAsync():

// Query returns mixed entity types
var result = await table.ExecutePartiQL<Order>(
"SELECT * FROM Orders WHERE pk = {0}",
"ORDER#123")
.ToCompoundEntityAsync();

// Extract each entity type
var orders = result.GetEntities<Order>();
var orderLines = result.GetEntities<OrderLine>();
var orderNotes = result.GetEntities<OrderNote>();

PartiQL vs Query/Scan

FeaturePartiQLQuery/Scan Builders
SyntaxSQL-likeFluent methods
Learning curveFamiliar to SQL usersDynamoDB-specific
Type safetyString-basedLambda expressions
Complex joinsLimited supportNot supported
PerformanceSame as Query/ScanSame as PartiQL
Batch operationsYes (BatchExecuteStatement)Yes (BatchGetItem, etc.)

When to Use PartiQL

  • Migrating from SQL databases
  • Complex queries that are easier to express in SQL
  • Ad-hoc queries in admin tools
  • When SQL syntax is more readable for your team

When to Use Query/Scan Builders

  • Type-safe queries with compile-time checking
  • IntelliSense support for entity properties
  • Consistent API across all operations
  • When lambda expressions are preferred

Error Handling

Syntax Errors

try
{
await table.ExecutePartiQL<User>(
"SELEC * FROM Users", // Typo in SELECT
"USER#123")
.ToListAsync();
}
catch (AmazonDynamoDBException ex)
{
// PartiQL syntax error from DynamoDB
Console.WriteLine($"PartiQL error: {ex.Message}");
}

Hydration Errors

try
{
// Query returns data that doesn't match User entity
var users = await table.ExecutePartiQL<User>(
"SELECT * FROM DifferentTable WHERE pk = {0}",
"ITEM#123")
.ToListAsync();
}
catch (DynamoDbMappingException ex)
{
Console.WriteLine($"Hydration error: {ex.Message}");
}

Best Practices

Use Parameterized Queries

Always use format placeholders instead of string concatenation:

// ✅ Good - parameterized
var users = await table.ExecutePartiQL<User>(
"SELECT * FROM Users WHERE pk = {0}",
userId)
.ToListAsync();

// ❌ Bad - string concatenation (SQL injection risk)
var users = await table.ExecutePartiQL<User>(
$"SELECT * FROM Users WHERE pk = '{userId}'")
.ToListAsync();

Quote Table Names with Special Characters

// Table names with hyphens need quotes
var items = await table.ExecutePartiQL<Item>(
"SELECT * FROM \"my-table-name\" WHERE pk = {0}",
"ITEM#123")
.ToListAsync();

Use ISO 8601 for Dates

// Use {0:o} format specifier for consistent date handling
var orders = await table.ExecutePartiQL<Order>(
"SELECT * FROM Orders WHERE created_at > {0:o}",
DateTime.UtcNow.AddDays(-30))
.ToListAsync();

See Also