Leif Harald Karlsen
Constraints: What must be true about the data
:Person(:per)
without nameSemantics: What is true in the domain (of the data)
:Person subClassOf :hasName some xsd:string
:Person(:per)
without name:per :hasName [ rdf:type xsd:string ] .
PRIMARY KEY
, UNIQUE
and
FOREIGN KEY
NOT NULL
CHECK
ex:Employee
-instances must be
ex:worksFor
-related to a
ex:Company
-instanceex:Employee[
ottr:IRI ?person,
xsd:string ?name,
xsd:string ?phone,
xsd:int ?age,
! ottr:IRI ?worksFor
] :: {
o-rdf:Type(?person, ex:Employee),
ottr:Triple(?person, ex:hasName, ?name),
ottr:Triple(?person, ex:hasPhoneNumber, ?phone),
ottr:Triple(?person, ex:hasAge, ?age),
ottr:Triple(?person, ex:worksFor, ?woksFor)
} .
ex:Company[
ottr:IRI ?company,
xsd:string ?name
] :: {
o-rdf:Type(?company, ex:Company),
ottr:Triple(?person, ex:hasName, ?name)
} .
# OK
ex:Employee(ex:per, "Per", "98765432", 32, ex:uio) .
ex:Employee(ex:kari, "Kari", "123456", 34, ex:dnb) .
ex:Company(ex:uio, "Universitetet i Oslo") .
ex:Company(ex:ntnu, "NTNU") .
# NOT OK
ex:Employee(ex:ole, "Ole", 12345678, "34", ex:uio) .
ex:Employee(ex:nils, "Nils", "23456789", 34) .
ex:Employee(ex:mari, "Mari", "34567890", 34, _:b) .
ex:Company(ex:ruter, "Ruter", <http://ruter.no>) .
Vocabulary:
ex:Employee a owl:Class .
ex:Company a owl:Class .
ex:hasName a owl:DatatypeProperty;
rdfs:range xsd:string .
ex:age a owl:DatatypeProperty;
rdfs:domain ex:Employee;
rdfs:range xsd:integer .
ex:hasPhoneNumber a owl:DatatypeProperty;
rdfs:domain ex:Employee;
rdfs:range xsd:string .
ex:worksFor a owl:ObjectProperty;
rdfs:domain ex:Employee;
rdfs:range ex:Company .
ex:contactPerson a owl:ObjectProperty;
rdfs:domain ex:Company;
rdfs:range ex:Person .
SHACL Shapes:
ex:EmployeeShape
a sh:NodeShape ;
sh:targetClass ex:Employee ; # Applies to all individuals of ex:Employee
sh:property [
sh:path ex:hasPhoneNumber ;
sh:datatype xsd:string ;
sh:pattern "^\\d{8}$" ; # Phone numbers are strings of 8 digits
] ;
sh:property [
sh:path ex:hasName ;
sh:minCount 1 ; # All employees must have at least one name
sh:maxCount 1 ; # All employees must have at most one name
sh:datatype xsd:string ;
sh:pattern "^[A-Z][a-z]+$" ; # Names starts with capitals, then lower-case letters
] ;
sh:property [
sh:path ex:age ;
sh:minInclusive 16 ; # Age is an xsd:int >= 16
sh:maxInclusive 150 ; # Age is <= 150
] ;
sh:property [
sh:path ex:worksFor ;
sh:minCount 1 ; # All employees must work for a company
sh:node ex:CompanyShape ; # The object must conform to the ex:CompanyShape
] .
ex:CompanyShape
a sh:NodeShape ;
sh:targetClass ex:Company ; # Applies to all individuals of ex:Employee
sh:property [
sh:path ex:hasName ;
sh:minCount 1 ; # All companies must have at least one name
sh:datatype xsd:string ; # But can be any string value
] .
Valid data:
ex:per rdf:type ex:Employee ;
ex:hasPhoneNumber "12345678" ;
ex:hasName "Per" ;
ex:hasAge 32 ;
ex:worksFor ex:UiO .
ex:kari rdf:type ex:Person ; # Not checked (not ex:Employee)
ex:hasPhoneNumber "98765432" ;
ex:hasName "Kari" ;
ex:worksFor ex:UiO .
ex:UiO rdf:type ex:Company ;
ex:hasName "Universitetet i Oslo" ;
ex:hasName "University of Oslo" .
Invalid data:
ex:peter rdf:type ex:Employee ;
ex:hasPhoneNumber "12345678" ;
ex:hasAge 32 ;
ex:hasName "Per" . # Missing ex:worksFor-relationship
ex:kari rdf:type ex:Employee ;
ex:hasPhoneNumber 12345678 ; # Wrong type
ex:hasName "Kari" ;
ex:hasName "Karry" ; # Two names
ex:worksFor ex:UiO . # ex:uib does not conform to the ex:CompanyShape
ex:UiO rdf:type ex:Company . # No name
ex:NTNU rdf:type ex:Company ;
ex:name "NTNU" . # Wrong relationship
ex:EmployeeShape
a sh:NodeShape ;
sh:targetClass ex:Employee ;
sh:property [
sh:path ex:hasPhoneNumber ;
sh:datatype xsd:string ;
sh:pattern "^\\d{8}$" ;
] ;
sh:property [
sh:path ex:hasName ;
sh:minCount 1 ;
sh:maxCount 1 ;
sh:datatype xsd:string ;
sh:pattern "^[A-Z][a-z]+$" ;
] ;
sh:property [
sh:path ex:age ;
sh:minInclusive 16 ;
sh:maxInclusive 150 ;
] ;
sh:property [
sh:path ex:worksFor ;
sh:minCount 1 ;
sh:node ex:CompanyShape ;
] .
ex:CompanyShape
above:ex:CompanyShape
# [...]
sh:property [
sh:path ex:hasContactPerson ;
sh:minCount 1 ;
sh:and ( # sh:and is logical conjunction of shapes
ex:EmployeeShape # Must be employee (Note: Recusive/cyclic)
[ # Must have a phone number
sh:property [
sh:path ex:hasPhoneNumber ;
sh:minCount 1 ;
]
]
)
] .
CREATE FUNCTION contactperson_trig_fn() RETURNS TRIGGER AS
$$
BEGIN
IF (SELECT phone IS NULL
FROM employee
WHERE eid = NEW.contactPerson
)
THEN
RAISE EXCEPTION 'Company ' || NEW.cname || ' has contact person without phone.';
END IF;
RETURN NEW;
END;
$$ language plpgsql;
CREATE TRIGGER contactperson_trig
BEFORE INSERT ON company
FOR EACH ROW EXECUTE PROCEDURE contactperson_trig_fn();
UPDATE
and DELETE
triggers as well-- Checks that all contactPersons have a phone number
-- Every answer is a violation of the constraint: "Every contact person must have phone number."
SELECT 'Contact person for company ' || c.cname
|| ' does not have a contact number!' AS violation
FROM employee AS e JOIN company AS c ON (e.worksFor = c.cid)
WHERE e.phone IS NULL;
SELECT (CONCAT("ERROR: Mising name for ", STR(?p)) AS ?error)
WHERE {
?p rdf:type ex:Employee .
FILTER NOT EXISTS { ?p ex:hasName ?n . }
}
Case 1:
# Mixing Turtle and OWL Manchester syntax here
:Company subClassOf
:hasContactPerson some (:Employee and :hasPhoneNumber some :PhoneNr ).
:abc a :Company ;
:hasContactPerson :mary .
:mary a :Employee ; :hasPhoneNr [ rdf:type :PhoneNr ] . #Inferred
Case 2:
:Company subClassOf
:hasContactPerson some (:Employee and :hasPhoneNumber some :PhoneNr ).
:id rdf:type owl:InverseFunctionalProperty .
:abc :hasContactPerson :mary .
:mary a :Person ;
:id "123" .
_:p :id "123" ;
:hasPhoneNumber "98765432" .
:mary :hasPhoneNr "987654332" . #Inferred
Note: Can speficy that values should be non-blank in SHACL
CREATE RELATION inconsitency(description text);
inconsistency('Company ' || pname || ' has contact person without phone number present!')
<- person(pid, pname, phone), company(cid, cname, pid) : phone IS NULL;
inconsistency('IRI ' || p || ' is both a ex:Person and a ex:Cat!')
<- rdf.type(p, qn('ex', 'Person')), rdf.type(p, qn('ex', 'Cat'));
CREATE FUNCTION inconsistency_fn() RETURNS trigger AS
$body$
BEGIN
RAISE EXCEPTION 'Inconsitency detected: ' || NEW.description;
RETURN NEW;
END;
$body$ language plpgsql;
CREATE TRIGGER inconsitency_trigger
BEFORE INSERT ON company
FOR EACH ROW EXECUTE PROCEDURE inconsitency_fn();
# (Mixing Turtle and OWL Manchester syntax here)
# Try to state "all companies MUST have a contact person that has a phone number"
:Company subClassOf
:hasContactPerson some (:Employee and :hasPhoneNumber some :PhoneNr ).
:abc a :Company ;
:hasContactPerson :mary .
:mary a :Employee ; :hasPhoneNr [ rdf:type :PhoneNr ] . #Inferred
"23"^^xsd:int
)
which is checkedrdf:type
)now
past
, present
and future
2021-01-21 10:15:00+01
)POINT(1.0 2.0)
,
LINESTRING(1.0 2.0, 2.1 3.2, 4.1 7.3)
here