Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,17 @@ def __init__(self, column, raw=False):
class OrderByExpression:
"""A helper class to manage select expressions."""

def __init__(self, column, direction="ASC", raw=False, bindings=()):
self.column = column.strip()

def __init__(self, column, direction="ASC", raw=False, bindings=(), builder=None):
self.raw = raw

self.direction = direction
self.bindings = bindings
self.builder = builder

if builder is not None:
self.column = None
return

self.column = column.strip()

if raw is False:
if self.column.endswith(" desc"):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,8 +164,11 @@ def offset(self, offset: int) -> "QueryBuilder":
self._offset = offset
return self

def order_by(self, column: str, direction: str = "asc") -> "QueryBuilder":
def order_by(self, column, direction: str = "asc") -> "QueryBuilder":
direction = direction.upper()
if isinstance(column, QueryBuilder):
self._order_by += (OrderByExpression(None, direction, builder=column),)
return self
for col in column.split(","):
col = col.strip()
self._order_by += (OrderByExpression(col, direction),)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ def _compile_select(self, qmark=False):
limit=self.process_limit(),
offset=self.process_offset(),
aggregates=self.process_aggregates(),
order_by=self.process_order_by(),
order_by=self.process_order_by(qmark=qmark),
group_by=self.process_group_by(),
having=self.process_having(),
lock=self.process_locks(),
Expand All @@ -112,7 +112,7 @@ def _compile_select(self, qmark=False):
limit=self.process_limit(),
offset=self.process_offset(),
aggregates=self.process_aggregates(),
order_by=self.process_order_by(),
order_by=self.process_order_by(qmark=qmark),
group_by=self.process_group_by(),
having=self.process_having(),
lock=self.process_locks(),
Expand Down Expand Up @@ -388,7 +388,7 @@ def process_aggregates(self):

return sql

