Skip to content

Commit 8d14efd

Browse files
namedgraphclaude
andcommitted
Model Northwind prices and deliveries with in-domain schema.org terms
Several schema.org properties were carrying values outside their declared domains. Each is moved to the term the vocabulary actually provides: - Order lines are now schema:OrderItem *and* schema:Offer. schema:OrderItem has no price properties at all, so schema:price/schema:priceCurrency were off-domain; typing the line as the offer that was accepted for it makes them legal without splitting the line across two nodes. - schema:Offer's own schema:price is the price of the offer as a whole, so it replaces schema:totalPrice (a Reservation/Ticket property) as the line total. The historical unit price moves to a schema:UnitPriceSpecification with schema:priceType schema:ListPrice. - schema:discount is dropped. It is an Order-level property in schema.org, and the list price, quantity and line total already pin the per-line rate exactly - unlike a stored rate, they cannot disagree with each other. - Product prices move from schema:Product to a schema:Offer reached through schema:offers. A product is not an offer, so this one needs its own node. - Freight moves off schema:ParcelDelivery, which has no price property, onto a schema:DeliveryChargeSpecification reached via schema:priceSpecification from a shipping schema:Offer. - The dispatch date moves off schema:availableFrom (a DeliveryEvent property borrowed onto a ParcelDelivery, and meaning "available for pickup" rather than "dispatched") onto a real schema:DeliveryEvent hanging off schema:deliveryStatus. Orders never dispatched get no event at all. - schema:orderedItem loses its rdfs:domain/rdfs:range. schema.org allows Order|OrderItem -> OrderItem|Product|Service, which a single domain/range pair cannot express; the narrowed pair entailed every OrderItem is a Product. Also fixes a pre-existing import bug: shipPostalCode sat in the required BGP but 19 orders have none, so those orders never entered the graph at all. Making it OPTIONAL restores them, taking the import from 811 to 830 orders and revenue from 1,215,812 to 1,265,792 - the canonical Northwind figure. Consuming queries follow the new structure. The six revenue queries drop their ?quantity * ?price * (1 - ?discount) recomputation for the materialised line total, which also settles a 0.1 discrepancy between the carrier chart and the rest. SelectProductsFromSupplier traverses schema:offers/schema:price. Verified by running all eleven import queries over the real CSVs and all 28 view and chart queries against the resulting graph: 830 orders, 2155 lines, freight totalling 64,943, and top product/employee/customer/category all matching canonical Northwind. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SYifJzEkGsTcJsuB4fGRAc
1 parent 170f3b5 commit 8d14efd

10 files changed

Lines changed: 168 additions & 51 deletions

File tree

demo/northwind-traders/admin/model/ns.ttl

Lines changed: 82 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,32 @@ schema:ParcelDelivery a owl:Class ;
4444
rdfs:label "Delivery" ;
4545
rdfs:isDefinedBy : .
4646

47+
schema:DeliveryEvent a owl:Class ;
48+
rdfs:label "Shipment" ;
49+
rdfs:isDefinedBy : .
50+
51+
schema:Offer a owl:Class ;
52+
rdfs:label "Offer" ;
53+
rdfs:isDefinedBy : .
54+
55+
schema:PriceSpecification a owl:Class ;
56+
rdfs:label "Price specification" ;
57+
rdfs:isDefinedBy : .
58+
59+
schema:DeliveryChargeSpecification a owl:Class ;
60+
rdfs:label "Delivery charge" ;
61+
rdfs:subClassOf schema:PriceSpecification ;
62+
rdfs:isDefinedBy : .
63+
64+
schema:UnitPriceSpecification a owl:Class ;
65+
rdfs:label "Unit price" ;
66+
rdfs:subClassOf schema:PriceSpecification ;
67+
rdfs:isDefinedBy : .
68+
69+
schema:DeliveryMethod a owl:Class ;
70+
rdfs:label "Delivery method" ;
71+
rdfs:isDefinedBy : .
72+
4773
schema:OrderItem a owl:Class ;
4874
rdfs:label "Order item" ;
4975
rdfs:isDefinedBy : .
@@ -100,6 +126,53 @@ schema:orderDelivery a owl:ObjectProperty ;
100126
rdfs:range schema:ParcelDelivery ;
101127
rdfs:isDefinedBy : .
102128

