Last active

Embed URL

HTTPS clone URL

SSH clone URL

You can clone with HTTPS or SSH.

Download Gist

Example of using Stardog for data validation for the example described at http://www.w3.org/2012/12/rdf-val/SOTA.

View commands.log
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
# Stardog commands and the output for RDF validation example
 
# First create the Stardog database and load data
 
$ ./stardog-admin db create -n sota sota-data.ttl
Bulk loading data to new database.
Loading data completed...Loaded 25 triples in 00:00:00 @ 0.4K triples/sec.
Successfully created database 'sota'.
 
# Then add the constraints to the database
 
$ ./stardog-admin icv add sota sota-constraints.ttl
Successfully added constraints in 00:00:00.
 
# Now run the validation command
# This command just prints which constraints are violated
 
$ ./stardog icv validate sota -r QL
Data is NOT valid.
The following constraints were violated:
AxiomConstraint{:related rdfs:range :Issue}
AxiomConstraint{:reportedOn rdfs:domain :Issue}
AxiomConstraint{:Issue rdfs:subClassOf (:reportedBy exactly 1 owl:Thing)}
AxiomConstraint{:reproducedBy rdfs:range foaf:Person}
AxiomConstraint{:reportedBy rdfs:range foaf:Person}
AxiomConstraint{:state rdfs:domain :Issue}
AxiomConstraint{:state rdfs:range :ValidState}
 
 
# Now run the explanation command to get details about violations
# We use the --merge option to group related violations together
# By default only one explanation is printed so we increase the limit to 10
 
$ ./stardog icv explain -r QL --limit 10 --merge sota
VIOLATED :reportedOn rdfs:domain :Issue
INFERRED :issue4 :reportedOn "x0"
NOT_INFERRED :issue4 a :Issue
 
1.1) VIOLATED :related rdfs:range :Issue
ASSERTED :issue7 :related :issue4
NOT_INFERRED :issue4 a :Issue
1.2) VIOLATED :related rdfs:range :Issue
ASSERTED :issue7 :related :issue3
NOT_INFERRED :issue3 a :Issue
1.3) VIOLATED :related rdfs:range :Issue
ASSERTED :issue7 :related :issue2
NOT_INFERRED :issue2 a :Issue
 
VIOLATED :Issue rdfs:subClassOf (:reportedBy exactly 1 owl:Thing)
ASSERTED :issue7 :reportedBy :user2
ASSERTED :issue7 a :Issue
INFERRED ASSERTED :issue7 :reportedBy :user6
NOT_INFERRED :issue7 :reportedBy <tag:stardog:api:variable:x0>
 
VIOLATED :reproducedBy rdfs:range foaf:Person
ASSERTED :issue7 :reproducedBy :user1
NOT_INFERRED :user1 a foaf:Person
 
VIOLATED :reportedBy rdfs:range foaf:Person
ASSERTED :issue7 :reportedBy :user6
NOT_INFERRED :user6 a foaf:Person
 
VIOLATED :state rdfs:domain :Issue
ASSERTED :issue4 :state :unsinged
NOT_INFERRED :issue4 a :Issue
 
VIOLATED :state rdfs:range :ValidState
ASSERTED :issue4 :state :unsinged
NOT_INFERRED :unsinged a :ValidState
 
 
# We can also add SPARQL queries as constraints
 
$ ./stardog-admin icv add sota-query.sparql
 
# We can run validation with a mixture of OWL constraints and SPARQL constraints
 
$ ./stardog icv validate sota
Data is NOT valid.
...
View commands.log
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix : <http://www.w3.org/2012/12/rdf-val/SOTA-ex#> .
@prefix foaf: <http://xmlns.com/foaf/0.1/'> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
 
 
:Issue a owl:Class ;
rdfs:subClassOf
[ owl:onProperty :state ; owl:cardinality 1 ] ,
[ owl:onProperty :reportedBy ; owl:cardinality 1 ] ,
[ owl:onProperty :reportedOn ; owl:cardinality 1 ] ,
[ owl:onProperty :reproducedBy ; owl:minCardinality 0 ] ,
[ owl:onProperty :reproducedOn ; owl:minCardinality 0 ] ,
[ owl:onProperty :related ; owl:minCardinality 0 ] .
 
