support a bit more of the dynamoDB ConditionExpression syntax
This commit is contained in:
		
							parent
							
								
									1095b7d94b
								
							
						
					
					
						commit
						f035b9613d
					
				| @ -31,6 +31,68 @@ def get_empty_str_error(): | |||||||
|                              )) |                              )) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | def condition_expression_to_expected(condition_expression, expression_attribute_names, expression_attribute_values): | ||||||
|  |     """ | ||||||
|  |     Limited condition expression syntax parsing. | ||||||
|  |     Supports Global Negation ex: NOT(inner expressions). | ||||||
|  |     Supports simple AND conditions ex: cond_a AND cond_b and cond_c. | ||||||
|  |     Atomic expressions supported are attribute_exists(key), attribute_not_exists(key) and #key = :value. | ||||||
|  |     """ | ||||||
|  |     expected = {} | ||||||
|  |     if condition_expression and 'OR' not in condition_expression: | ||||||
|  |         reverse_re = re.compile('^NOT\s*\((.*)\)$') | ||||||
|  |         reverse_m = reverse_re.match(condition_expression.strip()) | ||||||
|  | 
 | ||||||
|  |         reverse = False | ||||||
|  |         if reverse_m: | ||||||
|  |             reverse = True | ||||||
|  |             condition_expression = reverse_m.group(1) | ||||||
|  | 
 | ||||||
|  |         cond_items = [c.strip() for c in condition_expression.split('AND')] | ||||||
|  |         if cond_items: | ||||||
|  |             overwrite = False | ||||||
|  |             exists_re = re.compile('^attribute_exists\s*\((.*)\)$') | ||||||
|  |             not_exists_re = re.compile( | ||||||
|  |                 '^attribute_not_exists\s*\((.*)\)$') | ||||||
|  |             equals_re= re.compile('^(#?\w+)\s*=\s*(\:?\w+)') | ||||||
|  | 
 | ||||||
|  |         for cond in cond_items: | ||||||
|  |             exists_m = exists_re.match(cond) | ||||||
|  |             not_exists_m = not_exists_re.match(cond) | ||||||
|  |             equals_m = equals_re.match(cond) | ||||||
|  | 
 | ||||||
|  |             if exists_m: | ||||||
|  |                 attribute_name = expression_attribute_names_lookup(exists_m.group(1), expression_attribute_names) | ||||||
|  |                 expected[attribute_name] = {'Exists': True if not reverse else False} | ||||||
|  |             elif not_exists_m: | ||||||
|  |                 attribute_name = expression_attribute_names_lookup(not_exists_m.group(1), expression_attribute_names) | ||||||
|  |                 expected[attribute_name] = {'Exists': False if not reverse else True} | ||||||
|  |             elif equals_m: | ||||||
|  |                 attribute_name = expression_attribute_names_lookup(equals_m.group(1), expression_attribute_names) | ||||||
|  |                 attribute_value = expression_attribute_values_lookup(equals_m.group(2), expression_attribute_values) | ||||||
|  |                 expected[attribute_name] = { | ||||||
|  |                     'AttributeValueList': [attribute_value], | ||||||
|  |                     'ComparisonOperator': 'EQ' if not reverse else 'NEQ'} | ||||||
|  | 
 | ||||||
|  |     return expected | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def expression_attribute_names_lookup(attribute_name, expression_attribute_names): | ||||||
|  |     if attribute_name.startswith('#') and attribute_name in expression_attribute_names: | ||||||
|  |         return expression_attribute_names[attribute_name] | ||||||
|  |     else: | ||||||
|  |         return attribute_name | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def expression_attribute_values_lookup(attribute_value, expression_attribute_values): | ||||||
|  |     if isinstance(attribute_value, six.string_types) and \ | ||||||
|  |             attribute_value.startswith(':') and\ | ||||||
|  |             attribute_value in expression_attribute_values: | ||||||
|  |         return expression_attribute_values[attribute_value] | ||||||
|  |     else: | ||||||
|  |         return attribute_value | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| class DynamoHandler(BaseResponse): | class DynamoHandler(BaseResponse): | ||||||
| 
 | 
 | ||||||