129+
schema:acceptedOffer a owl:ObjectProperty ;
130+
rdfs:label "Accepted offer" ;
131+
rdfs:domain schema:Order ;
132+
rdfs:range schema:Offer ;
133+
rdfs:isDefinedBy : .
134+
135+
schema:offers a owl:ObjectProperty ;
136+
rdfs:label "Offer" ;
137+
rdfs:domain schema:Product ;
138+
rdfs:range schema:Offer ;
139+
rdfs:isDefinedBy : .
140+
141+
schema:priceType a owl:ObjectProperty ;
142+
rdfs:label "Price type" ;
143+
rdfs:domain schema:UnitPriceSpecification ;
144+
rdfs:isDefinedBy : .
145+
146+
schema:itemOffered a owl:ObjectProperty ;
147+
rdfs:label "Product" ;
148+
rdfs:domain schema:Offer ;
149+
rdfs:range schema:Product ;
150+
rdfs:isDefinedBy : .
151+
152+
schema:priceSpecification a owl:ObjectProperty ;
153+
rdfs:label "Price specification" ;
154+
rdfs:domain schema:Offer ;
155+
rdfs:range schema:PriceSpecification ;
156+
rdfs:isDefinedBy : .
157+
158+
schema:appliesToDeliveryMethod a owl:ObjectProperty ;
159+
rdfs:label "Delivery method" ;
160+
rdfs:domain schema:DeliveryChargeSpecification ;
161+
rdfs:range schema:DeliveryMethod ;
162+
rdfs:isDefinedBy : .
163+
164+
schema:deliveryStatus a owl:ObjectProperty ;
165+
rdfs:label "Shipment" ;
166+
rdfs:domain schema:ParcelDelivery ;
167+
rdfs:range schema:DeliveryEvent ;
168+
rdfs:isDefinedBy : .
169+
170+
schema:partOfOrder a owl:ObjectProperty ;
171+
rdfs:label "Order" ;
172+
rdfs:domain schema:ParcelDelivery ;
173+
rdfs:range schema:Order ;
174+
rdfs:isDefinedBy : .
175+
103176
schema:deliveryAddress a owl:ObjectProperty ;
104177
rdfs:label "Delivery address" ;
105178
rdfs:domain schema:ParcelDelivery ;
@@ -131,8 +204,6 @@ schema:sponsor a owl:ObjectProperty ;
131204

132205
schema:orderedItem a owl:ObjectProperty ;
133206
rdfs:label "Ordered item" ;
134-
rdfs:domain schema:Order ;
135-
rdfs:range schema:Product ;
136207
rdfs:isDefinedBy : .
137208

138209
schema:orderStatus a owl:ObjectProperty ;
@@ -220,22 +291,19 @@ schema:orderQuantity a owl:DatatypeProperty ;
220291
rdfs:domain schema:OrderItem ;
221292
rdfs:isDefinedBy : .
222293

223-
schema:discount a owl:DatatypeProperty ;
224-
rdfs:label "Discount" ;
225-
rdfs:domain schema:OrderItem ;
226-
rdfs:isDefinedBy : .
227-
294+
# left without an rdfs:domain on purpose: schema:price is carried both by the order
295+
# lines (as schema:Offer) and by the freight charge (as schema:DeliveryChargeSpecification)
228296
schema:price a owl:DatatypeProperty ;
229297
rdfs:label "Price" ;
230298
rdfs:isDefinedBy : .
231299

232-
schema:totalPrice a owl:DatatypeProperty ;
233-
rdfs:label "Total price" ;
300+
schema:priceCurrency a owl:DatatypeProperty ;
301+
rdfs:label "Currency" ;
234302
rdfs:isDefinedBy : .
235303

236-
schema:availableFrom a owl:DatatypeProperty ;
237-
rdfs:label "Shipped date" ;
238-
rdfs:domain schema:ParcelDelivery ;
304+
schema:startDate a owl:DatatypeProperty ;
305+
rdfs:label "Shipped" ;
306+
rdfs:domain schema:DeliveryEvent ;
239307
rdfs:isDefinedBy : .
240308

