-
Notifications
You must be signed in to change notification settings - Fork 116
DO NOT MERGE BEFORE PR#1727 Dynamodb insertions core actions and executor #1728
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,166 @@ | ||
| 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<DynamoDbInsertionDto> 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(); | ||
| for (int i = 0; i < insertions.size(); i++) { | ||
| try { | ||
| executeOne(client, insertions.get(i)); | ||
| } catch (RuntimeException e) { | ||
| handleFailedInsertion(results, insertions.size(), i); | ||
| throw new DynamoDbInsertionException(i, results, e); | ||
| } | ||
| } | ||
| results.executionResults = new ArrayList<>(Collections.nCopies(insertions.size(), true)); | ||
| return results; | ||
| } | ||
|
|
||
| /** | ||
| * Records the insertion that failed while preserving earlier successes. | ||
| * | ||
| * @param results insertion results to update | ||
| * @param insertionCount number of attempted insertions | ||
| * @param failedIndex zero-based index of the failed insertion | ||
| */ | ||
| private static void handleFailedInsertion( | ||
| DynamoDbInsertionResultsDto results, int insertionCount, int failedIndex) { | ||
| results.executionResults = new ArrayList<>(Collections.nCopies(insertionCount, false)); | ||
| for (int i = 0; i < failedIndex; i++) { | ||
| results.executionResults.set(i, true); | ||
| } | ||
| results.failedInsertionIndex = failedIndex; | ||
| } | ||
|
|
||
| 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<String, Object> item = new LinkedHashMap<>(); | ||
| for (DynamoDbAttributeValueDto attribute : insertion.attributes) { | ||
| Object builder = attributeValueClass.getMethod("builder").invoke(null); | ||
| String setter; | ||
| Object value; | ||
| switch (attribute.type) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why not having the same d action for all printable values? |
||
| case STRING: | ||
| setter = "s"; | ||
| value = attribute.value; | ||
| break; | ||
| case NUMBER: | ||
| setter = "n"; | ||
| value = attribute.value; | ||
| break; | ||
| case BOOLEAN: | ||
| 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); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. add string constants |
||
| 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); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. see comment above |
||
| 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; | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<PutItemRequest> 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.STRING, "Argentina")); | ||
| insertion.attributes.add(new DynamoDbAttributeValueDto("fifaId", DynamoDbScalarTypeDto.NUMBER, "10")); | ||
| insertion.attributes.add(new DynamoDbAttributeValueDto("captain", DynamoDbScalarTypeDto.BOOLEAN, "true")); | ||
| return insertion; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<DynamoDbAttributeGene> | ||
| ) : EnvironmentAction(listOf()) { | ||
|
|
||
| init { | ||
| addChildren(attributes.map { it.gene }) | ||
| } | ||
|
|
||
| /** Returns the genes that determine the inserted item values. */ | ||
| override fun seeTopGenes(): List<Gene> = 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) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. add constants |
||
| .append('=').append(it.gene.getValueAsRawString()) | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<DynamoDbAction>): 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.STRING -> (attribute.gene as StringGene).value | ||
| DynamoDbScalarTypeDto.NUMBER -> (attribute.gene as BigDecimalGene).value.toPlainString() | ||
| DynamoDbScalarTypeDto.BOOLEAN -> (attribute.gene as BooleanGene).value.toString() | ||
| } | ||
| ) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
add constant for these strings