---
title: "Dynoxide 0.12.0: PartiQL RETURNING, wasm conformance - Martin Hicks"
description: "Dynoxide 0.12.0 makes PartiQL honour the RETURNING clause it used to parse and throw away, and puts the browser build through the conformance suite for the first time as its own scored target. Plus a stack of fixes, and one breaking change for Rust crate consumers."
canonical: https://martinhicks.dev/articles/dynoxide-0120-release-notes
last-updated: 2026-07-28
---

![The Dynoxide hexagonal logo centred on a near-black background](https://martinhicks.dev/images/articles/dynoxide-0120-release-notes.png)28th July 2026

# Dynoxide 0.12.0: PartiQL RETURNING, wasm conformance

[Dynoxide](https://dynoxide.dev) 0.12.0 is out. PartiQL now honours `RETURNING`, and the browser build has been through the [conformance suite](https://martinhicks.dev/articles/dynoxide-conformance-suite) for the first time. There's a stack of smaller fixes below, and one breaking change if you build against the Rust crate.

## PartiQL RETURNING

Dynoxide has been parsing the `RETURNING` clause since PartiQL landed, and then dropping it on the floor. `DELETE FROM Music WHERE artist = 'Pixies' RETURNING ALL OLD *` parsed cleanly, deleted the item, and handed you back nothing.

It works now. `DELETE ... RETURNING ALL OLD *` gives you the deleted item in `Items`, and a present but empty `Items` array if the target wasn't there, which is what DynamoDB does over PartiQL rather than what the classic `DeleteItem` path does.

`UPDATE ... RETURNING <ALL|MODIFIED> <OLD|NEW> *` returns the matching projection. The `MODIFIED` variants are the fiddly ones: they return only the paths that actually changed, so a nested `SET a.b = :v` gives back the changed leaf rather than the whole `a` attribute. They exclude the primary key, and return an empty `Items` array when nothing was projected.

`BatchExecuteStatement` honours `RETURNING` on each member, `ExecuteTransaction` rejects one with a top-level `ValidationException`, and the three variants DynamoDB won't take on a `DELETE` (`MODIFIED OLD *`, `ALL NEW *`, `MODIFIED NEW *`) are now rejected with its exact validation message instead of ignored. ([#137](https://github.com/nubo-db/dynoxide/issues/137))

## The browser build on the conformance board

At the end of [the 0.11.0 post](https://martinhicks.dev/articles/dynoxide-0110-wasm-on-npm) I said the next job was holding the browser engine to the same standard as the native one. It's in the [24 July run](https://paritysuite.org/runs/2026-07-24) as [its own target](https://paritysuite.org/targets/dynoxide-wasm), scored separately rather than folded into native dynoxide.

100% across all three tiers, on the 785 of the suite's 998 tests it implements. The other 213 are operations the browser build doesn't do, so it covers 78.7% of the suite where the native build covers 98.6%. A perfect score over the three quarters of the suite the wasm build supports.

That's the core item and table surface, query and scan, the legacy API, validation ordering and error-message fidelity. PartiQL is in the other quarter, so the `RETURNING` work above is native-only.

The run needed a way in, so the engine gained a `dispatchHttp` worker op and there's now a test-only server behind `npm run wasm:serve` that drives the shipping `dist/` bundle in a headless Chromium. It isn't distributed and it isn't a way to run dynoxide - one page's worth of concurrency, no TLS, fresh in-memory database on every start. Details in [docs/wasm.md](https://github.com/nubo-db/dynoxide/blob/main/docs/wasm.md). The package is still published as `0.12.0-preview`.

## Everything else

-   **PartiQL `UPDATE` is not an upsert.** On a key that doesn't exist it now fails with `ConditionalCheckFailedException` and creates nothing. It used to write the item.
-   **Real list-index writes in PartiQL.** `SET tags[0] = :v` was treated as a write to a literal map key called `tags[0]`. It now updates the element, appends when the index sits at or past the end, and `REMOVE tags[0]` shifts the rest up. A `RETURNING MODIFIED` projection over list indices packs the changed elements densely in ascending index order.
-   **`{ "NULL": false }` is rejected again.** In 0.11.0 I made dynoxide accept it, after [watching AWS drop the true-only rule](https://martinhicks.dev/articles/dynamodb-changed-its-validation-errors). It flipped around across regions for a while and has settled on rejection everywhere. ([#145](https://github.com/nubo-db/dynoxide/issues/145))
-   **Table configuration over MCP.** `create_table` and `update_table` now accept `billing_mode`, `provisioned_throughput`, `table_class` and `on_demand_throughput`. Before this, every table created over MCP came out `PROVISIONED` with default throughput, and `update_table` hardcoded all three to none so the engine saw an empty update. ([#154](https://github.com/nubo-db/dynoxide/issues/154), [#156](https://github.com/nubo-db/dynoxide/issues/156), [#157](https://github.com/nubo-db/dynoxide/issues/157))
-   **MCP `describe_table` shows table configuration.** Billing mode, table class and the capacity settings for the mode the table is in. They were previously only visible under `raw: true`.
-   **`serve --schema`.** `dynoxide serve`, and `dynoxide` with no subcommand, take the same `DescribeTable` JSON that `import --schema` took, and create the tables on startup.
-   **Schema import no longer drops `BillingMode` and `TableClass`.** `DescribeTable` wraps both in summary objects the rebuilt `CreateTableRequest` never read, so an on-demand table came back `PROVISIONED`. A table's own `DescribeTable` output now round-trips without degrading. ([#140](https://github.com/nubo-db/dynoxide/issues/140))
-   **The `1 validation error detected: ` envelope.** `PutItem` and `UpdateItem` now carry it on exactly the request-validation families DynamoDB envelopes, and leave the data-plane, structural and limit families bare. Classified per family at the raising site rather than by matching message text, so the message is identical over HTTP, wasm, MCP and the in-process Rust API.
-   **`OnDemandThroughput` semantics, captured against eu-west-2.** Rejected when the effective billing mode is `PROVISIONED`, members at least 1, `-1` on `UpdateTable` removes the ceiling, a partial update merges member-wise rather than replacing. One deliberate divergence: DynamoDB returns `InternalFailure` for an empty object on `UpdateTable` and dynoxide returns a validation error instead of emulating a 500. ([#159](https://github.com/nubo-db/dynoxide/issues/159))
-   **Expression size limit.** Over 4096 bytes is rejected with `Expression size has exceeded the maximum allowed size`, measured on the raw string before substitution, across all five expression parameters. ([#146](https://github.com/nubo-db/dynoxide/issues/146))
-   **`UpdateTable` adding a GSI** validates the new index's key attributes against the request's own `AttributeDefinitions`, not the merged stored set. ([#144](https://github.com/nubo-db/dynoxide/issues/144))
-   **PartiQL error wording.** Parse errors use `Statement wasn't well formed, can't be processed: <detail>`, and a statement not opening with a DML keyword reports `Expected data manipulation`. `BatchExecuteStatement` echoes `TableName` per member, and a member that fails to parse carries the short-form `ValidationError`.

## What breaks

Rust crate consumers only. `partiql::parser::Statement` gained a `returning` field on `Update` and `Delete`, and `BatchStatementResponse` gained `table_name`. Both are now `#[non_exhaustive]`, so constructing or exhaustively matching either needs a `..`. The wire API and the CLI, server and MCP surfaces are unaffected.

0.12.0 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

## Further reading

-    [Dynoxide 0.11.3: closing a Windows port hijack
    
    Dynoxide 0.11.3 sets SO\_EXCLUSIVEADDRUSE on the DynamoDB and MCP listeners on Windows, closing a same-user port hijack that an investigation into TIME\_WAIT parity wasn't looking for, and runs the unit tests on Windows in CI for the first time.](https://martinhicks.dev/articles/dynoxide-0113-windows-listener-hardening)
-    [Dynoxide 0.11.0: wasm on npm, plus a stack of fixes
    
    Dynoxide 0.11.0 ships the browser engine as an npm package, @dynoxide/wasm-engine, and lands a stack of correctness fixes: secondary-index validation on write, sparse-index behaviour, and error messages brought back in line with real DynamoDB.](https://martinhicks.dev/articles/dynoxide-0110-wasm-on-npm)
-    [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-0120-release-notes
