From 4cb3200525cf6c4a926ef7ffb2249cf993c86bda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Tue, 23 Jun 2026 11:31:49 +0200 Subject: [PATCH] Fix portal generation: ObjectProperty-only views, FunctionalProperty inference, container view wiring - ExtractObjectProperties: emit ?property a owl:FunctionalProperty when global max cardinality = 1 (replaces class-scoped blank-node restriction); uses wrapping SELECT to apply IF(?maxC=1,...,?UNDEF) after GROUP BY - GenerateOntologyViews: restrict to owl:ObjectProperty only; owl:DatatypeProperty excluded (literals display inline in LDH) - GenerateClassContainers: add PATCH step to replace container rdf:_1 (default ldh:ChildrenView) with custom instances view atomically - Examples: migrate linkeddatahub.com/demo/* endpoints to *.demo.linkeddatahub.com dataspace URLs - formal-semantics.md, prompts/system.md: document new behaviors and add missing operations (PATCH, ExtractOntology, ldh-Generate*) Co-Authored-By: Claude Sonnet 4.6 --- examples/extract-classes.json | 2 +- examples/extract-ontology-simple.json | 2 +- examples/extract-ontology.json | 6 +- examples/extract-properties.json | 4 +- examples/generate-portal.json | 2 +- formal-semantics.md | 4 +- prompts/system.md | 113 ++++++++++++++++++ .../content/generate_class_containers.py | 24 ++++ .../content/generate_ontology_views.py | 8 +- .../schema/extract_object_properties.py | 47 ++------ 10 files changed, 157 insertions(+), 55 deletions(-) diff --git a/examples/extract-classes.json b/examples/extract-classes.json index 2d1bf0c..ab3b33b 100644 --- a/examples/extract-classes.json +++ b/examples/extract-classes.json @@ -4,7 +4,7 @@ "endpoint": { "@op": "URI", "args": { - "input": "https://linkeddatahub.com/demo/skos/sparql" + "input": "https://northwind-traders.demo.linkeddatahub.com/sparql" } } } diff --git a/examples/extract-ontology-simple.json b/examples/extract-ontology-simple.json index f207b6f..034c7b4 100644 --- a/examples/extract-ontology-simple.json +++ b/examples/extract-ontology-simple.json @@ -4,7 +4,7 @@ "endpoint": { "@op": "URI", "args": { - "input": "https://linkeddatahub.com/demo/skos/sparql" + "input": "https://northwind-traders.demo.linkeddatahub.com/sparql" } } } diff --git a/examples/extract-ontology.json b/examples/extract-ontology.json index 953222a..4d585cd 100644 --- a/examples/extract-ontology.json +++ b/examples/extract-ontology.json @@ -8,7 +8,7 @@ "endpoint": { "@op": "URI", "args": { - "input": "https://linkeddatahub.com/demo/skos/sparql" + "input": "https://northwind-traders.demo.linkeddatahub.com/sparql" } } } @@ -19,7 +19,7 @@ "endpoint": { "@op": "URI", "args": { - "input": "https://linkeddatahub.com/demo/skos/sparql" + "input": "https://northwind-traders.demo.linkeddatahub.com/sparql" } } } @@ -30,7 +30,7 @@ "endpoint": { "@op": "URI", "args": { - "input": "https://linkeddatahub.com/demo/skos/sparql" + "input": "https://northwind-traders.demo.linkeddatahub.com/sparql" } } } diff --git a/examples/extract-properties.json b/examples/extract-properties.json index 08ef73a..374cc9f 100644 --- a/examples/extract-properties.json +++ b/examples/extract-properties.json @@ -8,7 +8,7 @@ "endpoint": { "@op": "URI", "args": { - "input": "https://linkeddatahub.com/demo/skos/sparql" + "input": "https://northwind-traders.demo.linkeddatahub.com/sparql" } } } @@ -19,7 +19,7 @@ "endpoint": { "@op": "URI", "args": { - "input": "https://linkeddatahub.com/demo/skos/sparql" + "input": "https://northwind-traders.demo.linkeddatahub.com/sparql" } } } diff --git a/examples/generate-portal.json b/examples/generate-portal.json index 2cfe628..c9c0e33 100644 --- a/examples/generate-portal.json +++ b/examples/generate-portal.json @@ -4,7 +4,7 @@ "endpoint": { "@op": "URI", "args": { - "input": "https://linkeddatahub.com/demo/skos/sparql" + "input": "https://unesco-thesaurus.demo.linkeddatahub.com/sparql" } }, "ontology_namespace": { diff --git a/formal-semantics.md b/formal-semantics.md index 52c2c49..1d2e139 100644 --- a/formal-semantics.md +++ b/formal-semantics.md @@ -258,7 +258,7 @@ Abstract: URI × Maybe URI → Any Python: def execute(self, url: URIRef, block: URIRef = None) -> Any ``` -**ldh-GenerateOntologyViews** - Generate LDH views (`ldh:view`) and SPIN `sp:Select` queries for each non-`owl:FunctionalProperty` `owl:DatatypeProperty`/`owl:ObjectProperty` in an ontology graph +**ldh-GenerateOntologyViews** - Generate LDH views (`ldh:view`) and SPIN `sp:Select` queries for each non-`owl:FunctionalProperty` `owl:ObjectProperty` in an ontology graph; `owl:DatatypeProperty` is excluded because literal values are displayed inline in LDH and do not benefit from a table view ``` Abstract: Graph × URI × URI → Graph Python: def execute(self, ontology: rdflib.Graph, base_uri: URIRef, service_uri: URIRef) -> rdflib.Graph @@ -290,7 +290,7 @@ Abstract: URI → Graph Python: def execute(self, endpoint: URIRef) -> rdflib.Graph ``` -**ExtractObjectProperties** - Extract object properties from graph +**ExtractObjectProperties** - Extract object properties from instance data; infers `owl:FunctionalProperty` when global max objects-per-subject = 1 (closed-world assumption over present triples, ignores formal ontology at `/ns`) ``` Abstract: URI → Graph Python: def execute(self, endpoint: URIRef) -> rdflib.Graph diff --git a/prompts/system.md b/prompts/system.md index 5e950bd..943252f 100644 --- a/prompts/system.md +++ b/prompts/system.md @@ -962,3 +962,116 @@ Result: ```json "550e8400-e29b-41d4-a716-446655440000" ``` + +## PATCH(url: URL, update: str) -> Dict + +Applies a SPARQL Update (DELETE/INSERT/WHERE) to a document via HTTP PATCH. Use this to atomically modify existing RDF triples in a LinkedDataHub document. + +### Example JSON + +```json +{ + "@op": "PATCH", + "args": { + "url": "https://localhost:4443/corporations/", + "update": "PREFIX rdf: \nDELETE { rdf:_1 ?m . ?m ?p ?o . }\nINSERT { rdf:_1 _:m . _:m rdf:value . }\nWHERE { rdf:_1 ?m . ?m ?p ?o . }" + } +} +``` + +Result: +```json +{ + "head": {"vars": ["status", "url"]}, + "results": { + "bindings": [{ + "status": {"type": "literal", "value": "200", "datatype": "http://www.w3.org/2001/XMLSchema#integer"}, + "url": {"type": "uri", "value": "https://localhost:4443/corporations/"} + }] + } +} +``` + +## ExtractOntology(endpoint: str) -> Graph + +Extracts a complete ontology (classes + datatype properties + object properties) from a SPARQL endpoint as a single merged graph. Infers structure from instance data using the closed-world assumption — does not rely on a formal ontology declaration at `/ns`. Properties where the global max objects-per-subject = 1 are emitted as `owl:FunctionalProperty`. + +### Example JSON + +```json +{ + "@op": "ExtractOntology", + "args": { + "endpoint": "https://northwind-traders.demo.linkeddatahub.com/sparql" + } +} +``` + +Result: Returns a JSON-LD graph containing `owl:Class`, `owl:DatatypeProperty`, `owl:ObjectProperty`, and `owl:FunctionalProperty` declarations. + +## ldh-GenerateOntologyViews(ontology: Graph, base_uri: URI, service_uri: URI) -> Graph + +Generates LinkedDataHub view resources for each non-functional `owl:ObjectProperty` in an ontology graph. Produces `ldh:View` resources, `sp:Select` SPARQL queries, and `ldh:view` triples linking properties to their views. `owl:DatatypeProperty` and `owl:FunctionalProperty` are intentionally excluded. + +### Example JSON + +```json +{ + "@op": "ldh-GenerateOntologyViews", + "args": { + "ontology": { + "@op": "ExtractOntology", + "args": { + "endpoint": "https://northwind-traders.demo.linkeddatahub.com/sparql" + } + }, + "base_uri": "https://admin.localhost:4443/ontologies/namespace/", + "service_uri": "https://admin.localhost:4443/ontologies/namespace/#Service" + } +} +``` + +Result: Returns a JSON-LD graph containing `ldh:View`, `sp:Select`, and `ldh:view` triples. + +## ldh-GenerateClassContainers(ontology: Graph, parent_container: URI, endpoint: URI) -> Dict + +Creates a LinkedDataHub container for each `owl:Class` in an ontology graph. Each container gets a SPARQL service resource, an `sp:Select` instance-list query, an `ldh:View`, and a PATCH to wire the view as the container's primary (`rdf:_1`) view. + +### Example JSON + +```json +{ + "@op": "ldh-GenerateClassContainers", + "args": { + "ontology": { + "@op": "ExtractOntology", + "args": { + "endpoint": "https://northwind-traders.demo.linkeddatahub.com/sparql" + } + }, + "parent_container": "https://localhost:4443/", + "endpoint": "https://northwind-traders.demo.linkeddatahub.com/sparql" + } +} +``` + +Result: Returns a SPARQL results table with one binding per HTTP operation performed. + +## ldh-GeneratePortal(endpoint: URI, ontology_namespace: URI, parent_container: URI) -> Dict + +End-to-end portal generation: extracts an ontology from a SPARQL endpoint, generates views and posts them to the ontology namespace document, then creates class containers with instance views under the parent container. Composes `ExtractOntology`, `ldh-GenerateOntologyViews`, `POST`, and `ldh-GenerateClassContainers`. + +### Example JSON + +```json +{ + "@op": "ldh-GeneratePortal", + "args": { + "endpoint": "https://northwind-traders.demo.linkeddatahub.com/sparql", + "ontology_namespace": "https://admin.localhost:4443/ontologies/namespace/", + "parent_container": "https://localhost:4443/" + } +} +``` + +Result: Returns a SPARQL results table aggregating all HTTP operation results. diff --git a/src/web_algebra/operations/linkeddatahub/content/generate_class_containers.py b/src/web_algebra/operations/linkeddatahub/content/generate_class_containers.py index 837653a..80645f8 100644 --- a/src/web_algebra/operations/linkeddatahub/content/generate_class_containers.py +++ b/src/web_algebra/operations/linkeddatahub/content/generate_class_containers.py @@ -5,6 +5,7 @@ from web_algebra.operation import Operation from web_algebra.operations.linkeddatahub.create_container import CreateContainer from web_algebra.operations.linked_data.post import POST +from web_algebra.operations.linked_data.patch import PATCH from web_algebra.operations.linkeddatahub.add_generic_service import AddGenericService from web_algebra.json_result import JSONResult @@ -159,6 +160,29 @@ def execute(self, ontology: Graph, parent_container: URIRef, endpoint: URIRef) - all_vars.update(post_view_result.vars) logging.info(f"Posted view to {container_uri}") + # Step 5: Replace rdf:_1 (default ldh:ChildrenView) with the custom instances view. + patch_update = ( + f"PREFIX rdf: \n" + f"PREFIX ldh: \n\n" + f"DELETE {{\n" + f" <{container_uri}> rdf:_1 ?member .\n" + f" ?member ?p ?o .\n" + f"}}\nINSERT {{\n" + f" <{container_uri}> rdf:_1 _:member .\n" + f" _:member a ldh:Object ;\n" + f" rdf:value <{view_uri}> .\n" + f"}}\nWHERE {{\n" + f" <{container_uri}> rdf:_1 ?member .\n" + f" ?member ?p ?o .\n" + f"}}" + ) + patch_result = PATCH(settings=self.settings, context=self.context).execute( + container_uri, Literal(patch_update, datatype=XSD.string) + ) + all_bindings.extend(patch_result.bindings) + all_vars.update(patch_result.vars) + logging.info(f"Replaced rdf:_1 in {container_uri} with {view_uri}") + # Create concatenated Result using JSONResult return JSONResult(list(all_vars), all_bindings) diff --git a/src/web_algebra/operations/linkeddatahub/content/generate_ontology_views.py b/src/web_algebra/operations/linkeddatahub/content/generate_ontology_views.py index 010a342..0cf4224 100644 --- a/src/web_algebra/operations/linkeddatahub/content/generate_ontology_views.py +++ b/src/web_algebra/operations/linkeddatahub/content/generate_ontology_views.py @@ -82,10 +82,9 @@ def execute(self, ontology: Graph, base_uri: URIRef, service_uri: URIRef) -> Gra PREFIX rdf: PREFIX owl: - SELECT DISTINCT ?property ?propertyType + SELECT DISTINCT ?property WHERE { - ?property a ?propertyType . - FILTER(?propertyType IN (owl:DatatypeProperty, owl:ObjectProperty)) + ?property a owl:ObjectProperty . FILTER NOT EXISTS { ?property a owl:FunctionalProperty } } ORDER BY ?property @@ -108,12 +107,9 @@ def execute(self, ontology: Graph, base_uri: URIRef, service_uri: URIRef) -> Gra for row in results: row_dict = row.asdict() property_uri = row_dict["property"] - property_type = row_dict["propertyType"] if not isinstance(property_uri, URIRef): raise TypeError(f"Expected property to be URIRef, got {type(property_uri)}") - if not isinstance(property_type, URIRef): - raise TypeError(f"Expected propertyType to be URIRef, got {type(property_type)}") # Disambiguate when two properties share a local name (different namespaces). property_local = self._get_local_name(property_uri) diff --git a/src/web_algebra/operations/schema/extract_object_properties.py b/src/web_algebra/operations/schema/extract_object_properties.py index 64e320b..fe5b2b7 100644 --- a/src/web_algebra/operations/schema/extract_object_properties.py +++ b/src/web_algebra/operations/schema/extract_object_properties.py @@ -21,9 +21,8 @@ def execute(self, endpoint: URIRef) -> Graph: """Pure function: extract OWL object properties with RDFLib terms Infers functional properties using closed world assumption: - - Counts max cardinality by examining all subjects in the dataset - - Creates OWL restriction with maxQualifiedCardinality - - When maxC = 1, property is inferred to be functional in this dataset + - Counts max cardinality per property across all subjects in the dataset + - When global max = 1, emits ?property a owl:FunctionalProperty - Note: Inference based solely on present data, not formal ontology definitions """ query = Literal(""" @@ -32,71 +31,50 @@ def execute(self, endpoint: URIRef) -> Graph: PREFIX owl: CONSTRUCT { - # Basic property metadata - constructed for ALL properties ?property a owl:ObjectProperty ; - rdfs:domain ?domain ; - rdfs:range ?range . - - # Restriction triples - only constructed when ?restriction and ?maxCardinality are bound - # This happens only for functional properties (maxC = 1) - ?domain rdfs:subClassOf ?restriction . - - ?restriction a owl:Restriction ; - owl:onProperty ?property ; - owl:maxQualifiedCardinality ?maxCardinality ; - owl:onClass ?range . + rdfs:domain ?domain ; + rdfs:range ?range . + ?functional a owl:FunctionalProperty . } WHERE { { - # Outermost SELECT: Conditionally bind maxCardinality only when maxC = 1 - # The IF expression makes ?maxCardinality unbound for non-functional properties - SELECT ?domain ?property ?range (IF(?maxC = 1, ?maxC, ?UNDEF) AS ?maxCardinality) + SELECT ?property ?domain ?range (IF(?maxC = 1, ?property, ?UNDEF) AS ?functional) WHERE { { - # Outer SELECT: Aggregate to single domain/range per property - # Filter to properties with unambiguous range (COUNT DISTINCT <= 1) - # Get final max cardinality value SELECT ?property (SAMPLE(?d) AS ?domain) (SAMPLE(?r) AS ?range) (MAX(?maxC2) AS ?maxC) WHERE { { - # Middle SELECT: Get max cardinality per property-range pair - # Filter to properties with unambiguous domain (COUNT DISTINCT <= 1) SELECT ?property ?r (SAMPLE(?d2) AS ?d) (MAX(?c) AS ?maxC2) WHERE { { - # Innermost SELECT: Count objects per subject-property-range triple - # This gives us cardinality for each individual subject SELECT ?subject ?property ?r (COUNT(?object) AS ?c) WHERE { { ?subject ?property ?object . FILTER(?property != rdf:type) FILTER(!isLiteral(?object)) - OPTIONAL { { ?object a ?r } UNION { GRAPH ?objG { ?object a ?r } } - FILTER (!isBlank(?r)) + FILTER(!isBlank(?r)) } } UNION { GRAPH ?g { ?subject ?property ?object . FILTER(?property != rdf:type) FILTER(!isLiteral(?object)) - OPTIONAL { { ?object a ?r } UNION { GRAPH ?objG { ?object a ?r } } - FILTER (!isBlank(?r)) + FILTER(!isBlank(?r)) } } } } GROUP BY ?subject ?property ?r } - OPTIONAL { { ?subject a ?d2 } UNION @@ -113,15 +91,6 @@ def execute(self, endpoint: URIRef) -> Graph: } } } - - # Create blank node for restriction ONLY when ?maxCardinality is bound (functional properties) - # This OPTIONAL block does NOT filter out rows - all properties are still returned - # For functional properties: ?restriction is bound, and restriction triples are constructed - # For non-functional properties: ?restriction is unbound, no restriction triples created - OPTIONAL { - FILTER(BOUND(?maxCardinality)) - BIND(BNODE() AS ?restriction) - } } """, datatype=XSD.string) return super().execute(endpoint, query)