Asynchronous ECSQL Queries with createQueryReader

Use createQueryReader for asynchronous ECSQL queries on an IModelDb, ECDb, or frontend IModelConnection. In these examples, iModel can be any of those objects.

createQueryReader returns an ECSqlReader immediately. Query execution starts when you consume the reader with asynchronous iteration, step(), or toArray(). The reader fetches and buffers batches of rows.

For synchronous backend execution, use withQueryReader. It supplies a callback-scoped reader that steps one row at a time. See Choosing a query reader for differences in execution, connection selection, buffering, options, and lifetime.

See also:

The createQueryReader Function

All of the iModel classes above provide a createQueryReader method for executing ECSQL statements on an iModel and reading the results of the query. The execution and results are handled by the returned ECSqlReader.

For reference, here are all three createQueryReader methods.

Here is the TypeScript method signature for createQueryReader:

createQueryReader(ecsql: string, params?: QueryBinder, config?: QueryOptions): ECSqlReader
  • The ecsql string is the query to execute, for example:

    SELECT ECInstanceId, ECClassId FROM BisCore.Element
  • The params argument of type QueryBinder contains any bindings for the ECSQL statement.

  • The config argument of type QueryOptions is for additional options for how the query will be executed. Some examples are:

    • rowFormat for determining how query results will look. For an explanation of the available formats, see ECSQL Row Formats.
    • limit for specifying how many rows can be returned at most.
    • restartToken for canceling a previous query with the same token and starting a new one.
    • usePrimaryConn for queries that need to see unsaved changes on the owning backend connection. By default, concurrent queries use separate worker connections. This option does not remove result buffering.

Iterating Over Query Results

Use the ECSqlReader created by the createQueryReader function to iterate over query results. There are three primary ways to do so:

1. Stream them using ECSqlReader as an asynchronous iterator.

for await (const row of iModel.createQueryReader("SELECT ECInstanceId, ECClassId FROM bis.Element")) { console.log(`ECInstanceId is ${row[0]}`); console.log(`ECClassId is ${row.ecclassid}`); }

Results are QueryRowProxy objects. See Handling a Row of Query Results for how to handle the results.

2. Iterate over them manually using ECSqlReader.step.

const reader = iModel.createQueryReader("SELECT ECInstanceId, ECClassId FROM bis.Element"); while (await reader.step()) { console.log(`ECInstanceId is ${reader.current[0]}`); console.log(`ECClassId is ${reader.current.ecclassid}`); }

Results are QueryRowProxy objects. See Handling a Row of Query Results for how to handle the results.

3. Collect all remaining results using ECSqlReader.toArray.

const reader = iModel.createQueryReader("SELECT ECInstanceId, ECClassId FROM bis.Element"); const allRows = await reader.toArray(); console.log(`First ECInstanceId is ${allRows[0][0]}`); console.log(`First ECClassId is ${allRows[0][1]}`);

Each result is an array by default, or an object when a named row format is selected. Collecting all rows uses memory proportional to the result size; prefer iteration for large results.

Handling a Row of Query Results

Iteration and step() expose a QueryRowProxy for the current row. Access values by column index or by name. The proxy follows the reader's current row; materialize a row before retaining it across reader advances.

The rowFormat option controls materialized row shape and some value conversions. See ECSQL Row Formats.

Accessing Row Values By Index

When iterating with a for loop:

for await (const row of iModel.createQueryReader("SELECT ECInstanceId, ECClassId FROM bis.Element")) { console.log(`ECInstanceId is ${row[0]}`); console.log(`ECClassId is ${row[1]}`); }

When iterating with step:

const reader = iModel.createQueryReader("SELECT ECInstanceId, ECClassId FROM bis.Element"); while (await reader.step()) { console.log(`ECInstanceId is ${reader.current[0]}`); console.log(`ECClassId is ${reader.current[1]}`); }

Column indexes follow SELECT-column order in every row format. The queries below place ECInstanceId and ECClassId at indexes 0,1 and 1,0 respectively. The value representation can still depend on the format; for example, JS formatting converts unaliased class IDs to class names.

SELECT ECInstanceId, ECClassId FROM bis.Element SELECT ECClassId, ECInstanceId FROM bis.Element

Accessing Row Values By Name

When iterating with a for loop:

for await (const row of iModel.createQueryReader("SELECT ECInstanceId, ECClassId FROM bis.Element", undefined, { rowFormat: QueryRowFormat.UseECSqlPropertyNames })) { console.log(`ECInstanceId is ${row.ECInstanceId}`); console.log(`ECClassId is ${row.ECClassId}`); }

When iterating with step:

const reader = iModel.createQueryReader("SELECT ECInstanceId, ECClassId FROM bis.Element", undefined, { rowFormat: QueryRowFormat.UseECSqlPropertyNames }); while (await reader.step()) { console.log(`ECInstanceId is ${reader.current.ECInstanceId}`); console.log(`ECClassId is ${reader.current.ECClassId}`); }

Using Types with the Row Results

See property value types for result types. Properties can be absent when their value is null, as with Parent in this example:

for await (const row of iModel.createQueryReader( "SELECT ECInstanceId AS id, ec_classname(ECClassId, 's.c') AS className, Parent.Id AS parentId, LastMod AS lastMod FROM bis.Element WHERE Model.Id=?", QueryBinder.from(["0x10"]), { rowFormat: QueryRowFormat.UseECSqlPropertyNames }, )) { const id: Id64String = row.id; const className: string = row.className; const parentId: Id64String | undefined = row.parentId; const lastMod: string = row.lastMod; console.log({ id, className, parentId, lastMod }); }

Working with Rows as JavaScript Literals

Call row.toRow() to materialize the current row as a plain object. It uses ECSQL names unless the deprecated UseJsPropertyNames format was selected, including when the reader uses the default index format. Store these objects rather than the reusable row proxy.

row.toArray() returns only the current row's raw values. reader.toArray() collects all remaining rows, using the selected row format. See ECSQL Row Formats for the distinctions.

When iterating with a for loop:

