The T-Box, part 2: properties and the c360 ontology (OWL)
Relationships and attributes, the domain-and-range trap that catches every SQL person, and a tour of the complete c360 model, which you will load into Stardog by the end of the chapter.
One line in an ontology can look like a check but isn't. Take :purchasedBy rdfs:domain :Order. A property's domain is the kind of thing the property is used on: the subject, the left-hand side of the triple (Section 6.2 covers it properly). The line reads like "only orders can have :purchasedBy." What it actually tells Stardog is: "if something has :purchasedBy, label it an Order." So if bad data says Wax Paper :purchasedBy Carolyn, Stardog doesn't refuse it. With reasoning on, it quietly labels Wax Paper an Order. That is wrong: Wax Paper is a product. This chapter shows why it happens, how the c360 kit avoids it, and ends with the full ontology (161 triples) loaded as build step 2.
6.1Properties: the columns and foreign keys of a graph
The ten classes from Chapter 5 have no columns: our :Customer has no name, no email, no address. That is deliberate. In a graph, columns and foreign keys are played by a property: anything used as the predicate of a triple, such as :firstName or :purchasedBy. A property has its own IRI and is declared on its own, outside any class, so that many classes can share it and the model can say things about the property itself (its label, what it connects). OWL splits properties into two kinds:
An object property (owl:ObjectProperty) links a thing to another thing, an IRI you can keep following. order :purchasedBy customer:225. This is your foreign key.
A datatype property (owl:DatatypeProperty) links a thing to a literal (string, number, date). customer:225 :firstName "Carolyn". This is your plain column.
Figure 6.1: Object properties are relationships (foreign keys); datatype properties are attributes (columns).
Where does the real c360 vocabulary come from? The c360 knowledge kit (Chapter 1) ships its whole ontology as one Turtle file, schema.ttl, in Stardog's public GitHub repository: https://github.com/stardog-union/knowledge-kits/blob/main/examples/c360/models/schema.ttl. You'll load all of it at the end of this chapter. Here is its entry for :firstName, unchanged:
The last two lines say which class the property is used on (Customer) and what kind of value it takes (a string). so: is short for schema.org, a shared vocabulary for describing data on the web. The textbook way to write these two facts is rdfs:domain and rdfs:range, but that way has a trap (the Wax Paper problem from the opening). Sections 6.2 and 6.3 show the trap and why the kit uses so: instead.
Term
What it is
Why it exists
Tiny example
Property
A predicate declared as a thing itself
So it can be shared and described
:firstName
Object property
Links a thing to a thing
Relationships (FKs)
:purchasedBy
Datatype property
Links a thing to a literal
Attributes (columns)
:city
Domain
The class of a property's subject
Says what the property is used on
purchasedBy: Order
Range
The class or datatype of its object
Says what values it takes
purchasedBy: Customer
rdfs:domain / rdfs:range
RDFS's domain and range: inference rules
Let a reasoner derive types
see Figure 6.2
so:domainIncludes / so:rangeIncludes
schema.org's version: descriptions only
Say "expected here" without side effects
:name so:domainIncludes :Vendor
owl:inverseOf
One relationship read both ways
Name the reverse direction
placedOrder ↔ purchasedBy
Reasoning schema
A named set of graphs Stardog treats as the T-Box
So the reasoner knows where the axioms are
c360_schema
Object properties point to things; datatype properties point to literals. Both are predicates, and both can have a domain and a range.
Example: :purchasedBy → a Customer; :firstName → a string.
6.2rdfs:domain and rdfs:range: rules, not checks
RDFS describes a property with two statements. Here they are for :purchasedBy, written the textbook way:
The semicolon makes this two triples, and the subject of both is the property itself, :purchasedBy. So these are facts about the property, not about any particular order or customer:
Subject
Predicate
Object
:purchasedBy
rdfs:domain
:Order
:purchasedBy
rdfs:range
:Customer
To read them, picture a real data triple that uses the property:
order:0e7e… :purchasedBy customer:225
left side right side
rdfs:domain :Order says the left side of a :purchasedBy triple is an Order.
rdfs:range :Customer says the right side is a Customer.
In plain words: "purchasedBy goes from an Order to a Customer." Every SQL person reads that as a check: "only orders may have :purchasedBy, and its value must be a customer." That reading is wrong, and why is the most useful idea in this chapter.
With reasoning on, Stardog doesn't check these two statements; it applies them. Whatever appears on the left of a :purchasedBy triple gets labelled an Order, and whatever appears on the right gets labelled a Customer.
Nothing is ever rejected. In an open world (Chapter 5) the reasoner assumes the data is right and the type merely unstated, so it fills it in.
Press Play to replay the riddle.
Figure 6.2: rdfs:domain is not a check. With reasoning on, it turns a bad triple into a wrong type, silently; counting orders now counts a product.
It gets worse when a property is shared, like the kit's :name. Written with rdfs:domain:
:name rdfs:domain :Category, :Customer, :Product, :Vendor . # do NOT do this
Several domains do not mean "one of these". Each is its own rule and each fires, so every named thing becomes a Category and a Customer and a Product and a Vendor; Carolyn would become a product. (The formal fix, an owl:unionOf class as the domain, is heavy and still an inference rule, not a check.)
rdfs:domain and rdfs:range label things; they never check. Checking is SHACL's job.
Example: one bad triple product:268 :purchasedBy customer:225 turns Wax Paper into an Order.
6.3so:domainIncludes and so:rangeIncludes: the kit's answer
The c360 kit avoids the trap by not using rdfs:domain and rdfs:range at all. It uses two predicates from schema.org (prefix so: = https://schema.org/), the search engines' vocabulary for describing web content:
so:domainIncludes: "this property is expected on things of this class." Several values mean "any of these", which is what people mean.
so:rangeIncludes: "values are expected to be of this class or datatype."
The key word is expected. These predicates have no formal meaning in RDFS or OWL, so Stardog's reasoner draws no conclusions from them. :name so:domainIncludes :Category, :Customer, :Product, :Vendor says what the modeler meant, and reasoning in Chapter 10 will never make Carolyn a product.
They are documentation that machines can read. Stardog Designer writes them when you draw a property between two classes, and model views use them to show which properties belong to which class. Voicebox and your agent read them when writing SPARQL; :name so:domainIncludes :Category, :Customer, :Product, :Vendor is exactly the hint that :name can appear on a category, a customer, a product or a vendor, so a question like "what is customer 225's name?" is answered with :name. (How each tool uses them internally is up to that tool; if a view looks empty, tell Claude.)
6.4owl:inverseOf, and a wrinkle in the kit
Every relationship reads two ways: "order purchased by customer" is also "customer placed order". SPARQL can already walk an arrow backward (?order :purchasedBy customer:225), but sometimes you want a name for the reverse direction. owl:inverseOf says two properties are one relationship read in opposite directions. A correct example (not in the kit): :placedOrder owl:inverseOf :purchasedBy. With that line in the model, order:0e7e… :purchasedBy customer:225 also means customer:225 :placedOrder order:0e7e…: the same fact, read from the other end.
An inverse must swap the two ends. Now read what schema.ttl actually says:
Both owl:inverseOf triples deserve a raised eyebrow. :rewardsAccountUsed goes Order → RewardsAccount and :hasRewards goes Customer → RewardsAccount: both point at the account, so they are not each other's reverse. Taken literally with reasoning on, "order O used account R" implies "R has rewards O". The second, :hasRewards owl:inverseOf :rewardsAccount, is sensible in intent (the account points back at its customer), but :rewardsAccount is declared nowhere and no mapping produces it; the open world allows that.
We load the kit exactly as shipped, because the rest of the book assumes that file, and with reasoning off (until Chapter 10) inverse axioms have no effect. The practical risk is contained but real: 1,560 of the 2,500 purchases carry a rewards account (none of Carolyn's do), so with reasoning on under this schema each of them yields a bogus inferred triple, and a query that also requires ?c a :Customer filters those rows out. Chapter 10 shows how to spot them. The lesson: an ontology is code, and code has bugs. If rewards accounts ever show up where customers should be in a reasoning query, look here first.
6.5The whole c360 ontology
You can now read every line of schema.ttl: 10 classes, 11 object properties and 22 datatype properties. Figure 6.3 draws the classes and relationships.
Press Play to build the model class by class.
Figure 6.3: The complete c360 ontology: 10 classes (boxes) and 11 object properties (arrows, drawn from so:domainIncludes to so:rangeIncludes). The colors are this book's annotation, not part of the ontology: the model itself has no idea where data lives (Section 6.6).
The 11 relationships
Property
Label
From
To
SQL equivalent
:purchasedBy
purchased by
Order
Customer
purchase.cid
:itemPurchased
purchased
Order
Product
purchase.pid
:cardUsed
purchase card
Order
CreditCard
purchase.card
:rewardsAccountUsed
rewards account used
Order
RewardsAccount
purchase.reward_account
:address
has address
Customer
Address
customer.location
:hasRewards
has rewards account
Customer
RewardsAccount
rewards_account.cid, reversed
:cardHolder
card holder
CreditCard
Customer
credit_card.cid
:inCategory
in category
Product
Category
product.dept
:vendor
has vendor
Product
Vendor
product.brand
:msrp
MSRP
Product
UnitPrice
a node built from product.price
:industry
in industry
Vendor
Industry
vendor.industry
Two graph habits hide in that table. :hasRewards points from customer to account, the opposite of the SQL foreign key: modelers choose the direction that reads best as a sentence. And :msrp points at a UnitPricenode carrying a price and a currency, not a bare number: a value turned into a thing so it can hold more facts.
★ Sensitive.:ssn and :cardNumber carry a second type, m:SensitiveProperty (prefix m: = urn:stardog:modules:): a "treat as sensitive" marker. Chapter 12 covers how Stardog can act on it, and why the book keeps PII in a separate, permissioned graph.
Money.:purchasePrice is described as decimal, :hasPrice as float. so:rangeIncludes is only a description, so the mappings decide the real datatype; Chapter 9 explains why spend must come from the decimal.
6.6What the ontology does not contain
Notice what schema.ttl does not mention: Aurora, Redshift, table or column names, cid, connection strings, SQL. It does not know there are 2,500 orders, or contain a single customer. It is purely a description of meaning: these kinds of things exist, and this is how they relate.
That separation is the point. Connecting meaning to where the data lives is the job of the mappings (Chapter 8 for Aurora, Chapter 9 for Redshift): "each row of c360.customer is a :Customer, and its first_name column is :firstName". Swap Redshift for Snowflake tomorrow and not one triple of the ontology changes; only a mapping does.
The flip side: model and mappings can drift apart, and the open world will not warn you. The Redshift mapping writes a vendor :homepage the ontology never declares, and no mapping produces the declared customer :thumbnail. A declaration is a promise of meaning, not a guarantee of values.
6.7OWL versus RDFS on one page
RDFS
OWL
What it is
The minimal schema vocabulary
A richer ontology language built on RDFS
Classes
rdfs:Class, rdfs:subClassOf
owl:Class, plus equivalent, disjoint and union classes, restrictions
No (it can detect contradictions, which is different)
Used in c360
labels, comments; subClassOf in Chapter 10
owl:Class, both property types, owl:inverseOf
Stardog lets you choose how much of OWL its reasoner applies. The default setting (called SL) covers the everyday parts you've met in this book, such as subclasses, inverses, and domain and range, and it also runs Stardog's own IF/THEN rules, like "big spender" (Chapter 10). You never need to change that setting for this book. In one line: RDFS names kinds of things and arranges them in hierarchies, OWL says more precisely how things relate, and neither one checks your data.
6.8Real build step 2: the complete ontology
Four parts: A adds three properties by hand, B loads the whole schema.ttl on top, C registers the model graph as a reasoning schema, D looks at the result in Studio. Everything goes into database kit-c360, graph <urn:stardog:training-c360:1.0:model>, the same graph as Chapter 5. Keep the reasoning toggle off throughout.
Check yourself
Is :hasRewards an object or a datatype property, and what is its SQL counterpart?
An object property: its value is a RewardsAccount node. Its SQL counterpart is the foreign key rewards_account.cid, read the other way (customer to account).
Hypothetically the model says :quantity rdfs:range xsd:integer, and someone loads order :quantity "lots". What does Stardog do?
It stores the triple: rdfs:range is an inference rule, not a check. To have the value reported, write a SHACL shape with sh:datatype xsd:integer (Chapter 11).
Why would :name rdfs:domain :Category, :Customer, :Product, :Vendor be a bug, when the kit's so:domainIncludes version is fine?
Each rdfs:domain triple is a separate rule, so with reasoning on every named thing would become all four classes. so:domainIncludes only documents "any of these"; the reasoner ignores it.
Where in the ontology does it say that customers come from Aurora?
Nowhere. The ontology describes meaning only; the mappings (Chapters 8 and 9) connect classes and properties to physical data.
Why is :rewardsAccountUsed owl:inverseOf :hasRewards suspicious?
An inverse must swap the ends. Both properties point at a RewardsAccount, so they are not each other's reverse; with reasoning on, rewards accounts would "have rewards" orders.
You load schema.ttl on top of the 34 triples you typed. Why 161 and not 195?
The 34 typed triples are identical to triples in the file, and a graph is a set, so re-inserting them adds nothing.
Chapter recap
Object properties link things to things (foreign keys); datatype properties link things to literals (columns). Properties stand alone and can be shared.
rdfs:domain and rdfs:range are inference rules that add types and never reject data; several domains mean all of them at once.
The kit uses so:domainIncludes / so:rangeIncludes: descriptive, "any of these", ignored by the reasoner, read by tools and AI.
owl:inverseOf names a relationship's reverse. The kit's two inverse axioms are questionable, harmless with reasoning off.
The c360 ontology: 10 classes, 11 relationships, 22 attributes, 161 triples; :ssn and :cardNumber are marked sensitive.
The ontology knows nothing about Aurora or Redshift; mappings (Chapters 8 and 9) connect it to data.
Build step 2 is done: the full model (161 triples) in <urn:stardog:training-c360:1.0:model>, registered as reasoning.schemas = c360_schema=urn:stardog:training-c360:1.0:model.
Paste it to Claude with what you saw (the counts or results), so we can confirm it matches before the next build step relies on it.