TINKERPOP-2235 More consistent null handling for vertex mutations
Made addV(null) and property(id, null) work more consistently.
diff --git a/docs/src/upgrade/release-3.5.x.asciidoc b/docs/src/upgrade/release-3.5.x.asciidoc
index cdd920b..ce7a00e 100644
--- a/docs/src/upgrade/release-3.5.x.asciidoc
+++ b/docs/src/upgrade/release-3.5.x.asciidoc
@@ -358,6 +358,45 @@
link:https://issues.apache.org/jira/browse/TINKERPOP-2310[TINKERPOP-2310],
link:https://issues.apache.org/jira/browse/TINKERPOP-2311[TINKERPOP-2311]
+===== Null Semantics
+
+Graph providers should take note of the changes to `null` semantics described in the "users" section of these upgrade
+notes. As `null` is now acceptable as a `Traverser` object, this change may affect custom steps. Further note that
+`null` now works more consistently with mutation steps and graph providers may need to include additional logic to
+deal with those possible conditions. Please see the console session below which uses TinkerGraph to demonstrate the
+current behavioral expectations:
+
+[source,text]
+----
+gremlin> g.addV(null).property(id, null).property('name',null)
+==>v[0]
+gremlin> g.V().elementMap()
+==>[id:0,label:vertex]
+----
+
+In the above example, `addV()` defaults to `Vertex.DEFAULT_LABEL`, the `id` is generated and setting the "name"
+property to `null` results in the value not being set. If the property value is set to an actual value and then set
+to `null` TinkerGraph will remove the property key all together:
+
+[source,text]
+----
+gremlin> g.V().property('name','stephen')
+==>v[0]
+gremlin> g.V().elementMap()
+==>[id:0,label:vertex,name:stephen]
+gremlin> g.V().property('name',null)
+==>v[0]
+gremlin> g.V().elementMap()
+==>[id:0,label:vertex]
+----
+
+The above examples point out the default operations of TinkerGraph, but it can be configured to actually accept the
+`null` as a property value and it is up to graph providers to decided how they wish to treat a `null` property value.
+Providers should use the new `supportsNullPropertyValues()` feature to indicate to users how `null` is handled.
+
+See: link:https://issues.apache.org/jira/browse/TINKERPOP-2235[TINKERPOP-2235],
+link:https://issues.apache.org/jira/browse/TINKERPOP-2099[TINKERPOP-2099]
+
===== AbstractOpProcessor API Change
The `generateMetaData()` method was removed as it was deprecated in a previous version. There already was a preferred
@@ -373,6 +412,8 @@
See: link:https://issues.apache.org/jira/browse/TINKERPOP-2254[TINKERPOP-2254]
+==== Graph Driver Providers
+
===== TraversalOpProcessor Side-effects
`TraversalOpProcessor` no longer holds a cache of side-effects and more generally the entire side-effect protocol has
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/jsr223/JavaTranslator.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/jsr223/JavaTranslator.java
index bb9273c..6034793 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/jsr223/JavaTranslator.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/jsr223/JavaTranslator.java
@@ -235,8 +235,18 @@
// is valid hits like four different possible overloads, but we can rely on the most
// generic one which takes Object as the second parameter. that seems to work in this
// case, but it's a shame this isn't nicer. seems like nicer would mean a heavy
- // overhaul to Gremlin or to GLVs/bytecode and/or to serialization mechanisms
- if (i < argumentsCopy.length && ((null == argumentsCopy[i] && parameters[i].getType() == Object.class) ||
+ // overhaul to Gremlin or to GLVs/bytecode and/or to serialization mechanisms.
+ //
+ // the check where argumentsCopy[i] is null could be accompanied by a type check for
+ // allowable signatures like:
+ // null == argumentsCopy[i] && parameters[i].getType() == Object.class
+ // but that doesn't seem helpful. perhaps this approach is fine as long as we ensure
+ // consistency of null calls to all overloads. in other words addV(String) must behave
+ // the same as addV(Traversal) if null is used as the argument. so far, that seems to
+ // be the case. if we find that is not happening we either fix that specific
+ // inconsistency, start special casing those method finds here, or as mentioned above
+ // do something far more drastic that doesn't involve reflection.
+ if (i < argumentsCopy.length && (null == argumentsCopy[i] ||
(argumentsCopy[i] != null && (
parameters[i].getType().isAssignableFrom(argumentsCopy[i].getClass()) ||
(parameters[i].getType().isPrimitive() &&
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/ElementHelper.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/ElementHelper.java
index a121abd..0d6900b 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/ElementHelper.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/util/ElementHelper.java
@@ -122,12 +122,11 @@
*
* @param keyValues a list of key/value pairs
* @return the value associated with {@link T#id}
- * @throws NullPointerException if the value for the {@link T#id} key is {@code null}
*/
public static Optional<Object> getIdValue(final Object... keyValues) {
for (int i = 0; i < keyValues.length; i = i + 2) {
if (keyValues[i].equals(T.id))
- return Optional.of(keyValues[i + 1]);
+ return Optional.ofNullable(keyValues[i + 1]);
}
return Optional.empty();
}
diff --git a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/util/ElementHelperTest.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/util/ElementHelperTest.java
index d4f1a77..3d7be9c 100644
--- a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/util/ElementHelperTest.java
+++ b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/util/ElementHelperTest.java
@@ -134,9 +134,9 @@
assertFalse(ElementHelper.getIdValue("test", 321, "xyz", 123l, "testagain", "that").isPresent());
}
- @Test(expected = NullPointerException.class)
+ @Test
public void shouldNotFindAnIdValueBecauseItIsNull() {
- ElementHelper.getIdValue("test", 321, T.id, null, "testagain", "that");
+ assertEquals("default", ElementHelper.getIdValue("test", 321, T.id, null, "testagain", "that").orElse("default"));
}
@Test
diff --git a/gremlin-test/features/map/AddVertex.feature b/gremlin-test/features/map/AddVertex.feature
index 0fc338f..e8e1248 100644
--- a/gremlin-test/features/map/AddVertex.feature
+++ b/gremlin-test/features/map/AddVertex.feature
@@ -412,3 +412,12 @@
| result |
| name |
+ Scenario: g_addVXnullX_propertyXid_nullX
+ Given the empty graph
+ And the traversal of
+ """
+ g.addV(null).property(T.id, null)
+ """
+ When iterated to list
+ Then the result should have a count of 1
+ And the graph should return 1 for count of "g.V().hasLabel(\"vertex\")"
diff --git a/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexTest.java b/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexTest.java
index 8937a44..fec146d 100644
--- a/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexTest.java
+++ b/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddVertexTest.java
@@ -44,6 +44,7 @@
import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
/**
@@ -60,6 +61,8 @@
public abstract Traversal<Vertex, Vertex> get_g_V_hasLabelXpersonX_propertyXname_nullX();
+ public abstract Traversal<Vertex, Vertex> get_g_addVXnullX_propertyXid_nullX();
+
public abstract Traversal<Vertex, Vertex> get_g_addVXpersonX_propertyXsingle_name_stephenX_propertyXsingle_name_stephenmX();
public abstract Traversal<Vertex, Vertex> get_g_addVXpersonX_propertyXsingle_name_stephenX_propertyXsingle_name_stephenm_since_2010X();
@@ -95,7 +98,6 @@
assertEquals(7, IteratorUtils.count(g.V()));
}
-
@Test
@LoadGraphWith(MODERN)
@FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = Graph.Features.VertexFeatures.FEATURE_ADD_VERTICES)
@@ -116,6 +118,20 @@
}
@Test
+ @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = Graph.Features.VertexFeatures.FEATURE_ADD_VERTICES)
+ @FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = Graph.Features.VertexFeatures.FEATURE_ADD_PROPERTY)
+ public void g_addVXnullX_propertyXid_nullX() {
+ final Traversal<Vertex, Vertex> traversal = get_g_addVXnullX_propertyXid_nullX();
+ printTraversalForm(traversal);
+
+ final Vertex vertex = traversal.next();
+ assertEquals(Vertex.DEFAULT_LABEL, vertex.label());
+
+ // should generate an id for the null value
+ assertNotNull(vertex.id());
+ }
+
+ @Test
@LoadGraphWith(MODERN)
@FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = Graph.Features.VertexFeatures.FEATURE_ADD_VERTICES)
@FeatureRequirement(featureClass = Graph.Features.VertexFeatures.class, feature = Graph.Features.VertexFeatures.FEATURE_ADD_PROPERTY)
@@ -381,5 +397,11 @@
public Traversal<Vertex, Map<Object, Object>> get_g_V_asXaX_hasXname_markoX_outXcreatedX_asXbX_addVXselectXaX_labelX_propertyXtest_selectXbX_labelX_valueMap_withXtokensX() {
return g.V().as("a").has("name", "marko").out("created").as("b").addV(select("a").label()).property("test", select("b").label()).valueMap().with(WithOptions.tokens);
}
+
+ @Override
+ public Traversal<Vertex, Vertex> get_g_addVXnullX_propertyXid_nullX() {
+ // testing Traversal but should work the same for String
+ return g.addV((Traversal.Admin<?, String>) null).property(T.id, null);
+ }
}
}
\ No newline at end of file