|     def get_endpoint_name(self, headers): |     def get_endpoint_name(self, headers): | ||||||
| @ -220,24 +282,13 @@ class DynamoHandler(BaseResponse): | |||||||
|         # expression |         # expression | ||||||
|         if not expected: |         if not expected: | ||||||
|             condition_expression = self.body.get('ConditionExpression') |             condition_expression = self.body.get('ConditionExpression') | ||||||
|             if condition_expression and 'OR' not in condition_expression: |             expression_attribute_names = self.body.get('ExpressionAttributeNames', {}) | ||||||
|                 cond_items = [c.strip() |             expression_attribute_values = self.body.get('ExpressionAttributeValues', {}) | ||||||
|                               for c in condition_expression.split('AND')] |             expected = condition_expression_to_expected(condition_expression, | ||||||
| 
 |                                                         expression_attribute_names, | ||||||
|                 if cond_items: |                                                         expression_attribute_values) | ||||||
|                     expected = {} |             if expected: | ||||||
|                 overwrite = False |                 overwrite = False | ||||||
|                     exists_re = re.compile('^attribute_exists\s*\((.*)\)$') |  | ||||||
|                     not_exists_re = re.compile( |  | ||||||
|                         '^attribute_not_exists\s*\((.*)\)$') |  | ||||||
| 
 |  | ||||||
|                 for cond in cond_items: |  | ||||||
|                     exists_m = exists_re.match(cond) |  | ||||||
|                     not_exists_m = not_exists_re.match(cond) |  | ||||||
|                     if exists_m: |  | ||||||
|                         expected[exists_m.group(1)] = {'Exists': True} |  | ||||||
|                     elif not_exists_m: |  | ||||||
|                         expected[not_exists_m.group(1)] = {'Exists': False} |  | ||||||
| 
 | 
 | ||||||
|         try: |         try: | ||||||
|             result = self.dynamodb_backend.put_item(name, item, expected, overwrite) |             result = self.dynamodb_backend.put_item(name, item, expected, overwrite) | ||||||
| @ -590,23 +641,11 @@ class DynamoHandler(BaseResponse): | |||||||
|         # expression |         # expression | ||||||
|         if not expected: |         if not expected: | ||||||
|             condition_expression = self.body.get('ConditionExpression') |             condition_expression = self.body.get('ConditionExpression') | ||||||
|             if condition_expression and 'OR' not in condition_expression: |             expression_attribute_names = self.body.get('ExpressionAttributeNames', {}) | ||||||
|                 cond_items = [c.strip() |             expression_attribute_values = self.body.get('ExpressionAttributeValues', {}) | ||||||
|                               for c in condition_expression.split('AND')] |             expected = condition_expression_to_expected(condition_expression, | ||||||
| 
 |                                                         expression_attribute_names, | ||||||
|                 if cond_items: |                                                         expression_attribute_values) | ||||||
|                     expected = {} |  | ||||||
|                     exists_re = re.compile('^attribute_exists\s*\((.*)\)$') |  | ||||||
|                     not_exists_re = re.compile( |  | ||||||
|                         '^attribute_not_exists\s*\((.*)\)$') |  | ||||||
| 
 |  | ||||||
|                 for cond in cond_items: |  | ||||||
|                     exists_m = exists_re.match(cond) |  | ||||||
|                     not_exists_m = not_exists_re.match(cond) |  | ||||||
|                     if exists_m: |  | ||||||
|                         expected[exists_m.group(1)] = {'Exists': True} |  | ||||||
|                     elif not_exists_m: |  | ||||||
|                         expected[not_exists_m.group(1)] = {'Exists': False} |  | ||||||
| 
 | 
 | ||||||
|         # Support spaces between operators in an update expression |         # Support spaces between operators in an update expression | ||||||
|         # E.g. `a = b + c` -> `a=b+c` |         # E.g. `a = b + c` -> `a=b+c` | ||||||
|  | |||||||
| @ -1505,3 +1505,110 @@ def test_dynamodb_streams_2(): | |||||||
|     assert 'LatestStreamLabel' in resp['TableDescription'] |     assert 'LatestStreamLabel' in resp['TableDescription'] | ||||||
|     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') | ||||||
|  |     dynamodb = boto3.resource('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' | ||||||
|  |         } | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     with assert_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 assert_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 assert_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'} | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user