:state a owl:ObjectProperty ,
owl:FunctionalProperty ; rdfs:domain :Issue ; rdfs:range :ValidState .
:related a owl:ObjectProperty ; rdfs:domain :Issue ; rdfs:range :Issue .
:reportedBy a owl:ObjectProperty ; rdfs:domain :Issue ; rdfs:range foaf:Person .
:reportedOn a owl:DatatypeProperty ; rdfs:domain :Issue ; rdfs:range xsd:dateTime .
:reproducedBy a owl:ObjectProperty ; rdfs:domain :Issue ; rdfs:range foaf:Person .
:reproducedOn a owl:DatatypeProperty ; rdfs:domain :Issue ; rdfs:range xsd:dateTime .
 
View commands.log
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
@prefix : <http://www.w3.org/2012/12/rdf-val/SOTA-ex#> .
@prefix foaf: <http://xmlns.com/foaf/0.1/'> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@base <http://www.w3.org/2012/12/rdf-val/SOTA-ex#> .
 
<#issue7> a :Issue , :SecurityIssue ;
:state :unassigned ;
:reportedBy <#user6> , <#user2> ; # only one reportedBy permitted
:reportedOn "2012-12-31T23:57:00"^^xsd:dateTime ;
:reproducedBy <#user2>, <#user1> ;
:reproducedOn "2012-11-31T23:57:00"^^xsd:dateTime ; # reproduced before being reported
:related <#issue4>, <#issue3>, <#issue2> . # referenced issues not included
 
<#issue4> # a ??? - missing type arc
:state :unsinged ; # misspelled term in value set.# :reportedBy ??? - missing required property
:reportedOn "2012-12-31T23:57:00"^^xsd:dateTime .
 
<#user2> a foaf:Person ;
foaf:givenName "Alice" ;
foaf:familyName "Smith" ;
foaf:phone <tel:+1.555.222.2222> ;
foaf:mbox <mailto:alice@example.com> .
 
<#user6> a foaf:Agent ; # should be foaf:Person
foaf:givenName "Bob" ; # foaf:familyName "???" - missing required property
foaf:phone <tel:+.555.222.2222> ; # malformed tel: URL
foaf:mbox <mailto:alice@example.com> .
 
:assigned a :ValidState .
:unassigned a :ValidState .
View commands.log
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139
// Copyright (c) 2010 - 2013 -- Clark & Parsia, LLC. <http://www.clarkparsia.com>
// For more information about licensing and copyright of this software, please contact
// inquiries@clarkparsia.com or visit http://stardog.com
 
package com.clarkparsia.stardog.examples.api;
 
import java.io.File;
import java.util.Set;
 
import org.openrdf.model.Resource;
import org.openrdf.model.Statement;
import org.openrdf.query.BindingSet;
import org.openrdf.rio.RDFFormat;
 
import com.clarkparsia.common.base.Pair;
import com.clarkparsia.common.iterations.Iteration;
import com.clarkparsia.stardog.StardogDBMS;
import com.clarkparsia.stardog.StardogException;
import com.clarkparsia.stardog.api.Connection;
import com.clarkparsia.stardog.api.ConnectionConfiguration;
import com.clarkparsia.stardog.icv.ConstraintViolation;
import com.clarkparsia.stardog.icv.ICV;
import com.clarkparsia.stardog.icv.api.ICVConnection;
import com.clarkparsia.stardog.reasoning.ReasoningType;
import com.clarkparsia.stardog.util.TurtleValueWriter;
import com.google.common.base.Strings;
 
