change limit/offset with select related to be applied on a subquery and limit only main model query

This commit is contained in:
collerek
2020-12-21 18:42:17 +01:00
parent 514e8c4ad5
commit c8aad2385c
18 changed files with 329 additions and 69 deletions

View File

@ -9,7 +9,7 @@ Out of various types of ORM models inheritance `ormar` currently supports two of
The short summary of different types of inheritance is:
* **Mixins [SUPPORTED]** - don't even subclass `ormar.Model`, just define fields that are later used on several different models (like `created_date` and `updated_date` on each model), only actual models create tables but those fields from mixins are added
* **Mixins [SUPPORTED]** - don't subclass `ormar.Model`, just define fields that are later used on different models (like `created_date` and `updated_date` on each model), only actual models create tables, but those fields from mixins are added
* **Concrete table inheritance [SUPPORTED]** - means that parent is marked as abstract and each child has it's own table with columns from parent and own child columns, kind of similar to Mixins but parent also is a Model
* **Single table inheritance [NOT SUPPORTED]** - means that only one table is created with fields that are combination/sum of the parent and all children models but child models use only subset of column in db (all parent and own ones, skipping the other children ones)
* **Multi/ Joined table inheritance [NOT SUPPORTED]** - means that part of the columns is saved on parent model and part is saved on child model that are connected to each other by kind of one to one relation and under the hood you operate on two models at once
@ -83,7 +83,7 @@ class AuditModel(ormar.Model):
created_by: str = ormar.String(max_length=100)
updated_by: str = ormar.String(max_length=100, default="Sam")
# but if you provide it it will be inherited
# but if you provide it it will be inherited - DRY (Don't Repeat Yourself) in action
class DateFieldsModel(ormar.Model):
class Meta:
abstract = True
@ -117,3 +117,69 @@ Of course apart from that all fields from base classes are combined and created
So in example above `Category` cannot declare it's own `created_date` as this filed will be inherited from `DateFieldsMixins`.
If you try to the `ModelDefinitionError` will be raised.
## Redefining fields in subclasses
Note that you can redefine previously created fields like in normal python class inheritance.
Whenever you define a field with same name and new definition it will completely replace the previously defined one.
```python
# base class
class DateFieldsModel(ormar.Model):
class Meta:
abstract = True
metadata = metadata
database = db
# note that UniqueColumns need sqlalchemy db columns names not the ormar one
constraints = [ormar.UniqueColumns("creation_date", "modification_date")]
created_date: datetime.datetime = ormar.DateTime(
default=datetime.datetime.now, name="creation_date"
)
updated_date: datetime.datetime = ormar.DateTime(
default=datetime.datetime.now, name="modification_date"
)
class RedefinedField(DateFieldsModel):
class Meta(ormar.ModelMeta):
tablename = "redefines"
metadata = metadata
database = db
id: int = ormar.Integer(primary_key=True)
# here the created_date is replaced by the String field
created_date: str = ormar.String(max_length=200, name="creation_date")
# you can verify that the final field is correctly declared and created
changed_field = RedefinedField.Meta.model_fields["created_date"]
assert changed_field.default is None
assert changed_field.alias == "creation_date"
assert any(x.name == "creation_date" for x in RedefinedField.Meta.table.columns)
assert isinstance(
RedefinedField.Meta.table.columns["creation_date"].type,
sqlalchemy.sql.sqltypes.String,
)
```
!!!warning
If you declare `UniqueColumns` constraint with column names, the final model **has to have**
a column with the same name declared. Otherwise, the `ModelDefinitionError` will be raised.
So in example above if you do not provide `name` for `created_date` in `RedefinedField` model
ormar will complain.
`created_date: str = ormar.String(max_length=200) # exception`
`created_date: str = ormar.String(max_length=200, name="creation_date2") # exception`
## Relations in inheritance
You can declare relations in every step of inheritance, so both in parent and child classes.
But you always need to be aware of related_name parameter, that has to be unique across a model,
when you define multiple child classes that inherit the same relation.

View File

