@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.
Files changed (101) hide show
  1. package/CHANGELOG.md +8 -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/SQLiteDb.d.ts +52 -5
  25. package/lib/cjs/SQLiteDb.d.ts.map +1 -1
  26. package/lib/cjs/SQLiteDb.js +40 -5
  27. package/lib/cjs/SQLiteDb.js.map +1 -1
  28. package/lib/cjs/TxnManager.d.ts.map +1 -1
  29. package/lib/cjs/TxnManager.js +11 -6
  30. package/lib/cjs/TxnManager.js.map +1 -1
  31. package/lib/cjs/internal/ChangesetConflictArgs.d.ts +4 -0
  32. package/lib/cjs/internal/ChangesetConflictArgs.d.ts.map +1 -1
  33. package/lib/cjs/internal/ChangesetConflictArgs.js +1 -0
  34. package/lib/cjs/internal/ChangesetConflictArgs.js.map +1 -1
  35. package/lib/cjs/internal/QueryMetadataCache.d.ts +8 -0
  36. package/lib/cjs/internal/QueryMetadataCache.d.ts.map +1 -1
  37. package/lib/cjs/internal/QueryMetadataCache.js +23 -0
  38. package/lib/cjs/internal/QueryMetadataCache.js.map +1 -1
  39. package/lib/esm/ConcurrentQuery.d.ts +4 -0
  40. package/lib/esm/ConcurrentQuery.d.ts.map +1 -1
  41. package/lib/esm/ConcurrentQuery.js +29 -2
  42. package/lib/esm/ConcurrentQuery.js.map +1 -1
  43. package/lib/esm/ECDb.d.ts.map +1 -1
  44. package/lib/esm/ECDb.js +6 -11
  45. package/lib/esm/ECDb.js.map +1 -1
  46. package/lib/esm/ECSqlRowExecutor.d.ts +39 -55
  47. package/lib/esm/ECSqlRowExecutor.d.ts.map +1 -1
  48. package/lib/esm/ECSqlRowExecutor.js +119 -140
  49. package/lib/esm/ECSqlRowExecutor.js.map +1 -1
  50. package/lib/esm/ECSqlSyncReader.d.ts +21 -8
  51. package/lib/esm/ECSqlSyncReader.d.ts.map +1 -1
  52. package/lib/esm/ECSqlSyncReader.js +68 -26
  53. package/lib/esm/ECSqlSyncReader.js.map +1 -1
  54. package/lib/esm/IModelDb.d.ts.map +1 -1
  55. package/lib/esm/IModelDb.js +6 -11
  56. package/lib/esm/IModelDb.js.map +1 -1
  57. package/lib/esm/IModelHost.d.ts +7 -0
  58. package/lib/esm/IModelHost.d.ts.map +1 -1
  59. package/lib/esm/IModelHost.js +1 -0
  60. package/lib/esm/IModelHost.js.map +1 -1
  61. package/lib/esm/SQLiteDb.d.ts +52 -5
  62. package/lib/esm/SQLiteDb.d.ts.map +1 -1
  63. package/lib/esm/SQLiteDb.js +40 -5
  64. package/lib/esm/SQLiteDb.js.map +1 -1
  65. package/lib/esm/TxnManager.d.ts.map +1 -1
  66. package/lib/esm/TxnManager.js +12 -7
  67. package/lib/esm/TxnManager.js.map +1 -1
  68. package/lib/esm/internal/ChangesetConflictArgs.d.ts +4 -0
  69. package/lib/esm/internal/ChangesetConflictArgs.d.ts.map +1 -1
  70. package/lib/esm/internal/ChangesetConflictArgs.js +1 -0
  71. package/lib/esm/internal/ChangesetConflictArgs.js.map +1 -1
  72. package/lib/esm/internal/QueryMetadataCache.d.ts +8 -0
  73. package/lib/esm/internal/QueryMetadataCache.d.ts.map +1 -1
  74. package/lib/esm/internal/QueryMetadataCache.js +23 -0
  75. package/lib/esm/internal/QueryMetadataCache.js.map +1 -1
  76. package/lib/esm/test/IModelHost.test.js +103 -2
  77. package/lib/esm/test/IModelHost.test.js.map +1 -1
  78. package/lib/esm/test/ecdb/CTE.test.js +21 -0
  79. package/lib/esm/test/ecdb/CTE.test.js.map +1 -1
  80. package/lib/esm/test/ecdb/ConcurrentQuery.test.js +72 -0
  81. package/lib/esm/test/ecdb/ConcurrentQuery.test.js.map +1 -1
  82. package/lib/esm/test/ecdb/ConcurrentQueryLoad.test.js +51 -9
  83. package/lib/esm/test/ecdb/ConcurrentQueryLoad.test.js.map +1 -1
  84. package/lib/esm/test/ecdb/ECSqlQuery.test.js +2 -1
  85. package/lib/esm/test/ecdb/ECSqlQuery.test.js.map +1 -1
  86. package/lib/esm/test/ecdb/ECSqlSyncReader.test.js +338 -84
  87. package/lib/esm/test/ecdb/ECSqlSyncReader.test.js.map +1 -1
  88. package/lib/esm/test/ecdb/QueryMetadataCache.test.js +40 -4
  89. package/lib/esm/test/ecdb/QueryMetadataCache.test.js.map +1 -1
  90. package/lib/esm/test/ecdb/QueryReaders.test.js +14 -7
  91. package/lib/esm/test/ecdb/QueryReaders.test.js.map +1 -1
  92. package/lib/esm/test/standalone/ChangeMerge.test.js +71 -1
  93. package/lib/esm/test/standalone/ChangeMerge.test.js.map +1 -1
  94. package/lib/esm/test/standalone/IModelWrite.test.js +6 -2
  95. package/lib/esm/test/standalone/IModelWrite.test.js.map +1 -1
  96. package/lib/esm/test/standalone/MergeConflict.test.js +15 -0
  97. package/lib/esm/test/standalone/MergeConflict.test.js.map +1 -1
  98. package/lib/esm/test/standalone/SQLiteDb.test.d.ts.map +1 -1
  99. package/lib/esm/test/standalone/SQLiteDb.test.js +422 -1
  100. package/lib/esm/test/standalone/SQLiteDb.test.js.map +1 -1
  101. package/package.json +16 -16
@@ -4,193 +4,171 @@
4
4
  * See LICENSE.md in the project root for license terms and full copyright notice.
5
5
  *--------------------------------------------------------------------------------------------*/
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.ECSqlRowExecutor = void 0;
8
- exports.releaseECSqlStatement = releaseECSqlStatement;
7
+ exports.ECSqlRowExecutor = exports.SyncReaderRowFlags = void 0;
9
8
  const core_common_1 = require("@itwin/core-common");
10
9
  const core_bentley_1 = require("@itwin/core-bentley");
11
10
  const Symbols_1 = require("./internal/Symbols");
12
11
  const statementNotPreparedMessage = "Statement is not prepared. Likely cause: the db was closed before step is called or the ECSqlSyncReader is used outside the context of the callback passed to withQueryReader.";
13
- function logStatementCleanupError(loggerCategory, message, error) {
14
- try {
15
- core_bentley_1.Logger.logTrace(loggerCategory, message, () => ({ error: core_bentley_1.BentleyError.getErrorMessage(error) }));
16
- }
17
- catch {
18
- // Cleanup diagnostics must not replace the original query error.
19
- }
20
- }
21
- /** Returns a statement to its cache, falling back to direct disposal without throwing.
12
+ /** Column metadata keyed by native statement id and row options. Statement ids change whenever the native cache
13
+ * prepares the ECSql again, so entries never outlive the statement they describe.
14
+ */
15
+ const maxCachedMetadataEntries = 100;
16
+ /** Rows fetched by the first native batch. Small so single-row and early-exit queries do little extra work. */
17
+ const initialBatchSize = 4;
18
+ /** Each subsequent batch grows by this factor up to [[maxBatchSize]]. */
19
+ const batchGrowthFactor = 4;
20
+ /** Upper bound on rows rendered per native call; bounds memory held by the read-ahead buffer. */
21
+ const maxBatchSize = 128;
22
+ /** Number of distinct row-flag combinations; metadata keys pack the statement id and the row flags into one number. */
23
+ const rowFlagCombinations = 16;
24
+ const metadataCache = new WeakMap();
25
+ /** Receives the first batch and statement id from `prepareECSqlBatchReader`. Read immediately after each call. */
26
+ const prepareOut = [];
27
+ /** Row rendering flags passed to `prepareECSqlBatchReader`.
22
28
  * @internal
23
29
  */
