Repository navigation
The crate moves to dynoxide-rs 3.0.0: the expressions and partiql
modules are no longer public, and three request types gain fields. Everything
else a user installs carries 1.3.0.
Security
- An expression containing a multi-byte character could stop the server
(GHSA-gwhp-7jch-7xj6,
High). From 0.9.10 to 1.2.1, a character such as€or an emoji outside a
string in anUpdateExpressionorProjectionExpressionmade Dynoxide panic
while building the syntax error, and release builds abort on panic. It is
now refused with DynamoDB's syntax error. --helpshowed the MCP auth token
(GHSA-3rv6-mrj3-vf9j,
Low). From 0.10.0 to 1.2.1, subcommand help printed the value of
DYNOXIDE_MCP_AUTH_TOKENwhen it was set. Help now names the variable
without showing its value.aws-smithy-jsonmoves to 0.63.1 for GHSA-8ffr-xgwf-xj56. It reaches the
tree only through the AWS SDK the tests and the benchmarks use, never in a
released artefact, and the advisory covers smithy-rs generated servers,
which Dynoxide isn't. The fixed release needs Rust 1.94.1, so the test suite
now needs Rust 1.94.1 or later. The crate, and every build of it, still
compiles on 1.88.
Behaviour changes
- A PartiQL
NextTokenis bound to its statement the way DynamoDB binds it:
to the statement's text exactly as written, and to its parameters by value.
The same statement pages on, and may change itsLimit. A page whose
statement differs at all (a space, a keyword's case, the projection, a map
literal's key order) or whose parameters are different values is refused
withNextToken does not match request, where a statement that parsed
alike used to resume. A number parameter is the same value when it is the
same decimal at the same scale, so1E2against1e+2,01against1
and1.5E1against15resume, and0against0.0or100against
1E2are refused. A set parameter is the same value with its members in
any order, a number set's members compared by value whatever their scale.
A statement with a list or map parameter is never resumed, even with the
same parameter sent again: its first page still hands out a token, and the
next page is refused with it. A map literal with more than one key used to
be refused on its own next page, because its key order was drawn afresh
for every request; it pages now. Captured against eu-west-2 and us-east-1. UpdateItemis held to the 400KB limit the way DynamoDB holds it: by a
charge worked out from the request, alongside the finished item's own size,
rather than by the finished item less its key. Nothing the table already
stores enters the charge, so an update beside large attributes, maps, lists
or sets it leaves alone now succeeds where it was refused. Operands are
charged for what the request writes, so an arithmetic update, an
if_not_exists, alist_appendor a copy from another path near the limit
can now be refused where it succeeded, though the item barely grows. An
update whose charge is over the limit is refused before its condition is
evaluated. The legacyAttributeUpdatesparameter is measured by the
finished item alone. Captured against eu-west-2.TransactWriteItems
updates and PartiQLUPDATEare measured as before.- A
SETthrough a list index past the end of the list appends the value, as
DynamoDB does, where it used to pad the gap with NULLs; several such writes
to one list append in the order of their indexes. A path that steps through
an element past the end of a list, or through a NULL, is refused with
DynamoDB's document path error where it used to create a map or a list
there. This coversUpdateItemandTransactWriteItemsupdates; PartiQL
UPDATEalready appended. Captured against eu-west-2. - For Rust users of the crate:
AttributeValue'sSerializewrites set
members in DynamoDB's order (string and binary sets by their bytes, number
sets by value), so a set no longer keeps the order it was built in through
serde. Normalising an item for a write sorts its sets in place. ReturnValuesOnConditionCheckFailureworks on the three PartiQL
operations. Set toALL_OLD, anUPDATEorDELETEwhose WHERE clause is
false on an item that exists hands back the whole item: on that statement's
cancellation reason inExecuteTransaction, on the member'sErrorin
BatchExecuteStatement, and onExecuteStatement's
ConditionalCheckFailedException, which then writes its message as
Messageand its number sets in hash order.NONE, leaving it out, a
missing item, a duplicateINSERTand a statement without a condition give
no item. A value other thanALL_OLDorNONE, an empty string included,
is refused as a member, ahead of the statement and its parameters, where
ExecuteStatementandExecuteTransactionused to accept anything. A
transaction's mode takes part in itsClientRequestTokenmatch, leaving it
out counting asNONE, and its over-long echo prints the value sent.
ExecuteStatementlists its member errors in DynamoDB's order: the mode,
Limit, the statement,ReturnConsumedCapacity, then the parameters.
Captured against eu-west-2 and us-east-1; this corrects the 1.0.0 entry
that found the option inert on a batch.- Opening a database brings it up to schema version 11, and the upgrade is
one way. It changes what an older Dynoxide reads only for a global secondary
index with a multi-attribute key, whose entries are rebuilt, and a table with
a vector index, whose stored item sizes are recomputed. Earlier versions
don't check the version a file records, so one would open such a file and
read those indexes or sizes wrongly: if a file has either and you may need
to take it back to 1.2.x, keep a copy. From this release a file written by a newer Dynoxide is refused,
with an error naming its schema version and the newest this build reads,
and is left unchanged, where it used to be opened as it was. That covers
Database::new, an encrypted open, the snapshot a restore reads from, and
the browser build's database. - While it opens, a database file waits up to 30 seconds for a lock another
process holds, where it gave up after five, so a second Dynoxide opening a
file while the first brings it up to date waits for it and then finds the
work done. Once open it waits five seconds, as before, so a request held up
by another process's lock gives up after five seconds rather than holding
every other request up behind it. A restore still gives up on a busy
snapshot after five seconds, and an in-memory database is unchanged.
Added
- The MCP tools that wrap a DynamoDB read or write take
return_consumed_capacity(TOTAL,INDEXESorNONE) and return the
ConsumedCapacityblock the DynamoDB API returns for the same request. That
covers the item tools,query,scan,search_vectors,batch_get_item,
batch_write_item, both transaction tools and the three PartiQL tools. Any
other value is refused with DynamoDB's validation error. - The MCP
execute_partiqltool takesreturn_values_on_condition_check_failure
(ALL_OLDorNONE), asExecuteStatementdoes. A tool error for a failed
condition now carries the item the condition was evaluated against when
ALL_OLDwas asked for, onexecute_partiqland on the item tools, which
took the argument before but never handed the item back. --log quietsuppresses informational startup and shutdown messages on
stderr. It works onserveand the no-subcommand form,serve --mcp,mcp
(stdio and--http), andimport --serveor--mcp. Warnings, errors, the
first-run MCP token message and the import's own report still print. On
import,--logconflicts with--output. Without the flag the output is
unchanged.ServerOptions(at the crate root, and atserver::ServerOptions),
server::start_with_optionsandmcp::serve_http_with_optionsgive embedded
Rust callers the same switch for both servers.Storage::item_count_and_size,gsi_item_count_and_size,
lsi_item_count_and_sizeandvector_item_count_and_sizereturn a table's
or an index's item count and total size, the figures DescribeTable reports.
Changed
Breaking (Rust API)
- The
expressionsandpartiqlmodules are no longer public. They hold the
parsers for DynamoDB's expression syntax and for PartiQL, whose shape changes
as compatibility work lands, and this release changed both substantially.
Expressions and PartiQL statements are still sent throughDatabaseand the
actionsrequest types, which this does not change. ParameterizedStatementandExecuteStatementRequestgain
return_values_on_condition_check_failure, andBatchStatementErrorgains
item, so struct literals of them need the field adding, or
..Default::default().actions::index_capacity::WriteCapacity::condition_checktakes the table's
vector indexes, so a vector attribute is charged at its billed size.
Fixed
-
An
UpdateItemwhoseUpdateExpressionwas only whitespace, such as
" ", answered 200 and wrote the item: the key alone for a new item, or
the item as it was for an existing one. 1.2.1 did this too. An expression made
only of whitespace is now refused as DynamoDB refuses it, as a syntax error
at its end with the whitespace as sent:Invalid UpdateExpression: Syntax error; token: "<EOF>", near: " ", and nothing is written. A
ProjectionExpressionof whitespace is refused the same way on every read
that takes one, where it returned items with no attributes. A
ConditionExpression,KeyConditionExpressionorFilterExpressionof
whitespace was already refused, but with an emptynear; it now gives the
whitespace too, in the envelope onPutItem,UpdateItemandDeleteItem
and bare elsewhere. A transaction'sUpdatewith an empty
UpdateExpressionis refused up front withInvalid UpdateExpression: The expression can not be empty;, where it wrote an item holding only its key.
Captured against eu-west-2 and us-east-1, the transactions also against
ap-southeast-2 and eu-central-1.TransactGetItemsrefuses an empty or
whitespaceProjectionExpressionwith the same messagesGetItemgives,
and does so before it looks up any table, so aGeton a table that doesn't
exist gets the projection error (captured against eu-west-2). -
A legacy
QueryFilterorScanFiltercondition with aComparisonOperator
DynamoDB doesn't have, such asFOO, or with none at all, was dropped, so
the Query or Scan ran without it, and one whose every condition dropped
returned every item. A lower-caseeqran asEQ. 1.2.1 did both. An
unknown operator,eqincluded, is now refused withOne or more parameter values were invalid: Invalid comparison operator, and a missing one with1 validation error detected: Value null at 'scanFilter.a.member.comparisonOperator' failed to satisfy constraint: Member must not be null(queryFilteron a Query).KeyConditionstakes
the same rule, where it also raneqasEQand refused an unknown or
missing operator in Dynoxide's own words. Captured against eu-west-2,
ap-southeast-2, eu-central-1 and us-east-1; us-east-1 answers an unknown
operator with the member's enum constraint instead, and the other three
regions are followed. -
A legacy
AttributeUpdatesaction written in another case, such asput
orDelete, was read as its upper-case form and applied. 1.2.1 did this
too. An action other thanADD,DELETEorPUTis now refused before the
table is looked up, with DynamoDB's own text:1 validation error detected: Member must satisfy enum value set: [ADD, DELETE, PUT]' for Action.
Captured against eu-west-2 and us-east-1. -
A legacy condition on an attribute whose name holds a
-, a.or a
character outside ASCII, such asuser-idors.k, was refused with a
syntax error naming an internal placeholder, such asnear: "#kc_user-id".
1.2.1 did this too.KeyConditions,QueryFilter,ScanFilterand
Expectednow run on any attribute name, as DynamoDB runs them. Captured
against eu-west-2 and us-east-1. -
A legacy
QueryFilterorScanFiltercondition whose value didn't read,
such as{"NULL": false}or{"BOOL": "maybe"}, was dropped, so the Query
or Scan ran without that condition and returned items it should have
filtered out. This was in 1.2.1. Such a condition is now refused as
DynamoDB refuses it, inQueryFilter,ScanFilter,KeyConditionsand
Expectedalike: aNULLthat reads as false (false,"no","false")
withNull attribute value types must have the value of true, ahead of the
operator's own checks, and aBOOLorNULLthat doesn't read as a
boolean by failing the whole request withUnexpected token received from parser. AKeyConditionsthat didn't read no longer leaves the Query
asking for a key condition. A legacyINchecks the type of every value in
its list, where it looked at the first, and a value whose other member is
nulltakes the type of the member it has;Expectedrefuses anINof a
BOOLorNULLtoo, as the filters do. A condition'sExistsreads a
string as aBOOLmember does, and one read from a string makes the
request's refusals bare, as aBOOLorNULLread from a string does.
Captured against eu-west-2 and us-east-1. -
ExecuteTransactionlists a reason for every statement that failed, as
DynamoDB does, where it stopped at the first and reportedNonefor the
rest. A key of the wrong type on any statement is reported alone, with
Nonefor every other statement, a statement whose condition is false
included. ADELETEwith a condition, of an item that isn't there, does
nothing when the transaction commits, as before, and is reported as a failed
condition, with no item, when the transaction is cancelled. Captured against
eu-west-2 and us-east-1. -
A
TransactWriteItemsthat SQLite failed part way through, on a full disk
or an I/O error, could keep the actions that came after the failure. SQLite
rolls the whole transaction back on such an error, so each later action
committed on its own and the transaction's own rollback then failed. A
storage failure now stops the transaction where it happens and is returned
as an internal error, so nothing the transaction wrote is kept.
ExecuteTransactiondoes the same. The error is SQLite's own, such as
backend: database or disk is full, on any write, where it went on to say
that the rollback had failed too because SQLite had already ended the
transaction. -
Restoring a snapshot taken by an earlier version brings it up to date as
opening the file would, running the schema migrations its recorded version
calls for. It used to be restored as it was, so a snapshot from before a
schema change came back without that change. The copy is brought up to date
in a private temporary database before it replaces anything, so a restore
that fails leaves the database as it was, andDatabase::restore_fromopens
the snapshot file read-only: a path that doesn't exist is now an error,
where it used to create an empty file and restore that over the database,
and so is a file that isn't a Dynoxide snapshot (an empty file,:memory:
or another program's database). Restoring from a snapshot in WAL mode may
leave its-waland-shmfiles beside it. After a restore, transactions
cached for idempotent retries and vector index creation windows are
forgotten, as the data they described has gone. A backup or restore that
finds a database busy keeps trying until it has been busy for five seconds by
the clock (plus at most one busy wait of SQLite's own) and then reports it
locked; one that keeps starting over because another connection keeps
changing the database being copied pauses briefly after each restart and
gives up after 50 of them. -
A number whose exponent is too large for a 32-bit integer is range-checked
rather than read with an exponent of 0.1e99999999999was stored as 1; it
is now refused as an overflow, and1e-99999999999as an underflow, as
DynamoDB does. An exponent too large for a 64-bit integer is refused as a
value that cannot be converted, which is DynamoDB's message for it. -
Zero is held to the number range as well, as if its last written digit were
a 1, so0e126is refused as an overflow and0e-131as an underflow, where
both used to be stored as 0. -
A
KeyConditionExpressionrejects an operand its operator does not take, as
DynamoDB does:begins_withtakes a string or binary, and<,<=,>,
>=andBETWEENtake a string, number or binary
(#220). A number in
begins_with(sk, :v)used to run as a prefix match on the stored key, and a
boolean, null, list, map or set became an empty prefix that matched every
item in the partition. The check runs before the table is looked up, so a
missing table or index reports the operand rather than itself. -
PartiQL's
begins_withrejects an operand that is not a string or binary,
on any attribute and underNOT, where it used to accept the statement. -
A global secondary index whose key has more than one partition key or sort
key attribute works as DynamoDB's does. Items sort by each sort key attribute
in turn, an item missing any key attribute is left out of the index, a Query
takes an equality on every partition key attribute and on a leading run of
sort key attributes with a range condition allowed only on the last one
named, andLastEvaluatedKeycarries every key attribute. Such an index used
to sort and filter on its first sort key attribute alone, index items missing
a later one, and refuse any key condition with three clauses. In a database
an earlier version wrote, on disk or in the browser's OPFS storage (including
one from Dynoxide 0.10 to 0.13, which recorded no schema version), such an
index is rebuilt the first time the database is opened, in one transaction,
and an interrupted step leaves the database at the version before that step.UpdateTablecan now add one. CreateTable and UpdateTable
check such a key as DynamoDB does: at most four partition key and four sort
key attributes, partition key attributes first, no name twice. A local
secondary index still takes one sort key attribute. A PartiQLSELECTon such
an index is refused, as DynamoDB refuses it, withPartiQL statements cannot be executed on tables with multi-attribute HASH keys or multi-attribute RANGE keys.; the table itself and its other indexes stay readable and writable. -
A
KeyConditionExpressionwith more clauses than the key has attributes is
refused with DynamoDB's message,The number of query conditions (3) exceeds the number of key attributes defined in the schema (2), rather than
Unexpected token in KeyConditionExpression: AND. Two conditions on one key
attribute report that each key takes only one. -
A key condition value whose type differs from its key attribute's type is
refused withCondition parameter type does not match schema type, on a
table and on any index. That covers a string compared with a number sort
key, which used to match nothing, and a map, list, boolean, null or set
value, which used to fail withCannot convert partition key value to string. -
begins_within a key condition is case-sensitive. A lower-case prefix
matched upper-case sort keys too. -
A key condition can name the sort key before the partition key. A range
condition on a one-attribute partition key, such asbegins_with(pk, :v),
is refused withQuery key condition not supported, and like every key
condition problem that needs the key, only once the table and index are
found: a missing table or index reports itself first. -
CreateTable with an index reports an attribute definition no key uses as
Some AttributeDefinitions are not used, listing the definitions and the keys
used, rather than as a count mismatch. An index key attribute with no
definition names the first index missing one, and its lists, like the unused
ones, come in DynamoDB's own order. When the request defines fewer
attributes than that index has key attributes, CreateTable and UpdateTable
both giveInvalid KeySchema: Some index key attribute have no definition.
A GSI key schema of more than eight elements is refused as the request is
read. -
TransactWriteItemsrejects an expression over 4096 bytes on any action, and
aPutitem nested past 32 levels, with a top-levelValidationException
before any action runs, as DynamoDB does. Both used to come back as a
TransactionCanceledExceptionwith aValidationErrorreason. ThePut
item-size check moves forward with them, so all three now win over a missing
table, a duplicate target or an empty key value elsewhere in the request. -
An
ExpressionAttributeValuesentry nested 32 levels deep that a transaction
action only compares is no longer refused: the condition is evaluated, as
DynamoDB does. AnUpdatethat writes one into the item still cancels on the
item's nesting limit, now with DynamoDB's shorter reason message. From 33
levels an entry is refused up front with a top-levelValidationException
naming its key, used or not, where aPutwhose condition compared one used
to write its item. -
TransactWriteItemsrejects an empty or unparseableConditionExpressionon
any action with a top-levelValidationExceptionbefore any action runs,
where it used to cancel the transaction. An unparseableUpdateExpression,
anExpressionAttributeNamesorExpressionAttributeValuesentry no
expression uses, and a value a condition names but the request doesn't
define are refused the same way, ahead of a missing table, a duplicate
target or a key that doesn't match the schema. APutthat carries
ExpressionAttributeValuesand noConditionExpressionis accepted, as in
us-east-1, where it used to cancel. -
Expression syntax errors name the token the parse stopped at and the text
around it, as DynamoDB does:Syntax error; token: "x", near: ") x y", or
token: "<EOF>"for an expression that ends early. This covers condition,
filter, key condition and update expressions, which used to say things like
Expected operand, got end of expression. A malformed key condition also
gains itsInvalid KeyConditionExpression:prefix. -
An expression over 4096 bytes is refused without the trailing
expression size: <n>, as DynamoDB now sends it. A Scan'sFilterExpression
and a transaction'sConditionExpressionkeep the figure, which us-east-1
still sends there. -
PutItem, UpdateItem and DeleteItem wrap the nesting error for a value past
32 levels in1 validation error detected:, and DeleteItem wraps its
ConditionExpressionsyntax and size errors the same way. From 63 levels a
stored item's error drops the wrapper and an expression value is refused
naming its key. AnUpdateExpressionthat leaves the item nested too deep
gets DynamoDB's shortNesting Levels have exceeded supported limits. -
UpdateItem reports a
ConditionExpressionsyntax error ahead of an unused
expression attribute value. -
PutItem, UpdateItem and DeleteItem wrap an unused
ExpressionAttributeNames
orExpressionAttributeValuesentry in1 validation error detected:, and
report it, along with a value or name the condition uses but doesn't define,
before looking up the table. GetItem, Query and Scan send it bare, as
before. DeleteItem now envelopes all itsConditionExpressionerrors,
including reversed or mixed-type BETWEEN bounds and operand types, and an
undefined value in UpdateItem's condition gains its
Invalid ConditionExpression:prefix. -
A list index written with a leading zero, such as
l[00]orl[01], is a
syntax error in every expression (token: "0", near: "00]"), as DynamoDB
reads it. It used to be accepted as the index its digits spell. -
DescribeTablereportsTableSizeBytesas the total size of the table's
items, where it was always 0, so the MCP server'ssize_bytesis right too.
UpdateTable's response carries the sameItemCountandTableSizeBytes,
where both were 0. DynamoDB refreshes these figures about every six hours;
Dynoxide reports them live. Captured against eu-west-2. -
DescribeTableandUpdateTablereport each GSI's and LSI'sItemCountand
IndexSizeBytes: the number of items the index holds and the total size of
what it projects for them, where both were always 0. Each vector index
reports its entries and their billable size, as DynamoDB does: the vector at
4 bytes per dimension, its attribute's name and the rest of the projection. Captured against eu-west-2. -
On a table with a vector index, a write is charged for the vector at 4 bytes
per dimension, whatever numbers it holds, andTableSizeBytescounts it the
same way, as DynamoDB does. Dynoxide used to size it as a list of numbers, so
a write of a 1,024-dimension embedding could cost two or three units more
than on DynamoDB. Read units and the 400 KB item limit still size the vector
by its numbers, which is DynamoDB's rule for those, and so do a GSI's and an
LSI's share of the write and theirIndexSizeBytes, even when the index
projects the vector. A database written by an earlier version has its vector
tables' sizes recomputed once when it is opened, in one transaction, and
creating or deleting a vector index recomputes that table's. -
Database::sweep_ttl, and the MCPsweep_ttltool that calls it, no longer
hold the database lock for the whole sweep. The sweep takes the lock a page of
25 items at a time and lets requests already waiting for it go first between
pages, so a read or write waits for one page rather than for a read of every
TTL table. The first page of each table also counts its rows, so the longest
single hold is that count plus a page. Between pages the sweep waits up to a
second for the queued requests, including the time they hold the lock, before
it carries on. A sweep reads no more of a table than it held when the sweep
reached it, so a table that keeps growing cannot keep it running, and an item
written while the sweep is under way may be left for the next one. -
--helpno longer shows the value ofDYNOXIDE_MCP_AUTH_TOKENwhen it is
set. It still names the variable. The MCP auth types' debug output no
longer shows the token either. -
A prerelease tag such as
v1.3.0-rc.1no longer ships as if it were the
latest release. Its GitHub Release is flagged as a prerelease, so the GitHub
Action's defaultversion: lateststays on the last stable release, and the
Homebrew tap, the MCP registry and dynoxide.dev are left alone. A stable
crate version is not published to crates.io from a prerelease tag. npm still
publishes a prerelease tonext, and containers still get the exact tag
only.docs/versioning.mdhas the full list. -
docs/library.mdno longer tells readers to run
cargo add dynoxide-rs --features encryption, which stops on the
native-sqliteandencryptioncompile guard because the default features
stay on. It now adds--no-default-features, as the rest of the page does. -
PartiQL reads a number written with an exponent as one number, so
WHERE pk = 1e5matches 100000 rather than 1, andINSERT ... VALUE {'pk': 2e3}parses. A literal whose exponent, or whose scale (the digits
after the point less the exponent), is out of range is refused as a statement
that is not well formed, as DynamoDB does. -
PartiQL reads
--and/* ... */comments as whitespace wherever they
appear. A comment before the WHERE clause used to make a SELECT return every
row, and a block comment was refused. -
A PartiQL table alias (
FROM "T" torFROM "T" AS t) on SELECT, UPDATE or
DELETE is refused with DynamoDB's "Aliasing is not supported". A SELECT used
to run it as an unfiltered read. -
A PartiQL statement with a token left over after it, such as
WHERE pk = 1 foo, is refused with "Unexpected token after expression"
rather than run without the extra token. A SELECT'sORDER BYclause must
be well formed and last, so a WHERE written after it is refused rather than
dropped. -
PartiQL applies a SELECT's
ORDER BY. It used to be accepted and ignored,
soORDER BY sk DESCcame back ascending. As in DynamoDB, it reads each
partition the WHERE clause pins, in partition-key order, by the sort key
term's direction (descending when only the partition key is named), on a
table or an index, a page at a time when the read is paged. The forms
DynamoDB refuses are refused with its messages: no WHERE clause, a term that
is a nested path or not part of the key, a partition key not pinned with=
orIN, and more than one partition without the partition key in the
clause.NULLS FIRSTandNULLS LASTare accepted. -
PartiQL reads a number written with no digit before its point, such as
.5
or-.25e1, where it used to refuse the statement. A number takes one
point, so1.2.3is refused as a token left over, and an exponent marker
with no digits (5e) is refused as a statement that is not well formed. -
PartiQL checks a number written in a WHERE clause the way it checks one being
stored, soWHERE pk = 1e126is refused with DynamoDB's overflow message
rather than run as a comparison that matches nothing. Number errors on
PartiQL literals end with DynamoDB'sunder <path>, naming where the
number sits inside the value (under root,under root.x,under root[1]),
and are reported before the table or item is looked up, the first one
written first and an UPDATE's WHERE clause before its SET. A literal at the
very top of the exponent range is reported as an underflow, as DynamoDB's
PartiQL reports it. -
A PartiQL literal nested 32 levels deep is refused where it reaches that
level, with DynamoDB'sNesting Levels have exceeded supported limitsand
the path to it, and a parameter nested exactly 32 levels deep gets the same
short message rather than the long one. -
PartiQL statements check every parameter value before reading the
statement, as any attribute value is checked (its numbers, and empty or
duplicated sets), so a parameter DynamoDB can't store is refused even when
the statement doesn't use it: onExecuteStatement, against the statement in
aBatchExecuteStatement(an empty value and{"NULL": false}included,
where a{"NULL": false}parameter used to refuse the whole batch), and for
the whole request in anExecuteTransaction, which writes nothing. -
A PartiQL statement against a table that doesn't exist says
Requested resource not found, as DynamoDB does, without: Table: <name> not found;
in a transaction it cancels with theResourceNotFoundreason rather than
InternalError. -
An empty map key anywhere in a PartiQL literal is refused with DynamoDB's
Path component cannot be an empty string, where it used to be stored. -
A PartiQL SELECT whose OR branches name overlapping keys, such as
pk = 'a' OR pk = 'a'or sort-key ranges (begins_withincluded) that meet
in one partition, is refused with DynamoDB'sOverlapping conditions with range keys are not supported in where clause. As in DynamoDB, a clause with
a branch that puts two different conditions, orNOT ... BETWEEN, on the
sort key isn't checked, and the check comes after anORDER BYterm is
checked and before the rest of theORDER BYchecks. -
A PartiQL key value DynamoDB can't read the key by is refused with its
messages:Key value must be of type S, N, or Bnaming the key and the type
for a type no key can have, such as a map, rather thanInvalid key value,
andKey attribute's data type should match its data type in table's schema
for a key type that isn't the key's, where a SELECT, UPDATE or DELETE used to
match nothing and an INSERT gave PutItem's message. Both apply to literals
and parameters, to the sort key and to an index's keys, and a SELECT's OR
branches are checked in the order written until one that isn't a key read.
In a transaction, a SELECT key, or an UPDATE or DELETE key of the wrong key
type, cancels with GetItem'sThe provided key element does not match the schema, an UPDATE or DELETE key no key can have is refused up front, and an
INSERT keeps PutItem's message. -
A SELECT in a
BatchExecuteStatementor anExecuteTransactionmay name only
the primary key, as DynamoDB requires: each key attribute once, with=or
anINof one value, the same condition written twice counting once. One
that also filters on another attribute is refused rather than run, and
overlapping key conditions are reported first. -
ExecuteTransactionreports a statement that doesn't parse with
Validation failed in TransactStatements[N]:in front, as it already did
for its other per-statement refusals. -
A PartiQL statement that doesn't begin with SELECT, INSERT, UPDATE or DELETE
is refused as DynamoDB refuses it, reading it as an expression rather than
answeringExpected data manipulationto everything:Unsupported operation: Idfor a lone name,Path,Lit,Listand the like for other
whole expressions,EXISTS can only be used in ExecuteTransaction write requests.for an operator, comparison or call, and DynamoDB's syntax
messages where one breaks off or carries on (Unexpected token after expressionforINSRT INTO ...,Expected RIGHT_BRACKET,Unexpected keywordand others).SET a = 1andREMOVE aare refused for having no
table, and a verb with nothing it needs after it (DELETEalone,INSERT
with noINTO) is refused in DynamoDB's words. -
A PartiQL statement may end with a semicolon, as in DynamoDB, including
after aRETURNINGclause; a second statement after it is refused with
Unexpected token after semicolon. (Only one query is allowed.). -
A successful PartiQL INSERT, UPDATE or DELETE through
ExecuteStatement
returns an emptyItemslist unless it has aRETURNINGclause, and an
ExecuteTransactionof writes returns an emptyResponseslist, as DynamoDB
does. They used to return noItemsat all and one empty entry per
statement. -
A PartiQL WHERE clause that stops where a condition should start is refused
in DynamoDB's words:Unexpected termwhen nothing followsWHERE,NOTor
an opening parenthesis,Missing right-hand side expression of infix operatorwhen nothing followsANDorOR, andUnexpected operatoror
Unexpected keywordwhen an operator or a clause keyword stands in its
place. It used to sayExpected condition in WHERE. -
PartiQL runs
FROM "T" WHERE ... SET ...(orREMOVE ...) as an UPDATE, as
DynamoDB does, where it used to sayExpected data manipulation, and an
UPDATE of either form takes any number of SET and REMOVE clauses in any
order. An UPDATE or DELETE takes its key only from equalities among the WHERE
clause's top-level conditions: a key named only under anOR, aNOTor an
IN, or no WHERE clause at all, is refused withWhere clause does not contain a mandatory equality on all key attributes, and a key given two
values withMultiple conditions on same key.pk = 'a' AND pk = 'b'used
to update the first item it named. In a transaction, the duplicate-item
check takes the same key:(pk = 'b' OR x = 1) AND pk = 'a'beside another
statement onais refused as two operations on one item, and beside one on
bit isn't; the check used to read the key from the OR group. Captured
against eu-west-2 and us-east-1. -
A PartiQL WHERE clause takes
attribute_exists(path)and
attribute_not_exists(path), in any case and compared with a boolean, as
DynamoDB does; both used to fail withUnknown operator: (. An operand that
isn't a document path, or the wrong number of operands, is refused with
DynamoDB's messages. A call to a function DynamoDB doesn't recognise there,
EXISTS(path)among them, is refused asUnrecognized function: <name> at <line>:<column>:<length>before the table is looked up;EXISTS(path)used
to run asattribute_exists. An OR with no key condition under it no longer
counts as a second key range for the overlap check, sopk = 'a' AND (x = 1 OR x = 2)runs. A condition function compares with a path, a size, a
parameter or another call as the boolean it is, with=,<>or in an
INlist, written on either side (b = attribute_exists(a),
? = attribute_exists(a),size(s) = attribute_exists(a)) and as a
member of anINlist (b IN [attribute_exists(a)]), so
attribute_exists(a) = bholds wherebis the boolean the call is, and
is unequal to a missing value or one of another type. Each such comparison
is one condition, so tenattribute_exists(x) <> yANDed run, and
pk = 'a' AND attribute_exists(x) = yreads partitionaby its key and
takes anORDER BY. ItsINlist reads a bare name as an attribute, as
every otherINlist does, where a name used to be compared with the
string of itself. Captured against eu-west-2 and us-east-1. -
PartiQL's
set_addandset_deletework in an UPDATE's SET clause, as in
DynamoDB, where they used to fail withUnexpected token after expression.
A malformed call is refused with DynamoDB's message and position before the
table is looked up, members of the wrong type with its incorrect data type
message, and<<>>anywhere withEmpty bags are not supported. A function
call in an INSERT value is refused as an unsupported data type. -
A PartiQL number parameter is read the way DynamoDB reads it: an exponent or
a scale outside a 32-bit integer isThe parameter cannot be converted to a numeric value: <text>, and1e2147483647is an underflow, in every position
and on every PartiQL operation. They used to be range-checked as an item's
numbers are, as an overflow or underflow by the exponent's sign. -
A PartiQL WHERE clause's
IStests NULL, BOOLEAN, BLOB, STRING, NUMERIC,
STRUCT, TUPLE and LIST as well as MISSING, andIS NOTtheir complement, as
DynamoDB does; only MISSING used to work. Another type name is refused with
DynamoDB'sUnsupported data type for IS operatormessage, andINT4,
INT8,DOUBLE PRECISIONandCHARACTER VARYINGread as type names. An
operator where a value should be, as inpk = LIKE, isUnexpected operator;pk = LIKEused to compare with the stringLIKE. A negated or
<>sort key condition stops the overlap check, asNOT ... BETWEEN
already did.DELETE FROMwith no table isExpected identifier for simple path, andSELECT * FROMwith none isUnexpected termorUnexpected keyword. -
A PartiQL WHERE clause takes
size(path)andattribute_type(path, type),
which used to fail withUnknown operator: (, and compares any two operands
as DynamoDB does: a literal or parameter on the left ('a' = pknames the
item an UPDATE or DELETE changes), two paths, two literals, andsize()on
either side of a comparison, BETWEEN or IN. A size counts a string's UTF-16
code units, a binary's bytes and a collection's members. A comparison with a
missing attribute, or with the size of a value that has none, is true for
<>, as it is in a condition expression; it used to be false. An unknown
attribute_typetype, the wrong number of operands, the same path on both
sides of a comparison,MISSINGoutsideIS, an ordering operator on a
condition function and a size standing alone as a condition are each refused
with DynamoDB's message before the table is looked up, and a comparison with
nothing after its operator isMissing right-hand side expression of infix operator. A size takes a number's type checks: a boolean under an ordering
operator is refused, BETWEEN refuses bounds that are out of order, and a
string compared with a size is simply unequal (captured against eu-west-2
and us-east-1). BETWEEN also refuses bounds of different types, as it does
for a path. A BETWEEN bound or an IN member can be a path or a size as well
as a value:size(s) IN [n]compares with the attributen, where it used
to compare with the string'n', andn BETWEEN size(s) AND 10parses
(captured against eu-west-2 and us-east-1 for a size on either side; a
path between path bounds follows the same reading, not yet captured). -
A PartiQL
INwith one value on a key attribute is read as=, as
DynamoDB reads it:WHERE pk IN ['a']reads item a by its key and is
charged whatpk = 'a'is, where it used to scan the table, and two
statements naming the same item that way are duplicates in a batch or a
transaction (captured against eu-west-2 and us-east-1). -
PartiQL refuses a bag it can't store with DynamoDB's messages and positions:
one that starts with a string at its first member that isn't one, one that
starts with a number without a position, and a parameter,nullor Ion
literal among its members with their own messages. These used to beMixed types in ... set literalor a parse error. An Ion literal between backticks
is refused by its type where it stands;X'...'is a syntax error, as in
DynamoDB, rather than a name and a string; and a list, map or bag whose
members aren't separated by commas is a syntax error too. -
A PartiQL keyword such as
innerorvaluecan't be a path's dot component
unless it is quoted (m."inner"), as in DynamoDB; it used to be read as an
attribute name. Any call in a SELECT list is refused asUnexpected path componentat the function's name, and inside ExecuteTransaction with the
statement's index; onlyCOUNTused to be refused, and without the index.
EXISTS(SELECT ...)in a WHERE clause is refused asUnsupported token in expression: SELECT. -
When a PartiQL statement has more than one problem, the one reported is the
one DynamoDB reports: an unrecognised function, an unsupportedIStype, a
subquery and a literal it can't store come in the order written, WHERE clause
first, and ahead of an operand refusal. -
PartiQL refuses a
begins_withoperand that isn't a string or binary, and
BETWEEN bounds of different types or with the lower above the upper, with
DynamoDB's messages,NOTor not, before the table is looked up. -
A PartiQL INSERT of a key that already exists is reported as a duplicate
before an item nested to the limit is. -
BatchGetItem,BatchWriteItem,TransactGetItemsand
TransactWriteItemsrefuse aReturnConsumedCapacityother thanINDEXES,
TOTALorNONEwith the ValidationException the single-item operations
already return, before anything is read or written. They used to accept any
value and treat it asNONE, and the two writes went ahead.
TransactWriteItemsnow checks itsTransactItemslength, the length of
itsClientRequestTokenand thenReturnConsumedCapacitybefore looking the
token up, so a replayed token is checked too.BatchWriteItemchecks the
capacity mode ahead of a table name that fails its pattern, as DynamoDB does. -
BatchWriteItemandTransactWriteItemsrefuse a
ReturnItemCollectionMetricsother thanSIZEorNONEbefore writing
anything. They used to accept any value. A request with more than one of
these member errors, such as both enums, or an overlong
ClientRequestTokenbeside either, now gets them in one
N validation errors detected:message, as DynamoDB gives them. -
A malformed table name in a transaction is a top-level ValidationException
naming the action's position and type, such as
transactItems.2.member.update.tableName, checked before any action runs.
TransactGetItemsused to report it as a cancellation reason, and both
reported the path astableName. The name must now be 3 to 255 characters,
as DynamoDB requires inside a request body. -
BatchGetItemandBatchWriteItemreport a malformed table name as a
constraint on theRequestItemsmap's keys, echoing the map as DynamoDB
does, andBatchGetItemreports it together with an unknown
ReturnConsumedCapacity. An empty request list inBatchWriteItem, and an
emptyKeyslist inBatchGetItem, use DynamoDB's messages too. -
A
BatchWriteItemput whose item has no partition key is refused with "The
provided key element does not match the schema", as DynamoDB does, where it
named the missing key. -
A
TransactWriteItemsitem with no action is refused with DynamoDB's
"Invalid Request" message, and one with two actions with "TransactItems can
only contain one of Check, Put, Update or Delete". An item carrying both a
Putand aDeleteused to run thePutand drop theDelete. -
A
TransactGetItemsitem without itsGetis refused with DynamoDB's
"Value null at 'transactItems.N.member.get' failed to satisfy constraint:
Member must not be null", together with an unknown
ReturnConsumedCapacity. It used to come back as serde's own "missing
field" text. -
The single-item operations,
Query,ScanandSearchVectorsfollow the
member validation most regions now run. DynamoDB is moving each operation to
a newer validation layer region by region; these match the majority of seven
regions captured on 2026-10-09, which is the newer layer everywhere except
Scan.- A table name of one or two characters is refused for its length before
the table is looked up, where it used to answer
ResourceNotFoundException. A name that breaks more than one constraint
gets one error on most operations, both onUpdateItemandScan. - An empty table name gets the newer "Value at 'TableName'" wording on its
own, except onScan. PutItemandDeleteItemreport a table-name error together with the
other member errors;UpdateItemreports it alone;Scanreports it only
when nothing else is wrong.UpdateItemwithout its key reports every invalid enum, in the older
per-value wording, followed by the missing key, as all seven regions do.- An unknown
ReturnValuesgets the newer layer's message, which names
neither the value nor the member. - An unknown
ReturnValuesOnConditionCheckFailureis refused, on the
single-item operations and on everyTransactWriteItemsaction. It used to
be accepted. UpdateItemreports one invalid enum, in DynamoDB's order, without the
envelope forReturnValuesOnConditionCheckFailureand
ReturnConsumedCapacity.Queryreports aLimitof 0 and an empty name together, ahead of
everything else, and checksSelectafter mixingConditionalOperator
with an expression.Scan
reports its member errors in DynamoDB's order.- A
ConditionalOperatoris refused unless it joins two or more legacy
Expected,QueryFilterorScanFilterconditions, with DynamoDB's
messages, and joining two or more with a value other thanANDorOR
gets DynamoDB's generic enum message. Beside an expression it gets the
mixing message, whichDeleteItemnow wraps in the envelope asPutItem
andUpdateItemdo. It used to be accepted with nothing to join.Scan
still checks its value as an enum. SearchVectorsrefuses a short or malformed table name without the
envelope once its other members pass, aTopKbelow 1 in the newer
wording, and a missingIndexName,SearchVectororTopKwith
DynamoDB's own "missing field" message.
- A table name of one or two characters is refused for its length before
-
A request carrying an attribute value nested more than about 60 levels is
read and refused with DynamoDB's nestingValidationException, rather than
failing to parse with a bareSerializationException. As in DynamoDB, an
ExpressionAttributeValuesentry is named in the message and a member such
as a bad table name or enum is still reported first. Past about 2,000
levels, where DynamoDB itself stops reading the request, it stays a bare
SerializationException. -
A key,
ExclusiveStartKey, or legacyExpected,AttributeUpdates,
ScanFilter,QueryFilterorKeyConditionsvalue nested 32 or more levels
is refused with DynamoDB's nestingValidationExceptionbefore the table is
looked up. A key used to be reported as not matching the schema, a
transaction was cancelled, and a legacy condition or filter was evaluated.
As in DynamoDB, the message sits inside the validation envelope on
DeleteItem's and UpdateItem's key,ExpectedandAttributeUpdates, and
after the starting-key prefix on a Scan'sExclusiveStartKey, and a
ScanFiltervalue is refused from 33 levels. -
A Query's
ExclusiveStartKeythat doesn't fit the key schema is refused
with "The provided starting key is invalid" alone, as DynamoDB does, where it
used to add "The provided key element does not match the schema". An invalid
value in it (NULL: false, an empty set) is reported without the
starting-key prefix, and a start key from another partition or outside the
sort key condition is refused with DynamoDB's messages rather than paged
from. -
A legacy
KeyConditionsvalue whose type isn't the key's schema type is
refused with "Condition parameter type does not match schema type", and a
BETWEEN whose two values differ in type with "AttributeValues inside
AttributeValueList must be of same type". A map used to be reported as
"Cannot convert partition key value to string". -
A PartiQL parameter nested 33 or more levels is refused before its statement
is parsed or its table looked up, as DynamoDB does: at the top level on
ExecuteStatement and ExecuteTransaction, where a transaction used to be
cancelled, and against that statement alone in a BatchExecuteStatement. It
used to run, or come back as "Invalid key value". -
A BatchExecuteStatement member that fails on its own, for a parameter
nested too deep or holding an empty attribute name, takes no part in the
batch's read/write and duplicate checks, as DynamoDB does: a read beside a
write whose parameter is too deep, or two updates of one item where one is,
now return 200 and run the rest, where the whole batch used to be refused
(captured against eu-west-2 and us-east-1). The same is applied to a member
refused for a parameter value DynamoDB can't store or for a COUNT
projection, not yet captured. -
Set members come back in DynamoDB's order instead of the order they were
written in. String and binary sets are sorted by their bytes everywhere.
Number sets are sorted by value from GetItem, BatchGetItem, Query and the
ReturnValues of PutItem, UpdateItem and DeleteItem, and come back in the
order of a Java hash set from Scan, TransactGetItems, the PartiQL
operations, stream records and transaction cancellation reasons, as
DynamoDB returns them. Sets are now stored sorted, and a row stored before
this change comes back sorted in responses on the wire (HTTP, MCP and wasm);
the Rust API returns such a row's sets as they were stored. An update that
leaves such a set alone no longer fails the type check for an index key the
set's attribute is named after. -
An empty attribute name is refused with DynamoDB's
Empty attribute name
before the table is looked up: in an item or a key, as a map key at any
depth inside a value, in AttributesToGet, in the legacy Expected,
AttributeUpdates, QueryFilter and ScanFilter values, in a PartiQL
parameter, and as an ExpressionAttributeNames value. It used to be stored,
or ignored. PutItem, UpdateItem and DeleteItem report it inside the
validation envelope, as DynamoDB does. -
CreateTable lists a GSI's member errors before an LSI's, so two too-short
index names come back in DynamoDB's order. -
ItemCollectionMetricsreportsSizeEstimateRangeGBas DynamoDB does: a
lower and an upper bound in whole gigabytes, so a small item collection
reads[0.0, 1.0]. It used to give the collection's exact size twice. -
PutItem, UpdateItem and DeleteItem report more of their request-validation
errors inside the1 validation error detected:envelope, as DynamoDB does:
ExpressionAttributeNamesorExpressionAttributeValueswithout an
expression, either map empty, an emptyConditionExpression, and every
malformed legacyExpectedcondition. The values message no longer names
the missing expressions after it. DeleteItem now envelopes the same families
PutItem does, including{"NULL": false}wherever it sits, and refuses a
ReturnValuesother thanALL_OLDorNONEwith DynamoDB's
ReturnValues can only be ALL_OLD or NONE, before the table is looked up. -
A legacy
Expectedvalue DynamoDB refuses, such as{"NULL": false}or a
number it can't store, is refused on PutItem, UpdateItem and DeleteItem.
The condition used to be dropped and the write went ahead. -
A number DynamoDB can't store, and a set with a repeated member, are
refused in the words of the validation layer each operation runs on, as
DynamoDB refuses them. PutItem, UpdateItem and DeleteItem report them inside
the validation envelope wherever the value sits, and GetItem, BatchGetItem
and Query report them bare; an empty number endsnumeric value:, a
duplicated string or binary set lists its members quoted, and expression
values carry noExpressionAttributeValues contains invalid valuewrapper.
BatchWriteItem, the transactions, Scan and PartiQL keep the older wording,
read numbers as DynamoDB's older layer does (1e2147483647is an underflow
there) and name the key of a bad expression value. The transactions refuse
an empty or duplicated set in any action up front, before a missing table or
a condition's syntax error, rather than cancelling. A legacyExpected
value and anAttributeUpdatesvalue are checked as an item's are. A
PartiQL number set literal with a repeated value is refused naming its
members and path, in a WHERE clause too. -
Expressions containing non-ASCII characters are read as DynamoDB reads
them. A character such asé,€or an emoji outside a string is refused
with the syntax error naming that character, in every expression and in
transactions, and so is a no-break space, which DynamoDB doesn't treat as
whitespace. A placeholder whose own name holds one is refused as an invalid
key of its map, and a PartiQL statement with one outside a string as not
well formed. Captured against eu-west-2 and us-east-1; us-east-1 names an
emoji in a filter or a transaction as?, and Dynoxide follows eu-west-2.
In 1.2.1 some of these expressions stopped the server instead of being
refused, so anyone running Dynoxide where other people's clients can reach
it should upgrade. -
A
TransactWriteItemsUpdate whose SET paths overlap, such as
SET a.b = :v, a = :m, is refused before anything is written, with
DynamoDB's "Two document paths overlap" message, where it used to be
applied.UpdateItemnames the two paths in the order they were written.
Captured against eu-west-2 and us-east-1. -
A PartiQL statement longer than 8,192 characters, or an empty one, is
refused before it is parsed, as a request member with DynamoDB's message
(Member must have length less than or equal to 8192), on
ExecuteStatement,BatchExecuteStatementandExecuteTransaction. The
limit counts characters, not bytes. Captured against eu-west-2 and
us-east-1. -
A
BOOLorNULLmember's value is read as DynamoDB reads it. The
stringstrue,t,yes,yand1, andfalse,f,no,nand
0, in any case, are taken as the booleans they spell, so{"BOOL": "no"}
storesfalseand{"NULL": "yes"}stores a NULL, where both used to be
refused. Any other string is refused withUnexpected token received from parser, a number withNUMBER_VALUE(orDECIMAL_VALUE)cannot be converted to Boolean, and a list with aSerializationExceptionthat has
no message. A number is named by how it is written: a whole number of any
size,-0included, isNUMBER_VALUE, and one with a point or an exponent
DECIMAL_VALUE, where-0and an integer past 64 bits were named
DECIMAL_VALUE. ANULLthat reads as false is refused bare as the value is
read, and named by key in an expression value onUpdateItem,DeleteItem
andQuery, when it was a string or sits beside another member; a lone
{"NULL": false}keeps the envelope. A member set to JSONnullcounts as
absent, so{"S": null}is an empty value and{"S": "a", "N": null}a
string, andQuerynames the key of an empty expression value. Captured
against eu-west-2 and us-east-1. -
A request that holds a
BOOLorNULLwritten as a string, or a value with
more than one member, is read whole by DynamoDB's older validation layer,
whatever the operation, and Dynoxide now reads it the same way: every value
it refuses, wherever it sits, comes back bare onPutItem,UpdateItemand
DeleteItem, where the same refusal alone is wrapped in1 validation error detected:, andUpdateItemnames the key of the expression value at
fault (ExpressionAttributeValues contains invalid value: ... for key :v).
A value with more than one member is checked in DynamoDB's order: a member
that doesn't read as a boolean, then an empty set, then a number it can't
read, then aNULLthat reads as false, and only then the second member,
where the empty set came after the second member. Captured against
eu-west-2 and us-east-1. -
A request's own
ConsistentRead, onGetItem,Query,Scanand each
table ofBatchGetItem, andQuery'sScanIndexForward, read a string by
the rule aBOOLmember follows, as DynamoDB reads them:"yes"is a
strongly consistent read and"no"reads a Query descending, where every
string was refused withUnexpected token received from parser. Any other
string still is, and a number is named as it is for aBOOL. Anull
ScanIndexForwardreads forward, as leaving it out does, where it was
refused. Captured against eu-west-2 and us-east-1. -
A PartiQL statement that reads or writes by a key longer than a key can be
is refused with DynamoDB's words, where it used to run and find nothing: a
partition key over 2,048 bytes withSize of hashkey has exceeded the maximum size limit of2048 bytes, and a sort key over 1,024 bytes with
Aggregated size of all range keys has exceeded the size limit of 1024 bytes. That covers a SELECT on the table or on either kind of index, a
sort key under any key condition (=, a comparison,BETWEENor
begins_with), a partition key beside anORthat every alternative
shares (pk = <key> AND (x = 1 OR x = 2)), and the key of anUPDATEor a
DELETE; a statement that names several partitions, such as anORof
keys, still runs. The refusal stands
on its own onExecuteStatement, against that statement (with its table)
in aBatchExecuteStatement, where it is now charged nothing, and as a
ValidationErrorcancellation in anExecuteTransaction. Captured against
eu-west-2 and us-east-1. -
A PartiQL WHERE clause with more than 300 operators is refused with
DynamoDB's "The expression contains too many operators" message. AND, OR,
NOT, comparison operators, function calls,IN,BETWEENandISeach
count, the NOT inIS NOTorNOT INon its own, soAND size(s) > 0
andAND x IS NOT NULLcost three;BETWEEN's own AND, anINlist's
members and a bag literal's<<and>>count nothing. A run of NOTs no
longer counts as nesting, so one that was refused as nested too deeply now
runs. Captured against eu-west-2 and us-east-1. -
PartiQL accepts
x NOT IN [...]andx NOT BETWEEN lo AND hi, read as
NOT x IN [...]andNOT x BETWEEN lo AND hi, so an item missingx, or
holding a value of another type, matches a numeric range's negation; both
were refused as an unknown operator. They also follow asize(), a literal
or a parameter, andNOT INfollows a condition function, whereNOT BETWEENtakes the same refusal asBETWEEN. A sort key read withNOT BETWEENreads the range's complement, asNOT sk BETWEENdoes. A missing
AND in a WHERE clause'sBETWEENis now "Expected AND keyword", andx NOT
followed by anything butIN,BETWEENorLIKEis an unexpected token
rather than an unknown operator. Captured against eu-west-2 and us-east-1. -
A PartiQL
INorBETWEENon a path that also stands among its own
members or bounds, such asx IN [1, x]orx BETWEEN 1 AND x, is refused
with DynamoDB's "The first operand must be distinct from the remaining
operands" message, asx = xalready was. The message names a path by its
parts,[m, a]form.aand[l, [0]]forl[0], in a comparison too.
Where a WHERE clause holds more than one refusal,MISSINGcompared
withoutISnow outranks a refused operand whichever is written first, and
anINwith too many operands is reported in the order written against a
refused operand rather than ahead of it. AnINwith too many operands
that also holds its own path is refused for the path, wherever it stands
in the list, where the operand count used to be reported. Captured against
eu-west-2 and us-east-1. -
A PartiQL
UPDATEthat sets or removes a key attribute is refused with
Cannot update attribute <name>. This attribute is part of the key, and
writes nothing. It used to store the item under its old key with the new
key value inside it, so the item no longer matched its own key. It is
refused before the item is read, so a missing item or a false condition
makes no difference. InBatchExecuteStatementit fails its own statement
with aValidationErrorthat names the table, and inExecuteTransaction
it cancels the transaction with aValidationErrorreason. Captured against
eu-west-2 and us-east-1 on 10 October 2026. -
A quoted name in a PartiQL statement is one attribute whatever it holds,
as DynamoDB reads it:"a.b"and"a[0]"name the top-level attributes
a.banda[0]in a WHERE clause, a condition function,IS NOT MISSING
and a projection, which returns them under those names, andSET "x.y" = 5writes an attribute namedx.y. They used to be read asa.band
a[0]unquoted, a step intoa, andSET "x[0]"was refused as an
invalid document path."a"."b"and"a"[0]still step intoa. A key
attribute whose name holds a.or a[is named by quoting it, so such
a table is read by its key and its items can be updated and deleted.
Captured against eu-west-2 and us-east-1. -
PartiQL
size()calls nested inside one another, and arithmetic after a
size(), are held to the same depth as nested brackets. -
A PartiQL
INwith more than 100 operands, negated or not, written as
literals or as parameters, after a path or asize(), is refused with
DynamoDB's "The IN operator is provided with too many operands" message.
Captured against eu-west-2. -
A PartiQL WHERE clause that repeats a long value across the alternatives
it expands to, such as eight(a = 1 OR a = 2)pairs alongside a
4,000-character string, runs as it does on DynamoDB. The engine holds each
condition once however many alternatives share it, and checks the size of
an expansion before building it. Captured against eu-west-2. -
A PartiQL condition whose
?has no parameter to fill it no longer
matches an item.flag <> ?with no parameters used to hold for every item
withoutflag, so aDELETEorUPDATEshort of its parameters could
delete or change it. -
A PartiQL statement with more
?placeholders than the request has
parameters is refused with DynamoDB's "Number of parameters in request and
statement don't match." message: on its own onExecuteStatement, against
that statement alone inBatchExecuteStatement(where it no longer counts
towards the mixed read and write check), and with the statement's index on
ExecuteTransaction. More parameters than placeholders are still accepted.
An emptyParameterslist is refused as a request member, on a batch or
transaction member too, where it refuses the whole request.
Captured against eu-west-2 and us-east-1. -
A PartiQL request that breaks several member constraints at once, such as
an over-long statement with an emptyParameterslist or several over-long
statements in a batch, lists every one in DynamoDB's "N validation errors
detected" form rather than the first, and an empty statement in a batch or
transaction refuses the whole request as a member.ReturnConsumedCapacity
is listed with the rest rather than refused on its own: on
BatchExecuteStatementahead of the statement list's length, and on
ExecuteTransactionafter an over-longClientRequestTokenand the
statements (both orders captured against eu-west-2 and us-east-1). More
than 25 statements in a batch is refused as a member too, where it used to
give "Too many statements in BatchExecuteStatement"; its echo of the
statements is us-east-1's, as eu-west-2 printed a hash in their place that
can't be reproduced. A batch member's own errors follow
ReturnConsumedCapacity(captured). Inferred rather than captured: an empty
transaction, or one of more than 100 statements, listed with the token and
the statements rather than answering after them. The MCPexecute_partiql,batch_execute_partiql
andexecute_transaction_partiqltools list a bad mode the same way. -
A transaction's list longer than DynamoDB allows is echoed as DynamoDB
echoes it:ExecuteTransaction's statements asParameterizedStatement(...)
and an empty list as[], and the actions ofTransactWriteItemsand
TransactGetItemsasTransactWriteItem(...)andTransactGetItem(...),
every field named, where they echoed nothing or Dynoxide's own debugging
form. An attribute map in any of these echoes, and inBatchWriteItem's,
lists its entries in the order of a Java hash map sized to its entries, as
DynamoDB's do, rather than by name: an item, a nested map or
ExpressionAttributeNamesof 2 to 6 entries is laid out over four or eight
buckets, not sixteen. Entries that share a bucket are listed in name order,
where DynamoDB keeps the order they were sent in. Captured against
us-east-1, eu-west-1 and us-west-2 (eu-west-2 echoes no action list), the
map sizes against us-east-1 and eu-west-2. -
A PartiQL WHERE clause's alternatives are held to 16,384 condition slots
between them, where each alternative was already held to 4,096. Every slot is
evaluated against every item read, and a clause of a few hundred operators
could flatten to tens of thousands. The limit is Dynoxide's own, as DynamoDB
does not flatten a clause; a clause past it is refused as too complex. -
A refused member echoed in a constraint message (
BatchWriteItem's and
BatchGetItem's request items,BatchExecuteStatement's statements, and the
action listTransactWriteItemsandTransactGetItemsecho when it holds
more than 100) stops at 1 MiB and ends.... The Java form these echoes take runs to about
twenty times the request, so a rejected request near the body limit built
an error message of hundreds of megabytes. The limit is Dynoxide's own;
what DynamoDB does with an echo that size has not been captured. -
A malformed
Expectedcondition onPutItem,UpdateItemorDeleteItem
is refused rather than ignored. 1.2.1 ran the write with no condition at
all, so a guardedDeleteItemdeleted the item. An unknownComparisonOperatorgets
DynamoDB's "Invalid comparison operator" message, andEQwith an empty
AttributeValueListits "Invalid number of argument(s)" message. Captured
against eu-west-2 and us-east-1. -
The
Debugform of a mapAttributeValuewrites its entries in key order,
and the idempotency check on transactions sorts a map's keys itself rather
than rely on serde_json to, so a build in which another crate turns on
serde_json'spreserve_orderfeature still replays a same-token retry of
an identical request. -
PartiQL
begins_withmatches a binary attribute against a binary prefix by
its bytes, andNOT begins_withkeeps the rest. A binary prefix matched
nothing and its negation everything. A string prefix still matches only a
string value. Captured against eu-west-2 and us-east-1.