---
title: "Dynoxide 0.11.2: a stack of conformance fixes - Martin Hicks"
description: "Dynoxide 0.11.2 makes the engine report the transaction capacity numbers I pinned against real DynamoDB last week - the read/write split, a ConditionCheck billed as a write, an idempotent replay that recomputes a real read - plus an UpdateTable bug that dropped table keys, and a batch of projection and validation fixes."
canonical: https://martinhicks.dev/articles/dynoxide-0112-conformance-fixes
last-updated: 2026-07-02
---

![The Dynoxide hexagonal logo centred on a near-black background](https://martinhicks.dev/images/articles/dynoxide-0112-conformance-fixes.png)2nd July 2026

# Dynoxide 0.11.2: a stack of conformance fixes

[Dynoxide](https://dynoxide.dev) 0.11.2 is a conformance patch - a stack of fixes for spots where the engine did something real DynamoDB doesn't. Most of it is drift the [conformance suite](https://martinhicks.dev/articles/dynoxide-conformance-suite) turned up faster than I'd been closing it. The lead one I can tie to something concrete: last week I [worked out what a conditional DynamoDB transaction costs](https://martinhicks.dev/articles/what-a-conditional-dynamodb-transaction-costs) and pinned the numbers against real AWS. This release makes dynoxide report the same numbers.

## The transaction bill

That article measured what real DynamoDB charges inside a `TransactWriteItems`, and dynoxide wasn't reporting it the same way. Three things changed.

The read/write split now sits at the top level. `TransactWriteItems` reports `ReadCapacityUnits` and `WriteCapacityUnits` in its `ConsumedCapacity`, where before those numbers only lived in the nested `Table` breakdown. A standalone `ConditionCheck` - the action that asserts something about an item without touching it - costs 2 write units, which is the little counterintuitive one from the article: it reads an item to check a condition, but the transaction path bills it as a write.

The replay was the number I most wanted right. Send a `TransactWriteItems` with a `ClientRequestToken`, send the same token again inside the idempotency window, and DynamoDB doesn't repeat the write - it hands back the stored result and charges you read capacity for it. Dynoxide was faking that by relabelling the first call's write units as read. It now recomputes a genuine transactional read, rounded at 4KB read granularity rather than the 1KB writes round at, so above 1KB the two diverge: for a ~1.5KB item the first call reports 4 write units and the replay 2. That's the same separation the [suite proved against real AWS](https://martinhicks.dev/articles/what-a-conditional-dynamodb-transaction-costs) once I pushed the test item past 1KB.

PartiQL's `ExecuteTransaction` was worse off - it ignored `ClientRequestToken` entirely and re-applied the statements on every call. It now honours idempotency the same way, replaying the stored result within the window instead of re-running, and reports capacity split by statement kind (write for a write set, read for an all-`SELECT` read set, read on a replay) at 2 units per statement, where it used to hand back a flat 1-unit estimate with no split.

While I was in there I closed a race on the same path. A same-token `TransactWriteItems` now holds the idempotency lock across the whole first call. Before, the lock was released between the cache check and execution, so two concurrent calls with the same token could both run the transaction; the second now waits and replays the first's result.

## The UpdateTable bug

This one came in as a bug report from [Ryan Elian](https://github.com/ryanelian), and it's a genuine correctness bug rather than a wording mismatch. `UpdateTable` was replacing a table's `AttributeDefinitions` with only the attributes carried in the request, where DynamoDB treats them as a delta.

The failure shows up if you build single-table and add indexes as you go. Add a global secondary index and you only need to declare the new index's key attributes - the table keys and any earlier index's attributes are already known. Dynoxide took the request literally and overwrote the stored list, so adding a second GSI with delta-only attributes dropped the table keys and the first index's attributes from `DescribeTable`. The next `PutItem` then failed index-key validation with `Index key attribute GSI1PK missing from AttributeDefinitions` - a write breaking on a table that hadn't changed.

The definitions are now unioned by attribute name, so earlier declarations survive, and a redeclared attribute keeps its existing type (DynamoDB ignores a conflicting type in the delta rather than overwriting or rejecting it). Deleting a GSI prunes its now-orphaned key attributes, and an entry supplied in the delta that no key schema uses is dropped rather than stored. All verified against eu-west-2 ([#129](https://github.com/nubo-db/dynoxide/issues/129)).

## The rest

The other fixes are smaller, mostly around projection expressions and validation ordering:

-   **Projection validation now runs up front**, before any item is read, across `GetItem`, `Query`, `Scan`, `BatchGetItem` and `TransactGetItems`. Overlapping paths (`a` and `a.b`), duplicate paths, and undefined expression-attribute names are rejected with DynamoDB's `Invalid ProjectionExpression:` message, so a projection that matches nothing rejects rather than quietly returning an empty result.
-   **Projecting several indices of one list** now comes back compacted and in ascending order (`#l[2], #l[0]` on `[l0, l1, l2]` yields `[l0, l2]`), and naming two sub-attributes of the same list index merges them into one element instead of splitting each into its own ([#126](https://github.com/nubo-db/dynoxide/issues/126)).
-   **`Query` accepts a key-condition comparison with the value on the left** (`:lo <= #sk`, treated as `#sk >= :lo`), and rejects a nested path on a key attribute with DynamoDB's own message rather than dynoxide's wording.
-   **`CreateTable` turns away two combinations it used to accept**: a `StreamSpecification` that disables the stream but still supplies a `StreamViewType` ([#115](https://github.com/nubo-db/dynoxide/issues/115)), and a secondary index using `ProjectionType: INCLUDE` without a `NonKeyAttributes` list ([#116](https://github.com/nubo-db/dynoxide/issues/116)).
-   **A few message and ordering fixes**: `Query` and `Scan` now return DynamoDB's exact wording when `Select: SPECIFIC_ATTRIBUTES` is given with no projection ([#121](https://github.com/nubo-db/dynoxide/issues/121)); `BatchGetItem` rejects mixing a `ProjectionExpression` on one table's block with `AttributesToGet` on another; and `PutItem` and `UpdateItem` report an invalid `TableName` on its own, before the `Return*` enum checks, matching the order AWS reports them in.

0.11.2 is out everywhere dynoxide ships:

-   **npm:** `npm install --save-dev dynoxide`, then `npx dynoxide`
-   **Homebrew:** `brew install nubo-db/tap/dynoxide`
-   **Cargo:** `cargo install dynoxide-rs` ([crates.io](https://crates.io/crates/dynoxide-rs), [docs.rs](https://docs.rs/dynoxide-rs))
-   **Docker:** `ghcr.io/nubo-db/dynoxide`, mirrored best-effort to Docker Hub (`nubodb/dynoxide`) and ECR Public
-   **GitHub:** [source and pre-built binaries](https://github.com/nubo-db/dynoxide/releases), plus the `nubo-db/dynoxide/action` GitHub Action for CI

Same standing offer as always: if dynoxide does something real DynamoDB doesn't, that's a bug worth filing. The `UpdateTable` one above started exactly that way.

## Further reading

-    [What a conditional DynamoDB transaction actually costs
    
    The AWS docs aren't clear on what a conditional TransactWriteItems costs in capacity units, so I ran it against real DynamoDB and pinned the numbers.](https://martinhicks.dev/articles/what-a-conditional-dynamodb-transaction-costs)
-    [Dynoxide 0.11.1: four conformance fixes
    
    Dynoxide 0.11.1 is a small patch: four fixes where the engine evaluated a condition expression or validated a number differently from real DynamoDB - map and list equality, the 32-level nesting limit, number-set precision, and a leading-plus on numbers.](https://martinhicks.dev/articles/dynoxide-0111-conformance-fixes)
-    [Building a DynamoDB conformance suite
    
    How I built a 526-test DynamoDB conformance suite, and what it found in DynamoDB Local, LocalStack, Dynalite, and Dynoxide.](https://martinhicks.dev/articles/dynoxide-conformance-suite)

---

Source: https://martinhicks.dev/articles/dynoxide-0112-conformance-fixes