def process_order_by(self):
def process_order_by(self, qmark=False):
"""Compiles an order by for a query expression.

Returns:
Expand All @@ -408,6 +408,21 @@ def process_order_by(self):

continue

if getattr(order_bys, "builder", None) is not None:
if len(order_crit):
order_crit += ", "
if qmark:
subquery_sql = order_bys.builder.to_qmark()
if order_bys.builder._bindings:
self.add_binding(*order_bys.builder._bindings)
else:
subquery_sql = order_bys.builder.to_sql()
column_string = self.subquery_string().format(query=subquery_sql)
order_crit += self.order_by_format().format(
column=column_string, direction=order_bys.direction.upper()
)
continue

if len(order_crit):
order_crit += ", "
column = order_bys.column
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import unittest

from fastapi_startkit.masoniteorm.models.builder import QueryBuilder
from fastapi_startkit.masoniteorm.query.grammars.SQLiteGrammar import SQLiteGrammar
from fastapi_startkit.masoniteorm.query.grammars.MySQLGrammar import MySQLGrammar
from fastapi_startkit.masoniteorm.query.grammars.PostgresGrammar import PostgresGrammar
from fastapi_startkit.masoniteorm.query.grammars.MSSQLGrammar import MSSQLGrammar

GRAMMARS = {
"sqlite": SQLiteGrammar,
"mysql": MySQLGrammar,
"postgres": PostgresGrammar,
"mssql": MSSQLGrammar,
}

EXPECTED = {
"sqlite": {
"sql": 'SELECT * FROM "posts" ORDER BY (SELECT "categories"."name" FROM "categories" WHERE categories.id = posts.category_id) ASC',
"qmark": 'SELECT * FROM "posts" WHERE "posts"."status" = ? ORDER BY (SELECT "categories"."name" FROM "categories" WHERE categories.id = posts.category_id AND "categories"."active" = ?) ASC',
},
"mysql": {
"sql": "SELECT * FROM `posts` ORDER BY (SELECT `categories`.`name` FROM `categories` WHERE categories.id = posts.category_id) ASC",
"qmark": "SELECT * FROM `posts` WHERE `posts`.`status` = ? ORDER BY (SELECT `categories`.`name` FROM `categories` WHERE categories.id = posts.category_id AND `categories`.`active` = ?) ASC",
},
"postgres": {
"sql": 'SELECT * FROM "posts" ORDER BY (SELECT "categories"."name" FROM "categories" WHERE categories.id = posts.category_id) ASC',
"qmark": 'SELECT * FROM "posts" WHERE "posts"."status" = ? ORDER BY (SELECT "categories"."name" FROM "categories" WHERE categories.id = posts.category_id AND "categories"."active" = ?) ASC',
},
"mssql": {
"sql": "SELECT * FROM [posts] ORDER BY (SELECT [categories].[name] FROM [categories] WHERE categories.id = posts.category_id) ASC",
"qmark": "SELECT * FROM [posts] WHERE [posts].[status] = ? ORDER BY (SELECT [categories].[name] FROM [categories] WHERE categories.id = posts.category_id AND [categories].[active] = ?) ASC",
},
}


def qb(grammar, table):
q = QueryBuilder(connection=None, grammar=grammar, processor=None)
q._table = table
return q


def category_subquery(grammar):
return qb(grammar, "categories").select("name").where_column("categories.id", "posts.category_id")


class TestOrderBySubqueryGrammar(unittest.TestCase):
"""Grammar-level parity for order_by() with a correlated-subquery builder."""

def test_order_by_builder(self):
for name, grammar in GRAMMARS.items():
with self.subTest(grammar=name):
sql = qb(grammar, "posts").order_by(category_subquery(grammar)).to_sql()
self.assertEqual(sql, EXPECTED[name]["sql"])

def test_order_by_builder_desc(self):
for name, grammar in GRAMMARS.items():
with self.subTest(grammar=name):
sql = qb(grammar, "posts").order_by(category_subquery(grammar), "desc").to_sql()
self.assertTrue(sql.endswith(") DESC"))

def test_order_by_builder_qmark_and_binding_order(self):
for name, grammar in GRAMMARS.items():
with self.subTest(grammar=name):
q = (
qb(grammar, "posts")
.where("posts.status", "active")
.order_by(
qb(grammar, "categories")
.select("name")
.where_column("categories.id", "posts.category_id")
.where("categories.active", 1)
)
)
self.assertEqual(q.to_qmark(), EXPECTED[name]["qmark"])
# WHERE binding precedes the ORDER BY subquery binding.
self.assertEqual(list(q.get_bindings()), ["active", 1])

def test_string_order_by_unchanged(self):
for name, grammar in GRAMMARS.items():
with self.subTest(grammar=name):
sql = qb(grammar, "posts").order_by("id", "desc").to_sql()
self.assertTrue(sql.endswith('"id" DESC') or sql.endswith("`id` DESC") or sql.endswith("[id] DESC"))


if __name__ == "__main__":
unittest.main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
from fastapi_startkit.masoniteorm import Model

from ..test_case import TestCase


class Category(Model):
__table__ = "categories"
__timestamps__ = None
id: int
name: str


class Post(Model):
__table__ = "posts"
__timestamps__ = None
id: int
title: str
category_id: int


def category_name_subquery():
"""Correlated subquery selecting the parent category name for each post."""
return Category.query().select("name").where_column("categories.id", "posts.category_id")


class TestSqliteOrderBySubquery(TestCase):
async def asyncSetUp(self):
await super().asyncSetUp()
async with await self.schema.create("categories") as table:
table.id()
table.string("name")
async with await self.schema.create("posts") as table:
table.id()
table.string("title")
table.integer("category_id")
# Category ids deliberately NOT in alphabetical order, so ordering by the
# correlated name subquery differs from ordering by id/category_id.
await Category.query().insert(
[{"id": 1, "name": "Zebra"}, {"id": 2, "name": "Apple"}, {"id": 3, "name": "Mango"}]
)
await Post.query().insert(
[
{"id": 1, "title": "z", "category_id": 1},
{"id": 2, "title": "a", "category_id": 2},
{"id": 3, "title": "m", "category_id": 3},
{"id": 4, "title": "a2", "category_id": 2},
]
)

async def test_order_by_correlated_subquery(self):
posts = await Post.query().order_by(category_name_subquery()).get()
# Apple (ids 2,4) < Mango (3) < Zebra (1)
self.assertEqual([p.title for p in posts], ["a", "a2", "m", "z"])

async def test_order_by_correlated_subquery_desc(self):
posts = await Post.query().order_by(category_name_subquery(), "desc").get()
self.assertEqual([p.title for p in posts], ["z", "m", "a", "a2"])

async def test_full_chain_order_by_subquery_then_paginate(self):
page = await Post.query().order_by(category_name_subquery()).paginate(per_page=2, page=1)
meta = page.serialize()["meta"]
self.assertEqual(meta["total"], 4)
self.assertEqual(meta["last_page"], 2)
self.assertEqual(meta["current_page"], 1)
self.assertEqual(meta["count"], 2)
self.assertEqual(meta["next_page"], 2)
self.assertIsNone(meta["previous_page"])
self.assertEqual([d["title"] for d in page.serialize()["data"]], ["a", "a2"])

page2 = await Post.query().order_by(category_name_subquery()).paginate(per_page=2, page=2)
meta2 = page2.serialize()["meta"]
self.assertEqual(meta2["current_page"], 2)
self.assertEqual(meta2["previous_page"], 1)
self.assertIsNone(meta2["next_page"])
self.assertEqual([d["title"] for d in page2.serialize()["data"]], ["m", "z"])

async def test_string_order_by_still_works(self):
posts = await Post.query().order_by("id", "desc").get()
self.assertEqual([p.id for p in posts], [4, 3, 2, 1])
Loading