@itwin/core-backend 5.15.1 → 5.16.0-dev.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/CHANGELOG.md +1 -1
  2. package/lib/cjs/ConcurrentQuery.d.ts +4 -0
  3. package/lib/cjs/ConcurrentQuery.d.ts.map +1 -1
  4. package/lib/cjs/ConcurrentQuery.js +28 -1
  5. package/lib/cjs/ConcurrentQuery.js.map +1 -1
  6. package/lib/cjs/ECDb.d.ts.map +1 -1
  7. package/lib/cjs/ECDb.js +5 -10
  8. package/lib/cjs/ECDb.js.map +1 -1
  9. package/lib/cjs/ECSqlRowExecutor.d.ts +39 -55
  10. package/lib/cjs/ECSqlRowExecutor.d.ts.map +1 -1
  11. package/lib/cjs/ECSqlRowExecutor.js +118 -140
  12. package/lib/cjs/ECSqlRowExecutor.js.map +1 -1
  13. package/lib/cjs/ECSqlSyncReader.d.ts +21 -8
  14. package/lib/cjs/ECSqlSyncReader.d.ts.map +1 -1
  15. package/lib/cjs/ECSqlSyncReader.js +67 -25
  16. package/lib/cjs/ECSqlSyncReader.js.map +1 -1
  17. package/lib/cjs/IModelDb.d.ts.map +1 -1
  18. package/lib/cjs/IModelDb.js +5 -10
  19. package/lib/cjs/IModelDb.js.map +1 -1
  20. package/lib/cjs/IModelHost.d.ts +7 -0
  21. package/lib/cjs/IModelHost.d.ts.map +1 -1
  22. package/lib/cjs/IModelHost.js +1 -0
  23. package/lib/cjs/IModelHost.js.map +1 -1
  24. package/lib/cjs/TxnManager.d.ts.map +1 -1
  25. package/lib/cjs/TxnManager.js +11 -6
  26. package/lib/cjs/TxnManager.js.map +1 -1
  27. package/lib/cjs/internal/ChangesetConflictArgs.d.ts +4 -0
  28. package/lib/cjs/internal/ChangesetConflictArgs.d.ts.map +1 -1
  29. package/lib/cjs/internal/ChangesetConflictArgs.js +1 -0
  30. package/lib/cjs/internal/ChangesetConflictArgs.js.map +1 -1
  31. package/lib/cjs/internal/QueryMetadataCache.d.ts +8 -0
  32. package/lib/cjs/internal/QueryMetadataCache.d.ts.map +1 -1
  33. package/lib/cjs/internal/QueryMetadataCache.js +23 -0
  34. package/lib/cjs/internal/QueryMetadataCache.js.map +1 -1
  35. package/lib/esm/ConcurrentQuery.d.ts +4 -0
  36. package/lib/esm/ConcurrentQuery.d.ts.map +1 -1
  37. package/lib/esm/ConcurrentQuery.js +29 -2
  38. package/lib/esm/ConcurrentQuery.js.map +1 -1
  39. package/lib/esm/ECDb.d.ts.map +1 -1
  40. package/lib/esm/ECDb.js +6 -11
  41. package/lib/esm/ECDb.js.map +1 -1
  42. package/lib/esm/ECSqlRowExecutor.d.ts +39 -55
  43. package/lib/esm/ECSqlRowExecutor.d.ts.map +1 -1
  44. package/lib/esm/ECSqlRowExecutor.js +119 -140
  45. package/lib/esm/ECSqlRowExecutor.js.map +1 -1
  46. package/lib/esm/ECSqlSyncReader.d.ts +21 -8
  47. package/lib/esm/ECSqlSyncReader.d.ts.map +1 -1
  48. package/lib/esm/ECSqlSyncReader.js +68 -26
  49. package/lib/esm/ECSqlSyncReader.js.map +1 -1
  50. package/lib/esm/IModelDb.d.ts.map +1 -1
  51. package/lib/esm/IModelDb.js +6 -11
  52. package/lib/esm/IModelDb.js.map +1 -1
  53. package/lib/esm/IModelHost.d.ts +7 -0
  54. package/lib/esm/IModelHost.d.ts.map +1 -1
  55. package/lib/esm/IModelHost.js +1 -0
  56. package/lib/esm/IModelHost.js.map +1 -1
  57. package/lib/esm/TxnManager.d.ts.map +1 -1
  58. package/lib/esm/TxnManager.js +12 -7
  59. package/lib/esm/TxnManager.js.map +1 -1
  60. package/lib/esm/internal/ChangesetConflictArgs.d.ts +4 -0
  61. package/lib/esm/internal/ChangesetConflictArgs.d.ts.map +1 -1
  62. package/lib/esm/internal/ChangesetConflictArgs.js +1 -0
  63. package/lib/esm/internal/ChangesetConflictArgs.js.map +1 -1
  64. package/lib/esm/internal/QueryMetadataCache.d.ts +8 -0
  65. package/lib/esm/internal/QueryMetadataCache.d.ts.map +1 -1
  66. package/lib/esm/internal/QueryMetadataCache.js +23 -0
  67. package/lib/esm/internal/QueryMetadataCache.js.map +1 -1
  68. package/lib/esm/test/IModelHost.test.js +103 -2
  69. package/lib/esm/test/IModelHost.test.js.map +1 -1
  70. package/lib/esm/test/ecdb/CTE.test.js +21 -0
  71. package/lib/esm/test/ecdb/CTE.test.js.map +1 -1
  72. package/lib/esm/test/ecdb/ConcurrentQuery.test.js +72 -0
  73. package/lib/esm/test/ecdb/ConcurrentQuery.test.js.map +1 -1
  74. package/lib/esm/test/ecdb/ConcurrentQueryLoad.test.js +51 -9
  75. package/lib/esm/test/ecdb/ConcurrentQueryLoad.test.js.map +1 -1
  76. package/lib/esm/test/ecdb/ECSqlQuery.test.js +2 -1
  77. package/lib/esm/test/ecdb/ECSqlQuery.test.js.map +1 -1
  78. package/lib/esm/test/ecdb/ECSqlSyncReader.test.js +338 -84
  79. package/lib/esm/test/ecdb/ECSqlSyncReader.test.js.map +1 -1
  80. package/lib/esm/test/ecdb/QueryMetadataCache.test.js +40 -4
  81. package/lib/esm/test/ecdb/QueryMetadataCache.test.js.map +1 -1
  82. package/lib/esm/test/ecdb/QueryReaders.test.js +14 -7
  83. package/lib/esm/test/ecdb/QueryReaders.test.js.map +1 -1
  84. package/lib/esm/test/standalone/ChangeMerge.test.js +71 -1
  85. package/lib/esm/test/standalone/ChangeMerge.test.js.map +1 -1
  86. package/lib/esm/test/standalone/IModelWrite.test.js +6 -2
  87. package/lib/esm/test/standalone/IModelWrite.test.js.map +1 -1
  88. package/lib/esm/test/standalone/MergeConflict.test.js +15 -0
  89. package/lib/esm/test/standalone/MergeConflict.test.js.map +1 -1
  90. package/package.json +16 -16
