Repository navigation
fix(engine): compare the Return fields on a TransactWriteItems token replay - #526
LeeroyHannigan wants to merge 10 commits into
Conversation
…ngerprint The idempotency fingerprint hashed TransactItems only, so a replay that changed ReturnConsumedCapacity or ReturnItemCollectionMetrics was served as a replay. The service answers IdempotentParameterMismatchException for such a request and treats an absent field as NONE. Hash the two fields after the items when they are not NONE. A request with neither field, or with both NONE, keeps the value stored by earlier versions, pinned by a unit test. Use the service's mismatch message.
Cover each measured answer: a changed ReturnConsumedCapacity or ReturnItemCollectionMetrics on a token replay is a mismatch with the service's message, NONE equals absent, the matching replay reports read capacity and no ItemCollectionMetrics, and an invalid enum value is refused before the token comparison.
demohan
left a comment
There was a problem hiding this comment.
The fix matches the service on every TransactWriteItems shape we measured. We compared 288 replay pairs and the extras in us-east-1 on 10 November 2026, two sends each, on all four backends. Against the merge-base there were no regressions. The new test file passes against DynamoDB on two runs.
These items remain:
- The upgrade note covers one direction. A downgrade within the window refuses identical TOTAL, INDEXES and SIZE retries, and also a no-field retry of a TOTAL first call. Inline at
transact_write_helpers.rs:271. - On main,
partiql/transaction.rspassesClientRequestTokenandReturnConsumedCapacityintohandle_transact_write_items, so this change also decides ExecuteTransaction replays. This comes from main moving under the branch. Measured: the service refuses a replay that changes ReturnConsumedCapacity (none to TOTAL, TOTAL to none, TOTAL to INDEXES:IdempotentParameterMismatchException, two sends each), and the merged tree matches. Main without this change serves those as 200. After the rebase, add ExecuteTransaction replay tests for first TOTAL then absent, and first absent then TOTAL. - Five items in the tests and docstrings, inline.
| /// fields on a token replay and treats an absent field as `NONE`, so each | ||
| /// field contributes to the hash only when it is not `NONE`. A request with | ||
| /// neither field, or with both set to `NONE`, hashes the items alone, which | ||
| /// is the value stored by earlier versions; tokens stored before this change |
There was a problem hiding this comment.
This holds only for a request with neither field. With the merge-base writing tokens and this branch serving the same SQLite file, identical TOTAL, INDEXES and SIZE retries answer IdempotentParameterMismatchException, as the PR body says. In the other direction, with this branch writing and the merge-base retrying, the same three retries answer mismatch, and so does a no-field retry of a TOTAL first call. Please make the comment say which tokens keep replaying, and add the downgrade direction to the description.
| } | ||
|
|
||
| #[test] | ||
| fn fingerprint_differs_for_different_ops() { |
There was a problem hiding this comment.
This case uses NONE only. A mutation that hashes the items only when ReturnConsumedCapacity is NONE passes all 7 unit tests and all 21 wire tests. In that build, a retry with TOTAL and different items is served as a replay that writes nothing. Measured: the service answers that retry with IdempotentParameterMismatchException and stores nothing, for TOTAL and for SIZE, two sends each. Please add the same check with ReturnConsumedCapacity::Total.
| /// this body and token. A request with neither Return field, or with both | ||
| /// set to NONE, must keep producing it so tokens stored before the upgrade | ||
| /// still replay. | ||
| const PINNED_ITEMS_ONLY: &str = "ef18f4e7c37d6b12"; |
There was a problem hiding this comment.
Tokens stored with TOTAL, INDEXES or SIZE have the same upgrade exposure as the items-only value pinned here. Swapping the order of the two suffixes passes every test. Pinning the suffixed values for pinned_body() and tok-1 would catch that: TOTAL 8fa51ffc1314a046, INDEXES ee2c3412f0fdbdf1, SIZE 70eb830c93e9079f, TOTAL with SIZE e9957d3c7431e1d4 (HMAC-SHA256 recomputed outside the crate; the items-only value matches ef18f4e7c37d6b12).
|
|
||
| A replay with matching fields answers 200 and reports the replay as a | ||
| transactional read (ReadCapacityUnits, no WriteCapacityUnits) when | ||
| ReturnConsumedCapacity asks for capacity, and carries no |
There was a problem hiding this comment.
On a table with a local secondary index, the service returns ItemCollectionMetrics on a matching SIZE replay: 200 {"ItemCollectionMetrics":{"<table>":[{"ItemCollectionKey":{"pk":{"S":"..."}},"SizeEstimateRangeGB":[0.0,1.0]}]}}, two reps. ExtendDB returns {}, on main and here. The test at line 218 uses a table without one, where {} is correct. Please limit this sentence and the matching line in the description to tables without a local secondary index.
|
|
||
|
|
||
| def _fresh() -> tuple[str, str]: | ||
| return f"tok-{uuid.uuid4().hex}", uuid.uuid4().hex[:32] |
There was a problem hiding this comment.
This returns (token, pk), and every caller unpacks pk, token = _fresh(). No assertion changes, because both values are valid in both roles, but failure output names them the wrong way round.
| _call(dynamodb_client, table, pk, token) | ||
| with pytest.raises(ClientError) as exc: | ||
| _call(dynamodb_client, table, pk + "-b", token) | ||
| _assert_mismatch(exc) |
There was a problem hiding this comment.
The sibling test at line 136 reads the second key back, and this one does not. The service stores nothing here (two sends). Please add the same get_item check. A case with ReturnConsumedCapacity="TOTAL" on both calls and different items would cover the gap at transact_write_helpers.rs:384 end to end.
pdf-amzn
left a comment
There was a problem hiding this comment.
Code all looks reasonable.
AI-generated Findings
Should-fix
crates/engine/src/partiql/transaction.rs:419-421 on current main (not in the PR's base). ExecuteTransaction forwards ReturnConsumedCapacity into the TransactWriteItems body it hands to handle_transact_write_items. After this merges, an ExecuteTransaction replay that changes only RCC answers IdempotentParameterMismatchException instead of replaying. The PR measured TransactWriteItems only, so this is unmeasured for ExecuteTransaction. Rebase onto main, then either measure the ExecuteTransaction case on the service and add a dual-target test (next to tests/test_partiql_transaction.py:262 T5), or leave RCC out of the fingerprint on the PartiQL path. The same upgrade-compatibility note from the PR description (a stored token with TOTAL/INDEXES answers mismatch inside the 10-minute window) applies to that path too.
crates/engine/src/partiql/transaction.rs:505-528 on main. Once the classic handler uses the same message, the IdempotentParameterMismatchException arm in remap_write_error is a no-op, and its doc comment ("uses the PartiQL message") is stale. Drop the arm and fix the comment when rebasing, or keep it with a comment saying both surfaces share the message. The literal now appears twice; a shared const in transact_write_helpers.rs would keep the two copies from drifting.
Nit-or-follow-up
tests/test_transact_token_return_fields.py:69: _fresh() returns (token, pk), but every caller unpacks it as pk, token. So the "pk" is the tok-… string and the "token" is 32 hex characters. Both values are valid, so the tests pass, but the names are swapped. Reorder the return tuple.
tests/test_transact_token_return_fields.py:218: test_matching_replay_size_carries_no_metrics doesn't assert the first-call body. The PR records that DynamoDB returns {"ItemCollectionMetrics": {}} there and ExtendDB omits the key. Neither the test file nor docs/differences-from-dynamodb.md records that gap. File an issue or add a doc line.
Known gaps the PR lists but doesn't track: the fingerprint hashes raw JSON where the service compares the parsed request (1 vs 1.0, expression spacing, ExpressionAttributeNames aliasing, per-item ReturnValuesOnConditionCheckFailure: NONE, key order under the MongoDB preserve_order build). An unknown top-level field is ignored by the service and refused with SerializationException by ExtendDB. Record these in docs/differences-from-dynamodb.md or file an issue for each.
crates/engine/src/transact_write_helpers.rs:276: the docstring explains the compatibility rule, but docs/design/04-component-storage.md:817-822 still describes the fingerprint as covering the operations only. Update that section.
crates/engine/src/transact_write_helpers.rs:400: PINNED_ITEMS_ONLY is a good regression pin. Its comment could name the commit that produced the value (744ea29), so a later reader can check it.
Main is 247 commits ahead of the branch point (744ea29), and the merge is clean. crates/engine/src/transact_write_items.rs changed on both sides. Main checks the attribute name sizes of every member before the table lookups (#433) and rewords the comment on stream records (#576). This branch passes the two Return fields to compute_fingerprint and changes the mismatch message. Main adds the PartiQL ExecuteStatement, BatchExecuteStatement and ExecuteTransaction operations (#411). For an ExecuteTransaction write, crates/engine/src/partiql/transaction.rs builds a TransactWriteItems body from the statements. It copies the request's ClientRequestToken and ReturnConsumedCapacity into that body and passes the body to handle_transact_write_items. With this branch, the fingerprint of the body covers ReturnConsumedCapacity. An ExecuteTransaction retry that keeps the token and the statements but changes ReturnConsumedCapacity now answers IdempotentParameterMismatchException, where main serves it as a replay. Amazon DynamoDB answers the mismatch to three such retries: TOTAL then no field, no field then TOTAL, TOTAL then INDEXES (us-east-1, 2026-11-10, two sends each). remap_write_error in the same file rewrites the mismatch message to the text that the classic handler now returns.
The compute_fingerprint comment said that tokens stored before this change still replay. That holds only for a request with neither Return field, or with both set to NONE. The comment now gives the three cases for the 10 minutes after an upgrade or a downgrade. The function at 744ea29 hashes the TransactItems JSON alone. This version appends a suffix for TOTAL, INDEXES and SIZE, and nothing for NONE. All four storage backends replay a retry only when its fingerprint equals the stored one. A token for a request without the fields therefore replays in both directions. A token that an earlier version stored for TOTAL, INDEXES or SIZE answers IdempotentParameterMismatchException to an identical retry on this version. A retry without the field replays. A token that this version stored for one of those values answers the mismatch to every retry on an earlier version. The comment also says that ExecuteTransaction writes use this fingerprint, because they call handle_transact_write_items with their ReturnConsumedCapacity.
The fingerprint test for different items used ReturnConsumedCapacity NONE only. A build that hashes the items only when the field is NONE passed every unit test. In that build, a retry with TOTAL and different items replays and writes nothing. Amazon DynamoDB answers that retry with IdempotentParameterMismatchException and stores nothing, for TOTAL and for SIZE (us-east-1, 2026-11-10, two sends each). Two new tests run the different items check with TOTAL and with SIZE. The fingerprint stored for a request with TOTAL, INDEXES or SIZE must also stay the same across versions. Otherwise its token stops replaying after an upgrade. A new test pins the values for pinned_body() and token tok-1: TOTAL 8fa51ffc1314a046, INDEXES ee2c3412f0fdbdf1, SIZE 70eb830c93e9079f, and TOTAL with SIZE e9957d3c7431e1d4. An HMAC-SHA256 computed outside the crate gives the same four values. The comment on PINNED_ITEMS_ONLY now names 744ea29, the commit whose fingerprint produced that value.
After the merge of main, the mismatch message was a literal in two places: the classic TransactWriteItems handler and remap_write_error in crates/engine/src/partiql/transaction.rs. The remap arm for IdempotentParameterMismatchException replaced the handler's message with the same text, so it changed nothing. The text is now IDEMPOTENT_MISMATCH_MESSAGE in crates/engine/src/transact_write_helpers.rs, and the handler uses it. This change removes the remap arm, so the PartiQL surface passes the handler's error through. The comments on remap_write_error and on the module now say that it remaps only the reason of a duplicate-key INSERT. No answer changes.
tests/test_transact_token_return_fields.py changes in four places. The module docstring now limits the sentence about a matching replay to tables without a local secondary index. The tests use such a table: scoped_table keys it on pk alone. The docstring also gives the date of the measurement behind each read of a changed key. _fresh() returned (token, pk), and every caller unpacks pk, token = _fresh(). It now returns (pk, token), so failure output names the two values correctly. test_changed_items_alone_is_a_mismatch now reads the second key back and expects no item, as its sibling test does. For a retry with no Return field and another item, Amazon DynamoDB answers IdempotentParameterMismatchException. It stores no item for the second key (us-east-1, 2026-11-10, two sends). A new test sends ReturnConsumedCapacity TOTAL, or ReturnItemCollectionMetrics SIZE, on both calls, and another item on the second call. Amazon DynamoDB answers the second call with IdempotentParameterMismatchException and stores no item for its key (us-east-1, 2026-11-10, two sends each). The test checks the different items case of the fingerprint unit tests through the server.
An ExecuteTransaction write reaches handle_transact_write_items with
the request's ReturnConsumedCapacity, so the fingerprint that compares
the field decides ExecuteTransaction replays as well. Four tests next
to the T5 test in tests/test_partiql_transaction.py send one INSERT
twice with the same token.
Amazon DynamoDB answers the second call with
IdempotentParameterMismatchException in three cases: TOTAL then no
field, no field then TOTAL, and TOTAL then INDEXES. The message is the
one TransactWriteItems gives. With no field on both calls the service
answers 200 {"Responses": []} to both (us-east-1, 2026-11-10, two sends
each). Each case uses its own key and token.
docs/design/04-component-storage.md described the stored fingerprint
without its inputs. Section 9 now says that the fingerprint is an
HMAC-SHA256 keyed by the token over the TransactItems JSON. The
ReturnConsumedCapacity and ReturnItemCollectionMetrics values follow
the items when they are not NONE. ExtendDB translates an
ExecuteTransaction write into a TransactWriteItems request before it
computes the fingerprint. The IdempotentMismatch item of the storage
error list now says that the token exists with a different fingerprint.
docs/differences-from-dynamodb.md gets a Transactions section with the
replay gaps that remain. The rows about how a request writes its items
name TransactWriteItems, the operation of the measurements:
- The service replays a retry that changes only the spelling of a
number, the spacing of an expression or an ExpressionAttributeNames
alias. It also replays a retry that adds
ReturnValuesOnConditionCheckFailure NONE inside an item (measured
2026-10-24). ExtendDB answers each with
IdempotentParameterMismatchException (measured on SQLite at this
branch).
- The service answers 200 to a retry whose item lists the same
attributes in another key order. An identical retry gets the same
answer (us-east-1, 2026-11-10, two sends). A build with the MongoDB
backend keeps map keys in request order, so there the retry answers
the mismatch (from the code).
- The service answers 200 to a raw signed request with an unknown
top-level field and writes the item (us-east-1, 2026-11-10, two
sends). ExtendDB refuses the request with SerializationException
(measured on SQLite at this branch).
- On a table without a local secondary index, the service answers the
first SIZE call with {"ItemCollectionMetrics": {}} (us-east-1,
2026-11-10, two sends). ExtendDB answers {}.
docs/manuals/07-upgrade-manual.md gets a v0.1.15 entry for stored
tokens. After an upgrade, a token that v0.1.14 stored for a request
with TOTAL, INDEXES or SIZE answers the mismatch to an identical retry.
After a downgrade, a token that this release stored for such a request
answers the mismatch to every retry. v0.1.14 does not serve
ExecuteTransaction. PostgreSQL, SQLite and MongoDB keep a token for 10
minutes. The Cassandra backend stores a token without an expiry time
and does not check its age.
The compute_fingerprint comment said that a token expires after 10 minutes. It described the answers for 10 minutes after an upgrade or a downgrade. PostgreSQL, SQLite and MongoDB check the age of a token against 10 minutes. The Cassandra backend inserts a token without a TTL, and its token table has no default TTL. Its token check reads no creation time, and its cleanup of expired tokens does nothing. A Cassandra token therefore has no expiry. The comment now describes the answers for a token stored before the version change, and it gives the lifetime per backend. The upgrade manual entry states the same lifetimes.
|
Thanks, both. I pushed 6fabc1c. It merges main and answers each item. On 2026-11-10 I measured each service answer the new tests encode, two sends each, with Put and with Update members.
|
What
compute_fingerprintincrates/engine/src/transact_write_helpers.rsnow coversReturnConsumedCapacityandReturnItemCollectionMetrics. It stays an HMAC-SHA256 keyed by the token over theTransactItemsJSON. WhenReturnConsumedCapacityisTOTALorINDEXES, orReturnItemCollectionMetricsisSIZE, the field name and value follow the items in the hashed input. A request with neither field, or with both set toNONE, hashes the same bytes as before.The mismatch message is now the service's:
Specified idempotent token was used with different request parameters within the idempotency window. The text is the constantIDEMPOTENT_MISMATCH_MESSAGEincrates/engine/src/transact_write_helpers.rs, and the handler incrates/engine/src/transact_write_items.rsuses it. The four storage crates returnStorageError::IdempotentMismatchwithout text. This change does not touch them.remap_write_errorincrates/engine/src/partiql/transaction.rsno longer rewrites that error, because both surfaces carry the same text.The branch carries a merge of main at 5ada959 (628a7c6). On main,
crates/engine/src/partiql/transaction.rsbuilds aTransactWriteItemsbody from the statements of anExecuteTransactionwrite. It copies the request'sClientRequestTokenandReturnConsumedCapacityinto that body and callshandle_transact_write_items. So the fingerprint decidesExecuteTransactionreplays too. AnExecuteTransactionretry that keeps the token and the statements but changesReturnConsumedCapacitynow answers the mismatch. Main serves it as a replay. The service answers the mismatch (2026-11-10 table below).All four backends replay a retry only when its fingerprint equals the stored one. Versions before this change hash the items alone. A token stored before an upgrade or a downgrade therefore answers as follows while the backend keeps it.
NONE, has the same fingerprint in both versions. An identical retry replays in both directions.TOTAL,INDEXESorSIZEanswersIdempotentParameterMismatchExceptionto an identical retry on this version. A retry without the field replays.IdempotentParameterMismatchExceptionto every retry on an earlier version. This includes an identical retry and a retry without the field.Runs with two binaries at 386a775 checked the first two cases on SQLite and PostgreSQL. A main binary wrote the tokens, and this branch served the same database. The third case follows from the code and the pinned fingerprint values below. No run with two binaries covered it.
PostgreSQL, SQLite and MongoDB keep a token for 10 minutes. The Cassandra backend stores a token without an expiry and does not check its age. There a later retry gets these answers at any time. The
compute_fingerprintcomment states the three cases and the lifetimes.docs/manuals/07-upgrade-manual.mdgets a v0.1.15 entry with the same content. The entry also covers servers of both releases that share one database during a rolling upgrade. v0.1.14 does not serveExecuteTransaction, so no token that v0.1.14 stored comes from one.docs/design/04-component-storage.mdsection 9 now states what the fingerprint hashes. It also states that ExtendDB translates anExecuteTransactionwrite into aTransactWriteItemsrequest before it computes the fingerprint.docs/differences-from-dynamodb.mdgets a Transactions section with the replay gaps listed under "Not changed here".Why
TransactWriteItemsdecides whether a request with a knownClientRequestTokenis a replay by comparing a fingerprint of the request. The fingerprint coveredTransactItemsonly. So ExtendDB served a second request that kept the token and the items but changedReturnConsumedCapacityorReturnItemCollectionMetricsas a replay. The service answersIdempotentParameterMismatchExceptionfor that request and treats an absent field asNONE.Fixes #514.
Measured on the service
us-east-1, 2026-10-24 17:31 UTC and 17:36 UTC, client retries off. First call: a token and one
Update(SET v = :v) on a fresh key. The replay keeps the token and the items.{}ReturnConsumedCapacity: TOTALIdempotentParameterMismatchExceptionReturnConsumedCapacity: NONE{}ReturnItemCollectionMetrics: SIZEReturnItemCollectionMetrics: NONE{}TOTALTOTALINDEXESINDEXESTOTALSIZENONETOTALandSIZETOTALTOTALTOTALConsumedCapacity: [{CapacityUnits: 2.0, ReadCapacityUnits: 2.0, TableName}]INDEXESINDEXESTable: {CapacityUnits: 2.0, ReadCapacityUnits: 2.0}SIZESIZE{}, noItemCollectionMetricsReturnConsumedCapacity: totalValidationException,Value 'total' at 'returnConsumedCapacity' failed to satisfy constraint: Member must satisfy enum value set: [INDEXES, TOTAL, NONE]ReturnConsumedCapacity: BOGUSwith different itemsValidationException: enum validation runs before the token comparisonThe mismatch rows carry the message quoted above. The
SIZEandSIZErow describes a table without a local secondary index, the table kind the test file uses. ExtendDB already parses the request before it computes the fingerprint. It already renders a matching replay as a transactional read with noItemCollectionMetrics. So those rows passed on main and pin behaviour the change keeps.us-east-1, 2026-11-10, table keyed on
pkwith no local secondary index. Each case ran twice with a new token and new keys, and gave the same answer both times. The second call keeps the token.TransactWriteItemscases sent onePut(19:02 UTC and 20:01 UTC). Four of them ran again with oneUpdate(SET a = :v, 20:22 UTC) and gave the same answers.ExecuteTransactioncases sent oneINSERTstatement (19:03 UTC).TransactWriteItems,PutandUpdateTOTALTOTAL, another itemTransactWriteItems,PutandUpdateSIZESIZE, another itemTransactWriteItems,PutandUpdateTransactWriteItems,PutandUpdateTOTALTOTAL, the same itemConsumedCapacity: [{CapacityUnits: 2.0, ReadCapacityUnits: 2.0, TableName}]TransactWriteItems,Put{}ExecuteTransactionTOTALExecuteTransactionTOTALExecuteTransactionTOTALINDEXESExecuteTransaction{"Responses": []}ExecuteTransactionTOTALTOTAL, another statementThe mismatch rows carry the same message. Every first call answered 200. With
TOTALthe answer carriedConsumedCapacity: [{CapacityUnits: 2.0, WriteCapacityUnits: 2.0, TableName}], withSIZEit was{"ItemCollectionMetrics": {}}, and with no field{}.ExecuteTransactionaddsResponses: []. ATransactWriteItemsrequest with an unknown top-level field ("Bogus": 1), sent as a raw signed request with a new token, answered 200{}and wrote the item (two sends).Testing done
Tests
crates/engine/src/transact_write_helpers.rs, 10 fingerprint tests and 4 token tests (cargo +1.97.0 test --locked -p extenddb-engine transact_write, 14 passed at 6fabc1c). The fingerprint for a body with neither field equalsef18f4e7c37d6b12, the value from 744ea29.NONEon both fields equals absent.TOTAL,INDEXESandSIZEchange the fingerprint and differ from one another. Different items give different fingerprints withNONE, withTOTALand withSIZE. A test pins the values forpinned_body()and tokentok-1:8fa51ffc1314a046(TOTAL),ee2c3412f0fdbdf1(INDEXES),70eb830c93e9079f(SIZE) ande9957d3c7431e1d4(TOTALwithSIZE). An HMAC-SHA256 computed outside the crate gives the same values.tests/test_transact_token_return_fields.py, dual target: one test per row of the 2026-10-24 table, plustest_changed_items_with_the_same_return_field_is_a_mismatchwithrcc-totalandricm-sizefrom the 2026-11-10 table.test_changed_items_alone_is_a_mismatchreads the second key back and expects no item._fresh()now returns(pk, token), the order every caller unpacks. On main (744ea29, SQLite) 9 of the 21 tests at 386a775 fail. Main serves the seven changed-field rows as a replay, and the two item-mismatch rows carry the old message. At 386a775 all 21 pass on SQLite, PostgreSQL, MongoDB and Cassandra. At 6fabc1c all 23 pass against a dev-mode SQLite server built from 6fabc1c.tests/test_partiql_transaction.py, four new tests after T5:test_idempotent_replay_with_another_return_consumed_capacity_is_a_mismatchwithtotal-then-absent,absent-then-totalandtotal-then-indexes, andtest_idempotent_replay_without_return_consumed_capacity. Each case has its own key and token. At 6fabc1c the file gives 201 passed, 2 xfailed on the same SQLite server. The two xfails are the existing ones (TransactWriteItemsreports no index units).ReturnConsumedCapacityisNONE:fingerprint_differs_for_different_ops_with_totaland the pin test fail (12 passed, 2 failed), and the wire test[rcc-total]fails withDID NOT RAISE ClientError.ReturnItemCollectionMetricsisNONE:fingerprint_differs_for_different_ops_with_sizeand the pin test fail, and the wire test[ricm-size]fails the same way.TOTAL with SIZE(13 passed, 1 failed).differs_for_return_*tests and the pin test). 10 wire tests fail withDID NOT RAISE ClientError, the three newExecuteTransactionmismatch cases and the seven changed-field rows.test_idempotent_replay_without_return_consumed_capacitypasses, as a replay test should in both builds. The read-back intest_changed_items_alone_is_a_mismatchpasses in every build, since none of them writes the second item on a mismatch.Gates
At 386a775, before the merge of main:
cargo +1.97.0 fmt --all -- --check: clean.cargo +1.97.0 clippy --all-targets -- -D warnings: 0 warnings.cargo test --workspace --exclude extenddb-storage-cassandraon stable: 1771 passed, 0 failed, 26 ignored.cargo test -p extenddb-storage-cassandra --lib: 61 passed. The same two commands on 1.88.0: 1771 passed and 61 passed, 0 failed.idempotency or transact or transaction(15 files): 142 passed, 2 skipped on SQLite and on PostgreSQL.At 6fabc1c:
cargo +1.97.0 fmt --all -- --check: clean.cargo +1.97.0 clippy --locked --all-targets --features extenddb-storage-cassandra/test-hooks,extenddb-storage-mongodb/test-hooks -- -D warnings: clean, 13 workspace crates.cargo +1.97.0 clippy --locked --all-targets --features extenddb-engine/backup-test-hooks -- -D warnings: clean.cargo +1.97.0 test --locked -p extenddb-engine transact_write: 14 passed.cargo +1.97.0 test --locked -p extenddb-engine partiql::transaction: 10 passed.tests/test_transact_token_return_fields.py23 passed,tests/test_partiql_transaction.py201 passed, 2 xfailed.Not changed here
ReturnItemCollectionMetrics: SIZEon a first call against a table with no local secondary index: the service answers{"ItemCollectionMetrics": {}}(measured 2026-11-10). ExtendDB omits the key (measured on SQLite at this branch). Present on main. The test file does not assert the first-call body for that row.TransactItemsJSON as written, where the service compares the parsed request. Measured on 2026-10-24, the service replays a retry whose only difference is the spelling of a number (1and1.0) or the spacing of an expression. It also replays a retry that names an attribute through anExpressionAttributeNamesalias, or addsReturnValuesOnConditionCheckFailure: NONEinside an item. ExtendDB answers each with a mismatch, on main and here (measured on SQLite at this branch). In a build with the MongoDB backend, serde_json keeps map keys in request order. So there a retry whose item keys are in another order also answers a mismatch (from the code). The service replays that retry (2026-11-10 table). A build without that backend writes map keys in sorted order and replays it too (from the code).SerializationException, on main and here (measured on SQLite at this branch).docs/differences-from-dynamodb.mdrecords the rows above with these dates.USING TTL, its token table has no default TTL, and its read does not check the age. Present on main.git merge-tree --write-treeagainst fix(expressions): validate in the service's order, with its messages #399, fix(limits): attribute-name size limit on every write path #433 and fix(query): check legacy conditions against the key schema #507 was clean, and the three merged trees had the single four-argumentcompute_fingerprintcall. fix(limits): attribute-name size limit on every write path #433 is in main since 2026-11-03 and in this branch through the merge.Checklist
cargo test --workspace)cargo fmt --check)cargo clippy -- -W clippy::pedantic)Storagetrait, auth model, on-diskformat, or public CLI surface, an RFC has been accepted or is linked
below. Otherwise, an ADR captures the decision (link below).
Clippy ran with
-D warnings, without the pedantic group.ADR / RFC: n/a
By submitting this pull request, I confirm that my contribution is made under
the terms of the Apache License 2.0 and I agree to the Developer Certificate of
Origin (DCO). See CONTRIBUTING.md for details.