Summary
QueryBinder.bindIdSet throws when the id array contains undefined/null entries, e.g. bindIdSet(1, ["0x1", undefined, "0x2"]). the legacy ECSqlStatement.bindIdSet (native binder) silently drops such entries instead, so code migrating from withPreparedStatement/ECSqlStatement to withQueryReader/QueryBinder (see #9463) can crash on inputs that used to work fine.
Repro
const qb = new QueryBinder();
qb.bindIdSet(1, ["0x1", undefined, "0x2"]);
throws:
TypeError: Cannot read properties of undefined (reading 'length')
at Object.isId64 (core/bentley/.../Id.js)
at Object.isValidId64 (core/bentley/.../Id.js)
at compressIds (core/bentley/.../CompressedId64Set.js)
at compressArray
at sortAndCompress
at QueryBinder.bindIdSet (core/common/.../ConcurrentQuery.js)
Same crash reachable via withQueryReader/InVirtualSet using an id array bound through QueryBinder.
Root cause
QueryBinder.bindIdSet (core/common/src/ConcurrentQuery.ts) passes the raw array directly to CompressedId64Set.sortAndCompress, which calls Id64.isValidId64 on every entry. isValidId64 reads .length on the value, which throws for undefined/null instead of returning false.
Verified against the native implementation (iTwin/imodel-native, iModelJsNodeAddon/IModelJsNative.cpp, NativeECSqlBinder::BindIdSet): it uses the REQUIRE_ARGUMENT_STRING_ARRAY macro, which silently skips any non-string array entry before building the native IdSet. That's why the same input works through ECSqlStatement.bindIdSet but crashes through QueryBinder.bindIdSet.
-- created w/ help from AnmolDroid 🤖
Summary
QueryBinder.bindIdSetthrows when the id array containsundefined/nullentries, e.g.bindIdSet(1, ["0x1", undefined, "0x2"]). the legacyECSqlStatement.bindIdSet(native binder) silently drops such entries instead, so code migrating fromwithPreparedStatement/ECSqlStatementtowithQueryReader/QueryBinder(see #9463) can crash on inputs that used to work fine.Repro
throws:
Same crash reachable via
withQueryReader/InVirtualSetusing an id array bound throughQueryBinder.Root cause
QueryBinder.bindIdSet(core/common/src/ConcurrentQuery.ts) passes the raw array directly toCompressedId64Set.sortAndCompress, which callsId64.isValidId64on every entry.isValidId64reads.lengthon the value, which throws forundefined/nullinstead of returningfalse.Verified against the native implementation (
iTwin/imodel-native,iModelJsNodeAddon/IModelJsNative.cpp,NativeECSqlBinder::BindIdSet): it uses theREQUIRE_ARGUMENT_STRING_ARRAYmacro, which silently skips any non-string array entry before building the nativeIdSet. That's why the same input works throughECSqlStatement.bindIdSetbut crashes throughQueryBinder.bindIdSet.-- created w/ help from AnmolDroid 🤖