diff --git a/.gitignore b/.gitignore index c4b8c503..7f57e98e 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,5 @@ build/ python_env .ropeproject/ .pytest_cache/ +venv/ diff --git a/moto/dynamodb2/comparisons.py b/moto/dynamodb2/comparisons.py index 51d62fb8..53226c55 100644 --- a/moto/dynamodb2/comparisons.py +++ b/moto/dynamodb2/comparisons.py @@ -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), diff --git a/moto/dynamodb2/models.py b/moto/dynamodb2/models.py index db6bf04a..b327c7a4 100644 --- a/moto/dynamodb2/models.py +++ b/moto/dynamodb2/models.py @@ -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') diff --git a/tests/test_dynamodb2/test_dynamodb_table_without_range_key.py b/tests/test_dynamodb2/test_dynamodb_table_without_range_key.py index 5e635d5e..15e5284b 100644 --- a/tests/test_dynamodb2/test_dynamodb_table_without_range_key.py +++ b/tests/test_dynamodb2/test_dynamodb_table_without_range_key.py @@ -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()