/**
* Example of using Stardog Integrity Constraint functionality for data validation example described at http://www.w3.org/2012/12/rdf-val/SOTA
*
* @author Evren Sirin
*/
public class SOTAExample {
 
public static void main(String[] args) throws Exception {
if (args.length != 2) {
System.err.println("Usage: " + SOTAExample.class.getName() + " <data-file> <constraints-file>");
System.exit(1);
}
 
// the db name
String sota = "sota";
String dataFile = args[0];
String constraintsFile = args[1];
// first create a temporary database to use
// (if there is already a database with such a name, drop it first)
// Stardog should be running on the same machine locally for this example
StardogDBMS dbms = StardogDBMS.toServer("snarl://localhost:5820").credentials("admin", "admin".toCharArray()).login();
if (dbms.list().contains(sota)) {
dbms.drop(sota);
}
// Load the data in the db while creating it
dbms.memory(sota).create(new File(dataFile));
dbms.logout();
// obtain a connection to the database
Connection aConn = ConnectionConfiguration
.to(sota) // the name of the db to connect to
.url("snarl://localhost:5820")
.reasoning(ReasoningType.QL) // we can use reasoning but it is not required for this example
.credentials("admin", "admin") // credentials to use while connecting
.connect(); // now open the connection
 
 
// ok, we have a database, now need the validator
ICVConnection aValidator = aConn.as(ICVConnection.class);
// add the constraints, must do this in a transaction
aValidator.begin();
aValidator.addConstraints().format(RDFFormat.TURTLE).file(new File(constraintsFile));
aValidator.commit();
 
// we'll use qnames to print URIs
TurtleValueWriter aWriter = new TurtleValueWriter(aValidator.namespaces());
 
Iteration<ConstraintViolation<BindingSet>, StardogException> aViolationIter = aValidator.getViolationBindings();
while (aViolationIter.hasNext()) {
ConstraintViolation<BindingSet> aViolation = aViolationIter.next();
 
Iteration<Resource, StardogException> aViolatingIndividuals = ICV.asIndividuals(aViolation.getViolations());
// ALWAYS close Iterations when you're done with them!
aViolatingIndividuals.close();
 
System.out.format("Constraint:%n%s%n%n", aViolation.getConstraint());
Iteration<BindingSet, StardogException> aVioIter = aViolation.getViolations();
 
int aCount = 1;
while (aVioIter.hasNext()) {
BindingSet aBindingSet = aVioIter.next();
 
System.out.format("Violation %d:%n", aCount);
Pair<Set<Statement>, Set<Statement>> aExpl = ICV.getExplanation(aViolation.getConstraint(),
aBindingSet);
 
if (!aExpl.first.isEmpty()) {
System.out.println("Violating statements:");
writeTriples(aWriter, aExpl.first);
}
 
if (!aExpl.second.isEmpty()) {
System.out.println("Missing statements:");
writeTriples(aWriter, aExpl.second);
}
System.out.println();
}
aVioIter.close();
System.out.println(Strings.repeat("-", 80));
System.out.println();
}
 
// ALWAYS close Iterations when you're done with them!
aViolationIter.close();
// always close your connections when you're done
aConn.close();
}
 
private static void writeTriples(final TurtleValueWriter theWriter, Iterable<Statement> theGraph) throws Exception {
for (Statement aStmt : theGraph) {
writeTriple(theWriter, aStmt);
}
}
private static void writeTriple(final TurtleValueWriter theWriter, Statement theStmt) throws Exception {
System.out.print(theWriter.write(theStmt.getSubject()));
System.out.print(" ");
System.out.print(theWriter.write(theStmt.getPredicate()));
System.out.print(" ");
System.out.print(theWriter.write(theStmt.getObject()));
System.out.println();
}
}
View commands.log
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122
Bulk loading data to new database.
Loading data completed...Loaded 23 triples in 00:00:00 @ 2.3K triples/sec.
Successfully created database 'sota'.
 
Constraint:
AxiomConstraint{Range(SOTA-ex:related,SOTA-ex:Issue)}
 
Violation 1:
Violating statements:
:issue7 :related :issue4
Missing statements:
:issue4 rdf:type :Issue
 
Violation 1:
Violating statements:
:issue7 :related :issue3
Missing statements:
:issue3 rdf:type :Issue
 
Violation 1:
Violating statements:
:issue7 :related :issue2
Missing statements:
:issue2 rdf:type :Issue
 
--------------------------------------------------------------------------------
 
Constraint:
AxiomConstraint{Domain(SOTA-ex:reportedOn,SOTA-ex:Issue)}
 
Violation 1:
Violating statements:
:issue4 :reportedOn "x0"
Missing statements:
:issue4 rdf:type :Issue
 
--------------------------------------------------------------------------------
 
Constraint:
AxiomConstraint{SubClassOf(SOTA-ex:Issue,cardinality(SOTA-ex:reportedBy,1,owl:Thing))}
 
Violation 1:
Violating statements:
:issue7 :reportedBy :user2
:issue7 rdf:type :Issue
:issue7 rdf:type owl:Thing
:issue7 :reportedBy :user6
Missing statements:
:issue7 :reportedBy <urn:x0>
 
Violation 1:
Violating statements:
:issue7 :reportedBy :user2
:issue7 rdf:type :Issue
:issue7 rdf:type owl:Thing
:issue7 :reportedBy :user6
Missing statements:
:issue7 :reportedBy <urn:x0>
 
--------------------------------------------------------------------------------
 
Constraint:
AxiomConstraint{Range(SOTA-ex:reproducedBy,foaf:Person)}
 
Violation 1:
Violating statements:
:issue7 :reproducedBy :user1
Missing statements:
:user1 rdf:type foaf:Person
 
--------------------------------------------------------------------------------
 
