Source File
graphview.go
Belonging Package
github.com/freeeve/libcodex/rdf
package rdfimport// This file gives Dataset a zero-copy read path. Dataset.Graph materializes one// Triple per matching quad, which at corpus scale dominates the allocation// profile of any consumer that splits a dataset into its named graphs and then// only reads them. GraphView answers the same queries over positions into the// dataset's own Quads slice, so a view costs a few bytes per statement instead// of a whole Triple (three Terms) per statement.// GraphQuery is the read-only query surface shared by *Graph and *GraphView, so// a consumer can write one function over a materialized graph and a view alike.type GraphQuery interface {SubjectsOfType(typeIRI string) []TermObjects(subject Term, predicate string) []TermObject(subject Term, predicate string) (Term, bool)Literal(subject Term, predicate string) (string, bool)HasType(subject Term, typeIRI string) bool}var (_ GraphQuery = (*Graph)(nil)_ GraphQuery = (*GraphView)(nil))// GraphView is a read-only view of the statements in one named graph of a// Dataset. It answers the Graph query surface without copying any triple: its// lazy index holds int32 positions into the dataset's Quads, the same shape as// Graph's subject index. Pass a zero-value graph term to view the default graph.//// A view borrows the dataset, so the dataset must outlive it. Appending to the// dataset invalidates the view's index; the next query rebuilds it, so a view// stays correct across Add at the cost of one reindex. Views are not safe for// concurrent use with a writer, and the first query on a view builds the index,// so concurrent readers must either share a view whose index is already built// or hold one view each.type GraphView struct {d *Datasetgraph Termspo map[Term][]int32 // subject -> positions in d.Quads, carved from one shared arenagen int // len(d.Quads) when spo was built, to detect appendsspoOK bool}// GraphView returns a read-only, zero-copy view of the given graph term; pass a// zero-value term for the default graph. It is the allocation-free counterpart// to Graph, which materializes the same statements as a []Triple copy.func ( *Dataset) ( Term) *GraphView {return &GraphView{d: , graph: }}// subjectIndex builds the view's lazy subject index on first subject-keyed query,// in two passes mirroring Graph.index: count this graph's quads per subject, then// carve each subject's bucket from one shared arena at exactly its final size.// Buckets hold int32 positions into the dataset's Quads, so the index costs a few// bytes per statement and copies no triple. Both passes skip quads belonging to// other graphs, so the index is sized to the view rather than to the dataset.//// The whole-graph scans (Len, Triples, SubjectsOfType) deliberately do not go// through here: they need no subject keying, and building the map for them would// cost more than the scan itself.func ( *GraphView) () map[Term][]int32 {if .spoOK && .gen == len(.d.Quads) {return .spo}:= .d.Quads:= 0:= make(map[Term]int32, len()/8+1)for := range {if [].G == .graph {++[[].S]++}}:= make([]int32, 0, ):= make(map[Term][]int32, len())for := range {if [].G != .graph {continue}:= [].S, := []if ! {:= len() + int([])= [len():len():]= [:]}[] = append(, int32())}.spo, .gen, .spoOK = , len(), truereturn}// Len returns the number of statements in the view's graph. It reads the// dataset's cached per-graph counts, so it costs no scan after the first call on// that dataset, however many graphs are asked about.func ( *GraphView) () int { return .d.GraphLen(.graph) }// Empty reports whether the view's graph holds no statements. Like Len it answers// from the dataset's cached counts, so a consumer that reads a graph only when it// is populated — an editorial overlay, say — pays nothing to skip an absent one.func ( *GraphView) () bool { return !.d.HasGraph(.graph) }// GraphTerm returns the graph term this view is scoped to.func ( *GraphView) () Term { return .graph }// Triples iterates the view's statements in document order, projecting each quad// onto its triple. It builds no index and materializes no slice, yielding one// Triple value at a time from the dataset's quads; the only allocation is the// iterator closure itself, a fixed ~56 bytes per call regardless of graph size.//// The cost that matters is the pass, not the yield. Triples filters the whole// dataset, so walking it is one full-dataset pass per view — reading N graphs out// of one dataset costs N passes, even for graphs that turn out to be empty. Code// that consumes several graphs at once can beat that by fusing the dispatch into// a single hand-written pass over Dataset.Quads switching on each quad's graph// term. Use Empty to skip a graph without walking it at all: it reads the// dataset's cached counts and never scans.//// Per-triple overhead, by contrast, is not worth avoiding: in this package's// Corpus/Grain/SingleGraph Triples benchmarks the iterator beats an equivalent// hand-written single-graph loop at both corpus and per-grain scale, even against// a single-graph dataset whose loop needs no filter at all.func ( *GraphView) () iter.Seq[Triple] {return func( func(Triple) bool) {for := range .d.Quads {if := &.d.Quads[]; .G == .graph {if !(.Triple()) {return}}}}}// Objects returns the distinct objects of every statement in this graph with the// given subject and predicate IRI, in document order. As with [Graph.Objects], a// repeated statement yields its object once.func ( *GraphView) ( Term, string) []Term {:= .subjectIndex()[]var []Term:= objectSet{hint: len()}for , := range {if := &.d.Quads[]; .P.Kind == IRI && .P.Value == {= .append(, .O)}}return}// Object returns the first object for (subject, predicate) in this graph, or// false. It scans the index without allocating an intermediate slice.func ( *GraphView) ( Term, string) (Term, bool) {for , := range .subjectIndex()[] {if := &.d.Quads[]; .P.Kind == IRI && .P.Value == {return .O, true}}return Term{}, false}// HasType reports whether the subject has rdf:type typeIRI in this graph. It// scans the index without allocating.func ( *GraphView) ( Term, string) bool {for , := range .subjectIndex()[] {if := &.d.Quads[]; .P.Kind == IRI && .P.Value == TypeIRI && .O.Kind == IRI && .O.Value == {return true}}return false}// Literal returns the value of the subject's first literal object for the// predicate in this graph, or false. It scans the index without allocating an// intermediate slice, serving the frequent single-value field reads.func ( *GraphView) ( Term, string) (string, bool) {for , := range .subjectIndex()[] {if := &.d.Quads[]; .P.Kind == IRI && .P.Value == && .O.Kind == Literal {return .O.Value, true}}return "", false}// SubjectsOfType returns every subject in this graph with rdf:type typeIRI, in// document order (deduplicated). Like Graph.SubjectsOfType it scans rather than// keying off the subject index, so it never triggers an index build.func ( *GraphView) ( string) []Term {var []Term:= map[Term]bool{}for := range .d.Quads {:= &.d.Quads[]if .G == .graph && .P.Kind == IRI && .P.Value == TypeIRI && .O.Kind == IRI && .O.Value == {if ![.S] {[.S] = true= append(, .S)}}}return}
![]() |
The pages are generated with Golds v0.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. |