Skip to content

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()

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:

from ontosql import PrefixRegistry

reg = PrefixRegistry.curated("schema_org")  # or "dcterms"

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