Flatten one CSR half (RelationshipEdges) to row-based form — one row
per IfcRel* STEP record that named this (source, target, type) triple,
not one row per deduped edge. Two IfcRel* instances can legally name
the same triple (#3760); RelationshipGraphBuilder.addEdge collapses
that to one edge and keeps the extra ids in shadowedRelationshipIds
rather than dropping them, so an exporter that walked the raw
edgeRelIds column alone (Parquet's Relationships.parquet, the DuckDB
relationships table) would silently drop that STEP record from its
output (#3782 review). Both use this shared helper instead of
hand-rolling the CSR walk so they cannot drift out of sync on it again.
Walks edges.offsets to find source ids, so it needs a real CSR half —
an accessor that only implements getEdges/getTargets/hasAnyEdges
over a facade with empty offsets/counts (e.g. the server-loaded-model
path's hand-rolled RelationshipEdges) yields nothing here, regardless
of what getEdges itself would answer.
Flatten one CSR half (
RelationshipEdges) to row-based form — one row perIfcRel*STEP record that named this (source, target, type) triple, not one row per deduped edge. TwoIfcRel*instances can legally name the same triple (#3760);RelationshipGraphBuilder.addEdgecollapses that to one edge and keeps the extra ids inshadowedRelationshipIdsrather than dropping them, so an exporter that walked the rawedgeRelIdscolumn alone (Parquet'sRelationships.parquet, the DuckDBrelationshipstable) would silently drop that STEP record from its output (#3782 review). Both use this shared helper instead of hand-rolling the CSR walk so they cannot drift out of sync on it again.Walks
edges.offsetsto find source ids, so it needs a real CSR half — an accessor that only implementsgetEdges/getTargets/hasAnyEdgesover a facade with emptyoffsets/counts(e.g. the server-loaded-model path's hand-rolledRelationshipEdges) yields nothing here, regardless of whatgetEdgesitself would answer.