---
title: "Dynoxide 1.6.0 release notes - Martin Hicks"
description: "Dynoxide 1.6.0 makes PartiQL queries read only the items they ask for, as DynamoDB does, and fixes error messages for numbers in odd places. What changed and what to check before upgrading."
canonical: https://martinhicks.dev/articles/dynoxide-160-release-notes
last-updated: 2026-11-09
---

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

# Dynoxide 1.6.0 release notes

[Dynoxide](https://dynoxide.dev) 1.6.0 went out on Saturday night and I forgot to write it up, so this is a couple of days late. Most of the release is about PartiQL, and specifically about how much a `SELECT` reads.

## PartiQL reads what DynamoDB reads

Take a query for two orders in one customer's partition:

```
SELECT * FROM orders WHERE pk = 'customer#42' AND sk IN ['order#1', 'order#3']
```

DynamoDB treats that as two key lookups. It reads those two items and charges a read for each one, whether the item exists or not. Dynoxide read the whole partition instead and charged for all of it. The result set was right, so nothing failed, but the consumed capacity was whatever the partition happened to weigh. On a customer with a few hundred orders that's a number DynamoDB would never give you.

The same was true for an OR across sort keys (`sk = 'order#1' OR sk = 'order#3'`). And an OR where every branch named the same partition key was worse: it scanned the whole table, charging for rows in other partitions and counting them towards `Limit`.

All of those now read only the keys they name, on tables and on both kinds of secondary index. Pagination follows along. A `Limit` read that finishes on the last key doesn't hand back a `NextToken`, so fetching two keys with `Limit: 1` takes two pages, as it does on DynamoDB, where it used to take three.

I care about this more than most fixes because a lot of what I use Dynoxide for is working out what something will cost before it goes anywhere near AWS ([conditional transactions](https://martinhicks.dev/articles/what-a-conditional-dynamodb-transaction-costs), for example). If the capacity figures are off, so is everything built on them.

Thanks to [@jay23606](https://github.com/jay23606), who [raised](https://github.com/nubo-db/dynoxide/issues/229) and [fixed](https://github.com/nubo-db/dynoxide/pull/269) the table-scan case.

A handful of PartiQL queries that DynamoDB accepts and Dynoxide didn't (or the other way round) got sorted. `ORDER BY` works when OR branches share a partition, big OR filters with forty pairs of conditions run instead of failing, and a `NOT` over a key attribute in the wrong place gets DynamoDB's error. The release notes have the specifics.

## Numbers in the wrong place

The other chunk of work is about what happens when a request carries an odd number. Send ExecuteStatement a `Limit` of `1.5` and DynamoDB quietly truncates it to 1. Dynoxide refused it with an error from serde, the JSON library underneath, which isn't a message anyone's code expects to see. `1e400` as a Scan `Segment` got a bare `SerializationException`, and a number where `TableName` should be got a different message from the one DynamoDB gives, which depends on how you wrote the number (`NUMBER_VALUE` or `DECIMAL_VALUE`).

None of it matters much day to day. It matters if you've got tests asserting on validation errors. Each of these was captured against DynamoDB in eu-west-2 and us-east-1 last week, and Dynoxide now gives the same answer.

## The one that might break something

Expressions now refuse attribute names starting with an underscore unless they go through `ExpressionAttributeNames`, as DynamoDB does. Dynoxide used to let `_version` through in a filter or update expression, and anything relying on that would have worked locally and failed for real. This covers import rules too, so a `match` expression naming one needs an alias in its `names`.

If your tests assert on consumed capacity or page counts for PartiQL reads, some of those will also move. The new numbers are DynamoDB's.

## Also in this release

[@alfaproject](https://github.com/alfaproject) fixed `UpdateItem` with `ReturnValues: ALL_OLD` on an item that didn't exist yet. It returned the key attributes as the "old" image, where DynamoDB returns nothing ([#292](https://github.com/nubo-db/dynoxide/pull/292)).

There's also a zstd bump for four RustSec advisories. None of them reach the code Dynoxide uses zstd for, but it's built into the binary, so the update ships anyway.

The crate is `dynoxide-rs` 3.3.0 and everything else is 1.6.0. `npm install --save-dev dynoxide@1.6.0` if you're coming from npm. The [release notes](https://github.com/nubo-db/dynoxide/releases/tag/v1.6.0) have the full list.

## Further reading

-    [Dynoxide 1.5.0 release notes
    
    Dynoxide 1.5.0 charges vector reads the way DynamoDB now does, gives stream records unique eventIDs and DynamoDB-length sequence numbers, and matches SearchVectors errors. The full list, and what to check before upgrading.](https://martinhicks.dev/articles/dynoxide-150-release-notes)
-    [Dynoxide 1.4.0: clean shutdowns and JSON Lines import
    
    Dynoxide 1.4.0 adds JSON Lines import, a clean way to stop an embedded server, and closer DynamoDB behaviour for updates and scans. What changed and what to check before upgrading.](https://martinhicks.dev/articles/dynoxide-140-release-notes)
-    [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)

---

Source: https://martinhicks.dev/articles/dynoxide-160-release-notes
