Skip to content

@DynamoDbSecondaryPartitionKey and @DynamoDbSecondarySortKey annotations have no effect on table.createTable() #3923

@acouvreur

Description

@acouvreur

Describe the bug

Given the following class with two simple enums (Status and CommandType):

@DynamoDbBean
@Getter
@Setter
public class Command {

    public static final String COMMANDS_BY_STATUS = "commands_by_status";
    public static final String COMMANDS_BY_TYPE = "commands_by_type";

    private UUID id;
    private Status status;
    private CommandType type;

    @DynamoDbPartitionKey
    public UUID getId() {
        return id;
    }

    @DynamoDbSecondaryPartitionKey(indexNames = COMMANDS_BY_STATUS)
    public Status getStatus() {
        return this.status;
    }

    @DynamoDbSortKey
    @DynamoDbSecondarySortKey(indexNames = {COMMANDS_BY_STATUS, COMMANDS_BY_TYPE})
    public CommandType getType() {
        return this.type;
    }

And another service in charge of the initialization of this specific bean:

@Service
public class DynamoDbService {
    private static final TableSchema<Command> tableSchema = TableSchema.fromBean(Command.class);
    private final DynamoDbTable<Command> table;
    private final DynamoDbClient client;
    private final String tableName;

    @Autowired
    public DynamoDbService(DynamoDbEnhancedClient enhancedClient, DynamoDbClient client, DynamoDbTableNameResolver resolver) {
        this.client = client;
        this.tableName = resolver.resolve(Command.class);
        this.table = enhancedClient.table(tableName, tableSchema);
    }

    @PostConstruct
    public void createTableIfNotExists() {
        if (!this.tableExists(tableName)) {
            table.createTable()
        }
    }
}

The call to TableSchema.fromBean(Command.class); produces the following table metadata with the index by name map like:
image

Right here I can see the the schema correctly scans the annotations.


When calling table.createTable(), the following thing happens:

It makes a calls to

@Override
public void createTable() {
createTable(CreateTableEnhancedRequest.builder().build());
}

Which then goes all the way down with the default empty request to software.amazon.awssdk.enhanced.dynamodb.internal.operations.CreateTableOperation.java.

In the generateRequest you have the following piece of code:

String primaryPartitionKey = tableSchema.tableMetadata().primaryPartitionKey();
Optional<String> primarySortKey = tableSchema.tableMetadata().primarySortKey();
Set<String> dedupedIndexKeys = new HashSet<>();
dedupedIndexKeys.add(primaryPartitionKey);
primarySortKey.ifPresent(dedupedIndexKeys::add);
List<software.amazon.awssdk.services.dynamodb.model.GlobalSecondaryIndex> sdkGlobalSecondaryIndices = null;
List<software.amazon.awssdk.services.dynamodb.model.LocalSecondaryIndex> sdkLocalSecondaryIndices = null;
if (this.request.globalSecondaryIndices() != null) {
sdkGlobalSecondaryIndices =
this.request.globalSecondaryIndices().stream().map(gsi -> {
String indexPartitionKey = tableSchema.tableMetadata().indexPartitionKey(gsi.indexName());
Optional<String> indexSortKey = tableSchema.tableMetadata().indexSortKey(gsi.indexName());
dedupedIndexKeys.add(indexPartitionKey);
indexSortKey.ifPresent(dedupedIndexKeys::add);
return software.amazon.awssdk.services.dynamodb.model.GlobalSecondaryIndex
.builder()
.indexName(gsi.indexName())
.keySchema(generateKeySchema(indexPartitionKey, indexSortKey.orElse(null)))
.projection(gsi.projection())
.provisionedThroughput(gsi.provisionedThroughput())
.build();
}).collect(Collectors.toList());
}

As you can see:

