Troubleshooting

Common issues when running SparqlModel in development or production.

execute returns IRIs that get cannot load

Symptom: SPARQL SELECT finds a subject, but session.get(Model, iri) returns None.

Cause: With HttpStore, query / execute read the remote endpoint; get and cascade read the local mirror updated only by this store instance’s writes.

Mitigations:

  • put data through the same HttpStore session before get

  • Since 0.9.1, get and (since 0.9.2) refresh attempt CONSTRUCT pull into the mirror automatically

  • Call pull_subjects_into_mirror() for known IRIs

  • Use MemoryStore for single-process apps

  • Treat each HttpStore as the primary writer for its endpoint, or use mirror_mode="remote_authoritative" (since 0.10.0) when reads must track remote updates

See SparqlModel production guide — HttpStore mirror model and mirror modes.

Stale mirror after an external writer updated the remote

Symptom: execute or a SPARQL client shows new data, but get still returns old field values even though the subject exists in the mirror.

Cause (0.9.x): CONSTRUCT pull merged remote triples without removing old mirror triples for the same subject. 0.10.0+ uses replace-on-pull when a pull runs, but default writer mode only pulls when the subject is missing from the mirror.

Mitigations:

  • Call pull_subjects_into_mirror() for affected IRIs (replace-on-pull since 0.10.0)

  • Use HttpStore(..., mirror_mode="remote_authoritative") so every get / refresh re-pulls from remote (0.10.0+)

  • put through the same session/store if this instance should own the data

See SparqlModel production guide — mirror modes.

Mirror out of date after external bulk load

Symptom: Many subjects changed on the remote endpoint (ETL, Fuseki UI, another service) but get and cascade still reflect old mirror data.

Cause: The local mirror only updates on this store’s writes, per-subject CONSTRUCT pull, or remote_authoritative reads — not automatically after bulk remote changes.

Mitigations (0.12.0+):

See SparqlModel production guideMirror sync (0.12+).

Duplicate triples after UPDATE retry

Symptom: Remote dataset contains duplicated INSERT DATA rows after a transient 503 during put / update_graph.

Cause (0.11.0+): UPDATE chunks are retried on 502/503/504. The server may have applied the first attempt before returning an error.

Mitigation: Set max_retries=0 on the store for critical writes, or ensure chunks are idempotent. See SparqlModel production guide — UPDATE retries.

Transient HTTP errors (503 / timeouts)

Symptom: QueryError: SPARQL query failed or UPDATE failures during brief endpoint outages.

Cause: Remote SPARQL endpoint returned 502/503/504 or the TCP connection failed.

Mitigations (0.11.0+): HttpStore / AsyncHttpStore retry transient failures by default (max_retries=2, retry_backoff=0.5). Tune for your SLA or set max_retries=0 to fail fast. See SparqlModel production guideHTTP resilience.

Large put / cascade UPDATE fails or times out

Symptom: UPDATE fails with gateway timeout or payload errors when flushing a large graph delta.

Cause: A single SPARQL Update body exceeded endpoint or proxy limits (pre-0.11 sent one unbounded UPDATE per update_graph).

Mitigations (0.11.0+): Lower or raise max_triples_per_update (default 500) on the store. If a mid-batch chunk fails, the mirror is unchanged but the remote dataset may be partial — reconcile before retrying. See SparqlModel production guide — batched UPDATE.

SELECT over GET and URL length

Symptom: query_method="get" fails with 414 or empty responses for long SELECT strings.

Cause: GET encodes the query in the URL; proxies and servers impose length limits.

Fix: Use default query_method="post" for large SELECT text. GET remains useful for short, cache-friendly reads.

query().all() returns fewer rows than the remote SELECT

Symptom: session.query(Person).where(...).all() is empty or shorter than the same filter run in a SPARQL client against the endpoint.

Cause (0.9.1+): On HttpStore / AsyncHttpStore, the compiler runs a remote SELECT, then hydrates each binding with get(). get() reads the local mirror and, since 0.9.1, issues a CONSTRUCT to pull the subject into the mirror when it is missing. Rows still fail to appear if the remote CONSTRUCT returns no triples, the endpoint rejects CONSTRUCT, or the subject has no rdf:type matching your model.

