DynamoDB - Support dots in ConditionExpressions (#4383)

This commit is contained in:
Bert Blommers 2021-10-09 13:09:12 +00:00 committed by GitHub
parent 773e9a9f79
commit df05b608b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 368 additions and 328 deletions

View File

@ -179,11 +179,7 @@ class DynamoType(object):
Returns DynamoType or None.
"""
if isinstance(key, str) and self.is_map():
if "." in key and key.split(".")[0] in self.value:
return self.value[key.split(".")[0]].child_attr(
".".join(key.split(".")[1:])
)
elif "." not in key and key in self.value:
if key in self.value:
return DynamoType(self.value[key])
if isinstance(key, int) and self.is_list():

View File

@ -2720,329 +2720,6 @@ def test_dynamodb_streams_2():
assert "LatestStreamArn" in resp["TableDescription"]
@mock_dynamodb2
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"]
err["Code"].should.equal("ConditionalCheckFailedException")
err["Message"].should.equal("The conditional request failed")
@mock_dynamodb2
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"}],
)
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,
)
table.get_item(Key={"partitionKey": key})["Item"]["myAttr"].should.equal(
Decimal(res)
)
@mock_dynamodb2
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)
@mock_dynamodb2
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"}},
)
@mock_dynamodb2
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)
@mock_dynamodb2
def test_query_gsi_with_range_key():
dynamodb = boto3.client("dynamodb", region_name="us-east-1")

View File

@ -0,0 +1,367 @@
import boto3
import pytest
import sure # noqa
from decimal import Decimal
from moto import mock_dynamodb2
@mock_dynamodb2
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"}],
)
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"]
item.should.equal(
{"id": "key-0", "first": {},}
)
@mock_dynamodb2
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"]
err["Code"].should.equal("ConditionalCheckFailedException")
err["Message"].should.equal("The conditional request failed")
@mock_dynamodb2
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"}],
)
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,
)
table.get_item(Key={"partitionKey": key})["Item"]["myAttr"].should.equal(
Decimal(res)
)
@mock_dynamodb2
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)
@mock_dynamodb2
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"}},
)
@mock_dynamodb2
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)