Summary
A document query whose in clause sits on a non-terminal constrained index property (i.e. the range clause is followed by an equality clause on a later property of the same index) is accepted and answered with an empty result set, instead of being rejected as an invalid query.
The query grammar requires the range operator (in included) to be on the last constrained index property, so this shape is illegal — but nothing tells the client that. The query succeeds, returns 0 documents, and is indistinguishable from "no matching data".
Reproduction
Read-only, against public testnet (Drive 4.1.0, protocol 13). Contract 9oDC6xdg8WRixTD2j3FCBq3vtsrf6bRGjXSJbhtFoma9 has a post doctype with index ownerAndTime = [$ownerId, $createdAt]; any contract with a two-property index and one document reproduces it.
// npm i @dashevo/evo-sdk && node repro.mjs
import { EvoSDK } from '@dashevo/evo-sdk'
const CONTRACT = '9oDC6xdg8WRixTD2j3FCBq3vtsrf6bRGjXSJbhtFoma9'
const TYPE = 'post'
const sdk = EvoSDK.testnetTrusted({ settings: { timeoutMs: 30000 } })
await sdk.connect()
const count = (r) => (r instanceof Map ? r.size : Object.keys(r ?? {}).length)
const first = (r) => (r instanceof Map ? [...r.values()][0] : Object.values(r ?? {})[0])
const query = (where, orderBy) => sdk.documents.query({
dataContractId: CONTRACT, documentTypeName: TYPE, where,
...(orderBy ? { orderBy } : {}), limit: 1,
})
const sample = first(await sdk.documents.query({ dataContractId: CONTRACT, documentTypeName: TYPE, limit: 1 }))
const { : ownerId, : createdAt } = sample.toJSON()
// A) legal ground truth: == + ==
const eq = await query([['$ownerId', '==', ownerId], ['$createdAt', '==', createdAt]])
// B) legal control: in on the LAST constrained property
const inOnly = await query([['$ownerId', 'in', [ownerId]]], [['$ownerId', 'asc']])
// C) THE BUG: in on a NON-terminal property, == after it
const inEq = await query(
[['$ownerId', 'in', [ownerId]], ['$createdAt', '==', createdAt]],
[['$ownerId', 'asc'], ['$createdAt', 'asc']]
)
console.log(count(eq), count(inOnly), count(inEq))
// D) control: duplicate in-elements ARE rejected loudly
await query([['$ownerId', 'in', [ownerId, ownerId]]], [['$ownerId', 'asc']])
Observed output (2026-08-28, testnet):
A) $ownerId == X AND $createdAt == T -> 1 row(s)
B) $ownerId in [X] -> 1 row(s)
C) $ownerId in [X] AND $createdAt == T -> 0 row(s) <-- silent empty, no error
D) $ownerId in [X, X] -> rejected loudly: grpc InvalidArgument "invalid IN clause ..."
A and C constrain the same document by the same values; C additionally matches B's in clause on its own. C returning 0 with a success status is not a defensible answer for any semantics of that query.
Expected
Query C should be rejected with an InvalidArgument/invalid-query error, exactly like case D — e.g. "the range (in) clause must be on the last queried index property".
Why this matters (real-world impact)
This exact shape shipped in a production client (yappr, Jan 2026): a ['postId','==',X] lookup on the like doctype's [postId, $ownerId] index was rewritten to ['postId','in',[X]], ['$ownerId','==',Y] while working around an unrelated SDK byte-array issue. Because Drive answered with a clean empty set, "has this user liked this post?" silently returned no for every user for seven months — masked by optimistic in-session UI state, invisible in logs, and only found by accident during an index-design review. A loud error would have failed the very first call in development.
Silent-empty converts a client programming error into invisible data loss, and it also sits oddly with provable queries: the platform produces a valid-looking (provable) "no results" answer for a question the query grammar does not actually support.
Suggested fix
Validate the clause/index-position relationship at query-validation time and reject, rather than executing a query plan that can never match:
- The check belongs where the other clause-shape rules already live (rs-drive's query validation — the same layer that rejects duplicate
in elements, multiple range clauses, etc.): after resolving which index serves the query, if the range/in clause is not on the last constrained property of that index, return an invalid-query error.
- Rejection is preferable to trying to serve the query (e.g. post-filtering on the trailing equality), because (a) it matches the documented grammar and the platform's existing behavior for every other malformed shape, (b) it keeps query cost/index semantics predictable, and (c) any silent-result path for unsupported shapes will keep producing this class of invisible client bug.
Happy to provide more detail or test candidate fixes against our contracts.
Summary
A document query whose
inclause sits on a non-terminal constrained index property (i.e. the range clause is followed by an equality clause on a later property of the same index) is accepted and answered with an empty result set, instead of being rejected as an invalid query.The query grammar requires the range operator (
inincluded) to be on the last constrained index property, so this shape is illegal — but nothing tells the client that. The query succeeds, returns 0 documents, and is indistinguishable from "no matching data".Reproduction
Read-only, against public testnet (Drive 4.1.0, protocol 13). Contract
9oDC6xdg8WRixTD2j3FCBq3vtsrf6bRGjXSJbhtFoma9has apostdoctype with indexownerAndTime = [$ownerId, $createdAt]; any contract with a two-property index and one document reproduces it.Observed output (2026-08-28, testnet):
A and C constrain the same document by the same values; C additionally matches B's
inclause on its own. C returning 0 with a success status is not a defensible answer for any semantics of that query.Expected
Query C should be rejected with an
InvalidArgument/invalid-query error, exactly like case D — e.g. "the range (in) clause must be on the last queried index property".Why this matters (real-world impact)
This exact shape shipped in a production client (yappr, Jan 2026): a
['postId','==',X]lookup on thelikedoctype's[postId, $ownerId]index was rewritten to['postId','in',[X]], ['$ownerId','==',Y]while working around an unrelated SDK byte-array issue. Because Drive answered with a clean empty set, "has this user liked this post?" silently returned no for every user for seven months — masked by optimistic in-session UI state, invisible in logs, and only found by accident during an index-design review. A loud error would have failed the very first call in development.Silent-empty converts a client programming error into invisible data loss, and it also sits oddly with provable queries: the platform produces a valid-looking (provable) "no results" answer for a question the query grammar does not actually support.
Suggested fix
Validate the clause/index-position relationship at query-validation time and reject, rather than executing a query plan that can never match:
inelements, multiple range clauses, etc.): after resolving which index serves the query, if the range/inclause is not on the last constrained property of that index, return an invalid-query error.Happy to provide more detail or test candidate fixes against our contracts.