Repository: getmoto/moto
Pull request #7273 (2024-01-29 19:40:22)

DynamoDB: query() should sort the results before filtering/limiting
Fixes #7265 

Bug introduced in #7208 


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/getmoto/moto/pull/7273?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Comparison is base [(`496c1cd`)](https://app.codecov.io/gh/getmoto/moto/commit/496c1cd5594831bb8dcc55d5e11de7167cbf3c6f?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) 95.88% compared to head [(`e1cb8d2`)](https://app.codecov.io/gh/getmoto/moto/pull/7273?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) 95.88%.
> Report is 2 commits behind head on master.


<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #7273      +/-   ##
==========================================
- Coverage   95.88%   95.88%   -0.01%     
==========================================
  Files         843      843              
  Lines       82425    82546     +121     
==========================================
+ Hits        79031    79147     +116     
- Misses       3394     3399       +5     
```

| [Flag](https://app.codecov.io/gh/getmoto/moto/pull/7273/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | Coverage Δ | |
|---|---|---|
| [servertests](https://app.codecov.io/gh/getmoto/moto/pull/7273/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `32.52% <0.00%> (-0.03%)` | :arrow_down: |
| [unittests](https://app.codecov.io/gh/getmoto/moto/pull/7273/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `95.84% <100.00%> (+<0.01%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto#carryforward-flags-in-the-pull-request-comment) to find out more.


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/getmoto/moto/pull/7273?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).


Patch:
diff --git a/moto/dynamodb/models/table.py b/moto/dynamodb/models/table.py
index 0955a3290..6f715a485 100644
--- a/moto/dynamodb/models/table.py
+++ b/moto/dynamodb/models/table.py
@@ -707,6 +707,24 @@ class Table(CloudFormationModel):
                 if isinstance(item, Item) and item.hash_key == hash_key
             ]
 
+        # SORT
+        if index_name:
+            if index_range_key:
+                # Convert to float if necessary to ensure proper ordering
+                def conv(x: DynamoType) -> Any:
+                    return float(x.value) if x.type == "N" else x.value
+
+                possible_results.sort(
+                    key=lambda item: conv(item.attrs[index_range_key["AttributeName"]])  # type: ignore
+                    if item.attrs.get(index_range_key["AttributeName"])  # type: ignore
+                    else None
+                )
+        else:
+            possible_results.sort(key=lambda item: item.range_key)  # type: ignore
+
+        if scan_index_forward is False:
+            possible_results.reverse()
+
         # FILTER
         results: List[Item] = []
         result_size = 0
@@ -763,24 +781,6 @@ class Table(CloudFormationModel):
                         result_size += result.size()
                 scanned_count += 1
 
-        # SORT
-        if index_name:
-            if index_range_key:
-                # Convert to float if necessary to ensure proper ordering
-                def conv(x: DynamoType) -> Any:
-                    return float(x.value) if x.type == "N" else x.value
-
-                results.sort(
-                    key=lambda item: conv(item.attrs[index_range_key["AttributeName"]])  # type: ignore
-                    if item.attrs.get(index_range_key["AttributeName"])  # type: ignore
-                    else None
-                )
-        else:
-            results.sort(key=lambda item: item.range_key)  # type: ignore
-
-        if scan_index_forward is False:
-            results.reverse()
-
         results = copy.deepcopy(results)
         if index_name:
             index = self.get_index(index_name)


Test patch:
diff --git a/tests/test_dynamodb/test_dynamodb.py b/tests/test_dynamodb/test_dynamodb.py
index 7a9b5dc54..26d5e0545 100644
--- a/tests/test_dynamodb/test_dynamodb.py
+++ b/tests/test_dynamodb/test_dynamodb.py
@@ -1386,6 +1386,25 @@ def test_query_filter():
     )
     assert response["Count"] == 2
 
+    # Combine Limit + Scan
+    response = table.query(
+        KeyConditionExpression=Key("client").eq("client1"),
+        Limit=1,
+        ScanIndexForward=False,
+    )
+    assert response["Count"] == 1
+    assert response["ScannedCount"] == 1
+    assert response["Items"][0]["app"] == "app2"
+
+    response = table.query(
+        KeyConditionExpression=Key("client").eq("client1"),
+        Limit=1,
+        ScanIndexForward=True,
+    )
+    assert response["Count"] == 1
+    assert response["ScannedCount"] == 1
+    assert response["Items"][0]["app"] == "app1"
+
 
 @mock_aws
 def test_query_filter_overlapping_expression_prefixes():