Repository: powsybl/powsybl-core
Pull request #3330: Triple store queries: replace subqueries with LIMIT 1 by proper subqueries using GROUP BY
Author: login: zamarrenolm
Labels: name: bug
name: CGMES
name: Release 06/2025
Created: 2025-02-20T17:55:34Z | Merged: 2025-05-19T09:20:44Z

**Please check if the PR fulfills these requirements**
<!-- please use `'[x]'` to check the checkboxes, or submit the PR and then click the checkboxes -->
- [X] The commit message follows our guidelines
- [X] Tests for the changes have been added (for bug fixes / features)
- [ ] Docs have been added / updated (for bug fixes / features)


**Does this PR already have an issue describing the problem?**
<!-- If so, link to this issue using `'Fixes #XXX'` and skip the rest -->



**What kind of change does this PR introduce?**
<!-- Bug fix, feature, docs update, ... -->
Bug fix

**Does this PR introduce a new Powsybl Action implying to be implemented in simulators or pypowsybl?**
- [ ] Yes, the corresponding issue is [here](link)
- [x] No

**What is the current behavior?**
<!-- You can also link to an open issue here -->
Some SPARQL queries use subqueries with LIMIT 1 expecting inner variables to be instantiated by the outer scope. For example, query for regions and subregions of a substation. The subquery combined with LIMIT was used to obtain only the first region associated with a given substation.

