Skip to content

Commit 6c451f7

Browse files
authored
improv: Support a composite primary key in DynamoDBPersistenceLayer (#740)
* ISSUE-694: add sort key to DynamoDBPersistenceLayer * ISSUE-694: change default pk; revert test changes for now * ISSUE-694: PR Updates - remove test-func default name * ISSUE-694: Change key_attr_value based on PR feedback
1 parent 750f482 commit 6c451f7

File tree

2 files changed

+26
-8
lines changed

2 files changed

+26
-8
lines changed

aws_lambda_powertools/utilities/idempotency/persistence/dynamodb.py

+23-5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import datetime
22
import logging
3+
import os
34
from typing import Any, Dict, Optional
45

56
import boto3
67
from botocore.config import Config
78

9+
from aws_lambda_powertools.shared import constants
810
from aws_lambda_powertools.utilities.idempotency import BasePersistenceLayer
911
from aws_lambda_powertools.utilities.idempotency.exceptions import (
1012
IdempotencyItemAlreadyExistsError,
@@ -20,6 +22,8 @@ def __init__(
2022
self,
2123
table_name: str,
2224
key_attr: str = "id",
25+
static_pk_value: str = f"idempotency#{os.getenv(constants.LAMBDA_FUNCTION_NAME_ENV, '')}",
26+
sort_key_attr: Optional[str] = None,
2327
expiry_attr: str = "expiration",
2428
status_attr: str = "status",
2529
data_attr: str = "data",
@@ -35,7 +39,12 @@ def __init__(
3539
table_name: str
3640
Name of the table to use for storing execution records
3741
key_attr: str, optional
38-
DynamoDB attribute name for key, by default "id"
42+
DynamoDB attribute name for partition key, by default "id"
43+
static_pk_value: str, optional
44+
DynamoDB attribute value for partition key, by default "idempotency#<function-name>".
45+
This will be used if the sort_key_attr is set.
46+
sort_key_attr: str, optional
47+
DynamoDB attribute name for the sort key
3948
expiry_attr: str, optional
4049
DynamoDB attribute name for expiry timestamp, by default "expiration"
4150
status_attr: str, optional
@@ -64,10 +73,14 @@ def __init__(
6473

6574
self._boto_config = boto_config or Config()
6675
self._boto3_session = boto3_session or boto3.session.Session()
76+
if sort_key_attr == key_attr:
77+
raise ValueError(f"key_attr [{key_attr}] and sort_key_attr [{sort_key_attr}] cannot be the same!")
6778

6879
self._table = None
6980
self.table_name = table_name
7081
self.key_attr = key_attr
82+
self.static_pk_value = static_pk_value
83+
self.sort_key_attr = sort_key_attr
7184
self.expiry_attr = expiry_attr
7285
self.status_attr = status_attr
7386
self.data_attr = data_attr
@@ -93,6 +106,11 @@ def table(self, table):
93106
"""
94107
self._table = table
95108

109+
def _get_key(self, idempotency_key: str) -> dict:
110+
if self.sort_key_attr:
111+
return {self.key_attr: self.static_pk_value, self.sort_key_attr: idempotency_key}
112+
return {self.key_attr: idempotency_key}
113+
96114
def _item_to_data_record(self, item: Dict[str, Any]) -> DataRecord:
97115
"""
98116
Translate raw item records from DynamoDB to DataRecord
@@ -117,7 +135,7 @@ def _item_to_data_record(self, item: Dict[str, Any]) -> DataRecord:
117135
)
118136

119137
def _get_record(self, idempotency_key) -> DataRecord:
120-
response = self.table.get_item(Key={self.key_attr: idempotency_key}, ConsistentRead=True)
138+
response = self.table.get_item(Key=self._get_key(idempotency_key), ConsistentRead=True)
121139

122140
try:
123141
item = response["Item"]
@@ -127,7 +145,7 @@ def _get_record(self, idempotency_key) -> DataRecord:
127145

128146
def _put_record(self, data_record: DataRecord) -> None:
129147
item = {
130-
self.key_attr: data_record.idempotency_key,
148+
**self._get_key(data_record.idempotency_key),
131149
self.expiry_attr: data_record.expiry_timestamp,
132150
self.status_attr: data_record.status,
133151
}
@@ -168,7 +186,7 @@ def _update_record(self, data_record: DataRecord):
168186
expression_attr_names["#validation_key"] = self.validation_key_attr
169187

170188
kwargs = {
171-
"Key": {self.key_attr: data_record.idempotency_key},
189+
"Key": self._get_key(data_record.idempotency_key),
172190
"UpdateExpression": update_expression,
173191
"ExpressionAttributeValues": expression_attr_values,
174192
"ExpressionAttributeNames": expression_attr_names,
@@ -178,4 +196,4 @@ def _update_record(self, data_record: DataRecord):
178196

179197
def _delete_record(self, data_record: DataRecord) -> None:
180198
logger.debug(f"Deleting record for idempotency key: {data_record.idempotency_key}")
181-
self.table.delete_item(Key={self.key_attr: data_record.idempotency_key})
199+
self.table.delete_item(Key=self._get_key(data_record.idempotency_key))

tests/functional/idempotency/test_idempotency.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -783,11 +783,11 @@ def test_jmespath_with_powertools_json(
783783
# GIVEN an event_key_jmespath with powertools_json custom function
784784
persistence_store.configure(idempotency_config)
785785
sub_attr_value = "cognito_user"
786-
key_attr_value = "some_key"
787-
expected_value = [sub_attr_value, key_attr_value]
786+
static_pk_value = "some_key"
787+
expected_value = [sub_attr_value, static_pk_value]
788788
api_gateway_proxy_event = {
789789
"requestContext": {"authorizer": {"claims": {"sub": sub_attr_value}}},
790-
"body": serialize({"id": key_attr_value}),
790+
"body": serialize({"id": static_pk_value}),
791791
}
792792

793793
# WHEN calling _get_hashed_idempotency_key

0 commit comments

Comments
 (0)