Skip to content

Commit ba1bea5

Browse files
authored
Correct DynamoDB's Count/ScannedCount when paginating over numeric result keys (#9653)
1 parent b78de6b commit ba1bea5

File tree

5 files changed

+50
-11
lines changed

5 files changed

+50
-11
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"type": "bugfix",
3+
"category": "``dynamodb``",
4+
"description": "Correct Scan and Scanned Count values when resuming a scan or query with a ``--starting-token``"
5+
}

awscli/botocore/paginate.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,12 @@ def _handle_first_request(
411411
elif isinstance(sample, str):
412412
empty_value = ''
413413
elif isinstance(sample, (int, float)):
414-
empty_value = 0
414+
# Even though we may be resuming from a truncated page, we
415+
# still start from the actual numeric secondary result. For
416+
# DynamoDB's Count/ScannedCount, this will still show how many
417+
# items the server evaluated, even if the client is truncating
418+
# due to a StartingToken.
419+
empty_value = sample
415420
else:
416421
empty_value = None
417422
set_value_from_jmespath(parsed, token.expression, empty_value)

tests/functional/botocore/test_paginator_config.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,10 @@
131131
'xray.GetTraceSummaries.TracesProcessedCount',
132132
'xray.GetTraceSummaries.ApproximateTime',
133133
]
134+
KNOWN_PAGINATORS_WITH_INTEGER_OUTPUTS = (
135+
('dynamodb', 'Query'),
136+
('dynamodb', 'Scan'),
137+
)
134138

135139

136140
def _pagination_configs():
@@ -150,11 +154,12 @@ def _pagination_configs():
150154
)
151155
def test_lint_pagination_configs(operation_name, page_config, service_model):
152156
_validate_known_pagination_keys(page_config)
153-
_valiate_result_key_exists(page_config)
157+
_validate_result_key_exists(page_config)
154158
_validate_referenced_operation_exists(operation_name, service_model)
155159
_validate_operation_has_output(operation_name, service_model)
156160
_validate_input_keys_match(operation_name, page_config, service_model)
157161
_validate_output_keys_match(operation_name, page_config, service_model)
162+
_validate_new_numeric_keys(operation_name, page_config, service_model)
158163

159164

160165
def _validate_known_pagination_keys(page_config):
@@ -165,7 +170,7 @@ def _validate_known_pagination_keys(page_config):
165170
)
166171

167172

168-
def _valiate_result_key_exists(page_config):
173+
def _validate_result_key_exists(page_config):
169174
if 'result_key' not in page_config:
170175
raise AssertionError(
171176
"Required key 'result_key' is missing "
@@ -260,6 +265,30 @@ def _validate_output_keys_match(operation_name, page_config, service_model):
260265
)
261266

262267

268+
def _validate_new_numeric_keys(operation_name, page_config, service_model):
269+
output_shape = service_model.operation_model(operation_name).output_shape
270+
for key in _get_list_value(page_config, 'result_key'):
271+
current_shape = output_shape
272+
if '.' in key: # result_key is a JMESPath expression
273+
for part in key.split('.'):
274+
current_shape = current_shape.members[part]
275+
elif key in output_shape.members:
276+
current_shape = output_shape.members[key]
277+
278+
if (
279+
getattr(current_shape, 'type_name', None) == 'integer'
280+
and (service_model.service_name, operation_name)
281+
not in KNOWN_PAGINATORS_WITH_INTEGER_OUTPUTS
282+
):
283+
raise AssertionError(
284+
f'There is a new operation {operation_name} for service '
285+
f'{service_model.service_name} that is configured to sum '
286+
'integer outputs across pages. Verify that this behavior is '
287+
'correct before allow-listing, since whether or not it is '
288+
'appropriate to sum depends on the subject matter.'
289+
)
290+
291+
263292
def _looks_like_jmespath(expression):
264293
if all(ch in MEMBER_NAME_CHARS for ch in expression):
265294
return False

tests/functional/ddb/test_select.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def assert_yaml_response_equal(self, response, expected):
2727

2828
class TestSelect(BaseSelectTest):
2929
def setUp(self):
30-
super(TestSelect, self).setUp()
30+
super().setUp()
3131
self.parsed_response = {
3232
"Count": 1,
3333
"Items": [{"foo": {"S": "spam"}}],
@@ -509,7 +509,7 @@ def test_select_parsing_error_rc(self):
509509

510510
class TestSelectPagination(BaseSelectTest):
511511
def setUp(self):
512-
super(TestSelectPagination, self).setUp()
512+
super().setUp()
513513
self.parsed_responses = [
514514
{
515515
"Count": 1,
@@ -617,8 +617,8 @@ def test_starting_token(self):
617617
command, expected_params, expected_rc=0
618618
)
619619
expected_response = {
620-
'Count': 0,
620+
'Count': 1,
621621
'Items': [{'foo': 2}],
622-
'ScannedCount': 0,
622+
'ScannedCount': 1,
623623
}
624624
self.assert_yaml_response_equal(stdout, expected_response)

tests/unit/botocore/test_paginate.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1089,7 +1089,7 @@ def test_resume_with_secondary_result_as_string(self):
10891089
# they were in the original (first) response.
10901090
self.assertEqual(complete, {"Users": ["User2"], "Groups": ""})
10911091

1092-
def test_resume_with_secondary_result_as_integer(self):
1092+
def test_resume_with_secondary_result_does_not_reset_integer(self):
10931093
self.method.return_value = {"Users": ["User1", "User2"], "Groups": 123}
10941094
starting_token = encode_token(
10951095
{"Marker": None, "boto_truncate_amount": 1}
@@ -1098,9 +1098,9 @@ def test_resume_with_secondary_result_as_integer(self):
10981098
PaginationConfig={'MaxItems': 1, 'StartingToken': starting_token}
10991099
)
11001100
complete = pages.build_full_result()
1101-
# Note that the secondary keys ("Groups") becomes zero because
1102-
# they were in the original (first) response.
1103-
self.assertEqual(complete, {"Users": ["User2"], "Groups": 0})
1101+
# Note that we don't reset numeric secondary keys, for DynamoDB's
1102+
# Count/ScannedCount since the server still evaluates those items
1103+
self.assertEqual(complete, {"Users": ["User2"], "Groups": 123})
11041104

11051105

11061106
class TestMultipleInputKeys(unittest.TestCase):

0 commit comments

Comments
 (0)