// N-Quads serialization: writing a snapshot as a deterministic, byte-stable // N-Quads document (optionally gzip-compressed), the property Value -> RDF // literal mapping, and IRI percent-escaping. Split from nquads.go, which // holds the vocabulary, constants, and the parse/build (read) path.
package chickpeas import ( ) // WriteNQuads serializes the snapshot as an N-Quads document (see the // package-level vocabulary above). Output order is deterministic -- // version, then nodes in ascending id order (labels sorted, property // keys sorted, outgoing rels in CSR order, each followed by its sorted // rel properties) -- so equal graphs serialize byte-identically. func ( *Snapshot) ( io.Writer) error { var rdf.Encoder := make([]byte, 0, nqFlushSize+(nqFlushSize>>2)) := func() error { , := .Write() = [:0] return } if , := .Version(); { = .AppendQuad(, rdf.Quad{ S: rdf.NewIRI(nqGraphIRI), P: rdf.NewIRI(nqVersionIRI), O: rdf.NewLiteral(, "", ""), }) } := .Labels() := make([]*nodeset.Set, len()) := make([]rdf.Term, len()) for , := range { [], _ = .NodesWithLabel() [] = rdf.NewIRI(nqLabelPrefix + nqEscape()) } // Per-relKey invariants precompute once: the predicate IRI (an // allocating concat+escape) and the column lookup would otherwise // recompute per (rel x relKey) in the inner loop -- M*K times for K // distinct values. type struct { string rdf.Term Column } := make([], 0, len(.relColumns)) for , := range .relColumns { if , := .atoms.Resolve(); { = append(, { : , : rdf.NewIRI(nqPropPrefix + nqEscape()), : , }) } } sort.Slice(, func(, int) bool { return []. < []. }) // The rel-type predicate is likewise invariant per type atom: resolve // and escape each once instead of per rel. := make(map[RelType]rdf.Term, len(.typeIndex)) for := range .typeIndex { if , := .atoms.Resolve(.ID()); { [] = rdf.NewIRI(nqRelPrefix + nqEscape()) } } := int(.CSRIDSpace()) for := range { := NodeID() := rdf.NewIRI(nqNodePrefix + strconv.FormatUint(uint64(), 10)) for , := range { if .Contains() { = .AppendQuad(, rdf.Quad{ S: , P: rdf.NewIRI(rdf.TypeIRI), O: [], }) } } for , := range .NodePropertyKeys() { if , := .Prop(, ).Value(); { = .AppendQuad(, rdf.Quad{ S: , P: rdf.NewIRI(nqPropPrefix + nqEscape()), O: .nqLiteral(), }) } } for := .outOffsets[]; < .outOffsets[+1]; ++ { := rdf.NewIRI(nqRelIDPrefix + strconv.FormatUint(uint64(), 10)) = .AppendQuad(, rdf.Quad{ S: , P: [.outTypes[]], O: rdf.NewIRI(nqNodePrefix + strconv.FormatUint(uint64(.outNbrs[]), 10)), G: , }) for , := range { if , := ..Get(); { = .AppendQuad(, rdf.Quad{ S: , P: ., O: .nqLiteral(), }) } } } if len() >= nqFlushSize { if := (); != nil { return } } } return () } // WriteNQuadsFile writes the snapshot as an N-Quads file, gzip-compressed // when path ends in ".gz". Any other compression wraps WriteNQuads in a // caller-provided writer. func ( *Snapshot) ( string) error { , := os.Create() if != nil { return } var io.Writer = var *gzip.Writer if strings.HasSuffix(, ".gz") { = gzip.NewWriter() = } if := .WriteNQuads(); != nil { .Close() return } if != nil { if := .Close(); != nil { .Close() return } } return .Close() } // nqLiteral maps a property Value to a typed RDF literal. func ( *Snapshot) ( Value) rdf.Term { switch .Kind() { case KindI64: , := .I64() return rdf.NewLiteral(strconv.FormatInt(, 10), "", xsdInteger) case KindF64: , := .F64() return rdf.NewLiteral(nqFormatFloat(), "", xsdDouble) case KindBool: , := .Bool() return rdf.NewLiteral(strconv.FormatBool(), "", xsdBoolean) } , := .StrID() , := .atoms.Resolve() return rdf.NewLiteral(, "", "") } // nqFormatFloat writes the shortest round-tripping decimal form, using // the xsd:double lexicals for the non-finite values. func nqFormatFloat( float64) string { switch { case math.IsInf(, 1): return "INF" case math.IsInf(, -1): return "-INF" case math.IsNaN(): return "NaN" } return strconv.FormatFloat(, 'g', -1, 64) } // nqEscape percent-encodes the bytes an IRI cannot carry raw (control // characters, space, the N-Triples delimiters, and '%' itself), leaving // multi-byte UTF-8 intact. nqUnescape inverts it. func nqEscape( string) string { := func( byte) bool { return <= ' ' || == 0x7f || strings.IndexByte("<>\"{}|^`\\%", ) >= 0 } := 0 for < len() && !([]) { ++ } if == len() { return } const = "0123456789ABCDEF" := make([]byte, 0, len()+8) = append(, [:]...) for ; < len(); ++ { if := []; () { = append(, '%', [>>4], [&0xf]) } else { = append(, []) } } return string() }