Skip to content

Add imodel_geom_stream virtual table for GeometryStream decomposition#9239

Draft
khanaffan wants to merge 4 commits into
masterfrom
affan.khan/geom-stream-vtab-and-scalar-fns
Draft

Add imodel_geom_stream virtual table for GeometryStream decomposition#9239
khanaffan wants to merge 4 commits into
masterfrom
affan.khan/geom-stream-vtab-and-scalar-fns

Conversation

@khanaffan
Copy link
Copy Markdown
Contributor

@khanaffan khanaffan commented Apr 24, 2026

Native PR: iTwin/imodel-native#1396

Summary

Wires up the native imodel_geom_stream virtual table and five companion scalar functions into the iTwin.js TypeScript backend, enabling inspection and analytics over element geometry using pure ECSQL queries.

TypeScript changes

core/backend/src/IModelHost.ts

  • Added maxGeomStreamVTabBytes?: number to IModelHostOptions
  • Added IModelHostConfiguration.defaultMaxGeomStreamVTabBytes (50 MB)
  • IModelHost.startup() calls setMaxGeomStreamVTabBytes on the native layer
  • Added IModelHost.maxGeomStreamVTabBytes getter

core/backend/src/test/standalone/GeomStreamVTab.test.ts (new)
24 standalone tests covering the vtab, all five scalar functions, size-limit enforcement, and JSON round-trips.


New Virtual Table

imodel_geom_stream(GeometryStream)

Decomposes a raw GeometryStream blob into one row per geometry entry. Requires ECSQLOPTIONS ENABLE_EXPERIMENTAL_FEATURES appended to the query.

Column Type Description
EntryIndex int Zero-based position of the entry in the stream
OpCode string Raw opcode name (e.g. PointPrimitive, ParasolidBRep)
EntryType string Friendly type name (e.g. Arc3d, BRepEntity, Header, GeometryPart)
IsGeometry int 1 if this entry is a renderable geometry primitive
SubCategoryId long Active sub-category ID from accumulated symbology
Color int Override color (TBGR)
Weight int Line weight override
LineStyle int Line style override
Transparency double Transparency override (0–1)
GeomClass int Geometry class
DisplayPriority int Display priority override
MaterialId long Material ID override
GeometryPartId long ID of the referenced GeometryPart element (part instances only)
PartOriginX/Y/Z double Part instance origin
PartYaw/Pitch/Roll double Part instance orientation (degrees)
PartScale double Uniform part instance scale
RangeLowX/Y/Z double Sub-graphic range low corner (NULL if no SubGraphicRange opcode)
RangeHighX/Y/Z double Sub-graphic range high corner (NULL if no SubGraphicRange opcode)
HeaderFlags int Geometry stream header flags
TextContent string Text string content (TextString entries only)
GeometryBlob blob Raw [4-byte opcode][flatbuffer payload] — pass to imodel_geom_json()

Example queries:

-- Iterate every geometry entry in a specific element
SELECT gs.EntryIndex, gs.EntryType, gs.IsGeometry
FROM BisCore.GeometricElement3d e, imodel_geom_stream(e.GeometryStream) gs
WHERE e.ECInstanceId = ?
ECSQLOPTIONS ENABLE_EXPERIMENTAL_FEATURES

-- Count geometry primitives per element
SELECT e.ECInstanceId, COUNT(*) AS GeomCount
FROM BisCore.GeometricElement3d e, imodel_geom_stream(e.GeometryStream) gs
WHERE gs.IsGeometry = 1
GROUP BY e.ECInstanceId
ECSQLOPTIONS ENABLE_EXPERIMENTAL_FEATURES

-- Find all elements with BRep geometry using the virtual table
SELECT DISTINCT e.ECInstanceId
FROM BisCore.GeometricElement3d e, imodel_geom_stream(e.GeometryStream) gs
WHERE gs.EntryType = 'BRepEntity'
ECSQLOPTIONS ENABLE_EXPERIMENTAL_FEATURES

-- Extract per-entry bounding boxes
SELECT e.ECInstanceId, gs.EntryIndex,
       gs.RangeLowX, gs.RangeLowY, gs.RangeLowZ,
       gs.RangeHighX, gs.RangeHighY, gs.RangeHighZ
