]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Added NULLS FIRST and NULLS LAST support.
authorMichael Trier <mtrier@gmail.com>
Mon, 20 Dec 2010 00:25:33 +0000 (19:25 -0500)
committerMichael Trier <mtrier@gmail.com>
Mon, 20 Dec 2010 00:25:33 +0000 (19:25 -0500)
It's implemented as an extension to the asc() and desc() operators, called
nullsfirst() and nullslast().  [ticket:723]

CHANGES
lib/sqlalchemy/sql/compiler.py
lib/sqlalchemy/sql/expression.py
lib/sqlalchemy/sql/operators.py
test/lib/requires.py
test/sql/test_compiler.py
test/sql/test_query.py

diff --git a/CHANGES b/CHANGES
index 900cdd54f9feaab68324616424dd8402cfbdc08c..c4ef9e1a94d87774238366bfecbaa1756c7b2a82 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -6,6 +6,11 @@ CHANGES
 0.7.0b1
 =======
 
+- sql
+  - Added NULLS FIRST and NULLS LAST support. It's implemented
+    as an extension to the asc() and desc() operators, called
+    nullsfirst() and nullslast().  [ticket:723]
+
 - mssql
   - the String/Unicode types, and their counterparts VARCHAR/
     NVARCHAR, emit "max" as the length when no length is 
index 0c76f3e74fd792deea016dbaa377ae20221fe746..8bd728a7c722a541b8e1441d9853dd21d0ce44f0 100644 (file)
@@ -101,6 +101,8 @@ OPERATORS =  {
     # modifiers
     operators.desc_op : ' DESC',
     operators.asc_op : ' ASC',
+    operators.nullsfirst_op : ' NULLS FIRST',
+    operators.nullslast_op : ' NULLS LAST',
 }
 
 FUNCTIONS = {
index f090d14d64d63e03505b5f62e1c64665d82e6285..60ca33b93ea8f1a929ef31465a19325e071dec22 100644 (file)
@@ -46,12 +46,32 @@ __all__ = [
     'bindparam', 'case', 'cast', 'column', 'delete', 'desc', 'distinct',
     'except_', 'except_all', 'exists', 'extract', 'func', 'modifier',
     'collate', 'insert', 'intersect', 'intersect_all', 'join', 'label',
-    'literal', 'literal_column', 'not_', 'null', 'or_', 'outparam',
-    'outerjoin', 'select', 'subquery', 'table', 'text', 'tuple_', 'type_coerce',
-    'union', 'union_all', 'update', ]
+    'literal', 'literal_column', 'not_', 'null', 'nullsfirst', 'nullslast',
+    'or_', 'outparam', 'outerjoin', 'select', 'subquery', 'table', 'text',
+    'tuple_', 'type_coerce', 'union', 'union_all', 'update', ]
 
 PARSE_AUTOCOMMIT = util.symbol('PARSE_AUTOCOMMIT')
 
