From f55003f5d08007c170c86a5ab5a24eec4cf9a879 Mon Sep 17 00:00:00 2001 From: Ultraproduct <4291996-LightlessNight@users.noreply.gitlab.com> Date: Fri, 31 Jul 2020 01:38:09 -0500 Subject: [PATCH 01/10] Implement order by --- orm/models.py | 27 ++++++++++++++++++++++++- tests/test_models.py | 48 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/orm/models.py b/orm/models.py index e61d159..ca3859f 100644 --- a/orm/models.py +++ b/orm/models.py @@ -50,8 +50,9 @@ def __new__( class QuerySet: ESCAPE_CHARACTERS = ['%', '_'] - def __init__(self, model_cls=None, filter_clauses=None, select_related=None, limit_count=None, offset=None): + def __init__(self, model_cls=None, filter_clauses=None, select_related=None, limit_count=None, offset=None, order_args=None): self.model_cls = model_cls + self.order_args = [] if order_args is None else order_args self.filter_clauses = [] if filter_clauses is None else filter_clauses self._select_related = [] if select_related is None else select_related self.limit_count = limit_count @@ -90,6 +91,10 @@ def build_select_expression(self): clause = sqlalchemy.sql.and_(*self.filter_clauses) expr = expr.where(clause) + if self.order_args: + order_args = self._prepared_order() + expr = expr.order_by(*order_args) + if self.limit_count: expr = expr.limit(self.limit_count) @@ -181,6 +186,26 @@ def select_related(self, related): offset=self.query_offset ) + def _prepared_order(self): + prepared = [] + for order in self.order_args: + desc = order.startswith('-') + col_name = order.lstrip('-') if desc else order + col = self.model_cls.__table__.columns[col_name] + prepared.append(col.desc() if desc else col) + + return prepared + + def order_by(self, *args): + return self.__class__( + model_cls=self.model_cls, + filter_clauses=self.filter_clauses, + select_related=self._select_related, + limit_count=self.limit_count, + offset=self.query_offset, + order_args=args, + ) + async def exists(self) -> bool: expr = self.build_select_expression() expr = sqlalchemy.exists(expr).select() diff --git a/tests/test_models.py b/tests/test_models.py index ddd9fde..0ff9ad2 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,4 +1,5 @@ import asyncio +import datetime import functools import pytest @@ -20,6 +21,7 @@ class User(orm.Model): id = orm.Integer(primary_key=True) name = orm.String(max_length=100) + timestamp = orm.DateTime(allow_null=True) class Product(orm.Model): @@ -56,11 +58,12 @@ def run_sync(*args, **kwargs): def test_model_class(): - assert list(User.fields.keys()) == ["id", "name"] + assert list(User.fields.keys()) == ["id", "name", "timestamp"] assert isinstance(User.fields["id"], orm.Integer) assert User.fields["id"].primary_key is True assert isinstance(User.fields["name"], orm.String) assert User.fields["name"].max_length == 100 + assert isinstance(User.fields["timestamp"], orm.DateTime) assert isinstance(User.__table__, sqlalchemy.Table) @@ -157,6 +160,49 @@ async def test_model_filter(): assert await products.count() == 3 +@async_adapter +async def test_model_order_by(): + async with database: + await User.objects.create(name="Tom") + await User.objects.create(name="Allen") + await User.objects.create(name="Bob") + users = await User.objects.order_by("name").all() + assert len(users) == 3 + assert users[0].name == "Allen" + assert users[1].name == "Bob" + assert users[2].name == "Tom" + + +@async_adapter +async def test_model_order_by_desc(): + async with database: + await User.objects.create(name="Tom") + await User.objects.create(name="Allen") + await User.objects.create(name="Bob") + users = await User.objects.order_by("-name").all() + assert len(users) == 3 + assert users[0].name == "Tom" + assert users[1].name == "Bob" + assert users[2].name == "Allen" + + +@async_adapter +async def test_model_order_by_multi(): + async with database: + await User.objects.create(name="Tom", timestamp=datetime.datetime(2020, 1, 1)) + await User.objects.create(name="Tom", timestamp=datetime.datetime(2020, 7, 1)) + await User.objects.create(name="Allen", timestamp=datetime.datetime(2020, 6, 1)) + await User.objects.create(name="Acker", timestamp=datetime.datetime(2020, 12, 1)) + users = await User.objects.order_by("name", "-timestamp").all() + assert len(users) == 4 + assert users[0].name == "Acker" + assert users[1].name == "Allen" + assert users[2].name == "Tom" + assert users[2].timestamp == datetime.datetime(2020, 7, 1) + assert users[3].name == "Tom" + assert users[3].timestamp == datetime.datetime(2020, 1, 1) + + @async_adapter async def test_model_exists(): async with database: From 559f0239a36fd78fad96b4c836ab2e524a06f733 Mon Sep 17 00:00:00 2001 From: Amin Alaee Date: Thu, 19 Aug 2021 18:03:45 +0430 Subject: [PATCH 02/10] format fix --- orm/models.py | 19 ++++++++++--------- tests/test_columns.py | 2 +- tests/test_foreignkey.py | 2 +- tests/test_models.py | 6 ++++-- 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/orm/models.py b/orm/models.py index bbff84f..7d43172 100644 --- a/orm/models.py +++ b/orm/models.py @@ -48,14 +48,15 @@ def __new__( class QuerySet: ESCAPE_CHARACTERS = ["%", "_"] + def __init__( - self, - model_cls=None, - filter_clauses=None, - select_related=None, - limit_count=None, - offset=None, - order_args=None, + self, + model_cls=None, + filter_clauses=None, + select_related=None, + limit_count=None, + offset=None, + order_args=None, ): self.model_cls = model_cls self.order_args = [] if order_args is None else order_args @@ -209,8 +210,8 @@ def select_related(self, related): def _prepared_order(self): prepared = [] for order in self.order_args: - desc = order.startswith('-') - col_name = order.lstrip('-') if desc else order + desc = order.startswith("-") + col_name = order.lstrip("-") if desc else order col = self.model_cls.__table__.columns[col_name] prepared.append(col.desc() if desc else col) diff --git a/tests/test_columns.py b/tests/test_columns.py index 1dcb008..a2cb33c 100644 --- a/tests/test_columns.py +++ b/tests/test_columns.py @@ -3,10 +3,10 @@ import functools from enum import Enum -import databases import pytest import sqlalchemy +import databases import orm from tests.settings import DATABASE_URL diff --git a/tests/test_foreignkey.py b/tests/test_foreignkey.py index 4e0e957..58b5ad6 100644 --- a/tests/test_foreignkey.py +++ b/tests/test_foreignkey.py @@ -1,10 +1,10 @@ import asyncio import functools -import databases import pytest import sqlalchemy +import databases import orm from tests.settings import DATABASE_URL diff --git a/tests/test_models.py b/tests/test_models.py index e25d65a..099ddb5 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -2,10 +2,10 @@ import datetime import functools -import databases import pytest import sqlalchemy +import databases import orm from tests.settings import DATABASE_URL @@ -209,7 +209,9 @@ async def test_model_order_by_multi(): await User.objects.create(name="Tom", timestamp=datetime.datetime(2020, 1, 1)) await User.objects.create(name="Tom", timestamp=datetime.datetime(2020, 7, 1)) await User.objects.create(name="Allen", timestamp=datetime.datetime(2020, 6, 1)) - await User.objects.create(name="Acker", timestamp=datetime.datetime(2020, 12, 1)) + await User.objects.create( + name="Acker", timestamp=datetime.datetime(2020, 12, 1) + ) users = await User.objects.order_by("name", "-timestamp").all() assert len(users) == 4 assert users[0].name == "Acker" From 63637dea07d2ff8a15bc1cc5c9e0d54d83a61d52 Mon Sep 17 00:00:00 2001 From: Amin Alaee Date: Thu, 19 Aug 2021 18:08:49 +0430 Subject: [PATCH 03/10] fix imports --- tests/test_columns.py | 2 +- tests/test_foreignkey.py | 2 +- tests/test_models.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_columns.py b/tests/test_columns.py index a2cb33c..1dcb008 100644 --- a/tests/test_columns.py +++ b/tests/test_columns.py @@ -3,10 +3,10 @@ import functools from enum import Enum +import databases import pytest import sqlalchemy -import databases import orm from tests.settings import DATABASE_URL diff --git a/tests/test_foreignkey.py b/tests/test_foreignkey.py index 58b5ad6..4e0e957 100644 --- a/tests/test_foreignkey.py +++ b/tests/test_foreignkey.py @@ -1,10 +1,10 @@ import asyncio import functools +import databases import pytest import sqlalchemy -import databases import orm from tests.settings import DATABASE_URL diff --git a/tests/test_models.py b/tests/test_models.py index 099ddb5..e45344a 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -2,10 +2,10 @@ import datetime import functools +import databases import pytest import sqlalchemy -import databases import orm from tests.settings import DATABASE_URL From 55a9dd6d2fac7d929d3fe0bd15c45b78c072cd19 Mon Sep 17 00:00:00 2001 From: Amin Alaee Date: Thu, 19 Aug 2021 18:36:54 +0430 Subject: [PATCH 04/10] minor cleanups --- orm/models.py | 22 +++++++++++----------- tests/test_models.py | 28 ++++++++++------------------ 2 files changed, 21 insertions(+), 29 deletions(-) diff --git a/orm/models.py b/orm/models.py index 7d43172..afbef28 100644 --- a/orm/models.py +++ b/orm/models.py @@ -99,7 +99,7 @@ def build_select_expression(self): expr = expr.where(clause) if self.order_args: - order_args = self._prepared_order() + order_args = self._prepare_order_args() expr = expr.order_by(*order_args) if self.limit_count: @@ -207,16 +207,6 @@ def select_related(self, related): offset=self.query_offset, ) - def _prepared_order(self): - prepared = [] - for order in self.order_args: - desc = order.startswith("-") - col_name = order.lstrip("-") if desc else order - col = self.model_cls.__table__.columns[col_name] - prepared.append(col.desc() if desc else col) - - return prepared - def order_by(self, *args): return self.__class__( model_cls=self.model_cls, @@ -311,6 +301,16 @@ async def create(self, **kwargs): instance.pk = await self.database.execute(expr) return instance + def _prepare_order_args(self): + prepared_args = [] + for order_arg in self.order_args: + is_desc = order_arg.startswith("-") + column_name = order_arg.lstrip("-") if is_desc else order_arg + column = self.model_cls.__table__.columns[column_name] + prepared_args.append(column.desc() if is_desc else column) + + return prepared_args + class Model(typesystem.Schema, metaclass=ModelMetaclass): __abstract__ = True diff --git a/tests/test_models.py b/tests/test_models.py index e45344a..fd34027 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,5 +1,4 @@ import asyncio -import datetime import functools import databases @@ -20,7 +19,6 @@ class User(orm.Model): id = orm.Integer(primary_key=True) name = orm.String(max_length=100) - timestamp = orm.DateTime(allow_null=True) class Product(orm.Model): @@ -57,12 +55,11 @@ def run_sync(*args, **kwargs): def test_model_class(): - assert list(User.fields.keys()) == ["id", "name", "timestamp"] + assert list(User.fields.keys()) == ["id", "name"] assert isinstance(User.fields["id"], orm.Integer) assert User.fields["id"].primary_key is True assert isinstance(User.fields["name"], orm.String) assert User.fields["name"].max_length == 100 - assert isinstance(User.fields["timestamp"], orm.DateTime) assert isinstance(User.__table__, sqlalchemy.Table) @@ -206,20 +203,15 @@ async def test_model_order_by_desc(): @async_adapter async def test_model_order_by_multi(): async with database: - await User.objects.create(name="Tom", timestamp=datetime.datetime(2020, 1, 1)) - await User.objects.create(name="Tom", timestamp=datetime.datetime(2020, 7, 1)) - await User.objects.create(name="Allen", timestamp=datetime.datetime(2020, 6, 1)) - await User.objects.create( - name="Acker", timestamp=datetime.datetime(2020, 12, 1) - ) - users = await User.objects.order_by("name", "-timestamp").all() - assert len(users) == 4 - assert users[0].name == "Acker" - assert users[1].name == "Allen" - assert users[2].name == "Tom" - assert users[2].timestamp == datetime.datetime(2020, 7, 1) - assert users[3].name == "Tom" - assert users[3].timestamp == datetime.datetime(2020, 1, 1) + await User.objects.create(name="Tom") + await User.objects.create(name="Tom") + await User.objects.create(name="Allen") + users = await User.objects.order_by("name", "-id").all() + assert len(users) == 3 + assert users[0].name == "Allen" + assert users[0].id == 3 + assert users[1].name == "Tom" + assert users[1].id == 2 @async_adapter From 97eefa1671bb21547b8e98fd25002bce6e594aa7 Mon Sep 17 00:00:00 2001 From: Amin Alaee Date: Fri, 20 Aug 2021 11:12:13 +0430 Subject: [PATCH 05/10] try different `async_adapter` --- tests/test_models.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_models.py b/tests/test_models.py index fd34027..55025c1 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -47,7 +47,11 @@ def async_adapter(wrapped_func): @functools.wraps(wrapped_func) def run_sync(*args, **kwargs): - loop = asyncio.new_event_loop() + try: + loop = asyncio.get_running_loop() + except RuntimeError: + loop = asyncio.new_event_loop() + task = wrapped_func(*args, **kwargs) return loop.run_until_complete(task) From 89657f8e778c486e1d83033649cfd0e3305de22b Mon Sep 17 00:00:00 2001 From: Amin Alaee Date: Fri, 20 Aug 2021 11:15:44 +0430 Subject: [PATCH 06/10] revert async_adapter --- .github/workflows/test-suite.yml | 2 +- tests/test_models.py | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index 87ee45e..183fdc6 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -14,7 +14,7 @@ jobs: strategy: matrix: - python-version: ["3.6", "3.7", "3.8", "3.9", "3.10.0-rc.1"] + python-version: ["3.6", "3.7", "3.9", "3.10.0-rc.1"] steps: - uses: "actions/checkout@v2" diff --git a/tests/test_models.py b/tests/test_models.py index 55025c1..fd34027 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -47,11 +47,7 @@ def async_adapter(wrapped_func): @functools.wraps(wrapped_func) def run_sync(*args, **kwargs): - try: - loop = asyncio.get_running_loop() - except RuntimeError: - loop = asyncio.new_event_loop() - + loop = asyncio.new_event_loop() task = wrapped_func(*args, **kwargs) return loop.run_until_complete(task) From 7ba733727ddddb10c38450c97bbf875fb2e677f0 Mon Sep 17 00:00:00 2001 From: Amin Alaee Date: Fri, 20 Aug 2021 11:19:38 +0430 Subject: [PATCH 07/10] add python3.8 --- .github/workflows/test-suite.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index 183fdc6..87ee45e 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -14,7 +14,7 @@ jobs: strategy: matrix: - python-version: ["3.6", "3.7", "3.9", "3.10.0-rc.1"] + python-version: ["3.6", "3.7", "3.8", "3.9", "3.10.0-rc.1"] steps: - uses: "actions/checkout@v2" From 9f0e8dc02f631aae3d566d9cd031a49f5a7531b0 Mon Sep 17 00:00:00 2001 From: Amin Alaee Date: Fri, 20 Aug 2021 13:22:56 +0430 Subject: [PATCH 08/10] Update README.md --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 25df028..38359bd 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,10 @@ notes = await Note.objects.exclude(completed=False).all() # exact, iexact, contains, icontains, lt, lte, gt, gte, in notes = await Note.objects.filter(text__icontains="mum").all() +# .order_by() +# order by ascending name and descending id +notes = await Note.objects.order_by("name", "-id").all() + # .get() note = await Note.objects.get(id=1) @@ -100,6 +104,7 @@ await note.delete() # 'pk' always refers to the primary key note = await Note.objects.get(pk=2) note.pk # 2 + ``` ORM supports loading and filtering across foreign keys... From 8a4200402bf923291704f77b6cb4167b94568b08 Mon Sep 17 00:00:00 2001 From: Amin Alaee Date: Sat, 21 Aug 2021 16:43:06 +0430 Subject: [PATCH 09/10] Update models.py --- orm/models.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/orm/models.py b/orm/models.py index afbef28..acf98c3 100644 --- a/orm/models.py +++ b/orm/models.py @@ -56,14 +56,14 @@ def __init__( select_related=None, limit_count=None, offset=None, - order_args=None, + order_by=None, ): self.model_cls = model_cls - self.order_args = [] if order_args is None else order_args self.filter_clauses = [] if filter_clauses is None else filter_clauses self._select_related = [] if select_related is None else select_related self.limit_count = limit_count self.query_offset = offset + self._order_by = [] if order_by is None else order_by def __get__(self, instance, owner): return self.__class__(model_cls=owner) @@ -98,9 +98,9 @@ def build_select_expression(self): clause = sqlalchemy.sql.and_(*self.filter_clauses) expr = expr.where(clause) - if self.order_args: - order_args = self._prepare_order_args() - expr = expr.order_by(*order_args) + if self._order_by: + order_by = list(map(self._prepare_order_by, self._order_by)) + expr = expr.order_by(*order_by) if self.limit_count: expr = expr.limit(self.limit_count) @@ -192,6 +192,7 @@ def _filter_query(self, _exclude: bool = False, **kwargs): select_related=select_related, limit_count=self.limit_count, offset=self.query_offset, + order_by=self._order_by, ) def select_related(self, related): @@ -205,16 +206,17 @@ def select_related(self, related): select_related=related, limit_count=self.limit_count, offset=self.query_offset, + order_by=self._order_by, ) - def order_by(self, *args): + def order_by(self, *order_by): return self.__class__( model_cls=self.model_cls, filter_clauses=self.filter_clauses, select_related=self._select_related, limit_count=self.limit_count, offset=self.query_offset, - order_args=args, + order_by=order_by, ) async def exists(self) -> bool: @@ -229,6 +231,7 @@ def limit(self, limit_count: int): select_related=self._select_related, limit_count=limit_count, offset=self.query_offset, + order_by=self._order_by, ) def offset(self, offset: int): @@ -238,6 +241,7 @@ def offset(self, offset: int): select_related=self._select_related, limit_count=self.limit_count, offset=offset, + order_by=self._order_by, ) async def count(self) -> int: @@ -301,15 +305,11 @@ async def create(self, **kwargs): instance.pk = await self.database.execute(expr) return instance - def _prepare_order_args(self): - prepared_args = [] - for order_arg in self.order_args: - is_desc = order_arg.startswith("-") - column_name = order_arg.lstrip("-") if is_desc else order_arg - column = self.model_cls.__table__.columns[column_name] - prepared_args.append(column.desc() if is_desc else column) - - return prepared_args + def _prepare_order_by(self, order_by: str): + reverse = order_by.startswith("-") + order_by = order_by.lstrip("-") + order_col = self.table.columns[order_by] + return order_col.desc() if reverse else order_col class Model(typesystem.Schema, metaclass=ModelMetaclass): From 4251839ad351d4a56b4b7d453460e958d1e71270 Mon Sep 17 00:00:00 2001 From: Amin Alaee Date: Sat, 21 Aug 2021 16:52:02 +0430 Subject: [PATCH 10/10] add tests --- tests/test_models.py | 40 ++++++++++++++++++---------------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/tests/test_models.py b/tests/test_models.py index fd34027..c4f9f9f 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -177,41 +177,37 @@ async def test_model_filter(): @async_adapter async def test_model_order_by(): async with database: - await User.objects.create(name="Tom") + await User.objects.create(name="Bob") await User.objects.create(name="Allen") await User.objects.create(name="Bob") + users = await User.objects.order_by("name").all() - assert len(users) == 3 assert users[0].name == "Allen" assert users[1].name == "Bob" - assert users[2].name == "Tom" - -@async_adapter -async def test_model_order_by_desc(): - async with database: - await User.objects.create(name="Tom") - await User.objects.create(name="Allen") - await User.objects.create(name="Bob") users = await User.objects.order_by("-name").all() - assert len(users) == 3 - assert users[0].name == "Tom" assert users[1].name == "Bob" assert users[2].name == "Allen" - -@async_adapter -async def test_model_order_by_multi(): - async with database: - await User.objects.create(name="Tom") - await User.objects.create(name="Tom") - await User.objects.create(name="Allen") users = await User.objects.order_by("name", "-id").all() - assert len(users) == 3 assert users[0].name == "Allen" + assert users[0].id == 2 + assert users[1].name == "Bob" + assert users[1].id == 3 + + users = await User.objects.filter(name="Bob").order_by("-id").all() + assert users[0].name == "Bob" assert users[0].id == 3 - assert users[1].name == "Tom" - assert users[1].id == 2 + assert users[1].name == "Bob" + assert users[1].id == 1 + + users = await User.objects.order_by("id").limit(1).all() + assert users[0].name == "Bob" + assert users[0].id == 1 + + users = await User.objects.order_by("id").limit(1).offset(1).all() + assert users[0].name == "Allen" + assert users[0].id == 2 @async_adapter