VIEW THIS AS

Auto mode follows the Route Engine until you choose a viewpoint.

YOU ARE HERE

ROUTE CHECK

CONNECTED TO

WHAT NEXT

Use the canonical route for this room, or HELP if you are unsure.

Validate a Fabric Query Result: Range Evidence, Rich Queries and Phantom Changes

Three students in school uniforms work through open books at a classroom table, with textbooks and stationery nearby and study notes on the whiteboard behind them.

中文版 · Blockchain learning guide · Previous article

Blockchain learning · Extension BCX018

Three students in school uniforms work through open books at a classroom table, with textbooks and stationery nearby and study notes on the whiteboard behind them.
Study photograph for illustration; it does not establish that the pictured students are studying blockchain.

Two queries can return the same records during simulation yet protect different facts during validation. A captured lexical range can detect a new key inside its protected interval. A rich-query packet can protect the versions of captured result keys without detecting an additional record that newly matches its selector.

The useful question is therefore not simply “What did the query return?” Ask what evidence the simulator captured, which prior valid writes affect the comparison state, and which validation condition that evidence can actually test.

This lesson supplies fictional catalogue records and complete evidence packets. You will calculate validation outcomes, explain a stale count that passes the modeled checks, and distinguish a genuinely partial range witness from a chaincode loop that merely saw one row. Twenty cases and 320 finite comparisons support the worksheet; no Fabric peer, CouchDB query, chaincode execution or ledger commit was run.

Choose your reading route

In this guide

  • For the main range-versus-rich comparison, read sections 1–4.
  • For version changes, endpoints and empty results, read sections 5–7.
  • For partial scans and the crucial peer-batching distinction, read sections 8–9 together.
  • For ordering, reproducibility and an assessment, continue through sections 10–12.

The prerequisite Fabric endorsement, ordering and validation lesson covers the transaction flow. General database transaction and phantom concepts already have their own lesson. Here the new task is inspecting the particular evidence produced by two Fabric query paths.

Back to contents · Next section

1. Read the supplied dataset without changing its meaning

There is one public chaincode namespace. Initially, item:a1 has shelf A, value 10 and version (40,1). item:a3 has shelf A, value 30 and version (40,2). item:b1 has shelf B, value 50 and version (40,3). Versions are supplied identities, separate from the application values.

The full lexical range is [item:a, item:b): include keys at or above item:a and below item:b. The alternative rich selector is {“selector”:{“shelf”:”A”}}. In this particular dataset, both return item:a1 and item:a3. A key interval and a JSON field predicate are different criteria; their agreement here is a supplied coincidence of the data layout.

Both alternative proposals would write report:A with count 2. That report key is outside the scanned range and has no extra read dependency. The packets are alternative histories, not two proposals that must be submitted together. The rich-query write deliberately illustrates an unsafe dependency assumption, rather than recommending this design for deployment.

All other transaction checks and application authorization are supplied successful. MVCC validation is enabled. Keys are unique, endpoints are bounded nonempty ASCII strings, and the exercise uses raw key/version range evidence. There is no private data, pagination, hash-summary comparison, query error, multiple namespace or chaincode nondeterminism.

Full witnesses genuinely reached iterator exhaustion below the total query limit. Earlier writes use fresh version heights. We separately mark whether a preceding transaction is valid; only valid preceding writes enter the comparison state.

Back to contents · Next section

2. Inspect the two evidence packets

The exhausted range packet has no additional ordinary point reads. Its RangeQueryInfo records StartKey=item:a, EndKey=item:b, ItrExhausted=true, and the ordered pairs item:a1 at (40,1), then item:a3 at (40,2).

The rich-query packet records those two key/version pairs in its ordinary Reads list and has no RangeQueryInfo. Its selector explains how the records were selected during simulation, but the packet does not contain a range witness that rechecks that selector’s complete matching set at validation.

These are descriptions of the supplied captured packets. Do not reconstruct a different packet from what you think an application loop probably consumed. Later sections show why peer-side response handling can make that inference wrong.

