Hybrid SQL + Graph Deployments¶
OntoSQL 0.4 closes the loop between SQL operational stores and RDF graphs. SQL remains the system of record; graphs are derived for interoperability, SPARQL queries, and validation.
See ECOSYSTEM.md for how OntoSQL, TripleModel, and SparqlModel fit together.
Architecture¶
flowchart LR
App[Application]
SQL[(SQLite / Postgres)]
Graph[(In-memory or SPARQL graph)]
App --> OntoSession
OntoSession --> SQL
OntoSession -->|graph_sync on save| Graph
OntoSession -->|materialize_find| Graph
| Layer | Role |
|---|---|
| OntoSession | CRUD over mapped SQL tables |
| ontosql.sync | Push semantic instances to a graph (patch, replace, add) |
| ontosql.import_ | Hydrate OntoModel from JSON-LD / Turtle (no SQL write) |
SparqlModel (ontosql[sparql]) |
Optional SPARQLSession for graph-native reads/writes |
ontosql.shacl (ontosql[shacl]) |
Generate and validate SHACL shapes from maps |
Push on save¶
Wire a graph target when creating a session. Graph updates are queued during save() and delete(), then applied after the SQL transaction commits when the session context exits (__exit__ / __aexit__). If the session rolls back, queued graph updates are discarded.
from ontosql import OntoSession
from ontosql.sync import StoreSyncTarget
graph_target = StoreSyncTarget()
with OntoSession(
engine,
maps=[PersonMap, OrganizationMap],
graph_sync=graph_target,
graph_sync_mode="replace", # or "patch" (default)
) as session:
person = session.save(Person(id=1, name="Ada", employer=org))
# Graph is updated here, after commit — not inside save()
flush() applies pending SQL writes and queues graph sync for those entities; graph updates still run at commit.
delete() queues removal of the instance subgraph from the graph target (via remove_instance).
graph_sync accepts any object with graph and update_graph(add=, remove=) — including SparqlModel stores that implement the GraphSyncTarget protocol.
Graph sync failures (split-brain)¶
Graph updates run after SQL commit(). If flush_graph_sync() fails partway through, SQL is already durable but the graph may be stale or partially updated.
OntoSQL processes removes then pushes one instance at a time. Completed operations stay applied; the remaining queue is preserved and GraphSyncError is raised with details in session.graph_sync_failures.
from ontosql import OntoSession
from ontosql.session import GraphSyncError
try:
with OntoSession(engine, maps=[PersonMap], graph_sync=target) as session:
session.save(person_a)
session.save(person_b)
except GraphSyncError as exc:
# SQL committed; graph may be partial
assert session.graph_sync_pending
# Fix remote graph / credentials, then:
session.retry_graph_sync()
Reconciliation pattern (recommended for production)¶
Do not treat SQL + in-process graph as a single transaction. Pick one:
| Pattern | When |
|---|---|
| Outbox table | Enqueue graph ops in SQL before commit; worker retries pushes |
| Nightly reconcile | materialize_find → diff → repair graph |
| Disable graph_sync on hot path | Batch export jobs only |
Log GraphSyncError at warning level and alert on session.graph_sync_pending after request handling.
Manual push / pull (SparqlModel)¶
For explicit control, use OntoGraphSync with a SPARQLSession:
from sparqlmodel import SPARQLSession
from ontosql.sync.sparql import OntoGraphSync
graph_session = SPARQLSession()
sync = OntoGraphSync(graph_session, maps=[PersonMap, OrganizationMap], mode="replace")
sync.push(person) # SQL instance → graph triples
pulled = sync.pull(Person, iri=sync.instance_iri(person)) # graph → OntoModel (read-only)
OntoSQL never converts OntoModel to SPARQLModel; it writes triples via store.update_graph.
Materialized views¶
Build a read-only RDF graph from session query results — useful for SPARQL CONSTRUCT endpoints or seeding a graph mirror:
from ontosql.export import instances_to_graph
from ontosql.sync import materialize_find
graph = materialize_find(session, Person, where=Person.name.startswith("A"), limit=100)
single = instances_to_graph([person])
RDF import (round-trip)¶
Import hydrates semantic instances from RDF using mapper metadata (not TripleModel subclassing):
from ontosql.import_ import import_from_jsonld, import_from_rdf
doc = person.to_jsonld()
restored = import_from_jsonld(doc, PersonMap)
turtle = person.to_rdf(format="turtle")
restored = import_from_rdf(turtle, PersonMap, format="turtle")
Or via Person.from_jsonld(doc, mapper=PersonMap).
SHACL validation¶
Generate shapes from maps and validate exported graphs:
from ontosql.shacl import shapes_from_mapper, validate_instance
shapes = shapes_from_mapper(PersonMap)
report = validate_instance(person, PersonMap, shapes=shapes)
assert report.conforms
Requires pip install ontosql[shacl].
Prefix bundles¶
Use curated vocabulary defaults for consistent @context across SQL and graph exports:
CascadePolicy.REPLACE¶
When a nested association changes, REPLACE deletes the old nested row (from session snapshot) before upserting the new one, but only when no other parent row still references that nested row. Use LINK or IGNORE for shared entities referenced by multiple parents.
When to use what¶
| Need | Approach |
|---|---|
| SQL CRUD + automatic graph mirror | OntoSession(..., graph_sync=...) |
| Batch graph push from SQL reads | materialize_find |
| SPARQL queries over exported data | OntoGraphSync + SPARQLSession |
| Validate API payloads / exports | ontosql.shacl |
| RDF → Python without SQL | import_from_jsonld / import_from_rdf |
Related documents¶
- guides/cascade-policies.md — nested write policies
- SECURITY.md — graph/SQL consistency
- guides/graph-sync-operations.md — production runbook
- SPECS.md — API contract
- ECOSYSTEM.md — package boundaries
- examples/hybrid_person_org.py — runnable demo