Repository: getmoto/moto

Issue:
DynamoDB: queries are not sorted when the sort key is numerical
While building unit tests for my AWS application, I noticed some unexpected behaviour of the DynamoDB mock

## Expected behaviour, tested on local dynamoDb instance

### Start the local dynamoDb instance
 `java -Djava.library.path=./DynamoDBLocal_lib -jar DynamoDBLocal.jar -sharedDb

### Create a table CarCollection with numerical sort key
`aws dynamodb create-table --endpoint-url http://localhost:8000 --table-name CarCollection --attribute-definitions AttributeName=CarModel,AttributeType=S AttributeName=CarPrice,AttributeType=N --key-schema AttributeName=CarModel,KeyType=HASH AttributeName=CarPrice,KeyType=RANGE --provisioned-throughput ReadCapacityUnits=1,WriteCapacityUnits=1`

### Add 4 unsorted items 
```
aws dynamodb put-item --endpoint-url http://localhost:8000 --table-name CarCollection --item '{"CarModel": {"S": "M"}, "CarPrice": {"N": "2"}}'
aws dynamodb put-item --endpoint-url http://localhost:8000 --table-name CarCollection --item '{"CarModel": {"S": "M"}, "CarPrice": {"N": "1"}}'
aws dynamodb put-item --endpoint-url http://localhost:8000 --table-name CarCollection --item '{"CarModel": {"S": "M"}, "CarPrice": {"N": "10"}}'
aws dynamodb put-item --endpoint-url http://localhost:8000 --table-name CarCollection --item '{"CarModel": {"S": "M"}, "CarPrice": {"N": "3"}}'
```
### Output of query
`
 aws dynamodb query --endpoint-url http://localhost:8000 --table-name CarCollection --key-condition-expression 'CarModel = :v1' --expression-attribute-values '{":v1": {"S": "M"}}'
`
```json
{
    "Count": 3, 
    "Items": [
        {
            "CarModel": {
                "S": "M"
            }, 
            "CarPrice": {
                "N": "1"
            }
        }, 
        {
            "CarModel": {
                "S": "M"
            }, 
            "CarPrice": {
                "N": "2"
            }
        }, 
        {
            "CarModel": {
                "S": "M"
            }, 
            "CarPrice": {
                "N": "10"
            }
        }
    ], 
    "ScannedCount": 3, 
    "ConsumedCapacity": null
}
```

The local dynamoDb instance behaves as expected.

## Same example with moto
Moto was installed with pip on a fresh virtual environment running Python 3.6.7
Moto configuration:
```
moto==1.3.13`
boto==2.49.0
boto3==1.9.211
botocore==1.12.211
```
### Unittest
```python
import unittest
import boto3
from moto import mock_dynamodb2
from boto3.dynamodb.conditions import Key
from decimal import Decimal


class TestMoto(unittest.TestCase):

    def setUp(self):
        mock_dynamodb2().start()
        self.dynamodb = boto3.resource('dynamodb', region_name='us-west-2')
        self.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 tearDown(self):
        mock_dynamodb2().stop()
    
    def test_numerical_sort_query(self):

        def create_item(price):
            return {"CarModel": "M", "CarPrice": price}

        table = self.dynamodb.Table('CarCollection')
        # Generate 4 items 
        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']
        self.assertEqual(len(items), len(response_items))
        self.assertTrue(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()
        self.assertEqual(expected_prices, response_prices, msg="result items are not sorted by numerical value")
```

### Test outputs

Tests are run with `unittest`
`python -m unittest tests.test_moto`

```
FF
======================================================================
FAIL: test_numerical_sort_query (tests.test_moto.TestMoto)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "~/tests/test_moto.py", line 64, in test_numerical_sort_query
    self.assertEqual(expected_prices, response_prices, msg="result items are not sorted by numerical value")
AssertionError: Lists differ: [Decimal('1'), Decimal('2'), Decimal('3'), Decimal('10')] != [Decimal('1'), Decimal('10'), Decimal('2'), Decimal('3')]

First differing element 1:
Decimal('2')
Decimal('10')

- [Decimal('1'), Decimal('2'), Decimal('3'), Decimal('10')]
+ [Decimal('1'), Decimal('10'), Decimal('2'), Decimal('3')] : result items are not sorted by numerical value

----------------------------------------------------------------------
Ran 1 tests in 0.105s

FAILED (failures=1)
```

## Conclusion

It seems that the query sort is based on the string representation of decimals.


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
@@ -53,16 +53,16 @@ def __eq__(self, other):
         )
 
     def __lt__(self, other):
-        return self.value < other.value
+        return self.cast_value < other.cast_value
 
     def __le__(self, other):
-        return self.value <= other.value
+        return self.cast_value <= other.cast_value
 
     def __gt__(self, other):
-        return self.value > other.value
+        return self.cast_value > other.cast_value
 
     def __ge__(self, other):
-        return self.value >= other.value
+        return self.cast_value >= other.cast_value
 
     def __repr__(self):
         return "DynamoType: {0}".format(self.to_json())


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
@@ -2295,6 +2295,35 @@ def test_index_with_unknown_attributes_should_fail():
     ex.exception.response['Error']['Message'].should.contain(expected_exception)
 
 
+@mock_dynamodb2
+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/spulec/moto/issues/1874
 @mock_dynamodb2
 def test_item_size_is_under_400KB():