Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add null ordering params to order by clause #730

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions pypika/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ class Order(Enum):
desc = "DESC"


class NullOrder(Enum):
first = "NULLS FIRST"
last = "NULLS LAST"


class JoinType(Enum):
inner = ""
left = "LEFT"
Expand Down
36 changes: 21 additions & 15 deletions pypika/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -550,7 +550,7 @@ def orderby(self, *fields: Field, **kwargs: Any) -> "_SetOperation":
else self.base_query.wrap_constant(field)
)

self._orderbys.append((field, kwargs.get("order")))
self._orderbys.append((field, kwargs.get("order"), kwargs.get("null_order")))

@builder
def limit(self, limit: int) -> "_SetOperation":
Expand Down Expand Up @@ -640,24 +640,27 @@ def get_sql(self, with_alias: bool = False, subquery: bool = False, **kwargs: An
def _orderby_sql(self, quote_char: Optional[str] = None, **kwargs: Any) -> str:
"""
Produces the ORDER BY part of the query. This is a list of fields and possibly their directionality, ASC or
DESC. The clauses are stored in the query under self._orderbys as a list of tuples containing the field and
directionality (which can be None).
DESC. The clauses are stored in the query under self._orderbys as a list of tuples containing the field,
directionality (which can be None) and null ordering (which can also be None).

If an order by field is used in the select clause, determined by a matching , then the ORDER BY clause will use
the alias, otherwise the field will be rendered as SQL.
"""
clauses = []
selected_aliases = {s.alias for s in self.base_query._selects}
for field, directionality in self._orderbys:
for field, directionality, null_order in self._orderbys:
term = (
format_quotes(field.alias, quote_char)
if field.alias and field.alias in selected_aliases
else field.get_sql(quote_char=quote_char, **kwargs)
)

clauses.append(
"{term} {orient}".format(term=term, orient=directionality.value) if directionality is not None else term
)
current_clause = term
if directionality is not None:
current_clause += f" {directionality.value}"
if null_order is not None:
current_clause += f" {null_order.value}"
clauses.append(current_clause)

return " ORDER BY {orderby}".format(orderby=",".join(clauses))

Expand Down Expand Up @@ -812,7 +815,7 @@ def replace_table(self, current_table: Optional[Table], new_table: Optional[Tabl
self._groupbys = [groupby.replace_table(current_table, new_table) for groupby in self._groupbys]
self._havings = self._havings.replace_table(current_table, new_table) if self._havings else None
self._orderbys = [
(orderby[0].replace_table(current_table, new_table), orderby[1]) for orderby in self._orderbys
(orderby[0].replace_table(current_table, new_table), orderby[1], orderby[2]) for orderby in self._orderbys
]
self._joins = [join.replace_table(current_table, new_table) for join in self._joins]

Expand Down Expand Up @@ -990,7 +993,7 @@ def orderby(self, *fields: Any, **kwargs: Any) -> "QueryBuilder":
for field in fields:
field = Field(field, table=self._from[0]) if isinstance(field, str) else self.wrap_constant(field)

self._orderbys.append((field, kwargs.get("order")))
self._orderbys.append((field, kwargs.get("order"), kwargs.get("null_order")))

@builder
def join(
Expand Down Expand Up @@ -1491,8 +1494,8 @@ def _orderby_sql(
) -> str:
"""
Produces the ORDER BY part of the query. This is a list of fields and possibly their directionality, ASC or
DESC. The clauses are stored in the query under self._orderbys as a list of tuples containing the field and
directionality (which can be None).
DESC. The clauses are stored in the query under self._orderbys as a list of tuples containing the field,
directionality (which can be None) and null ordering (which can also be None).

If an order by field is used in the select clause,
determined by a matching, and the orderby_alias
Expand All @@ -1501,16 +1504,19 @@ def _orderby_sql(
"""
clauses = []
selected_aliases = {s.alias for s in self._selects}
for field, directionality in self._orderbys:
for field, directionality, null_order in self._orderbys:
term = (
format_quotes(field.alias, alias_quote_char or quote_char)
if orderby_alias and field.alias and field.alias in selected_aliases
else field.get_sql(quote_char=quote_char, alias_quote_char=alias_quote_char, **kwargs)
)

clauses.append(
"{term} {orient}".format(term=term, orient=directionality.value) if directionality is not None else term
)
current_clause = term
if directionality is not None:
current_clause += f" {directionality.value}"
if null_order is not None:
current_clause += f" {null_order.value}"
clauses.append(current_clause)

return " ORDER BY {orderby}".format(orderby=",".join(clauses))

Expand Down
28 changes: 28 additions & 0 deletions pypika/tests/test_selects.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
functions as fn,
SYSTEM_TIME,
)
from pypika.enums import NullOrder
from pypika.terms import ValueWrapper

__author__ = "Timothy Heys"
Expand Down Expand Up @@ -997,6 +998,33 @@ def test_orderby_alias(self):

self.assertEqual('SELECT SUM("foo"),"bar" "bar01" FROM "abc" ORDER BY "bar01"', q.get_sql())

def test_order_by_nulls_first(self):
q = Query.from_(self.t).orderby(self.t.foo, null_order=NullOrder.first).select(self.t.foo)

self.assertEqual('SELECT "foo" FROM "abc" ORDER BY "foo" NULLS FIRST', str(q))

def test_order_by_nulls_last(self):
q = Query.from_(self.t).orderby(self.t.foo, null_order=NullOrder.last).select(self.t.foo)

self.assertEqual('SELECT "foo" FROM "abc" ORDER BY "foo" NULLS LAST', str(q))

def test_order_by_asc_nulls_first(self):
q = Query.from_(self.t).orderby(self.t.foo, order=Order.asc, null_order=NullOrder.first).select(self.t.foo)

self.assertEqual('SELECT "foo" FROM "abc" ORDER BY "foo" ASC NULLS FIRST', str(q))

def test_order_by_desc_nulls_first(self):
q = Query.from_(self.t).orderby(self.t.foo, order=Order.desc, null_order=NullOrder.first).select(self.t.foo)

self.assertEqual('SELECT "foo" FROM "abc" ORDER BY "foo" DESC NULLS FIRST', str(q))

def test_orderby_multi_fields_null_order(self):
q = Query.from_(self.t).orderby(
self.t.foo, self.t.bar, null_order=NullOrder.last
).select(self.t.foo, self.t.bar)

self.assertEqual('SELECT "foo","bar" FROM "abc" ORDER BY "foo" NULLS LAST,"bar" NULLS LAST', str(q))


class AliasTests(unittest.TestCase):
t = Table("abc")
Expand Down