This is an automated email from the ASF dual-hosted git repository.
imbajin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hugegraph.git
The following commit(s) were added to refs/heads/master by this push:
new 454dd3d79 fix(server): keep schema ~create_time as Date after reload
(#3026)
454dd3d79 is described below
commit 454dd3d799c70ea899ab158e2153e4fb667cc3ad
Author: Davide Polato <[email protected]>
AuthorDate: Tue May 19 11:35:51 2026 +0200
fix(server): keep schema ~create_time as Date after reload (#3026)
Normalize server-side schema ~create_time userdata in SchemaElement so
serializer reloads and fromMap paths keep the Date contract.
Add SchemaElement, TextSerializer, and BinarySerializer coverage.
The builder accumulates userdata via Userdata.put() before eliminate()
runs, so `.userdata(CREATE_TIME, "").eliminate()` parsed "" as a date
and threw before the key-only removal path. Pass a blank ~create_time
through unchanged; non-empty malformed values still throw on the add
path, so the existing contract is unchanged.
---
.../org/apache/hugegraph/schema/SchemaElement.java | 10 +
.../java/org/apache/hugegraph/schema/Userdata.java | 46 +++++
.../apache/hugegraph/core/PropertyKeyCoreTest.java | 20 ++
.../org/apache/hugegraph/unit/UnitTestSuite.java | 4 +
.../hugegraph/unit/core/SchemaElementTest.java | 212 +++++++++++++++++++++
.../unit/serializer/BinarySerializerTest.java | 27 +++
.../unit/serializer/TextSerializerTest.java | 56 ++++++
7 files changed, 375 insertions(+)
diff --git
a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/SchemaElement.java
b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/SchemaElement.java
index 966d3eed8..c36eeaf63 100644
---
a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/SchemaElement.java
+++
b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/SchemaElement.java
@@ -96,13 +96,22 @@ public abstract class SchemaElement implements Nameable,
Typeable,
return Collections.unmodifiableMap(this.userdata);
}
+ /**
+ * Add userdata. String values of {@link Userdata#CREATE_TIME} are
+ * normalized to {@link java.util.Date} and malformed strings are rejected.
+ */
public void userdata(String key, Object value) {
E.checkArgumentNotNull(key, "userdata key");
E.checkArgumentNotNull(value, "userdata value");
this.userdata.put(key, value);
}
+ /**
+ * Add userdata in bulk. String values of {@link Userdata#CREATE_TIME} are
+ * normalized to {@link java.util.Date} and malformed strings are rejected.
+ */
public void userdata(Userdata userdata) {
+ E.checkArgumentNotNull(userdata, "userdata");
this.userdata.putAll(userdata);
}
@@ -112,6 +121,7 @@ public abstract class SchemaElement implements Nameable,
Typeable,
}
public void removeUserdata(Userdata userdata) {
+ E.checkArgumentNotNull(userdata, "userdata");
for (String key : userdata.keySet()) {
this.userdata.remove(key);
}
diff --git
a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/Userdata.java
b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/Userdata.java
index d485e558b..9d2b9dbd1 100644
---
a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/Userdata.java
+++
b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/Userdata.java
@@ -22,6 +22,7 @@ import java.util.Map;
import org.apache.hugegraph.exception.NotAllowException;
import org.apache.hugegraph.type.define.Action;
+import org.apache.hugegraph.util.DateUtil;
public class Userdata extends HashMap<String, Object> {
@@ -37,6 +38,51 @@ public class Userdata extends HashMap<String, Object> {
this.putAll(map);
}
+ /**
+ * Normalizes the value before storing so the {@link #CREATE_TIME}-is-Date
+ * invariant holds regardless of how entries are added.
+ */
+ @Override
+ public Object put(String key, Object value) {
+ return super.put(key, normalizeValue(key, value));
+ }
+
+ @Override
+ public void putAll(Map<? extends String, ?> map) {
+ for (Map.Entry<? extends String, ?> e : map.entrySet()) {
+ this.put(e.getKey(), e.getValue());
+ }
+ }
+
+ /**
+ * Normalize internal userdata values whose runtime type can diverge from
+ * their serialized form. The only such key today is {@link #CREATE_TIME}:
+ * it is written as a {@link java.util.Date} but persisted as a formatted
+ * JSON string by the backend serializers, and Jackson cannot re-type a
+ * value to {@code Date} when the target is a raw {@code Map}. This method
+ * restores the original type after deserialization. Idempotent for values
+ * already of the expected type.
+ * <p>
+ * An empty string is passed through unchanged: it is the key-only
+ * placeholder used by the {@code eliminate()}/{@code DELETE} builder flow
+ * (e.g. {@code .userdata(CREATE_TIME, "").eliminate()}), where the value
is
+ * ignored and only the key drives {@code removeUserdata}. Parsing it would
+ * fail before the eliminate path can apply its key-only semantics.
+ */
+ public static Object normalizeValue(String key, Object value) {
+ if (CREATE_TIME.equals(key) && value instanceof String &&
+ !((String) value).isEmpty()) {
+ try {
+ return DateUtil.parse((String) value);
+ } catch (RuntimeException e) {
+ throw new IllegalArgumentException(String.format(
+ "Invalid userdata '%s' value: '%s'",
+ CREATE_TIME, value), e);
+ }
+ }
+ return value;
+ }
+
public static void check(Userdata userdata, Action action) {
if (userdata == null) {
return;
diff --git
a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/PropertyKeyCoreTest.java
b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/PropertyKeyCoreTest.java
index 5cd1193fc..0609f607b 100644
---
a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/PropertyKeyCoreTest.java
+++
b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/PropertyKeyCoreTest.java
@@ -631,6 +631,26 @@ public class PropertyKeyCoreTest extends SchemaCoreTest {
Assert.assertEquals(0, age.userdata().get("min"));
}
+ @Test
+ public void testEliminatePropertyKeyCreateTimeUserdata() {
+ SchemaManager schema = graph().schema();
+
+ PropertyKey age = schema.propertyKey("age")
+ .userdata("min", 0)
+ .create();
+ Assert.assertEquals(2, age.userdata().size());
+ Assert.assertTrue(age.userdata().containsKey(Userdata.CREATE_TIME));
+
+ // "" is a key-only placeholder for eliminate; it must not be parsed
+ // as a date (regression: normalization on Userdata.put()).
+ age = schema.propertyKey("age")
+ .userdata(Userdata.CREATE_TIME, "")
+ .eliminate();
+ Assert.assertEquals(1, age.userdata().size());
+ Assert.assertFalse(age.userdata().containsKey(Userdata.CREATE_TIME));
+ Assert.assertEquals(0, age.userdata().get("min"));
+ }
+
@Test
public void testUpdatePropertyKeyWithoutUserdata() {
SchemaManager schema = graph().schema();
diff --git
a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java
b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java
index f9542ecac..0780b03a6 100644
---
a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java
+++
b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java
@@ -43,6 +43,7 @@ import org.apache.hugegraph.unit.core.QueryTest;
import org.apache.hugegraph.unit.core.RangeTest;
import org.apache.hugegraph.unit.core.RolePermissionTest;
import org.apache.hugegraph.unit.core.RowLockTest;
+import org.apache.hugegraph.unit.core.SchemaElementTest;
import org.apache.hugegraph.unit.core.SecurityManagerTest;
import org.apache.hugegraph.unit.core.SerialEnumTest;
import org.apache.hugegraph.unit.core.ServerInfoManagerTest;
@@ -65,6 +66,7 @@ import
org.apache.hugegraph.unit.serializer.SerializerFactoryTest;
import org.apache.hugegraph.unit.serializer.StoreSerializerTest;
import org.apache.hugegraph.unit.serializer.TableBackendEntryTest;
import org.apache.hugegraph.unit.serializer.TextBackendEntryTest;
+import org.apache.hugegraph.unit.serializer.TextSerializerTest;
import org.apache.hugegraph.unit.store.RamIntObjectMapTest;
import org.apache.hugegraph.unit.util.CompressUtilTest;
import org.apache.hugegraph.unit.util.JsonUtilTest;
@@ -129,6 +131,7 @@ import org.junit.runners.Suite;
ServerInfoManagerTest.class,
RoleElectionStateMachineTest.class,
HugeGraphAuthProxyTest.class,
+ SchemaElementTest.class,
/* serializer */
BytesBufferTest.class,
@@ -139,6 +142,7 @@ import org.junit.runners.Suite;
BinarySerializerTest.class,
BinaryScatterSerializerTest.class,
StoreSerializerTest.class,
+ TextSerializerTest.class,
/* cassandra */
CassandraTest.class,
diff --git
a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/SchemaElementTest.java
b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/SchemaElementTest.java
new file mode 100644
index 000000000..0bcdddf89
--- /dev/null
+++
b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/SchemaElementTest.java
@@ -0,0 +1,212 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hugegraph.unit.core;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.hugegraph.backend.id.IdGenerator;
+import org.apache.hugegraph.schema.PropertyKey;
+import org.apache.hugegraph.schema.SchemaElement;
+import org.apache.hugegraph.schema.Userdata;
+import org.apache.hugegraph.schema.VertexLabel;
+import org.apache.hugegraph.testutil.Assert;
+import org.apache.hugegraph.unit.FakeObjects;
+import org.apache.hugegraph.util.DateUtil;
+import org.junit.Test;
+
+public class SchemaElementTest {
+
+ private static SchemaElement newSchema() {
+ return new PropertyKey(null, IdGenerator.of(1L), "test");
+ }
+
+ @Test
+ public void testSingleSetterNormalizesCreateTimeStringToDate() {
+ SchemaElement schema = newSchema();
+ String formatted = "2026-05-14 10:11:12.345";
+
+ schema.userdata(Userdata.CREATE_TIME, formatted);
+
+ Object value = schema.userdata().get(Userdata.CREATE_TIME);
+ Assert.assertTrue("CREATE_TIME should be a Date, was " +
+ (value == null ? "null" : value.getClass()),
+ value instanceof Date);
+ Assert.assertEquals(DateUtil.parse(formatted), value);
+ }
+
+ @Test
+ public void testSingleSetterKeepsCreateTimeDateUnchanged() {
+ SchemaElement schema = newSchema();
+ Date now = DateUtil.now();
+
+ schema.userdata(Userdata.CREATE_TIME, now);
+
+ Assert.assertSame(now, schema.userdata().get(Userdata.CREATE_TIME));
+ }
+
+ @Test
+ public void testSingleSetterRejectsInvalidCreateTimeString() {
+ SchemaElement schema = newSchema();
+
+ Assert.assertThrows(IllegalArgumentException.class, () -> {
+ schema.userdata(Userdata.CREATE_TIME, "not-a-date");
+ }, e -> {
+ Assert.assertContains(Userdata.CREATE_TIME, e.getMessage());
+ Assert.assertContains("not-a-date", e.getMessage());
+ Assert.assertNotNull(e.getCause());
+ });
+ }
+
+ @Test
+ public void testSingleSetterRejectsNullCreateTime() {
+ SchemaElement schema = newSchema();
+
+ Assert.assertThrows(IllegalArgumentException.class, () -> {
+ schema.userdata(Userdata.CREATE_TIME, null);
+ }, e -> {
+ Assert.assertContains("userdata value", e.getMessage());
+ });
+ }
+
+ @Test
+ public void testSingleSetterPassesThroughBlankCreateTime() {
+ // "" is the key-only placeholder for the eliminate()/DELETE builder
+ // flow (.userdata(CREATE_TIME, "").eliminate()); it must not be
parsed.
+ SchemaElement schema = newSchema();
+
+ schema.userdata(Userdata.CREATE_TIME, "");
+
+ Object value = schema.userdata().get(Userdata.CREATE_TIME);
+ Assert.assertEquals("", value);
+ }
+
+ @Test
+ public void testSingleSetterLeavesOtherStringKeysUntouched() {
+ SchemaElement schema = newSchema();
+
+ schema.userdata("note", "2026-05-14 10:11:12.345");
+
+ Object value = schema.userdata().get("note");
+ Assert.assertTrue(value instanceof String);
+ Assert.assertEquals("2026-05-14 10:11:12.345", value);
+ }
+
+ @Test
+ public void testUserdataConstructorNormalizesCreateTimeString() {
+ String formatted = "2026-05-14 10:11:12.345";
+ Map<String, Object> map = new HashMap<>();
+ map.put(Userdata.CREATE_TIME, formatted);
+
+ Userdata userdata = new Userdata(map);
+
+ Object createTime = userdata.get(Userdata.CREATE_TIME);
+ Assert.assertTrue(createTime instanceof Date);
+ Assert.assertEquals(DateUtil.parse(formatted),
+ createTime);
+ }
+
+ @Test
+ public void testUserdataConstructorLeavesOtherEntriesUntouched() {
+ Map<String, Object> map = new HashMap<>();
+ map.put("note", "2026-05-14 10:11:12.345");
+ map.put("count", 42);
+
+ Userdata userdata = new Userdata(map);
+
+ Assert.assertEquals("2026-05-14 10:11:12.345",
+ userdata.get("note"));
+ Assert.assertEquals(42, userdata.get("count"));
+ }
+
+ @Test
+ public void testUserdataConstructorRejectsInvalidCreateTimeString() {
+ Map<String, Object> map = new HashMap<>();
+ map.put(Userdata.CREATE_TIME, "not-a-date");
+
+ Assert.assertThrows(IllegalArgumentException.class, () -> {
+ new Userdata(map);
+ }, e -> {
+ Assert.assertContains(Userdata.CREATE_TIME, e.getMessage());
+ Assert.assertContains("not-a-date", e.getMessage());
+ Assert.assertNotNull(e.getCause());
+ });
+ }
+
+ @Test
+ public void testBulkSetterNormalizesCreateTimeAndKeepsOtherEntries() {
+ SchemaElement schema = newSchema();
+ Userdata bulk = new Userdata();
+ String formatted = "2026-05-14 10:11:12.345";
+ bulk.put(Userdata.CREATE_TIME, formatted);
+ bulk.put("note", "hello");
+ bulk.put("count", 42);
+
+ schema.userdata(bulk);
+
+ Object createTime = schema.userdata().get(Userdata.CREATE_TIME);
+ Assert.assertTrue(createTime instanceof Date);
+ Assert.assertEquals(DateUtil.parse(formatted), createTime);
+ Assert.assertEquals("hello", schema.userdata().get("note"));
+ Assert.assertEquals(42, schema.userdata().get("count"));
+ }
+
+ @Test
+ public void testBulkSetterKeepsCreateTimeDateUnchanged() {
+ SchemaElement schema = newSchema();
+ Userdata bulk = new Userdata();
+ Date now = DateUtil.now();
+ bulk.put(Userdata.CREATE_TIME, now);
+
+ schema.userdata(bulk);
+
+ Assert.assertSame(now, schema.userdata().get(Userdata.CREATE_TIME));
+ }
+
+ @Test
+ public void testVertexLabelFromMapNormalizesCreateTimeString() {
+ String formatted = "2026-05-14 10:11:12.345";
+ Map<String, Object> userdata = new HashMap<>();
+ userdata.put(Userdata.CREATE_TIME, formatted);
+
+ Map<String, Object> map = new HashMap<>();
+ map.put(VertexLabel.P.ID, 1);
+ map.put(VertexLabel.P.NAME, "person");
+ map.put(VertexLabel.P.USERDATA, userdata);
+
+ VertexLabel vertexLabel = VertexLabel.fromMap(map,
+ new
FakeObjects().graph());
+
+ Object createTime = vertexLabel.userdata().get(Userdata.CREATE_TIME);
+ Assert.assertTrue(createTime instanceof Date);
+ Assert.assertEquals(DateUtil.parse(formatted),
+ createTime);
+ }
+
+ @Test
+ public void testBulkSetterRejectsNullUserdata() {
+ SchemaElement schema = newSchema();
+
+ Assert.assertThrows(IllegalArgumentException.class, () -> {
+ schema.userdata(null);
+ }, e -> {
+ Assert.assertContains("userdata", e.getMessage());
+ });
+ }
+}
diff --git
a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/BinarySerializerTest.java
b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/BinarySerializerTest.java
index 7a5aa4443..ba5136923 100644
---
a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/BinarySerializerTest.java
+++
b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/BinarySerializerTest.java
@@ -17,15 +17,21 @@
package org.apache.hugegraph.unit.serializer;
+import java.util.Date;
+
+import org.apache.hugegraph.backend.id.IdGenerator;
import org.apache.hugegraph.backend.serializer.BinarySerializer;
import org.apache.hugegraph.backend.store.BackendEntry;
import org.apache.hugegraph.config.HugeConfig;
+import org.apache.hugegraph.schema.PropertyKey;
+import org.apache.hugegraph.schema.Userdata;
import org.apache.hugegraph.structure.HugeEdge;
import org.apache.hugegraph.structure.HugeVertex;
import org.apache.hugegraph.testutil.Assert;
import org.apache.hugegraph.testutil.Whitebox;
import org.apache.hugegraph.unit.BaseUnitTest;
import org.apache.hugegraph.unit.FakeObjects;
+import org.apache.hugegraph.util.DateUtil;
import org.junit.Test;
public class BinarySerializerTest extends BaseUnitTest {
@@ -105,6 +111,27 @@ public class BinarySerializerTest extends BaseUnitTest {
Assert.assertNull(ser.readVertex(edge.graph(), null));
}
+ @Test
+ public void testPropertyKeyUserdataCreateTimeRoundTripsAsDate() {
+ HugeConfig config = FakeObjects.newConfig();
+ BinarySerializer ser = new BinarySerializer(config);
+
+ FakeObjects objects = new FakeObjects();
+ PropertyKey original = objects.newPropertyKey(IdGenerator.of(1L),
+ "name");
+ Date created = DateUtil.parse("2026-05-14 10:11:12.345");
+ original.userdata(Userdata.CREATE_TIME, created);
+
+ BackendEntry entry = ser.writePropertyKey(original);
+ PropertyKey reloaded = ser.readPropertyKey(objects.graph(), entry);
+
+ Object value = reloaded.userdata().get(Userdata.CREATE_TIME);
+ Assert.assertTrue("CREATE_TIME should be a Date after round-trip, " +
+ "was " + (value == null ? "null" : value.getClass()),
+ value instanceof Date);
+ Assert.assertEquals(created, value);
+ }
+
@Test
public void testEdgeForPartition() {
BinarySerializer ser = new BinarySerializer(true, true, true);
diff --git
a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/TextSerializerTest.java
b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/TextSerializerTest.java
new file mode 100644
index 000000000..97df554c4
--- /dev/null
+++
b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/TextSerializerTest.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hugegraph.unit.serializer;
+
+import java.util.Date;
+
+import org.apache.hugegraph.backend.id.IdGenerator;
+import org.apache.hugegraph.backend.serializer.TextSerializer;
+import org.apache.hugegraph.backend.store.BackendEntry;
+import org.apache.hugegraph.config.HugeConfig;
+import org.apache.hugegraph.schema.PropertyKey;
+import org.apache.hugegraph.schema.Userdata;
+import org.apache.hugegraph.testutil.Assert;
+import org.apache.hugegraph.unit.BaseUnitTest;
+import org.apache.hugegraph.unit.FakeObjects;
+import org.apache.hugegraph.util.DateUtil;
+import org.junit.Test;
+
+public class TextSerializerTest extends BaseUnitTest {
+
+ @Test
+ public void testPropertyKeyUserdataCreateTimeRoundTripsAsDate() {
+ HugeConfig config = FakeObjects.newConfig();
+ TextSerializer ser = new TextSerializer(config);
+
+ FakeObjects objects = new FakeObjects();
+ PropertyKey original = objects.newPropertyKey(IdGenerator.of(1L),
+ "name");
+ Date created = DateUtil.parse("2026-05-14 10:11:12.345");
+ original.userdata(Userdata.CREATE_TIME, created);
+
+ BackendEntry entry = ser.writePropertyKey(original);
+ PropertyKey reloaded = ser.readPropertyKey(objects.graph(), entry);
+
+ Object value = reloaded.userdata().get(Userdata.CREATE_TIME);
+ Assert.assertTrue("CREATE_TIME should be a Date after round-trip, " +
+ "was " + (value == null ? "null" : value.getClass()),
+ value instanceof Date);
+ Assert.assertEquals(created, value);
+ }
+}