Add imodel_geom_stream virtual table for GeometryStream decomposition#9239
Draft
khanaffan wants to merge 4 commits into
Draft
Add imodel_geom_stream virtual table for GeometryStream decomposition#9239khanaffan wants to merge 4 commits into
khanaffan wants to merge 4 commits into
Conversation
…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>
Contributor
There was a problem hiding this comment.
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
maxGeomStreamVTabBytesstartup option + default constant + runtime getter onIModelHost. - 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). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Native PR: iTwin/imodel-native#1396
Summary
Wires up the native
imodel_geom_streamvirtual 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.tsmaxGeomStreamVTabBytes?: numbertoIModelHostOptionsIModelHostConfiguration.defaultMaxGeomStreamVTabBytes(50 MB)IModelHost.startup()callssetMaxGeomStreamVTabByteson the native layerIModelHost.maxGeomStreamVTabBytesgettercore/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_FEATURESappended to the query.EntryIndexOpCodePointPrimitive,ParasolidBRep)EntryTypeArc3d,BRepEntity,Header,GeometryPart)IsGeometrySubCategoryIdColorWeightLineStyleTransparencyGeomClassDisplayPriorityMaterialIdGeometryPartIdPartOriginX/Y/ZPartYaw/Pitch/RollPartScaleRangeLowX/Y/ZSubGraphicRangeopcode)RangeHighX/Y/ZSubGraphicRangeopcode)HeaderFlagsTextContentGeometryBlob[4-byte opcode][flatbuffer payload]— pass toimodel_geom_json()Example queries:
New Scalar Functions
imodel_geom_json(GeometryBlob) → TEXTDecodes a
GeometryBlobcolumn value (fromimodel_geom_stream) into an iModel.js-compatible geometry JSON string. BRep entries returnNULL.imodel_geom_text(GeometryStream) → TEXTExtracts all
TextStringcontent from a raw GeometryStream blob. Multiple text entries are joined with newlines. ReturnsNULLif none found.imodel_geom_entry_count(GeometryStream) → INTEGERCounts renderable geometry primitives (
IsGeometry = 1) in a raw GeometryStream blob without full deserialization. ReturnsNULLon failure.imodel_geom_part_ids(GeometryStream) → TEXTReturns a JSON array of all
GeometryPartIdreferences (as hex strings) in a raw GeometryStream blob. ReturnsNULLif none found.imodel_geom_has_brep(GeometryStream) → INTEGERReturns
1if the GeometryStream contains any Parasolid BRep geometry,0otherwise. ReturnsNULLon failure.Size limit
The process-wide limit on how large a GeometryStream blob may be before the vtab skips it can be configured at startup:
Default is 50 MB (
IModelHostConfiguration.defaultMaxGeomStreamVTabBytes). The native layer enforces a minimum of 4 KB.Validation
GeomStreamVTabtests pass locally (npx mocha --timeout 60000 --grep GeomStreamVTab lib/cjs/test/standalone/GeomStreamVTab.test.js)ECSqlQuerysuite (unrelated); 39 skipped tests in core-backend are pre-existing