// Builder finalization: the mutable staging area becomes an immutable// Snapshot -- parallel CSR build (both directions), label/type indexes, and// per-column dense / rank-select / sparse storage selection. The rank/// select thresholds and the str dense rule mirror the Rust finalize;// i64/f64/bool dense selection deliberately diverges to full-coverage-only// (missingness, tasks/041 -- mirror task filed in the Rust repo), so a// staged input with partially-filled numeric/bool columns no longer// finalizes byte-identically with Rust until they converge.package chickpeasimport ()// rankSelectMinLen is the minimum column span before a moderately-sparse// column is stored rank/select instead of binary-searched sparse. Below it// the sparse array stays cache-friendly and the rank index isn't worth its// cost.const rankSelectMinLen = 1_000_000// rankSelectWorth: the span must be large enough to matter, and the// rank/select layout (presence bits + packed valueBits-bit values) must not// use more memory than the sparse pair array (sparsePairBytes per entry,// including alignment padding).func rankSelectWorth(, , , int) bool {if < rankSelectMinLen {returnfalse } := + * := * * 8return <= }// sortPairsLastWriteWins sorts staged pairs by position keeping the LAST// staged write per position, so re-set properties resolve deterministically// to the newest value (the dense fill loop already behaves this way).func sortPairsLastWriteWins[ any]( [], func() uint32) [] {sort.SliceStable(, func(, int) bool { return ([]) < ([]) }) := [:0]for , := range {if +1 < len() && ([+1]) == () {continue// a later write for the same position follows } = append(, ) }return}// denseThreshold (i64/f64/bool): only a FULL column (every position// staged) stores dense. Those dense layouts have no presence// representation -- an in-range read always reports present -- so storing// a partially-filled column dense silently turns "missing" into the zero// value, and observable property semantics (IS NULL, comparisons,// aggregates, the monotonic walk) would shift with the storage heuristic.// A partial column goes to rank-select or sparse, which represent absence// exactly. This diverges from the Rust finalize's historical >=80% rule// (tasks/041; a mirror task is filed in the Rust repo). Reading a legacy// file whose dense column was written at partial fill still reports the// destroyed positions as present-zero: that information is gone.//// pairCount counts staged writes, so duplicate writes to one position can// reach span without full coverage; the column builders confirm with// coversAllPositions before committing to dense.func denseThreshold(, int) bool {return >= }// denseStrThreshold: str keeps the historical >=80% rule -- the dense str// layout encodes missing as atom 0 by design (the read layer folds the// empty-string check into Prop.Str), so partial fill loses nothing that// the format doesn't already conflate. The gate counts DISTINCT staged// positions, not writes (the caller passes distinctPositions), matching// the Rust finalize's tasks/237 rule so str selection stays// byte-identical across the two writers.func denseStrThreshold(, int) bool {return >= int(float64()*0.8)}// distinctPositions counts the distinct positions the pairs touch -- the// dense gates' real quantity: a staged-write count can reach any// threshold through duplicate writes to a few positions while the fill// stays tiny (rustychickpeas tasks/237).func distinctPositions[ interface{ () uint32 }]( [], int) int { := bitset.New() := 0for , := range {if := int(.()); !.Get() { .Set(, true) ++ } }return}// coversAllPositions reports whether the pairs touch every position of the// span (the exactness check behind the dense selection).func coversAllPositions[ interface{ () uint32 }]( [], int) bool {returndistinctPositions(, ) == }func columnFromPairsI64( []i64Pair, int) Column {ifdenseThreshold(len(), ) && coversAllPositions(, ) { := make([]int64, )for , := range { [.id] = .val }returnnarrowI64Column() } = sortPairsLastWriteWins(, func( i64Pair) uint32 { return .id }) , := make([]uint32, len()), make([]int64, len())for , := range { [], [] = .id, .val }ifrankSelectWorth(, len(), 64, 16) {if , := narrowI64Vals(); {returnrankI64NarrowCol{rankIndex: buildRankIndex(, ), narrowI64Vec: } }returnrankI64Col{rankIndex: buildRankIndex(, ), vals: } }if , := narrowI64Vals(); {returnsparseI64NarrowCol{ids: , narrowI64Vec: } }returnsparseI64Col{ids: , vals: }}func columnFromPairsF64( []f64Pair, int) Column {ifdenseThreshold(len(), ) && coversAllPositions(, ) { := make(denseF64Col, )for , := range { [.id] = .val }return } = sortPairsLastWriteWins(, func( f64Pair) uint32 { return .id }) , := make([]uint32, len()), make([]float64, len())for , := range { [], [] = .id, .val }ifrankSelectWorth(, len(), 64, 16) {returnrankF64Col{rankIndex: buildRankIndex(, ), vals: } }returnsparseF64Col{ids: , vals: }}func columnFromPairsBool( []boolPair, int) Column {ifdenseThreshold(len(), ) && coversAllPositions(, ) { := bitset.New()for , := range { .Set(int(.id), .val) }returndenseBoolCol{bits: } } = sortPairsLastWriteWins(, func( boolPair) uint32 { return .id }) := make([]uint32, len())for , := range { [] = .id }ifrankSelectWorth(, len(), 1, 8) { := bitset.New(len())for , := range { .Set(, .val) }returnrankBoolCol{rankIndex: buildRankIndex(, ), vals: } } := make([]bool, len())for , := range { [] = .val }returnsparseBoolCol{ids: , vals: }}func columnFromPairsStr( []strPair, int) Column {ifdenseStrThreshold(distinctPositions(, ), ) { := make(denseStrCol, )for , := range { [.id] = .val }return } = sortPairsLastWriteWins(, func( strPair) uint32 { return .id }) , := make([]uint32, len()), make([]uint32, len())for , := range { [], [] = .id, .val }ifrankSelectWorth(, len(), 32, 8) {returnrankStrCol{rankIndex: buildRankIndex(, ), vals: } }returnsparseStrCol{ids: , vals: }}// buildCSRDirection fills one CSR direction by counting sort, preserving// rel insertion order within each node's range, plus the rel-index -> CSR// position map.func buildCSRDirection( int, [][2]NodeID, []RelType, []uint32, int) ( []uint32, []NodeID, []RelType, []uint32) { := len() = make([]uint32, +1)for := range { [+1] = [] + [] } = make([]NodeID, ) = make([]RelType, ) = make([]uint32, ) := make([]uint32, )copy(, [:])for , := range { , := [], [1-] := [] [] = [] = [] [] = []++ }return , , , }// buildTypeIndex indexes rels by outgoing-CSR position per type (FORMAT.md// section 4), built from the finalized outTypes so it stays correct when rels// are staged out of source order -- mirroring the Rust fix (rustychickpeas// 96243bb).func buildTypeIndex( *Snapshot) { := map[RelType][]uint32{}for , := range .outTypes { [] = append([], uint32()) }for , := range { := roaring.New() .AddMany() // ascending CSR order, duplicate-free .typeIndex[] = nodeset.FromBitmap() }}// finalizeLabels builds the per-label node bitmaps, sharing the source// snapshot's bitmap for every label whose membership is untouched. With no// dirty label the whole index aliases and the O(n) label scan is skipped.func ( *Builder) ( *Snapshot, int, *aliasPlan) {if .src != nil && .labels {maps.Copy(.labelIndex, .src.labelIndex) .allLabels = truereturn } := map[Label][]uint32{}for , := range .nodeLabels[:min(, len(.nodeLabels))] {for , := range {if .labelClean() {continue } [] = append([], uint32()) } }if .src != nil {for , := range .src.labelIndex {if .labelClean() { .labelIndex[] = .aliasedLabels[] = struct{}{} } } }for , := range { := roaring.New() .AddMany() // ascending and deduped by construction order .labelIndex[] = nodeset.FromBitmap() }}// finalizeNodeColumns stores every staged node column, sharing the source// snapshot's column for each untouched key.func finalizeNodeColumns[ any]( *Snapshot, *aliasPlan, map[PropertyKey][], int, func([], int) Column) {for , := range {if , := .nodeColumn(); { .columns[] = continue } .columns[] = (, ) }}// stagedLens is a staged column family reduced to per-key pair counts,// feeding the cross-typed drop diagnostic.func stagedLens[ any]( map[PropertyKey][]) map[PropertyKey]int {iflen() == 0 {returnnil } := make(map[PropertyKey]int, len())for , := range { [] = len() }return}// crossTypedDropCount counts the staged pairs a multi-kind key discards:// the column families finalize in a fixed order into one column map, so// for a key staged under several kinds only the LAST family's pairs// survive and every earlier family's pairs are dropped. Families must be// passed in finalize order.func crossTypedDropCount( ...map[PropertyKey]int) int { := map[PropertyKey]int{} := map[PropertyKey]int{} := map[PropertyKey]bool{}for , := range {for , := range {if , := []; { [] = true } [] += [] = } } := 0for := range { += [] - [] }return}// finalizeRelColumns stores every staged rel column, sharing the source's for// each untouched key. A rebuilt column remaps its staged rel indexes to// outgoing-CSR positions, the addressing the snapshot stores rel properties// under.func finalizeRelColumns[ relPair[]]( *Snapshot, *aliasPlan, map[PropertyKey][], int, []uint32, func([], int) Column) {for , := range {if , := .relColumn(); { .relColumns[] = continue } := make([], len())for , := range { [] = .withPairID([.pairID()]) } .relColumns[] = (, ) }}// Finalize consumes the builder into an immutable Snapshot; the builder// must not be used afterwards. indexProperties optionally names property// keys whose (label, key) equality indexes are built upfront (faster first// queries, more memory); all others build lazily on first access.//// A builder thawed from a snapshot rebuilds only the components it dirtied// and shares the rest with its source (cow.go), so a property-only edit skips// the O(m) CSR rebuild and every untouched column, and the source's lazy// caches carry forward.func ( *Builder) ( ...string) *Snapshot {// Fold pending removals (rel tombstones + detach-delete cascades) into // the staging state first; a no-op when nothing was removed. .compactRemovals()// The CSR id space covers 0..=maxUsedNodeID (at least one slot). := 1if !.knownNodes.IsEmpty() { = int(.knownNodes.Maximum()) + 1 } := len(.rels) := newSnapshot() .nNodes = uint32(.knownNodes.GetCardinality()) .nRels = uint64() .version = .version// Retain the known-nodes set: on a sparse id space it is the only // record distinguishing nodes from gaps (NodeExists). .existence = nodeset.FromBitmap(.knownNodes.Clone()) := .newAliasPlan(, )// Rel properties are stored by outgoing-CSR position, so the successor // needs the incoming -> outgoing map whenever it has any. := len(.relColI64)+len(.relColF64)+len(.relColBool)+len(.relColStr) > 0var []uint32if .csr { .aliasCSR(, ) = .srcRelToOutCSR .finalizeLabels(, , ) } else {// The three build phases are independent pure reads of the staging // state, so they run as a parallel join; results are deterministic.parallel.Join(func() { .outOffsets, .outNbrs, .outTypes, =buildCSRDirection(, .rels, .relTypes, .degOut, 0) },func() {// The in-direction rel->CSR map fed only the eager inToOut // build, now lazy (getInToOut re-derives from the CSR). .inOffsets, .inNbrs, .inTypes, _ =buildCSRDirection(, .rels, .relTypes, .degIn, 1) },func() { .finalizeLabels(, , ) }, )buildTypeIndex()// inToOut is built lazily on the first rel-position read (getInToOut), // not materialized here -- a loaded graph never queried for rel // properties keeps its ~64MB (SF1). Record only whether it is needed. .hasRelProps = }finalizeNodeColumns(, , .nodeColI64, , columnFromPairsI64)finalizeNodeColumns(, , .nodeColF64, , columnFromPairsF64)finalizeNodeColumns(, , .nodeColBool, , columnFromPairsBool)finalizeNodeColumns(, , .nodeColStr, , columnFromPairsStr)finalizeRelColumns(, , .relColI64, , , columnFromPairsI64)finalizeRelColumns(, , .relColF64, , , columnFromPairsF64)finalizeRelColumns(, , .relColBool, , , columnFromPairsBool)finalizeRelColumns(, , .relColStr, , , columnFromPairsStr)// The family order above is the diagnostic's authority: later kinds // overwrite earlier ones per key, so the count must mirror it. .droppedCrossTyped = crossTypedDropCount(stagedLens(.nodeColI64), stagedLens(.nodeColF64),stagedLens(.nodeColBool), stagedLens(.nodeColStr), ) + crossTypedDropCount(stagedLens(.relColI64), stagedLens(.relColF64),stagedLens(.relColBool), stagedLens(.relColStr), ) .atoms = .aliasAtoms() .carryLazyCaches()// Eagerly build the requested equality indexes from the finished // columns -- identical to what the lazy path would build on demand, and // skipped for an index already carried forward from the source.for , := range { , := .atoms.ID()if ! {continue } , := .columns[]if ! {continue }for , := range .labelIndex { := propIndexKey{label: , key: }if , := .propIndex[]; {continue } .propIndex[] = buildPropValueIndex(, ) } }return}
The pages are generated with Goldsv0.8.7. (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu.
PR and bug reports are welcome and can be submitted to the issue list.
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds.