diff --git a/.changes/next-release/bugfix-AmazonDynamoDB-1db6c7e.json b/.changes/next-release/bugfix-AmazonDynamoDB-1db6c7e.json new file mode 100644 index 000000000000..e3c7c53e0746 --- /dev/null +++ b/.changes/next-release/bugfix-AmazonDynamoDB-1db6c7e.json @@ -0,0 +1,6 @@ +{ + "type": "bugfix", + "category": "Amazon DynamoDB", + "description": "Fixed DurationAttributeConverter negative fractional duration serialization and parsing. Values like Duration.ofMillis(-1) now serialize as -0.001000000 and round trip correctly. Fixes [#7060](https://github.com/aws/aws-sdk-java-v2/issues/7060).", + "contributor": "Arnab Nandy" +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/attribute/DurationAttributeConverter.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/attribute/DurationAttributeConverter.java index a135dba1416e..b0abd94ae005 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/attribute/DurationAttributeConverter.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/attribute/DurationAttributeConverter.java @@ -79,9 +79,16 @@ public AttributeValueType attributeValueType() { @Override public AttributeValue transformFrom(Duration input) { + long seconds = input.getSeconds(); + int nanos = input.getNano(); + String value = seconds + (nanos == 0 ? "" : "." + padLeft(9, nanos)); + + if (input.isNegative() && nanos != 0) { + value = "-" + Math.abs(seconds + 1) + "." + padLeft(9, 1_000_000_000 - nanos); + } + return AttributeValue.builder() - .n(input.getSeconds() + - (input.getNano() == 0 ? "" : "." + padLeft(9, input.getNano()))) + .n(value) .build(); } @@ -103,10 +110,11 @@ private Visitor() { public Duration convertNumber(String value) { String[] splitOnDecimal = ConverterUtils.splitNumberOnDecimal(value); + boolean isNegative = value.startsWith("-"); long seconds = Long.parseLong(splitOnDecimal[0]); int nanoAdjustment = Integer.parseInt(padRight(splitOnDecimal[1])); - if (seconds < 0) { + if (isNegative) { nanoAdjustment = -nanoAdjustment; } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/attribute/DurationAttributeConverterTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/attribute/DurationAttributeConverterTest.java index 477273721b3c..289ccabf0e1a 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/attribute/DurationAttributeConverterTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/attribute/DurationAttributeConverterTest.java @@ -61,6 +61,14 @@ void testConvertTo_NoPadding(String value, Duration expected) { assertThat(converted).isEqualByComparingTo(expected); } + @ParameterizedTest + @MethodSource("roundTripDurations") + void roundTrip(Duration duration) { + Duration converted = converter.transformTo(converter.transformFrom(duration)); + + assertThat(converted).isEqualByComparingTo(duration); + } + static Stream noPadding() { return Stream.of( Arguments.of("0.123456789", Duration.ofNanos(123_456_789)), @@ -74,7 +82,10 @@ static Stream noPadding() { Arguments.of("0.1", Duration.ofMillis(100)), Arguments.of("0.001", Duration.ofMillis(1)), Arguments.of("0.000001", Duration.of(1, MICROS)), - Arguments.of("0.001", Duration.ofMillis(1)) + Arguments.of("0.001", Duration.ofMillis(1)), + Arguments.of("-0.1", Duration.ofMillis(-100)), + Arguments.of("-0.001", Duration.ofMillis(-1)), + Arguments.of("-0.000001", Duration.of(-1, MICROS)) ); } @@ -107,7 +118,22 @@ static Stream durations() { Arguments.of("9", Duration.ofSeconds(9)), Arguments.of("-9", Duration.ofSeconds(-9)), Arguments.of("0.001000000", Duration.ofMillis(1)), - Arguments.of("0.000000001", Duration.ofNanos(1))); + Arguments.of("0.000000001", Duration.ofNanos(1)), + Arguments.of("-0.001000000", Duration.ofMillis(-1)), + Arguments.of("-0.000000001", Duration.ofNanos(-1)), + Arguments.of("-1.234567890", Duration.ofNanos(-1_234_567_890))); + } + + static Stream roundTripDurations() { + return Stream.of( + Duration.ZERO, + Duration.ofNanos(1), + Duration.ofMillis(1), + Duration.ofSeconds(1, 234_567_890), + Duration.ofSeconds(-9), + Duration.ofMillis(-1), + Duration.ofNanos(-1), + Duration.ofNanos(-1_234_567_890)); } -} \ No newline at end of file +}