@@ -60,11 +60,11 @@ import * as path from "node:path";
60
60
  import { QueryBinder, QueryOptionsBuilder, QueryRowFormat } from "@itwin/core-common";
61
61
  import { SnapshotDb } from "../../IModelDb";
62
62
  import { IModelTestUtils } from "../IModelTestUtils";
63
- import { Id64 } from "@itwin/core-bentley";
64
- import { ECSqlStatement } from "../../ECSqlStatement";
63
+ import { DbResult, Id64 } from "@itwin/core-bentley";
64
+ import { _nativeDb } from "../../internal/Symbols";
65
+ import { SyncReaderRowFlags } from "../../ECSqlRowExecutor";
65
66
  import { ECDbTestHelper } from "./ECDbTestHelper";
66
67
  import { KnownTestLocations } from "../KnownTestLocations";
67
- import { IModelNative } from "../../internal/NativePlatform";
68
68
  import { IModelJsFs } from "../../IModelJsFs";
69
69
  describe("WithQueryReaderTests", () => {
70
70
  let iModel;
@@ -90,9 +90,166 @@ describe("WithQueryReaderTests", () => {
90
90
  }
91
91
  assert.equal(loopCount, 10);
92
92
  iModel.clearCaches();
93
- reader.step(); // step should fail after clearCaches
93
+ // Rows already read ahead are still returned; stepping past them must fail.
94
+ while (reader.step())
95
+ assert.isDefined(reader.current[0]);
94
96
  })).to.throw("Step failed");
95
97
  });
