Behaviour for OBDA query engines, so a caller can dispatch to whichever engine a given mapping/resource actually needs without hand-checking which concrete module to call.
AshR2RML today has two independently-evolved OBDA execution paths --
AshR2RML.OBDA.Ontop (real Ontop CLI process, SPARQL-to-SQL rewriting over a
JDBC-connected Postgres) and AshR2RML.OBDA.InMemory (real Ash.read!/2 +
in-process SPARQL algebra, for any data layer Ontop cannot reach) -- with no
shared interface between them. This behaviour is modeled after Gno's
Gno.Store.Adapter (~/gno/lib/gno/store/adapter.ex): a minimal callback
contract that lets a caller dispatch/2 by config-struct type, the same way
Gno.Store pattern-matches %adapter_type{} to call adapter_type.<fn>.
What is deliberately not ported from Gno.Store.Adapter: SPARQL Graph
Store Protocol endpoint resolution (determine_query_endpoint/1,
determine_update_endpoint/1, determine_graph_store_endpoint/1), HTTP
dispatch (Gno.Store.GenSPARQL), and default/union graph-semantics
configuration. AshR2RML never talks to a live SPARQL protocol server -- Ontop
is invoked as a one-shot CLI process per query, and InMemory never leaves
the BEAM -- so there is no persistent store connection to abstract over.
Summary
Callbacks
The engine's canonical identifier, e.g. :ontop or :in_memory.
Executes sparql against whatever config names (a mapping file pair for
Ontop, an Ash resource + mapping IR for InMemory), returning an
engine-specific observation struct wrapped in {:ok, _} on success or
{:error, %Refusal{}} / {:error, observation} on failure -- unified only
at the ok/error-tuple level, since the two engines' observation structs
(AshR2RML.OBDA.Observation vs AshR2RML.SPARQL.Observation) carry
genuinely different evidence (an external process's exit status and stdout
vs. an in-process materialization's row set) and forcing them into one shape
would erase real information rather than unify a real abstraction.
Functions
Dispatches query/3 to the adapter module matching config's struct type
(mirroring Gno.Store's %adapter_type{} -> adapter_type.<fn> dispatch).
Callbacks
@callback engine_name() :: atom()
The engine's canonical identifier, e.g. :ontop or :in_memory.
@callback query( config :: struct() | keyword() | map(), sparql :: String.t(), opts :: keyword() ) :: {:ok, struct()} | {:error, struct() | AshR2RML.Refusal.t()}
Executes sparql against whatever config names (a mapping file pair for
Ontop, an Ash resource + mapping IR for InMemory), returning an
engine-specific observation struct wrapped in {:ok, _} on success or
{:error, %Refusal{}} / {:error, observation} on failure -- unified only
at the ok/error-tuple level, since the two engines' observation structs
(AshR2RML.OBDA.Observation vs AshR2RML.SPARQL.Observation) carry
genuinely different evidence (an external process's exit status and stdout
vs. an in-process materialization's row set) and forcing them into one shape
would erase real information rather than unify a real abstraction.