Merge pull request #2266 from garrettheel/feat/dynamodb-expressions

Improve DynamoDB condition expression support
This commit is contained in:
Steve Pulec 2019-07-09 18:22:55 -05:00 committed by GitHub
commit 6a13d54616
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 1172 additions and 452 deletions

View file

@ -838,44 +838,47 @@ def test_filter_expression():
filter_expr.expr(row1).should.be(True)
# NOT test 2
filter_expr = moto.dynamodb2.comparisons.get_filter_expression('NOT (Id = :v0)', {}, {':v0': {'N': 8}})
filter_expr = moto.dynamodb2.comparisons.get_filter_expression('NOT (Id = :v0)', {}, {':v0': {'N': '8'}})
filter_expr.expr(row1).should.be(False) # Id = 8 so should be false
# AND test
filter_expr = moto.dynamodb2.comparisons.get_filter_expression('Id > :v0 AND Subs < :v1', {}, {':v0': {'N': 5}, ':v1': {'N': 7}})
filter_expr = moto.dynamodb2.comparisons.get_filter_expression('Id > :v0 AND Subs < :v1', {}, {':v0': {'N': '5'}, ':v1': {'N': '7'}})
filter_expr.expr(row1).should.be(True)
filter_expr.expr(row2).should.be(False)
# OR test
filter_expr = moto.dynamodb2.comparisons.get_filter_expression('Id = :v0 OR Id=:v1', {}, {':v0': {'N': 5}, ':v1': {'N': 8}})
filter_expr = moto.dynamodb2.comparisons.get_filter_expression('Id = :v0 OR Id=:v1', {}, {':v0': {'N': '5'}, ':v1': {'N': '8'}})
filter_expr.expr(row1).should.be(True)
# BETWEEN test
filter_expr = moto.dynamodb2.comparisons.get_filter_expression('Id BETWEEN :v0 AND :v1', {}, {':v0': {'N': 5}, ':v1': {'N': 10}})
filter_expr = moto.dynamodb2.comparisons.get_filter_expression('Id BETWEEN :v0 AND :v1', {}, {':v0': {'N': '5'}, ':v1': {'N': '10'}})
filter_expr.expr(row1).should.be(True)
# PAREN test
filter_expr = moto.dynamodb2.comparisons.get_filter_expression('Id = :v0 AND (Subs = :v0 OR Subs = :v1)', {}, {':v0': {'N': 8}, ':v1': {'N': 5}})
filter_expr = moto.dynamodb2.comparisons.get_filter_expression('Id = :v0 AND (Subs = :v0 OR Subs = :v1)', {}, {':v0': {'N': '8'}, ':v1': {'N': '5'}})
filter_expr.expr(row1).should.be(True)
# IN test
filter_expr = moto.dynamodb2.comparisons.get_filter_expression('Id IN :v0', {}, {':v0': {'NS': [7, 8, 9]}})
filter_expr = moto.dynamodb2.comparisons.get_filter_expression('Id IN (:v0, :v1, :v2)', {}, {
':v0': {'N': '7'},
':v1': {'N': '8'},
':v2': {'N': '9'}})
filter_expr.expr(row1).should.be(True)
# attribute function tests (with extra spaces)
filter_expr = moto.dynamodb2.comparisons.get_filter_expression('attribute_exists(Id) AND attribute_not_exists (User)', {}, {})
filter_expr.expr(row1).should.be(True)
filter_expr = moto.dynamodb2.comparisons.get_filter_expression('attribute_type(Id, N)', {}, {})
filter_expr = moto.dynamodb2.comparisons.get_filter_expression('attribute_type(Id, :v0)', {}, {':v0': {'S': 'N'}})
filter_expr.expr(row1).should.be(True)
# beginswith function test
filter_expr = moto.dynamodb2.comparisons.get_filter_expression('begins_with(Desc, Some)', {}, {})
filter_expr = moto.dynamodb2.comparisons.get_filter_expression('begins_with(Desc, :v0)', {}, {':v0': {'S': 'Some'}})
filter_expr.expr(row1).should.be(True)
filter_expr.expr(row2).should.be(False)
# contains function test
filter_expr = moto.dynamodb2.comparisons.get_filter_expression('contains(KV, test1)', {}, {})
filter_expr = moto.dynamodb2.comparisons.get_filter_expression('contains(KV, :v0)', {}, {':v0': {'S': 'test1'}})
filter_expr.expr(row1).should.be(True)
filter_expr.expr(row2).should.be(False)
@ -916,14 +919,26 @@ def test_query_filter():
TableName='test1',
Item={
'client': {'S': 'client1'},
'app': {'S': 'app1'}
'app': {'S': 'app1'},
'nested': {'M': {
'version': {'S': 'version1'},
'contents': {'L': [
{'S': 'value1'}, {'S': 'value2'},
]},
}},
}
)
client.put_item(
TableName='test1',
Item={
'client': {'S': 'client1'},
'app': {'S': 'app2'}
'app': {'S': 'app2'},
'nested': {'M': {
'version': {'S': 'version2'},
'contents': {'L': [
{'S': 'value1'}, {'S': 'value2'},
]},
}},
}
)
@ -945,6 +960,18 @@ def test_query_filter():
)
assert response['Count'] == 2
response = table.query(
KeyConditionExpression=Key('client').eq('client1'),
FilterExpression=Attr('nested.version').contains('version')
)
assert response['Count'] == 2
response = table.query(
KeyConditionExpression=Key('client').eq('client1'),
FilterExpression=Attr('nested.contents[0]').eq('value1')
)
assert response['Count'] == 2
@mock_dynamodb2
def test_scan_filter():
@ -1223,7 +1250,7 @@ def test_delete_item():
with assert_raises(ClientError) as ex:
table.delete_item(Key={'client': 'client1', 'app': 'app1'},
ReturnValues='ALL_NEW')
# Test deletion and returning old value
response = table.delete_item(Key={'client': 'client1', 'app': 'app1'}, ReturnValues='ALL_OLD')
response['Attributes'].should.contain('client')
@ -1526,7 +1553,7 @@ def test_put_return_attributes():
ReturnValues='NONE'
)
assert 'Attributes' not in r
r = dynamodb.put_item(
TableName='moto-test',
Item={'id': {'S': 'foo'}, 'col1': {'S': 'val2'}},
@ -1543,7 +1570,7 @@ def test_put_return_attributes():
ex.exception.response['Error']['Code'].should.equal('ValidationException')
ex.exception.response['ResponseMetadata']['HTTPStatusCode'].should.equal(400)
ex.exception.response['Error']['Message'].should.equal('Return values set to invalid value')
@mock_dynamodb2
def test_query_global_secondary_index_when_created_via_update_table_resource():
@ -1651,7 +1678,7 @@ def test_dynamodb_streams_1():
'StreamViewType': 'NEW_AND_OLD_IMAGES'
}
)
assert 'StreamSpecification' in resp['TableDescription']
assert resp['TableDescription']['StreamSpecification'] == {
'StreamEnabled': True,
@ -1659,11 +1686,11 @@ def test_dynamodb_streams_1():
}
assert 'LatestStreamLabel' in resp['TableDescription']
assert 'LatestStreamArn' in resp['TableDescription']
resp = conn.delete_table(TableName='test-streams')
assert 'StreamSpecification' in resp['TableDescription']
@mock_dynamodb2
def test_dynamodb_streams_2():
@ -1694,11 +1721,10 @@ def test_dynamodb_streams_2():
assert 'LatestStreamLabel' in resp['TableDescription']
assert 'LatestStreamArn' in resp['TableDescription']
@mock_dynamodb2
def test_condition_expressions():
client = boto3.client('dynamodb', region_name='us-east-1')
dynamodb = boto3.resource('dynamodb', region_name='us-east-1')
# Create the DynamoDB table.
client.create_table(
@ -1751,6 +1777,57 @@ def test_condition_expressions():
}
)
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 assert_raises(client.exceptions.ConditionalCheckFailedException):
client.put_item(
TableName='test1',
@ -1803,6 +1880,89 @@ def test_condition_expressions():
}
)
# 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 assert_raises(client.exceptions.ConditionalCheckFailedException):
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',
},
)
@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 assert_raises(client.exceptions.ConditionalCheckFailedException):
update_if_attr_doesnt_exist()
@mock_dynamodb2
def test_query_gsi_with_range_key():