diff --git a/moto/dynamodb/models/__init__.py b/moto/dynamodb/models/__init__.py index 83896110f..af84cdfa7 100644 --- a/moto/dynamodb/models/__init__.py +++ b/moto/dynamodb/models/__init__.py @@ -1381,9 +1381,7 @@ class DynamoDBBackend(BaseBackend): for key in keys: if key in table.hash_key_names: return key, None - # for potential_hash, potential_range in zip(table.hash_key_names, table.range_key_names): - # if set([potential_hash, potential_range]) == set(keys): - # return potential_hash, potential_range + potential_hash, potential_range = None, None for key in set(keys): if key in table.hash_key_names: diff --git a/moto/dynamodb/responses.py b/moto/dynamodb/responses.py index 044ac354c..39fcc72f5 100644 --- a/moto/dynamodb/responses.py +++ b/moto/dynamodb/responses.py @@ -68,7 +68,10 @@ def include_consumed_capacity(val=1.0): return _inner -def put_has_empty_keys(field_updates, table): +def get_empty_keys_on_put(field_updates, table): + """ + Return the first key-name that has an empty value. None if all keys are filled + """ if table: key_names = table.attribute_keys @@ -78,7 +81,9 @@ def put_has_empty_keys(field_updates, table): for (key, val) in field_updates.items() if next(iter(val.keys())) in ["S", "B"] and next(iter(val.values())) == "" ] - return any([keyname in empty_str_fields for keyname in key_names]) + return next( + (keyname for keyname in key_names if keyname in empty_str_fields), None + ) return False @@ -371,9 +376,10 @@ class DynamoHandler(BaseResponse): if return_values not in ("ALL_OLD", "NONE"): raise MockValidationException("Return values set to invalid value") - if put_has_empty_keys(item, self.dynamodb_backend.get_table(name)): + empty_key = get_empty_keys_on_put(item, self.dynamodb_backend.get_table(name)) + if empty_key: raise MockValidationException( - "One or more parameter values were invalid: An AttributeValue may not contain an empty string" + f"One or more parameter values were invalid: An AttributeValue may not contain an empty string. Key: {empty_key}" ) if put_has_empty_attrs(item, self.dynamodb_backend.get_table(name)): raise MockValidationException( @@ -421,17 +427,29 @@ class DynamoHandler(BaseResponse): def batch_write_item(self): table_batches = self.body["RequestItems"] - + put_requests = [] + delete_requests = [] for table_name, table_requests in table_batches.items(): + table = self.dynamodb_backend.get_table(table_name) for table_request in table_requests: request_type = list(table_request.keys())[0] request = list(table_request.values())[0] if request_type == "PutRequest": item = request["Item"] - self.dynamodb_backend.put_item(table_name, item) + empty_key = get_empty_keys_on_put(item, table) + if empty_key: + raise MockValidationException( + f"One or more parameter values are not valid. The AttributeValue for a key attribute cannot contain an empty string value. Key: {empty_key}" + ) + put_requests.append((table_name, item)) elif request_type == "DeleteRequest": keys = request["Key"] - self.dynamodb_backend.delete_item(table_name, keys) + delete_requests.append((table_name, keys)) + + for (table_name, item) in put_requests: + self.dynamodb_backend.put_item(table_name, item) + for (table_name, keys) in delete_requests: + self.dynamodb_backend.delete_item(table_name, keys) response = { "ConsumedCapacity": [ diff --git a/tests/test_dynamodb/exceptions/test_dynamodb_exceptions.py b/tests/test_dynamodb/exceptions/test_dynamodb_exceptions.py index 81122363b..8ce7ce1e1 100644 --- a/tests/test_dynamodb/exceptions/test_dynamodb_exceptions.py +++ b/tests/test_dynamodb/exceptions/test_dynamodb_exceptions.py @@ -625,3 +625,45 @@ def test_update_expression_with_trailing_comma(): err["Message"].should.equal( 'Invalid UpdateExpression: Syntax error; token: "", near: ","' ) + + +@mock_dynamodb +def test_batch_put_item_with_empty_value(): + ddb = boto3.resource("dynamodb", region_name="us-east-1") + ddb.create_table( + AttributeDefinitions=[ + {"AttributeName": "pk", "AttributeType": "S"}, + {"AttributeName": "sk", "AttributeType": "S"}, + ], + TableName="test-table", + KeySchema=[ + {"AttributeName": "pk", "KeyType": "HASH"}, + {"AttributeName": "sk", "KeyType": "SORT"}, + ], + ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5}, + ) + table = ddb.Table("test-table") + + # Empty Partition Key throws an error + with pytest.raises(botocore.exceptions.ClientError) as exc: + with table.batch_writer() as batch: + batch.put_item(Item={"pk": "", "sk": "sth"}) + err = exc.value.response["Error"] + err["Message"].should.equal( + "One or more parameter values are not valid. The AttributeValue for a key attribute cannot contain an empty string value. Key: pk" + ) + err["Code"].should.equal("ValidationException") + + # Empty SortKey throws an error + with pytest.raises(botocore.exceptions.ClientError) as exc: + with table.batch_writer() as batch: + batch.put_item(Item={"pk": "sth", "sk": ""}) + err = exc.value.response["Error"] + err["Message"].should.equal( + "One or more parameter values are not valid. The AttributeValue for a key attribute cannot contain an empty string value. Key: sk" + ) + err["Code"].should.equal("ValidationException") + + # Empty regular parameter workst just fine though + with table.batch_writer() as batch: + batch.put_item(Item={"pk": "sth", "sk": "else", "par": ""}) diff --git a/tests/test_dynamodb/test_dynamodb.py b/tests/test_dynamodb/test_dynamodb.py index 3d886888f..2988fc376 100644 --- a/tests/test_dynamodb/test_dynamodb.py +++ b/tests/test_dynamodb/test_dynamodb.py @@ -200,7 +200,7 @@ def test_item_add_empty_string_hash_key_exception(): ex.value.response["Error"]["Code"].should.equal("ValidationException") ex.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) - ex.value.response["Error"]["Message"].should.equal( + ex.value.response["Error"]["Message"].should.match( "One or more parameter values were invalid: An AttributeValue may not contain an empty string" ) @@ -241,7 +241,7 @@ def test_item_add_empty_string_range_key_exception(): ex.value.response["Error"]["Code"].should.equal("ValidationException") ex.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) - ex.value.response["Error"]["Message"].should.equal( + ex.value.response["Error"]["Message"].should.match( "One or more parameter values were invalid: An AttributeValue may not contain an empty string" )