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

1168 lines
36 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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: <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.
```sparql
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.
```sparql
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.
```sparql
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?
```sparql
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).
```sparql
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?
```sparql
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?
```sparql
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.
```sparql
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.
```sparql
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?
```sparql
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?
```sparql
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.
```sparql
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?
```sparql
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).
```sparql
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?
```sparql
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?
```sparql
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.
```sparql
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?
```sparql
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`.
```sparql
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.
```sparql
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.
```sparql
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`.
```sparql
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.
```sparql
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).
```sparql
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?
```sparql
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.
```sparql
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.
```sparql
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).
```sparql
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**:
```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: <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.
```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)