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);
+    }
+}

Reply via email to