Repository: boto/boto

Issue:
Fixed how ``reverse`` works in DDBv2.
Fixes #2070 & #2115.

Review please? @danielgtaylor



Discussion:

Resolving patch:
diff --git a/boto/dynamodb2/table.py b/boto/dynamodb2/table.py
--- a/boto/dynamodb2/table.py
+++ b/boto/dynamodb2/table.py
@@ -816,6 +816,20 @@ def _build_filters(self, filter_kwargs, using=QUERY_OPERATORS):
     def query(self, limit=None, index=None, reverse=False, consistent=False,
               attributes=None, max_page_size=None, **filter_kwargs):
         """
+        **WARNING:** This method is provided **strictly** for
+        backward-compatibility. It returns results in an incorrect order.
+
+        If you are writing new code, please use ``Table.query_2``.
+        """
+        reverse = not reverse
+        return self.query_2(limit=limit, index=index, reverse=reverse,
+                            consistent=consistent, attributes=attributes,
+                            max_page_size=max_page_size, **filter_kwargs)
+
+    def query_2(self, limit=None, index=None, reverse=False,
+                consistent=False, attributes=None, max_page_size=None,
+                **filter_kwargs):
+        """
         Queries for a set of matching items in a DynamoDB table.
 
         Queries can be performed against a hash key, a hash+range key or
@@ -837,7 +851,7 @@ def query(self, limit=None, index=None, reverse=False, consistent=False,
         (Default: ``None``)
 
         Optionally accepts a ``reverse`` parameter, which will present the
-        results in reverse order. (Default: ``None`` - normal order)
+        results in reverse order. (Default: ``False`` - normal order)
 
         Optionally accepts a ``consistent`` parameter, which should be a
         boolean. If you provide ``True``, it will force a consistent read of
@@ -988,12 +1002,14 @@ def _query(self, limit=None, index=None, reverse=False, consistent=False,
         kwargs = {
             'limit': limit,
             'index_name': index,
-            'scan_index_forward': reverse,
             'consistent_read': consistent,
             'select': select,
-            'attributes_to_get': attributes_to_get
+            'attributes_to_get': attributes_to_get,
         }
 
+        if reverse:
+            kwargs['scan_index_forward'] = False
+
         if exclusive_start_key:
             kwargs['exclusive_start_key'] = {}
 


Test patch:
diff --git a/tests/integration/dynamodb2/test_highlevel.py b/tests/integration/dynamodb2/test_highlevel.py
--- a/tests/integration/dynamodb2/test_highlevel.py
+++ b/tests/integration/dynamodb2/test_highlevel.py
@@ -219,7 +219,7 @@ def test_integration(self):
         self.assertEqual(serverside_sadie['first_name'], 'Sadie')
 
         # Test the eventually consistent query.
-        results = users.query(
+        results = users.query_2(
             username__eq='johndoe',
             last_name__eq='Doe',
             index='LastNameIndex',
@@ -232,7 +232,7 @@ def test_integration(self):
             self.assertEqual(res.keys(), ['username'])
 
         # Ensure that queries with attributes don't return the hash key.
-        results = users.query(
+        results = users.query_2(
             username__eq='johndoe',
             friend_count__eq=4,
             attributes=('first_name',)
@@ -243,7 +243,7 @@ def test_integration(self):
             self.assertEqual(res.keys(), ['first_name'])
 
         # Test the strongly consistent query.
-        c_results = users.query(
+        c_results = users.query_2(
             username__eq='johndoe',
             last_name__eq='Doe',
             index='LastNameIndex',
@@ -322,7 +322,7 @@ def test_integration(self):
             username__eq='johndoe'
         )
         # But it shouldn't break on more complex tables.
-        res = users.query(username__eq='johndoe')
+        res = users.query_2(username__eq='johndoe')
 
         # Test putting with/without sets.
         mau5_created = users.put_item(data={
@@ -458,14 +458,14 @@ def test_gsi_with_just_hash_key(self):
         })
 
         # Try the main key. Should be fine.
-        rs = users.query(
+        rs = users.query_2(
             user_id__eq='24'
         )
         results = sorted([user['username'] for user in rs])
         self.assertEqual(results, ['alice'])
 
         # Now try the GSI. Also should work.
-        rs = users.query(
+        rs = users.query_2(
             username__eq='johndoe',
             index='UsernameIndex'
         )
@@ -504,7 +504,7 @@ def test_query_with_limits(self):
         time.sleep(5)
 
         # Test the reduced page size.
-        results = posts.query(
+        results = posts.query_2(
             thread__eq='Favorite chiptune band?',
             posted_on__gte='2013-12-24T00:00:00',
             max_page_size=2
@@ -515,4 +515,117 @@ def test_query_with_limits(self):
             [post['posted_by'] for post in all_posts],
             ['joe', 'jane', 'joe', 'joe', 'jane', 'joe']
         )
-        self.assertEqual(results._fetches, 3)
+        self.assertTrue(results._fetches >= 3)
+
+    def test_query_with_reverse(self):
+        posts = Table.create('more-posts', schema=[
+            HashKey('thread'),
+            RangeKey('posted_on')
+        ], throughput={
+            'read': 5,
+            'write': 5,
+        })
+        self.addCleanup(posts.delete)
+
+        # Wait for it.
+        time.sleep(60)
+
+        # Add some data.
+        test_data_path = os.path.join(
+            os.path.dirname(__file__),
+            'forum_test_data.json'
+        )
+        with open(test_data_path, 'r') as test_data:
+            data = json.load(test_data)
+
+            with posts.batch_write() as batch:
+                for post in data:
+                    batch.put_item(post)
+
+        time.sleep(5)
+
+        # Test the default order (ascending).
+        results = posts.query_2(
+            thread__eq='Favorite chiptune band?',
+            posted_on__gte='2013-12-24T00:00:00'
+        )
+        self.assertEqual(
+            [post['posted_on'] for post in results],
+            [
+                '2013-12-24T12:30:54',
+                '2013-12-24T12:35:40',
+                '2013-12-24T13:45:30',
+                '2013-12-24T14:15:14',
+                '2013-12-24T14:25:33',
+                '2013-12-24T15:22:22',
+            ]
+        )
+
+        # Test the explicit ascending order.
+        results = posts.query_2(
+            thread__eq='Favorite chiptune band?',
+            posted_on__gte='2013-12-24T00:00:00',
+            reverse=False
+        )
+        self.assertEqual(
+            [post['posted_on'] for post in results],
+            [
+                '2013-12-24T12:30:54',
+                '2013-12-24T12:35:40',
+                '2013-12-24T13:45:30',
+                '2013-12-24T14:15:14',
+                '2013-12-24T14:25:33',
+                '2013-12-24T15:22:22',
+            ]
+        )
+
+        # Test the explicit descending order.
+        results = posts.query_2(
+            thread__eq='Favorite chiptune band?',
+            posted_on__gte='2013-12-24T00:00:00',
+            reverse=True
+        )
+        self.assertEqual(
+            [post['posted_on'] for post in results],
+            [
+                '2013-12-24T15:22:22',
+                '2013-12-24T14:25:33',
+                '2013-12-24T14:15:14',
+                '2013-12-24T13:45:30',
+                '2013-12-24T12:35:40',
+                '2013-12-24T12:30:54',
+            ]
+        )
+
+        # Test the old, broken style.
+        results = posts.query(
+            thread__eq='Favorite chiptune band?',
+            posted_on__gte='2013-12-24T00:00:00'
+        )
+        self.assertEqual(
+            [post['posted_on'] for post in results],
+            [
+                '2013-12-24T15:22:22',
+                '2013-12-24T14:25:33',
+                '2013-12-24T14:15:14',
+                '2013-12-24T13:45:30',
+                '2013-12-24T12:35:40',
+                '2013-12-24T12:30:54',
+            ]
+        )
+        results = posts.query(
+            thread__eq='Favorite chiptune band?',
+            posted_on__gte='2013-12-24T00:00:00',
+            reverse=True
+        )
+        self.assertEqual(
+            [post['posted_on'] for post in results],
+            [
+                '2013-12-24T12:30:54',
+                '2013-12-24T12:35:40',
+                '2013-12-24T13:45:30',
+                '2013-12-24T14:15:14',
+                '2013-12-24T14:25:33',
+                '2013-12-24T15:22:22',
+            ]
+        )
diff --git a/tests/unit/dynamodb2/test_table.py b/tests/unit/dynamodb2/test_table.py
--- a/tests/unit/dynamodb2/test_table.py
+++ b/tests/unit/dynamodb2/test_table.py
@@ -883,7 +883,7 @@ def test_max_page_size_and_bigger_limit_fetch_more(self):
         self.results.fetch_more()
         self.result_function.assert_called_with('john', greeting='Hello', limit=10)
         self.result_function.reset_mock()
-    
+
     def test_fetch_more(self):
         # First "page".
         self.results.fetch_more()
@@ -2196,7 +2196,7 @@ def test_private_query(self):
 
         mock_query.assert_called_once_with('users',
             consistent_read=False,
-            scan_index_forward=True,
+            scan_index_forward=False,
             index_name=None,
             attributes_to_get=None,
             limit=4,
@@ -2243,7 +2243,7 @@ def test_private_query(self):
             },
             index_name=None,
             attributes_to_get=None,
-            scan_index_forward=True,
+            scan_index_forward=False,
             limit=4,
             exclusive_start_key={
                 'username': {
@@ -2376,7 +2376,7 @@ def test_query(self):
             'last_key': 'jane',
         }
 
-        results = self.users.query(last_name__eq='Doe')
+        results = self.users.query_2(last_name__eq='Doe')
         self.assertTrue(isinstance(results, ResultSet))
         self.assertEqual(len(results._results), 0)
         self.assertEqual(results.the_callable, self.users._query)
@@ -2430,7 +2430,7 @@ def test_query_with_specific_attributes(self):
             'last_key': 'jane',
         }
 
-        results = self.users.query(last_name__eq='Doe',
+        results = self.users.query_2(last_name__eq='Doe',
                                    attributes=['username'])
         self.assertTrue(isinstance(results, ResultSet))
         self.assertEqual(len(results._results), 0)