moto/tests/test_dynamodb/test_dynamodb_condition_expressions.py

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

575 lines
21 KiB
Python
Raw Normal View History

import re
from decimal import Decimal
import boto3
import pytest
from botocore.exceptions import ClientError
2024-01-07 12:03:33 +00:00
from moto import mock_aws
2024-01-07 12:03:33 +00:00
@mock_aws
def test_condition_expression_with_dot_in_attr_name():
dynamodb = boto3.resource("dynamodb", region_name="us-east-2")
table_name = "Test"
dynamodb.create_table(
TableName=table_name,
KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}],
2022-02-10 20:09:45 +00:00
BillingMode="PAY_PER_REQUEST",
)
table = dynamodb.Table(table_name)
email_like_str = "test@foo.com"
record = {"id": "key-0", "first": {email_like_str: {"third": {"VALUE"}}}}
table.put_item(Item=record)
table.update_item(
Key={"id": "key-0"},
UpdateExpression="REMOVE #first.#second, #other",
ExpressionAttributeNames={
"#first": "first",
"#second": email_like_str,
"#third": "third",
"#other": "other",
},
ExpressionAttributeValues={":value": "VALUE", ":one": 1},
ConditionExpression="size(#first.#second.#third) = :one AND contains(#first.#second.#third, :value)",
ReturnValues="ALL_NEW",
)
item = table.get_item(Key={"id": "key-0"})["Item"]
assert item == {"id": "key-0", "first": {}}
2024-01-07 12:03:33 +00:00
@mock_aws
def test_condition_expressions():
client = boto3.client("dynamodb", region_name="us-east-1")
# Create the DynamoDB table.
client.create_table(
TableName="test1",
AttributeDefinitions=[
{"AttributeName": "client", "AttributeType": "S"},
{"AttributeName": "app", "AttributeType": "S"},
],
KeySchema=[
{"AttributeName": "client", "KeyType": "HASH"},
{"AttributeName": "app", "KeyType": "RANGE"},
],
ProvisionedThroughput={"ReadCapacityUnits": 123, "WriteCapacityUnits": 123},
)
client.put_item(
TableName="test1",
Item={
"client": {"S": "client1"},
"app": {"S": "app1"},
"match": {"S": "match"},
"existing": {"S": "existing"},
},
)
client.put_item(
TableName="test1",
Item={
"client": {"S": "client1"},
"app": {"S": "app1"},
"match": {"S": "match"},
"existing": {"S": "existing"},
},
ConditionExpression="attribute_exists(#existing) AND attribute_not_exists(#nonexistent) AND #match = :match",
ExpressionAttributeNames={
"#existing": "existing",
"#nonexistent": "nope",
"#match": "match",
},
ExpressionAttributeValues={":match": {"S": "match"}},
)
client.put_item(
TableName="test1",
Item={
"client": {"S": "client1"},
"app": {"S": "app1"},
"match": {"S": "match"},
"existing": {"S": "existing"},
},
ConditionExpression="NOT(attribute_exists(#nonexistent1) AND attribute_exists(#nonexistent2))",
ExpressionAttributeNames={"#nonexistent1": "nope", "#nonexistent2": "nope2"},
)
client.put_item(
TableName="test1",
Item={
"client": {"S": "client1"},
"app": {"S": "app1"},
"match": {"S": "match"},
"existing": {"S": "existing"},
},
ConditionExpression="attribute_exists(#nonexistent) OR attribute_exists(#existing)",
ExpressionAttributeNames={"#nonexistent": "nope", "#existing": "existing"},
)
client.put_item(
TableName="test1",
Item={
"client": {"S": "client1"},
"app": {"S": "app1"},
"match": {"S": "match"},
"existing": {"S": "existing"},
},
ConditionExpression="#client BETWEEN :a AND :z",
ExpressionAttributeNames={"#client": "client"},
ExpressionAttributeValues={":a": {"S": "a"}, ":z": {"S": "z"}},
)
client.put_item(
TableName="test1",
Item={
"client": {"S": "client1"},
"app": {"S": "app1"},
"match": {"S": "match"},
"existing": {"S": "existing"},
},
ConditionExpression="#client IN (:client1, :client2)",
ExpressionAttributeNames={"#client": "client"},
ExpressionAttributeValues={
":client1": {"S": "client1"},
":client2": {"S": "client2"},
},
)
with pytest.raises(client.exceptions.ConditionalCheckFailedException):
client.put_item(
TableName="test1",
Item={
"client": {"S": "client1"},
"app": {"S": "app1"},
"match": {"S": "match"},
"existing": {"S": "existing"},
},
ConditionExpression="attribute_exists(#nonexistent1) AND attribute_exists(#nonexistent2)",
ExpressionAttributeNames={
"#nonexistent1": "nope",
"#nonexistent2": "nope2",
},
)
with pytest.raises(client.exceptions.ConditionalCheckFailedException):
client.put_item(
TableName="test1",
Item={
"client": {"S": "client1"},
"app": {"S": "app1"},
"match": {"S": "match"},
"existing": {"S": "existing"},
},
ConditionExpression="NOT(attribute_not_exists(#nonexistent1) AND attribute_not_exists(#nonexistent2))",
ExpressionAttributeNames={
"#nonexistent1": "nope",
"#nonexistent2": "nope2",
},
)
with pytest.raises(client.exceptions.ConditionalCheckFailedException):
client.put_item(
TableName="test1",
Item={
"client": {"S": "client1"},
"app": {"S": "app1"},
"match": {"S": "match"},
"existing": {"S": "existing"},
},
ConditionExpression="attribute_exists(#existing) AND attribute_not_exists(#nonexistent) AND #match = :match",
ExpressionAttributeNames={
"#existing": "existing",
"#nonexistent": "nope",
"#match": "match",
},
ExpressionAttributeValues={":match": {"S": "match2"}},
)
# Make sure update_item honors ConditionExpression as well
client.update_item(
TableName="test1",
Key={"client": {"S": "client1"}, "app": {"S": "app1"}},
UpdateExpression="set #match=:match",
ConditionExpression="attribute_exists(#existing)",
ExpressionAttributeNames={"#existing": "existing", "#match": "match"},
ExpressionAttributeValues={":match": {"S": "match"}},
)
with pytest.raises(client.exceptions.ConditionalCheckFailedException) as exc:
client.update_item(
TableName="test1",
Key={"client": {"S": "client1"}, "app": {"S": "app1"}},
UpdateExpression="set #match=:match",
ConditionExpression="attribute_not_exists(#existing)",
ExpressionAttributeValues={":match": {"S": "match"}},
ExpressionAttributeNames={"#existing": "existing", "#match": "match"},
)
_assert_conditional_check_failed_exception(exc)
with pytest.raises(client.exceptions.ConditionalCheckFailedException) as exc:
client.update_item(
TableName="test1",
Key={"client": {"S": "client2"}, "app": {"S": "app1"}},
UpdateExpression="set #match=:match",
ConditionExpression="attribute_exists(#existing)",
ExpressionAttributeValues={":match": {"S": "match"}},
ExpressionAttributeNames={"#existing": "existing", "#match": "match"},
)
_assert_conditional_check_failed_exception(exc)
with pytest.raises(client.exceptions.ConditionalCheckFailedException):
client.delete_item(
TableName="test1",
Key={"client": {"S": "client1"}, "app": {"S": "app1"}},
ConditionExpression="attribute_not_exists(#existing)",
ExpressionAttributeValues={":match": {"S": "match"}},
ExpressionAttributeNames={"#existing": "existing", "#match": "match"},
)
def _assert_conditional_check_failed_exception(exc):
err = exc.value.response["Error"]
assert err["Code"] == "ConditionalCheckFailedException"
assert err["Message"] == "The conditional request failed"
2024-01-07 12:03:33 +00:00
@mock_aws
def test_condition_expression_numerical_attribute():
dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
dynamodb.create_table(
TableName="my-table",
KeySchema=[{"AttributeName": "partitionKey", "KeyType": "HASH"}],
AttributeDefinitions=[{"AttributeName": "partitionKey", "AttributeType": "S"}],
2022-02-10 20:09:45 +00:00
BillingMode="PAY_PER_REQUEST",
)
table = dynamodb.Table("my-table")
table.put_item(Item={"partitionKey": "pk-pos", "myAttr": 5})
table.put_item(Item={"partitionKey": "pk-neg", "myAttr": -5})
# try to update the item we put in the table using numerical condition expression
# Specifically, verify that we can compare with a zero-value
# First verify that > and >= work on positive numbers
update_numerical_con_expr(
key="pk-pos", con_expr="myAttr > :zero", res="6", table=table
)
update_numerical_con_expr(
key="pk-pos", con_expr="myAttr >= :zero", res="7", table=table
)
# Second verify that < and <= work on negative numbers
update_numerical_con_expr(
key="pk-neg", con_expr="myAttr < :zero", res="-4", table=table
)
update_numerical_con_expr(
key="pk-neg", con_expr="myAttr <= :zero", res="-3", table=table
)
def update_numerical_con_expr(key, con_expr, res, table):
table.update_item(
Key={"partitionKey": key},
UpdateExpression="ADD myAttr :one",
ExpressionAttributeValues={":zero": 0, ":one": 1},
ConditionExpression=con_expr,
)
assert table.get_item(Key={"partitionKey": key})["Item"]["myAttr"] == Decimal(res)
2024-01-07 12:03:33 +00:00
@mock_aws
def test_condition_expression__attr_doesnt_exist():
client = boto3.client("dynamodb", region_name="us-east-1")
client.create_table(
TableName="test",
KeySchema=[{"AttributeName": "forum_name", "KeyType": "HASH"}],
AttributeDefinitions=[{"AttributeName": "forum_name", "AttributeType": "S"}],
ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 1},
)
client.put_item(
TableName="test", Item={"forum_name": {"S": "foo"}, "ttl": {"N": "bar"}}
)
def update_if_attr_doesnt_exist():
# Test nonexistent top-level attribute.
client.update_item(
TableName="test",
Key={"forum_name": {"S": "the-key"}, "subject": {"S": "the-subject"}},
UpdateExpression="set #new_state=:new_state, #ttl=:ttl",
ConditionExpression="attribute_not_exists(#new_state)",
ExpressionAttributeNames={"#new_state": "foobar", "#ttl": "ttl"},
ExpressionAttributeValues={
":new_state": {"S": "some-value"},
":ttl": {"N": "12345.67"},
},
ReturnValues="ALL_NEW",
)
update_if_attr_doesnt_exist()
# Second time should fail
with pytest.raises(client.exceptions.ConditionalCheckFailedException) as exc:
update_if_attr_doesnt_exist()
_assert_conditional_check_failed_exception(exc)
2024-01-07 12:03:33 +00:00
@mock_aws
def test_condition_expression__or_order():
client = boto3.client("dynamodb", region_name="us-east-1")
client.create_table(
TableName="test",
KeySchema=[{"AttributeName": "forum_name", "KeyType": "HASH"}],
AttributeDefinitions=[{"AttributeName": "forum_name", "AttributeType": "S"}],
ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 1},
)
# ensure that the RHS of the OR expression is not evaluated if the LHS
# returns true (as it would result an error)
client.update_item(
TableName="test",
Key={"forum_name": {"S": "the-key"}},
UpdateExpression="set #ttl=:ttl",
ConditionExpression="attribute_not_exists(#ttl) OR #ttl <= :old_ttl",
ExpressionAttributeNames={"#ttl": "ttl"},
ExpressionAttributeValues={":ttl": {"N": "6"}, ":old_ttl": {"N": "5"}},
)
2024-01-07 12:03:33 +00:00
@mock_aws
def test_condition_expression__and_order():
client = boto3.client("dynamodb", region_name="us-east-1")
client.create_table(
TableName="test",
KeySchema=[{"AttributeName": "forum_name", "KeyType": "HASH"}],
AttributeDefinitions=[{"AttributeName": "forum_name", "AttributeType": "S"}],
ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 1},
)
# ensure that the RHS of the AND expression is not evaluated if the LHS
# returns true (as it would result an error)
with pytest.raises(client.exceptions.ConditionalCheckFailedException) as exc:
client.update_item(
TableName="test",
Key={"forum_name": {"S": "the-key"}},
UpdateExpression="set #ttl=:ttl",
ConditionExpression="attribute_exists(#ttl) AND #ttl <= :old_ttl",
ExpressionAttributeNames={"#ttl": "ttl"},
ExpressionAttributeValues={":ttl": {"N": "6"}, ":old_ttl": {"N": "5"}},
)
_assert_conditional_check_failed_exception(exc)
2024-01-07 12:03:33 +00:00
@mock_aws
def test_condition_expression_with_reserved_keyword_as_attr_name():
dynamodb = boto3.resource("dynamodb", region_name="us-east-2")
table_name = "Test"
dynamodb.create_table(
TableName=table_name,
KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}],
2022-02-10 20:09:45 +00:00
BillingMode="PAY_PER_REQUEST",
)
table = dynamodb.Table(table_name)
email_like_str = "test@foo.com"
record = {"id": "key-0", "first": {email_like_str: {"end": {"VALUE"}}}}
table.put_item(Item=record)
expected_error_message = re.escape(
"An error occurred (ValidationException) when "
"calling the UpdateItem operation: Invalid ConditionExpression: Attribute name "
"is a reserved keyword; reserved keyword: end"
)
with pytest.raises(
dynamodb.meta.client.exceptions.ClientError, match=expected_error_message
):
table.update_item(
Key={"id": "key-0"},
UpdateExpression="REMOVE #first.#second, #other",
ExpressionAttributeNames={
"#first": "first",
"#second": email_like_str,
"#other": "other",
},
ExpressionAttributeValues={":value": "VALUE", ":one": 1},
ConditionExpression="size(#first.#second.end) = :one AND contains(#first.#second.end, :value)",
ReturnValues="ALL_NEW",
)
# table is unchanged
item = table.get_item(Key={"id": "key-0"})["Item"]
assert item == record
# using attribute names solves the issue
table.update_item(
Key={"id": "key-0"},
UpdateExpression="REMOVE #first.#second, #other",
ExpressionAttributeNames={
"#first": "first",
"#second": email_like_str,
"#other": "other",
"#end": "end",
},
ExpressionAttributeValues={":value": "VALUE", ":one": 1},
ConditionExpression="size(#first.#second.#end) = :one AND contains(#first.#second.#end, :value)",
ReturnValues="ALL_NEW",
)
item = table.get_item(Key={"id": "key-0"})["Item"]
assert item == {"id": "key-0", "first": {}}
@mock_aws
def test_condition_check_failure_exception_is_raised_when_values_are_returned_for_an_item_with_a_top_level_list():
# This explicitly tests for a failure in handling JSONification of DynamoType
# when lists are at the top level of an item.
# This exception should not be raised:
# TypeError: Object of type DynamoType is not JSON serializable
dynamodb_client = boto3.client("dynamodb", region_name="us-east-1")
dynamodb_client.create_table(
TableName="example_table",
KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
AttributeDefinitions=[
{"AttributeName": "id", "AttributeType": "S"},
],
BillingMode="PAY_PER_REQUEST",
)
record = {
"id": {"S": "example_id"},
"some_list": {"L": [{"M": {"hello": {"S": "h"}}}]},
}
dynamodb_client.put_item(
TableName="example_table",
Item=record,
)
with pytest.raises(ClientError) as error:
dynamodb_client.update_item(
TableName="example_table",
Key={"id": {"S": "example_id"}},
UpdateExpression="set some_list=list_append(some_list, :w)",
ExpressionAttributeValues={
":w": {"L": [{"M": {"world": {"S": "w"}}}]},
":id": {"S": "incorrect id"},
},
ConditionExpression="id = :id",
ReturnValuesOnConditionCheckFailure="ALL_OLD",
)
assert error.type.__name__ == "ConditionalCheckFailedException"
assert error.value.response["Error"] == {
"Message": "The conditional request failed",
"Code": "ConditionalCheckFailedException",
}
assert error.value.response["Item"] == {
"id": {"S": "example_id"},
"some_list": {"L": [{"M": {"hello": {"S": "h"}}}]},
}
@mock_aws
def test_condition_check_failure_exception_is_raised_when_values_are_returned_for_an_item_with_a_top_level_string_set():
# This explicitly tests for a failure in handling JSONification of DynamoType
# when string sets are at the top level of an item.
# These exception should not be raised:
# TypeError: Object of type DynamoType is not JSON serializable
# AttributeError: 'str' object has no attribute 'to_regular_json'
dynamodb_client = boto3.client("dynamodb", region_name="us-east-1")
dynamodb_client.create_table(
TableName="example_table",
KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
AttributeDefinitions=[
{"AttributeName": "id", "AttributeType": "S"},
],
BillingMode="PAY_PER_REQUEST",
)
record = {
"id": {"S": "example_id"},
"some_list": {"SS": ["hello"]},
}
dynamodb_client.put_item(
TableName="example_table",
Item=record,
)
with pytest.raises(ClientError) as error:
dynamodb_client.update_item(
TableName="example_table",
Key={"id": {"S": "example_id"}},
UpdateExpression="set some_list=list_append(some_list, :w)",
ExpressionAttributeValues={
":w": {"SS": ["world"]},
":id": {"S": "incorrect id"},
},
ConditionExpression="id = :id",
ReturnValuesOnConditionCheckFailure="ALL_OLD",
)
assert error.type.__name__ == "ConditionalCheckFailedException"
assert error.value.response["Error"] == {
"Message": "The conditional request failed",
"Code": "ConditionalCheckFailedException",
}
assert error.value.response["Item"] == {
"id": {"S": "example_id"},
"some_list": {"SS": ["hello"]},
}
@mock_aws
def test_condition_check_failure_exception_is_raised_when_values_are_returned_for_an_item_with_a_list_in_a_map():
# This explicitly tests for a failure in handling JSONification of DynamoType
# when lists are inside a map
dynamodb_client = boto3.client("dynamodb", region_name="us-east-1")
dynamodb_client.create_table(
TableName="example_table",
KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
AttributeDefinitions=[
{"AttributeName": "id", "AttributeType": "S"},
],
BillingMode="PAY_PER_REQUEST",
)
record = {
"id": {"S": "example_id"},
"some_list_in_a_map": {
"M": {"some_list": {"L": [{"M": {"hello": {"S": "h"}}}]}}
},
}
dynamodb_client.put_item(
TableName="example_table",
Item=record,
)
with pytest.raises(ClientError) as error:
dynamodb_client.update_item(
TableName="example_table",
Key={"id": {"S": "example_id"}},
UpdateExpression="set some_list_in_a_map.some_list=list_append(some_list_in_a_map.some_list, :w)",
ExpressionAttributeValues={
":w": {"L": [{"M": {"world": {"S": "w"}}}]},
":id": {"S": "incorrect id"},
},
ConditionExpression="id = :id",
ReturnValuesOnConditionCheckFailure="ALL_OLD",
)
assert error.type.__name__ == "ConditionalCheckFailedException"
assert error.value.response["Error"] == {
"Message": "The conditional request failed",
"Code": "ConditionalCheckFailedException",
}
assert error.value.response["Item"] == {
"id": {"S": "example_id"},
"some_list_in_a_map": {
"M": {"some_list": {"L": [{"M": {"hello": {"S": "h"}}}]}}
},
}