Skip to content

Commit 28f6637

Browse files
committed
added support for timeToLive attributes + tests
1 parent f272552 commit 28f6637

22 files changed

+1543
-3
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "feature",
3+
"category": "Amazon DynamoDB Enhanced Client",
4+
"contributor": "",
5+
"description": "Added support for TimeToLive attributes in DynamoDB Enhanced Client"
6+
}

services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/DynamoDbAsyncTable.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import software.amazon.awssdk.enhanced.dynamodb.model.DeleteItemEnhancedRequest;
2424
import software.amazon.awssdk.enhanced.dynamodb.model.DeleteItemEnhancedResponse;
2525
import software.amazon.awssdk.enhanced.dynamodb.model.DescribeTableEnhancedResponse;
26+
import software.amazon.awssdk.enhanced.dynamodb.model.DescribeTimeToLiveEnhancedResponse;
2627
import software.amazon.awssdk.enhanced.dynamodb.model.GetItemEnhancedRequest;
2728
import software.amazon.awssdk.enhanced.dynamodb.model.GetItemEnhancedResponse;
2829
import software.amazon.awssdk.enhanced.dynamodb.model.Page;
@@ -34,6 +35,7 @@
3435
import software.amazon.awssdk.enhanced.dynamodb.model.ScanEnhancedRequest;
3536
import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedRequest;
3637
import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedResponse;
38+
import software.amazon.awssdk.enhanced.dynamodb.model.UpdateTimeToLiveEnhancedResponse;
3739
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
3840
import software.amazon.awssdk.services.dynamodb.model.ConsumedCapacity;
3941
import software.amazon.awssdk.services.dynamodb.model.DescribeTableRequest;
@@ -947,4 +949,12 @@ default CompletableFuture<Void> deleteTable() {
947949
default CompletableFuture<DescribeTableEnhancedResponse> describeTable() {
948950
throw new UnsupportedOperationException();
949951
}
952+
953+
default CompletableFuture<DescribeTimeToLiveEnhancedResponse> describeTimeToLive() {
954+
throw new UnsupportedOperationException();
955+
}
956+
957+
default CompletableFuture<UpdateTimeToLiveEnhancedResponse> updateTimeToLive(boolean enabled) {
958+
throw new UnsupportedOperationException();
959+
}
950960
}