FROM BisCore.GeometricElement3d e, imodel_geom_stream(e.GeometryStream) gs
WHERE gs.IsGeometry = 1
ECSQLOPTIONS ENABLE_EXPERIMENTAL_FEATURES

-- List all GeometryPart references with placement
SELECT e.ECInstanceId, gs.GeometryPartId, gs.PartOriginX, gs.PartOriginY, gs.PartOriginZ, gs.PartScale
FROM BisCore.GeometricElement3d e, imodel_geom_stream(e.GeometryStream) gs
WHERE gs.EntryType = 'GeometryPart'
ECSQLOPTIONS ENABLE_EXPERIMENTAL_FEATURES

-- Read text annotations from geometry streams
SELECT e.ECInstanceId, gs.TextContent
FROM BisCore.GeometricElement3d e, imodel_geom_stream(e.GeometryStream) gs
WHERE gs.EntryType = 'TextString' AND gs.TextContent IS NOT NULL
ECSQLOPTIONS ENABLE_EXPERIMENTAL_FEATURES

New Scalar Functions

imodel_geom_json(GeometryBlob) → TEXT

Decodes a GeometryBlob column value (from imodel_geom_stream) into an iModel.js-compatible geometry JSON string. BRep entries return NULL.

-- Convert each geometry entry to iModel.js geometry JSON
SELECT gs.EntryIndex, gs.EntryType, imodel_geom_json(gs.GeometryBlob) AS GeomJSON
FROM BisCore.GeometricElement3d e, imodel_geom_stream(e.GeometryStream) gs
WHERE e.ECInstanceId = ?
  AND gs.IsGeometry = 1
  AND gs.EntryType != 'BRepEntity'
ECSQLOPTIONS ENABLE_EXPERIMENTAL_FEATURES

imodel_geom_text(GeometryStream) → TEXT

Extracts all TextString content from a raw GeometryStream blob. Multiple text entries are joined with newlines. Returns NULL if none found.

-- Find all elements containing specific text in their geometry
SELECT e.ECInstanceId, imodel_geom_text(e.GeometryStream) AS TextContent
FROM BisCore.GeometricElement3d e
WHERE imodel_geom_text(e.GeometryStream) LIKE '%valve%'

imodel_geom_entry_count(GeometryStream) → INTEGER

Counts renderable geometry primitives (IsGeometry = 1) in a raw GeometryStream blob without full deserialization. Returns NULL on failure.

-- Find elements with more than 10 geometry entries
SELECT e.ECInstanceId, imodel_geom_entry_count(e.GeometryStream) AS GeomCount
FROM BisCore.GeometricElement3d e
WHERE imodel_geom_entry_count(e.GeometryStream) > 10
ORDER BY GeomCount DESC

imodel_geom_part_ids(GeometryStream) → TEXT

Returns a JSON array of all GeometryPartId references (as hex strings) in a raw GeometryStream blob. Returns NULL if none found.

-- List elements and the GeometryPart IDs they reference
SELECT e.ECInstanceId, imodel_geom_part_ids(e.GeometryStream) AS PartIds
FROM BisCore.GeometricElement3d e
WHERE imodel_geom_part_ids(e.GeometryStream) IS NOT NULL

-- Find all elements that reference a specific GeometryPart (by hex id)
SELECT e.ECInstanceId
FROM BisCore.GeometricElement3d e
WHERE imodel_geom_part_ids(e.GeometryStream) LIKE '%0x1a3%'

imodel_geom_has_brep(GeometryStream) → INTEGER

Returns 1 if the GeometryStream contains any Parasolid BRep geometry, 0 otherwise. Returns NULL on failure.

-- Find all elements that contain BRep geometry
SELECT e.ECInstanceId
FROM BisCore.GeometricElement3d e
WHERE imodel_geom_has_brep(e.GeometryStream) = 1

-- Count BRep vs non-BRep elements in a model
SELECT imodel_geom_has_brep(e.GeometryStream) AS HasBRep, COUNT(*) AS ElementCount
FROM BisCore.GeometricElement3d e
GROUP BY HasBRep

Size limit

The process-wide limit on how large a GeometryStream blob may be before the vtab skips it can be configured at startup:

