DynamoDB - Throw exception when updating key using SET operation (#4245)

This commit is contained in:
Bert Blommers 2021-10-09 21:02:53 +00:00 committed by GitHub
parent c642e8b4a7
commit 7f912b7a5b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 127 additions and 26 deletions

View File

@ -190,6 +190,17 @@ class TransactionCanceledException(ValueError):
class EmptyKeyAttributeException(MockValidationException):
empty_str_msg = "One or more parameter values were invalid: An AttributeValue may not contain an empty string"
# AWS has a different message for empty index keys
empty_index_msg = "One or more parameter values are not valid. The update expression attempted to update a secondary index key to a value that is not supported. The AttributeValue for a key attribute cannot contain an empty string value."
def __init__(self):
super(EmptyKeyAttributeException, self).__init__(self.empty_str_msg)
def __init__(self, key_in_index=False):
super(EmptyKeyAttributeException, self).__init__(
self.empty_index_msg if key_in_index else self.empty_str_msg
)
class UpdateHashRangeKeyException(MockValidationException):
msg = "One or more parameter values were invalid: Cannot update attribute {}. This attribute is part of the key"
def __init__(self, key_name):
super(UpdateHashRangeKeyException, self).__init__(self.msg.format(key_name))

View File

@ -13,6 +13,7 @@ from moto.dynamodb2.exceptions import (
InvalidUpdateExpressionInvalidDocumentPath,
ProvidedKeyDoesNotExist,
EmptyKeyAttributeException,
UpdateHashRangeKeyException,
)
from moto.dynamodb2.models import DynamoType
from moto.dynamodb2.parsing.ast_nodes import (
@ -337,7 +338,22 @@ class EmptyStringKeyValueValidator(DepthFirstTraverser):
and val_node.type in ["S", "B"]
and key in self.key_attributes
):
raise EmptyKeyAttributeException
raise EmptyKeyAttributeException(key_in_index=True)
return node
class UpdateHashRangeKeyValidator(DepthFirstTraverser):
def __init__(self, table_key_attributes):
self.table_key_attributes = table_key_attributes
def _processing_map(self):
return {UpdateExpressionPath: self.check_for_hash_or_range_key}
def check_for_hash_or_range_key(self, node):
"""Check that hash and range keys are not updated"""
key_to_update = node.children[0].children[0]
if key_to_update in self.table_key_attributes:
raise UpdateHashRangeKeyException(key_to_update)
return node
@ -386,6 +402,7 @@ class UpdateExpressionValidator(Validator):
def get_ast_processors(self):
"""Get the different processors that go through the AST tree and processes the nodes."""
processors = [
UpdateHashRangeKeyValidator(self.table.table_key_attrs),
ExpressionAttributeValueProcessor(self.expression_attribute_values),
ExpressionAttributeResolvingProcessor(
self.expression_attribute_names, self.item

View File

@ -5527,6 +5527,54 @@ def test_gsi_key_can_be_updated():
item["main_key"].should.equal({"S": "testkey1"})
@mock_dynamodb2
def test_gsi_key_cannot_be_empty():
name = "TestTable"
conn = boto3.client("dynamodb", region_name="eu-west-2")
conn.create_table(
TableName=name,
KeySchema=[{"AttributeName": "main_key", "KeyType": "HASH"}],
AttributeDefinitions=[
{"AttributeName": "main_key", "AttributeType": "S"},
{"AttributeName": "index_key", "AttributeType": "S"},
],
ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
GlobalSecondaryIndexes=[
{
"IndexName": "test_index",
"KeySchema": [{"AttributeName": "index_key", "KeyType": "HASH"}],
"Projection": {"ProjectionType": "ALL",},
"ProvisionedThroughput": {
"ReadCapacityUnits": 1,
"WriteCapacityUnits": 1,
},
}
],
)
conn.put_item(
TableName=name,
Item={
"main_key": {"S": "testkey1"},
"extra_data": {"S": "testdata"},
"index_key": {"S": "indexkey1"},
},
)
with pytest.raises(ClientError) as ex:
conn.update_item(
TableName=name,
Key={"main_key": {"S": "testkey1"}},
UpdateExpression="set index_key=:new_index_key",
ExpressionAttributeValues={":new_index_key": {"S": ""}},
)
err = ex.value.response["Error"]
err["Code"].should.equal("ValidationException")
err["Message"].should.equal(
"One or more parameter values are not valid. The update expression attempted to update a secondary index key to a value that is not supported. The AttributeValue for a key attribute cannot contain an empty string value."
)
@mock_dynamodb2
def test_create_backup_for_non_existent_table_raises_error():
client = boto3.client("dynamodb", "us-east-1")

View File

@ -14,6 +14,7 @@ import pytest
from moto import mock_dynamodb2, mock_dynamodb2_deprecated
from boto.exception import JSONResponseError
from tests.helpers import requires_boto_gte
from uuid import uuid4
try:
from boto.dynamodb2.fields import GlobalAllIndex, HashKey, RangeKey, AllIndex
@ -2283,3 +2284,48 @@ def test_scan_by_index():
assert last_eval_key["id"]["S"] == "1"
assert last_eval_key["range_key"]["S"] == "1"
assert last_eval_key["lsi_range_key"]["S"] == "1"
@mock_dynamodb2
@pytest.mark.parametrize("create_item_first", [False, True])
@pytest.mark.parametrize(
"expression", ["set h=:New", "set r=:New", "set x=:New, r=:New"]
)
def test_update_item_throws_exception_when_updating_hash_or_range_key(
create_item_first, expression
):
client = boto3.client("dynamodb", region_name="ap-northeast-3")
table_name = "testtable_3877"
client.create_table(
TableName=table_name,
KeySchema=[
{"AttributeName": "h", "KeyType": "HASH"},
{"AttributeName": "r", "KeyType": "RANGE"},
],
AttributeDefinitions=[
{"AttributeName": "h", "AttributeType": "S"},
{"AttributeName": "r", "AttributeType": "S"},
],
)
initial_val = str(uuid4())
if create_item_first:
client.put_item(
TableName=table_name, Item={"h": {"S": initial_val}, "r": {"S": "1"}},
)
# Updating the HASH key should fail
with pytest.raises(ClientError) as ex:
client.update_item(
TableName=table_name,
Key={"h": {"S": initial_val}, "r": {"S": "1"}},
UpdateExpression=expression,
ExpressionAttributeValues={":New": {"S": "2"}},
)
err = ex.value.response["Error"]
err["Code"].should.equal("ValidationException")
err["Message"].should.match(
r"One or more parameter values were invalid: Cannot update attribute (r|h). This attribute is part of the key"
)

View File

@ -20,9 +20,9 @@ from moto.dynamodb2.parsing.validators import UpdateExpressionValidator
def test_valid_update_expression(table):
update_expression = "set forum_name=:NewName, forum_type=:NewType"
update_expression = "set forum_desc=:Desc, forum_type=:NewType"
update_expression_values = {
":NewName": {"S": "AmazingForum"},
":Desc": {"S": "AmazingForum"},
":NewType": {"S": "BASIC"},
}
update_expression_ast = UpdateExpressionParser.make(update_expression)
@ -42,27 +42,6 @@ def test_valid_update_expression(table):
).validate()
def test_validation_of_empty_string_key_val(table):
with pytest.raises(EmptyKeyAttributeException):
update_expression = "set forum_name=:NewName"
update_expression_values = {":NewName": {"S": ""}}
update_expression_ast = UpdateExpressionParser.make(update_expression)
item = Item(
hash_key=DynamoType({"S": "forum_name"}),
hash_key_type="TYPE",
range_key=None,
range_key_type=None,
attrs={"forum_name": {"S": "hello"}},
)
UpdateExpressionValidator(
update_expression_ast,
expression_attribute_names=None,
expression_attribute_values=update_expression_values,
item=item,
table=table,
).validate()
def test_validation_of_update_expression_with_keyword(table):
try:
update_expression = "SET myNum = path + :val"