rdfgo

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 30, 2026 License: MIT Imports: 7 Imported by: 0

README

rdfgo

Go Reference Go Report Card MIT

A SPARQL 1.1 engine and a SHACL validator in Go, over a triple store you supply. No dependenciesgo list -m all is one line.

go get github.com/liliang-cn/rdfgo
engine := rdfgo.New(myStore)

rows, err := engine.ExecuteSPARQL(ctx,
    `SELECT ?s WHERE { ?s a <http://example.org/Person> }`)

report, err := engine.ValidateSHACL(ctx, shapesGraph)

Your store is four methods

type Store interface {
    FindTriples(ctx context.Context, pattern TriplePattern) ([]RDFTriple, error)
    UpsertTriple(ctx context.Context, triple *RDFTriple) error
    DeleteTriple(ctx context.Context, triple RDFTriple) error
    ListNamespaces(ctx context.Context) ([]Namespace, error)
}

That is the whole coupling: 13 call sites, against 49 types and 173 functions the engine owns. The interface was not designed — it was measured off working code: it ran against a SQL-backed store for a year before it was lifted out. The test fixture is an in-memory Store, which is also the proof that four methods are enough.

Parsing is a separate module

go get github.com/liliang-cn/rdfgo/rdfio   # Turtle and TriG

Split so the parser's dependency stays out of your go.sum if you only want to query.

What it does not do

No OWL reasoning — there is no reasoner here, and none worth the name in Go. Materialise entailments before you query. No RDFS forward chaining — that rule engine is a writer and stayed behind.

Two behaviours worth knowing, both measured in the original and neither introduced here: parseSPARQL calls ListNamespaces once per query, so a SQL store pays a round trip per query; and sh:class is a direct rdf:type lookup with no subclass closure.

Tests

17 in the core and 2 in rdfio, ported with their assertions unchanged, plus a fuzz target: 1.3 million executions, no crashes.

MIT.

Documentation

Overview

Package rdfgo is a SPARQL 1.1 query/update engine and a SHACL validator that run over any triple store you can express in four methods.

It was extracted from a graph database, where the same engine was a set of methods on a SQLite-backed GraphStore. Nothing in the query evaluator wanted SQLite; it wanted triples. The extraction is what proves it: across 3800 lines of SPARQL and 500 of SHACL, the number of distinct things the engine asks its storage for is four —

sparql.go   FindTriples x4   UpsertTriple x2   DeleteTriple x3   ListNamespaces x1
shacl.go    FindTriples x3

— and everything else those files call, they define themselves. That count is the whole reason this package exists as a package: an engine coupled to storage at four call sites is an engine that can be lifted off it, and one coupled at forty is not.

What it does

SELECT, ASK, CONSTRUCT and DESCRIBE, with OPTIONAL, UNION, MINUS, VALUES, BIND, FILTER, sub-queries, property paths, aggregates and solution modifiers; and the updates INSERT DATA, DELETE DATA, DELETE WHERE, and the INSERT/DELETE ... WHERE modify form. Plus SHACL node and property shapes: datatype, cardinality, value range, pattern, sh:in, node kind, and class constraints, reported as a conformance report rather than an error.

What it does not do

There is no OWL reasoning here, and no RDFS entailment either. This package answers queries against the triples the store actually holds; it does not materialise or infer new ones. The RDFS forward-chainer it grew up beside stayed where it was, because it is a writer — it derives triples and puts them back — and a writer is a policy about your graph rather than a way of reading it. If you want entailment, infer into the store and query the result; the engine will see the inferred triples like any others.

Nor does it parse or serialise RDF documents. That is deliberate and it is why this package's go.mod has no requires: a caller who wants to run a SPARQL query should not have to compile a Turtle parser to do it. Parsing lives in the separate rdfio package, which depends on one, and which you can leave out of your build entirely.

The store is yours

Implement Store over whatever you keep triples in — a SQL table, a map, a remote service — and hand it to New. The interface is the four methods above and no more. It is stated narrowly on purpose: every method added to it is a thing every future store has to implement, and the only defensible reason to add one is that the engine cannot answer a query without it.

Index

Constants