for await (const row of iModel.createQueryReader("SELECT * FROM bis.Element")) { const jsRow: object = row.toRow(); // explicitly typed for example purposes }

When iterating with step:

const reader = iModel.createQueryReader("SELECT * FROM bis.Element"); while (await reader.step()) { const jsRow: object = reader.current.toRow(); // explicitly typed for example purposes }

Select an object format when collecting all rows with reader.toArray():

const reader = iModel.createQueryReader("SELECT * FROM bis.Element", undefined, { rowFormat: QueryRowFormat.UseECSqlPropertyNames }); const jsRows = await reader.toArray();

Specifying Row Formats

Set config.rowFormat to a QueryRowFormat value. These examples show the three formats; ECSQL Row Formats defines naming, class-ID conversion, and null handling.

QueryRowFormat.UseECSqlPropertyIndexes

This is the default format. reader.toArray() produces arrays of values in SELECT-column order.

for await (const row of iModel.createQueryReader("SELECT ECInstanceId, ECClassId, Parent, LastMod FROM bis.Element WHERE Model.Id=?", QueryBinder.from(["0x10"]), { rowFormat: QueryRowFormat.UseECSqlPropertyIndexes })) { console.log(`ECInstanceId is ${row[0]}`); console.log(`ECClassId is ${row[1]}`); console.log(`Parent is ${row[2]}`); console.log(`LastMod is ${row[3]}`); }

Here is an example using .toArray:

const reader = iModel.createQueryReader("SELECT ECInstanceId,ECClassId,Parent,LastMod FROM bis.Element WHERE Model.Id=?", QueryBinder.from(["0x10"]), { rowFormat: QueryRowFormat.UseECSqlPropertyIndexes }); const jsRows = await reader.toArray(); console.log(jsRows);

Example Output:

Notice that the individual rows are returned as arrays.

[ [ "0x17", "0x8d", null, "2017-07-25T20:44:59.711Z" ], [ "0x18", "0x67", { "Id": "0x17", "RelECClassId": "0x66" }, "2017-07-25T20:44:59.711Z" ] ]

QueryRowFormat.UseECSqlPropertyNames

reader.toArray() produces objects keyed by ECSQL column names or aliases.

for await (const row of iModel.createQueryReader("SELECT ECInstanceId, ECClassId, Parent, LastMod FROM bis.Element WHERE Model.Id=?", QueryBinder.from(["0x10"]), { rowFormat: QueryRowFormat.UseECSqlPropertyNames })) { console.log(`ECInstanceId is ${row.ECInstanceId}`); console.log(`ECClassId is ${row.ECClassId}`); console.log(`Parent is ${row.Parent}`); console.log(`LastMod is ${row.LastMod}`); }

Here is an example using .toArray:

const reader = iModel.createQueryReader("SELECT ECInstanceId,ECClassId,Parent,LastMod FROM bis.Element WHERE Model.Id=?", QueryBinder.from(["0x10"]), { rowFormat: QueryRowFormat.UseECSqlPropertyNames }); const jsRows = await reader.toArray(); console.log(jsRows);

Example Output:

[ { "ECInstanceId": "0x17", "ECClassId": "0x8d", "LastMod": "2017-07-25T20:44:59.711Z" }, { "ECInstanceId": "0x18", "ECClassId": "0x67", "Parent": { "Id": "0x17", "RelECClassId": "0x66" }, "LastMod": "2017-07-25T20:44:59.711Z" } ]

Deprecated QueryRowFormat.UseJsPropertyNames

This legacy format converts unaliased class-ID values to class names and maps property keys to names such as id, className, and navigation relClassName. It is deprecated; use it only while preserving an existing result contract. New queries should use UseECSqlPropertyNames, explicit aliases, and ec_classname() projections. See ECSQL Row Formats.

// eslint-disable-next-line @typescript-eslint/no-deprecated for await (const row of iModel.createQueryReader("SELECT ECInstanceId,ECClassId,Parent,LastMod FROM bis.Element WHERE Model.Id=?", QueryBinder.from(["0x10"]), { rowFormat: QueryRowFormat.UseJsPropertyNames })) { console.log(`ECInstanceId is ${row.id}`); console.log(`ECClassId is ${row.className}`); console.log(`Parent is ${row.parent}`); console.log(`LastMod is ${row.lastMod}`); }

Here is an example using .toArray:

// eslint-disable-next-line @typescript-eslint/no-deprecated const reader = iModel.createQueryReader("SELECT ECInstanceId,ECClassId,Parent,LastMod FROM bis.Element WHERE Model.Id=?", QueryBinder.from(["0x10"]), { rowFormat: QueryRowFormat.UseJsPropertyNames }); const jsRows = await reader.toArray(); console.log(jsRows);

Example Output:

[ { "id": "0x17", "className": "BisCore.SpatialCategory", "lastMod": "2017-07-25T20:44:59.711Z" }, { "id": "0x18", "className": "BisCore.SubCategory", "parent": { "id": "0x17", "relClassName": "BisCore.CategoryOwnsSubCategories" }, "lastMod": "2017-07-25T20:44:59.711Z" } ]

ECInstanceId becomes id, and ECClassId becomes className with a qualified class-name value.

Parameter Bindings

See ECSQL Parameter Types to learn which types to use for the parameters when binding.

Positional parameters

for await (const row of iModel.createQueryReader("SELECT ECInstanceId,ECClassId,Parent,LastMod FROM bis.Element WHERE CodeValue=? AND LastMod>=?", QueryBinder.from(["MyCode", "2018-01-01T12:00:00Z"]), { rowFormat: QueryRowFormat.UseECSqlPropertyNames })) { console.log(`${row.ECInstanceId}, ${row.ECClassId}, ${row.Parent}, ${row.LastMod}`); }

Named parameters

for await (const row of iModel.createQueryReader("SELECT ECInstanceId,ECClassId,Parent,LastMod FROM bis.Element WHERE CodeValue=:code AND LastMod>=:lastmod", QueryBinder.from({ code: "MyCode", lastmod: "2018-01-01T12:00:00Z" }), { rowFormat: QueryRowFormat.UseECSqlPropertyNames })) { console.log(`${row.ECInstanceId}, ${row.ECClassId}, ${row.Parent}, ${row.LastMod}`); }

Navigation properties

Filter navigation properties by their members. For example, bind the related instance ID to a predicate on Parent.Id. Whole navigation-value bindings are not supported by the query readers.

for await (const row of iModel.createQueryReader("SELECT ECInstanceId FROM bis.Element WHERE Parent.Id=?", new QueryBinder().bindId(1, "0x132"), { rowFormat: QueryRowFormat.UseECSqlPropertyNames })) { console.log(`${row.ECInstanceId}`); }

Struct properties

Parameterize individual struct members. Whole-struct bindings are not supported by the query readers. This example uses the sample schema in Struct properties in ECSQL.

for await (const row of iModel.createQueryReader("SELECT Name FROM myschema.Company WHERE Location.Street=? AND Location.Zip=?", QueryBinder.from(["7123 Main Street", 32443]), { rowFormat: QueryRowFormat.UseECSqlPropertyNames })) { console.log(`${row.Name}`); }

ID sets

Use QueryBinder.bindIdSet to bind a set of Id64 values for InVirtualSet:

const binder = new QueryBinder().bindIdSet(1, ["0x1", "0x2"]); for await (const row of iModel.createQueryReader("SELECT ECInstanceId, ECClassId FROM bis.Element WHERE InVirtualSet(?, ECInstanceId)", binder, { rowFormat: QueryRowFormat.UseECSqlPropertyNames })) { console.log(`${row.ECInstanceId}, ${row.ECClassId}`); }

Array properties

The query readers do not support arbitrary ECSQL array-property parameters. ID-set bindings are a separate facility. See parameter support and legacy statement bindings.

Last Updated: 02 October, 2026