@ -1,3 +1,19 @@
# 0.8.0
* **Breaking:** removing parent from child side in reverse ForeignKey relation now requires passing a relation `name`,
as the same model can be registered multiple times and ormar needs to know from which relation on the parent you want to remove the child.
* **Breaking:** applying limit and offset with select related is by default applied only on the main table before the join -> meaning that not the total
number of rows is limited but just main models (first one in the query, the one to used to construct it)
* **Breaking:** issuing `first()` now fetches the first row ordered by the primary key asc (so first one inserted (can be different for non number primary keys - i.e. alphabetical order of string)) and also can be used with `prefetch_related`
* **Breaking:** issuing `get()` **without any filters** now fetches the first row ordered by the primary key desc (so should be last one inserted (can be different for non number primary keys - i.e. alphabetical order of string))
* Introduce inheritance, for now two types of inheritance are possible:
* **Mixins** - don't subclass `ormar.Model`, just define fields that are later used on different models (like `created_date` and `updated_date` on each model), only actual models create tables, but those fields from mixins are added
* **Concrete table inheritance** - means that parent is marked as abstract and each child has its own table with columns from the parent and own child columns, kind of similar to Mixins but parent also is a Model
* To read more check the docs on models -> inheritance section.
* Fix bug in order_by for primary model order bys
* Fix in `prefetch_query` for multiple related_names for the same model.
* Split and cleanup in docs.
# 0.7.5
* Fix for wrong relation column name in many_to_many relation joins (fix [#73][#73])

View File

@ -48,8 +48,8 @@ def register_many_to_many_relation_on_build(
:param field: relation field
:type field: ManyToManyField class
"""
alias_manager.add_relation_type(field.through, new_model.get_name(), is_multi=True)
alias_manager.add_relation_type(field.through, field.to.get_name(), is_multi=True)
alias_manager.add_relation_type(field.through, new_model.get_name())
alias_manager.add_relation_type(field.through, field.to.get_name())
def expand_reverse_relationships(model: Type["Model"]) -> None:

View File

@ -17,13 +17,12 @@ from typing import (
Union,
)
from ormar.exceptions import ModelPersistenceError, RelationshipInstanceError
from ormar.queryset.utils import translate_list_to_dict, update
import ormar # noqa: I100
from ormar.exceptions import ModelPersistenceError
from ormar.fields import BaseField, ManyToManyField
from ormar.fields.foreign_key import ForeignKeyField
from ormar.models.metaclass import ModelMeta
from ormar.queryset.utils import translate_list_to_dict, update
if TYPE_CHECKING: # pragma no cover
from ormar import Model
@ -76,9 +75,10 @@ class ModelTableProxy:
)
field = target_model.Meta.model_fields[field_name]
if issubclass(field, ormar.fields.ManyToManyField):
sub_field = target_model.resolve_relation_field(
field.through, parent_model
field_name = parent_model.resolve_relation_name(
field.through, field.to, explicit_multi=True
)
sub_field = field.through.Meta.model_fields[field_name]
return field.through, sub_field.get_alias()
return target_model, field.get_alias()
target_field = target_model.get_column_alias(target_model.Meta.pkname)
@ -86,17 +86,14 @@ class ModelTableProxy:
@staticmethod
def get_column_name_for_id_extraction(
parent_model: Type["Model"],
target_model: Type["Model"],
reverse: bool,
use_raw: bool,
parent_model: Type["Model"], reverse: bool, related: str, use_raw: bool,
) -> str:
if reverse:
column_name = parent_model.Meta.pkname
return (
parent_model.get_column_alias(column_name) if use_raw else column_name
)
column = target_model.resolve_relation_field(parent_model, target_model)
column = parent_model.Meta.model_fields[related]
return column.get_alias() if use_raw else column.name
@classmethod
@ -322,19 +319,6 @@ class ModelTableProxy:
f"No relation between {item.get_name()} and {related.get_name()}"
) # pragma nocover
@staticmethod
def resolve_relation_field(
item: Union["Model", Type["Model"]], related: Union["Model", Type["Model"]]
) -> Type[BaseField]:
name = ModelTableProxy.resolve_relation_name(item, related)
to_field = item.Meta.model_fields.get(name)
if not to_field: # pragma no cover
raise RelationshipInstanceError(
f"Model {item.__class__} does not have "
f"reference to model {related.__class__}"
)
return to_field
@classmethod
def translate_columns_to_aliases(cls, new_kwargs: Dict) -> Dict:
for field_name, field in cls.Meta.model_fields.items():

View File

@ -224,8 +224,8 @@ class NewBaseModel(
def db_backend_name(cls) -> str:
return cls.Meta.database._backend._dialect.name
def remove(self, name: "T") -> None:
self._orm.remove_parent(self, name)
def remove(self, parent: "T", name: str) -> None:
self._orm.remove_parent(self, parent, name)
def set_save_status(self, status: bool) -> None:
object.__setattr__(self, "_orm_saved", status)

View File

@ -138,7 +138,7 @@ class QueryClause:
through_field = model_cls.Meta.model_fields[part]
previous_model = through_field.through
part2 = model_cls.resolve_relation_name(
through_field.through, through_field.to, explicit_multi=True
previous_model, through_field.to, explicit_multi=True
)
manager = model_cls.Meta.alias_manager
table_prefix = manager.resolve_relation_join(previous_model, part2)

View File

@ -123,15 +123,15 @@ class PrefetchQuery:
return list_of_ids
def _extract_required_ids(
self, parent_model: Type["Model"], target_model: Type["Model"], reverse: bool,
self, parent_model: Type["Model"], reverse: bool, related: str,
) -> Set:
use_raw = parent_model.get_name() not in self.models
column_name = parent_model.get_column_name_for_id_extraction(
parent_model=parent_model,
target_model=target_model,
reverse=reverse,
related=related,
use_raw=use_raw,
)
@ -152,7 +152,7 @@ class PrefetchQuery:
related: str,
) -> List:
ids = self._extract_required_ids(
parent_model=parent_model, target_model=target_model, reverse=reverse,
parent_model=parent_model, reverse=reverse, related=related
)
if ids:
(
@ -343,6 +343,7 @@ class PrefetchQuery:
fields=fields,
exclude_fields=exclude_fields,
order_bys=None,
limit_raw_sql=False,
)
expr = qry.build_select_expression()
# print(expr.compile(compile_kwargs={"literal_binds": True}))

View File

@ -25,6 +25,7 @@ class Query:
fields: Optional[Union[Dict, Set]],
exclude_fields: Optional[Union[Dict, Set]],
order_bys: Optional[List],
limit_raw_sql: bool,
) -> None:
self.query_offset = offset
self.limit_count = limit_count
@ -45,6 +46,8 @@ class Query:
self.sorted_orders: OrderedDict = OrderedDict()
self._init_sorted_orders()
self.limit_raw_sql = limit_raw_sql
def _init_sorted_orders(self) -> None:
if self.order_columns:
for clause in self.order_columns:
@ -62,16 +65,31 @@ class Query:
if self.order_columns:
for clause in self.order_columns:
if "__" not in clause:
clause = (
text_clause = (
text(f"{self.alias(clause[1:])} desc")
if clause.startswith("-")
else text(self.alias(clause))
)
self.sorted_orders[clause] = clause
self.sorted_orders[clause] = text_clause
else:
order = text(self.prefixed_pk_name)
self.sorted_orders[self.prefixed_pk_name] = order
def _pagination_query_required(self) -> bool:
"""
Checks if limit or offset are set, the flag limit_sql_raw is not set
and query has select_related applied. Otherwise we can limit/offset normally
at the end of whole query.
:return: result of the check
:rtype: bool
"""
return bool(
(self.limit_count or self.query_offset)
and not self.limit_raw_sql
and self._select_related
)
def build_select_expression(self) -> Tuple[sqlalchemy.sql.select, List[str]]:
self_related_fields = self.model_cls.own_table_columns(
model=self.model_cls,
@ -83,7 +101,10 @@ class Query:
"", self.table, self_related_fields
)
self.apply_order_bys_for_primary_model()
self.select_from = self.table
if self._pagination_query_required():
self.select_from = self._build_pagination_subquery()
else:
self.select_from = self.table
self._select_related.sort(key=lambda item: (item, -len(item)))
@ -120,6 +141,46 @@ class Query:
return expr
def _build_pagination_subquery(self) -> sqlalchemy.sql.select:
"""
In order to apply limit and offset on main table in join only
(otherwise you can get only partially constructed main model
if number of children exceeds the applied limit and select_related is used)
Used also to change first and get() without argument behaviour.
Needed only if limit or offset are set, the flag limit_sql_raw is not set
and query has select_related applied. Otherwise we can limit/offset normally
at the end of whole query.
:return: constructed subquery on main table with limit, offset and order applied
:rtype: sqlalchemy.sql.select
"""
expr = sqlalchemy.sql.select(self.model_cls.Meta.table.columns)
expr = LimitQuery(limit_count=self.limit_count).apply(expr)
expr = OffsetQuery(query_offset=self.query_offset).apply(expr)
filters_to_use = [
filter_clause
for filter_clause in self.filter_clauses
if filter_clause.text.startswith(f"{self.table.name}.")
]
excludes_to_use = [
filter_clause
for filter_clause in self.exclude_clauses
if filter_clause.text.startswith(f"{self.table.name}.")
]
sorts_to_use = {
k: v
for k, v in self.sorted_orders.items()
if k.startswith(f"{self.table.name}.")
}
expr = FilterQuery(filter_clauses=filters_to_use).apply(expr)
expr = FilterQuery(filter_clauses=excludes_to_use, exclude=True).apply(expr)
expr = OrderQuery(sorted_orders=sorts_to_use).apply(expr)
expr = expr.alias(f"{self.table}")
self.filter_clauses = list(set(self.filter_clauses) - set(filters_to_use))
self.exclude_clauses = list(set(self.exclude_clauses) - set(excludes_to_use))
return expr
def _apply_expression_modifiers(
self, expr: sqlalchemy.sql.select
) -> sqlalchemy.sql.select:
@ -127,8 +188,9 @@ class Query:
expr = FilterQuery(filter_clauses=self.exclude_clauses, exclude=True).apply(
expr
)
expr = LimitQuery(limit_count=self.limit_count).apply(expr)
expr = OffsetQuery(query_offset=self.query_offset).apply(expr)
if not self._pagination_query_required():
expr = LimitQuery(limit_count=self.limit_count).apply(expr)
expr = OffsetQuery(query_offset=self.query_offset).apply(expr)
expr = OrderQuery(sorted_orders=self.sorted_orders).apply(expr)
return expr

View File

@ -32,6 +32,7 @@ class QuerySet:
exclude_columns: Dict = None,
order_bys: List = None,
prefetch_related: List = None,
limit_raw_sql: bool = False,
) -> None:
self.model_cls = model_cls
self.filter_clauses = [] if filter_clauses is None else filter_clauses
@ -43,6 +44,7 @@ class QuerySet:
self._columns = columns or {}
self._exclude_columns = exclude_columns or {}
self.order_bys = order_bys or []
self.limit_sql_raw = limit_raw_sql
def __get__(
self,
@ -123,17 +125,20 @@ class QuerySet:
def table(self) -> sqlalchemy.Table:
return self.model_meta.table
def build_select_expression(self) -> sqlalchemy.sql.select:
def build_select_expression(
self, limit: int = None, offset: int = None, order_bys: List = None,
) -> sqlalchemy.sql.select:
qry = Query(
model_cls=self.model,
select_related=self._select_related,
filter_clauses=self.filter_clauses,
exclude_clauses=self.exclude_clauses,
offset=self.query_offset,
limit_count=self.limit_count,
offset=offset or self.query_offset,
limit_count=limit or self.limit_count,
fields=self._columns,
exclude_fields=self._exclude_columns,
order_bys=self.order_bys,
order_bys=order_bys or self.order_bys,
limit_raw_sql=self.limit_sql_raw,
)
exp = qry.build_select_expression()
# print(exp.compile(compile_kwargs={"literal_binds": True}))
@ -164,6 +169,7 @@ class QuerySet:
exclude_columns=self._exclude_columns,
order_bys=self.order_bys,
prefetch_related=self._prefetch_related,
limit_raw_sql=self.limit_sql_raw,
)
def exclude(self, **kwargs: Any) -> "QuerySet": # noqa: A003
@ -185,6 +191,7 @@ class QuerySet:
exclude_columns=self._exclude_columns,
order_bys=self.order_bys,
prefetch_related=self._prefetch_related,
limit_raw_sql=self.limit_sql_raw,
)
def prefetch_related(self, related: Union[List, str]) -> "QuerySet":
@ -203,6 +210,7 @@ class QuerySet:
exclude_columns=self._exclude_columns,
order_bys=self.order_bys,
prefetch_related=related,
limit_raw_sql=self.limit_sql_raw,
)
def exclude_fields(self, columns: Union[List, str, Set, Dict]) -> "QuerySet":
@ -226,6 +234,7 @@ class QuerySet:
exclude_columns=current_excluded,
order_bys=self.order_bys,
prefetch_related=self._prefetch_related,
limit_raw_sql=self.limit_sql_raw,
)
def fields(self, columns: Union[List, str, Set, Dict]) -> "QuerySet":
@ -249,6 +258,7 @@ class QuerySet:
exclude_columns=self._exclude_columns,
order_bys=self.order_bys,
prefetch_related=self._prefetch_related,
limit_raw_sql=self.limit_sql_raw,
)
def order_by(self, columns: Union[List, str]) -> "QuerySet":
@ -267,6 +277,7 @@ class QuerySet:
exclude_columns=self._exclude_columns,
order_bys=order_bys,
prefetch_related=self._prefetch_related,
limit_raw_sql=self.limit_sql_raw,
)
async def exists(self) -> bool:
@ -308,7 +319,8 @@ class QuerySet:
)
return await self.database.execute(expr)
def limit(self, limit_count: int) -> "QuerySet":
def limit(self, limit_count: int, limit_raw_sql: bool = None) -> "QuerySet":
limit_raw_sql = self.limit_sql_raw if limit_raw_sql is None else limit_raw_sql
return self.__class__(
model_cls=self.model,
filter_clauses=self.filter_clauses,
@ -320,9 +332,11 @@ class QuerySet:
exclude_columns=self._exclude_columns,
order_bys=self.order_bys,
prefetch_related=self._prefetch_related,
limit_raw_sql=limit_raw_sql,
)
def offset(self, offset: int) -> "QuerySet":
def offset(self, offset: int, limit_raw_sql: bool = None) -> "QuerySet":
limit_raw_sql = self.limit_sql_raw if limit_raw_sql is None else limit_raw_sql
return self.__class__(
model_cls=self.model,
filter_clauses=self.filter_clauses,
@ -334,23 +348,33 @@ class QuerySet:
exclude_columns=self._exclude_columns,
order_bys=self.order_bys,
prefetch_related=self._prefetch_related,
limit_raw_sql=limit_raw_sql,
)
async def first(self, **kwargs: Any) -> "Model":
if kwargs:
return await self.filter(**kwargs).first()
rows = await self.limit(1).all()
self.check_single_result_rows_count(rows)
return rows[0] # type: ignore
expr = self.build_select_expression(
limit=1, order_bys=[f"{self.model.Meta.pkname}"]
)
rows = await self.database.fetch_all(expr)
processed_rows = self._process_query_result_rows(rows)
if self._prefetch_related and processed_rows:
processed_rows = await self._prefetch_related_models(processed_rows, rows)
self.check_single_result_rows_count(processed_rows)
return processed_rows[0] # type: ignore
async def get(self, **kwargs: Any) -> "Model":
if kwargs:
return await self.filter(**kwargs).get()
expr = self.build_select_expression()
if not self.filter_clauses:
expr = expr.limit(2)
expr = self.build_select_expression(
limit=1, order_bys=[f"-{self.model.Meta.pkname}"]
)
else:
expr = self.build_select_expression()
rows = await self.database.fetch_all(expr)
processed_rows = self._process_query_result_rows(rows)

View File

@ -40,7 +40,7 @@ class AliasManager:
return text(f"{name} {alias}_{name}")
def add_relation_type(
self, source_model: Type["Model"], relation_name: str, is_multi: bool = False
self, source_model: Type["Model"], relation_name: str
) -> None:
parent_key = f"{source_model.get_name()}_{relation_name}"
if parent_key not in self._aliases_new:
@ -50,7 +50,7 @@ class AliasManager:
related_name = to_field.related_name
if not related_name:
related_name = child_model.resolve_relation_name(
child_model, source_model, explicit_multi=is_multi
child_model, source_model, explicit_multi=True
)
child_key = f"{child_model.get_name()}_{related_name}"
if child_key not in self._aliases_new:

View File

@ -53,7 +53,7 @@ class QuerysetProxy(ormar.QuerySetProtocol):
def _assign_child_to_parent(self, child: Optional["T"]) -> None:
if child:
owner = self._owner
rel_name = owner.resolve_relation_name(owner, child)
rel_name = self.relation.field_name
setattr(owner, rel_name, child)
def _register_related(self, child: Union["T", Sequence[Optional["T"]]]) -> None:

View File

@ -89,11 +89,10 @@ class RelationsManager:
@staticmethod
def remove_parent(
item: Union["NewBaseModel", Type["NewBaseModel"]], name: "Model"
item: Union["NewBaseModel", Type["NewBaseModel"]], parent: "Model", name: str
) -> None:
related_model = name
rel_name = item.resolve_relation_name(item, related_model)
if rel_name in item._orm:
relation_name = item.resolve_relation_name(related_model, item)
item._orm.remove(rel_name, related_model)
related_model._orm.remove(relation_name, item)
relation_name = (
item.Meta.model_fields[name].related_name or item.get_name() + "s"
)
item._orm.remove(name, parent)
parent._orm.remove(relation_name, item)

View File

@ -89,6 +89,10 @@ async def test_working_with_aliases():
first_name="Son", last_name="2", born_year=1995
)
await artist.children.create(
first_name="Son", last_name="3", born_year=1998
)
album = await Album.objects.select_related("artist").first()
assert album.artist.last_name == "Mosbey"
@ -99,9 +103,10 @@ async def test_working_with_aliases():
assert album.name == "Aunt Robin"
artist = await Artist.objects.select_related("children").get()
assert len(artist.children) == 2
assert len(artist.children) == 3
assert artist.children[0].first_name == "Son"
assert artist.children[1].last_name == "2"
assert artist.children[2].last_name == "3"
await artist.update(last_name="Bundy")
await Artist.objects.filter(pk=artist.pk).update(born_year=1974)

View File

@ -205,11 +205,11 @@ async def test_model_removal_from_relations():
album = await Album.objects.select_related("tracks").get(name="Chichi")
assert track1.album == album
track1.remove(album)
track1.remove(album, name="album")
assert track1.album is None
assert len(album.tracks) == 2
track2.remove(album)
track2.remove(album, name="album")
assert track2.album is None
assert len(album.tracks) == 1

View File

@ -102,6 +102,7 @@ class Car(ormar.Model):
name: str = ormar.String(max_length=50)
owner: Person = ormar.ForeignKey(Person)
co_owner: Person = ormar.ForeignKey(Person, related_name="coowned")
created_date: datetime.datetime = ormar.DateTime(default=datetime.datetime.now)
class Truck(Car):
@ -291,11 +292,22 @@ async def test_inheritance_with_relation():
).get(name="Joe")
assert joe_check.pk == joe.pk
assert joe_check.coowned_trucks[0] == shelby
assert joe_check.coowned_trucks[0].created_date is not None
assert joe_check.coowned_buses[0] == unicorn
assert joe_check.coowned_buses[0].created_date is not None
joe_check = await Person.objects.prefetch_related(
["coowned_trucks", "coowned_buses"]
).get(name="Joe")
joe_check = (
await Person.objects.exclude_fields(
{
"coowned_trucks": {"created_date"},
"coowned_buses": {"created_date"},
}
)
.prefetch_related(["coowned_trucks", "coowned_buses"])
.get(name="Joe")
)
assert joe_check.pk == joe.pk
assert joe_check.coowned_trucks[0] == shelby
assert joe_check.coowned_trucks[0].created_date is None
assert joe_check.coowned_buses[0] == unicorn
assert joe_check.coowned_buses[0].created_date is None

View File

@ -58,6 +58,16 @@ class User(ormar.Model):
name: str = ormar.String(max_length=100, default="")
class User2(ormar.Model):
class Meta:
tablename = "users2"
metadata = metadata
database = database
id: str = ormar.String(primary_key=True, max_length=100)
name: str = ormar.String(max_length=100, default="")
class Product(ormar.Model):
class Meta:
tablename = "product"
@ -215,8 +225,9 @@ async def test_model_get():
assert lookup == user
user = await User.objects.create(name="Jane")
await User.objects.create(name="Jane")
with pytest.raises(ormar.MultipleMatches):
await User.objects.get()
await User.objects.get(name="Jane")
same_user = await User.objects.get(pk=user.id)
assert same_user.id == user.id
@ -467,3 +478,32 @@ async def test_start_and_end_filters():
users = await User.objects.filter(name__endswith="igo").all()
assert len(users) == 2
@pytest.mark.asyncio
async def test_get_and_first():
async with database:
async with database.transaction(force_rollback=True):
await User.objects.create(name="Tom")
await User.objects.create(name="Jane")
await User.objects.create(name="Lucy")
await User.objects.create(name="Zack")
await User.objects.create(name="Ula")
user = await User.objects.get()
assert user.name == "Ula"
user = await User.objects.first()
assert user.name == "Tom"
await User2.objects.create(id="Tom", name="Tom")
await User2.objects.create(id="Jane", name="Jane")
await User2.objects.create(id="Lucy", name="Lucy")
await User2.objects.create(id="Zack", name="Zack")
await User2.objects.create(id="Ula", name="Ula")
user = await User2.objects.get()
assert user.name == "Zack"
user = await User2.objects.first()
assert user.name == "Jane"

View File

@ -266,7 +266,7 @@ async def test_prefetch_related_with_select_related():
await Album.objects.select_related(["tracks", "shops"])
.filter(name="Malibu")
.prefetch_related(["cover_pictures", "shops__division"])
.get()
.first()
)
assert len(album.tracks) == 0

View File

@ -90,14 +90,65 @@ async def test_create_primary_models():
await p1.keywords.add(keyword)
else:
await p2.keywords.add(keyword)
models = await PrimaryModel.objects.prefetch_related("keywords").limit(5).all()
models = await PrimaryModel.objects.select_related("keywords").limit(5).all()
# This test fails, because of the keywords relation.
assert len(models) == 5
assert len(models[0].keywords) == 2
assert len(models[1].keywords) == 3
assert len(models[2].keywords) == 0
models2 = (
await PrimaryModel.objects.select_related("keywords")
.limit(5)
.offset(3)
.all()
)
assert len(models2) == 5
assert [x.name for x in models2] != [x.name for x in models]
assert [x.name for x in models2] == [
"Primary 4",
"Primary 5",
"Primary 6",
"Primary 7",
"Primary 8",
]
models3 = (
await PrimaryModel.objects.select_related("keywords")
.limit(5, limit_raw_sql=True)
.all()
)
assert len(models3) == 2
assert len(models3[0].keywords) == 2
assert len(models3[1].keywords) == 3
models4 = (
await PrimaryModel.objects.offset(1)
.select_related("keywords")
.limit(5, limit_raw_sql=True)
.all()
)
assert len(models4) == 3
assert [x.name for x in models4] == ["Primary 1", "Primary 2", "Primary 3"]
assert len(models4[0].keywords) == 1
assert len(models4[1].keywords) == 3
assert len(models4[2].keywords) == 0
models5 = (
await PrimaryModel.objects.select_related("keywords")
.offset(2, limit_raw_sql=True)
.limit(5)
.all()
)
assert len(models5) == 3
assert [x.name for x in models5] == ["Primary 2", "Primary 3", "Primary 4"]
assert len(models5[0].keywords) == 3
assert len(models5[1].keywords) == 0
assert len(models5[2].keywords) == 0
@pytest.fixture(autouse=True, scope="module")
def create_test_database():