Constraint:
AxiomConstraint{EquivalentClasses(SOTA-ex:ValidState, oneOf(SOTA-ex:unassigned, SOTA-ex:assigned))}
 
Violation 1:
Violating statements:
:unassigned rdf:type :ValidState
:unassigned rdf:type owl:Thing
Missing statements:
:unassigned rdf:type :ValidState
 
--------------------------------------------------------------------------------
 
Constraint:
AxiomConstraint{Range(SOTA-ex:reportedBy,foaf:Person)}
 
Violation 1:
Violating statements:
:issue7 :reportedBy :user6
Missing statements:
:user6 rdf:type foaf:Person
 
--------------------------------------------------------------------------------
 
Constraint:
AxiomConstraint{Domain(SOTA-ex:state,SOTA-ex:Issue)}
 
Violation 1:
Violating statements:
:issue4 :state :unsinged
Missing statements:
:issue4 rdf:type :Issue
 
--------------------------------------------------------------------------------
 
Constraint:
AxiomConstraint{Range(SOTA-ex:state,SOTA-ex:ValidState)}
 
Violation 1:
Violating statements:
:issue7 :state :unassigned
Missing statements:
:unassigned rdf:type :ValidState
 
Violation 1:
Violating statements:
:issue4 :state :unsinged
Missing statements:
:unsinged rdf:type :ValidState
 
--------------------------------------------------------------------------------
View commands.log
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103
// Copyright (c) 2010 - 2013 -- Clark & Parsia, LLC. <http://www.clarkparsia.com>
// For more information about licensing and copyright of this software, please contact
// inquiries@clarkparsia.com or visit http://stardog.com
 
package com.clarkparsia.stardog.examples.api;
 
import java.io.File;
 
import org.openrdf.query.BindingSet;
 
import com.clarkparsia.common.iterations.Iteration;
import com.clarkparsia.stardog.StardogDBMS;
import com.clarkparsia.stardog.StardogException;
import com.clarkparsia.stardog.api.Connection;
import com.clarkparsia.stardog.api.ConnectionConfiguration;
import com.clarkparsia.stardog.icv.Constraint;
import com.clarkparsia.stardog.icv.ConstraintFactory;
import com.clarkparsia.stardog.icv.ConstraintViolation;
import com.clarkparsia.stardog.icv.api.ICVConnection;
import com.clarkparsia.stardog.reasoning.ReasoningType;
import com.clarkparsia.stardog.util.TextTableQueryResultWriter;
import com.google.common.base.Charsets;
import com.google.common.collect.Lists;
import com.google.common.io.Files;
 
/**
* Example of using Stardog Integrity Constraint functionality for data validation example described at http://www.w3.org/2012/12/rdf-val/SOTA
*
* @author Evren Sirin
*/
public class SOTAQueryExample {
 
public static void main(String[] args) throws Exception {
if (args.length != 2) {
System.err.println("Usage: " + SOTAQueryExample.class.getName() + " <data-file> <constraints-file>");
System.exit(1);
}
 
// the db name
String sota = "sota";
String dataFile = args[0];
String constraintFile = args[1];
// first create a temporary database to use
// (if there is already a database with such a name, drop it first)
// Stardog should be running on the same machine locally for this example
StardogDBMS dbms = StardogDBMS.toServer("snarl://localhost:5820").credentials("admin", "admin".toCharArray()).login();
if (dbms.list().contains(sota)) {
dbms.drop(sota);
}
// Load the data in the db while creating it
dbms.memory(sota).create(new File(dataFile));
dbms.logout();
// obtain a connection to the database
Connection aConn = ConnectionConfiguration
.to(sota) // the name of the db to connect to
.url("snarl://localhost:5820")
.reasoning(ReasoningType.QL) // we can use reasoning but it is not required for this example
.credentials("admin", "admin") // credentials to use while connecting
.connect(); // now open the connection
 
 
// ok, we have a database, now need the validator
ICVConnection aValidator = aConn.as(ICVConnection.class);
// read the SPARQL constraint from file and add it to the database
Constraint aConstraint = ConstraintFactory.constraint(Files.toString(new File(constraintFile), Charsets.UTF_8));
aValidator.begin();
aValidator.addConstraint(aConstraint);
aValidator.commit();
// validate the constraint
Iteration<ConstraintViolation<BindingSet>, StardogException> aViolationIter = aValidator.getViolationBindings();
// pretty print constraint violations
TextTableQueryResultWriter aWriter = new TextTableQueryResultWriter(System.out);
aWriter.addNamespaces(aConn.namespaces());
boolean aStarted = false;
while (aViolationIter.hasNext()) {
Iteration<BindingSet, StardogException> aVioIter = aViolationIter.next().getViolations();
while (aVioIter.hasNext()) {
BindingSet aBindingSet = aVioIter.next();
if (!aStarted) {
aWriter.startQueryResult(Lists.newArrayList(aBindingSet.getBindingNames()));
aStarted = false;
}
aWriter.handleSolution(aBindingSet);
}
aVioIter.close();
}
aWriter.endQueryResult();
 
// ALWAYS close Iterations when you're done with them!
aViolationIter.close();
// always close your connections when you're done
aConn.close();
}
}
View commands.log
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
Bulk loading data to new database.
Loading data completed...Loaded 23 triples in 00:00:00 @ 2.6K triples/sec.
Successfully created database 'sota'.
 
