diff --git a/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/ExtraHeuristicsDto.java b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/ExtraHeuristicsDto.java index 3bd2ccf5d0..33866eb705 100644 --- a/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/ExtraHeuristicsDto.java +++ b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/ExtraHeuristicsDto.java @@ -1,6 +1,7 @@ package org.evomaster.client.java.controller.api.dto; import org.evomaster.client.java.controller.api.dto.database.execution.RedisExecutionsDto; +import org.evomaster.client.java.controller.api.dto.database.execution.DynamoDbExecutionsDto; import org.evomaster.client.java.controller.api.dto.database.execution.SqlExecutionsDto; import org.evomaster.client.java.controller.api.dto.database.execution.MongoExecutionsDto; import java.util.ArrayList; @@ -9,7 +10,6 @@ /** * Represents possible extra heuristics related to the code * execution and that do apply to all the reached testing targets. - * * Example: rewarding SQL "select" operations that return non-empty sets */ public class ExtraHeuristicsDto { @@ -24,4 +24,6 @@ public class ExtraHeuristicsDto { public MongoExecutionsDto mongoExecutionsDto; public RedisExecutionsDto redisExecutionsDto; + + public DynamoDbExecutionsDto dynamoDbExecutionsDto; } diff --git a/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/operations/DynamoDbInsertionKeyBuilder.java b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/operations/DynamoDbInsertionKeyBuilder.java new file mode 100644 index 0000000000..1cebad83ff --- /dev/null +++ b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/operations/DynamoDbInsertionKeyBuilder.java @@ -0,0 +1,31 @@ +package org.evomaster.client.java.controller.api.dto.database.operations; + +import java.util.List; + +/** + * Builds stable identifiers for inferred DynamoDB insertions. + */ +public final class DynamoDbInsertionKeyBuilder { + + /** + * Prevents instantiation of this utility class. + */ + private DynamoDbInsertionKeyBuilder() { + } + + /** + * Builds the insertion key from a table and its ordered scalar attributes. + * + * @param tableName target DynamoDB table + * @param attributes ordered attributes in the insertion + * @return stable key in the {@code table|name:type=value} format + */ + public static String fromAttributes(String tableName, List attributes) { + StringBuilder insertionKey = new StringBuilder(tableName); + for (DynamoDbAttributeValueDto attribute : attributes) { + insertionKey.append('|').append(attribute.attributeName) + .append(':').append(attribute.type).append('=').append(attribute.value); + } + return insertionKey.toString(); + } +} diff --git a/client-java/controller-api/src/test/java/org/evomaster/client/java/controller/api/dto/database/operations/DynamoDbInsertionKeyBuilderTest.java b/client-java/controller-api/src/test/java/org/evomaster/client/java/controller/api/dto/database/operations/DynamoDbInsertionKeyBuilderTest.java new file mode 100644 index 0000000000..cc80204bc6 --- /dev/null +++ b/client-java/controller-api/src/test/java/org/evomaster/client/java/controller/api/dto/database/operations/DynamoDbInsertionKeyBuilderTest.java @@ -0,0 +1,23 @@ +package org.evomaster.client.java.controller.api.dto.database.operations; + +import org.junit.jupiter.api.Test; + +import java.util.Arrays; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Tests the canonical representation of inferred DynamoDB insertion keys. + */ +public class DynamoDbInsertionKeyBuilderTest { + + @Test + public void testBuildsWorldCupPlayerInsertionKey() { + assertEquals("WorldCupPlayers|country:STRING=Argentina|fifaId:NUMBER=10|captain:BOOLEAN=true", + DynamoDbInsertionKeyBuilder.fromAttributes("WorldCupPlayers", Arrays.asList( + new DynamoDbAttributeValueDto("country", DynamoDbScalarTypeDto.STRING, "Argentina"), + new DynamoDbAttributeValueDto("fifaId", DynamoDbScalarTypeDto.NUMBER, "10"), + new DynamoDbAttributeValueDto("captain", DynamoDbScalarTypeDto.BOOLEAN, "true") + ))); + } +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/internal/db/dynamodb/DynamoDbHandler.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/internal/db/dynamodb/DynamoDbHandler.java index 439cb9b60c..8d7fd19333 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/internal/db/dynamodb/DynamoDbHandler.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/internal/db/dynamodb/DynamoDbHandler.java @@ -1,30 +1,63 @@ package org.evomaster.client.java.controller.internal.db.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.DynamoDbInsertionKeyBuilder; +import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbScalarTypeDto; import org.evomaster.client.java.controller.dynamodb.DynamoDbRequestParser; import org.evomaster.client.java.controller.dynamodb.ParsedDynamoDbRequest; +import org.evomaster.client.java.controller.dynamodb.operations.AndOperation; +import org.evomaster.client.java.controller.dynamodb.operations.QueryOperation; +import org.evomaster.client.java.controller.dynamodb.operations.comparison.EqualsOperation; import org.evomaster.client.java.controller.internal.TaintHandlerExecutionTracer; import org.evomaster.client.java.instrumentation.DynamoDbCommand; +import org.evomaster.client.java.instrumentation.DynamoDbOperationNames; import org.evomaster.client.java.utils.SimpleLogger; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Set; /** * Processes DynamoDB commands captured from the SUT and computes database heuristics for them. */ public class DynamoDbHandler { + /** Commands captured from the SUT and awaiting evaluation. */ private final List commands = new ArrayList<>(); + + /** Heuristic results accumulated for the current action. */ private final List evaluatedCommands = new ArrayList<>(); + + /** Failed reads accumulated for insertion generation during the current action. */ + private final List failedQueries = new ArrayList<>(); + + /** Canonical keys used to avoid reporting duplicate inferred insertions. */ + private final Set insertionKeys = new LinkedHashSet<>(); + + /** Parser used to extract predicates and table names from captured requests. */ private final DynamoDbRequestParser requestParser = new DynamoDbRequestParser(); + + /** Calculator used to measure how close table items are to satisfying a request. */ private final DynamoDbHeuristicsCalculator calculator = new DynamoDbHeuristicsCalculator(new TaintHandlerExecutionTracer()); + + /** Accessor used to load table items through the configured DynamoDB client. */ private final DynamoDbTableDataAccessor tableDataAccessor = new DynamoDbTableDataAccessor(); + /** Whether captured commands should produce heuristic results. */ private volatile boolean calculateHeuristics; + + /** Whether failed reads should be captured for insertion generation. */ + private volatile boolean extractDynamoDbExecution; + + /** SDK v2 synchronous or asynchronous DynamoDB client supplied by the SUT controller. */ private Object dynamoDbClient; /** @@ -32,6 +65,7 @@ public class DynamoDbHandler { */ public DynamoDbHandler() { calculateHeuristics = true; + extractDynamoDbExecution = true; } /** @@ -40,6 +74,8 @@ public DynamoDbHandler() { public void reset() { commands.clear(); evaluatedCommands.clear(); + failedQueries.clear(); + insertionKeys.clear(); } /** @@ -58,6 +94,22 @@ public void setCalculateHeuristics(boolean calculateHeuristics) { this.calculateHeuristics = calculateHeuristics; } + /** + * @return whether failed DynamoDB reads are extracted + */ + public boolean isExtractDynamoDbExecution() { + return extractDynamoDbExecution; + } + + /** + * Enables or disables extraction of failed DynamoDB reads. + * + * @param extractDynamoDbExecution new extraction state + */ + public void setExtractDynamoDbExecution(boolean extractDynamoDbExecution) { + this.extractDynamoDbExecution = extractDynamoDbExecution; + } + /** * Sets the SDK v2 client used to read table contents. * @@ -84,7 +136,7 @@ public void handle(DynamoDbCommand command) { * @return evaluated commands for the current action */ public List getEvaluatedDynamoDbCommands() { - if (!calculateHeuristics) { + if (!calculateHeuristics && !extractDynamoDbExecution) { commands.clear(); return Collections.emptyList(); } @@ -97,6 +149,15 @@ public List getEvaluatedDynamoDbCommands() { return new ArrayList<>(evaluatedCommands); } + /** + * @return failed reads captured for the current action + */ + public DynamoDbExecutionsDto getExecutionDto() { + DynamoDbExecutionsDto dto = new DynamoDbExecutionsDto(); + dto.failedQueries = new ArrayList<>(failedQueries); + return dto; + } + /** * Evaluates one successfully executed command and reuses table scans within the current batch. * @@ -131,14 +192,123 @@ private void evaluateCommand(DynamoDbCommand command, Map 0.0d) { + registerFailedQuery(command, tableName, parsed); + } } catch (RuntimeException e) { registerFailure(command, tableName, e); } } } + /** + * Registers one positive-distance DynamoDB read when its conditions can define an insertion item. + * + * @param command intercepted DynamoDB read + * @param tableName table read by the command + * @param parsed parsed request conditions + */ + private void registerFailedQuery(DynamoDbCommand command, String tableName, ParsedDynamoDbRequest parsed) { + if (command.getOperationName() != DynamoDbOperationNames.GET_ITEM + && command.getOperationName() != DynamoDbOperationNames.QUERY) { + return; + } + + Map attributes = new LinkedHashMap<>(); + if (!evaluateEqualitiesAsFlatAttributes(parsed.getKeyCondition(), attributes) + || !evaluateEqualitiesAsFlatAttributes(parsed.getFilterExpression(), attributes) + || attributes.isEmpty()) { + return; + } + + List insertionAttributes = new ArrayList<>(attributes.values()); + String insertionKey = DynamoDbInsertionKeyBuilder.fromAttributes(tableName, insertionAttributes); + if (insertionKeys.add(insertionKey)) { + failedQueries.add(new DynamoDbFailedQuery(tableName, insertionAttributes)); + } + } + + /** + * Evaluates whether a condition can be represented as flat scalar insertion attributes. + *

+ * The condition must be {@code null}, a conjunction, or a top-level equality whose value is a string, + * number, or boolean. Equalities are iteratively flattened into {@code attributes}; the method returns + * {@code false} for unsupported predicates, nested document paths, or conflicting values for one attribute. + * For example, the resolved condition + * {@code country = "Argentina" AND (fifaId = 10 AND captain = true)} produces the flat attributes + * {@code country -> (STRING, Argentina)}, {@code fifaId -> (NUMBER, 10)}, and + * {@code captain -> (BOOLEAN, true)}. + * A syntactically supported condition can still fail: {@code country = "Argentina" AND country = "Brazil"} + * returns {@code false}, because one flat insertion item cannot assign two different values to {@code country}. + * + * @param operation condition to evaluate + * @param attributes inferred insertion attributes, populated when the condition is supported + * @return {@code true} if the condition is representable as flat scalar attributes + */ + private boolean evaluateEqualitiesAsFlatAttributes( + QueryOperation operation, + Map attributes) { + if (operation == null) { + return true; + } + + List pending = new ArrayList<>(); + pending.add(operation); + while (!pending.isEmpty()) { + QueryOperation current = pending.remove(pending.size() - 1); + if (current instanceof AndOperation) { + List conditions = ((AndOperation) current).getConditions(); + for (int i = conditions.size() - 1; i >= 0; i--) { + pending.add(conditions.get(i)); + } + continue; + } + if (!(current instanceof EqualsOperation)) { + return false; + } + + EqualsOperation equality = (EqualsOperation) current; + String name = equality.getFieldName(); + if (name == null || name.isEmpty() || name.contains(".") || name.contains("[")) { + return false; + } + DynamoDbAttributeValueDto attribute = toAttribute(name, equality.getValue()); + if (attribute == null) { + return false; + } + DynamoDbAttributeValueDto existing = attributes.get(name); + if (existing != null && (existing.type != attribute.type || !existing.value.equals(attribute.value))) { + return false; + } + attributes.put(name, attribute); + } + return true; + } + + /** + * Converts a supported scalar equality value to the DTO used for a DynamoDB insertion attribute. + * + * @param name attribute name + * @param value equality value + * @return the corresponding scalar attribute, or {@code null} when the value is unsupported + */ + private DynamoDbAttributeValueDto toAttribute(String name, Object value) { + if (value instanceof String) { + return new DynamoDbAttributeValueDto(name, DynamoDbScalarTypeDto.STRING, (String) value); + } + if (value instanceof Number) { + return new DynamoDbAttributeValueDto(name, DynamoDbScalarTypeDto.NUMBER, String.valueOf(value)); + } + if (value instanceof Boolean) { + return new DynamoDbAttributeValueDto(name, DynamoDbScalarTypeDto.BOOLEAN, String.valueOf(value)); + } + return null; + } + /** * Records a failed evaluation for every table referenced by a command. * diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/internal/db/dynamodb/DynamoDbHandlerTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/internal/db/dynamodb/DynamoDbHandlerTest.java index 6625010c91..f4a08be5fe 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/internal/db/dynamodb/DynamoDbHandlerTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/internal/db/dynamodb/DynamoDbHandlerTest.java @@ -1,5 +1,9 @@ package org.evomaster.client.java.controller.internal.db.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.client.java.instrumentation.DynamoDbCommand; import org.evomaster.client.java.instrumentation.DynamoDbOperationNames; import org.junit.jupiter.api.Test; @@ -101,6 +105,43 @@ public void testDisabledAndReset() { assertTrue(handler.getEvaluatedDynamoDbCommands().isEmpty()); } + @Test + public void testExtractsAndDeduplicatesNestedWorldCupPlayerQueries() { + DynamoDbHandler handler = enabledHandler(new SyncDynamoDbClient()); + handler.handle(nestedQueryCommand()); + handler.handle(nestedQueryCommand()); + + handler.getEvaluatedDynamoDbCommands(); + DynamoDbExecutionsDto execution = handler.getExecutionDto(); + + assertEquals(1, execution.failedQueries.size()); + DynamoDbFailedQuery failedQuery = execution.failedQueries.get(0); + assertEquals(TABLE, failedQuery.tableName); + assertAttribute(failedQuery.attributes, "country", DynamoDbScalarTypeDto.STRING, "Argentina"); + assertAttribute(failedQuery.attributes, "playerName", DynamoDbScalarTypeDto.STRING, "Lionel Scaloni"); + assertAttribute(failedQuery.attributes, "fifaId", DynamoDbScalarTypeDto.NUMBER, "10"); + assertAttribute(failedQuery.attributes, "captain", DynamoDbScalarTypeDto.BOOLEAN, "true"); + + handler.reset(); + assertTrue(handler.getExecutionDto().failedQueries.isEmpty()); + } + + @Test + public void testExtractionCanRunWithoutHeuristicCollection() { + DynamoDbHandler handler = enabledHandler(new SyncDynamoDbClient()); + handler.setCalculateHeuristics(false); + handler.handle(queryCommand("Lionel Scaloni")); + + assertTrue(handler.getEvaluatedDynamoDbCommands().isEmpty()); + assertEquals(1, handler.getExecutionDto().failedQueries.size()); + + handler.reset(); + handler.setExtractDynamoDbExecution(false); + handler.handle(queryCommand("Lionel Scaloni")); + handler.getEvaluatedDynamoDbCommands(); + assertTrue(handler.getExecutionDto().failedQueries.isEmpty()); + } + private DynamoDbHandler enabledHandler(Object client) { DynamoDbHandler handler = new DynamoDbHandler(); handler.setCalculateHeuristics(true); @@ -122,6 +163,35 @@ private DynamoDbCommand queryCommand(String playerName) { request, true, 1L); } + private DynamoDbCommand nestedQueryCommand() { + Map values = new HashMap<>(); + values.put(":country", AttributeValue.builder().s("Argentina").build()); + values.put(":player", AttributeValue.builder().s("Lionel Scaloni").build()); + values.put(":fifaId", AttributeValue.builder().n("10").build()); + values.put(":captain", AttributeValue.builder().bool(true).build()); + QueryRequest request = QueryRequest.builder() + .tableName(TABLE) + .keyConditionExpression("country = :country") + .filterExpression("playerName = :player AND (fifaId = :fifaId AND captain = :captain)") + .expressionAttributeValues(values) + .build(); + return new DynamoDbCommand(Collections.singletonList(TABLE), DynamoDbOperationNames.QUERY, + request, true, 1L); + } + + private void assertAttribute( + List attributes, + String name, + DynamoDbScalarTypeDto type, + String value) { + DynamoDbAttributeValueDto attribute = attributes.stream() + .filter(candidate -> name.equals(candidate.attributeName)) + .findFirst() + .orElseThrow(() -> new AssertionError("Missing attribute " + name)); + assertEquals(type, attribute.type); + assertEquals(value, attribute.value); + } + private static Map item(String country, String playerName) { Map item = new HashMap<>(); item.put("country", AttributeValue.builder().s(country).build()); @@ -154,7 +224,7 @@ public ScanResponse scan(ScanRequest request) { } return ScanResponse.builder() .items(Collections.singletonList(item("Argentina", "Lionel Messi"))) - .lastEvaluatedKey(Collections.emptyMap()) + .lastEvaluatedKey(Collections.emptyMap()) .build(); } } diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/InstrumentationController.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/InstrumentationController.java index 518a81be48..49c1d94b44 100644 --- a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/InstrumentationController.java +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/InstrumentationController.java @@ -64,6 +64,10 @@ public static void setExecutingInitRedis(boolean executingInitRedis){ ExecutionTracer.setExecutingInitRedis(executingInitRedis); } + public static void setExecutingInitDynamoDb(boolean executingInitDynamoDb){ + ExecutionTracer.setExecutingInitDynamoDB(executingInitDynamoDb); + } + public static void setExecutingAction(boolean executingAction){ ExecutionTracer.setExecutingAction(executingAction); } diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/external/AgentController.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/external/AgentController.java index 79aa3b7ff2..a9b728bf2c 100644 --- a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/external/AgentController.java +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/external/AgentController.java @@ -102,6 +102,10 @@ public static void start(int port){ handleExecutingInitRedis(); sendCommand(Command.ACK); break; + case EXECUTING_INIT_DYNAMODB: + handleExecutingInitDynamoDb(); + sendCommand(Command.ACK); + break; case EXECUTING_ACTION: handleExecutingAction(); sendCommand(Command.ACK); @@ -208,6 +212,15 @@ private static void handleExecutingInitRedis() { } } + private static void handleExecutingInitDynamoDb() { + try { + Object msg = in.readObject(); + InstrumentationController.setExecutingInitDynamoDb((Boolean) msg); + } catch (Exception e){ + SimpleLogger.error("Failure in handling executing-init-dynamodb: " + e.getMessage()); + } + } + private static void handleExecutingAction() { try { Object msg = in.readObject(); diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/external/Command.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/external/Command.java index f7a7ba2ac6..892cbf211b 100644 --- a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/external/Command.java +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/external/Command.java @@ -21,6 +21,7 @@ public enum Command implements Serializable { EXECUTING_INIT_MONGO, EXECUTING_INIT_CASSANDRA, EXECUTING_INIT_REDIS, + EXECUTING_INIT_DYNAMODB, EXECUTING_ACTION, BOOT_TIME_INFO, EXTRACT_JVM_DTO, diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/external/ServerController.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/external/ServerController.java index 34b359b1f3..256218cf70 100644 --- a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/external/ServerController.java +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/external/ServerController.java @@ -122,8 +122,7 @@ public synchronized Object waitAndGetResponse() { } try { - Object obj = in.readObject(); - return obj; + return in.readObject(); } catch (IOException e) { SimpleLogger.error("IO exception while waiting for response", e); return null; @@ -212,6 +211,10 @@ public boolean setExecutingInitRedis(boolean executingInitRedis) { return sendWithDataAndExpectACK(Command.EXECUTING_INIT_REDIS, executingInitRedis); } + public boolean setExecutingInitDynamoDb(boolean executingInitDynamoDb) { + return sendWithDataAndExpectACK(Command.EXECUTING_INIT_DYNAMODB, executingInitDynamoDb); + } + public boolean setExecutingAction(boolean executingAction){ return sendWithDataAndExpectACK(Command.EXECUTING_ACTION, executingAction); } 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 index ee28b2ca59..c4186e93c2 100644 --- a/core/src/main/kotlin/org/evomaster/core/database/dynamodb/DynamoDbAction.kt +++ b/core/src/main/kotlin/org/evomaster/core/database/dynamodb/DynamoDbAction.kt @@ -1,5 +1,7 @@ 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.DynamoDbInsertionKeyBuilder 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 @@ -29,12 +31,6 @@ class DynamoDbAction( val attributes: List ) : EnvironmentAction(listOf()) { - companion object { - private const val ATTRIBUTE_SEPARATOR = '|' - private const val TYPE_SEPARATOR = ':' - private const val VALUE_SEPARATOR = '=' - } - init { addChildren(attributes.map { it.gene }) } @@ -55,11 +51,10 @@ class DynamoDbAction( 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(ATTRIBUTE_SEPARATOR).append(it.attributeName).append(TYPE_SEPARATOR).append(it.type) - .append(VALUE_SEPARATOR).append(it.gene.getValueAsRawString()) + fun insertionKey(): String = DynamoDbInsertionKeyBuilder.fromAttributes( + tableName, + attributes.map { + DynamoDbAttributeValueDto(it.attributeName, it.type, it.gene.getValueAsRawString()) } - } + ) }