glam/docs/SPARQL_QUERIES_ORGANIZATIONAL.md
kempersc 2761857b0d Add scripts for converting OWL/Turtle ontology to Mermaid and PlantUML diagrams
- 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.
2025-11-22 23:01:13 +01:00

36 KiB
Raw Blame History

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

  1. Prefixes
  2. Staff Queries
  3. Collection Queries
  4. Combined Staff + Collection Queries
  5. Organizational Change Queries
  6. Validation Queries (SPARQL)
  7. 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:

  1. Collection → managing unit
  2. 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:

  1. Collection → custody history
  2. Custody event → old/new units + transfer date
  3. 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:

  1. 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
    
  2. Load into Triple Store (e.g., Apache Jena Fuseki or GraphDB).

  3. Execute Queries via SPARQL endpoint or rdflib.

  4. 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


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)