From 1d8f2dbae09fcb587eb76d237ce22706da9f3cd6 Mon Sep 17 00:00:00 2001 From: Evgeniy Cheban Date: Tue, 3 Nov 2026 12:42:07 +0200 Subject: [PATCH] Consider CurrentTimestamp annotation support Closes gh-158 --- README.md | 15 ++++++ .../annotations/CurrentTimestamp.java | 47 ++++++++++++++++++ .../annotations/ReindexAnnotationScanner.java | 49 ++++++++++++++----- .../reindexer/connector/ReindexerTest.java | 42 ++++++++++++++++ 4 files changed, 140 insertions(+), 13 deletions(-) create mode 100644 src/main/java/ru/rt/restream/reindexer/annotations/CurrentTimestamp.java diff --git a/README.md b/README.md index 7a10c35..3993fb6 100644 --- a/README.md +++ b/README.md @@ -365,6 +365,21 @@ db.query("items", Item.class) .update(); ``` +The `now()` function can also be applied using the `@CurrentTimestamp` annotation: + +```java + +@CurrentTimestamp(unit = TimeUnit.SECONDS, strategy = PreceptApplyStrategy.WHEN_NULL) +@Reindex(name = "created_at") +LocalDateTime createdAt; + +@CurrentTimestamp(unit = TimeUnit.SECONDS, strategy = PreceptApplyStrategy.ALWAYS) +@Reindex(name = "updated_at") +LocalDateTime updatedAt; +``` + +See Javadoc for `@CurrentTimestamp` for more information. + ##### serial() The `serial()` function returns a unique identifier from the sequence of integer for each document in the namespace. This function can be used in both `INSERT` and `UPDATE` queries. diff --git a/src/main/java/ru/rt/restream/reindexer/annotations/CurrentTimestamp.java b/src/main/java/ru/rt/restream/reindexer/annotations/CurrentTimestamp.java new file mode 100644 index 0000000..c52de25 --- /dev/null +++ b/src/main/java/ru/rt/restream/reindexer/annotations/CurrentTimestamp.java @@ -0,0 +1,47 @@ +/* + * Copyright 2020-present Restream + * + * Licensed 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 ru.rt.restream.reindexer.annotations; + +import ru.rt.restream.reindexer.PreceptApplyStrategy; +import ru.rt.restream.reindexer.TimeUnit; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Specifies that the value for the annotated field should be set to the current timestamp using {@code now()} function. + */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface CurrentTimestamp { + + /** + * A time unit to use in {@code now()} function. Default is {@link TimeUnit#SECONDS}. + * + * @return the time unit to use in {@code now()} function, defaults to {@link TimeUnit#SECONDS} + */ + TimeUnit unit() default TimeUnit.SECONDS; + + /** + * A precept apply strategy. Default is {@link PreceptApplyStrategy#ALWAYS}. + * + * @return the precept apply strategy to use, defaults to {@link PreceptApplyStrategy#ALWAYS} + */ + PreceptApplyStrategy strategy() default PreceptApplyStrategy.ALWAYS; + +} diff --git a/src/main/java/ru/rt/restream/reindexer/annotations/ReindexAnnotationScanner.java b/src/main/java/ru/rt/restream/reindexer/annotations/ReindexAnnotationScanner.java index f421655..464bc8c 100644 --- a/src/main/java/ru/rt/restream/reindexer/annotations/ReindexAnnotationScanner.java +++ b/src/main/java/ru/rt/restream/reindexer/annotations/ReindexAnnotationScanner.java @@ -148,19 +148,7 @@ && getFieldTypeByClass(fieldInfo.componentType) == COMPOSITE) { String collate = reindex.collate(); CollateMode collateMode = getCollateMode(collate); String sortOrder = getSortOrder(collateMode, collate); - ReindexerPrecept precept = null; - Serial serial = field.getAnnotation(Serial.class); - if (serial != null) { - if (!fieldInfo.isInt()) { - throw new IllegalStateException("@Serial is allowed only for int types (i.e INT, INT64)"); - } - precept = ReindexerPrecept.builder() - .fieldPath(fieldPath) - .jsonPath(jsonPath) - .expression(Expression.serial()) - .strategy(serial.strategy()) - .build(); - } + ReindexerPrecept precept = getReindexerPrecept(field, fieldInfo, fieldPath, jsonPath); FullTextConfig fullTextConfig = getFullTextConfig(field, reindex.type()); validateUuidIndexHasTypeUuidOrString(reindex, fieldInfo); boolean isUuid = reindex.isUuid() || fieldInfo.fieldType == UUID; @@ -221,6 +209,37 @@ && getFieldTypeByClass(fieldInfo.componentType) == COMPOSITE) { return indexes; } + private ReindexerPrecept getReindexerPrecept(Field field, FieldInfo fieldInfo, String fieldPath, String jsonPath) { + Serial serial = field.getAnnotation(Serial.class); + CurrentTimestamp currentTimestamp = field.getAnnotation(CurrentTimestamp.class); + if (serial != null && currentTimestamp != null) { + throw new IllegalStateException("@Serial and @CurrentTimestamp are mutually exclusive"); + } + if (serial != null) { + if (!fieldInfo.isInt() && !fieldInfo.isText()) { + throw new IllegalStateException("@Serial is allowed only for int types (i.e., INT, INT64) or string types"); + } + return ReindexerPrecept.builder() + .fieldPath(fieldPath) + .jsonPath(jsonPath) + .expression(Expression.serial()) + .strategy(serial.strategy()) + .build(); + } + if (currentTimestamp != null) { + if (!fieldInfo.isInt() && !fieldInfo.isText()) { + throw new IllegalStateException("@CurrentTimestamp is allowed only for int types (i.e., INT, INT64)"); + } + return ReindexerPrecept.builder() + .fieldPath(fieldPath) + .jsonPath(jsonPath) + .expression(Expression.now(currentTimestamp.unit())) + .strategy(currentTimestamp.strategy()) + .build(); + } + return null; + } + private IndexType specifyDefaultIndexType(Reindex reindex, Field field, FieldInfo fieldInfo) { if (reindex.type() != IndexType.DEFAULT) { return reindex.type(); @@ -405,6 +424,10 @@ private static class FieldInfo { private boolean isInt() { return fieldType == INT || fieldType == INT64; } + + private boolean isText() { + return fieldType == STRING; + } } } diff --git a/src/test/java/ru/rt/restream/reindexer/connector/ReindexerTest.java b/src/test/java/ru/rt/restream/reindexer/connector/ReindexerTest.java index 85f2af0..56cda0d 100644 --- a/src/test/java/ru/rt/restream/reindexer/connector/ReindexerTest.java +++ b/src/test/java/ru/rt/restream/reindexer/connector/ReindexerTest.java @@ -37,6 +37,7 @@ import ru.rt.restream.reindexer.TimeUnit; import ru.rt.restream.reindexer.Transaction; import ru.rt.restream.reindexer.annotations.Convert; +import ru.rt.restream.reindexer.annotations.CurrentTimestamp; import ru.rt.restream.reindexer.annotations.Enumerated; import ru.rt.restream.reindexer.annotations.Reindex; import ru.rt.restream.reindexer.annotations.Serial; @@ -1982,6 +1983,32 @@ public void testWithNowPreceptReturnsGeneratedTimestamp() { assertThat(updated.updatedAt, greaterThan(updatedAt)); } + @SneakyThrows + @Test + public void testWithCurrentTimestampReturnsGeneratedTimestamp() { + FieldConverterRegistryFactory.INSTANCE.registerFieldConverter(CurrentTimestampTestItem.class, "createdAt", new LocalDateTimeUnitFieldConverter(TimeUnit.MILLIS)); + FieldConverterRegistryFactory.INSTANCE.registerFieldConverter(CurrentTimestampTestItem.class, "updatedAt", new LocalDateTimeUnitFieldConverter(TimeUnit.MILLIS)); + String namespaceName = "items"; + db.openNamespace(namespaceName, NamespaceOptions.defaultOptions(), CurrentTimestampTestItem.class); + + CurrentTimestampTestItem item = new CurrentTimestampTestItem(1, null, null); + CurrentTimestampTestItem inserted = db.insert(namespaceName, item); + assertThat(inserted, sameInstance(item)); + assertThat(inserted.id, is(1)); + LocalDateTime createdAt = inserted.createdAt; + LocalDateTime updatedAt = inserted.updatedAt; + assertThat(createdAt, notNullValue()); + assertThat(updatedAt, notNullValue()); + + // Small delay to shift the updatedAt precept. + Thread.sleep(1000); + CurrentTimestampTestItem updated = db.update(namespaceName, inserted); + assertThat(updated, sameInstance(inserted)); + assertThat(updated.id, is(1)); + assertThat(updated.createdAt, is(createdAt)); + assertThat(updated.updatedAt, greaterThan(updatedAt)); + } + @Test public void testTransactionUpsertWithSerialId() { String namespaceName = "items"; @@ -3288,6 +3315,21 @@ public static class SerialIdTestItem { private Integer version; } + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + public static class CurrentTimestampTestItem { + @Reindex(name = "id", isPrimaryKey = true) + private Integer id; + @CurrentTimestamp(unit = TimeUnit.MILLIS, strategy = PreceptApplyStrategy.WHEN_NULL) + @Reindex(name = "createdAt") + private LocalDateTime createdAt; + @CurrentTimestamp(unit = TimeUnit.MILLIS) + @Reindex(name = "updatedAt") + private LocalDateTime updatedAt; + } + @Getter @Setter @Reindex(name = "composite", subIndexes = {"id", "name"})