await IModelHost.startup({ maxGeomStreamVTabBytes: 100 * 1024 * 1024 }); // 100 MB

Default is 50 MB (IModelHostConfiguration.defaultMaxGeomStreamVTabBytes). The native layer enforces a minimum of 4 KB.

Validation

  • Targeted verification: All 24 GeomStreamVTab tests pass locally (npx mocha --timeout 60000 --grep GeomStreamVTab lib/cjs/test/standalone/GeomStreamVTab.test.js)
  • Known baseline issues: 1 pre-existing flaky timeout in ECSqlQuery suite (unrelated); 39 skipped tests in core-backend are pre-existing

…functions

- Add `maxGeomStreamVTabBytes` option to `IModelHostOptions` and
  `IModelHostConfiguration` (default 50 MB) wired into native
  `setMaxGeomStreamVTabBytes` during `IModelHost.startup()`
- Add `IModelHost.maxGeomStreamVTabBytes` getter backed by native API
- Add standalone tests for the `imodel_geom_stream` virtual table and
  the five new scalar functions:
  - `imodel_geom_json` — per-entry geometry blob → iModel.js JSON
  - `imodel_geom_entry_count` — entry count from raw geometry stream
  - `imodel_geom_has_brep` — 0/1 BRep presence flag
  - `imodel_geom_part_ids` — JSON array of referenced part hex IDs

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@khanaffan khanaffan changed the title feat(@itwin/core-backend): expose imodel_geom_stream vtab and scalar functions Add imodel_geom_stream virtual table for GeometryStream decomposition Apr 24, 2026
@khanaffan khanaffan marked this pull request as ready for review April 24, 2026 22:11
@khanaffan khanaffan requested review from a team as code owners April 24, 2026 22:11
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds backend support and documentation for the native imodel_geom_stream virtual table and related GeometryStream ECSQL helpers, including a new host-level size-limit configuration to guard against extremely large GeometryStream blobs.

Changes:

  • Add maxGeomStreamVTabBytes startup option + default constant + runtime getter on IModelHost.
  • Add a standalone backend test suite for the vtab and several scalar functions.
  • Add new learning docs + changelog entry describing the ECSQL virtual table/functions and usage constraints (withQueryReader, experimental flag).

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
docs/learning/backend/index.md Adds navigation link to the new GeometryStream ECSQL documentation page.
docs/learning/backend/WithQueryReaderCodeExamples.md Cross-links GeometryStream ECSQL functions from withQueryReader examples.
docs/learning/backend/GeometryStreamFunctions.md New documentation page describing the vtab/functions, constraints, and examples.
docs/changehistory/NextVersion.md Adds changelog entry introducing the GeometryStream ECSQL functionality.
core/backend/src/test/standalone/GeomStreamVTab.test.ts Adds standalone tests for vtab + scalar functions and size-limit configuration.
core/backend/src/IModelHost.ts Introduces maxGeomStreamVTabBytes option, default, native startup wiring, and getter.
common/changes/@itwin/core-backend/affan.khan-geom-stream-vtab-and-scalar-fns_2026-04-24-22-00.json Rush change file documenting the update.
common/api/core-backend.api.md Updates extracted API surface for the new @beta APIs.

const ecsql = `
SELECT e.ECInstanceId, imodel_geom_part_ids(e.GeometryStream) AS partIds
FROM BisCore.GeometricElement3d e
WHERE partIds IS NOT NULL`;

> **Backend only.** These functions call into the iModel native layer and must be executed via [IModelDb.withQueryReader]($backend) or [ECDb.withQueryReader]($backend) — the synchronous, backend-only reader. They are **not available** through the async `createQueryReader` / [IModelConnection.createQueryReader]($frontend) path.

> **50 MB limit — silent skip.** By default, geometry streams larger than **50 MB** (uncompressed) are silently skipped — the function returns zero rows or `NULL` for that element. This is not an error. You can raise the limit via [IModelHostConfiguration.maxGeomStreamVTabBytes]($backend).
Comment on lines +22 to +26
| [`imodel_geom_stream(blob)`](#imodel_geom_stream) | Virtual table | `GeometryStream` column | One row per geometry entry |
| [`imodel_geom_json(blob)`](#imodel_geom_json) | Scalar | `GeometryBlob` column from vtab | iTwin.js JSON string, or `NULL` for BRep |
| [`imodel_geom_entry_count(blob)`](#imodel_geom_entry_count) | Scalar | `GeometryStream` column | `INTEGER` — total primitive count |
| [`imodel_geom_has_brep(blob)`](#imodel_geom_has_brep) | Scalar | `GeometryStream` column | `0` or `1` |
| [`imodel_geom_part_ids(blob)`](#imodel_geom_part_ids) | Scalar | `GeometryStream` column | JSON array of hex IDs, or `NULL` |
}

/**
* The current process-wide maximum uncompressed GeometryStream size in bytes that the `dgn_geom_stream`
Comment on lines +113 to +115
Five new built-in ECSQL functions make it possible to inspect, count, classify, and convert element geometry streams entirely inside an ECSQL query — without loading the element into TypeScript or round-tripping through the geometry API.

> **Backend only.** These functions call into the iModel native layer and must be executed via [IModelDb.withQueryReader]($backend) or [ECDb.withQueryReader]($backend) — the synchronous, backend-only reader. They are **not available** through the async `createQueryReader` / [IModelConnection.createQueryReader]($frontend) path.
Comment on lines +158 to +162
/** The process-wide maximum uncompressed GeometryStream size in bytes that the `dgn_geom_stream` virtual table will decompose.
* Blobs exceeding this limit are silently skipped (the vtab returns zero rows for them).
* Defaults to 50 MB. Minimum enforced by native layer: 4 KB.
* @beta
*/
// Count geometry primitives per element (no vtab join needed)
iModel.withQueryReader(
`SELECT e.ECInstanceId, imodel_geom_entry_count(e.GeometryStream) AS cnt
FROM BisCore.GeometricElement3d e WHERE cnt > 1`,
Comment on lines +323 to +325
// ─── imodel_geom_* scalar functions ─────────────────────────────────────────

describe("imodel_geom_json scalar function", () => {
Comment on lines +260 to +281
// Insert a simple element
const builder = new GeometryStreamBuilder();
builder.appendGeometry(Arc3d.createXY(Point3d.createZero(), 1));
const elemId = insertElement(imodel, txn, builder.geometryStream);
txn.saveChanges();

// Confirm rows are returned at the default limit
const rowsBefore = queryGeomStreamRows(imodel, elemId);
expect(rowsBefore.length).to.be.greaterThan(0);

// Shrink the limit to 4 KB (the enforced minimum — still larger than our tiny blob is fine,
// but if we set it extremely small the vtab should silently skip)
const tinyLimit = 4096;
IModelNative.platform.setMaxGeomStreamVTabBytes(tinyLimit);

// Rows should still appear because our test geometry is tiny; the limit only skips very large blobs.
// This test confirms the API is callable and the vtab still works for small streams.
const rowsAfterRestrict = queryGeomStreamRows(imodel, elemId);
expect(rowsAfterRestrict.length).to.be.greaterThanOrEqual(0, "vtab should not throw, just skip or return rows");

// Restore default
IModelNative.platform.setMaxGeomStreamVTabBytes(IModelHostConfiguration.defaultMaxGeomStreamVTabBytes);

> **Experimental feature flag required.** Queries that use the `imodel_geom_stream` virtual table must include the `ECSQLOPTIONS ENABLE_EXPERIMENTAL_FEATURES` clause. The scalar functions (`imodel_geom_json`, `imodel_geom_entry_count`, `imodel_geom_has_brep`, `imodel_geom_part_ids`) do not require this flag.

> **50 MB size limit — silent skip, not an error.** By default, any geometry stream whose uncompressed size exceeds **50 MB** is silently ignored by all geometry stream functions. `imodel_geom_stream` returns zero rows for that element, and the scalar functions return `NULL`. No exception is thrown and the query continues normally with remaining rows. If you are working with unusually large geometry streams and expect empty results, check whether the stream size exceeds this limit. The limit can be raised via [IModelHostConfiguration.maxGeomStreamVTabBytes]($backend) at startup or read at runtime with [IModelHost.maxGeomStreamVTabBytes]($backend).
@khanaffan khanaffan marked this pull request as draft April 27, 2026 14:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants