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 966d3eed8a..2b5337f767 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,14 +96,27 @@ public Map userdata() { 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); + this.userdata.put(key, Userdata.normalizeValue(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) { - this.userdata.putAll(userdata); + E.checkArgumentNotNull(userdata, "userdata"); + for (Map.Entry e : userdata.entrySet()) { + this.userdata.put(e.getKey(), + Userdata.normalizeValue(e.getKey(), + e.getValue())); + } } public void removeUserdata(String key) { @@ -112,6 +125,7 @@ public void removeUserdata(String key) { } 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 d485e558b8..f046537264 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 org.apache.hugegraph.exception.NotAllowException; import org.apache.hugegraph.type.define.Action; +import org.apache.hugegraph.util.DateUtil; public class Userdata extends HashMap { @@ -34,7 +35,31 @@ public Userdata() { } public Userdata(Map map) { - this.putAll(map); + for (Map.Entry e : map.entrySet()) { + this.put(e.getKey(), normalizeValue(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. + */ + public static Object normalizeValue(String key, Object value) { + if (CREATE_TIME.equals(key) && value instanceof String) { + 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) { 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 ce249a967e..7b14909843 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.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.SystemSchemaStoreTest; @@ -64,6 +65,7 @@ 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; @@ -127,6 +129,7 @@ SystemSchemaStoreTest.class, RoleElectionStateMachineTest.class, HugeGraphAuthProxyTest.class, + SchemaElementTest.class, /* serializer */ BytesBufferTest.class, @@ -137,6 +140,7 @@ 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 0000000000..d110ae4013 --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/SchemaElementTest.java @@ -0,0 +1,175 @@ +/* + * 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 final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss.SSS"; + + 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, DATE_FORMAT), 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 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 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, DATE_FORMAT), + createTime); + } + + @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, DATE_FORMAT), 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 userdata = new HashMap<>(); + userdata.put(Userdata.CREATE_TIME, formatted); + + Map 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, DATE_FORMAT), + 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 7a5aa44436..6d4031a839 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,28 @@ public void testVertexForPartition() { 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", + "yyyy-MM-dd HH:mm:ss.SSS"); + 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 0000000000..938a6cd710 --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/TextSerializerTest.java @@ -0,0 +1,58 @@ +/* + * 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 { + + private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss.SSS"; + + @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", DATE_FORMAT); + 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); + } +}