Structured add/update/replace/remove diff between two RDF.Graph
snapshots of a rendered mapping (e.g. two successive R2RML Turtle renders of
the same AshR2RML.Mapping.Resource, or two successive
AshR2RML.OBDA.InMemory.materialize/3 snapshots).
Ported from Gno's Gno.Changeset action-graph algebra
(~/gno/lib/gno/changeset/{changeset,action,validation}.ex), stripped of
everything that assumes a live, queryable triple store: Gno's changeset is
ultimately applied to and diffed against a running SPARQL store via
Gno.EffectiveChangeset.Query's SPARQL-CONSTRUCT algorithm. AshR2RML never
has such a store -- Ontop is virtual OBDA over Postgres, not a materialized
graph -- so this module keeps only the store-independent half: the action
vocabulary (add/update/replace/remove), its mutual-exclusion
validation rules, and inversion, all as pure RDF.Graph transformations.
Action semantics
:add-- statements present in the new snapshot but absent from the old:remove-- statements present in the old snapshot but absent from the new:update-- statements whose subject+predicate persist across both snapshots but whose object changed (a predicate-level replacement):replace-- statements for a subject that exists in both snapshots but whose entire description changed (a subject-level replacement); mutually exclusive with:updatefor the same subject, matching Gno's validation rule that a subject cannot be both partially updated and wholly replaced in the same changeset
Use diff/3 to compute a changeset between two snapshots (defaults to a pure
add/remove diff -- the :update/:replace classification requires the
caller to say which predicates constitute an entity's identity vs. its
mutable state, which AshR2RML's own AshR2RML.Mapping.Resource IR already
knows and can supply via subject_identity_predicates).
Summary
Functions
The changeset action field names, in a fixed, deterministic order.
Computes a pure add/remove changeset between two RDF.Graph snapshots:
statements only in new_graph become :add; statements only in old_graph
become :remove. Statements in both graphs are left out of the changeset
entirely (unchanged). This is the store-independent replacement for Gno's
EffectiveChangeset.Query SPARQL-CONSTRUCT diff -- both inputs are already
in-memory graphs, so no store round-trip is needed.
True if the changeset has no statements in any action (a no-op diff).
The graph of statements this changeset would insert if applied: :add,
:update, and :replace all contribute new statements; :remove does not.
Inverts a changeset: what would undo its effect if applied on top of the
result. :add becomes :remove and vice versa; :update and :replace
fold into :remove (the caller must supply the prior state's statements to
restore them -- inversion alone cannot recover data an update/replace
overwrote, matching Gno's own documented limitation for these two actions).
Merges every action graph of changeset into a single graph of all
statements it touches, regardless of action (add/update/replace/remove
combined) -- the union Gno calls merged_graph/1.
Builds a changeset from keyword/map options; each of :add, :update,
:replace, :remove is optional and defaults to nil (no statements for
that action). Returns {:error, %Refusal{}} if validation fails.
The graph of statements this changeset would remove if applied.
Validates a changeset's mutual-exclusion invariants (ported from
Gno.Changeset.Validation)
Types
@type t() :: %AshR2RML.Mapping.Changeset{ add: RDF.Graph.t() | nil, remove: RDF.Graph.t() | nil, replace: RDF.Graph.t() | nil, update: RDF.Graph.t() | nil }
Functions
@spec action_fields() :: [atom()]
The changeset action field names, in a fixed, deterministic order.
@spec diff(RDF.Graph.t(), RDF.Graph.t()) :: {:ok, t()} | {:error, AshR2RML.Refusal.t()}
Computes a pure add/remove changeset between two RDF.Graph snapshots:
statements only in new_graph become :add; statements only in old_graph
become :remove. Statements in both graphs are left out of the changeset
entirely (unchanged). This is the store-independent replacement for Gno's
EffectiveChangeset.Query SPARQL-CONSTRUCT diff -- both inputs are already
in-memory graphs, so no store round-trip is needed.
True if the changeset has no statements in any action (a no-op diff).
@spec inserts(t()) :: RDF.Graph.t()
The graph of statements this changeset would insert if applied: :add,
:update, and :replace all contribute new statements; :remove does not.
Inverts a changeset: what would undo its effect if applied on top of the
result. :add becomes :remove and vice versa; :update and :replace
fold into :remove (the caller must supply the prior state's statements to
restore them -- inversion alone cannot recover data an update/replace
overwrote, matching Gno's own documented limitation for these two actions).
@spec merged_graph(t()) :: RDF.Graph.t()
Merges every action graph of changeset into a single graph of all
statements it touches, regardless of action (add/update/replace/remove
combined) -- the union Gno calls merged_graph/1.
@spec new(keyword() | map()) :: {:ok, t()} | {:error, AshR2RML.Refusal.t()}
Builds a changeset from keyword/map options; each of :add, :update,
:replace, :remove is optional and defaults to nil (no statements for
that action). Returns {:error, %Refusal{}} if validation fails.
@spec removals(t()) :: RDF.Graph.t()
The graph of statements this changeset would remove if applied.
@spec validate(t()) :: {:ok, t()} | {:error, AshR2RML.Refusal.t()}
Validates a changeset's mutual-exclusion invariants (ported from
Gno.Changeset.Validation):
- the same triple must not appear in both
:addand:remove - the same subject must not be described in both
:updateand:replace
Returns {:ok, changeset} on success, {:error, %Refusal{}} naming the
conflicting subject/triple otherwise.