Agentic Marketing: Databricks, Virtuoso, and Both Together

One decision loop, three paths: Databricks-native, Virtuoso-native, and the two together over ODBC, with the live proof on demo.openlinksw.com.

Executive SummaryBy Elena Tesser · Databricks · 2026-10-01

Synopsis

Agentic marketing uses AI agents grounded in trusted customer, business and decision context to recommend the next best action for each customer within guardrails marketers set. The post argues that identity resolution is the foundation, that agents need four kinds of context (customer, business, decision and control), that an agentic CDP such as Databricks CustomerLake turns that context into governed action, and that measurement belongs inside the decision loop, with incrementality as the shared language between marketing and finance.

View this analysis as a KG entity
The Source

What the Article Says

Elena Tesser and Alexandra Haefele of Databricks argue that an agent can only act as well as the context it is given, that identity is what holds that context together, and that measurement belongs inside the decision loop rather than after it.

4
Kinds of context
Customer, business, decision and control. One profile can run to thousands of columns: a single transaction carries 30 to 50 metadata parameters.
5
Incremental impact, true profit contribution, contribution by lever, marginal return, and customer value over time.
30% / 15%
Acxiom on Databricks
Roughly 30 percent faster time to actionable customer insight and about 15 percent lower operating cost, after rebuilding identity services as a native layer with Lovelytics.
2
Agent types
Profile Agents build Customer 360 profiles; Campaign Agents build audiences, recommend next-best actions and activate.
15,000+
Martech tools
The article's figure, citing State of Martech 2026, before counting the MCP integrations agents add.

The worked example is a lapsed quick-service-restaurant customer. The reflexive move is another discount; the better move starts with the decision, assembles purchase history and pre-purchase signals, asks whether an incentive would change behaviour or merely subsidise an order, applies guardrails on discount depth, contact frequency and consent, and measures the result against taking no action. CustomerLake, governed by Unity Catalog and explored through Genie, is the article's answer to where that loop runs.

Two points are carried into the comparison below: the article treats identity resolution as a capability maintained continuously rather than a profile-building project with a finish line, and it names incrementality as the shared language of marketing and finance. It does not describe a published schema for the four kinds of context, identifiers that resolve outside the platform, or how an agent's own standing rules are stored, which is where the Virtuoso paths are strongest.

The Alternatives

Three Paths Through the Same Loop

The article's loop is resolve identity, assemble context, decide within guardrails, act, measure against doing nothing, and feed the result back. Each path below runs that loop on a different foundation, and the third combines the first two.

Path A · Databricks-native

CustomerLake in the lakehouse

  1. Agentic Identity Resolution builds Customer 360 profiles
  2. Context and decisions live next to the data, governed by Unity Catalog
  3. Campaign Agents decide and activate; Genie answers questions
  4. Incrementality feeds the next decision

Strength: one managed platform, a partner-backed identity engine and activation.

Path B · Virtuoso-native

Virtuoso with agent-rdf-memory

  1. Identity signals, consent, offers, guardrails, decisions and outcomes become entities with resolvable IRIs
  2. An RDF View projects SQL tables as a graph at query time; no copy
  3. SQL, SPARQL and SPASQL query the same rows; entailment groups the four kinds of context
  4. The harness keeps the working agent's standing rules and session memory as HowToSteps in the same store

Strength: resolvable identifiers, an explicit context model, auditable decisions and an open query model. Proven live below.

Path C · Complementary

Databricks tables attached over ODBC

  1. The lakehouse stays the system of record
  2. Virtuoso attaches the tables through an ODBC data source
  3. Generators emit a quad map, an ontology and rewrite rules over them
  4. The Databricks estate gains entity IRIs and SPARQL without a copy

Strength: no migration. One extra step: every mapped column needs privacy review.

The honest division of labour: Path A is where identity matching, models and activation happen; Path B is a linked, auditable record of context, decisions and outcomes; Path C lets the two meet over data that never moves.

Thirteen Axes

Where the Approaches Diverge

Each row states the Databricks position as the article gives it, the Virtuoso position as the deployed stack (Virtuoso with the agent-rdf-memory harness and its skills) demonstrates or documents it, and an assessment that names what the difference costs. Every row carries an evidence label, because a claim demonstrated live and a claim stated by a vendor are not the same kind of claim. Rows are ranked on the evidence for both sides; Even means genuine parity, and a row that names a difference leans the way the difference points. A lean is a judgement about one axis, not a verdict on either platform.

0
Lean Databricks
None on this evidence. The article describes probabilistic matching, activation and three measurement methods but shows none, and none is tested here.
11
Lean Virtuoso
Identity reconciliation by reasoning, resolvable identifiers, the four-group context model, decision memory, guardrail audit, natural-language access, the activation queue, the incrementality mechanism, open query languages, federation and deployment breadth.
2
Even
Where governed context lives, and using the two together: genuine parity, not a hedge.

Showing one card per platform so the thirteen dimensions stay readable on a narrow screen.

DimensionDatabricksVirtuoso + agent-rdf-memoryAssessment
Resolving who is whoAgentic Identity Resolution blends deterministic, probabilistic and agentic matching with Acxiom's graph-based identity, hygiene, matching and enrichment. Acxiom's Real ID engine runs natively inside Databricks, so brands resolve data where it lives. The article describes matching engines and a partner graph; it does not describe ontology-based reasoning.Identity reconciliation is native reasoning, with no third-party engine: owl:sameAs applies explicitly and an owl:InverseFunctionalProperty designation applies implicitly. On demo, a graph of CRM, app and loyalty records is reconciled with the customers in the view (Query 3): with no reasoning each customer has one name; with explicit owl:sameAs Avery Quinn gains her loyalty record; with ckg:emailHash declared inverse functional the CRM export and the app profile that share her hash join too, so she is known under four records and Blake Moreno under two, while a record that matches nobody stays apart. Attaching Databricks tables over ODBC brings the same reasoning to that data without a copy. For master lookups, OpenLink hosts DBpedia and many enclaves of the LOD Cloud (stated by OpenLink). Match method and confidence are recorded per signal (Query 2). A probabilistic or agentic matcher is not shown here; matches from any engine can be held as owl:sameAs and reasoned over.Virtuoso leads on reconciliation, because reasoning applies the same identity rules to every record in the store, explicitly or by property designation, without a pipeline or a partner, and it is demonstrated here and extends to Databricks data attached over ODBC. The article describes a matching engine of a different kind, probabilistic and agentic with a partner graph, which this page does not show for Virtuoso; the two are complementary, since matches from either can be asserted as owl:sameAs and reasoned over.
Virtuoso edge

Evidence: Virtuoso: demonstrated on this page, Queries 3 and 2, over synthetic records, on a physical graph; DBpedia and LOD Cloud hosting stated by OpenLink. Databricks: the article, including the Acxiom and Lovelytics figures, which are vendor statements not independently checked.

Identity that outlives the systemThe article treats identity as a maintained profile and a framework that traces a result back to the customer and the decision that produced it. It does not describe identifiers that resolve outside the platform.Every entity in the CustomerKG view has an HTTP IRI that answers with a 303 to a description page and can be referenced from any other graph. A decision, an offer and a guardrail are as linkable as a customer. Hyperlinks act as stable identifiers across stores, and 176 entity IRIs were checked to resolve.For one estate this is a convenience. It becomes the main difference when exposure, engagement and outcome live in different organisations' systems and must be joined without a shared key.
Virtuoso edge

Evidence: Virtuoso: demonstrated on this page, entity IRIs resolve. Databricks: the article is silent.

Where governed context livesCustomerLake is embedded in the lakehouse and governed by Unity Catalog, so marketing operates on the same data and AI foundation as the rest of the business instead of creating another copy.An RDF View projects the tables in place and inherits their SQL grants (demonstrated). Over ODBC, Databricks tables stay in Databricks and Virtuoso attaches them, so the view is computed from the remote rows at query time. SPARQL SERVICE reads the same view from a second instance without copying it (Queries 13 and 14).Genuine parity on this axis: both keep context where it already lives and neither asks the marketer to maintain another copy. The differences between the approaches are on the neighbouring rows.
Even

Evidence: Virtuoso: demonstrated on this page for local tables and across two instances; the ODBC attachment is documented and its metadata read, not queried. Databricks: the article.

Modelling the four kinds of contextThe article names the four kinds of context and notes that one profile can run to thousands of columns, but it does not publish a schema for them.An ontology makes the four groups classes: ConsentRecord is both a customer fact and a control, and an RDFS rule set lets one query for ControlElement return the 5 guardrails and 38 consent records that are only ever typed as their subclasses (Query 6). Without the rule set the same query returns nothing.A silence in an article is not a gap in a product, so this axis records what is demonstrated. Virtuoso shows an explicit, queryable model; Databricks may well have one that the article does not describe.
Virtuoso edge

Evidence: Virtuoso: demonstrated on this page, Queries 6 and 7 and the ontology. Databricks: the article names the four kinds only.

Remembering what was decided and whyDecision context captures the offers a customer has already received, how they responded and why earlier decisions were made, and the result goes back into the customer's context for the next decision.Each decision is a row with its arm, action, offer, channel, rationale and approval flag, linked to the outcome that followed (Queries 1 and 8). Context items record which of the four kinds each fact was read from. The agent-rdf-memory harness applies the same pattern to the working agent: sessions, lessons and standing preferences are queryable named graphs retrieved by an ontology-routed query.Both describe the same loop. Virtuoso's version is demonstrated, with a rationale column an auditor can read, context items tagged by kind and each outcome linked to its decision; Databricks' is described as product behaviour in the article. The lean follows what is shown, not a measured advantage.
Virtuoso edge

Evidence: Virtuoso: demonstrated on this page, Queries 1 and 8; harness memory documented in the repository. Databricks: the article.

Guardrails, consent and approval pointsControl is the layer of intent, guardrails, permissions and approval points that people define and agents must respect, including where human approval is required.Guardrails are entities linked to every decision they constrain (52 links). Query 4 audits the human-approval rules against the decisions that triggered them, and Query 9 joins withdrawn consent in the graph to the native decision table and finds no contact on a withdrawn channel. The harness holds the working agent's own standing rules the same way, as HowToSteps.Both treat control as first-class. What differs is what is shown: Virtuoso's guardrails, their application and their audit are demonstrated as queries, while Databricks' control layer is described in the article. Enforcement, meaning whether an agent can be stopped from breaking a rule, was tested on neither side.
Virtuoso edge

Evidence: Virtuoso: demonstrated on this page, Queries 4, 6 and 9, as audit rather than enforcement. Databricks: the article.

Asking in plain languageGenie lets marketers use natural language to explore customer context and build audiences, reducing the wait for custom data pulls.Natural-language access is part of the deployed stack: OPAL agents, the OpenLink MCP tools (the virtuoso-support-agent skill documents 25), and packaged SKILL.md skills such as data-twingler, dbpedia-query-skill and wikidata-query-skill that turn a question into SQL, SPARQL or SPASQL. This whole page was produced from a plain-language request, with the view, ontology and rewrite-rule generators called through the OpenLink MCP tools and the DDL and DML run through exec over XMLA.Both offer plain-language access to the data. Virtuoso's is demonstrated here in building and querying the estate; Genie's is described in the article. Genie is presented as an integrated experience for marketers and this page shows no marketer-facing interface on the Virtuoso side, so the lean rests on demonstrated availability, not on a comparison of usability.
Virtuoso edge

Evidence: Virtuoso: demonstrated by how this page was built; skills and MCP tooling documented in the repository. Databricks: the article.

Acting across channelsCampaign Agents activate across channels and Infinity Campaigns are always-on loops that identify an audience, recommend the next-best offer or channel, activate across destinations, and optimise or suppress based on performance. CustomerLake works with partners already in a brand's stack.The estate yields an activation queue as a query (Query 5): each decision that has a channel, joined to the consent record that allows that channel, so every row says what would be sent, to whom, on which owned or paid channel and under which consent, and suppressions, holdouts and unconsented contacts are excluded by the join. Delivery uses the deployed stack's own channels: email through the OpenLink send_email function and the weblog digests of the weblog-from-webdav skill, ActivityPub publishing through the fediverse-crud skill, and commerce flows through the acp-client and ucp-client skills. Nothing was sent from this page.Virtuoso leads on what is shown: the consent-checked handoff is demonstrated as a query over the decision data, where the article describes activation across destinations without showing it. Delivery was run for neither side here and no destination catalogue is shown for either, so the lean rests on the demonstrated handoff and not on delivery breadth.
Virtuoso edge

Evidence: Virtuoso: demonstrated on this page, Query 5, as a queue rather than a send; delivery skills documented in the repository. Databricks: the article.

Measuring what changed because of the actionIncrementality is the common language between marketing and finance, supported by attribution, incrementality testing and marketing mix modeling, with conversational analytics to explore what happened and why. The article lists five things a CMO must show and gives no schema, numbers or worked calculation.A randomised holdout arm and an outcome per decision make the lift a SQL query (Query 10): in the reactivation campaign the treated order rate is 66 percent against 40, with about 4 dollars of incremental profit; in the discount test the treated rate is 75 percent against 100 and incremental profit is minus 15 dollars, a subsidy. Query 11 splits contribution by owned channel, paid channel and no channel. Every interaction, administrative or querying, can be carried out in natural language against the same linked data that the RDF Views publish, so a further analysis is a question put to that data rather than a product feature to wait for.Virtuoso leads on what is shown: a holdout, an outcome per decision and the lift, profit contribution, contribution by lever and a subsidy finding as queries over linked rows, on synthetic numbers, with any further analysis available by asking in natural language, where the article names the concept, the methods and the five proofs without showing one. Neither side demonstrates attribution or a marketing mix model here, so no method-count lead is claimed for either.
Virtuoso edge

Evidence: Virtuoso: demonstrated on this page, Queries 10 and 11, on synthetic data; natural-language access demonstrated by how this page was built. Databricks: the article.

Languages and standardsNatural language through Genie over the lakehouse. The article names no query language.SQL, SPARQL, SPASQL and GraphQL in one engine, with GQL stated by OpenLink; SPASQL nests a SPARQL query in a SQL SELECT so a graph result joins native tables (Queries 8 and 9), and the SPARQL compiler's generated SQL can be read (Query 12).An open W3C query language, several languages in one engine and one-statement graph-to-table joins are advantages for integration and audit, and the generated SQL can be inspected. The article describes one interface, natural language, and is silent on inspecting what it generates.
Virtuoso edge

Evidence: Virtuoso: demonstrated on this page, Queries 1 to 14; GQL stated by OpenLink. Databricks: the article.

Joining with identity and measurement partnersCustomerLake is designed to work with the identity, activation, measurement and customer-experience partners a brand already uses, so trusted context flows across channels without a new silo. It launched with Acxiom and is supported by Lovelytics.Resolvable IRIs and SPARQL federation let a query span graphs held by different instances, each answering for its own data. Demonstrated here between two OpenLink instances: Queries 13 and 14 run on URIBurner and read the CustomerKG view on demo through SERVICE. That is cross-instance, not cross-organisation; a query across different parties' graphs was not run.Databricks' route is a partner ecosystem on one platform, as the article states; Virtuoso's is links between platforms, and the mechanism is demonstrated across two instances. The lean rests on the demonstrated mechanism; whether a given partner exposes a SPARQL endpoint is a separate question.
Virtuoso edge

Evidence: Virtuoso: demonstrated on this page, Queries 13 and 14, across two instances. Databricks: the article.

Operating the platformA managed lakehouse platform with the CDP, governance, assistant and agents delivered as products, and a services ecosystem to put them into production.Deployment is a choice: on premises, Docker containers, cloud machine images on AWS, Azure and Google Cloud including pay-as-you-go offers, and a self-managed service on AWS. OpenLink Managed services are announced for September 2026; their availability was not checked and they are not credited here. Configuration and administration are available in natural language through the OpenLink MCP tooling and packaged SKILL.md skills, and this build is an instance of that.Databricks offers a managed platform with its products integrated; Virtuoso offers broader deployment choice and agent-driven administration, which this page demonstrates, while its managed service is only announced. The lean follows deployment breadth and demonstrated agent administration, not measured operational cost or effort.
Virtuoso edge

Evidence: Databricks: the article. Virtuoso: cloud offers confirmed in OpenLink community posts on 2026-09-30; other deployment options and managed services stated by OpenLink without independent checking; agent administration demonstrated on this page.

Using the two togetherDatabricks is the platform where the data, models and governance already sit.Virtuoso attaches Databricks tables over ODBC and generates RDF Views, rewrite rules and an ontology over them, so the Databricks estate gains resolvable entity IRIs, SPARQL and identity reconciliation by reasoning without a copy. Demo already holds such an attachment, and the generator's output for it is shown on this page without being executed.Complementary rather than competing, which is genuine parity on this axis. The lakehouse stays the system of record and the place for identity matching, models and activation; Virtuoso adds a linked, resolvable, auditable view over the same rows. The one extra step is a privacy review of every column the mapping publishes, as the card-number column shows.
Even

Evidence: Virtuoso: ODBC attachment present on demo, view generated and not executed. Databricks: tables remain in place.

DatabricksThe article
Agentic Identity Resolution blends deterministic, probabilistic and agentic matching with Acxiom's graph-based identity, hygiene, matching and enrichment. Acxiom's Real ID engine runs natively inside Databricks, so brands resolve data where it lives. The article describes matching engines and a partner graph; it does not describe ontology-based reasoning.
The article treats identity as a maintained profile and a framework that traces a result back to the customer and the decision that produced it. It does not describe identifiers that resolve outside the platform.
CustomerLake is embedded in the lakehouse and governed by Unity Catalog, so marketing operates on the same data and AI foundation as the rest of the business instead of creating another copy.
The article names the four kinds of context and notes that one profile can run to thousands of columns, but it does not publish a schema for them.
Decision context captures the offers a customer has already received, how they responded and why earlier decisions were made, and the result goes back into the customer's context for the next decision.
Control is the layer of intent, guardrails, permissions and approval points that people define and agents must respect, including where human approval is required.
Genie lets marketers use natural language to explore customer context and build audiences, reducing the wait for custom data pulls.
Campaign Agents activate across channels and Infinity Campaigns are always-on loops that identify an audience, recommend the next-best offer or channel, activate across destinations, and optimise or suppress based on performance. CustomerLake works with partners already in a brand's stack.
Incrementality is the common language between marketing and finance, supported by attribution, incrementality testing and marketing mix modeling, with conversational analytics to explore what happened and why. The article lists five things a CMO must show and gives no schema, numbers or worked calculation.
Natural language through Genie over the lakehouse. The article names no query language.
CustomerLake is designed to work with the identity, activation, measurement and customer-experience partners a brand already uses, so trusted context flows across channels without a new silo. It launched with Acxiom and is supported by Lovelytics.
A managed lakehouse platform with the CDP, governance, assistant and agents delivered as products, and a services ecosystem to put them into production.
Databricks is the platform where the data, models and governance already sit.
Identity reconciliation is native reasoning, with no third-party engine: owl:sameAs applies explicitly and an owl:InverseFunctionalProperty designation applies implicitly. On demo, a graph of CRM, app and loyalty records is reconciled with the customers in the view (Query 3): with no reasoning each customer has one name; with explicit owl:sameAs Avery Quinn gains her loyalty record; with ckg:emailHash declared inverse functional the CRM export and the app profile that share her hash join too, so she is known under four records and Blake Moreno under two, while a record that matches nobody stays apart. Attaching Databricks tables over ODBC brings the same reasoning to that data without a copy. For master lookups, OpenLink hosts DBpedia and many enclaves of the LOD Cloud (stated by OpenLink). Match method and confidence are recorded per signal (Query 2). A probabilistic or agentic matcher is not shown here; matches from any engine can be held as owl:sameAs and reasoned over.
Every entity in the CustomerKG view has an HTTP IRI that answers with a 303 to a description page and can be referenced from any other graph. A decision, an offer and a guardrail are as linkable as a customer. Hyperlinks act as stable identifiers across stores, and 176 entity IRIs were checked to resolve.
An RDF View projects the tables in place and inherits their SQL grants (demonstrated). Over ODBC, Databricks tables stay in Databricks and Virtuoso attaches them, so the view is computed from the remote rows at query time. SPARQL SERVICE reads the same view from a second instance without copying it (Queries 13 and 14).
An ontology makes the four groups classes: ConsentRecord is both a customer fact and a control, and an RDFS rule set lets one query for ControlElement return the 5 guardrails and 38 consent records that are only ever typed as their subclasses (Query 6). Without the rule set the same query returns nothing.
Each decision is a row with its arm, action, offer, channel, rationale and approval flag, linked to the outcome that followed (Queries 1 and 8). Context items record which of the four kinds each fact was read from. The agent-rdf-memory harness applies the same pattern to the working agent: sessions, lessons and standing preferences are queryable named graphs retrieved by an ontology-routed query.
Guardrails are entities linked to every decision they constrain (52 links). Query 4 audits the human-approval rules against the decisions that triggered them, and Query 9 joins withdrawn consent in the graph to the native decision table and finds no contact on a withdrawn channel. The harness holds the working agent's own standing rules the same way, as HowToSteps.
Natural-language access is part of the deployed stack: OPAL agents, the OpenLink MCP tools (the virtuoso-support-agent skill documents 25), and packaged SKILL.md skills such as data-twingler, dbpedia-query-skill and wikidata-query-skill that turn a question into SQL, SPARQL or SPASQL. This whole page was produced from a plain-language request, with the view, ontology and rewrite-rule generators called through the OpenLink MCP tools and the DDL and DML run through exec over XMLA.
The estate yields an activation queue as a query (Query 5): each decision that has a channel, joined to the consent record that allows that channel, so every row says what would be sent, to whom, on which owned or paid channel and under which consent, and suppressions, holdouts and unconsented contacts are excluded by the join. Delivery uses the deployed stack's own channels: email through the OpenLink send_email function and the weblog digests of the weblog-from-webdav skill, ActivityPub publishing through the fediverse-crud skill, and commerce flows through the acp-client and ucp-client skills. Nothing was sent from this page.
A randomised holdout arm and an outcome per decision make the lift a SQL query (Query 10): in the reactivation campaign the treated order rate is 66 percent against 40, with about 4 dollars of incremental profit; in the discount test the treated rate is 75 percent against 100 and incremental profit is minus 15 dollars, a subsidy. Query 11 splits contribution by owned channel, paid channel and no channel. Every interaction, administrative or querying, can be carried out in natural language against the same linked data that the RDF Views publish, so a further analysis is a question put to that data rather than a product feature to wait for.
SQL, SPARQL, SPASQL and GraphQL in one engine, with GQL stated by OpenLink; SPASQL nests a SPARQL query in a SQL SELECT so a graph result joins native tables (Queries 8 and 9), and the SPARQL compiler's generated SQL can be read (Query 12).
Resolvable IRIs and SPARQL federation let a query span graphs held by different instances, each answering for its own data. Demonstrated here between two OpenLink instances: Queries 13 and 14 run on URIBurner and read the CustomerKG view on demo through SERVICE. That is cross-instance, not cross-organisation; a query across different parties' graphs was not run.
Deployment is a choice: on premises, Docker containers, cloud machine images on AWS, Azure and Google Cloud including pay-as-you-go offers, and a self-managed service on AWS. OpenLink Managed services are announced for September 2026; their availability was not checked and they are not credited here. Configuration and administration are available in natural language through the OpenLink MCP tooling and packaged SKILL.md skills, and this build is an instance of that.
Virtuoso attaches Databricks tables over ODBC and generates RDF Views, rewrite rules and an ontology over them, so the Databricks estate gains resolvable entity IRIs, SPARQL and identity reconciliation by reasoning without a copy. Demo already holds such an attachment, and the generator's output for it is shown on this page without being executed.
AssessmentWhat it costs
Virtuoso leads on reconciliation, because reasoning applies the same identity rules to every record in the store, explicitly or by property designation, without a pipeline or a partner, and it is demonstrated here and extends to Databricks data attached over ODBC. The article describes a matching engine of a different kind, probabilistic and agentic with a partner graph, which this page does not show for Virtuoso; the two are complementary, since matches from either can be asserted as owl:sameAs and reasoned over.
Virtuoso edge

Evidence: Virtuoso: demonstrated on this page, Queries 3 and 2, over synthetic records, on a physical graph; DBpedia and LOD Cloud hosting stated by OpenLink. Databricks: the article, including the Acxiom and Lovelytics figures, which are vendor statements not independently checked.

For one estate this is a convenience. It becomes the main difference when exposure, engagement and outcome live in different organisations' systems and must be joined without a shared key.
Virtuoso edge

Evidence: Virtuoso: demonstrated on this page, entity IRIs resolve. Databricks: the article is silent.

Genuine parity on this axis: both keep context where it already lives and neither asks the marketer to maintain another copy. The differences between the approaches are on the neighbouring rows.
Even

Evidence: Virtuoso: demonstrated on this page for local tables and across two instances; the ODBC attachment is documented and its metadata read, not queried. Databricks: the article.

A silence in an article is not a gap in a product, so this axis records what is demonstrated. Virtuoso shows an explicit, queryable model; Databricks may well have one that the article does not describe.
Virtuoso edge

Evidence: Virtuoso: demonstrated on this page, Queries 6 and 7 and the ontology. Databricks: the article names the four kinds only.

Both describe the same loop. Virtuoso's version is demonstrated, with a rationale column an auditor can read, context items tagged by kind and each outcome linked to its decision; Databricks' is described as product behaviour in the article. The lean follows what is shown, not a measured advantage.
Virtuoso edge

Evidence: Virtuoso: demonstrated on this page, Queries 1 and 8; harness memory documented in the repository. Databricks: the article.

Both treat control as first-class. What differs is what is shown: Virtuoso's guardrails, their application and their audit are demonstrated as queries, while Databricks' control layer is described in the article. Enforcement, meaning whether an agent can be stopped from breaking a rule, was tested on neither side.
Virtuoso edge

Evidence: Virtuoso: demonstrated on this page, Queries 4, 6 and 9, as audit rather than enforcement. Databricks: the article.

Both offer plain-language access to the data. Virtuoso's is demonstrated here in building and querying the estate; Genie's is described in the article. Genie is presented as an integrated experience for marketers and this page shows no marketer-facing interface on the Virtuoso side, so the lean rests on demonstrated availability, not on a comparison of usability.
Virtuoso edge

Evidence: Virtuoso: demonstrated by how this page was built; skills and MCP tooling documented in the repository. Databricks: the article.

Virtuoso leads on what is shown: the consent-checked handoff is demonstrated as a query over the decision data, where the article describes activation across destinations without showing it. Delivery was run for neither side here and no destination catalogue is shown for either, so the lean rests on the demonstrated handoff and not on delivery breadth.
Virtuoso edge

Evidence: Virtuoso: demonstrated on this page, Query 5, as a queue rather than a send; delivery skills documented in the repository. Databricks: the article.

Virtuoso leads on what is shown: a holdout, an outcome per decision and the lift, profit contribution, contribution by lever and a subsidy finding as queries over linked rows, on synthetic numbers, with any further analysis available by asking in natural language, where the article names the concept, the methods and the five proofs without showing one. Neither side demonstrates attribution or a marketing mix model here, so no method-count lead is claimed for either.
Virtuoso edge

Evidence: Virtuoso: demonstrated on this page, Queries 10 and 11, on synthetic data; natural-language access demonstrated by how this page was built. Databricks: the article.

An open W3C query language, several languages in one engine and one-statement graph-to-table joins are advantages for integration and audit, and the generated SQL can be inspected. The article describes one interface, natural language, and is silent on inspecting what it generates.
Virtuoso edge

Evidence: Virtuoso: demonstrated on this page, Queries 1 to 14; GQL stated by OpenLink. Databricks: the article.

Databricks' route is a partner ecosystem on one platform, as the article states; Virtuoso's is links between platforms, and the mechanism is demonstrated across two instances. The lean rests on the demonstrated mechanism; whether a given partner exposes a SPARQL endpoint is a separate question.
Virtuoso edge

Evidence: Virtuoso: demonstrated on this page, Queries 13 and 14, across two instances. Databricks: the article.

Databricks offers a managed platform with its products integrated; Virtuoso offers broader deployment choice and agent-driven administration, which this page demonstrates, while its managed service is only announced. The lean follows deployment breadth and demonstrated agent administration, not measured operational cost or effort.
Virtuoso edge

Evidence: Databricks: the article. Virtuoso: cloud offers confirmed in OpenLink community posts on 2026-09-30; other deployment options and managed services stated by OpenLink without independent checking; agent administration demonstrated on this page.

Complementary rather than competing, which is genuine parity on this axis. The lakehouse stays the system of record and the place for identity matching, models and activation; Virtuoso adds a linked, resolvable, auditable view over the same rows. The one extra step is a privacy review of every column the mapping publishes, as the card-number column shows.
Even

Evidence: Virtuoso: ODBC attachment present on demo, view generated and not executed. Databricks: tables remain in place.

Live Demonstration

CustomerKG — The Decision Loop, Running

The demonstration is about a SQL system of record, which is what a lakehouse table is to a graph layer. The article's scenario, a quick-service-restaurant brand deciding what to do for each lapsing customer, was built as ordinary SQL tables on demo.openlinksw.com, given an ontology, projected as an RDF View, made resolvable with URL rewrite rules and queried with SQL, SPASQL and SPARQL. The rows are synthetic and none of it is Databricks data; it proves the Virtuoso mechanics the loop would run on. The material is grouped by server, then by function, then by query language.

12
Normalised, foreign-key constrained, 228 synthetic rows on demo.openlinksw.com. Every name is invented.
419
16 classes and 47 properties under schema.org supertypes, with a four-group context hierarchy.
1,160
Computed at query time from the tables, and matching a hand count from the source rows. Not one is stored.
14
Live queries
12 on demo (7 SPARQL, 2 SPASQL, 3 SQL) and 2 SPARQL queries on URIBurner that reach the view on demo through SERVICE. All run before being written down.
Server 1

demo.openlinksw.com

Where the data lives. Everything here was executed as the kidehen account: DDL, RDF View generation, Linked Data deployment and 12 queries.

All of it in one script

1 card