For a range witness, record the start, recorded end, exhaustion flag and exact ordered key/version list. For ordinary reads, record each key and version. Keep the proposed report write in a separate column. A write proposal is the intended output; it is not evidence that its input set remained unchanged.

This distinction is supported by the pinned v2.5.16 query executor: range results and rich-query results populate different dependency structures. The lesson models their relevant public-data paths, not every query API or summary representation.

Back to contents · Next section

3. Construct the validation state, then run the checks

Begin with the supplied committed state and overlay preceding valid inserts, updates and deletes in their given order. A preceding invalid transaction does not contribute its proposed write to this comparison state. Within our fictional block, fresh writes receive new version identities even when their values repeat old values.

Check ordinary captured key versions first within the modeled namespace. A missing captured key or mismatched version produces MVCC_READ_CONFLICT. Then compare each range’s validation-state key/version sequence against its captured sequence; a mismatch produces PHANTOM_READ_CONFLICT.

For an exhausted range, use the inclusive start and exclusive original end. For a non-exhausted witness, include its recorded end key. We will calculate that second case explicitly rather than guessing the interval from a query’s original arguments.

A packet passes our state-validation stage only when all its supplied checks pass. Under the additional successful-check assumptions, its proposed report can apply in the model. This is a teaching outcome, not an observation that a real commit happened.

Neither a count comparison nor a comparison of application values replaces these checks. A new version with the same value can invalidate a dependency, and two different key sets can have the same number of records.

Back to contents · Next section

4. Main comparison: insert item:a2 before validation

A valid preceding transaction inserts item:a2, shelf A, value 20, with fresh version (41,0). The original a1 and a3 versions remain unchanged. At the query proposal’s validation point, there are now three shelf-A records.

For the full range packet, the validation-state sequence is a1 at (40,1), a2 at (41,0), then a3 at (40,2). Compare it with the captured sequence a1, a3. The first pair matches, but the next current key is a2 where the witness expects a3. The range check fails with PHANTOM_READ_CONFLICT. Its count-2 report does not apply.

For the rich packet, the ordinary read checks ask about a1 at (40,1) and a3 at (40,2). Both still match. There is no captured dependency that asks whether a new a2 also satisfies shelf=A. The packet passes the modeled state check, and its proposed count-2 report can apply even though three matching records exist.

This is missing result-set protection, not absence of every kind of rich-query validation. The returned keys still carry version dependencies. Calling rich queries “completely unchecked” would predict the wrong results in the next section.

Official CouchDB guidance warns about using rich queries with ledger updates because matching sets can change. The fictional report makes that limitation visible; adding a query index would not itself add the missing validation predicate.

Back to contents · Next section

5. Captured-key deletion and same-value replacement

Reset to the original state. A valid preceding transaction deletes item:a3. The range packet’s expected second key is missing, so its range comparison fails with PHANTOM_READ_CONFLICT. The rich packet also fails, but its ordinary read dependency on a3 produces MVCC_READ_CONFLICT.

Reset again. Rewrite item:a1 with the same shelf A and the same value 10, but under a fresh version. The range still contains two keys, and their visible application values look unchanged. Nevertheless, the recorded a1 version no longer matches.

The range-only packet therefore fails PHANTOM_READ_CONFLICT, while the rich packet fails MVCC_READ_CONFLICT. The difference in code follows from where the dependency was captured, rather than proving that only one query noticed the change.

For another reset, delete a3 and insert a2 as preceding valid changes. The matching count remains two, but the identities differ. The range detects the changed sequence. The rich packet detects that its captured a3 is gone. Neither proposal is rescued by the aggregate count returning to two.

If a real packet contained both ordinary reads and range evidence, the first failing check would depend on that complete packet and the relevant validation order. Our range-only and rich-only packets deliberately isolate the cause so the answer is reproducible.

Back to contents · Next section

6. Endpoint and outside-change tests

