SparqlModel Technical Specification
Overview
This document specifies the SparqlModel ORM layer: session API, query compilation, hydration, cascade policy, and stores.
SparqlModel — the SQLModel of SPARQL.
Mapping (literals, terms, to_graph, sync_to_graph, from_graph, parse, serialize) is specified and implemented by TripleModel >=0.10, a required dependency (Pydantic TripleModel classes). SparqlModel integrates TripleModel internally; application code uses SPARQLSession and Pydantic v2 SPARQLModel unless doing stateless file I/O.
Concern |
SparqlModel |
TripleModel |
|---|---|---|
|
Yes |
No |
Query DSL + compiler |
Yes |
No |
Cascade / orphans on |
Yes |
No |
Hydration |
Yes |
No |
Stores |
Yes |
No |
Model ↔ triples, terms, files |
|
Yes |
ORM.md · ECOSYSTEM.md · ROADMAP.md · PRODUCTION.md
Production ORM checklist (1.3 GA gate)
Normative checklist for declaring SparqlModel production-ready (version 0.15). See ROADMAP.md — Forward roadmap and SPARQLMojo parity backlog for milestone versions.
Parity tiers: P0 = required for production HTTP/API apps; P1 = SQLModel / SPARQLMojo parity; P2 = advanced RDF / ecosystem.
P0 — Production APIs
SPARQLModel,Field,Relationship,IRI, Pydantic validationSPARQLSession—add,put,delete,get,query,execute, context managerQuery filters:
==,!=,&,|, ordering,in_, multi-hop paths;(A & B) | Cprecedence (0.2+)limit,first,allwith hydrationdepth0–2Identity map +
flush/rollback_pending/put(..., flush=False)MemoryStoreandHttpStore(documented mirror semantics)FastAPI
SessionDep,init_app,http_store_lifespanSession I/O via TripleModel (
put/get/ hydrate) — 0.3.0Option A —
SPARQLModel(TripleModel);_triple.pyremoved;rdf_bridge+ directfrom_graph— 0.4.0AsyncSPARQLSession— async CRUD,async with,async def execute— 0.6.0AsyncStoreProtocol+AsyncHttpStore(httpx.AsyncClient) +AsyncMemoryStore— 0.6.0AsyncQuery—async def all()/first(); same expression DSL as sync — 0.6.0FastAPI
AsyncSessionDep+async_http_store_lifespan— 0.6.0Async/sync parity contract tests on memory and HTTP stores — 0.6.0
Query.offset(n)— 0.8.0Query.order_by(...)— 0.8.0Query.count()— 0.8.0OPTIONAL / absence filters for nullable
Relationship | None— 0.8.0HttpStore partial mirror sync —
pull_subjects_into_mirror, auto-pull onget— 0.9.1; onrefresh— 0.9.2HttpStore replace-on-pull +
mirror_mode(writer/remote_authoritative) — 0.10.0HttpStore retries, batched UPDATE, SELECT
query_methodGET/POST — 0.11.0HttpStore full mirror sync (GSP
sync_mirror) — 0.12.0Scoped session pattern documented (FastAPI + scripts) — 0.9.0
Threading / asyncio concurrency model documented — 0.6 (async) + 0.9 (threads)
P1 — SQLModel / SPARQLMojo parity
See ROADMAP — SPARQLMojo parity backlog for the full catch-up list.
merge,refresh,expunge,expunge_allon session — 0.9.0HttpStore separate read/write endpoint URLs — 0.9.1
Multi-valued scalar and relationship fields (
set[...]/list[...]where TripleModel allows) — 0.13.0Language-tagged literals (
LangString,MultiLangString) — 0.13.0Polymorphic queries (
Query.polymorphic(),Rdf.ontology_registry) — 0.13.0Property paths (inverse
^,+/*,property_eqescape hatch) — 0.13.0Inverse /
back_populates/Relationship(..., inverse=)— 0.13.0SchemaRegistry(OntologyRegistryalias, lite hints) — 0.13.0IRI field string filters (
FieldRef.str()/lower()/upper()) — 0.13.0VALUES clause in query DSL (
Query.values(...)) — 0.13.0Query negation (
not_()/~expr) — 0.13.0Filters on collection fields (
.in_()onset/listrefs and scalars) — 0.13.0HttpStore
query_methodGET vs POST — 0.11.0Optional SHACL validation on
put— 0.14
P2 — Advanced
session.ask(...)orQuery.exists()helper wrapping ASK — 0.14+CONSTRUCT / DESCRIBE helpers — 0.14+
Named graph scope on session/store — 0.14+
Oxigraph or additional store backends — 0.14+
SPARQL federation in query layer — future
Explicit non-goals: OWL editor, built-in reasoner, duplicate TripleModel mapping in graph.py.
SPARQLSession
ORM entry point. Binds a Store (default MemoryStore) and namespace registry.
with SPARQLSession() as session:
session.put(person)
found = session.query(Person).where(Person.name == "Odos").first()
Methods
Method |
Behavior |
|---|---|
|
Append triples; no removal of existing subject triples |
|
Remove owned subjects (cascade), then write; queue when |
|
Remove owned triples for root + embedded composition |
|
Load one resource; optional relationship depth 0–2 |
|
Return |
|
Raw SELECT; auto-prefixes when configured |
|
Apply or discard pending |
|
Call |
|
Evict identity and hydration cache for an IRI (0.9 also drops pending |
|
Detach one instance from session cache (store unchanged) — 0.9 |
|
Clear identity map and hydration cache (pending queue unchanged) — 0.9 |
|
Reload from store into cached instance when present — 0.9 |
|
Return canonical session instance for identity key (no store write) — 0.9 |
Context manager
On clean exit: flush() if the pending queue is non-empty. On exception: rollback_pending() when rollback_on_error=True (default). Always calls close() when close_on_exit=True (default). Does not undo already-flushed writes.
Properties
store— backing storegraph—triplemodel.Store(MemoryStoregraph, orHttpStorelocal mirror — not the remote dataset)namespaces—NamespaceRegistryfor compiler and serialization
Session lifecycle (target API)
Current (0.9): Context manager flushes pending put queue on success; rollback_pending on error; expire(model_cls, iri) evicts identity and hydration cache; merge, refresh, expunge, and expunge_all for explicit cache control (sync and async). Not thread-safe.
Target (1.2):
Method |
Behavior |
|---|---|
|
Attach detached/transient instance to session; reconcile with identity map |
|
Reload from store; replace cached attributes |
|
Remove one instance from identity map |
|
Clear identity map and hydration cache |
|
Factory for request-scoped sessions (FastAPI pattern) |
Object states (SQLAlchemy-aligned):
transient → (add|put) → pending (flush=False) → persistent (in store + identity map)
persistent → delete → (removed from store; expunge clears session)
persistent → expunge → detached (no session; may merge again)
Threading: One SPARQLSession per task/request unless documented otherwise; shared HttpStore requires external synchronization or single-writer discipline.
Query builder
with SPARQLSession() as session:
session.query(Person).where(Person.name == "Odos").all()
session.query(Person).where(Person.works_for.name == "Acme").limit(10).first()
.where(*expr)—CompareExpr,AndExpr, or top-levelOrExpr.limit(n)— non-negative integer.offset(n)— non-negative integer (0.8).order_by(field, *, desc=False)— scalar field only; repeatable (0.8).count()— returnsint; ignores limit/offset/order_by (0.8).first()— always usesLIMIT 1; ignores any prior.limit()or.offset()on the same query.use_not_exists_for_ne()— compile!=withNOT EXISTS(default since 0.5.2).use_inequality_for_ne()— legacy inequality!=(pre-0.5.2 default).all(*, depth=0)/.first(*, depth=0)— execute and hydrate
Query builder (target API)
Current (0.8): .offset(n), .order_by(field, *, desc=False), .count() (ignores limit/offset/order_by). .first() always LIMIT 1 and ignores .limit() / .offset(). Nullable relationship hops use OPTIONAL; relationship.is_(None) / is_not(None) for absence/presence. No distinct or field projection.
Target (post-1.3):
Method |
SPARQL |
|---|---|
|
|
Precedence: Python & binds tighter than |; (A & B) | C is two disjuncts (fixed 0.2).
SPARQL compilation
Person.name == "Odos" → SPARQL triple patterns bound to ?person.
Operator |
Semantics |
|---|---|
|
Pattern match |
|
|
|
Conjoin patterns ( |
|
Disjunction via |
|
Ordering on bound literal variables |
|
|
|
Raises |
Nested attribute paths (Person.works_for.located_in.name) support arbitrary hop length via join variables and related-type patterns.
Implementation: compiler.py — SparqlModel only; TripleModel does not compile Python filters.
Hydration
with SPARQLSession() as session:
session.get(Person, iri, depth=2)
session.query(Person).where(...).all(depth=1)
|
Loads |
|---|---|
|
Scalars on root |
|
One hop of |
|
Two hops |
validate_depth rejects values outside 0–2.
Integration note (0.3.x): scalar and relationship loading uses sparql_from_graph → TripleModel from_graph via interim _triple.py. 0.4+: SPARQLModel.from_graph on the unified subclass + SparqlModel depth hydration.
SPARQLModel
ORM entity base class. SQLModel-style declaration:
class Person(SPARQLModel):
rdf_type = "schema:Person"
__prefixes__ = {"schema": "https://schema.org/"}
id: IRI
name: str = Field("schema:name")
Metaclass enables
Person.name == "x"in queries (FieldRef)ensure_id()assignsurn:uuid:…whenidis unsetJSON-LD helpers:
model_dump_jsonld/model_validate_jsonld(ORM dict API; file JSON-LD viaserialize— 0.7)Subclasses
TripleModel(Option A, 0.4+); merged metaclass for queryFieldRefmodel_configusesextra="forbid"Field/Relationshipare ORM sugar overrdf_field/Predicate(built at class creation, noexec)
Interim (0.3.x): dynamic shadow TripleModel classes via sparqlmodel._triple — removed in 0.4.
See also Models and Pydantic validation for application patterns.
Validation architecture
Three layers; all are complementary, not interchangeable.
Layer |
When |
Mechanism |
|---|---|---|
Application (Pydantic) |
|
Field types, |
Mapping (TripleModel) |
|
Expected |
Graph shapes (optional) |
|
SHACL via |
Write path (0.4+): validated SPARQLModel → cascade in graph.py → sync_to_graph(model, store.graph, …).
Write path (0.3.x interim): validated SPARQLModel → to_triplemodel → TripleModel.model_validate → sync_to_graph.
Read path (0.4+): graph → SPARQLModel.from_graph → optional depth hydration → identity map; Pydantic ValidationError surfaced as HydrationError.
Planning rule: new ORM features should extend Pydantic annotations and Field kwargs before adding ad-hoc validation in session or compiler code. See SparqlModel Roadmap (Pydantic-first).
Relationships
works_for: Organization | None = Relationship("schema:worksFor", model=Organization)
Value type |
Semantics |
|---|---|
Embedded |
Composition — cascade on |
|
Reference — no cascade delete of target |
Relationships and hydration (target API)
Current (0.2): Single object per predicate on load; depth 0–2 eager-loads Relationship fields; composition cascade on put/delete.
Target (0.13):
list[T]/ collection fields for multi-valued literals and IRIs (via TripleModel) — 0.13Language-tagged fields (
LangString, multi-lang maps) — 0.13 (TripleModel)Polymorphic
session.query(Base).where(...)matching subclasses — 0.13Property paths, VALUES clause, IRI string filters, query negation — 0.13 (parity backlog)
Compiler emits
OPTIONALfor nullable relationship paths in filters — 0.8.0Optional
Relationship(..., back_populates=...)/ inverse navigation — 0.13
Persistence policy
SparqlModel-specific; orchestrates which subjects TripleModel (or interim graph.py) syncs.
put
Compute
cascade_subjects_for_removal(root, nested embeds, orphans on relationship change)Remove
owned_triples_for_subjectsfrom store graphAdd current model graph (
model_to_graph→ future: TripleModel export + cascade)
delete
Remove owned triples for cascade subject set (no re-add).
Ownership rules
Only declared predicates +
rdf:typeare ownedExtension triples on a subject are not removed by
put/deleteOrphan keys use expanded IRIs and stable
_:bnodekeys
Mapping integration (TripleModel)
Dependencies (0.5+): triplemodel>=0.10.0,<2, pyoxigraph>=0.5,<0.6 in pyproject.toml (no core rdflib).
Today (0.7+): SPARQLModel(TripleModel); session graphs are triplemodel.Store; graph.py holds cascade/orphan policy; rdf_bridge owns graph I/O. serializers.py is thin wrappers over TripleModel infer_format, load_graph, and serialize.
Target wiring (0.4+):
SparqlModel surface |
TripleModel API |
|---|---|
|
|
|
|
|
|
Predicate metadata |
|
Cascade orchestration remains in SparqlModel after wiring.
HttpStore
SPARQL 1.1 over HTTP (httpx) with a local mirror (stores/http.py).
Method |
Target |
|---|---|
|
Remote |
|
Remote SELECT |
|
Mirror only |
External writers or SELECT-only visibility without a matching mirror update can make get return None while execute returns bindings. Single-writer per endpoint is assumed. If both auth and bearer_token are set, Basic auth wins.
put may send DELETE DATA followed by INSERT DATA in one SPARQL Update request; whether that is atomic depends on the endpoint (not guaranteed in 0.2). After HttpStore.close(), query, update_graph, and pull_subjects_into_mirror raise RuntimeError (same for AsyncHttpStore.aclose()).
HTTP resilience (0.11+)
Constructor kwargs on HttpStore / AsyncHttpStore (sync + async parity):
Parameter |
Default |
Behavior |
|---|---|---|
|
|
Retry 502/503/504 and connection/timeouts on SELECT, CONSTRUCT pull, and each UPDATE chunk |
|
|
Exponential backoff between attempts (cap 30s) |
|
|
Split |
|
|
|
Mirror updates run only after all remote UPDATE chunks succeed. Mid-batch remote failure leaves the mirror unchanged; remote state may be partial.
Store protocol (target API)
Current (0.2): Store — graph, query(sparql), update_graph(add=, remove=).
Target (Production HttpStore 0.10–0.12):
Capability |
Notes |
|---|---|
|
SPARQL 1.1 SELECT (required) |
|
Chunked |
|
GET vs POST for remote SELECT — shipped 0.11.0 |
|
Optional protocol methods for existence and graph-shaped reads — 0.14 (P2) |
HttpStore |
Fuseki-style split URLs — 0.9.1 (shipped) |
Replace-on-pull, |
Shipped 0.10.0 |
Mirror sync (GSP |
Shipped 0.12.0 ( |
Retries, batch size limits |
Shipped 0.11.0 ( |
|
Optional — 0.14+ |
Protocols: SPARQL 1.1 Query, SPARQL 1.1 Update, Graph Store HTTP.
Security (SPARQL generation)
Current (0.5+): Filter values serialized via SparqlModel N3 helpers (rdf_n3) on pyoxigraph terms and string IRIs. IRIs with invalid characters raise QueryError. Predicates come from model metadata (trusted code).
Target (1.3 GA):
No public API that concatenates untrusted strings into SPARQL text
Predicates and class IRIs remain declaration-time only
LIMIT/OFFSETremain integer-typed at API boundarySecurity review documented before 1.3 GA
Async API (target 0.6)
Parallel to the sync stack; sync API remains supported.
Component |
Sync (shipped) |
Async (0.6.0) |
|---|---|---|
Session |
|
|
Store |
|
|
Query |
|
|
FastAPI |
|
|
Semantics: Same identity map, cascade, compiler, and hydration rules as sync. One session per asyncio task (not shared across concurrent tasks). HttpStore uses httpx.Client; AsyncHttpStore uses httpx.AsyncClient with the same mirror contract.
Non-goals for 0.6: Replacing sync session; async TripleModel mapping APIs (unified model stays sync; in-memory graph work stays on the event loop thread).
Known limitations
Until 0.4 (unified model)
Area |
Behavior |
|---|---|
Dual model types |
0.3 uses interim |
HttpStore mirror (0.12+)
Area |
Behavior |
|---|---|
Mirror vs remote |
|
External writers |
Use |
Multi-writer endpoints |
Assume single writer per endpoint; reconcile with |
Permanent constraints
Area |
Behavior |
|---|---|
Composition vs reference |
Embedded |
Owned triples |
Only declared predicates + |
|
|
|
Pending models not visible in |
|
Not a full remote transaction; partial failure re-queues remainder (0.2+) |
Sessions |
Not thread-safe; one session per task unless scoped externally |
Closed session |
After |
Interim mapping |
0.3.0: |
Other (current)
Area |
Behavior |
|---|---|
Duplicate predicates |
Two fields with the same expanded predicate on one model class → |
Write-path cycles |
Cyclic embedded |
Shared composition |
Orphan cleanup skips embedded targets still linked from subjects outside the current put cascade |
Pending |
Identity for that subject evicted when queued; |
Nested query filters |
Related resource must have expected |
AND filters, same path |
Compiler reuses join variables per relationship path within one WHERE / EXISTS block |
JSON-LD |
|
Export without |
|
Optional: export and files
ORM workflows do not require sparqlmodel.serializers.
Long term: all formats via TripleModel; SparqlModel may expose session-scoped helpers only.
FastAPI (optional extra)
Install sparqlmodel[fastapi]. fastapi/deps.py provides init_app, get_session, SessionDep, http_store_lifespan. fastapi/__init__.py provides turtle_response, jsonld_response, negotiated_response.
Feature ownership
Feature |
Owner |
|---|---|
SHACL shapes / validation engine |
TripleModel |
SHACL on |
SparqlModel hook calling TripleModel |
Named graphs / Dataset |
TripleModel; SparqlModel consumes |
SPARQL federation in apps |
SparqlModel |
Alternate store backends |
SparqlModel |
OWL reasoner |
Out of scope |
Maintainer boundaries
For end users, use ORM.md. This table is for contributors.
Symptom |
Fix in |
|---|---|
Wrong XSD / literal on export |
TripleModel |
Subject IRI collision |
TripleModel |
Stale predicate after |
TripleModel sync + SparqlModel cascade |
Orphan after relationship change |
SparqlModel |
|
SparqlModel |
New RDF format |
TripleModel |
Fuseki / HTTP store |
SparqlModel |
Anti-patterns: new mapping code only in graph.py; session/compiler in TripleModel; triplemodel importing sparqlmodel.
Package layout
sparqlmodel/
session.py # ORM unit of work
query.py # query builder
compiler.py # ORM-only
hydration.py # depth; → TripleModel load
model.py # SPARQLModel(TripleModel) — 0.4+
fields.py # Field/Relationship sugar → rdf_field / Predicate
graph.py # cascade/orphan policy only
serializers.py # thin TripleModel parse/serialize wrappers (0.7)
stores/
rdf_bridge.py # graph I/O (Option A; replaced _triple.py in 0.4)
Dependencies
pydantic>=2.5,<3
pyoxigraph>=0.5,<0.6
triplemodel>=0.10.0,<2
typing-extensions>=4.8
Optional: httpx, fastapi