Repository: getmoto/moto
Issue: DynamoDb2: Query response items ordered wrong for GSI with numeric range key, with negative numbers

Hi, thanks again so much for this library.

It appears that moto is confusing order of items in GSI's when the range key is numeric and the values are negative. Here's a minimal script that demonstrates the bug:

```python
from decimal import Decimal
from pprint import pprint

import boto3
from boto3.dynamodb.conditions import Key

from moto import mock_dynamodb2


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'),
}

with mock_dynamodb2():
    dynamodb = boto3.resource('dynamodb')
    dynamodb.create_table(TableName='test-table', BillingMode='PAY_PER_REQUEST', **table_schema)
    table = dynamodb.Table('test-table')
    table.put_item(Item=item1)
    table.put_item(Item=item2)

    resp = table.query(
        KeyConditionExpression=Key('gsiK1PartitionKey').eq('gsi-k1'),
        IndexName='GSI-K1',
    )
    pprint(resp['Items'])
```

Running this using boto against a real dynamo table gives the following output:

```python
[{'gsiK1PartitionKey': 'gsi-k1',
  'gsiK1SortKey': Decimal('-0.7'),
  'partitionKey': 'pk-2'},
 {'gsiK1PartitionKey': 'gsi-k1',
  'gsiK1SortKey': Decimal('-0.6'),
  'partitionKey': 'pk-1'}]
```

However, with the latest version of moto (`1.3.15.dev414`), the script outputs:

```python
[{'gsiK1PartitionKey': 'gsi-k1',
  'gsiK1SortKey': Decimal('-0.6'),
  'partitionKey': 'pk-1'},
 {'gsiK1PartitionKey': 'gsi-k1',
  'gsiK1SortKey': Decimal('-0.7'),
  'partitionKey': 'pk-2'}]
```

Note moto gets the order backwards. This seems to happen only with negative numbers in the range key.

Changed files:
moto/dynamodb2/models.py, tests/test_dynamodb2/test_dynamodb.py

diff --git a/moto/dynamodb2/models.py b/moto/dynamodb2/models.py
index 1527821ed..a80b3211d 100644
--- a/moto/dynamodb2/models.py
+++ b/moto/dynamodb2/models.py
@@ -981,8 +981,13 @@ class Table(BaseModel):
         if index_name:
 
             if index_range_key:
+
+                # Convert to float if necessary to ensure proper ordering
+                def conv(x):
+                    return float(x.value) if x.type == "N" else x.value
+
                 results.sort(
-                    key=lambda item: item.attrs[index_range_key["AttributeName"]].value
+                    key=lambda item: conv(item.attrs[index_range_key["AttributeName"]])
                     if item.attrs.get(index_range_key["AttributeName"])
                     else None
                 )
diff --git a/tests/test_dynamodb2/test_dynamodb.py b/tests/test_dynamodb2/test_dynamodb.py
index 82f82ccc9..5d39e3805 100644
--- a/tests/test_dynamodb2/test_dynamodb.py
+++ b/tests/test_dynamodb2/test_dynamodb.py
@@ -4026,3 +4026,61 @@ def test_valid_transact_get_items():
             "Table": {"CapacityUnits": 2.0, "ReadCapacityUnits": 2.0,},
         }
     )
+
+
+@mock_dynamodb2
+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
+    [float(item["gsiK1SortKey"]) for item in resp["Items"]].should.equal(
+        [-0.7, -0.6, 0.7]
+    )