The Dynoxide hexagonal logo centred on a near-black background

Dynoxide 0.12.0: PartiQL RETURNING, wasm conformance

Dynoxide 0.12.0 is out. PartiQL now honours RETURNING, and the browser build has been through the 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)

The browser build on the conformance board

At the end of the 0.11.0 post 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 as its own target, 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. 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. It flipped around across regions for a while and has settled on rejection everywhere. (#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, #156, #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)
  • 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)
  • 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)
  • UpdateTable adding a GSI validates the new index's key attributes against the request's own AttributeDefinitions, not the merged stored set. (#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, docs.rs)
  • Docker: ghcr.io/nubo-db/dynoxide, mirrored best-effort to Docker Hub (nubodb/dynoxide) and ECR Public
  • GitHub: source and pre-built binaries, plus the nubo-db/dynoxide/action GitHub Action for CI