Repository: getmoto/moto

Issue:
Cannot scan all DynamoDB table entries
The following script creates 15 entries in an empty DynamoDB table, then scans through the table with pagination (page size set to 10). The script verifies the scan is complete, i.e. all the entries were fetched. The script runs in production (i.e. without `moto`), and it does not fail. Specifically, the asserts at the end do not trigger:
```bash
❯ ./test_dynamodb_in_aws.py 
boto3:          1.34.72
botocore:       1.34.72

Called scan() with {"Limit": 10}                                    , got LastEvaluatedKey {"id": "k14"}, items IDs ['k9', 'k6', 'k7', 'k1', 'k3', 'k13', 'k12', 'k5', 'k2', 'k14']
Called scan() with {"Limit": 10, "ExclusiveStartKey": {"id": "k14"}}, got LastEvaluatedKey null         , items IDs ['k11', 'k15', 'k10', 'k4', 'k8']
first_page_ids: {'k1', 'k12', 'k9', 'k5', 'k14', 'k6', 'k2', 'k7', 'k13', 'k3'}
second_page_ids: {'k15', 'k4', 'k10', 'k8', 'k11'}
```
Code:
```python3
#!/usr/bin/env python3
import json

import boto3
from boto3.dynamodb.conditions import Attr


DYNAMODB = boto3.resource('dynamodb', 'eu-central-1')
DYNAMODB_TABLE = DYNAMODB.Table('table_name')

# populate table
for i in range(1, 16):
    data = f'val_{i}'
    db_id = f'k{i}'
    res = DYNAMODB_TABLE.put_item(
        Item={'id': db_id, 'data': data},
        ConditionExpression=Attr('id').not_exists())
    assert res['ResponseMetadata']['HTTPStatusCode'] == 200

params = dict(Limit=10)
res = DYNAMODB_TABLE.scan(**params)
items = [item['id'] for item in res['Items']]
print(
    f"Called scan() with {json.dumps(params):49}, got LastEvaluatedKey {json.dumps(res.get('LastEvaluatedKey')):13}, items IDs {items}")
first_page_ids = {item['id'] for item in res['Items']}

params['ExclusiveStartKey'] = res.get('LastEvaluatedKey')
res = DYNAMODB_TABLE.scan(**params)
items = [item['id'] for item in res['Items']]
print(
    f"Called scan() with {json.dumps(params):49}, got LastEvaluatedKey {json.dumps(res.get('LastEvaluatedKey')):13}, items IDs {items}")
second_page_ids = {item['id'] for item in res['Items']}

print(f"first_page_ids: {first_page_ids}")
print(f"second_page_ids: {second_page_ids}")

assert len(first_page_ids & second_page_ids) == 0
assert len(first_page_ids | second_page_ids) == 15
```

Doing the same with `moto`, it is impossible to scan through all the entries of the table:
```bash
❯ ./test_dynamodb.py
{'Items': [{'id': 'k1', 'data': 'val_1'}, {'id': 'k2', 'data': 'val_2'}, {'id': 'k3', 'data': 'val_3'}, {'id': 'k4', 'data': 'val_4'}, {'id': 'k5', 'data': 'val_5'}, {'id': 'k6', 'data': 'val_6'}, {'id': 'k7', 'data': 'val_7'}, {'id': 'k8', 'data': 'val_8'}, {'id': 'k9', 'data': 'val_9'}, {'id': 'k10', 'data': 'val_10'}], 'Count': 10, 'ScannedCount': 10, 'LastEvaluatedKey': {'id': 'k10'}, 'ResponseMetadata': {'RequestId': '3FPC2VL7DuOEE7h6x1q0vCSHozB0TESdTEK0VAfsd7HeDdRXubv7', 'HTTPStatusCode': 200, 'HTTPHeaders': {'server': 'amazon.com', 'date': 'Thu, 28 Mar 2024 13:40:20 GMT', 'x-amzn-requestid': '3FPC2VL7DuOEE7h6x1q0vCSHozB0TESdTEK0VAfsd7HeDdRXubv7', 'x-amz-crc32': '2600487097'}, 'RetryAttempts': 0}}
{'Items': [{'id': 'k2', 'data': 'val_2'}, {'id': 'k3', 'data': 'val_3'}, {'id': 'k4', 'data': 'val_4'}, {'id': 'k5', 'data': 'val_5'}, {'id': 'k6', 'data': 'val_6'}, {'id': 'k7', 'data': 'val_7'}, {'id': 'k8', 'data': 'val_8'}, {'id': 'k9', 'data': 'val_9'}, {'id': 'k10', 'data': 'val_10'}, {'id': 'k11', 'data': 'val_11'}], 'Count': 10, 'ScannedCount': 10, 'LastEvaluatedKey': {'id': 'k11'}, 'ResponseMetadata': {'RequestId': 'x9so4xzNNbk7YZ1rzC3CzmmTuDoTdnbWx53PXavIR0afh7RH2Luj', 'HTTPStatusCode': 200, 'HTTPHeaders': {'server': 'amazon.com', 'date': 'Thu, 28 Mar 2024 13:40:20 GMT', 'x-amzn-requestid': 'x9so4xzNNbk7YZ1rzC3CzmmTuDoTdnbWx53PXavIR0afh7RH2Luj', 'x-amz-crc32': '4219525820'}, 'RetryAttempts': 0}}
first_page_ids: {'k1', 'k2', 'k4', 'k10', 'k6', 'k9', 'k8', 'k3', 'k7', 'k5'}
second_page_ids: {'k2', 'k4', 'k10', 'k6', 'k9', 'k8', 'k3', 'k11', 'k7', 'k5'}
Traceback (most recent call last):
  File "/home/tshalev/dev/swg-api/./test_dynamodb.py", line 57, in <module>
    assert len(first_page_ids & second_page_ids) == 0
AssertionError
```
Code for `moto` test:
```python3
#!/usr/bin/env python3
import json

import boto3
from boto3.dynamodb.conditions import Attr

import moto

DYNAMODB_TABLE_NAME = 'table_name'
DYNAMODB = boto3.resource('dynamodb', 'eu-central-1')
DYNAMODB_TABLE = DYNAMODB.Table(DYNAMODB_TABLE_NAME)

with moto.mock_aws():
    DYNAMODB.create_table(
        TableName=DYNAMODB_TABLE_NAME,
        KeySchema=[
            {'AttributeName': 'id', 'KeyType': 'HASH'},
        ],
        AttributeDefinitions=[
            {'AttributeName': 'id', 'AttributeType': 'S'},
        ],
        ProvisionedThroughput={
            'ReadCapacityUnits': 10,
            'WriteCapacityUnits': 10
        }
    )

    # populate table
    for i in range(1, 16):
        data = f'val_{i}'
        db_id = f'k{i}'
        res = DYNAMODB_TABLE.put_item(
            Item={'id': db_id, 'data': data},
            ConditionExpression=Attr('id').not_exists())
        assert res['ResponseMetadata']['HTTPStatusCode'] == 200

    params = dict(Limit=10)
    res = DYNAMODB_TABLE.scan(**params)
    print(res)
    first_page_ids = {item['id'] for item in res['Items']}

    params['ExclusiveStartKey'] = res.get('LastEvaluatedKey')
    res = DYNAMODB_TABLE.scan(**params)
    print(res)
    second_page_ids = {item['id'] for item in res['Items']}

    print(f"first_page_ids: {first_page_ids}")
    print(f"second_page_ids: {second_page_ids}")

    assert len(first_page_ids & second_page_ids) == 0
    assert len(first_page_ids | second_page_ids) == 15
```

## Versions
boto3:          1.34.72
botocore:       1.34.72
moto:           5.0.4



Discussion:
This issue is a breaking change.
Works with `moto` 5.0.3
Fails with `moto` 5.0.4
This regression is probably caused by the following commit: https://github.com/getmoto/moto/commit/194088829624fa720c374f55e047e7c09e26a9ab
FYI, @bblommers 

Resolving patch:
diff --git a/moto/dynamodb/models/table.py b/moto/dynamodb/models/table.py
--- a/moto/dynamodb/models/table.py
+++ b/moto/dynamodb/models/table.py
@@ -1,6 +1,6 @@
 import copy
 from collections import defaultdict
-from typing import Any, Dict, Iterator, List, Optional, Sequence, Tuple
+from typing import Any, Dict, List, Optional, Sequence, Tuple
 
 from moto.core.common_models import BaseModel, CloudFormationModel
 from moto.core.utils import unix_time, unix_time_millis, utcnow
@@ -713,7 +713,7 @@ def query(
         else:
             possible_results = [
                 item
-                for item in list(self.all_items())
+                for item in self.all_items()
                 if isinstance(item, Item) and item.hash_key == hash_key
             ]
 
@@ -747,7 +747,7 @@ def conv(x: DynamoType) -> Any:
             # Cycle through the previous page of results
             # When we encounter our start key, we know we've reached the end of the previous page
             if processing_previous_page:
-                if self._item_smaller_than_dct(result, exclusive_start_key):
+                if self._item_comes_before_dct(result, exclusive_start_key):
                     continue
                 else:
                     processing_previous_page = False
@@ -807,13 +807,18 @@ def conv(x: DynamoType) -> Any:
 
         return results, scanned_count, last_evaluated_key
 
-    def all_items(self) -> Iterator[Item]:
+    def all_items(self) -> List[Item]:
+        items: List[Item] = []
         for hash_set in self.items.values():
             if self.range_key_attr:
                 for item in hash_set.values():
-                    yield item
+                    items.append(item)
             else:
-                yield hash_set  # type: ignore
+                items.append(hash_set)  # type: ignore
+        return sorted(
+            items,
+            key=lambda x: (x.hash_key, x.range_key) if x.range_key else x.hash_key,
+        )
 
     def all_indexes(self) -> Sequence[SecondaryIndex]:
         return (self.global_indexes or []) + (self.indexes or [])  # type: ignore
@@ -827,18 +832,24 @@ def get_index(self, index_name: str, error_if_not: bool = False) -> SecondaryInd
             )
         return indexes_by_name[index_name]
 
-    def has_idx_items(self, index_name: str) -> Iterator[Item]:
+    def has_idx_items(self, index_name: str) -> List[Item]:
         idx = self.get_index(index_name)
         idx_col_set = set([i["AttributeName"] for i in idx.schema])
 
+        items: List[Item] = []
+
         for hash_set in self.items.values():
             if self.range_key_attr:
                 for item in hash_set.values():
                     if idx_col_set.issubset(set(item.attrs)):
-                        yield item
+                        items.append(item)
             else:
                 if idx_col_set.issubset(set(hash_set.attrs)):  # type: ignore
