Repository navigation
fix(query): check legacy conditions against the key schema - #507
Open
LeeroyHannigan wants to merge 14 commits into
Open
LeeroyHannigan wants to merge 14 commits into
LeeroyHannigan wants to merge 14 commits into
Conversation
…ts messages Issue #108 lists 106 requests whose response differs from Amazon DynamoDB. Every expectation in this change was measured against the service (us-east-1, 2026-10-14); the probes and captured responses are in the issue's notes, and every new test file was run against the service before it was run here. Semantic validation of expressions. After the grammar pass, the service walks the parsed expression once, in expression order, checking each node after its operands, then checks overlapping paths, then unused names and values. A new `validate_update_semantics` and `validate_condition_semantics` in the core crate follow that walk; the parsers now do grammar only, keep function names as written, and record section order for the walk. - UpdateExpression: `+` and `-` take numbers, `list_append` takes lists, `ADD` takes a number or a set, `DELETE` takes a set; a placeholder of the wrong type is refused with the service's type vocabulary (`S`, `N`, `L` for operators and functions; `STRING`, `NUMBER`, `Binary`, `BOOL`, `NULL`, `LIST`, `MAP` for ADD and DELETE). Function names are case-sensitive: `IF_NOT_EXISTS(x, :v)` and `foo(x)` are `Invalid function name`, a condition function is `not allowed in an update expression`, `list_append` is refused as an operand of a function or of `+`, both functions take two operands, the first operand of `if_not_exists` is a document path. A repeated section is reported after that section's own actions (`SET a = :v SET b = :q` reports `:q`). Previously `IF_NOT_EXISTS`, `DELETE s :string`, and `SET a = :string + :n` were accepted and written. - ConditionExpression and FilterExpression: the six condition functions are matched as written (`ATTRIBUTE_EXISTS(pk)` was accepted and evaluated); `if_not_exists` and `list_append` are `not allowed in a condition expression`; a boolean function in operand position and `size` in condition position are `not allowed to be used this way`; operand counts are fixed per function; `attribute_exists` and `attribute_not_exists` take a document path; `begins_with` takes a string or binary and `attribute_type` a string naming a type, both checked at request time with the operation's prefix. - Every unused name or value is listed in one set, sorted, so the message is the same on every backend; it named one key chosen by hash order. Request stage on the read APIs. Query and Scan check the FilterExpression, then the KeyConditionExpression (with its `Invalid KeyConditionExpression:` prefix, and ahead of the key schema match, so an undefined `#n` is reported as such), then the ProjectionExpression, then unused names and values, all before the table is looked up. An undefined `:q` in a filter returned 200 on an empty table. Limit and Segment checks precede the expressions on Scan. Single-item order. UpdateItem checks the whole ConditionExpression before the UpdateExpression. PutItem, UpdateItem, and DeleteItem wrap every expression-stage error in `1 validation error detected: ` and omit the `: ConditionExpression is null` suffix; the read, batch, and transaction APIs return the messages bare. The ` expression size: N` suffix follows the operation. Three existing dual-target tests asserted the unprefixed text and fail against the service; they are corrected. Transactions. TransactGetItems and TransactWriteItems validate in phases, each over every item before the next: per item, expressions (UpdateExpression before ConditionExpression), unused names and values, then the item's 400 KB size; then table lookup; then the 4 MB total; then key schema, as a cancellation with ValidationError on the first offending item only and None on every other (it flagged every offender); then duplicates. An undefined reference is the request error the service returns rather than a per-item cancellation reason, and a key schema mismatch leaves other items' conditions unevaluated. TransactWriteItems returns `"ItemCollectionMetrics": {}` when SIZE is requested and no table has a local secondary index, and its ConsumedCapacity entries are in first-appearance order rather than per-process hash order. Key-shaped messages. GetItem, DeleteItem, UpdateItem, BatchGetItem, and BatchWriteItem (Put and Delete) refuse a key with a missing, mistyped, or extra attribute with `The provided key element does not match the schema`; PutItem keeps `Missing the key pk in the item` and `Type mismatch for key`. BatchWriteItem returns `"ItemCollectionMetrics": {}` under SIZE with no LSI. Table ARNs in `TableName` (eight #108 rows) stay unsupported by decision: an ExtendDB table ARN names ExtendDB's account and region, so a table that streams or replicates to Amazon DynamoDB would answer to two ARNs. docs/differences-from-dynamodb.md records this. Remaining #108 gaps after this change, all stated in the PR: the parser's syntax wording (`Syntax error; token: X, near: Y`), the ResourceNotFound message text, the 4 MB message (the service's carries its own payload size), the service's rendering of over-limit list values, and the table ARN rows. Testing: tests/test_transact_validation_order.py (31 cases), tests/test_update_expression_rules.py (37), and tests/test_expression_semantics.py (155) pass against the service and on SQLite, PostgreSQL, and MongoDB; 164 of the 186 cases in the first and third fail on main. The expression, validation, transaction, batch, query, and scan suites pass on all three backends (767 on SQLite and PostgreSQL, 751 on MongoDB with the four #387 cases unchanged). Of the 106 #108 rows, 83 now return the service's status, error type, and message (30 on main). cargo test --workspace 1,500 passed; fmt; clippy -D warnings on 1.97.0; cargo +1.88.0 check. Closes part of #108.
… three comments The move of the FilterExpression operand-type checks into the semantic pass dropped them for the legacy QueryFilter and ScanFilter parameters, which bypass the parser. A ScanFilter with BEGINS_WITH and a number, or LT and a boolean, was accepted at request time and refused only when a row was evaluated, so an empty table returned 200. The desugared legacy filter now runs the same begins_with and ordering operand checks as before, with the FilterExpression prefix, before any row is read. Wire tests cover both parameters on an empty table. Comments reworded after review: the parsers state that grammar errors are reported before semantic errors and why the semantic checks are deferred; the empty operand list comment names the measured cases; the PerTableCapacity doc states that first-appearance order is a firmer guarantee than the service gives, with the measured orders.
Conflicted files and how each was resolved: - crates/engine/src/transact_get_items.rs: main (#418) replaced the inline item-count checks with request_limits::check_transact_count and reads key info through OperationContext::table_key_info_uncached. The branch reorders the handler into the service's phases (projection checks, table lookup, key schema as a first-offender cancellation, duplicates). Kept the branch's phase order and applied both of main's changes inside it: the count check stays first and the key info loop in the table-lookup phase calls table_key_info_uncached. - crates/engine/src/transact_write_items.rs: only the import block conflicted. The branch no longer imports parse_optional_condition; main adds request_limits. Kept build_expression_maps and added request_limits. Main's check_transact_count call and the four table_key_info_uncached lookups in prepare_write_op merged on their own and are kept. Files changed on both sides that merged on their own were read for overlap: crates/core/src/validation/mod.rs (main adds index projection limits, the branch changes key-shape messages; different functions), crates/engine/src/batch_write_item.rs (main replaces the count checks, the branch changes ItemCollectionMetrics; different regions), and docs/differences-from-dynamodb.md, where main's authorization row and the branch's control-plane row both describe a table ARN as TableName; the branch's row now points at the authorization row.
…'s message A ScanFilter or QueryFilter condition whose AttributeValueList holds a value of a type its ComparisonOperator does not take is refused at request time with the service's text, `One or more parameter values were invalid: ComparisonOperator <op> is not valid for <type> AttributeValue type`, naming the first such value. Measured against Amazon DynamoDB on 2026-10-23 on an empty table: BEGINS_WITH with N, LT and GT with BOOL, and BETWEEN with L then N, which names the L. The check runs in desugar_filter on each condition before it is desugared; the `Invalid FilterExpression:` wording that the previous commit restored is gone. attribute_type_code is exported from extenddb-core for the type tag. Pin the condition-expression order for a syntax error after a semantic error: `foo(pk) AND`, `ATTRIBUTE_EXISTS(pk) AND`, `begins_with(pk, :n) AND` and `attribute_exists(#x) AND` each answer the syntax error on the service (measured 2026-10-23), and the test asserts that no semantic message is returned. Document the order of ConsumedCapacity entries in transaction responses. With two tables the service returned the same table first for both request orders (measured 2026-10-23), so the order does not follow the request; ExtendDB keeps first appearance in the request on every backend. No wire test asserts a multi-table order.
… and messages Measured the full ComparisonOperator by AttributeValue type grid against Amazon DynamoDB on 2026-10-23 for ScanFilter, QueryFilter and KeyConditions, with the value counts, mixed-type lists, inverted BETWEEN ranges and unknown operator names. validate_legacy_condition in legacy_filter.rs now applies the measured rules to every legacy condition before it is desugared and before the table is looked up: the operator name (`Invalid comparison operator`), the value count (`Invalid number of argument(s) for the <op> ComparisonOperator`), the first value's type (EQ and NE take every type; BEGINS_WITH takes S and B; CONTAINS and NOT_CONTAINS take S, N, B, BOOL and NULL; LE, LT, GE, GT, BETWEEN and IN take S, N and B), that every value has the first value's type (`AttributeValues inside AttributeValueList must be of same type`), and that a BETWEEN lower bound is not above its upper bound. Each message carries the `One or more parameter values were invalid: ` prefix. KeyConditions get the same checks ahead of the table lookup, and a range key operator the service does not index (NE, NULL, NOT_NULL, CONTAINS, NOT_CONTAINS, IN) is refused with `Attempted conditional constraint is not an indexable operation`. The engine's own count messages for these parameters are gone. compare_orderable is exported from extenddb-core for the range check. The grid is pinned in legacy_filter_tests.rs and one wire row per message in TestLegacyFilterOperandType. The ConsumedCapacity order note now cites the three-table measurement: 16 transactions in eight request orders all listed the tables in name order.
Amazon DynamoDB documents no order for the ConsumedCapacity entries of TransactGetItems and TransactWriteItems. Measured on 2026-10-23 with three single-hash-key tables whose names sort opposite to their creation order, in three request orders: both operations listed the tables by creation time, oldest first, whatever order the request named them in. TableKeyInfo now carries the table's creation time. Every backend reads it from the catalog row or document it already fetches for the key schema (SQLite and PostgreSQL creation_date_time, MongoDB creation_date_time, Cassandra created_at), so no request makes another catalog read. PerTableCapacity moves from capacity_helpers.rs to per_table_capacity.rs, takes the TableKeyInfo of each operation, and lists tables oldest first, with name order breaking a tie on the same creation time. Both transaction handlers use it, including the idempotent replay path of TransactWriteItems, which now has a key_info accessor on PreparedOp. tests/test_transact_capacity_order.py creates three tables whose names sort opposite to their creation order, waiting for ACTIVE between them, and asserts the order for both operations at TOTAL and INDEXES over the three measured request orders; it runs against the service and against ExtendDB. The row in docs/differences-from-dynamodb.md states the measured order and ExtendDB's tie rule.
create_table wrote the catalog creation time as whole seconds, so two tables created inside the same second carried the same value. The transaction ConsumedCapacity order sorts tables by that value and falls back to name order on a tie, which the service does not do, and tests/test_transact_capacity_order.py failed 5 of 6 on this backend. The write now keeps the millisecond part, which is the precision bson::DateTime stores. Every reader already converts through timestamp_millis, so DescribeTable, ListTables, the backup and restore paths and the transaction key-info fetcher return the sub-second value, the same as the PostgreSQL backend. Catalog rows written by earlier builds keep whole seconds and can still tie.
The ConsumedCapacity order row said two tables created in the same millisecond are listed in name order. The precision of the stored creation time differs per backend, so the tie does too: nanoseconds on SQLite, microseconds on PostgreSQL, milliseconds on MongoDB and Cassandra, and whole seconds for a MongoDB catalog row written before the precision fix. The row now says which, and names the backends the order was measured on. The service-side measurement no longer claims the three tables had a single hash key, which the recorded run does not state. Same wording in the test docstring and the PerTableCapacity doc comment. The empty-operand-list comment in the condition parser now cites the 2026-10-23 measurement as well as the 2026-10-14 one.
semantics.rs had grown to 819 lines. It is now a module directory with one file per rule family: update_rules.rs for the update-expression rules, condition_rules.rs for the condition-expression rules, refs.rs for the name and value usage checks both passes share, errors.rs for the message builders, and tests.rs for the tests. mod.rs keeps the module documentation, the two function-name tables and the two public entry points. No behaviour change. The functions moved unedited; the only edits are the per-file imports, pub(super) on the helpers the siblings call, and the test module's imports in place of its use super::*. The nine semantic-pass tests keep their names and pass.
update_parser.rs had grown from 571 lines to 630. It is now a module directory: mod.rs holds the parser at 417 lines and tests.rs holds the 21 tests at 215 lines. No behaviour change. The test module moved unedited, keeping its use super::*, and the tests keep their names. One comment inside it lost an em dash.
The update-parser ADD/DELETE doc comment and the scan Segment check comment carried an em dash. Both are now two clauses.
…e service does Legacy QueryFilter, ScanFilter and KeyConditions values are now held to the service's rules that need the key schema, and to its number check, on every backend (issue #477, stacked on #399): - a legacy N value that is not a number answers the number message before the operator, count, type and range checks on that entry, inside NS, L and M too; - a KeyConditions value whose type is not the key's AttributeDefinitions type answers "Condition parameter type does not match schema type" (the range key answered 500 before, the hash key 200); - a non-indexable operator on any KeyConditions entry, the hash key included, is refused at request time; - an empty string on a key through KeyConditions is refused with the shared key message, hash key first, after the schema checks; - a QueryFilter entry on a key of the queried table or index is refused after every other Query check; - BatchGetItem charges each absent key the minimum read and lists every requested table in ConsumedCapacity, in table name order. Which entry the service names when several legacy entries are invalid is not a function of the request; the differences document records it.
A legacy condition value {"N": ""} now answers "The parameter cannot be
converted to a numeric value: " with the separator kept, the text the
service returns on a ScanFilter EQ condition. The item path keeps its own
text, which has no separator.
BatchGetItem still lists ConsumedCapacity tables by name, but the comment
and the wire test no longer claim that is the service's order: twelve
calls over three tables on the service returned all six orders, and one
request order came back in two different orders. The test now checks that
each table is listed once with its units.
The comments on the QueryFilter key check now say it is reported after the
key schema and Select checks and before the key value size check, which
the service reports later.
LeeroyHannigan
requested review from
amrith,
c33howard,
jcshepherd,
pdf-amzn,
robinnsc and
yesyayen
as code owners
October 24, 2026 12:34
…y sentence matches the other backends (cherry picked from commit 0294106)
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #399 (fix/transact-validation-order): this branch starts at its head, 4f4fc21, and the diff against main includes that PR until it merges.
Summary
Issue #477 lists eight differences left on the legacy
QueryFilter,ScanFilterandKeyConditionspaths after #399. Items 2 to 8 are fixed here: aQueryFilteron a key attribute (2), a legacyNvalue that is not a number (3, 4), aKeyConditionsvalue of the wrong schema type (5 and 7; the range key answered 500), an empty key string (6), andBatchGetItemcapacity for absent keys (8). The checks live in the engine, and the wire suite was run on SQLite, PostgreSQL and MongoDB; CI also runs the new file on Cassandra. The non-indexable operators on the hash key, which the same request-time pass covers, get the service's message as well. Item 1, which entry the service names when two legacy entries are both invalid, changed between two runs of the same request, so no rule can be encoded; the differences document records it.Fixes #477.
What changed
crates/engine/src/legacy_filter.rsvalidate_legacy_conditionchecks every number in the value list first (N,NS, and the numbers insideLandM) and answersThe parameter cannot be converted to a numeric value: <raw>or the overflow, underflow and 38-digit messages, before the operator name, count, type, same-type and range checks on that entry. An emptyNanswers the same text with nothing after the separator (... numeric value:), as the service does on this path; the item path keeps its own text, which has no separator.validate_key_conditions: the value checks plus the non-indexable refusal (Attempted conditional constraint is not an indexable operation) on anyKeyConditionsentry, hash key included. Query runs it before the table lookup. Before, a hash key withNE,IN,NULL,NOT_NULL,CONTAINSorNOT_CONTAINSansweredQuery key condition not supported.desugar_key_conditionstakes the key schema and the attribute definitions of the queried table or index and walks the keys in schema order: an absent hash key, then the value type against the key'sAttributeDefinitionstype (One or more parameter values were invalid: Condition parameter type does not match schema type), then the hash key's operator, then conditions on non-key attributes, then an emptySorBkey value (One or more parameter values are not valid. The AttributeValue for a key attribute cannot contain an empty string value. Key: <name>, hash key named first).reject_query_filter_on_key: a legacyQueryFilterentry on a key of the queried table or index answersQueryFilter can only contain non-primary key attributes: Primary key attribute: <name>, hash key first. Query runs it after the Select checks and before theExclusiveStartKeycheck. The service reports it after the table and index lookup, the key schema, empty key and Select checks, and before the key value size check.crates/engine/src/query.rs: the three call sites.crates/engine/src/batch_get_item.rs: an absent key is charged the minimum read (0.5, or 1.0 withConsistentRead) and keeps its table inConsumedCapacity. The per-table map is aBTreeMap, so ExtendDB lists the tables in name order. The service's order is not stable: twelve calls over three tables returned all six orders, and one request order came back in two different orders, so the tests check that each table is listed once and do not check the order.crates/core/src/validation/mod.rs:validate_attribute_numberandvalidate_no_empty_key_valueare nowpubso the engine reuses the item-path checks.docs/differences-from-dynamodb.md: a "Query and Scan" section with one row for the entry-naming difference.crates/storage-cassandra/src/data/index.rs: an unknownIndexNameon Cassandra answeredThe table does not have the specified index: Index nope not found, where the other backends and the service answer... specified index: nope. The new tests are the first to check that text on Cassandra. The one-line fix is the same change as in fix(query): refuse key condition values over the key size limits #481 and fix(query): refuse a Query ExclusiveStartKey the key condition cannot reach #483, so the three merge cleanly in any order.Service measurements
All against Amazon DynamoDB, us-east-1, 2026-10-24 between 08:51 and 12:08 UTC, on an S/S table, a hash-only table and a new S/N table with a GSI on two S attributes.
N "abc"; EQ[N abc, S x]and[S x, N abc]; IN[S x, N abc]; BETWEEN[N 2, N abc]and[N abc, N 2]; EQNS ["1","abc"],L [N abc],M {k: N abc}; LTL [N abc]ValidationExceptionThe parameter cannot be converted to a numeric value: abcN ""The parameter cannot be converted to a numeric value:(the separator with nothing after it)N "1e400","1e-400", 39 digitsNumber overflow. Attempting to store a number with magnitude larger than supported range;Number underflow. Attempting to store a number with magnitude smaller than supported range;Attempting to store more than 38 significant digits in a NumberConditionalOperator can only be used when Filter or Expected has two or more elementspk EQ N abcwith a bad QueryFilter typeOne or more parameter values were invalid: Condition parameter type does not match schema typeOne or more parameter values were invalid: Attempted conditional constraint is not an indexable operationQuery key condition not supportedInvalid number of argument(s) for the EQ ComparisonOperatorQuery condition missed key schema element: pk(first two),... element: sk(third)The table does not have the specified index: nope;Consistent reads are not supported on global secondary indexes;ResourceNotFoundException; the schema type messageOne or more parameter values are not valid. The AttributeValue for a key attribute cannot contain an empty string value. Key: <pk, sk, gpk or gsk>QueryFilter can only contain non-primary key attributes: Primary key attribute: pk(sk alone names sk)... Primary key attribute: gpk;... gsk; Count 0; Count 1; Count 2ResourceNotFoundException; the index message; schema type;missed key schema element: sk;missed ... pk; non-indexable;not supported; empty key;Either the KeyConditions or KeyConditionExpression parameter must be specified in the request.; the type message; the ConsistentRead message; the Select message; the QueryFilter message[{TableName, CapacityUnits: 0.5}]; 1.0; 1.0; 0.5; 50.0; 1.0; 2.0; 0.5; 0.5[{TableName, CapacityUnits: 0.5, Table: {CapacityUnits: 0.5}}]; no ConsumedCapacity; no ConsumedCapacityTests
tests/test_legacy_condition_parity.py(new, 60 cases): number messages and their order on ScanFilter, QueryFilter and KeyConditions, the emptyNincluded; schema type on S and N keys and on GSI keys; non-indexable on the hash key and its order; the hash key's type-then-operator order; empty key strings on hash, range and index keys under every operator; QueryFilter on a key and its placement after the key schema and Select checks; BatchGetItem capacity for absent keys in every measured shape. On main, 56 of the 60 fail (the four that pass are controls); the failures include the 11InternalServerErroranswers of item 5.crates/engine/src/legacy_filter_tests.rs: five new unit tests.Interactions with open PRs
Checked with
git merge-tree --write-tree --merge-base=origin/main fix/legacy-condition-parity origin/<branch>against every open branch that touches a file this PR changes.query.rs; fix(limits): attribute-name size limit on every write path #433, fix: validate secondary-index keys on UpdateItem and transaction writes the way the service does #416, fix(limits): size items and updates the way the service does #409, feat(storage): SnapshotEngine, a consistent table snapshot on every backend [backup/restore 4/4] #395, feat(partiql): implement ExecuteStatement, BatchExecuteStatement, ExecuteTransaction #411, fix(backup): report point-in-time recovery as unsupported #335 in the transaction and Cassandra files; fix(mongodb): retry an unconditional PutItem that conflicts on a GSI table #487 in the differences document.fix/427-lone-non-equality) inserts two key-condition calls aftervalidate_select_projectioninquery.rs, where this PR insertsreject_query_filter_on_key; the merge keeps both, fix(query): refuse a key condition with no HASH equality the way the service does #479's calls first, then this PR's, since the service reports key condition errors before a QueryFilter on a key. fix(query): classify and order key conditions on a multi-attribute GSI the way the service does #429 (fix/gsi-multi-range-query) adds a multipart branch at the top ofdesugar_key_conditionson the old&[(String, bool)]key schema parameter, which this PR replaces with&[KeySchemaElement]and&[AttributeDefinition]; the merge keeps this PR's signature and rewrites fix(query): classify and order key conditions on a multi-attribute GSI the way the service does #429's hash and range counts overKeySchemaElement.query.rsalso conflicts with fix(query): classify and order key conditions on a multi-attribute GSI the way the service does #429 through fix(expressions): validate in the service's order, with its messages #399. After that merge, fix(query): classify and order key conditions on a multi-attribute GSI the way the service does #429'svalidate_name_refs_in_exprloses its only call in the rewritten filter block and clippy fails on the dead function, so the merge has to restore that call; fix(query): classify and order key conditions on a multi-attribute GSI the way the service does #429's tests pass on the merged tree. fix: validate secondary-index keys on UpdateItem and transaction writes the way the service does #416, feat(partiql): implement ExecuteStatement, BatchExecuteStatement, ExecuteTransaction #411 and fix(backup): report point-in-time recovery as unsupported #335 add rows or sections next to the new "Query and Scan" section of the differences document; the merge keeps both sides.feat/docs-site) touches the differences document and conflicts the same way against the fix(expressions): validate in the service's order, with its messages #399 head, in its own files. fix: accept a table ARN as TableName in data-plane operations #200 and Add TiDB storage backend #140 also touch it;git merge-treecannot read their trees, so they were not checked.Not changed here
The number of query conditions (n) exceeds the number of key attributes defined in the schema (m)and the name inQuery condition missed key schema elementfor a non-key entry (the service names the first absent key; ExtendDB names the hash key): both pre-existing on main, outside the eight.Cannot specify the AttributesToGet when choosing to get only the Count, which ExtendDB does not have; SPECIFIC_ATTRIBUTES without a projection; ALL_PROJECTED_ATTRIBUTES without an IndexName) run before the table lookup on the service and after it in ExtendDB; pre-existing on main and on the expression path.KeyConditions: the service reports it after a QueryFilter on a key, and ExtendDB does not check it on this path. fix(query): refuse key condition values over the key size limits #481 adds it.TransactGetItemscapacity for an absent key (2.0 on the service; not measured on ExtendDB).