Merge pull request #31 from collerek/add_order_by
Add order_by to QuerySet
This commit is contained in:
8
.github/workflows/deploy-docs.yml
vendored
8
.github/workflows/deploy-docs.yml
vendored
@ -1,11 +1,7 @@
|
|||||||
name: Build Documentation using MkDocs
|
name: Build Documentation using MkDocs
|
||||||
# Controls when the action will run. Triggers the workflow on push or pull request
|
|
||||||
# events but only for the master branch
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [master]
|
branches: [ master ]
|
||||||
pull_request:
|
|
||||||
branches: [master]
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build and Deploy Documentation
|
name: Build and Deploy Documentation
|
||||||
@ -23,5 +19,5 @@ jobs:
|
|||||||
pip install mkdocs-material
|
pip install mkdocs-material
|
||||||
- name: Deploy
|
- name: Deploy
|
||||||
run: |
|
run: |
|
||||||
git pull
|
git pull origin master
|
||||||
mkdocs gh-deploy
|
mkdocs gh-deploy
|
||||||
|
|||||||
@ -164,7 +164,7 @@ assert len(tracks) == 1
|
|||||||
* `count() -> int`
|
* `count() -> int`
|
||||||
* `exists() -> bool`
|
* `exists() -> bool`
|
||||||
* `fields(columns: Union[List, str]) -> QuerySet`
|
* `fields(columns: Union[List, str]) -> QuerySet`
|
||||||
|
* `order_by(columns:Union[List, str]) -> QuerySet`
|
||||||
|
|
||||||
#### Relation types
|
#### Relation types
|
||||||
|
|
||||||
|
|||||||
@ -164,6 +164,7 @@ assert len(tracks) == 1
|
|||||||
* `count() -> int`
|
* `count() -> int`
|
||||||
* `exists() -> bool`
|
* `exists() -> bool`
|
||||||
* `fields(columns: Union[List, str]) -> QuerySet`
|
* `fields(columns: Union[List, str]) -> QuerySet`
|
||||||
|
* `order_by(columns:Union[List, str]) -> QuerySet`
|
||||||
|
|
||||||
|
|
||||||
#### Relation types
|
#### Relation types
|
||||||
|
|||||||
@ -370,5 +370,86 @@ With `fields()` you can select subset of model columns to limit the data load.
|
|||||||
|
|
||||||
Something like `Track.object.select_related("album").filter(album__name="Malibu").offset(1).limit(1).all()`
|
Something like `Track.object.select_related("album").filter(album__name="Malibu").offset(1).limit(1).all()`
|
||||||
|
|
||||||
|
### order_by
|
||||||
|
|
||||||
|
`order_by(columns: Union[List, str]) -> QuerySet`
|
||||||
|
|
||||||
|
With `order_by()` you can order the results from database based on your choice of fields.
|
||||||
|
|
||||||
|
You can provide a string with field name or list of strings with different fields.
|
||||||
|
|
||||||
|
Ordering in sql will be applied in order of names you provide in order_by.
|
||||||
|
|
||||||
|
!!!tip
|
||||||
|
By default if you do not provide ordering `ormar` explicitly orders by all primary keys
|
||||||
|
|
||||||
|
!!!warning
|
||||||
|
If you are sorting by nested models that causes that the result rows are unsorted by the main model
|
||||||
|
`ormar` will combine those children rows into one main model.
|
||||||
|
|
||||||
|
Sample raw database rows result (sort by child model desc):
|
||||||
|
```
|
||||||
|
MODEL: 1 - Child Model - 3
|
||||||
|
MODEL: 2 - Child Model - 2
|
||||||
|
MODEL: 1 - Child Model - 1
|
||||||
|
```
|
||||||
|
|
||||||
|
will result in 2 rows of result:
|
||||||
|
```
|
||||||
|
MODEL: 1 - Child Models: [3, 1] # encountered first in result, all children rows combined
|
||||||
|
MODEL: 2 - Child Modles: [2]
|
||||||
|
```
|
||||||
|
|
||||||
|
The main model will never duplicate in the result
|
||||||
|
|
||||||
|
Given sample Models like following:
|
||||||
|
|
||||||
|
```python
|
||||||
|
--8<-- "../docs_src/queries/docs007.py"
|
||||||
|
```
|
||||||
|
|
||||||
|
To order by main model field just provide a field name
|
||||||
|
|
||||||
|
```python
|
||||||
|
toys = await Toy.objects.select_related("owner").order_by("name").all()
|
||||||
|
assert [x.name.replace("Toy ", "") for x in toys] == [
|
||||||
|
str(x + 1) for x in range(6)
|
||||||
|
]
|
||||||
|
assert toys[0].owner == zeus
|
||||||
|
assert toys[1].owner == aphrodite
|
||||||
|
```
|
||||||
|
|
||||||
|
To sort on nested models separate field names with dunder '__'.
|
||||||
|
|
||||||
|
You can sort this way across all relation types -> `ForeignKey`, reverse virtual FK and `ManyToMany` fields.
|
||||||
|
|
||||||
|
```python
|
||||||
|
toys = await Toy.objects.select_related("owner").order_by("owner__name").all()
|
||||||
|
assert toys[0].owner.name == toys[1].owner.name == "Aphrodite"
|
||||||
|
assert toys[2].owner.name == toys[3].owner.name == "Hermes"
|
||||||
|
assert toys[4].owner.name == toys[5].owner.name == "Zeus"
|
||||||
|
```
|
||||||
|
|
||||||
|
To sort in descending order provide a hyphen in front of the field name
|
||||||
|
|
||||||
|
```python
|
||||||
|
owner = (
|
||||||
|
await Owner.objects.select_related("toys")
|
||||||
|
.order_by("-toys__name")
|
||||||
|
.filter(name="Zeus")
|
||||||
|
.get()
|
||||||
|
)
|
||||||
|
assert owner.toys[0].name == "Toy 4"
|
||||||
|
assert owner.toys[1].name == "Toy 1"
|
||||||
|
```
|
||||||
|
|
||||||
|
!!!note
|
||||||
|
All methods that do not return the rows explicitly returns a QueySet instance so you can chain them together
|
||||||
|
|
||||||
|
So operations like `filter()`, `select_related()`, `limit()` and `offset()` etc. can be chained.
|
||||||
|
|
||||||
|
Something like `Track.object.select_related("album").filter(album__name="Malibu").offset(1).limit(1).all()`
|
||||||
|
|
||||||
|
|
||||||
[models]: ./models.md
|
[models]: ./models.md
|
||||||
[relations]: ./relations.md
|
[relations]: ./relations.md
|
||||||
42
docs_src/queries/docs007.py
Normal file
42
docs_src/queries/docs007.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import databases
|
||||||
|
import sqlalchemy
|
||||||
|
|
||||||
|
import ormar
|
||||||
|
from tests.settings import DATABASE_URL
|
||||||
|
|
||||||
|
database = databases.Database(DATABASE_URL, force_rollback=True)
|
||||||
|
metadata = sqlalchemy.MetaData()
|
||||||
|
|
||||||
|
|
||||||
|
class Owner(ormar.Model):
|
||||||
|
class Meta:
|
||||||
|
tablename = "owners"
|
||||||
|
metadata = metadata
|
||||||
|
database = database
|
||||||
|
|
||||||
|
id: int = ormar.Integer(primary_key=True)
|
||||||
|
name: str = ormar.String(max_length=100)
|
||||||
|
|
||||||
|
|
||||||
|
class Toy(ormar.Model):
|
||||||
|
class Meta:
|
||||||
|
tablename = "toys"
|
||||||
|
metadata = metadata
|
||||||
|
database = database
|
||||||
|
|
||||||
|
id: int = ormar.Integer(primary_key=True)
|
||||||
|
name: str = ormar.String(max_length=100)
|
||||||
|
owner: Owner = ormar.ForeignKey(Owner)
|
||||||
|
|
||||||
|
|
||||||
|
# build some sample data
|
||||||
|
aphrodite = await Owner.objects.create(name="Aphrodite")
|
||||||
|
hermes = await Owner.objects.create(name="Hermes")
|
||||||
|
zeus = await Owner.objects.create(name="Zeus")
|
||||||
|
|
||||||
|
await Toy.objects.create(name="Toy 4", owner=zeus)
|
||||||
|
await Toy.objects.create(name="Toy 5", owner=hermes)
|
||||||
|
await Toy.objects.create(name="Toy 2", owner=aphrodite)
|
||||||
|
await Toy.objects.create(name="Toy 1", owner=zeus)
|
||||||
|
await Toy.objects.create(name="Toy 3", owner=aphrodite)
|
||||||
|
await Toy.objects.create(name="Toy 6", owner=hermes)
|
||||||
@ -30,7 +30,7 @@ class UndefinedType: # pragma no cover
|
|||||||
|
|
||||||
Undefined = UndefinedType()
|
Undefined = UndefinedType()
|
||||||
|
|
||||||
__version__ = "0.4.0"
|
__version__ = "0.4.1"
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Integer",
|
"Integer",
|
||||||
"BigInteger",
|
"BigInteger",
|
||||||
|
|||||||
@ -37,10 +37,7 @@ class Model(NewBaseModel):
|
|||||||
objects: "QuerySet"
|
objects: "QuerySet"
|
||||||
|
|
||||||
def __repr__(self) -> str: # pragma nocover
|
def __repr__(self) -> str: # pragma nocover
|
||||||
attrs_to_include = ["tablename", "columns", "pkname"]
|
_repr = {k: getattr(self, k) for k, v in self.Meta.model_fields.items()}
|
||||||
_repr = {k: v for k, v in self.Meta.model_fields.items()}
|
|
||||||
for atr in attrs_to_include:
|
|
||||||
_repr[atr] = getattr(self.Meta, atr)
|
|
||||||
return f"{self.__class__.__name__}({str(_repr)})"
|
return f"{self.__class__.__name__}({str(_repr)})"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import inspect
|
import inspect
|
||||||
|
from collections import OrderedDict
|
||||||
from typing import Dict, List, Sequence, Set, TYPE_CHECKING, Type, TypeVar, Union
|
from typing import Dict, List, Sequence, Set, TYPE_CHECKING, Type, TypeVar, Union
|
||||||
|
|
||||||
import ormar
|
import ormar
|
||||||
@ -181,11 +182,18 @@ class ModelTableProxy:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def merge_instances_list(cls, result_rows: Sequence["Model"]) -> Sequence["Model"]:
|
def merge_instances_list(cls, result_rows: Sequence["Model"]) -> Sequence["Model"]:
|
||||||
merged_rows: List["Model"] = []
|
merged_rows: List["Model"] = []
|
||||||
for index, model in enumerate(result_rows):
|
grouped_instances: OrderedDict = OrderedDict()
|
||||||
if index > 0 and model is not None and model.pk == merged_rows[-1].pk:
|
|
||||||
merged_rows[-1] = cls.merge_two_instances(model, merged_rows[-1])
|
for model in result_rows:
|
||||||
else:
|
grouped_instances.setdefault(model.pk, []).append(model)
|
||||||
merged_rows.append(model)
|
|
||||||
|
for group in grouped_instances.values():
|
||||||
|
model = group.pop(0)
|
||||||
|
if group:
|
||||||
|
for next_model in group:
|
||||||
|
model = cls.merge_two_instances(next_model, model)
|
||||||
|
merged_rows.append(model)
|
||||||
|
|
||||||
return merged_rows
|
return merged_rows
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
from typing import List, NamedTuple, TYPE_CHECKING, Tuple, Type
|
from collections import OrderedDict
|
||||||
|
from typing import List, NamedTuple, Optional, TYPE_CHECKING, Tuple, Type
|
||||||
|
|
||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
@ -18,19 +19,21 @@ class JoinParameters(NamedTuple):
|
|||||||
|
|
||||||
|
|
||||||
class SqlJoin:
|
class SqlJoin:
|
||||||
def __init__(
|
def __init__( # noqa: CFQ002
|
||||||
self,
|
self,
|
||||||
used_aliases: List,
|
used_aliases: List,
|
||||||
select_from: sqlalchemy.sql.select,
|
select_from: sqlalchemy.sql.select,
|
||||||
order_bys: List[sqlalchemy.sql.elements.TextClause],
|
|
||||||
columns: List[sqlalchemy.Column],
|
columns: List[sqlalchemy.Column],
|
||||||
fields: List,
|
fields: List,
|
||||||
|
order_columns: Optional[List],
|
||||||
|
sorted_orders: OrderedDict,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.used_aliases = used_aliases
|
self.used_aliases = used_aliases
|
||||||
self.select_from = select_from
|
self.select_from = select_from
|
||||||
self.order_bys = order_bys
|
|
||||||
self.columns = columns
|
self.columns = columns
|
||||||
self.fields = fields
|
self.fields = fields
|
||||||
|
self.order_columns = order_columns
|
||||||
|
self.sorted_orders = sorted_orders
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def relation_manager(model_cls: Type["Model"]) -> AliasManager:
|
def relation_manager(model_cls: Type["Model"]) -> AliasManager:
|
||||||
@ -46,20 +49,26 @@ class SqlJoin:
|
|||||||
|
|
||||||
def build_join(
|
def build_join(
|
||||||
self, item: str, join_parameters: JoinParameters
|
self, item: str, join_parameters: JoinParameters
|
||||||
) -> Tuple[List, sqlalchemy.sql.select, List, List]:
|
) -> Tuple[List, sqlalchemy.sql.select, List, OrderedDict]:
|
||||||
for part in item.split("__"):
|
for part in item.split("__"):
|
||||||
if issubclass(
|
if issubclass(
|
||||||
join_parameters.model_cls.Meta.model_fields[part], ManyToManyField
|
join_parameters.model_cls.Meta.model_fields[part], ManyToManyField
|
||||||
):
|
):
|
||||||
_fields = join_parameters.model_cls.Meta.model_fields
|
_fields = join_parameters.model_cls.Meta.model_fields
|
||||||
new_part = _fields[part].to.get_name()
|
new_part = _fields[part].to.get_name()
|
||||||
|
self._switch_many_to_many_order_columns(part, new_part)
|
||||||
join_parameters = self._build_join_parameters(
|
join_parameters = self._build_join_parameters(
|
||||||
part, join_parameters, is_multi=True
|
part, join_parameters, is_multi=True
|
||||||
)
|
)
|
||||||
part = new_part
|
part = new_part
|
||||||
join_parameters = self._build_join_parameters(part, join_parameters)
|
join_parameters = self._build_join_parameters(part, join_parameters)
|
||||||
|
|
||||||
return self.used_aliases, self.select_from, self.columns, self.order_bys
|
return (
|
||||||
|
self.used_aliases,
|
||||||
|
self.select_from,
|
||||||
|
self.columns,
|
||||||
|
self.sorted_orders,
|
||||||
|
)
|
||||||
|
|
||||||
def _build_join_parameters(
|
def _build_join_parameters(
|
||||||
self, part: str, join_params: JoinParameters, is_multi: bool = False
|
self, part: str, join_params: JoinParameters, is_multi: bool = False
|
||||||
@ -108,7 +117,9 @@ class SqlJoin:
|
|||||||
)
|
)
|
||||||
|
|
||||||
pkname_alias = model_cls.get_column_alias(model_cls.Meta.pkname)
|
pkname_alias = model_cls.get_column_alias(model_cls.Meta.pkname)
|
||||||
self.order_bys.append(text(f"{alias}_{to_table}.{pkname_alias}"))
|
if not is_multi:
|
||||||
|
self.get_order_bys(alias, to_table, pkname_alias, part)
|
||||||
|
|
||||||
self_related_fields = model_cls.own_table_columns(
|
self_related_fields = model_cls.own_table_columns(
|
||||||
model_cls, self.fields, nested=True,
|
model_cls, self.fields, nested=True,
|
||||||
)
|
)
|
||||||
@ -119,6 +130,40 @@ class SqlJoin:
|
|||||||
)
|
)
|
||||||
self.used_aliases.append(alias)
|
self.used_aliases.append(alias)
|
||||||
|
|
||||||
|
def _switch_many_to_many_order_columns(self, part: str, new_part: str) -> None:
|
||||||
|
if self.order_columns:
|
||||||
|
split_order_columns = [
|
||||||
|
x.split("__") for x in self.order_columns if "__" in x
|
||||||
|
]
|
||||||
|
for condition in split_order_columns:
|
||||||
|
if condition[-2] == part or condition[-2][1:] == part:
|
||||||
|
condition[-2] = condition[-2].replace(part, new_part)
|
||||||
|
self.order_columns = [x for x in self.order_columns if "__" not in x] + [
|
||||||
|
"__".join(x) for x in split_order_columns
|
||||||
|
]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _check_if_condition_apply(condition: List, part: str) -> bool:
|
||||||
|
return len(condition) >= 2 and (
|
||||||
|
condition[-2] == part or condition[-2][1:] == part
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_order_bys( # noqa: CCR001
|
||||||
|
self, alias: str, to_table: str, pkname_alias: str, part: str
|
||||||
|
) -> None:
|
||||||
|
if self.order_columns:
|
||||||
|
split_order_columns = [
|
||||||
|
x.split("__") for x in self.order_columns if "__" in x
|
||||||
|
]
|
||||||
|
for condition in split_order_columns:
|
||||||
|
if self._check_if_condition_apply(condition, part):
|
||||||
|
direction = f"{'desc' if condition[0][0] == '-' else ''}"
|
||||||
|
order = text(f"{alias}_{to_table}.{condition[-1]} {direction}")
|
||||||
|
self.sorted_orders["__".join(condition)] = order
|
||||||
|
else:
|
||||||
|
order = text(f"{alias}_{to_table}.{pkname_alias}")
|
||||||
|
self.sorted_orders[f"{to_table}.{pkname_alias}"] = order
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_to_and_from_keys(
|
def get_to_and_from_keys(
|
||||||
join_params: JoinParameters,
|
join_params: JoinParameters,
|
||||||
|
|||||||
@ -1,14 +1,15 @@
|
|||||||
from typing import List
|
from typing import Dict
|
||||||
|
|
||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
|
|
||||||
|
|
||||||
class OrderQuery:
|
class OrderQuery:
|
||||||
def __init__(self, order_bys: List) -> None:
|
def __init__(self, sorted_orders: Dict) -> None:
|
||||||
self.order_bys = order_bys
|
self.sorted_orders = sorted_orders
|
||||||
|
|
||||||
def apply(self, expr: sqlalchemy.sql.select) -> sqlalchemy.sql.select:
|
def apply(self, expr: sqlalchemy.sql.select) -> sqlalchemy.sql.select:
|
||||||
if self.order_bys:
|
if self.sorted_orders:
|
||||||
for order in self.order_bys:
|
for order in list(self.sorted_orders.values()):
|
||||||
expr = expr.order_by(order)
|
if order is not None:
|
||||||
|
expr = expr.order_by(order)
|
||||||
return expr
|
return expr
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
from collections import OrderedDict
|
||||||
from typing import List, Optional, TYPE_CHECKING, Tuple, Type
|
from typing import List, Optional, TYPE_CHECKING, Tuple, Type
|
||||||
|
|
||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
@ -21,6 +22,7 @@ class Query:
|
|||||||
limit_count: Optional[int],
|
limit_count: Optional[int],
|
||||||
offset: Optional[int],
|
offset: Optional[int],
|
||||||
fields: Optional[List],
|
fields: Optional[List],
|
||||||
|
order_bys: Optional[List],
|
||||||
) -> None:
|
) -> None:
|
||||||
self.query_offset = offset
|
self.query_offset = offset
|
||||||
self.limit_count = limit_count
|
self.limit_count = limit_count
|
||||||
@ -36,13 +38,34 @@ class Query:
|
|||||||
|
|
||||||
self.select_from: List[str] = []
|
self.select_from: List[str] = []
|
||||||
self.columns = [sqlalchemy.Column]
|
self.columns = [sqlalchemy.Column]
|
||||||
self.order_bys: List[sqlalchemy.sql.elements.TextClause] = []
|
self.order_columns = order_bys
|
||||||
|
self.sorted_orders: OrderedDict = OrderedDict()
|
||||||
|
self._init_sorted_orders()
|
||||||
|
|
||||||
|
def _init_sorted_orders(self) -> None:
|
||||||
|
if self.order_columns:
|
||||||
|
for clause in self.order_columns:
|
||||||
|
self.sorted_orders[clause] = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def prefixed_pk_name(self) -> str:
|
def prefixed_pk_name(self) -> str:
|
||||||
pkname_alias = self.model_cls.get_column_alias(self.model_cls.Meta.pkname)
|
pkname_alias = self.model_cls.get_column_alias(self.model_cls.Meta.pkname)
|
||||||
return f"{self.table.name}.{pkname_alias}"
|
return f"{self.table.name}.{pkname_alias}"
|
||||||
|
|
||||||
|
def apply_order_bys_for_primary_model(self) -> None: # noqa: CCR001
|
||||||
|
if self.order_columns:
|
||||||
|
for clause in self.order_columns:
|
||||||
|
if "__" not in clause:
|
||||||
|
clause = (
|
||||||
|
text(f"{clause[1:]} desc")
|
||||||
|
if clause.startswith("-")
|
||||||
|
else text(clause)
|
||||||
|
)
|
||||||
|
self.sorted_orders[clause] = clause
|
||||||
|
else:
|
||||||
|
order = text(self.prefixed_pk_name)
|
||||||
|
self.sorted_orders[self.prefixed_pk_name] = order
|
||||||
|
|
||||||
def build_select_expression(self) -> Tuple[sqlalchemy.sql.select, List[str]]:
|
def build_select_expression(self) -> Tuple[sqlalchemy.sql.select, List[str]]:
|
||||||
self_related_fields = self.model_cls.own_table_columns(
|
self_related_fields = self.model_cls.own_table_columns(
|
||||||
self.model_cls, self.fields
|
self.model_cls, self.fields
|
||||||
@ -50,7 +73,7 @@ class Query:
|
|||||||
self.columns = self.model_cls.Meta.alias_manager.prefixed_columns(
|
self.columns = self.model_cls.Meta.alias_manager.prefixed_columns(
|
||||||
"", self.table, self_related_fields
|
"", self.table, self_related_fields
|
||||||
)
|
)
|
||||||
self.order_bys = [text(self.prefixed_pk_name)]
|
self.apply_order_bys_for_primary_model()
|
||||||
self.select_from = self.table
|
self.select_from = self.table
|
||||||
|
|
||||||
self._select_related.sort(key=lambda item: (item, -len(item)))
|
self._select_related.sort(key=lambda item: (item, -len(item)))
|
||||||
@ -64,15 +87,16 @@ class Query:
|
|||||||
used_aliases=self.used_aliases,
|
used_aliases=self.used_aliases,
|
||||||
select_from=self.select_from,
|
select_from=self.select_from,
|
||||||
columns=self.columns,
|
columns=self.columns,
|
||||||
order_bys=self.order_bys,
|
|
||||||
fields=self.fields,
|
fields=self.fields,
|
||||||
|
order_columns=self.order_columns,
|
||||||
|
sorted_orders=self.sorted_orders,
|
||||||
)
|
)
|
||||||
|
|
||||||
(
|
(
|
||||||
self.used_aliases,
|
self.used_aliases,
|
||||||
self.select_from,
|
self.select_from,
|
||||||
self.columns,
|
self.columns,
|
||||||
self.order_bys,
|
self.sorted_orders,
|
||||||
) = sql_join.build_join(item, join_parameters)
|
) = sql_join.build_join(item, join_parameters)
|
||||||
|
|
||||||
expr = sqlalchemy.sql.select(self.columns)
|
expr = sqlalchemy.sql.select(self.columns)
|
||||||
@ -94,12 +118,11 @@ class Query:
|
|||||||
)
|
)
|
||||||
expr = LimitQuery(limit_count=self.limit_count).apply(expr)
|
expr = LimitQuery(limit_count=self.limit_count).apply(expr)
|
||||||
expr = OffsetQuery(query_offset=self.query_offset).apply(expr)
|
expr = OffsetQuery(query_offset=self.query_offset).apply(expr)
|
||||||
expr = OrderQuery(order_bys=self.order_bys).apply(expr)
|
expr = OrderQuery(sorted_orders=self.sorted_orders).apply(expr)
|
||||||
return expr
|
return expr
|
||||||
|
|
||||||
def _reset_query_parameters(self) -> None:
|
def _reset_query_parameters(self) -> None:
|
||||||
self.select_from = []
|
self.select_from = []
|
||||||
self.columns = []
|
self.columns = []
|
||||||
self.order_bys = []
|
|
||||||
self.used_aliases = []
|
self.used_aliases = []
|
||||||
self.fields = []
|
self.fields = []
|
||||||
|
|||||||
@ -27,6 +27,7 @@ class QuerySet:
|
|||||||
limit_count: int = None,
|
limit_count: int = None,
|
||||||
offset: int = None,
|
offset: int = None,
|
||||||
columns: List = None,
|
columns: List = None,
|
||||||
|
order_bys: List = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.model_cls = model_cls
|
self.model_cls = model_cls
|
||||||
self.filter_clauses = [] if filter_clauses is None else filter_clauses
|
self.filter_clauses = [] if filter_clauses is None else filter_clauses
|
||||||
@ -35,7 +36,7 @@ class QuerySet:
|
|||||||
self.limit_count = limit_count
|
self.limit_count = limit_count
|
||||||
self.query_offset = offset
|
self.query_offset = offset
|
||||||
self._columns = columns or []
|
self._columns = columns or []
|
||||||
self.order_bys = None
|
self.order_bys = order_bys or []
|
||||||
|
|
||||||
def __get__(
|
def __get__(
|
||||||
self,
|
self,
|
||||||
@ -110,6 +111,7 @@ class QuerySet:
|
|||||||
offset=self.query_offset,
|
offset=self.query_offset,
|
||||||
limit_count=self.limit_count,
|
limit_count=self.limit_count,
|
||||||
fields=self._columns,
|
fields=self._columns,
|
||||||
|
order_bys=self.order_bys,
|
||||||
)
|
)
|
||||||
exp = qry.build_select_expression()
|
exp = qry.build_select_expression()
|
||||||
# print(exp.compile(compile_kwargs={"literal_binds": True}))
|
# print(exp.compile(compile_kwargs={"literal_binds": True}))
|
||||||
@ -137,6 +139,7 @@ class QuerySet:
|
|||||||
limit_count=self.limit_count,
|
limit_count=self.limit_count,
|
||||||
offset=self.query_offset,
|
offset=self.query_offset,
|
||||||
columns=self._columns,
|
columns=self._columns,
|
||||||
|
order_bys=self.order_bys,
|
||||||
)
|
)
|
||||||
|
|
||||||
def exclude(self, **kwargs: Any) -> "QuerySet": # noqa: A003
|
def exclude(self, **kwargs: Any) -> "QuerySet": # noqa: A003
|
||||||
@ -155,6 +158,7 @@ class QuerySet:
|
|||||||
limit_count=self.limit_count,
|
limit_count=self.limit_count,
|
||||||
offset=self.query_offset,
|
offset=self.query_offset,
|
||||||
columns=self._columns,
|
columns=self._columns,
|
||||||
|
order_bys=self.order_bys,
|
||||||
)
|
)
|
||||||
|
|
||||||
def fields(self, columns: Union[List, str]) -> "QuerySet":
|
def fields(self, columns: Union[List, str]) -> "QuerySet":
|
||||||
@ -170,6 +174,23 @@ class QuerySet:
|
|||||||
limit_count=self.limit_count,
|
limit_count=self.limit_count,
|
||||||
offset=self.query_offset,
|
offset=self.query_offset,
|
||||||
columns=columns,
|
columns=columns,
|
||||||
|
order_bys=self.order_bys,
|
||||||
|
)
|
||||||
|
|
||||||
|
def order_by(self, columns: Union[List, str]) -> "QuerySet":
|
||||||
|
if not isinstance(columns, list):
|
||||||
|
columns = [columns]
|
||||||
|
|
||||||
|
order_bys = self.order_bys + [x for x in columns if x not in self.order_bys]
|
||||||
|
return self.__class__(
|
||||||
|
model_cls=self.model,
|
||||||
|
filter_clauses=self.filter_clauses,
|
||||||
|
exclude_clauses=self.exclude_clauses,
|
||||||
|
select_related=self._select_related,
|
||||||
|
limit_count=self.limit_count,
|
||||||
|
offset=self.query_offset,
|
||||||
|
columns=self._columns,
|
||||||
|
order_bys=order_bys,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def exists(self) -> bool:
|
async def exists(self) -> bool:
|
||||||
@ -218,6 +239,7 @@ class QuerySet:
|
|||||||
limit_count=limit_count,
|
limit_count=limit_count,
|
||||||
offset=self.query_offset,
|
offset=self.query_offset,
|
||||||
columns=self._columns,
|
columns=self._columns,
|
||||||
|
order_bys=self.order_bys,
|
||||||
)
|
)
|
||||||
|
|
||||||
def offset(self, offset: int) -> "QuerySet":
|
def offset(self, offset: int) -> "QuerySet":
|
||||||
@ -229,6 +251,7 @@ class QuerySet:
|
|||||||
limit_count=self.limit_count,
|
limit_count=self.limit_count,
|
||||||
offset=offset,
|
offset=offset,
|
||||||
columns=self._columns,
|
columns=self._columns,
|
||||||
|
order_bys=self.order_bys,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def first(self, **kwargs: Any) -> "Model":
|
async def first(self, **kwargs: Any) -> "Model":
|
||||||
|
|||||||
277
tests/test_order_by.py
Normal file
277
tests/test_order_by.py
Normal file
@ -0,0 +1,277 @@
|
|||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
import databases
|
||||||
|
import pytest
|
||||||
|
import sqlalchemy
|
||||||
|
|
||||||
|
import ormar
|
||||||
|
from tests.settings import DATABASE_URL
|
||||||
|
|
||||||
|
database = databases.Database(DATABASE_URL, force_rollback=True)
|
||||||
|
metadata = sqlalchemy.MetaData()
|
||||||
|
|
||||||
|
|
||||||
|
class Song(ormar.Model):
|
||||||
|
class Meta:
|
||||||
|
tablename = "songs"
|
||||||
|
metadata = metadata
|
||||||
|
database = database
|
||||||
|
|
||||||
|
id: int = ormar.Integer(primary_key=True)
|
||||||
|
name: str = ormar.String(max_length=100)
|
||||||
|
sort_order: int = ormar.Integer()
|
||||||
|
|
||||||
|
|
||||||
|
class Owner(ormar.Model):
|
||||||
|
class Meta:
|
||||||
|
tablename = "owners"
|
||||||
|
metadata = metadata
|
||||||
|
database = database
|
||||||
|
|
||||||
|
id: int = ormar.Integer(primary_key=True)
|
||||||
|
name: str = ormar.String(max_length=100)
|
||||||
|
|
||||||
|
|
||||||
|
class Toy(ormar.Model):
|
||||||
|
class Meta:
|
||||||
|
tablename = "toys"
|
||||||
|
metadata = metadata
|
||||||
|
database = database
|
||||||
|
|
||||||
|
id: int = ormar.Integer(primary_key=True)
|
||||||
|
name: str = ormar.String(max_length=100)
|
||||||
|
owner: Owner = ormar.ForeignKey(Owner)
|
||||||
|
|
||||||
|
|
||||||
|
class Factory(ormar.Model):
|
||||||
|
class Meta:
|
||||||
|
tablename = "factories"
|
||||||
|
metadata = metadata
|
||||||
|
database = database
|
||||||
|
|
||||||
|
id: int = ormar.Integer(primary_key=True)
|
||||||
|
name: str = ormar.String(max_length=100)
|
||||||
|
|
||||||
|
|
||||||
|
class Car(ormar.Model):
|
||||||
|
class Meta:
|
||||||
|
tablename = "cars"
|
||||||
|
metadata = metadata
|
||||||
|
database = database
|
||||||
|
|
||||||
|
id: int = ormar.Integer(primary_key=True)
|
||||||
|
name: str = ormar.String(max_length=100)
|
||||||
|
factory: Optional[Factory] = ormar.ForeignKey(Factory)
|
||||||
|
|
||||||
|
|
||||||
|
class UsersCar(ormar.Model):
|
||||||
|
class Meta:
|
||||||
|
tablename = "cars_x_users"
|
||||||
|
metadata = metadata
|
||||||
|
database = database
|
||||||
|
|
||||||
|
|
||||||
|
class User(ormar.Model):
|
||||||
|
class Meta:
|
||||||
|
tablename = "users"
|
||||||
|
metadata = metadata
|
||||||
|
database = database
|
||||||
|
|
||||||
|
id: int = ormar.Integer(primary_key=True)
|
||||||
|
name: str = ormar.String(max_length=100)
|
||||||
|
cars: List[Car] = ormar.ManyToMany(Car, through=UsersCar)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True, scope="module")
|
||||||
|
def create_test_database():
|
||||||
|
engine = sqlalchemy.create_engine(DATABASE_URL)
|
||||||
|
metadata.drop_all(engine)
|
||||||
|
metadata.create_all(engine)
|
||||||
|
yield
|
||||||
|
metadata.drop_all(engine)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_sort_order_on_main_model():
|
||||||
|
async with database:
|
||||||
|
await Song.objects.create(name="Song 3", sort_order=3)
|
||||||
|
await Song.objects.create(name="Song 1", sort_order=1)
|
||||||
|
await Song.objects.create(name="Song 2", sort_order=2)
|
||||||
|
|
||||||
|
songs = await Song.objects.all()
|
||||||
|
assert songs[0].name == "Song 3"
|
||||||
|
assert songs[1].name == "Song 1"
|
||||||
|
assert songs[2].name == "Song 2"
|
||||||
|
|
||||||
|
songs = await Song.objects.order_by("-sort_order").all()
|
||||||
|
assert songs[0].name == "Song 3"
|
||||||
|
assert songs[1].name == "Song 2"
|
||||||
|
assert songs[2].name == "Song 1"
|
||||||
|
|
||||||
|
songs = await Song.objects.order_by("sort_order").all()
|
||||||
|
assert songs[0].name == "Song 1"
|
||||||
|
assert songs[1].name == "Song 2"
|
||||||
|
assert songs[2].name == "Song 3"
|
||||||
|
|
||||||
|
songs = await Song.objects.order_by("name").all()
|
||||||
|
assert songs[0].name == "Song 1"
|
||||||
|
assert songs[1].name == "Song 2"
|
||||||
|
assert songs[2].name == "Song 3"
|
||||||
|
|
||||||
|
await Song.objects.create(name="Song 4", sort_order=1)
|
||||||
|
|
||||||
|
songs = await Song.objects.order_by(["sort_order", "name"]).all()
|
||||||
|
assert songs[0].name == "Song 1"
|
||||||
|
assert songs[1].name == "Song 4"
|
||||||
|
assert songs[2].name == "Song 2"
|
||||||
|
assert songs[3].name == "Song 3"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_sort_order_on_related_model():
|
||||||
|
async with database:
|
||||||
|
aphrodite = await Owner.objects.create(name="Aphrodite")
|
||||||
|
hermes = await Owner.objects.create(name="Hermes")
|
||||||
|
zeus = await Owner.objects.create(name="Zeus")
|
||||||
|
|
||||||
|
await Toy.objects.create(name="Toy 4", owner=zeus)
|
||||||
|
await Toy.objects.create(name="Toy 5", owner=hermes)
|
||||||
|
await Toy.objects.create(name="Toy 2", owner=aphrodite)
|
||||||
|
await Toy.objects.create(name="Toy 1", owner=zeus)
|
||||||
|
await Toy.objects.create(name="Toy 3", owner=aphrodite)
|
||||||
|
await Toy.objects.create(name="Toy 6", owner=hermes)
|
||||||
|
|
||||||
|
toys = await Toy.objects.select_related("owner").order_by("name").all()
|
||||||
|
assert [x.name.replace("Toy ", "") for x in toys] == [
|
||||||
|
str(x + 1) for x in range(6)
|
||||||
|
]
|
||||||
|
assert toys[0].owner == zeus
|
||||||
|
assert toys[1].owner == aphrodite
|
||||||
|
|
||||||
|
toys = await Toy.objects.select_related("owner").order_by("owner__name").all()
|
||||||
|
assert toys[0].owner.name == toys[1].owner.name == "Aphrodite"
|
||||||
|
assert toys[2].owner.name == toys[3].owner.name == "Hermes"
|
||||||
|
assert toys[4].owner.name == toys[5].owner.name == "Zeus"
|
||||||
|
|
||||||
|
owner = (
|
||||||
|
await Owner.objects.select_related("toys")
|
||||||
|
.order_by("toys__name")
|
||||||
|
.filter(name="Zeus")
|
||||||
|
.get()
|
||||||
|
)
|
||||||
|
assert owner.toys[0].name == "Toy 1"
|
||||||
|
assert owner.toys[1].name == "Toy 4"
|
||||||
|
|
||||||
|
owner = (
|
||||||
|
await Owner.objects.select_related("toys")
|
||||||
|
.order_by("-toys__name")
|
||||||
|
.filter(name="Zeus")
|
||||||
|
.get()
|
||||||
|
)
|
||||||
|
assert owner.toys[0].name == "Toy 4"
|
||||||
|
assert owner.toys[1].name == "Toy 1"
|
||||||
|
|
||||||
|
owners = (
|
||||||
|
await Owner.objects.select_related("toys")
|
||||||
|
.order_by("-toys__name")
|
||||||
|
.filter(name__in=["Zeus", "Hermes"])
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
assert owners[0].toys[0].name == "Toy 6"
|
||||||
|
assert owners[0].toys[1].name == "Toy 5"
|
||||||
|
assert owners[0].name == "Hermes"
|
||||||
|
|
||||||
|
assert owners[1].toys[0].name == "Toy 4"
|
||||||
|
assert owners[1].toys[1].name == "Toy 1"
|
||||||
|
assert owners[1].name == "Zeus"
|
||||||
|
|
||||||
|
await Toy.objects.create(name="Toy 7", owner=zeus)
|
||||||
|
|
||||||
|
owners = (
|
||||||
|
await Owner.objects.select_related("toys")
|
||||||
|
.order_by("-toys__name")
|
||||||
|
.filter(name__in=["Zeus", "Hermes"])
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
assert owners[0].toys[0].name == "Toy 7"
|
||||||
|
assert owners[0].toys[1].name == "Toy 4"
|
||||||
|
assert owners[0].toys[2].name == "Toy 1"
|
||||||
|
assert owners[0].name == "Zeus"
|
||||||
|
|
||||||
|
assert owners[1].toys[0].name == "Toy 6"
|
||||||
|
assert owners[1].toys[1].name == "Toy 5"
|
||||||
|
assert owners[1].name == "Hermes"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_sort_order_on_many_to_many():
|
||||||
|
async with database:
|
||||||
|
factory1 = await Factory.objects.create(name="Factory 1")
|
||||||
|
factory2 = await Factory.objects.create(name="Factory 2")
|
||||||
|
|
||||||
|
car1 = await Car.objects.create(name="Buggy", factory=factory1)
|
||||||
|
car2 = await Car.objects.create(name="Volkswagen", factory=factory2)
|
||||||
|
car3 = await Car.objects.create(name="Ferrari", factory=factory1)
|
||||||
|
car4 = await Car.objects.create(name="Volvo", factory=factory2)
|
||||||
|
car5 = await Car.objects.create(name="Skoda", factory=factory1)
|
||||||
|
car6 = await Car.objects.create(name="Seat", factory=factory2)
|
||||||
|
|
||||||
|
user1 = await User.objects.create(name="Mark")
|
||||||
|
user2 = await User.objects.create(name="Julie")
|
||||||
|
|
||||||
|
await user1.cars.add(car1)
|
||||||
|
await user1.cars.add(car3)
|
||||||
|
await user1.cars.add(car4)
|
||||||
|
await user1.cars.add(car5)
|
||||||
|
|
||||||
|
await user2.cars.add(car1)
|
||||||
|
await user2.cars.add(car2)
|
||||||
|
await user2.cars.add(car5)
|
||||||
|
await user2.cars.add(car6)
|
||||||
|
|
||||||
|
user = (
|
||||||
|
await User.objects.select_related("cars")
|
||||||
|
.filter(name="Mark")
|
||||||
|
.order_by("cars__name")
|
||||||
|
.get()
|
||||||
|
)
|
||||||
|
assert user.cars[0].name == "Buggy"
|
||||||
|
assert user.cars[1].name == "Ferrari"
|
||||||
|
assert user.cars[2].name == "Skoda"
|
||||||
|
assert user.cars[3].name == "Volvo"
|
||||||
|
|
||||||
|
user = (
|
||||||
|
await User.objects.select_related("cars")
|
||||||
|
.filter(name="Mark")
|
||||||
|
.order_by("-cars__name")
|
||||||
|
.get()
|
||||||
|
)
|
||||||
|
assert user.cars[3].name == "Buggy"
|
||||||
|
assert user.cars[2].name == "Ferrari"
|
||||||
|
assert user.cars[1].name == "Skoda"
|
||||||
|
assert user.cars[0].name == "Volvo"
|
||||||
|
|
||||||
|
users = await User.objects.select_related("cars").order_by("-cars__name").all()
|
||||||
|
assert users[0].name == "Mark"
|
||||||
|
assert users[1].cars[0].name == "Volkswagen"
|
||||||
|
assert users[1].cars[1].name == "Skoda"
|
||||||
|
assert users[1].cars[2].name == "Seat"
|
||||||
|
assert users[1].cars[3].name == "Buggy"
|
||||||
|
|
||||||
|
users = (
|
||||||
|
await User.objects.select_related(["cars", "cars__factory"])
|
||||||
|
.order_by(["-cars__factory__name", "cars__name"])
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
|
assert users[0].name == "Julie"
|
||||||
|
assert users[0].cars[0].name == "Seat"
|
||||||
|
assert users[0].cars[1].name == "Volkswagen"
|
||||||
|
assert users[0].cars[2].name == "Buggy"
|
||||||
|
assert users[0].cars[3].name == "Skoda"
|
||||||
|
|
||||||
|
assert users[1].name == "Mark"
|
||||||
|
assert users[1].cars[0].name == "Volvo"
|
||||||
|
assert users[1].cars[1].name == "Buggy"
|
||||||
|
assert users[1].cars[2].name == "Ferrari"
|
||||||
|
assert users[1].cars[3].name == "Skoda"
|
||||||
Reference in New Issue
Block a user