-                    yield hash_set  # type: ignore
+                    items.append(hash_set)  # type: ignore
+        return sorted(
+            items,
+            key=lambda x: (x.hash_key, x.range_key) if x.range_key else x.hash_key,
+        )
 
     def scan(
         self,
@@ -872,7 +883,7 @@ def scan(
             # Cycle through the previous page of results
             # When we encounter our start key, we know we've reached the end of the previous page
             if processing_previous_page:
-                if self._item_smaller_than_dct(item, exclusive_start_key):
+                if self._item_comes_before_dct(item, exclusive_start_key):
                     continue
                 else:
                     processing_previous_page = False
@@ -926,12 +937,15 @@ def scan(
 
         return results, scanned_count, last_evaluated_key
 
-    def _item_smaller_than_dct(self, item: Item, dct: Dict[str, Any]) -> bool:
+    def _item_comes_before_dct(self, item: Item, dct: Dict[str, Any]) -> bool:
         hash_key = DynamoType(dct.get(self.hash_key_attr))  # type: ignore[arg-type]
         range_key = dct.get(self.range_key_attr) if self.range_key_attr else None
         if range_key is not None:
             range_key = DynamoType(range_key)
-            return item.hash_key <= hash_key and item.range_key <= range_key
+            if item.hash_key < hash_key:
+                return True
+            else:
+                return item.hash_key <= hash_key and item.range_key <= range_key
         return item.hash_key <= hash_key
 
     def _get_last_evaluated_key(


Test patch:
diff --git a/tests/test_dynamodb/__init__.py b/tests/test_dynamodb/__init__.py
--- a/tests/test_dynamodb/__init__.py
+++ b/tests/test_dynamodb/__init__.py
@@ -7,7 +7,16 @@
 from moto import mock_aws
 
 
-def dynamodb_aws_verified(create_table: bool = True, add_range: bool = False):
+def dynamodb_aws_verified(
+    create_table: bool = True,
+    add_range: bool = False,
+    numeric_range: bool = False,
+    add_gsi: bool = False,
+    add_gsi_range: bool = False,
+    add_lsi: bool = False,
+    numeric_gsi_range: bool = False,
+    numeric_lsi_range: bool = False,
+):
     """
     Function that is verified to work against AWS.
     Can be run against AWS at any time by setting:
@@ -46,17 +55,76 @@ def pagination_wrapper():
         def create_table_and_test(table_name):
             client = boto3.client("dynamodb", region_name="us-east-1")
 
-            schema = [{"AttributeName": "pk", "KeyType": "HASH"}]
-            defs = [{"AttributeName": "pk", "AttributeType": "S"}]
-            if add_range:
-                schema.append({"AttributeName": "sk", "KeyType": "RANGE"})
-                defs.append({"AttributeName": "sk", "AttributeType": "S"})
+            range_type = "N" if numeric_range else "S"
+            gsi_range_type = "N" if numeric_gsi_range else "S"
+            lsi_range_type = "N" if numeric_lsi_range else "S"
+
+            kwargs = {
+                "KeySchema": [{"AttributeName": "pk", "KeyType": "HASH"}],
+                "AttributeDefinitions": [{"AttributeName": "pk", "AttributeType": "S"}],
+            }
+            if add_range or numeric_range:
+                kwargs["KeySchema"].append({"AttributeName": "sk", "KeyType": "RANGE"})
+                kwargs["AttributeDefinitions"].append(
+                    {"AttributeName": "sk", "AttributeType": range_type}
+                )
+            if add_gsi:
+                kwargs["GlobalSecondaryIndexes"] = [
+                    {
+                        "IndexName": "test_gsi",
+                        "KeySchema": [
+                            {"AttributeName": "gsi_pk", "KeyType": "HASH"},
+                        ],
+                        "Projection": {"ProjectionType": "ALL"},
+                        "ProvisionedThroughput": {
+                            "ReadCapacityUnits": 1,
+                            "WriteCapacityUnits": 1,
+                        },
+                    }
+                ]
+                kwargs["AttributeDefinitions"].append(
+                    {"AttributeName": "gsi_pk", "AttributeType": "S"}
+                )
+            if add_gsi_range or numeric_gsi_range:
+                kwargs["GlobalSecondaryIndexes"] = [
+                    {
+                        "IndexName": "test_gsi",
+                        "KeySchema": [
+                            {"AttributeName": "gsi_pk", "KeyType": "HASH"},
+                            {"AttributeName": "gsi_sk", "KeyType": "RANGE"},
+                        ],
+                        "Projection": {"ProjectionType": "ALL"},
+                        "ProvisionedThroughput": {
+                            "ReadCapacityUnits": 1,
+                            "WriteCapacityUnits": 1,
+                        },
+                    }
+                ]
+                kwargs["AttributeDefinitions"].append(
+                    {"AttributeName": "gsi_pk", "AttributeType": "S"}
+                )
+                kwargs["AttributeDefinitions"].append(
+                    {"AttributeName": "gsi_sk", "AttributeType": gsi_range_type}
+                )
+            if add_lsi or numeric_lsi_range:
+                kwargs["LocalSecondaryIndexes"] = [
+                    {
+                        "IndexName": "test_lsi",
+                        "KeySchema": [
+                            {"AttributeName": "pk", "KeyType": "HASH"},
+                            {"AttributeName": "lsi_sk", "KeyType": "RANGE"},
+                        ],
+                        "Projection": {"ProjectionType": "ALL"},
+                    }
+                ]
+                kwargs["AttributeDefinitions"].append(
+                    {"AttributeName": "lsi_sk", "AttributeType": lsi_range_type}
+                )
             client.create_table(
                 TableName=table_name,
-                KeySchema=schema,
-                AttributeDefinitions=defs,
                 ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 5},
                 Tags=[{"Key": "environment", "Value": "moto_tests"}],
+                **kwargs,
             )
             waiter = client.get_waiter("table_exists")
             waiter.wait(TableName=table_name)
diff --git a/tests/test_dynamodb/test_dynamodb.py b/tests/test_dynamodb/test_dynamodb.py
--- a/tests/test_dynamodb/test_dynamodb.py
+++ b/tests/test_dynamodb/test_dynamodb.py
@@ -1312,379 +1312,6 @@ def test_filter_expression():
     assert filter_expr.expr(row2) is False
 
 
-@mock_aws
-def test_query_filter():
-    client = boto3.client("dynamodb", region_name="us-east-1")
-    dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
-
-    # Create the DynamoDB table.
-    client.create_table(
-        TableName="test1",
-        AttributeDefinitions=[
-            {"AttributeName": "client", "AttributeType": "S"},
-            {"AttributeName": "app", "AttributeType": "S"},
-        ],
-        KeySchema=[
-            {"AttributeName": "client", "KeyType": "HASH"},
-            {"AttributeName": "app", "KeyType": "RANGE"},
-        ],
-        ProvisionedThroughput={"ReadCapacityUnits": 123, "WriteCapacityUnits": 123},
-    )
-    client.put_item(
-        TableName="test1",
-        Item={
-            "client": {"S": "client1"},
-            "app": {"S": "app1"},
-            "nested": {
-                "M": {
-                    "version": {"S": "version1"},
-                    "contents": {"L": [{"S": "value1"}, {"S": "value2"}]},
-                }
-            },
-        },
-    )
-    client.put_item(
-        TableName="test1",
-        Item={
-            "client": {"S": "client1"},
-            "app": {"S": "app2"},
-            "nested": {
-                "M": {
-                    "version": {"S": "version2"},
-                    "contents": {"L": [{"S": "value1"}, {"S": "value2"}]},
-                }
-            },
-        },
-    )
-
-    table = dynamodb.Table("test1")
-    response = table.query(KeyConditionExpression=Key("client").eq("client1"))
-    assert response["Count"] == 2
-    assert response["ScannedCount"] == 2
-
-    response = table.query(
-        KeyConditionExpression=Key("client").eq("client1"),
-        FilterExpression=Attr("app").eq("app2"),
-    )
-    assert response["Count"] == 1
-    assert response["ScannedCount"] == 2
-    assert response["Items"][0]["app"] == "app2"
-    response = table.query(
-        KeyConditionExpression=Key("client").eq("client1"),
-        FilterExpression=Attr("app").contains("app"),
-    )
-    assert response["Count"] == 2
-
-    response = table.query(
-        KeyConditionExpression=Key("client").eq("client1"),
-        FilterExpression=Attr("nested.version").contains("version"),
-    )
-    assert response["Count"] == 2
-
-    response = table.query(
-        KeyConditionExpression=Key("client").eq("client1"),
-        FilterExpression=Attr("nested.contents[0]").eq("value1"),
-    )
-    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():
-    client = boto3.client("dynamodb", region_name="us-east-1")
-    dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
-
-    # Create the DynamoDB table.
-    client.create_table(
-        TableName="test1",
-        AttributeDefinitions=[
-            {"AttributeName": "client", "AttributeType": "S"},
-            {"AttributeName": "app", "AttributeType": "S"},
-        ],
-        KeySchema=[
-            {"AttributeName": "client", "KeyType": "HASH"},
-            {"AttributeName": "app", "KeyType": "RANGE"},
-        ],
-        ProvisionedThroughput={"ReadCapacityUnits": 123, "WriteCapacityUnits": 123},
-    )
-
-    client.put_item(
-        TableName="test1",
-        Item={
-            "client": {"S": "client1"},
-            "app": {"S": "app1"},
-            "nested": {
-                "M": {
-                    "version": {"S": "version1"},
-                    "contents": {"L": [{"S": "value1"}, {"S": "value2"}]},
-                }
-            },
-        },
-    )
-
-    table = dynamodb.Table("test1")
-    response = table.query(
-        KeyConditionExpression=Key("client").eq("client1") & Key("app").eq("app1"),
-        ProjectionExpression="#1, #10, nested",
-        ExpressionAttributeNames={"#1": "client", "#10": "app"},
-    )
-
-    assert response["Count"] == 1
-    assert response["Items"][0] == {
-        "client": "client1",
-        "app": "app1",
-        "nested": {"version": "version1", "contents": ["value1", "value2"]},
-    }
-
-
-@mock_aws
-def test_scan_filter():
-    client = boto3.client("dynamodb", region_name="us-east-1")
-    dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
-
-    # Create the DynamoDB table.
-    client.create_table(
-        TableName="test1",
-        AttributeDefinitions=[
-            {"AttributeName": "client", "AttributeType": "S"},
-            {"AttributeName": "app", "AttributeType": "S"},
-        ],
-        KeySchema=[
-            {"AttributeName": "client", "KeyType": "HASH"},
-            {"AttributeName": "app", "KeyType": "RANGE"},
-        ],
-        ProvisionedThroughput={"ReadCapacityUnits": 123, "WriteCapacityUnits": 123},
-    )
-    client.put_item(
-        TableName="test1", Item={"client": {"S": "client1"}, "app": {"S": "app1"}}
-    )
-
-    table = dynamodb.Table("test1")
-    response = table.scan(FilterExpression=Attr("app").eq("app2"))
-    assert response["Count"] == 0
-
-    response = table.scan(FilterExpression=Attr("app").eq("app1"))
-    assert response["Count"] == 1
-
-    response = table.scan(FilterExpression=Attr("app").ne("app2"))
-    assert response["Count"] == 1
-
-    response = table.scan(FilterExpression=Attr("app").ne("app1"))
-    assert response["Count"] == 0
-
-
-@mock_aws
-def test_scan_filter2():
-    client = boto3.client("dynamodb", region_name="us-east-1")
-
-    # Create the DynamoDB table.
-    client.create_table(
-        TableName="test1",
-        AttributeDefinitions=[
-            {"AttributeName": "client", "AttributeType": "S"},
-            {"AttributeName": "app", "AttributeType": "N"},
-        ],
-        KeySchema=[
-            {"AttributeName": "client", "KeyType": "HASH"},
-            {"AttributeName": "app", "KeyType": "RANGE"},
-        ],
-        ProvisionedThroughput={"ReadCapacityUnits": 123, "WriteCapacityUnits": 123},
-    )
-    client.put_item(
-        TableName="test1", Item={"client": {"S": "client1"}, "app": {"N": "1"}}
-    )
-
-    response = client.scan(
-        TableName="test1",
-        Select="ALL_ATTRIBUTES",
-        FilterExpression="#tb >= :dt",
-        ExpressionAttributeNames={"#tb": "app"},
-        ExpressionAttributeValues={":dt": {"N": str(1)}},
-    )
-    assert response["Count"] == 1
-
-
-@mock_aws
-def test_scan_filter3():
-    client = boto3.client("dynamodb", region_name="us-east-1")
-    dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
-
-    # Create the DynamoDB table.
-    client.create_table(
-        TableName="test1",
-        AttributeDefinitions=[
-            {"AttributeName": "client", "AttributeType": "S"},
-            {"AttributeName": "app", "AttributeType": "N"},
-        ],
-        KeySchema=[
-            {"AttributeName": "client", "KeyType": "HASH"},
-            {"AttributeName": "app", "KeyType": "RANGE"},
-        ],
-        ProvisionedThroughput={"ReadCapacityUnits": 123, "WriteCapacityUnits": 123},
-    )
-    client.put_item(
-        TableName="test1",
-        Item={"client": {"S": "client1"}, "app": {"N": "1"}, "active": {"BOOL": True}},
-    )
-
-    table = dynamodb.Table("test1")
-    response = table.scan(FilterExpression=Attr("active").eq(True))
-    assert response["Count"] == 1
-
-    response = table.scan(FilterExpression=Attr("active").ne(True))
-    assert response["Count"] == 0
-
-    response = table.scan(FilterExpression=Attr("active").ne(False))
-    assert response["Count"] == 1
-
-    response = table.scan(FilterExpression=Attr("app").ne(1))
-    assert response["Count"] == 0
-
-    response = table.scan(FilterExpression=Attr("app").ne(2))
-    assert response["Count"] == 1
-
-
-@mock_aws
-def test_scan_filter4():
-    client = boto3.client("dynamodb", region_name="us-east-1")
-    dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
-
-    # Create the DynamoDB table.
-    client.create_table(
-        TableName="test1",
-        AttributeDefinitions=[
-            {"AttributeName": "client", "AttributeType": "S"},
-            {"AttributeName": "app", "AttributeType": "N"},
-        ],
-        KeySchema=[
-            {"AttributeName": "client", "KeyType": "HASH"},
-            {"AttributeName": "app", "KeyType": "RANGE"},
-        ],
-        ProvisionedThroughput={"ReadCapacityUnits": 123, "WriteCapacityUnits": 123},
-    )
-
-    table = dynamodb.Table("test1")
-    response = table.scan(
-        FilterExpression=Attr("epoch_ts").lt(7) & Attr("fanout_ts").not_exists()
-    )
-    # Just testing
-    assert response["Count"] == 0
-
-
-@mock_aws
-def test_scan_filter_should_not_return_non_existing_attributes():
-    table_name = "my-table"
-    item = {"partitionKey": "pk-2", "my-attr": 42}
-    # Create table
-    res = boto3.resource("dynamodb", region_name="us-east-1")
-    res.create_table(
-        TableName=table_name,
-        KeySchema=[{"AttributeName": "partitionKey", "KeyType": "HASH"}],
-        AttributeDefinitions=[{"AttributeName": "partitionKey", "AttributeType": "S"}],
-        BillingMode="PAY_PER_REQUEST",
-    )
-    table = res.Table(table_name)
-    # Insert items
-    table.put_item(Item={"partitionKey": "pk-1"})
-    table.put_item(Item=item)
-    # Verify a few operations
-    # Assert we only find the item that has this attribute
-    assert table.scan(FilterExpression=Attr("my-attr").lt(43))["Items"] == [item]
-    assert table.scan(FilterExpression=Attr("my-attr").lte(42))["Items"] == [item]
-    assert table.scan(FilterExpression=Attr("my-attr").gte(42))["Items"] == [item]
-    assert table.scan(FilterExpression=Attr("my-attr").gt(41))["Items"] == [item]
-    # Sanity check that we can't find the item if the FE is wrong
-    assert table.scan(FilterExpression=Attr("my-attr").gt(43))["Items"] == []
-
-
-@mock_aws
-def test_bad_scan_filter():
-    client = boto3.client("dynamodb", region_name="us-east-1")
-    dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
-
-    # Create the DynamoDB table.
-    client.create_table(
-        TableName="test1",
-        AttributeDefinitions=[
-            {"AttributeName": "client", "AttributeType": "S"},
-            {"AttributeName": "app", "AttributeType": "S"},
-        ],
-        KeySchema=[
-            {"AttributeName": "client", "KeyType": "HASH"},
-            {"AttributeName": "app", "KeyType": "RANGE"},
-        ],
-        ProvisionedThroughput={"ReadCapacityUnits": 123, "WriteCapacityUnits": 123},
-    )
-    table = dynamodb.Table("test1")
-
-    # Bad expression
-    with pytest.raises(ClientError) as exc:
-        table.scan(FilterExpression="client test")
-    assert exc.value.response["Error"]["Code"] == "ValidationException"
-
-
-@mock_aws
-def test_scan_with_scanfilter():
-    table_name = "my-table"
-    item = {"partitionKey": "pk-2", "my-attr": 42}
-    client = boto3.client("dynamodb", region_name="us-east-1")
-    res = boto3.resource("dynamodb", region_name="us-east-1")
-    res.create_table(
-        TableName=table_name,
-        KeySchema=[{"AttributeName": "partitionKey", "KeyType": "HASH"}],
-        AttributeDefinitions=[{"AttributeName": "partitionKey", "AttributeType": "S"}],
-        BillingMode="PAY_PER_REQUEST",
-    )
-    table = res.Table(table_name)
-    table.put_item(Item={"partitionKey": "pk-1"})
-    table.put_item(Item=item)
-
-    # ScanFilter: EQ
-    # The DynamoDB table-resource sends the AttributeValueList in the wrong format
-    # So this operation never finds any data, in Moto or AWS
-    table.scan(
-        ScanFilter={
-            "my-attr": {"AttributeValueList": [{"N": "42"}], "ComparisonOperator": "EQ"}
-        }
-    )
-
-    # ScanFilter: EQ
-    # If we use the boto3-client, we do receive the correct data
-    items = client.scan(
-        TableName=table_name,
-        ScanFilter={
-            "partitionKey": {
-                "AttributeValueList": [{"S": "pk-1"}],
-                "ComparisonOperator": "EQ",
-            }
-        },
-    )["Items"]
-    assert items == [{"partitionKey": {"S": "pk-1"}}]
-
-    # ScanFilter: NONE
-    # Note that we can use the table-resource here, because we're not using the AttributeValueList
-    items = table.scan(ScanFilter={"my-attr": {"ComparisonOperator": "NULL"}})["Items"]
-    assert items == [{"partitionKey": "pk-1"}]
-
-
 @mock_aws
 def test_duplicate_create():
     client = boto3.client("dynamodb", region_name="us-east-1")
@@ -2409,63 +2036,6 @@ def test_query_global_secondary_index_when_created_via_update_table_resource():
     }
 
 
-@mock_aws
-def test_query_gsi_with_range_key():
-    dynamodb = boto3.client("dynamodb", region_name="us-east-1")
-    dynamodb.create_table(
-        TableName="test",
-        KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
-        AttributeDefinitions=[
-            {"AttributeName": "id", "AttributeType": "S"},
-            {"AttributeName": "gsi_hash_key", "AttributeType": "S"},
-            {"AttributeName": "gsi_range_key", "AttributeType": "S"},
-        ],
-        ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 1},
-        GlobalSecondaryIndexes=[
-            {
-                "IndexName": "test_gsi",
-                "KeySchema": [
-                    {"AttributeName": "gsi_hash_key", "KeyType": "HASH"},
-                    {"AttributeName": "gsi_range_key", "KeyType": "RANGE"},
-                ],
-                "Projection": {"ProjectionType": "ALL"},
-                "ProvisionedThroughput": {
-                    "ReadCapacityUnits": 1,
-                    "WriteCapacityUnits": 1,
-                },
-            }
-        ],
-    )
-
-    dynamodb.put_item(
-        TableName="test",
-        Item={
-            "id": {"S": "test1"},
-            "gsi_hash_key": {"S": "key1"},
-            "gsi_range_key": {"S": "range1"},
-        },
-    )
-    dynamodb.put_item(
-        TableName="test", Item={"id": {"S": "test2"}, "gsi_hash_key": {"S": "key1"}}
-    )
-
-    res = dynamodb.query(
-        TableName="test",
-        IndexName="test_gsi",
-        KeyConditionExpression="gsi_hash_key = :gsi_hash_key and gsi_range_key = :gsi_range_key",
-        ExpressionAttributeValues={
-            ":gsi_hash_key": {"S": "key1"},
-            ":gsi_range_key": {"S": "range1"},
-        },
-    )
-    assert res["Count"] == 1
-    assert res["Items"][0] == {
-        "id": {"S": "test1"},
-        "gsi_hash_key": {"S": "key1"},
-        "gsi_range_key": {"S": "range1"},
-    }
-
-
 @mock_aws
 def test_scan_by_non_exists_index():
     dynamodb = boto3.client("dynamodb", region_name="us-east-1")
@@ -2921,43 +2491,6 @@ def create_table_with_list(table_name):
     return client
 
 
-@mock_aws
-def test_sorted_query_with_numerical_sort_key():
-    dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
-    dynamodb.create_table(
-        TableName="CarCollection",
-        KeySchema=[
-            {"AttributeName": "CarModel", "KeyType": "HASH"},
-            {"AttributeName": "CarPrice", "KeyType": "RANGE"},
-        ],
-        AttributeDefinitions=[
-            {"AttributeName": "CarModel", "AttributeType": "S"},
-            {"AttributeName": "CarPrice", "AttributeType": "N"},
-        ],
-        ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 1},
-    )
-
-    def create_item(price):
-        return {"CarModel": "M", "CarPrice": price}
-
-    table = dynamodb.Table("CarCollection")
-    items = list(map(create_item, [2, 1, 10, 3]))
-    for item in items:
-        table.put_item(Item=item)
-
-    response = table.query(KeyConditionExpression=Key("CarModel").eq("M"))
-
-    response_items = response["Items"]
-    assert len(items) == len(response_items)
-    assert all(isinstance(item["CarPrice"], Decimal) for item in response_items)
-    response_prices = [item["CarPrice"] for item in response_items]
-    expected_prices = [Decimal(item["CarPrice"]) for item in items]
-    expected_prices.sort()
-    assert (
-        expected_prices == response_prices
-    ), "result items are not sorted by numerical value"
-
-
 # https://github.com/getmoto/moto/issues/1874
 @mock_aws
 def test_item_size_is_under_400KB():
@@ -3673,61 +3206,6 @@ def test_valid_transact_get_items():
     }
 
 
-@mock_aws
-def test_gsi_verify_negative_number_order():
-    table_schema = {
-        "KeySchema": [{"AttributeName": "partitionKey", "KeyType": "HASH"}],
-        "GlobalSecondaryIndexes": [
-            {
-                "IndexName": "GSI-K1",
-                "KeySchema": [
-                    {"AttributeName": "gsiK1PartitionKey", "KeyType": "HASH"},
-                    {"AttributeName": "gsiK1SortKey", "KeyType": "RANGE"},
-                ],
-                "Projection": {"ProjectionType": "KEYS_ONLY"},
-            }
-        ],
-        "AttributeDefinitions": [
-            {"AttributeName": "partitionKey", "AttributeType": "S"},
-            {"AttributeName": "gsiK1PartitionKey", "AttributeType": "S"},
-            {"AttributeName": "gsiK1SortKey", "AttributeType": "N"},
-        ],
-    }
-
-    item1 = {
-        "partitionKey": "pk-1",
-        "gsiK1PartitionKey": "gsi-k1",
-        "gsiK1SortKey": Decimal("-0.6"),
-    }
-
-    item2 = {
-        "partitionKey": "pk-2",
-        "gsiK1PartitionKey": "gsi-k1",
-        "gsiK1SortKey": Decimal("-0.7"),
-    }
-
-    item3 = {
-        "partitionKey": "pk-3",
-        "gsiK1PartitionKey": "gsi-k1",
-        "gsiK1SortKey": Decimal("0.7"),
-    }
-
-    dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
-    dynamodb.create_table(
-        TableName="test-table", BillingMode="PAY_PER_REQUEST", **table_schema
-    )
-    table = dynamodb.Table("test-table")
-    table.put_item(Item=item3)
-    table.put_item(Item=item1)
-    table.put_item(Item=item2)
-
-    resp = table.query(
-        KeyConditionExpression=Key("gsiK1PartitionKey").eq("gsi-k1"), IndexName="GSI-K1"
-    )
-    # Items should be ordered with the lowest number first
-    assert [float(item["gsiK1SortKey"]) for item in resp["Items"]] == [-0.7, -0.6, 0.7]
-
-
 @mock_aws
 def test_transact_write_items_put():
     table_schema = {
@@ -3887,8 +3365,8 @@ def test_transact_write_items_conditioncheck_passes():
     # Assert all are present
     items = dynamodb.scan(TableName="test-table")["Items"]
     assert len(items) == 2
-    assert items[0] == {"email_address": {"S": "foo@moto.com"}, "id": {"S": "foo"}}
-    assert items[1] == {"email_address": {"S": "bar@moto.com"}, "id": {"S": "bar"}}
+    assert {"email_address": {"S": "foo@moto.com"}, "id": {"S": "foo"}} in items
+    assert {"email_address": {"S": "bar@moto.com"}, "id": {"S": "bar"}} in items
 
 
 @mock_aws
@@ -6019,64 +5497,3 @@ def test_query_with_unknown_last_evaluated_key(table_name=None):
         ExclusiveStartKey=different_key,
     )
     assert p4["Items"] == [{"pk": {"S": "hash_value"}, "sk": {"S": "range_value6"}}]
-
-
-@pytest.mark.aws_verified
-@dynamodb_aws_verified(add_range=True)
-def test_scan_with_unknown_last_evaluated_key(table_name=None):
-    client = boto3.client("dynamodb", region_name="us-east-1")
-
-    for i in range(10):
-        client.put_item(
-            TableName=table_name,
-            Item={
-                "pk": {"S": "hash_value"},
-                "sk": {"S": f"range_value{i}"},
-            },
-        )
-
-    p1 = client.scan(
-        TableName=table_name,
-        FilterExpression="#h = :h",
-        ExpressionAttributeNames={"#h": "pk"},
-        ExpressionAttributeValues={":h": {"S": "hash_value"}},
-        Limit=1,
-    )
-    assert p1["Items"] == [{"pk": {"S": "hash_value"}, "sk": {"S": "range_value0"}}]
-
-    # Using the Exact ExclusiveStartKey provided
-    p2 = client.scan(
-        TableName=table_name,
-        FilterExpression="#h = :h",
-        ExpressionAttributeNames={"#h": "pk"},
-        ExpressionAttributeValues={":h": {"S": "hash_value"}},
-        Limit=1,
-        ExclusiveStartKey=p1["LastEvaluatedKey"],
-    )
-    assert p2["Items"] == [{"pk": {"S": "hash_value"}, "sk": {"S": "range_value1"}}]
-
-    # We can change ExclusiveStartKey
-    # It doesn't need to match - it just needs to be >= page1, but < page1
-    different_key = copy.copy(p1["LastEvaluatedKey"])
-    different_key["sk"]["S"] = different_key["sk"]["S"] + "0"
-    p3 = client.scan(
-        TableName=table_name,
-        FilterExpression="#h = :h",
-        ExpressionAttributeNames={"#h": "pk"},
-        ExpressionAttributeValues={":h": {"S": "hash_value"}},
-        Limit=1,
-        ExclusiveStartKey=different_key,
-    )
-    assert p3["Items"] == [{"pk": {"S": "hash_value"}, "sk": {"S": "range_value1"}}]
-
-    # Sanity check - increasing the sk to something much greater will result in a different outcome
-    different_key["sk"]["S"] = "range_value500"
-    p4 = client.scan(
-        TableName=table_name,
-        FilterExpression="#h = :h",
-        ExpressionAttributeNames={"#h": "pk"},
-        ExpressionAttributeValues={":h": {"S": "hash_value"}},
-        Limit=1,
-        ExclusiveStartKey=different_key,
-    )
-    assert p4["Items"] == [{"pk": {"S": "hash_value"}, "sk": {"S": "range_value6"}}]
diff --git a/tests/test_dynamodb/test_dynamodb_query.py b/tests/test_dynamodb/test_dynamodb_query.py
new file mode 100644
--- /dev/null
+++ b/tests/test_dynamodb/test_dynamodb_query.py
@@ -0,0 +1,558 @@
+from decimal import Decimal
+
+import boto3
+import pytest
+from boto3.dynamodb.conditions import Attr, Key
+
+from moto import mock_aws
+
+from . import dynamodb_aws_verified
+
+
+@pytest.mark.aws_verified
+@dynamodb_aws_verified(add_range=True, numeric_gsi_range=True)
+def test_query_gsi_range_comparison(table_name=None):
+    dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
+    table = dynamodb.Table(table_name)
+
+    table.put_item(
+        Item={"pk": "the-key", "sk": "123", "gsi_pk": "johndoe", "gsi_sk": 3}
+    )
+    table.put_item(
+        Item={"pk": "the-key", "sk": "456", "gsi_pk": "johndoe", "gsi_sk": 1}
+    )
+    table.put_item(
+        Item={"pk": "the-key", "sk": "789", "gsi_pk": "johndoe", "gsi_sk": 2}
+    )
+    table.put_item(
+        Item={"pk": "the-key", "sk": "159", "gsi_pk": "janedoe", "gsi_sk": 2}
+    )
+    table.put_item(
+        Item={"pk": "the-key", "sk": "601", "gsi_pk": "janedoe", "gsi_sk": 5}
+    )
+
+    # Test a query returning all johndoe items
+    results = table.query(
+        KeyConditionExpression=Key("gsi_pk").eq("johndoe") & Key("gsi_sk").gt(0),
+        ScanIndexForward=True,
+        IndexName="test_gsi",
+    )
+    assert results["ScannedCount"] == 3
+    assert [r["sk"] for r in results["Items"]] == ["456", "789", "123"]
+
+    # Return all johndoe items again, but in reverse
+    results = table.query(
+        KeyConditionExpression=Key("gsi_pk").eq("johndoe") & Key("gsi_sk").gt(0),
+        ScanIndexForward=False,
+        IndexName="test_gsi",
+    )
+    assert [r["sk"] for r in results["Items"]] == ["123", "789", "456"]
+
+    # Filter the creation to only return some of the results
+    # And reverse order of hash + range key
+    results = table.query(
+        KeyConditionExpression=Key("gsi_sk").gt(1) & Key("gsi_pk").eq("johndoe"),
+        IndexName="test_gsi",
+    )
+    assert results["Count"] == 2
+    assert [r["gsi_sk"] for r in results["Items"]] == [Decimal("2"), Decimal("3")]
+
+    # Filter to return no results
+    results = table.query(
+        KeyConditionExpression=Key("gsi_pk").eq("janedoe") & Key("gsi_sk").gt(9),
+        IndexName="test_gsi",
+    )
+    assert results["Count"] == 0
+
+    results = table.query(
+        KeyConditionExpression=Key("gsi_pk").eq("janedoe") & Key("gsi_sk").eq(5),
+        IndexName="test_gsi",
+    )
+    assert results["Count"] == 1
+
+    # Test range key sorting
+    results = table.query(
+        KeyConditionExpression=Key("gsi_pk").eq("johndoe") & Key("gsi_sk").gt(0),
+        IndexName="test_gsi",
+    )
+    expected = [Decimal("1"), Decimal("2"), Decimal("3")]
+    assert [r["gsi_sk"] for r in results["Items"]] == expected
+
+
+@mock_aws
+def test_key_condition_expressions():
+    dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
+
+    # Create the DynamoDB table.
+    dynamodb.create_table(
+        TableName="users",
+        KeySchema=[
+            {"AttributeName": "forum_name", "KeyType": "HASH"},
+            {"AttributeName": "subject", "KeyType": "RANGE"},
+        ],
+        AttributeDefinitions=[
+            {"AttributeName": "forum_name", "AttributeType": "S"},
+            {"AttributeName": "subject", "AttributeType": "S"},
+        ],
+        ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
+    )
+    table = dynamodb.Table("users")
+
+    table.put_item(Item={"forum_name": "the-key", "subject": "123"})
+    table.put_item(Item={"forum_name": "the-key", "subject": "456"})
+    table.put_item(Item={"forum_name": "the-key", "subject": "789"})
+
+    # Test a query returning all items
+    results = table.query(
+        KeyConditionExpression=Key("forum_name").eq("the-key") & Key("subject").gt("1"),
+        ScanIndexForward=True,
+    )
+    expected = ["123", "456", "789"]
+    for index, item in enumerate(results["Items"]):
+        assert item["subject"] == expected[index]
+
+    # Return all items again, but in reverse
+    results = table.query(
+        KeyConditionExpression=Key("forum_name").eq("the-key") & Key("subject").gt("1"),
+        ScanIndexForward=False,
+    )
+    for index, item in enumerate(reversed(results["Items"])):
+        assert item["subject"] == expected[index]
+
+    # Filter the subjects to only return some of the results
+    results = table.query(
+        KeyConditionExpression=Key("forum_name").eq("the-key")
+        & Key("subject").gt("234"),
+        ConsistentRead=True,
+    )
+    assert results["Count"] == 2
+
+    # Filter to return no results
+    results = table.query(
+        KeyConditionExpression=Key("forum_name").eq("the-key")
+        & Key("subject").gt("9999")
+    )
+    assert results["Count"] == 0
+
+    results = table.query(
+        KeyConditionExpression=Key("forum_name").eq("the-key")
+        & Key("subject").begins_with("12")
+    )
+    assert results["Count"] == 1
+
+    results = table.query(
+        KeyConditionExpression=Key("subject").begins_with("7")
+        & Key("forum_name").eq("the-key")
+    )
+    assert results["Count"] == 1
+
+    results = table.query(
+        KeyConditionExpression=Key("forum_name").eq("the-key")
+        & Key("subject").between("567", "890")
+    )
+    assert results["Count"] == 1
+
+
+@pytest.mark.aws_verified
+@dynamodb_aws_verified(add_range=True)
+def test_query_pagination(table_name=None):
+    dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
+    table = dynamodb.Table(table_name)
+
+    for i in range(3, 7):
+        table.put_item(Item={"pk": "the-key", "sk": f"{i}"})
+
+    for i in range(9, 6, -1):
+        table.put_item(Item={"pk": "the-key", "sk": f"{i}"})
+
+    for i in range(3):
+        table.put_item(Item={"pk": "the-key", "sk": f"{i}"})
+
+    page1 = table.query(KeyConditionExpression=Key("pk").eq("the-key"), Limit=6)
+    assert page1["Count"] == 6
+    assert page1["ScannedCount"] == 6
+    assert len(page1["Items"]) == 6
+    assert [i["sk"] for i in page1["Items"]] == ["0", "1", "2", "3", "4", "5"]
+
+    page2 = table.query(
+        KeyConditionExpression=Key("pk").eq("the-key"),
+        Limit=6,
+        ExclusiveStartKey=page1["LastEvaluatedKey"],
+    )
+    assert page2["Count"] == 4
+    assert page2["ScannedCount"] == 4
+    assert len(page2["Items"]) == 4
+    assert "LastEvaluatedKey" not in page2
+    assert [i["sk"] for i in page2["Items"]] == ["6", "7", "8", "9"]
+
+    results = page1["Items"] + page2["Items"]
+    subjects = set([int(r["sk"]) for r in results])
+    assert subjects == set(range(10))
+
+
+@pytest.mark.aws_verified
+@dynamodb_aws_verified(add_range=True, numeric_gsi_range=True)
+def test_query_gsi_pagination(table_name=None):
+    dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
+    table = dynamodb.Table(table_name)
+
+    for i in range(3, 7):
+        table.put_item(
+            Item={
+                "pk": "the-key",
+                "sk": f"{i}",
+                "gsi_pk": "johndoe",
+                "gsi_sk": Decimal(f"{i}"),
+            }
+        )
+
+    for i in range(9, 6, -1):
+        table.put_item(
+            Item={
+                "pk": "the-key",
+                "sk": f"{i}",
+                "gsi_pk": "johndoe",
+                "gsi_sk": Decimal(f"{i}"),
+            }
+        )
+
+    for i in range(3):
+        table.put_item(
+            Item={
+                "pk": "the-key",
+                "sk": f"{i}",
+                "gsi_pk": "johndoe",
+                "gsi_sk": Decimal(f"{i}"),
+            }
+        )
+
+    page1 = table.query(
+        KeyConditionExpression=Key("gsi_pk").eq("johndoe"),
+        IndexName="test_gsi",
+        Limit=6,
+    )
+    assert page1["Count"] == 6
+    assert page1["ScannedCount"] == 6
+    assert len(page1["Items"]) == 6
+    assert [i["sk"] for i in page1["Items"]] == ["0", "1", "2", "3", "4", "5"]
+
+    page2 = table.query(
+        KeyConditionExpression=Key("gsi_pk").eq("johndoe"),
+        IndexName="test_gsi",
+        Limit=6,
+        ExclusiveStartKey=page1["LastEvaluatedKey"],
+    )
+    assert page2["Count"] == 4
+    assert page2["ScannedCount"] == 4
+    assert len(page2["Items"]) == 4
+    assert "LastEvaluatedKey" not in page2
+    assert [i["sk"] for i in page2["Items"]] == ["6", "7", "8", "9"]
+
+    results = page1["Items"] + page2["Items"]
+    subjects = set([int(r["sk"]) for r in results])
+    assert subjects == set(range(10))
+
+
+@pytest.mark.aws_verified
+@dynamodb_aws_verified(add_range=True, add_lsi=True)
+def test_query_lsi_pagination(table_name=None):
+    dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
+    table = dynamodb.Table(table_name)
+
+    for i in range(3, 7):
+        table.put_item(Item={"pk": "the-key", "sk": f"{i}", "lsi_sk": f"{i}"})
+    for i in range(9, 6, -1):
+        table.put_item(Item={"pk": "the-key", "sk": f"{i}", "lsi_sk": f"{i}"})
+    for i in range(3):
+        table.put_item(Item={"pk": "the-key", "sk": f"{i}", "lsi_sk": f"{i}"})
+    for i in range(15, 20):
+        table.put_item(Item={"pk": "the-key", "sk": f"{i}"})
+    for i in range(25, 30):
+        table.put_item(Item={"pk": "other", "sk": f"{i}"})
+    for i in range(35, 40):
+        table.put_item(Item={"pk": "other_in_lsi", "sk": f"{i}", "lsi_sk": f"{i}"})
+
+    res = table.query(
+        KeyConditionExpression=Key("pk").eq("the-key") & Key("lsi_sk").eq("1"),
+        IndexName="test_lsi",
+    )
+    assert res["Count"] == 1
+    assert res["Items"] == [{"lsi_sk": "1", "pk": "the-key", "sk": "1"}]
+
+    res = table.query(
+        KeyConditionExpression=Key("pk").eq("the-key") & Key("lsi_sk").eq("2"),
+        IndexName="test_lsi",
+        ConsistentRead=True,
+    )
+    assert res["Count"] == 1
+    assert res["Items"] == [{"lsi_sk": "2", "pk": "the-key", "sk": "2"}]
+
+    # Verify pagination when getting all items with a specific hash
+    page1 = table.query(
+        KeyConditionExpression=Key("pk").eq("the-key"),
+        IndexName="test_lsi",
+        Limit=6,
+    )
+    assert [i["sk"] for i in page1["Items"]] == ["0", "1", "2", "3", "4", "5"]
+    assert page1["LastEvaluatedKey"] == {"pk": "the-key", "sk": "5", "lsi_sk": "5"}
+
+    page2 = table.query(
+        KeyConditionExpression=Key("pk").eq("the-key"),
+        IndexName="test_lsi",
+        ExclusiveStartKey=page1["LastEvaluatedKey"],
+    )
+    assert [i["sk"] for i in page2["Items"]] == ["6", "7", "8", "9"]
+
+
+@pytest.mark.aws_verified
+@dynamodb_aws_verified(add_range=True, numeric_lsi_range=True)
+def test_query_lsi_pagination_with_numerical_local_range_key(table_name=None):
+    dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
+    table = dynamodb.Table(table_name)
+
+    for i in range(3, 7):
+        table.put_item(Item={"pk": "the-key", "sk": f"{i}", "lsi_sk": Decimal(f"{i}")})
+    for i in range(9, 6, -1):
+        table.put_item(Item={"pk": "the-key", "sk": f"{i}", "lsi_sk": Decimal(f"{i}")})
+    for i in range(3):
+        table.put_item(Item={"pk": "the-key", "sk": f"{i}", "lsi_sk": Decimal(f"{i}")})
+
+    items = table.query(
+        KeyConditionExpression=Key("pk").eq("the-key") & Key("lsi_sk").eq(Decimal("1")),
+        IndexName="test_lsi",
+    )["Items"]
+    items == [{"pk": "the-key", "sk": "1", "lsi_sk": Decimal("1")}]
+
+    # Verify pagination when getting all items with a specific hash
+    page1 = table.query(
+        KeyConditionExpression=Key("pk").eq("the-key"),
+        IndexName="test_lsi",
+        Limit=6,
+    )
+    assert [i["lsi_sk"] for i in page1["Items"]] == [Decimal(i) for i in range(6)]
+    assert page1["LastEvaluatedKey"] == {
+        "pk": "the-key",
+        "sk": "5",
+        "lsi_sk": Decimal("5"),
+    }
+
+    page2 = table.query(
+        KeyConditionExpression=Key("pk").eq("the-key"),
+        IndexName="test_lsi",
+        ExclusiveStartKey=page1["LastEvaluatedKey"],
+    )
+    assert [i["lsi_sk"] for i in page2["Items"]] == [Decimal(i) for i in range(6, 10)]
+
+
+@pytest.mark.aws_verified
+@dynamodb_aws_verified(add_gsi_range=True)
+def test_query_gsi_with_range_key(table_name=None):
+    dynamodb = boto3.client("dynamodb", region_name="us-east-1")
+
+    item_with_gsi_pk_and_sk = {
+        "pk": {"S": "test1"},
+        "gsi_pk": {"S": "key1"},
+        "gsi_sk": {"S": "range1"},
+    }
+    dynamodb.put_item(TableName=table_name, Item=item_with_gsi_pk_and_sk)
+    dynamodb.put_item(
+        TableName=table_name, Item={"pk": {"S": "test2"}, "gsi_pk": {"S": "key1"}}
+    )
+
+    res = dynamodb.query(
+        TableName=table_name,
+        IndexName="test_gsi",
+        KeyConditionExpression="gsi_pk = :gsi_hash_key and gsi_sk = :gsi_range_key",
+        ExpressionAttributeValues={
+            ":gsi_hash_key": {"S": "key1"},
+            ":gsi_range_key": {"S": "range1"},
+        },
+    )
+    assert res["Count"] == 1
+    assert res["Items"][0] == item_with_gsi_pk_and_sk
+
+
+@pytest.mark.aws_verified
+@dynamodb_aws_verified(numeric_range=True)
+def test_sorted_query_with_numerical_sort_key(table_name=None):
+    dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
+
+    def create_item(price):
+        return {"pk": "M", "sk": price}
+
+    table = dynamodb.Table(table_name)
+    items = list(map(create_item, [2, 1, 10, 3]))
+    for item in items:
+        table.put_item(Item=item)
+
+    response = table.query(KeyConditionExpression=Key("pk").eq("M"))
+
+    response_items = response["Items"]
+    assert len(items) == len(response_items)
+    assert all(isinstance(item["sk"], Decimal) for item in response_items)
+    response_prices = [item["sk"] for item in response_items]
+    expected_prices = [Decimal(item["sk"]) for item in items]
+    expected_prices.sort()
+    assert expected_prices == response_prices
+
+
+@pytest.mark.aws_verified
+@dynamodb_aws_verified(numeric_gsi_range=True)
+def test_gsi_verify_negative_number_order(table_name=None):
+    item1 = {"pk": "pk-1", "gsi_pk": "gsi-k1", "gsi_sk": Decimal("-0.6")}
+    item2 = {"pk": "pk-2", "gsi_pk": "gsi-k1", "gsi_sk": Decimal("-0.7")}
+    item3 = {"pk": "pk-3", "gsi_pk": "gsi-k1", "gsi_sk": Decimal("0.7")}
+
+    dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
+    table = dynamodb.Table(table_name)
+    table.put_item(Item=item3)
+    table.put_item(Item=item1)
+    table.put_item(Item=item2)
+
+    resp = table.query(
+        KeyConditionExpression=Key("gsi_pk").eq("gsi-k1"), IndexName="test_gsi"
+    )
+    # Items should be ordered with the lowest number first
+    assert [float(item["gsi_sk"]) for item in resp["Items"]] == [-0.7, -0.6, 0.7]
+
+
+@mock_aws
+class TestFilterExpression:
+    def test_query_filter(self):
+        client = boto3.client("dynamodb", region_name="us-east-1")
+        dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
+
+        # Create the DynamoDB table.
+        client.create_table(
+            TableName="test1",
+            AttributeDefinitions=[
+                {"AttributeName": "client", "AttributeType": "S"},
+                {"AttributeName": "app", "AttributeType": "S"},
+            ],
+            KeySchema=[
+                {"AttributeName": "client", "KeyType": "HASH"},
+                {"AttributeName": "app", "KeyType": "RANGE"},
+            ],
+            ProvisionedThroughput={"ReadCapacityUnits": 123, "WriteCapacityUnits": 123},
+        )
+        client.put_item(
+            TableName="test1",
+            Item={
+                "client": {"S": "client1"},
+                "app": {"S": "app1"},
+                "nested": {
+                    "M": {
+                        "version": {"S": "version1"},
+                        "contents": {"L": [{"S": "value1"}, {"S": "value2"}]},
+                    }
+                },
+            },
+        )
+        client.put_item(
+            TableName="test1",
+            Item={
+                "client": {"S": "client1"},
+                "app": {"S": "app2"},
+                "nested": {
+                    "M": {
+                        "version": {"S": "version2"},
+                        "contents": {"L": [{"S": "value1"}, {"S": "value2"}]},
+                    }
+                },
+            },
+        )
+
+        table = dynamodb.Table("test1")
+        response = table.query(KeyConditionExpression=Key("client").eq("client1"))
+        assert response["Count"] == 2
+        assert response["ScannedCount"] == 2
+
+        response = table.query(
+            KeyConditionExpression=Key("client").eq("client1"),
+            FilterExpression=Attr("app").eq("app2"),
+        )
+        assert response["Count"] == 1
+        assert response["ScannedCount"] == 2
+        assert response["Items"][0]["app"] == "app2"
+        response = table.query(
+            KeyConditionExpression=Key("client").eq("client1"),
+            FilterExpression=Attr("app").contains("app"),
+        )
+        assert response["Count"] == 2
+
+        response = table.query(
+            KeyConditionExpression=Key("client").eq("client1"),
+            FilterExpression=Attr("nested.version").contains("version"),
+        )
+        assert response["Count"] == 2
+
+        response = table.query(
+            KeyConditionExpression=Key("client").eq("client1"),
+            FilterExpression=Attr("nested.contents[0]").eq("value1"),
+        )
+        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"
+
+    def test_query_filter_overlapping_expression_prefixes(self):
+        client = boto3.client("dynamodb", region_name="us-east-1")
+        dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
+
+        # Create the DynamoDB table.
+        client.create_table(
+            TableName="test1",
+            AttributeDefinitions=[
+                {"AttributeName": "client", "AttributeType": "S"},
+                {"AttributeName": "app", "AttributeType": "S"},
+            ],
+            KeySchema=[
+                {"AttributeName": "client", "KeyType": "HASH"},
+                {"AttributeName": "app", "KeyType": "RANGE"},
+            ],
+            ProvisionedThroughput={"ReadCapacityUnits": 123, "WriteCapacityUnits": 123},
+        )
+
+        client.put_item(
+            TableName="test1",
+            Item={
+                "client": {"S": "client1"},
+                "app": {"S": "app1"},
+                "nested": {
+                    "M": {
+                        "version": {"S": "version1"},
+                        "contents": {"L": [{"S": "value1"}, {"S": "value2"}]},
+                    }
+                },
+            },
+        )
+
+        table = dynamodb.Table("test1")
+        response = table.query(
+            KeyConditionExpression=Key("client").eq("client1") & Key("app").eq("app1"),
+            ProjectionExpression="#1, #10, nested",
+            ExpressionAttributeNames={"#1": "client", "#10": "app"},
+        )
+
+        assert response["Count"] == 1
+        assert response["Items"][0] == {
+            "client": "client1",
+            "app": "app1",
+            "nested": {"version": "version1", "contents": ["value1", "value2"]},
+        }
diff --git a/tests/test_dynamodb/test_dynamodb_scan.py b/tests/test_dynamodb/test_dynamodb_scan.py
new file mode 100644
--- /dev/null
+++ b/tests/test_dynamodb/test_dynamodb_scan.py
@@ -0,0 +1,587 @@
+import copy
+from decimal import Decimal as dec
+
+import boto3
+import pytest
+from boto3.dynamodb.conditions import Attr
+from botocore.exceptions import ClientError
+
+from moto import mock_aws
+
+from . import dynamodb_aws_verified
+
+
+@pytest.mark.aws_verified
+@dynamodb_aws_verified(add_range=True)
+def test_scan_with_unknown_last_evaluated_key(table_name=None):
+    client = boto3.client("dynamodb", region_name="us-east-1")
+
+    for i in range(10):
+        client.put_item(
+            TableName=table_name,
+            Item={
+                "pk": {"S": "hash_value"},
+                "sk": {"S": f"range_value{i}"},
+            },
+        )
+
+    p1 = client.scan(
+        TableName=table_name,
+        FilterExpression="#h = :h",
+        ExpressionAttributeNames={"#h": "pk"},
+        ExpressionAttributeValues={":h": {"S": "hash_value"}},
+        Limit=1,
+    )
+    assert p1["Items"] == [{"pk": {"S": "hash_value"}, "sk": {"S": "range_value0"}}]
+
+    # Using the Exact ExclusiveStartKey provided
+    p2 = client.scan(
+        TableName=table_name,
+        FilterExpression="#h = :h",
+        ExpressionAttributeNames={"#h": "pk"},
+        ExpressionAttributeValues={":h": {"S": "hash_value"}},
+        Limit=1,
+        ExclusiveStartKey=p1["LastEvaluatedKey"],
+    )
+    assert p2["Items"] == [{"pk": {"S": "hash_value"}, "sk": {"S": "range_value1"}}]
+
+    # We can change ExclusiveStartKey
+    # It doesn't need to match - it just needs to be >= page1, but < page1
+    different_key = copy.copy(p1["LastEvaluatedKey"])
+    different_key["sk"]["S"] = different_key["sk"]["S"] + "0"
+    p3 = client.scan(
+        TableName=table_name,
+        FilterExpression="#h = :h",
+        ExpressionAttributeNames={"#h": "pk"},
+        ExpressionAttributeValues={":h": {"S": "hash_value"}},
+        Limit=1,
+        ExclusiveStartKey=different_key,
+    )
+    assert p3["Items"] == [{"pk": {"S": "hash_value"}, "sk": {"S": "range_value1"}}]
+
+    # Sanity check - increasing the sk to something much greater will result in a different outcome
+    different_key["sk"]["S"] = "range_value500"
+    p4 = client.scan(
+        TableName=table_name,
+        FilterExpression="#h = :h",
+        ExpressionAttributeNames={"#h": "pk"},
+        ExpressionAttributeValues={":h": {"S": "hash_value"}},
+        Limit=1,
+        ExclusiveStartKey=different_key,
+    )
+    assert p4["Items"] == [{"pk": {"S": "hash_value"}, "sk": {"S": "range_value6"}}]
+
+
+@pytest.mark.aws_verified
+@dynamodb_aws_verified(add_range=True)
+def test_scan_with_alternating_hash_keys(table_name=None):
+    ddb = boto3.resource("dynamodb", "us-east-1")
+    table = ddb.Table(table_name)
+
+    # Insert Data
+    data = [dict(pk="A" if i % 2 else "B", sk=str(i)) for i in range(8)]
+    with table.batch_writer() as batch:
+        for item in data:
+            batch.put_item(Item=item)
+    # Also add some range keys in reverse, to verify they come back in a natural order
+    data = [dict(pk="A", sk=str(i)) for i in range(20, 15, -1)]
+    with table.batch_writer() as batch:
+        for item in data:
+            batch.put_item(Item=item)
+
+    responses = []
+
+    resp = table.scan(Limit=3)
+    responses.append(resp["Items"])
+
+    while "LastEvaluatedKey" in resp:
+        resp = table.scan(Limit=3, ExclusiveStartKey=resp["LastEvaluatedKey"])
+        responses.append(resp["Items"])
+
+    assert len(responses) == 5
+    assert responses[0] == [
+        {"pk": "A", "sk": "1"},
+        {"pk": "A", "sk": "16"},
+        {"pk": "A", "sk": "17"},
+    ]
+    assert responses[1] == [
+        {"pk": "A", "sk": "18"},
+        {"pk": "A", "sk": "19"},
+        {"pk": "A", "sk": "20"},
+    ]
+    assert responses[2] == [
+        {"pk": "A", "sk": "3"},
+        {"pk": "A", "sk": "5"},
+        {"pk": "A", "sk": "7"},
+    ]
+    assert responses[3] == [
+        {"pk": "B", "sk": "0"},
+        {"pk": "B", "sk": "2"},
+        {"pk": "B", "sk": "4"},
+    ]
+    assert responses[4] == [{"pk": "B", "sk": "6"}]
+
+
+@pytest.mark.aws_verified
+@dynamodb_aws_verified(numeric_range=True)
+def test_scan_with_numeric_range_key(table_name=None):
+    ddb = boto3.resource("dynamodb", "us-east-1")
+    table = ddb.Table(table_name)
+
+    # Insert Data
+    data = [dict(pk="A" if i % 2 else "B", sk=i) for i in range(8)]
+    with table.batch_writer() as batch:
+        for item in data:
+            batch.put_item(Item=item)
+    # Also add some range keys in reverse, to verify they come back in a natural order
+    data = [dict(pk="A", sk=i) for i in range(20, 15, -1)]
+    with table.batch_writer() as batch:
+        for item in data:
+            batch.put_item(Item=item)
+
+    responses = []
+
+    resp = table.scan(Limit=3)
+    responses.append(resp["Items"])
+
+    while "LastEvaluatedKey" in resp:
+        resp = table.scan(Limit=3, ExclusiveStartKey=resp["LastEvaluatedKey"])
+        responses.append(resp["Items"])
+
+    assert len(responses) == 5
+    assert responses[0] == [
+        {"pk": "A", "sk": dec("1")},
+        {"pk": "A", "sk": dec("3")},
+        {"pk": "A", "sk": dec("5")},
+    ]
+    assert responses[1] == [
+        {"pk": "A", "sk": dec("7")},
+        {"pk": "A", "sk": dec("16")},
+        {"pk": "A", "sk": dec("17")},
+    ]
+    assert responses[2] == [
+        {"pk": "A", "sk": dec("18")},
+        {"pk": "A", "sk": dec("19")},
+        {"pk": "A", "sk": dec("20")},
+    ]
+    assert responses[3] == [
+        {"pk": "B", "sk": dec("0")},
+        {"pk": "B", "sk": dec("2")},
+        {"pk": "B", "sk": dec("4")},
+    ]
+    assert responses[4] == [{"pk": "B", "sk": dec("6")}]
+
+
+@pytest.mark.aws_verified
+@dynamodb_aws_verified(add_gsi=True)
+def test_scan_by_global_index(table_name=None):
+    dynamodb = boto3.client("dynamodb", region_name="us-east-1")
+    resource = boto3.resource("dynamodb", region_name="us-east-1")
+    table = resource.Table(table_name)
+
+    # Insert Data
+    data = [dict(pk=f"A{i}", gsi_pk=f"gsi{i}") for i in range(5)]
+    with table.batch_writer() as batch:
+        for item in data:
+            batch.put_item(Item=item)
+    # Also add some range keys in reverse, to verify they come back in a natural order
+    data = [dict(pk=f"A{i}", gsi_pk=f"gsi{i}") for i in range(20, 15, -1)]
+    with table.batch_writer() as batch:
+        for item in data:
+            batch.put_item(Item=item)
+
+    res = dynamodb.scan(TableName=table_name)
+    assert res["Count"] == 10
+    assert len(res["Items"]) == 10
+
+    res = dynamodb.scan(TableName=table_name, ConsistentRead=True)
+    assert res["Count"] == 10
+    assert len(res["Items"]) == 10
+
+    res = dynamodb.scan(TableName=table_name, IndexName="test_gsi")
+    assert res["Count"] == 10
+
+    page1 = dynamodb.scan(TableName=table_name, IndexName="test_gsi", Limit=6)
+    assert page1["Count"] == 6
+    page1_items = {i["gsi_pk"]["S"] for i in page1["Items"]}
+
+    page2 = dynamodb.scan(
+        TableName=table_name,
+        IndexName="test_gsi",
+        ExclusiveStartKey=page1["LastEvaluatedKey"],
+    )
+    assert page2["Count"] == 4
+    page2_items = {i["gsi_pk"]["S"] for i in page2["Items"]}
+
+    # AWS does not order items
+    # So we can only verify that all items appear at some point
+    expected = {
+        "gsi0",
+        "gsi1",
+        "gsi16",
+        "gsi17",
+        "gsi18",
+        "gsi19",
+        "gsi2",
+        "gsi20",
+        "gsi3",
+        "gsi4",
+    }
+    assert page1_items.union(page2_items) == expected
+
+
+@mock_aws
+def test_scan_by_global_and_local_index():
+    dynamodb = boto3.client("dynamodb", region_name="us-east-1")
+
+    dynamodb.create_table(
+        TableName="test",
+        KeySchema=[
+            {"AttributeName": "id", "KeyType": "HASH"},
+            {"AttributeName": "range_key", "KeyType": "RANGE"},
+        ],
+        AttributeDefinitions=[
+            {"AttributeName": "id", "AttributeType": "S"},
+            {"AttributeName": "range_key", "AttributeType": "S"},
+            {"AttributeName": "gsi_col", "AttributeType": "S"},
+            {"AttributeName": "gsi_range_key", "AttributeType": "S"},
+            {"AttributeName": "lsi_range_key", "AttributeType": "S"},
+        ],
+        ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 1},
+        GlobalSecondaryIndexes=[
+            {
+                "IndexName": "test_gsi",
+                "KeySchema": [
+                    {"AttributeName": "gsi_col", "KeyType": "HASH"},
+                    {"AttributeName": "gsi_range_key", "KeyType": "RANGE"},
+                ],
+                "Projection": {"ProjectionType": "ALL"},
+                "ProvisionedThroughput": {
+                    "ReadCapacityUnits": 1,
+                    "WriteCapacityUnits": 1,
+                },
+            }
+        ],
+        LocalSecondaryIndexes=[
+            {
+                "IndexName": "test_lsi",
+                "KeySchema": [
+                    {"AttributeName": "id", "KeyType": "HASH"},
+                    {"AttributeName": "lsi_range_key", "KeyType": "RANGE"},
+                ],
+                "Projection": {"ProjectionType": "ALL"},
+            }
+        ],
+    )
+
+    dynamodb.put_item(
+        TableName="test",
+        Item={
+            "id": {"S": "1"},
+            "range_key": {"S": "1"},
+            "col1": {"S": "val1"},
+            "gsi_col": {"S": "1"},
+            "gsi_range_key": {"S": "1"},
+            "lsi_range_key": {"S": "1"},
+        },
+    )
+
+    dynamodb.put_item(
+        TableName="test",
+        Item={
+            "id": {"S": "1"},
+            "range_key": {"S": "2"},
+            "col1": {"S": "val2"},
+            "gsi_col": {"S": "1"},
+            "gsi_range_key": {"S": "2"},
+            "lsi_range_key": {"S": "2"},
+        },
+    )
+
+    dynamodb.put_item(
+        TableName="test",
+        Item={"id": {"S": "3"}, "range_key": {"S": "1"}, "col1": {"S": "val3"}},
+    )
+
+    res = dynamodb.scan(TableName="test")
+    assert res["Count"] == 3
+    assert len(res["Items"]) == 3
+
+    res = dynamodb.scan(TableName="test", Limit=1)
+    assert res["Count"] == 1
+    assert res["ScannedCount"] == 1
+
+    res = dynamodb.scan(TableName="test", ExclusiveStartKey=res["LastEvaluatedKey"])
+    assert res["Count"] == 2
+    assert res["ScannedCount"] == 2
+
+    res = dynamodb.scan(TableName="test", IndexName="test_gsi")
+    assert res["Count"] == 2
+    assert res["ScannedCount"] == 2
+    assert len(res["Items"]) == 2
+
+    res = dynamodb.scan(TableName="test", IndexName="test_gsi", Limit=1)
+    assert res["Count"] == 1
+    assert res["ScannedCount"] == 1
+    assert len(res["Items"]) == 1
+    last_eval_key = res["LastEvaluatedKey"]
+    assert last_eval_key["id"]["S"] == "1"
+    assert last_eval_key["gsi_col"]["S"] == "1"
+    assert last_eval_key["gsi_range_key"]["S"] == "1"
+
+    res = dynamodb.scan(
+        TableName="test", IndexName="test_gsi", ExclusiveStartKey=last_eval_key
+    )
+    assert res["Count"] == 1
+    assert res["ScannedCount"] == 1
+
+    res = dynamodb.scan(TableName="test", IndexName="test_lsi")
+    assert res["Count"] == 2
+    assert res["ScannedCount"] == 2
+    assert len(res["Items"]) == 2
+
+    res = dynamodb.scan(TableName="test", IndexName="test_lsi", ConsistentRead=True)
+    assert res["Count"] == 2
+    assert res["ScannedCount"] == 2
+    assert len(res["Items"]) == 2
+
+    res = dynamodb.scan(TableName="test", IndexName="test_lsi", Limit=1)
+    assert res["Count"] == 1
+    assert res["ScannedCount"] == 1
+    assert len(res["Items"]) == 1
+    last_eval_key = res["LastEvaluatedKey"]
+    assert last_eval_key["id"]["S"] == "1"
+    assert last_eval_key["range_key"]["S"] == "1"
+    assert last_eval_key["lsi_range_key"]["S"] == "1"
+
+
+@mock_aws
+class TestFilterExpression:
+    def test_scan_filter(self):
+        client = boto3.client("dynamodb", region_name="us-east-1")
+        dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
+
+        # Create the DynamoDB table.
+        client.create_table(
+            TableName="test1",
+            AttributeDefinitions=[
+                {"AttributeName": "client", "AttributeType": "S"},
+                {"AttributeName": "app", "AttributeType": "S"},
+            ],
+            KeySchema=[
+                {"AttributeName": "client", "KeyType": "HASH"},
+                {"AttributeName": "app", "KeyType": "RANGE"},
+            ],
+            ProvisionedThroughput={"ReadCapacityUnits": 123, "WriteCapacityUnits": 123},
+        )
+        client.put_item(
+            TableName="test1", Item={"client": {"S": "client1"}, "app": {"S": "app1"}}
+        )
+
+        table = dynamodb.Table("test1")
+        response = table.scan(FilterExpression=Attr("app").eq("app2"))
+        assert response["Count"] == 0
+
+        response = table.scan(FilterExpression=Attr("app").eq("app1"))
+        assert response["Count"] == 1
+
+        response = table.scan(FilterExpression=Attr("app").ne("app2"))
+        assert response["Count"] == 1
+
+        response = table.scan(FilterExpression=Attr("app").ne("app1"))
+        assert response["Count"] == 0
+
+    def test_scan_filter2(self):
+        client = boto3.client("dynamodb", region_name="us-east-1")
+
+        # Create the DynamoDB table.
+        client.create_table(
+            TableName="test1",
+            AttributeDefinitions=[
+                {"AttributeName": "client", "AttributeType": "S"},
+                {"AttributeName": "app", "AttributeType": "N"},
+            ],
+            KeySchema=[
+                {"AttributeName": "client", "KeyType": "HASH"},
+                {"AttributeName": "app", "KeyType": "RANGE"},
+            ],
+            ProvisionedThroughput={"ReadCapacityUnits": 123, "WriteCapacityUnits": 123},
+        )
+        client.put_item(
+            TableName="test1", Item={"client": {"S": "client1"}, "app": {"N": "1"}}
+        )
+
+        response = client.scan(
+            TableName="test1",
+            Select="ALL_ATTRIBUTES",
+            FilterExpression="#tb >= :dt",
+            ExpressionAttributeNames={"#tb": "app"},
+            ExpressionAttributeValues={":dt": {"N": str(1)}},
+        )
+        assert response["Count"] == 1
+
+    def test_scan_filter3(self):
+        client = boto3.client("dynamodb", region_name="us-east-1")
+        dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
+
+        # Create the DynamoDB table.
+        client.create_table(
+            TableName="test1",
+            AttributeDefinitions=[
+                {"AttributeName": "client", "AttributeType": "S"},
+                {"AttributeName": "app", "AttributeType": "N"},
+            ],
+            KeySchema=[
+                {"AttributeName": "client", "KeyType": "HASH"},
+                {"AttributeName": "app", "KeyType": "RANGE"},
+            ],
+            ProvisionedThroughput={"ReadCapacityUnits": 123, "WriteCapacityUnits": 123},
+        )
+        client.put_item(
+            TableName="test1",
+            Item={
+                "client": {"S": "client1"},
+                "app": {"N": "1"},
+                "active": {"BOOL": True},
+            },
+        )
+
+        table = dynamodb.Table("test1")
+        response = table.scan(FilterExpression=Attr("active").eq(True))
+        assert response["Count"] == 1
+
+        response = table.scan(FilterExpression=Attr("active").ne(True))
+        assert response["Count"] == 0
+
+        response = table.scan(FilterExpression=Attr("active").ne(False))
+        assert response["Count"] == 1
+
+        response = table.scan(FilterExpression=Attr("app").ne(1))
+        assert response["Count"] == 0
+
+        response = table.scan(FilterExpression=Attr("app").ne(2))
+        assert response["Count"] == 1
+
+    def test_scan_filter4(self):
+        client = boto3.client("dynamodb", region_name="us-east-1")
+        dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
+
+        # Create the DynamoDB table.
+        client.create_table(
+            TableName="test1",
+            AttributeDefinitions=[
+                {"AttributeName": "client", "AttributeType": "S"},
+                {"AttributeName": "app", "AttributeType": "N"},
+            ],
+            KeySchema=[
+                {"AttributeName": "client", "KeyType": "HASH"},
+                {"AttributeName": "app", "KeyType": "RANGE"},
+            ],
+            ProvisionedThroughput={"ReadCapacityUnits": 123, "WriteCapacityUnits": 123},
+        )
+
+        table = dynamodb.Table("test1")
+        response = table.scan(
+            FilterExpression=Attr("epoch_ts").lt(7) & Attr("fanout_ts").not_exists()
+        )
+        # Just testing
+        assert response["Count"] == 0
+
+    def test_filter_should_not_return_non_existing_attributes(self):
+        table_name = "my-table"
+        item = {"partitionKey": "pk-2", "my-attr": 42}
+        # Create table
+        res = boto3.resource("dynamodb", region_name="us-east-1")
+        res.create_table(
+            TableName=table_name,
+            KeySchema=[{"AttributeName": "partitionKey", "KeyType": "HASH"}],
+            AttributeDefinitions=[
+                {"AttributeName": "partitionKey", "AttributeType": "S"}
+            ],
+            BillingMode="PAY_PER_REQUEST",
+        )
+        table = res.Table(table_name)
+        # Insert items
+        table.put_item(Item={"partitionKey": "pk-1"})
+        table.put_item(Item=item)
+        # Verify a few operations
+        # Assert we only find the item that has this attribute
+        assert table.scan(FilterExpression=Attr("my-attr").lt(43))["Items"] == [item]
+        assert table.scan(FilterExpression=Attr("my-attr").lte(42))["Items"] == [item]
+        assert table.scan(FilterExpression=Attr("my-attr").gte(42))["Items"] == [item]
+        assert table.scan(FilterExpression=Attr("my-attr").gt(41))["Items"] == [item]
+        # Sanity check that we can't find the item if the FE is wrong
+        assert table.scan(FilterExpression=Attr("my-attr").gt(43))["Items"] == []
+
+    def test_bad_scan_filter(self):
+        client = boto3.client("dynamodb", region_name="us-east-1")
+        dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
+
+        # Create the DynamoDB table.
+        client.create_table(
+            TableName="test1",
+            AttributeDefinitions=[
+                {"AttributeName": "client", "AttributeType": "S"},
+                {"AttributeName": "app", "AttributeType": "S"},
+            ],
+            KeySchema=[
+                {"AttributeName": "client", "KeyType": "HASH"},
+                {"AttributeName": "app", "KeyType": "RANGE"},
+            ],
+            ProvisionedThroughput={"ReadCapacityUnits": 123, "WriteCapacityUnits": 123},
+        )
+        table = dynamodb.Table("test1")
+
+        # Bad expression
+        with pytest.raises(ClientError) as exc:
+            table.scan(FilterExpression="client test")
+        assert exc.value.response["Error"]["Code"] == "ValidationException"
+
+    def test_scan_with_scanfilter(self):
+        table_name = "my-table"
+        item = {"partitionKey": "pk-2", "my-attr": 42}
+        client = boto3.client("dynamodb", region_name="us-east-1")
+        res = boto3.resource("dynamodb", region_name="us-east-1")
+        res.create_table(
+            TableName=table_name,
+            KeySchema=[{"AttributeName": "partitionKey", "KeyType": "HASH"}],
+            AttributeDefinitions=[
+                {"AttributeName": "partitionKey", "AttributeType": "S"}
+            ],
+            BillingMode="PAY_PER_REQUEST",
+        )
+        table = res.Table(table_name)
+        table.put_item(Item={"partitionKey": "pk-1"})
+        table.put_item(Item=item)
+
+        # ScanFilter: EQ
+        # The DynamoDB table-resource sends the AttributeValueList in the wrong format
+        # So this operation never finds any data, in Moto or AWS
+        table.scan(
+            ScanFilter={
+                "my-attr": {
+                    "AttributeValueList": [{"N": "42"}],
+                    "ComparisonOperator": "EQ",
+                }
+            }
+        )
+
+        # ScanFilter: EQ
+        # If we use the boto3-client, we do receive the correct data
+        items = client.scan(
+            TableName=table_name,
+            ScanFilter={
+                "partitionKey": {
+                    "AttributeValueList": [{"S": "pk-1"}],
+                    "ComparisonOperator": "EQ",
+                }
+            },
+        )["Items"]
+        assert items == [{"partitionKey": {"S": "pk-1"}}]
+
+        # ScanFilter: NONE
+        # Note that we can use the table-resource here, because we're not using the AttributeValueList
+        items = table.scan(ScanFilter={"my-attr": {"ComparisonOperator": "NULL"}})[
+            "Items"
+        ]
+        assert items == [{"partitionKey": "pk-1"}]
diff --git a/tests/test_dynamodb/test_dynamodb_table_with_range_key.py b/tests/test_dynamodb/test_dynamodb_table_with_range_key.py
--- a/tests/test_dynamodb/test_dynamodb_table_with_range_key.py
+++ b/tests/test_dynamodb/test_dynamodb_table_with_range_key.py
@@ -77,80 +77,6 @@ def test_query_filter_boto3():
     assert res["Items"] == [{"pk": "pk", "sk": "sk-1"}, {"pk": "pk", "sk": "sk-2"}]
 
 
-@mock_aws
-def test_boto3_conditions():
-    dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
-
-    # Create the DynamoDB table.
-    table = dynamodb.create_table(
-        TableName="users",
-        KeySchema=[
-            {"AttributeName": "forum_name", "KeyType": "HASH"},
-            {"AttributeName": "subject", "KeyType": "RANGE"},
-        ],
-        AttributeDefinitions=[
-            {"AttributeName": "forum_name", "AttributeType": "S"},
-            {"AttributeName": "subject", "AttributeType": "S"},
-        ],
-        ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
-    )
-    table = dynamodb.Table("users")
-
-    table.put_item(Item={"forum_name": "the-key", "subject": "123"})
-    table.put_item(Item={"forum_name": "the-key", "subject": "456"})
-    table.put_item(Item={"forum_name": "the-key", "subject": "789"})
-
-    # Test a query returning all items
-    results = table.query(
-        KeyConditionExpression=Key("forum_name").eq("the-key") & Key("subject").gt("1"),
-        ScanIndexForward=True,
-    )
-    expected = ["123", "456", "789"]
-    for index, item in enumerate(results["Items"]):
-        assert item["subject"] == expected[index]
-
-    # Return all items again, but in reverse
-    results = table.query(
-        KeyConditionExpression=Key("forum_name").eq("the-key") & Key("subject").gt("1"),
-        ScanIndexForward=False,
-    )
-    for index, item in enumerate(reversed(results["Items"])):
-        assert item["subject"] == expected[index]
-
-    # Filter the subjects to only return some of the results
-    results = table.query(
-        KeyConditionExpression=Key("forum_name").eq("the-key")
-        & Key("subject").gt("234"),
-        ConsistentRead=True,
-    )
-    assert results["Count"] == 2
-
-    # Filter to return no results
-    results = table.query(
-        KeyConditionExpression=Key("forum_name").eq("the-key")
-        & Key("subject").gt("9999")
-    )
-    assert results["Count"] == 0
-
-    results = table.query(
-        KeyConditionExpression=Key("forum_name").eq("the-key")
-        & Key("subject").begins_with("12")
-    )
-    assert results["Count"] == 1
-
-    results = table.query(
-        KeyConditionExpression=Key("subject").begins_with("7")
-        & Key("forum_name").eq("the-key")
-    )
-    assert results["Count"] == 1
-
-    results = table.query(
-        KeyConditionExpression=Key("forum_name").eq("the-key")
-        & Key("subject").between("567", "890")
-    )
-    assert results["Count"] == 1
-
-
 @mock_aws
 def test_boto3_conditions_ignorecase():
     dynamodb = boto3.client("dynamodb", region_name="us-east-1")
@@ -743,108 +669,12 @@ def test_update_item_delete_with_expression():
     )
 
 
-@mock_aws
-def test_boto3_query_gsi_range_comparison():
-    table = _create_table_with_range_key()
-
-    table.put_item(
-        Item={
-            "forum_name": "the-key",
-            "subject": "123",
-            "username": "johndoe",
-            "created": 3,
-        }
-    )
-    table.put_item(
-        Item={
-            "forum_name": "the-key",
-            "subject": "456",
-            "username": "johndoe",
-            "created": 1,
-        }
-    )
-    table.put_item(
-        Item={
-            "forum_name": "the-key",
-            "subject": "789",
-            "username": "johndoe",
-            "created": 2,
-        }
-    )
-    table.put_item(
-        Item={
-            "forum_name": "the-key",
-            "subject": "159",
-            "username": "janedoe",
-            "created": 2,
-        }
-    )
-    table.put_item(
-        Item={
-            "forum_name": "the-key",
-            "subject": "601",
-            "username": "janedoe",
-            "created": 5,
-        }
-    )
-
-    # Test a query returning all johndoe items
-    results = table.query(
-        KeyConditionExpression=Key("username").eq("johndoe") & Key("created").gt(0),
-        ScanIndexForward=True,
-        IndexName="TestGSI",
-    )
-    assert results["ScannedCount"] == 3
-    expected = ["456", "789", "123"]
-    for index, item in enumerate(results["Items"]):
-        assert item["subject"] == expected[index]
-
-    # Return all johndoe items again, but in reverse
-    results = table.query(
-        KeyConditionExpression=Key("username").eq("johndoe") & Key("created").gt(0),
-        ScanIndexForward=False,
-        IndexName="TestGSI",
-    )
-    for index, item in enumerate(reversed(results["Items"])):
-        assert item["subject"] == expected[index]
-
-    # Filter the creation to only return some of the results
-    # And reverse order of hash + range key
-    results = table.query(
-        KeyConditionExpression=Key("created").gt(1) & Key("username").eq("johndoe"),
-        IndexName="TestGSI",
-    )
-    assert results["Count"] == 2
-
-    # Filter to return no results
-    results = table.query(
-        KeyConditionExpression=Key("username").eq("janedoe") & Key("created").gt(9),
-        IndexName="TestGSI",
-    )
-    assert results["Count"] == 0
-
-    results = table.query(
-        KeyConditionExpression=Key("username").eq("janedoe") & Key("created").eq(5),
-        IndexName="TestGSI",
-    )
-    assert results["Count"] == 1
-
-    # Test range key sorting
-    results = table.query(
-        KeyConditionExpression=Key("username").eq("johndoe") & Key("created").gt(0),
-        IndexName="TestGSI",
-    )
-    expected = [Decimal("1"), Decimal("2"), Decimal("3")]
-    for index, item in enumerate(results["Items"]):
-        assert item["created"] == expected[index]
-
-
 @mock_aws
 def test_boto3_update_table_throughput():
     dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
 
     # Create the DynamoDB table.
-    table = dynamodb.create_table(
+    dynamodb.create_table(
         TableName="users",
         KeySchema=[
             {"AttributeName": "forum_name", "KeyType": "HASH"},
@@ -1062,234 +892,6 @@ def test_update_table_gsi_throughput():
     assert len(table.global_secondary_indexes) == 0
 
 
-@mock_aws
-def test_query_pagination():
-    table = _create_table_with_range_key()
-    for i in range(10):
-        table.put_item(
-            Item={
-                "forum_name": "the-key",
-                "subject": f"{i}",
-                "username": "johndoe",
-                "created": Decimal("3"),
-            }
-        )
-
-    page1 = table.query(KeyConditionExpression=Key("forum_name").eq("the-key"), Limit=6)
-    assert page1["Count"] == 6
-    assert page1["ScannedCount"] == 6
-    assert len(page1["Items"]) == 6
-
-    page2 = table.query(
-        KeyConditionExpression=Key("forum_name").eq("the-key"),
-        Limit=6,
-        ExclusiveStartKey=page1["LastEvaluatedKey"],
-    )
-    assert page2["Count"] == 4
-    assert page2["ScannedCount"] == 4
-    assert len(page2["Items"]) == 4
-    assert "LastEvaluatedKey" not in page2
-
-    results = page1["Items"] + page2["Items"]
-    subjects = set([int(r["subject"]) for r in results])
-    assert subjects == set(range(10))
-
-
-@mock_aws
-def test_query_by_local_secondary_index():
-    dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
-
-    table = dynamodb.create_table(
-        TableName="test",
-        KeySchema=[
-            {"AttributeName": "id", "KeyType": "HASH"},
-            {"AttributeName": "range_key", "KeyType": "RANGE"},
-        ],
-        AttributeDefinitions=[
-            {"AttributeName": "id", "AttributeType": "S"},
-            {"AttributeName": "range_key", "AttributeType": "S"},
-            {"AttributeName": "lsi_range_key", "AttributeType": "S"},
-        ],
-        ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 1},
-        LocalSecondaryIndexes=[
-            {
-                "IndexName": "test_lsi",
-                "KeySchema": [
-                    {"AttributeName": "id", "KeyType": "HASH"},
-                    {"AttributeName": "lsi_range_key", "KeyType": "RANGE"},
-                ],
-                "Projection": {"ProjectionType": "ALL"},
-            }
-        ],
-    )
-
-    table.put_item(
-        Item={
-            "id": "1",
-            "range_key": "1",
-            "col1": "val1",
-            "lsi_range_key": "1",
-        },
-    )
-
-    table.put_item(
-        Item={
-            "id": "1",
-            "range_key": "2",
-            "col1": "val2",
-            "lsi_range_key": "2",
-        },
-    )
-
-    table.put_item(
-        Item={"id": "3", "range_key": "1", "col1": "val3"},
-    )
-
-    res = table.query(
-        KeyConditionExpression=Key("id").eq("1") & Key("lsi_range_key").eq("1"),
-        IndexName="test_lsi",
-    )
-    assert res["Count"] == 1
-    assert res["Items"] == [
-        {"id": "1", "range_key": "1", "col1": "val1", "lsi_range_key": "1"}
-    ]
-
-    res = table.query(
-        KeyConditionExpression=Key("id").eq("1") & Key("lsi_range_key").eq("2"),
-        IndexName="test_lsi",
-        ConsistentRead=True,
-    )
-    assert res["Count"] == 1
-    assert res["Items"] == [
-        {"id": "1", "range_key": "2", "col1": "val2", "lsi_range_key": "2"}
-    ]
-
-
-@mock_aws
-def test_scan_by_index():
-    dynamodb = boto3.client("dynamodb", region_name="us-east-1")
-
-    dynamodb.create_table(
-        TableName="test",
-        KeySchema=[
-            {"AttributeName": "id", "KeyType": "HASH"},
-            {"AttributeName": "range_key", "KeyType": "RANGE"},
-        ],
-        AttributeDefinitions=[
-            {"AttributeName": "id", "AttributeType": "S"},
-            {"AttributeName": "range_key", "AttributeType": "S"},
-            {"AttributeName": "gsi_col", "AttributeType": "S"},
-            {"AttributeName": "gsi_range_key", "AttributeType": "S"},
-            {"AttributeName": "lsi_range_key", "AttributeType": "S"},
-        ],
-        ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 1},
-        GlobalSecondaryIndexes=[
-            {
-                "IndexName": "test_gsi",
-                "KeySchema": [
-                    {"AttributeName": "gsi_col", "KeyType": "HASH"},
-                    {"AttributeName": "gsi_range_key", "KeyType": "RANGE"},
-                ],
-                "Projection": {"ProjectionType": "ALL"},
-                "ProvisionedThroughput": {
-                    "ReadCapacityUnits": 1,
-                    "WriteCapacityUnits": 1,
-                },
-            }
-        ],
-        LocalSecondaryIndexes=[
-            {
-                "IndexName": "test_lsi",
-                "KeySchema": [
-                    {"AttributeName": "id", "KeyType": "HASH"},
-                    {"AttributeName": "lsi_range_key", "KeyType": "RANGE"},
-                ],
-                "Projection": {"ProjectionType": "ALL"},
-            }
-        ],
-    )
-
-    dynamodb.put_item(
-        TableName="test",
-        Item={
-            "id": {"S": "1"},
-            "range_key": {"S": "1"},
-            "col1": {"S": "val1"},
-            "gsi_col": {"S": "1"},
-            "gsi_range_key": {"S": "1"},
-            "lsi_range_key": {"S": "1"},
-        },
-    )
-
-    dynamodb.put_item(
-        TableName="test",
-        Item={
-            "id": {"S": "1"},
-            "range_key": {"S": "2"},
-            "col1": {"S": "val2"},
-            "gsi_col": {"S": "1"},
-            "gsi_range_key": {"S": "2"},
-            "lsi_range_key": {"S": "2"},
-        },
-    )
-
-    dynamodb.put_item(
-        TableName="test",
-        Item={"id": {"S": "3"}, "range_key": {"S": "1"}, "col1": {"S": "val3"}},
-    )
-
-    res = dynamodb.scan(TableName="test")
-    assert res["Count"] == 3
-    assert len(res["Items"]) == 3
-
-    res = dynamodb.scan(TableName="test", Limit=1)
-    assert res["Count"] == 1
-    assert res["ScannedCount"] == 1
-
-    res = dynamodb.scan(TableName="test", ExclusiveStartKey=res["LastEvaluatedKey"])
-    assert res["Count"] == 2
-    assert res["ScannedCount"] == 2
-
-    res = dynamodb.scan(TableName="test", IndexName="test_gsi")
-    assert res["Count"] == 2
-    assert res["ScannedCount"] == 2
-    assert len(res["Items"]) == 2
-
-    res = dynamodb.scan(TableName="test", IndexName="test_gsi", Limit=1)
-    assert res["Count"] == 1
-    assert res["ScannedCount"] == 1
-    assert len(res["Items"]) == 1
-    last_eval_key = res["LastEvaluatedKey"]
-    assert last_eval_key["id"]["S"] == "1"
-    assert last_eval_key["gsi_col"]["S"] == "1"
-    assert last_eval_key["gsi_range_key"]["S"] == "1"
-
-    res = dynamodb.scan(
-        TableName="test", IndexName="test_gsi", ExclusiveStartKey=last_eval_key
-    )
-    assert res["Count"] == 1
-    assert res["ScannedCount"] == 1
-
-    res = dynamodb.scan(TableName="test", IndexName="test_lsi")
-    assert res["Count"] == 2
-    assert res["ScannedCount"] == 2
-    assert len(res["Items"]) == 2
-
-    res = dynamodb.scan(TableName="test", IndexName="test_lsi", ConsistentRead=True)
-    assert res["Count"] == 2
-    assert res["ScannedCount"] == 2
-    assert len(res["Items"]) == 2
-
-    res = dynamodb.scan(TableName="test", IndexName="test_lsi", Limit=1)
-    assert res["Count"] == 1
-    assert res["ScannedCount"] == 1
-    assert len(res["Items"]) == 1
-    last_eval_key = res["LastEvaluatedKey"]
-    assert last_eval_key["id"]["S"] == "1"
-    assert last_eval_key["range_key"]["S"] == "1"
-    assert last_eval_key["lsi_range_key"]["S"] == "1"
-
-
 @mock_aws
 @pytest.mark.parametrize("create_item_first", [False, True])
 @pytest.mark.parametrize(
diff --git a/tests/test_dynamodb/test_dynamodb_table_without_range_key.py b/tests/test_dynamodb/test_dynamodb_table_without_range_key.py
--- a/tests/test_dynamodb/test_dynamodb_table_without_range_key.py
+++ b/tests/test_dynamodb/test_dynamodb_table_without_range_key.py
@@ -535,60 +535,3 @@ def test_scan_pagination(table_name=None):
 
     usernames = set(page1_results + page2_results)
     assert usernames == set(expected_usernames)
-
-
-@mock_aws
-def test_scan_by_index():
-    dynamodb = boto3.client("dynamodb", region_name="us-east-1")
-
-    dynamodb.create_table(
-        TableName="test",
-        KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
-        AttributeDefinitions=[
-            {"AttributeName": "id", "AttributeType": "S"},
-            {"AttributeName": "gsi_col", "AttributeType": "S"},
-        ],
-        ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 1},
-        GlobalSecondaryIndexes=[
-            {
-                "IndexName": "test_gsi",
-                "KeySchema": [{"AttributeName": "gsi_col", "KeyType": "HASH"}],
-                "Projection": {"ProjectionType": "ALL"},
-                "ProvisionedThroughput": {
-                    "ReadCapacityUnits": 1,
-                    "WriteCapacityUnits": 1,
-                },
-            }
-        ],
-    )
-
-    dynamodb.put_item(
-        TableName="test",
-        Item={"id": {"S": "1"}, "col1": {"S": "val1"}, "gsi_col": {"S": "gsi_val1"}},
-    )
-
-    dynamodb.put_item(
-        TableName="test",
-        Item={"id": {"S": "2"}, "col1": {"S": "val2"}, "gsi_col": {"S": "gsi_val2"}},
-    )
-
-    dynamodb.put_item(TableName="test", Item={"id": {"S": "3"}, "col1": {"S": "val3"}})
-
-    res = dynamodb.scan(TableName="test")
-    assert res["Count"] == 3
-    assert len(res["Items"]) == 3
-
-    res = dynamodb.scan(TableName="test", ConsistentRead=True)
-    assert res["Count"] == 3
-    assert len(res["Items"]) == 3
-
-    res = dynamodb.scan(TableName="test", IndexName="test_gsi")
-    assert res["Count"] == 2
-    assert len(res["Items"]) == 2
-
-    res = dynamodb.scan(TableName="test", IndexName="test_gsi", Limit=1)
-    assert res["Count"] == 1
-    assert len(res["Items"]) == 1
-    last_eval_key = res["LastEvaluatedKey"]
-    assert last_eval_key["id"]["S"] == "1"
-    assert last_eval_key["gsi_col"]["S"] == "gsi_val1"