98
+ function readCountedRows(db) {
99
+ const ecsql = "WITH RECURSIVE cnt(x) AS (SELECT 1 UNION ALL SELECT x + 1 FROM cnt WHERE x < ?) SELECT x FROM cnt";
100
+ // Read-only dbs read ahead in batches of 4 -> 16 -> 64 -> 128 rows, so these counts cover each boundary and its neighbours.
101
+ for (const rowCount of [1, 3, 4, 5, 19, 20, 21, 83, 84, 85, 211, 212, 213, 339, 340, 341, 1000]) {
102
+ const values = db.withQueryReader(ecsql, (reader) => {
103
+ const rows = [];
104
+ while (reader.step())
105
+ rows.push(reader.current[0]);
106
+ assert.isFalse(reader.step(), "stepping after the end keeps returning false");
107
+ return rows;
108
+ }, QueryBinder.from([rowCount]));
109
+ expect(values).to.deep.equal(Array.from({ length: rowCount }, (_, i) => i + 1), `rowCount=${rowCount}`);
110
+ }
111
+ }
112
+ it("returns every row in order across read-ahead batch boundaries", () => {
113
+ assert.isTrue(iModel.isReadonly);
114
+ readCountedRows(iModel);
115
+ });
116
+ it("returns every row in order from a writable db", () => {
117
+ const env_1 = { stack: [], error: void 0, hasError: false };
118
+ try {
119
+ const ecdb = __addDisposableResource(env_1, ECDbTestHelper.createECDb(KnownTestLocations.outputDir, "syncReaderWritableRowOrder.ecdb"), false);
120
+ readCountedRows(ecdb);
121
+ }
122
+ catch (e_1) {
123
+ env_1.error = e_1;
124
+ env_1.hasError = true;
125
+ }
126
+ finally {
127
+ __disposeResources(env_1);
128
+ }
129
+ });
130
+ function readMixedRows(db) {
131
+ // Infinity cannot be written as JSON, so those rows are rendered individually between JSON-rendered rows.
132
+ const ecsql = "WITH RECURSIVE cnt(x) AS (SELECT 1 UNION ALL SELECT x + 1 FROM cnt WHERE x < 100) SELECT x, CASE WHEN x % 2 = 0 THEN NULL ELSE x END, CASE WHEN x % 7 = 0 THEN 1e308 * 10 ELSE x * 2 END FROM cnt";
133
+ const rows = db.withQueryReader(ecsql, (reader) => {
134
+ const result = [];
135
+ while (reader.step())
136
+ result.push([reader.current[0], reader.current[1], reader.current[2]]);
137
+ return result;
138
+ });
139
+ expect(rows).to.deep.equal(Array.from({ length: 100 }, (_, i) => {
140
+ const x = i + 1;
141
+ return [x, x % 2 === 0 ? undefined : x, x % 7 === 0 ? Infinity : x * 2];
142
+ }));
143
+ }
144
+ it("returns null cells as undefined and non-finite values from read-ahead batches", () => {
145
+ readMixedRows(iModel);
146
+ });
147
+ it("returns null cells as undefined and non-finite values from a writable db", () => {
148
+ const env_2 = { stack: [], error: void 0, hasError: false };
149
+ try {
150
+ const ecdb = __addDisposableResource(env_2, ECDbTestHelper.createECDb(KnownTestLocations.outputDir, "syncReaderMixedRows.ecdb"), false);
151
+ readMixedRows(ecdb);
152
+ }
153
+ catch (e_2) {
154
+ env_2.error = e_2;
155
+ env_2.hasError = true;
156
+ }
157
+ finally {
158
+ __disposeResources(env_2);
159
+ }
160
+ });
161
+ it("returns the same rows whether the native batch is rendered as JSON or cell by cell", () => {
162
+ const readNative = (ecsql, renderAsJson, options = {}) => {
163
+ let rowFlags = SyncReaderRowFlags.DoNotConvertClassIdsToClassNamesWhenAliased;
164
+ if (options.abbreviateBlobs)
165
+ rowFlags |= SyncReaderRowFlags.AbbreviateBlobs;
166
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
167
+ if (options.convertClassIdsToClassNames)
168
+ rowFlags |= SyncReaderRowFlags.ClassIdsToClassNames;
169
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
170
+ if (options.rowFormat === QueryRowFormat.UseJsPropertyNames)
171
+ rowFlags |= SyncReaderRowFlags.UseJsName;
172
+ const out = [];
173
+ const reader = iModel[_nativeDb].prepareECSqlBatchReader(ecsql, undefined, rowFlags, 0, out);
174
+ try {
175
+ expect(out[0]).to.be.undefined;
176
+ const rows = [];
177
+ for (;;) {
178
+ const batch = reader.stepBatch(16, renderAsJson);
179
+ if (typeof batch === "number") {
180
+ expect(batch).to.equal(DbResult.BE_SQLITE_DONE);
181
+ return rows;
182
+ }
183
+ if (renderAsJson)
184
+ expect(batch).to.be.a("string");
185
+ rows.push(...(typeof batch === "string" ? JSON.parse(batch) : batch));
186
+ }
187
+ }
188
+ finally {
189
+ reader.dispose();
190
+ }
191
+ };
192
+ for (const ecsql of [
193
+ "SELECT * FROM bis.Element",
194
+ "SELECT * FROM bis.GeometricElement3d",
195
+ "SELECT ECInstanceId,ECClassId,Model,Parent,Origin,BBoxLow,BBoxHigh FROM bis.GeometricElement3d ORDER BY ECInstanceId",
196
+ ]) {
197
+ for (const options of [
198
+ {},
199
+ { convertClassIdsToClassNames: true },
200
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
201
+ { rowFormat: QueryRowFormat.UseJsPropertyNames, convertClassIdsToClassNames: true },
202
+ { abbreviateBlobs: true },
203
+ {},
204
+ ]) {
205
+ const expected = readNative(ecsql, false, options);
206
+ assert.isAbove(expected.length, 0);
207
+ const actual = iModel.withQueryReader(ecsql, (reader) => {
208
+ const rows = [];
209
+ while (reader.step())
210
+ rows.push(reader.current.toArray());
211
+ return rows;
212
+ }, undefined, { ...options });
213
+ expect(actual).to.deep.equal(expected, ecsql);
214
+ }
215
+ }
216
+ const blobs = readNative("SELECT GeometryStream FROM bis.GeometricElement3d WHERE GeometryStream IS NOT NULL", false);
217
+ assert.isAbove(blobs.length, 0);
218
+ assert.isTrue(blobs.every((row) => row[0] instanceof Uint8Array));
219
+ });
220
+ it("preserves non-finite mapped double values when using the prepared renderer", () => {
221
+ const env_3 = { stack: [], error: void 0, hasError: false };
222
+ try {
223
+ const schema = `<ECSchema schemaName="SyncRendering" alias="sr" version="01.00.00" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2">
224
+ <ECEntityClass typeName="Sample"><ECProperty propertyName="Amount" typeName="double"/></ECEntityClass>
225
+ </ECSchema>`;
226
+ const ecdb = __addDisposableResource(env_3, ECDbTestHelper.createECDb(KnownTestLocations.outputDir, "syncReaderMappedDoubles.ecdb", schema), false);
227
+ for (const value of ["42.0", "1e308 * 10", "-1e308 * 10", "43.0"])
228
+ ecdb.withCachedWriteStatement(`INSERT INTO sr.Sample(Amount) VALUES(${value})`, (stmt) => stmt.stepForInsert());
229
+ ecdb.saveChanges();
230
+ const actual = ecdb.withQueryReader("SELECT Amount FROM sr.Sample ORDER BY ECInstanceId", (reader) => reader.toArray());
231
+ expect(actual).to.deep.equal([[42], [Infinity], [-Infinity], [43]]);
232
+ }
233
+ catch (e_3) {
234
+ env_3.error = e_3;
235
+ env_3.hasError = true;
236
+ }
237
+ finally {
238
+ __disposeResources(env_3);
239
+ }
240
+ });
241
+ it("restarts from the first row when a query is reused after an early exit", () => {
242
+ const ecsql = "WITH RECURSIVE cnt(x) AS (SELECT 1 UNION ALL SELECT x + 1 FROM cnt WHERE x < 500) SELECT x FROM cnt";
243
+ for (const take of [1, 4, 5, 30]) {
244
+ const values = iModel.withQueryReader(ecsql, (reader) => {
245
+ const rows = [];
246
+ while (rows.length < take && reader.step())
247
+ rows.push(reader.current[0]);
248
+ return rows;
249
+ });
250
+ expect(values).to.deep.equal(Array.from({ length: take }, (_, i) => i + 1));
251
+ }
252
+ });
96
253
  it("should throw error if we try to step on a closed iModelDb object", () => {
97
254
  const imodelPath = iModel.pathName;
98
255
  expect(() => iModel.withQueryReader("SELECT * FROM bis.Element", (reader) => {
@@ -178,91 +335,166 @@ describe("WithQueryReaderTests", () => {
178
335
  assert.equal(classId, "BisCore.DrawingCategory");
179
336
  }, undefined, config);
180
337
  });
338
+ /** Records the native readers leased by `withQueryReader` on `db`. N-API prototype methods are not configurable,
339
+ * so the native db and each reader are instrumented through own properties.
340
+ */
341
+ function instrumentSyncReaders(db) {
342
+ const nativeDb = db[_nativeDb];
343
+ const prepare = nativeDb.prepareECSqlBatchReader.bind(nativeDb);
344
+ const statementIds = [];
345
+ const getMetadataSpy = sinon.spy();
346
+ Object.defineProperty(nativeDb, "prepareECSqlBatchReader", {
347
+ configurable: true,
348
+ value: (ecsql, args, rowFlags, maxRows, out, logErrors) => {
349
+ const reader = prepare(ecsql, args, rowFlags, maxRows, out, logErrors);
350
+ statementIds.push(out[1]);
351
+ expect(out[1]).to.equal(reader.getStatementId());
352
+ const getMetadata = reader.getMetadata.bind(reader);
353
+ Object.defineProperty(reader, "getMetadata", {
354
+ configurable: true,
355
+ value: () => {
356
+ getMetadataSpy();
357
+ return getMetadata();
358
+ },
359
+ });
360
+ return reader;
361
+ },
362
+ });
363
+ return {
364
+ statementIds,
365
+ getMetadataSpy,
366
+ [Symbol.dispose]: () => { delete nativeDb.prepareECSqlBatchReader; },
367
+ };
368
+ }
181
369
  describe("statement caching (regression)", () => {
182
- // `withQueryReader` used to build a fresh `ECSqlStatement` and re-compile the ECSQL on every
183
- // call (via `ECSqlRowExecutor`), with no caching. Callers such as `IModelTransformer` invoke
184
- // per-element helpers (e.g. `hasSubModel`) once per element, so on large models this turned one
185
- // cached ECSQL prepare into N full native compiles - a significant performance regression. The
186
- // reader now reuses a prepared statement from the owning db's `_statementCache`.
370
+ // `withQueryReader` used to build a fresh statement and re-compile the ECSQL on every call. Callers such as
371
+ // `IModelTransformer` invoke per-element helpers (e.g. `hasSubModel`) once per element, so on large models this
372
+ // turned one cached ECSQL prepare into N full native compiles. The reader now leases a prepared statement from
373
+ // the native concurrent query statement cache of the primary connection.
187
374
  const sql = "SELECT ECInstanceId FROM bis.Element WHERE ECInstanceId=?";
188
375
  afterEach(() => sinon.restore());
189
376
  it("reuses a single prepared statement across repeated calls instead of re-preparing", () => {
190
- iModel.clearCaches();
191
- const prepareSpy = sinon.spy(ECSqlStatement.prototype, "prepare"); // eslint-disable-line @typescript-eslint/no-deprecated
192
- for (let i = 0; i < 20; i++) {
193
- iModel.withQueryReader(sql, (reader) => {
194
- reader.step();
195
- }, new QueryBinder().bindId(1, "0x1"));
377
+ const env_4 = { stack: [], error: void 0, hasError: false };
378
+ try {
379
+ iModel.clearCaches();
380
+ const readers = __addDisposableResource(env_4, instrumentSyncReaders(iModel), false);
381
+ for (let i = 0; i < 20; i++) {
382
+ iModel.withQueryReader(sql, (reader) => {
383
+ reader.step();
384
+ }, new QueryBinder().bindId(1, "0x1"));
385
+ }
386
+ expect(readers.statementIds).to.have.lengthOf(20);
387
+ expect(new Set(readers.statementIds).size).to.equal(1, "queries with identical ECSQL text should re-use one cached prepared statement");
388
+ }
389
+ catch (e_4) {
390
+ env_4.error = e_4;
391
+ env_4.hasError = true;
392
+ }
393
+ finally {
394
+ __disposeResources(env_4);
196
395
  }
197
- expect(prepareSpy.callCount).to.equal(1, "queries with identical ECSQL text should re-use one cached prepared statement");
198
396
  });
199
397
  it("keeps nested withQueryReader calls on the same SQL independent", () => {
200
- iModel.clearCaches();
201
- // A nested reader on the same SQL must get its own statement (findAndRemove hands the outer
202
- // reader's statement out of the cache), so results do not corrupt each other.
203
- const outerId = iModel.withQueryReader(sql, (outer) => {
204
- assert.isTrue(outer.step());
205
- const innerId = iModel.withQueryReader(sql, (inner) => {
206
- assert.isTrue(inner.step());
207
- return inner.current[0];
208
- }, new QueryBinder().bindId(1, "0xe"));
209
- expect(innerId).to.equal("0xe");
210
- // Outer reader's cursor must be unaffected by the nested reader.
211
- return outer.current[0];
212
- }, new QueryBinder().bindId(1, "0x1"));
213
- expect(outerId).to.equal("0x1");
398
+ const env_5 = { stack: [], error: void 0, hasError: false };
399
+ try {
400
+ iModel.clearCaches();
401
+ const readers = __addDisposableResource(env_5, instrumentSyncReaders(iModel), false);
402
+ // A nested reader on the same SQL must get its own statement while the outer one is leased,
403
+ // so results do not corrupt each other.
404
+ const outerId = iModel.withQueryReader(sql, (outer) => {
405
+ assert.isTrue(outer.step());
406
+ const innerId = iModel.withQueryReader(sql, (inner) => {
407
+ assert.isTrue(inner.step());
408
+ return inner.current[0];
409
+ }, new QueryBinder().bindId(1, "0xe"));
410
+ expect(innerId).to.equal("0xe");
411
+ // Outer reader's cursor must be unaffected by the nested reader.
412
+ return outer.current[0];
413
+ }, new QueryBinder().bindId(1, "0x1"));
414
+ expect(outerId).to.equal("0x1");
415
+ expect(readers.statementIds[0]).to.not.equal(readers.statementIds[1]);
416
+ }
417
+ catch (e_5) {
418
+ env_5.error = e_5;
419
+ env_5.hasError = true;
420
+ }
421
+ finally {
422
+ __disposeResources(env_5);
423
+ }
214
424
  });
215
425
  it("recovers from a prepare failure without poisoning the cache", () => {
216
- iModel.clearCaches();
217
- const invalidSql = "SELECT * FROM bis.ThisClassDoesNotExist";
218
- const prepareSpy = sinon.spy(ECSqlStatement.prototype, "prepare"); // eslint-disable-line @typescript-eslint/no-deprecated
219
- expect(() => iModel.withQueryReader(invalidSql, (reader) => reader.step())).to.throw();
220
- expect(() => iModel.withQueryReader(invalidSql, (reader) => reader.step())).to.throw();
221
- expect(prepareSpy.callCount).to.equal(2, "a statement that failed to prepare must not be cached");
222
- iModel.withQueryReader(sql, (reader) => {
223
- assert.isTrue(reader.step());
224
- expect(reader.current[0]).to.equal("0x1");
225
- }, new QueryBinder().bindId(1, "0x1"));
426
+ const env_6 = { stack: [], error: void 0, hasError: false };
427
+ try {
428
+ iModel.clearCaches();
429
+ const invalidSql = "SELECT * FROM bis.ThisClassDoesNotExist";
430
+ expect(() => iModel.withQueryReader(invalidSql, (reader) => reader.step())).to.throw();
431
+ expect(() => iModel.withQueryReader(invalidSql, (reader) => reader.step())).to.throw();
432
+ const readers = __addDisposableResource(env_6, instrumentSyncReaders(iModel), false);
433
+ for (let i = 0; i < 2; i++) {
434
+ iModel.withQueryReader(sql, (reader) => {
435
+ assert.isTrue(reader.step());
436
+ expect(reader.current[0]).to.equal("0x1");
437
+ }, new QueryBinder().bindId(1, "0x1"));
438
+ }
439
+ expect(new Set(readers.statementIds).size).to.equal(1);
440
+ }
441
+ catch (e_6) {
442
+ env_6.error = e_6;
443
+ env_6.hasError = true;
444
+ }
445
+ finally {
446
+ __disposeResources(env_6);
447
+ }
226
448
  });
227
449
  it("releases the executor and cached statement when binding fails during reader construction", () => {
228
- iModel.clearCaches();
229
- const prepareSpy = sinon.spy(ECSqlStatement.prototype, "prepare"); // eslint-disable-line @typescript-eslint/no-deprecated
230
- iModel.withQueryReader(sql, (reader) => reader.step(), new QueryBinder().bindId(1, "0x1"));
231
- const listenerCount = iModel.onBeforeClose.numberOfListeners;
232
- const bindStub = sinon.stub(ECSqlStatement.prototype, "bindParams").throws(new Error("Expected bind failure")); // eslint-disable-line @typescript-eslint/no-deprecated
233
- expect(() => iModel.withQueryReader(sql, () => undefined, new QueryBinder().bindId(1, "0x1"))).to.throw("Expected bind failure");
234
- bindStub.restore();
235
- expect(iModel.onBeforeClose.numberOfListeners).to.equal(listenerCount);
236
- iModel.withQueryReader(sql, (reader) => reader.step(), new QueryBinder().bindId(1, "0x1"));
237
- expect(prepareSpy.callCount).to.equal(1, "the statement checked out before the bind failure should be returned to the cache");
450
+ const env_7 = { stack: [], error: void 0, hasError: false };
451
+ try {
452
+ iModel.clearCaches();
453
+ iModel.withQueryReader(sql, (reader) => reader.step(), new QueryBinder().bindId(1, "0x1"));
454
+ const listenerCount = iModel.onBeforeClose.numberOfListeners;
455
+ const readers = __addDisposableResource(env_7, instrumentSyncReaders(iModel), false);
456
+ expect(() => iModel.withQueryReader(sql, () => undefined, new QueryBinder().bindId("missing", "0x1"))).to.throw("Failed to bind parameters");
457
+ expect(iModel.onBeforeClose.numberOfListeners).to.equal(listenerCount);
458
+ expect(readers.statementIds).to.have.lengthOf(0, "a reader is not created when binding fails");
459
+ iModel.withQueryReader(sql, (reader) => reader.step(), new QueryBinder().bindId(1, "0x1"));
460
+ iModel.withQueryReader(sql, (reader) => reader.step(), new QueryBinder().bindId(1, "0x1"));
461
+ expect(new Set(readers.statementIds).size).to.equal(1, "the statement leased before the bind failure should be returned to the cache");
462
+ }
463
+ catch (e_7) {
464
+ env_7.error = e_7;
465
+ env_7.hasError = true;
466
+ }
467
+ finally {
468
+ __disposeResources(env_7);
469
+ }
238
470
  });
239
471
  it("also caches for ECDb.withQueryReader (shared ECSqlRowExecutor path)", () => {
240
- const env_1 = { stack: [], error: void 0, hasError: false };
472
+ const env_8 = { stack: [], error: void 0, hasError: false };
241
473
  try {
242
- const ecdb = __addDisposableResource(env_1, ECDbTestHelper.createECDb(KnownTestLocations.outputDir, "syncReaderStmtCache.ecdb", `<ECSchema schemaName="Test" alias="ts" version="01.00.00" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2">
474
+ const ecdb = __addDisposableResource(env_8, ECDbTestHelper.createECDb(KnownTestLocations.outputDir, "syncReaderStmtCache.ecdb", `<ECSchema schemaName="Test" alias="ts" version="01.00.00" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2">
243
475
  <ECEntityClass typeName="Foo" modifier="Sealed">
244
476
  <ECProperty propertyName="n" typeName="int"/>
245
477
  </ECEntityClass>
246
478
  </ECSchema>`), false);
247
479
  ecdb.saveChanges();
248
480
  const ecdbSql = "SELECT ECInstanceId FROM meta.ECClassDef WHERE ECInstanceId=?";
249
- const prepareSpy = sinon.spy(ECSqlStatement.prototype, "prepare"); // eslint-disable-line @typescript-eslint/no-deprecated
481
+ const readers = __addDisposableResource(env_8, instrumentSyncReaders(ecdb), false);
250
482
  for (let i = 0; i < 10; i++)
251
483
  ecdb.withQueryReader(ecdbSql, (reader) => reader.step(), new QueryBinder().bindId(1, "0x1"));
252
- expect(prepareSpy.callCount).to.equal(1, "ECDb.withQueryReader should also reuse a cached prepared statement");
484
+ expect(new Set(readers.statementIds).size).to.equal(1, "ECDb.withQueryReader should also reuse a cached prepared statement");
253
485
  }
254
- catch (e_1) {
255
- env_1.error = e_1;
256
- env_1.hasError = true;
486
+ catch (e_8) {
487
+ env_8.error = e_8;
488
+ env_8.hasError = true;
257
489
  }
258
490
  finally {
259
- __disposeResources(env_1);
491
+ __disposeResources(env_8);
260
492
  }
261
493
  });
262
494
  });
263
495
  describe("metadata caching", () => {
264
496
  // Column metadata depends only on the prepared statement and the row-adaptor options, so it is cached
265
- // on the cached ECSqlStatement instead of being fetched from native code on every withQueryReader call.
497
+ // per native statement id instead of being fetched from native code on every withQueryReader call.
266
498
  const sql = "SELECT ECInstanceId, ECClassId, CodeValue, GeometryStream FROM bis.GeometricElement3d WHERE ECInstanceId=?";
267
499
  let elementId;
268
500
  before(() => {
@@ -309,26 +541,29 @@ describe("WithQueryReaderTests", () => {
309
541
  }
310
542
  });
311
543
  it("fetches native metadata once per statement and row options", () => {
312
- // Native prototype methods cannot be spied on directly, so count calls through a subclass.
313
- const getMetadataSpy = sinon.spy();
314
- const nativeStatement = IModelNative.platform.ECSqlStatement;
315
- sinon.replace(IModelNative.platform, "ECSqlStatement", class extends nativeStatement {
316
- getMetadata(...args) {
317
- getMetadataSpy();
318
- return super.getMetadata(...args);
319
- }
320
- });
321
- iModel.clearCaches();
322
- const options = { rowFormat: QueryRowFormat.UseECSqlPropertyNames };
323
- for (let i = 0; i < 10; i++)
544
+ const env_9 = { stack: [], error: void 0, hasError: false };
545
+ try {
546
+ const readers = __addDisposableResource(env_9, instrumentSyncReaders(iModel), false);
547
+ const getMetadataSpy = readers.getMetadataSpy;
548
+ iModel.clearCaches();
549
+ const options = { rowFormat: QueryRowFormat.UseECSqlPropertyNames };
550
+ for (let i = 0; i < 10; i++)
551
+ readMetadata({ ...options });
552
+ expect(getMetadataSpy.callCount).to.equal(1);
553
+ readMetadata({ ...options, abbreviateBlobs: true });
554
+ readMetadata({ ...options, abbreviateBlobs: true });
555
+ expect(getMetadataSpy.callCount).to.equal(2, "different row options need their own metadata");
556
+ iModel.clearCaches();
324
557
  readMetadata({ ...options });
325
- expect(getMetadataSpy.callCount).to.equal(1);
326
- readMetadata({ ...options, abbreviateBlobs: true });
327
- readMetadata({ ...options, abbreviateBlobs: true });
328
- expect(getMetadataSpy.callCount).to.equal(2, "different row options need their own metadata");
329
- iModel.clearCaches();
330
- readMetadata({ ...options });
331
- expect(getMetadataSpy.callCount).to.equal(3, "a re-prepared statement must not reuse cached metadata");
558
+ expect(getMetadataSpy.callCount).to.equal(3, "a re-prepared statement must not reuse cached metadata");
559
+ }
560
+ catch (e_9) {
561
+ env_9.error = e_9;
562
+ env_9.hasError = true;
563
+ }
564
+ finally {
565
+ __disposeResources(env_9);
566
+ }
332
567
  });
333
568
  it("does not share metadata objects between calls", () => {
334
569
  iModel.clearCaches();
@@ -347,6 +582,25 @@ describe("WithQueryReaderTests", () => {
347
582
  expect(meta).to.deep.equal(expected);
348
583
  expect(row.ECInstanceId).to.equal(elementId);
349
584
  });
585
+ it("isolates metadata exposed only through the row proxy, including nested readers", () => {
586
+ const options = { rowFormat: QueryRowFormat.UseECSqlPropertyNames };
587
+ const expected = structuredClone(readMetadata({ ...options }).meta);
588
+ iModel.withQueryReader(sql, (outer) => {
589
+ assert.isTrue(outer.step());
590
+ const outerMeta = outer.current.getMetaData();
591
+ iModel.withQueryReader(sql, (inner) => {
592
+ assert.isTrue(inner.step());
593
+ const innerMeta = inner.current.getMetaData();
594
+ expect(innerMeta).to.deep.equal(expected);
595
+ innerMeta[0].name = "Changed";
596
+ innerMeta.reverse();
597
+ }, new QueryBinder().bindId(1, elementId), { ...options });
598
+ expect(outerMeta).to.deep.equal(expected);
599
+ outerMeta[0].name = "OuterChanged";
600
+ outerMeta.push({ ...outerMeta[0], name: "Extra" });
601
+ }, new QueryBinder().bindId(1, elementId), { ...options });
602
+ expect(readMetadata({ ...options }).meta).to.deep.equal(expected);
603
+ });
350
604
  it("returns metadata for queries that produce no rows", () => {
351
605
  iModel.clearCaches();
352
606
  const options = { rowFormat: QueryRowFormat.UseECSqlPropertyNames };
@@ -363,7 +617,7 @@ describe("WithQueryReaderTests", () => {
363
617
  }
364
618
  });
365
619
  it("reflects a schema import between calls", () => {
366
- const env_2 = { stack: [], error: void 0, hasError: false };
620
+ const env_10 = { stack: [], error: void 0, hasError: false };
367
621
  try {
368
622
  const schemaXml = (version, extraProperty) => `<ECSchema schemaName="Test" alias="ts" version="${version}" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2">
369
623
  <ECEntityClass typeName="Foo" modifier="Sealed">
@@ -371,7 +625,7 @@ describe("WithQueryReaderTests", () => {
371
625
  ${extraProperty}
372
626
  </ECEntityClass>
373
627
  </ECSchema>`;
374
- const ecdb = __addDisposableResource(env_2, ECDbTestHelper.createECDb(KnownTestLocations.outputDir, "syncReaderMetadataCache.ecdb", schemaXml("01.00.00", "")), false);
628
+ const ecdb = __addDisposableResource(env_10, ECDbTestHelper.createECDb(KnownTestLocations.outputDir, "syncReaderMetadataCache.ecdb", schemaXml("01.00.00", "")), false);
375
629
  ecdb.withCachedWriteStatement("INSERT INTO ts.Foo(n) VALUES(1)", (stmt) => stmt.stepForInsert());
376
630
  ecdb.saveChanges();
377
631
  const fooSql = "SELECT * FROM ts.Foo";
@@ -388,12 +642,12 @@ describe("WithQueryReaderTests", () => {
388
642
  ecdb.saveChanges();
389
643
  expect(readNames()).to.deep.equal(["ECInstanceId", "ECClassId", "n", "m"]);
390
644
  }
391
- catch (e_2) {
392
- env_2.error = e_2;
393
- env_2.hasError = true;
645
+ catch (e_10) {
646
+ env_10.error = e_10;
647
+ env_10.hasError = true;
394
648
  }
395
649
  finally {
396
- __disposeResources(env_2);
650
+ __disposeResources(env_10);
397
651
  }
398
652
  });
399
653
  });