services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/DynamoDbTable.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import software.amazon.awssdk.enhanced.dynamodb.model.DeleteItemEnhancedRequest;
2424
import software.amazon.awssdk.enhanced.dynamodb.model.DeleteItemEnhancedResponse;
2525
import software.amazon.awssdk.enhanced.dynamodb.model.DescribeTableEnhancedResponse;
26+
import software.amazon.awssdk.enhanced.dynamodb.model.DescribeTimeToLiveEnhancedResponse;
2627
import software.amazon.awssdk.enhanced.dynamodb.model.GetItemEnhancedRequest;
2728
import software.amazon.awssdk.enhanced.dynamodb.model.GetItemEnhancedResponse;
2829
import software.amazon.awssdk.enhanced.dynamodb.model.Page;
@@ -34,6 +35,7 @@
3435
import software.amazon.awssdk.enhanced.dynamodb.model.ScanEnhancedRequest;
3536
import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedRequest;
3637
import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedResponse;
38+
import software.amazon.awssdk.enhanced.dynamodb.model.UpdateTimeToLiveEnhancedResponse;
3739
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
3840
import software.amazon.awssdk.services.dynamodb.model.ConsumedCapacity;
3941
import software.amazon.awssdk.services.dynamodb.model.DescribeTableRequest;
@@ -926,4 +928,12 @@ default void deleteTable() {
926928
default DescribeTableEnhancedResponse describeTable() {
927929
throw new UnsupportedOperationException();
928930
}
931+
932+
default DescribeTimeToLiveEnhancedResponse describeTimeToLive() {
933+
throw new UnsupportedOperationException();
934+
}
935+
936+
default UpdateTimeToLiveEnhancedResponse updateTimeToLive(boolean enabled) {
937+
throw new UnsupportedOperationException();
938+
}
929939
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.enhanced.dynamodb.extensions;
17+
18+
import java.time.Duration;
19+
import java.time.Instant;
20+
import java.time.LocalDate;
21+
import java.time.LocalDateTime;
22+
import java.time.LocalTime;
23+
import java.time.ZoneOffset;
24+
import java.time.ZonedDateTime;
25+
import java.time.temporal.ChronoUnit;
26+
import java.time.temporal.TemporalUnit;
27+
import java.util.Collections;
28+
import java.util.HashMap;
29+
import java.util.Map;
30+
import java.util.function.Consumer;
31+
import software.amazon.awssdk.annotations.SdkPublicApi;
32+
import software.amazon.awssdk.annotations.ThreadSafe;
33+
import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType;
34+
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClientExtension;
35+
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbExtensionContext;
36+
import software.amazon.awssdk.enhanced.dynamodb.EnhancedType;
37+
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTag;
38+
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableMetadata;
39+
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
40+
import software.amazon.awssdk.utils.StringUtils;
41+
import software.amazon.awssdk.utils.Validate;
42+
43+
@SdkPublicApi
44+
@ThreadSafe
45+
public final class TimeToLiveExtension implements DynamoDbEnhancedClientExtension {
46+
47+
public static final String CUSTOM_METADATA_KEY = "TimeToLiveExtension:TimeToLiveAttribute";
48+
49+
private TimeToLiveExtension() {
50+
}
51+
52+
public static TimeToLiveExtension.Builder builder() {
53+
return new TimeToLiveExtension.Builder();
54+
}
55+
56+
/**
57+
* @return an Instance of {@link TimeToLiveExtension}
58+
*/
59+
public static TimeToLiveExtension create() {
60+
return new TimeToLiveExtension();
61+
}
62+
63+
@Override
64+
public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite context) {
65+
Map<String, ?> customTTLMetadata = context.tableMetadata()
66+
.customMetadataObject(CUSTOM_METADATA_KEY, Map.class).orElse(null);
67+
68+
if (customTTLMetadata != null) {
69+
String ttlAttributeName = (String) customTTLMetadata.get("attributeName");
70+
String baseFieldName = (String) customTTLMetadata.get("baseField");
71+
Long duration = (Long) customTTLMetadata.get("duration");
72+
TemporalUnit unit = (TemporalUnit) customTTLMetadata.get("unit");
73+
74+
Map<String, AttributeValue> itemToTransform = new HashMap<>(context.items());
75+
76+
if (!itemToTransform.containsKey(ttlAttributeName) && StringUtils.isNotBlank(baseFieldName)
77+
&& itemToTransform.containsKey(baseFieldName)) {
78+
Object baseFieldValue = context.tableSchema().converterForAttribute(baseFieldName)
79+
.transformTo(itemToTransform.get(baseFieldName));
80+
Long ttlEpochSeconds = computeTTLFromBase(baseFieldValue, duration, unit);
81+
itemToTransform.put(ttlAttributeName, AttributeValue.builder().n(String.valueOf(ttlEpochSeconds)).build());
82+
83+
return WriteModification.builder().transformedItem(Collections.unmodifiableMap(itemToTransform)).build();
84+
}
85+
}
86+
87+
return WriteModification.builder().build();
88+
}
89+
90+
private static Long computeTTLFromBase(Object baseValue, long duration, TemporalUnit unit) {
91+
if (baseValue instanceof Instant) {
92+
return ((Instant) baseValue).plus(duration, unit).getEpochSecond();
93+
}
94+
if (baseValue instanceof LocalDate) {
95+
return ((LocalDate) baseValue).atStartOfDay(ZoneOffset.UTC).plus(duration, unit).toEpochSecond();
96+
}
97+
if (baseValue instanceof LocalDateTime) {
98+
return ((LocalDateTime) baseValue).plus(duration, unit).toEpochSecond(ZoneOffset.UTC);
99+
}
100+
if (baseValue instanceof LocalTime) {
101+
return LocalDate.now().atTime((LocalTime) baseValue).plus(duration, unit).toEpochSecond(ZoneOffset.UTC);
102+
}
103+
if (baseValue instanceof ZonedDateTime) {
104+
return ((ZonedDateTime) baseValue).plus(duration, unit).toEpochSecond();
105+
}
106+
if (baseValue instanceof Long) {
107+
return (Long) baseValue + Duration.of(duration, unit).getSeconds();
108+
}
109+
110+
throw new IllegalArgumentException("Unsupported base field type for TTL computation: " + baseValue.getClass().getName());
111+
}
112+
113+
public static final class Builder {
114+
private Builder() {
115+
}
116+
117+
public TimeToLiveExtension build() {
118+
return new TimeToLiveExtension();
119+
}
120+
}
121+
122+
public static final class AttributeTags {
123+
private AttributeTags() {
124+
}
125+
126+
/**
127+
* Used to explicitly designate an attribute to determine the TTL on the table.
128+
*
129+
* <p><b>How this works</b></p>
130+
* <ul>
131+
* <li>If a TTL attribute is set, it takes precedence over <i>baseField</i>.</li>
132+
* <li>If no TTL attribute is set, it checks for <i>baseField</i>.</li>
133+
* <li>If <i>baseField</i> is present, the TTL is calculated using its value, <i>duration</i>, and <i>unit</i>.</li>
134+
* <li>The final TTL value is converted to epoch seconds before storing in DynamoDB.</li>
135+
* </ul>
136+
*
137+
* @param baseField Optional attribute name used to determine the TTL value.
138+
* @param duration Additional long value used for TTL calculation.
139+
* @param unit {@link ChronoUnit} value specifying the TTL duration unit.
140+
*/
141+
public static StaticAttributeTag timeToLiveAttribute(String baseField, long duration, ChronoUnit unit) {
142+
return new TimeToLiveAttribute(baseField, duration, unit);
143+
}
144+
}
145+
146+
private static final class TimeToLiveAttribute implements StaticAttributeTag {
147+
148+
public String baseField;
149+
public long duration;
150+
public ChronoUnit unit;
151+
152+
private TimeToLiveAttribute(String baseField, long duration, ChronoUnit unit) {
153+
this.baseField = baseField;
154+
this.duration = duration;
155+
this.unit = unit;
156+
}
157+
158+
@Override
159+
public <R> void validateType(String attributeName, EnhancedType<R> type,
160+
AttributeValueType attributeValueType) {
161+
162+
Validate.notNull(type, "type is null");
163+
Validate.notNull(type.rawClass(), "rawClass is null");
164+
Validate.notNull(attributeValueType, "attributeValueType is null");
165+
166+
if (!type.rawClass().equals(Long.class)) {
167+
throw new IllegalArgumentException(String.format(
168+
"Attribute '%s' of type %s is not a suitable type to be used as a TTL attribute. Only type Long " +
169+
"is supported.", attributeName, type.rawClass()));
170+
}
171+
}
172+
173+
@Override
174+
public Consumer<StaticTableMetadata.Builder> modifyMetadata(String attributeName,
175+
AttributeValueType attributeValueType) {
176+
Map<String, Object> customMetadataMap = new HashMap<>();
177+
customMetadataMap.put("attributeName", attributeName);
178+
customMetadataMap.put("baseField", baseField);
179+
customMetadataMap.put("duration", duration);
180+
customMetadataMap.put("unit", unit);
181+
182+
return metadata -> metadata.addCustomMetadataObject(CUSTOM_METADATA_KEY,
183+
Collections.unmodifiableMap(customMetadataMap));
184+
}
185+
}
186+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.enhanced.dynamodb.extensions.annotations;
17+
18+
import java.lang.annotation.ElementType;
19+
import java.lang.annotation.Retention;
20+
import java.lang.annotation.RetentionPolicy;
21+
import java.lang.annotation.Target;
22+
import java.time.temporal.ChronoUnit;
23+
import software.amazon.awssdk.annotations.SdkPublicApi;
24+
import software.amazon.awssdk.enhanced.dynamodb.internal.extensions.TimeToLiveAttributeTags;
25+
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.BeanTableSchemaAttributeTag;
26+
27+
/**
28+
* Annotation used to mark an attribute in a DynamoDB-enhanced client model as a Time-To-Live (TTL) field.
29+
* <p>
30+
* This annotation allows automatic computation and assignment of a TTL value based on another field (the {@code baseField})
31+
* and a time offset defined by {@code duration} and {@code unit}. The TTL value is stored in epoch seconds and
32+
* can be configured to expire items from the table automatically.
33+
* <p>
34+
* To use this, the annotated method should return a {@link Long} value, which will be populated by the SDK at write time.
35+
* The {@code baseField} can be a temporal type such as {@link java.time.Instant}, {@link java.time.LocalDate},
36+
* {@link java.time.LocalDateTime}, etc., or a {@link Long} representing epoch seconds directly, serving as the reference point
37+
* for TTL calculation.
38+
*/
39+
@Target(ElementType.METHOD)
40+
@Retention(RetentionPolicy.RUNTIME)
41+
@BeanTableSchemaAttributeTag(TimeToLiveAttributeTags.class)
42+
@SdkPublicApi
43+
public @interface DynamoDbTimeToLiveAttribute {
44+
45+
/**
46+
* The name of the attribute whose value will serve as the base for TTL computation.
47+
* This can be a temporal type (e.g., {@link java.time.Instant}, {@link java.time.LocalDateTime})
48+
* or a {@link Long} representing epoch seconds.
49+
*
50+
* @return the attribute name to use as the base timestamp for TTL
51+
*/
52+
String baseField() default "";
53+
54+
/**
55+
* The amount of time to add to the {@code baseField} when computing the TTL value.
56+
* The resulting time will be converted to epoch seconds.
57+
*
58+
* @return the time offset to apply to the base field
59+
*/
60+
long duration() default 0;
61+
62+
/**
63+
* The time unit associated with the {@code duration}. Defaults to {@link ChronoUnit#SECONDS}.
64+
*
65+
* @return the time unit to use with the duration
66+
*/
67+
ChronoUnit unit() default ChronoUnit.SECONDS;
68+
}

services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/client/DefaultDynamoDbAsyncTable.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,20 @@
3131
import software.amazon.awssdk.enhanced.dynamodb.internal.operations.DeleteItemOperation;
3232
import software.amazon.awssdk.enhanced.dynamodb.internal.operations.DeleteTableOperation;
3333
import software.amazon.awssdk.enhanced.dynamodb.internal.operations.DescribeTableOperation;
34+
import software.amazon.awssdk.enhanced.dynamodb.internal.operations.DescribeTimeToLiveOperation;
3435
import software.amazon.awssdk.enhanced.dynamodb.internal.operations.GetItemOperation;
3536
import software.amazon.awssdk.enhanced.dynamodb.internal.operations.PaginatedTableOperation;
3637
import software.amazon.awssdk.enhanced.dynamodb.internal.operations.PutItemOperation;
3738
import software.amazon.awssdk.enhanced.dynamodb.internal.operations.QueryOperation;
3839
import software.amazon.awssdk.enhanced.dynamodb.internal.operations.ScanOperation;
3940
import software.amazon.awssdk.enhanced.dynamodb.internal.operations.TableOperation;
4041
import software.amazon.awssdk.enhanced.dynamodb.internal.operations.UpdateItemOperation;
42+
import software.amazon.awssdk.enhanced.dynamodb.internal.operations.UpdateTimeToLiveOperation;
4143
import software.amazon.awssdk.enhanced.dynamodb.model.CreateTableEnhancedRequest;
4244
import software.amazon.awssdk.enhanced.dynamodb.model.DeleteItemEnhancedRequest;
4345
import software.amazon.awssdk.enhanced.dynamodb.model.DeleteItemEnhancedResponse;
4446
import software.amazon.awssdk.enhanced.dynamodb.model.DescribeTableEnhancedResponse;
47+
import software.amazon.awssdk.enhanced.dynamodb.model.DescribeTimeToLiveEnhancedResponse;
4548
import software.amazon.awssdk.enhanced.dynamodb.model.GetItemEnhancedRequest;
4649
import software.amazon.awssdk.enhanced.dynamodb.model.GetItemEnhancedResponse;
4750
import software.amazon.awssdk.enhanced.dynamodb.model.PagePublisher;
@@ -52,9 +55,14 @@
5255
import software.amazon.awssdk.enhanced.dynamodb.model.ScanEnhancedRequest;
5356
import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedRequest;
5457
import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedResponse;
58+
import software.amazon.awssdk.enhanced.dynamodb.model.UpdateTimeToLiveEnhancedResponse;
5559
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
5660
import software.amazon.awssdk.services.dynamodb.model.DescribeTableRequest;
5761
import software.amazon.awssdk.services.dynamodb.model.DescribeTableResponse;
62+
import software.amazon.awssdk.services.dynamodb.model.DescribeTimeToLiveRequest;
63+
import software.amazon.awssdk.services.dynamodb.model.DescribeTimeToLiveResponse;
64+
import software.amazon.awssdk.services.dynamodb.model.UpdateTimeToLiveRequest;
65+
import software.amazon.awssdk.services.dynamodb.model.UpdateTimeToLiveResponse;
5866

5967
@SdkInternalApi
6068
public final class DefaultDynamoDbAsyncTable<T> implements DynamoDbAsyncTable<T> {
@@ -326,6 +334,20 @@ public CompletableFuture<DescribeTableEnhancedResponse> describeTable() {
326334
return operation.executeOnPrimaryIndexAsync(tableSchema, tableName, extension, dynamoDbClient);
327335
}
328336

337+
@Override
338+
public CompletableFuture<DescribeTimeToLiveEnhancedResponse> describeTimeToLive() {
339+
TableOperation<T, DescribeTimeToLiveRequest, DescribeTimeToLiveResponse, DescribeTimeToLiveEnhancedResponse> operation =
340+
DescribeTimeToLiveOperation.create();
341+
return operation.executeOnPrimaryIndexAsync(tableSchema, tableName, extension, dynamoDbClient);
342+
}
343+
344+
@Override
345+
public CompletableFuture<UpdateTimeToLiveEnhancedResponse> updateTimeToLive(boolean enabled) {
346+
TableOperation<T, UpdateTimeToLiveRequest, UpdateTimeToLiveResponse, UpdateTimeToLiveEnhancedResponse> operation =
347+
UpdateTimeToLiveOperation.create(enabled);
348+
return operation.executeOnPrimaryIndexAsync(tableSchema, tableName, extension, dynamoDbClient);
349+
}
350+
329351
@Override
330352
public boolean equals(Object o) {
331353
if (this == o) {

0 commit comments

Comments
 (0)