Tweak comparison to treat NULL/NOT_NULL correctly. (#1709)

The AWS documentation says that a ComparisonOperator of NULL means
the attribute should not exist, whereas NOT_NULL means that the
attribute should exist. It explicitly says that an attribute with a
value of NULL is considered to exist, which contradicts our previous
implementation. This affects both put_item and get_item in dynamodb2.

https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Condition.html
This commit is contained in:
Gary Donovan 2018-07-13 19:11:10 +10:00 committed by Terry Cain
parent 7d201c48b5
commit 802402bdba
4 changed files with 107 additions and 12 deletions

1
.gitignore vendored
View File

@ -14,4 +14,5 @@ build/
python_env
.ropeproject/
.pytest_cache/
venv/

View File

@ -29,8 +29,10 @@ COMPARISON_FUNCS = {
'GT': GT_FUNCTION,
'>': GT_FUNCTION,
'NULL': lambda item_value: item_value is None,
'NOT_NULL': lambda item_value: item_value is not None,
# NULL means the value should not exist at all
'NULL': lambda item_value: False,
# NOT_NULL means the value merely has to exist, and values of None are valid
'NOT_NULL': lambda item_value: True,
'CONTAINS': lambda item_value, test_value: test_value in item_value,
'NOT_CONTAINS': lambda item_value, test_value: test_value not in item_value,
'BEGINS_WITH': lambda item_value, test_value: item_value.startswith(test_value),

View File

@ -409,7 +409,8 @@ class Table(BaseModel):
current_attr = current
for key, val in expected.items():
if 'Exists' in val and val['Exists'] is False:
if 'Exists' in val and val['Exists'] is False \
or 'ComparisonOperator' in val and val['ComparisonOperator'] == 'NULL':
if key in current_attr:
raise ValueError("The conditional request failed")
elif key not in current_attr:
@ -419,8 +420,10 @@ class Table(BaseModel):
elif 'ComparisonOperator' in val:
comparison_func = get_comparison_func(
val['ComparisonOperator'])
dynamo_types = [DynamoType(ele) for ele in val[
"AttributeValueList"]]
dynamo_types = [
DynamoType(ele) for ele in
val.get("AttributeValueList", [])
]
for t in dynamo_types:
if not comparison_func(current_attr[key].value, t.value):
raise ValueError('The conditional request failed')
@ -827,7 +830,8 @@ class DynamoDBBackend(BaseBackend):
expected = {}
for key, val in expected.items():
if 'Exists' in val and val['Exists'] is False:
if 'Exists' in val and val['Exists'] is False \
or 'ComparisonOperator' in val and val['ComparisonOperator'] == 'NULL':
if key in item_attr:
raise ValueError("The conditional request failed")
elif key not in item_attr:
@ -837,8 +841,10 @@ class DynamoDBBackend(BaseBackend):
elif 'ComparisonOperator' in val:
comparison_func = get_comparison_func(
val['ComparisonOperator'])
dynamo_types = [DynamoType(ele) for ele in val[
"AttributeValueList"]]
dynamo_types = [
DynamoType(ele) for ele in
val.get("AttributeValueList", [])
]
for t in dynamo_types:
if not comparison_func(item_attr[key].value, t.value):
raise ValueError('The conditional request failed')

View File

@ -596,7 +596,50 @@ def test_boto3_conditions():
@mock_dynamodb2
def test_boto3_put_item_conditions_fails():
def test_boto3_put_item_conditions_pass():
table = _create_user_table()
table.put_item(Item={'username': 'johndoe', 'foo': 'bar'})
table.put_item(
Item={'username': 'johndoe', 'foo': 'baz'},
Expected={
'foo': {
'ComparisonOperator': 'EQ',
'AttributeValueList': ['bar']
}
})
final_item = table.get_item(Key={'username': 'johndoe'})
assert dict(final_item)['Item']['foo'].should.equal("baz")
@mock_dynamodb2
def test_boto3_put_item_conditions_pass_because_expect_not_exists_by_compare_to_null():
table = _create_user_table()
table.put_item(Item={'username': 'johndoe', 'foo': 'bar'})
table.put_item(
Item={'username': 'johndoe', 'foo': 'baz'},
Expected={
'whatever': {
'ComparisonOperator': 'NULL',
}
})
final_item = table.get_item(Key={'username': 'johndoe'})
assert dict(final_item)['Item']['foo'].should.equal("baz")
@mock_dynamodb2
def test_boto3_put_item_conditions_pass_because_expect_exists_by_compare_to_not_null():
table = _create_user_table()
table.put_item(Item={'username': 'johndoe', 'foo': 'bar'})
table.put_item(
Item={'username': 'johndoe', 'foo': 'baz'},
Expected={
'foo': {
'ComparisonOperator': 'NOT_NULL',
}
})
final_item = table.get_item(Key={'username': 'johndoe'})
assert dict(final_item)['Item']['foo'].should.equal("baz")
@mock_dynamodb2
def test_boto3_put_item_conditions_fail():
table = _create_user_table()
table.put_item(Item={'username': 'johndoe', 'foo': 'bar'})
table.put_item.when.called_with(
@ -609,7 +652,7 @@ def test_boto3_put_item_conditions_fails():
}).should.throw(botocore.client.ClientError)
@mock_dynamodb2
def test_boto3_update_item_conditions_fails():
def test_boto3_update_item_conditions_fail():
table = _create_user_table()
table.put_item(Item={'username': 'johndoe', 'foo': 'baz'})
table.update_item.when.called_with(
@ -622,7 +665,7 @@ def test_boto3_update_item_conditions_fails():
}).should.throw(botocore.client.ClientError)
@mock_dynamodb2
def test_boto3_update_item_conditions_fails_because_expect_not_exists():
def test_boto3_update_item_conditions_fail_because_expect_not_exists():
table = _create_user_table()
table.put_item(Item={'username': 'johndoe', 'foo': 'baz'})
table.update_item.when.called_with(
@ -634,6 +677,19 @@ def test_boto3_update_item_conditions_fails_because_expect_not_exists():
}
}).should.throw(botocore.client.ClientError)
@mock_dynamodb2
def test_boto3_update_item_conditions_fail_because_expect_not_exists_by_compare_to_null():
table = _create_user_table()
table.put_item(Item={'username': 'johndoe', 'foo': 'baz'})
table.update_item.when.called_with(
Key={'username': 'johndoe'},
UpdateExpression='SET foo=bar',
Expected={
'foo': {
'ComparisonOperator': 'NULL',
}
}).should.throw(botocore.client.ClientError)
@mock_dynamodb2
def test_boto3_update_item_conditions_pass():
table = _create_user_table()
@ -650,7 +706,7 @@ def test_boto3_update_item_conditions_pass():
assert dict(returned_item)['Item']['foo'].should.equal("baz")
@mock_dynamodb2
def test_boto3_update_item_conditions_pass_because_expext_not_exists():
def test_boto3_update_item_conditions_pass_because_expect_not_exists():
table = _create_user_table()
table.put_item(Item={'username': 'johndoe', 'foo': 'bar'})
table.update_item(
@ -664,6 +720,36 @@ def test_boto3_update_item_conditions_pass_because_expext_not_exists():
returned_item = table.get_item(Key={'username': 'johndoe'})
assert dict(returned_item)['Item']['foo'].should.equal("baz")
@mock_dynamodb2
def test_boto3_update_item_conditions_pass_because_expect_not_exists_by_compare_to_null():
table = _create_user_table()
table.put_item(Item={'username': 'johndoe', 'foo': 'bar'})
table.update_item(
Key={'username': 'johndoe'},
UpdateExpression='SET foo=baz',
Expected={
'whatever': {
'ComparisonOperator': 'NULL',
}
})
returned_item = table.get_item(Key={'username': 'johndoe'})
assert dict(returned_item)['Item']['foo'].should.equal("baz")
@mock_dynamodb2
def test_boto3_update_item_conditions_pass_because_expect_exists_by_compare_to_not_null():
table = _create_user_table()
table.put_item(Item={'username': 'johndoe', 'foo': 'bar'})
table.update_item(
Key={'username': 'johndoe'},
UpdateExpression='SET foo=baz',
Expected={
'foo': {
'ComparisonOperator': 'NOT_NULL',
}
})
returned_item = table.get_item(Key={'username': 'johndoe'})
assert dict(returned_item)['Item']['foo'].should.equal("baz")
@mock_dynamodb2
def test_boto3_put_item_conditions_pass():
table = _create_user_table()