An update of item:b1 from value 50 to 51 is outside the full interval and outside the rich packet’s captured keys. Both supplied packets pass their modeled checks. This verdict is bounded to these dependencies; it does not claim b1 can never matter to any other application rule.

Insert a key exactly equal to item:b. The exhausted range’s upper endpoint is exclusive, so that new key is not part of its comparison sequence. The full range remains valid under the stated assumptions.

Insert a key exactly equal to item:a instead. The lower endpoint is inclusive. This adds a new first result before a1, and the full range fails PHANTOM_READ_CONFLICT.

These two tests are intentionally exact endpoint tests, not vague “near the boundary” examples. Using starts-with reasoning or treating both ends as inclusive would produce an incorrect result. All strings here use the supplied simple lexical ordering.

Also separate key membership from the shelf field. The range criterion is about key order. Our inserted lower-bound record is supplied with shelf A, but a different shelf value would not remove a key that still lies inside the lexical interval from the range comparison.

Back to contents · Next section

7. Empty does not always mean no dependency

Use a separate exhausted range [item:c, item:d), which initially returns no keys. Its evidence still records the interval, exhaustion and an empty result list. A valid preceding insertion of item:c2 makes the validation-state result nonempty, so this range witness fails PHANTOM_READ_CONFLICT.

Now use a separate rich selector for shelf C, also initially empty. Its supplied packet has no captured result-key reads and no range witness. The same newly inserted C record creates no mismatch in that empty ordinary read list. The modeled state stage passes, although the proposed count-0 report no longer describes the matching set.

An empty result can therefore encode a protected absence over a captured range, or merely leave no returned-key dependencies in a rich packet. You must inspect the evidence structure rather than infer its protection from the number zero.

The examples do not add absent-key ordinary reads or a custom application guard. If those existed, they would be additional evidence to evaluate. The conclusion concerns the exact two packets supplied here.

Back to contents · Next section

8. A supplied partial witness protects a captured prefix

Consider a different packet whose RangeQueryInfo is StartKey=item:a, EndKey=item:a1, ItrExhausted=false, with one captured pair: a1 at (40,1). This exact witness is supplied as input. We do not infer it from a chaincode loop showing one row.

For this non-exhausted witness, the recorded end a1 is included. Its protected interval runs from item:a through item:a1 inclusively. The original query may have asked for a longer range, but this captured evidence does not certify that whole range.

Insert item:a2 after the recorded end. The prefix sequence is unchanged, so this packet passes. Its proposed output is explicitly captured_prefix_count 1, not a complete count of shelf A. Calling that output “the total is one” would overstate the evidence even before any concurrent change.

Insert item:a0 instead. It lies before a1 but within the protected interval, adding a result and causing PHANTOM_READ_CONFLICT. Rewrite a1 to the same value with a fresh version, and the witness also fails because its included end key has changed version.

The partial result is therefore neither full protection nor no protection. It has a precise interval and version boundary. The next section explains why observing one application-visible row does not by itself establish that this was the actual captured interval.

Back to contents · Next section

9. Peer response batching can capture more than the loop saw

Use a source-shaped probe with initially empty pending results, no pagination, MaxResultLimit=1, totalReturnLimit at least 2, and no iterator error. These deliberately tiny parameters are teaching inputs, not production defaults.

The peer response builder consumes a1 and queues it. It then consumes a3 before deciding to return the preceding batch. At that point a1 can be delivered to chaincode while a3 is already captured by the simulator and buffered for a later response.

The captured range in this probe ends at a3 with ItrExhausted=false and contains both a1 and a3. It differs from section 8’s one-key witness, even though the application has so far received only a1.

Now insert a2. It falls inside the captured interval through a3, so the probe’s range comparison fails PHANTOM_READ_CONFLICT. Predicting acceptance merely because the chaincode visibly called Next once would use the wrong evidence packet.

The pinned response builder explains this buffering boundary. The lesson does not predict every deployment’s batch size or total limit. The practical rule is to inspect actual captured query evidence, including the recorded end and exhaustion flag, rather than equate application-visible consumption with peer-side iterator consumption.

