DynamoDB - Throw exception when updating key using SET operation (#4245)
This commit is contained in:
parent
c642e8b4a7
commit
7f912b7a5b
@ -190,6 +190,17 @@ class TransactionCanceledException(ValueError):
|
|||||||
|
|
||||||
class EmptyKeyAttributeException(MockValidationException):
|
class EmptyKeyAttributeException(MockValidationException):
|
||||||
empty_str_msg = "One or more parameter values were invalid: An AttributeValue may not contain an empty string"
|
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):
|
def __init__(self, key_in_index=False):
|
||||||
super(EmptyKeyAttributeException, self).__init__(self.empty_str_msg)
|
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))
|
||||||
|
@ -13,6 +13,7 @@ from moto.dynamodb2.exceptions import (
|
|||||||
InvalidUpdateExpressionInvalidDocumentPath,
|
InvalidUpdateExpressionInvalidDocumentPath,
|
||||||
ProvidedKeyDoesNotExist,
|
ProvidedKeyDoesNotExist,
|
||||||
EmptyKeyAttributeException,
|
EmptyKeyAttributeException,
|
||||||
|
UpdateHashRangeKeyException,
|
||||||
)
|
)
|
||||||
from moto.dynamodb2.models import DynamoType
|
from moto.dynamodb2.models import DynamoType
|
||||||
from moto.dynamodb2.parsing.ast_nodes import (
|
from moto.dynamodb2.parsing.ast_nodes import (
|
||||||
@ -337,7 +338,22 @@ class EmptyStringKeyValueValidator(DepthFirstTraverser):
|
|||||||
and val_node.type in ["S", "B"]
|
and val_node.type in ["S", "B"]
|
||||||
and key in self.key_attributes
|
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
|
return node
|
||||||
|
|
||||||
|
|
||||||
@ -386,6 +402,7 @@ class UpdateExpressionValidator(Validator):
|
|||||||
def get_ast_processors(self):
|
def get_ast_processors(self):
|
||||||
"""Get the different processors that go through the AST tree and processes the nodes."""
|
"""Get the different processors that go through the AST tree and processes the nodes."""
|
||||||
processors = [
|
processors = [
|
||||||
|
UpdateHashRangeKeyValidator(self.table.table_key_attrs),
|
||||||
ExpressionAttributeValueProcessor(self.expression_attribute_values),
|
ExpressionAttributeValueProcessor(self.expression_attribute_values),
|
||||||
ExpressionAttributeResolvingProcessor(
|
ExpressionAttributeResolvingProcessor(
|
||||||
self.expression_attribute_names, self.item
|
self.expression_attribute_names, self.item
|
||||||
|
@ -5527,6 +5527,54 @@ def test_gsi_key_can_be_updated():
|
|||||||
item["main_key"].should.equal({"S": "testkey1"})
|
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
|
@mock_dynamodb2
|
||||||
def test_create_backup_for_non_existent_table_raises_error():
|
def test_create_backup_for_non_existent_table_raises_error():
|
||||||
client = boto3.client("dynamodb", "us-east-1")
|
client = boto3.client("dynamodb", "us-east-1")
|
||||||
|
@ -14,6 +14,7 @@ import pytest
|
|||||||
from moto import mock_dynamodb2, mock_dynamodb2_deprecated
|
from moto import mock_dynamodb2, mock_dynamodb2_deprecated
|
||||||
from boto.exception import JSONResponseError
|
from boto.exception import JSONResponseError
|
||||||
from tests.helpers import requires_boto_gte
|
from tests.helpers import requires_boto_gte
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from boto.dynamodb2.fields import GlobalAllIndex, HashKey, RangeKey, AllIndex
|
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["id"]["S"] == "1"
|
||||||
assert last_eval_key["range_key"]["S"] == "1"
|
assert last_eval_key["range_key"]["S"] == "1"
|
||||||
assert last_eval_key["lsi_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"
|
||||||
|
)
|
||||||
|
@ -20,9 +20,9 @@ from moto.dynamodb2.parsing.validators import UpdateExpressionValidator
|
|||||||
|
|
||||||
|
|
||||||
def test_valid_update_expression(table):
|
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 = {
|
update_expression_values = {
|
||||||
":NewName": {"S": "AmazingForum"},
|
":Desc": {"S": "AmazingForum"},
|
||||||
":NewType": {"S": "BASIC"},
|
":NewType": {"S": "BASIC"},
|
||||||
}
|
}
|
||||||
update_expression_ast = UpdateExpressionParser.make(update_expression)
|
update_expression_ast = UpdateExpressionParser.make(update_expression)
|
||||||
@ -42,27 +42,6 @@ def test_valid_update_expression(table):
|
|||||||
).validate()
|
).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):
|
def test_validation_of_update_expression_with_keyword(table):
|
||||||
try:
|
try:
|
||||||
update_expression = "SET myNum = path + :val"
|
update_expression = "SET myNum = path + :val"
|
||||||
|
Loading…
Reference in New Issue
Block a user