Repository: apache/jena (java)

Issue:
Inconsistent default graph handling in RIOT writers
### Version

5.0.0

### What happened?

To be honest, I don't know if this is a bug or I'm just horribly confused. I wrote a deserializer for Jena that outputs quads with "null" in the graph term position when it's a default graph. When I then tried to serialize such a dataset using RIOT's serializers, I've noticed that for JSON-LD 1.1 the serialization fails completely, while for others it works.

I dug deeper and found out that Jena has 3 ways to represent the default graph: `null`, `Quad.defaultGraphIRI`, and `Quad.defaultGraphNodeGenerated`. I read the javadoc of the latter two and honestly, I still don't fully understand what's the difference. I then checked how the different RIOT writer implementations handle the different flavors of default graphs.

The code is here (Apache 2.0): https://github.com/Ostrzyciel/jena-default-graphs/blob/main/jena-default-graphs/src/main/java/org/example/Main.java

Output is attached below.

N-Quads, RDF-PROTO, RDF-THRIFT serialize/deserialize the default graph correctly in all three variants. JSON-LD 1.1 fails with a null pointer exception for the null variant (easy fix). TriG fails silently and just outputs nothing for the null variant (also likely to be an easy fix).

All parsers output `Quad.defaultGraphIRI` which I guess makes it the "right" choice when implementing parsers.

What I don't know is if this is a bug at all or I'm just being confused. If this is a bug, I can happily fix the null handling in TriG and JSON-LD.

### Relevant output and stacktrace

```shell
Empty output for null graph in TriG/pretty
Failed to write null graph in JSON-LD-11/pretty
org.apache.jena.shared.JenaException: Exception while writing JSON-LD 1.1
	at org.apache.jena.riot.writer.JsonLD11Writer.write$(JsonLD11Writer.java:123)
	at org.apache.jena.riot.writer.JsonLD11Writer.write(JsonLD11Writer.java:73)
	at org.apache.jena.riot.RDFWriter.write$(RDFWriter.java:261)
	at org.apache.jena.riot.RDFWriter.output(RDFWriter.java:219)
	at org.apache.jena.riot.RDFWriter.output(RDFWriter.java:158)
	at org.apache.jena.riot.RDFWriterBuilder.output(RDFWriterBuilder.java:207)
	at org.apache.jena.riot.RDFDataMgr.write$(RDFDataMgr.java:809)
	at org.apache.jena.riot.RDFDataMgr.write(RDFDataMgr.java:754)
	at org.example.Main.main(Main.java:63)
Caused by: java.lang.NullPointerException: Cannot invoke "org.apache.jena.graph.Node.isBlank()" because "node" is null
	at org.apache.jena.riot.system.JenaTitanium.resource(JenaTitanium.java:133)
	at org.apache.jena.riot.system.JenaTitanium.lambda$convert$0(JenaTitanium.java:60)
	at org.apache.jena.atlas.iterator.Iter$IterMap.lambda$forEachRemaining$0(Iter.java:432)
	at org.apache.jena.mem2.store.fast.FastArrayBunch$1.forEachRemaining(FastArrayBunch.java:164)
	at org.apache.jena.mem2.iterator.IteratorOfJenaSets.forEachRemaining(IteratorOfJenaSets.java:71)
	at org.apache.jena.atlas.iterator.Iter$IterMap.forEachRemaining(Iter.java:432)
	at org.apache.jena.atlas.iterator.Iter.forEachRemaining(Iter.java:927)
	at org.apache.jena.atlas.iterator.IteratorConcat.forEachRemaining(IteratorConcat.java:99)
	at org.apache.jena.riot.system.JenaTitanium.convert(JenaTitanium.java:49)
	at org.apache.jena.riot.writer.JsonLD11Writer.write$(JsonLD11Writer.java:91)
	... 8 more

To serialize                       Format                   Status/deserialized
null graph                         TriG/pretty              empty output   
null graph                         JSON-LD-11/pretty        serialization failed
null graph                         N-Quads/utf-8            Quad.defaultGraphIRI
null graph                         RDF-PROTO                Quad.defaultGraphIRI
null graph                         RDF-THRIFT               Quad.defaultGraphIRI
Quad.defaultGraphNodeGenerated     TriG/pretty              Quad.defaultGraphIRI
Quad.defaultGraphNodeGenerated     JSON-LD-11/pretty        Quad.defaultGraphIRI
Quad.defaultGraphNodeGenerated     N-Quads/utf-8            Quad.defaultGraphIRI
Quad.defaultGraphNodeGenerated     RDF-PROTO                Quad.defaultGraphIRI
Quad.defaultGraphNodeGenerated     RDF-THRIFT               Quad.defaultGraphIRI
Quad.defaultGraphIRI               TriG/pretty              Quad.defaultGraphIRI
Quad.defaultGraphIRI               JSON-LD-11/pretty        Quad.defaultGraphIRI
Quad.defaultGraphIRI               N-Quads/utf-8            Quad.defaultGraphIRI
Quad.defaultGraphIRI               RDF-PROTO                Quad.defaultGraphIRI
Quad.defaultGraphIRI               RDF-THRIFT               Quad.defaultGraphIRI
```


### Are you interested in making a pull request?

Yes

Pull request:
GH-2578: Fix Quad.isTriple() handling in JenaTitanium
GitHub issue resolved #2578

Pull request Description: This commit adds a `Quad.isTriple()` check when writing JSON-LD files, to cover the case where we have a "triple in quad". I've also added a test to check if this works. The test fails on the current main branch version of Jena.