View Source
const (
	// RDFTermIRI represents an IRI/resource term.
	RDFTermIRI = "iri"
	// RDFTermBlankNode represents a blank node term.
	RDFTermBlankNode = "blank_node"
	// RDFTermLiteral represents a literal term.
	RDFTermLiteral = "literal"
)
View Source
const (
	SHACLNamespace = "http://www.w3.org/ns/shacl#"
	RDFNamespace   = "http://www.w3.org/1999/02/22-rdf-syntax-ns#"
	XSDNamespace   = "http://www.w3.org/2001/XMLSchema#"

	SHACLNodeShape     = SHACLNamespace + "NodeShape"
	SHACLPropertyShape = SHACLNamespace + "PropertyShape"
	SHACLProperty      = SHACLNamespace + "property"
	SHACLPath          = SHACLNamespace + "path"
	SHACLTargetClass   = SHACLNamespace + "targetClass"
	SHACLTargetNode    = SHACLNamespace + "targetNode"
	SHACLDatatype      = SHACLNamespace + "datatype"
	SHACLMinCount      = SHACLNamespace + "minCount"
	SHACLMaxCount      = SHACLNamespace + "maxCount"
	SHACLMinInclusive  = SHACLNamespace + "minInclusive"
	SHACLMaxInclusive  = SHACLNamespace + "maxInclusive"
	SHACLPattern       = SHACLNamespace + "pattern"
	SHACLIn            = SHACLNamespace + "in"
	SHACLNodeKind      = SHACLNamespace + "nodeKind"
	SHACLClass         = SHACLNamespace + "class"
	SHACLSeverity      = SHACLNamespace + "severity"
	SHACLMessage       = SHACLNamespace + "message"

	SHACLSeverityInfo      = SHACLNamespace + "Info"
	SHACLSeverityWarning   = SHACLNamespace + "Warning"
	SHACLSeverityViolation = SHACLNamespace + "Violation"

	SHACLIRI                = SHACLNamespace + "IRI"
	SHACLBlankNode          = SHACLNamespace + "BlankNode"
	SHACLLiteral            = SHACLNamespace + "Literal"
	SHACLBlankNodeOrIRI     = SHACLNamespace + "BlankNodeOrIRI"
	SHACLBlankNodeOrLiteral = SHACLNamespace + "BlankNodeOrLiteral"
	SHACLIRIOrLiteral       = SHACLNamespace + "IRIOrLiteral"

	RDFType = RDFNamespace + "type"
)

SHACL IRIs

View Source
const (
	// SPARQLQuerySelect executes a tabular SELECT query.
	SPARQLQuerySelect = "select"
	// SPARQLQueryAsk executes a boolean ASK query.
	SPARQLQueryAsk = "ask"
	// SPARQLQueryConstruct executes a graph-producing CONSTRUCT query.
	SPARQLQueryConstruct = "construct"
	// SPARQLQueryDescribe executes a graph-producing DESCRIBE query.
	SPARQLQueryDescribe = "describe"
	// SPARQLQueryInsertData executes an INSERT DATA update.
	SPARQLQueryInsertData = "insert_data"
	// SPARQLQueryDeleteData executes a DELETE DATA update.
	SPARQLQueryDeleteData = "delete_data"
	// SPARQLQueryDeleteWhere executes a DELETE WHERE update.
	SPARQLQueryDeleteWhere = "delete_where"
	// SPARQLQueryModify executes INSERT ... WHERE / DELETE ... INSERT ... WHERE style updates.
	SPARQLQueryModify = "modify"
)

Variables

This section is empty.

Functions

This section is empty.

Types

type Engine

type Engine struct {
	// contains filtered or unexported fields
}

Engine executes SPARQL queries and SHACL validation against a Store.

It holds no state of its own beyond the store, so one Engine is safe to share for concurrent queries exactly as far as the underlying Store is. Everything a query needs while it runs — bindings, prefixes, execution options — lives on the stack of the call that is running it.

func New

func New(s Store) *Engine

New returns an Engine reading and writing through s.

The store is not validated here and cannot be: whether it can answer is a thing you find out by asking it, and the first query will say so. Passing nil is a programming error and will panic on the first call rather than return a query result that quietly means "your graph is empty".

func (*Engine) ExecuteSPARQL

func (e *Engine) ExecuteSPARQL(ctx context.Context, query string) (*SPARQLResult, error)

ExecuteSPARQL runs a practical SPARQL SELECT/ASK subset against the embedded RDF layer.

func (*Engine) Store

func (e *Engine) Store() Store

Store returns the store this engine was built over.

It is here because a caller who has an Engine usually also wants to write triples into the thing it queries, and forcing them to carry both values around is how the two drift apart and a query ends up running against a different graph than the one that was just written.

func (*Engine) ValidateSHACL

func (e *Engine) ValidateSHACL(ctx context.Context, shapeTriples []RDFTriple) (*SHACLReport, error)

ValidateSHACL runs SHACL validation against the graph store using the provided shapes.

type Namespace

type Namespace struct {
	Prefix string `json:"prefix"`
	URI    string `json:"uri"`
}

Namespace represents a prefix to IRI mapping.

type RDFTerm

type RDFTerm struct {
	Kind     string `json:"kind"`
	Value    string `json:"value"`
	Datatype string `json:"datatype,omitempty"`
	Language string `json:"language,omitempty"`
}

RDFTerm represents one RDF term.

func NewBlankNode

func NewBlankNode(value string) RDFTerm

NewBlankNode creates a blank node term.