Mitigations: put through the session first, call pull_subjects_into_mirror() for known IRIs, set mirror_mode="remote_authoritative" when each row must be hydrated from remote (0.10.0+), use MemoryStore for single-process apps, or use raw execute() and handle bindings without query().all().

Stale data after put

Symptom: Old predicate values still appear in queries.

Cause: add() appends triples and does not remove stale predicates. Only put() runs orphan cleanup.

Fix: Use put() for upserts. Prefer add() only when you know the subject has no conflicting triples.

Exception in with block leaves session open

Symptom: After an error inside with SPARQLSession(...), the session still accepts calls or still has a pending queue.

Cause: With rollback_on_error=False, pending put(..., flush=False) entries are kept on error and close() is not called (so the original exception is not masked by a pending-queue RuntimeError).

Fix: Use rollback_on_error=True (default), call rollback_pending() before handling the error, or discard the session and open a new one.

Pending put not visible in get

Symptom: put(model, flush=False) then get returns old or missing data.

Cause: Pending queue is not flushed until flush() or context manager exit. The graph is unchanged until flush. The identity map is cleared for that subject when enqueueing a pending put, so get may return None or reload from the store rather than a stale instance.

Fix: Call session.flush() or exit the with SPARQLSession() block successfully. Do not call close() while pending writes remain — use flush() or rollback_pending() first.

QueryError on None or wrong model class

Symptom: Filter raises QueryError.

Causes:

  • Comparing a field to None (unsupported in DSL)

  • Filtering Person.name on a query(Organization) chain

  • Combining OR and AND with &, e.g. ((A | B) & C) or (C & (A | B)) — use .where((A | B), C) instead

  • Passing a bare string to .in_() — use a one-element tuple or list, e.g. .in_(("value",))

Fix: Use nullable-hop != (compiler emits OPTIONAL since 0.8), or Person.relationship.is_(None) / is_not(None) for explicit absence. For complex patterns, raw execute() remains available.

QueryError: Cannot combine OR and AND

Symptom: Building or running a filter like ((Person.name == "A") | (Person.name == "B")) & (Person.name != "C") or (Person.name != "C") & ((Person.name == "A") | (Person.name == "B")) raises QueryError.

Cause: Python & between an OrExpr and a comparison (in either order) would produce an invalid expression tree for the compiler.

Fix: Pass separate .where() arguments::

session.query(Person).where(
    (Person.name == "A") | (Person.name == "B"),
    Person.name != "C",
).all()

Or use (A & B) | C when OR should bind less tightly than AND.

Remote and mirror diverge after editing session.graph

Symptom: query / execute return data that get cannot load, or triples added via session.graph.add never appear on the remote endpoint.

Cause: On HttpStore, session.graph is the local mirror only. Direct graph mutation does not send SPARQL Update to the server.

Fix: Use session.put / delete for persistence, or MemoryStore when tests need low-level graph edits.

!= behaves unexpectedly

Symptom: Resources with no value for a field still match or fail unexpectedly.

Fix: Default != uses NOT EXISTS since 0.5.2. For pre-0.5.2 inequality (excludes unbound), use .use_inequality_for_ne().

URL strings become IRIs

Symptom: Filter Person.homepage == "https://example.org" compiles as IRI not literal.

Cause: Field type is IRI or value matches IRI heuristics.

Fix: Use a str field for literal URLs (0.2+ compiles URL-shaped strings on str fields as literals).

RuntimeError: Cannot use a closed SPARQLSession

Symptom: CRUD or query / execute fails after the session was closed.

Cause: SPARQLSession.close() (or exiting a with block when close_on_exit=True) marks the session closed. Further use of that session object is invalid.

Fix: Open a new session for the same store::

with SPARQLSession(store=shared_store) as session:
    session.put(model)

Do not keep a session reference past request teardown when using FastAPI SessionDep.

Thread safety / corrupted session state

Symptom: Intermittent wrong cache or flush behavior under concurrency.

Cause: Sharing one SPARQLSession across threads or async tasks.

Fix: One session per request/task; share the store only.

Build / import errors

Error

Fix

No module named 'httpx'

pip install "sparqlmodel[http]"

No module named 'fastapi'

pip install "sparqlmodel[fastapi]"

triplemodel version conflict

pip install "triplemodel>=0.10.0,<2" "pyoxigraph>=0.5,<0.6"

Getting help