|
62 | 62 | "contains(One, :oneA)": Attr("One").contains(":oneA"),
|
63 | 63 | "contains(One, :oneB)": Attr("One").contains(":oneB"),
|
64 | 64 | # Hard-coding returning the input string for these cases.
|
65 |
| - # These conditions test "undocumented behavior" in DynamoDB that can't be easily expressed with boto3 Conditions. |
66 |
| - # The "undocumented behavior" is that `contains`' first parameter can be a value, |
| 65 | + # These conditions test undocumented behavior in DynamoDB that can't be expressed with boto3 Conditions. |
| 66 | + # The undocumented behavior is that `contains`' first parameter can be a value, |
67 | 67 | # and does not need to be an attribute name.
|
68 | 68 | # DynamoDB documentation names `contains`' first argument as `path`,
|
69 | 69 | # and only ever documents accepting an attribute name for `path`.
|
70 | 70 | # However, testing with an AWS SDK reveals that `path` can be a value;
|
71 | 71 | # i.e. a hardcoded string or an attribute value,
|
72 | 72 | # so this expression is valid.
|
73 | 73 | # But I can't find a way to express this via boto3 Conditions,
|
74 |
| - # where Contains expects to have some attribute name. |
| 74 | + # where Contains requires an attribute name. |
75 | 75 | # For these strings, do not attempt to convert to boto3 conditions,
|
76 | 76 | # and just return the input string.
|
77 | 77 | # The input string is still passed to the table and tested.
|
|
95 | 95 | "Comp1 = :cmp1b": Attr("Comp1").eq(":cmp1b"),
|
96 | 96 | }
|
97 | 97 |
|
98 |
| - |
99 |
| -def convert_client_expression_to_conditions(expression): |
100 |
| - """ |
101 |
| - Crypto Tools internal method to convert a DynamoDB filter/key expression to boto3 Resource tokens. |
102 |
| -
|
103 |
| - THIS SHOULD NOT BE USED BY ANY EXTERNAL USERS. |
104 |
| - This is a basic implementation for simple expressions that will fail with complex expressions. |
105 |
| - |
106 |
| - I have two suggestions for extending this to support more complex expressions: |
107 |
| -
|
108 |
| - 1) To support one or a few complex expressions, consider extending the existing logic. |
109 |
| -
|
110 |
| - 2) To support all expressions, consider extending DBESDK for DynamoDB's generated Dafny-Python code. |
111 |
| - DBESDK for DynamoDB's generated Dafny-Python code has a DynamoDB filter/conditions expression syntax parser. |
112 |
| -
|
113 |
| - Stub code for using the parser from Dafny: |
114 |
| - ``` |
115 |
| - from aws_dbesdk_dynamodb.internaldafny.generated.DynamoDBFilterExpr import default__ as filter_expr |
116 |
| - import _dafny |
117 |
| - from smithy_dafny_standard_library.internaldafny.generated import Wrappers |
118 |
| -
|
119 |
| - dafny_expr_token = filter_expr.ParseExpr( |
120 |
| - _dafny.Seq( |
121 |
| - expression |
122 |
| - ), |
123 |
| - ) |
124 |
| - ``` |
125 |
| - This will parse a _dafny.Seq of an expression and produce Dafny tokens for the expression. |
126 |
| -
|
127 |
| - Reusing this parser and extending it to support boto3 tokens this will involve: |
128 |
| -
|
129 |
| - 1. Mapping Dafny tokens to boto3 Resource tokens. |
130 |
| - (e.g. Dafny class Token_Between -> boto3.dynamodb.conditions.Between) |
131 |
| - 2. Converting Dafny token grammar to boto3 Resource token grammar. |
132 |
| - (e.g. |
133 |
| - Dafny: [Token_Between, Token_Open, Token_Attr, Token_And, Token_Attr, Token_Close] |
134 |
| - -> |
135 |
| - boto3: [Between(Attr, Attr)] |
136 |
| - ) |
137 |
| -
|
138 |
| - :param expression: A string of the DynamoDB client expression (e.g., "AttrName = :val"). |
139 |
| - :return: A boto3.dynamodb.conditions object (Key, Attr, or a combination of them). |
140 |
| - """ |
141 |
| - |
142 |
| - # Recursive parser for complex expressions |
143 |
| - def parse_expression(expr_tokens): |
144 |
| - # simple between |
145 |
| - if "BETWEEN" == expr_tokens[1].upper(): |
146 |
| - attr_name = expr_tokens[0] |
147 |
| - value1 = expr_tokens[2] |
148 |
| - value2 = expr_tokens[4] |
149 |
| - return Key(attr_name).between(value1, value2) |
150 |
| - |
151 |
| - # simple in |
152 |
| - elif "IN" == expr_tokens[1].upper(): |
153 |
| - print(f"IN {expr_tokens=}") |
154 |
| - attr_name = expr_tokens[0] |
155 |
| - values_in_list = expr_tokens[3:-1] |
156 |
| - for i in range(len(values_in_list)): |
157 |
| - if values_in_list[i][-1] == ",": |
158 |
| - values_in_list[i] = values_in_list[i][:-1] |
159 |
| - return Attr(attr_name).is_in(values_in_list) |
160 |
| - |
161 |
| - # simple contains |
162 |
| - elif "CONTAINS" == expr_tokens[0].upper(): |
163 |
| - attr_name = expr_tokens[2] |
164 |
| - if attr_name[-1] == ",": |
165 |
| - attr_name = attr_name[:-1] |
166 |
| - value = expr_tokens[3] |
167 |
| - return Attr(attr_name).contains(value) |
168 |
| - |
169 |
| - # simple begins_with |
170 |
| - elif "BEGINS_WITH" == expr_tokens[0].upper(): |
171 |
| - attr_name = expr_tokens[2] |
172 |
| - if attr_name[-1] == ",": |
173 |
| - attr_name = attr_name[:-1] |
174 |
| - value = expr_tokens[3] |
175 |
| - return Attr(attr_name).begins_with(value) |
176 |
| - |
177 |
| - # Base case: Single comparison or condition |
178 |
| - if "AND" not in [t.upper() for t in expr_tokens] and "OR" not in [t.upper() for t in expr_tokens]: |
179 |
| - |
180 |
| - # simple comparison |
181 |
| - attr_name = expr_tokens[0] |
182 |
| - operator = expr_tokens[1].upper() |
183 |
| - value = expr_tokens[2] |
184 |
| - |
185 |
| - # Map operator to Key or Attr |
186 |
| - if operator == "=": |
187 |
| - return Key(attr_name).eq(value) |
188 |
| - elif operator == "<": |
189 |
| - return Key(attr_name).lt(value) |
190 |
| - elif operator == "<=": |
191 |
| - return Key(attr_name).lte(value) |
192 |
| - elif operator == ">": |
193 |
| - return Key(attr_name).gt(value) |
194 |
| - elif operator == ">=": |
195 |
| - return Key(attr_name).gte(value) |
196 |
| - elif operator in ("!=", "<>"): |
197 |
| - return Attr(attr_name).ne(value) |
198 |
| - else: |
199 |
| - raise ValueError(f"Unsupported operator: {operator}") |
200 |
| - |
201 |
| - # Recursive case: Logical AND/OR |
202 |
| - stack = [] |
203 |
| - current_expr = [] |
204 |
| - |
205 |
| - for token in expr_tokens: |
206 |
| - if token.upper() in ("AND", "OR"): |
207 |
| - left = parse_expression(current_expr) |
208 |
| - current_expr = [] |
209 |
| - stack.append((left, token)) # Save the left condition and operator |
210 |
| - else: |
211 |
| - current_expr.append(token) |
212 |
| - |
213 |
| - # Handle the final condition on the right |
214 |
| - right = parse_expression(current_expr) |
215 |
| - |
216 |
| - # Combine the stack of conditions |
217 |
| - while stack: |
218 |
| - left, operator = stack.pop() |
219 |
| - if operator.upper() == "AND": |
220 |
| - right = And(left, right) |
221 |
| - elif operator.upper() == "OR": |
222 |
| - right = Or(left, right) |
223 |
| - |
224 |
| - return right |
225 |
| - |
226 |
| - # Tokenize the expression and parse it |
227 |
| - tokens = expression.replace("(", " ( ").replace(")", " ) ").split() |
228 |
| - return parse_expression(tokens) |
229 |
| - |
230 | 98 | # TestVectors-only override of ._flush method:
|
231 | 99 | # persist response in self._response for TestVectors output processing.
|
232 | 100 | def _flush_and_persist_response(self):
|
@@ -278,7 +146,11 @@ def get_item(self, **kwargs):
|
278 | 146 | return client_output
|
279 | 147 |
|
280 | 148 | def batch_write_item(self, **kwargs):
|
281 |
| - # print(f"batch_write_item {kwargs=}") |
| 149 | + table_input = self._client_shape_to_resource_shape_converter.batch_write_item_request(kwargs) |
| 150 | + table_output = self._table.batch_write_item(**table_input) |
| 151 | + client_output = self._resource_shape_to_client_shape_converter.batch_write_item_response(table_output) |
| 152 | + return client_output |
| 153 | + |
282 | 154 | # Parse boto3 client.batch_write_item input into table.batch_writer() calls
|
283 | 155 | # client.batch_write_item: https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb/client/batch_write_item.html
|
284 | 156 | # table.batch_writer(): https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb/table/batch_writer.html
|
@@ -359,11 +231,13 @@ def batch_get_item(self, **kwargs):
|
359 | 231 |
|
360 | 232 | def scan(self, **kwargs):
|
361 | 233 | table_input = self._client_shape_to_resource_shape_converter.scan_request(kwargs)
|
| 234 | + # To exhaustively test Tables, |
| 235 | + # convert the string-based KeyConditionExpression and FilterExpression |
| 236 | + # into the boto3.conditions.Key and boto3.conditions.Attr resource-formatted queries. |
362 | 237 | if "KeyConditionExpression" in table_input:
|
363 | 238 | if table_input["KeyConditionExpression"] in known_query_string_to_condition_map:
|
364 | 239 | # Turn the query into the resource-formatted query
|
365 | 240 | query = known_query_string_to_condition_map[table_input["KeyConditionExpression"]]
|
366 |
| - print(f"{query=}") |
367 | 241 | table_input["KeyConditionExpression"] = query
|
368 | 242 | if "FilterExpression" in table_input:
|
369 | 243 | if table_input["FilterExpression"] in known_query_string_to_condition_map:
|
@@ -415,18 +289,18 @@ def transact_write_items(self, **kwargs):
|
415 | 289 |
|
416 | 290 | def query(self, **kwargs):
|
417 | 291 | table_input = self._client_shape_to_resource_shape_converter.query_request(kwargs)
|
418 |
| - print(f"pre maybe transform {table_input=}") |
| 292 | + # To exhaustively test Tables, |
| 293 | + # convert the string-based KeyConditionExpression and FilterExpression |
| 294 | + # into the boto3.conditions.Key and boto3.conditions.Attr resource-formatted queries. |
419 | 295 | if "KeyConditionExpression" in table_input:
|
420 | 296 | if table_input["KeyConditionExpression"] in known_query_string_to_condition_map:
|
421 | 297 | # Turn the query into the resource-formatted query
|
422 | 298 | query = known_query_string_to_condition_map[table_input["KeyConditionExpression"]]
|
423 |
| - print(f"{query=}") |
424 | 299 | table_input["KeyConditionExpression"] = query
|
425 | 300 | if "FilterExpression" in table_input:
|
426 | 301 | if table_input["FilterExpression"] in known_query_string_to_condition_map:
|
427 | 302 | # Turn the query into the resource-formatted query
|
428 | 303 | table_input["FilterExpression"] = known_query_string_to_condition_map[table_input["FilterExpression"]]
|
429 |
| - print(f"post maybe transform {table_input=}") |
430 | 304 | table_output = self._table.query(**table_input)
|
431 | 305 | client_output = self._resource_shape_to_client_shape_converter.query_response(table_output)
|
432 | 306 | return client_output
|
|
0 commit comments