This way of querying the triple store works in some situations, but is not a guarantee, according to the SPARQL language specification. In theory it should never work: _Subqueries are executed first and then its results are projected on the outer query_ [https://www.w3.org/TR/sparql11-query/#subqueries](https://www.w3.org/TR/sparql11-query/).

As a minimum example, this query returns correct results for looking one of the past projects of a person using our current version of RDF4J:
```sparql
prefix foaf: <http://xmlns.com/foaf/0.1/>
SELECT *
WHERE {
    ?Person a foaf:Person ;
        foaf:name ?name .
    OPTIONAL {
        {
            SELECT ?Project
            WHERE {
                ?Person foaf:pastProject ?Project .
            }
            LIMIT 1
        }
        ?Project foaf:name ?projectName .
    }
}
```
It should return the same Project for all Persons, because in the subquery the ?Person variable should not be instantiated from the outer scope, and just the first project of any Person should be returned up. But instead, this version of the query returns a (proper) Project for every Person.

Just by altering very slightly the query, we obtain (as expected) bad results:
```sparql
prefix foaf: <http://xmlns.com/foaf/0.1/>
SELECT *
WHERE {
    ?Person a foaf:Person ;
        foaf:name ?name .
    OPTIONAL {
        {
            SELECT ?Project ?projectName
            WHERE {
                ?Person foaf:pastProject ?Project .
                ?Project foaf:name ?projectName .
            }
            LIMIT 1
        }
    }
}
```

**What is the new behavior (if this is a feature change)?**
The proper way of obtaining the intended data is to use the subquery with the variable that we want to match in the outer scope also projected. In the subquery, the additional data for it should be obtained using a GROUP BY clause and  aggregate function. Performance wise, SAMPLE could be used to aggregate, but that could lead to nondeterministic results, so MIN is used.

This is the proposed way of rewriting the subqueries to remove the LIMIT 1 related to the (wrong) assumption that inner variables were already instantiated:
```sparql
prefix foaf: <http://xmlns.com/foaf/0.1/>
SELECT *
WHERE {
    ?Person a foaf:Person ;
        foaf:name ?name .
    OPTIONAL {
        {
            SELECT ?Person (MIN(?Project) AS ?Project)
            WHERE {
                ?Person foaf:pastProject ?Project .
            }
            GROUP BY ?Person
        }
        ?Project foaf:name ?projectName .
    }
}
```

**Does this PR introduce a breaking change or deprecate an API?**
- [ ] Yes
- [X] No

**If yes, please check if the following requirements are fulfilled**
<!-- If no breaking changes or API deprecations were introduced, delete this section -->
- [ ] The *Breaking Change* or *Deprecated* label has been added
- [ ] The migration steps are described in the following section

**What changes might users need to make in their application due to this PR? (migration steps)**
<!-- If this PR introduces a breaking change, describe the migration steps -->
<!-- The content of this section will be copied in the migration guide -->



**Other information**:
<!-- if any of the questions/checkboxes don't apply, please delete them entirely -->

To evaluate the potential impact on performance the change has been verified importing a huge network (an 1.6 GB EQ XML file of a real world network). The size of the network is summarised in the following table:

Class | # of objects
-- | --:
Substations | 5,000
Voltage levels | 9,000
Connectivity nodes | 60,000
Busbars | 12,000
Lines | 9,000
Transformers | 2,500
Switches | 70,000
Terminals | 180,000

The import of the network takes an average of 72s in a reference machine before the change.
With this change the average time for import is 73s.
The figures have been obtained from 6 sample runs on an Apple MacBook Pro 2.3 GHz 8-core Intel Core i9 with 16 GB main memory.


Diff:
diff --git a/cgmes/cgmes-model/src/main/resources/CIM16.sparql b/cgmes/cgmes-model/src/main/resources/CIM16.sparql
index c55090fd482..a867beeea59 100644
--- a/cgmes/cgmes-model/src/main/resources/CIM16.sparql
+++ b/cgmes/cgmes-model/src/main/resources/CIM16.sparql
@@ -185,22 +185,22 @@ WHERE {
     # Optional is required to keep the ?Substation variable already bounded in the sub-queries ...
     OPTIONAL {
         {
-            SELECT ?SubRegion
+            SELECT ?Substation (MIN(?SubRegion) AS ?SubRegion)
             WHERE {
                 ?Substation cim:Substation.Region ?SubRegion .
                 ?SubRegion a cim:SubGeographicalRegion .
             }
-            LIMIT 1
+            GROUP BY ?Substation
         }
         ?SubRegion cim:IdentifiedObject.name ?subRegionName .
         OPTIONAL {
             {
-                SELECT ?Region
+                SELECT ?SubRegion (MIN(?Region) AS ?Region)
                 WHERE {
                     ?SubRegion cim:SubGeographicalRegion.Region ?Region .
                     ?Region a cim:GeographicalRegion .
                 }
-                LIMIT 1
+                GROUP BY ?SubRegion
             }
             ?Region cim:IdentifiedObject.name ?regionName .
         }
@@ -275,7 +275,7 @@ WHERE {
 }}
 OPTIONAL {
     {
-        SELECT ?TopologicalNode
+        SELECT ?ConnectivityNode (MIN(?TopologicalNode) AS ?TopologicalNode)
         WHERE {
             {
                 GRAPH ?graphTPCN { ?ConnectivityNode cim:ConnectivityNode.TopologicalNode ?TopologicalNode }
@@ -285,15 +285,15 @@ OPTIONAL {
                 # For TSOs missing the direct relationship ConnectivityNode -> TopologicalNode in TP
                 # We have to locate the TopologicalNode through one of the Terminals connected to the
                 # ConnectivityNode
-                SELECT ?TopologicalNode
+                SELECT ?ConnectivityNode (MIN(?TopologicalNode) AS ?TopologicalNode)
                 WHERE {
                     GRAPH ?graphEQ { ?Terminal cim:Terminal.ConnectivityNode ?ConnectivityNode }
                     GRAPH ?graphTPCN2 { ?Terminal cim:Terminal.TopologicalNode ?TopologicalNode }
                 }
-                LIMIT 1
+                GROUP BY ?ConnectivityNode
             }
         }
-        LIMIT 1
+        GROUP BY ?ConnectivityNode
     }
     ?TopologicalNode cim:TopologicalNode.BaseVoltage ?BaseVoltage
     OPTIONAL { GRAPH ?graphSVT {
diff --git a/triple-store/triple-store-api/src/main/java/com/powsybl/triplestore/api/PropertyBags.java b/triple-store/triple-store-api/src/main/java/com/powsybl/triplestore/api/PropertyBags.java
index 02ee950c76f..1735874bd58 100644
--- a/triple-store/triple-store-api/src/main/java/com/powsybl/triplestore/api/PropertyBags.java
+++ b/triple-store/triple-store-api/src/main/java/com/powsybl/triplestore/api/PropertyBags.java
@@ -51,6 +51,12 @@ public List<String> pluckIdentifiers(String property) {
             .collect(Collectors.toList());
     }
 
+    public List<String> pluckLocalsUnsorted(String property) {
+        return stream()
+                .map(r -> r.getLocal(property))
+                .collect(Collectors.toList());
+    }
+
     public PropertyBags pivot(
         String idProperty,
         String keyProperty,
diff --git a/triple-store/triple-store-test/src/test/java/com/powsybl/triplestore/test/CgmesRatioTapChangerTest.java b/triple-store/triple-store-test/src/test/java/com/powsybl/triplestore/test/CgmesRatioTapChangerTest.java
index 229dc01ef4f..eddb37266ca 100644
--- a/triple-store/triple-store-test/src/test/java/com/powsybl/triplestore/test/CgmesRatioTapChangerTest.java
+++ b/triple-store/triple-store-test/src/test/java/com/powsybl/triplestore/test/CgmesRatioTapChangerTest.java
@@ -32,37 +32,39 @@ static void setUp() {
     @Test
     void testTapChangerControls() {
         Expected expected = new Expected().expect("TapChangerControl",
-                "_97110e84-7da6-479c-846c-696fdaa83d56", "_ee42c6c2-39e7-43c2-9bdd-d397c5dc980b");
+                "_ee42c6c2-39e7-43c2-9bdd-d397c5dc980b", "_97110e84-7da6-479c-846c-696fdaa83d56");
         tester.testQuery(queries.get("tapChangerControls"), expected);
     }
 
     @Test
     void testTapChangerControlsOptionalSsh() {
         Expected expected = new Expected().expect("TapChangerControl",
-                "_97110e84-7da6-479c-846c-696fdaa83d56", "_ee42c6c2-39e7-43c2-9bdd-d397c5dc980b");
+                "_ee42c6c2-39e7-43c2-9bdd-d397c5dc980b", "_97110e84-7da6-479c-846c-696fdaa83d56");
         tester.testQuery(queries.get("tapChangerControlsOptionalSSH"), expected);
     }
 
     @Test
     void testTapChangerControlsOnlySshData() {
         Expected expected = new Expected().expect("TapChangerControl",
-                "_38f972bc-b7fd-4e75-8c24-379a86fbb506", "_ee42c6c2-39e7-43c2-9bdd-d397c5dc980b");
+                "_ee42c6c2-39e7-43c2-9bdd-d397c5dc980b", "_38f972bc-b7fd-4e75-8c24-379a86fbb506");
         tester.testQuery(queries.get("tapChangerControlsOnlySSHData"), expected);
     }
 
     @Test
     void testTapChangerOptionalControlOptionalSsh() {
-        Expected expected = new Expected().expect(
-                "RatioTapChanger",
-                "_11111111-4a10-4031-b008-60c0dc340a07",
-                "_83cc66dd-8d93-4a2c-8103-f1f5a9cf7e2e",
-                "_955d9cd0-4a10-4031-b008-60c0dc340a07",
-                "_fe25f43a-7341-446e-a71a-8ab7119ba806")
+        Expected expected = new Expected()
+                .expect(
+                        "RatioTapChanger",
+                        "_11111111-4a10-4031-b008-60c0dc340a07",
+                        "_955d9cd0-4a10-4031-b008-60c0dc340a07",
+                        "_83cc66dd-8d93-4a2c-8103-f1f5a9cf7e2e",
+                        "_fe25f43a-7341-446e-a71a-8ab7119ba806")
                 .expect(
                         "TapChangerControl",
-                        "_97110e84-7da6-479c-846c-696fdaa83d56",
+                        null,
                         "_ee42c6c2-39e7-43c2-9bdd-d397c5dc980b",
-                        null, null);
+                        "_97110e84-7da6-479c-846c-696fdaa83d56",
+                        null);
         tester.testQuery(queries.get("tapChangerOptionalControlOptionalSSH"), expected);
     }
 
diff --git a/triple-store/triple-store-test/src/test/java/com/powsybl/triplestore/test/FoafGraphsTest.java b/triple-store/triple-store-test/src/test/java/com/powsybl/triplestore/test/FoafGraphsTest.java
index d5171b4bb6f..2c431cc5f1f 100644
--- a/triple-store/triple-store-test/src/test/java/com/powsybl/triplestore/test/FoafGraphsTest.java
+++ b/triple-store/triple-store-test/src/test/java/com/powsybl/triplestore/test/FoafGraphsTest.java
@@ -38,14 +38,14 @@ static void setUp() {
 
     @Test
     void testLastNames() {
-        Expected expected = new Expected().expect("lastName", "Channing", "Liddell", "Marley");
+        Expected expected = new Expected().expect("lastName", "Liddell", "Marley", "Channing");
         tester.testQuery(queries.get("lastNames"), expected);
     }
 
     @Test
     void testLastNamesGraph() {
         Expected expected = new Expected()
-                .expect("lastName", "Channing", "Liddell", "Marley")
+                .expect("lastName", "Liddell", "Marley", "Channing")
                 .expect("graphLastnames",
                         "contexts:foaf/abc-lastNames.ttl",
                         "contexts:foaf/abc-lastNames.ttl",
@@ -60,11 +60,11 @@ void testLastNamesGraph() {
     @Test
     void testLastNameOnlyIfNick() {
         Expected expected = new Expected()
-                .expect("lastName", "Channing", "Liddell", null)
+                .expect("lastName", "Liddell", null, "Channing")
                 .expect("graphLastnames",
                         "contexts:foaf/abc-lastNames.ttl",
-                        "contexts:foaf/abc-lastNames.ttl",
-                        null)
+                        null,
+                        "contexts:foaf/abc-lastNames.ttl")
                 .expect("graphPersons",
                         "contexts:foaf/abc-nicks.ttl",
                         "contexts:foaf/abc-nicks.ttl",
diff --git a/triple-store/triple-store-test/src/test/java/com/powsybl/triplestore/test/FoafOptionalsTest.java b/triple-store/triple-store-test/src/test/java/com/powsybl/triplestore/test/FoafOptionalsTest.java
index e7231f06727..e1bf4f51434 100644
--- a/triple-store/triple-store-test/src/test/java/com/powsybl/triplestore/test/FoafOptionalsTest.java
+++ b/triple-store/triple-store-test/src/test/java/com/powsybl/triplestore/test/FoafOptionalsTest.java
@@ -32,13 +32,13 @@ static void setUp() {
     void testAllQuads() {
         Expected expected = new Expected().expect(
                 "o",
+                "mailto:alice@example",
                 "Alice",
-                "Bob",
-                "SweetCaroline",
                 "Wonderland",
-                "mailto:alice@example",
                 "mailto:bob@example",
-                "mailto:carol@example");
+                "Bob",
+                "mailto:carol@example",
+                "SweetCaroline");
         tester.testQuery("SELECT * { GRAPH ?g { ?s ?p ?o }}", expected);
     }
 
@@ -52,7 +52,7 @@ void testOptional() {
     void testMultipleOptionals() {
         Expected expected = new Expected()
                 .expect("name", "Alice", "Bob", null)
-                .expect("nick", "SweetCaroline", "Wonderland", null);
+                .expect("nick", "Wonderland", null, "SweetCaroline");
         tester.testQuery(queries.get("multipleOptionals"), expected);
     }
 
@@ -65,7 +65,7 @@ void testMultipleOptionalsSameVariable() {
     @Test
     void testOptionalWithUnion() {
         Expected expected = new Expected()
-                .expect("label", "Alice", "Bob", "SweetCaroline", "Wonderland");
+                .expect("label", "Alice", "Wonderland", "Bob", "SweetCaroline");
         tester.testQuery(queries.get("optionalWithUnion"), expected);
     }
 
@@ -73,7 +73,7 @@ void testOptionalWithUnion() {
     void testNestedOptionals() {
         Expected expected = new Expected()
                 .expect("name", "Alice", null, null)
-                .expect("nick", "SweetCaroline", "Wonderland", null);
+                .expect("nick", "Wonderland", null, "SweetCaroline");
         tester.testQuery(queries.get("nestedOptionals"), expected);
     }
 
@@ -87,7 +87,7 @@ void testOptionalAnd() {
 
     @Test
     void testOptionalNestedFilter() {
-        Expected expected = new Expected().expect("name", "Bob", null, null);
+        Expected expected = new Expected().expect("name", null, "Bob", null);
         tester.testQuery(queries.get("optionalNestedFilter"), expected);
     }
 
diff --git a/triple-store/triple-store-test/src/test/java/com/powsybl/triplestore/test/TripleStoreClearTest.java b/triple-store/triple-store-test/src/test/java/com/powsybl/triplestore/test/TripleStoreClearTest.java
index 3f82ade90f8..22f02cfdff4 100644
--- a/triple-store/triple-store-test/src/test/java/com/powsybl/triplestore/test/TripleStoreClearTest.java
+++ b/triple-store/triple-store-test/src/test/java/com/powsybl/triplestore/test/TripleStoreClearTest.java
@@ -29,7 +29,7 @@ static void setUp() {
         tester.load();
 
         expectedTapChangerControls = new Expected().expect("TapChangerControl",
-            "_97110e84-7da6-479c-846c-696fdaa83d56", "_ee42c6c2-39e7-43c2-9bdd-d397c5dc980b");
+            "_ee42c6c2-39e7-43c2-9bdd-d397c5dc980b", "_97110e84-7da6-479c-846c-696fdaa83d56");
         expectedEmpty = new Expected();
     }
 
diff --git a/triple-store/triple-store-test/src/test/java/com/powsybl/triplestore/test/TripleStoreCopyTest.java b/triple-store/triple-store-test/src/test/java/com/powsybl/triplestore/test/TripleStoreCopyTest.java
index 06d46bf45ad..e93d2dde417 100644
--- a/triple-store/triple-store-test/src/test/java/com/powsybl/triplestore/test/TripleStoreCopyTest.java
+++ b/triple-store/triple-store-test/src/test/java/com/powsybl/triplestore/test/TripleStoreCopyTest.java
@@ -38,7 +38,7 @@ void testCopy() {
             assertEquals(expected.getNamespaces(), actual.getNamespaces());
         });
 
-        Expected expectedContents = new Expected().expect("nick", "SweetCaroline", "Wonderland");
+        Expected expectedContents = new Expected().expect("nick", "Wonderland", "SweetCaroline");
         tester.testQuery(queries.get("selectNickName"), expectedContents);
         tester.testQueryOnCopies(queries.get("selectNickName"), expectedContents);
 
diff --git a/triple-store/triple-store-test/src/test/java/com/powsybl/triplestore/test/TripleStoreSubqueryLimit1Test.java b/triple-store/triple-store-test/src/test/java/com/powsybl/triplestore/test/TripleStoreSubqueryLimit1Test.java
new file mode 100644
index 00000000000..d2aac1ddbd0
--- /dev/null
+++ b/triple-store/triple-store-test/src/test/java/com/powsybl/triplestore/test/TripleStoreSubqueryLimit1Test.java
@@ -0,0 +1,70 @@
+/**
+ * Copyright (c) 2025, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+package com.powsybl.triplestore.test;
+
+import com.powsybl.triplestore.api.QueryCatalog;
+import com.powsybl.triplestore.api.TripleStoreFactory;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * @author Luma Zamarreño {@literal <zamarrenolm at aia.es>}
+ */
+class TripleStoreSubqueryLimit1Test {
+
+    private static final TripleStoreTester.Expected EXPECTED_MIN_ID_PROJECTS = new TripleStoreTester.Expected()
+            .expect("name", "Alice", "Bob")
+            .expect("projectName", "Personal finances", "Industrial plant control");
+    private static final TripleStoreTester.Expected EXPECTED_FIRST_PROJECTS = new TripleStoreTester.Expected()
+            .expect("name", "Alice", "Bob")
+            .expect("projectName", "Railway simulator", "SCADA system");
+    private static final TripleStoreTester.Expected EXPECTED_WRONG_FIRST_PROJECTS = new TripleStoreTester.Expected()
+            .expect("name", "Alice", "Bob")
+            .expect("projectName", "Railway simulator", "Railway simulator");
+    private static final TripleStoreTester.Expected EXPECTED_ALL_PROJECTS = new TripleStoreTester.Expected()
+            .expect("name", "Alice", "Alice", "Bob", "Bob")
+            .expect("projectName", "Railway simulator", "Personal finances", "SCADA system", "Industrial plant control");
+
+    @BeforeEach
+    void setUp() {
+        String base = "foo:foaf";
+        String[] inputs = {"foaf/abc-multiple-projects.ttl"};
+        tester = new TripleStoreTester(
+                TripleStoreFactory.allImplementations(), base, inputs);
+        tester.load();
+        queries = new QueryCatalog("foaf/foaf.sparql");
+    }
+
+    @Test
+    void testAllProjects() {
+        tester.testQuery(queries.get("projects"), EXPECTED_ALL_PROJECTS);
+    }
+
+    @Test
+    void testMinProjectRightQuery() {
+        tester.testQuery(queries.get("minProjectRightQuery"), EXPECTED_MIN_ID_PROJECTS);
+    }
+
+    @Test
+    void testFirstProjectBadQueryButReturnsRightResults() {
+        tester.testQuery(queries.get("firstProjectBadQueryButReturnsRightResults"), EXPECTED_FIRST_PROJECTS);
+    }
+
+    @Test
+    void testFirstProjectBadQueryAndReturnsWrongResults() {
+        tester.testQuery(queries.get("firstProjectBadQueryAndReturnsWrongResults"), EXPECTED_WRONG_FIRST_PROJECTS);
+    }
+
+    @Test
+    void testFirstProjectBadQueryNoOptionalAndReturnsWrongResults() {
+        tester.testQuery(queries.get("firstProjectBadQueryNoOptionalAndReturnsWrongResults"), EXPECTED_WRONG_FIRST_PROJECTS);
+    }
+
+    private static QueryCatalog queries;
+    private TripleStoreTester tester;
+}
diff --git a/triple-store/triple-store-test/src/test/java/com/powsybl/triplestore/test/TripleStoreTester.java b/triple-store/triple-store-test/src/test/java/com/powsybl/triplestore/test/TripleStoreTester.java
index 5620c6f53bc..68ae8a7492d 100644
--- a/triple-store/triple-store-test/src/test/java/com/powsybl/triplestore/test/TripleStoreTester.java
+++ b/triple-store/triple-store-test/src/test/java/com/powsybl/triplestore/test/TripleStoreTester.java
@@ -15,12 +15,7 @@
 
 import java.io.IOException;
 import java.io.InputStream;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.stream.Collectors;
+import java.util.*;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -87,11 +82,11 @@ void testQueryResults(String impl, PropertyBags results, Expected expected) {
         if (expected.isEmpty()) {
             assertTrue(results.isEmpty());
         } else {
-            assertTrue(!results.isEmpty());
+            assertFalse(results.isEmpty());
             int size = expected.values().iterator().next().size();
             assertEquals(size, results.size());
             expected.keySet()
-                .forEach(property -> assertEquals(expected.get(property), results.pluckLocals(property)));
+                .forEach(property -> assertEquals(expected.get(property), results.pluckLocalsUnsorted(property)));
         }
     }
 
@@ -123,7 +118,7 @@ void clear(String contextName, String namespace) {
             String qcontextName = namespace + contextName;
             assertTrue(before.contains(qcontextName));
             assertFalse(after.contains(qcontextName));
-            Set<String> expected = before.stream().collect(Collectors.toSet());
+            Set<String> expected = new HashSet<>(before);
             expected.remove(qcontextName);
             Set<String> actual = after;
             assertEquals(expected, actual);
diff --git a/triple-store/triple-store-test/src/test/java/com/powsybl/triplestore/test/TripleStoreUpdateTest.java b/triple-store/triple-store-test/src/test/java/com/powsybl/triplestore/test/TripleStoreUpdateTest.java
index 963efeff156..f57b2c6631b 100644
--- a/triple-store/triple-store-test/src/test/java/com/powsybl/triplestore/test/TripleStoreUpdateTest.java
+++ b/triple-store/triple-store-test/src/test/java/com/powsybl/triplestore/test/TripleStoreUpdateTest.java
@@ -38,16 +38,16 @@ void setUp() {
 
     @Test
     void testInsert() {
-        Expected expectedContents = new Expected().expect("nick", "SweetCaroline", "Wonderland");
+        Expected expectedContents = new Expected().expect("nick", "Wonderland", "SweetCaroline");
         tester.testQuery(queries.get("selectNickName"), expectedContents);
         tester.testUpdate(queries.get("insertNickName"));
-        Expected expectedContentsUpdated = new Expected().expect("nick", "BG", "SweetCaroline", "Wonderland");
+        Expected expectedContentsUpdated = new Expected().expect("nick", "Wonderland", "BG", "SweetCaroline");
         tester.testQuery(queries.get("selectNickName"), expectedContentsUpdated);
     }
 
     @Test
     void testDelete() {
-        Expected expectedContents = new Expected().expect("lastName", "Channing", "Liddell", "Marley");
+        Expected expectedContents = new Expected().expect("lastName", "Liddell", "Marley", "Channing");
         tester.testQuery(queries.get("selectLastName"), expectedContents);
         tester.testUpdate(queries.get("deleteLastName"));
         Expected expectedUpdated = new Expected().expect("lastName", "Liddell", "Marley");
@@ -70,7 +70,7 @@ void testUpdate() {
     @Test
     void testUpdateTwoGraphs() {
         Expected expectedContents = new Expected()
-            .expect("lastName", "Channing", "Liddell", "Marley")
+            .expect("lastName", "Liddell", "Marley", "Channing")
             .expect("person",
                 "http://example/alice",
                 "http://example/bob",
@@ -81,15 +81,15 @@ void testUpdateTwoGraphs() {
                 "mailto:carol@example");
         tester.testQuery(queries.get("selectPersonTwoGraphs"), expectedContents);
         tester.testUpdate(queries.get("updatePersonTwoGraphs"));
-        Expected expectedUpdated = new Expected().expect("lastName", "Channing", "Marley", "Walker")
-            .expect("person",
-                "http://example/alice",
-                "http://example/bob",
-                "http://example/carol")
-            .expect("mbox",
-                "mailto:aliceNowWalker@example",
-                "mailto:bob@example",
-                "mailto:carol@example");
+        Expected expectedUpdated = new Expected().expect("lastName", "Marley", "Channing", "Walker")
+                .expect("person",
+                        "http://example/bob",
+                        "http://example/carol",
+                        "http://example/alice")
+                .expect("mbox",
+                        "mailto:bob@example",
+                        "mailto:carol@example",
+                        "mailto:aliceNowWalker@example");
         tester.testQuery(queries.get("selectPersonTwoGraphs"), expectedUpdated);
     }
 
@@ -98,18 +98,19 @@ void testUpdateOnlyModifiesCopiedTriplestore() {
         tester.createCopies();
         // Check that an update operation applied to a copied triplestore
         // do not change the source triplestore, only the copy
-        Expected expectedContents = new Expected().expect("nick", "SweetCaroline", "Wonderland");
+        Expected expectedContents = new Expected().expect("nick", "Wonderland", "SweetCaroline");
         tester.testQuery(queries.get("selectNickName"), expectedContents);
         tester.testQueryOnCopies(queries.get("selectNickName"), expectedContents);
         tester.testUpdateOnCopies(queries.get("insertNickName"));
-        Expected expectedContentsUpdated = new Expected().expect("nick", "BG", "SweetCaroline", "Wonderland");
+        Expected expectedContentsUpdated = new Expected().expect("nick", "Wonderland", "BG", "SweetCaroline");
         tester.testQuery(queries.get("selectNickName"), expectedContents);
         tester.testQueryOnCopies(queries.get("selectNickName"), expectedContentsUpdated);
     }
 
     @Test
     void testMalformedQuery() {
-        assertThrows(TripleStoreException.class, () -> tester.testUpdate(queries.get("malformedQuery")));
+        String query = queries.get("malformedQuery");
+        assertThrows(TripleStoreException.class, () -> tester.testUpdate(query));
     }
 
     private static QueryCatalog queries;
diff --git a/triple-store/triple-store-test/src/test/resources/foaf/abc-multiple-projects.ttl b/triple-store/triple-store-test/src/test/resources/foaf/abc-multiple-projects.ttl
new file mode 100644
index 00000000000..401723519c5
--- /dev/null
+++ b/triple-store/triple-store-test/src/test/resources/foaf/abc-multiple-projects.ttl
@@ -0,0 +1,31 @@
+#
+# Copyright (c) 2025, RTE (http://www.rte-france.com)
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+
+@prefix : <http://example/> .
+@prefix foaf: <http://xmlns.com/foaf/0.1/> .
+
+:alice a                foaf:Person .
+:alice foaf:name        "Alice" .
+:alice foaf:pastProject :railway_simulator .
+:alice foaf:pastProject :personal_finances .
+
+:bob a                  foaf:Person .
+:bob foaf:name          "Bob" .
+:bob foaf:pastProject   :scada_system .
+:bob foaf:pastProject   :plant_control .
+
+:railway_simulator a          foaf:Project .
+:railway_simulator foaf:name  "Railway simulator" .
+
+:personal_finances a          foaf:Project .
+:personal_finances foaf:name  "Personal finances" .
+
+:scada_system a               foaf:Project .
+:scada_system foaf:name       "SCADA system" .
+
+:plant_control     a          foaf:Project .
+:plant_control     foaf:name  "Industrial plant control" .
diff --git a/triple-store/triple-store-test/src/test/resources/foaf/foaf.sparql b/triple-store/triple-store-test/src/test/resources/foaf/foaf.sparql
index 58ddce23584..3da39edbe13 100644
--- a/triple-store/triple-store-test/src/test/resources/foaf/foaf.sparql
+++ b/triple-store/triple-store-test/src/test/resources/foaf/foaf.sparql
@@ -105,3 +105,89 @@ WHERE {
     ?x foaf:lastName ?y .
     FILTER (?y = "Channing")
 }
+
+# query: projects
+prefix foaf: <http://xmlns.com/foaf/0.1/>
+SELECT *
+WHERE {
+    ?Person a foaf:Person ;
+        foaf:name ?name ;
+        foaf:pastProject ?Project .
+    ?Project a foaf:Project ;
+        foaf:name ?projectName .
+}
+
+# query: minProjectRightQuery
+prefix foaf: <http://xmlns.com/foaf/0.1/>
+SELECT *
+WHERE {
+    ?Person a foaf:Person ;
+        foaf:name ?name .
+    OPTIONAL {
+        {
+            SELECT ?Person (MIN(?Project) AS ?Project)
+            WHERE {
+                ?Person foaf:pastProject ?Project .
+            }
+            GROUP BY ?Person
+        }
+        ?Project foaf:name ?projectName .
+    }
+}
+
+# query: firstProjectBadQueryAndReturnsWrongResults
+prefix foaf: <http://xmlns.com/foaf/0.1/>
+SELECT *
+WHERE {
+    ?Person a foaf:Person ;
+        foaf:name ?name .
+    OPTIONAL {
+        {
+            SELECT ?Project ?projectName
+            WHERE {
+                ?Person foaf:pastProject ?Project .
+                ?Project foaf:name ?projectName .
+            }
+            LIMIT 1
+        }
+    }
+}
+
+# query: firstProjectBadQueryButReturnsRightResults
+prefix foaf: <http://xmlns.com/foaf/0.1/>
+SELECT *
+WHERE {
+    ?Person a foaf:Person ;
+        foaf:name ?name .
+    OPTIONAL {
+        {
+            SELECT ?Project
+            WHERE {
+                ?Person foaf:pastProject ?Project .
+            }
+            LIMIT 1
+        }
+        ?Project foaf:name ?projectName .
+    }
+}
+
+# query: firstProjectBadQueryNoOptionalAndReturnsWrongResults
+prefix foaf: <http://xmlns.com/foaf/0.1/>
+SELECT *
+WHERE {
+    ?Person a foaf:Person ;
+        foaf:name ?name .
+    {
+        {
+            SELECT ?Project
+            WHERE {
+                ?Person foaf:pastProject ?Project .
+            }
+            LIMIT 1
+        }
+        ?Project foaf:name ?projectName .
+    }
+}
+
+
+


Reviews:
user: login: rcourtier
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-03-13T14:22:33Z
user: login: olperr1
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-16T14:22:13Z
user: login: rcourtier
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-19T08:06:07Z
user: login: olperr1
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-19T08:28:19Z

Review comments:
diff_hunk: @@ -0,0 +1,60 @@
+package com.powsybl.triplestore.test;
path: triple-store/triple-store-test/src/test/java/com/powsybl/triplestore/test/TripleStoreSubqueryLimit1Test.java
user: login: olperr1
body: The copyright is missing:

```suggestion
/**
 * Copyright (c) 2025, RTE (http://www.rte-france.com)
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 * SPDX-License-Identifier: MPL-2.0
 */
package com.powsybl.triplestore.test;
```
created_at: 2025-05-16T14:18:25Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,60 @@
+package com.powsybl.triplestore.test;
+
+import com.powsybl.triplestore.api.QueryCatalog;
+import com.powsybl.triplestore.api.TripleStoreFactory;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class TripleStoreSubqueryLimit1Test {
path: triple-store/triple-store-test/src/test/java/com/powsybl/triplestore/test/TripleStoreSubqueryLimit1Test.java
user: login: olperr1
body: The authorship is missing:

```suggestion
/**
 * @author Luma Zamarreño {@literal <zamarrenolm at aia.es>}
 */
class TripleStoreSubqueryLimit1Test {
```
created_at: 2025-05-16T14:18:51Z
author_association: MEMBER

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-05-19T09:12:12Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=com.powsybl%3Apowsybl-core&pullRequest=3330) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=com.powsybl%3Apowsybl-core&pullRequest=3330&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=com.powsybl%3Apowsybl-core&pullRequest=3330&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=com.powsybl%3Apowsybl-core&pullRequest=3330&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [100.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=com.powsybl%3Apowsybl-core&pullRequest=3330&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=com.powsybl%3Apowsybl-core&pullRequest=3330&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=com.powsybl%3Apowsybl-core&pullRequest=3330)


author_association: NONE