+def nullsfirst(column):
+    """Return a NULLS FIRST ``ORDER BY`` clause element.
+
+    e.g.::
+
+      order_by = [desc(table1.mycol).nullsfirst()]
+
+    """
+    return _UnaryExpression(column, modifier=operators.nullsfirst_op)
+
+def nullslast(column):
+    """Return a NULLS LAST ``ORDER BY`` clause element.
+
+    e.g.::
+
+      order_by = [desc(table1.mycol).nullslast()]
+
+    """
+    return _UnaryExpression(column, modifier=operators.nullslast_op)
+
 def desc(column):
     """Return a descending ``ORDER BY`` clause element.
 
@@ -1570,6 +1590,12 @@ class ColumnOperators(Operators):
     def asc(self):
         return self.operate(operators.asc_op)
 
+    def nullsfirst(self):
+        return self.operate(operators.nullsfirst_op)
+
+    def nullslast(self):
+        return self.operate(operators.nullslast_op)
+
     def collate(self, collation):
         return self.operate(operators.collate, collation)
 
@@ -1813,6 +1839,16 @@ class _CompareMixin(ColumnOperators):
 
         return asc(self)
 
+    def nullsfirst(self):
+        """Produce a NULLS FIRST clause, i.e. ``NULLS FIRST``"""
+
+        return nullsfirst(self)
+
+    def nullslast(self):
+        """Produce a NULLS LAST clause, i.e. ``NULLS LAST``"""
+
+        return nullslast(self)
+
     def distinct(self):
         """Produce a DISTINCT clause, i.e. ``DISTINCT <columnname>``"""
 
index 0577e66687b34ee13f67a4e1e330ff34464b3562..77916e80ed21dcad7a470f396997d31a21d8beaa 100644 (file)
@@ -83,6 +83,12 @@ def desc_op(a):
 def asc_op(a):
     return a.asc()
 
+def nullsfirst_op(a):
+    return a.nullsfirst()
+
+def nullslast_op(a):
+    return a.nullslast()
+
 _commutative = set([eq, ne, add, mul])
 
 def is_commutative(op):
index d43b601a45b328daa3da9bebd9bf35e6f5e3c589..222dc93f6f2a06cb4c79ac6d7a5f399f1affc15d 100644 (file)
@@ -270,6 +270,13 @@ def sane_multi_rowcount(fn):
         skip_if(lambda: not testing.db.dialect.supports_sane_multi_rowcount)
     )
 
+def nullsordering(fn):
+    """Target backends that support nulls ordering."""
+    return _chain_decorators_on(
+        fn,
+        fails_on_everything_except('postgresql', 'oracle', 'firebird')
+    )
 def reflects_pk_names(fn):
     """Target driver reflects the name of primary key constraints."""
     return _chain_decorators_on(
index bc0a2e9c3b04a7a254abde29b1520b1292997859..b34eaeaaec0fcd42d67aa53f31c115a7ea63b66f 100644 (file)
@@ -921,6 +921,37 @@ class SelectTest(TestBase, AssertsCompiledSQL):
             "OR mytable.myid = :myid_2 OR mytable.myid = :myid_3"
         )
 
+    def test_order_by_nulls(self):
+        self.assert_compile(
+            table2.select(order_by = [table2.c.otherid, table2.c.othername.desc().nullsfirst()]),
+            "SELECT myothertable.otherid, myothertable.othername FROM "
+            "myothertable ORDER BY myothertable.otherid, myothertable.othername DESC NULLS FIRST"
+        )
+
+        self.assert_compile(
+            table2.select(order_by = [table2.c.otherid, table2.c.othername.desc().nullslast()]),
+            "SELECT myothertable.otherid, myothertable.othername FROM "
+            "myothertable ORDER BY myothertable.otherid, myothertable.othername DESC NULLS LAST"
+        )
+
+        self.assert_compile(
+            table2.select(order_by = [table2.c.otherid.nullslast(), table2.c.othername.desc().nullsfirst()]),
+            "SELECT myothertable.otherid, myothertable.othername FROM "
+            "myothertable ORDER BY myothertable.otherid NULLS LAST, myothertable.othername DESC NULLS FIRST"
+        )
+
+        self.assert_compile(
+            table2.select(order_by = [table2.c.otherid.nullsfirst(), table2.c.othername.desc()]),
+            "SELECT myothertable.otherid, myothertable.othername FROM "
+            "myothertable ORDER BY myothertable.otherid NULLS FIRST, myothertable.othername DESC"
+        )
+
+        self.assert_compile(
+            table2.select(order_by = [table2.c.otherid.nullsfirst(), table2.c.othername.desc().nullslast()]),
+            "SELECT myothertable.otherid, myothertable.othername FROM "
+            "myothertable ORDER BY myothertable.otherid NULLS FIRST, myothertable.othername DESC NULLS LAST"
+        )
+
     def test_orderby_groupby(self):
         self.assert_compile(
             table2.select(order_by = [table2.c.otherid, asc(table2.c.othername)]),
index e14f5301e5f78a89430fec5e0f775d8494546eb1..276653e56148a2b8e4d8d1b6d9ff0921df2959c9 100644 (file)
@@ -576,7 +576,63 @@ class QueryTest(TestBase):
                         use_labels=labels,
                         order_by=[users.c.user_id.desc()]),
                  [(3,), (2,), (1,)])
-    
+
+    @testing.requires.nullsordering
+    def test_order_by_nulls(self):
+        """Exercises ORDER BY clause generation.
+
+        Tests simple, compound, aliased and DESC clauses.
+        """
+
+        users.insert().execute(user_id=1)
+        users.insert().execute(user_id=2, user_name='b')
+        users.insert().execute(user_id=3, user_name='a')
+
+        def a_eq(executable, wanted):
+            got = list(executable.execute())
+            eq_(got, wanted)
+
+        for labels in False, True:
+            a_eq(users.select(order_by=[users.c.user_name.nullsfirst()],
+                              use_labels=labels),
+                 [(1, None), (3, 'a'), (2, 'b')])
+
+            a_eq(users.select(order_by=[users.c.user_name.nullslast()],
+                              use_labels=labels),
+                 [(3, 'a'), (2, 'b'), (1, None)])
+
+            a_eq(users.select(order_by=[asc(users.c.user_name).nullsfirst()],
+                              use_labels=labels),
+                 [(1, None), (3, 'a'), (2, 'b')])
+
+            a_eq(users.select(order_by=[asc(users.c.user_name).nullslast()],
+                              use_labels=labels),
+                 [(3, 'a'), (2, 'b'), (1, None)])
+
+            a_eq(users.select(order_by=[users.c.user_name.desc().nullsfirst()],
+                              use_labels=labels),
+                 [(1, None), (2, 'b'), (3, 'a')])
+
+            a_eq(users.select(order_by=[users.c.user_name.desc().nullslast()],
+                              use_labels=labels),
+                 [(2, 'b'), (3, 'a'), (1, None)])
+
+            a_eq(users.select(order_by=[desc(users.c.user_name).nullsfirst()],
+                              use_labels=labels),
+                 [(1, None), (2, 'b'), (3, 'a')])
+
+            a_eq(users.select(order_by=[desc(users.c.user_name).nullslast()],
+                              use_labels=labels),
+                 [(2, 'b'), (3, 'a'), (1, None)])
+
+            a_eq(users.select(order_by=[users.c.user_name.nullsfirst(), users.c.user_id],
+                              use_labels=labels),
+                 [(1, None), (3, 'a'), (2, 'b')])
+
+            a_eq(users.select(order_by=[users.c.user_name.nullslast(), users.c.user_id],
+                              use_labels=labels),
+                 [(3, 'a'), (2, 'b'), (1, None)])
+
     @testing.fails_on("+pyodbc", "pyodbc row doesn't seem to accept slices")
     def test_column_slices(self):
         users.insert().execute(user_id=1, user_name='john')