- Implemented `owl_to_mermaid.py` to convert OWL/Turtle files into Mermaid class diagrams. - Implemented `owl_to_plantuml.py` to convert OWL/Turtle files into PlantUML class diagrams. - Added two new PlantUML files for custodian multi-aspect diagrams.
36 KiB
SPARQL Query Library for Heritage Custodian Ontology
Version: 1.0.0
Schema Version: v0.7.0
Last Updated: 2025-11-22
Purpose: Query patterns for organizational structures, collections, and staff relationships
Table of Contents
- Prefixes
- Staff Queries
- Collection Queries
- Combined Staff + Collection Queries
- Organizational Change Queries
- Validation Queries (SPARQL)
- Advanced Temporal Queries
Prefixes
All queries use these standard prefixes:
PREFIX custodian: <https://nde.nl/ontology/hc/custodian/>
PREFIX org: <http://www.w3.org/ns/org#>
PREFIX pico: <https://w3id.org/pico/ontology/>
PREFIX schema: <https://schema.org/>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
PREFIX prov: <http://www.w3.org/ns/prov#>
PREFIX time: <http://www.w3.org/2006/time#>
1. Staff Queries
1.1 Find All Curators
Use Case: List all staff with curator roles across all institutions.
PREFIX custodian: <https://nde.nl/ontology/hc/custodian/>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT ?person ?personLabel ?unit ?unitName
WHERE {
?person a custodian:PersonObservation ;
custodian:staff_role "CURATOR" ;
custodian:unit_affiliation ?unit .
?unit custodian:unit_name ?unitName .
OPTIONAL { ?person rdfs:label ?personLabel }
}
ORDER BY ?unitName ?personLabel
Expected Result (from test data):
| person | personLabel | unit | unitName |
|---|---|---|---|
hc/person-obs/nl-rm/sophia-van-gogh/curator-dutch-paintings |
Sophia van Gogh | hc/org-unit/rm-paintings-dept |
Paintings Department |
Explanation: Filters PersonObservation by staff_role = "CURATOR" and joins with organizational units via unit_affiliation.
1.2 List Staff in Organizational Unit
Use Case: Find all staff members in a specific department or division.
PREFIX custodian: <https://nde.nl/ontology/hc/custodian/>
PREFIX org: <http://www.w3.org/ns/org#>
SELECT ?person ?role ?startDate ?endDate
WHERE {
<https://nde.nl/ontology/hc/org-unit/rm-paintings-dept>
org:hasMember ?person .
?person custodian:staff_role ?role .
OPTIONAL { ?person custodian:employment_start_date ?startDate }
OPTIONAL { ?person custodian:employment_end_date ?endDate }
}
ORDER BY ?role ?startDate
Expected Result (from test data):
| person | role | startDate | endDate |
|---|---|---|---|
hc/person-obs/nl-rm/sophia-van-gogh/curator-dutch-paintings |
CURATOR | 2015-06-01 | null |
hc/person-obs/nl-rm/pieter-de-vries/curator-flemish-paintings |
CURATOR | 2018-09-01 | null |
Explanation: Uses org:hasMember (same as custodian:staff_members) to retrieve all persons in the unit. Filters by specific unit URI.
1.3 Track Role Changes Over Time
Use Case: Find staff who have changed roles within an institution.
PREFIX custodian: <https://nde.nl/ontology/hc/custodian/>
PREFIX pico: <https://w3id.org/pico/ontology/>
SELECT ?personLabel ?role1 ?endDate1 ?role2 ?startDate2 ?unit
WHERE {
# First role observation
?obs1 a custodian:PersonObservation ;
pico:hasRole ?personLabel ;
custodian:staff_role ?role1 ;
custodian:employment_end_date ?endDate1 .
# Second role observation (same person, different role)
?obs2 a custodian:PersonObservation ;
pico:hasRole ?personLabel ;
custodian:staff_role ?role2 ;
custodian:employment_start_date ?startDate2 ;
custodian:unit_affiliation ?unit .
# Ensure second role starts after first ends
FILTER(?startDate2 >= ?endDate1)
# Ensure different roles
FILTER(?role1 != ?role2)
}
ORDER BY ?personLabel ?endDate1
Expected Result: Shows role transitions (e.g., ASSISTANT_CURATOR → CURATOR).
Explanation: Uses PiCo pattern to link multiple PersonObservation instances for the same person with different roles. Temporal filter ensures chronological order.
1.4 Find Staff by Time Period
Use Case: Who was working in a unit during a specific date range?
PREFIX custodian: <https://nde.nl/ontology/hc/custodian/>
PREFIX schema: <https://schema.org/>
SELECT ?person ?role ?unit ?unitName
WHERE {
?person a custodian:PersonObservation ;
custodian:staff_role ?role ;
custodian:unit_affiliation ?unit ;
custodian:employment_start_date ?startDate .
?unit custodian:unit_name ?unitName .
# Either no end date (still employed) or end date after query period
OPTIONAL { ?person custodian:employment_end_date ?endDate }
# Query period: 2015-01-01 to 2020-12-31
FILTER(?startDate <= "2020-12-31"^^xsd:date)
FILTER(!BOUND(?endDate) || ?endDate >= "2015-01-01"^^xsd:date)
}
ORDER BY ?unitName ?role
Expected Result: Lists all staff employed during the 2015-2020 period.
Explanation: Uses temporal overlap logic:
- Start date ≤ query end date
- End date ≥ query start date OR no end date (still employed)
1.5 Find Staff by Expertise
Use Case: Locate staff with specific subject expertise (e.g., Dutch Golden Age art).
PREFIX custodian: <https://nde.nl/ontology/hc/custodian/>
SELECT ?person ?expertise ?role ?unit ?unitName
WHERE {
?person a custodian:PersonObservation ;
custodian:subject_expertise ?expertise ;
custodian:staff_role ?role ;
custodian:unit_affiliation ?unit .
?unit custodian:unit_name ?unitName .
# Search for expertise containing "Dutch"
FILTER(CONTAINS(LCASE(?expertise), "dutch"))
}
ORDER BY ?expertise
Expected Result: Staff with "Dutch painting", "Dutch Golden Age", etc. in expertise field.
Explanation: Uses CONTAINS() for case-insensitive substring matching on subject_expertise.
2. Collection Queries
2.1 Find Managing Unit for a Collection
Use Case: Which department manages a specific collection?
PREFIX custodian: <https://nde.nl/ontology/hc/custodian/>
SELECT ?collection ?collectionName ?unit ?unitName ?unitType
WHERE {
<https://nde.nl/ontology/hc/collection/rm-dutch-paintings>
custodian:collection_name ?collectionName ;
custodian:managing_unit ?unit .
?unit custodian:unit_name ?unitName ;
custodian:unit_type ?unitType .
BIND(<https://nde.nl/ontology/hc/collection/rm-dutch-paintings> AS ?collection)
}
Expected Result:
| collection | collectionName | unit | unitName | unitType |
|---|---|---|---|---|
hc/collection/rm-dutch-paintings |
Dutch Golden Age Paintings | hc/org-unit/rm-paintings-dept |
Paintings Department | DEPARTMENT |
Explanation: Direct lookup via custodian:managing_unit property. Returns unit metadata.
2.2 List All Collections Managed by a Unit
Use Case: What collections does a department oversee?
PREFIX custodian: <https://nde.nl/ontology/hc/custodian/>
SELECT ?collection ?collectionName ?collectionScope ?extent
WHERE {
<https://nde.nl/ontology/hc/org-unit/rm-paintings-dept>
custodian:managed_collections ?collection .
?collection custodian:collection_name ?collectionName ;
custodian:collection_scope ?collectionScope ;
custodian:extent ?extent .
}
ORDER BY ?collectionName
Expected Result (from test data):
| collection | collectionName | collectionScope | extent |
|---|---|---|---|
hc/collection/rm-dutch-paintings |
Dutch Golden Age Paintings | 17th-century Dutch painting | 1,200 paintings |
hc/collection/rm-flemish-paintings |
Flemish Baroque Paintings | 17th-century Flemish painting | 450 paintings |
hc/collection/rm-italian-paintings |
Italian Renaissance Paintings | Italian Renaissance and Baroque painting | 280 paintings |
Explanation: Uses custodian:managed_collections (inverse of managing_unit) to retrieve all collections linked to a unit.
2.3 Find Collections by Type
Use Case: Search for all born-digital archival collections.
PREFIX custodian: <https://nde.nl/ontology/hc/custodian/>
SELECT ?collection ?collectionName ?managingUnit ?unitName
WHERE {
?collection a custodian:CustodianCollection ;
custodian:collection_name ?collectionName ;
custodian:collection_type ?type .
# Filter for born-digital archives
FILTER(?type = "born_digital" || ?type = "archival")
OPTIONAL {
?collection custodian:managing_unit ?managingUnit .
?managingUnit custodian:unit_name ?unitName
}
}
ORDER BY ?collectionName
Expected Result:
| collection | collectionName | managingUnit | unitName |
|---|---|---|---|
hc/collection/na-born-digital-archives |
Born-Digital Government Records | hc/org-unit/na-digital-preservation |
Digital Preservation Division |
Explanation: Filters CustodianCollection by collection_type values. Uses OPTIONAL for unit join (collections may not have managing unit assigned).
2.4 Find Collections by Temporal Coverage
Use Case: Find all collections covering the 17th century.
PREFIX custodian: <https://nde.nl/ontology/hc/custodian/>
PREFIX time: <http://www.w3.org/2006/time#>
SELECT ?collection ?collectionName ?beginDate ?endDate
WHERE {
?collection a custodian:CustodianCollection ;
custodian:collection_name ?collectionName ;
custodian:temporal_coverage ?temporalCoverage .
?temporalCoverage custodian:begin_of_the_begin ?beginDate ;
custodian:end_of_the_end ?endDate .
# Query for 17th century: 1600-1699
FILTER(?beginDate <= "1699-12-31"^^xsd:date)
FILTER(?endDate >= "1600-01-01"^^xsd:date)
}
ORDER BY ?beginDate ?collectionName
Expected Result (from test data):
| collection | collectionName | beginDate | endDate |
|---|---|---|---|
hc/collection/rm-dutch-paintings |
Dutch Golden Age Paintings | 1600-01-01 | 1699-12-31 |
hc/collection/rm-flemish-paintings |
Flemish Baroque Paintings | 1600-01-01 | 1699-12-31 |
Explanation: Uses temporal overlap logic with Allen interval algebra:
- Collection begins before query period ends
- Collection ends after query period begins
2.5 Count Collections by Institution
Use Case: How many collections does each heritage custodian maintain?
PREFIX custodian: <https://nde.nl/ontology/hc/custodian/>
SELECT ?custodian ?collectionCount
WHERE {
{
SELECT ?custodian (COUNT(DISTINCT ?collection) AS ?collectionCount)
WHERE {
?collection a custodian:CustodianCollection ;
custodian:refers_to_custodian ?custodian .
}
GROUP BY ?custodian
}
}
ORDER BY DESC(?collectionCount)
Expected Result:
| custodian | collectionCount |
|---|---|
hc/custodian/nl-rm |
3 |
hc/custodian/nl-na |
2 |
Explanation: Aggregates collections by custodian using refers_to_custodian property. Uses subquery for aggregation.
3. Combined Staff + Collection Queries
3.1 Find Curator Managing Specific Collection
Use Case: Who is the curator responsible for the Dutch Paintings collection?
PREFIX custodian: <https://nde.nl/ontology/hc/custodian/>
PREFIX org: <http://www.w3.org/ns/org#>
SELECT ?curator ?role ?expertise ?collection ?collectionName
WHERE {
# Collection and its managing unit
<https://nde.nl/ontology/hc/collection/rm-dutch-paintings>
custodian:collection_name ?collectionName ;
custodian:managing_unit ?unit .
# Staff in that unit with curator role
?unit org:hasMember ?curator .
?curator custodian:staff_role ?role ;
custodian:subject_expertise ?expertise .
# Filter for curators only
FILTER(?role = "CURATOR")
BIND(<https://nde.nl/ontology/hc/collection/rm-dutch-paintings> AS ?collection)
}
Expected Result:
| curator | role | expertise | collection | collectionName |
|---|---|---|---|---|
hc/person-obs/nl-rm/sophia-van-gogh/curator-dutch-paintings |
CURATOR | Dutch Golden Age painting | hc/collection/rm-dutch-paintings |
Dutch Golden Age Paintings |
Explanation: Two-step join:
- Collection → managing unit
- Managing unit → staff members (filtered by role = CURATOR)
3.2 List Collections and Curators by Department
Use Case: Department inventory showing collections + responsible curators.
PREFIX custodian: <https://nde.nl/ontology/hc/custodian/>
PREFIX org: <http://www.w3.org/ns/org#>
SELECT ?unit ?unitName ?collection ?collectionName ?curator ?curatorExpertise
WHERE {
# Organizational unit
?unit a custodian:OrganizationalStructure ;
custodian:unit_name ?unitName ;
custodian:unit_type "DEPARTMENT" .
# Collections managed by unit
OPTIONAL {
?unit custodian:managed_collections ?collection .
?collection custodian:collection_name ?collectionName
}
# Curators in unit
OPTIONAL {
?unit org:hasMember ?curator .
?curator custodian:staff_role "CURATOR" ;
custodian:subject_expertise ?curatorExpertise
}
}
ORDER BY ?unitName ?collectionName ?curatorExpertise
Expected Result (Paintings Department):
| unit | unitName | collection | collectionName | curator | curatorExpertise |
|---|---|---|---|---|---|
hc/org-unit/rm-paintings-dept |
Paintings Department | hc/collection/rm-dutch-paintings |
Dutch Golden Age Paintings | hc/person-obs/nl-rm/sophia-van-gogh/... |
Dutch Golden Age painting |
hc/org-unit/rm-paintings-dept |
Paintings Department | hc/collection/rm-flemish-paintings |
Flemish Baroque Paintings | hc/person-obs/nl-rm/pieter-de-vries/... |
Flemish Baroque painting |
| ... | ... | ... | ... | ... | ... |
Explanation: Retrieves all departments with OPTIONAL joins for both collections and curators. This produces a Cartesian product (all collections × all curators per department).
3.3 Match Curators to Collections by Subject Expertise
Use Case: Which curator's expertise matches which collection's scope?
PREFIX custodian: <https://nde.nl/ontology/hc/custodian/>
PREFIX org: <http://www.w3.org/ns/org#>
SELECT ?curator ?expertise ?collection ?collectionName ?collectionScope
WHERE {
# Curator in a unit
?curator a custodian:PersonObservation ;
custodian:staff_role "CURATOR" ;
custodian:subject_expertise ?expertise ;
custodian:unit_affiliation ?unit .
# Collections managed by that unit
?unit custodian:managed_collections ?collection .
?collection custodian:collection_name ?collectionName ;
custodian:collection_scope ?collectionScope .
# Match expertise to collection scope (case-insensitive substring)
FILTER(CONTAINS(LCASE(?collectionScope), LCASE(?expertise)) ||
CONTAINS(LCASE(?expertise), LCASE(?collectionScope)))
}
ORDER BY ?unit ?curator
Expected Result:
| curator | expertise | collection | collectionName | collectionScope |
|---|---|---|---|---|
hc/person-obs/nl-rm/sophia-van-gogh/... |
Dutch Golden Age painting | hc/collection/rm-dutch-paintings |
Dutch Golden Age Paintings | 17th-century Dutch painting |
Explanation: Fuzzy matching between curator's subject_expertise and collection's collection_scope. Uses bidirectional CONTAINS for partial matches.
3.4 Department Inventory Report
Use Case: Generate comprehensive report of department resources (staff, collections, temporal validity).
PREFIX custodian: <https://nde.nl/ontology/hc/custodian/>
PREFIX org: <http://www.w3.org/ns/org#>
SELECT ?unitName ?staffCount ?staffRole ?collectionCount ?validFrom ?validTo
WHERE {
?unit a custodian:OrganizationalStructure ;
custodian:unit_name ?unitName ;
custodian:unit_type "DEPARTMENT" ;
custodian:staff_count ?staffCount .
OPTIONAL { ?unit custodian:valid_from ?validFrom }
OPTIONAL { ?unit custodian:valid_to ?validTo }
# Count collections
{
SELECT ?unit (COUNT(?collection) AS ?collectionCount)
WHERE {
?unit custodian:managed_collections ?collection
}
GROUP BY ?unit
}
# Count staff by role
OPTIONAL {
SELECT ?unit ?staffRole (COUNT(?staff) AS ?staffRoleCount)
WHERE {
?unit org:hasMember ?staff .
?staff custodian:staff_role ?staffRole
}
GROUP BY ?unit ?staffRole
}
}
ORDER BY ?unitName
Expected Result: Summary statistics per department.
Explanation: Combines multiple aggregations:
- Collection count (subquery)
- Staff count by role (OPTIONAL subquery)
- Temporal validity (OPTIONAL fields)
4. Organizational Change Queries
4.1 Track Custody Transfers During Mergers
Use Case: Which collections were transferred when two organizations merged?
PREFIX custodian: <https://nde.nl/ontology/hc/custodian/>
PREFIX prov: <http://www.w3.org/ns/prov#>
SELECT ?collection ?collectionName ?oldUnit ?newUnit ?transferDate ?changeEvent
WHERE {
# Collection with custody history
?collection a custodian:CustodianCollection ;
custodian:collection_name ?collectionName ;
custodian:custody_history ?custodyEvent .
# Custody event details
?custodyEvent custodian:previous_custodian ?oldUnit ;
custodian:new_custodian ?newUnit ;
custodian:transfer_date ?transferDate ;
prov:wasInformedBy ?changeEvent .
# Change event = MERGER
?changeEvent a custodian:OrganizationalChangeEvent ;
custodian:change_type "MERGER" .
}
ORDER BY ?transferDate ?collectionName
Expected Result (from test data merger scenario):
| collection | collectionName | oldUnit | newUnit | transferDate | changeEvent |
|---|---|---|---|---|---|
hc/collection/rm-paintings-conservation-records |
Conservation Treatment Records | hc/org-unit/rm-paintings-conservation-pre-2013 |
hc/org-unit/rm-conservation-division-post-2013 |
2013-03-01 | hc/event/rm-conservation-merger-2013 |
Explanation: Three-way join:
- Collection → custody history
- Custody event → old/new units + transfer date
- Custody event → organizational change event (filtered by MERGER type)
Uses prov:wasInformedBy to link custody transfers to organizational changes.
4.2 Find Staff Affected by Restructuring
Use Case: Which staff members changed units during a reorganization?
PREFIX custodian: <https://nde.nl/ontology/hc/custodian/>
SELECT ?person ?oldUnit ?newUnit ?changeDate ?changeType
WHERE {
# Staff with unit affiliation
?person a custodian:PersonObservation ;
custodian:unit_affiliation ?newUnit .
# New unit has organizational change history
?newUnit custodian:organizational_history ?changeEvent .
?changeEvent custodian:change_type ?changeType ;
custodian:change_date ?changeDate ;
custodian:previous_organization ?oldUnit .
# Filter for staff employed before change date
?person custodian:employment_start_date ?startDate .
FILTER(?startDate <= ?changeDate)
# Filter for restructuring events
FILTER(?changeType IN ("MERGER", "REORGANIZATION", "SPLIT"))
}
ORDER BY ?changeDate ?person
Expected Result: Staff who were transferred between units during mergers/reorganizations.
Explanation: Temporal logic:
- Staff employment start date ≤ change date (they were there before the change)
- Unit's organizational history contains restructuring event
- Links person to previous/new organizational unit
4.3 Timeline of Organizational Changes
Use Case: Show chronological history of all organizational changes for an institution.
PREFIX custodian: <https://nde.nl/ontology/hc/custodian/>
SELECT ?changeDate ?changeType ?description ?affectedUnit ?resultingUnit
WHERE {
# All organizational change events
?event a custodian:OrganizationalChangeEvent ;
custodian:change_date ?changeDate ;
custodian:change_type ?changeType ;
custodian:change_description ?description .
# Units affected by change
OPTIONAL { ?event custodian:previous_organization ?affectedUnit }
OPTIONAL { ?event custodian:new_organization ?resultingUnit }
# Filter by institution
FILTER(CONTAINS(STR(?event), "nl-rm"))
}
ORDER BY ?changeDate
Expected Result: Chronological list of mergers, reorganizations, splits, etc.
Explanation: Retrieves all OrganizationalChangeEvent instances filtered by institution identifier (here: Rijksmuseum "nl-rm"). Uses OPTIONAL for affected/resulting units (not all change types have both).
4.4 Collections Impacted by Unit Dissolution
Use Case: When a department closed, which collections were affected and where did they go?
PREFIX custodian: <https://nde.nl/ontology/hc/custodian/>
SELECT ?dissolvedUnit ?collection ?collectionName ?newUnit ?dissolutionDate
WHERE {
# Dissolved unit
?dissolvedUnit a custodian:OrganizationalStructure ;
custodian:valid_to ?dissolutionDate .
# Collections that were managed by dissolved unit
?collection custodian:managing_unit ?newUnit ;
custodian:collection_name ?collectionName ;
custodian:custody_history ?custodyEvent .
# Custody event shows transfer from dissolved unit
?custodyEvent custodian:previous_custodian ?dissolvedUnit ;
custodian:new_custodian ?newUnit ;
custodian:transfer_date ?dissolutionDate .
}
ORDER BY ?dissolutionDate ?collectionName
Expected Result: Collections transferred when a unit closed (valid_to date ≠ null).
Explanation: Identifies closed units (valid_to is set), then traces collections via custody history to find what was transferred and where.
5. Validation Queries (SPARQL)
These queries implement the 5 validation rules from Phase 5 in SPARQL.
5.1 Temporal Consistency: Collection Managed Before Unit Exists
Use Case: Find collections with managing_unit temporal inconsistencies.
Validation Rule: Collection's valid_from must be ≥ managing unit's valid_from.
PREFIX custodian: <https://nde.nl/ontology/hc/custodian/>
SELECT ?collection ?collectionName ?collectionValidFrom ?unit ?unitName ?unitValidFrom
WHERE {
# Collection with managing unit
?collection a custodian:CustodianCollection ;
custodian:collection_name ?collectionName ;
custodian:managing_unit ?unit ;
custodian:valid_from ?collectionValidFrom .
?unit custodian:unit_name ?unitName ;
custodian:valid_from ?unitValidFrom .
# VIOLATION: Collection starts before unit exists
FILTER(?collectionValidFrom < ?unitValidFrom)
}
ORDER BY ?collectionValidFrom
Expected Result: Empty (no violations) or list of temporally inconsistent collections.
Explanation: Implements Rule 1 from Phase 5 validation framework. Finds collections where custody begins before the managing unit was established.
5.2 Bidirectional Consistency: Missing Inverse Relationship
Use Case: Find collections that reference a managing unit, but the unit doesn't reference them back.
Validation Rule: If collection.managing_unit = unit, then unit.managed_collections must include collection.
PREFIX custodian: <https://nde.nl/ontology/hc/custodian/>
SELECT ?collection ?collectionName ?unit ?unitName
WHERE {
# Collection references unit
?collection a custodian:CustodianCollection ;
custodian:collection_name ?collectionName ;
custodian:managing_unit ?unit .
?unit custodian:unit_name ?unitName .
# VIOLATION: Unit does NOT reference collection back
FILTER NOT EXISTS {
?unit custodian:managed_collections ?collection
}
}
ORDER BY ?unitName ?collectionName
Expected Result: Empty (no violations) or list of broken bidirectional relationships.
Explanation: Implements Rule 2 from Phase 5. Uses FILTER NOT EXISTS to find missing inverse relationships.
5.3 Custody Transfer Continuity Check
Use Case: Find collections with gaps or overlaps in custody history.
Validation Rule: Custody end date of previous unit = custody start date of new unit.
PREFIX custodian: <https://nde.nl/ontology/hc/custodian/>
SELECT ?collection ?collectionName ?prevUnit ?prevEndDate ?newUnit ?newStartDate ?gap
WHERE {
# Collection with custody history
?collection a custodian:CustodianCollection ;
custodian:collection_name ?collectionName ;
custodian:custody_history ?event1 ;
custodian:custody_history ?event2 .
# First custody period
?event1 custodian:new_custodian ?prevUnit ;
custodian:transfer_date ?prevEndDate .
# Second custody period (chronologically after)
?event2 custodian:new_custodian ?newUnit ;
custodian:transfer_date ?newStartDate .
FILTER(?newStartDate > ?prevEndDate)
# Calculate gap in days
BIND((xsd:date(?newStartDate) - xsd:date(?prevEndDate)) AS ?gap)
# VIOLATION: Gap > 1 day
FILTER(?gap > 1)
}
ORDER BY ?collection ?prevEndDate
Expected Result: Collections with custody gaps (e.g., 30 days between transfers).
Explanation: Implements Rule 3 from Phase 5. Identifies discontinuities in custody chain using date arithmetic.
5.4 Staff-Unit Temporal Consistency
Use Case: Find staff employed before their unit was established.
Validation Rule: Staff employment_start_date must be ≥ unit's valid_from.
PREFIX custodian: <https://nde.nl/ontology/hc/custodian/>
SELECT ?person ?role ?employmentStart ?unit ?unitName ?unitValidFrom
WHERE {
# Staff with unit affiliation
?person a custodian:PersonObservation ;
custodian:staff_role ?role ;
custodian:unit_affiliation ?unit ;
custodian:employment_start_date ?employmentStart .
?unit custodian:unit_name ?unitName ;
custodian:valid_from ?unitValidFrom .
# VIOLATION: Employment starts before unit exists
FILTER(?employmentStart < ?unitValidFrom)
}
ORDER BY ?unitValidFrom ?employmentStart
Expected Result: Empty (no violations) or list of temporally inconsistent staff records.
Explanation: Implements Rule 4 from Phase 5. Analogous to collection-unit temporal check but for staff.
5.5 Staff-Unit Bidirectional Consistency
Use Case: Find staff who reference a unit, but the unit doesn't list them as members.
Validation Rule: If person.unit_affiliation = unit, then unit.staff_members must include person.
PREFIX custodian: <https://nde.nl/ontology/hc/custodian/>
PREFIX org: <http://www.w3.org/ns/org#>
SELECT ?person ?role ?unit ?unitName
WHERE {
# Person references unit
?person a custodian:PersonObservation ;
custodian:staff_role ?role ;
custodian:unit_affiliation ?unit .
?unit custodian:unit_name ?unitName .
# VIOLATION: Unit does NOT list person as member
FILTER NOT EXISTS {
?unit org:hasMember ?person
}
}
ORDER BY ?unitName ?role
Expected Result: Empty (no violations) or list of broken staff-unit relationships.
Explanation: Implements Rule 5 from Phase 5. Uses org:hasMember (same as custodian:staff_members).
6. Advanced Temporal Queries
6.1 Point-in-Time Snapshot
Use Case: Reconstruct organizational state at a specific historical date (e.g., 2015-06-01).
PREFIX custodian: <https://nde.nl/ontology/hc/custodian/>
PREFIX org: <http://www.w3.org/ns/org#>
SELECT ?unit ?unitName ?collection ?collectionName ?staff ?staffRole
WHERE {
# Units that existed on 2015-06-01
?unit a custodian:OrganizationalStructure ;
custodian:unit_name ?unitName ;
custodian:valid_from ?unitValidFrom .
OPTIONAL { ?unit custodian:valid_to ?unitValidTo }
FILTER(?unitValidFrom <= "2015-06-01"^^xsd:date)
FILTER(!BOUND(?unitValidTo) || ?unitValidTo >= "2015-06-01"^^xsd:date)
# Collections managed on that date
OPTIONAL {
?unit custodian:managed_collections ?collection .
?collection custodian:collection_name ?collectionName ;
custodian:valid_from ?collectionValidFrom .
OPTIONAL { ?collection custodian:valid_to ?collectionValidTo }
FILTER(?collectionValidFrom <= "2015-06-01"^^xsd:date)
FILTER(!BOUND(?collectionValidTo) || ?collectionValidTo >= "2015-06-01"^^xsd:date)
}
# Staff employed on that date
OPTIONAL {
?unit org:hasMember ?staff .
?staff custodian:staff_role ?staffRole ;
custodian:employment_start_date ?employmentStart .
OPTIONAL { ?staff custodian:employment_end_date ?employmentEnd }
FILTER(?employmentStart <= "2015-06-01"^^xsd:date)
FILTER(!BOUND(?employmentEnd) || ?employmentEnd >= "2015-06-01"^^xsd:date)
}
}
ORDER BY ?unitName ?collectionName ?staffRole
Expected Result: Complete snapshot of units, collections, and staff on 2015-06-01.
Explanation: Uses temporal overlap logic across three entity types (units, collections, staff) to reconstruct past state. OPTIONAL joins prevent missing data from excluding entire record.
6.2 Change Frequency Analysis
Use Case: Which units have the most organizational changes?
PREFIX custodian: <https://nde.nl/ontology/hc/custodian/>
SELECT ?unit ?unitName (COUNT(?changeEvent) AS ?changeCount)
WHERE {
?unit a custodian:OrganizationalStructure ;
custodian:unit_name ?unitName ;
custodian:organizational_history ?changeEvent .
}
GROUP BY ?unit ?unitName
ORDER BY DESC(?changeCount)
Expected Result: Units ranked by number of reorganizations.
Explanation: Aggregates organizational_history events per unit to identify frequently restructured departments.
6.3 Collection Provenance Chain
Use Case: Trace complete custody history of a collection from founding to present.
PREFIX custodian: <https://nde.nl/ontology/hc/custodian/>
PREFIX prov: <http://www.w3.org/ns/prov#>
SELECT ?collection ?collectionName ?custodian ?transferDate ?changeEvent
WHERE {
<https://nde.nl/ontology/hc/collection/rm-dutch-paintings>
custodian:collection_name ?collectionName ;
custodian:custody_history ?custodyEvent .
?custodyEvent custodian:new_custodian ?custodian ;
custodian:transfer_date ?transferDate .
OPTIONAL {
?custodyEvent prov:wasInformedBy ?changeEvent .
?changeEvent custodian:change_description ?changeDescription
}
BIND(<https://nde.nl/ontology/hc/collection/rm-dutch-paintings> AS ?collection)
}
ORDER BY ?transferDate
Expected Result: Chronological list of all custodians (organizational units) that managed the collection.
Explanation: Retrieves all custody transfer events ordered by date, creating a provenance chain. Links to organizational change events when custody transfers were caused by restructuring.
6.4 Staff Tenure Analysis
Use Case: Calculate average staff tenure by role.
PREFIX custodian: <https://nde.nl/ontology/hc/custodian/>
SELECT ?role (AVG(?tenureYears) AS ?avgTenure) (COUNT(?person) AS ?staffCount)
WHERE {
?person a custodian:PersonObservation ;
custodian:staff_role ?role ;
custodian:employment_start_date ?startDate .
OPTIONAL { ?person custodian:employment_end_date ?endDate }
# Calculate tenure (end date or current date)
BIND(IF(BOUND(?endDate), ?endDate, NOW()) AS ?effectiveEndDate)
BIND((YEAR(?effectiveEndDate) - YEAR(?startDate)) AS ?tenureYears)
}
GROUP BY ?role
ORDER BY DESC(?avgTenure)
Expected Result: Roles ranked by average years of employment.
Explanation: Calculates tenure using start/end dates (or current date if still employed). Uses aggregation to compute averages per role.
6.5 Organizational Complexity Score
Use Case: Measure complexity of organizational structure (units, collections, staff).
PREFIX custodian: <https://nde.nl/ontology/hc/custodian/>
PREFIX org: <http://www.w3.org/ns/org#>
SELECT ?custodian
(COUNT(DISTINCT ?unit) AS ?unitCount)
(COUNT(DISTINCT ?collection) AS ?collectionCount)
(COUNT(DISTINCT ?staff) AS ?staffCount)
((?unitCount + ?collectionCount + ?staffCount) AS ?complexityScore)
WHERE {
# Units
?unit a custodian:OrganizationalStructure ;
custodian:refers_to_custodian ?custodian .
# Collections
OPTIONAL {
?collection a custodian:CustodianCollection ;
custodian:refers_to_custodian ?custodian
}
# Staff
OPTIONAL {
?staff a custodian:PersonObservation .
?unit org:hasMember ?staff
}
}
GROUP BY ?custodian
ORDER BY DESC(?complexityScore)
Expected Result: Institutions ranked by organizational complexity (total entities).
Explanation: Aggregates distinct units, collections, and staff per custodian to create a simple complexity metric. Can be weighted differently (e.g., staff count × 2).
Usage Examples
Execute Query on RDF Triple Store
Using Apache Jena Fuseki:
# Load RDF data
tdbloader2 --loc=/path/to/tdb custodian_data.ttl
# Start Fuseki server
fuseki-server --loc=/path/to/tdb --port=3030 /custodian
# Execute query via HTTP
curl -X POST http://localhost:3030/custodian/sparql \
--data-urlencode "query=$(cat query.sparql)"
Using Python rdflib:
from rdflib import Graph
# Load RDF data
g = Graph()
g.parse("custodian_data.ttl", format="turtle")
# Execute SPARQL query
query = """
PREFIX custodian: <https://nde.nl/ontology/hc/custodian/>
SELECT ?collection ?collectionName ?unit
WHERE {
?collection custodian:collection_name ?collectionName ;
custodian:managing_unit ?unit .
}
"""
results = g.query(query)
for row in results:
print(f"{row.collectionName} → {row.unit}")
Query Optimization Tips
1. Use Property Paths Sparingly
Property paths (e.g., ?a custodian:unit*/custodian:parent ?b) are powerful but slow. Use explicit triple patterns when possible.
2. Filter Early
Place FILTER clauses close to the triple patterns they filter to reduce intermediate results.
# GOOD - Filter immediately after relevant triple
?person custodian:staff_role ?role .
FILTER(?role = "CURATOR")
# BAD - Filter at the end after many joins
?person custodian:staff_role ?role .
# ... 10 more triple patterns ...
FILTER(?role = "CURATOR")
3. Use OPTIONAL for Sparse Data
Collections may not always have managing units. Use OPTIONAL to avoid excluding entire records:
OPTIONAL { ?collection custodian:managing_unit ?unit }
4. Limit Result Sets
Add LIMIT for exploratory queries:
SELECT ?collection ?collectionName
WHERE { ... }
LIMIT 100
5. Index Temporal Properties
For large datasets, ensure triple stores index date properties (valid_from, valid_to, employment_start_date, etc.) for faster temporal queries.
Testing Queries Against Test Data
To verify these queries work correctly:
-
Convert YAML to RDF:
linkml-convert -s schemas/20251121/linkml/01_custodian_name_modular.yaml \ -t rdf \ schemas/20251121/examples/collection_department_integration_examples.yaml \ > test_data.ttl -
Load into Triple Store (e.g., Apache Jena Fuseki or GraphDB).
-
Execute Queries via SPARQL endpoint or
rdflib. -
Verify Expected Results match test data from
collection_department_integration_examples.yaml.
Next Steps
Phase 7: SHACL Shapes
Convert validation queries (Section 5) into SHACL constraints for automatic RDF validation.
Phase 8: LinkML Schema Constraints
Embed temporal validation rules directly into LinkML schema using minimum_value, pattern, and custom validators.
Phase 9: Real-World Integration
Apply queries to production heritage institution data (ISIL registries, museum databases, archival finding aids).
References
- Schema:
schemas/20251121/linkml/01_custodian_name_modular.yaml(v0.7.0) - Test Data:
schemas/20251121/examples/collection_department_integration_examples.yaml - RDF Output:
schemas/20251121/rdf/01_custodian_name_modular_20251122_205111.owl.ttl - Validation Rules:
docs/VALIDATION_RULES.md - SPARQL 1.1 Spec: https://www.w3.org/TR/sparql11-query/
- W3C PROV-O: https://www.w3.org/TR/prov-o/
- W3C Org Ontology: https://www.w3.org/TR/vocab-org/
Document Version: 1.0.0
Schema Version: v0.7.0
Last Updated: 2025-11-22
Total Queries: 31 (5 staff + 5 collection + 4 combined + 4 org change + 5 validation + 8 advanced)