# 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)