PartiQL
Dynoxide supports PartiQL across all three operations: ExecuteStatement, BatchExecuteStatement, and ExecuteTransaction. SELECT, INSERT, UPDATE and DELETE all work, with the full WHERE grammar.
aws dynamodb execute-statement \
--endpoint-url http://localhost:8000 \
--statement "SELECT * FROM Users WHERE pk = 'user#1'"
Parameters work in every position, including inside nested lists and maps:
aws dynamodb execute-statement \
--endpoint-url http://localhost:8000 \
--statement "SELECT * FROM Users WHERE pk = ?" \
--parameters '[{"S": "user#1"}]'
Supported grammar
| Category | Support |
|---|---|
| Comparisons | =, <>, <, >, <=, >= |
| Range and membership | BETWEEN, IN |
| Functions | EXISTS, NOT EXISTS, BEGINS_WITH, CONTAINS |
| Existence | IS MISSING, IS NOT MISSING |
| Logical | AND, OR, NOT, and parenthesised grouping |
| Projections | Nested dot-notation paths, COUNT(*) |
| Pagination | LIMIT and NextToken |
| Literals | Set literals (<< >>), negative numbers, escaped quotes |
INSERT supports IF NOT EXISTS and rejects duplicates. UPDATE supports SET with expressions and REMOVE, including real list-index writes: SET tags[0] = :v updates that element (appending when the index is at or past the end) and REMOVE tags[0] deletes it and shifts the rest down.
RETURNING
New in 0.12.0. Before that Dynoxide parsed the clause and silently dropped it, so a statement that should have returned an item returned nothing.
UPDATE Users SET status = 'active' WHERE pk = 'user#1' RETURNING ALL NEW *
DELETE FROM Users WHERE pk = 'user#1' AND sk = 'profile' RETURNING ALL OLD *
What each operation accepts:
| Statement | Accepted | Rejected |
|---|---|---|
UPDATE |
All four of ALL OLD, ALL NEW, MODIFIED OLD, MODIFIED NEW |
- |
DELETE |
ALL OLD * only |
MODIFIED OLD *, ALL NEW *, MODIFIED NEW * |
The variants DynamoDB does not allow on DELETE are rejected with its exact validation message rather than ignored.
The MODIFIED projections return only the paths that changed, and exclude the primary key. A nested SET a.b returns just the changed leaf rather than the whole a attribute, and when nothing was projected you get an empty Items array rather than no field. Over list-index paths the changed elements come back packed into a dense list in ascending index order, so SET a[0], a[2] yields {a: [v0, v2]}, matching DynamoDB.
DELETE ... RETURNING ALL OLD * against a key that isn't there returns a present but empty Items array. That differs from the classic DeleteItem path, and it matches real DynamoDB.
BatchExecuteStatement honours RETURNING on each member. ExecuteTransaction rejects it outright with a top-level ValidationException.
Where PartiQL differs from the classic API
Three behaviours catch people out, and all three match real DynamoDB:
UPDATEis not an upsert. The target has to exist already. Updating a key that isn't there fails withConditionalCheckFailedException("The conditional request failed") and creates nothing.DELETErequires the full key. On a table with a composite key you must supply the sort key as well as the partition key.SELECTwithout a key condition scans. The same cost model as the classic API applies, so aWHEREclause on a non-key attribute reads the whole table and filters afterwards.
Errors
Parse failures use DynamoDB's wording: Statement wasn't well formed, can't be processed: <detail>. A statement that doesn't begin with a DML keyword reports Expected data manipulation.
On BatchExecuteStatement, a member that fails to parse carries the short-form ValidationError code, matching a per-statement execution error rather than a request-level one. Each successful member echoes its TableName.
Over MCP
The MCP server exposes all three operations as tools: execute_partiql, batch_execute_partiql, and execute_transaction_partiql. See the MCP docs.