diff --git a/boto3/dynamodb/conditions.py b/boto3/dynamodb/conditions.py new file mode 100644 index 0000000000..1f38724767 --- /dev/null +++ b/boto3/dynamodb/conditions.py @@ -0,0 +1,416 @@ +# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +from collections import namedtuple +import functools +import re + +from boto3.exceptions import DynanmoDBOperationNotSupportedError +from boto3.exceptions import DynamoDBNeedsConditionError +from boto3.exceptions import DynamoDBNeedsKeyConditionError + + +ATTR_NAME_REGEX = re.compile(r'[^.\[\]]+(?![^\[]*\])') + + +class ConditionBase(object): + + expression_format = '' + expression_operator = '' + has_grouped_values = False + + def __init__(self, *values): + self._values = values + + def __and__(self, other): + if not isinstance(other, ConditionBase): + raise DynanmoDBOperationNotSupportedError('AND', other) + return And(self, other) + + def __or__(self, other): + if not isinstance(other, ConditionBase): + raise DynanmoDBOperationNotSupportedError('OR', other) + return Or(self, other) + + def __invert__(self): + return Not(self) + + def get_expression(self): + return {'format': self.expression_format, + 'operator': self.expression_operator, + 'values': self._values} + + def __eq__(self, other): + if isinstance(other, type(self)): + if self._values == other._values: + return True + return False + + def __ne__(self, other): + return not self.__eq__(other) + + +class AttributeBase(object): + def __init__(self, name): + self.name = name + + def __and__(self, value): + raise DynanmoDBOperationNotSupportedError('AND', self) + + def __or__(self, value): + raise DynanmoDBOperationNotSupportedError('OR', self) + + def __invert__(self): + raise DynanmoDBOperationNotSupportedError('NOT', self) + + def eq(self, value): + """Creates a condtion where the attribute is equal to the value. + + :param value: The value that the attribute is equal to. + """ + return Equals(self, value) + + def lt(self, value): + """Creates a condtion where the attribute is less than the value. + + :param value: The value that the attribute is less than. + """ + return LessThan(self, value) + + def lte(self, value): + """Creates a condtion where the attribute is less than or equal to the + value. + + :param value: The value that the attribute is less than or equal to. + """ + return LessThanEquals(self, value) + + def gt(self, value): + """Creates a condtion where the attribute is greater than the value. + + :param value: The value that the attribute is greater than. + """ + return GreaterThan(self, value) + + def gte(self, value): + """Creates a condtion where the attribute is greater than or equal to + the value. + + :param value: The value that the attribute is greater than or equal to. + """ + return GreaterThanEquals(self, value) + + def begins_with(self, value): + """Creates a condtion where the attribute begins with the value. + + :param value: The value that the attribute begins with. + """ + return BeginsWith(self, value) + + def between(self, low_value, high_value): + """Creates a condtion where the attribute is between the low value and + the high value. + + :param low_value: The value that the attribute is greater than. + :param high_value: The value that the attribute is less than. + """ + return Between(self, low_value, high_value) + + +class ConditionAttributeBase(ConditionBase, AttributeBase): + """This base class is for conditions that can have attribute methods. + + One example is the Size condition. To complete a condition, you need + to apply another AttributeBase method like eq(). + """ + def __init__(self, *values): + ConditionBase.__init__(self, *values) + # This is assuming the first value to the condition is the attribute + # in which can be used to generate its attribute base. + AttributeBase.__init__(self, values[0].name) + + +class ComparisonCondition(ConditionBase): + expression_format = '{0} {operator} {1}' + + +class Equals(ComparisonCondition): + expression_operator = '=' + + +class NotEquals(ComparisonCondition): + expression_operator = '<>' + + +class LessThan(ComparisonCondition): + expression_operator = '<' + + +class LessThanEquals(ComparisonCondition): + expression_operator = '<=' + + +class GreaterThan(ComparisonCondition): + expression_operator = '>' + + +class GreaterThanEquals(ComparisonCondition): + expression_operator = '>=' + + +class In(ComparisonCondition): + expression_operator = 'IN' + has_grouped_values = True + + +class Between(ConditionBase): + expression_operator = 'BETWEEN' + expression_format = '{0} {operator} {1} AND {2}' + + +class BeginsWith(ConditionBase): + expression_operator = 'begins_with' + expression_format = '{operator}({0}, {1})' + + +class Contains(ConditionBase): + expression_operator = 'contains' + expression_format = '{operator}({0}, {1})' + + +class Size(ConditionAttributeBase): + expression_operator = 'size' + expression_format = '{operator}({0})' + + +class AttributeType(ConditionBase): + expression_operator = 'attribute_type' + expression_format = '{operator}({0}, {1})' + + +class AttributeExists(ConditionBase): + expression_operator = 'attribute_exists' + expression_format = '{operator}({0})' + + +class AttributeNotExists(ConditionBase): + expression_operator = 'attribute_not_exists' + expression_format = '{operator}({0})' + + +class And(ConditionBase): + expression_operator = 'AND' + expression_format = '({0} {operator} {1})' + + +class Or(ConditionBase): + expression_operator = 'OR' + expression_format = '({0} {operator} {1})' + + +class Not(ConditionBase): + expression_operator = 'NOT' + expression_format = '({operator} {0})' + + +class Key(AttributeBase): + pass + + +class Attr(AttributeBase): + """Represents an DynamoDB item's attribute.""" + def ne(self, value): + """Creates a condtion where the attribute is not equal to the value + + :param value: The value that the attribute is not equal to. + """ + return NotEquals(self, value) + + def is_in(self, value): + """Creates a condtion where the attribute is in the value, + + :type value: list + :param value: The value that the attribute is in. + """ + return In(self, value) + + def exists(self): + """Creates a condtion where the attribute exists.""" + return AttributeExists(self) + + def not_exists(self): + """Creates a condtion where the attribute does not exist.""" + return AttributeNotExists(self) + + def contains(self, value): + """Creates a condition where the attribute contains the value. + + :param value: The value the attribute contains. + """ + return Contains(self, value) + + def size(self): + """Creates a condition for the attribute size. + + Note another AttributeBase method must be called on the returned + size condition to be a valid DynamoDB condition. + """ + return Size(self) + + def attribute_type(self, value): + """Creates a condition for the attribute type. + + :param value: The type of the attribute. + """ + return AttributeType(self, value) + + +BuiltConditionExpression = namedtuple( + 'BuiltConditionExpression', + ['condition_expression', 'attribute_name_placeholders', + 'attribute_value_placeholders'] +) + + +class ConditionExpressionBuilder(object): + """This class is used to build condition expressions with placeholders""" + def __init__(self): + self._name_count = 0 + self._value_count = 0 + self._name_placeholder = 'n' + self._value_placeholder = 'v' + + def _get_name_placeholder(self): + return '#' + self._name_placeholder + str(self._name_count) + + def _get_value_placeholder(self): + return ':' + self._value_placeholder + str(self._value_count) + + def reset(self): + """Resets the placeholder name and values""" + self._name_count = 0 + self._value_count = 0 + + def build_expression(self, condition, is_key_condition=False): + """Builds the condition expression and the dictionary of placeholders. + + :type condition: ConditionBase + :param condition: A condition to be built into a condition expression + string with any necessary placeholders. + + :type is_key_condition: Boolean + :param is_key_condition: True if the expression is for a + KeyConditionExpression. False otherwise. + + :rtype: (string, dict, dict) + :returns: Will return a string representing the condition with + placeholders inserted where necessary, a dictionary of + placeholders for attribute names, and a dictionary of + placeholders for attribute values. Here is a sample return value: + + ('#n0 = :v0', {'#n0': 'myattribute'}, {':v1': 'myvalue'}) + """ + if not isinstance(condition, ConditionBase): + raise DynamoDBNeedsConditionError(condition) + attribute_name_placeholders = {} + attribute_value_placeholders = {} + condition_expression = self._build_expression( + condition, attribute_name_placeholders, + attribute_value_placeholders, is_key_condition=is_key_condition) + return BuiltConditionExpression( + condition_expression=condition_expression, + attribute_name_placeholders=attribute_name_placeholders, + attribute_value_placeholders=attribute_value_placeholders + ) + + def _build_expression(self, condition, attribute_name_placeholders, + attribute_value_placeholders, is_key_condition): + expression_dict = condition.get_expression() + replaced_values = [] + for value in expression_dict['values']: + # Build the necessary placeholders for that value. + # Placeholders are built for both attribute names and values. + replaced_value = self._build_expression_component( + value, attribute_name_placeholders, + attribute_value_placeholders, condition.has_grouped_values, + is_key_condition) + replaced_values.append(replaced_value) + # Fill out the expression using the operator and the + # values that have been replaced with placeholders. + return expression_dict['format'].format( + *replaced_values, operator=expression_dict['operator']) + + def _build_expression_component(self, value, attribute_name_placeholders, + attribute_value_placeholders, + has_grouped_values, is_key_condition): + # Continue to recurse if the value is a ConditionBase in order + # to extract out all parts of the expression. + if isinstance(value, ConditionBase): + return self._build_expression( + value, attribute_name_placeholders, + attribute_value_placeholders, is_key_condition) + # If it is not a ConditionBase, we can recurse no further. + # So we check if it is an attribute and add placeholders for + # its name + elif isinstance(value, AttributeBase): + if is_key_condition and not isinstance(value, Key): + raise DynamoDBNeedsKeyConditionError( + 'Attribute object %s is of type %s. ' + 'KeyConditionExpression only supports Attribute objects ' + 'of type Key' % (value.name, type(value))) + return self._build_name_placeholder( + value, attribute_name_placeholders) + # If it is anything else, we treat it as a value and thus placeholders + # are needed for the value. + else: + return self._build_value_placeholder( + value, attribute_value_placeholders, has_grouped_values) + + def _build_name_placeholder(self, value, attribute_name_placeholders): + attribute_name = value.name + # Figure out which parts of the attribute name that needs replacement. + attribute_name_parts = ATTR_NAME_REGEX.findall(attribute_name) + + # Add a temporary placeholder for each of these parts. + placeholder_format = ATTR_NAME_REGEX.sub('%s', attribute_name) + str_format_args = [] + for part in attribute_name_parts: + name_placeholder = self._get_name_placeholder() + self._name_count += 1 + str_format_args.append(name_placeholder) + # Add the placeholder and value to dictionary of name placeholders. + attribute_name_placeholders[name_placeholder] = part + # Replace the temporary placeholders with the designated placeholders. + return placeholder_format % tuple(str_format_args) + + def _build_value_placeholder(self, value, attribute_value_placeholders, + has_grouped_values=False): + # If the values are grouped, we need to add a placeholder for + # each element inside of the actual value. + if has_grouped_values: + placeholder_list = [] + for v in value: + value_placeholder = self._get_value_placeholder() + self._value_count += 1 + placeholder_list.append(value_placeholder) + attribute_value_placeholders[value_placeholder] = v + # Assuming the values are grouped by parenthesis. + # IN is the currently the only one that uses this so it maybe + # needed to be changed in future. + return '(' + ', '.join(placeholder_list) + ')' + # Otherwise, treat the value as a single value that needs only + # one placeholder. + else: + value_placeholder = self._get_value_placeholder() + self._value_count += 1 + attribute_value_placeholders[value_placeholder] = value + return value_placeholder diff --git a/boto3/exceptions.py b/boto3/exceptions.py index 75d4a6334d..3e3e7c910e 100644 --- a/boto3/exceptions.py +++ b/boto3/exceptions.py @@ -31,3 +31,28 @@ class S3TransferFailedError(Exception): class S3UploadFailedError(Exception): pass + + +class DynanmoDBOperationNotSupportedError(Exception): + """Raised for operantions that are not supported for an operand""" + def __init__(self, operation, value): + msg = ( + '%s operation cannot be applied to value %s of type %s directly. ' + 'Must use AttributeBase object methods (i.e. Attr().eq()). to ' + 'generate ConditionBase instances first.' % + (operation, value, type(value))) + Exception.__init__(self, msg) + + +class DynamoDBNeedsConditionError(Exception): + """Raised when input is not a condition""" + def __init__(self, value): + msg = ( + 'Expecting a ConditionBase object. Got %s of type %s. ' + 'Use AttributeBase object methods (i.e. Attr().eq()). to ' + 'generate ConditionBase instances.' % (value, type(value))) + Exception.__init__(self, msg) + + +class DynamoDBNeedsKeyConditionError(Exception): + pass diff --git a/tests/unit/dynamodb/test_conditions.py b/tests/unit/dynamodb/test_conditions.py new file mode 100644 index 0000000000..f6e8c82913 --- /dev/null +++ b/tests/unit/dynamodb/test_conditions.py @@ -0,0 +1,473 @@ +# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +from tests import unittest + +from boto3.exceptions import DynanmoDBOperationNotSupportedError +from boto3.exceptions import DynamoDBNeedsConditionError +from boto3.exceptions import DynamoDBNeedsKeyConditionError +from boto3.dynamodb.conditions import Attr, Key +from boto3.dynamodb.conditions import And, Or, Not, Equals, LessThan +from boto3.dynamodb.conditions import LessThanEquals, GreaterThan +from boto3.dynamodb.conditions import GreaterThanEquals, BeginsWith, Between +from boto3.dynamodb.conditions import NotEquals, In, AttributeExists +from boto3.dynamodb.conditions import AttributeNotExists, Contains, Size +from boto3.dynamodb.conditions import AttributeType +from boto3.dynamodb.conditions import ConditionExpressionBuilder + + +class TestK(unittest.TestCase): + def setUp(self): + self.attr = Key('mykey') + self.attr2 = Key('myotherkey') + self.value = 'foo' + self.value2 = 'foo2' + + def test_and(self): + with self.assertRaisesRegexp( + DynanmoDBOperationNotSupportedError, 'AND'): + self.attr & self.attr2 + + def test_or(self): + with self.assertRaisesRegexp( + DynanmoDBOperationNotSupportedError, 'OR'): + self.attr | self.attr2 + + def test_not(self): + with self.assertRaisesRegexp( + DynanmoDBOperationNotSupportedError, 'NOT'): + ~self.attr + + def test_eq(self): + self.assertEqual( + self.attr.eq(self.value), Equals(self.attr, self.value)) + + def test_lt(self): + self.assertEqual( + self.attr.lt(self.value), LessThan(self.attr, self.value)) + + def test_lte(self): + self.assertEqual( + self.attr.lte(self.value), LessThanEquals(self.attr, self.value)) + + def test_gt(self): + self.assertEqual( + self.attr.gt(self.value), GreaterThan(self.attr, self.value)) + + def test_gte(self): + self.assertEqual( + self.attr.gte(self.value), + GreaterThanEquals(self.attr, self.value)) + + def test_begins_with(self): + self.assertEqual(self.attr.begins_with(self.value), + BeginsWith(self.attr, self.value)) + + def test_between(self): + self.assertEqual(self.attr.between(self.value, self.value2), + Between(self.attr, self.value, self.value2)) + + +class TestA(TestK): + def setUp(self): + self.attr = Attr('mykey') + self.attr2 = Attr('myotherkey') + self.value = 'foo' + self.value2 = 'foo2' + + def test_ne(self): + self.assertEqual(self.attr.ne(self.value), + NotEquals(self.attr, self.value)) + + def test_is_in(self): + self.assertEqual(self.attr.is_in([self.value]), + In(self.attr, [self.value])) + + def test_exists(self): + self.assertEqual(self.attr.exists(), AttributeExists(self.attr)) + + def test_not_exists(self): + self.assertEqual(self.attr.not_exists(), AttributeNotExists(self.attr)) + + def test_contains(self): + self.assertEqual(self.attr.contains(self.value), + Contains(self.attr, self.value)) + + def test_size(self): + self.assertEqual(self.attr.size(), Size(self.attr)) + + def test_attribute_type(self): + self.assertEqual(self.attr.attribute_type(self.value), + AttributeType(self.attr, self.value)) + + +class TestConditions(unittest.TestCase): + def setUp(self): + self.value = Attr('mykey') + self.value2 = 'foo' + + def build_and_assert_expression(self, condition, + reference_expression_dict): + expression_dict = condition.get_expression() + self.assertDictEqual(expression_dict, reference_expression_dict) + + def test_equal_operator(self): + cond1 = Equals(self.value, self.value2) + cond2 = Equals(self.value, self.value2) + self.assertTrue(cond1 == cond2) + + def test_equal_operator_type(self): + cond1 = Equals(self.value, self.value2) + cond2 = NotEquals(self.value, self.value2) + self.assertFalse(cond1 == cond2) + + def test_equal_operator_value(self): + cond1 = Equals(self.value, self.value2) + cond2 = Equals(self.value, self.value) + self.assertFalse(cond1 == cond2) + + def test_not_equal_operator(self): + cond1 = Equals(self.value, self.value2) + cond2 = NotEquals(self.value, self.value) + self.assertTrue(cond1 != cond2) + + def test_and_operator(self): + cond1 = Equals(self.value, self.value2) + cond2 = Equals(self.value, self.value2) + self.assertEqual(cond1 & cond2, And(cond1, cond2)) + + def test_and_operator_throws_excepetion(self): + cond1 = Equals(self.value, self.value2) + with self.assertRaisesRegexp( + DynanmoDBOperationNotSupportedError, 'AND'): + cond1 & self.value2 + + def test_or_operator(self): + cond1 = Equals(self.value, self.value2) + cond2 = Equals(self.value, self.value2) + self.assertEqual(cond1 | cond2, Or(cond1, cond2)) + + def test_or_operator_throws_excepetion(self): + cond1 = Equals(self.value, self.value2) + with self.assertRaisesRegexp( + DynanmoDBOperationNotSupportedError, 'OR'): + cond1 | self.value2 + + def test_not_operator(self): + cond1 = Equals(self.value, self.value2) + self.assertEqual(~cond1, Not(cond1)) + + def test_eq(self): + self.build_and_assert_expression( + Equals(self.value, self.value2), + {'format': '{0} {operator} {1}', + 'operator': '=', 'values': (self.value, self.value2)}) + + def test_ne(self): + self.build_and_assert_expression( + NotEquals(self.value, self.value2), + {'format': '{0} {operator} {1}', + 'operator': '<>', 'values': (self.value, self.value2)}) + + def test_lt(self): + self.build_and_assert_expression( + LessThan(self.value, self.value2), + {'format': '{0} {operator} {1}', + 'operator': '<', 'values': (self.value, self.value2)}) + + def test_lte(self): + self.build_and_assert_expression( + LessThanEquals(self.value, self.value2), + {'format': '{0} {operator} {1}', + 'operator': '<=', 'values': (self.value, self.value2)}) + + def test_gt(self): + self.build_and_assert_expression( + GreaterThan(self.value, self.value2), + {'format': '{0} {operator} {1}', + 'operator': '>', 'values': (self.value, self.value2)}) + + def test_gte(self): + self.build_and_assert_expression( + GreaterThanEquals(self.value, self.value2), + {'format': '{0} {operator} {1}', + 'operator': '>=', 'values': (self.value, self.value2)}) + + def test_in(self): + cond = In(self.value, (self.value2)) + self.build_and_assert_expression( + cond, + {'format': '{0} {operator} {1}', + 'operator': 'IN', 'values': (self.value, (self.value2))}) + self.assertTrue(cond.has_grouped_values) + + def test_bet(self): + self.build_and_assert_expression( + Between(self.value, self.value2, 'foo2'), + {'format': '{0} {operator} {1} AND {2}', + 'operator': 'BETWEEN', + 'values': (self.value, self.value2, 'foo2')}) + + def test_beg(self): + self.build_and_assert_expression( + BeginsWith(self.value, self.value2), + {'format': '{operator}({0}, {1})', + 'operator': 'begins_with', 'values': (self.value, self.value2)}) + + def test_cont(self): + self.build_and_assert_expression( + Contains(self.value, self.value2), + {'format': '{operator}({0}, {1})', + 'operator': 'contains', 'values': (self.value, self.value2)}) + + def test_ae(self): + self.build_and_assert_expression( + AttributeExists(self.value), + {'format': '{operator}({0})', + 'operator': 'attribute_exists', 'values': (self.value,)}) + + def test_ane(self): + self.build_and_assert_expression( + AttributeNotExists(self.value), + {'format': '{operator}({0})', + 'operator': 'attribute_not_exists', 'values': (self.value,)}) + + def test_size(self): + self.build_and_assert_expression( + Size(self.value), + {'format': '{operator}({0})', + 'operator': 'size', 'values': (self.value,)}) + + def test_size_can_use_attr_methods(self): + size = Size(self.value) + self.build_and_assert_expression( + size.eq(self.value), + {'format': '{0} {operator} {1}', + 'operator': '=', 'values': (size, self.value)}) + + def test_size_can_use_and(self): + size = Size(self.value) + ae = AttributeExists(self.value) + self.build_and_assert_expression( + size & ae, + {'format': '({0} {operator} {1})', + 'operator': 'AND', 'values': (size, ae)}) + + def test_attribute_type(self): + self.build_and_assert_expression( + AttributeType(self.value, self.value2), + {'format': '{operator}({0}, {1})', + 'operator': 'attribute_type', + 'values': (self.value, self.value2)}) + + def test_and(self): + cond1 = Equals(self.value, self.value2) + cond2 = Equals(self.value, self.value2) + and_cond = And(cond1, cond2) + self.build_and_assert_expression( + and_cond, + {'format': '({0} {operator} {1})', + 'operator': 'AND', 'values': (cond1, cond2)}) + + def test_or(self): + cond1 = Equals(self.value, self.value2) + cond2 = Equals(self.value, self.value2) + or_cond = Or(cond1, cond2) + self.build_and_assert_expression( + or_cond, + {'format': '({0} {operator} {1})', + 'operator': 'OR', 'values': (cond1, cond2)}) + + def test_not(self): + cond = Equals(self.value, self.value2) + not_cond = Not(cond) + self.build_and_assert_expression( + not_cond, + {'format': '({operator} {0})', + 'operator': 'NOT', 'values': (cond,)}) + + +class TestConditionExpressionBuilder(unittest.TestCase): + def setUp(self): + self.builder = ConditionExpressionBuilder() + + def assert_condition_expression_build( + self, condition, ref_string, ref_names, ref_values, + is_key_condition=False): + exp_string, names, values = self.builder.build_expression( + condition, is_key_condition=is_key_condition) + self.assertEqual(exp_string, ref_string) + self.assertEqual(names, ref_names) + self.assertEqual(values, ref_values) + + def test_bad_input(self): + a = Attr('myattr') + with self.assertRaises(DynamoDBNeedsConditionError): + self.builder.build_expression(a) + + def test_build_expression_eq(self): + a = Attr('myattr') + self.assert_condition_expression_build( + a.eq('foo'), '#n0 = :v0', {'#n0': 'myattr'}, {':v0': 'foo'}) + + def test_reset(self): + a = Attr('myattr') + self.assert_condition_expression_build( + a.eq('foo'), '#n0 = :v0', {'#n0': 'myattr'}, {':v0': 'foo'}) + + self.assert_condition_expression_build( + a.eq('foo'), '#n1 = :v1', {'#n1': 'myattr'}, {':v1': 'foo'}) + + self.builder.reset() + self.assert_condition_expression_build( + a.eq('foo'), '#n0 = :v0', {'#n0': 'myattr'}, {':v0': 'foo'}) + + def test_build_expression_lt(self): + a = Attr('myattr') + self.assert_condition_expression_build( + a.lt('foo'), '#n0 < :v0', {'#n0': 'myattr'}, {':v0': 'foo'}) + + def test_build_expression_lte(self): + a1 = Attr('myattr') + self.assert_condition_expression_build( + a1.lte('foo'), '#n0 <= :v0', {'#n0': 'myattr'}, {':v0': 'foo'}) + + def test_build_expression_gt(self): + a = Attr('myattr') + self.assert_condition_expression_build( + a.gt('foo'), '#n0 > :v0', {'#n0': 'myattr'}, {':v0': 'foo'}) + + def test_build_expression_gte(self): + a = Attr('myattr') + self.assert_condition_expression_build( + a.gte('foo'), '#n0 >= :v0', {'#n0': 'myattr'}, {':v0': 'foo'}) + + def test_build_expression_begins_with(self): + a = Attr('myattr') + self.assert_condition_expression_build( + a.begins_with('foo'), 'begins_with(#n0, :v0)', + {'#n0': 'myattr'}, {':v0': 'foo'}) + + def test_build_expression_between(self): + a = Attr('myattr') + self.assert_condition_expression_build( + a.between('foo', 'foo2'), '#n0 BETWEEN :v0 AND :v1', + {'#n0': 'myattr'}, {':v0': 'foo', ':v1': 'foo2'}) + + def test_build_expression_ne(self): + a = Attr('myattr') + self.assert_condition_expression_build( + a.ne('foo'), '#n0 <> :v0', {'#n0': 'myattr'}, {':v0': 'foo'}) + + def test_build_expression_in(self): + a = Attr('myattr') + self.assert_condition_expression_build( + a.is_in([1, 2, 3]), '#n0 IN (:v0, :v1, :v2)', + {'#n0': 'myattr'}, {':v0': 1, ':v1': 2, ':v2': 3}) + + def test_build_expression_exists(self): + a = Attr('myattr') + self.assert_condition_expression_build( + a.exists(), 'attribute_exists(#n0)', {'#n0': 'myattr'}, {}) + + def test_build_expression_not_exists(self): + a = Attr('myattr') + self.assert_condition_expression_build( + a.not_exists(), 'attribute_not_exists(#n0)', {'#n0': 'myattr'}, {}) + + def test_build_contains(self): + a = Attr('myattr') + self.assert_condition_expression_build( + a.contains('foo'), 'contains(#n0, :v0)', + {'#n0': 'myattr'}, {':v0': 'foo'}) + + def test_build_size(self): + a = Attr('myattr') + self.assert_condition_expression_build( + a.size(), 'size(#n0)', {'#n0': 'myattr'}, {}) + + def test_build_size_with_other_conditons(self): + a = Attr('myattr') + self.assert_condition_expression_build( + a.size().eq(5), 'size(#n0) = :v0', {'#n0': 'myattr'}, {':v0': 5}) + + def test_build_attribute_type(self): + a = Attr('myattr') + self.assert_condition_expression_build( + a.attribute_type('foo'), 'attribute_type(#n0, :v0)', + {'#n0': 'myattr'}, {':v0': 'foo'}) + + def test_build_and(self): + a = Attr('myattr') + a2 = Attr('myattr2') + self.assert_condition_expression_build( + a.eq('foo') & a2.eq('bar'), '(#n0 = :v0 AND #n1 = :v1)', + {'#n0': 'myattr', '#n1': 'myattr2'}, {':v0': 'foo', ':v1': 'bar'}) + + def test_build_or(self): + a = Attr('myattr') + a2 = Attr('myattr2') + self.assert_condition_expression_build( + a.eq('foo') | a2.eq('bar'), '(#n0 = :v0 OR #n1 = :v1)', + {'#n0': 'myattr', '#n1': 'myattr2'}, {':v0': 'foo', ':v1': 'bar'}) + + def test_build_not(self): + a = Attr('myattr') + self.assert_condition_expression_build( + ~a.eq('foo'), '(NOT #n0 = :v0)', + {'#n0': 'myattr'}, {':v0': 'foo'}) + + def test_build_attribute_with_attr_value(self): + a = Attr('myattr') + value = Attr('myreference') + self.assert_condition_expression_build( + a.eq(value), '#n0 = #n1', + {'#n0': 'myattr', '#n1': 'myreference'}, {}) + + def test_build_with_is_key_condition(self): + k = Key('myattr') + self.assert_condition_expression_build( + k.eq('foo'), '#n0 = :v0', + {'#n0': 'myattr'}, {':v0': 'foo'}, is_key_condition=True) + + def test_build_with_is_key_condition_throws_error(self): + a = Attr('myattr') + with self.assertRaises(DynamoDBNeedsKeyConditionError): + self.builder.build_expression(a.eq('foo'), is_key_condition=True) + + def test_build_attr_map(self): + a = Attr('MyMap.MyKey') + self.assert_condition_expression_build( + a.eq('foo'), '#n0.#n1 = :v0', {'#n0': 'MyMap', '#n1': 'MyKey'}, + {':v0': 'foo'}) + + def test_build_attr_list(self): + a = Attr('MyList[0]') + self.assert_condition_expression_build( + a.eq('foo'), '#n0[0] = :v0', {'#n0': 'MyList'}, {':v0': 'foo'}) + + def test_build_nested_attr_map_list(self): + a = Attr('MyMap.MyList[2].MyElement') + self.assert_condition_expression_build( + a.eq('foo'), '#n0.#n1[2].#n2 = :v0', + {'#n0': 'MyMap', '#n1': 'MyList', '#n2': 'MyElement'}, + {':v0': 'foo'}) + + def test_build_double_nested_and_or(self): + a = Attr('myattr') + a2 = Attr('myattr2') + self.assert_condition_expression_build( + (a.eq('foo') & a2.eq('foo2')) | (a.eq('bar') & a2.eq('bar2')), + '((#n0 = :v0 AND #n1 = :v1) OR (#n2 = :v2 AND #n3 = :v3))', + {'#n0': 'myattr', '#n1': 'myattr2', '#n2': 'myattr', + '#n3': 'myattr2'}, + {':v0': 'foo', ':v1': 'foo2', ':v2': 'bar', ':v3': 'bar2'})