241309
schema:orderDate a owl:DatatypeProperty ;
@@ -354,7 +422,7 @@ WHERE
354422
{ ?product a schema:Product ;
355423
schema:provider $about ;
356424
schema:category ?category ;
357-
schema:price ?price
425+
schema:offers/schema:price ?price
358426
}
359427
}
360428
ORDER BY ?product
@@ -440,8 +508,7 @@ WHERE
440508
{ $about schema:orderedItem ?item .
441509
?item schema:orderedItem ?product ;
442510
schema:orderQuantity ?quantity ;
443-
schema:price ?price ;
444-
schema:totalPrice ?lineTotal
511+
schema:price ?lineTotal
445512
}
446513
}
447514
ORDER BY ?item

demo/northwind-traders/categories.ttl

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,7 @@ WHERE {
5757
GRAPH ?orderGraph {
5858
?order schema:orderedItem ?orderItem .
5959
?orderItem schema:orderedItem ?product ;
60-
schema:orderQuantity ?quantity ;
61-
schema:price ?price ;
62-
schema:discount ?discount .
63-
BIND(?quantity * ?price * (1 - ?discount) AS ?sale)
60+
schema:price ?sale .
6461
}
6562
GRAPH ?productGraph {
6663
?product schema:category ?category .

demo/northwind-traders/customers.ttl

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,7 @@ WHERE {
4747
GRAPH ?orderGraph {
4848
?order schema:customer ?customer ;
4949
schema:orderedItem ?orderItem .
50-
?orderItem schema:orderQuantity ?quantity ;
51-
schema:price ?price ;
52-
schema:discount ?discount .
53-
BIND(?quantity * ?price * (1 - ?discount) AS ?sale)
50+
?orderItem schema:price ?sale .
5451
}
5552
GRAPH ?customerGraph {
5653
?customer schema:legalName ?companyName .

demo/northwind-traders/employees.ttl

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,7 @@ WHERE {
5555
GRAPH ?orderGraph {
5656
?order schema:broker ?employee ;
5757
schema:orderedItem ?orderItem .
58-
?orderItem schema:orderQuantity ?quantity ;
59-
schema:price ?price ;
60-
schema:discount ?discount .
61-
BIND(?quantity * ?price * (1 - ?discount) AS ?sale)
58+
?orderItem schema:price ?sale .
6259
}
6360
GRAPH ?employeeGraph {
6461
?employee schema:givenName ?givenName ;

demo/northwind-traders/orders.ttl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ WHERE {
174174
schema:orderDate ?orderDate ;
175175
schema:orderDelivery ?delivery ;
176176
schema:orderedItem ?orderItem .
177-
?orderItem schema:totalPrice ?lineTotal .
177+
?orderItem schema:price ?lineTotal .
178178
?delivery schema:provider ?shipper .
179179
}
180180
GRAPH ?shipperGraph {

demo/northwind-traders/orders/order_details.rq

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,32 @@ CONSTRUCT
77
{
88
GRAPH ?graph
99
{
10-
?graph foaf:topic ?orderItem .
10+
?graph foaf:topic ?orderItem, ?listPrice .
1111

1212
?order schema:orderedItem ?orderItem .
1313

14-
?orderItem a schema:OrderItem ;
14+
# schema:OrderItem carries no price in schema.org - its only properties are
15+
# orderedItem, orderQuantity, orderItemNumber, orderItemStatus and orderDelivery.
16+
# Prices belong to schema:Offer, so the line is both: the item that was ordered
17+
# and the offer that was accepted for it. schema:Offer's own schema:price is the
18+
# price of the offer as a whole, i.e. what this line came to.
19+
?orderItem a schema:OrderItem, schema:Offer ;
1520
schema:orderedItem ?product ;
21+
schema:itemOffered ?product ;
1622
schema:orderQuantity ?quantity ;
17-
schema:discount ?discount ;
23+
schema:price ?lineTotal ;
24+
schema:priceCurrency "USD" ;
25+
schema:priceSpecification ?listPrice .
26+
27+
# Northwind's per-line discount rate has no home in schema.org - schema:discount
28+
# is an Order-level property. Nothing is lost by dropping it: the undiscounted
29+
# unit price below, the quantity above and the line total pin the rate exactly,
30+
# and unlike a stored rate they cannot disagree with each other.
31+
?listPrice a schema:UnitPriceSpecification ;
32+
dct:title "List price" ;
33+
schema:priceType schema:ListPrice ;
1834
schema:price ?price ;
19-
schema:totalPrice ?lineTotal .
35+
schema:priceCurrency "USD" .
2036
}
2137
}
2238
WHERE
@@ -30,6 +46,7 @@ WHERE
3046
BIND(uri(concat(str($base), "orders/", encode_for_uri(?orderID), "/")) AS ?graph)
3147
BIND(uri(concat(str(?graph), "#this")) AS ?order)
3248
BIND(uri(concat(str(?graph), "#", STRUUID())) AS ?orderItem)
49+
BIND(uri(concat(str(?orderItem), "-list-price")) AS ?listPrice)
3350
BIND(uri(concat(str($base), "products/", encode_for_uri(?productID), "/#this")) AS ?product)
3451
BIND (STRDT(?unitPrice, xsd:float) AS ?price)
3552
BIND (STRDT(?quantityString, xsd:integer) AS ?quantity)

demo/northwind-traders/orders/orders.rq

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ CONSTRUCT
99
{
1010
?graph dct:title ?orderID ;
1111
foaf:primaryTopic ?order ;
12-
foaf:topic ?orderDelivery, ?deliveryAddress .
12+
foaf:topic ?orderDelivery, ?deliveryAddress, ?shipment,
13+
?shippingOffer, ?freightCharge .
1314

1415
?order a schema:Order ;
1516
schema:identifier ?orderID ;
@@ -18,16 +19,41 @@ CONSTRUCT
1819
schema:broker ?employee ;
1920
schema:orderDate ?orderDate ;
2021
schema:orderStatus ?status ;
22+
schema:acceptedOffer ?shippingOffer ;
2123
schema:orderDelivery ?orderDelivery .
2224

2325
?orderDelivery a schema:ParcelDelivery ;
2426
foaf:page ?graph ;
27+
schema:partOfOrder ?order ;
2528
schema:expectedArrivalUntil ?requiredDate ;
26-
schema:availableFrom ?shippedDate ;
27-
schema:price ?freight ;
29+
schema:deliveryStatus ?shipment ;
2830
schema:deliveryAddress ?deliveryAddress ;
2931
schema:provider ?shipper .
3032

33+
# schema:ParcelDelivery has no price property either. A delivery charge is a
34+
# schema:DeliveryChargeSpecification, and the only way into a PriceSpecification
35+
# is schema:priceSpecification from an Offer - so the freight the customer
36+
# accepted is spelled out as exactly that.
37+
?shippingOffer a schema:Offer ;
38+
foaf:page ?graph ;
39+
dct:title "Shipping" ;
40+
schema:priceSpecification ?freightCharge .
41+
42+
?freightCharge a schema:DeliveryChargeSpecification ;
43+
foaf:page ?graph ;
44+
dct:title "Freight" ;
45+
schema:price ?freight ;
46+
schema:priceCurrency "USD" ;
47+
schema:appliesToDeliveryMethod schema:ParcelService .
48+
49+
# schema:ParcelDelivery has no date of its own for "when did this leave the warehouse".
50+
# The vocabulary models the journey as a series of schema:DeliveryEvent legs hanging
51+
# off schema:deliveryStatus; Northwind knows exactly one leg, the dispatch.
52+
?shipment a schema:DeliveryEvent ;
53+
foaf:page ?graph ;
54+
dct:title ?shipmentTitle ;
55+
schema:startDate ?shippedDate .
56+
3157
?deliveryAddress a schema:PostalAddress ;
3258
foaf:page ?graph ;
3359
schema:addressCountry ?shipCountry ;
@@ -49,22 +75,35 @@ WHERE
4975
<#shipName> ?shipName ;
5076
<#shipAddress> ?shipAddress ;
5177
<#shipCity> ?shipCity ;
52-
<#shipPostalCode> ?shipPostalCode ;
5378
<#shipCountry> ?shipCountry .
5479

80+
# ?shipment is bound inside the OPTIONAL so that the 21 orders that were never
81+
# dispatched get no DeliveryEvent at all, rather than an empty one. It is built from
82+
# $base (a constant, substituted before execution) and a re-matched ?orderID rather
83+
# than from ?graph: an OPTIONAL group is evaluated standalone, so variables BINDed in
84+
# the enclosing group are not in scope inside it.
5585
OPTIONAL {
56-
?order_row <#shippedDate> ?shippedDateString
86+
?order_row <#orderID> ?orderID ;
87+
<#shippedDate> ?shippedDateString .
5788
BIND(strdt(?shippedDateString, xsd:date) AS ?shippedDate)
89+
BIND(uri(concat(str($base), "orders/", encode_for_uri(?orderID), "/#shipment")) AS ?shipment)
90+
BIND(concat("Shipped ", ?shippedDateString) AS ?shipmentTitle)
5891
}
5992
OPTIONAL {
6093
?order_row <#shipRegion> ?shipRegion
6194
}
95+
# 19 orders carry no postal code; requiring one here dropped them from the import
96+
OPTIONAL {
97+
?order_row <#shipPostalCode> ?shipPostalCode
98+
}
6299

63100
BIND(uri(concat(str($base), "orders/")) AS ?container)
64101
BIND(uri(concat(str(?container), encode_for_uri(?orderID), "/")) AS ?graph)
65102
BIND(uri(concat(str(?graph), "#this")) AS ?order)
66103
BIND(uri(concat(str(?graph), "#delivery")) AS ?orderDelivery)
67104
BIND(uri(concat(str(?graph), "#delivery-address")) AS ?deliveryAddress)
105+
BIND(uri(concat(str(?graph), "#shipping")) AS ?shippingOffer)
106+
BIND(uri(concat(str(?graph), "#freight")) AS ?freightCharge)
68107
BIND(uri(concat(str($base), "employees/", encode_for_uri(?employeeID), "/#this")) AS ?employee)
69108
BIND(uri(concat(str($base), "customers/", encode_for_uri(?customerID), "/#this")) AS ?customer)
70109
BIND(uri(concat(str($base), "shippers/", encode_for_uri(?shipVia), "/#this")) AS ?shipper)

demo/northwind-traders/products.ttl

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,7 @@ WHERE
5858
{ GRAPH ?orderGraph
5959
{ ?order schema:orderedItem ?orderItem .
6060
?orderItem schema:orderedItem ?product ;
61-
schema:orderQuantity ?quantity ;
62-
schema:price ?price ;
63-
schema:discount ?discount
64-
BIND (?quantity * ?price * (1 - ?discount) AS ?sale)
61+
schema:price ?sale
6562
}
6663
GRAPH ?productGraph
6764
{ ?product schema:name ?productName }

demo/northwind-traders/products/products.rq

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ CONSTRUCT
88
GRAPH ?graph
99
{
1010
?graph dct:title ?productName ;
11-
foaf:primaryTopic ?product .
11+
foaf:primaryTopic ?product ;
12+
foaf:topic ?offer .
1213

1314
?product a schema:Product ;
1415
schema:identifier ?productID ;
@@ -17,11 +18,21 @@ CONSTRUCT
1718
schema:provider ?supplier ;
1819
schema:category ?category ;
1920
schema:description ?quantityPerUnit ;
20-
schema:price ?unitPrice .
21+
schema:offers ?offer .
2122
# ?unitsInStock ;
2223
# ?unitsOnOrder ;
2324
# ?reorderLevel ;
2425
# ?discontinued - schema:Discontinued
26+
27+
# schema:Product has no price of its own - schema:price belongs to schema:Offer,
28+
# which a product reaches through schema:offers. This is the catalogue price;
29+
# what a given order line was actually billed at lives on that line's own offer.
30+
?offer a schema:Offer ;
31+
foaf:page ?graph ;
32+
dct:title "List price" ;
33+
schema:itemOffered ?product ;
34+
schema:price ?unitPrice ;
35+
schema:priceCurrency "USD" .
2536
}
2637
}
2738
WHERE
@@ -40,6 +51,7 @@ WHERE
4051
BIND (uri(concat(str($base), "products/")) AS ?container)
4152
BIND(uri(concat(str(?container), encode_for_uri(?productID), "/")) AS ?graph)
4253
BIND(uri(concat(str(?graph), "#this")) AS ?product)
54+
BIND(uri(concat(str(?graph), "#offer")) AS ?offer)
4355
BIND(uri(concat(str($base), "suppliers/", encode_for_uri(?supplierID), "/#this")) AS ?supplier)
4456
BIND(uri(concat(str($base), "categories/", encode_for_uri(?categoryID), "/#this")) AS ?category)
4557
BIND (STRDT(?unitPriceString, xsd:float) AS ?unitPrice)

0 commit comments

Comments
 (0)