36
36
CONDITIONAL_OPERATORS , NULL , NOT_NULL , SHORT_ATTR_TYPES , DELETE ,
37
37
ITEMS , DEFAULT_ENCODING , BINARY_SHORT , BINARY_SET_SHORT , LAST_EVALUATED_KEY , RESPONSES , UNPROCESSED_KEYS ,
38
38
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 )
40
41
from pynamodb .exceptions import (
41
42
TableError , QueryError , PutError , DeleteError , UpdateError , GetError , ScanError , TableDoesNotExist ,
42
43
VerboseClientError
@@ -134,11 +135,11 @@ def get_attribute_type(self, attribute_name, value=None):
134
135
for attr in self .data .get (ATTR_DEFINITIONS ):
135
136
if attr .get (ATTR_NAME ) == attribute_name :
136
137
return attr .get (ATTR_TYPE )
137
- attr_names = [attr .get (ATTR_NAME ) for attr in self .data .get (ATTR_DEFINITIONS )]
138
138
if value is not None and isinstance (value , dict ):
139
139
for key in SHORT_ATTR_TYPES :
140
140
if key in value :
141
141
return key
142
+ attr_names = [attr .get (ATTR_NAME ) for attr in self .data .get (ATTR_DEFINITIONS )]
142
143
raise ValueError ("No attribute {0} in {1}" .format (attribute_name , attr_names ))
143
144
144
145
def get_identifier_map (self , hash_key , range_key = None , key = KEY ):
@@ -782,17 +783,26 @@ def delete_item(self,
782
783
"""
783
784
operation_kwargs = {TABLE_NAME : table_name }
784
785
operation_kwargs .update (self .get_identifier_map (table_name , hash_key , range_key ))
786
+ name_placeholders = {}
787
+ expression_attribute_values = {}
785
788
786
- if expected :
787
- operation_kwargs .update (self .get_expected_map (table_name , expected ))
788
789
if return_values :
789
790
operation_kwargs .update (self .get_return_values_map (return_values ))
790
791
if return_consumed_capacity :
791
792
operation_kwargs .update (self .get_consumed_capacity_map (return_consumed_capacity ))
792
793
if return_item_collection_metrics :
793
794
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
+
796
806
try :
797
807
return self .dispatch (DELETE_ITEM , operation_kwargs )
798
808
except BOTOCORE_EXCEPTIONS as e :
@@ -813,16 +823,15 @@ def update_item(self,
813
823
"""
814
824
operation_kwargs = {TABLE_NAME : table_name }
815
825
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
+
818
829
if return_consumed_capacity :
819
830
operation_kwargs .update (self .get_consumed_capacity_map (return_consumed_capacity ))
820
831
if return_item_collection_metrics :
821
832
operation_kwargs .update (self .get_item_collection_map (return_item_collection_metrics ))
822
833
if return_values :
823
834
operation_kwargs .update (self .get_return_values_map (return_values ))
824
- if conditional_operator :
825
- operation_kwargs .update (self .get_conditional_operator (conditional_operator ))
826
835
if not attribute_updates :
827
836
raise ValueError ("{0} cannot be empty" .format (ATTR_UPDATES ))
828
837
@@ -840,6 +849,18 @@ def update_item(self,
840
849
}
841
850
if action .upper () != DELETE :
842
851
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
+
843
864
try :
844
865
return self .dispatch (UPDATE_ITEM , operation_kwargs )
845
866
except BOTOCORE_EXCEPTIONS as e :
@@ -860,6 +881,9 @@ def put_item(self,
860
881
"""
861
882
operation_kwargs = {TABLE_NAME : table_name }
862
883
operation_kwargs .update (self .get_identifier_map (table_name , hash_key , range_key , key = ITEM ))
884
+ name_placeholders = {}
885
+ expression_attribute_values = {}
886
+
863
887
if attributes :
864
888
attrs = self .get_item_attribute_map (table_name , attributes )
865
889
operation_kwargs [ITEM ].update (attrs [ITEM ])
@@ -869,10 +893,17 @@ def put_item(self,
869
893
operation_kwargs .update (self .get_item_collection_map (return_item_collection_metrics ))
870
894
if return_values :
871
895
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 )
872
898
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
+
876
907
try :
877
908
return self .dispatch (PUT_ITEM , operation_kwargs )
878
909
except BOTOCORE_EXCEPTIONS as e :
@@ -1141,11 +1172,11 @@ def scan(self,
1141
1172
"""
1142
1173
operation_kwargs = {TABLE_NAME : table_name }
1143
1174
name_placeholders = {}
1175
+ expression_attribute_values = {}
1176
+
1144
1177
if attributes_to_get is not None :
1145
1178
projection_expression = create_projection_expression (attributes_to_get , name_placeholders )
1146
1179
operation_kwargs [PROJECTION_EXPRESSION ] = projection_expression
1147
- if name_placeholders :
1148
- operation_kwargs [EXPRESSION_ATTRIBUTE_NAMES ] = self ._reverse_dict (name_placeholders )
1149
1180
if limit is not None :
1150
1181
operation_kwargs [LIMIT ] = limit
1151
1182
if return_consumed_capacity :
@@ -1157,24 +1188,17 @@ def scan(self,
1157
1188
if total_segments :
1158
1189
operation_kwargs [TOTAL_SEGMENTS ] = total_segments
1159
1190
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
1176
1195
if consistent_read :
1177
1196
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
+
1178
1202
try :
1179
1203
return self .dispatch (SCAN , operation_kwargs )
1180
1204
except BOTOCORE_EXCEPTIONS as e :
@@ -1211,7 +1235,7 @@ def query(self,
1211
1235
else :
1212
1236
hash_keyname = tbl .hash_keyname
1213
1237
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 )
1215
1239
if key_conditions is None or len (key_conditions ) == 0 :
1216
1240
pass # No comparisons on sort key
1217
1241
elif len (key_conditions ) > 1 :
@@ -1223,7 +1247,7 @@ def query(self,
1223
1247
raise ValueError ("{0} must be one of {1}" .format (COMPARISON_OPERATOR , COMPARISON_OPERATOR_VALUES ))
1224
1248
operator = KEY_CONDITION_OPERATOR_MAP [operator ]
1225
1249
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 )
1227
1251
key_condition_expression = key_condition_expression & sort_key_expression
1228
1252
1229
1253
operation_kwargs [KEY_CONDITION_EXPRESSION ] = key_condition_expression .serialize (
@@ -1242,10 +1266,12 @@ def query(self,
1242
1266
operation_kwargs [LIMIT ] = limit
1243
1267
if return_consumed_capacity :
1244
1268
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 )
1245
1271
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
1249
1275
if select :
1250
1276
if select .upper () not in SELECT_VALUES :
1251
1277
raise ValueError ("{0} must be one of {1}" .format (SELECT , SELECT_VALUES ))
@@ -1262,10 +1288,77 @@ def query(self,
1262
1288
except BOTOCORE_EXCEPTIONS as e :
1263
1289
raise QueryError ("Failed to query items: {0}" .format (e ), e )
1264
1290
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 )
1269
1362
1270
1363
@staticmethod
1271
1364
def _reverse_dict (d ):
0 commit comments