func NewIRI

func NewIRI(value string) RDFTerm

NewIRI creates an IRI term.

func NewLangLiteral

func NewLangLiteral(value, language string) RDFTerm

NewLangLiteral creates a language-tagged literal term.

func NewLiteral

func NewLiteral(value string) RDFTerm

NewLiteral creates a plain literal term.

func NewTypedLiteral

func NewTypedLiteral(value, datatype string) RDFTerm

NewTypedLiteral creates a typed literal term.

func (RDFTerm) String

func (t RDFTerm) String() string

String renders the term using RDF-compatible syntax.

type RDFTriple

type RDFTriple struct {
	ID         string   `json:"id,omitempty"`
	Subject    RDFTerm  `json:"subject"`
	Predicate  RDFTerm  `json:"predicate"`
	Object     RDFTerm  `json:"object"`
	Graph      *RDFTerm `json:"graph,omitempty"`
	Inferred   bool     `json:"inferred,omitempty"`
	Rule       string   `json:"rule,omitempty"`
	SupportIDs []string `json:"support_ids,omitempty"`
}

RDFTriple represents one RDF triple or quad when Graph is set.

func (RDFTriple) String

func (t RDFTriple) String() string

String renders the triple/quad using RDF syntax.

type SHACLReport

type SHACLReport struct {
	Conforms bool                    `json:"conforms"`
	Results  []SHACLValidationResult `json:"results,omitempty"`
}

SHACLReport contains the outcome of SHACL validation.

type SHACLValidationResult

type SHACLValidationResult struct {
	FocusNode RDFTerm `json:"focus_node"`
	Path      RDFTerm `json:"path"`
	Value     RDFTerm `json:"value,omitempty"`
	Message   string  `json:"message"`
	Severity  string  `json:"severity"`
	Source    RDFTerm `json:"source_shape"`
}

SHACLValidationResult represents a single constraint violation.

type SPARQLResult

type SPARQLResult struct {
	QueryType string               `json:"query_type"`
	Vars      []string             `json:"vars,omitempty"`
	Bindings  []map[string]RDFTerm `json:"bindings,omitempty"`
	Triples   []RDFTriple          `json:"triples,omitempty"`
	Boolean   bool                 `json:"boolean,omitempty"`
	Count     int                  `json:"count"`
}

SPARQLResult contains the result of executing a SPARQL query.

type Store

type Store interface {
	// FindTriples returns every triple matching the pattern. Nil pattern
	// fields match anything. It is the engine's only read path: a basic graph
	// pattern, a property path step, a DESCRIBE expansion and a SHACL target
	// search are all this call with a different pattern.
	FindTriples(ctx context.Context, pattern TriplePattern) ([]RDFTriple, error)
	// UpsertTriple writes one triple, which must be idempotent: SPARQL's
	// INSERT DATA on a triple already present is defined as a no-op, not a
	// duplicate, and the engine relies on the store for that rather than
	// reading before every write.
	//
	// It takes a pointer because a store may fill in RDFTriple.ID and the
	// caller of an update may want to see it.
	UpsertTriple(ctx context.Context, triple *RDFTriple) error
	// DeleteTriple removes one triple by its content. Deleting a triple that
	// is not there must succeed: DELETE WHERE computes its deletions from a
	// pattern match and the same triple can legitimately be named twice by two
	// solutions, so a store that errored on a miss would fail ordinary
	// updates.
	DeleteTriple(ctx context.Context, triple RDFTriple) error
	// ListNamespaces returns the prefix bindings the store knows. The parser
	// asks once per query, so that a query may use a prefix the store has
	// registered without repeating it in a PREFIX clause. A store with no
	// notion of namespaces returns nil, and then only in-query PREFIX
	// declarations resolve.
	ListNamespaces(ctx context.Context) ([]Namespace, error)
}

Store is the triple store the engine runs over.

These four methods are not a design; they are a measurement. They are exactly what sparql.go and shacl.go call on their storage and nothing else, which is what makes the interface implementable by a store that is a map in a test and by one that is a database in production without either of them pretending.

A pattern with nil fields is a wildcard in those positions, so FindTriples with an empty TriplePattern is "everything" — an implementation that treated nil as "match the zero term" would silently return nothing and every query would come back empty rather than failing.

type TriplePattern

type TriplePattern struct {
	Subject   *RDFTerm `json:"subject,omitempty"`
	Predicate *RDFTerm `json:"predicate,omitempty"`
	Object    *RDFTerm `json:"object,omitempty"`
	Graph     *RDFTerm `json:"graph,omitempty"`
	Inferred  *bool    `json:"inferred,omitempty"`
	Limit     int      `json:"limit,omitempty"`
}

TriplePattern filters triple lookup operations. Nil fields behave as wildcards.

Directories

Path Synopsis
rdfio module

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL