DynamoDB - Support dots in ConditionExpressions (#4383)
This commit is contained in:
parent
773e9a9f79
commit
df05b608b0
@ -179,11 +179,7 @@ class DynamoType(object):
|
|||||||
Returns DynamoType or None.
|
Returns DynamoType or None.
|
||||||
"""
|
"""
|
||||||
if isinstance(key, str) and self.is_map():
|
if isinstance(key, str) and self.is_map():
|
||||||
if "." in key and key.split(".")[0] in self.value:
|
if key in self.value:
|
||||||
return self.value[key.split(".")[0]].child_attr(
|
|
||||||
".".join(key.split(".")[1:])
|
|
||||||
)
|
|
||||||
elif "." not in key and key in self.value:
|
|
||||||
return DynamoType(self.value[key])
|
return DynamoType(self.value[key])
|
||||||
|
|
||||||
if isinstance(key, int) and self.is_list():
|
if isinstance(key, int) and self.is_list():
|
||||||
|
@ -2720,329 +2720,6 @@ def test_dynamodb_streams_2():
|
|||||||
assert "LatestStreamArn" in resp["TableDescription"]
|
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
|
@mock_dynamodb2
|
||||||
def test_query_gsi_with_range_key():
|
def test_query_gsi_with_range_key():
|
||||||
dynamodb = boto3.client("dynamodb", region_name="us-east-1")
|
dynamodb = boto3.client("dynamodb", region_name="us-east-1")
|
||||||
|
367
tests/test_dynamodb2/test_dynamodb_condition_expressions.py
Normal file
367
tests/test_dynamodb2/test_dynamodb_condition_expressions.py
Normal 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)
|
Loading…
Reference in New Issue
Block a user