diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 80ff981cdf8..7f94ed55c3e 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -25,6 +25,8 @@ image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima [[release-3-7-7]] === TinkerPop 3.7.7 (Release Date: NOT OFFICIALLY RELEASED YET) +* Fixed conjoin has incorrect null handling. + [[release-3-7-6]] === TinkerPop 3.7.6 (Release Date: April 1, 2026) diff --git a/docs/src/upgrade/release-3.7.x.asciidoc b/docs/src/upgrade/release-3.7.x.asciidoc index e003ee0946d..70fea1d83ee 100644 --- a/docs/src/upgrade/release-3.7.x.asciidoc +++ b/docs/src/upgrade/release-3.7.x.asciidoc @@ -30,6 +30,35 @@ image::gremlin-zamfir.png[width=185] Please see the link:https://github.com/apache/tinkerpop/blob/3.7.7/CHANGELOG.asciidoc#release-3-7-7[changelog] for a complete list of all the modifications that are part of this release. +=== Upgrading for Users + +==== conjoin() Step Null Handling + +The `conjoin()` step previously returned `null` when elements in the incoming list are `null`. This behavior has +been changed so that `conjoin()` now returns an empty string (`""`) in that case. + +[source,groovy] +---- +// 3.7.6 +gremlin> g.inject([null]).conjoin("-") +==>null +gremlin> g.inject([null, null]).conjoin("-") +==>null + +// 3.7.7 +gremlin> g.inject([null]).conjoin("+") +==> +gremlin> g.inject([null, null]).conjoin("+") +==> +---- + +Code that checks the result of `conjoin()` for lists that include `null` elements should be updated to check for +an empty string instead. + +See: link:https://issues.apache.org/jira/browse/TINKERPOP-3225[TINKERPOP-3225] + + + == TinkerPop 3.7.6 *Release Date: April 1, 2026* diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/ConjoinStep.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/ConjoinStep.java index 926e4982b2d..a57d9cab1dc 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/ConjoinStep.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/ConjoinStep.java @@ -62,7 +62,7 @@ protected String map(Traverser.Admin traverser) { joinResult.delete(joinResult.length() - delimiter.length(), joinResult.length()); return joinResult.toString(); } else { - return null; + return ""; } } diff --git a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/ConjoinStepTest.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/ConjoinStepTest.java index 184c80c9fc3..35ddefa5e35 100644 --- a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/ConjoinStepTest.java +++ b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/ConjoinStepTest.java @@ -51,7 +51,8 @@ public void testReturnTypes() { assertEquals("5AA8AA10", __.__(new long[] {5L, 8L, 10L}).conjoin("AA").next()); assertEquals("715", __.__(1).constant(new Long[] {7L, 15L}).conjoin("").next()); assertEquals("5.5,8.0,10.1", __.__(new double[] {5.5, 8.0, 10.1}).conjoin(",").next()); - assertNull(__.__(Arrays.asList(null, null)).conjoin(",").next()); + assertEquals("", __.__(Arrays.asList(null, null)).conjoin(",").next()); + assertEquals("1", __.__(Arrays.asList(null, 1, null)).conjoin(",").next()); final Set set = new LinkedHashSet<>(); set.add(10); set.add(11); set.add(12); diff --git a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs index 154a0bcf53f..0a8e187dc2f 100644 --- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs +++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs @@ -633,6 +633,8 @@ private static IDictionary, ITraversal>> {(g,p) =>g.V().Out().Out().Path().By("name").Conjoin("")}}, {"g_injectXa_null_bX_conjoinXxyzX", new List, ITraversal>> {(g,p) =>g.Inject(p["xx1"]).Conjoin("xyz")}}, {"g_injectX3_threeX_conjoinX_X", new List, ITraversal>> {(g,p) =>g.Inject(p["xx1"]).Conjoin(";")}}, + {"g_injectXnull_a_null_bX_conjoinXplusX", new List, ITraversal>> {(g,p) =>g.Inject(p["xx1"]).Conjoin("+")}}, + {"g_injectXnull_nullX_conjoinXplusX", new List, ITraversal>> {(g,p) =>g.Inject(p["xx1"]).Conjoin("+")}}, {"g_V_connectedComponent_hasXcomponentX", new List, ITraversal>> {(g,p) =>g.V().ConnectedComponent().Has("gremlin.connectedComponentVertexProgram.component")}}, {"g_V_dedup_connectedComponent_hasXcomponentX", new List, ITraversal>> {(g,p) =>g.V().Dedup().ConnectedComponent().Has("gremlin.connectedComponentVertexProgram.component")}}, {"g_V_hasLabelXsoftwareX_connectedComponent_project_byXnameX_byXcomponentX", new List, ITraversal>> {(g,p) =>g.V().HasLabel("software").ConnectedComponent().Project("name","component").By("name").By("gremlin.connectedComponentVertexProgram.component")}}, diff --git a/gremlin-go/driver/cucumber/gremlin.go b/gremlin-go/driver/cucumber/gremlin.go index 4ddf3b743ca..ba1a6b1ae08 100644 --- a/gremlin-go/driver/cucumber/gremlin.go +++ b/gremlin-go/driver/cucumber/gremlin.go @@ -604,6 +604,8 @@ var translationMap = map[string][]func(g *gremlingo.GraphTraversalSource, p map[ "g_V_out_out_path_byXnameX_conjoinXX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Out().Out().Path().By("name").Conjoin("")}}, "g_injectXa_null_bX_conjoinXxyzX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject(p["xx1"]).Conjoin("xyz")}}, "g_injectX3_threeX_conjoinX_X": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject(p["xx1"]).Conjoin(";")}}, + "g_injectXnull_a_null_bX_conjoinXplusX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject(p["xx1"]).Conjoin("+")}}, + "g_injectXnull_nullX_conjoinXplusX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject(p["xx1"]).Conjoin("+")}}, "g_V_connectedComponent_hasXcomponentX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().ConnectedComponent().Has("gremlin.connectedComponentVertexProgram.component")}}, "g_V_dedup_connectedComponent_hasXcomponentX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Dedup().ConnectedComponent().Has("gremlin.connectedComponentVertexProgram.component")}}, "g_V_hasLabelXsoftwareX_connectedComponent_project_byXnameX_byXcomponentX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().HasLabel("software").ConnectedComponent().Project("name", "component").By("name").By("gremlin.connectedComponentVertexProgram.component")}}, diff --git a/gremlin-javascript/src/main/javascript/gremlin-javascript/test/cucumber/gremlin.js b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/cucumber/gremlin.js index d1cda9d6ecd..b6de78479ba 100644 --- a/gremlin-javascript/src/main/javascript/gremlin-javascript/test/cucumber/gremlin.js +++ b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/cucumber/gremlin.js @@ -624,6 +624,8 @@ const gremlins = { g_V_out_out_path_byXnameX_conjoinXX: [function({g}) { return g.V().out().out().path().by("name").conjoin("") }], g_injectXa_null_bX_conjoinXxyzX: [function({g, xx1}) { return g.inject(xx1).conjoin("xyz") }], g_injectX3_threeX_conjoinX_X: [function({g, xx1}) { return g.inject(xx1).conjoin(";") }], + g_injectXnull_a_null_bX_conjoinXplusX: [function({g, xx1}) { return g.inject(xx1).conjoin("+") }], + g_injectXnull_nullX_conjoinXplusX: [function({g, xx1}) { return g.inject(xx1).conjoin("+") }], g_V_connectedComponent_hasXcomponentX: [function({g}) { return g.V().connectedComponent().has("gremlin.connectedComponentVertexProgram.component") }], g_V_dedup_connectedComponent_hasXcomponentX: [function({g}) { return g.V().dedup().connectedComponent().has("gremlin.connectedComponentVertexProgram.component") }], g_V_hasLabelXsoftwareX_connectedComponent_project_byXnameX_byXcomponentX: [function({g}) { return g.V().hasLabel("software").connectedComponent().project("name","component").by("name").by("gremlin.connectedComponentVertexProgram.component") }], diff --git a/gremlin-python/src/main/python/tests/feature/gremlin.py b/gremlin-python/src/main/python/tests/feature/gremlin.py index ad25f4fb971..3d14a7224de 100644 --- a/gremlin-python/src/main/python/tests/feature/gremlin.py +++ b/gremlin-python/src/main/python/tests/feature/gremlin.py @@ -606,6 +606,8 @@ 'g_V_out_out_path_byXnameX_conjoinXX': [(lambda g:g.V().out().out().path().by('name').conjoin(''))], 'g_injectXa_null_bX_conjoinXxyzX': [(lambda g, xx1=None:g.inject(xx1).conjoin('xyz'))], 'g_injectX3_threeX_conjoinX_X': [(lambda g, xx1=None:g.inject(xx1).conjoin(';'))], + 'g_injectXnull_a_null_bX_conjoinXplusX': [(lambda g, xx1=None:g.inject(xx1).conjoin('+'))], + 'g_injectXnull_nullX_conjoinXplusX': [(lambda g, xx1=None:g.inject(xx1).conjoin('+'))], 'g_V_connectedComponent_hasXcomponentX': [(lambda g:g.V().connected_component().has('gremlin.connectedComponentVertexProgram.component'))], 'g_V_dedup_connectedComponent_hasXcomponentX': [(lambda g:g.V().dedup().connected_component().has('gremlin.connectedComponentVertexProgram.component'))], 'g_V_hasLabelXsoftwareX_connectedComponent_project_byXnameX_byXcomponentX': [(lambda g:g.V().has_label('software').connected_component().project('name','component').by('name').by('gremlin.connectedComponentVertexProgram.component'))], diff --git a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/Conjoin.feature b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/Conjoin.feature index c91e98882c1..949dcc102da 100644 --- a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/Conjoin.feature +++ b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/Conjoin.feature @@ -153,3 +153,29 @@ Feature: Step - conjoin() Then the result should be unordered | result | | 3;three | + + @GraphComputerVerificationInjectionNotSupported + Scenario: g_injectXnull_a_null_bX_conjoinXplusX + Given the empty graph + And using the parameter xx1 defined as "l[null,a,null,b]" + And the traversal of + """ + g.inject(xx1).conjoin("+") + """ + When iterated to list + Then the result should be unordered + | result | + | str[a+b] | + + @GraphComputerVerificationInjectionNotSupported + Scenario: g_injectXnull_nullX_conjoinXplusX + Given the empty graph + And using the parameter xx1 defined as "l[null,null]" + And the traversal of + """ + g.inject(xx1).conjoin("+") + """ + When iterated to list + Then the result should be unordered + | result | + | str[] |