----

 - [x] Tests are included.
 - [x] Documentation change and updates are provided for the [Apache Jena website](https://github.com/apache/jena-site/)
 - [x] Commits have been squashed to remove intermediate development commit messages.
 - [x] Key commit messages start with the issue number (GH-xxxx)

By submitting this pull request, I acknowledge that I am making a contribution to the Apache Software Foundation under the terms and conditions of the [Contributor's Agreement](https://www.apache.org/licenses/contributor-agreements.html).

----

See the [Apache Jena "Contributing" guide](https://github.com/apache/jena/blob/main/CONTRIBUTING.md).


Patch:
diff --git a/jena-arq/src/main/java/org/apache/jena/http/sys/ExecHTTPBuilder.java b/jena-arq/src/main/java/org/apache/jena/http/sys/ExecHTTPBuilder.java
index a1f1340b17..c5b6718d27 100644
--- a/jena-arq/src/main/java/org/apache/jena/http/sys/ExecHTTPBuilder.java
+++ b/jena-arq/src/main/java/org/apache/jena/http/sys/ExecHTTPBuilder.java
@@ -44,6 +44,7 @@ public abstract class ExecHTTPBuilder<X, Y> {
     protected String serviceURL = null;
     private Query query = null;
     protected String queryString = null;
+    protected boolean parseCheck = true;
     private HttpClient httpClient = null;
     protected Map<String, String> httpHeaders = new HashMap<>();
     protected Params params = Params.create();
@@ -72,6 +73,12 @@ public abstract class ExecHTTPBuilder<X, Y> {
         return thisBuilder();
     }
 
+    /** Whether to parse query strings passed to {@link #query(String)}. */
+    public Y parseCheck(boolean parseCheck) {
+        this.parseCheck = parseCheck;
+        return thisBuilder();
+    }
+
     /** Set the query - this also sets the query string to agree with the query argument. */
     public Y query(Query query) {
         Objects.requireNonNull(query);
@@ -86,7 +93,7 @@ public abstract class ExecHTTPBuilder<X, Y> {
      */
     public Y query(String queryStr) {
         Objects.requireNonNull(queryStr);
-        Query query = QueryFactory.create(queryStr);
+        Query query = parseCheck ? QueryFactory.create(queryStr) : null;
         setQuery(query, queryStr);
         return thisBuilder();
     }
@@ -324,9 +331,15 @@ public abstract class ExecHTTPBuilder<X, Y> {
         String queryStringActual = queryString;
 
         if ( substitutionMap != null && ! substitutionMap.isEmpty() ) {
-            if ( query == null )
-                throw new QueryException("Substitution only supported if a Query object was provided");
-            queryActual = QueryTransformOps.transform(query, substitutionMap);
+            if ( queryActual == null ) {
+                // Substitution requires a query object. Attempt to parse the given string.
+                try {
+                    queryActual = QueryFactory.create(queryString);
+                } catch (Exception e) {
+                    throw new QueryException("Substitution only supported for Query objects. Failed to parse the given string as a Query object.", e);
+                }
+            }
+            queryActual = QueryTransformOps.transform(queryActual, substitutionMap);
             queryStringActual = queryActual.toString();
         }
         Context cxt = contextAcc.context();
diff --git a/jena-arq/src/main/java/org/apache/jena/http/sys/ExecUpdateHTTPBuilder.java b/jena-arq/src/main/java/org/apache/jena/http/sys/ExecUpdateHTTPBuilder.java
index 52d16809a0..19bbe0f801 100644
--- a/jena-arq/src/main/java/org/apache/jena/http/sys/ExecUpdateHTTPBuilder.java
+++ b/jena-arq/src/main/java/org/apache/jena/http/sys/ExecUpdateHTTPBuilder.java
@@ -20,12 +20,11 @@ package org.apache.jena.http.sys;
 
 import java.net.http.HttpClient;
 import java.util.*;
+import java.util.stream.Collectors;
 
-import org.apache.jena.atlas.lib.InternalErrorException;
 import org.apache.jena.graph.Node;
 import org.apache.jena.http.HttpEnv;
 import org.apache.jena.query.ARQ;
-import org.apache.jena.query.QueryException;
 import org.apache.jena.sparql.core.Var;
 import org.apache.jena.sparql.engine.binding.Binding;
 import org.apache.jena.sparql.exec.http.Params;
@@ -41,11 +40,105 @@ import org.apache.jena.update.UpdateRequest;
 
 public abstract class ExecUpdateHTTPBuilder<X, Y> {
 
+    /** Update element. Either an Update object or a string. */
+    private record UpdateElt(Update update, String updateString) {
+        UpdateElt(Update update) { this(Objects.requireNonNull(update), null); }
+        UpdateElt(String updateString) { this(null, Objects.requireNonNull(updateString)); }
+        boolean isParsed() { return update != null; }
+
+        @Override
+        public String toString() {
+            return isParsed()
+                    ? new UpdateRequest(update()).toString() // Reuse UpdateRequest's serialization approach
+                    : updateString();
+        }
+    }
+
+    /** Accumulator for update elements. Can build an overall string or UpdateRequest from the elements. */
+    private class UpdateEltAcc implements Iterable<UpdateElt> {
+        /** Delimiter for joining multiple SPARQL update strings into a single one. */
+        public static final String DELIMITER = ";\n";
+
+        private List<UpdateElt> updateOperations = new ArrayList<>();
+        private List<UpdateElt> updateOperationsView = Collections.unmodifiableList(updateOperations);
+        private boolean isParsed = true; // True iff there are no strings in updateOperations
+
+        public boolean isParsed() {
+            return isParsed;
+        }
+
+        public void add(UpdateElt updateElt) {
+            isParsed = isParsed && updateElt.isParsed();
+            updateOperations.add(updateElt);
+        }
+
+        public void add(Update update) {
+            add(new UpdateElt(update));
+        }
+
+        public void add(String updateRequestString) {
+            UpdateRequest updateRequest = UpdateFactory.create(updateRequestString);
+            add(updateRequest);
+        }
+
+        public void add(UpdateRequest updateRequest) {
+            updateRequest.getOperations().forEach(this::add);
+        }
+
+        /** Add a string without parsing it. */
+        public void addString(String updateRequestString) {
+            add(new UpdateElt(updateRequestString));
+        }
+
+        /** Attempt to build an UpdateRequest from the state of this accumulator. Attempts to parse any string elements. */
+        public UpdateRequest buildUpdateRequest() {
+            return addToUpdateRequest(new UpdateRequest());
+        }
+
+        public UpdateRequest addToUpdateRequest(UpdateRequest updateRequest) {
+            for (UpdateElt elt : updateOperations) {
+                if (elt.isParsed()) {
+                    updateRequest.add(elt.update());
+                } else {
+                    try {
+                        updateRequest.add(elt.updateString());
+                    } catch (Exception e) {
+                        // Expose the string that failed to parse
+                        e.addSuppressed(new RuntimeException("Failed to parse: " + elt.updateString()));
+                        throw e;
+                    }
+                }
+            }
+            return updateRequest;
+        }
+
+        public void clear() {
+            updateOperations.clear();
+            isParsed = true;
+        }
+
+        public boolean isEmpty() {
+            return updateOperations.isEmpty();
+        }
+
+        @Override
+        public Iterator<UpdateElt> iterator() {
+            return updateOperationsView.iterator();
+        }
+
+        public String buildString() {
+            return updateOperations.stream()
+                .map(UpdateElt::toString)
+                .collect(Collectors.joining(DELIMITER));
+        }
+    }
+
     static { JenaSystem.init(); }
 
     protected String serviceURL;
-    protected String updateString;
-    private UpdateRequest updateOperations = null;
+    protected boolean parseCheck = true;
+    private UpdateEltAcc updateEltAcc = new UpdateEltAcc();
+
     protected Params params = Params.create();
     protected boolean allowCompression;
     protected Map<String, String> httpHeaders = new HashMap<>();
@@ -68,26 +161,24 @@ public abstract class ExecUpdateHTTPBuilder<X, Y> {
 
     public Y update(UpdateRequest updateRequest) {
         Objects.requireNonNull(updateRequest);
-        ensureUpdateRequest();
-        updateRequest.getOperations().forEach(this::update);
-        this.updateString = null;
+        updateEltAcc.add(updateRequest);
         return thisBuilder();
     }
 
     public Y update(String updateRequestString) {
-        ensureUpdateRequest();
-        UpdateRequest more = UpdateFactory.create(updateRequestString);
-        more.getOperations().forEach(this::update);
-        this.updateString = null;
+        Objects.requireNonNull(updateRequestString);
+        if (parseCheck) {
+            updateEltAcc.add(updateRequestString);
+        } else {
+            updateEltAcc.addString(updateRequestString);
+        }
         return thisBuilder();
     }
 
     /** Add the update. */
     public Y update(Update update) {
         Objects.requireNonNull(update);
-        ensureUpdateRequest();
-        this.updateOperations.add(update);
-        this.updateString = null;
+        updateEltAcc.add(update);
         return thisBuilder();
     }
 
@@ -98,8 +189,13 @@ public abstract class ExecUpdateHTTPBuilder<X, Y> {
      */
     public Y updateString(String updateString) {
         Objects.requireNonNull(updateString);
-        this.updateOperations = null;
-        this.updateString = updateString;
+        updateEltAcc.clear();
+        updateEltAcc.addString(updateString);
+        return thisBuilder();
+    }
+
+    public Y parseCheck(boolean parseCheck) {
+        this.parseCheck = parseCheck;
         return thisBuilder();
     }
 
@@ -200,28 +296,35 @@ public abstract class ExecUpdateHTTPBuilder<X, Y> {
             context = Context.create();
     }
 
-    private void ensureUpdateRequest() {
-        if ( updateOperations == null )
-            updateOperations = new UpdateRequest();
-    }
-
     public X build() {
         Objects.requireNonNull(serviceURL, "No service URL");
-        if ( updateOperations == null && updateString == null )
-            throw new QueryException("No update for UpdateExecutionHTTP");
-        if ( updateOperations != null && updateString != null )
-            throw new InternalErrorException("UpdateRequest and update string");
+        if ( updateEltAcc.isEmpty() )
+            throw new UpdateException("No update for UpdateExecutionHTTP");
+
         HttpClient hClient = HttpEnv.getHttpClient(serviceURL, httpClient);
 
-        UpdateRequest updateActual = updateOperations;
+        // If all elements are objects then immediately build the UpdateRequest.
+        UpdateRequest updateActual = updateEltAcc.isParsed() ? updateEltAcc.buildUpdateRequest() : null;
+
         if ( substitutionMap != null && ! substitutionMap.isEmpty() ) {
-            if ( updateActual == null )
-                throw new UpdateException("Substitution only supported if an UpdateRequest object was provided");
+            // If updateActual is null it means that some elements are strings
+            // and we need to parse those now.
+            if ( updateActual == null ) {
+                try {
+                    updateActual = updateEltAcc.buildUpdateRequest();
+                } catch (Exception e) {
+                    throw new UpdateException("Substitution only supported for UpdateRequest objects. Failed to parse a given string as an UpdateRequest object.", e);
+                }
+            }
             updateActual = UpdateTransformOps.transform(updateActual, substitutionMap);
         }
+
+        // If the UpdateRequest object wasn't built until now then build the string instead.
+        String updateStringActual = updateActual == null ? updateEltAcc.buildString() : null;
+
         Context cxt = (context!=null) ? context : ARQ.getContext().copy();
-        return buildX(hClient, updateActual, cxt);
+        return buildX(hClient, updateActual, updateStringActual, cxt);
     }
 
-    protected abstract X buildX(HttpClient hClient, UpdateRequest updateActual, Context cxt);
+    protected abstract X buildX(HttpClient hClient, UpdateRequest updateActual, String updateStringActual, Context cxt);
 }
diff --git a/jena-arq/src/main/java/org/apache/jena/query/QueryExecutionBuilder.java b/jena-arq/src/main/java/org/apache/jena/query/QueryExecutionBuilder.java
index bd12171ec4..6ab6a6b7c4 100644
--- a/jena-arq/src/main/java/org/apache/jena/query/QueryExecutionBuilder.java
+++ b/jena-arq/src/main/java/org/apache/jena/query/QueryExecutionBuilder.java
@@ -42,6 +42,9 @@ public interface QueryExecutionBuilder {
 
     public QueryExecutionBuilder query(String queryString, Syntax syntax);
 
+    /** Hint whether to immediately parse query strings passed to {@link #query(String)} */
+    public QueryExecutionBuilder parseCheck(boolean parseCheck);
+
     public QueryExecutionBuilder set(Symbol symbol, Object value);
 
     public QueryExecutionBuilder set(Symbol symbol, boolean value);
diff --git a/jena-arq/src/main/java/org/apache/jena/query/QueryExecutionDatasetBuilder.java b/jena-arq/src/main/java/org/apache/jena/query/QueryExecutionDatasetBuilder.java
index d4f3c60978..ab4fe51b12 100644
--- a/jena-arq/src/main/java/org/apache/jena/query/QueryExecutionDatasetBuilder.java
+++ b/jena-arq/src/main/java/org/apache/jena/query/QueryExecutionDatasetBuilder.java
@@ -67,6 +67,12 @@ public class QueryExecutionDatasetBuilder implements QueryExecutionBuilder {
         return this;
     }
 
+    @Override
+    public QueryExecutionBuilder parseCheck(boolean parseCheck) {
+        builder.parseCheck(parseCheck);
+        return this;
+    }
+
     public QueryExecutionDatasetBuilder dataset(Dataset dataset) {
         this.dataset = dataset;
         builder.dataset(dataset.asDatasetGraph());
diff --git a/jena-arq/src/main/java/org/apache/jena/riot/system/JenaTitanium.java b/jena-arq/src/main/java/org/apache/jena/riot/system/JenaTitanium.java
index 74931c77da..e6c7abbe5a 100644
--- a/jena-arq/src/main/java/org/apache/jena/riot/system/JenaTitanium.java
+++ b/jena-arq/src/main/java/org/apache/jena/riot/system/JenaTitanium.java
@@ -51,7 +51,7 @@ public class JenaTitanium {
             RdfResource predicate = resource(provider, labelMapping, quad.getPredicate());
             RdfValue object = nodeToValue(provider, labelMapping, quad.getObject());
 
-            if ( quad.isDefaultGraph() ) {
+            if ( quad.isTriple() || quad.isDefaultGraph() ) {
                 RdfTriple t = provider.createTriple(subject, predicate, object);
                 rdfDataset.add(t);
             }
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/exec/QueryExecBuilder.java b/jena-arq/src/main/java/org/apache/jena/sparql/exec/QueryExecBuilder.java
index a2c4fdbc4a..43aa09b10f 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/exec/QueryExecBuilder.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/exec/QueryExecBuilder.java
@@ -42,6 +42,9 @@ public interface QueryExecBuilder extends QueryExecMod {
     /** Set the query. */
     public QueryExecBuilder query(String queryString, Syntax syntax);
 
+    /** Hint whether to immediately parse query strings passed to {@link #query(String)} */
+    public QueryExecBuilder parseCheck(boolean parseCheck);
+
     /** Set a context entry. */
     public QueryExecBuilder set(Symbol symbol, Object value);
 
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/exec/QueryExecBuilderAdapter.java b/jena-arq/src/main/java/org/apache/jena/sparql/exec/QueryExecBuilderAdapter.java
index f5b7b42a67..be6c951ef5 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/exec/QueryExecBuilderAdapter.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/exec/QueryExecBuilderAdapter.java
@@ -106,6 +106,12 @@ public class QueryExecBuilderAdapter
         return this;
     }
 
+    @Override
+    public QueryExecBuilder parseCheck(boolean parseCheck) {
+        builder = builder.parseCheck(parseCheck);
+        return this;
+    }
+
     @Override
     public QueryExecBuilder set(Symbol symbol, Object value) {
         builder = builder.set(symbol, value);
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/exec/QueryExecDatasetBuilder.java b/jena-arq/src/main/java/org/apache/jena/sparql/exec/QueryExecDatasetBuilder.java
index 24ca3b9ae3..a080f9a8e5 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/exec/QueryExecDatasetBuilder.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/exec/QueryExecDatasetBuilder.java
@@ -91,6 +91,12 @@ public class QueryExecDatasetBuilder implements QueryExecMod, QueryExecBuilder {
         return this;
     }
 
+    /** The parse-check flag has no effect for query execs over datasets. */
+    @Override
+    public QueryExecDatasetBuilder parseCheck(boolean parseCheck) {
+        return this;
+    }
+
     @Override
     public QueryExecDatasetBuilder query(String queryString, Syntax syntax) {
         this.queryString = queryString;
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/exec/QueryExecutionBuilderAdapter.java b/jena-arq/src/main/java/org/apache/jena/sparql/exec/QueryExecutionBuilderAdapter.java
index b91879c3e1..c63be1db4c 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/exec/QueryExecutionBuilderAdapter.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/exec/QueryExecutionBuilderAdapter.java
@@ -80,6 +80,12 @@ public class QueryExecutionBuilderAdapter implements QueryExecutionBuilder {
         return this;
     }
 
+    @Override
+    public QueryExecutionBuilderAdapter parseCheck(boolean parseCheck) {
+        builder.parseCheck(parseCheck);
+        return this;
+    }
+
     @Override
     public QueryExecutionBuilderAdapter set(Symbol symbol, Object value) {
         builder.set(symbol, value);
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/exec/UpdateExecBuilder.java b/jena-arq/src/main/java/org/apache/jena/sparql/exec/UpdateExecBuilder.java
index 591d6d3474..46434ed84f 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/exec/UpdateExecBuilder.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/exec/UpdateExecBuilder.java
@@ -29,15 +29,19 @@ import org.apache.jena.update.UpdateRequest;
 
 public interface UpdateExecBuilder {
 
-    /** Set the update. */
+    /** Append the updates in an {@link UpdateRequest} to the {@link UpdateRequest} being built. */
     public UpdateExecBuilder update(UpdateRequest request);
 
-    /** Set the update. */
+    /** Add the {@link Update} to the {@link UpdateRequest} being built. */
     public UpdateExecBuilder update(Update update);
 
-    /** Set the update. */
+    /** Add the string to the {@link UpdateRequest} being built.
+     *  Implementations may support the {@link #parseCheck(boolean)} hint to control whether or not to parse the given strings. */
     public UpdateExecBuilder update(String updateString);
 
+    /** Hint whether to immediately parse strings passed to {@link #update(String)}. */
+    public UpdateExecBuilder parseCheck(boolean parseCheck);
+
     /** Set a context entry. */
     public UpdateExecBuilder set(Symbol symbol, Object value);
 
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/exec/UpdateExecBuilderAdapter.java b/jena-arq/src/main/java/org/apache/jena/sparql/exec/UpdateExecBuilderAdapter.java
index d9d90729da..1355939aa6 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/exec/UpdateExecBuilderAdapter.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/exec/UpdateExecBuilderAdapter.java
@@ -76,6 +76,12 @@ public class UpdateExecBuilderAdapter
         return this;
     }
 
+    @Override
+    public UpdateExecBuilder parseCheck(boolean parseCheck) {
+        builder = builder.parseCheck(parseCheck);
+        return this;
+    }
+
     @Override
     public UpdateExecBuilder set(Symbol symbol, Object value) {
         builder = builder.set(symbol, value);
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/exec/UpdateExecDatasetBuilder.java b/jena-arq/src/main/java/org/apache/jena/sparql/exec/UpdateExecDatasetBuilder.java
index d69d3bcaaa..52979eedf7 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/exec/UpdateExecDatasetBuilder.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/exec/UpdateExecDatasetBuilder.java
@@ -77,6 +77,12 @@ public class UpdateExecDatasetBuilder implements UpdateExecBuilder {
         return this;
     }
 
+    /** Hint has no effect on update execs over datasets. */
+    @Override
+    public UpdateExecBuilder parseCheck(boolean parseCheck) {
+        return this;
+    }
+
     public UpdateExecDatasetBuilder dataset(DatasetGraph dsg) {
         this.dataset = dsg;
         return this;
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/exec/UpdateExecutionBuilderAdapter.java b/jena-arq/src/main/java/org/apache/jena/sparql/exec/UpdateExecutionBuilderAdapter.java
index 2c287d192a..48240e6822 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/exec/UpdateExecutionBuilderAdapter.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/exec/UpdateExecutionBuilderAdapter.java
@@ -66,6 +66,12 @@ public class UpdateExecutionBuilderAdapter implements UpdateExecutionBuilder {
         return this;
     }
 
+    @Override
+    public UpdateExecutionBuilder parseCheck(boolean parseCheck) {
+        builder.parseCheck(parseCheck);
+        return this;
+    }
+
     @Override
     public UpdateExecutionBuilder set(Symbol symbol, Object value) {
         builder.set(symbol, value);
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/exec/http/UpdateExecHTTPBuilder.java b/jena-arq/src/main/java/org/apache/jena/sparql/exec/http/UpdateExecHTTPBuilder.java
index 10dfdea44a..fb11b20985 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/exec/http/UpdateExecHTTPBuilder.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/exec/http/UpdateExecHTTPBuilder.java
@@ -40,8 +40,8 @@ public class UpdateExecHTTPBuilder extends ExecUpdateHTTPBuilder<UpdateExecHTTP,
     }
 
     @Override
-    protected UpdateExecHTTP buildX(HttpClient hClient, UpdateRequest updateActual, Context cxt) {
-        return new UpdateExecHTTP(serviceURL, updateActual, updateString, hClient, params,
+    protected UpdateExecHTTP buildX(HttpClient hClient, UpdateRequest updateActual, String updateStringActual, Context cxt) {
+        return new UpdateExecHTTP(serviceURL, updateActual, updateStringActual, hClient, params,
                                   copyArray(usingGraphURIs),
                                   copyArray(usingNamedGraphURIs),
                                   new HashMap<>(httpHeaders),
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/exec/http/UpdateExecutionHTTPBuilder.java b/jena-arq/src/main/java/org/apache/jena/sparql/exec/http/UpdateExecutionHTTPBuilder.java
index 0d07628664..5d6350c040 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/exec/http/UpdateExecutionHTTPBuilder.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/exec/http/UpdateExecutionHTTPBuilder.java
@@ -47,8 +47,8 @@ public class UpdateExecutionHTTPBuilder
     }
 
     @Override
-    protected UpdateExecutionHTTP buildX(HttpClient hClient, UpdateRequest updateActual, Context cxt) {
-        UpdateExecHTTP uExec = new UpdateExecHTTP(serviceURL, updateActual, updateString, hClient, params,
+    protected UpdateExecutionHTTP buildX(HttpClient hClient, UpdateRequest updateActual, String updateStringActual, Context cxt) {
+        UpdateExecHTTP uExec = new UpdateExecHTTP(serviceURL, updateActual, updateStringActual, hClient, params,
                                                   copyArray(usingGraphURIs),
                                                   copyArray(usingNamedGraphURIs),
                                                   new HashMap<>(httpHeaders),
diff --git a/jena-arq/src/main/java/org/apache/jena/update/UpdateExecutionBuilder.java b/jena-arq/src/main/java/org/apache/jena/update/UpdateExecutionBuilder.java
index e2cef17799..0a294e3c22 100644
--- a/jena-arq/src/main/java/org/apache/jena/update/UpdateExecutionBuilder.java
+++ b/jena-arq/src/main/java/org/apache/jena/update/UpdateExecutionBuilder.java
@@ -34,6 +34,9 @@ public interface UpdateExecutionBuilder {
     /** Parse and update operations to the {@link UpdateRequest} being built. */
     public UpdateExecutionBuilder update(String updateRequestString);
 
+    /** Hint whether to immediately parse update strings passed to {@link #update(String)}. */
+    public UpdateExecutionBuilder parseCheck(boolean parseCheck);
+
     public UpdateExecutionBuilder set(Symbol symbol, Object value);
 
     public UpdateExecutionBuilder set(Symbol symbol, boolean value);
diff --git a/jena-arq/src/main/java/org/apache/jena/update/UpdateExecutionDatasetBuilder.java b/jena-arq/src/main/java/org/apache/jena/update/UpdateExecutionDatasetBuilder.java
index 219fc69140..63efc743f4 100644
--- a/jena-arq/src/main/java/org/apache/jena/update/UpdateExecutionDatasetBuilder.java
+++ b/jena-arq/src/main/java/org/apache/jena/update/UpdateExecutionDatasetBuilder.java
@@ -66,6 +66,12 @@ public class UpdateExecutionDatasetBuilder implements UpdateExecutionBuilder {
         return this;
     }
 
+    @Override
+    public UpdateExecutionBuilder parseCheck(boolean parseCheck) {
+        builder.parseCheck(parseCheck);
+        return this;
+    }
+
     public UpdateExecutionDatasetBuilder dataset(Dataset dataset) {
         builder.dataset(dataset.asDatasetGraph());
         return this;
diff --git a/jena-rdfconnection/src/main/java/org/apache/jena/rdfconnection/RDFConnectionWrapper.java b/jena-rdfconnection/src/main/java/org/apache/jena/rdfconnection/RDFConnectionWrapper.java
index 382cfb2f4b..f4eff12d2f 100644
--- a/jena-rdfconnection/src/main/java/org/apache/jena/rdfconnection/RDFConnectionWrapper.java
+++ b/jena-rdfconnection/src/main/java/org/apache/jena/rdfconnection/RDFConnectionWrapper.java
@@ -49,6 +49,11 @@ public class RDFConnectionWrapper implements RDFConnection {
         return get().query(query);
     }
 
+    @Override
+    public QueryExecution query(String queryString) {
+        return get().query(queryString);
+    }
+
     @Override
     public QueryExecutionBuilder newQuery() {
         return get().newQuery();
diff --git a/jena-rdfconnection/src/main/java/org/apache/jena/rdflink/RDFLinkHTTP.java b/jena-rdfconnection/src/main/java/org/apache/jena/rdflink/RDFLinkHTTP.java
index e88f9a4dc2..df90a00883 100644
--- a/jena-rdfconnection/src/main/java/org/apache/jena/rdflink/RDFLinkHTTP.java
+++ b/jena-rdfconnection/src/main/java/org/apache/jena/rdflink/RDFLinkHTTP.java
@@ -300,7 +300,7 @@ public class RDFLinkHTTP implements RDFLink {
 
         /** This method is far only called from RDFLinkHTTP. It validates the query string if parseCheckQueries is enabled.  */
         protected QueryExecHTTPBuilder queryStringValidated(String queryString, Syntax syntax, QueryType fallbackQueryType) {
-            Query parsedQuery = RDFLinkHTTP.this.parseCheckQueries
+            Query parsedQuery = parseCheck
                     ? QueryFactory.create(queryString, syntax)
                     : null;
 
@@ -373,7 +373,7 @@ public class RDFLinkHTTP implements RDFLink {
     private QueryExecHTTPBuilderOverRDFLinkHTTP createQExecBuilder() {
         checkQuery();
         QueryExecHTTPBuilderOverRDFLinkHTTP builder = new QueryExecHTTPBuilderOverRDFLinkHTTP();
-        builder.endpoint(svcQuery).httpClient(httpClient).sendMode(querySendMode);
+        builder.endpoint(svcQuery).httpClient(httpClient).sendMode(querySendMode).parseCheck(parseCheckQueries);
         return builder;
     }
 
@@ -400,7 +400,7 @@ public class RDFLinkHTTP implements RDFLink {
     /** Create a builder, configured with the link setup. */
     private UpdateExecHTTPBuilder createUExecBuilder() {
         return UpdateExecHTTPBuilder.create().endpoint(svcUpdate).httpClient(httpClient)
-                .sendMode(updateSendMode);
+                .sendMode(updateSendMode).parseCheck(parseCheckUpdates);
     }
 
     @Override
diff --git a/jena-rdfconnection/src/main/java/org/apache/jena/rdflink/RDFLinkHTTPBuilder.java b/jena-rdfconnection/src/main/java/org/apache/jena/rdflink/RDFLinkHTTPBuilder.java
index 76fbfcc5d2..a11ad05795 100644
--- a/jena-rdfconnection/src/main/java/org/apache/jena/rdflink/RDFLinkHTTPBuilder.java
+++ b/jena-rdfconnection/src/main/java/org/apache/jena/rdflink/RDFLinkHTTPBuilder.java
@@ -91,6 +91,7 @@ public class RDFLinkHTTPBuilder {
         parseCheckUpdates   = base.parseCheckUpdates;
 
         querySendMode       = base.querySendMode;
+        updateSendMode      = base.updateSendMode;
     }
 
     /** URL of the remote SPARQL endpoint.
diff --git a/jena-rdfconnection/src/main/java/org/apache/jena/rdflink/RDFLinkModular.java b/jena-rdfconnection/src/main/java/org/apache/jena/rdflink/RDFLinkModular.java
index d383ba7338..0c032c5259 100644
--- a/jena-rdfconnection/src/main/java/org/apache/jena/rdflink/RDFLinkModular.java
+++ b/jena-rdfconnection/src/main/java/org/apache/jena/rdflink/RDFLinkModular.java
@@ -101,6 +101,11 @@ public class RDFLinkModular implements RDFLink {
         return queryConnection().query(query);
     }
 
+    @Override
+    public QueryExec query(String queryString) {
+        return queryConnection().query(queryString);
+    }
+
     @Override
     public QueryExecBuilder newQuery() {
         return queryConnection().newQuery();


Test patch:
diff --git a/jena-arq/src/test/java/org/apache/jena/system/TestJenaTitanium.java b/jena-arq/src/test/java/org/apache/jena/system/TestJenaTitanium.java
index 3629b45756..19de9c176d 100644
--- a/jena-arq/src/test/java/org/apache/jena/system/TestJenaTitanium.java
+++ b/jena-arq/src/test/java/org/apache/jena/system/TestJenaTitanium.java
@@ -20,6 +20,7 @@ package org.apache.jena.system;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
 import java.io.IOException;
@@ -31,11 +32,13 @@ import com.apicatalog.rdf.io.error.RdfWriterException;
 import com.apicatalog.rdf.io.nquad.NQuadsWriter;
 
 import org.apache.jena.atlas.lib.StrUtils;
+import org.apache.jena.graph.NodeFactory;
 import org.apache.jena.riot.RDFParser;
 import org.apache.jena.riot.system.JenaTitanium;
 import org.apache.jena.riot.system.RiotLib;
 import org.apache.jena.sparql.core.DatasetGraph;
 import org.apache.jena.sparql.core.DatasetGraphFactory;
+import org.apache.jena.sparql.core.Quad;
 import org.apache.jena.sparql.sse.SSE;
 import org.apache.jena.sparql.util.IsoMatcher;
 import org.junit.Test;
@@ -106,4 +109,28 @@ public class TestJenaTitanium {
         DatasetGraph dsg2 = JenaTitanium.convert(rdfDataset, RiotLib.dftProfile());
         assertTrue(IsoMatcher.isomorphic(dsg1, dsg2));
     }
+
+    @Test public void convertDatasetWithNullGraph() throws IOException, RdfWriterException {
+        // .createTxnMem() returns an implementation that does not allow tripleInQuad
+        DatasetGraph dsg1 = DatasetGraphFactory.create();
+
+        // Add a triple in quad -- the graph term is set to null.
+        // The S, P, O terms can be whatever else.
+        // See: https://github.com/apache/jena/issues/2578
+        dsg1.add(Quad.create(
+                Quad.tripleInQuad,
+                NodeFactory.createBlankNode(),
+                NodeFactory.createBlankNode(),
+                NodeFactory.createBlankNode()
+        ));
+
+        RdfDataset rdfDataset = JenaTitanium.convert(dsg1);
+
+        // Try converting it back – it should not output any nulls.
+        DatasetGraph dsg2 = JenaTitanium.convert(rdfDataset, RiotLib.dftProfile());
+        dsg2.find().forEachRemaining(q->{
+            assertNotNull(q.getGraph());
+            assertTrue(q.isDefaultGraph());
+        });
+    }
 }
diff --git a/jena-integration-tests/src/test/java/org/apache/jena/test/rdfconnection/TestRDFConnectionRemote.java b/jena-integration-tests/src/test/java/org/apache/jena/test/rdfconnection/TestRDFConnectionRemote.java
index 3237a79f10..20dd3d2bd6 100644
--- a/jena-integration-tests/src/test/java/org/apache/jena/test/rdfconnection/TestRDFConnectionRemote.java
+++ b/jena-integration-tests/src/test/java/org/apache/jena/test/rdfconnection/TestRDFConnectionRemote.java
@@ -34,6 +34,8 @@ import org.apache.jena.rdfconnection.RDFConnectionRemote;
 import org.apache.jena.sparql.core.DatasetGraph ;
 import org.apache.jena.sparql.core.DatasetGraphFactory ;
 import org.apache.jena.system.Txn ;
+import org.apache.jena.update.UpdateException;
+import org.apache.jena.vocabulary.RDF;
 import org.apache.jena.web.HttpSC.Code;
 import org.junit.AfterClass ;
 import org.junit.Before ;
@@ -135,6 +137,105 @@ public class TestRDFConnectionRemote extends AbstractTestRDFConnection {
         }
     }
 
+    /** Non-standard query syntax on remote connection with parse check enabled is expected to fail. */
+    @Test(expected = QueryParseException.class)
+    public void non_standard_syntax_query_remote_1a() {
+        try ( RDFConnection conn = RDFConnectionRemote.service(server.datasetURL("/ds")).parseCheckSPARQL(true).build() ) {
+            try (QueryExecution qe = conn.newQuery().query("FOOBAR").build()) { }
+        }
+    }
+
+    /** Non-standard query syntax on remote connection with parse flag overridden on the builder is expected to work. */
+    @Test
+    public void non_standard_syntax_query_remote_1b() {
+        try ( RDFConnection conn = RDFConnectionRemote.service(server.datasetURL("/ds")).parseCheckSPARQL(true).build() ) {
+            try (QueryExecution qe = conn.newQuery().parseCheck(false).query("FOOBAR").build()) { }
+        }
+    }
+
+    /** Non-standard query syntax on remote connection with parse check disabled is expected to work. */
+    @Test
+    public void non_standard_syntax_query_remote_2a() {
+        try ( RDFConnection conn = RDFConnectionRemote.service(server.datasetURL("/ds")).parseCheckSPARQL(false).build() ) {
+            try (QueryExecution qe = conn.newQuery().query("FOOBAR").build()) { }
+        }
+    }
+
+    /** Non-standard query syntax on remote connection with parse flag overridden on the builder is expected to fail. */
+    @Test(expected = QueryParseException.class)
+    public void non_standard_syntax_query_remote_2b() {
+        try ( RDFConnection conn = RDFConnectionRemote.service(server.datasetURL("/ds")).parseCheckSPARQL(false).build() ) {
+            try (QueryExecution qe = conn.newQuery().parseCheck(true).query("FOOBAR").build()) { }
+        }
+    }
+
+    /** Non-standard update syntax on remote connection with parse check enabled is expected to fail. */
+    @Test(expected = QueryParseException.class)
+    public void non_standard_syntax_update_remote_1a() {
+        try ( RDFConnection conn = RDFConnectionRemote.service(server.datasetURL("/ds")).parseCheckSPARQL(true).build() ) {
+            conn.newUpdate().update("FOOBAR").build();
+        }
+    }
+
+    /** Non-standard update syntax on remote connection with parse flag overridden on the builder is expected to work. */
+    @Test
+    public void non_standard_syntax_update_remote_1b() {
+        try ( RDFConnection conn = RDFConnectionRemote.service(server.datasetURL("/ds")).parseCheckSPARQL(true).build() ) {
+            conn.newUpdate().parseCheck(false).update("FOOBAR").build();
+        }
+    }
+
+    /** Non-standard update syntax on remote connection with parse check disabled is expected to work. */
+    @Test
+    public void non_standard_syntax_update_remote_2a() {
+        try ( RDFConnection conn = RDFConnectionRemote.service(server.datasetURL("/ds")).parseCheckSPARQL(false).build() ) {
+            conn.newUpdate().update("FOOBAR").build();
+        }
+    }
+
+    /** Non-standard update syntax on remote connection with parse flag overridden on the builder is expected to fail. */
+    @Test(expected = QueryParseException.class)
+    public void non_standard_syntax_update_remote_2b() {
+        try ( RDFConnection conn = RDFConnectionRemote.service(server.datasetURL("/ds")).parseCheckSPARQL(false).build() ) {
+            conn.newUpdate().parseCheck(true).update("FOOBAR").build();
+        }
+    }
+
+    /** Non-standard update syntax on remote connection with parse check disabled is expected to work. */
+    @Test
+    public void non_standard_syntax_update_remote_3a() {
+        try ( RDFConnection conn = RDFConnectionRemote.service(server.datasetURL("/ds")).parseCheckSPARQL(false).build() ) {
+            conn.newUpdate().update("FOO").update("BAR").build();
+        }
+    }
+
+    /** Non-standard update syntax on remote connection with parse flag disabled on the builder is expected to work. */
+    public void non_standard_syntax_update_remote_3b() {
+        try ( RDFConnection conn = RDFConnectionRemote.service(server.datasetURL("/ds")).parseCheckSPARQL(true).build() ) {
+            conn.newUpdate().parseCheck(false).update("FOO").update("BAR").build();
+        }
+    }
+
+    /** Non-standard update syntax with substitution should fail on build. */
+    @Test(expected = UpdateException.class)
+    public void non_standard_syntax_update_remote_3c() {
+        try ( RDFConnection conn = RDFConnectionRemote.service(server.datasetURL("/ds")).parseCheckSPARQL(false).build() ) {
+            conn.newUpdate().parseCheck(false).update("FOO").update("BAR")
+                .substitution("foo", RDF.type).build();
+        }
+    }
+
+    /** Standard update syntax with substitution should work. */
+    @Test
+    public void standard_syntax_update_remote_1a() {
+        try ( RDFConnection conn = RDFConnectionRemote.service(server.datasetURL("/ds")).parseCheckSPARQL(false).build() ) {
+            conn.newUpdate()
+                .update("INSERT DATA { <a> <b> <c> }")
+                .update("INSERT DATA { <x> <y> <z> }")
+                .substitution("foo", RDF.type).build();
+        }
+    }
+
     // Should work.
     private static void test_named_graph_load_remote_200(RDFConnection connection, String target) {
         String testDataFile = DIR+"data.ttl";
diff --git a/jena-rdfconnection/src/test/java/org/apache/jena/rdfconnection/AbstractTestRDFConnection.java b/jena-rdfconnection/src/test/java/org/apache/jena/rdfconnection/AbstractTestRDFConnection.java
index 854783e27a..18e2b699eb 100644
--- a/jena-rdfconnection/src/test/java/org/apache/jena/rdfconnection/AbstractTestRDFConnection.java
+++ b/jena-rdfconnection/src/test/java/org/apache/jena/rdfconnection/AbstractTestRDFConnection.java
@@ -390,16 +390,16 @@ public abstract class AbstractTestRDFConnection {
     }
 
     @Test public void update_03() {
-    	UpdateRequest update = new UpdateRequest();
-    	update.add("INSERT DATA { <urn:ex:s> <urn:ex:p> <urn:ex:o>}");
+        UpdateRequest update = new UpdateRequest();
+        update.add("INSERT DATA { <urn:ex:s> <urn:ex:p> <urn:ex:o>}");
         try ( RDFConnection conn = connection() ) {
             conn.update(update);
         }
     }
 
     @Test public void update_04() {
-    	UpdateRequest update = new UpdateRequest();
-    	update.add("INSERT DATA { <urn:ex:s> <urn:ex:p> <urn:ex:o>}");
+        UpdateRequest update = new UpdateRequest();
+        update.add("INSERT DATA { <urn:ex:s> <urn:ex:p> <urn:ex:o>}");
         try ( RDFConnection conn = connection() ) {
             Txn.executeWrite(conn, ()->conn.update(update));
         }
@@ -457,5 +457,37 @@ public abstract class AbstractTestRDFConnection {
             conn.end();
         }
     }
+
+    /** Non-standard query syntax on local connection is expected to fail (regardless of syntax checking hint) */
+    @Test(expected = QueryParseException.class)
+    public void non_standard_syntax_query_local_1a() {
+        try ( RDFConnection conn = RDFConnection.connect(DatasetFactory.empty()) ) {
+            try (QueryExecution qe = conn.newQuery().parseCheck(false).query("FOOBAR").build()) { }
+        }
+    }
+
+    /** Non-standard query syntax on local connection is expected to fail (regardless of syntax checking hint) */
+    @Test(expected = QueryParseException.class)
+    public void non_standard_syntax_query_local_1b() {
+        try ( RDFConnection conn = RDFConnection.connect(DatasetFactory.empty()) ) {
+            try (QueryExecution qe = conn.newQuery().parseCheck(true).query("FOOBAR").build()) { }
+        }
+    }
+
+    /** Non-standard update syntax on local connection is expected to fail (regardless of syntax checking hint) */
+    @Test(expected = QueryParseException.class)
+    public void non_standard_syntax_update_local_1a() {
+        try ( RDFConnection conn = RDFConnection.connect(DatasetFactory.empty()) ) {
+            conn.newUpdate().parseCheck(false).update("FOOBAR").build();
+        }
+    }
+
+    /** Non-standard update syntax on local connection is expected to fail (regardless of syntax checking hint) */
+    @Test(expected = QueryParseException.class)
+    public void non_standard_syntax_update_local_1b() {
+        try ( RDFConnection conn = RDFConnection.connect(DatasetFactory.empty()) ) {
+            conn.newUpdate().parseCheck(true).update("FOOBAR").build();
+        }
+    }
 }