+---------+----------+------------+---------------+---------------+---------------------+----------------------+----------------------+
| issue | typeArc | stateValue | reportedByArc | reportedOnArc | reportedByArcCount | reproducedOnSequence | missingRelatedIssues |
+---------+----------+------------+---------------+---------------+---------------------+----------------------+----------------------+
| :issue7 | "passed" | "passed" | "passed" | "passed" | "expected 1, got 2" | "<http://www.w3.org/ | |
| | | | | | | 2012/12/rdf-val/SOTA | |
| | | | | | | -ex#issue2> | |
| | | | | | | <http://www.w3.org/2 | |
| | | | | | | 012/12/rdf-val/SOTA- | |
| | | | | | | ex#issue3>" | |
| :issue4 | "passed" | "invalid" | "missing" | "passed" | "expected 1, got 0" | "passed" | "passed" |
+---------+----------+------------+---------------+---------------+---------------------+----------------------+----------------------+
 
Query returned 2 results in 00:00:00.069
View commands.log
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
PREFIX : <http://www.w3.org/2012/12/rdf-val/SOTA-ex#>
PREFIX foaf: <http://xmlns.com/foaf/0.1/'>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
 
SELECT DISTINCT ?issue
(if(BOUND(?t), "passed", "missing") AS ?typeArc)
(if(BOUND(?state) && (?state=:unassigned || ?state=:assigned),
"passed", "invalid") AS ?stateValue)
(if(BOUND(?reportedBy), "passed", "missing") AS ?reportedByArc)
(if(BOUND(?reportedOn), "passed", "missing") AS ?reportedOnArc)
(if(!BOUND(?reportedByCount), "expected 1, got 0",
if(?reportedByCount=1, "passed",
CONCAT("expected 1, got ", STR(?reportedByCount)))) AS ?reportedByArcCount)
(if(!BOUND(?reproducedOn) || ?reproducedOn > ?reportedOn,
"passed", "bad sequence") AS ?reproducedOnSequence)
(if(BOUND(?missingRelatedIssuesStr), ?missingRelatedIssuesStr, "passed")
AS ?missingRelatedIssues)
 
WHERE {
 
# Get all viable :Issues by use of related predicates.
{ SELECT DISTINCT ?issue WHERE {
{ ?issue a :Issue }
UNION { ?issue :reportedBy|:reportedOn|:reproducedBy|:reproducedOn|:related ?rprt }
}
}
 
# Test for a type arc and state.
OPTIONAL { ?issue a ?t FILTER (?t = :Issue) }
OPTIONAL { ?issue :state ?state }
 
# Must have 1 reportedBy.
OPTIONAL { SELECT ?issue
(SAMPLE(?reportedBy1) AS ?reportedBy)
(COUNT(?reportedBy1) AS ?reportedByCount)
WHERE {
OPTIONAL { ?issue :reportedBy ?reportedBy1 }
} GROUP BY ?issue
}
OPTIONAL { ?issue :reportedOn ?reportedOn }
OPTIONAL { ?issue :reproducedBy ?reproducedBy }
OPTIONAL { ?issue :reproducedOn ?reproducedOn }
 
# All :related issues must be known entities.
OPTIONAL {
SELECT ?issue
(GROUP_CONCAT(CONCAT("<", STR(?referent), ">"))
AS ?missingRelatedIssuesStr) {
 
# List of missing issues related to ?issue.
SELECT ?issue ?referent
(SUM(if(BOUND(?referentP), 1, 0)) AS ?referentCount)
WHERE {
?issue :related ?referent
OPTIONAL { ?referent ?referentP ?referentO }
} GROUP BY ?issue ?referent
HAVING (SUM(if(BOUND(?referentP), 1, 0)) = 0)
} GROUP BY ?issue
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.