From ad583850193f6e55a1148cc537ba30ed9c1500c2 Mon Sep 17 00:00:00 2001 From: aschenzle Date: Sat, 29 Aug 2026 23:40:31 -0700 Subject: [PATCH 1/2] DynamoDB DTOs and DSL --- .../execution/DynamoDbExecutionsDto.java | 15 ++++ .../execution/DynamoDbFailedQuery.java | 29 +++++++ .../operations/DynamoDbAttributeValueDto.java | 27 +++++++ .../DynamoDbDatabaseCommandsDto.java | 12 +++ .../operations/DynamoDbInsertionDto.java | 13 +++ .../DynamoDbInsertionResultsDto.java | 28 +++++++ .../operations/DynamoDbScalarTypeDto.java | 10 +++ .../controller/dynamodb/dsl/DynamoDbDsl.java | 81 +++++++++++++++++++ .../dynamodb/dsl/DynamoDbSequenceDsl.java | 15 ++++ .../dynamodb/dsl/DynamoDbStatementDsl.java | 23 ++++++ .../dynamodb/dsl/DynamoDbDslTest.java | 63 +++++++++++++++ 11 files changed, 316 insertions(+) create mode 100644 client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/execution/DynamoDbExecutionsDto.java create mode 100644 client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/execution/DynamoDbFailedQuery.java create mode 100644 client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/operations/DynamoDbAttributeValueDto.java create mode 100644 client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/operations/DynamoDbDatabaseCommandsDto.java create mode 100644 client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/operations/DynamoDbInsertionDto.java create mode 100644 client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/operations/DynamoDbInsertionResultsDto.java create mode 100644 client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/operations/DynamoDbScalarTypeDto.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/dsl/DynamoDbDsl.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/dsl/DynamoDbSequenceDsl.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/dsl/DynamoDbStatementDsl.java create mode 100644 client-java/controller/src/test/java/org/evomaster/client/java/controller/dynamodb/dsl/DynamoDbDslTest.java diff --git a/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/execution/DynamoDbExecutionsDto.java b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/execution/DynamoDbExecutionsDto.java new file mode 100644 index 0000000000..0d57642ac4 --- /dev/null +++ b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/execution/DynamoDbExecutionsDto.java @@ -0,0 +1,15 @@ +package org.evomaster.client.java.controller.api.dto.database.execution; + +import java.util.ArrayList; +import java.util.List; + +/** + * DynamoDB reads that can be satisfied by generated initialization data. + */ +public class DynamoDbExecutionsDto { + + public List failedQueries = new ArrayList<>(); + + public DynamoDbExecutionsDto() { + } +} diff --git a/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/execution/DynamoDbFailedQuery.java b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/execution/DynamoDbFailedQuery.java new file mode 100644 index 0000000000..159c73bb86 --- /dev/null +++ b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/execution/DynamoDbFailedQuery.java @@ -0,0 +1,29 @@ +package org.evomaster.client.java.controller.api.dto.database.execution; + +import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbAttributeValueDto; + +import java.util.ArrayList; +import java.util.List; + +/** + * Equality constraints from a successful DynamoDB read that returned no matching item. + */ +public class DynamoDbFailedQuery { + + public String tableName; + public List attributes = new ArrayList<>(); + + public DynamoDbFailedQuery() { + } + + /** + * Creates a failed-query description. + * + * @param tableName target table + * @param attributes equality-constrained attributes + */ + public DynamoDbFailedQuery(String tableName, List attributes) { + this.tableName = tableName; + this.attributes = new ArrayList<>(attributes); + } +} diff --git a/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/operations/DynamoDbAttributeValueDto.java b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/operations/DynamoDbAttributeValueDto.java new file mode 100644 index 0000000000..d405c92759 --- /dev/null +++ b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/operations/DynamoDbAttributeValueDto.java @@ -0,0 +1,27 @@ +package org.evomaster.client.java.controller.api.dto.database.operations; + +/** + * A named scalar DynamoDB attribute value. + */ +public class DynamoDbAttributeValueDto { + + public String attributeName; + public DynamoDbScalarTypeDto type; + public String value; + + public DynamoDbAttributeValueDto() { + } + + /** + * Creates an attribute value. + * + * @param attributeName attribute name + * @param type DynamoDB scalar type + * @param value string-preserved value + */ + public DynamoDbAttributeValueDto(String attributeName, DynamoDbScalarTypeDto type, String value) { + this.attributeName = attributeName; + this.type = type; + this.value = value; + } +} diff --git a/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/operations/DynamoDbDatabaseCommandsDto.java b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/operations/DynamoDbDatabaseCommandsDto.java new file mode 100644 index 0000000000..17f45b5d4e --- /dev/null +++ b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/operations/DynamoDbDatabaseCommandsDto.java @@ -0,0 +1,12 @@ +package org.evomaster.client.java.controller.api.dto.database.operations; + +import java.util.ArrayList; +import java.util.List; + +/** + * DynamoDB insertion commands sent to the controller. + */ +public class DynamoDbDatabaseCommandsDto { + + public List insertions = new ArrayList<>(); +} diff --git a/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/operations/DynamoDbInsertionDto.java b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/operations/DynamoDbInsertionDto.java new file mode 100644 index 0000000000..a6a29d5671 --- /dev/null +++ b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/operations/DynamoDbInsertionDto.java @@ -0,0 +1,13 @@ +package org.evomaster.client.java.controller.api.dto.database.operations; + +import java.util.ArrayList; +import java.util.List; + +/** + * An item to insert into a DynamoDB table. + */ +public class DynamoDbInsertionDto { + + public String tableName; + public List attributes = new ArrayList<>(); +} diff --git a/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/operations/DynamoDbInsertionResultsDto.java b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/operations/DynamoDbInsertionResultsDto.java new file mode 100644 index 0000000000..86f0f12142 --- /dev/null +++ b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/operations/DynamoDbInsertionResultsDto.java @@ -0,0 +1,28 @@ +package org.evomaster.client.java.controller.api.dto.database.operations; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Results of a sequence of DynamoDB insertions. + */ +public class DynamoDbInsertionResultsDto { + + public List executionResults = new ArrayList<>(); + public Integer failedInsertionIndex; + + /** + * Records the insertion that failed while preserving earlier successes. + * + * @param insertions attempted insertions + * @param failedIndex zero-based index of the failed insertion + */ + public void handleFailedInsertion(List insertions, int failedIndex) { + executionResults = new ArrayList<>(Collections.nCopies(insertions.size(), false)); + for (int i = 0; i < failedIndex; i++) { + executionResults.set(i, true); + } + failedInsertionIndex = failedIndex; + } +} diff --git a/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/operations/DynamoDbScalarTypeDto.java b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/operations/DynamoDbScalarTypeDto.java new file mode 100644 index 0000000000..7438336709 --- /dev/null +++ b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/operations/DynamoDbScalarTypeDto.java @@ -0,0 +1,10 @@ +package org.evomaster.client.java.controller.api.dto.database.operations; + +/** + * Scalar DynamoDB attribute types supported by generated insertions. + */ +public enum DynamoDbScalarTypeDto { + S, + N, + BOOL +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/dsl/DynamoDbDsl.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/dsl/DynamoDbDsl.java new file mode 100644 index 0000000000..d84705f813 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/dsl/DynamoDbDsl.java @@ -0,0 +1,81 @@ +package org.evomaster.client.java.controller.dynamodb.dsl; + +import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbAttributeValueDto; +import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbInsertionDto; +import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbScalarTypeDto; + +import java.util.ArrayList; +import java.util.List; + +/** + * DSL for DynamoDB insertions in generated tests. + */ +public final class DynamoDbDsl implements DynamoDbSequenceDsl, DynamoDbStatementDsl { + + private List insertions = new ArrayList<>(); + private DynamoDbInsertionDto current; + + private DynamoDbDsl() { + } + + /** + * @return a new DynamoDB insertion sequence + */ + public static DynamoDbSequenceDsl dynamoDb() { + return new DynamoDbDsl(); + } + + @Override + public DynamoDbStatementDsl insertInto(String tableName) { + checkOpen(); + if (tableName == null || tableName.isEmpty()) { + throw new IllegalArgumentException("Unspecified table"); + } + current = new DynamoDbInsertionDto(); + current.tableName = tableName; + insertions.add(current); + return this; + } + + @Override + public DynamoDbStatementDsl s(String name, String value) { + return attribute(name, DynamoDbScalarTypeDto.S, value); + } + + @Override + public DynamoDbStatementDsl n(String name, String value) { + return attribute(name, DynamoDbScalarTypeDto.N, value); + } + + @Override + public DynamoDbStatementDsl bool(String name, boolean value) { + return attribute(name, DynamoDbScalarTypeDto.BOOL, Boolean.toString(value)); + } + + @Override + public List dtos() { + checkOpen(); + List result = insertions; + insertions = null; + current = null; + return result; + } + + private DynamoDbStatementDsl attribute(String name, DynamoDbScalarTypeDto type, String value) { + checkOpen(); + if (current == null) { + throw new IllegalStateException("Call insertInto before adding attributes"); + } + if (name == null || name.isEmpty()) { + throw new IllegalArgumentException("Unspecified attribute name"); + } + current.attributes.add(new DynamoDbAttributeValueDto(name, type, value)); + return this; + } + + private void checkOpen() { + if (insertions == null) { + throw new IllegalStateException("DTO was already built for this object"); + } + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/dsl/DynamoDbSequenceDsl.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/dsl/DynamoDbSequenceDsl.java new file mode 100644 index 0000000000..8b043cb8f2 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/dsl/DynamoDbSequenceDsl.java @@ -0,0 +1,15 @@ +package org.evomaster.client.java.controller.dynamodb.dsl; + +/** + * Entry point for a DynamoDB insertion sequence. + */ +public interface DynamoDbSequenceDsl { + + /** + * Starts an item insertion. + * + * @param tableName target table + * @return item statement + */ + DynamoDbStatementDsl insertInto(String tableName); +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/dsl/DynamoDbStatementDsl.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/dsl/DynamoDbStatementDsl.java new file mode 100644 index 0000000000..f45f32b082 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/dsl/DynamoDbStatementDsl.java @@ -0,0 +1,23 @@ +package org.evomaster.client.java.controller.dynamodb.dsl; + +import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbInsertionDto; + +import java.util.List; + +/** + * Fluent definition of one DynamoDB item. + */ +public interface DynamoDbStatementDsl extends DynamoDbSequenceDsl { + + /** Adds a string attribute. */ + DynamoDbStatementDsl s(String name, String value); + + /** Adds a number attribute while preserving its exact text. */ + DynamoDbStatementDsl n(String name, String value); + + /** Adds a boolean attribute. */ + DynamoDbStatementDsl bool(String name, boolean value); + + /** @return the completed insertion DTOs */ + List dtos(); +} diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/dynamodb/dsl/DynamoDbDslTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/dynamodb/dsl/DynamoDbDslTest.java new file mode 100644 index 0000000000..d0f28c8c39 --- /dev/null +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/dynamodb/dsl/DynamoDbDslTest.java @@ -0,0 +1,63 @@ +package org.evomaster.client.java.controller.dynamodb.dsl; + +import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbAttributeValueDto; +import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbInsertionDto; +import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbScalarTypeDto; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** Tests the DynamoDB insertion DTOs produced by the generated-test DSL. */ +public class DynamoDbDslTest { + + @Test + public void testBuildsWorldCupPlayerInsertions() { + List insertions = DynamoDbDsl.dynamoDb() + .insertInto("WorldCupPlayers") + .s("country", "Argentina") + .n("fifaId", "10") + .bool("captain", true) + .insertInto("WorldCupPlayers") + .s("country", "Brazil") + .n("fifaId", "1") + .dtos(); + + assertEquals(2, insertions.size()); + assertInsertion(insertions.get(0), "WorldCupPlayers", "country", DynamoDbScalarTypeDto.S, "Argentina"); + assertInsertion(insertions.get(0), "WorldCupPlayers", "fifaId", DynamoDbScalarTypeDto.N, "10"); + assertInsertion(insertions.get(0), "WorldCupPlayers", "captain", DynamoDbScalarTypeDto.BOOL, "true"); + assertInsertion(insertions.get(1), "WorldCupPlayers", "country", DynamoDbScalarTypeDto.S, "Brazil"); + assertInsertion(insertions.get(1), "WorldCupPlayers", "fifaId", DynamoDbScalarTypeDto.N, "1"); + } + + @Test + public void testRejectsIncompleteInsertionDefinitions() { + assertThrows(IllegalArgumentException.class, () -> DynamoDbDsl.dynamoDb().insertInto(null)); + assertThrows(IllegalArgumentException.class, () -> DynamoDbDsl.dynamoDb().insertInto("")); + + DynamoDbStatementDsl statement = (DynamoDbStatementDsl) DynamoDbDsl.dynamoDb(); + assertThrows(IllegalStateException.class, () -> statement.s("country", "Argentina")); + + DynamoDbStatementDsl completed = DynamoDbDsl.dynamoDb().insertInto("WorldCupPlayers"); + completed.dtos(); + assertThrows(IllegalStateException.class, () -> completed.insertInto("WorldCupPlayers")); + } + + private void assertInsertion( + DynamoDbInsertionDto insertion, + String tableName, + String attributeName, + DynamoDbScalarTypeDto type, + String value) { + assertEquals(tableName, insertion.tableName); + DynamoDbAttributeValueDto attribute = insertion.attributes.stream() + .filter(candidate -> attributeName.equals(candidate.attributeName)) + .findFirst() + .orElseThrow(() -> new AssertionError("Missing attribute " + attributeName)); + assertEquals(type, attribute.type); + assertEquals(value, attribute.value); + } +} From 40b1e6c03b1a2e9684be62d847a7ba86fadd84be Mon Sep 17 00:00:00 2001 From: aschenzle Date: Sun, 30 Aug 2026 11:04:48 -0700 Subject: [PATCH 2/2] DynamoDB insertions core actions, executor, builder. --- .../dynamodb/DynamoDbCommandExecutor.java | 151 ++++++++++++++++++ .../dynamodb/DynamoDbCommandExecutorTest.java | 99 ++++++++++++ .../core/database/dynamodb/DynamoDbAction.kt | 59 +++++++ .../database/dynamodb/DynamoDbActionResult.kt | 32 ++++ .../dynamodb/DynamoDbActionTransformer.kt | 34 ++++ .../database/dynamodb/DynamoDbExecution.kt | 15 ++ .../dynamodb/DynamoDbInsertBuilder.kt | 55 +++++++ .../database/dynamodb/DynamoDbActionTest.kt | 65 ++++++++ .../dynamodb/DynamoDbActionTransformerTest.kt | 42 +++++ .../dynamodb/DynamoDbInsertBuilderTest.kt | 63 ++++++++ 10 files changed, 615 insertions(+) create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/DynamoDbCommandExecutor.java create mode 100644 client-java/controller/src/test/java/org/evomaster/client/java/controller/dynamodb/DynamoDbCommandExecutorTest.java create mode 100644 core/src/main/kotlin/org/evomaster/core/database/dynamodb/DynamoDbAction.kt create mode 100644 core/src/main/kotlin/org/evomaster/core/database/dynamodb/DynamoDbActionResult.kt create mode 100644 core/src/main/kotlin/org/evomaster/core/database/dynamodb/DynamoDbActionTransformer.kt create mode 100644 core/src/main/kotlin/org/evomaster/core/database/dynamodb/DynamoDbExecution.kt create mode 100644 core/src/main/kotlin/org/evomaster/core/database/dynamodb/DynamoDbInsertBuilder.kt create mode 100644 core/src/test/kotlin/org/evomaster/core/database/dynamodb/DynamoDbActionTest.kt create mode 100644 core/src/test/kotlin/org/evomaster/core/database/dynamodb/DynamoDbActionTransformerTest.kt create mode 100644 core/src/test/kotlin/org/evomaster/core/database/dynamodb/DynamoDbInsertBuilderTest.kt diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/DynamoDbCommandExecutor.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/DynamoDbCommandExecutor.java new file mode 100644 index 0000000000..87aca45252 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/dynamodb/DynamoDbCommandExecutor.java @@ -0,0 +1,151 @@ +package org.evomaster.client.java.controller.dynamodb; + +import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbAttributeValueDto; +import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbInsertionDto; +import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbInsertionResultsDto; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletionStage; + +/** + * Executes DynamoDB insertions without binding the controller API to an AWS SDK version. + */ +public final class DynamoDbCommandExecutor { + + private DynamoDbCommandExecutor() { + } + + /** + * Executes insertions using a synchronous or asynchronous AWS SDK v2 client. + * + * @param client DynamoDB client + * @param insertions items to insert + * @return per-insertion results + */ + public static DynamoDbInsertionResultsDto executeInsert(Object client, List insertions) { + if (client == null) { + throw new IllegalArgumentException("No DynamoDB client"); + } + if (insertions == null || insertions.isEmpty()) { + throw new IllegalArgumentException("No data to insert"); + } + + DynamoDbInsertionResultsDto results = new DynamoDbInsertionResultsDto(); + results.executionResults = new ArrayList<>(Collections.nCopies(insertions.size(), false)); + for (int i = 0; i < insertions.size(); i++) { + try { + executeOne(client, insertions.get(i)); + results.executionResults.set(i, true); + } catch (RuntimeException e) { + results.failedInsertionIndex = i; + throw new DynamoDbInsertionException(i, results, e); + } + } + return results; + } + + private static void executeOne(Object client, DynamoDbInsertionDto insertion) { + try { + ClassLoader loader = client.getClass().getClassLoader(); + Class attributeValueClass = Class.forName( + "software.amazon.awssdk.services.dynamodb.model.AttributeValue", true, loader); + Class attributeValueBuilderClass = Class.forName( + "software.amazon.awssdk.services.dynamodb.model.AttributeValue$Builder", true, loader); + Class putItemRequestClass = Class.forName( + "software.amazon.awssdk.services.dynamodb.model.PutItemRequest", true, loader); + Class putItemRequestBuilderClass = Class.forName( + "software.amazon.awssdk.services.dynamodb.model.PutItemRequest$Builder", true, loader); + + Map item = new LinkedHashMap<>(); + for (DynamoDbAttributeValueDto attribute : insertion.attributes) { + Object builder = attributeValueClass.getMethod("builder").invoke(null); + String setter; + Object value; + switch (attribute.type) { + case S: + setter = "s"; + value = attribute.value; + break; + case N: + setter = "n"; + value = attribute.value; + break; + case BOOL: + setter = "bool"; + value = Boolean.valueOf(attribute.value); + break; + default: + throw new IllegalArgumentException("Unsupported DynamoDB attribute type: " + attribute.type); + } + attributeValueBuilderClass.getMethod(setter, value.getClass()).invoke(builder, value); + item.put(attribute.attributeName, attributeValueBuilderClass.getMethod("build").invoke(builder)); + } + + Object requestBuilder = putItemRequestClass.getMethod("builder").invoke(null); + putItemRequestBuilderClass.getMethod("tableName", String.class) + .invoke(requestBuilder, insertion.tableName); + putItemRequestBuilderClass.getMethod("item", Map.class).invoke(requestBuilder, item); + Object request = putItemRequestBuilderClass.getMethod("build").invoke(requestBuilder); + Method putItem = findPutItemMethod(client, loader, putItemRequestClass); + Object response = putItem.invoke(client, request); + if (response instanceof CompletionStage) { + ((CompletionStage) response).toCompletableFuture().join(); + } + } catch (InvocationTargetException e) { + Throwable cause = e.getCause() == null ? e : e.getCause(); + throw new RuntimeException("Failed DynamoDB insertion into table '" + insertion.tableName + "'", cause); + } catch (ReflectiveOperationException e) { + throw new RuntimeException("Failed DynamoDB insertion into table '" + insertion.tableName + "'", e); + } + } + + private static Method findPutItemMethod(Object client, ClassLoader loader, Class putItemRequestClass) + throws ClassNotFoundException, NoSuchMethodException { + Class syncClientClass = Class.forName("software.amazon.awssdk.services.dynamodb.DynamoDbClient", true, loader); + if (syncClientClass.isInstance(client)) { + return syncClientClass.getMethod("putItem", putItemRequestClass); + } + + Class asyncClientClass = Class.forName("software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient", true, loader); + if (asyncClientClass.isInstance(client)) { + return asyncClientClass.getMethod("putItem", putItemRequestClass); + } + + throw new IllegalArgumentException("Unsupported DynamoDB client: " + client.getClass().getName()); + } + + /** + * Exception carrying partial insertion results. + */ + public static class DynamoDbInsertionException extends RuntimeException { + + private final int failedIndex; + private final DynamoDbInsertionResultsDto results; + + private DynamoDbInsertionException(int failedIndex, DynamoDbInsertionResultsDto results, Throwable cause) { + super("Failed DynamoDB insertion at index " + failedIndex, cause); + this.failedIndex = failedIndex; + this.results = results; + } + + /** + * @return failed insertion index + */ + public int getFailedIndex() { + return failedIndex; + } + + /** + * @return partial results + */ + public DynamoDbInsertionResultsDto getResults() { + return results; + } + } +} diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/dynamodb/DynamoDbCommandExecutorTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/dynamodb/DynamoDbCommandExecutorTest.java new file mode 100644 index 0000000000..800091d406 --- /dev/null +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/dynamodb/DynamoDbCommandExecutorTest.java @@ -0,0 +1,99 @@ +package org.evomaster.client.java.controller.dynamodb; + +import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbAttributeValueDto; +import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbInsertionDto; +import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbInsertionResultsDto; +import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbScalarTypeDto; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; +import software.amazon.awssdk.services.dynamodb.model.PutItemResponse; + +import java.util.Arrays; +import java.util.Collections; +import java.util.concurrent.CompletableFuture; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** Tests DynamoDB item insertion through synchronous and asynchronous AWS clients. */ +public class DynamoDbCommandExecutorTest { + + @Test + public void testExecuteInsertWithSynchronousClient() { + DynamoDbClient client = mock(DynamoDbClient.class); + when(client.putItem(any(PutItemRequest.class))).thenReturn(PutItemResponse.builder().build()); + + DynamoDbInsertionResultsDto results = DynamoDbCommandExecutor.executeInsert( + client, Collections.singletonList(worldCupPlayer())); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(PutItemRequest.class); + verify(client).putItem(requestCaptor.capture()); + PutItemRequest request = requestCaptor.getValue(); + assertEquals("WorldCupPlayers", request.tableName()); + assertEquals("Argentina", request.item().get("country").s()); + assertEquals("10", request.item().get("fifaId").n()); + assertTrue(request.item().get("captain").bool()); + assertEquals(Collections.singletonList(true), results.executionResults); + assertNull(results.failedInsertionIndex); + } + + @Test + public void testExecuteInsertWithAsynchronousClient() { + DynamoDbAsyncClient client = mock(DynamoDbAsyncClient.class); + when(client.putItem(any(PutItemRequest.class))).thenReturn( + CompletableFuture.completedFuture(PutItemResponse.builder().build())); + + DynamoDbInsertionResultsDto results = DynamoDbCommandExecutor.executeInsert( + client, Collections.singletonList(worldCupPlayer())); + + verify(client).putItem(any(PutItemRequest.class)); + assertEquals(Collections.singletonList(true), results.executionResults); + } + + @Test + public void testFailureContainsPartialResults() { + DynamoDbClient client = mock(DynamoDbClient.class); + when(client.putItem(any(PutItemRequest.class))) + .thenReturn(PutItemResponse.builder().build()) + .thenThrow(new IllegalStateException("DynamoDB unavailable")); + + DynamoDbCommandExecutor.DynamoDbInsertionException error = assertThrows( + DynamoDbCommandExecutor.DynamoDbInsertionException.class, + () -> DynamoDbCommandExecutor.executeInsert( + client, Arrays.asList(worldCupPlayer(), worldCupPlayer()))); + + assertEquals(1, error.getFailedIndex()); + assertEquals(Arrays.asList(true, false), error.getResults().executionResults); + assertEquals(Integer.valueOf(1), error.getResults().failedInsertionIndex); + verify(client, times(2)).putItem(any(PutItemRequest.class)); + } + + @Test + public void testRejectsMissingClientOrInsertions() { + DynamoDbClient client = mock(DynamoDbClient.class); + + assertThrows(IllegalArgumentException.class, + () -> DynamoDbCommandExecutor.executeInsert(null, Collections.singletonList(worldCupPlayer()))); + assertThrows(IllegalArgumentException.class, + () -> DynamoDbCommandExecutor.executeInsert(client, null)); + assertThrows(IllegalArgumentException.class, + () -> DynamoDbCommandExecutor.executeInsert(client, Collections.emptyList())); + verify(client, times(0)).putItem(any(PutItemRequest.class)); + } + + private DynamoDbInsertionDto worldCupPlayer() { + DynamoDbInsertionDto insertion = new DynamoDbInsertionDto(); + insertion.tableName = "WorldCupPlayers"; + insertion.attributes.add(new DynamoDbAttributeValueDto("country", DynamoDbScalarTypeDto.S, "Argentina")); + insertion.attributes.add(new DynamoDbAttributeValueDto("fifaId", DynamoDbScalarTypeDto.N, "10")); + insertion.attributes.add(new DynamoDbAttributeValueDto("captain", DynamoDbScalarTypeDto.BOOL, "true")); + return insertion; + } +} diff --git a/core/src/main/kotlin/org/evomaster/core/database/dynamodb/DynamoDbAction.kt b/core/src/main/kotlin/org/evomaster/core/database/dynamodb/DynamoDbAction.kt new file mode 100644 index 0000000000..8dc307396b --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/database/dynamodb/DynamoDbAction.kt @@ -0,0 +1,59 @@ +package org.evomaster.core.database.dynamodb + +import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbScalarTypeDto +import org.evomaster.core.search.action.Action +import org.evomaster.core.search.action.EnvironmentAction +import org.evomaster.core.search.gene.Gene + +/** + * A typed attribute gene belonging to a DynamoDB item. + * + * @property attributeName name of the DynamoDB item attribute + * @property type supported DynamoDB scalar type + * @property gene evolvable value for the attribute + */ +data class DynamoDbAttributeGene( + val attributeName: String, + val type: DynamoDbScalarTypeDto, + val gene: Gene +) + +/** + * An initialization action that inserts one DynamoDB item. + * + * @property tableName target DynamoDB table + * @property attributes item attributes to insert + */ +class DynamoDbAction( + val tableName: String, + val attributes: List +) : EnvironmentAction(listOf()) { + + init { + addChildren(attributes.map { it.gene }) + } + + /** Returns the genes that determine the inserted item values. */ + override fun seeTopGenes(): List = attributes.map { it.gene } + + /** Creates an independent action with copies of all attribute genes. */ + override fun copyContent(): Action = DynamoDbAction( + tableName, + attributes.map { DynamoDbAttributeGene(it.attributeName, it.type, it.gene.copy()) } + ) + + /** Returns the descriptive name of this insertion action. */ + override fun getName(): String = "DynamoDB_INSERT_$tableName" + + /** Returns the grouping key for DynamoDB initialization actions. */ + override fun getActionGroupKey(): String = DynamoDbAction::class.java.name + + /** Stable key used to avoid adding the same inferred insertion twice. */ + fun insertionKey(): String = buildString { + append(tableName) + attributes.forEach { + append('|').append(it.attributeName).append(':').append(it.type) + .append('=').append(it.gene.getValueAsRawString()) + } + } +} diff --git a/core/src/main/kotlin/org/evomaster/core/database/dynamodb/DynamoDbActionResult.kt b/core/src/main/kotlin/org/evomaster/core/database/dynamodb/DynamoDbActionResult.kt new file mode 100644 index 0000000000..48634f2d89 --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/database/dynamodb/DynamoDbActionResult.kt @@ -0,0 +1,32 @@ +package org.evomaster.core.database.dynamodb + +import org.evomaster.core.search.action.Action +import org.evomaster.core.search.action.ActionResult + +/** Result of executing a [DynamoDbAction]. */ +class DynamoDbActionResult : ActionResult { + + /** Creates a result for the action identified by [sourceLocalId]. */ + constructor(sourceLocalId: String, stopping: Boolean = false) : super(sourceLocalId, stopping) + + /** Creates a copy of another DynamoDB action result. */ + constructor(other: DynamoDbActionResult) : super(other) + + companion object { + const val INSERT_DYNAMODB_EXECUTE_SUCCESSFULLY = "INSERT_DYNAMODB_EXECUTE_SUCCESSFULLY" + } + + /** Creates an independent copy of this result. */ + override fun copy(): DynamoDbActionResult = DynamoDbActionResult(this) + + /** Records whether the insertion completed successfully. */ + fun setInsertExecutionResult(success: Boolean) = + addResultValue(INSERT_DYNAMODB_EXECUTE_SUCCESSFULLY, success.toString()) + + /** Returns whether the insertion completed successfully. */ + fun getInsertExecutionResult(): Boolean = + getResultValue(INSERT_DYNAMODB_EXECUTE_SUCCESSFULLY)?.toBoolean() ?: false + + /** Returns whether [action] is a DynamoDB insertion action. */ + override fun matchedType(action: Action): Boolean = action is DynamoDbAction +} diff --git a/core/src/main/kotlin/org/evomaster/core/database/dynamodb/DynamoDbActionTransformer.kt b/core/src/main/kotlin/org/evomaster/core/database/dynamodb/DynamoDbActionTransformer.kt new file mode 100644 index 0000000000..c01a6f81a6 --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/database/dynamodb/DynamoDbActionTransformer.kt @@ -0,0 +1,34 @@ +package org.evomaster.core.database.dynamodb + +import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbAttributeValueDto +import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbDatabaseCommandsDto +import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbInsertionDto +import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbScalarTypeDto +import org.evomaster.core.search.gene.BooleanGene +import org.evomaster.core.search.gene.numeric.BigDecimalGene +import org.evomaster.core.search.gene.string.StringGene + +/** Transforms DynamoDB actions into controller insertion commands. */ +object DynamoDbActionTransformer { + + /** Converts initialization actions to the controller's DynamoDB insertion DTO. */ + fun transform(actions: List): DynamoDbDatabaseCommandsDto = + DynamoDbDatabaseCommandsDto().also { commands -> + commands.insertions = actions.map { action -> + DynamoDbInsertionDto().also { insertion -> + insertion.tableName = action.tableName + insertion.attributes = action.attributes.map { attribute -> + DynamoDbAttributeValueDto( + attribute.attributeName, + attribute.type, + when (attribute.type) { + DynamoDbScalarTypeDto.S -> (attribute.gene as StringGene).value + DynamoDbScalarTypeDto.N -> (attribute.gene as BigDecimalGene).value.toPlainString() + DynamoDbScalarTypeDto.BOOL -> (attribute.gene as BooleanGene).value.toString() + } + ) + } + } + } + } +} diff --git a/core/src/main/kotlin/org/evomaster/core/database/dynamodb/DynamoDbExecution.kt b/core/src/main/kotlin/org/evomaster/core/database/dynamodb/DynamoDbExecution.kt new file mode 100644 index 0000000000..8c684d54ac --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/database/dynamodb/DynamoDbExecution.kt @@ -0,0 +1,15 @@ +package org.evomaster.core.database.dynamodb + +import org.evomaster.client.java.controller.api.dto.database.execution.DynamoDbExecutionsDto +import org.evomaster.client.java.controller.api.dto.database.execution.DynamoDbFailedQuery + +/** Failed DynamoDB reads observed during one action. */ +class DynamoDbExecution(val failedQueries: List) { + + companion object { + + /** Creates an execution view from the controller response, handling a missing response. */ + fun fromDto(dto: DynamoDbExecutionsDto?): DynamoDbExecution = + DynamoDbExecution(dto?.failedQueries ?: emptyList()) + } +} diff --git a/core/src/main/kotlin/org/evomaster/core/database/dynamodb/DynamoDbInsertBuilder.kt b/core/src/main/kotlin/org/evomaster/core/database/dynamodb/DynamoDbInsertBuilder.kt new file mode 100644 index 0000000000..1f61f61ca6 --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/database/dynamodb/DynamoDbInsertBuilder.kt @@ -0,0 +1,55 @@ +package org.evomaster.core.database.dynamodb + +import org.evomaster.client.java.controller.api.dto.database.execution.DynamoDbFailedQuery +import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbAttributeValueDto +import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbScalarTypeDto +import org.evomaster.core.search.gene.BooleanGene +import org.evomaster.core.search.gene.numeric.BigDecimalGene +import org.evomaster.core.search.gene.string.StringGene + +/** Builds evolvable DynamoDB insertion actions from failed equality reads. */ +object DynamoDbInsertBuilder { + + /** + * Builds unique, supported DynamoDB insertion actions from failed equality reads. + * + * @param failedQueries failed reads reported by the controller + * @param existingInsertionKeys keys of insertions that have already been added to the individual + * @return inferred actions not already represented by [existingInsertionKeys] + */ + fun buildInsertActions( + failedQueries: List, + existingInsertionKeys: Set + ): List = failedQueries + .mapNotNull(::toActionOrNull) + .filterNot { it.insertionKey() in existingInsertionKeys } + .distinctBy { it.insertionKey() } + + /** Converts one failed read into an insertion action, or returns null when it is incomplete. */ + private fun toActionOrNull(query: DynamoDbFailedQuery): DynamoDbAction? { + val tableName = query.tableName + val queryAttributes = query.attributes + if (tableName.isNullOrBlank() || queryAttributes.isNullOrEmpty()) return null + + val attributes = queryAttributes.map { attribute -> + toAttributeOrNull(attribute) ?: return null + } + + return DynamoDbAction(tableName, attributes) + } + + /** Converts a supported scalar DynamoDB attribute into its evolvable representation. */ + private fun toAttributeOrNull(attribute: DynamoDbAttributeValueDto): DynamoDbAttributeGene? { + val type = attribute.type ?: return null + val value = attribute.value ?: return null + val gene = when (type) { + DynamoDbScalarTypeDto.S -> StringGene(attribute.attributeName, value) + DynamoDbScalarTypeDto.N -> value.toBigDecimalOrNull()?.let { + BigDecimalGene(attribute.attributeName, it) + } ?: return null + DynamoDbScalarTypeDto.BOOL -> BooleanGene(attribute.attributeName, value.toBoolean()) + } + + return DynamoDbAttributeGene(attribute.attributeName, type, gene) + } +} diff --git a/core/src/test/kotlin/org/evomaster/core/database/dynamodb/DynamoDbActionTest.kt b/core/src/test/kotlin/org/evomaster/core/database/dynamodb/DynamoDbActionTest.kt new file mode 100644 index 0000000000..a5e40694b8 --- /dev/null +++ b/core/src/test/kotlin/org/evomaster/core/database/dynamodb/DynamoDbActionTest.kt @@ -0,0 +1,65 @@ +package org.evomaster.core.database.dynamodb + +import org.evomaster.client.java.controller.api.dto.database.execution.DynamoDbExecutionsDto +import org.evomaster.client.java.controller.api.dto.database.execution.DynamoDbFailedQuery +import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbAttributeValueDto +import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbScalarTypeDto +import org.evomaster.core.search.gene.string.StringGene +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNotSame +import org.junit.jupiter.api.Assertions.assertSame +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +/** Tests the DynamoDB initialization action and its execution result. */ +class DynamoDbActionTest { + + @Test + fun actionExposesStableMetadataAndCopiesGenesIndependently() { + val original = DynamoDbAction( + "WorldCupPlayers", + listOf(DynamoDbAttributeGene("country", DynamoDbScalarTypeDto.S, StringGene("country", "Argentina"))) + ) + + val copy = original.copy() as DynamoDbAction + (copy.attributes.single().gene as StringGene).value = "Brazil" + + assertEquals("DynamoDB_INSERT_WorldCupPlayers", original.getName()) + assertEquals(DynamoDbAction::class.java.name, original.getActionGroupKey()) + assertEquals("WorldCupPlayers|country:S=Argentina", original.insertionKey()) + assertSame(original.attributes.single().gene, original.seeTopGenes().single()) + assertNotSame(original.attributes.single().gene, copy.attributes.single().gene) + assertEquals("Argentina", (original.attributes.single().gene as StringGene).value) + assertEquals("Brazil", (copy.attributes.single().gene as StringGene).value) + } + + @Test + fun actionResultTracksInsertionOutcomeAndMatchesDynamoDbActions() { + val action = DynamoDbAction( + "WorldCupPlayers", + listOf(DynamoDbAttributeGene("country", DynamoDbScalarTypeDto.S, StringGene("country", "Argentina"))) + ) + val result = DynamoDbActionResult("source") + + assertFalse(result.getInsertExecutionResult()) + result.setInsertExecutionResult(true) + + assertTrue(result.getInsertExecutionResult()) + assertTrue(result.matchedType(action)) + assertTrue(result.copy().getInsertExecutionResult()) + } + + @Test + fun executionPreservesFailedQueriesAndAcceptsMissingDto() { + val query = DynamoDbFailedQuery( + "WorldCupPlayers", + listOf(DynamoDbAttributeValueDto("country", DynamoDbScalarTypeDto.S, "Argentina")) + ) + val dto = DynamoDbExecutionsDto() + dto.failedQueries.add(query) + + assertSame(query, DynamoDbExecution.fromDto(dto).failedQueries.single()) + assertTrue(DynamoDbExecution.fromDto(null).failedQueries.isEmpty()) + } +} diff --git a/core/src/test/kotlin/org/evomaster/core/database/dynamodb/DynamoDbActionTransformerTest.kt b/core/src/test/kotlin/org/evomaster/core/database/dynamodb/DynamoDbActionTransformerTest.kt new file mode 100644 index 0000000000..53137d3c1f --- /dev/null +++ b/core/src/test/kotlin/org/evomaster/core/database/dynamodb/DynamoDbActionTransformerTest.kt @@ -0,0 +1,42 @@ +package org.evomaster.core.database.dynamodb + +import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbScalarTypeDto +import org.evomaster.core.search.gene.BooleanGene +import org.evomaster.core.search.gene.numeric.BigDecimalGene +import org.evomaster.core.search.gene.string.StringGene +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import java.math.BigDecimal + +/** Tests conversion of DynamoDB initialization actions to controller DTOs. */ +class DynamoDbActionTransformerTest { + + @Test + fun transformsAllSupportedScalarTypes() { + val action = DynamoDbAction( + "WorldCupPlayers", + listOf( + DynamoDbAttributeGene("country", DynamoDbScalarTypeDto.S, StringGene("country", "Argentina")), + DynamoDbAttributeGene("fifaId", DynamoDbScalarTypeDto.N, BigDecimalGene("fifaId", BigDecimal("10.50"))), + DynamoDbAttributeGene("captain", DynamoDbScalarTypeDto.BOOL, BooleanGene("captain", true)) + ) + ) + + val insertion = DynamoDbActionTransformer.transform(listOf(action)).insertions.single() + + assertEquals("WorldCupPlayers", insertion.tableName) + assertEquals("Argentina", insertion.attributes[0].value) + assertEquals("10.50", insertion.attributes[1].value) + assertEquals("true", insertion.attributes[2].value) + assertEquals( + listOf(DynamoDbScalarTypeDto.S, DynamoDbScalarTypeDto.N, DynamoDbScalarTypeDto.BOOL), + insertion.attributes.map { it.type } + ) + } + + @Test + fun transformsAnEmptyActionList() { + assertTrue(DynamoDbActionTransformer.transform(emptyList()).insertions.isEmpty()) + } +} diff --git a/core/src/test/kotlin/org/evomaster/core/database/dynamodb/DynamoDbInsertBuilderTest.kt b/core/src/test/kotlin/org/evomaster/core/database/dynamodb/DynamoDbInsertBuilderTest.kt new file mode 100644 index 0000000000..d936f84651 --- /dev/null +++ b/core/src/test/kotlin/org/evomaster/core/database/dynamodb/DynamoDbInsertBuilderTest.kt @@ -0,0 +1,63 @@ +package org.evomaster.core.database.dynamodb + +import org.evomaster.client.java.controller.api.dto.database.execution.DynamoDbFailedQuery +import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbAttributeValueDto +import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbScalarTypeDto +import org.evomaster.core.search.gene.BooleanGene +import org.evomaster.core.search.gene.numeric.BigDecimalGene +import org.evomaster.core.search.gene.string.StringGene +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +/** Tests inference of DynamoDB initialization actions from failed reads. */ +class DynamoDbInsertBuilderTest { + + @Test + fun buildsTypedActionFromSupportedFailedQuery() { + val actions = DynamoDbInsertBuilder.buildInsertActions(listOf(validQuery()), emptySet()) + + assertEquals(1, actions.size) + val attributes = actions.single().attributes + assertEquals("WorldCupPlayers", actions.single().tableName) + assertEquals("Argentina", (attributes[0].gene as StringGene).value) + assertEquals("10.50", (attributes[1].gene as BigDecimalGene).value.toPlainString()) + assertTrue((attributes[2].gene as BooleanGene).value) + } + + @Test + fun skipsInvalidQueriesAndRemovesDuplicateActions() { + val valid = validQuery() + val invalidNumber = DynamoDbFailedQuery( + "WorldCupPlayers", + listOf(DynamoDbAttributeValueDto("fifaId", DynamoDbScalarTypeDto.N, "ten")) + ) + val blankTable = DynamoDbFailedQuery( + "", + listOf(DynamoDbAttributeValueDto("country", DynamoDbScalarTypeDto.S, "Argentina")) + ) + val missingType = DynamoDbFailedQuery( + "WorldCupPlayers", + listOf(DynamoDbAttributeValueDto("country", null, "Argentina")) + ) + + val actions = DynamoDbInsertBuilder.buildInsertActions( + listOf(valid, valid, invalidNumber, blankTable, missingType), + emptySet() + ) + + assertEquals(1, actions.size) + assertTrue( + DynamoDbInsertBuilder.buildInsertActions(listOf(valid), setOf(actions.single().insertionKey())).isEmpty() + ) + } + + private fun validQuery(): DynamoDbFailedQuery = DynamoDbFailedQuery( + "WorldCupPlayers", + listOf( + DynamoDbAttributeValueDto("country", DynamoDbScalarTypeDto.S, "Argentina"), + DynamoDbAttributeValueDto("fifaId", DynamoDbScalarTypeDto.N, "10.50"), + DynamoDbAttributeValueDto("captain", DynamoDbScalarTypeDto.BOOL, "true") + ) + ) +}