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.


Discussion:

Resolving patch:
diff --git a/moto/dynamodb2/models.py b/moto/dynamodb2/models.py
--- a/moto/dynamodb2/models.py
+++ b/moto/dynamodb2/models.py
@@ -981,8 +981,13 @@ def query(
         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
                 )


Test patch:
diff --git a/tests/test_dynamodb2/test_dynamodb.py b/tests/test_dynamodb2/test_dynamodb.py
--- 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]
+    )