From 0e22b04955683dceb45cb8680bd4fedb17ee2e77 Mon Sep 17 00:00:00 2001 From: anmolshres98 Date: Wed, 22 Jul 2026 14:41:21 -0500 Subject: [PATCH 1/6] fix: QueryBinder.bindIdSet skips invalid id entries instead of throwing (#9530) --- ...-9530-bindidset-null_2026-07-22-19-25.json | 10 +++++++++ core/common/src/ConcurrentQuery.ts | 22 ++++++++++++++++--- core/common/src/test/ConcurrentQuery.test.ts | 22 +++++++++++++++++++ 3 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 common/changes/@itwin/core-common/issue-9530-bindidset-null_2026-07-22-19-25.json diff --git a/common/changes/@itwin/core-common/issue-9530-bindidset-null_2026-07-22-19-25.json b/common/changes/@itwin/core-common/issue-9530-bindidset-null_2026-07-22-19-25.json new file mode 100644 index 00000000000..89a63630621 --- /dev/null +++ b/common/changes/@itwin/core-common/issue-9530-bindidset-null_2026-07-22-19-25.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@itwin/core-common", + "comment": "Fixed QueryBinder.bindIdSet throwing on entries that are not valid Id64Strings; such entries are now skipped with a warning, matching ECSqlStatement.bindIdSet.", + "type": "none" + } + ], + "packageName": "@itwin/core-common" +} diff --git a/core/common/src/ConcurrentQuery.ts b/core/common/src/ConcurrentQuery.ts index 5876f01bb2e..c5aca5c4aff 100644 --- a/core/common/src/ConcurrentQuery.ts +++ b/core/common/src/ConcurrentQuery.ts @@ -5,10 +5,12 @@ /** @packageDocumentation * @module iModels */ -import { BentleyError, CompressedId64Set, DbResult, Id64, Id64String, OrderedId64Iterable } from "@itwin/core-bentley"; +import { BentleyError, CompressedId64Set, DbResult, Id64, Id64String, Logger, OrderedId64Iterable } from "@itwin/core-bentley"; import { LowAndHighXYZ, Point2d, Point3d, Range3d } from "@itwin/core-geometry"; import { Base64 } from "js-base64"; +const loggerCategory = "core-common.QueryBinder"; + /** * Specifies the format of the rows returned by the `query` and `restartQuery` methods of * [IModelConnection]($frontend), [IModelDb]($backend), and [ECDb]($backend). @@ -478,15 +480,29 @@ export class QueryBinder { * @param indexOrName Specify parameter index or its name used in ECSQL statement. * @param val @type OrderedId64Iterable value to bind to ECSQL statement. * @returns @type QueryBinder to allow fluent interface. + * @note Entries that are not valid [Id64String]($bentley)s - including `undefined` and `null` - are + * ignored, matching the behavior of [ECSqlStatement.bindIdSet]($backend). A warning is logged when + * any entry is skipped. */ public bindIdSet(indexOrName: string | number, val: OrderedId64Iterable) { this.verify(indexOrName); const name = String(indexOrName); - OrderedId64Iterable.uniqueIterator(val); + const ids: Id64String[] = []; + let skipped = 0; + for (const id of val) { + if (typeof id === "string" && Id64.isValidId64(id)) + ids.push(id); + else + skipped++; + } + + if (skipped > 0) + Logger.logWarning(loggerCategory, `QueryBinder.bindIdSet: skipped ${skipped} entries that are not valid Id64Strings`); + Object.defineProperty(this._args, name, { enumerable: true, value: { type: QueryParamType.IdSet, - value: CompressedId64Set.sortAndCompress(OrderedId64Iterable.uniqueIterator(val)), + value: CompressedId64Set.sortAndCompress(ids), }, }); return this; diff --git a/core/common/src/test/ConcurrentQuery.test.ts b/core/common/src/test/ConcurrentQuery.test.ts index ad0af51f497..6bebad607a5 100644 --- a/core/common/src/test/ConcurrentQuery.test.ts +++ b/core/common/src/test/ConcurrentQuery.test.ts @@ -111,6 +111,28 @@ describe("QueryBinder", () => { ); }); + it("bindIdSet skips entries that are not valid Id64Strings", () => { + const queryBinder = new QueryBinder(); + queryBinder.bindIdSet("idSetValue", ["0x22bd8", undefined, null, "0", "50", "0x22bd9"] as unknown as Id64String[]); + assert.deepEqual(queryBinder.serialize(), { + idSetValue: { + type: QueryParamType.IdSet, + value: "+22BD8+1", + }, + }); + }); + + it("bindIdSet sorts and deduplicates ids", () => { + const queryBinder = new QueryBinder(); + queryBinder.bindIdSet("idSetValue", ["0x22bd9", "0x22bd8", "0x22bd8"]); + assert.deepEqual(queryBinder.serialize(), { + idSetValue: { + type: QueryParamType.IdSet, + value: "+22BD8+1", + }, + }); + }); + it("allows bulk binding", () => { assert.deepEqual(QueryBinder.from(undefined), new QueryBinder()); From 23c2a80e2ab8a9f897bd0f36a45eab0696070725 Mon Sep 17 00:00:00 2001 From: anmolshres98 Date: Wed, 22 Jul 2026 14:58:01 -0500 Subject: [PATCH 2/6] docs: shorten bindIdSet note --- core/common/src/ConcurrentQuery.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/core/common/src/ConcurrentQuery.ts b/core/common/src/ConcurrentQuery.ts index c5aca5c4aff..1da42b6768f 100644 --- a/core/common/src/ConcurrentQuery.ts +++ b/core/common/src/ConcurrentQuery.ts @@ -480,9 +480,7 @@ export class QueryBinder { * @param indexOrName Specify parameter index or its name used in ECSQL statement. * @param val @type OrderedId64Iterable value to bind to ECSQL statement. * @returns @type QueryBinder to allow fluent interface. - * @note Entries that are not valid [Id64String]($bentley)s - including `undefined` and `null` - are - * ignored, matching the behavior of [ECSqlStatement.bindIdSet]($backend). A warning is logged when - * any entry is skipped. + * @note Entries that are not valid [Id64String]($bentley)s are skipped with a logged warning. */ public bindIdSet(indexOrName: string | number, val: OrderedId64Iterable) { this.verify(indexOrName); From 08142311d3233cdb80985199b7b572540c7c5bd1 Mon Sep 17 00:00:00 2001 From: anmolshres98 Date: Wed, 22 Jul 2026 15:31:08 -0500 Subject: [PATCH 3/6] fix: address review comments - use CommonLoggerCategory, avoid extra array copy --- common/api/core-common.api.md | 1 + core/common/src/CommonLoggerCategory.ts | 2 ++ core/common/src/ConcurrentQuery.ts | 8 ++++---- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/common/api/core-common.api.md b/common/api/core-common.api.md index 970b2b71672..b8dbbfaa0f2 100644 --- a/common/api/core-common.api.md +++ b/common/api/core-common.api.md @@ -1740,6 +1740,7 @@ export enum CommonLoggerCategory { Annotations = "core-common.Annotations", ElementProps = "core-common.ElementProps", Geometry = "core-common.Geometry", + QueryBinder = "core-common.QueryBinder", RpcInterfaceBackend = "core-backend.RpcInterface", RpcInterfaceFrontend = "core-frontend.RpcInterface" } diff --git a/core/common/src/CommonLoggerCategory.ts b/core/common/src/CommonLoggerCategory.ts index 4b57d798d4a..d488dfffe85 100644 --- a/core/common/src/CommonLoggerCategory.ts +++ b/core/common/src/CommonLoggerCategory.ts @@ -18,6 +18,8 @@ export enum CommonLoggerCategory { ElementProps = "core-common.ElementProps", /** The logger category used by common classes relating to Geometry. */ Geometry = "core-common.Geometry", + /** The logger category used by [QueryBinder]($common). */ + QueryBinder = "core-common.QueryBinder", /** The logger category used by the portions of the RpcInterface framework that run on the backend. */ RpcInterfaceBackend = "core-backend.RpcInterface", /** The logger category used by the portions of the RpcInterface framework that run on the frontend. */ diff --git a/core/common/src/ConcurrentQuery.ts b/core/common/src/ConcurrentQuery.ts index 1da42b6768f..4cb8101abba 100644 --- a/core/common/src/ConcurrentQuery.ts +++ b/core/common/src/ConcurrentQuery.ts @@ -8,8 +8,7 @@ import { BentleyError, CompressedId64Set, DbResult, Id64, Id64String, Logger, OrderedId64Iterable } from "@itwin/core-bentley"; import { LowAndHighXYZ, Point2d, Point3d, Range3d } from "@itwin/core-geometry"; import { Base64 } from "js-base64"; - -const loggerCategory = "core-common.QueryBinder"; +import { CommonLoggerCategory } from "./CommonLoggerCategory"; /** * Specifies the format of the rows returned by the `query` and `restartQuery` methods of @@ -495,12 +494,13 @@ export class QueryBinder { } if (skipped > 0) - Logger.logWarning(loggerCategory, `QueryBinder.bindIdSet: skipped ${skipped} entries that are not valid Id64Strings`); + Logger.logWarning(CommonLoggerCategory.QueryBinder, `QueryBinder.bindIdSet: skipped ${skipped} entries that are not valid Id64Strings`); + OrderedId64Iterable.sortArray(ids); Object.defineProperty(this._args, name, { enumerable: true, value: { type: QueryParamType.IdSet, - value: CompressedId64Set.sortAndCompress(ids), + value: CompressedId64Set.compressArray(ids), }, }); return this; From c8ed2b91ffc1f9cbc09d4260eea59e5cb02528f9 Mon Sep 17 00:00:00 2001 From: anmolshres98 Date: Wed, 22 Jul 2026 15:50:49 -0500 Subject: [PATCH 4/6] fix: treat single Id64String as one id in bindIdSet instead of iterating characters --- core/common/src/ConcurrentQuery.ts | 4 +++- core/common/src/test/ConcurrentQuery.test.ts | 11 +++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/core/common/src/ConcurrentQuery.ts b/core/common/src/ConcurrentQuery.ts index 4cb8101abba..f5b2a769ccc 100644 --- a/core/common/src/ConcurrentQuery.ts +++ b/core/common/src/ConcurrentQuery.ts @@ -486,7 +486,9 @@ export class QueryBinder { const name = String(indexOrName); const ids: Id64String[] = []; let skipped = 0; - for (const id of val) { + // `string` is an Iterable. In that case assume caller passed a single Id64String, matching CompressedId64Set.sortAndCompress. + const iterable = typeof val === "string" ? [val] : val; + for (const id of iterable) { if (typeof id === "string" && Id64.isValidId64(id)) ids.push(id); else diff --git a/core/common/src/test/ConcurrentQuery.test.ts b/core/common/src/test/ConcurrentQuery.test.ts index 6bebad607a5..a5d420e5d43 100644 --- a/core/common/src/test/ConcurrentQuery.test.ts +++ b/core/common/src/test/ConcurrentQuery.test.ts @@ -133,6 +133,17 @@ describe("QueryBinder", () => { }); }); + it("bindIdSet treats a single Id64String as a single id, not a string of characters", () => { + const queryBinder = new QueryBinder(); + queryBinder.bindIdSet("idSetValue", "0x22bd8"); + assert.deepEqual(queryBinder.serialize(), { + idSetValue: { + type: QueryParamType.IdSet, + value: "+22BD8", + }, + }); + }); + it("allows bulk binding", () => { assert.deepEqual(QueryBinder.from(undefined), new QueryBinder()); From f7b8a77e1f1f7b6d9b33a0be3366d74a735ce580 Mon Sep 17 00:00:00 2001 From: anmolshres98 Date: Fri, 24 Jul 2026 12:44:01 -0500 Subject: [PATCH 5/6] fix(core-common): throw ITwinError from QueryBinder.bindIdSet on invalid ids --- ...-9530-bindidset-null_2026-07-22-19-25.json | 2 +- core/common/src/ConcurrentQuery.ts | 20 +++--- core/common/src/test/ConcurrentQuery.test.ts | 63 ++++++++++++++++++- docs/changehistory/NextVersion.md | 8 +++ 4 files changed, 78 insertions(+), 15 deletions(-) diff --git a/common/changes/@itwin/core-common/issue-9530-bindidset-null_2026-07-22-19-25.json b/common/changes/@itwin/core-common/issue-9530-bindidset-null_2026-07-22-19-25.json index 89a63630621..18d3cd788b8 100644 --- a/common/changes/@itwin/core-common/issue-9530-bindidset-null_2026-07-22-19-25.json +++ b/common/changes/@itwin/core-common/issue-9530-bindidset-null_2026-07-22-19-25.json @@ -2,7 +2,7 @@ "changes": [ { "packageName": "@itwin/core-common", - "comment": "Fixed QueryBinder.bindIdSet throwing on entries that are not valid Id64Strings; such entries are now skipped with a warning, matching ECSqlStatement.bindIdSet.", + "comment": "QueryBinder.bindIdSet now throws a descriptive ITwinError (scope \"itwin-QueryBinder\", key \"invalid-arguments\") when an entry is not a valid Id64String, instead of an uncaught TypeError.", "type": "none" } ], diff --git a/core/common/src/ConcurrentQuery.ts b/core/common/src/ConcurrentQuery.ts index f5b2a769ccc..165e86915c1 100644 --- a/core/common/src/ConcurrentQuery.ts +++ b/core/common/src/ConcurrentQuery.ts @@ -5,10 +5,9 @@ /** @packageDocumentation * @module iModels */ -import { BentleyError, CompressedId64Set, DbResult, Id64, Id64String, Logger, OrderedId64Iterable } from "@itwin/core-bentley"; +import { BentleyError, CompressedId64Set, DbResult, Id64, Id64String, ITwinError, OrderedId64Iterable } from "@itwin/core-bentley"; import { LowAndHighXYZ, Point2d, Point3d, Range3d } from "@itwin/core-geometry"; import { Base64 } from "js-base64"; -import { CommonLoggerCategory } from "./CommonLoggerCategory"; /** * Specifies the format of the rows returned by the `query` and `restartQuery` methods of @@ -479,25 +478,24 @@ export class QueryBinder { * @param indexOrName Specify parameter index or its name used in ECSQL statement. * @param val @type OrderedId64Iterable value to bind to ECSQL statement. * @returns @type QueryBinder to allow fluent interface. - * @note Entries that are not valid [Id64String]($bentley)s are skipped with a logged warning. + * @throws an [[ITwinError]] with scope `"itwin-QueryBinder"` and key `"invalid-arguments"` if any entry is not a valid [Id64String]($bentley). */ public bindIdSet(indexOrName: string | number, val: OrderedId64Iterable) { this.verify(indexOrName); const name = String(indexOrName); const ids: Id64String[] = []; - let skipped = 0; // `string` is an Iterable. In that case assume caller passed a single Id64String, matching CompressedId64Set.sortAndCompress. const iterable = typeof val === "string" ? [val] : val; for (const id of iterable) { - if (typeof id === "string" && Id64.isValidId64(id)) - ids.push(id); - else - skipped++; + if (typeof id !== "string" || !Id64.isValidId64(id)) { + ITwinError.throwError({ + message: `QueryBinder.bindIdSet: "${id}" is not a valid Id64String`, + iTwinErrorId: { scope: "itwin-QueryBinder", key: "invalid-arguments" }, + }); + } + ids.push(id); } - if (skipped > 0) - Logger.logWarning(CommonLoggerCategory.QueryBinder, `QueryBinder.bindIdSet: skipped ${skipped} entries that are not valid Id64Strings`); - OrderedId64Iterable.sortArray(ids); Object.defineProperty(this._args, name, { enumerable: true, value: { diff --git a/core/common/src/test/ConcurrentQuery.test.ts b/core/common/src/test/ConcurrentQuery.test.ts index a5d420e5d43..168c7353ca5 100644 --- a/core/common/src/test/ConcurrentQuery.test.ts +++ b/core/common/src/test/ConcurrentQuery.test.ts @@ -7,7 +7,7 @@ import { Point2d, Point3d, Range3d } from "@itwin/core-geometry"; import { assert, describe, it } from "vitest"; import { Base64 } from "js-base64"; import { QueryBinder, QueryParamType } from "../ConcurrentQuery"; -import { Id64String } from "@itwin/core-bentley"; +import { Id64String, ITwinError } from "@itwin/core-bentley"; describe("QueryBinder", () => { it("binds values", async () => { @@ -111,9 +111,66 @@ describe("QueryBinder", () => { ); }); - it("bindIdSet skips entries that are not valid Id64Strings", () => { + describe("bindIdSet invalid entries", () => { + const invalidIds = [undefined, null, "0", "50", "", "not an id", 123, {}, ["0x1"]]; + const cases = invalidIds.flatMap((invalidId) => [ + { label: `${JSON.stringify(invalidId)} as the first entry`, ids: [invalidId, "0x22bd8"] }, + { label: `${JSON.stringify(invalidId)} as the last entry`, ids: ["0x22bd8", invalidId] }, + { label: `${JSON.stringify(invalidId)} as the only entry`, ids: [invalidId] }, + ]); + + for (const { label, ids } of cases) { + it(`throws an ITwinError with ${label}`, () => { + const queryBinder = new QueryBinder(); + let thrown: unknown; + try { + queryBinder.bindIdSet("idSetValue", ids as unknown as Id64String[]); + } catch (error) { + thrown = error; + } + assert.isDefined(thrown, "expected bindIdSet to throw"); + assert.isTrue(ITwinError.isError(thrown, "itwin-QueryBinder", "invalid-arguments"), 'expected an ITwinError with scope "itwin-QueryBinder" and key "invalid-arguments"'); + }); + } + + it("throws when a single invalid Id64String (not wrapped in an array) is passed directly", () => { + const queryBinder = new QueryBinder(); + assert.throws(() => queryBinder.bindIdSet("idSetValue", "not an id")); + }); + + it("does not bind a value when it throws", () => { + const queryBinder = new QueryBinder(); + assert.throws(() => queryBinder.bindIdSet("idSetValue", ["0x22bd8", undefined] as unknown as Id64String[])); + assert.deepEqual(queryBinder.serialize(), {}); + }); + + it("includes the offending value in the error message", () => { + const queryBinder = new QueryBinder(); + try { + queryBinder.bindIdSet("idSetValue", ["0x22bd8", "not an id"]); + assert.fail("expected bindIdSet to throw"); + } catch (error) { + if (!ITwinError.isError(error, "itwin-QueryBinder", "invalid-arguments")) + throw error; + assert.include(error.message, "not an id"); + } + }); + }); + + it("bindIdSet accepts an empty iterable", () => { + const queryBinder = new QueryBinder(); + queryBinder.bindIdSet("idSetValue", []); + assert.deepEqual(queryBinder.serialize(), { + idSetValue: { + type: QueryParamType.IdSet, + value: "", + }, + }); + }); + + it("bindIdSet accepts any Iterable, not just arrays", () => { const queryBinder = new QueryBinder(); - queryBinder.bindIdSet("idSetValue", ["0x22bd8", undefined, null, "0", "50", "0x22bd9"] as unknown as Id64String[]); + queryBinder.bindIdSet("idSetValue", new Set(["0x22bd9", "0x22bd8"])); assert.deepEqual(queryBinder.serialize(), { idSetValue: { type: QueryParamType.IdSet, diff --git a/docs/changehistory/NextVersion.md b/docs/changehistory/NextVersion.md index c2cb663757d..b969df1790f 100644 --- a/docs/changehistory/NextVersion.md +++ b/docs/changehistory/NextVersion.md @@ -11,6 +11,8 @@ publish: false - [Backend-to-frontend IPC invoke](#backend-to-frontend-ipc-invoke) - [@itwin/core-backend](#itwincore-backend) - [ChangesetReader.setBatchSize](#changesetreadersetbatchsize) + - [@itwin/core-common](#itwincore-common) + - [QueryBinder.bindIdSet now throws on invalid ids](#querybinderbindidset-now-throws-on-invalid-ids) - [@itwin/core-geometry](#itwincore-geometry) - [Region Boolean enhancements](#region-boolean-enhancements) @@ -104,6 +106,12 @@ while (reader.step()) { /* ... */ } | SqliteBackedCache | 1,000 | 0.399 | 0.207 | 48.1% | | SqliteBackedCache | 10,000 | 3.342 | 1.981 | 40.7% | +## @itwin/core-common + +### QueryBinder.bindIdSet now throws on invalid ids + +[QueryBinder.bindIdSet]($common) previously threw an uncaught `TypeError` when given an entry that was not a valid [Id64String]($bentley) (for example `undefined` or `null`, which can occur when collecting ids from a nullable column via [ECSqlReader]($common)). It now throws a descriptive [ITwinError]($bentley) instead, identifiable via `ITwinError.isError(error, "itwin-QueryBinder", "invalid-arguments")`, so callers can catch and diagnose the invalid entry rather than encountering an opaque internal error. + ## @itwin/core-geometry ### Region Boolean enhancements From 690e01cf76ab842ca178143a05135518e3445fe6 Mon Sep 17 00:00:00 2001 From: anmolshres98 Date: Fri, 24 Jul 2026 13:22:52 -0500 Subject: [PATCH 6/6] copilot review comments update --- common/api/core-common.api.md | 1 - core/common/src/CommonLoggerCategory.ts | 2 -- core/common/src/ConcurrentQuery.ts | 2 +- core/common/src/test/ConcurrentQuery.test.ts | 18 ++++++++++++++++-- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/common/api/core-common.api.md b/common/api/core-common.api.md index b8dbbfaa0f2..970b2b71672 100644 --- a/common/api/core-common.api.md +++ b/common/api/core-common.api.md @@ -1740,7 +1740,6 @@ export enum CommonLoggerCategory { Annotations = "core-common.Annotations", ElementProps = "core-common.ElementProps", Geometry = "core-common.Geometry", - QueryBinder = "core-common.QueryBinder", RpcInterfaceBackend = "core-backend.RpcInterface", RpcInterfaceFrontend = "core-frontend.RpcInterface" } diff --git a/core/common/src/CommonLoggerCategory.ts b/core/common/src/CommonLoggerCategory.ts index d488dfffe85..4b57d798d4a 100644 --- a/core/common/src/CommonLoggerCategory.ts +++ b/core/common/src/CommonLoggerCategory.ts @@ -18,8 +18,6 @@ export enum CommonLoggerCategory { ElementProps = "core-common.ElementProps", /** The logger category used by common classes relating to Geometry. */ Geometry = "core-common.Geometry", - /** The logger category used by [QueryBinder]($common). */ - QueryBinder = "core-common.QueryBinder", /** The logger category used by the portions of the RpcInterface framework that run on the backend. */ RpcInterfaceBackend = "core-backend.RpcInterface", /** The logger category used by the portions of the RpcInterface framework that run on the frontend. */ diff --git a/core/common/src/ConcurrentQuery.ts b/core/common/src/ConcurrentQuery.ts index 165e86915c1..720895595a7 100644 --- a/core/common/src/ConcurrentQuery.ts +++ b/core/common/src/ConcurrentQuery.ts @@ -489,7 +489,7 @@ export class QueryBinder { for (const id of iterable) { if (typeof id !== "string" || !Id64.isValidId64(id)) { ITwinError.throwError({ - message: `QueryBinder.bindIdSet: "${id}" is not a valid Id64String`, + message: `QueryBinder.bindIdSet: entry ${JSON.stringify(id)} for parameter "${name}" is not a valid Id64String`, iTwinErrorId: { scope: "itwin-QueryBinder", key: "invalid-arguments" }, }); } diff --git a/core/common/src/test/ConcurrentQuery.test.ts b/core/common/src/test/ConcurrentQuery.test.ts index 168c7353ca5..79974cba9cf 100644 --- a/core/common/src/test/ConcurrentQuery.test.ts +++ b/core/common/src/test/ConcurrentQuery.test.ts @@ -144,7 +144,7 @@ describe("QueryBinder", () => { assert.deepEqual(queryBinder.serialize(), {}); }); - it("includes the offending value in the error message", () => { + it("includes the offending value and the parameter name in the error message", () => { const queryBinder = new QueryBinder(); try { queryBinder.bindIdSet("idSetValue", ["0x22bd8", "not an id"]); @@ -152,7 +152,21 @@ describe("QueryBinder", () => { } catch (error) { if (!ITwinError.isError(error, "itwin-QueryBinder", "invalid-arguments")) throw error; - assert.include(error.message, "not an id"); + assert.include(error.message, "\"not an id\""); + assert.include(error.message, "idSetValue"); + } + }); + + it("distinguishes a string entry from an array entry in the error message", () => { + const queryBinder = new QueryBinder(); + try { + queryBinder.bindIdSet("idSetValue", [["0x1"]] as unknown as Id64String[]); + assert.fail("expected bindIdSet to throw"); + } catch (error) { + if (!ITwinError.isError(error, "itwin-QueryBinder", "invalid-arguments")) + throw error; + // JSON.stringify(["0x1"]) preserves the brackets; naive string interpolation would collapse it to "0x1", indistinguishable from a valid id. + assert.include(error.message, "[\"0x1\"]"); } }); });