  • sdkGlobalSecondaryIndices is initialized to null
  • sdkLocalSecondaryIndices is initialized to null

And the following call

if (this.request.globalSecondaryIndices() != null) {

Directly checks the empty default request.

It does not check for the table schema metadata on secondary indices.

Expected Behavior

I'd expect the @DynamoDbSecondaryPartitionKey and @DynamoDbSecondarySortKey annotations to be used when calling table.createTable() from the table schema of the same bean.

Current Behavior

The current call to table.createTable() produces the following schema:

{
  "AttributeDefinitions": [
    {
      "AttributeName": "id",
      "AttributeType": "S"
    },
    {
      "AttributeName": "type",
      "AttributeType": "S"
    }
  ],
  "TableName": "local_command",
  "KeySchema": [
    {
      "AttributeName": "id",
      "KeyType": "HASH"
    },
    {
      "AttributeName": "type",
      "KeyType": "RANGE"
    }
  ],
  "TableStatus": "ACTIVE",
  "CreationDateTime": "2023-04-20T13:40:20.610Z",
  "ProvisionedThroughput": {
    "LastIncreaseDateTime": "1970-01-01T00:00:00.000Z",
    "LastDecreaseDateTime": "1970-01-01T00:00:00.000Z",
    "NumberOfDecreasesToday": 0,
    "ReadCapacityUnits": 0,
    "WriteCapacityUnits": 0
  },
  "TableSizeBytes": 0,
  "ItemCount": 0,
  "TableArn": "arn:aws:dynamodb:ddblocal:000000000000:table/local_command",
  "BillingModeSummary": {
    "BillingMode": "PAY_PER_REQUEST",
    "LastUpdateToPayPerRequestDateTime": "2023-04-20T13:40:20.610Z"
  }
}

If you look closely at my description of the issue, I explain how I searched for the payload crafting.

Reproduction Steps

Create the @DynamoDbBean from my example and try to create the table from the schema.

Possible Solution

The fix is counter intuitive because of how I expected the annotations to work.

I just used the createTable method with the request argument to produce the following:

if (!this.tableExists(tableName)) {
        Projection all = Projection.builder()
                .projectionType(ProjectionType.ALL)
                .build();

        table.createTable(r -> {
                r.globalSecondaryIndices(
                EnhancedGlobalSecondaryIndex.builder()
                        .indexName(COMMANDS_BY_STATUS)
                        .projection(all)
                        .build(),
                EnhancedGlobalSecondaryIndex.builder()
                        .indexName(COMMANDS_BY_TYPE)
                        .projection(all)
                        .build()
                );
        });
}

Which produces the final and expected schema on DynamoD:

{
  "AttributeDefinitions": [
    {
      "AttributeName": "id",
      "AttributeType": "S"
    },
    {
      "AttributeName": "type",
      "AttributeType": "S"
    },
    {
      "AttributeName": "status",
      "AttributeType": "S"
    }
  ],
  "TableName": "local_command",
  "KeySchema": [
    {
      "AttributeName": "id",
      "KeyType": "HASH"
    },
    {
      "AttributeName": "type",
      "KeyType": "RANGE"
    }
  ],
  "TableStatus": "ACTIVE",
  "CreationDateTime": "2023-04-20T13:46:23.072Z",
  "ProvisionedThroughput": {
    "LastIncreaseDateTime": "1970-01-01T00:00:00.000Z",
    "LastDecreaseDateTime": "1970-01-01T00:00:00.000Z",
    "NumberOfDecreasesToday": 0,
    "ReadCapacityUnits": 0,
    "WriteCapacityUnits": 0
  },
  "TableSizeBytes": 0,
  "ItemCount": 0,
  "TableArn": "arn:aws:dynamodb:ddblocal:000000000000:table/local_command",
  "BillingModeSummary": {
    "BillingMode": "PAY_PER_REQUEST",
    "LastUpdateToPayPerRequestDateTime": "2023-04-20T13:46:23.072Z"
  },
  "GlobalSecondaryIndexes": [
    {
      "IndexName": "commands_by_status",
      "KeySchema": [
        {
          "AttributeName": "status",
          "KeyType": "HASH"
        },
        {
          "AttributeName": "type",
          "KeyType": "RANGE"
        }
      ],
      "Projection": {
        "ProjectionType": "ALL",
      },
      "IndexStatus": "ACTIVE",
      "ProvisionedThroughput": {
        "ReadCapacityUnits": 0,
        "WriteCapacityUnits": 0
      },
      "IndexSizeBytes": 0,
      "ItemCount": 0,
      "IndexArn": "arn:aws:dynamodb:ddblocal:000000000000:table/local_command/index/commands_by_status"
    },
    {
      "IndexName": "commands_by_type",
      "KeySchema": [
        {
          "AttributeName": "id",
          "KeyType": "HASH"
        },
        {
          "AttributeName": "type",
          "KeyType": "RANGE"
        }
      ],
      "Projection": {
        "ProjectionType": "ALL",
      },
      "IndexStatus": "ACTIVE",
      "ProvisionedThroughput": {
        "ReadCapacityUnits": 0,
        "WriteCapacityUnits": 0
      },
      "IndexSizeBytes": 0,
      "ItemCount": 0,
      "IndexArn": "arn:aws:dynamodb:ddblocal:000000000000:table/local_command/index/commands_by_type"
    }
  ]
}

Additional Information/Context

No response

AWS Java SDK version used

2.20.30

JDK version used

18.0.2

Operating System and version

MacOS M1 (13.3.1)

Metadata

Metadata

Labels

bugThis issue is a bug.dynamodb-enhancedp2This is a standard priority issue

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions