Synchronous Backend ECSQL Queries with withQueryReader
Use IModelDb.withQueryReader or ECDb.withQueryReader when backend code requires synchronous query execution. These APIs are currently beta.
withQueryReader prepares a query, invokes a callback with an ECSqlSyncReader, and returns the callback's result. The reader steps synchronously on the owning database connection, one row at a time, without buffering result batches. Query execution blocks the calling JavaScript thread.
For asynchronous queries on either the frontend or backend, use createQueryReader. Its reader is consumed asynchronously and buffers batches of results. See Choosing a query reader for the differences in execution, connection selection, options, and lifetime.
The withQueryReader Function
ecsqlis the ECSQL query to execute.callbackconsumes the reader and can return materialized rows or a computed value. Finish using the reader before the callback completes.paramsis a QueryBinder containing any parameter bindings.configis a SynchronousQueryOptions object. UserowFormatandabbreviateBlobsto control result formatting. The inheritedconvertClassIdsToClassNamesoption is deprecated; project class names explicitly withec_classname(). See ECSQL Row Formats.
The synchronous options do not include usePrimaryConn: this reader already uses the owning connection and can read its unsaved changes. They also omit concurrent-query controls such as priority, restartToken, and quota. To limit the result count, use an ECSQL LIMIT clause; the async reader's config.limit option is not available here.
Iterating Over Query Results
The examples below use an open IModelDb or ECDb named iModel, with these imports:
Synchronous iterator
Use for...of to step through the result. Each iteration exposes a QueryRowProxy:
Manual stepping
step() returns true when a row is available through reader.current, or false when the result is exhausted:
Collecting rows and returning a value
reader.toArray() collects all remaining rows. By default, each row is an array of values in SELECT-column order. Select UseECSqlPropertyNames to collect objects:
Materialized rows can be used after the callback completes. Collecting all rows uses memory proportional to the result size; prefer iteration for large results.
Handling Row Values
The async and sync readers share the same row formats and materialization methods:
- Use
row[index]orrow.propertyNameto read the current row. - Use
row.toRow()to retain a plain object. It uses ECSQL names unless the deprecatedUseJsPropertyNamesformat was selected. - Use
row.toArray()for the current row's raw values, orreader.toArray()for all remaining rows.
The proxy follows the reader's current row. Materialize a row before retaining it across calls to step() or iterator advances.
JavaScript-friendly property names
Use aliases with QueryRowFormat.UseECSqlPropertyNames, and project class names with ec_classname():
This avoids the deprecated UseJsPropertyNames format while producing an explicit, stable result contract. See property names and values.
Parameter Bindings
Supply a QueryBinder before execution. This example binds a class name and converts it to a class ID in the query with ec_classid():
The shared binding examples also apply to this API. Pass the binder as the third argument to withQueryReader. Bind navigation and struct members individually; whole navigation/struct values and arbitrary ECSQL array parameters are not supported by the readers. See ECSQL parameter types.
Getting Column Metadata
ECSqlSyncReader.getMetaData returns metadata for the selected columns and can be called inside the callback before or after stepping. The synchronous call returns the metadata directly; the async reader's getMetaData() returns a promise.
Reader Lifetime
Keep the reader inside its callback. Return materialized rows or computed results rather than the reader or its current-row proxy. If the callback returns a Promise, the reader remains valid until that promise settles; each reader operation is still synchronous. After the callback completes, its statement is released and may be reused from the statement cache. Statement reuse does not buffer query results.
Do not close the database or call clearCaches() while using the reader; these actions invalidate its statement.
See Migrating backend ECSQL code for guidance on replacing withPreparedStatement.
Last Updated: 02 October, 2026