The copy-paste batch for your own Virtuoso: the same statements as the cards below, in the order a fresh database needs them.

One script: tables, rows, IRI classes, ontology step, RDF View, grants, rewrite rules

SQL

The consolidated copy-paste script for running the whole CustomerKG demonstration on your own Virtuoso. Its purpose is execution, not exposition: the same statements as the cards above in the order a fresh database needs them. The table and row statements are standard SQL; the IRI class, quad map and rewrite-rule statements are Virtuoso-specific and are the part most likely to need adjustment on another version.

-- customerkg-agentic-marketing.sql
-- CustomerKG: a synthetic quick-service-restaurant estate that holds customer, business, decision and control context,
-- projected as an RDF View. All rows are invented for illustration. Standard SQL up to Part 2; Parts 3 to 6 are Virtuoso extensions.
-- Run:  isql <host>:1111 dba <password> < customerkg-agentic-marketing.sql      (or paste into Conductor's Interactive SQL page)
-- Change CustomerKG.kidehen if you want another qualifier or owner; keep it identical everywhere in the file.

-- Part 1: tables, parents before children
CREATE TABLE CustomerKG.kidehen.channel (channel_id INTEGER NOT NULL PRIMARY KEY, channel_name VARCHAR(40) NOT NULL, channel_kind VARCHAR(8) NOT NULL);
CREATE TABLE CustomerKG.kidehen.product (product_id INTEGER NOT NULL PRIMARY KEY, product_name VARCHAR(48) NOT NULL, category VARCHAR(16) NOT NULL, margin_pct INTEGER NOT NULL);
CREATE TABLE CustomerKG.kidehen.customer (customer_id INTEGER NOT NULL PRIMARY KEY, full_name VARCHAR(48) NOT NULL, lifecycle_stage VARCHAR(12) NOT NULL, loyalty_tier VARCHAR(8) NOT NULL, home_city VARCHAR(24) NOT NULL, usual_order_day VARCHAR(10) NOT NULL);
CREATE TABLE CustomerKG.kidehen.identity_signal (signal_id INTEGER NOT NULL PRIMARY KEY, customer_id INTEGER, signal_kind VARCHAR(16) NOT NULL, signal_value VARCHAR(24) NOT NULL, match_method VARCHAR(16) NOT NULL, confidence INTEGER NOT NULL, seen_date DATE NOT NULL, FOREIGN KEY (customer_id) REFERENCES CustomerKG.kidehen.customer (customer_id));
CREATE TABLE CustomerKG.kidehen.consent (consent_id INTEGER NOT NULL PRIMARY KEY, customer_id INTEGER NOT NULL, channel_id INTEGER NOT NULL, status VARCHAR(10) NOT NULL, updated_date DATE NOT NULL, FOREIGN KEY (customer_id) REFERENCES CustomerKG.kidehen.customer (customer_id), FOREIGN KEY (channel_id) REFERENCES CustomerKG.kidehen.channel (channel_id));
CREATE TABLE CustomerKG.kidehen.offer (offer_id INTEGER NOT NULL PRIMARY KEY, offer_name VARCHAR(40) NOT NULL, offer_kind VARCHAR(16) NOT NULL, discount_pct INTEGER NOT NULL, product_id INTEGER, FOREIGN KEY (product_id) REFERENCES CustomerKG.kidehen.product (product_id));
CREATE TABLE CustomerKG.kidehen.guardrail (guardrail_id INTEGER NOT NULL PRIMARY KEY, rule_name VARCHAR(48) NOT NULL, rule_kind VARCHAR(20) NOT NULL, limit_value INTEGER NOT NULL, offer_id INTEGER, FOREIGN KEY (offer_id) REFERENCES CustomerKG.kidehen.offer (offer_id));
CREATE TABLE CustomerKG.kidehen.campaign (campaign_id INTEGER NOT NULL PRIMARY KEY, campaign_name VARCHAR(48) NOT NULL, goal VARCHAR(80) NOT NULL, start_date DATE NOT NULL);
CREATE TABLE CustomerKG.kidehen.decision (decision_id INTEGER NOT NULL PRIMARY KEY, customer_id INTEGER NOT NULL, campaign_id INTEGER NOT NULL, arm VARCHAR(10) NOT NULL, action_kind VARCHAR(16) NOT NULL, offer_id INTEGER, channel_id INTEGER, decided_date DATE NOT NULL, rationale VARCHAR(160) NOT NULL, approved_by_human INTEGER NOT NULL, FOREIGN KEY (customer_id) REFERENCES CustomerKG.kidehen.customer (customer_id), FOREIGN KEY (campaign_id) REFERENCES CustomerKG.kidehen.campaign (campaign_id), FOREIGN KEY (offer_id) REFERENCES CustomerKG.kidehen.offer (offer_id), FOREIGN KEY (channel_id) REFERENCES CustomerKG.kidehen.channel (channel_id));
CREATE TABLE CustomerKG.kidehen.decision_guardrail (decision_id INTEGER NOT NULL, guardrail_id INTEGER NOT NULL, PRIMARY KEY (decision_id, guardrail_id), FOREIGN KEY (decision_id) REFERENCES CustomerKG.kidehen.decision (decision_id), FOREIGN KEY (guardrail_id) REFERENCES CustomerKG.kidehen.guardrail (guardrail_id));
CREATE TABLE CustomerKG.kidehen.context_item (item_id INTEGER NOT NULL PRIMARY KEY, decision_id INTEGER NOT NULL, context_type VARCHAR(10) NOT NULL, context_key VARCHAR(24) NOT NULL, context_value VARCHAR(120) NOT NULL, FOREIGN KEY (decision_id) REFERENCES CustomerKG.kidehen.decision (decision_id));
CREATE TABLE CustomerKG.kidehen.outcome (outcome_id INTEGER NOT NULL PRIMARY KEY, decision_id INTEGER NOT NULL, ordered INTEGER NOT NULL, revenue_usd INTEGER NOT NULL, margin_usd INTEGER NOT NULL, media_cost_usd INTEGER NOT NULL, discount_cost_usd INTEGER NOT NULL, repeat_visit_30d INTEGER NOT NULL, window_end DATE NOT NULL, FOREIGN KEY (decision_id) REFERENCES CustomerKG.kidehen.decision (decision_id));

-- Part 2: rows, one INSERT each (Virtuoso has no multi-row VALUES)
INSERT INTO CustomerKG.kidehen.channel VALUES (1, 'App push', 'owned');
INSERT INTO CustomerKG.kidehen.channel VALUES (2, 'Email', 'owned');
INSERT INTO CustomerKG.kidehen.channel VALUES (3, 'SMS', 'owned');
INSERT INTO CustomerKG.kidehen.channel VALUES (4, 'Paid social', 'paid');
INSERT INTO CustomerKG.kidehen.channel VALUES (5, 'Display retargeting', 'paid');
INSERT INTO CustomerKG.kidehen.product VALUES (1, 'Classic Burger Meal', 'meal', 38);
INSERT INTO CustomerKG.kidehen.product VALUES (2, 'Family Meal Bundle', 'bundle', 31);
INSERT INTO CustomerKG.kidehen.product VALUES (3, 'Side of Fries', 'side', 62);
INSERT INTO CustomerKG.kidehen.product VALUES (4, 'Weekly Friday Combo', 'meal', 35);
INSERT INTO CustomerKG.kidehen.product VALUES (5, 'Dessert Treat', 'dessert', 58);
INSERT INTO CustomerKG.kidehen.customer VALUES (1, 'Avery Quinn', 'lapsing', 'gold', 'Austin', 'Friday');
INSERT INTO CustomerKG.kidehen.customer VALUES (2, 'Blake Moreno', 'lapsing', 'silver', 'Denver', 'Friday');
INSERT INTO CustomerKG.kidehen.customer VALUES (3, 'Casey Lindholm', 'lapsed', 'gold', 'Seattle', 'Saturday');
INSERT INTO CustomerKG.kidehen.customer VALUES (4, 'Devon Okafor', 'lapsed', 'bronze', 'Atlanta', 'Thursday');
INSERT INTO CustomerKG.kidehen.customer VALUES (5, 'Emery Castillo', 'lapsing', 'silver', 'Phoenix', 'Friday');
INSERT INTO CustomerKG.kidehen.customer VALUES (6, 'Frankie Novak', 'lapsed', 'bronze', 'Chicago', 'Sunday');
INSERT INTO CustomerKG.kidehen.customer VALUES (7, 'Gray Whitfield', 'lapsing', 'silver', 'Boston', 'Friday');
INSERT INTO CustomerKG.kidehen.customer VALUES (8, 'Harper Doyle', 'lapsed', 'bronze', 'Portland', 'Saturday');
INSERT INTO CustomerKG.kidehen.customer VALUES (9, 'Indigo Raman', 'lapsed', 'gold', 'Dallas', 'Friday');
INSERT INTO CustomerKG.kidehen.customer VALUES (10, 'Jules Barrow', 'lapsing', 'silver', 'Miami', 'Friday');
INSERT INTO CustomerKG.kidehen.customer VALUES (11, 'Kendall Osei', 'lapsed', 'bronze', 'Tampa', 'Saturday');
INSERT INTO CustomerKG.kidehen.customer VALUES (12, 'Lennox Pryce', 'lapsed', 'bronze', 'Raleigh', 'Sunday');
INSERT INTO CustomerKG.kidehen.customer VALUES (13, 'Marlowe Tran', 'lapsing', 'gold', 'San Jose', 'Friday');
INSERT INTO CustomerKG.kidehen.customer VALUES (14, 'Noel Ferreira', 'lapsed', 'bronze', 'Newark', 'Thursday');
INSERT INTO CustomerKG.kidehen.customer VALUES (15, 'Oakley Brandt', 'active', 'silver', 'Omaha', 'Friday');
INSERT INTO CustomerKG.kidehen.customer VALUES (16, 'Parker Zielinski', 'active', 'gold', 'Madison', 'Saturday');
INSERT INTO CustomerKG.kidehen.customer VALUES (17, 'Quincy Abara', 'active', 'silver', 'Memphis', 'Friday');
INSERT INTO CustomerKG.kidehen.customer VALUES (18, 'Rowan Eklund', 'active', 'bronze', 'Boise', 'Sunday');
INSERT INTO CustomerKG.kidehen.customer VALUES (19, 'Sage Whitlock', 'active', 'gold', 'Tulsa', 'Friday');
INSERT INTO CustomerKG.kidehen.customer VALUES (20, 'Tatum Reyes', 'active', 'silver', 'Reno', 'Friday');
INSERT INTO CustomerKG.kidehen.identity_signal VALUES (1, 1, 'email', 'hem:a1f3', 'deterministic', 100, stringdate('2026-08-02'));
INSERT INTO CustomerKG.kidehen.identity_signal VALUES (2, 1, 'loyalty_id', 'LY-10001', 'deterministic', 100, stringdate('2026-08-02'));
INSERT INTO CustomerKG.kidehen.identity_signal VALUES (3, 1, 'app_session', 'sess:77a1', 'probabilistic', 86, stringdate('2026-09-24'));
INSERT INTO CustomerKG.kidehen.identity_signal VALUES (4, 2, 'email', 'hem:b2c4', 'deterministic', 100, stringdate('2026-08-05'));
INSERT INTO CustomerKG.kidehen.identity_signal VALUES (5, 2, 'device', 'dev:ios-31c', 'probabilistic', 82, stringdate('2026-09-20'));
INSERT INTO CustomerKG.kidehen.identity_signal VALUES (6, 3, 'email', 'hem:c3d5', 'deterministic', 100, stringdate('2026-07-30'));
INSERT INTO CustomerKG.kidehen.identity_signal VALUES (7, 3, 'app_session', 'sess:5e90', 'agentic', 91, stringdate('2026-09-27'));
INSERT INTO CustomerKG.kidehen.identity_signal VALUES (8, 3, 'ad_exposure', 'imp:vid-204', 'probabilistic', 74, stringdate('2026-09-25'));
INSERT INTO CustomerKG.kidehen.identity_signal VALUES (9, 4, 'email', 'hem:d4e6', 'deterministic', 100, stringdate('2026-07-15'));
INSERT INTO CustomerKG.kidehen.identity_signal VALUES (10, 4, 'device', 'dev:and-8b2', 'probabilistic', 79, stringdate('2026-09-18'));
INSERT INTO CustomerKG.kidehen.identity_signal VALUES (11, 5, 'email', 'hem:e5f7', 'deterministic', 100, stringdate('2026-08-11'));
INSERT INTO CustomerKG.kidehen.identity_signal VALUES (12, 5, 'app_session', 'sess:a310', 'agentic', 88, stringdate('2026-09-26'));
INSERT INTO CustomerKG.kidehen.identity_signal VALUES (13, 6, 'loyalty_id', 'LY-10006', 'deterministic', 100, stringdate('2026-06-21'));
INSERT INTO CustomerKG.kidehen.identity_signal VALUES (14, 6, 'ad_exposure', 'imp:soc-118', 'probabilistic', 71, stringdate('2026-09-23'));
INSERT INTO CustomerKG.kidehen.identity_signal VALUES (15, 7, 'email', 'hem:f708', 'deterministic', 100, stringdate('2026-08-19'));
INSERT INTO CustomerKG.kidehen.identity_signal VALUES (16, 7, 'device', 'dev:ios-99d', 'deterministic', 100, stringdate('2026-09-21'));
INSERT INTO CustomerKG.kidehen.identity_signal VALUES (17, 8, 'email', 'hem:0819', 'deterministic', 100, stringdate('2026-07-02'));
INSERT INTO CustomerKG.kidehen.identity_signal VALUES (18, 8, 'app_session', 'sess:c442', 'probabilistic', 77, stringdate('2026-09-19'));
INSERT INTO CustomerKG.kidehen.identity_signal VALUES (19, 9, 'email', 'hem:192a', 'deterministic', 100, stringdate('2026-08-08'));
INSERT INTO CustomerKG.kidehen.identity_signal VALUES (20, 9, 'app_session', 'sess:1d6b', 'agentic', 93, stringdate('2026-09-27'));
INSERT INTO CustomerKG.kidehen.identity_signal VALUES (21, 10, 'email', 'hem:2a3b', 'deterministic', 100, stringdate('2026-08-14'));
INSERT INTO CustomerKG.kidehen.identity_signal VALUES (22, 11, 'loyalty_id', 'LY-10011', 'deterministic', 100, stringdate('2026-05-30'));
INSERT INTO CustomerKG.kidehen.identity_signal VALUES (23, 12, 'email', 'hem:3b4c', 'deterministic', 100, stringdate('2026-06-12'));
INSERT INTO CustomerKG.kidehen.identity_signal VALUES (24, 13, 'email', 'hem:4c5d', 'deterministic', 100, stringdate('2026-08-22'));
INSERT INTO CustomerKG.kidehen.identity_signal VALUES (25, 13, 'app_session', 'sess:e8f2', 'probabilistic', 84, stringdate('2026-09-24'));
INSERT INTO CustomerKG.kidehen.identity_signal VALUES (26, 14, 'email', 'hem:5d6e', 'deterministic', 100, stringdate('2026-06-03'));
INSERT INTO CustomerKG.kidehen.identity_signal VALUES (27, NULL, 'app_session', 'sess:9b13', 'unmatched', 0, stringdate('2026-09-28'));
INSERT INTO CustomerKG.kidehen.identity_signal VALUES (28, NULL, 'ad_exposure', 'imp:soc-402', 'unmatched', 0, stringdate('2026-09-28'));
INSERT INTO CustomerKG.kidehen.identity_signal VALUES (29, NULL, 'device', 'dev:and-c10', 'unmatched', 0, stringdate('2026-09-29'));
INSERT INTO CustomerKG.kidehen.identity_signal VALUES (30, NULL, 'app_session', 'sess:2f77', 'unmatched', 0, stringdate('2026-09-29'));
INSERT INTO CustomerKG.kidehen.identity_signal VALUES (31, 15, 'email', 'hem:6e7f', 'deterministic', 100, stringdate('2026-08-01'));
INSERT INTO CustomerKG.kidehen.identity_signal VALUES (32, 16, 'email', 'hem:7f80', 'deterministic', 100, stringdate('2026-08-01'));
INSERT INTO CustomerKG.kidehen.identity_signal VALUES (33, 17, 'email', 'hem:8091', 'deterministic', 100, stringdate('2026-08-01'));
INSERT INTO CustomerKG.kidehen.identity_signal VALUES (34, 18, 'email', 'hem:91a2', 'deterministic', 100, stringdate('2026-08-01'));
INSERT INTO CustomerKG.kidehen.identity_signal VALUES (35, 19, 'email', 'hem:a2b3', 'deterministic', 100, stringdate('2026-08-01'));
INSERT INTO CustomerKG.kidehen.identity_signal VALUES (36, 20, 'email', 'hem:b3c4', 'deterministic', 100, stringdate('2026-08-01'));
INSERT INTO CustomerKG.kidehen.consent VALUES (1, 1, 2, 'granted', stringdate('2026-08-01'));
INSERT INTO CustomerKG.kidehen.consent VALUES (2, 2, 2, 'granted', stringdate('2026-08-01'));
INSERT INTO CustomerKG.kidehen.consent VALUES (3, 3, 2, 'granted', stringdate('2026-08-01'));
INSERT INTO CustomerKG.kidehen.consent VALUES (4, 4, 2, 'granted', stringdate('2026-08-01'));
INSERT INTO CustomerKG.kidehen.consent VALUES (5, 5, 2, 'granted', stringdate('2026-08-01'));
INSERT INTO CustomerKG.kidehen.consent VALUES (6, 6, 2, 'granted', stringdate('2026-08-01'));
INSERT INTO CustomerKG.kidehen.consent VALUES (7, 7, 2, 'granted', stringdate('2026-08-01'));
INSERT INTO CustomerKG.kidehen.consent VALUES (8, 8, 2, 'granted', stringdate('2026-08-01'));
INSERT INTO CustomerKG.kidehen.consent VALUES (9, 9, 2, 'granted', stringdate('2026-08-01'));
INSERT INTO CustomerKG.kidehen.consent VALUES (10, 10, 2, 'granted', stringdate('2026-08-01'));
INSERT INTO CustomerKG.kidehen.consent VALUES (11, 11, 2, 'withdrawn', stringdate('2026-08-01'));
INSERT INTO CustomerKG.kidehen.consent VALUES (12, 12, 2, 'granted', stringdate('2026-08-01'));
INSERT INTO CustomerKG.kidehen.consent VALUES (13, 13, 2, 'granted', stringdate('2026-08-01'));
INSERT INTO CustomerKG.kidehen.consent VALUES (14, 14, 2, 'withdrawn', stringdate('2026-08-01'));
INSERT INTO CustomerKG.kidehen.consent VALUES (15, 15, 2, 'granted', stringdate('2026-08-01'));
INSERT INTO CustomerKG.kidehen.consent VALUES (16, 16, 2, 'granted', stringdate('2026-08-01'));
INSERT INTO CustomerKG.kidehen.consent VALUES (17, 17, 2, 'granted', stringdate('2026-08-01'));
INSERT INTO CustomerKG.kidehen.consent VALUES (18, 18, 2, 'granted', stringdate('2026-08-01'));
INSERT INTO CustomerKG.kidehen.consent VALUES (19, 19, 2, 'granted', stringdate('2026-08-01'));
INSERT INTO CustomerKG.kidehen.consent VALUES (20, 20, 2, 'granted', stringdate('2026-08-01'));
INSERT INTO CustomerKG.kidehen.consent VALUES (21, 1, 1, 'granted', stringdate('2026-08-01'));
INSERT INTO CustomerKG.kidehen.consent VALUES (22, 3, 1, 'granted', stringdate('2026-08-01'));
INSERT INTO CustomerKG.kidehen.consent VALUES (23, 5, 1, 'granted', stringdate('2026-08-01'));
INSERT INTO CustomerKG.kidehen.consent VALUES (24, 9, 1, 'granted', stringdate('2026-08-01'));
INSERT INTO CustomerKG.kidehen.consent VALUES (25, 13, 1, 'granted', stringdate('2026-08-01'));
INSERT INTO CustomerKG.kidehen.consent VALUES (26, 15, 1, 'granted', stringdate('2026-08-01'));
INSERT INTO CustomerKG.kidehen.consent VALUES (27, 16, 1, 'granted', stringdate('2026-08-01'));
INSERT INTO CustomerKG.kidehen.consent VALUES (28, 19, 1, 'granted', stringdate('2026-08-01'));
INSERT INTO CustomerKG.kidehen.consent VALUES (29, 8, 1, 'withdrawn', stringdate('2026-09-10'));
INSERT INTO CustomerKG.kidehen.consent VALUES (30, 4, 3, 'granted', stringdate('2026-08-01'));
INSERT INTO CustomerKG.kidehen.consent VALUES (31, 6, 3, 'granted', stringdate('2026-08-01'));
INSERT INTO CustomerKG.kidehen.consent VALUES (32, 12, 3, 'granted', stringdate('2026-08-01'));
INSERT INTO CustomerKG.kidehen.consent VALUES (33, 17, 3, 'granted', stringdate('2026-08-01'));
INSERT INTO CustomerKG.kidehen.consent VALUES (34, 3, 4, 'granted', stringdate('2026-08-01'));
INSERT INTO CustomerKG.kidehen.consent VALUES (35, 6, 4, 'granted', stringdate('2026-08-01'));
INSERT INTO CustomerKG.kidehen.consent VALUES (36, 11, 4, 'granted', stringdate('2026-08-01'));
INSERT INTO CustomerKG.kidehen.consent VALUES (37, 4, 5, 'granted', stringdate('2026-08-01'));
INSERT INTO CustomerKG.kidehen.consent VALUES (38, 12, 5, 'granted', stringdate('2026-08-01'));
INSERT INTO CustomerKG.kidehen.offer VALUES (1, 'Friday reminder', 'reminder', 0, 4);
INSERT INTO CustomerKG.kidehen.offer VALUES (2, 'Free side with any meal', 'incentive', 15, 3);
INSERT INTO CustomerKG.kidehen.offer VALUES (3, 'Family bundle, 20 percent off', 'bundle-incentive', 20, 2);
INSERT INTO CustomerKG.kidehen.offer VALUES (4, '10 percent off next order', 'incentive', 10, NULL);
INSERT INTO CustomerKG.kidehen.offer VALUES (5, 'Dessert on us', 'incentive', 12, 5);
INSERT INTO CustomerKG.kidehen.guardrail VALUES (1, 'Maximum discount depth 20 percent', 'discount-cap', 20, NULL);
INSERT INTO CustomerKG.kidehen.guardrail VALUES (2, 'At most 3 contacts per 14 days', 'contact-frequency', 3, NULL);
INSERT INTO CustomerKG.kidehen.guardrail VALUES (3, 'Contact only on a consented channel', 'consent-required', 1, NULL);
INSERT INTO CustomerKG.kidehen.guardrail VALUES (4, 'Human approval above 15 percent discount', 'human-approval', 15, NULL);
INSERT INTO CustomerKG.kidehen.guardrail VALUES (5, 'Bundle offers need human approval', 'human-approval', 1, 3);
INSERT INTO CustomerKG.kidehen.campaign VALUES (1, 'Reactivate lapsed customers', 'reactivate lapsed customers', stringdate('2026-09-22'));
INSERT INTO CustomerKG.kidehen.campaign VALUES (2, 'Test whether blanket discounts subsidise orders', 'avoid subsidising purchases that would have happened anyway', stringdate('2026-09-22'));
INSERT INTO CustomerKG.kidehen.decision VALUES (1, 1, 1, 'treatment', 'send-reminder', 1, 1, stringdate('2026-09-22'), 'Usual Friday order missed once, usual order browsed on Thursday: a reminder is enough', 0);
INSERT INTO CustomerKG.kidehen.decision VALUES (2, 2, 1, 'treatment', 'send-reminder', 1, 2, stringdate('2026-09-22'), 'Missed one Friday; email consent granted, push not offered', 0);
INSERT INTO CustomerKG.kidehen.decision VALUES (3, 3, 1, 'treatment', 'send-incentive', 3, 1, stringdate('2026-09-22'), 'Six weeks lapsed, browsing a family meal without checkout: bundle incentive', 1);
INSERT INTO CustomerKG.kidehen.decision VALUES (4, 4, 1, 'treatment', 'switch-channel', NULL, 5, stringdate('2026-09-22'), 'Ignored email twice: move to display retargeting with no offer', 0);
INSERT INTO CustomerKG.kidehen.decision VALUES (5, 5, 1, 'treatment', 'send-incentive', 2, 1, stringdate('2026-09-22'), 'Lapsing and price sensitive: free side', 0);
INSERT INTO CustomerKG.kidehen.decision VALUES (6, 6, 1, 'treatment', 'switch-channel', NULL, 4, stringdate('2026-09-22'), 'Repeated push without response: move to paid social with no offer', 0);
INSERT INTO CustomerKG.kidehen.decision VALUES (7, 7, 1, 'treatment', 'send-incentive', 4, 2, stringdate('2026-09-22'), 'Lapsing: ten percent off by email', 0);
INSERT INTO CustomerKG.kidehen.decision VALUES (8, 8, 1, 'treatment', 'suppress', NULL, NULL, stringdate('2026-09-22'), 'Four messages without response and push consent withdrawn: stop spend for now', 0);
INSERT INTO CustomerKG.kidehen.decision VALUES (9, 9, 1, 'treatment', 'send-incentive', 3, 1, stringdate('2026-09-22'), 'Lapsed gold member browsing a family meal: bundle incentive', 1);
INSERT INTO CustomerKG.kidehen.decision VALUES (10, 10, 1, 'holdout', 'no-action', NULL, NULL, stringdate('2026-09-22'), 'Randomised holdout: no contact', 0);
INSERT INTO CustomerKG.kidehen.decision VALUES (11, 11, 1, 'holdout', 'no-action', NULL, NULL, stringdate('2026-09-22'), 'Randomised holdout: no contact', 0);
INSERT INTO CustomerKG.kidehen.decision VALUES (12, 12, 1, 'holdout', 'no-action', NULL, NULL, stringdate('2026-09-22'), 'Randomised holdout: no contact', 0);
INSERT INTO CustomerKG.kidehen.decision VALUES (13, 13, 1, 'holdout', 'no-action', NULL, NULL, stringdate('2026-09-22'), 'Randomised holdout: no contact', 0);
INSERT INTO CustomerKG.kidehen.decision VALUES (14, 14, 1, 'holdout', 'no-action', NULL, NULL, stringdate('2026-09-22'), 'Randomised holdout: no contact', 0);
INSERT INTO CustomerKG.kidehen.decision VALUES (15, 15, 2, 'treatment', 'send-incentive', 4, 1, stringdate('2026-09-22'), 'Habitual Friday customer given the standard discount to test subsidy', 0);
INSERT INTO CustomerKG.kidehen.decision VALUES (16, 16, 2, 'treatment', 'send-incentive', 4, 1, stringdate('2026-09-22'), 'Habitual Saturday customer given the standard discount to test subsidy', 0);
INSERT INTO CustomerKG.kidehen.decision VALUES (17, 17, 2, 'treatment', 'send-incentive', 5, 3, stringdate('2026-09-22'), 'Habitual Friday customer given a dessert offer by SMS', 0);
INSERT INTO CustomerKG.kidehen.decision VALUES (18, 18, 2, 'treatment', 'send-incentive', 4, 2, stringdate('2026-09-22'), 'Habitual Sunday customer given the standard discount by email', 0);
INSERT INTO CustomerKG.kidehen.decision VALUES (19, 19, 2, 'holdout', 'no-action', NULL, NULL, stringdate('2026-09-22'), 'Randomised holdout: no contact', 0);
INSERT INTO CustomerKG.kidehen.decision VALUES (20, 20, 2, 'holdout', 'no-action', NULL, NULL, stringdate('2026-09-22'), 'Randomised holdout: no contact', 0);
INSERT INTO CustomerKG.kidehen.decision_guardrail VALUES (1, 2);
INSERT INTO CustomerKG.kidehen.decision_guardrail VALUES (1, 3);
INSERT INTO CustomerKG.kidehen.decision_guardrail VALUES (2, 2);
INSERT INTO CustomerKG.kidehen.decision_guardrail VALUES (2, 3);
INSERT INTO CustomerKG.kidehen.decision_guardrail VALUES (3, 1);
INSERT INTO CustomerKG.kidehen.decision_guardrail VALUES (3, 2);
INSERT INTO CustomerKG.kidehen.decision_guardrail VALUES (3, 3);
INSERT INTO CustomerKG.kidehen.decision_guardrail VALUES (3, 4);
INSERT INTO CustomerKG.kidehen.decision_guardrail VALUES (3, 5);
INSERT INTO CustomerKG.kidehen.decision_guardrail VALUES (4, 2);
INSERT INTO CustomerKG.kidehen.decision_guardrail VALUES (4, 3);
INSERT INTO CustomerKG.kidehen.decision_guardrail VALUES (5, 1);
INSERT INTO CustomerKG.kidehen.decision_guardrail VALUES (5, 2);
INSERT INTO CustomerKG.kidehen.decision_guardrail VALUES (5, 3);
INSERT INTO CustomerKG.kidehen.decision_guardrail VALUES (6, 2);
INSERT INTO CustomerKG.kidehen.decision_guardrail VALUES (6, 3);
INSERT INTO CustomerKG.kidehen.decision_guardrail VALUES (7, 1);
INSERT INTO CustomerKG.kidehen.decision_guardrail VALUES (7, 2);
INSERT INTO CustomerKG.kidehen.decision_guardrail VALUES (7, 3);
INSERT INTO CustomerKG.kidehen.decision_guardrail VALUES (8, 2);
INSERT INTO CustomerKG.kidehen.decision_guardrail VALUES (8, 3);
INSERT INTO CustomerKG.kidehen.decision_guardrail VALUES (9, 1);
INSERT INTO CustomerKG.kidehen.decision_guardrail VALUES (9, 2);
INSERT INTO CustomerKG.kidehen.decision_guardrail VALUES (9, 3);
INSERT INTO CustomerKG.kidehen.decision_guardrail VALUES (9, 4);
INSERT INTO CustomerKG.kidehen.decision_guardrail VALUES (9, 5);
INSERT INTO CustomerKG.kidehen.decision_guardrail VALUES (10, 2);
INSERT INTO CustomerKG.kidehen.decision_guardrail VALUES (10, 3);
INSERT INTO CustomerKG.kidehen.decision_guardrail VALUES (11, 2);
INSERT INTO CustomerKG.kidehen.decision_guardrail VALUES (11, 3);
INSERT INTO CustomerKG.kidehen.decision_guardrail VALUES (12, 2);
INSERT INTO CustomerKG.kidehen.decision_guardrail VALUES (12, 3);
INSERT INTO CustomerKG.kidehen.decision_guardrail VALUES (13, 2);
INSERT INTO CustomerKG.kidehen.decision_guardrail VALUES (13, 3);
INSERT INTO CustomerKG.kidehen.decision_guardrail VALUES (14, 2);
INSERT INTO CustomerKG.kidehen.decision_guardrail VALUES (14, 3);
INSERT INTO CustomerKG.kidehen.decision_guardrail VALUES (15, 1);
INSERT INTO CustomerKG.kidehen.decision_guardrail VALUES (15, 2);
INSERT INTO CustomerKG.kidehen.decision_guardrail VALUES (15, 3);
INSERT INTO CustomerKG.kidehen.decision_guardrail VALUES (16, 1);
INSERT INTO CustomerKG.kidehen.decision_guardrail VALUES (16, 2);
INSERT INTO CustomerKG.kidehen.decision_guardrail VALUES (16, 3);
INSERT INTO CustomerKG.kidehen.decision_guardrail VALUES (17, 1);
INSERT INTO CustomerKG.kidehen.decision_guardrail VALUES (17, 2);
INSERT INTO CustomerKG.kidehen.decision_guardrail VALUES (17, 3);
INSERT INTO CustomerKG.kidehen.decision_guardrail VALUES (18, 1);
INSERT INTO CustomerKG.kidehen.decision_guardrail VALUES (18, 2);
INSERT INTO CustomerKG.kidehen.decision_guardrail VALUES (18, 3);
INSERT INTO CustomerKG.kidehen.decision_guardrail VALUES (19, 2);
INSERT INTO CustomerKG.kidehen.decision_guardrail VALUES (19, 3);
INSERT INTO CustomerKG.kidehen.decision_guardrail VALUES (20, 2);
INSERT INTO CustomerKG.kidehen.decision_guardrail VALUES (20, 3);
INSERT INTO CustomerKG.kidehen.context_item VALUES (1, 1, 'customer', 'usual_order_day', 'Friday; skipped last week');
INSERT INTO CustomerKG.kidehen.context_item VALUES (2, 1, 'business', 'inventory', 'Weekly Friday Combo in stock');
INSERT INTO CustomerKG.kidehen.context_item VALUES (3, 1, 'decision', 'prior_offer', 'None in the last 30 days');
INSERT INTO CustomerKG.kidehen.context_item VALUES (4, 1, 'control', 'contact_frequency', '0 of 3 contacts used in 14 days');
INSERT INTO CustomerKG.kidehen.context_item VALUES (5, 3, 'customer', 'behaviour', 'Browsed a family meal six times without checkout');
INSERT INTO CustomerKG.kidehen.context_item VALUES (6, 3, 'business', 'margin', 'Family Meal Bundle margin is 31 percent');
INSERT INTO CustomerKG.kidehen.context_item VALUES (7, 3, 'decision', 'prior_offer', '10 percent off ignored five weeks ago');
INSERT INTO CustomerKG.kidehen.context_item VALUES (8, 3, 'control', 'approval', 'Discount of 20 percent needs human approval: granted');
INSERT INTO CustomerKG.kidehen.context_item VALUES (9, 8, 'customer', 'consent', 'Push consent withdrawn on 2026-09-10');
INSERT INTO CustomerKG.kidehen.context_item VALUES (10, 8, 'business', 'channel_constraint', 'Email contacts are capped at 3 per 14 days');
INSERT INTO CustomerKG.kidehen.context_item VALUES (11, 8, 'decision', 'prior_response', 'Four messages, no response');
INSERT INTO CustomerKG.kidehen.context_item VALUES (12, 8, 'control', 'action', 'Suppress and revisit after 30 days');
INSERT INTO CustomerKG.kidehen.context_item VALUES (13, 15, 'customer', 'lifecycle', 'Active, orders every Friday');
INSERT INTO CustomerKG.kidehen.context_item VALUES (14, 15, 'business', 'objective', 'Avoid subsidising habitual orders');
INSERT INTO CustomerKG.kidehen.context_item VALUES (15, 15, 'decision', 'prior_offer', '10 percent off redeemed three times');
INSERT INTO CustomerKG.kidehen.context_item VALUES (16, 15, 'control', 'discount_cap', '10 percent sits inside the 20 percent cap');
INSERT INTO CustomerKG.kidehen.context_item VALUES (17, 19, 'customer', 'lifecycle', 'Active, orders every Friday');
INSERT INTO CustomerKG.kidehen.context_item VALUES (18, 19, 'business', 'objective', 'Measure what happens with no offer');
INSERT INTO CustomerKG.kidehen.context_item VALUES (19, 19, 'decision', 'prior_offer', 'None');
INSERT INTO CustomerKG.kidehen.context_item VALUES (20, 19, 'control', 'holdout', 'Randomised no-contact arm');
INSERT INTO CustomerKG.kidehen.outcome VALUES (1, 1, 1, 24, 9, 0, 0, 1, stringdate('2026-10-06'));
INSERT INTO CustomerKG.kidehen.outcome VALUES (2, 2, 1, 22, 8, 0, 0, 1, stringdate('2026-10-06'));
INSERT INTO CustomerKG.kidehen.outcome VALUES (3, 3, 1, 48, 15, 0, 10, 1, stringdate('2026-10-06'));
INSERT INTO CustomerKG.kidehen.outcome VALUES (4, 4, 0, 0, 0, 2, 0, 0, stringdate('2026-10-06'));
INSERT INTO CustomerKG.kidehen.outcome VALUES (5, 5, 1, 19, 7, 0, 3, 0, stringdate('2026-10-06'));
INSERT INTO CustomerKG.kidehen.outcome VALUES (6, 6, 0, 0, 0, 2, 0, 0, stringdate('2026-10-06'));
INSERT INTO CustomerKG.kidehen.outcome VALUES (7, 7, 1, 26, 10, 0, 3, 1, stringdate('2026-10-06'));
INSERT INTO CustomerKG.kidehen.outcome VALUES (8, 8, 0, 0, 0, 0, 0, 0, stringdate('2026-10-06'));
INSERT INTO CustomerKG.kidehen.outcome VALUES (9, 9, 1, 52, 16, 0, 10, 1, stringdate('2026-10-06'));
INSERT INTO CustomerKG.kidehen.outcome VALUES (10, 10, 1, 25, 9, 0, 0, 1, stringdate('2026-10-06'));
INSERT INTO CustomerKG.kidehen.outcome VALUES (11, 11, 0, 0, 0, 0, 0, 0, stringdate('2026-10-06'));
INSERT INTO CustomerKG.kidehen.outcome VALUES (12, 12, 0, 0, 0, 0, 0, 0, stringdate('2026-10-06'));
INSERT INTO CustomerKG.kidehen.outcome VALUES (13, 13, 1, 21, 8, 0, 0, 0, stringdate('2026-10-06'));
INSERT INTO CustomerKG.kidehen.outcome VALUES (14, 14, 0, 0, 0, 0, 0, 0, stringdate('2026-10-06'));
INSERT INTO CustomerKG.kidehen.outcome VALUES (15, 15, 1, 23, 9, 0, 2, 1, stringdate('2026-10-06'));
INSERT INTO CustomerKG.kidehen.outcome VALUES (16, 16, 1, 21, 8, 0, 2, 0, stringdate('2026-10-06'));
INSERT INTO CustomerKG.kidehen.outcome VALUES (17, 17, 1, 25, 10, 1, 3, 1, stringdate('2026-10-06'));
INSERT INTO CustomerKG.kidehen.outcome VALUES (18, 18, 0, 0, 0, 0, 0, 0, stringdate('2026-10-06'));
INSERT INTO CustomerKG.kidehen.outcome VALUES (19, 19, 1, 24, 9, 0, 0, 1, stringdate('2026-10-06'));
INSERT INTO CustomerKG.kidehen.outcome VALUES (20, 20, 1, 22, 8, 0, 0, 1, stringdate('2026-10-06'));

-- Part 3: IRI classes (the SPARQL keyword and angle-bracket class names are required through isql)
SPARQL CREATE IRI CLASS <http://demo.openlinksw.com/schemas/CustomerKG/channel_iri> "http://demo.openlinksw.com/CustomerKG/channel/%d#this" (in channel_id integer not null);
SPARQL CREATE IRI CLASS <http://demo.openlinksw.com/schemas/CustomerKG/product_iri> "http://demo.openlinksw.com/CustomerKG/product/%d#this" (in product_id integer not null);
SPARQL CREATE IRI CLASS <http://demo.openlinksw.com/schemas/CustomerKG/customer_iri> "http://demo.openlinksw.com/CustomerKG/customer/%d#this" (in customer_id integer not null);
SPARQL CREATE IRI CLASS <http://demo.openlinksw.com/schemas/CustomerKG/signal_iri> "http://demo.openlinksw.com/CustomerKG/signal/%d#this" (in signal_id integer not null);
SPARQL CREATE IRI CLASS <http://demo.openlinksw.com/schemas/CustomerKG/consent_iri> "http://demo.openlinksw.com/CustomerKG/consent/%d#this" (in consent_id integer not null);
SPARQL CREATE IRI CLASS <http://demo.openlinksw.com/schemas/CustomerKG/offer_iri> "http://demo.openlinksw.com/CustomerKG/offer/%d#this" (in offer_id integer not null);
SPARQL CREATE IRI CLASS <http://demo.openlinksw.com/schemas/CustomerKG/guardrail_iri> "http://demo.openlinksw.com/CustomerKG/guardrail/%d#this" (in guardrail_id integer not null);
SPARQL CREATE IRI CLASS <http://demo.openlinksw.com/schemas/CustomerKG/campaign_iri> "http://demo.openlinksw.com/CustomerKG/campaign/%d#this" (in campaign_id integer not null);
SPARQL CREATE IRI CLASS <http://demo.openlinksw.com/schemas/CustomerKG/decision_iri> "http://demo.openlinksw.com/CustomerKG/decision/%d#this" (in decision_id integer not null);
SPARQL CREATE IRI CLASS <http://demo.openlinksw.com/schemas/CustomerKG/context_iri> "http://demo.openlinksw.com/CustomerKG/context/%d#this" (in item_id integer not null);
SPARQL CREATE IRI CLASS <http://demo.openlinksw.com/schemas/CustomerKG/outcome_iri> "http://demo.openlinksw.com/CustomerKG/outcome/%d#this" (in outcome_id integer not null);

-- Part 4: ontology and entailment rule set
-- Load customerkg-ontology-claude_sonnet_5_5-1.ttl into the graph below before you rely on entailment:
--   DB.DBA.TTLP(file_to_string_output('customerkg-ontology-claude_sonnet_5_5-1.ttl'), '', 'http://demo.openlinksw.com/schemas/CustomerKG/');
--   DB.DBA.rdfs_rule_set('urn:customerkg:inference', 'http://demo.openlinksw.com/schemas/CustomerKG/');

-- Part 5: the RDF View. Keep every mapping for the graph in one statement; a second statement replaces the first.
SPARQL
ALTER QUAD STORAGE virtrdf:DefaultQuadStorage
  FROM CustomerKG.kidehen.channel AS ch
  FROM CustomerKG.kidehen.product AS pr
  FROM CustomerKG.kidehen.customer AS cu
  FROM CustomerKG.kidehen.identity_signal AS sg
  FROM CustomerKG.kidehen.consent AS co
  FROM CustomerKG.kidehen.offer AS ofr
  FROM CustomerKG.kidehen.guardrail AS gr
  FROM CustomerKG.kidehen.campaign AS ca
  FROM CustomerKG.kidehen.decision AS de
  FROM CustomerKG.kidehen.decision_guardrail AS dg
  FROM CustomerKG.kidehen.context_item AS ci
  FROM CustomerKG.kidehen.outcome AS ou
{
  GRAPH <http://demo.openlinksw.com/CustomerKG#>
  {
    <http://demo.openlinksw.com/schemas/CustomerKG/channel_iri>(ch.channel_id) <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://demo.openlinksw.com/schemas/CustomerKG/Channel> as virtrdf:CustomerKG-channel-type .
    <http://demo.openlinksw.com/schemas/CustomerKG/channel_iri>(ch.channel_id) <http://schema.org/name> ch.channel_name as virtrdf:CustomerKG-channel-name .
    <http://demo.openlinksw.com/schemas/CustomerKG/channel_iri>(ch.channel_id) <http://demo.openlinksw.com/schemas/CustomerKG/channelKind> ch.channel_kind as virtrdf:CustomerKG-channel-kind .
    <http://demo.openlinksw.com/schemas/CustomerKG/product_iri>(pr.product_id) <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://demo.openlinksw.com/schemas/CustomerKG/Product> as virtrdf:CustomerKG-product-type .
    <http://demo.openlinksw.com/schemas/CustomerKG/product_iri>(pr.product_id) <http://schema.org/name> pr.product_name as virtrdf:CustomerKG-product-name .
    <http://demo.openlinksw.com/schemas/CustomerKG/product_iri>(pr.product_id) <http://demo.openlinksw.com/schemas/CustomerKG/category> pr.category as virtrdf:CustomerKG-product-category .
    <http://demo.openlinksw.com/schemas/CustomerKG/product_iri>(pr.product_id) <http://demo.openlinksw.com/schemas/CustomerKG/marginPct> pr.margin_pct as virtrdf:CustomerKG-product-margin .
    <http://demo.openlinksw.com/schemas/CustomerKG/customer_iri>(cu.customer_id) <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://demo.openlinksw.com/schemas/CustomerKG/Customer> as virtrdf:CustomerKG-customer-type .
    <http://demo.openlinksw.com/schemas/CustomerKG/customer_iri>(cu.customer_id) <http://schema.org/name> cu.full_name as virtrdf:CustomerKG-customer-name .
    <http://demo.openlinksw.com/schemas/CustomerKG/customer_iri>(cu.customer_id) <http://demo.openlinksw.com/schemas/CustomerKG/lifecycleStage> cu.lifecycle_stage as virtrdf:CustomerKG-customer-lifecycle .
    <http://demo.openlinksw.com/schemas/CustomerKG/customer_iri>(cu.customer_id) <http://demo.openlinksw.com/schemas/CustomerKG/loyaltyTier> cu.loyalty_tier as virtrdf:CustomerKG-customer-tier .
    <http://demo.openlinksw.com/schemas/CustomerKG/customer_iri>(cu.customer_id) <http://demo.openlinksw.com/schemas/CustomerKG/homeCity> cu.home_city as virtrdf:CustomerKG-customer-city .
    <http://demo.openlinksw.com/schemas/CustomerKG/customer_iri>(cu.customer_id) <http://demo.openlinksw.com/schemas/CustomerKG/usualOrderDay> cu.usual_order_day as virtrdf:CustomerKG-customer-day .
    <http://demo.openlinksw.com/schemas/CustomerKG/signal_iri>(sg.signal_id) <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://demo.openlinksw.com/schemas/CustomerKG/IdentitySignal> as virtrdf:CustomerKG-signal-type .
    <http://demo.openlinksw.com/schemas/CustomerKG/signal_iri>(sg.signal_id) <http://demo.openlinksw.com/schemas/CustomerKG/signalKind> sg.signal_kind as virtrdf:CustomerKG-signal-kind .
    <http://demo.openlinksw.com/schemas/CustomerKG/signal_iri>(sg.signal_id) <http://demo.openlinksw.com/schemas/CustomerKG/signalValue> sg.signal_value as virtrdf:CustomerKG-signal-value .
    <http://demo.openlinksw.com/schemas/CustomerKG/signal_iri>(sg.signal_id) <http://demo.openlinksw.com/schemas/CustomerKG/matchMethod> sg.match_method as virtrdf:CustomerKG-signal-method .
    <http://demo.openlinksw.com/schemas/CustomerKG/signal_iri>(sg.signal_id) <http://demo.openlinksw.com/schemas/CustomerKG/confidence> sg.confidence as virtrdf:CustomerKG-signal-confidence .
    <http://demo.openlinksw.com/schemas/CustomerKG/signal_iri>(sg.signal_id) <http://demo.openlinksw.com/schemas/CustomerKG/seenDate> sg.seen_date as virtrdf:CustomerKG-signal-seen .
    <http://demo.openlinksw.com/schemas/CustomerKG/signal_iri>(sg.signal_id) <http://demo.openlinksw.com/schemas/CustomerKG/resolvesTo> <http://demo.openlinksw.com/schemas/CustomerKG/customer_iri>(sg.customer_id) where (^{sg.}^.customer_id is not null) as virtrdf:CustomerKG-signal-customer .
    <http://demo.openlinksw.com/schemas/CustomerKG/consent_iri>(co.consent_id) <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://demo.openlinksw.com/schemas/CustomerKG/ConsentRecord> as virtrdf:CustomerKG-consent-type .
    <http://demo.openlinksw.com/schemas/CustomerKG/consent_iri>(co.consent_id) <http://demo.openlinksw.com/schemas/CustomerKG/consentStatus> co.status as virtrdf:CustomerKG-consent-status .
    <http://demo.openlinksw.com/schemas/CustomerKG/consent_iri>(co.consent_id) <http://demo.openlinksw.com/schemas/CustomerKG/updatedDate> co.updated_date as virtrdf:CustomerKG-consent-updated .
    <http://demo.openlinksw.com/schemas/CustomerKG/consent_iri>(co.consent_id) <http://demo.openlinksw.com/schemas/CustomerKG/forCustomer> <http://demo.openlinksw.com/schemas/CustomerKG/customer_iri>(co.customer_id) as virtrdf:CustomerKG-consent-customer .
    <http://demo.openlinksw.com/schemas/CustomerKG/consent_iri>(co.consent_id) <http://demo.openlinksw.com/schemas/CustomerKG/forChannel> <http://demo.openlinksw.com/schemas/CustomerKG/channel_iri>(co.channel_id) as virtrdf:CustomerKG-consent-channel .
    <http://demo.openlinksw.com/schemas/CustomerKG/offer_iri>(ofr.offer_id) <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://demo.openlinksw.com/schemas/CustomerKG/Offer> as virtrdf:CustomerKG-offer-type .
    <http://demo.openlinksw.com/schemas/CustomerKG/offer_iri>(ofr.offer_id) <http://schema.org/name> ofr.offer_name as virtrdf:CustomerKG-offer-name .
    <http://demo.openlinksw.com/schemas/CustomerKG/offer_iri>(ofr.offer_id) <http://demo.openlinksw.com/schemas/CustomerKG/offerKind> ofr.offer_kind as virtrdf:CustomerKG-offer-kind .
    <http://demo.openlinksw.com/schemas/CustomerKG/offer_iri>(ofr.offer_id) <http://demo.openlinksw.com/schemas/CustomerKG/discountPct> ofr.discount_pct as virtrdf:CustomerKG-offer-discount .
    <http://demo.openlinksw.com/schemas/CustomerKG/offer_iri>(ofr.offer_id) <http://demo.openlinksw.com/schemas/CustomerKG/promotes> <http://demo.openlinksw.com/schemas/CustomerKG/product_iri>(ofr.product_id) where (^{ofr.}^.product_id is not null) as virtrdf:CustomerKG-offer-product .
    <http://demo.openlinksw.com/schemas/CustomerKG/guardrail_iri>(gr.guardrail_id) <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://demo.openlinksw.com/schemas/CustomerKG/Guardrail> as virtrdf:CustomerKG-guardrail-type .
    <http://demo.openlinksw.com/schemas/CustomerKG/guardrail_iri>(gr.guardrail_id) <http://schema.org/name> gr.rule_name as virtrdf:CustomerKG-guardrail-name .
    <http://demo.openlinksw.com/schemas/CustomerKG/guardrail_iri>(gr.guardrail_id) <http://demo.openlinksw.com/schemas/CustomerKG/ruleKind> gr.rule_kind as virtrdf:CustomerKG-guardrail-kind .
    <http://demo.openlinksw.com/schemas/CustomerKG/guardrail_iri>(gr.guardrail_id) <http://demo.openlinksw.com/schemas/CustomerKG/limitValue> gr.limit_value as virtrdf:CustomerKG-guardrail-limit .
    <http://demo.openlinksw.com/schemas/CustomerKG/guardrail_iri>(gr.guardrail_id) <http://demo.openlinksw.com/schemas/CustomerKG/appliesToOffer> <http://demo.openlinksw.com/schemas/CustomerKG/offer_iri>(gr.offer_id) where (^{gr.}^.offer_id is not null) as virtrdf:CustomerKG-guardrail-offer .
    <http://demo.openlinksw.com/schemas/CustomerKG/campaign_iri>(ca.campaign_id) <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://demo.openlinksw.com/schemas/CustomerKG/Campaign> as virtrdf:CustomerKG-campaign-type .
    <http://demo.openlinksw.com/schemas/CustomerKG/campaign_iri>(ca.campaign_id) <http://schema.org/name> ca.campaign_name as virtrdf:CustomerKG-campaign-name .
    <http://demo.openlinksw.com/schemas/CustomerKG/campaign_iri>(ca.campaign_id) <http://demo.openlinksw.com/schemas/CustomerKG/goal> ca.goal as virtrdf:CustomerKG-campaign-goal .
    <http://demo.openlinksw.com/schemas/CustomerKG/campaign_iri>(ca.campaign_id) <http://demo.openlinksw.com/schemas/CustomerKG/startDate> ca.start_date as virtrdf:CustomerKG-campaign-start .
    <http://demo.openlinksw.com/schemas/CustomerKG/decision_iri>(de.decision_id) <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://demo.openlinksw.com/schemas/CustomerKG/Decision> as virtrdf:CustomerKG-decision-type .
    <http://demo.openlinksw.com/schemas/CustomerKG/decision_iri>(de.decision_id) <http://demo.openlinksw.com/schemas/CustomerKG/arm> de.arm as virtrdf:CustomerKG-decision-arm .
    <http://demo.openlinksw.com/schemas/CustomerKG/decision_iri>(de.decision_id) <http://demo.openlinksw.com/schemas/CustomerKG/actionKind> de.action_kind as virtrdf:CustomerKG-decision-action .
    <http://demo.openlinksw.com/schemas/CustomerKG/decision_iri>(de.decision_id) <http://demo.openlinksw.com/schemas/CustomerKG/decidedDate> de.decided_date as virtrdf:CustomerKG-decision-date .
    <http://demo.openlinksw.com/schemas/CustomerKG/decision_iri>(de.decision_id) <http://demo.openlinksw.com/schemas/CustomerKG/rationale> de.rationale as virtrdf:CustomerKG-decision-rationale .
    <http://demo.openlinksw.com/schemas/CustomerKG/decision_iri>(de.decision_id) <http://demo.openlinksw.com/schemas/CustomerKG/approvedByHuman> de.approved_by_human as virtrdf:CustomerKG-decision-approved .
    <http://demo.openlinksw.com/schemas/CustomerKG/decision_iri>(de.decision_id) <http://demo.openlinksw.com/schemas/CustomerKG/forCustomer> <http://demo.openlinksw.com/schemas/CustomerKG/customer_iri>(de.customer_id) as virtrdf:CustomerKG-decision-customer .
    <http://demo.openlinksw.com/schemas/CustomerKG/decision_iri>(de.decision_id) <http://demo.openlinksw.com/schemas/CustomerKG/inCampaign> <http://demo.openlinksw.com/schemas/CustomerKG/campaign_iri>(de.campaign_id) as virtrdf:CustomerKG-decision-campaign .
    <http://demo.openlinksw.com/schemas/CustomerKG/decision_iri>(de.decision_id) <http://demo.openlinksw.com/schemas/CustomerKG/usesOffer> <http://demo.openlinksw.com/schemas/CustomerKG/offer_iri>(de.offer_id) where (^{de.}^.offer_id is not null) as virtrdf:CustomerKG-decision-offer .
    <http://demo.openlinksw.com/schemas/CustomerKG/decision_iri>(de.decision_id) <http://demo.openlinksw.com/schemas/CustomerKG/viaChannel> <http://demo.openlinksw.com/schemas/CustomerKG/channel_iri>(de.channel_id) where (^{de.}^.channel_id is not null) as virtrdf:CustomerKG-decision-channel .
    <http://demo.openlinksw.com/schemas/CustomerKG/decision_iri>(dg.decision_id) <http://demo.openlinksw.com/schemas/CustomerKG/constrainedBy> <http://demo.openlinksw.com/schemas/CustomerKG/guardrail_iri>(dg.guardrail_id) as virtrdf:CustomerKG-decision-guardrail .
    <http://demo.openlinksw.com/schemas/CustomerKG/context_iri>(ci.item_id) <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://demo.openlinksw.com/schemas/CustomerKG/ContextItem> as virtrdf:CustomerKG-context-type .
    <http://demo.openlinksw.com/schemas/CustomerKG/context_iri>(ci.item_id) <http://demo.openlinksw.com/schemas/CustomerKG/contextType> ci.context_type as virtrdf:CustomerKG-context-kind .
    <http://demo.openlinksw.com/schemas/CustomerKG/context_iri>(ci.item_id) <http://demo.openlinksw.com/schemas/CustomerKG/contextKey> ci.context_key as virtrdf:CustomerKG-context-key .
    <http://demo.openlinksw.com/schemas/CustomerKG/context_iri>(ci.item_id) <http://demo.openlinksw.com/schemas/CustomerKG/contextValue> ci.context_value as virtrdf:CustomerKG-context-value .
    <http://demo.openlinksw.com/schemas/CustomerKG/context_iri>(ci.item_id) <http://demo.openlinksw.com/schemas/CustomerKG/informs> <http://demo.openlinksw.com/schemas/CustomerKG/decision_iri>(ci.decision_id) as virtrdf:CustomerKG-context-decision .
    <http://demo.openlinksw.com/schemas/CustomerKG/outcome_iri>(ou.outcome_id) <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://demo.openlinksw.com/schemas/CustomerKG/Outcome> as virtrdf:CustomerKG-outcome-type .
    <http://demo.openlinksw.com/schemas/CustomerKG/outcome_iri>(ou.outcome_id) <http://demo.openlinksw.com/schemas/CustomerKG/ordered> ou.ordered as virtrdf:CustomerKG-outcome-ordered .
    <http://demo.openlinksw.com/schemas/CustomerKG/outcome_iri>(ou.outcome_id) <http://demo.openlinksw.com/schemas/CustomerKG/revenueUsd> ou.revenue_usd as virtrdf:CustomerKG-outcome-revenue .
    <http://demo.openlinksw.com/schemas/CustomerKG/outcome_iri>(ou.outcome_id) <http://demo.openlinksw.com/schemas/CustomerKG/marginUsd> ou.margin_usd as virtrdf:CustomerKG-outcome-margin .
    <http://demo.openlinksw.com/schemas/CustomerKG/outcome_iri>(ou.outcome_id) <http://demo.openlinksw.com/schemas/CustomerKG/mediaCostUsd> ou.media_cost_usd as virtrdf:CustomerKG-outcome-media .
    <http://demo.openlinksw.com/schemas/CustomerKG/outcome_iri>(ou.outcome_id) <http://demo.openlinksw.com/schemas/CustomerKG/discountCostUsd> ou.discount_cost_usd as virtrdf:CustomerKG-outcome-discount .
    <http://demo.openlinksw.com/schemas/CustomerKG/outcome_iri>(ou.outcome_id) <http://demo.openlinksw.com/schemas/CustomerKG/repeatVisit30d> ou.repeat_visit_30d as virtrdf:CustomerKG-outcome-repeat .
    <http://demo.openlinksw.com/schemas/CustomerKG/outcome_iri>(ou.outcome_id) <http://demo.openlinksw.com/schemas/CustomerKG/windowEnd> ou.window_end as virtrdf:CustomerKG-outcome-window .
    <http://demo.openlinksw.com/schemas/CustomerKG/outcome_iri>(ou.outcome_id) <http://demo.openlinksw.com/schemas/CustomerKG/ofDecision> <http://demo.openlinksw.com/schemas/CustomerKG/decision_iri>(ou.decision_id) as virtrdf:CustomerKG-outcome-decision .
  }
} ;

-- Part 6: access. The view inherits the SQL privileges of its tables.
GRANT SELECT ON CustomerKG.kidehen.channel TO "SPARQL";
GRANT SELECT ON CustomerKG.kidehen.channel TO PUBLIC;
GRANT SELECT ON CustomerKG.kidehen.channel TO "demo";
GRANT SELECT ON CustomerKG.kidehen.channel TO "vdb";
GRANT SELECT ON CustomerKG.kidehen.product TO "SPARQL";
GRANT SELECT ON CustomerKG.kidehen.product TO PUBLIC;
GRANT SELECT ON CustomerKG.kidehen.product TO "demo";
GRANT SELECT ON CustomerKG.kidehen.product TO "vdb";
GRANT SELECT ON CustomerKG.kidehen.customer TO "SPARQL";
GRANT SELECT ON CustomerKG.kidehen.customer TO PUBLIC;
GRANT SELECT ON CustomerKG.kidehen.customer TO "demo";
GRANT SELECT ON CustomerKG.kidehen.customer TO "vdb";
GRANT SELECT ON CustomerKG.kidehen.identity_signal TO "SPARQL";
GRANT SELECT ON CustomerKG.kidehen.identity_signal TO PUBLIC;
GRANT SELECT ON CustomerKG.kidehen.identity_signal TO "demo";
GRANT SELECT ON CustomerKG.kidehen.identity_signal TO "vdb";
GRANT SELECT ON CustomerKG.kidehen.consent TO "SPARQL";
GRANT SELECT ON CustomerKG.kidehen.consent TO PUBLIC;
GRANT SELECT ON CustomerKG.kidehen.consent TO "demo";
GRANT SELECT ON CustomerKG.kidehen.consent TO "vdb";
GRANT SELECT ON CustomerKG.kidehen.offer TO "SPARQL";
GRANT SELECT ON CustomerKG.kidehen.offer TO PUBLIC;
GRANT SELECT ON CustomerKG.kidehen.offer TO "demo";
GRANT SELECT ON CustomerKG.kidehen.offer TO "vdb";
GRANT SELECT ON CustomerKG.kidehen.guardrail TO "SPARQL";
GRANT SELECT ON CustomerKG.kidehen.guardrail TO PUBLIC;
GRANT SELECT ON CustomerKG.kidehen.guardrail TO "demo";
GRANT SELECT ON CustomerKG.kidehen.guardrail TO "vdb";
GRANT SELECT ON CustomerKG.kidehen.campaign TO "SPARQL";
GRANT SELECT ON CustomerKG.kidehen.campaign TO PUBLIC;
GRANT SELECT ON CustomerKG.kidehen.campaign TO "demo";
GRANT SELECT ON CustomerKG.kidehen.campaign TO "vdb";
GRANT SELECT ON CustomerKG.kidehen.decision TO "SPARQL";
GRANT SELECT ON CustomerKG.kidehen.decision TO PUBLIC;
GRANT SELECT ON CustomerKG.kidehen.decision TO "demo";
GRANT SELECT ON CustomerKG.kidehen.decision TO "vdb";
GRANT SELECT ON CustomerKG.kidehen.decision_guardrail TO "SPARQL";
GRANT SELECT ON CustomerKG.kidehen.decision_guardrail TO PUBLIC;
GRANT SELECT ON CustomerKG.kidehen.decision_guardrail TO "demo";
GRANT SELECT ON CustomerKG.kidehen.decision_guardrail TO "vdb";
GRANT SELECT ON CustomerKG.kidehen.context_item TO "SPARQL";
GRANT SELECT ON CustomerKG.kidehen.context_item TO PUBLIC;
GRANT SELECT ON CustomerKG.kidehen.context_item TO "demo";
GRANT SELECT ON CustomerKG.kidehen.context_item TO "vdb";
GRANT SELECT ON CustomerKG.kidehen.outcome TO "SPARQL";
GRANT SELECT ON CustomerKG.kidehen.outcome TO PUBLIC;
GRANT SELECT ON CustomerKG.kidehen.outcome TO "demo";
GRANT SELECT ON CustomerKG.kidehen.outcome TO "vdb";

-- Part 7: URL rewrite rules, generated by RDFVIEW_GENERATE_DATA_RULES with iri_path_segment=CustomerKG and include_ontology_rules=1.
-- Without them the entity IRIs return 404. Run through isql or SELECT exec() one statement at a time.
DB.DBA.URLREWRITE_CREATE_REGEX_RULE (
'customerkg_rule2',
1,
'(/[^#]*)',
vector('path'),
1,
'/sparql?query=DESCRIBE+%%3Chttp%%3A//^{URIQADefaultHost}^%U%%23this%%3E+FROM+%%3Chttp%%3A//^{URIQADefaultHost}^/CustomerKG%%23%%3E&format=%U',
vector('path', '*accept*'),
null,
'(text/rdf.n3)|(application/rdf.xml)|(text/n3)|(application/json)|(text/turtle)',
2,
null
);
DB.DBA.URLREWRITE_CREATE_REGEX_RULE (
'customerkg_rule4',
1,
'/CustomerKG/stat([^#]*)',
vector('path'),
1,
'/sparql?query=DESCRIBE+%%3Chttp%%3A//^{URIQADefaultHost}^/CustomerKG/stat%%23%%3E+%%3Fo+FROM+%%3Chttp%%3A//^{URIQADefaultHost}^/CustomerKG%%23%%3E+WHERE+{+%%3Chttp%%3A//^{URIQADefaultHost}^/CustomerKG/stat%%23%%3E+%%3Fp+%%3Fo+}&format=%U',
vector('*accept*'),
null,
'(text/rdf.n3)|(application/rdf.xml)|(text/n3)|(application/json)|(text/turtle)',
2,
null
);
DB.DBA.URLREWRITE_CREATE_REGEX_RULE (
'customerkg_rule6',
1,
'/CustomerKG/objects/([^#]*)',
vector('path'),
1,
'/sparql?query=DESCRIBE+%%3Chttp%%3A//^{URIQADefaultHost}^/CustomerKG/objects/%U%%3E+FROM+%%3Chttp%%3A//^{URIQADefaultHost}^/CustomerKG%%23%%3E&format=%U',
vector('path', '*accept*'),
null,
'(text/rdf.n3)|(application/rdf.xml)|(text/n3)|(application/json)|(text/turtle)',
2,
null
);
DB.DBA.URLREWRITE_CREATE_REGEX_RULE (
'customerkg_rule1',
1,
'([^#]*)',
vector('path'),
1,
'/describe/?url=http%%3A//^{URIQADefaultHost}^%U%%23this&graph=http%%3A//^{URIQADefaultHost}^/CustomerKG%%23&distinct=0',
vector('path'),
null,
null,
2,
303
);
DB.DBA.URLREWRITE_CREATE_REGEX_RULE (
'customerkg_rule7',
1,
'/CustomerKG/stat([^#]*)',
vector('path'),
1,
'/describe/?url=http%%3A//^{URIQADefaultHost}^/CustomerKG/stat%%23&graph=http%%3A//^{URIQADefaultHost}^/CustomerKG%%23',
vector('path'),
null,
null,
2,
303
);
DB.DBA.URLREWRITE_CREATE_REGEX_RULE (
'customerkg_rule5',
1,
'/CustomerKG/objects/(.*)',
vector('path'),
1,
'/services/rdf/object.binary?path=%%2FCustomerKG%%2Fobjects%%2F%U&accept=%U',
vector('path', '*accept*'),
null,
null,
2,
null
);
DB.DBA.URLREWRITE_CREATE_RULELIST ( 'customerkg_rule_list1', 1, vector ( 'customerkg_rule1', 'customerkg_rule7', 'customerkg_rule5', 'customerkg_rule2', 'customerkg_rule4', 'customerkg_rule6'));
DB.DBA.VHOST_REMOVE (lpath=>'/CustomerKG');
DB.DBA.VHOST_DEFINE (lpath=>'/CustomerKG', ppath=>'/', vsp_user=>'dba', is_dav=>0,
is_brws=>0, opts=>vector ('url_rewrite', 'customerkg_rule_list1')
);
DB.DBA.URLREWRITE_CREATE_REGEX_RULE (
'customerkg_owl_rule2',
1,
'(/[^#]*)',
vector('path'),
1,
'/sparql?query=DESCRIBE+%%3Chttp%%3A//^{URIQADefaultHost}^%U%%3E+FROM+%%3Chttp%%3A//^{URIQADefaultHost}^/schemas/CustomerKG%%23%%3E&format=%U',
vector('path', '*accept*'),
null,
'(text/rdf.n3)|(application/rdf.xml)|(text/n3)|(application/json)|(text/turtle)',
2,
null
);
DB.DBA.URLREWRITE_CREATE_REGEX_RULE (
'customerkg_owl_rule1',
1,
'([^#]*)',
vector('path'),
1,
'/describe/?url=http://^{URIQADefaultHost}^%U',
vector('path'),
null,
null,
2,
303
);
DB.DBA.URLREWRITE_CREATE_RULELIST ( 'customerkg_owl_rule_list1', 1, vector ( 'customerkg_owl_rule1', 'customerkg_owl_rule2'));
DB.DBA.VHOST_REMOVE (lpath=>'/schemas/CustomerKG');
DB.DBA.VHOST_DEFINE (lpath=>'/schemas/CustomerKG', ppath=>'/', vsp_user=>'dba', is_dav=>0,
is_brws=>0, opts=>vector ('url_rewrite', 'customerkg_owl_rule_list1')
);

-- Part 8: identity reconciliation graph. Inverse-functional-property and owl:sameAs reasoning over a physical graph that
-- carries facts derived from the view plus records from other systems. Query it with the pragmas shown in the page.
-- 1. Vocabulary: declare the identifying property inverse functional, in its own graph
DB.DBA.TTLP(file_to_string_output('customerkg-identity-vocabulary-claude_sonnet_5_5-1.ttl'), '', 'http://demo.openlinksw.com/schemas/CustomerKG/identity/');
-- The Turtle loaded by that call:
@prefix ckg: <http://demo.openlinksw.com/schemas/CustomerKG/> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix schema: <http://schema.org/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

<http://demo.openlinksw.com/schemas/CustomerKG/identity/> a owl:Ontology ;
    rdfs:label "CustomerKG identity reconciliation vocabulary"@en .

ckg:emailHash a owl:InverseFunctionalProperty, owl:DatatypeProperty ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/identity/> ;
    rdfs:label "email hash"@en ;
    rdfs:comment "A pseudonymous hash of an email address. Two records that carry the same hash describe the same person, so the property is declared inverse functional and a reasoner derives owl:sameAs between them."@en ;
    rdfs:domain schema:Person ;
    rdfs:range xsd:string .

-- 2. Rule set: derive reasoning rules from that vocabulary
DB.DBA.rdfs_rule_set('urn:customerkg:identity-inference', 'http://demo.openlinksw.com/schemas/CustomerKG/identity/');

-- 3. Identity graph, part one: facts derived from the live RDF View (nothing is retyped)
SPARQL PREFIX ckg: <http://demo.openlinksw.com/schemas/CustomerKG/> PREFIX schema: <http://schema.org/>
INSERT INTO GRAPH <http://demo.openlinksw.com/CustomerKG/identity#> { ?c ckg:emailHash ?v }
WHERE { GRAPH <http://demo.openlinksw.com/CustomerKG#> { ?s a ckg:IdentitySignal ; ckg:signalKind "email" ; ckg:signalValue ?v ; ckg:resolvesTo ?c } };

SPARQL PREFIX ckg: <http://demo.openlinksw.com/schemas/CustomerKG/> PREFIX schema: <http://schema.org/>
INSERT INTO GRAPH <http://demo.openlinksw.com/CustomerKG/identity#> { ?c a schema:Person ; schema:name ?n }
WHERE { GRAPH <http://demo.openlinksw.com/CustomerKG#> { ?c a ckg:Customer ; schema:name ?n } };

-- 4. Identity graph, part two: records from other systems, one explicit owl:sameAs, one record that matches nobody
SPARQL PREFIX ckg: <http://demo.openlinksw.com/schemas/CustomerKG/> PREFIX schema: <http://schema.org/> PREFIX owl: <http://www.w3.org/2002/07/owl#>
INSERT DATA INTO GRAPH <http://demo.openlinksw.com/CustomerKG/identity#> {
  <http://demo.openlinksw.com/CustomerKG/external/crm/501#this> a schema:Person ; schema:name "A. Quinn (CRM export)" ; ckg:emailHash "hem:a1f3" .
  <http://demo.openlinksw.com/CustomerKG/external/app/77#this> a schema:Person ; schema:name "Avery Q. (app profile)" ; ckg:emailHash "hem:a1f3" .
  <http://demo.openlinksw.com/CustomerKG/external/crm/502#this> a schema:Person ; schema:name "B. Moreno (CRM export)" ; ckg:emailHash "hem:b2c4" .
  <http://demo.openlinksw.com/CustomerKG/external/crm/599#this> a schema:Person ; schema:name "Unmatched (CRM export)" ; ckg:emailHash "hem:ffff" .
  <http://demo.openlinksw.com/CustomerKG/external/loyalty/LY-10001#this> a schema:Person ; schema:name "Loyalty member LY-10001" ; owl:sameAs <http://demo.openlinksw.com/CustomerKG/customer/1#this> .
};

1. DDL: tables, rows and access

3 cards

Standard SQL, run through SELECT exec() over authenticated XMLA. Foreign keys are declared; nullable ones are deliberate.

Twelve tables, foreign keys declared

SQL

The relational estate: channels, products, customers, identity signals, consent, offers, guardrails, campaigns, decisions, the decision-to-guardrail link table, context items and outcomes. Standard SQL; nullable foreign keys are deliberate, for unmatched signals and for actions without an offer or channel.

CREATE TABLE CustomerKG.kidehen.channel (channel_id INTEGER NOT NULL PRIMARY KEY, channel_name VARCHAR(40) NOT NULL, channel_kind VARCHAR(8) NOT NULL);

CREATE TABLE CustomerKG.kidehen.product (product_id INTEGER NOT NULL PRIMARY KEY, product_name VARCHAR(48) NOT NULL, category VARCHAR(16) NOT NULL, margin_pct INTEGER NOT NULL);

CREATE TABLE CustomerKG.kidehen.customer (customer_id INTEGER NOT NULL PRIMARY KEY, full_name VARCHAR(48) NOT NULL, lifecycle_stage VARCHAR(12) NOT NULL, loyalty_tier VARCHAR(8) NOT NULL, home_city VARCHAR(24) NOT NULL, usual_order_day VARCHAR(10) NOT NULL);

CREATE TABLE CustomerKG.kidehen.identity_signal (signal_id INTEGER NOT NULL PRIMARY KEY, customer_id INTEGER, signal_kind VARCHAR(16) NOT NULL, signal_value VARCHAR(24) NOT NULL, match_method VARCHAR(16) NOT NULL, confidence INTEGER NOT NULL, seen_date DATE NOT NULL, FOREIGN KEY (customer_id) REFERENCES CustomerKG.kidehen.customer (customer_id));

CREATE TABLE CustomerKG.kidehen.consent (consent_id INTEGER NOT NULL PRIMARY KEY, customer_id INTEGER NOT NULL, channel_id INTEGER NOT NULL, status VARCHAR(10) NOT NULL, updated_date DATE NOT NULL, FOREIGN KEY (customer_id) REFERENCES CustomerKG.kidehen.customer (customer_id), FOREIGN KEY (channel_id) REFERENCES CustomerKG.kidehen.channel (channel_id));

CREATE TABLE CustomerKG.kidehen.offer (offer_id INTEGER NOT NULL PRIMARY KEY, offer_name VARCHAR(40) NOT NULL, offer_kind VARCHAR(16) NOT NULL, discount_pct INTEGER NOT NULL, product_id INTEGER, FOREIGN KEY (product_id) REFERENCES CustomerKG.kidehen.product (product_id));

CREATE TABLE CustomerKG.kidehen.guardrail (guardrail_id INTEGER NOT NULL PRIMARY KEY, rule_name VARCHAR(48) NOT NULL, rule_kind VARCHAR(20) NOT NULL, limit_value INTEGER NOT NULL, offer_id INTEGER, FOREIGN KEY (offer_id) REFERENCES CustomerKG.kidehen.offer (offer_id));

CREATE TABLE CustomerKG.kidehen.campaign (campaign_id INTEGER NOT NULL PRIMARY KEY, campaign_name VARCHAR(48) NOT NULL, goal VARCHAR(80) NOT NULL, start_date DATE NOT NULL);

CREATE TABLE CustomerKG.kidehen.decision (decision_id INTEGER NOT NULL PRIMARY KEY, customer_id INTEGER NOT NULL, campaign_id INTEGER NOT NULL, arm VARCHAR(10) NOT NULL, action_kind VARCHAR(16) NOT NULL, offer_id INTEGER, channel_id INTEGER, decided_date DATE NOT NULL, rationale VARCHAR(160) NOT NULL, approved_by_human INTEGER NOT NULL, FOREIGN KEY (customer_id) REFERENCES CustomerKG.kidehen.customer (customer_id), FOREIGN KEY (campaign_id) REFERENCES CustomerKG.kidehen.campaign (campaign_id), FOREIGN KEY (offer_id) REFERENCES CustomerKG.kidehen.offer (offer_id), FOREIGN KEY (channel_id) REFERENCES CustomerKG.kidehen.channel (channel_id));

CREATE TABLE CustomerKG.kidehen.decision_guardrail (decision_id INTEGER NOT NULL, guardrail_id INTEGER NOT NULL, PRIMARY KEY (decision_id, guardrail_id), FOREIGN KEY (decision_id) REFERENCES CustomerKG.kidehen.decision (decision_id), FOREIGN KEY (guardrail_id) REFERENCES CustomerKG.kidehen.guardrail (guardrail_id));

CREATE TABLE CustomerKG.kidehen.context_item (item_id INTEGER NOT NULL PRIMARY KEY, decision_id INTEGER NOT NULL, context_type VARCHAR(10) NOT NULL, context_key VARCHAR(24) NOT NULL, context_value VARCHAR(120) NOT NULL, FOREIGN KEY (decision_id) REFERENCES CustomerKG.kidehen.decision (decision_id));

CREATE TABLE CustomerKG.kidehen.outcome (outcome_id INTEGER NOT NULL PRIMARY KEY, decision_id INTEGER NOT NULL, ordered INTEGER NOT NULL, revenue_usd INTEGER NOT NULL, margin_usd INTEGER NOT NULL, media_cost_usd INTEGER NOT NULL, discount_cost_usd INTEGER NOT NULL, repeat_visit_30d INTEGER NOT NULL, window_end DATE NOT NULL, FOREIGN KEY (decision_id) REFERENCES CustomerKG.kidehen.decision (decision_id));

Synthetic rows, one per table shown

SQL

Two hundred and twenty-eight rows in all. The data is synthetic and illustrative: it models the article's quick-service-restaurant scenario and is not Databricks data; every customer name is invented.

-- One representative row per table; the batch script carries all 228.
INSERT INTO CustomerKG.kidehen.channel VALUES (1, 'App push', 'owned');
INSERT INTO CustomerKG.kidehen.product VALUES (1, 'Classic Burger Meal', 'meal', 38);
INSERT INTO CustomerKG.kidehen.customer VALUES (1, 'Avery Quinn', 'lapsing', 'gold', 'Austin', 'Friday');
INSERT INTO CustomerKG.kidehen.identity_signal VALUES (1, 1, 'email', 'hem:a1f3', 'deterministic', 100, stringdate('2026-08-02'));
INSERT INTO CustomerKG.kidehen.consent VALUES (1, 1, 2, 'granted', stringdate('2026-08-01'));
INSERT INTO CustomerKG.kidehen.offer VALUES (1, 'Friday reminder', 'reminder', 0, 4);
INSERT INTO CustomerKG.kidehen.guardrail VALUES (1, 'Maximum discount depth 20 percent', 'discount-cap', 20, NULL);
INSERT INTO CustomerKG.kidehen.campaign VALUES (1, 'Reactivate lapsed customers', 'reactivate lapsed customers', stringdate('2026-09-22'));
INSERT INTO CustomerKG.kidehen.decision VALUES (1, 1, 1, 'treatment', 'send-reminder', 1, 1, stringdate('2026-09-22'), 'Usual Friday order missed once, usual order browsed on Thursday: a reminder is enough', 0);
INSERT INTO CustomerKG.kidehen.decision_guardrail VALUES (1, 2);
INSERT INTO CustomerKG.kidehen.context_item VALUES (1, 1, 'customer', 'usual_order_day', 'Friday; skipped last week');
INSERT INTO CustomerKG.kidehen.outcome VALUES (1, 1, 1, 24, 9, 0, 0, 1, stringdate('2026-10-06'));

Access: the view inherits SQL grants

SQL

An RDF View has no access rules of its own; it inherits the SQL privileges of its source tables. SPARQL serves the anonymous endpoint, PUBLIC the rest, and demo and vdb the Query Builder and XMLA routes.

GRANT SELECT ON CustomerKG.kidehen.channel TO "SPARQL";
GRANT SELECT ON CustomerKG.kidehen.channel TO PUBLIC;
GRANT SELECT ON CustomerKG.kidehen.channel TO "demo";
GRANT SELECT ON CustomerKG.kidehen.channel TO "vdb";
-- ...repeated for each of the 12 tables (48 statements in the batch script)

2. RDF View generation: IRI classes, ontology, quad map and identity graph

4 cards

The Virtuoso-specific part: IRI classes and the ALTER QUAD STORAGE statement are SPARQL-keyword DDL, the ontology is plain Turtle, and the identity graph is a small physical graph that reconciles records by reasoning. The view inherits the SQL grants above.

Eleven IRI classes

SPARQL

Each IRI class turns a key column into a dereferenceable IRI. Through isql the statement needs the SPARQL keyword in front and the class name as an angle-bracket IRI. On the shared demo server the eleven statements together took several minutes.

SPARQL CREATE IRI CLASS <http://demo.openlinksw.com/schemas/CustomerKG/channel_iri> "http://demo.openlinksw.com/CustomerKG/channel/%d#this" (in channel_id integer not null);
SPARQL CREATE IRI CLASS <http://demo.openlinksw.com/schemas/CustomerKG/product_iri> "http://demo.openlinksw.com/CustomerKG/product/%d#this" (in product_id integer not null);
SPARQL CREATE IRI CLASS <http://demo.openlinksw.com/schemas/CustomerKG/customer_iri> "http://demo.openlinksw.com/CustomerKG/customer/%d#this" (in customer_id integer not null);
SPARQL CREATE IRI CLASS <http://demo.openlinksw.com/schemas/CustomerKG/signal_iri> "http://demo.openlinksw.com/CustomerKG/signal/%d#this" (in signal_id integer not null);
SPARQL CREATE IRI CLASS <http://demo.openlinksw.com/schemas/CustomerKG/consent_iri> "http://demo.openlinksw.com/CustomerKG/consent/%d#this" (in consent_id integer not null);
SPARQL CREATE IRI CLASS <http://demo.openlinksw.com/schemas/CustomerKG/offer_iri> "http://demo.openlinksw.com/CustomerKG/offer/%d#this" (in offer_id integer not null);
SPARQL CREATE IRI CLASS <http://demo.openlinksw.com/schemas/CustomerKG/guardrail_iri> "http://demo.openlinksw.com/CustomerKG/guardrail/%d#this" (in guardrail_id integer not null);
SPARQL CREATE IRI CLASS <http://demo.openlinksw.com/schemas/CustomerKG/campaign_iri> "http://demo.openlinksw.com/CustomerKG/campaign/%d#this" (in campaign_id integer not null);
SPARQL CREATE IRI CLASS <http://demo.openlinksw.com/schemas/CustomerKG/decision_iri> "http://demo.openlinksw.com/CustomerKG/decision/%d#this" (in decision_id integer not null);
SPARQL CREATE IRI CLASS <http://demo.openlinksw.com/schemas/CustomerKG/context_iri> "http://demo.openlinksw.com/CustomerKG/context/%d#this" (in item_id integer not null);
SPARQL CREATE IRI CLASS <http://demo.openlinksw.com/schemas/CustomerKG/outcome_iri> "http://demo.openlinksw.com/CustomerKG/outcome/%d#this" (in outcome_id integer not null);

The ontology: every term the view uses, declared

Turtle

The vocabulary for the view's graph: 16 classes and 47 properties with labels, comments, domains and ranges, cross-referenced to schema.org supertypes and close matches, and the four-group context hierarchy and groundedIn property hierarchy the entailment queries use.

@prefix :       <http://demo.openlinksw.com/schemas/CustomerKG/> .
@prefix ckg:    <http://demo.openlinksw.com/schemas/CustomerKG/> .
@prefix schema: <http://schema.org/> .
@prefix xsd:    <http://www.w3.org/2001/XMLSchema#> .
@prefix rdf:    <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs:   <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl:    <http://www.w3.org/2002/07/owl#> .
@prefix skos:   <http://www.w3.org/2004/02/skos/core#> .

# TBox for the CustomerKG RDF View on demo.openlinksw.com. The ontology is the entity
# <http://demo.openlinksw.com/schemas/CustomerKG/>; this document is a separate CreativeWork.

<http://demo.openlinksw.com/schemas/CustomerKG/ontology-document> a schema:CreativeWork ;
    schema:name "CustomerKG ontology (TBox)"@en ;
    schema:description "RDFS/OWL vocabulary for the CustomerKG demonstration: customers, identity signals, consent, channels, products, offers, guardrails, campaigns, decisions, context items and outcomes, grouped under the four kinds of context the Databricks agentic-marketing article names. Loaded into the named graph http://demo.openlinksw.com/schemas/CustomerKG/."@en ;
    schema:dateCreated "2026-10-02T00:00:00Z"^^xsd:dateTime ;
    schema:dateModified "2026-10-02T00:00:00Z"^^xsd:dateTime ;
    schema:author <https://www.linkedin.com/in/kidehen#this> ;
    schema:about <http://demo.openlinksw.com/schemas/CustomerKG/> .

<http://demo.openlinksw.com/schemas/CustomerKG/> a owl:Ontology ;
    rdfs:label "CustomerKG ontology"@en ;
    rdfs:comment "Vocabulary for describing the customer, business, decision and control context an agent reads before choosing a marketing action, and the measured outcome that follows."@en ;
    owl:versionInfo "1.0"@en .

# ── Classes ────────────────────────────────────────────────────────────────

ckg:ContextElement a owl:Class ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "Context element"@en ;
    rdfs:comment "Anything an agent may read before deciding what to do for a customer. The four groups below follow the four kinds of context the Databricks article names: customer, business, decision and control."@en ;
    rdfs:subClassOf schema:Thing .

ckg:CustomerContextElement a owl:Class ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "Customer context element"@en ;
    rdfs:comment "Identity, behaviour, preference, consent and lifecycle facts about a customer."@en ;
    rdfs:subClassOf ckg:ContextElement .

ckg:BusinessContextElement a owl:Class ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "Business context element"@en ;
    rdfs:comment "Growth objectives, margin, brand rules, inventory and channel constraints."@en ;
    rdfs:subClassOf ckg:ContextElement .

ckg:DecisionContextElement a owl:Class ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "Decision context element"@en ;
    rdfs:comment "The actions already taken, how customers responded and why earlier decisions were made."@en ;
    rdfs:subClassOf ckg:ContextElement .

ckg:ControlElement a owl:Class ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "Control element"@en ;
    rdfs:comment "The intent, guardrails, permissions and approval points that people define and agents must respect."@en ;
    rdfs:subClassOf ckg:ContextElement .

ckg:Customer a owl:Class ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "Customer"@en ;
    rdfs:comment "A person who buys from the brand, with a lifecycle stage and a loyalty tier."@en ;
    rdfs:subClassOf schema:Person, ckg:CustomerContextElement .

ckg:IdentitySignal a owl:Class ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "Identity signal"@en ;
    rdfs:comment "One observation that may identify a customer: an email hash, device, app session, ad exposure or loyalty id, with the method that matched it and the confidence. Unmatched pseudonymous signals have no customer."@en ;
    rdfs:subClassOf schema:Observation, ckg:CustomerContextElement .

ckg:ConsentRecord a owl:Class ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "Consent record"@en ;
    rdfs:comment "Whether a customer allows contact on one channel. It is both a customer fact and a control, so it sits under both groups."@en ;
    rdfs:subClassOf schema:Permit, ckg:CustomerContextElement, ckg:ControlElement .

ckg:Channel a owl:Class ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "Channel"@en ;
    rdfs:comment "A way to reach a customer, owned or paid."@en ;
    rdfs:subClassOf schema:Thing, ckg:BusinessContextElement .

ckg:Product a owl:Class ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "Product"@en ;
    rdfs:comment "A menu item with a category and a margin percentage."@en ;
    rdfs:subClassOf schema:Product, ckg:BusinessContextElement .

ckg:Offer a owl:Class ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "Offer"@en ;
    rdfs:comment "A reminder or incentive the brand may extend, with a discount depth and an optional promoted product."@en ;
    rdfs:subClassOf schema:Offer, ckg:BusinessContextElement .

ckg:Campaign a owl:Class ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "Campaign"@en ;
    rdfs:comment "A goal-driven effort such as reactivating lapsed customers, within which decisions are made."@en ;
    rdfs:subClassOf schema:Action, ckg:BusinessContextElement .

ckg:Guardrail a owl:Class ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "Guardrail"@en ;
    rdfs:comment "A rule the agent must respect: a discount cap, a contact-frequency limit, a consent requirement or a human-approval threshold."@en ;
    rdfs:subClassOf schema:Rule, ckg:ControlElement .

ckg:Decision a owl:Class ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "Decision"@en ;
    rdfs:comment "One choice about one customer: act or not, with which offer and channel, in which experiment arm, and why."@en ;
    rdfs:subClassOf schema:Action, ckg:DecisionContextElement .

ckg:ContextItem a owl:Class ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "Context item"@en ;
    rdfs:comment "A single labelled fact an agent read when making a decision, tagged with the kind of context it belongs to."@en ;
    rdfs:subClassOf schema:PropertyValue, ckg:ContextElement .

ckg:Outcome a owl:Class ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "Outcome"@en ;
    rdfs:comment "What happened after a decision: whether the customer ordered and the revenue, margin and costs involved."@en ;
    rdfs:subClassOf schema:Event, ckg:DecisionContextElement .

# ── Properties ─────────────────────────────────────────────────────────────

ckg:channelKind a owl:DatatypeProperty ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "channel kind"@en ;
    rdfs:comment "Whether a channel is owned (the brand controls it) or paid (media is bought)."@en ;
    rdfs:domain ckg:Channel ;
    rdfs:range xsd:string ;
    skos:closeMatch schema:category .

ckg:category a owl:DatatypeProperty ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "category"@en ;
    rdfs:comment "Menu category such as meal, bundle, side or dessert."@en ;
    rdfs:domain ckg:Product ;
    rdfs:range xsd:string ;
    skos:closeMatch schema:category .

ckg:marginPct a owl:DatatypeProperty ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "margin percentage"@en ;
    rdfs:comment "Gross margin of the product, in percent of price."@en ;
    rdfs:domain ckg:Product ;
    rdfs:range xsd:integer .

ckg:lifecycleStage a owl:DatatypeProperty ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "lifecycle stage"@en ;
    rdfs:comment "Where the customer sits in the relationship: active, lapsing or lapsed."@en ;
    rdfs:domain ckg:Customer ;
    rdfs:range xsd:string .

ckg:loyaltyTier a owl:DatatypeProperty ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "loyalty tier"@en ;
    rdfs:comment "Loyalty programme tier: bronze, silver or gold."@en ;
    rdfs:domain ckg:Customer ;
    rdfs:range xsd:string ;
    skos:closeMatch schema:memberOf .

ckg:homeCity a owl:DatatypeProperty ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "home city"@en ;
    rdfs:comment "The city the customer usually orders in. Residence, never birthplace."@en ;
    rdfs:domain ckg:Customer ;
    rdfs:range xsd:string ;
    skos:closeMatch schema:homeLocation .

ckg:usualOrderDay a owl:DatatypeProperty ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "usual order day"@en ;
    rdfs:comment "The weekday on which the customer usually orders."@en ;
    rdfs:domain ckg:Customer ;
    rdfs:range xsd:string .

ckg:signalKind a owl:DatatypeProperty ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "signal kind"@en ;
    rdfs:comment "The kind of signal: email, loyalty_id, device, app_session or ad_exposure."@en ;
    rdfs:domain ckg:IdentitySignal ;
    rdfs:range xsd:string ;
    skos:closeMatch schema:additionalType .

ckg:signalValue a owl:DatatypeProperty ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "signal value"@en ;
    rdfs:comment "A pseudonymous token for the signal, never a raw identifier."@en ;
    rdfs:domain ckg:IdentitySignal ;
    rdfs:range xsd:string ;
    skos:closeMatch schema:value .

ckg:matchMethod a owl:DatatypeProperty ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "match method"@en ;
    rdfs:comment "How the signal was tied to a customer: deterministic, probabilistic, agentic, or unmatched."@en ;
    rdfs:domain ckg:IdentitySignal ;
    rdfs:range xsd:string .

ckg:confidence a owl:DatatypeProperty ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "confidence"@en ;
    rdfs:comment "Match confidence in percent; zero for an unmatched signal."@en ;
    rdfs:domain ckg:IdentitySignal ;
    rdfs:range xsd:integer .

ckg:seenDate a owl:DatatypeProperty ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "seen date"@en ;
    rdfs:comment "The date the signal was last observed."@en ;
    rdfs:domain ckg:IdentitySignal ;
    rdfs:range xsd:date ;
    skos:closeMatch schema:observationDate .

ckg:resolvesTo a owl:ObjectProperty ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "resolves to"@en ;
    rdfs:comment "The customer a signal has been resolved to. Absent for unmatched signals."@en ;
    rdfs:domain ckg:IdentitySignal ;
    rdfs:range ckg:Customer ;
    skos:closeMatch schema:about .

ckg:consentStatus a owl:DatatypeProperty ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "consent status"@en ;
    rdfs:comment "granted or withdrawn."@en ;
    rdfs:domain ckg:ConsentRecord ;
    rdfs:range xsd:string ;
    skos:closeMatch schema:status .

ckg:updatedDate a owl:DatatypeProperty ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "updated date"@en ;
    rdfs:comment "The date the consent status last changed."@en ;
    rdfs:domain ckg:ConsentRecord ;
    rdfs:range xsd:date ;
    skos:closeMatch schema:dateModified .

ckg:forChannel a owl:ObjectProperty ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "for channel"@en ;
    rdfs:comment "The channel the consent applies to."@en ;
    rdfs:domain ckg:ConsentRecord ;
    rdfs:range ckg:Channel .

ckg:offerKind a owl:DatatypeProperty ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "offer kind"@en ;
    rdfs:comment "reminder, incentive or bundle-incentive."@en ;
    rdfs:domain ckg:Offer ;
    rdfs:range xsd:string ;
    skos:closeMatch schema:category .

ckg:discountPct a owl:DatatypeProperty ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "discount percentage"@en ;
    rdfs:comment "Discount depth in percent of price."@en ;
    rdfs:domain ckg:Offer ;
    rdfs:range xsd:integer ;
    skos:closeMatch schema:discount .

ckg:promotes a owl:ObjectProperty ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "promotes"@en ;
    rdfs:comment "The product an offer promotes, when it promotes one."@en ;
    rdfs:domain ckg:Offer ;
    rdfs:range ckg:Product ;
    skos:closeMatch schema:itemOffered .

ckg:ruleKind a owl:DatatypeProperty ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "rule kind"@en ;
    rdfs:comment "discount-cap, contact-frequency, consent-required or human-approval."@en ;
    rdfs:domain ckg:Guardrail ;
    rdfs:range xsd:string ;
    skos:closeMatch schema:category .

ckg:limitValue a owl:DatatypeProperty ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "limit value"@en ;
    rdfs:comment "The threshold the rule enforces."@en ;
    rdfs:domain ckg:Guardrail ;
    rdfs:range xsd:integer ;
    skos:closeMatch schema:value .

ckg:appliesToOffer a owl:ObjectProperty ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "applies to offer"@en ;
    rdfs:comment "Narrows a guardrail to one offer; absent means the rule is global."@en ;
    rdfs:domain ckg:Guardrail ;
    rdfs:range ckg:Offer .

ckg:goal a owl:DatatypeProperty ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "goal"@en ;
    rdfs:comment "The business outcome the campaign is meant to move."@en ;
    rdfs:domain ckg:Campaign ;
    rdfs:range xsd:string ;
    skos:closeMatch schema:description .

ckg:startDate a owl:DatatypeProperty ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "start date"@en ;
    rdfs:comment "When the campaign started."@en ;
    rdfs:domain ckg:Campaign ;
    rdfs:range xsd:date ;
    skos:closeMatch schema:startDate .

ckg:arm a owl:DatatypeProperty ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "experiment arm"@en ;
    rdfs:comment "treatment, or holdout for the randomised group that is deliberately left alone."@en ;
    rdfs:domain ckg:Decision ;
    rdfs:range xsd:string .

ckg:actionKind a owl:DatatypeProperty ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "action kind"@en ;
    rdfs:comment "send-reminder, send-incentive, switch-channel, suppress or no-action."@en ;
    rdfs:domain ckg:Decision ;
    rdfs:range xsd:string ;
    skos:closeMatch schema:additionalType .

ckg:decidedDate a owl:DatatypeProperty ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "decided date"@en ;
    rdfs:comment "When the decision was taken."@en ;
    rdfs:domain ckg:Decision ;
    rdfs:range xsd:date ;
    skos:closeMatch schema:startTime .

ckg:rationale a owl:DatatypeProperty ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "rationale"@en ;
    rdfs:comment "Why the agent chose this action, in words a marketer can audit."@en ;
    rdfs:domain ckg:Decision ;
    rdfs:range xsd:string ;
    skos:closeMatch schema:description .

ckg:approvedByHuman a owl:DatatypeProperty ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "approved by human"@en ;
    rdfs:comment "1 when a person approved the action, 0 otherwise."@en ;
    rdfs:domain ckg:Decision ;
    rdfs:range xsd:integer .

ckg:groundedIn a owl:ObjectProperty ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "grounded in"@en ;
    rdfs:comment "Super-property of every link from a decision to a thing it was grounded in: its customer, campaign, offer, channel and guardrails."@en ;
    rdfs:domain ckg:Decision ;
    rdfs:range ckg:ContextElement .

ckg:forCustomer a owl:ObjectProperty ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "for customer"@en ;
    rdfs:comment "The customer a decision or consent record concerns."@en ;
    rdfs:domain ckg:ContextElement ;
    rdfs:range ckg:Customer ;
    rdfs:subPropertyOf ckg:groundedIn ;
    skos:closeMatch schema:about .

ckg:inCampaign a owl:ObjectProperty ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "in campaign"@en ;
    rdfs:comment "The campaign a decision belongs to."@en ;
    rdfs:domain ckg:Decision ;
    rdfs:range ckg:Campaign ;
    rdfs:subPropertyOf ckg:groundedIn ;
    skos:closeMatch schema:isPartOf .

ckg:usesOffer a owl:ObjectProperty ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "uses offer"@en ;
    rdfs:comment "The offer a decision extends. Absent for reminders without an offer, suppressions and holdouts."@en ;
    rdfs:domain ckg:Decision ;
    rdfs:range ckg:Offer ;
    rdfs:subPropertyOf ckg:groundedIn ;
    skos:closeMatch schema:object .

ckg:viaChannel a owl:ObjectProperty ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "via channel"@en ;
    rdfs:comment "The channel a decision acts through. Absent for a suppression or a holdout."@en ;
    rdfs:domain ckg:Decision ;
    rdfs:range ckg:Channel ;
    rdfs:subPropertyOf ckg:groundedIn ;
    skos:closeMatch schema:instrument .

ckg:constrainedBy a owl:ObjectProperty ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "constrained by"@en ;
    rdfs:comment "A guardrail that applied to the decision."@en ;
    rdfs:domain ckg:Decision ;
    rdfs:range ckg:Guardrail ;
    rdfs:subPropertyOf ckg:groundedIn .

ckg:contextType a owl:DatatypeProperty ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "context type"@en ;
    rdfs:comment "customer, business, decision or control: which of the four kinds of context the item is."@en ;
    rdfs:domain ckg:ContextItem ;
    rdfs:range xsd:string ;
    skos:closeMatch schema:additionalType .

ckg:contextKey a owl:DatatypeProperty ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "context key"@en ;
    rdfs:comment "A short label for the fact."@en ;
    rdfs:domain ckg:ContextItem ;
    rdfs:range xsd:string ;
    skos:closeMatch schema:name .

ckg:contextValue a owl:DatatypeProperty ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "context value"@en ;
    rdfs:comment "The fact itself."@en ;
    rdfs:domain ckg:ContextItem ;
    rdfs:range xsd:string ;
    skos:closeMatch schema:value .

ckg:informs a owl:ObjectProperty ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "informs"@en ;
    rdfs:comment "The decision the item was read for."@en ;
    rdfs:domain ckg:ContextItem ;
    rdfs:range ckg:Decision .

ckg:ordered a owl:DatatypeProperty ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "ordered"@en ;
    rdfs:comment "1 when the customer ordered inside the measurement window, 0 otherwise."@en ;
    rdfs:domain ckg:Outcome ;
    rdfs:range xsd:integer .

ckg:revenueUsd a owl:DatatypeProperty ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "revenue (USD)"@en ;
    rdfs:comment "Revenue from the order, in whole US dollars."@en ;
    rdfs:domain ckg:Outcome ;
    rdfs:range xsd:integer ;
    skos:closeMatch schema:price .

ckg:marginUsd a owl:DatatypeProperty ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "margin (USD)"@en ;
    rdfs:comment "Gross margin on the order before discount and media costs."@en ;
    rdfs:domain ckg:Outcome ;
    rdfs:range xsd:integer .

ckg:mediaCostUsd a owl:DatatypeProperty ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "media cost (USD)"@en ;
    rdfs:comment "Paid-media cost attributed to the decision."@en ;
    rdfs:domain ckg:Outcome ;
    rdfs:range xsd:integer .

ckg:discountCostUsd a owl:DatatypeProperty ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "discount cost (USD)"@en ;
    rdfs:comment "Discount given on the order."@en ;
    rdfs:domain ckg:Outcome ;
    rdfs:range xsd:integer .

ckg:repeatVisit30d a owl:DatatypeProperty ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "repeat visit within 30 days"@en ;
    rdfs:comment "1 when the customer returned within 30 days of the order."@en ;
    rdfs:domain ckg:Outcome ;
    rdfs:range xsd:integer .

ckg:windowEnd a owl:DatatypeProperty ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "window end"@en ;
    rdfs:comment "The last day of the measurement window."@en ;
    rdfs:domain ckg:Outcome ;
    rdfs:range xsd:date ;
    skos:closeMatch schema:endTime .

ckg:ofDecision a owl:ObjectProperty ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/> ;
    rdfs:label "of decision"@en ;
    rdfs:comment "The decision whose result this is."@en ;
    rdfs:domain ckg:Outcome ;
    rdfs:range ckg:Decision .

The RDF View: one ALTER QUAD STORAGE statement

SPARQL

One statement maps all twelve tables to 1,160 triples at query time in the named graph http://demo.openlinksw.com/CustomerKG#. Every table in the FROM list is aliased, five nullable foreign keys carry an IS NOT NULL guard, and the link table maps the constrainedBy predicate.

SPARQL
ALTER QUAD STORAGE virtrdf:DefaultQuadStorage
  FROM CustomerKG.kidehen.channel AS ch
  FROM CustomerKG.kidehen.product AS pr
  FROM CustomerKG.kidehen.customer AS cu
  FROM CustomerKG.kidehen.identity_signal AS sg
  FROM CustomerKG.kidehen.consent AS co
  FROM CustomerKG.kidehen.offer AS ofr
  FROM CustomerKG.kidehen.guardrail AS gr
  FROM CustomerKG.kidehen.campaign AS ca
  FROM CustomerKG.kidehen.decision AS de
  FROM CustomerKG.kidehen.decision_guardrail AS dg
  FROM CustomerKG.kidehen.context_item AS ci
  FROM CustomerKG.kidehen.outcome AS ou
{
  GRAPH <http://demo.openlinksw.com/CustomerKG#>
  {
    <http://demo.openlinksw.com/schemas/CustomerKG/channel_iri>(ch.channel_id) <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://demo.openlinksw.com/schemas/CustomerKG/Channel> as virtrdf:CustomerKG-channel-type .
    <http://demo.openlinksw.com/schemas/CustomerKG/channel_iri>(ch.channel_id) <http://schema.org/name> ch.channel_name as virtrdf:CustomerKG-channel-name .
    <http://demo.openlinksw.com/schemas/CustomerKG/channel_iri>(ch.channel_id) <http://demo.openlinksw.com/schemas/CustomerKG/channelKind> ch.channel_kind as virtrdf:CustomerKG-channel-kind .
    <http://demo.openlinksw.com/schemas/CustomerKG/product_iri>(pr.product_id) <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://demo.openlinksw.com/schemas/CustomerKG/Product> as virtrdf:CustomerKG-product-type .
    <http://demo.openlinksw.com/schemas/CustomerKG/product_iri>(pr.product_id) <http://schema.org/name> pr.product_name as virtrdf:CustomerKG-product-name .
    <http://demo.openlinksw.com/schemas/CustomerKG/product_iri>(pr.product_id) <http://demo.openlinksw.com/schemas/CustomerKG/category> pr.category as virtrdf:CustomerKG-product-category .
    <http://demo.openlinksw.com/schemas/CustomerKG/product_iri>(pr.product_id) <http://demo.openlinksw.com/schemas/CustomerKG/marginPct> pr.margin_pct as virtrdf:CustomerKG-product-margin .
    <http://demo.openlinksw.com/schemas/CustomerKG/customer_iri>(cu.customer_id) <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://demo.openlinksw.com/schemas/CustomerKG/Customer> as virtrdf:CustomerKG-customer-type .
    <http://demo.openlinksw.com/schemas/CustomerKG/customer_iri>(cu.customer_id) <http://schema.org/name> cu.full_name as virtrdf:CustomerKG-customer-name .
    <http://demo.openlinksw.com/schemas/CustomerKG/customer_iri>(cu.customer_id) <http://demo.openlinksw.com/schemas/CustomerKG/lifecycleStage> cu.lifecycle_stage as virtrdf:CustomerKG-customer-lifecycle .
    <http://demo.openlinksw.com/schemas/CustomerKG/customer_iri>(cu.customer_id) <http://demo.openlinksw.com/schemas/CustomerKG/loyaltyTier> cu.loyalty_tier as virtrdf:CustomerKG-customer-tier .
    <http://demo.openlinksw.com/schemas/CustomerKG/customer_iri>(cu.customer_id) <http://demo.openlinksw.com/schemas/CustomerKG/homeCity> cu.home_city as virtrdf:CustomerKG-customer-city .
    <http://demo.openlinksw.com/schemas/CustomerKG/customer_iri>(cu.customer_id) <http://demo.openlinksw.com/schemas/CustomerKG/usualOrderDay> cu.usual_order_day as virtrdf:CustomerKG-customer-day .
    <http://demo.openlinksw.com/schemas/CustomerKG/signal_iri>(sg.signal_id) <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://demo.openlinksw.com/schemas/CustomerKG/IdentitySignal> as virtrdf:CustomerKG-signal-type .
    <http://demo.openlinksw.com/schemas/CustomerKG/signal_iri>(sg.signal_id) <http://demo.openlinksw.com/schemas/CustomerKG/signalKind> sg.signal_kind as virtrdf:CustomerKG-signal-kind .
    <http://demo.openlinksw.com/schemas/CustomerKG/signal_iri>(sg.signal_id) <http://demo.openlinksw.com/schemas/CustomerKG/signalValue> sg.signal_value as virtrdf:CustomerKG-signal-value .
    <http://demo.openlinksw.com/schemas/CustomerKG/signal_iri>(sg.signal_id) <http://demo.openlinksw.com/schemas/CustomerKG/matchMethod> sg.match_method as virtrdf:CustomerKG-signal-method .
    <http://demo.openlinksw.com/schemas/CustomerKG/signal_iri>(sg.signal_id) <http://demo.openlinksw.com/schemas/CustomerKG/confidence> sg.confidence as virtrdf:CustomerKG-signal-confidence .
    <http://demo.openlinksw.com/schemas/CustomerKG/signal_iri>(sg.signal_id) <http://demo.openlinksw.com/schemas/CustomerKG/seenDate> sg.seen_date as virtrdf:CustomerKG-signal-seen .
    <http://demo.openlinksw.com/schemas/CustomerKG/signal_iri>(sg.signal_id) <http://demo.openlinksw.com/schemas/CustomerKG/resolvesTo> <http://demo.openlinksw.com/schemas/CustomerKG/customer_iri>(sg.customer_id) where (^{sg.}^.customer_id is not null) as virtrdf:CustomerKG-signal-customer .
    <http://demo.openlinksw.com/schemas/CustomerKG/consent_iri>(co.consent_id) <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://demo.openlinksw.com/schemas/CustomerKG/ConsentRecord> as virtrdf:CustomerKG-consent-type .
    <http://demo.openlinksw.com/schemas/CustomerKG/consent_iri>(co.consent_id) <http://demo.openlinksw.com/schemas/CustomerKG/consentStatus> co.status as virtrdf:CustomerKG-consent-status .
    <http://demo.openlinksw.com/schemas/CustomerKG/consent_iri>(co.consent_id) <http://demo.openlinksw.com/schemas/CustomerKG/updatedDate> co.updated_date as virtrdf:CustomerKG-consent-updated .
    <http://demo.openlinksw.com/schemas/CustomerKG/consent_iri>(co.consent_id) <http://demo.openlinksw.com/schemas/CustomerKG/forCustomer> <http://demo.openlinksw.com/schemas/CustomerKG/customer_iri>(co.customer_id) as virtrdf:CustomerKG-consent-customer .
    <http://demo.openlinksw.com/schemas/CustomerKG/consent_iri>(co.consent_id) <http://demo.openlinksw.com/schemas/CustomerKG/forChannel> <http://demo.openlinksw.com/schemas/CustomerKG/channel_iri>(co.channel_id) as virtrdf:CustomerKG-consent-channel .
    <http://demo.openlinksw.com/schemas/CustomerKG/offer_iri>(ofr.offer_id) <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://demo.openlinksw.com/schemas/CustomerKG/Offer> as virtrdf:CustomerKG-offer-type .
    <http://demo.openlinksw.com/schemas/CustomerKG/offer_iri>(ofr.offer_id) <http://schema.org/name> ofr.offer_name as virtrdf:CustomerKG-offer-name .
    <http://demo.openlinksw.com/schemas/CustomerKG/offer_iri>(ofr.offer_id) <http://demo.openlinksw.com/schemas/CustomerKG/offerKind> ofr.offer_kind as virtrdf:CustomerKG-offer-kind .
    <http://demo.openlinksw.com/schemas/CustomerKG/offer_iri>(ofr.offer_id) <http://demo.openlinksw.com/schemas/CustomerKG/discountPct> ofr.discount_pct as virtrdf:CustomerKG-offer-discount .
    <http://demo.openlinksw.com/schemas/CustomerKG/offer_iri>(ofr.offer_id) <http://demo.openlinksw.com/schemas/CustomerKG/promotes> <http://demo.openlinksw.com/schemas/CustomerKG/product_iri>(ofr.product_id) where (^{ofr.}^.product_id is not null) as virtrdf:CustomerKG-offer-product .
    <http://demo.openlinksw.com/schemas/CustomerKG/guardrail_iri>(gr.guardrail_id) <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://demo.openlinksw.com/schemas/CustomerKG/Guardrail> as virtrdf:CustomerKG-guardrail-type .
    <http://demo.openlinksw.com/schemas/CustomerKG/guardrail_iri>(gr.guardrail_id) <http://schema.org/name> gr.rule_name as virtrdf:CustomerKG-guardrail-name .
    <http://demo.openlinksw.com/schemas/CustomerKG/guardrail_iri>(gr.guardrail_id) <http://demo.openlinksw.com/schemas/CustomerKG/ruleKind> gr.rule_kind as virtrdf:CustomerKG-guardrail-kind .
    <http://demo.openlinksw.com/schemas/CustomerKG/guardrail_iri>(gr.guardrail_id) <http://demo.openlinksw.com/schemas/CustomerKG/limitValue> gr.limit_value as virtrdf:CustomerKG-guardrail-limit .
    <http://demo.openlinksw.com/schemas/CustomerKG/guardrail_iri>(gr.guardrail_id) <http://demo.openlinksw.com/schemas/CustomerKG/appliesToOffer> <http://demo.openlinksw.com/schemas/CustomerKG/offer_iri>(gr.offer_id) where (^{gr.}^.offer_id is not null) as virtrdf:CustomerKG-guardrail-offer .
    <http://demo.openlinksw.com/schemas/CustomerKG/campaign_iri>(ca.campaign_id) <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://demo.openlinksw.com/schemas/CustomerKG/Campaign> as virtrdf:CustomerKG-campaign-type .
    <http://demo.openlinksw.com/schemas/CustomerKG/campaign_iri>(ca.campaign_id) <http://schema.org/name> ca.campaign_name as virtrdf:CustomerKG-campaign-name .
    <http://demo.openlinksw.com/schemas/CustomerKG/campaign_iri>(ca.campaign_id) <http://demo.openlinksw.com/schemas/CustomerKG/goal> ca.goal as virtrdf:CustomerKG-campaign-goal .
    <http://demo.openlinksw.com/schemas/CustomerKG/campaign_iri>(ca.campaign_id) <http://demo.openlinksw.com/schemas/CustomerKG/startDate> ca.start_date as virtrdf:CustomerKG-campaign-start .
    <http://demo.openlinksw.com/schemas/CustomerKG/decision_iri>(de.decision_id) <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://demo.openlinksw.com/schemas/CustomerKG/Decision> as virtrdf:CustomerKG-decision-type .
    <http://demo.openlinksw.com/schemas/CustomerKG/decision_iri>(de.decision_id) <http://demo.openlinksw.com/schemas/CustomerKG/arm> de.arm as virtrdf:CustomerKG-decision-arm .
    <http://demo.openlinksw.com/schemas/CustomerKG/decision_iri>(de.decision_id) <http://demo.openlinksw.com/schemas/CustomerKG/actionKind> de.action_kind as virtrdf:CustomerKG-decision-action .
    <http://demo.openlinksw.com/schemas/CustomerKG/decision_iri>(de.decision_id) <http://demo.openlinksw.com/schemas/CustomerKG/decidedDate> de.decided_date as virtrdf:CustomerKG-decision-date .
    <http://demo.openlinksw.com/schemas/CustomerKG/decision_iri>(de.decision_id) <http://demo.openlinksw.com/schemas/CustomerKG/rationale> de.rationale as virtrdf:CustomerKG-decision-rationale .
    <http://demo.openlinksw.com/schemas/CustomerKG/decision_iri>(de.decision_id) <http://demo.openlinksw.com/schemas/CustomerKG/approvedByHuman> de.approved_by_human as virtrdf:CustomerKG-decision-approved .
    <http://demo.openlinksw.com/schemas/CustomerKG/decision_iri>(de.decision_id) <http://demo.openlinksw.com/schemas/CustomerKG/forCustomer> <http://demo.openlinksw.com/schemas/CustomerKG/customer_iri>(de.customer_id) as virtrdf:CustomerKG-decision-customer .
    <http://demo.openlinksw.com/schemas/CustomerKG/decision_iri>(de.decision_id) <http://demo.openlinksw.com/schemas/CustomerKG/inCampaign> <http://demo.openlinksw.com/schemas/CustomerKG/campaign_iri>(de.campaign_id) as virtrdf:CustomerKG-decision-campaign .
    <http://demo.openlinksw.com/schemas/CustomerKG/decision_iri>(de.decision_id) <http://demo.openlinksw.com/schemas/CustomerKG/usesOffer> <http://demo.openlinksw.com/schemas/CustomerKG/offer_iri>(de.offer_id) where (^{de.}^.offer_id is not null) as virtrdf:CustomerKG-decision-offer .
    <http://demo.openlinksw.com/schemas/CustomerKG/decision_iri>(de.decision_id) <http://demo.openlinksw.com/schemas/CustomerKG/viaChannel> <http://demo.openlinksw.com/schemas/CustomerKG/channel_iri>(de.channel_id) where (^{de.}^.channel_id is not null) as virtrdf:CustomerKG-decision-channel .
    <http://demo.openlinksw.com/schemas/CustomerKG/decision_iri>(dg.decision_id) <http://demo.openlinksw.com/schemas/CustomerKG/constrainedBy> <http://demo.openlinksw.com/schemas/CustomerKG/guardrail_iri>(dg.guardrail_id) as virtrdf:CustomerKG-decision-guardrail .
    <http://demo.openlinksw.com/schemas/CustomerKG/context_iri>(ci.item_id) <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://demo.openlinksw.com/schemas/CustomerKG/ContextItem> as virtrdf:CustomerKG-context-type .
    <http://demo.openlinksw.com/schemas/CustomerKG/context_iri>(ci.item_id) <http://demo.openlinksw.com/schemas/CustomerKG/contextType> ci.context_type as virtrdf:CustomerKG-context-kind .
    <http://demo.openlinksw.com/schemas/CustomerKG/context_iri>(ci.item_id) <http://demo.openlinksw.com/schemas/CustomerKG/contextKey> ci.context_key as virtrdf:CustomerKG-context-key .
    <http://demo.openlinksw.com/schemas/CustomerKG/context_iri>(ci.item_id) <http://demo.openlinksw.com/schemas/CustomerKG/contextValue> ci.context_value as virtrdf:CustomerKG-context-value .
    <http://demo.openlinksw.com/schemas/CustomerKG/context_iri>(ci.item_id) <http://demo.openlinksw.com/schemas/CustomerKG/informs> <http://demo.openlinksw.com/schemas/CustomerKG/decision_iri>(ci.decision_id) as virtrdf:CustomerKG-context-decision .
    <http://demo.openlinksw.com/schemas/CustomerKG/outcome_iri>(ou.outcome_id) <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://demo.openlinksw.com/schemas/CustomerKG/Outcome> as virtrdf:CustomerKG-outcome-type .
    <http://demo.openlinksw.com/schemas/CustomerKG/outcome_iri>(ou.outcome_id) <http://demo.openlinksw.com/schemas/CustomerKG/ordered> ou.ordered as virtrdf:CustomerKG-outcome-ordered .
    <http://demo.openlinksw.com/schemas/CustomerKG/outcome_iri>(ou.outcome_id) <http://demo.openlinksw.com/schemas/CustomerKG/revenueUsd> ou.revenue_usd as virtrdf:CustomerKG-outcome-revenue .
    <http://demo.openlinksw.com/schemas/CustomerKG/outcome_iri>(ou.outcome_id) <http://demo.openlinksw.com/schemas/CustomerKG/marginUsd> ou.margin_usd as virtrdf:CustomerKG-outcome-margin .
    <http://demo.openlinksw.com/schemas/CustomerKG/outcome_iri>(ou.outcome_id) <http://demo.openlinksw.com/schemas/CustomerKG/mediaCostUsd> ou.media_cost_usd as virtrdf:CustomerKG-outcome-media .
    <http://demo.openlinksw.com/schemas/CustomerKG/outcome_iri>(ou.outcome_id) <http://demo.openlinksw.com/schemas/CustomerKG/discountCostUsd> ou.discount_cost_usd as virtrdf:CustomerKG-outcome-discount .
    <http://demo.openlinksw.com/schemas/CustomerKG/outcome_iri>(ou.outcome_id) <http://demo.openlinksw.com/schemas/CustomerKG/repeatVisit30d> ou.repeat_visit_30d as virtrdf:CustomerKG-outcome-repeat .
    <http://demo.openlinksw.com/schemas/CustomerKG/outcome_iri>(ou.outcome_id) <http://demo.openlinksw.com/schemas/CustomerKG/windowEnd> ou.window_end as virtrdf:CustomerKG-outcome-window .
    <http://demo.openlinksw.com/schemas/CustomerKG/outcome_iri>(ou.outcome_id) <http://demo.openlinksw.com/schemas/CustomerKG/ofDecision> <http://demo.openlinksw.com/schemas/CustomerKG/decision_iri>(ou.decision_id) as virtrdf:CustomerKG-outcome-decision .
  }
} ;

The identity graph: reconciliation by reasoning

SPARQL

A small physical graph for identity reconciliation. A vocabulary declares ckg:emailHash an owl:InverseFunctionalProperty, a rule set is built from it, customer facts are derived from the live view with SPARQL INSERT ... WHERE, and records from other systems are added: two that share a customer's email hash, one linked by an explicit owl:sameAs, and one that matches nobody. It is separate from the view because owl:sameAs merging works over physical graphs. Query 3 reads it.

-- 1. Vocabulary: declare the identifying property inverse functional, in its own graph
DB.DBA.TTLP(file_to_string_output('customerkg-identity-vocabulary-claude_sonnet_5_5-1.ttl'), '', 'http://demo.openlinksw.com/schemas/CustomerKG/identity/');
-- The Turtle loaded by that call:
@prefix ckg: <http://demo.openlinksw.com/schemas/CustomerKG/> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix schema: <http://schema.org/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

<http://demo.openlinksw.com/schemas/CustomerKG/identity/> a owl:Ontology ;
    rdfs:label "CustomerKG identity reconciliation vocabulary"@en .

ckg:emailHash a owl:InverseFunctionalProperty, owl:DatatypeProperty ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/identity/> ;
    rdfs:label "email hash"@en ;
    rdfs:comment "A pseudonymous hash of an email address. Two records that carry the same hash describe the same person, so the property is declared inverse functional and a reasoner derives owl:sameAs between them."@en ;
    rdfs:domain schema:Person ;
    rdfs:range xsd:string .

-- 2. Rule set: derive reasoning rules from that vocabulary
DB.DBA.rdfs_rule_set('urn:customerkg:identity-inference', 'http://demo.openlinksw.com/schemas/CustomerKG/identity/');

-- 3. Identity graph, part one: facts derived from the live RDF View (nothing is retyped)
SPARQL PREFIX ckg: <http://demo.openlinksw.com/schemas/CustomerKG/> PREFIX schema: <http://schema.org/>
INSERT INTO GRAPH <http://demo.openlinksw.com/CustomerKG/identity#> { ?c ckg:emailHash ?v }
WHERE { GRAPH <http://demo.openlinksw.com/CustomerKG#> { ?s a ckg:IdentitySignal ; ckg:signalKind "email" ; ckg:signalValue ?v ; ckg:resolvesTo ?c } };

SPARQL PREFIX ckg: <http://demo.openlinksw.com/schemas/CustomerKG/> PREFIX schema: <http://schema.org/>
INSERT INTO GRAPH <http://demo.openlinksw.com/CustomerKG/identity#> { ?c a schema:Person ; schema:name ?n }
WHERE { GRAPH <http://demo.openlinksw.com/CustomerKG#> { ?c a ckg:Customer ; schema:name ?n } };

-- 4. Identity graph, part two: records from other systems, one explicit owl:sameAs, one record that matches nobody
SPARQL PREFIX ckg: <http://demo.openlinksw.com/schemas/CustomerKG/> PREFIX schema: <http://schema.org/> PREFIX owl: <http://www.w3.org/2002/07/owl#>
INSERT DATA INTO GRAPH <http://demo.openlinksw.com/CustomerKG/identity#> {
  <http://demo.openlinksw.com/CustomerKG/external/crm/501#this> a schema:Person ; schema:name "A. Quinn (CRM export)" ; ckg:emailHash "hem:a1f3" .
  <http://demo.openlinksw.com/CustomerKG/external/app/77#this> a schema:Person ; schema:name "Avery Q. (app profile)" ; ckg:emailHash "hem:a1f3" .
  <http://demo.openlinksw.com/CustomerKG/external/crm/502#this> a schema:Person ; schema:name "B. Moreno (CRM export)" ; ckg:emailHash "hem:b2c4" .
  <http://demo.openlinksw.com/CustomerKG/external/crm/599#this> a schema:Person ; schema:name "Unmatched (CRM export)" ; ckg:emailHash "hem:ffff" .
  <http://demo.openlinksw.com/CustomerKG/external/loyalty/LY-10001#this> a schema:Person ; schema:name "Loyalty member LY-10001" ; owl:sameAs <http://demo.openlinksw.com/CustomerKG/customer/1#this> .
};

3. Linked Data deployment: URL rewrite rules

1 card

Without these, every entity IRI returns 404. They were generated by RDFVIEW_GENERATE_DATA_RULES and run in the same pass as the view.

URL rewrite rules: making the entity IRIs resolve

SQL

The RDF View defines the IRIs but does not by itself make them resolve. The generator RDFVIEW_GENERATE_DATA_RULES was called with iri_path_segment CustomerKG and include_ontology_rules 1 and returned these fourteen statements: six regular-expression rules, two rule lists and two virtual directories, /CustomerKG and /schemas/CustomerKG. After they ran, an entity IRI answers with a 303 to a describe page.

DB.DBA.URLREWRITE_CREATE_REGEX_RULE (
'customerkg_rule2',
1,
'(/[^#]*)',
vector('path'),
1,
'/sparql?query=DESCRIBE+%%3Chttp%%3A//^{URIQADefaultHost}^%U%%23this%%3E+FROM+%%3Chttp%%3A//^{URIQADefaultHost}^/CustomerKG%%23%%3E&format=%U',
vector('path', '*accept*'),
null,
'(text/rdf.n3)|(application/rdf.xml)|(text/n3)|(application/json)|(text/turtle)',
2,
null
);
DB.DBA.URLREWRITE_CREATE_REGEX_RULE (
'customerkg_rule4',
1,
'/CustomerKG/stat([^#]*)',
vector('path'),
1,
'/sparql?query=DESCRIBE+%%3Chttp%%3A//^{URIQADefaultHost}^/CustomerKG/stat%%23%%3E+%%3Fo+FROM+%%3Chttp%%3A//^{URIQADefaultHost}^/CustomerKG%%23%%3E+WHERE+{+%%3Chttp%%3A//^{URIQADefaultHost}^/CustomerKG/stat%%23%%3E+%%3Fp+%%3Fo+}&format=%U',
vector('*accept*'),
null,
'(text/rdf.n3)|(application/rdf.xml)|(text/n3)|(application/json)|(text/turtle)',
2,
null
);
DB.DBA.URLREWRITE_CREATE_REGEX_RULE (
'customerkg_rule6',
1,
'/CustomerKG/objects/([^#]*)',
vector('path'),
1,
'/sparql?query=DESCRIBE+%%3Chttp%%3A//^{URIQADefaultHost}^/CustomerKG/objects/%U%%3E+FROM+%%3Chttp%%3A//^{URIQADefaultHost}^/CustomerKG%%23%%3E&format=%U',
vector('path', '*accept*'),
null,
'(text/rdf.n3)|(application/rdf.xml)|(text/n3)|(application/json)|(text/turtle)',
2,
null
);
DB.DBA.URLREWRITE_CREATE_REGEX_RULE (
'customerkg_rule1',
1,
'([^#]*)',
vector('path'),
1,
'/describe/?url=http%%3A//^{URIQADefaultHost}^%U%%23this&graph=http%%3A//^{URIQADefaultHost}^/CustomerKG%%23&distinct=0',
vector('path'),
null,
null,
2,
303
);
DB.DBA.URLREWRITE_CREATE_REGEX_RULE (
'customerkg_rule7',
1,
'/CustomerKG/stat([^#]*)',
vector('path'),
1,
'/describe/?url=http%%3A//^{URIQADefaultHost}^/CustomerKG/stat%%23&graph=http%%3A//^{URIQADefaultHost}^/CustomerKG%%23',
vector('path'),
null,
null,
2,
303
);
DB.DBA.URLREWRITE_CREATE_REGEX_RULE (
'customerkg_rule5',
1,
'/CustomerKG/objects/(.*)',
vector('path'),
1,
'/services/rdf/object.binary?path=%%2FCustomerKG%%2Fobjects%%2F%U&accept=%U',
vector('path', '*accept*'),
null,
null,
2,
null
);
DB.DBA.URLREWRITE_CREATE_RULELIST ( 'customerkg_rule_list1', 1, vector ( 'customerkg_rule1', 'customerkg_rule7', 'customerkg_rule5', 'customerkg_rule2', 'customerkg_rule4', 'customerkg_rule6'));
DB.DBA.VHOST_REMOVE (lpath=>'/CustomerKG');
DB.DBA.VHOST_DEFINE (lpath=>'/CustomerKG', ppath=>'/', vsp_user=>'dba', is_dav=>0,
is_brws=>0, opts=>vector ('url_rewrite', 'customerkg_rule_list1')
);
DB.DBA.URLREWRITE_CREATE_REGEX_RULE (
'customerkg_owl_rule2',
1,
'(/[^#]*)',
vector('path'),
1,
'/sparql?query=DESCRIBE+%%3Chttp%%3A//^{URIQADefaultHost}^%U%%3E+FROM+%%3Chttp%%3A//^{URIQADefaultHost}^/schemas/CustomerKG%%23%%3E&format=%U',
vector('path', '*accept*'),
null,
'(text/rdf.n3)|(application/rdf.xml)|(text/n3)|(application/json)|(text/turtle)',
2,
null
);
DB.DBA.URLREWRITE_CREATE_REGEX_RULE (
'customerkg_owl_rule1',
1,
'([^#]*)',
vector('path'),
1,
'/describe/?url=http://^{URIQADefaultHost}^%U',
vector('path'),
null,
null,
2,
303
);
DB.DBA.URLREWRITE_CREATE_RULELIST ( 'customerkg_owl_rule_list1', 1, vector ( 'customerkg_owl_rule1', 'customerkg_owl_rule2'));
DB.DBA.VHOST_REMOVE (lpath=>'/schemas/CustomerKG');
DB.DBA.VHOST_DEFINE (lpath=>'/schemas/CustomerKG', ppath=>'/', vsp_user=>'dba', is_dav=>0,
is_brws=>0, opts=>vector ('url_rewrite', 'customerkg_owl_rule_list1')
);

4. Instance data interactions, by query language

3 groups

The SPARQL links run anonymously against the public endpoint. The Query Builder links run SQL, SPARQL and SPASQL alike but need a signed-in session, so the SPASQL and SQL queries, and the identity reconciliation query, have no anonymous link: authenticate to demo.openlinksw.com with the account demo or vdb, where the user id and the password are identical. The instance is protected by ABAC ACLs. Every query that returns entities leads with the entity's IRI.

SPARQL
7 cards

Queries 1 to 7 against the RDF View and the identity graph, including reasoning-based identity reconciliation, the activation queue, entailment and a property hierarchy read from the TBox. Query 3 needs a signed-in session.

The decision ledger

Query 1 · SPARQL

Every decision with its customer, campaign, experiment arm, action, offer, channel and the order that followed. Seven patterns and two OPTIONALs walk decision, customer, campaign, offer, channel and outcome, and the decision IRI leads every row so each row is a key that resolves.

PREFIX ckg: <http://demo.openlinksw.com/schemas/CustomerKG/>
PREFIX schema: <http://schema.org/>
SELECT ?decisionIri ?customer ?campaign ?arm ?action ?offer ?channel ?ordered ?revenueUsd
FROM <http://demo.openlinksw.com/CustomerKG#>
WHERE {
  ?decisionIri a ckg:Decision ; ckg:forCustomer ?customerIri ; ckg:inCampaign ?campaignIri ;
               ckg:arm ?arm ; ckg:actionKind ?action .
  ?customerIri schema:name ?customer .
  ?campaignIri schema:name ?campaign .
  OPTIONAL { ?decisionIri ckg:usesOffer ?offerIri . ?offerIri schema:name ?offer }
  OPTIONAL { ?decisionIri ckg:viaChannel ?channelIri . ?channelIri schema:name ?channel }
  ?outcomeIri ckg:ofDecision ?decisionIri ; ckg:ordered ?ordered ; ckg:revenueUsd ?revenueUsd .
}
ORDER BY ?campaign ?arm ?customer

Verified result: 20 rows: 14 decisions in the reactivation campaign (9 treatment, 5 holdout) and 6 in the discount test (4 treatment, 2 holdout). Casey Lindholm received the family bundle by app push and ordered 48 dollars; holdout customer Jules Barrow was never contacted and ordered 25.

Identity signals by match method

Query 2 · SPARQL

How many signals each matching method produced, how many resolved to a customer and the average confidence, with one example signal IRI per group. Unmatched pseudonymous signals have no resolvesTo edge, which the OPTIONAL keeps visible.

PREFIX ckg: <http://demo.openlinksw.com/schemas/CustomerKG/>
PREFIX schema: <http://schema.org/>
SELECT ?matchMethod (SAMPLE(?signalIri) AS ?exampleSignalIri) (COUNT(?signalIri) AS ?signals) (COUNT(?customerIri) AS ?resolved) (ROUND(AVG(?confidence)) AS ?avgConfidence)
FROM <http://demo.openlinksw.com/CustomerKG#>
WHERE {
  ?signalIri a ckg:IdentitySignal ; ckg:matchMethod ?matchMethod ; ckg:confidence ?confidence .
  OPTIONAL { ?signalIri ckg:resolvesTo ?customerIri }
}
GROUP BY ?matchMethod
ORDER BY DESC(?signals)

Verified result: 4 rows: deterministic 22 signals, all resolved, confidence 100; probabilistic 7, all resolved, 79; agentic 3, all resolved, 91; unmatched 4, none resolved.

Identity reconciliation by reasoning: one person, several records

Query 3 · SPARQL

Reads the identity graph, which holds customer facts derived from the live view plus CRM, app and loyalty records. Each branch asks for the names of one customer. With no pragmas the answer is one name per customer; with input:same-as an explicit owl:sameAs adds the loyalty record; with the rule set that declares ckg:emailHash inverse functional, the CRM export and the app profile that share a hash join implicitly. Removing the pragmas shows the contrast.

DEFINE input:inference "urn:customerkg:identity-inference"
DEFINE input:same-as "yes"
PREFIX schema: <http://schema.org/>
SELECT DISTINCT ?personIri ?name
FROM <http://demo.openlinksw.com/CustomerKG/identity#>
WHERE {
  { <http://demo.openlinksw.com/CustomerKG/customer/1#this> schema:name ?name . BIND(<http://demo.openlinksw.com/CustomerKG/customer/1#this> AS ?personIri) } UNION { <http://demo.openlinksw.com/CustomerKG/customer/2#this> schema:name ?name . BIND(<http://demo.openlinksw.com/CustomerKG/customer/2#this> AS ?personIri) } UNION { <http://demo.openlinksw.com/CustomerKG/customer/3#this> schema:name ?name . BIND(<http://demo.openlinksw.com/CustomerKG/customer/3#this> AS ?personIri) }
}

Verified result: 7 rows with both pragmas: customer 1, Avery Quinn, under four names (her own, the CRM export, the app profile and the loyalty member) and customer 2, Blake Moreno, under two, while customer 3, Casey Lindholm, has no matching records and keeps one. With input:same-as alone there are 4 rows, the loyalty record being the only addition, and with neither there are 3. The CRM record that matches nobody never joins a customer.

Worth knowing: Signed-in only: the same query without DISTINCT returned the 100,000-row cap of repeated rows on the anonymous HTTP endpoint, and the DISTINCT form did not return within the five-minute limit, so only the signed-in route, the XMLA connection the Query Builder uses, is verified. owl:sameAs merging works over physical graphs, which is why the customer facts are copied into the identity graph from the view rather than joined to it live.

Sign in first: Needs a signed-in session. Authenticate to demo.openlinksw.com with the account demo or vdb, where the user id and the password are identical. The instance is protected by attribute-based access control (ABAC) ACLs.

Human-approval audit

Query 4 · SPARQL

Which decisions were constrained by a human-approval guardrail, what the offer's discount was, the rule's threshold, and whether a person approved. Decisions reach guardrails through the decision_guardrail link table, which is mapped as the constrainedBy predicate.

PREFIX ckg: <http://demo.openlinksw.com/schemas/CustomerKG/>
PREFIX schema: <http://schema.org/>
SELECT ?decisionIri ?customer ?offer ?discountPct ?rule ?limitValue ?approvedByHuman
FROM <http://demo.openlinksw.com/CustomerKG#>
WHERE {
  ?decisionIri ckg:constrainedBy ?guardrailIri ; ckg:forCustomer ?customerIri ; ckg:usesOffer ?offerIri ; ckg:approvedByHuman ?approvedByHuman .
  ?guardrailIri ckg:ruleKind "human-approval" ; schema:name ?rule ; ckg:limitValue ?limitValue .
  ?customerIri schema:name ?customer .
  ?offerIri schema:name ?offer ; ckg:discountPct ?discountPct .
}

Verified result: 4 rows: decisions 3 and 9 (Casey Lindholm and Indigo Raman), each on the 20 percent family bundle and each constrained by both the above-15-percent rule and the bundle rule, approvedByHuman 1 on all four.

Worth knowing: Adding ORDER BY to this query raised SQ142 on this build, so it carries none.

The activation queue: consent-checked messages ready to send

Query 5 · SPARQL

Which message goes to whom, on which channel, under which action, with the consent record that allows it. A decision appears only when its customer has a granted consent record for the channel the decision uses, so suppressions, holdouts and any unconsented contact are excluded by the join. It is the handoff a delivery tool would consume; nothing is sent.

PREFIX ckg: <http://demo.openlinksw.com/schemas/CustomerKG/>
PREFIX schema: <http://schema.org/>
SELECT ?decisionIri ?customer ?channel ?channelKind ?offer ?actionKind ?consentIri
FROM <http://demo.openlinksw.com/CustomerKG#>
WHERE {
  ?decisionIri a ckg:Decision ; ckg:forCustomer ?custIri ; ckg:viaChannel ?chanIri ; ckg:actionKind ?actionKind .
  ?custIri schema:name ?customer .
  ?chanIri schema:name ?channel ; ckg:channelKind ?channelKind .
  ?consentIri a ckg:ConsentRecord ; ckg:forCustomer ?custIri ; ckg:forChannel ?chanIri ; ckg:consentStatus "granted" .
  OPTIONAL { ?decisionIri ckg:usesOffer ?offerIri . ?offerIri schema:name ?offer }
}

Verified result: 12 rows: 10 on owned channels (app push, email and SMS) and 2 on paid channels (display retargeting and paid social), each led by the decision IRI and carrying the IRI of the consent record that allows it. The 8 decisions that are suppressions or holdouts do not appear.

Worth knowing: Delivery was not run: the queue shows what would be sent and that consent allows it, not that anything was sent.

Entailment: everything that is a control

Query 6 · SPARQL

Nothing in the data is typed ControlElement; guardrails are typed Guardrail and consent records ConsentRecord. The TBox says both are subclasses of ControlElement, and one DEFINE input:inference pragma activates the rule set derived from it.

DEFINE input:inference "urn:customerkg:inference"
PREFIX ckg: <http://demo.openlinksw.com/schemas/CustomerKG/>
PREFIX schema: <http://schema.org/>
SELECT DISTINCT ?controlIri ?ruleKind ?consentStatus
FROM <http://demo.openlinksw.com/CustomerKG#>
WHERE {
  ?controlIri a ckg:ControlElement .
  OPTIONAL { ?controlIri ckg:ruleKind ?ruleKind }
  OPTIONAL { ?controlIri ckg:consentStatus ?consentStatus }
}

Verified result: 43 rows: the 5 guardrails and the 38 consent records. Without the pragma the same query returns 0.

Worth knowing: Under the pragma the same pattern without DISTINCT returned 68 rows for 5 guardrails, so SELECT DISTINCT is required here; a variable class in the pattern raised SQ200 and COUNT raised SQ142, so the query lists members instead of counting them.

The property hierarchy, read from the TBox

Query 7 · SPARQL

Five properties are declared sub-properties of groundedIn. The query joins the TBox graph in explicitly, finds every sub-property used on a decision, and counts the links, as the earlier ScaleKG estate required.

PREFIX ckg: <http://demo.openlinksw.com/schemas/CustomerKG/>
PREFIX schema: <http://schema.org/>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT ?relation (COUNT(DISTINCT ?decisionIri) AS ?decisions) (COUNT(*) AS ?links) (SAMPLE(?decisionIri) AS ?exampleDecisionIri)
FROM <http://demo.openlinksw.com/CustomerKG#>
FROM <http://demo.openlinksw.com/schemas/CustomerKG/>
WHERE {
  ?decisionIri a ckg:Decision ; ?relationIri ?targetIri .
  ?relationIri rdfs:subPropertyOf ckg:groundedIn ; rdfs:label ?relation .
}
GROUP BY ?relation
ORDER BY DESC(?links)

Verified result: 5 rows: constrained by 52 links over 20 decisions, in campaign 20, for customer 20, via channel 12 and uses offer 10.

SPASQL
2 cards

Queries 8 and 9: a SPARQL query nested in a SQL SELECT, so graph results meet native tables and SQL arithmetic. Sign in as demo or vdb first (user id and password identical; the instance is protected by ABAC ACLs).

Profit per decision: SQL arithmetic over graph rows

Query 8 · SPASQL

A SPARQL query nested in a SQL SELECT. The graph side returns each decision's IRI, customer, arm and its margin, media cost and discount cost; the SQL side computes profit as margin minus media minus discount and sorts by it.

SELECT d, customer, arm, margin_usd, media_usd, discount_usd, margin_usd - media_usd - discount_usd AS profit_usd
FROM (SPARQL
  PREFIX ckg: <http://demo.openlinksw.com/schemas/CustomerKG/>
  PREFIX schema: <http://schema.org/>
  SELECT ?d ?customer ?arm ?margin_usd ?media_usd ?discount_usd
  FROM <http://demo.openlinksw.com/CustomerKG#>
  WHERE {
    ?d a ckg:Decision ; ckg:forCustomer ?cust ; ckg:arm ?arm .
    ?cust schema:name ?customer .
    ?o ckg:ofDecision ?d ; ckg:marginUsd ?margin_usd ; ckg:mediaCostUsd ?media_usd ; ckg:discountCostUsd ?discount_usd .
  }
) AS x
ORDER BY profit_usd DESC, customer

Verified result: 20 rows, each led by the decision IRI, from a profit of 9 dollars (Avery Quinn, Jules Barrow and Sage Whitlock, the last two in holdout arms) down to minus 2 for each of the two paid-channel switches, which cost media and produced no order.

Worth knowing: Wider nested queries and SQL aggregates over a SPARQL derived table raised SQ142 on this build; this five-variable form with row-level arithmetic ran cleanly.

Sign in first: Needs a signed-in session. Authenticate to demo.openlinksw.com with the account demo or vdb, where the user id and the password are identical. The instance is protected by attribute-based access control (ABAC) ACLs.

Withdrawn consent joined to native decisions

Query 9 · SPASQL

SPARQL finds consent records whose status is withdrawn, with the customer and channel names; SQL joins those rows to the native customer, channel and decision tables and counts decisions that used the withdrawn channel. The consent IRI leads each row.

SELECT x.consent, x.customer_name, x.channel_name, COUNT(d.decision_id) AS contacts_after_withdrawal
FROM (SPARQL
  PREFIX ckg: <http://demo.openlinksw.com/schemas/CustomerKG/>
  PREFIX schema: <http://schema.org/>
  SELECT ?consent ?customer_name ?channel_name
  FROM <http://demo.openlinksw.com/CustomerKG#>
  WHERE {
    ?consent a ckg:ConsentRecord ; ckg:consentStatus "withdrawn" ; ckg:forCustomer ?cust ; ckg:forChannel ?chan .
    ?cust schema:name ?customer_name .
    ?chan schema:name ?channel_name .
  }
) AS x
JOIN CustomerKG.kidehen.customer cu ON cu.full_name = x.customer_name
JOIN CustomerKG.kidehen.channel ch ON ch.channel_name = x.channel_name
LEFT JOIN CustomerKG.kidehen.decision d ON d.customer_id = cu.customer_id AND d.channel_id = ch.channel_id
GROUP BY x.consent, x.customer_name, x.channel_name
ORDER BY x.customer_name

Verified result: 3 rows, each with 0 contacts after withdrawal: Harper Doyle on app push, Kendall Osei on email and Noel Ferreira on email. The graph supplies the rule, the table supplies the evidence.

Sign in first: Needs a signed-in session. Authenticate to demo.openlinksw.com with the account demo or vdb, where the user id and the password are identical. The instance is protected by attribute-based access control (ABAC) ACLs.

SQL
3 cards

Queries 10 to 12: plain SQL over the same tables, and the SQL Virtuoso compiles a SPARQL query into. Same sign-in as the SPASQL queries: demo or vdb, user id and password identical.

Incrementality: treated against holdout

Query 10 · SQL

Plain SQL over the native tables: per campaign, the order rate and profit contribution of the treated arm against the randomised holdout, and the profit the treated customers added beyond what holdout behaviour predicts.

SELECT ca.campaign_id, ca.campaign_name,
       SUM(CASE WHEN de.arm = 'treatment' THEN 1 ELSE 0 END) AS treated,
       SUM(CASE WHEN de.arm = 'holdout' THEN 1 ELSE 0 END) AS held_out,
       SUM(CASE WHEN de.arm = 'treatment' THEN ou.ordered ELSE 0 END) * 100 / SUM(CASE WHEN de.arm = 'treatment' THEN 1 ELSE 0 END) AS treated_rate_pct,
       SUM(CASE WHEN de.arm = 'holdout' THEN ou.ordered ELSE 0 END) * 100 / SUM(CASE WHEN de.arm = 'holdout' THEN 1 ELSE 0 END) AS holdout_rate_pct,
       SUM(CASE WHEN de.arm = 'treatment' THEN ou.margin_usd - ou.media_cost_usd - ou.discount_cost_usd ELSE 0 END) AS treated_profit_usd,
       SUM(CASE WHEN de.arm = 'holdout' THEN ou.margin_usd - ou.media_cost_usd - ou.discount_cost_usd ELSE 0 END) AS holdout_profit_usd,
       ROUND(SUM(CASE WHEN de.arm = 'treatment' THEN ou.margin_usd - ou.media_cost_usd - ou.discount_cost_usd ELSE 0 END)
             - SUM(CASE WHEN de.arm = 'treatment' THEN 1 ELSE 0 END)
               * CAST(SUM(CASE WHEN de.arm = 'holdout' THEN ou.margin_usd - ou.media_cost_usd - ou.discount_cost_usd ELSE 0 END) AS DOUBLE PRECISION)
               / SUM(CASE WHEN de.arm = 'holdout' THEN 1 ELSE 0 END), 1) AS incremental_profit_usd
FROM CustomerKG.kidehen.decision de
JOIN CustomerKG.kidehen.outcome ou ON ou.decision_id = de.decision_id
JOIN CustomerKG.kidehen.campaign ca ON ca.campaign_id = de.campaign_id
GROUP BY ca.campaign_id, ca.campaign_name
ORDER BY ca.campaign_id

Verified result: 2 rows. Reactivation: treated order rate 66 percent against 40 (a 26 point lift), treated profit 35 dollars against 17 for the holdout, incremental profit about 4. Discount test: treated 75 percent against 100, incremental profit minus 15 dollars, so the incentive subsidised orders.

Worth knowing: Order rates use integer division, so 6 of 9 shows as 66. Incremental profit is the treated profit minus the treated count times the holdout's profit per customer, which is 4.4 for the first campaign by hand.

Sign in first: Needs a signed-in session. Authenticate to demo.openlinksw.com with the account demo or vdb, where the user id and the password are identical. The instance is protected by attribute-based access control (ABAC) ACLs.

Contribution by lever

Query 11 · SQL

Plain SQL: decisions, orders, revenue, media cost, discount cost and profit grouped by whether the decision used an owned channel, a paid channel or none, which is the article's contribution-by-lever question in miniature.

SELECT COALESCE(ch.channel_kind, 'none') AS lever,
       COUNT(*) AS decisions,
       SUM(ou.ordered) AS orders,
       SUM(ou.revenue_usd) AS revenue_usd,
       SUM(ou.media_cost_usd) AS media_usd,
       SUM(ou.discount_cost_usd) AS discount_usd,
       SUM(ou.margin_usd - ou.media_cost_usd - ou.discount_cost_usd) AS profit_usd
FROM CustomerKG.kidehen.decision de
JOIN CustomerKG.kidehen.outcome ou ON ou.decision_id = de.decision_id
LEFT JOIN CustomerKG.kidehen.channel ch ON ch.channel_id = de.channel_id
GROUP BY COALESCE(ch.channel_kind, 'none')
ORDER BY revenue_usd DESC

Verified result: 3 rows: owned 10 decisions, 9 orders, 260 dollars revenue, profit 58; none (suppressions and holdouts) 8 decisions, 4 orders, 92 dollars, profit 34; paid 2 decisions, no orders, profit minus 4.

Sign in first: Needs a signed-in session. Authenticate to demo.openlinksw.com with the account demo or vdb, where the user id and the password are identical. The instance is protected by attribute-based access control (ABAC) ACLs.

The SQL Virtuoso compiles a SPARQL query into

Query 12 · SQL

sparql_to_sql_text returns the SQL Virtuoso generates for a SPARQL query, so the translation can be read rather than trusted. length() and subseq() keep the transfer small.

SELECT length(sparql_to_sql_text('SPARQL PREFIX ckg: <http://demo.openlinksw.com/schemas/CustomerKG/> PREFIX schema: <http://schema.org/> SELECT ?customer ?lifecycle FROM <http://demo.openlinksw.com/CustomerKG#> WHERE { ?c a ckg:Customer ; schema:name ?customer ; ckg:lifecycleStage ?lifecycle }')) AS sql_length,
       subseq(sparql_to_sql_text('SPARQL PREFIX ckg: <http://demo.openlinksw.com/schemas/CustomerKG/> PREFIX schema: <http://schema.org/> SELECT ?customer ?lifecycle FROM <http://demo.openlinksw.com/CustomerKG#> WHERE { ?c a ckg:Customer ; schema:name ?customer ; ckg:lifecycleStage ?lifecycle }'), 0, 1500) AS sql_head

Verified result: One row: the generated SQL is 20,204 characters long and begins with a __ro2sq conversion over the customer table's mapped columns.

Worth knowing: The generated SQL is large because the compiler considers every quad map that could supply each pattern and prunes with filters; the answer is still computed from the live rows.

Sign in first: Needs a signed-in session. Authenticate to demo.openlinksw.com with the account demo or vdb, where the user id and the password are identical. The instance is protected by attribute-based access control (ABAC) ACLs.

Server 2

URIBurner

Holds none of the CustomerKG data. These two SPARQL queries run on URIBurner's endpoint and use SERVICE to read the view on demo at query time, so the answer crosses two instances and every IRI is a key that resolves on demo.

Instance data interactions, SPARQL (federated)
2 cards

Queries 13 and 14. Anonymous on the public endpoint; URIBurner can take about 30 seconds to answer.

Class counts across instances: URIBurner asks demo

Query 13 · SPARQL

Run on URIBurner's SPARQL endpoint. A SERVICE call asks demo.openlinksw.com for the number of entities per class in the CustomerKG view. URIBurner holds no CustomerKG data, so every number is fetched live from demo at query time, and each class IRI is a resolvable key.

SELECT ?classIri ?entities
WHERE {
  SERVICE <https://demo.openlinksw.com/sparql> {
    SELECT ?classIri (COUNT(DISTINCT ?entityIri) AS ?entities)
    FROM <http://demo.openlinksw.com/CustomerKG#>
    WHERE { ?entityIri a ?classIri }
    GROUP BY ?classIri
  }
}
ORDER BY DESC(?entities)

Verified result: 11 rows, one per class: ConsentRecord 38, IdentitySignal 36, ContextItem, Customer, Decision and Outcome 20 each, Channel, Product, Offer and Guardrail 5 each, Campaign 2. They match the class counts read directly on demo.

Worth knowing: Slow on the shared servers, about 30 seconds, and the first attempt returned a transaction deadlock in iri_to_id that cleared on the second try.

The four kinds of context behind one decision, fetched across instances

Query 14 · SPARQL

Run on URIBurner. The SERVICE call returns the context items attached to decision 3 on demo, one per kind of context, each led by the context item's IRI. It is the article's four kinds of context read by an agent that does not host the data.

PREFIX ckg: <http://demo.openlinksw.com/schemas/CustomerKG/>
SELECT ?itemIri ?contextType ?contextKey ?contextValue
WHERE {
  SERVICE <https://demo.openlinksw.com/sparql> {
    SELECT ?itemIri ?contextType ?contextKey ?contextValue
    FROM <http://demo.openlinksw.com/CustomerKG#>
    WHERE {
      ?itemIri a ckg:ContextItem ; ckg:informs <http://demo.openlinksw.com/CustomerKG/decision/3#this> ;
               ckg:contextType ?contextType ; ckg:contextKey ?contextKey ; ckg:contextValue ?contextValue .
    }
  }
}

Verified result: 4 rows for decision 3, Casey Lindholm's family bundle: customer behaviour (browsed a family meal six times without checkout), business margin (31 percent), decision prior offer (10 percent off ignored five weeks ago) and control approval (a 20 percent discount needs human approval: granted).

What the build taught

Findings from deploying it, stated as they happened, including the places Virtuoso did not do what was expected.

Four query shapes raised SQ142 or SQ200 on this build; the working shapes are narrower

ORDER BY over a pattern that touches the link-table predicate constrainedBy raised SQ142; an aggregate SPARQL query with conditional sums by arm raised SQ142 even run directly; a SQL aggregate over a derived SPARQL table raised it too, while the same table read with SELECT star or with five-variable row arithmetic worked; and under the entailment pragma a variable class raised SQ200 and COUNT raised SQ142. The working forms are plain SQL aggregates over the native tables, row-level SPASQL and SELECT DISTINCT lists. Verified by running each shape, not by reading documentation.

Under the entailment pragma a plain pattern returned 68 rows for 5 guardrails

With the rule set active, a query for guardrails with their names returned 68 solutions, because the inference expansion reaches the same quad-map rows by several routes. SELECT DISTINCT returned the correct 5. The same applies to the 43 controls in Query 6, which also needs DISTINCT. Counting under the pragma was not reliable on this build, so the page lists members.

Nullable foreign keys carry an IS NOT NULL guard from the first deployment

Five foreign keys are nullable by design: the customer on an identity signal, the product on an offer, the offer a guardrail applies to, and the offer and channel on a decision. Each mapping has where (^{alias.}^.column is not null), and the deployed view held exactly 1,160 triples, the same number a hand count from the source rows gives. An earlier estate on the same server projected ten spurious triples before the guard was added.

The rewrite rules were installed in the same pass as the view

RDFVIEW_GENERATE_DATA_RULES returned fourteen statements, which ran through SELECT exec(). Afterwards /CustomerKG/customer/3 and /CustomerKG/decision/3 answered with a 303 to the describe page, and so did a term in the ontology namespace. A view without these rules defines IRIs that return 404.

Identity reasoning over the anonymous HTTP endpoint returned 100,000 repeated rows where the signed-in route returned 7

The reconciliation query returned 7 rows in half a second over XMLA, and the same text on the anonymous HTTP endpoint returned the 100,000-row cap of repeats for one customer; with DISTINCT it did not return within five minutes. The page therefore offers Query 3 only through the Query Builder. Separately, owl:sameAs merging did not reach across the RDF View: a join of the view's customers to sameAs-linked records returned nothing, so the customer facts were copied into a physical graph with SPARQL INSERT ... WHERE. Inverse-functional-property reasoning was confirmed by contrast: with only input:same-as a customer had two names, and with the rule set it had four.

The demo server's write path for DDL was SELECT exec() over authenticated XMLA, and it dropped connections under load

The XMLA endpoint rejects non-SELECT statements directly, but SELECT exec('statement') runs DDL and DML as the authenticated user. Each IRI class took tens of seconds to minutes on the shared server, so completion was checked in the virtrdf schema graph. Several reads failed with a closed connection and succeeded on retry, so one failed fetch is not evidence of a broken query.

The Complementary Path

Databricks Tables Attached over ODBC

Path C keeps the lakehouse as the system of record. Virtuoso attaches Databricks tables through an ODBC data source so SQL, SPARQL and RDF Views treat them like local tables, and the same generators used for CustomerKG then produce a quad map, an ontology and rewrite rules over them. The Databricks estate gains resolvable entity IRIs and a SPARQL endpoint without a copy of any row.

1
ODBC data source
databricks_odbc was already registered on demo. This session did not create or change it.
5
Remote tables registered
Attached through that data source, one Bakehouse table and four travel-sample tables.
10
Columns the generator mapped
All of them, including cardNumber, which a real deployment would not publish.
0
Rows queried
Only catalog metadata was read, and the generated scripts were not executed.

Demo already holds an attached Databricks data source, which made the complementary path demonstrable without a new connection

Reading the catalog showed a databricks_odbc data source and ten attached or virtual tables under the databricks qualifier, one of which maps a remote Bakehouse table over ODBC. This session read column metadata and generated scripts from it, and did not query its rows or change it. The generator mapped all ten columns, including cardNumber, as predicates.

What is executed and what is only generated

Everything on the CustomerKG side was executed. On this side the cards are configuration shape, an attach statement shape, two catalog reads, and generator output for the existing attachment, all on demo.openlinksw.com and grouped below by function. The generator output is shown verbatim and was not run, because running it would publish an attached table's ten columns, a card number among them, as Linked Data. That is the review step the complementary path always needs: choose the columns, then generate.

Connection configuration

1 card

The ODBC data source for a Databricks SQL warehouse, as an odbc.ini shape with placeholders.

The ODBC data source for a Databricks SQL warehouse

Config

The shape of a DSN for the Databricks ODBC driver. Placeholders stand where the workspace host, HTTP path and token go; the token belongs in a credential store and never in a file that is committed. Not executed in this session; the existing databricks_odbc data source on demo was not modified.

; odbc.ini (iODBC or unixODBC). Placeholders in angle brackets; keep the token in a credential store, not in this file.
[databricks_odbc]
Driver  = <path to the Databricks ODBC driver library>
Host    = <workspace-host>
Port    = 443
HTTPPath= <SQL warehouse HTTP path>
SSL     = 1
ThriftTransport = 2
AuthMech= 3
UID     = token
PWD     = <personal access token>

DDL: attach the table

1 card

The documented ATTACH TABLE form that registers the remote table in Virtuoso.

Attach the Databricks table to Virtuoso

SQL

The documented form of ATTACH TABLE: the remote name, an optional primary key, a local name and the data source. Once attached, the table answers SQL, appears in the schema browser and can be the subject of an RDF View. Shown as the shape of the statement; this session did not attach new tables.

-- Shape of the statement: remote name, key, local name, data source.
-- Run in isql or Conductor as an account allowed to register remote tables.
ATTACH TABLE bakehouse.sales_transactions PRIMARY KEY (transactionID)
  AS "databricks"."bakehouse"."sales_transactions_viritual_odbc"
  FROM 'databricks_odbc' ;

-- Then give the SPARQL account read access, as for any other table:
GRANT SELECT ON "databricks"."bakehouse"."sales_transactions_viritual_odbc" TO "SPARQL" ;

Catalog reads (SQL)

1 card

Two read-only queries against Virtuoso's own catalog on demo. They make no call to Databricks.

What demo already holds: metadata read, rows untouched

SQL

Two read-only queries against Virtuoso's own catalog tables, which make no call to Databricks: the remote-table registry that shows databricks_odbc and the remote name behind the attached table, and its ten column names and types.

-- 1. Which data source and remote name sit behind the attached tables (catalog read; no call to Databricks)
SELECT RT_NAME AS local_table, RT_DSN AS data_source, RT_REMOTE_NAME AS remote_name
FROM DB.DBA.SYS_REMOTE_TABLE
WHERE lower(RT_NAME) LIKE 'databricks.%'
ORDER BY 1;

-- 2. The ten columns of the attached Bakehouse table (catalog read; no call to Databricks)
SELECT "COLUMN" AS column_name, COL_DTP AS type_code, COL_PREC AS precision
FROM DB.DBA.SYS_COLS
WHERE "TABLE" = 'databricks.bakehouse.sales_transactions_viritual_odbc'
ORDER BY COL_ID;

RDF View generation

1 card

The view and IRI class the generator proposes for the attached table, with the privacy caveat.

The RDF View the generator proposes for the attached table

SPARQL

Output of RDFVIEW_FROM_TABLES for databricks.bakehouse.sales_transactions_viritual_odbc with iri_path_segment DbxSales, generated from the live table metadata and shown verbatim, not executed. Note that it maps every column, including cardNumber, as a predicate; a real deployment would drop that column and likely customerID and transactionID too, or pseudonymise them first.

SPARQL
prefix DbxSales: <http://demo.openlinksw.com/schemas/DbxSales/> 
create iri class DbxSales:sales_transactions_viritual_odbc "http://^{URIQADefaultHost}^/DbxSales/sales_transactions_viritual_odbc/transactionID/%ld#this" (in _transactionID integer not null) . ;


SPARQL
prefix DbxSales: <http://demo.openlinksw.com/schemas/DbxSales/> 
prefix aowl: <http://bblfish.net/work/atom-owl/2006-06-06/> 
alter quad storage virtrdf:DefaultQuadStorage 
 from "databricks"."bakehouse"."sales_transactions_viritual_odbc" as sales_transactions_viritual_odbc_s
 { 
   create DbxSales:qm-sales_transactions_viritual_odbc as graph iri ("http://^{URIQADefaultHost}^/DbxSales#") option (exclusive) 
    { 
      # Maps from columns of "databricks.bakehouse.sales_transactions_viritual_odbc"
      DbxSales:sales_transactions_viritual_odbc (sales_transactions_viritual_odbc_s."transactionID")  a DbxSales:sales_transactions_viritual_odbc ;
      DbxSales:transactionid sales_transactions_viritual_odbc_s."transactionID" as DbxSales:bakehouse-sales_transactions_viritual_odbc-transactionid ;
      DbxSales:customerid sales_transactions_viritual_odbc_s."customerID" as DbxSales:bakehouse-sales_transactions_viritual_odbc-customerid ;
      DbxSales:franchiseid sales_transactions_viritual_odbc_s."franchiseID" as DbxSales:bakehouse-sales_transactions_viritual_odbc-franchiseid ;
      DbxSales:datetime sales_transactions_viritual_odbc_s."dateTime" as DbxSales:bakehouse-sales_transactions_viritual_odbc-datetime ;
      DbxSales:product sales_transactions_viritual_odbc_s."product" as DbxSales:bakehouse-sales_transactions_viritual_odbc-product ;
      DbxSales:quantity sales_transactions_viritual_odbc_s."quantity" as DbxSales:bakehouse-sales_transactions_viritual_odbc-quantity ;
      DbxSales:unitprice sales_transactions_viritual_odbc_s."unitPrice" as DbxSales:bakehouse-sales_transactions_viritual_odbc-unitprice ;
      DbxSales:totalprice sales_transactions_viritual_odbc_s."totalPrice" as DbxSales:bakehouse-sales_transactions_viritual_odbc-totalprice ;
      DbxSales:paymentmethod sales_transactions_viritual_odbc_s."paymentMethod" as DbxSales:bakehouse-sales_transactions_viritual_odbc-paymentmethod ;
      DbxSales:cardnumber sales_transactions_viritual_odbc_s."cardNumber" as DbxSales:bakehouse-sales_transactions_viritual_odbc-cardnumber .

    }
 }

;

Linked Data deployment: ontology and rewrite rules

1 card

The ontology and rewrite rules the generators produced for the same table.

Ontology and rewrite rules for the attached table

SQL

RDFVIEW_ONTOLOGY_FROM_TABLES and RDFVIEW_GENERATE_DATA_RULES were also called for the same table. The ontology declares one class and ten datatype properties with ranges, and the rules have exactly the shape of the fourteen statements in the CustomerKG card, with the prefix dbxsales_ and the path segment DbxSales. Generated and not executed.

-- Called on demo with iri_path_segment = 'DbxSales' and tables = ['databricks.bakehouse.sales_transactions_viritual_odbc'].
-- RDFVIEW_ONTOLOGY_FROM_TABLES returned one rdfs:Class and ten owl:DatatypeProperty terms with xsd ranges.
-- RDFVIEW_GENERATE_DATA_RULES (include_ontology_rules = 1) returned fourteen statements. The first and the last of the data rules:
DB.DBA.URLREWRITE_CREATE_REGEX_RULE (
'dbxsales_rule1', 1, '([^#]*)', vector('path'), 1,
'/describe/?url=http%%3A//^{URIQADefaultHost}^%U%%23this&graph=http%%3A//^{URIQADefaultHost}^/DbxSales%%23&distinct=0',
vector('path'), null, null, 2, 303
);
DB.DBA.VHOST_DEFINE (lpath=>'/DbxSales', ppath=>'/', vsp_user=>'dba', is_dav=>0,
is_brws=>0, opts=>vector ('url_rewrite', 'dbxsales_rule_list1')
);
-- Not executed: running them would publish the attached table's ten columns, including cardNumber, as Linked Data.

Rows are read from Databricks over the same ODBC route any other Databricks client uses, so the transfer is no different from theirs; what Virtuoso adds is the mapping, the resolvable identifiers, the reasoning over them and the queries. The one extra step is the column review above.

The Harness Layer

agent-rdf-memory: Rules and Memory as Linked Data

The article's control layer is the intent, guardrails and approval points that agents must respect. The agent-rdf-memory harness applies the same idea to the coding agent that built this page: standing preferences are HowToSteps in Turtle, retrieval is routed by an ontology, and every memory document is its own named graph in the same Virtuoso store that serves the CustomerKG view.

519
Memory files
On disk at the start of this session, in five folders.
511
Named graphs
Loaded into Virtuoso, one per memory document; four how-to files and four session files were not yet loaded, so the folder-level check reported CHECK.
36,701
Triples
Queryable with the same SPARQL that queries the CustomerKG view.
1,336
HowToSteps
Each standing rule is a schema:HowToStep an agent can retrieve and follow.
184
HowTo documents
Procedures an agent re-reads when a task triggers one.

The counts come from the harness's own memory-graph verification run at the start of this session, which compares files on disk with graphs loaded in Virtuoso. The same loop applies to a marketing agent: guardrails, consent and approval points as queryable entities, each decision recorded with its rationale and linked to its outcome, and retrieval by query rather than by re-reading a prompt. The harness does not make an agent obey a rule; it makes the rule, its history and its use inspectable.

How-To

How-To Guide

1

Create the tables

Run the twelve CREATE TABLE statements under a qualifier and owner you control, parents before children. The demonstration used CustomerKG.kidehen.

2

Load the rows

Insert the 228 rows. Virtuoso has no multi-row VALUES, so send one INSERT per row, parents before children.

3

Create the IRI classes

Run the eleven SPARQL CREATE IRI CLASS statements. On a busy server each can take tens of seconds; confirm completion by querying the virtrdf schema graph rather than reissuing.

4

Load the ontology and build the rule set

Load customerkg-ontology-claude_sonnet_5_5-1.ttl into http://demo.openlinksw.com/schemas/CustomerKG/ with DB.DBA.TTLP, then create the rule set with DB.DBA.rdfs_rule_set('urn:customerkg:inference', 'http://demo.openlinksw.com/schemas/CustomerKG/').

5

Declare the RDF View

Send the single ALTER QUAD STORAGE statement. Keep every mapping for the graph in one statement, because a second statement for the same graph replaces the first, and guard nullable foreign keys with IS NOT NULL.

6

Grant access

GRANT SELECT on each table to the SPARQL account and PUBLIC, plus any accounts that run SPASQL. The view inherits these privileges.

7

Install the rewrite rules

Call RDFVIEW_GENERATE_DATA_RULES with the IRI path segment CustomerKG and include_ontology_rules set to 1, and run the returned script, which defines the two virtual directories. Then request an entity IRI: it should answer with a 303 to a describe page.

8

Build the identity graph

Load the identity vocabulary into http://demo.openlinksw.com/schemas/CustomerKG/identity/, build the rule set urn:customerkg:identity-inference from it, then derive the customer facts from the view with SPARQL INSERT ... WHERE and add the records from other systems. Query it with DEFINE input:same-as yes and the rule set pragma, signed in.

9

Check the count

SELECT COUNT(*) over the named graph should return 1,160. If it is higher, look for NULL foreign keys missing their guard.

10

Run the queries

Run Queries 1 to 14. Expect 20, 4, 7, 4, 12, 43, 5, 20, 3, 2, 3, 1, 11 and 4 rows. Queries 1 to 7 run from the demo SPARQL endpoint, except Query 3, which needs the signed-in route; Queries 8 to 12 and Query 3 need a signed-in route such as the SPASQL Query Builder or isql, authenticating as demo or vdb with an identical user id and password; Queries 13 and 14 run from the URIBurner SPARQL endpoint and read the view on demo through SERVICE.

11

Extend to Databricks over ODBC

Create the ODBC data source with the workspace host, HTTP path and a token held in a credential store, attach the tables, then call RDFVIEW_FROM_TABLES, RDFVIEW_ONTOLOGY_FROM_TABLES and RDFVIEW_GENERATE_DATA_RULES for them. Before executing the output, remove or pseudonymise personal and payment columns.

FAQ

Frequently Asked Questions

AI agents that evaluate current customer and business context to choose the next best action within human-defined guardrails, including the option to take no action, and that use measured outcomes to improve future decisions. The article contrasts it with marketing automation, which executes rules and journeys that people design in advance.

Because an agent can only act as well as the context it is given, and identity is what holds that context together. The article says identity has to be maintained continuously, connecting known records with pseudonymous signals such as anonymous app sessions or ad exposure without compromising privacy, governance or trust.

Customer context (identity, behaviour, preferences, consent status, lifecycle stage), business context (growth objectives, margin, brand rules, inventory, channel constraints), decision context (offers already received, responses and the reasons for earlier decisions) and control (the intent, guardrails, permissions and approval points people define and agents must respect).

The article describes CustomerLake as the Agentic CDP from Databricks, embedded in the lakehouse and governed by Unity Catalog so that marketing engagement and personalization run on the same data and AI foundation as the rest of the business. It is organised around Profile Agents and Campaign Agents, with Genie for natural-language exploration.

Through incrementality, the difference compared with taking no action, supported by attribution, incrementality testing and marketing mix modeling. It lists five things a CMO must show: incremental impact, true profit contribution, contribution by lever, marginal return, and customer value over time.

Model the same loop as linked data: customers, signals, consent, offers, guardrails, decisions and outcomes as entities with resolvable IRIs, projected from SQL tables by an RDF View or held as triples, queried with SQL, SPARQL and SPASQL, with the agent-rdf-memory harness keeping the working agent's standing rules and session memory in the same store.

Virtuoso attaches a Databricks table through an ODBC data source so SQL treats it like a local table, and RDF View generators then produce a quad map, an ontology and rewrite rules over it. The rows stay in Databricks and are fetched at query time. Demo already holds such an attachment, and this page shows the generator's output for it without executing it.

No. CustomerKG is a synthetic estate of 228 rows invented for this page, on demo.openlinksw.com, with invented customer names. It proves the Virtuoso mechanics, SQL tables projected to RDF with resolvable IRIs and queried live, and simulates the loop the article describes. It makes no claim about Databricks performance or behaviour.

Yes, after URL rewrite rules were installed in the same pass as the view. Each entity IRI answers with a 303 to a describe page, and so do terms in the ontology namespace. A view without rewrite rules defines IRIs that return 404.

In the synthetic data the treated customers ordered at 75 percent and the holdout at 100 percent, so the incentive subsidised orders that would have happened anyway, and incremental profit is minus 15 dollars. It is the article's suppress-offers-that-subsidise scenario, with invented numbers, expressed as a SQL query over linked decision and outcome rows.

As entities linked to every decision they constrain. One query lists the human-approval rules against the decisions that triggered them and whether a person approved; another joins withdrawn consent to the native decision table and finds no contact on a withdrawn channel. Auditing is shown here; enforcement is a separate property that this page does not test.

On none of the thirteen axes does the evidence put it ahead, and the page says where that evidence is thin. The article describes probabilistic and agentic identity matching with a partner graph, activation across destinations and three measurement methods, but shows none of them, and this page tests none of them on either side. They are where Databricks capabilities would feed the Virtuoso paths rather than compete with them: matches from any engine can be asserted as owl:sameAs and reasoned over, and Databricks tables attached over ODBC gain that reasoning without a copy.

By reasoning over the graph. An explicit owl:sameAs merges two identities, and declaring a property such as an email hash an owl:InverseFunctionalProperty makes any two records that share its value the same person, with no matching pipeline or third-party engine. Query 3 shows it: Avery Quinn is known under four records through one explicit link and two implied by a shared hash. Databricks tables attached over ODBC and projected by an RDF View take part in the same reasoning, and for master lookups OpenLink hosts DBpedia and many enclaves of the LOD Cloud.

Review every column before an RDF View exposes it. The generator mapped all ten columns of the attached Bakehouse table, including cardNumber, as predicates. Drop or pseudonymise payment and personal columns, apply consent to the view, and grant SELECT narrowly, because the view inherits SQL privileges and publishes whatever it maps.

They need a signed-in session, which the anonymous SPARQL endpoint does not give. Authenticate with the account demo or vdb, where the user id and the password are identical, then run them in the SPASQL Query Builder or any SQL route. The instance is protected by attribute-based access control (ABAC) ACLs, and every table in the CustomerKG estate grants SELECT to those two accounts, to PUBLIC and to the SPARQL account.

The article's control layer holds the intent, guardrails and approval points that agents must respect. The harness applies the same idea to the coding agent that built this page: standing preferences are schema:HowToStep entities retrieved by ontology-routed queries, and each memory document is a named graph in the Virtuoso store that also serves the CustomerKG view.

Glossary

Glossary of Terms

Profile Agents

CustomerLake agents that convert fragmented customer data into business-ready Customer 360 profiles, using Agentic Identity Resolution that blends deterministic, probabilistic and agentic matching with Acxiom's graph-based identity capabilities.

Campaign Agents

CustomerLake agents that use governed customer context to build audiences, recommend next-best actions, activate across channels and continuously optimise around business goals.

Agentic marketing

Marketing in which AI agents grounded in trusted customer, business and decision context recommend or take the next best action for each customer in near-real-time, within goals and guardrails that marketers define, with each action measured against what would have happened without it.

Identity resolution

Recognising that records, devices, sessions and ad exposures belong to one customer. In the article it is a continuous capability that supports action, not a one-off profile-building project.

Incrementality

The difference in revenue, margin or customer behaviour compared with taking no action. It separates outcomes marketing was associated with from outcomes that changed because the brand acted.

Customer data platform

Software that unifies customer data from many sources into persistent profiles and activates audiences. The article's agentic CDP adds agents that decide and act.

Agentic CDP

A customer data platform that combines identity resolution, audience building and activation with AI agents that analyse signals, decide on next-best actions and act across channels.

Infinity Campaigns

Databricks' name for always-on engagement loops that replace the cycle of building, launching and rebuilding one-off campaigns.

owl:sameAs

An OWL property asserting that two IRIs denote the same thing. Under reasoning, queries about one see the facts held about the other.

Inverse functional property

A property whose value identifies its subject uniquely, such as a hashed email address. If two records share the value, a reasoner concludes they describe the same entity.

Holdout

A randomised group deliberately left alone, so the difference between its outcomes and the treated group's outcomes measures what the action changed.

Guardrail

A rule an agent must respect, such as a discount cap, a contact-frequency limit, a consent requirement or a human-approval threshold.

Marketing mix modeling

Statistical estimation of how marketing inputs contribute to outcomes in aggregate; one of three measurement methods the article names.

Attribution

Assigning credit for an outcome to marketing touchpoints; one of the three measurement methods the article names.

RDF View

A declarative mapping from relational tables to RDF triples computed at query time. In Virtuoso it is declared with ALTER QUAD STORAGE and is also called a quad map.

IRI class

A Virtuoso template that builds a dereferenceable IRI from a key column, such as http://demo.openlinksw.com/CustomerKG/customer/%d#this.

SPASQL

A SPARQL query nested inside a SQL SELECT, so a graph result set joins native relational tables in one statement.

Attached table

A table that lives in a remote database and is registered in Virtuoso over ODBC or JDBC, so it can be queried and mapped as if it were local.

ODBC

A standard C API for connecting applications to databases through drivers. Virtuoso uses it to attach remote tables such as a Databricks SQL warehouse.

Named graph

An RDF graph identified by an IRI. The CustomerKG view lives in http://demo.openlinksw.com/CustomerKG# and its TBox in http://demo.openlinksw.com/schemas/CustomerKG/.

Entailment

Deriving facts not stored explicitly from a declared ontology, such as a consent record being a ControlElement because ConsentRecord is a subclass of it.

Linked data

Publishing structured data so that its identifiers are HTTP IRIs that resolve to descriptions and link to other data.

Knowledge graph

A graph of entities and relationships with meaning attached. Here it is computed from relational tables rather than stored separately.

SPARQL

The W3C query language for RDF, used here against the RDF View.

SQL

The relational query language. An RDF View compiles SPARQL into it.

Data governance

The policies and controls over who may use which data and how. In the article Unity Catalog provides it; in Virtuoso the view inherits SQL grants.

Prediction economy

The article's name, via Jake LaDuke, for a model that rewards predicting what a customer needs, acting on it, proving the outcome and using it as the accelerant for what comes next, replacing an impression currency of reach and frequency.

Incremental profit

The profit the treated customers added beyond what the holdout's behaviour predicts, after media costs and discounts. Negative when an incentive only subsidises orders that would have happened anyway.

Knowledge Graph Explorer 207 nodes · 416 links

Interactive graph visualization derived from the companion RDF. Click nodes to resolve, drag to explore. Graph data embedded from companion RDF at generation time.

Agentic Marketing: Databricks, Virtuoso, and Both Together

Nodes: 0 Links: 0
Click SVG to activate zoom, click outside to release | Drag nodes to pin, double-click to unpin
Classes Properties Instances

SPARQL Workbench 29 sample queries

Query this knowledge graph on URIBurner. The editor opens on the canonical SAMPLE entity-type summary (DAV named graph). Pick a recipe, edit freely, then run live or copy.

Sample Queries

Reproduced verbatim from the companion RDF. Execute loads the query into the workbench below and runs it live.

The decision ledger
PREFIX ckg: <http://demo.openlinksw.com/schemas/CustomerKG/>
PREFIX schema: <http://schema.org/>
SELECT ?decisionIri ?customer ?campaign ?arm ?action ?offer ?channel ?ordered ?revenueUsd
FROM <http://demo.openlinksw.com/CustomerKG#>
WHERE {
  ?decisionIri a ckg:Decision ; ckg:forCustomer ?customerIri ; ckg:inCampaign ?campaignIri ;
               ckg:arm ?arm ; ckg:actionKind ?action .
  ?customerIri schema:name ?customer .
  ?campaignIri schema:name ?campaign .
  OPTIONAL { ?decisionIri ckg:usesOffer ?offerIri . ?offerIri schema:name ?offer }
  OPTIONAL { ?decisionIri ckg:viaChannel ?channelIri . ?channelIri schema:name ?channel }
  ?outcomeIri ckg:ofDecision ?decisionIri ; ckg:ordered ?ordered ; ckg:revenueUsd ?revenueUsd .
}
ORDER BY ?campaign ?arm ?customer
Identity signals by match method
PREFIX ckg: <http://demo.openlinksw.com/schemas/CustomerKG/>
PREFIX schema: <http://schema.org/>
SELECT ?matchMethod (SAMPLE(?signalIri) AS ?exampleSignalIri) (COUNT(?signalIri) AS ?signals) (COUNT(?customerIri) AS ?resolved) (ROUND(AVG(?confidence)) AS ?avgConfidence)
FROM <http://demo.openlinksw.com/CustomerKG#>
WHERE {
  ?signalIri a ckg:IdentitySignal ; ckg:matchMethod ?matchMethod ; ckg:confidence ?confidence .
  OPTIONAL { ?signalIri ckg:resolvesTo ?customerIri }
}
GROUP BY ?matchMethod
ORDER BY DESC(?signals)
Identity reconciliation by reasoning: one person, several records
DEFINE input:inference "urn:customerkg:identity-inference"
DEFINE input:same-as "yes"
PREFIX schema: <http://schema.org/>
SELECT DISTINCT ?personIri ?name
FROM <http://demo.openlinksw.com/CustomerKG/identity#>
WHERE {
  { <http://demo.openlinksw.com/CustomerKG/customer/1#this> schema:name ?name . BIND(<http://demo.openlinksw.com/CustomerKG/customer/1#this> AS ?personIri) } UNION { <http://demo.openlinksw.com/CustomerKG/customer/2#this> schema:name ?name . BIND(<http://demo.openlinksw.com/CustomerKG/customer/2#this> AS ?personIri) } UNION { <http://demo.openlinksw.com/CustomerKG/customer/3#this> schema:name ?name . BIND(<http://demo.openlinksw.com/CustomerKG/customer/3#this> AS ?personIri) }
}
Human-approval audit
PREFIX ckg: <http://demo.openlinksw.com/schemas/CustomerKG/>
PREFIX schema: <http://schema.org/>
SELECT ?decisionIri ?customer ?offer ?discountPct ?rule ?limitValue ?approvedByHuman
FROM <http://demo.openlinksw.com/CustomerKG#>
WHERE {
  ?decisionIri ckg:constrainedBy ?guardrailIri ; ckg:forCustomer ?customerIri ; ckg:usesOffer ?offerIri ; ckg:approvedByHuman ?approvedByHuman .
  ?guardrailIri ckg:ruleKind "human-approval" ; schema:name ?rule ; ckg:limitValue ?limitValue .
  ?customerIri schema:name ?customer .
  ?offerIri schema:name ?offer ; ckg:discountPct ?discountPct .
}
The activation queue: consent-checked messages ready to send
PREFIX ckg: <http://demo.openlinksw.com/schemas/CustomerKG/>
PREFIX schema: <http://schema.org/>
SELECT ?decisionIri ?customer ?channel ?channelKind ?offer ?actionKind ?consentIri
FROM <http://demo.openlinksw.com/CustomerKG#>
WHERE {
  ?decisionIri a ckg:Decision ; ckg:forCustomer ?custIri ; ckg:viaChannel ?chanIri ; ckg:actionKind ?actionKind .
  ?custIri schema:name ?customer .
  ?chanIri schema:name ?channel ; ckg:channelKind ?channelKind .
  ?consentIri a ckg:ConsentRecord ; ckg:forCustomer ?custIri ; ckg:forChannel ?chanIri ; ckg:consentStatus "granted" .
  OPTIONAL { ?decisionIri ckg:usesOffer ?offerIri . ?offerIri schema:name ?offer }
}
Entailment: everything that is a control
DEFINE input:inference "urn:customerkg:inference"
PREFIX ckg: <http://demo.openlinksw.com/schemas/CustomerKG/>
PREFIX schema: <http://schema.org/>
SELECT DISTINCT ?controlIri ?ruleKind ?consentStatus
FROM <http://demo.openlinksw.com/CustomerKG#>
WHERE {
  ?controlIri a ckg:ControlElement .
  OPTIONAL { ?controlIri ckg:ruleKind ?ruleKind }
  OPTIONAL { ?controlIri ckg:consentStatus ?consentStatus }
}
The property hierarchy, read from the TBox
PREFIX ckg: <http://demo.openlinksw.com/schemas/CustomerKG/>
PREFIX schema: <http://schema.org/>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT ?relation (COUNT(DISTINCT ?decisionIri) AS ?decisions) (COUNT(*) AS ?links) (SAMPLE(?decisionIri) AS ?exampleDecisionIri)
FROM <http://demo.openlinksw.com/CustomerKG#>
FROM <http://demo.openlinksw.com/schemas/CustomerKG/>
WHERE {
  ?decisionIri a ckg:Decision ; ?relationIri ?targetIri .
  ?relationIri rdfs:subPropertyOf ckg:groundedIn ; rdfs:label ?relation .
}
GROUP BY ?relation
ORDER BY DESC(?links)
Class counts across instances: URIBurner asks demo
SELECT ?classIri ?entities
WHERE {
  SERVICE <https://demo.openlinksw.com/sparql> {
    SELECT ?classIri (COUNT(DISTINCT ?entityIri) AS ?entities)
    FROM <http://demo.openlinksw.com/CustomerKG#>
    WHERE { ?entityIri a ?classIri }
    GROUP BY ?classIri
  }
}
ORDER BY DESC(?entities)
The four kinds of context behind one decision, fetched across instances
PREFIX ckg: <http://demo.openlinksw.com/schemas/CustomerKG/>
SELECT ?itemIri ?contextType ?contextKey ?contextValue
WHERE {
  SERVICE <https://demo.openlinksw.com/sparql> {
    SELECT ?itemIri ?contextType ?contextKey ?contextValue
    FROM <http://demo.openlinksw.com/CustomerKG#>
    WHERE {
      ?itemIri a ckg:ContextItem ; ckg:informs <http://demo.openlinksw.com/CustomerKG/decision/3#this> ;
               ckg:contextType ?contextType ; ckg:contextKey ?contextKey ; ckg:contextValue ?contextValue .
    }
  }
}
Eleven IRI classes
SPARQL CREATE IRI CLASS <http://demo.openlinksw.com/schemas/CustomerKG/channel_iri> "http://demo.openlinksw.com/CustomerKG/channel/%d#this" (in channel_id integer not null);
SPARQL CREATE IRI CLASS <http://demo.openlinksw.com/schemas/CustomerKG/product_iri> "http://demo.openlinksw.com/CustomerKG/product/%d#this" (in product_id integer not null);
SPARQL CREATE IRI CLASS <http://demo.openlinksw.com/schemas/CustomerKG/customer_iri> "http://demo.openlinksw.com/CustomerKG/customer/%d#this" (in customer_id integer not null);
SPARQL CREATE IRI CLASS <http://demo.openlinksw.com/schemas/CustomerKG/signal_iri> "http://demo.openlinksw.com/CustomerKG/signal/%d#this" (in signal_id integer not null);
SPARQL CREATE IRI CLASS <http://demo.openlinksw.com/schemas/CustomerKG/consent_iri> "http://demo.openlinksw.com/CustomerKG/consent/%d#this" (in consent_id integer not null);
SPARQL CREATE IRI CLASS <http://demo.openlinksw.com/schemas/CustomerKG/offer_iri> "http://demo.openlinksw.com/CustomerKG/offer/%d#this" (in offer_id integer not null);
SPARQL CREATE IRI CLASS <http://demo.openlinksw.com/schemas/CustomerKG/guardrail_iri> "http://demo.openlinksw.com/CustomerKG/guardrail/%d#this" (in guardrail_id integer not null);
SPARQL CREATE IRI CLASS <http://demo.openlinksw.com/schemas/CustomerKG/campaign_iri> "http://demo.openlinksw.com/CustomerKG/campaign/%d#this" (in campaign_id integer not null);
SPARQL CREATE IRI CLASS <http://demo.openlinksw.com/schemas/CustomerKG/decision_iri> "http://demo.openlinksw.com/CustomerKG/decision/%d#this" (in decision_id integer not null);
SPARQL CREATE IRI CLASS <http://demo.openlinksw.com/schemas/CustomerKG/context_iri> "http://demo.openlinksw.com/CustomerKG/context/%d#this" (in item_id integer not null);
SPARQL CREATE IRI CLASS <http://demo.openlinksw.com/schemas/CustomerKG/outcome_iri> "http://demo.openlinksw.com/CustomerKG/outcome/%d#this" (in outcome_id integer not null);
The RDF View: one ALTER QUAD STORAGE statement
SPARQL
ALTER QUAD STORAGE virtrdf:DefaultQuadStorage
  FROM CustomerKG.kidehen.channel AS ch
  FROM CustomerKG.kidehen.product AS pr
  FROM CustomerKG.kidehen.customer AS cu
  FROM CustomerKG.kidehen.identity_signal AS sg
  FROM CustomerKG.kidehen.consent AS co
  FROM CustomerKG.kidehen.offer AS ofr
  FROM CustomerKG.kidehen.guardrail AS gr
  FROM CustomerKG.kidehen.campaign AS ca
  FROM CustomerKG.kidehen.decision AS de
  FROM CustomerKG.kidehen.decision_guardrail AS dg
  FROM CustomerKG.kidehen.context_item AS ci
  FROM CustomerKG.kidehen.outcome AS ou
{
  GRAPH <http://demo.openlinksw.com/CustomerKG#>
  {
    <http://demo.openlinksw.com/schemas/CustomerKG/channel_iri>(ch.channel_id) <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://demo.openlinksw.com/schemas/CustomerKG/Channel> as virtrdf:CustomerKG-channel-type .
    <http://demo.openlinksw.com/schemas/CustomerKG/channel_iri>(ch.channel_id) <http://schema.org/name> ch.channel_name as virtrdf:CustomerKG-channel-name .
    <http://demo.openlinksw.com/schemas/CustomerKG/channel_iri>(ch.channel_id) <http://demo.openlinksw.com/schemas/CustomerKG/channelKind> ch.channel_kind as virtrdf:CustomerKG-channel-kind .
    <http://demo.openlinksw.com/schemas/CustomerKG/product_iri>(pr.product_id) <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://demo.openlinksw.com/schemas/CustomerKG/Product> as virtrdf:CustomerKG-product-type .
    <http://demo.openlinksw.com/schemas/CustomerKG/product_iri>(pr.product_id) <http://schema.org/name> pr.product_name as virtrdf:CustomerKG-product-name .
    <http://demo.openlinksw.com/schemas/CustomerKG/product_iri>(pr.product_id) <http://demo.openlinksw.com/schemas/CustomerKG/category> pr.category as virtrdf:CustomerKG-product-category .
    <http://demo.openlinksw.com/schemas/CustomerKG/product_iri>(pr.product_id) <http://demo.openlinksw.com/schemas/CustomerKG/marginPct> pr.margin_pct as virtrdf:CustomerKG-product-margin .
    <http://demo.openlinksw.com/schemas/CustomerKG/customer_iri>(cu.customer_id) <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://demo.openlinksw.com/schemas/CustomerKG/Customer> as virtrdf:CustomerKG-customer-type .
    <http://demo.openlinksw.com/schemas/CustomerKG/customer_iri>(cu.customer_id) <http://schema.org/name> cu.full_name as virtrdf:CustomerKG-customer-name .
    <http://demo.openlinksw.com/schemas/CustomerKG/customer_iri>(cu.customer_id) <http://demo.openlinksw.com/schemas/CustomerKG/lifecycleStage> cu.lifecycle_stage as virtrdf:CustomerKG-customer-lifecycle .
    <http://demo.openlinksw.com/schemas/CustomerKG/customer_iri>(cu.customer_id) <http://demo.openlinksw.com/schemas/CustomerKG/loyaltyTier> cu.loyalty_tier as virtrdf:CustomerKG-customer-tier .
    <http://demo.openlinksw.com/schemas/CustomerKG/customer_iri>(cu.customer_id) <http://demo.openlinksw.com/schemas/CustomerKG/homeCity> cu.home_city as virtrdf:CustomerKG-customer-city .
    <http://demo.openlinksw.com/schemas/CustomerKG/customer_iri>(cu.customer_id) <http://demo.openlinksw.com/schemas/CustomerKG/usualOrderDay> cu.usual_order_day as virtrdf:CustomerKG-customer-day .
    <http://demo.openlinksw.com/schemas/CustomerKG/signal_iri>(sg.signal_id) <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://demo.openlinksw.com/schemas/CustomerKG/IdentitySignal> as virtrdf:CustomerKG-signal-type .
    <http://demo.openlinksw.com/schemas/CustomerKG/signal_iri>(sg.signal_id) <http://demo.openlinksw.com/schemas/CustomerKG/signalKind> sg.signal_kind as virtrdf:CustomerKG-signal-kind .
    <http://demo.openlinksw.com/schemas/CustomerKG/signal_iri>(sg.signal_id) <http://demo.openlinksw.com/schemas/CustomerKG/signalValue> sg.signal_value as virtrdf:CustomerKG-signal-value .
    <http://demo.openlinksw.com/schemas/CustomerKG/signal_iri>(sg.signal_id) <http://demo.openlinksw.com/schemas/CustomerKG/matchMethod> sg.match_method as virtrdf:CustomerKG-signal-method .
    <http://demo.openlinksw.com/schemas/CustomerKG/signal_iri>(sg.signal_id) <http://demo.openlinksw.com/schemas/CustomerKG/confidence> sg.confidence as virtrdf:CustomerKG-signal-confidence .
    <http://demo.openlinksw.com/schemas/CustomerKG/signal_iri>(sg.signal_id) <http://demo.openlinksw.com/schemas/CustomerKG/seenDate> sg.seen_date as virtrdf:CustomerKG-signal-seen .
    <http://demo.openlinksw.com/schemas/CustomerKG/signal_iri>(sg.signal_id) <http://demo.openlinksw.com/schemas/CustomerKG/resolvesTo> <http://demo.openlinksw.com/schemas/CustomerKG/customer_iri>(sg.customer_id) where (^{sg.}^.customer_id is not null) as virtrdf:CustomerKG-signal-customer .
    <http://demo.openlinksw.com/schemas/CustomerKG/consent_iri>(co.consent_id) <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://demo.openlinksw.com/schemas/CustomerKG/ConsentRecord> as virtrdf:CustomerKG-consent-type .
    <http://demo.openlinksw.com/schemas/CustomerKG/consent_iri>(co.consent_id) <http://demo.openlinksw.com/schemas/CustomerKG/consentStatus> co.status as virtrdf:CustomerKG-consent-status .
    <http://demo.openlinksw.com/schemas/CustomerKG/consent_iri>(co.consent_id) <http://demo.openlinksw.com/schemas/CustomerKG/updatedDate> co.updated_date as virtrdf:CustomerKG-consent-updated .
    <http://demo.openlinksw.com/schemas/CustomerKG/consent_iri>(co.consent_id) <http://demo.openlinksw.com/schemas/CustomerKG/forCustomer> <http://demo.openlinksw.com/schemas/CustomerKG/customer_iri>(co.customer_id) as virtrdf:CustomerKG-consent-customer .
    <http://demo.openlinksw.com/schemas/CustomerKG/consent_iri>(co.consent_id) <http://demo.openlinksw.com/schemas/CustomerKG/forChannel> <http://demo.openlinksw.com/schemas/CustomerKG/channel_iri>(co.channel_id) as virtrdf:CustomerKG-consent-channel .
    <http://demo.openlinksw.com/schemas/CustomerKG/offer_iri>(ofr.offer_id) <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://demo.openlinksw.com/schemas/CustomerKG/Offer> as virtrdf:CustomerKG-offer-type .
    <http://demo.openlinksw.com/schemas/CustomerKG/offer_iri>(ofr.offer_id) <http://schema.org/name> ofr.offer_name as virtrdf:CustomerKG-offer-name .
    <http://demo.openlinksw.com/schemas/CustomerKG/offer_iri>(ofr.offer_id) <http://demo.openlinksw.com/schemas/CustomerKG/offerKind> ofr.offer_kind as virtrdf:CustomerKG-offer-kind .
    <http://demo.openlinksw.com/schemas/CustomerKG/offer_iri>(ofr.offer_id) <http://demo.openlinksw.com/schemas/CustomerKG/discountPct> ofr.discount_pct as virtrdf:CustomerKG-offer-discount .
    <http://demo.openlinksw.com/schemas/CustomerKG/offer_iri>(ofr.offer_id) <http://demo.openlinksw.com/schemas/CustomerKG/promotes> <http://demo.openlinksw.com/schemas/CustomerKG/product_iri>(ofr.product_id) where (^{ofr.}^.product_id is not null) as virtrdf:CustomerKG-offer-product .
    <http://demo.openlinksw.com/schemas/CustomerKG/guardrail_iri>(gr.guardrail_id) <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://demo.openlinksw.com/schemas/CustomerKG/Guardrail> as virtrdf:CustomerKG-guardrail-type .
    <http://demo.openlinksw.com/schemas/CustomerKG/guardrail_iri>(gr.guardrail_id) <http://schema.org/name> gr.rule_name as virtrdf:CustomerKG-guardrail-name .
    <http://demo.openlinksw.com/schemas/CustomerKG/guardrail_iri>(gr.guardrail_id) <http://demo.openlinksw.com/schemas/CustomerKG/ruleKind> gr.rule_kind as virtrdf:CustomerKG-guardrail-kind .
    <http://demo.openlinksw.com/schemas/CustomerKG/guardrail_iri>(gr.guardrail_id) <http://demo.openlinksw.com/schemas/CustomerKG/limitValue> gr.limit_value as virtrdf:CustomerKG-guardrail-limit .
    <http://demo.openlinksw.com/schemas/CustomerKG/guardrail_iri>(gr.guardrail_id) <http://demo.openlinksw.com/schemas/CustomerKG/appliesToOffer> <http://demo.openlinksw.com/schemas/CustomerKG/offer_iri>(gr.offer_id) where (^{gr.}^.offer_id is not null) as virtrdf:CustomerKG-guardrail-offer .
    <http://demo.openlinksw.com/schemas/CustomerKG/campaign_iri>(ca.campaign_id) <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://demo.openlinksw.com/schemas/CustomerKG/Campaign> as virtrdf:CustomerKG-campaign-type .
    <http://demo.openlinksw.com/schemas/CustomerKG/campaign_iri>(ca.campaign_id) <http://schema.org/name> ca.campaign_name as virtrdf:CustomerKG-campaign-name .
    <http://demo.openlinksw.com/schemas/CustomerKG/campaign_iri>(ca.campaign_id) <http://demo.openlinksw.com/schemas/CustomerKG/goal> ca.goal as virtrdf:CustomerKG-campaign-goal .
    <http://demo.openlinksw.com/schemas/CustomerKG/campaign_iri>(ca.campaign_id) <http://demo.openlinksw.com/schemas/CustomerKG/startDate> ca.start_date as virtrdf:CustomerKG-campaign-start .
    <http://demo.openlinksw.com/schemas/CustomerKG/decision_iri>(de.decision_id) <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://demo.openlinksw.com/schemas/CustomerKG/Decision> as virtrdf:CustomerKG-decision-type .
    <http://demo.openlinksw.com/schemas/CustomerKG/decision_iri>(de.decision_id) <http://demo.openlinksw.com/schemas/CustomerKG/arm> de.arm as virtrdf:CustomerKG-decision-arm .
    <http://demo.openlinksw.com/schemas/CustomerKG/decision_iri>(de.decision_id) <http://demo.openlinksw.com/schemas/CustomerKG/actionKind> de.action_kind as virtrdf:CustomerKG-decision-action .
    <http://demo.openlinksw.com/schemas/CustomerKG/decision_iri>(de.decision_id) <http://demo.openlinksw.com/schemas/CustomerKG/decidedDate> de.decided_date as virtrdf:CustomerKG-decision-date .
    <http://demo.openlinksw.com/schemas/CustomerKG/decision_iri>(de.decision_id) <http://demo.openlinksw.com/schemas/CustomerKG/rationale> de.rationale as virtrdf:CustomerKG-decision-rationale .
    <http://demo.openlinksw.com/schemas/CustomerKG/decision_iri>(de.decision_id) <http://demo.openlinksw.com/schemas/CustomerKG/approvedByHuman> de.approved_by_human as virtrdf:CustomerKG-decision-approved .
    <http://demo.openlinksw.com/schemas/CustomerKG/decision_iri>(de.decision_id) <http://demo.openlinksw.com/schemas/CustomerKG/forCustomer> <http://demo.openlinksw.com/schemas/CustomerKG/customer_iri>(de.customer_id) as virtrdf:CustomerKG-decision-customer .
    <http://demo.openlinksw.com/schemas/CustomerKG/decision_iri>(de.decision_id) <http://demo.openlinksw.com/schemas/CustomerKG/inCampaign> <http://demo.openlinksw.com/schemas/CustomerKG/campaign_iri>(de.campaign_id) as virtrdf:CustomerKG-decision-campaign .
    <http://demo.openlinksw.com/schemas/CustomerKG/decision_iri>(de.decision_id) <http://demo.openlinksw.com/schemas/CustomerKG/usesOffer> <http://demo.openlinksw.com/schemas/CustomerKG/offer_iri>(de.offer_id) where (^{de.}^.offer_id is not null) as virtrdf:CustomerKG-decision-offer .
    <http://demo.openlinksw.com/schemas/CustomerKG/decision_iri>(de.decision_id) <http://demo.openlinksw.com/schemas/CustomerKG/viaChannel> <http://demo.openlinksw.com/schemas/CustomerKG/channel_iri>(de.channel_id) where (^{de.}^.channel_id is not null) as virtrdf:CustomerKG-decision-channel .
    <http://demo.openlinksw.com/schemas/CustomerKG/decision_iri>(dg.decision_id) <http://demo.openlinksw.com/schemas/CustomerKG/constrainedBy> <http://demo.openlinksw.com/schemas/CustomerKG/guardrail_iri>(dg.guardrail_id) as virtrdf:CustomerKG-decision-guardrail .
    <http://demo.openlinksw.com/schemas/CustomerKG/context_iri>(ci.item_id) <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://demo.openlinksw.com/schemas/CustomerKG/ContextItem> as virtrdf:CustomerKG-context-type .
    <http://demo.openlinksw.com/schemas/CustomerKG/context_iri>(ci.item_id) <http://demo.openlinksw.com/schemas/CustomerKG/contextType> ci.context_type as virtrdf:CustomerKG-context-kind .
    <http://demo.openlinksw.com/schemas/CustomerKG/context_iri>(ci.item_id) <http://demo.openlinksw.com/schemas/CustomerKG/contextKey> ci.context_key as virtrdf:CustomerKG-context-key .
    <http://demo.openlinksw.com/schemas/CustomerKG/context_iri>(ci.item_id) <http://demo.openlinksw.com/schemas/CustomerKG/contextValue> ci.context_value as virtrdf:CustomerKG-context-value .
    <http://demo.openlinksw.com/schemas/CustomerKG/context_iri>(ci.item_id) <http://demo.openlinksw.com/schemas/CustomerKG/informs> <http://demo.openlinksw.com/schemas/CustomerKG/decision_iri>(ci.decision_id) as virtrdf:CustomerKG-context-decision .
    <http://demo.openlinksw.com/schemas/CustomerKG/outcome_iri>(ou.outcome_id) <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://demo.openlinksw.com/schemas/CustomerKG/Outcome> as virtrdf:CustomerKG-outcome-type .
    <http://demo.openlinksw.com/schemas/CustomerKG/outcome_iri>(ou.outcome_id) <http://demo.openlinksw.com/schemas/CustomerKG/ordered> ou.ordered as virtrdf:CustomerKG-outcome-ordered .
    <http://demo.openlinksw.com/schemas/CustomerKG/outcome_iri>(ou.outcome_id) <http://demo.openlinksw.com/schemas/CustomerKG/revenueUsd> ou.revenue_usd as virtrdf:CustomerKG-outcome-revenue .
    <http://demo.openlinksw.com/schemas/CustomerKG/outcome_iri>(ou.outcome_id) <http://demo.openlinksw.com/schemas/CustomerKG/marginUsd> ou.margin_usd as virtrdf:CustomerKG-outcome-margin .
    <http://demo.openlinksw.com/schemas/CustomerKG/outcome_iri>(ou.outcome_id) <http://demo.openlinksw.com/schemas/CustomerKG/mediaCostUsd> ou.media_cost_usd as virtrdf:CustomerKG-outcome-media .
    <http://demo.openlinksw.com/schemas/CustomerKG/outcome_iri>(ou.outcome_id) <http://demo.openlinksw.com/schemas/CustomerKG/discountCostUsd> ou.discount_cost_usd as virtrdf:CustomerKG-outcome-discount .
    <http://demo.openlinksw.com/schemas/CustomerKG/outcome_iri>(ou.outcome_id) <http://demo.openlinksw.com/schemas/CustomerKG/repeatVisit30d> ou.repeat_visit_30d as virtrdf:CustomerKG-outcome-repeat .
    <http://demo.openlinksw.com/schemas/CustomerKG/outcome_iri>(ou.outcome_id) <http://demo.openlinksw.com/schemas/CustomerKG/windowEnd> ou.window_end as virtrdf:CustomerKG-outcome-window .
    <http://demo.openlinksw.com/schemas/CustomerKG/outcome_iri>(ou.outcome_id) <http://demo.openlinksw.com/schemas/CustomerKG/ofDecision> <http://demo.openlinksw.com/schemas/CustomerKG/decision_iri>(ou.decision_id) as virtrdf:CustomerKG-outcome-decision .
  }
} ;
The identity graph: reconciliation by reasoning
-- 1. Vocabulary: declare the identifying property inverse functional, in its own graph
DB.DBA.TTLP(file_to_string_output('customerkg-identity-vocabulary-claude_sonnet_5_5-1.ttl'), '', 'http://demo.openlinksw.com/schemas/CustomerKG/identity/');
-- The Turtle loaded by that call:
@prefix ckg: <http://demo.openlinksw.com/schemas/CustomerKG/> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix schema: <http://schema.org/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

<http://demo.openlinksw.com/schemas/CustomerKG/identity/> a owl:Ontology ;
    rdfs:label "CustomerKG identity reconciliation vocabulary"@en .

ckg:emailHash a owl:InverseFunctionalProperty, owl:DatatypeProperty ;
    rdfs:isDefinedBy <http://demo.openlinksw.com/schemas/CustomerKG/identity/> ;
    rdfs:label "email hash"@en ;
    rdfs:comment "A pseudonymous hash of an email address. Two records that carry the same hash describe the same person, so the property is declared inverse functional and a reasoner derives owl:sameAs between them."@en ;
    rdfs:domain schema:Person ;
    rdfs:range xsd:string .

-- 2. Rule set: derive reasoning rules from that vocabulary
DB.DBA.rdfs_rule_set('urn:customerkg:identity-inference', 'http://demo.openlinksw.com/schemas/CustomerKG/identity/');

-- 3. Identity graph, part one: facts derived from the live RDF View (nothing is retyped)
SPARQL PREFIX ckg: <http://demo.openlinksw.com/schemas/CustomerKG/> PREFIX schema: <http://schema.org/>
INSERT INTO GRAPH <http://demo.openlinksw.com/CustomerKG/identity#> { ?c ckg:emailHash ?v }
WHERE { GRAPH <http://demo.openlinksw.com/CustomerKG#> { ?s a ckg:IdentitySignal ; ckg:signalKind "email" ; ckg:signalValue ?v ; ckg:resolvesTo ?c } };

SPARQL PREFIX ckg: <http://demo.openlinksw.com/schemas/CustomerKG/> PREFIX schema: <http://schema.org/>
INSERT INTO GRAPH <http://demo.openlinksw.com/CustomerKG/identity#> { ?c a schema:Person ; schema:name ?n }
WHERE { GRAPH <http://demo.openlinksw.com/CustomerKG#> { ?c a ckg:Customer ; schema:name ?n } };

-- 4. Identity graph, part two: records from other systems, one explicit owl:sameAs, one record that matches nobody
SPARQL PREFIX ckg: <http://demo.openlinksw.com/schemas/CustomerKG/> PREFIX schema: <http://schema.org/> PREFIX owl: <http://www.w3.org/2002/07/owl#>
INSERT DATA INTO GRAPH <http://demo.openlinksw.com/CustomerKG/identity#> {
  <http://demo.openlinksw.com/CustomerKG/external/crm/501#this> a schema:Person ; schema:name "A. Quinn (CRM export)" ; ckg:emailHash "hem:a1f3" .
  <http://demo.openlinksw.com/CustomerKG/external/app/77#this> a schema:Person ; schema:name "Avery Q. (app profile)" ; ckg:emailHash "hem:a1f3" .
  <http://demo.openlinksw.com/CustomerKG/external/crm/502#this> a schema:Person ; schema:name "B. Moreno (CRM export)" ; ckg:emailHash "hem:b2c4" .
  <http://demo.openlinksw.com/CustomerKG/external/crm/599#this> a schema:Person ; schema:name "Unmatched (CRM export)" ; ckg:emailHash "hem:ffff" .
  <http://demo.openlinksw.com/CustomerKG/external/loyalty/LY-10001#this> a schema:Person ; schema:name "Loyalty member LY-10001" ; owl:sameAs <http://demo.openlinksw.com/CustomerKG/customer/1#this> .
};
The RDF View the generator proposes for the attached table
SPARQL
prefix DbxSales: <http://demo.openlinksw.com/schemas/DbxSales/> 
create iri class DbxSales:sales_transactions_viritual_odbc "http://^{URIQADefaultHost}^/DbxSales/sales_transactions_viritual_odbc/transactionID/%ld#this" (in _transactionID integer not null) . ;


SPARQL
prefix DbxSales: <http://demo.openlinksw.com/schemas/DbxSales/> 
prefix aowl: <http://bblfish.net/work/atom-owl/2006-06-06/> 
alter quad storage virtrdf:DefaultQuadStorage 
 from "databricks"."bakehouse"."sales_transactions_viritual_odbc" as sales_transactions_viritual_odbc_s
 { 
   create DbxSales:qm-sales_transactions_viritual_odbc as graph iri ("http://^{URIQADefaultHost}^/DbxSales#") option (exclusive) 
    { 
      # Maps from columns of "databricks.bakehouse.sales_transactions_viritual_odbc"
      DbxSales:sales_transactions_viritual_odbc (sales_transactions_viritual_odbc_s."transactionID")  a DbxSales:sales_transactions_viritual_odbc ;
      DbxSales:transactionid sales_transactions_viritual_odbc_s."transactionID" as DbxSales:bakehouse-sales_transactions_viritual_odbc-transactionid ;
      DbxSales:customerid sales_transactions_viritual_odbc_s."customerID" as DbxSales:bakehouse-sales_transactions_viritual_odbc-customerid ;
      DbxSales:franchiseid sales_transactions_viritual_odbc_s."franchiseID" as DbxSales:bakehouse-sales_transactions_viritual_odbc-franchiseid ;
      DbxSales:datetime sales_transactions_viritual_odbc_s."dateTime" as DbxSales:bakehouse-sales_transactions_viritual_odbc-datetime ;
      DbxSales:product sales_transactions_viritual_odbc_s."product" as DbxSales:bakehouse-sales_transactions_viritual_odbc-product ;
      DbxSales:quantity sales_transactions_viritual_odbc_s."quantity" as DbxSales:bakehouse-sales_transactions_viritual_odbc-quantity ;
      DbxSales:unitprice sales_transactions_viritual_odbc_s."unitPrice" as DbxSales:bakehouse-sales_transactions_viritual_odbc-unitprice ;
      DbxSales:totalprice sales_transactions_viritual_odbc_s."totalPrice" as DbxSales:bakehouse-sales_transactions_viritual_odbc-totalprice ;
      DbxSales:paymentmethod sales_transactions_viritual_odbc_s."paymentMethod" as DbxSales:bakehouse-sales_transactions_viritual_odbc-paymentmethod ;
      DbxSales:cardnumber sales_transactions_viritual_odbc_s."cardNumber" as DbxSales:bakehouse-sales_transactions_viritual_odbc-cardnumber .

    }
 }

;

Query editor

▶ Run live on URIBurner SELECT: text/x-html+tr | DESCRIBE/CONSTRUCT: text/x-html-nice-turtle