Back to contents · Next section

10. Invalid predecessors and later writes change the interpretation

Reset to the full baseline witness. A preceding proposal would insert a2, but it is marked invalid. Its write does not enter the comparison state. The full range still sees a1 and a3 at their captured versions and passes. Ordering before the query is insufficient; the earlier write must also be valid to affect this calculation.

Reverse the main valid-insertion order instead. Let the full query validate first, while only a1 and a3 match, so its count-2 report can apply. Then let an otherwise-valid write-only insertion add a2. After both, the matching count is three while the stored report remains two.

There is no contradiction. The range check protected the supplied observation at its validation position. It did not create a permanent lock, promise that future records could not appear, or maintain a live aggregate automatically.

The later insertion’s validity is a supplied assumption about a write-only transaction with all other checks satisfied. The exercise does not establish that an arbitrary later transaction must pass. It simply separates validation-time consistency from indefinite report freshness.

A useful report should therefore retain its relevant observation or validation context. A plain number without a time or state boundary can invite a stronger interpretation than the original evidence supports.

Back to contents · Next section

11. Reproduce the finite checks and document their limits

The twenty specified cases include unchanged packets, the main insertion pair, deletion, same-value/new-version changes, outside updates, both full-range endpoints, three partial-range variants, two empty-result variants, an invalid predecessor and equal-count membership replacements.

The local checker compares an ordered-sequence validator with a separately expressed membership-and-version predicate. It generates 64 bounded states by assigning four modes to each of three keys, then checks five packet types against every state. That gives 64 times 5, or 320, comparisons. The reverse-order assertion and batching probe are additional checks.

Agreement checks internal consistency of these finite formulations. It is not execution of Fabric’s validator or an independent implementation conformance result. No real CouchDB query or commit is involved, and no claim follows about omitted private data, hash summaries, pagination or unbounded endpoints.

The source packet pins thirteen files to Fabric v2.5.16, including query capture, response construction, range comparison, combined iteration and the vendored shim interface. That makes the implementation boundary inspectable without treating every future version as identical.

Your worksheet should preserve the query criterion, ordinary Reads, each captured range’s start/end/exhaustion/list, preceding valid changes, resulting comparison sequence, first failed check and proposed-write outcome. Those columns make it possible to disagree about one precise step instead of exchanging broad claims about “MVCC safety.”

Back to contents · Next section

12. Exercises and model answers

  1. A new a2 matches shelf A, while a1/a3 versions stay unchanged. Which packet rejects it?
  2. Does rewriting a1 to value 10 under a new version preserve both packets?
  3. Why can inserting a2 pass the one-key partial witness but fail the batching probe?
  4. Does an empty exhausted range have nothing to validate?
  5. Does the full range stay invalid when the earlier insertion was itself invalid?
  6. Can a valid full-range report become stale after a later valid insertion?

Answers: 1. The full range rejects the changed sequence; the supplied rich packet passes its captured-key checks. 2. No; both fail through their respective range or ordinary-read dependency. 3. Their recorded ends differ: a1 versus a3, with the non-exhausted end included. 4. No; a new key in the recorded interval contradicts its empty witness. 5. No; the invalid write is excluded from comparison state. 6. Yes; validation does not maintain the report after subsequent writes.

For an extension, ask what evidence is missing from a screenshot showing only “two rows returned.” It omits query type, captured key versions, interval and exhaustion, peer capture behavior and the preceding valid writes. Without those inputs, the validation outcome is not determined.

Back to contents · Next section

Sources and further reading

Source review date: 13 October 2026. The implementation and corresponding documentation are pinned to Hyperledger Fabric v2.5.16, not asserted to be the newest release.

For the broader transaction sequence, return to Fabric endorsement, ordering and validation. This lesson’s answer remains narrower: determine what the captured query packet protects, then compare exactly that evidence at the supplied validation point.

Back to contents