# 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](#prefixes)
2. [Staff Queries](#1-staff-queries)
3. [Collection Queries](#2-collection-queries)
4. [Combined Staff + Collection Queries](#3-combined-staff--collection-queries)
5. [Organizational Change Queries](#4-organizational-change-queries)
6. [Validation Queries (SPARQL)](#5-validation-queries-sparql)
7. [Advanced Temporal Queries](#6-advanced-temporal-queries)
---
## Prefixes
All queries use these standard prefixes:
```sparql
PREFIX custodian:
PREFIX org:
PREFIX pico:
PREFIX schema:
PREFIX rdfs:
PREFIX xsd:
PREFIX skos:
PREFIX prov:
PREFIX time:
```
---
## 1. Staff Queries
### 1.1 Find All Curators
**Use Case**: List all staff with curator roles across all institutions.
```sparql
PREFIX custodian:
PREFIX rdfs:
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.
```sparql
PREFIX custodian:
PREFIX org:
SELECT ?person ?role ?startDate ?endDate
WHERE {
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.
```sparql
PREFIX custodian:
PREFIX pico:
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?
```sparql
PREFIX custodian:
PREFIX schema:
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).
```sparql
PREFIX 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?
```sparql
PREFIX custodian:
SELECT ?collection ?collectionName ?unit ?unitName ?unitType
WHERE {
custodian:collection_name ?collectionName ;
custodian:managing_unit ?unit .
?unit custodian:unit_name ?unitName ;
custodian:unit_type ?unitType .
BIND( 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?
```sparql
PREFIX custodian:
SELECT ?collection ?collectionName ?collectionScope ?extent
WHERE {
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.
```sparql
PREFIX 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.
```sparql
PREFIX custodian:
PREFIX 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?
```sparql
PREFIX 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?
```sparql
PREFIX custodian:
PREFIX org:
SELECT ?curator ?role ?expertise ?collection ?collectionName
WHERE {
# Collection and its managing unit
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( 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.
```sparql
PREFIX custodian:
PREFIX 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?
```sparql
PREFIX custodian:
PREFIX 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).
```sparql
PREFIX custodian:
PREFIX 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?
```sparql
PREFIX custodian:
PREFIX 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?
```sparql
PREFIX 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.
```sparql
PREFIX 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?
```sparql
PREFIX 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`.
```sparql
PREFIX 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.
```sparql
PREFIX 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.
```sparql
PREFIX 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`.
```sparql
PREFIX 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.
```sparql
PREFIX custodian:
PREFIX 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).
```sparql
PREFIX custodian:
PREFIX 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?
```sparql
PREFIX 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.
```sparql
PREFIX custodian:
PREFIX prov:
SELECT ?collection ?collectionName ?custodian ?transferDate ?changeEvent
WHERE {
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( 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.
```sparql
PREFIX 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).
```sparql
PREFIX custodian:
PREFIX 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**:
```bash
# 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**:
```python
from rdflib import Graph
# Load RDF data
g = Graph()
g.parse("custodian_data.ttl", format="turtle")
# Execute SPARQL query
query = """
PREFIX 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.
```sparql
# 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:
```sparql
OPTIONAL { ?collection custodian:managing_unit ?unit }
```
### 4. Limit Result Sets
Add `LIMIT` for exploratory queries:
```sparql
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**:
```bash
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
- **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)