24
- // eslint-disable-next-line @typescript-eslint/no-deprecated
25
- function releaseECSqlStatement(stmt, cache, loggerCategory, canCache) {
26
- if (!canCache || !stmt.isPrepared) {
27
- try {
28
- stmt[Symbol.dispose]();
29
- }
30
- catch (error) {
31
- logStatementCleanupError(loggerCategory, "Failed to dispose an ECSQL statement that could not be cached.", error);
32
- }
33
- return;
30
+ var SyncReaderRowFlags;
31
+ (function (SyncReaderRowFlags) {
32
+ SyncReaderRowFlags[SyncReaderRowFlags["None"] = 0] = "None";
33
+ SyncReaderRowFlags[SyncReaderRowFlags["AbbreviateBlobs"] = 1] = "AbbreviateBlobs";
34
+ SyncReaderRowFlags[SyncReaderRowFlags["ClassIdsToClassNames"] = 2] = "ClassIdsToClassNames";
35
+ SyncReaderRowFlags[SyncReaderRowFlags["UseJsName"] = 4] = "UseJsName";
36
+ SyncReaderRowFlags[SyncReaderRowFlags["DoNotConvertClassIdsToClassNamesWhenAliased"] = 8] = "DoNotConvertClassIdsToClassNamesWhenAliased";
37
+ })(SyncReaderRowFlags || (exports.SyncReaderRowFlags = SyncReaderRowFlags = {}));
38
+ function getCachedMetadata(db, key, fetch) {
39
+ let entries = metadataCache.get(db);
40
+ if (!entries) {
41
+ entries = new Map();
42
+ metadataCache.set(db, entries);
34
43
  }
35
- try {
36
- cache.addOrDispose(stmt);
44
+ let properties = entries.get(key);
45
+ if (properties) {
46
+ entries.delete(key);
37
47
  }
38
- catch (error) {
39
- try {
40
- stmt[Symbol.dispose]();
41
- }
42
- catch (disposeError) {
43
- logStatementCleanupError(loggerCategory, "Failed to dispose an ECSQL statement after it could not be returned to the statement cache.", disposeError);
44
- }
45
- logStatementCleanupError(loggerCategory, "Failed to return an ECSQL statement to the statement cache; attempted direct disposal instead.", error);
48
+ else {
49
+ properties = fetch();
50
+ if (entries.size >= maxCachedMetadataEntries)
51
+ entries.delete(entries.keys().next().value);
46
52
  }
53
+ entries.set(key, properties);
54
+ return properties;
47
55
  }
48
56
  // --------------------------------------------------------------------------------------------
49
57
  // ECSqlRowExecutor
50
58
  // --------------------------------------------------------------------------------------------
51
59
  /**
52
- * Executes ECSql queries one row at a time against an IModelDb, maintaining statement state between
53
- * successive calls so the caller can page through results via offset-based requests.
60
+ * Executes an ECSql query on the db's primary connection and hands out rows one at a time. On a read-only db, rows are
61
+ * read from native code in growing batches to reduce JS/native transitions; on a writable db native code returns one row
62
+ * per call so rows are never read ahead of writes made while iterating. The prepared statement is leased from the native
63
+ * concurrent query statement cache and returned to it when the executor is disposed.
54
64
  * @internal
55
65
  */
56
66
  class ECSqlRowExecutor {
57
67
  _db;
58
- _stmt;
59
- _loggerCategory;
60
68
  _removeListener;
61
- _isDisposed = false;
62
- _canCacheStatement = true;
63
- /** Whether the statement completed preparation and can be returned to its cache.
64
- * @internal
65
- */
66
- get canCacheStatement() { return this._canCacheStatement; }
67
- // eslint-disable-next-line @typescript-eslint/no-deprecated
68
- constructor(_db, _stmt, _loggerCategory) {
69
+ _reader;
70
+ /** Key of this statement's metadata in `metadataCache`, or undefined when its metadata must not be cached. */
71
+ _metadataKey;
72
+ _rows = [];
73
+ _rowIndex = 0;
74
+ _batchSize = initialBatchSize;
75
+ /** Status of the last native batch. Anything other than BE_SQLITE_ROW means no further batch is fetched. */
76
+ _batchStatus = core_bentley_1.DbResult.BE_SQLITE_ROW;
77
+ constructor(_db) {
69
78
  this._db = _db;
70
- this._stmt = _stmt;
71
- this._loggerCategory = _loggerCategory;
72
- this._removeListener = this._db.onBeforeClose.addListener(() => this.cleanup());
79
+ this._removeListener = this._db.onBeforeClose.addListener(() => this.releaseReader());
73
80
  }
74
81
  // --------------------------------------------------------------------------------------------
75
82
  // Lifecycle
76
83
  // --------------------------------------------------------------------------------------------
77
- /** Disposes the currently held statement without returning it to the cache.
78
- * Invoked when the db signals it is closing (the statement cache is cleared on close, so the
79
- * checked-out statement must be disposed directly to avoid a use-after-free or double dispose).
80
- * @internal
81
- */
82
- cleanup() {
83
- this._isDisposed = true;
84
- try {
85
- this._stmt[Symbol.dispose]();
86
- }
87
- catch (error) {
88
- logStatementCleanupError(this._loggerCategory, "Failed to dispose an ECSQL statement while closing its database.", error);
89
- }
84
+ /** Returns the leased statement to the native cache. Safe to call more than once. */
85
+ releaseReader() {
86
+ const reader = this._reader;
87
+ this._reader = undefined;
88
+ this.resetBatch();
89
+ reader?.dispose();
90
+ }
91
+ resetBatch() {
92
+ this._rows = [];
93
+ this._rowIndex = 0;
94
+ this._batchSize = initialBatchSize;
95
+ this._batchStatus = core_bentley_1.DbResult.BE_SQLITE_ROW;
90
96
  }
91
- /** Removes the database-close listener owned by this row executor.
97
+ /** Returns the leased statement and removes the database-close listener owned by this row executor.
92
98
  * @internal
93
99
  */
94
100
  [Symbol.dispose]() {
95
- this._isDisposed = true;
96
101
  this._removeListener();
102
+ this.releaseReader();
97
103
  }
98
104
  // --------------------------------------------------------------------------------------------
99
105
  // Core execution
100
106
  // --------------------------------------------------------------------------------------------
101
- /** Prepare the statement and bind parameters in one step.
102
- * Call once during reader initialization — avoids the per-row `ensureStatementReady` check.
107
+ /** Prepare the statement and bind parameters in one step. On a read-only db the first batch of rows is read by the
108
+ * same native call.
103
109
  * @param query - The ECSql text to prepare.
104
- * @param args - Optional bind parameters.
110
+ * @param args - Optional bind parameters as flat `[key, type, value, ...]` triples.
111
+ * @param rowFlags - [[SyncReaderRowFlags]] used for every row and for metadata.
105
112
  * @throws IModelError on preparation or binding failure.
106
113
  * @internal
107
114
  */
108
- prepareAndBind(query, args) {
109
- const prepResult = this.prepareStmt(query);
110
- if (!prepResult.isSuccessful)
111
- throw new core_common_1.IModelError(core_bentley_1.DbResult.BE_SQLITE_ERROR, prepResult.message ?? `Failed to prepare statement: ${query}`);
112
- if (args) {
113
- const bindResult = this.bindValues(args);
114
- if (!bindResult.isSuccessful)
115
- throw new core_common_1.IModelError(core_bentley_1.DbResult.BE_SQLITE_ERROR, bindResult.message ?? `Failed to bind values: ${query}`);
115
+ prepareAndBind(query, args, rowFlags) {
116
+ this.releaseReader();
117
+ try {
118
+ this._reader = this._db[Symbols_1._nativeDb].prepareECSqlBatchReader(query, args, rowFlags, this._batchSize, prepareOut);
116
119
  }
120
+ catch (error) {
121
+ throw new core_common_1.IModelError(core_bentley_1.DbResult.BE_SQLITE_ERROR, core_bentley_1.BentleyError.getErrorMessage(error));
122
+ }
123
+ const firstBatch = prepareOut[0];
124
+ // Metadata of a dynamic column (e.g. $->prop) changes as the statement is stepped, so it is never cached.
125
+ this._metadataKey = prepareOut[2] ? undefined : prepareOut[1] * rowFlagCombinations + rowFlags;
126
+ prepareOut.length = 0;
127
+ if (firstBatch !== undefined)
128
+ this.acceptBatch(firstBatch);
117
129
  }
118
- /** Fast-path: step the cursor once and return row data directly.
119
- *
120
- * Returns the row data array if a row is available.
121
- * Returns `undefined` if the result set is exhausted (DONE).
122
- *
123
- * This avoids all intermediate object allocations (StepResult, RowDataResult,
124
- * DbRuntimeStats, DbQueryResponse) that the general `execute()` path creates per row.
125
- *
126
- * @param options - Native row-adaptor options (should be cached and reused across rows).
127
- * @throws IModelError on step failure or row extraction failure.
128
- * @internal
129
- */
130
- stepNextRow(options) {
131
- if (this._isDisposed || !this._stmt.isPrepared)
132
- throw new core_common_1.IModelError(core_bentley_1.DbResult.BE_SQLITE_ERROR, statementNotPreparedMessage);
133
- const stepResult = this._stmt.step();
134
- if (stepResult === core_bentley_1.DbResult.BE_SQLITE_ROW)
135
- return this._stmt.toRow(options).data;
136
- if (stepResult === core_bentley_1.DbResult.BE_SQLITE_DONE)
137
- return undefined;
138
- throw new core_common_1.IModelError(stepResult, `Step failed with code ${stepResult}`);
130
+ /** Store a batch returned by native code: a JSON array string, an array of rows, or the final DbResult. */
131
+ acceptBatch(batch) {
132
+ this._batchSize = Math.min(this._batchSize * batchGrowthFactor, maxBatchSize);
133
+ this._rowIndex = 0;
134
+ if (typeof batch === "number") {
135
+ this._rows = [];
136
+ this._batchStatus = batch;
137
+ }
138
+ else {
139
+ this._rows = typeof batch === "string" ? JSON.parse(batch) : batch;
140
+ }
139
141
  }
140
- /** Get column metadata directly from the prepared statement.
141
- * Call once after `prepareAndBind` — the metadata does not change between rows.
142
- * @param options - Native row-adaptor options that influence property naming.
143
- * @returns Column metadata owned by the caller.
142
+ /** Return the next row data array, or `undefined` when the result set is exhausted. Rows are served from the
143
+ * read-ahead buffer; a new native batch is fetched only when the buffer is empty.
144
+ * @throws IModelError on step failure or when the statement is no longer available. A failure reported by a batch is
145
+ * thrown after the rows stepped before it have been returned.
144
146
  * @internal
145
147
  */
146
- fetchMetadata(options) {
147
- if (this._isDisposed || !this._stmt.isPrepared)
148
+ stepNextRow() {
149
+ if (this._rowIndex < this._rows.length)
150
+ return this._rows[this._rowIndex++];
151
+ const reader = this._reader;
152
+ if (!reader)
148
153
  throw new core_common_1.IModelError(core_bentley_1.DbResult.BE_SQLITE_ERROR, statementNotPreparedMessage);
149
- return this._stmt.getMetadata(options);
150
- }
151
- // --------------------------------------------------------------------------------------------
152
- // Execution phases
153
- // --------------------------------------------------------------------------------------------
154
- /** Prepares the ECSql statement when the caller did not supply one from its cache.
155
- * @param ecsql - The ECSql text to prepare.
156
- * @returns An `OperationResult` indicating success or failure.
157
- * @internal
158
- */
159
- prepareStmt(ecsql) {
160
- if (this._stmt.isPrepared)
161
- return { isSuccessful: true };
162
- try {
163
- this._stmt.prepare(this._db[Symbols_1._nativeDb], ecsql);
164
- return { isSuccessful: true };
165
- }
166
- catch (error) {
167
- this._canCacheStatement = false;
168
- try {
169
- this._stmt[Symbol.dispose]();
170
- }
171
- catch (disposeError) {
172
- logStatementCleanupError(this._loggerCategory, "Failed to dispose an ECSQL statement after preparation failed.", disposeError);
173
- }
174
- return { isSuccessful: false, message: error.message };
154
+ if (this._batchStatus === core_bentley_1.DbResult.BE_SQLITE_ROW) {
155
+ this.acceptBatch(reader.stepBatch(this._batchSize));
156
+ if (this._rows.length > 0)
157
+ return this._rows[this._rowIndex++];
175
158
  }
159
+ if (this._batchStatus === core_bentley_1.DbResult.BE_SQLITE_DONE)
160
+ return undefined;
161
+ throw new core_common_1.IModelError(this._batchStatus, `Step failed with code ${this._batchStatus}`);
176
162
  }
177
- /** Binds the supplied parameter values to the prepared statement.
178
- * @param args - The parameter object to bind, or `undefined` when no parameters are needed.
179
- * @returns An `OperationResult` indicating success or failure.
163
+ /** Get shared internal column metadata. Readers must copy it before exposing mutable metadata to callers.
180
164
  * @internal
181
165
  */
182
- bindValues(args) {
183
- try {
184
- if (args === undefined)
185
- return { isSuccessful: true };
186
- if (!this._stmt.isPrepared)
187
- return { isSuccessful: false, message: statementNotPreparedMessage };
188
- this._stmt.bindParams(args);
189
- return { isSuccessful: true };
190
- }
191
- catch (error) {
192
- return { isSuccessful: false, message: error.message };
193
- }
166
+ fetchMetadata() {
167
+ const reader = this._reader;
168
+ if (!reader)
169
+ throw new core_common_1.IModelError(core_bentley_1.DbResult.BE_SQLITE_ERROR, statementNotPreparedMessage);
170
+ const fetch = () => new core_common_1.PropertyMetaDataMap(reader.getMetadata());
171
+ return this._metadataKey === undefined ? fetch() : getCachedMetadata(this._db, this._metadataKey, fetch);
194
172
  }
195
173
  }
196
174
  exports.ECSqlRowExecutor = ECSqlRowExecutor;
@@ -1 +1 @@
1
- {"version":3,"file":"ECSqlRowExecutor.js","sourceRoot":"","sources":["../../src/ECSqlRowExecutor.ts"],"names":[],"mappings":";AAAA;;;+FAG+F;;;AAyB/F,sDAoBC;AA3CD,oDAAsE;AAGtE,sDAAqE;AAErE,gDAA+C;AAI/C,MAAM,2BAA2B,GAAG,gLAAgL,CAAC;AAErN,SAAS,wBAAwB,CAAC,cAAsB,EAAE,OAAe,EAAE,KAAc;IACvF,IAAI,CAAC;QACH,qBAAM,CAAC,QAAQ,CAAC,cAAc,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,2BAAY,CAAC,eAAe,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC;IACnG,CAAC;IAAC,MAAM,CAAC;QACP,iEAAiE;IACnE,CAAC;AACH,CAAC;AAED;;GAEG;AACH,4DAA4D;AAC5D,SAAgB,qBAAqB,CAAC,IAAoB,EAAE,KAAqC,EAAE,cAAsB,EAAE,QAAiB;IAC1I,IAAI,CAAC,QAAQ,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC;QAClC,IAAI,CAAC;YACH,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;QACzB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,wBAAwB,CAAC,cAAc,EAAE,gEAAgE,EAAE,KAAK,CAAC,CAAC;QACpH,CAAC;QACD,OAAO;IACT,CAAC;IAED,IAAI,CAAC;QACH,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;IAC3B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,CAAC;YACH,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;QACzB,CAAC;QAAC,OAAO,YAAY,EAAE,CAAC;YACtB,wBAAwB,CAAC,cAAc,EAAE,6FAA6F,EAAE,YAAY,CAAC,CAAC;QACxJ,CAAC;QACD,wBAAwB,CAAC,cAAc,EAAE,gGAAgG,EAAE,KAAK,CAAC,CAAC;IACpJ,CAAC;AACH,CAAC;AAcD,+FAA+F;AAC/F,mBAAmB;AACnB,+FAA+F;AAE/F;;;;GAIG;AACH,MAAa,gBAAgB;IAWS;IAAuC;IAAwC;IAV3G,eAAe,CAAa;IAC5B,WAAW,GAAG,KAAK,CAAC;IACpB,kBAAkB,GAAG,IAAI,CAAC;IAElC;;OAEG;IACH,IAAW,iBAAiB,KAAc,OAAO,IAAI,CAAC,kBAAkB,CAAC,CAAC,CAAC;IAE3E,4DAA4D;IAC5D,YAAoC,GAAoB,EAAmB,KAAqB,EAAmB,eAAuB;QAAtG,QAAG,GAAH,GAAG,CAAiB;QAAmB,UAAK,GAAL,KAAK,CAAgB;QAAmB,oBAAe,GAAf,eAAe,CAAQ;QACxI,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC,GAAG,CAAC,aAAa,CAAC,WAAW,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;IAClF,CAAC;IAED,+FAA+F;IAC/F,YAAY;IACZ,+FAA+F;IAE/F;;;;OAIG;IACK,OAAO;QACb,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC;QACxB,IAAI,CAAC;YACH,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;QAC/B,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,wBAAwB,CAAC,IAAI,CAAC,eAAe,EAAE,kEAAkE,EAAE,KAAK,CAAC,CAAC;QAC5H,CAAC;IACH,CAAC;IAED;;OAEG;IACI,CAAC,MAAM,CAAC,OAAO,CAAC;QACrB,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC;QACxB,IAAI,CAAC,eAAe,EAAE,CAAC;IACzB,CAAC;IAED,+FAA+F;IAC/F,iBAAiB;IACjB,+FAA+F;IAE/F;;;;;;OAMG;IACI,cAAc,CAAC,KAAa,EAAE,IAAa;QAEhD,MAAM,UAAU,GAAG,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;QAC3C,IAAI,CAAC,UAAU,CAAC,YAAY;YAC1B,MAAM,IAAI,yBAAW,CAAC,uBAAQ,CAAC,eAAe,EAAE,UAAU,CAAC,OAAO,IAAI,gCAAgC,KAAK,EAAE,CAAC,CAAC;QAEjH,IAAI,IAAI,EAAE,CAAC;YACT,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;YACzC,IAAI,CAAC,UAAU,CAAC,YAAY;gBAC1B,MAAM,IAAI,yBAAW,CAAC,uBAAQ,CAAC,eAAe,EAAE,UAAU,CAAC,OAAO,IAAI,0BAA0B,KAAK,EAAE,CAAC,CAAC;QAC7G,CAAC;IACH,CAAC;IAED;;;;;;;;;;;OAWG;IACI,WAAW,CAAC,OAA8C;QAC/D,IAAI,IAAI,CAAC,WAAW,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU;YAC5C,MAAM,IAAI,yBAAW,CAAC,uBAAQ,CAAC,eAAe,EAAE,2BAA2B,CAAC,CAAC;QAC/E,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;QACrC,IAAI,UAAU,KAAK,uBAAQ,CAAC,aAAa;YACvC,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC;QACxC,IAAI,UAAU,KAAK,uBAAQ,CAAC,cAAc;YACxC,OAAO,SAAS,CAAC;QACnB,MAAM,IAAI,yBAAW,CAAC,UAAU,EAAE,yBAAyB,UAAU,EAAE,CAAC,CAAC;IAC3E,CAAC;IAED;;;;;OAKG;IACI,aAAa,CAAC,OAA8C;QACjE,IAAI,IAAI,CAAC,WAAW,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU;YAC5C,MAAM,IAAI,yBAAW,CAAC,uBAAQ,CAAC,eAAe,EAAE,2BAA2B,CAAC,CAAC;QAC/E,OAAO,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;IACzC,CAAC;IAED,+FAA+F;IAC/F,mBAAmB;IACnB,+FAA+F;IAE/F;;;;OAIG;IACK,WAAW,CAAC,KAAa;QAC/B,IAAI,IAAI,CAAC,KAAK,CAAC,UAAU;YACvB,OAAO,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC;QAEhC,IAAI,CAAC;YACH,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,mBAAS,CAAC,EAAE,KAAK,CAAC,CAAC;YAC/C,OAAO,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC;QAChC,CAAC;QAAC,OAAO,KAAU,EAAE,CAAC;YACpB,IAAI,CAAC,kBAAkB,GAAG,KAAK,CAAC;YAChC,IAAI,CAAC;gBACH,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;YAC/B,CAAC;YAAC,OAAO,YAAY,EAAE,CAAC;gBACtB,wBAAwB,CAAC,IAAI,CAAC,eAAe,EAAE,gEAAgE,EAAE,YAAY,CAAC,CAAC;YACjI,CAAC;YACD,OAAO,EAAE,YAAY,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC;QACzD,CAAC;IACH,CAAC;IAED;;;;OAIG;IACK,UAAU,CAAC,IAAwB;QACzC,IAAI,CAAC;YACH,IAAI,IAAI,KAAK,SAAS;gBACpB,OAAO,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC;YAEhC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU;gBACxB,OAAO,EAAE,YAAY,EAAE,KAAK,EAAE,OAAO,EAAE,2BAA2B,EAAE,CAAC;YAEvE,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;YAC5B,OAAO,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC;QAChC,CAAC;QAAC,OAAO,KAAU,EAAE,CAAC;YACpB,OAAO,EAAE,YAAY,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC;QACzD,CAAC;IACH,CAAC;CACF;AAlJD,4CAkJC","sourcesContent":["/*---------------------------------------------------------------------------------------------\r\n* Copyright (c) Bentley Systems, Incorporated. All rights reserved.\r\n* See LICENSE.md in the project root for license terms and full copyright notice.\r\n*--------------------------------------------------------------------------------------------*/\r\n\r\nimport { IModelError, PropertyMetaDataMap } from \"@itwin/core-common\";\r\nimport type { IModelDb } from \"./IModelDb\";\r\nimport { ECSqlStatement } from \"./ECSqlStatement\";\r\nimport { BentleyError, DbResult, Logger } from \"@itwin/core-bentley\";\r\nimport { IModelJsNative } from \"@bentley/imodeljs-native\";\r\nimport { _nativeDb } from \"./internal/Symbols\";\r\nimport type { ECDb } from \"./ECDb\";\r\nimport type { StatementCache } from \"./SqliteStatement\";\r\n\r\nconst statementNotPreparedMessage = \"Statement is not prepared. Likely cause: the db was closed before step is called or the ECSqlSyncReader is used outside the context of the callback passed to withQueryReader.\";\r\n\r\nfunction logStatementCleanupError(loggerCategory: string, message: string, error: unknown): void {\r\n try {\r\n Logger.logTrace(loggerCategory, message, () => ({ error: BentleyError.getErrorMessage(error) }));\r\n } catch {\r\n // Cleanup diagnostics must not replace the original query error.\r\n }\r\n}\r\n\r\n/** Returns a statement to its cache, falling back to direct disposal without throwing.\r\n * @internal\r\n */\r\n// eslint-disable-next-line @typescript-eslint/no-deprecated\r\nexport function releaseECSqlStatement(stmt: ECSqlStatement, cache: StatementCache<ECSqlStatement>, loggerCategory: string, canCache: boolean): void {\r\n if (!canCache || !stmt.isPrepared) {\r\n try {\r\n stmt[Symbol.dispose]();\r\n } catch (error) {\r\n logStatementCleanupError(loggerCategory, \"Failed to dispose an ECSQL statement that could not be cached.\", error);\r\n }\r\n return;\r\n }\r\n\r\n try {\r\n cache.addOrDispose(stmt);\r\n } catch (error) {\r\n try {\r\n stmt[Symbol.dispose]();\r\n } catch (disposeError) {\r\n logStatementCleanupError(loggerCategory, \"Failed to dispose an ECSQL statement after it could not be returned to the statement cache.\", disposeError);\r\n }\r\n logStatementCleanupError(loggerCategory, \"Failed to return an ECSQL statement to the statement cache; attempted direct disposal instead.\", error);\r\n }\r\n}\r\n\r\n// --------------------------------------------------------------------------------------------\r\n// Internal result types\r\n// --------------------------------------------------------------------------------------------\r\n\r\n/** Result of an internal operation that may fail with a message.\r\n * @internal\r\n */\r\ninterface OperationResult {\r\n isSuccessful: boolean;\r\n message?: string;\r\n}\r\n\r\n// --------------------------------------------------------------------------------------------\r\n// ECSqlRowExecutor\r\n// --------------------------------------------------------------------------------------------\r\n\r\n/**\r\n * Executes ECSql queries one row at a time against an IModelDb, maintaining statement state between\r\n * successive calls so the caller can page through results via offset-based requests.\r\n * @internal\r\n */\r\nexport class ECSqlRowExecutor implements Disposable {\r\n private _removeListener: () => void;\r\n private _isDisposed = false;\r\n private _canCacheStatement = true;\r\n\r\n /** Whether the statement completed preparation and can be returned to its cache.\r\n * @internal\r\n */\r\n public get canCacheStatement(): boolean { return this._canCacheStatement; }\r\n\r\n // eslint-disable-next-line @typescript-eslint/no-deprecated\r\n public constructor(private readonly _db: IModelDb | ECDb, private readonly _stmt: ECSqlStatement, private readonly _loggerCategory: string) {\r\n this._removeListener = this._db.onBeforeClose.addListener(() => this.cleanup());\r\n }\r\n\r\n // --------------------------------------------------------------------------------------------\r\n // Lifecycle\r\n // --------------------------------------------------------------------------------------------\r\n\r\n /** Disposes the currently held statement without returning it to the cache.\r\n * Invoked when the db signals it is closing (the statement cache is cleared on close, so the\r\n * checked-out statement must be disposed directly to avoid a use-after-free or double dispose).\r\n * @internal\r\n */\r\n private cleanup(): void {\r\n this._isDisposed = true;\r\n try {\r\n this._stmt[Symbol.dispose]();\r\n } catch (error) {\r\n logStatementCleanupError(this._loggerCategory, \"Failed to dispose an ECSQL statement while closing its database.\", error);\r\n }\r\n }\r\n\r\n /** Removes the database-close listener owned by this row executor.\r\n * @internal\r\n */\r\n public [Symbol.dispose](): void {\r\n this._isDisposed = true;\r\n this._removeListener();\r\n }\r\n\r\n // --------------------------------------------------------------------------------------------\r\n // Core execution\r\n // --------------------------------------------------------------------------------------------\r\n\r\n /** Prepare the statement and bind parameters in one step.\r\n * Call once during reader initialization — avoids the per-row `ensureStatementReady` check.\r\n * @param query - The ECSql text to prepare.\r\n * @param args - Optional bind parameters.\r\n * @throws IModelError on preparation or binding failure.\r\n * @internal\r\n */\r\n public prepareAndBind(query: string, args?: object): void {\r\n\r\n const prepResult = this.prepareStmt(query);\r\n if (!prepResult.isSuccessful)\r\n throw new IModelError(DbResult.BE_SQLITE_ERROR, prepResult.message ?? `Failed to prepare statement: ${query}`);\r\n\r\n if (args) {\r\n const bindResult = this.bindValues(args);\r\n if (!bindResult.isSuccessful)\r\n throw new IModelError(DbResult.BE_SQLITE_ERROR, bindResult.message ?? `Failed to bind values: ${query}`);\r\n }\r\n }\r\n\r\n /** Fast-path: step the cursor once and return row data directly.\r\n *\r\n * Returns the row data array if a row is available.\r\n * Returns `undefined` if the result set is exhausted (DONE).\r\n *\r\n * This avoids all intermediate object allocations (StepResult, RowDataResult,\r\n * DbRuntimeStats, DbQueryResponse) that the general `execute()` path creates per row.\r\n *\r\n * @param options - Native row-adaptor options (should be cached and reused across rows).\r\n * @throws IModelError on step failure or row extraction failure.\r\n * @internal\r\n */\r\n public stepNextRow(options: IModelJsNative.ECSqlRowAdaptorOptions): any {\r\n if (this._isDisposed || !this._stmt.isPrepared)\r\n throw new IModelError(DbResult.BE_SQLITE_ERROR, statementNotPreparedMessage);\r\n const stepResult = this._stmt.step();\r\n if (stepResult === DbResult.BE_SQLITE_ROW)\r\n return this._stmt.toRow(options).data;\r\n if (stepResult === DbResult.BE_SQLITE_DONE)\r\n return undefined;\r\n throw new IModelError(stepResult, `Step failed with code ${stepResult}`);\r\n }\r\n\r\n /** Get column metadata directly from the prepared statement.\r\n * Call once after `prepareAndBind` — the metadata does not change between rows.\r\n * @param options - Native row-adaptor options that influence property naming.\r\n * @returns Column metadata owned by the caller.\r\n * @internal\r\n */\r\n public fetchMetadata(options: IModelJsNative.ECSqlRowAdaptorOptions): PropertyMetaDataMap {\r\n if (this._isDisposed || !this._stmt.isPrepared)\r\n throw new IModelError(DbResult.BE_SQLITE_ERROR, statementNotPreparedMessage);\r\n return this._stmt.getMetadata(options);\r\n }\r\n\r\n // --------------------------------------------------------------------------------------------\r\n // Execution phases\r\n // --------------------------------------------------------------------------------------------\r\n\r\n /** Prepares the ECSql statement when the caller did not supply one from its cache.\r\n * @param ecsql - The ECSql text to prepare.\r\n * @returns An `OperationResult` indicating success or failure.\r\n * @internal\r\n */\r\n private prepareStmt(ecsql: string): OperationResult {\r\n if (this._stmt.isPrepared)\r\n return { isSuccessful: true };\r\n\r\n try {\r\n this._stmt.prepare(this._db[_nativeDb], ecsql);\r\n return { isSuccessful: true };\r\n } catch (error: any) {\r\n this._canCacheStatement = false;\r\n try {\r\n this._stmt[Symbol.dispose]();\r\n } catch (disposeError) {\r\n logStatementCleanupError(this._loggerCategory, \"Failed to dispose an ECSQL statement after preparation failed.\", disposeError);\r\n }\r\n return { isSuccessful: false, message: error.message };\r\n }\r\n }\r\n\r\n /** Binds the supplied parameter values to the prepared statement.\r\n * @param args - The parameter object to bind, or `undefined` when no parameters are needed.\r\n * @returns An `OperationResult` indicating success or failure.\r\n * @internal\r\n */\r\n private bindValues(args: object | undefined): OperationResult {\r\n try {\r\n if (args === undefined)\r\n return { isSuccessful: true };\r\n\r\n if (!this._stmt.isPrepared)\r\n return { isSuccessful: false, message: statementNotPreparedMessage };\r\n\r\n this._stmt.bindParams(args);\r\n return { isSuccessful: true };\r\n } catch (error: any) {\r\n return { isSuccessful: false, message: error.message };\r\n }\r\n }\r\n}"]}
1
+ {"version":3,"file":"ECSqlRowExecutor.js","sourceRoot":"","sources":["../../src/ECSqlRowExecutor.ts"],"names":[],"mappings":"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","sourcesContent":["/*---------------------------------------------------------------------------------------------\r\n* Copyright (c) Bentley Systems, Incorporated. All rights reserved.\r\n* See LICENSE.md in the project root for license terms and full copyright notice.\r\n*--------------------------------------------------------------------------------------------*/\r\n\r\nimport { IModelError, PropertyMetaDataMap } from \"@itwin/core-common\";\r\nimport type { IModelDb } from \"./IModelDb\";\r\nimport { BentleyError, DbResult } from \"@itwin/core-bentley\";\r\nimport { IModelJsNative } from \"@bentley/imodeljs-native\";\r\nimport { _nativeDb } from \"./internal/Symbols\";\r\nimport type { ECDb } from \"./ECDb\";\r\n\r\nconst statementNotPreparedMessage = \"Statement is not prepared. Likely cause: the db was closed before step is called or the ECSqlSyncReader is used outside the context of the callback passed to withQueryReader.\";\r\n\r\n/** Column metadata keyed by native statement id and row options. Statement ids change whenever the native cache\r\n * prepares the ECSql again, so entries never outlive the statement they describe.\r\n */\r\nconst maxCachedMetadataEntries = 100;\r\n\r\n/** Rows fetched by the first native batch. Small so single-row and early-exit queries do little extra work. */\r\nconst initialBatchSize = 4;\r\n/** Each subsequent batch grows by this factor up to [[maxBatchSize]]. */\r\nconst batchGrowthFactor = 4;\r\n/** Upper bound on rows rendered per native call; bounds memory held by the read-ahead buffer. */\r\nconst maxBatchSize = 128;\r\n/** Number of distinct row-flag combinations; metadata keys pack the statement id and the row flags into one number. */\r\nconst rowFlagCombinations = 16;\r\nconst metadataCache = new WeakMap<IModelDb | ECDb, Map<number, PropertyMetaDataMap>>();\r\n/** Receives the first batch and statement id from `prepareECSqlBatchReader`. Read immediately after each call. */\r\nconst prepareOut: any[] = [];\r\n\r\n/** Row rendering flags passed to `prepareECSqlBatchReader`.\r\n * @internal\r\n */\r\nexport enum SyncReaderRowFlags {\r\n None = 0,\r\n AbbreviateBlobs = 1,\r\n ClassIdsToClassNames = 2,\r\n UseJsName = 4,\r\n DoNotConvertClassIdsToClassNamesWhenAliased = 8,\r\n}\r\n\r\nfunction getCachedMetadata(db: IModelDb | ECDb, key: number, fetch: () => PropertyMetaDataMap): PropertyMetaDataMap {\r\n let entries = metadataCache.get(db);\r\n if (!entries) {\r\n entries = new Map();\r\n metadataCache.set(db, entries);\r\n }\r\n\r\n let properties = entries.get(key);\r\n if (properties) {\r\n entries.delete(key);\r\n } else {\r\n properties = fetch();\r\n if (entries.size >= maxCachedMetadataEntries)\r\n entries.delete(entries.keys().next().value as number);\r\n }\r\n entries.set(key, properties);\r\n return properties;\r\n}\r\n\r\n// --------------------------------------------------------------------------------------------\r\n// ECSqlRowExecutor\r\n// --------------------------------------------------------------------------------------------\r\n\r\n/**\r\n * Executes an ECSql query on the db's primary connection and hands out rows one at a time. On a read-only db, rows are\r\n * read from native code in growing batches to reduce JS/native transitions; on a writable db native code returns one row\r\n * per call so rows are never read ahead of writes made while iterating. The prepared statement is leased from the native\r\n * concurrent query statement cache and returned to it when the executor is disposed.\r\n * @internal\r\n */\r\nexport class ECSqlRowExecutor implements Disposable {\r\n private _removeListener: () => void;\r\n private _reader?: IModelJsNative.ECSqlBatchReader;\r\n /** Key of this statement's metadata in `metadataCache`, or undefined when its metadata must not be cached. */\r\n private _metadataKey: number | undefined;\r\n private _rows: any[] = [];\r\n private _rowIndex = 0;\r\n private _batchSize = initialBatchSize;\r\n /** Status of the last native batch. Anything other than BE_SQLITE_ROW means no further batch is fetched. */\r\n private _batchStatus = DbResult.BE_SQLITE_ROW;\r\n\r\n public constructor(private readonly _db: IModelDb | ECDb) {\r\n this._removeListener = this._db.onBeforeClose.addListener(() => this.releaseReader());\r\n }\r\n\r\n // --------------------------------------------------------------------------------------------\r\n // Lifecycle\r\n // --------------------------------------------------------------------------------------------\r\n\r\n /** Returns the leased statement to the native cache. Safe to call more than once. */\r\n private releaseReader(): void {\r\n const reader = this._reader;\r\n this._reader = undefined;\r\n this.resetBatch();\r\n reader?.dispose();\r\n }\r\n\r\n private resetBatch(): void {\r\n this._rows = [];\r\n this._rowIndex = 0;\r\n this._batchSize = initialBatchSize;\r\n this._batchStatus = DbResult.BE_SQLITE_ROW;\r\n }\r\n\r\n /** Returns the leased statement and removes the database-close listener owned by this row executor.\r\n * @internal\r\n */\r\n public [Symbol.dispose](): void {\r\n this._removeListener();\r\n this.releaseReader();\r\n }\r\n\r\n // --------------------------------------------------------------------------------------------\r\n // Core execution\r\n // --------------------------------------------------------------------------------------------\r\n\r\n /** Prepare the statement and bind parameters in one step. On a read-only db the first batch of rows is read by the\r\n * same native call.\r\n * @param query - The ECSql text to prepare.\r\n * @param args - Optional bind parameters as flat `[key, type, value, ...]` triples.\r\n * @param rowFlags - [[SyncReaderRowFlags]] used for every row and for metadata.\r\n * @throws IModelError on preparation or binding failure.\r\n * @internal\r\n */\r\n public prepareAndBind(query: string, args: any[] | undefined, rowFlags: SyncReaderRowFlags): void {\r\n this.releaseReader();\r\n try {\r\n this._reader = this._db[_nativeDb].prepareECSqlBatchReader(query, args, rowFlags, this._batchSize, prepareOut);\r\n } catch (error) {\r\n throw new IModelError(DbResult.BE_SQLITE_ERROR, BentleyError.getErrorMessage(error));\r\n }\r\n const firstBatch = prepareOut[0];\r\n // Metadata of a dynamic column (e.g. $->prop) changes as the statement is stepped, so it is never cached.\r\n this._metadataKey = prepareOut[2] ? undefined : prepareOut[1] * rowFlagCombinations + rowFlags;\r\n prepareOut.length = 0;\r\n if (firstBatch !== undefined)\r\n this.acceptBatch(firstBatch);\r\n }\r\n\r\n /** Store a batch returned by native code: a JSON array string, an array of rows, or the final DbResult. */\r\n private acceptBatch(batch: string | any[] | DbResult): void {\r\n this._batchSize = Math.min(this._batchSize * batchGrowthFactor, maxBatchSize);\r\n this._rowIndex = 0;\r\n if (typeof batch === \"number\") {\r\n this._rows = [];\r\n this._batchStatus = batch;\r\n } else {\r\n this._rows = typeof batch === \"string\" ? JSON.parse(batch) : batch;\r\n }\r\n }\r\n\r\n /** Return the next row data array, or `undefined` when the result set is exhausted. Rows are served from the\r\n * read-ahead buffer; a new native batch is fetched only when the buffer is empty.\r\n * @throws IModelError on step failure or when the statement is no longer available. A failure reported by a batch is\r\n * thrown after the rows stepped before it have been returned.\r\n * @internal\r\n */\r\n public stepNextRow(): any {\r\n if (this._rowIndex < this._rows.length)\r\n return this._rows[this._rowIndex++];\r\n\r\n const reader = this._reader;\r\n if (!reader)\r\n throw new IModelError(DbResult.BE_SQLITE_ERROR, statementNotPreparedMessage);\r\n\r\n if (this._batchStatus === DbResult.BE_SQLITE_ROW) {\r\n this.acceptBatch(reader.stepBatch(this._batchSize));\r\n if (this._rows.length > 0)\r\n return this._rows[this._rowIndex++];\r\n }\r\n\r\n if (this._batchStatus === DbResult.BE_SQLITE_DONE)\r\n return undefined;\r\n throw new IModelError(this._batchStatus, `Step failed with code ${this._batchStatus}`);\r\n }\r\n\r\n /** Get shared internal column metadata. Readers must copy it before exposing mutable metadata to callers.\r\n * @internal\r\n */\r\n public fetchMetadata(): PropertyMetaDataMap {\r\n const reader = this._reader;\r\n if (!reader)\r\n throw new IModelError(DbResult.BE_SQLITE_ERROR, statementNotPreparedMessage);\r\n const fetch = () => new PropertyMetaDataMap(reader.getMetadata());\r\n return this._metadataKey === undefined ? fetch() : getCachedMetadata(this._db, this._metadataKey, fetch);\r\n }\r\n}\r\n"]}
@@ -9,8 +9,7 @@ export type SynchronousQueryOptions = Omit<QueryOptions, "suppressLogErrors" | "
9
9
  * Execute ECSQL statements synchronously and read the results one row at a time.
10
10
  *
11
11
  * This is the synchronous counterpart of [[ECSqlReader]] from `@itwin/core-common`.
12
- * It uses [[ECSqlRowExecutor]] directly — each call to [[step]] fetches exactly one row
13
- * with no internal caching, paging, or offset tracking.
12
+ * It uses [[ECSqlRowExecutor]] directly, with read-ahead batches on read-only dbs and live stepping on writable dbs.
14
13
  *
15
14
  * The query results are returned one row at a time. The format of the row is dictated by the
16
15
  * `rowFormat` specified in the `options` parameter of the constructed ECSqlSyncReader object.
@@ -30,12 +29,16 @@ export declare class ECSqlSyncReader extends ECSqlReaderBase implements Iterable
30
29
  readonly query: string;
31
30
  private _currentRow;
32
31
  private _options;
33
- /** Cached native row-adaptor options — built once and reused for every row. */
34
- private _cachedRowOptions;
32
+ private _ownsMetadata;
35
33
  /**
36
34
  * @internal
37
35
  */
38
36
  constructor(_executor: ECSqlRowExecutor, query: string, param?: QueryBinder, options?: SynchronousQueryOptions);
37
+ /** Flatten the bindings of `param` into `[key, type, value, ...]` triples, the form read by native code without
38
+ * per-property lookups. Entries lacking a type or value are skipped, as the object form skipped them.
39
+ * @internal
40
+ */
41
+ private static flattenArgs;
39
42
  /**
40
43
  * @internal
41
44
  */
@@ -44,11 +47,17 @@ export declare class ECSqlSyncReader extends ECSqlReaderBase implements Iterable
44
47
  * Reads a single row from the executor. Returns the row data or `undefined` when the
45
48
  * result set is exhausted.
46
49
  *
47
- * Uses the fast-path `stepNextRow` method on the executor, which calls `stmt.step()`
48
- * and `stmt.toRow()` directly — no intermediate request/response objects are allocated per row.
50
+ * Uses the fast-path `stepNextRow` method on the executor, which serves rows from a read-ahead buffer filled by
51
+ * native code in batches — no intermediate request/response objects are allocated per row.
49
52
  * @internal
50
53
  */
51
54
  private readRow;
55
+ /**
56
+ * Native rows parsed from JSON hold `null` for null cells and base64 strings for blobs. Convert them in place to
57
+ * `undefined` and `Uint8Array`, matching rows whose cells were created directly.
58
+ * @internal
59
+ */
60
+ private static normalizeRow;
52
61
  /**
53
62
  * Steps the cursor with retry logic for transient busy/interrupt conditions.
54
63
  * Returns the row data array, or `undefined` when the result set is exhausted.
@@ -66,11 +75,15 @@ export declare class ECSqlSyncReader extends ECSqlReaderBase implements Iterable
66
75
  * @beta
67
76
  */
68
77
  getMetaData(): QueryPropertyMetaData[];
78
+ /** Copy shared metadata only when it leaves the reader, including through the row proxy.
79
+ * @internal
80
+ */
81
+ protected getMetaDataInternal(): QueryPropertyMetaData[];
69
82
  /**
70
83
  * Step to the next row of the query result.
71
84
  *
72
- * Each call executes exactly one step on the underlying statement — there is no
73
- * internal row caching or offset tracking.
85
+ * On a writable db each step reads the next row from the underlying statement, so changes made inside the callback
86
+ * are visible to later steps. On a read-only db rows are read ahead in small, growing batches.
74
87
  *
75
88
  * @returns `true` if a row can be read from `current`.<br/>
76
89
  * `false` if there are no more rows; i.e., all rows have been stepped through already.
@@ -1 +1 @@
1
- {"version":3,"file":"ECSqlSyncReader.d.ts","sourceRoot":"","sources":["../../src/ECSqlSyncReader.ts"],"names":[],"mappings":"AAIA;;GAEG;AACH,OAAO,EAEL,eAAe,EAEf,WAAW,EAAE,YAAY,EAAuB,qBAAqB,EAAkB,aAAa,EACrG,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAGtD,YAAY;AACZ,MAAM,MAAM,uBAAuB,GAAG,IAAI,CAAC,YAAY,EAAE,mBAAmB,GAAG,iBAAiB,GAAG,OAAO,GAAG,UAAU,GAAG,cAAc,GAAG,OAAO,GAAG,gBAAgB,GAAG,OAAO,CAAC,CAAC;AAEjL;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,eAAgB,SAAQ,eAAgB,YAAW,gBAAgB,CAAC,aAAa,CAAC;IAS1E,OAAO,CAAC,SAAS;aAAoC,KAAK,EAAE,MAAM;IARrF,OAAO,CAAC,WAAW,CAAM;IACzB,OAAO,CAAC,QAAQ,CAA0B;IAC1C,+EAA+E;IAC/E,OAAO,CAAC,iBAAiB,CAAM;IAE/B;;OAEG;gBACwB,SAAS,EAAE,gBAAgB,EAAkB,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE,uBAAuB;IA+B7I;;OAEG;cACgB,cAAc,IAAI,GAAG,EAAE;IAM1C;;;;;;;OAOG;IACH,OAAO,CAAC,OAAO;IAaf;;;;OAIG;IACH,OAAO,CAAC,aAAa;IASrB;;;;;;;;;OASG;IACI,WAAW,IAAI,qBAAqB,EAAE;IAO7C;;;;;;;;;OASG;IACI,IAAI,IAAI,OAAO;IAYtB;;;;;OAKG;IACI,OAAO,IAAI,GAAG,EAAE;IAQvB;;;;;OAKG;IACI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,gBAAgB,CAAC,aAAa,CAAC;IAI3D;;;;;;;OAOG;IACI,IAAI,IAAI,cAAc,CAAC,aAAa,EAAE,GAAG,CAAC;CAalD"}
1
+ {"version":3,"file":"ECSqlSyncReader.d.ts","sourceRoot":"","sources":["../../src/ECSqlSyncReader.ts"],"names":[],"mappings":"AAIA;;GAEG;AACH,OAAO,EAEL,eAAe,EAGf,WAAW,EAAE,YAAY,EAAuB,qBAAqB,EAAkB,aAAa,EACrG,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,gBAAgB,EAAsB,MAAM,oBAAoB,CAAC;AAG1E,YAAY;AACZ,MAAM,MAAM,uBAAuB,GAAG,IAAI,CAAC,YAAY,EAAE,mBAAmB,GAAG,iBAAiB,GAAG,OAAO,GAAG,UAAU,GAAG,cAAc,GAAG,OAAO,GAAG,gBAAgB,GAAG,OAAO,CAAC,CAAC;AAEjL;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBAAa,eAAgB,SAAQ,eAAgB,YAAW,gBAAgB,CAAC,aAAa,CAAC;IAQ1E,OAAO,CAAC,SAAS;aAAoC,KAAK,EAAE,MAAM;IAPrF,OAAO,CAAC,WAAW,CAAM;IACzB,OAAO,CAAC,QAAQ,CAA0B;IAC1C,OAAO,CAAC,aAAa,CAAS;IAE9B;;OAEG;gBACwB,SAAS,EAAE,gBAAgB,EAAkB,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE,uBAAuB;IA4B7I;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,WAAW;IAW1B;;OAEG;cACgB,cAAc,IAAI,GAAG,EAAE;IAM1C;;;;;;;OAOG;IACH,OAAO,CAAC,OAAO;IAcf;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,YAAY;IAc3B;;;;OAIG;IACH,OAAO,CAAC,aAAa;IASrB;;;;;;;;;OASG;IACI,WAAW,IAAI,qBAAqB,EAAE;IAQ7C;;OAEG;cACgB,mBAAmB,IAAI,qBAAqB,EAAE;IAQjE;;;;;;;;;OASG;IACI,IAAI,IAAI,OAAO;IAYtB;;;;;OAKG;IACI,OAAO,IAAI,GAAG,EAAE;IAQvB;;;;;OAKG;IACI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,gBAAgB,CAAC,aAAa,CAAC;IAI3D;;;;;;;OAOG;IACI,IAAI,IAAI,cAAc,CAAC,aAAa,EAAE,GAAG,CAAC;CAalD"}
@@ -9,13 +9,13 @@ exports.ECSqlSyncReader = void 0;
9
9
  * @module ECSQL
10
10
  */
11
11
  const core_common_1 = require("@itwin/core-common");
12
+ const ECSqlRowExecutor_1 = require("./ECSqlRowExecutor");
12
13
  const core_bentley_1 = require("@itwin/core-bentley");
13
14
  /**
14
15
  * Execute ECSQL statements synchronously and read the results one row at a time.
15
16
  *
16
17
  * This is the synchronous counterpart of [[ECSqlReader]] from `@itwin/core-common`.
17
- * It uses [[ECSqlRowExecutor]] directly — each call to [[step]] fetches exactly one row
18
- * with no internal caching, paging, or offset tracking.
18
+ * It uses [[ECSqlRowExecutor]] directly, with read-ahead batches on read-only dbs and live stepping on writable dbs.
19
19
  *
20
20
  * The query results are returned one row at a time. The format of the row is dictated by the
21
21
  * `rowFormat` specified in the `options` parameter of the constructed ECSqlSyncReader object.
@@ -35,8 +35,7 @@ class ECSqlSyncReader extends core_common_1.ECSqlReaderBase {
35
35
  query;
36
36
  _currentRow;
37
37
  _options;
38
- /** Cached native row-adaptor options — built once and reused for every row. */
39
- _cachedRowOptions;
38
+ _ownsMetadata = false;
40
39
  /**
41
40
  * @internal
42
41
  */
@@ -51,22 +50,33 @@ class ECSqlSyncReader extends core_common_1.ECSqlReaderBase {
51
50
  throw new core_common_1.IModelError(core_bentley_1.DbResult.BE_SQLITE_ERROR, "expecting non-empty ecsql statement");
52
51
  }
53
52
  this._options = resolvedOptions;
54
- const serializedParam = param ? param.serialize() : new core_common_1.QueryBinder().serialize();
55
- // Prepare the statement and bind parameters once — avoids per-row ensureStatementReady overhead.
56
- const args = Object.keys(serializedParam).length > 0 ? serializedParam : undefined;
57
- this._executor.prepareAndBind(this.query, args);
58
- // Build and cache the native row-adaptor options — reused for every step call.
53
+ // Row options are passed to native code once, as flags, and used for every row.
54
+ let rowFlags = ECSqlRowExecutor_1.SyncReaderRowFlags.DoNotConvertClassIdsToClassNamesWhenAliased;
55
+ if (this._options.abbreviateBlobs)
56
+ rowFlags |= ECSqlRowExecutor_1.SyncReaderRowFlags.AbbreviateBlobs;
59
57
  // eslint-disable-next-line @typescript-eslint/no-deprecated
60
- const valueFormat = this._options.rowFormat === core_common_1.QueryRowFormat.UseJsPropertyNames ? core_common_1.DbValueFormat.JsNames : core_common_1.DbValueFormat.ECSqlNames;
61
- this._cachedRowOptions = {
62
- abbreviateBlobs: this._options.abbreviateBlobs ?? false,
63
- // eslint-disable-next-line @typescript-eslint/no-deprecated
64
- classIdsToClassNames: this._options.convertClassIdsToClassNames ?? false,
65
- useJsName: valueFormat === core_common_1.DbValueFormat.JsNames,
66
- doNotConvertClassIdsToClassNamesWhenAliased: true,
67
- };
58
+ if (this._options.convertClassIdsToClassNames)
59
+ rowFlags |= ECSqlRowExecutor_1.SyncReaderRowFlags.ClassIdsToClassNames;
60
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
61
+ if (this._options.rowFormat === core_common_1.QueryRowFormat.UseJsPropertyNames)
62
+ rowFlags |= ECSqlRowExecutor_1.SyncReaderRowFlags.UseJsName;
63
+ this._executor.prepareAndBind(this.query, param ? ECSqlSyncReader.flattenArgs(param) : undefined, rowFlags);
68
64
  this._currentRow = undefined;
69
65
  }
66
+ /** Flatten the bindings of `param` into `[key, type, value, ...]` triples, the form read by native code without
67
+ * per-property lookups. Entries lacking a type or value are skipped, as the object form skipped them.
68
+ * @internal
69
+ */
70
+ static flattenArgs(param) {
71
+ const serialized = param.serialize();
72
+ let args;
73
+ for (const key of Object.keys(serialized)) {
74
+ const arg = serialized[key];
75
+ if (typeof arg === "object" && arg !== null && "type" in arg && "value" in arg)
76
+ (args ??= []).push(key, arg.type, arg.value);
77
+ }
78
+ return args;
79
+ }
70
80
  /**
71
81
  * @internal
72
82
  */
@@ -79,8 +89,8 @@ class ECSqlSyncReader extends core_common_1.ECSqlReaderBase {
79
89
  * Reads a single row from the executor. Returns the row data or `undefined` when the
80
90
  * result set is exhausted.
81
91
  *
82
- * Uses the fast-path `stepNextRow` method on the executor, which calls `stmt.step()`
83
- * and `stmt.toRow()` directly — no intermediate request/response objects are allocated per row.
92
+ * Uses the fast-path `stepNextRow` method on the executor, which serves rows from a read-ahead buffer filled by
93
+ * native code in batches — no intermediate request/response objects are allocated per row.
84
94
  * @internal
85
95
  */
86
96
  readRow() {
@@ -89,19 +99,40 @@ class ECSqlSyncReader extends core_common_1.ECSqlReaderBase {
89
99
  }
90
100
  // Fetch metadata once on the first call.
91
101
  if (this._props.length === 0) {
92
- this._props = this._executor.fetchMetadata(this._cachedRowOptions);
102
+ this._props = this._executor.fetchMetadata();
103
+ this._ownsMetadata = false;
93
104
  }
94
105
  return this.stepWithRetry();
95
106
  }
107
+ /**
108
+ * Native rows parsed from JSON hold `null` for null cells and base64 strings for blobs. Convert them in place to
109
+ * `undefined` and `Uint8Array`, matching rows whose cells were created directly.
110
+ * @internal
111
+ */
112
+ static normalizeRow(row) {
113
+ for (let i = 0; i < row.length; ++i) {
114
+ const val = row[i];
115
+ if (val === null) {
116
+ row[i] = undefined;
117
+ }
118
+ else if (typeof val === "string") {
119
+ if (core_common_1.Base64EncodedString.hasPrefix(val))
120
+ row[i] = core_common_1.Base64EncodedString.toUint8Array(val);
121
+ }
122
+ else if (typeof val === "object" && !(val instanceof Uint8Array)) {
123
+ ECSqlSyncReader.replaceBase64WithUint8Array(val);
124
+ }
125
+ }
126
+ }
96
127
  /**
97
128
  * Steps the cursor with retry logic for transient busy/interrupt conditions.
98
129
  * Returns the row data array, or `undefined` when the result set is exhausted.
99
130
  * @internal
100
131
  */
101
132
  stepWithRetry() {
102
- const result = this._executor.stepNextRow(this._cachedRowOptions);
133
+ const result = this._executor.stepNextRow();
103
134
  if (result) {
104
- ECSqlSyncReader.replaceBase64WithUint8Array(result);
135
+ ECSqlSyncReader.normalizeRow(result);
105
136
  return result;
106
137
  }
107
138
  return undefined; // undefined (done) → undefined
@@ -118,15 +149,26 @@ class ECSqlSyncReader extends core_common_1.ECSqlReaderBase {
118
149
  */
119
150
  getMetaData() {
120
151
  if (this._props.length === 0) {
121
- this._props = this._executor.fetchMetadata(this._cachedRowOptions);
152
+ this._props = this._executor.fetchMetadata();
153
+ this._ownsMetadata = false;
154
+ }
155
+ return this.getMetaDataInternal();
156
+ }
157
+ /** Copy shared metadata only when it leaves the reader, including through the row proxy.
158
+ * @internal
159
+ */
160
+ getMetaDataInternal() {
161
+ if (!this._ownsMetadata) {
162
+ this._props = new core_common_1.PropertyMetaDataMap(this._props.properties.map((property) => ({ ...property })));
163
+ this._ownsMetadata = true;
122
164
  }
123
165
  return this._props.properties;
124
166
  }
125
167
  /**
126
168
  * Step to the next row of the query result.
127
169
  *
128
- * Each call executes exactly one step on the underlying statement — there is no
129
- * internal row caching or offset tracking.
170
+ * On a writable db each step reads the next row from the underlying statement, so changes made inside the callback
171
+ * are visible to later steps. On a read-only db rows are read ahead in small, growing batches.
130
172
  *
131
173
  * @returns `true` if a row can be read from `current`.<br/>
132
174
  * `false` if there are no more rows; i.e., all rows have been stepped through already.