@itwin/core-backend 5.15.0 → 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.
- package/CHANGELOG.md +8 -1
- package/lib/cjs/ConcurrentQuery.d.ts +4 -0
- package/lib/cjs/ConcurrentQuery.d.ts.map +1 -1
- package/lib/cjs/ConcurrentQuery.js +28 -1
- package/lib/cjs/ConcurrentQuery.js.map +1 -1
- package/lib/cjs/ECDb.d.ts.map +1 -1
- package/lib/cjs/ECDb.js +5 -10
- package/lib/cjs/ECDb.js.map +1 -1
- package/lib/cjs/ECSqlRowExecutor.d.ts +39 -55
- package/lib/cjs/ECSqlRowExecutor.d.ts.map +1 -1
- package/lib/cjs/ECSqlRowExecutor.js +118 -140
- package/lib/cjs/ECSqlRowExecutor.js.map +1 -1
- package/lib/cjs/ECSqlSyncReader.d.ts +21 -8
- package/lib/cjs/ECSqlSyncReader.d.ts.map +1 -1
- package/lib/cjs/ECSqlSyncReader.js +67 -25
- package/lib/cjs/ECSqlSyncReader.js.map +1 -1
- package/lib/cjs/IModelDb.d.ts.map +1 -1
- package/lib/cjs/IModelDb.js +5 -10
- package/lib/cjs/IModelDb.js.map +1 -1
- package/lib/cjs/IModelHost.d.ts +7 -0
- package/lib/cjs/IModelHost.d.ts.map +1 -1
- package/lib/cjs/IModelHost.js +1 -0
- package/lib/cjs/IModelHost.js.map +1 -1
- package/lib/cjs/SQLiteDb.d.ts +52 -5
- package/lib/cjs/SQLiteDb.d.ts.map +1 -1
- package/lib/cjs/SQLiteDb.js +40 -5
- package/lib/cjs/SQLiteDb.js.map +1 -1
- package/lib/cjs/TxnManager.d.ts.map +1 -1
- package/lib/cjs/TxnManager.js +11 -6
- package/lib/cjs/TxnManager.js.map +1 -1
- package/lib/cjs/internal/ChangesetConflictArgs.d.ts +4 -0
- package/lib/cjs/internal/ChangesetConflictArgs.d.ts.map +1 -1
- package/lib/cjs/internal/ChangesetConflictArgs.js +1 -0
- package/lib/cjs/internal/ChangesetConflictArgs.js.map +1 -1
- package/lib/cjs/internal/QueryMetadataCache.d.ts +8 -0
- package/lib/cjs/internal/QueryMetadataCache.d.ts.map +1 -1
- package/lib/cjs/internal/QueryMetadataCache.js +23 -0
- package/lib/cjs/internal/QueryMetadataCache.js.map +1 -1
- package/lib/esm/ConcurrentQuery.d.ts +4 -0
- package/lib/esm/ConcurrentQuery.d.ts.map +1 -1
- package/lib/esm/ConcurrentQuery.js +29 -2
- package/lib/esm/ConcurrentQuery.js.map +1 -1
- package/lib/esm/ECDb.d.ts.map +1 -1
- package/lib/esm/ECDb.js +6 -11
- package/lib/esm/ECDb.js.map +1 -1
- package/lib/esm/ECSqlRowExecutor.d.ts +39 -55
- package/lib/esm/ECSqlRowExecutor.d.ts.map +1 -1
- package/lib/esm/ECSqlRowExecutor.js +119 -140
- package/lib/esm/ECSqlRowExecutor.js.map +1 -1
- package/lib/esm/ECSqlSyncReader.d.ts +21 -8
- package/lib/esm/ECSqlSyncReader.d.ts.map +1 -1
- package/lib/esm/ECSqlSyncReader.js +68 -26
- package/lib/esm/ECSqlSyncReader.js.map +1 -1
- package/lib/esm/IModelDb.d.ts.map +1 -1
- package/lib/esm/IModelDb.js +6 -11
- package/lib/esm/IModelDb.js.map +1 -1
- package/lib/esm/IModelHost.d.ts +7 -0
- package/lib/esm/IModelHost.d.ts.map +1 -1
- package/lib/esm/IModelHost.js +1 -0
- package/lib/esm/IModelHost.js.map +1 -1
- package/lib/esm/SQLiteDb.d.ts +52 -5
- package/lib/esm/SQLiteDb.d.ts.map +1 -1
- package/lib/esm/SQLiteDb.js +40 -5
- package/lib/esm/SQLiteDb.js.map +1 -1
- package/lib/esm/TxnManager.d.ts.map +1 -1
- package/lib/esm/TxnManager.js +12 -7
- package/lib/esm/TxnManager.js.map +1 -1
- package/lib/esm/internal/ChangesetConflictArgs.d.ts +4 -0
- package/lib/esm/internal/ChangesetConflictArgs.d.ts.map +1 -1
- package/lib/esm/internal/ChangesetConflictArgs.js +1 -0
- package/lib/esm/internal/ChangesetConflictArgs.js.map +1 -1
- package/lib/esm/internal/QueryMetadataCache.d.ts +8 -0
- package/lib/esm/internal/QueryMetadataCache.d.ts.map +1 -1
- package/lib/esm/internal/QueryMetadataCache.js +23 -0
- package/lib/esm/internal/QueryMetadataCache.js.map +1 -1
- package/lib/esm/test/IModelHost.test.js +103 -2
- package/lib/esm/test/IModelHost.test.js.map +1 -1
- package/lib/esm/test/ecdb/CTE.test.js +21 -0
- package/lib/esm/test/ecdb/CTE.test.js.map +1 -1
- package/lib/esm/test/ecdb/ConcurrentQuery.test.js +72 -0
- package/lib/esm/test/ecdb/ConcurrentQuery.test.js.map +1 -1
- package/lib/esm/test/ecdb/ConcurrentQueryLoad.test.js +51 -9
- package/lib/esm/test/ecdb/ConcurrentQueryLoad.test.js.map +1 -1
- package/lib/esm/test/ecdb/ECSqlQuery.test.js +2 -1
- package/lib/esm/test/ecdb/ECSqlQuery.test.js.map +1 -1
- package/lib/esm/test/ecdb/ECSqlSyncReader.test.js +338 -84
- package/lib/esm/test/ecdb/ECSqlSyncReader.test.js.map +1 -1
- package/lib/esm/test/ecdb/QueryMetadataCache.test.js +40 -4
- package/lib/esm/test/ecdb/QueryMetadataCache.test.js.map +1 -1
- package/lib/esm/test/ecdb/QueryReaders.test.js +14 -7
- package/lib/esm/test/ecdb/QueryReaders.test.js.map +1 -1
- package/lib/esm/test/standalone/ChangeMerge.test.js +71 -1
- package/lib/esm/test/standalone/ChangeMerge.test.js.map +1 -1
- package/lib/esm/test/standalone/IModelWrite.test.js +6 -2
- package/lib/esm/test/standalone/IModelWrite.test.js.map +1 -1
- package/lib/esm/test/standalone/MergeConflict.test.js +15 -0
- package/lib/esm/test/standalone/MergeConflict.test.js.map +1 -1
- package/lib/esm/test/standalone/SQLiteDb.test.d.ts.map +1 -1
- package/lib/esm/test/standalone/SQLiteDb.test.js +422 -1
- package/lib/esm/test/standalone/SQLiteDb.test.js.map +1 -1
- 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 {
|
|
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
|
-
|
|
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
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
|
472
|
+
const env_8 = { stack: [], error: void 0, hasError: false };
|
|
241
473
|
try {
|
|
242
|
-
const ecdb = __addDisposableResource(
|
|
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
|
|
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(
|
|
484
|
+
expect(new Set(readers.statementIds).size).to.equal(1, "ECDb.withQueryReader should also reuse a cached prepared statement");
|
|
253
485
|
}
|
|
254
|
-
catch (
|
|
255
|
-
|
|
256
|
-
|
|
486
|
+
catch (e_8) {
|
|
487
|
+
env_8.error = e_8;
|
|
488
|
+
env_8.hasError = true;
|
|
257
489
|
}
|
|
258
490
|
finally {
|
|
259
|
-
__disposeResources(
|
|
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
|
-
//
|
|
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
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
|
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(
|
|
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 (
|
|
392
|
-
|
|
393
|
-
|
|
645
|
+
catch (e_10) {
|
|
646
|
+
env_10.error = e_10;
|
|
647
|
+
env_10.hasError = true;
|
|
394
648
|
}
|
|
395
649
|
finally {
|
|
396
|
-
__disposeResources(
|
|
650
|
+
__disposeResources(env_10);
|
|
397
651
|
}
|
|
398
652
|
});
|
|
399
653
|
});
|