Skip to content

Commit 46112de

Browse files
authored
Replace Expected / QueryFilter / ScanFilter with ConditionExpresion / FilterExpression (#333)
1 parent 0dbac0d commit 46112de

File tree

8 files changed

+441
-289
lines changed

8 files changed

+441
-289
lines changed

pynamodb/attributes.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from dateutil.tz import tzutc
1111
from pynamodb.constants import (
1212
STRING, STRING_SHORT, NUMBER, BINARY, UTC, DATETIME_FORMAT, BINARY_SET, STRING_SET, NUMBER_SET,
13-
MAP, MAP_SHORT, LIST, LIST_SHORT, DEFAULT_ENCODING, BOOLEAN, ATTR_TYPE_MAP, NUMBER_SHORT, NULL
13+
MAP, MAP_SHORT, LIST, LIST_SHORT, DEFAULT_ENCODING, BOOLEAN, ATTR_TYPE_MAP, NUMBER_SHORT, NULL, SHORT_ATTR_TYPES
1414
)
1515
from pynamodb.expressions.condition import Path
1616
import collections
@@ -130,7 +130,17 @@ def __getitem__(self, idx):
130130
raise TypeError("'{0}' object has no attribute __getitem__".format(self.attribute.__class__.__name__))
131131
return super(AttributePath, self).__getitem__(idx)
132132

133+
def contains(self, item):
134+
if self.attribute.attr_type in [BINARY_SET, NUMBER_SET, STRING_SET]:
135+
# Set attributes assume the values to be serialized are sets.
136+
(attr_type, attr_value), = self._serialize([item]).items()
137+
item = {attr_type[0]: attr_value[0]}
138+
return super(AttributePath, self).contains(item)
139+
133140
def _serialize(self, value):
141+
# Check to see if value is already serialized
142+
if isinstance(value, dict) and len(value) == 1 and list(value.keys())[0] in SHORT_ATTR_TYPES:
143+
return value
134144
if self.attribute.attr_type == LIST and not isinstance(value, list):
135145
# List attributes assume the values to be serialized are lists.
136146
return self.attribute.serialize([value])[0]

pynamodb/connection/base.py

Lines changed: 133 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@
3636
CONDITIONAL_OPERATORS, NULL, NOT_NULL, SHORT_ATTR_TYPES, DELETE,
3737
ITEMS, DEFAULT_ENCODING, BINARY_SHORT, BINARY_SET_SHORT, LAST_EVALUATED_KEY, RESPONSES, UNPROCESSED_KEYS,
3838
UNPROCESSED_ITEMS, STREAM_SPECIFICATION, STREAM_VIEW_TYPE, STREAM_ENABLED,
39-
EXPRESSION_ATTRIBUTE_NAMES, EXPRESSION_ATTRIBUTE_VALUES, KEY_CONDITION_OPERATOR_MAP)
39+
EXPRESSION_ATTRIBUTE_NAMES, EXPRESSION_ATTRIBUTE_VALUES, KEY_CONDITION_OPERATOR_MAP,
40+
CONDITION_EXPRESSION, FILTER_EXPRESSION, FILTER_EXPRESSION_OPERATOR_MAP, NOT_CONTAINS, AND)
4041
from pynamodb.exceptions import (
4142
TableError, QueryError, PutError, DeleteError, UpdateError, GetError, ScanError, TableDoesNotExist,
4243
VerboseClientError
@@ -134,11 +135,11 @@ def get_attribute_type(self, attribute_name, value=None):
134135
for attr in self.data.get(ATTR_DEFINITIONS):
135136
if attr.get(ATTR_NAME) == attribute_name:
136137
return attr.get(ATTR_TYPE)
137-
attr_names = [attr.get(ATTR_NAME) for attr in self.data.get(ATTR_DEFINITIONS)]
138138
if value is not None and isinstance(value, dict):
139139
for key in SHORT_ATTR_TYPES:
140140
if key in value:
141141
return key
142+
attr_names = [attr.get(ATTR_NAME) for attr in self.data.get(ATTR_DEFINITIONS)]
142143
raise ValueError("No attribute {0} in {1}".format(attribute_name, attr_names))
143144

144145
def get_identifier_map(self, hash_key, range_key=None, key=KEY):
@@ -782,17 +783,26 @@ def delete_item(self,
782783
"""
783784
operation_kwargs = {TABLE_NAME: table_name}
784785
operation_kwargs.update(self.get_identifier_map(table_name, hash_key, range_key))
786+
name_placeholders = {}
787+
expression_attribute_values = {}
785788

786-
if expected:
787-
operation_kwargs.update(self.get_expected_map(table_name, expected))
788789
if return_values:
789790
operation_kwargs.update(self.get_return_values_map(return_values))
790791
if return_consumed_capacity:
791792
operation_kwargs.update(self.get_consumed_capacity_map(return_consumed_capacity))
792793
if return_item_collection_metrics:
793794
operation_kwargs.update(self.get_item_collection_map(return_item_collection_metrics))
794-
if conditional_operator:
795-
operation_kwargs.update(self.get_conditional_operator(conditional_operator))
795+
# We read the conditional operator even without expected passed in to maintain existing behavior.
796+
conditional_operator = self.get_conditional_operator(conditional_operator or AND)
797+
if expected:
798+
condition_expression = self._get_condition_expression(
799+
table_name, expected, conditional_operator, name_placeholders, expression_attribute_values)
800+
operation_kwargs[CONDITION_EXPRESSION] = condition_expression
801+
if name_placeholders:
802+
operation_kwargs[EXPRESSION_ATTRIBUTE_NAMES] = self._reverse_dict(name_placeholders)
803+
if expression_attribute_values:
804+
operation_kwargs[EXPRESSION_ATTRIBUTE_VALUES] = expression_attribute_values
805+
796806
try:
797807
return self.dispatch(DELETE_ITEM, operation_kwargs)
798808
except BOTOCORE_EXCEPTIONS as e:
@@ -813,16 +823,15 @@ def update_item(self,
813823
"""
814824
operation_kwargs = {TABLE_NAME: table_name}
815825
operation_kwargs.update(self.get_identifier_map(table_name, hash_key, range_key))
816-
if expected:
817-
operation_kwargs.update(self.get_expected_map(table_name, expected))
826+
name_placeholders = {}
827+
expression_attribute_values = {}
828+
818829
if return_consumed_capacity:
819830
operation_kwargs.update(self.get_consumed_capacity_map(return_consumed_capacity))
820831
if return_item_collection_metrics:
821832
operation_kwargs.update(self.get_item_collection_map(return_item_collection_metrics))
822833
if return_values:
823834
operation_kwargs.update(self.get_return_values_map(return_values))
824-
if conditional_operator:
825-
operation_kwargs.update(self.get_conditional_operator(conditional_operator))
826835
if not attribute_updates:
827836
raise ValueError("{0} cannot be empty".format(ATTR_UPDATES))
828837

@@ -840,6 +849,18 @@ def update_item(self,
840849
}
841850
if action.upper() != DELETE:
842851
operation_kwargs[ATTR_UPDATES][key][VALUE] = {attr_type: value}
852+
853+
# We read the conditional operator even without expected passed in to maintain existing behavior.
854+
conditional_operator = self.get_conditional_operator(conditional_operator or AND)
855+
if expected:
856+
condition_expression = self._get_condition_expression(
857+
table_name, expected, conditional_operator, name_placeholders, expression_attribute_values)
858+
operation_kwargs[CONDITION_EXPRESSION] = condition_expression
859+
if name_placeholders:
860+
operation_kwargs[EXPRESSION_ATTRIBUTE_NAMES] = self._reverse_dict(name_placeholders)
861+
if expression_attribute_values:
862+
operation_kwargs[EXPRESSION_ATTRIBUTE_VALUES] = expression_attribute_values
863+
843864
try:
844865
return self.dispatch(UPDATE_ITEM, operation_kwargs)
845866
except BOTOCORE_EXCEPTIONS as e:
@@ -860,6 +881,9 @@ def put_item(self,
860881
"""
861882
operation_kwargs = {TABLE_NAME: table_name}
862883
operation_kwargs.update(self.get_identifier_map(table_name, hash_key, range_key, key=ITEM))
884+
name_placeholders = {}
885+
expression_attribute_values = {}
886+
863887
if attributes:
864888
attrs = self.get_item_attribute_map(table_name, attributes)
865889
operation_kwargs[ITEM].update(attrs[ITEM])
@@ -869,10 +893,17 @@ def put_item(self,
869893
operation_kwargs.update(self.get_item_collection_map(return_item_collection_metrics))
870894
if return_values:
871895
operation_kwargs.update(self.get_return_values_map(return_values))
896+
# We read the conditional operator even without expected passed in to maintain existing behavior.
897+
conditional_operator = self.get_conditional_operator(conditional_operator or AND)
872898
if expected:
873-
operation_kwargs.update(self.get_expected_map(table_name, expected))
874-
if conditional_operator:
875-
operation_kwargs.update(self.get_conditional_operator(conditional_operator))
899+
condition_expression = self._get_condition_expression(
900+
table_name, expected, conditional_operator, name_placeholders, expression_attribute_values)
901+
operation_kwargs[CONDITION_EXPRESSION] = condition_expression
902+
if name_placeholders:
903+
operation_kwargs[EXPRESSION_ATTRIBUTE_NAMES] = self._reverse_dict(name_placeholders)
904+
if expression_attribute_values:
905+
operation_kwargs[EXPRESSION_ATTRIBUTE_VALUES] = expression_attribute_values
906+
876907
try:
877908
return self.dispatch(PUT_ITEM, operation_kwargs)
878909
except BOTOCORE_EXCEPTIONS as e:
@@ -1141,11 +1172,11 @@ def scan(self,
11411172
"""
11421173
operation_kwargs = {TABLE_NAME: table_name}
11431174
name_placeholders = {}
1175+
expression_attribute_values = {}
1176+
11441177
if attributes_to_get is not None:
11451178
projection_expression = create_projection_expression(attributes_to_get, name_placeholders)
11461179
operation_kwargs[PROJECTION_EXPRESSION] = projection_expression
1147-
if name_placeholders:
1148-
operation_kwargs[EXPRESSION_ATTRIBUTE_NAMES] = self._reverse_dict(name_placeholders)
11491180
if limit is not None:
11501181
operation_kwargs[LIMIT] = limit
11511182
if return_consumed_capacity:
@@ -1157,24 +1188,17 @@ def scan(self,
11571188
if total_segments:
11581189
operation_kwargs[TOTAL_SEGMENTS] = total_segments
11591190
if scan_filter:
1160-
operation_kwargs[SCAN_FILTER] = {}
1161-
for key, condition in scan_filter.items():
1162-
operator = condition.get(COMPARISON_OPERATOR)
1163-
if operator not in SCAN_FILTER_VALUES:
1164-
raise ValueError("{0} must be one of {1}".format(COMPARISON_OPERATOR, SCAN_FILTER_VALUES))
1165-
values = []
1166-
for value in condition.get(ATTR_VALUE_LIST, []):
1167-
attr_type = self.get_attribute_type(table_name, key, value)
1168-
values.append({attr_type: self.parse_attribute(value)})
1169-
operation_kwargs[SCAN_FILTER][key] = {
1170-
COMPARISON_OPERATOR: operator
1171-
}
1172-
if len(values):
1173-
operation_kwargs[SCAN_FILTER][key][ATTR_VALUE_LIST] = values
1174-
if conditional_operator:
1175-
operation_kwargs.update(self.get_conditional_operator(conditional_operator))
1191+
conditional_operator = self.get_conditional_operator(conditional_operator or AND)
1192+
filter_expression = self._get_filter_expression(
1193+
table_name, scan_filter, conditional_operator, name_placeholders, expression_attribute_values)
1194+
operation_kwargs[FILTER_EXPRESSION] = filter_expression
11761195
if consistent_read:
11771196
operation_kwargs[CONSISTENT_READ] = consistent_read
1197+
if name_placeholders:
1198+
operation_kwargs[EXPRESSION_ATTRIBUTE_NAMES] = self._reverse_dict(name_placeholders)
1199+
if expression_attribute_values:
1200+
operation_kwargs[EXPRESSION_ATTRIBUTE_VALUES] = expression_attribute_values
1201+
11781202
try:
11791203
return self.dispatch(SCAN, operation_kwargs)
11801204
except BOTOCORE_EXCEPTIONS as e:
@@ -1211,7 +1235,7 @@ def query(self,
12111235
else:
12121236
hash_keyname = tbl.hash_keyname
12131237

1214-
key_condition_expression = self._get_condition_expression(table_name, hash_keyname, '__eq__', hash_key)
1238+
key_condition_expression = self._get_condition(table_name, hash_keyname, '__eq__', hash_key)
12151239
if key_conditions is None or len(key_conditions) == 0:
12161240
pass # No comparisons on sort key
12171241
elif len(key_conditions) > 1:
@@ -1223,7 +1247,7 @@ def query(self,
12231247
raise ValueError("{0} must be one of {1}".format(COMPARISON_OPERATOR, COMPARISON_OPERATOR_VALUES))
12241248
operator = KEY_CONDITION_OPERATOR_MAP[operator]
12251249
values = condition.get(ATTR_VALUE_LIST)
1226-
sort_key_expression = self._get_condition_expression(table_name, key, operator, *values)
1250+
sort_key_expression = self._get_condition(table_name, key, operator, *values)
12271251
key_condition_expression = key_condition_expression & sort_key_expression
12281252

12291253
operation_kwargs[KEY_CONDITION_EXPRESSION] = key_condition_expression.serialize(
@@ -1242,10 +1266,12 @@ def query(self,
12421266
operation_kwargs[LIMIT] = limit
12431267
if return_consumed_capacity:
12441268
operation_kwargs.update(self.get_consumed_capacity_map(return_consumed_capacity))
1269+
# We read the conditional operator even without a query filter passed in to maintain existing behavior.
1270+
conditional_operator = self.get_conditional_operator(conditional_operator or AND)
12451271
if query_filters:
1246-
operation_kwargs.update(self.get_query_filter_map(table_name, query_filters))
1247-
if conditional_operator:
1248-
operation_kwargs.update(self.get_conditional_operator(conditional_operator))
1272+
filter_expression = self._get_filter_expression(
1273+
table_name, query_filters, conditional_operator, name_placeholders, expression_attribute_values)
1274+
operation_kwargs[FILTER_EXPRESSION] = filter_expression
12491275
if select:
12501276
if select.upper() not in SELECT_VALUES:
12511277
raise ValueError("{0} must be one of {1}".format(SELECT, SELECT_VALUES))
@@ -1262,10 +1288,77 @@ def query(self,
12621288
except BOTOCORE_EXCEPTIONS as e:
12631289
raise QueryError("Failed to query items: {0}".format(e), e)
12641290

1265-
def _get_condition_expression(self, table_name, attribute_name, operator, *values):
1266-
attr_type = self.get_attribute_type(table_name, attribute_name)
1267-
values = [{attr_type: self.parse_attribute(value)} for value in values]
1268-
return getattr(Path(attribute_name), operator)(*values)
1291+
def _get_condition_expression(self, table_name, expected, conditional_operator,
1292+
name_placeholders, expression_attribute_values):
1293+
"""
1294+
Builds the ConditionExpression needed for DeleteItem, PutItem, and UpdateItem operations
1295+
"""
1296+
condition_expression = None
1297+
conditional_operator = conditional_operator[CONDITIONAL_OPERATOR]
1298+
# We sort the keys here for determinism. This is mostly done to simplify testing.
1299+
keys = list(expected.keys())
1300+
keys.sort()
1301+
for key in keys:
1302+
condition = expected[key]
1303+
if EXISTS in condition:
1304+
operator = NOT_NULL if condition.get(EXISTS, True) else NULL
1305+
values = []
1306+
elif VALUE in condition:
1307+
operator = EQ
1308+
values = [condition.get(VALUE)]
1309+
else:
1310+
operator = condition.get(COMPARISON_OPERATOR)
1311+
values = condition.get(ATTR_VALUE_LIST, [])
1312+
if operator not in QUERY_FILTER_VALUES:
1313+
raise ValueError("{0} must be one of {1}".format(COMPARISON_OPERATOR, QUERY_FILTER_VALUES))
1314+
not_contains = operator == NOT_CONTAINS
1315+
operator = FILTER_EXPRESSION_OPERATOR_MAP[operator]
1316+
condition = self._get_condition(table_name, key, operator, *values)
1317+
if not_contains:
1318+
condition = ~condition
1319+
if condition_expression is None:
1320+
condition_expression = condition
1321+
elif conditional_operator == AND:
1322+
condition_expression = condition_expression & condition
1323+
else:
1324+
condition_expression = condition_expression | condition
1325+
return condition_expression.serialize(name_placeholders, expression_attribute_values)
1326+
1327+
def _get_filter_expression(self, table_name, filters, conditional_operator,
1328+
name_placeholders, expression_attribute_values):
1329+
"""
1330+
Builds the FilterExpression needed for Query and Scan operations
1331+
"""
1332+
condition_expression = None
1333+
conditional_operator = conditional_operator[CONDITIONAL_OPERATOR]
1334+
# We sort the keys here for determinism. This is mostly done to simplify testing.
1335+
keys = list(filters.keys())
1336+
keys.sort()
1337+
for key in keys:
1338+
condition = filters[key]
1339+
operator = condition.get(COMPARISON_OPERATOR)
1340+
if operator not in QUERY_FILTER_VALUES:
1341+
raise ValueError("{0} must be one of {1}".format(COMPARISON_OPERATOR, QUERY_FILTER_VALUES))
1342+
not_contains = operator == NOT_CONTAINS
1343+
operator = FILTER_EXPRESSION_OPERATOR_MAP[operator]
1344+
values = condition.get(ATTR_VALUE_LIST, [])
1345+
condition = self._get_condition(table_name, key, operator, *values)
1346+
if not_contains:
1347+
condition = ~condition
1348+
if condition_expression is None:
1349+
condition_expression = condition
1350+
elif conditional_operator == AND:
1351+
condition_expression = condition_expression & condition
1352+
else:
1353+
condition_expression = condition_expression | condition
1354+
return condition_expression.serialize(name_placeholders, expression_attribute_values)
1355+
1356+
def _get_condition(self, table_name, attribute_name, operator, *values):
1357+
values = [
1358+
{self.get_attribute_type(table_name, attribute_name, value): self.parse_attribute(value)}
1359+
for value in values
1360+
]
1361+
return getattr(Path(attribute_name, attribute_name=True), operator)(*values)
12691362

12701363
@staticmethod
12711364
def _reverse_dict(d):

pynamodb/constants.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,10 @@
6363
KEY = 'Key'
6464

6565
# Expression Parameters
66+
CONDITION_EXPRESSION = 'ConditionExpression'
6667
EXPRESSION_ATTRIBUTE_NAMES = 'ExpressionAttributeNames'
6768
EXPRESSION_ATTRIBUTE_VALUES = 'ExpressionAttributeValues'
69+
FILTER_EXPRESSION = 'FilterExpression'
6870
KEY_CONDITION_EXPRESSION = 'KeyConditionExpression'
6971
PROJECTION_EXPRESSION = 'ProjectionExpression'
7072

@@ -216,6 +218,21 @@
216218
SCAN_FILTER_VALUES = [EQ, NE, LE, LT, GE, GT, NOT_NULL, NULL, CONTAINS, NOT_CONTAINS, BEGINS_WITH, IN, BETWEEN]
217219
QUERY_FILTER_VALUES = SCAN_FILTER_VALUES
218220
DELETE_FILTER_VALUES = SCAN_FILTER_VALUES
221+
FILTER_EXPRESSION_OPERATOR_MAP = {
222+
EQ: '__eq__',
223+
NE: '__ne__',
224+
LE: '__le__',
225+
LT: '__lt__',
226+
GE: '__ge__',
227+
GT: '__gt__',
228+
NOT_NULL: 'exists',
229+
NULL: 'not_exists',
230+
CONTAINS: 'contains',
231+
NOT_CONTAINS: 'contains', # special cased
232+
BEGINS_WITH: 'startswith',
233+
IN: 'is_in',
234+
BETWEEN: 'between'
235+
}
219236

220237

221238
# These are constants used in the expected condition for PutItem

pynamodb/expressions/condition.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from copy import copy
2-
from pynamodb.constants import AND, ATTR_TYPE_MAP, BETWEEN, IN, OR, SHORT_ATTR_TYPES, STRING_SHORT
2+
from pynamodb.constants import (
3+
AND, ATTR_TYPE_MAP, BETWEEN, BINARY_SHORT, IN, NUMBER_SHORT, OR, SHORT_ATTR_TYPES, STRING_SHORT
4+
)
35
from pynamodb.expressions.util import get_value_placeholder, substitute_names
46
from six.moves import range
57

@@ -68,7 +70,7 @@ def __init__(self, path):
6870
def _serialize(self, value):
6971
if not isinstance(value, int):
7072
raise TypeError("size must be compared to an integer, not {0}".format(type(value).__name__))
71-
return {'N': str(value)}
73+
return {NUMBER_SHORT: str(value)}
7274

7375
def __str__(self):
7476
return "size({0})".format(self.path)
@@ -235,8 +237,8 @@ class Contains(Condition):
235237

236238
def __init__(self, path, item):
237239
(attr_type, value), = item.items()
238-
if attr_type != STRING_SHORT:
239-
raise ValueError("{0} must be a string".format(value))
240+
if attr_type not in [BINARY_SHORT, NUMBER_SHORT, STRING_SHORT]:
241+
raise ValueError("{0} must be a string, number, or binary element".format(value))
240242
super(Contains, self).__init__(path, 'contains', item)
241243

242244

0 commit comments

Comments
 (0)