This is an automated email from the ASF dual-hosted git repository.
colegreer pushed a commit to branch 3.7-dev
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
The following commit(s) were added to refs/heads/3.7-dev by this push:
new 7e9615ef3b TINKERPOP-3225 conjoin has incorrect null handling (#3372)
7e9615ef3b is described below
commit 7e9615ef3bbd95147f51a310180c61c9d55b7c55
Author: Guian Gumpac <[email protected]>
AuthorDate: Wed Apr 15 11:16:33 2026 -0700
TINKERPOP-3225 conjoin has incorrect null handling (#3372)
---
CHANGELOG.asciidoc | 2 ++
docs/src/upgrade/release-3.7.x.asciidoc | 29 ++++++++++++++++++++++
.../process/traversal/step/map/ConjoinStep.java | 2 +-
.../traversal/step/map/ConjoinStepTest.java | 3 ++-
.../Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs | 2 ++
gremlin-go/driver/cucumber/gremlin.go | 2 ++
.../gremlin-javascript/test/cucumber/gremlin.js | 2 ++
.../src/main/python/tests/feature/gremlin.py | 2 ++
.../gremlin/test/features/map/Conjoin.feature | 26 +++++++++++++++++++
9 files changed, 68 insertions(+), 2 deletions(-)
diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc
index 80ff981cdf..7f94ed55c3 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 e003ee0946..70fea1d83e 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 926e4982b2..a57d9cab1d 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 @@ public final class ConjoinStep<S> extends ScalarMapStep<S,
String> implements Li
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 184c80c9fc..35ddefa5e3 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 class ConjoinStepTest extends StepTest {
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<Integer> 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 154a0bcf53..0a8e187dc2 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 @@ namespace Gremlin.Net.IntegrationTest.Gherkin
{"g_V_out_out_path_byXnameX_conjoinXX", new
List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>>
{(g,p) =>g.V().Out().Out().Path().By("name").Conjoin("")}},
{"g_injectXa_null_bX_conjoinXxyzX", new
List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>>
{(g,p) =>g.Inject(p["xx1"]).Conjoin("xyz")}},
{"g_injectX3_threeX_conjoinX_X", new
List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>>
{(g,p) =>g.Inject(p["xx1"]).Conjoin(";")}},
+ {"g_injectXnull_a_null_bX_conjoinXplusX", new
List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>>
{(g,p) =>g.Inject(p["xx1"]).Conjoin("+")}},
+ {"g_injectXnull_nullX_conjoinXplusX", new
List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>>
{(g,p) =>g.Inject(p["xx1"]).Conjoin("+")}},
{"g_V_connectedComponent_hasXcomponentX", new
List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>>
{(g,p)
=>g.V().ConnectedComponent().Has("gremlin.connectedComponentVertexProgram.component")}},
{"g_V_dedup_connectedComponent_hasXcomponentX", new
List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>>
{(g,p)
=>g.V().Dedup().ConnectedComponent().Has("gremlin.connectedComponentVertexProgram.component")}},
{"g_V_hasLabelXsoftwareX_connectedComponent_project_byXnameX_byXcomponentX",
new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>>
{(g,p)
=>g.V().HasLabel("software").ConnectedComponent().Project<object>("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 4ddf3b743c..ba1a6b1ae0 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 d1cda9d6ec..b6de78479b 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 ad25f4fb97..3d14a7224d 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 @@ world.gremlins = {
'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 c91e98882c..949dcc102d 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[] |