change limit/offset with select related to be applied on a subquery and limit only main model query
This commit is contained in:
@ -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:
|
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
|
* **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)
|
* **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
|
* **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)
|
created_by: str = ormar.String(max_length=100)
|
||||||
updated_by: str = ormar.String(max_length=100, default="Sam")
|
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 DateFieldsModel(ormar.Model):
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
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`.
|
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.
|
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.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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
|
# 0.7.5
|
||||||
|
|
||||||
* Fix for wrong relation column name in many_to_many relation joins (fix [#73][#73])
|
* Fix for wrong relation column name in many_to_many relation joins (fix [#73][#73])
|
||||||
|
|||||||
@ -48,8 +48,8 @@ def register_many_to_many_relation_on_build(
|
|||||||
:param field: relation field
|
:param field: relation field
|
||||||
:type field: ManyToManyField class
|
: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, new_model.get_name())
|
||||||
alias_manager.add_relation_type(field.through, field.to.get_name(), is_multi=True)
|
alias_manager.add_relation_type(field.through, field.to.get_name())
|
||||||
|
|
||||||
|
|
||||||
def expand_reverse_relationships(model: Type["Model"]) -> None:
|
def expand_reverse_relationships(model: Type["Model"]) -> None:
|
||||||
|
|||||||
@ -17,13 +17,12 @@ from typing import (
|
|||||||
Union,
|
Union,
|
||||||
)
|
)
|
||||||
|
|
||||||
from ormar.exceptions import ModelPersistenceError, RelationshipInstanceError
|
|
||||||
from ormar.queryset.utils import translate_list_to_dict, update
|
|
||||||
|
|
||||||
import ormar # noqa: I100
|
import ormar # noqa: I100
|
||||||
|
from ormar.exceptions import ModelPersistenceError
|
||||||
from ormar.fields import BaseField, ManyToManyField
|
from ormar.fields import BaseField, ManyToManyField
|
||||||
from ormar.fields.foreign_key import ForeignKeyField
|
from ormar.fields.foreign_key import ForeignKeyField
|
||||||
from ormar.models.metaclass import ModelMeta
|
from ormar.models.metaclass import ModelMeta
|
||||||
|
from ormar.queryset.utils import translate_list_to_dict, update
|
||||||
|
|
||||||
if TYPE_CHECKING: # pragma no cover
|
if TYPE_CHECKING: # pragma no cover
|
||||||
from ormar import Model
|
from ormar import Model
|
||||||
@ -76,9 +75,10 @@ class ModelTableProxy:
|
|||||||
)
|
)
|
||||||
field = target_model.Meta.model_fields[field_name]
|
field = target_model.Meta.model_fields[field_name]
|
||||||
if issubclass(field, ormar.fields.ManyToManyField):
|
if issubclass(field, ormar.fields.ManyToManyField):
|
||||||
sub_field = target_model.resolve_relation_field(
|
field_name = parent_model.resolve_relation_name(
|
||||||
field.through, parent_model
|
field.through, field.to, explicit_multi=True
|
||||||
)
|
)
|
||||||
|
sub_field = field.through.Meta.model_fields[field_name]
|
||||||
return field.through, sub_field.get_alias()
|
return field.through, sub_field.get_alias()
|
||||||
return target_model, field.get_alias()
|
return target_model, field.get_alias()
|
||||||
target_field = target_model.get_column_alias(target_model.Meta.pkname)
|
target_field = target_model.get_column_alias(target_model.Meta.pkname)
|
||||||
@ -86,17 +86,14 @@ class ModelTableProxy:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_column_name_for_id_extraction(
|
def get_column_name_for_id_extraction(
|
||||||
parent_model: Type["Model"],
|
parent_model: Type["Model"], reverse: bool, related: str, use_raw: bool,
|
||||||
target_model: Type["Model"],
|
|
||||||
reverse: bool,
|
|
||||||
use_raw: bool,
|
|
||||||
) -> str:
|
) -> str:
|
||||||
if reverse:
|
if reverse:
|
||||||
column_name = parent_model.Meta.pkname
|
column_name = parent_model.Meta.pkname
|
||||||
return (
|
return (
|
||||||
parent_model.get_column_alias(column_name) if use_raw else column_name
|
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
|
return column.get_alias() if use_raw else column.name
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -322,19 +319,6 @@ class ModelTableProxy:
|
|||||||
f"No relation between {item.get_name()} and {related.get_name()}"
|
f"No relation between {item.get_name()} and {related.get_name()}"
|
||||||
) # pragma nocover
|
) # 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
|
@classmethod
|
||||||
def translate_columns_to_aliases(cls, new_kwargs: Dict) -> Dict:
|
def translate_columns_to_aliases(cls, new_kwargs: Dict) -> Dict:
|
||||||
for field_name, field in cls.Meta.model_fields.items():
|
for field_name, field in cls.Meta.model_fields.items():
|
||||||
|
|||||||
@ -224,8 +224,8 @@ class NewBaseModel(
|
|||||||
def db_backend_name(cls) -> str:
|
def db_backend_name(cls) -> str:
|
||||||
return cls.Meta.database._backend._dialect.name
|
return cls.Meta.database._backend._dialect.name
|
||||||
|
|
||||||
def remove(self, name: "T") -> None:
|
def remove(self, parent: "T", name: str) -> None:
|
||||||
self._orm.remove_parent(self, name)
|
self._orm.remove_parent(self, parent, name)
|
||||||
|
|
||||||
def set_save_status(self, status: bool) -> None:
|
def set_save_status(self, status: bool) -> None:
|
||||||
object.__setattr__(self, "_orm_saved", status)
|
object.__setattr__(self, "_orm_saved", status)
|
||||||
|
|||||||
@ -138,7 +138,7 @@ class QueryClause:
|
|||||||
through_field = model_cls.Meta.model_fields[part]
|
through_field = model_cls.Meta.model_fields[part]
|
||||||
previous_model = through_field.through
|
previous_model = through_field.through
|
||||||
part2 = model_cls.resolve_relation_name(
|
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
|
manager = model_cls.Meta.alias_manager
|
||||||
table_prefix = manager.resolve_relation_join(previous_model, part2)
|
table_prefix = manager.resolve_relation_join(previous_model, part2)
|
||||||
|
|||||||
@ -123,15 +123,15 @@ class PrefetchQuery:
|
|||||||
return list_of_ids
|
return list_of_ids
|
||||||
|
|
||||||
def _extract_required_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:
|
) -> Set:
|
||||||
|
|
||||||
use_raw = parent_model.get_name() not in self.models
|
use_raw = parent_model.get_name() not in self.models
|
||||||
|
|
||||||
column_name = parent_model.get_column_name_for_id_extraction(
|
column_name = parent_model.get_column_name_for_id_extraction(
|
||||||
parent_model=parent_model,
|
parent_model=parent_model,
|
||||||
target_model=target_model,
|
|
||||||
reverse=reverse,
|
reverse=reverse,
|
||||||
|
related=related,
|
||||||
use_raw=use_raw,
|
use_raw=use_raw,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -152,7 +152,7 @@ class PrefetchQuery:
|
|||||||
related: str,
|
related: str,
|
||||||
) -> List:
|
) -> List:
|
||||||
ids = self._extract_required_ids(
|
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:
|
if ids:
|
||||||
(
|
(
|
||||||
@ -343,6 +343,7 @@ class PrefetchQuery:
|
|||||||
fields=fields,
|
fields=fields,
|
||||||
exclude_fields=exclude_fields,
|
exclude_fields=exclude_fields,
|
||||||
order_bys=None,
|
order_bys=None,
|
||||||
|
limit_raw_sql=False,
|
||||||
)
|
)
|
||||||
expr = qry.build_select_expression()
|
expr = qry.build_select_expression()
|
||||||
# print(expr.compile(compile_kwargs={"literal_binds": True}))
|
# print(expr.compile(compile_kwargs={"literal_binds": True}))
|
||||||
|
|||||||
@ -25,6 +25,7 @@ class Query:
|
|||||||
fields: Optional[Union[Dict, Set]],
|
fields: Optional[Union[Dict, Set]],
|
||||||
exclude_fields: Optional[Union[Dict, Set]],
|
exclude_fields: Optional[Union[Dict, Set]],
|
||||||
order_bys: Optional[List],
|
order_bys: Optional[List],
|
||||||
|
limit_raw_sql: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.query_offset = offset
|
self.query_offset = offset
|
||||||
self.limit_count = limit_count
|
self.limit_count = limit_count
|
||||||
@ -45,6 +46,8 @@ class Query:
|
|||||||
self.sorted_orders: OrderedDict = OrderedDict()
|
self.sorted_orders: OrderedDict = OrderedDict()
|
||||||
self._init_sorted_orders()
|
self._init_sorted_orders()
|
||||||
|
|
||||||
|
self.limit_raw_sql = limit_raw_sql
|
||||||
|
|
||||||
def _init_sorted_orders(self) -> None:
|
def _init_sorted_orders(self) -> None:
|
||||||
if self.order_columns:
|
if self.order_columns:
|
||||||
for clause in self.order_columns:
|
for clause in self.order_columns:
|
||||||
@ -62,16 +65,31 @@ class Query:
|
|||||||
if self.order_columns:
|
if self.order_columns:
|
||||||
for clause in self.order_columns:
|
for clause in self.order_columns:
|
||||||
if "__" not in clause:
|
if "__" not in clause:
|
||||||
clause = (
|
text_clause = (
|
||||||
text(f"{self.alias(clause[1:])} desc")
|
text(f"{self.alias(clause[1:])} desc")
|
||||||
if clause.startswith("-")
|
if clause.startswith("-")
|
||||||
else text(self.alias(clause))
|
else text(self.alias(clause))
|
||||||
)
|
)
|
||||||
self.sorted_orders[clause] = clause
|
self.sorted_orders[clause] = text_clause
|
||||||
else:
|
else:
|
||||||
order = text(self.prefixed_pk_name)
|
order = text(self.prefixed_pk_name)
|
||||||
self.sorted_orders[self.prefixed_pk_name] = order
|
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]]:
|
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(
|
||||||
model=self.model_cls,
|
model=self.model_cls,
|
||||||
@ -83,6 +101,9 @@ class Query:
|
|||||||
"", self.table, self_related_fields
|
"", self.table, self_related_fields
|
||||||
)
|
)
|
||||||
self.apply_order_bys_for_primary_model()
|
self.apply_order_bys_for_primary_model()
|
||||||
|
if self._pagination_query_required():
|
||||||
|
self.select_from = self._build_pagination_subquery()
|
||||||
|
else:
|
||||||
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)))
|
||||||
@ -120,6 +141,46 @@ class Query:
|
|||||||
|
|
||||||
return expr
|
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(
|
def _apply_expression_modifiers(
|
||||||
self, expr: sqlalchemy.sql.select
|
self, expr: sqlalchemy.sql.select
|
||||||
) -> sqlalchemy.sql.select:
|
) -> sqlalchemy.sql.select:
|
||||||
@ -127,6 +188,7 @@ class Query:
|
|||||||
expr = FilterQuery(filter_clauses=self.exclude_clauses, exclude=True).apply(
|
expr = FilterQuery(filter_clauses=self.exclude_clauses, exclude=True).apply(
|
||||||
expr
|
expr
|
||||||
)
|
)
|
||||||
|
if not self._pagination_query_required():
|
||||||
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(sorted_orders=self.sorted_orders).apply(expr)
|
expr = OrderQuery(sorted_orders=self.sorted_orders).apply(expr)
|
||||||
|
|||||||
@ -32,6 +32,7 @@ class QuerySet:
|
|||||||
exclude_columns: Dict = None,
|
exclude_columns: Dict = None,
|
||||||
order_bys: List = None,
|
order_bys: List = None,
|
||||||
prefetch_related: List = None,
|
prefetch_related: List = None,
|
||||||
|
limit_raw_sql: bool = False,
|
||||||
) -> 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
|
||||||
@ -43,6 +44,7 @@ class QuerySet:
|
|||||||
self._columns = columns or {}
|
self._columns = columns or {}
|
||||||
self._exclude_columns = exclude_columns or {}
|
self._exclude_columns = exclude_columns or {}
|
||||||
self.order_bys = order_bys or []
|
self.order_bys = order_bys or []
|
||||||
|
self.limit_sql_raw = limit_raw_sql
|
||||||
|
|
||||||
def __get__(
|
def __get__(
|
||||||
self,
|
self,
|
||||||
@ -123,17 +125,20 @@ class QuerySet:
|
|||||||
def table(self) -> sqlalchemy.Table:
|
def table(self) -> sqlalchemy.Table:
|
||||||
return self.model_meta.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(
|
qry = Query(
|
||||||
model_cls=self.model,
|
model_cls=self.model,
|
||||||
select_related=self._select_related,
|
select_related=self._select_related,
|
||||||
filter_clauses=self.filter_clauses,
|
filter_clauses=self.filter_clauses,
|
||||||
exclude_clauses=self.exclude_clauses,
|
exclude_clauses=self.exclude_clauses,
|
||||||
offset=self.query_offset,
|
offset=offset or self.query_offset,
|
||||||
limit_count=self.limit_count,
|
limit_count=limit or self.limit_count,
|
||||||
fields=self._columns,
|
fields=self._columns,
|
||||||
exclude_fields=self._exclude_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()
|
exp = qry.build_select_expression()
|
||||||
# print(exp.compile(compile_kwargs={"literal_binds": True}))
|
# print(exp.compile(compile_kwargs={"literal_binds": True}))
|
||||||
@ -164,6 +169,7 @@ class QuerySet:
|
|||||||
exclude_columns=self._exclude_columns,
|
exclude_columns=self._exclude_columns,
|
||||||
order_bys=self.order_bys,
|
order_bys=self.order_bys,
|
||||||
prefetch_related=self._prefetch_related,
|
prefetch_related=self._prefetch_related,
|
||||||
|
limit_raw_sql=self.limit_sql_raw,
|
||||||
)
|
)
|
||||||
|
|
||||||
def exclude(self, **kwargs: Any) -> "QuerySet": # noqa: A003
|
def exclude(self, **kwargs: Any) -> "QuerySet": # noqa: A003
|
||||||
@ -185,6 +191,7 @@ class QuerySet:
|
|||||||
exclude_columns=self._exclude_columns,
|
exclude_columns=self._exclude_columns,
|
||||||
order_bys=self.order_bys,
|
order_bys=self.order_bys,
|
||||||
prefetch_related=self._prefetch_related,
|
prefetch_related=self._prefetch_related,
|
||||||
|
limit_raw_sql=self.limit_sql_raw,
|
||||||
)
|
)
|
||||||
|
|
||||||
def prefetch_related(self, related: Union[List, str]) -> "QuerySet":
|
def prefetch_related(self, related: Union[List, str]) -> "QuerySet":
|
||||||
@ -203,6 +210,7 @@ class QuerySet:
|
|||||||
exclude_columns=self._exclude_columns,
|
exclude_columns=self._exclude_columns,
|
||||||
order_bys=self.order_bys,
|
order_bys=self.order_bys,
|
||||||
prefetch_related=related,
|
prefetch_related=related,
|
||||||
|
limit_raw_sql=self.limit_sql_raw,
|
||||||
)
|
)
|
||||||
|
|
||||||
def exclude_fields(self, columns: Union[List, str, Set, Dict]) -> "QuerySet":
|
def exclude_fields(self, columns: Union[List, str, Set, Dict]) -> "QuerySet":
|
||||||
@ -226,6 +234,7 @@ class QuerySet:
|
|||||||
exclude_columns=current_excluded,
|
exclude_columns=current_excluded,
|
||||||
order_bys=self.order_bys,
|
order_bys=self.order_bys,
|
||||||
prefetch_related=self._prefetch_related,
|
prefetch_related=self._prefetch_related,
|
||||||
|
limit_raw_sql=self.limit_sql_raw,
|
||||||
)
|
)
|
||||||
|
|
||||||
def fields(self, columns: Union[List, str, Set, Dict]) -> "QuerySet":
|
def fields(self, columns: Union[List, str, Set, Dict]) -> "QuerySet":
|
||||||
@ -249,6 +258,7 @@ class QuerySet:
|
|||||||
exclude_columns=self._exclude_columns,
|
exclude_columns=self._exclude_columns,
|
||||||
order_bys=self.order_bys,
|
order_bys=self.order_bys,
|
||||||
prefetch_related=self._prefetch_related,
|
prefetch_related=self._prefetch_related,
|
||||||
|
limit_raw_sql=self.limit_sql_raw,
|
||||||
)
|
)
|
||||||
|
|
||||||
def order_by(self, columns: Union[List, str]) -> "QuerySet":
|
def order_by(self, columns: Union[List, str]) -> "QuerySet":
|
||||||
@ -267,6 +277,7 @@ class QuerySet:
|
|||||||
exclude_columns=self._exclude_columns,
|
exclude_columns=self._exclude_columns,
|
||||||
order_bys=order_bys,
|
order_bys=order_bys,
|
||||||
prefetch_related=self._prefetch_related,
|
prefetch_related=self._prefetch_related,
|
||||||
|
limit_raw_sql=self.limit_sql_raw,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def exists(self) -> bool:
|
async def exists(self) -> bool:
|
||||||
@ -308,7 +319,8 @@ class QuerySet:
|
|||||||
)
|
)
|
||||||
return await self.database.execute(expr)
|
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__(
|
return self.__class__(
|
||||||
model_cls=self.model,
|
model_cls=self.model,
|
||||||
filter_clauses=self.filter_clauses,
|
filter_clauses=self.filter_clauses,
|
||||||
@ -320,9 +332,11 @@ class QuerySet:
|
|||||||
exclude_columns=self._exclude_columns,
|
exclude_columns=self._exclude_columns,
|
||||||
order_bys=self.order_bys,
|
order_bys=self.order_bys,
|
||||||
prefetch_related=self._prefetch_related,
|
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__(
|
return self.__class__(
|
||||||
model_cls=self.model,
|
model_cls=self.model,
|
||||||
filter_clauses=self.filter_clauses,
|
filter_clauses=self.filter_clauses,
|
||||||
@ -334,23 +348,33 @@ class QuerySet:
|
|||||||
exclude_columns=self._exclude_columns,
|
exclude_columns=self._exclude_columns,
|
||||||
order_bys=self.order_bys,
|
order_bys=self.order_bys,
|
||||||
prefetch_related=self._prefetch_related,
|
prefetch_related=self._prefetch_related,
|
||||||
|
limit_raw_sql=limit_raw_sql,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def first(self, **kwargs: Any) -> "Model":
|
async def first(self, **kwargs: Any) -> "Model":
|
||||||
if kwargs:
|
if kwargs:
|
||||||
return await self.filter(**kwargs).first()
|
return await self.filter(**kwargs).first()
|
||||||
|
|
||||||
rows = await self.limit(1).all()
|
expr = self.build_select_expression(
|
||||||
self.check_single_result_rows_count(rows)
|
limit=1, order_bys=[f"{self.model.Meta.pkname}"]
|
||||||
return rows[0] # type: ignore
|
)
|
||||||
|
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":
|
async def get(self, **kwargs: Any) -> "Model":
|
||||||
if kwargs:
|
if kwargs:
|
||||||
return await self.filter(**kwargs).get()
|
return await self.filter(**kwargs).get()
|
||||||
|
|
||||||
expr = self.build_select_expression()
|
|
||||||
if not self.filter_clauses:
|
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)
|
rows = await self.database.fetch_all(expr)
|
||||||
processed_rows = self._process_query_result_rows(rows)
|
processed_rows = self._process_query_result_rows(rows)
|
||||||
|
|||||||
@ -40,7 +40,7 @@ class AliasManager:
|
|||||||
return text(f"{name} {alias}_{name}")
|
return text(f"{name} {alias}_{name}")
|
||||||
|
|
||||||
def add_relation_type(
|
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:
|
) -> None:
|
||||||
parent_key = f"{source_model.get_name()}_{relation_name}"
|
parent_key = f"{source_model.get_name()}_{relation_name}"
|
||||||
if parent_key not in self._aliases_new:
|
if parent_key not in self._aliases_new:
|
||||||
@ -50,7 +50,7 @@ class AliasManager:
|
|||||||
related_name = to_field.related_name
|
related_name = to_field.related_name
|
||||||
if not related_name:
|
if not related_name:
|
||||||
related_name = child_model.resolve_relation_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}"
|
child_key = f"{child_model.get_name()}_{related_name}"
|
||||||
if child_key not in self._aliases_new:
|
if child_key not in self._aliases_new:
|
||||||
|
|||||||
@ -53,7 +53,7 @@ class QuerysetProxy(ormar.QuerySetProtocol):
|
|||||||
def _assign_child_to_parent(self, child: Optional["T"]) -> None:
|
def _assign_child_to_parent(self, child: Optional["T"]) -> None:
|
||||||
if child:
|
if child:
|
||||||
owner = self._owner
|
owner = self._owner
|
||||||
rel_name = owner.resolve_relation_name(owner, child)
|
rel_name = self.relation.field_name
|
||||||
setattr(owner, rel_name, child)
|
setattr(owner, rel_name, child)
|
||||||
|
|
||||||
def _register_related(self, child: Union["T", Sequence[Optional["T"]]]) -> None:
|
def _register_related(self, child: Union["T", Sequence[Optional["T"]]]) -> None:
|
||||||
|
|||||||
@ -89,11 +89,10 @@ class RelationsManager:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def remove_parent(
|
def remove_parent(
|
||||||
item: Union["NewBaseModel", Type["NewBaseModel"]], name: "Model"
|
item: Union["NewBaseModel", Type["NewBaseModel"]], parent: "Model", name: str
|
||||||
) -> None:
|
) -> None:
|
||||||
related_model = name
|
relation_name = (
|
||||||
rel_name = item.resolve_relation_name(item, related_model)
|
item.Meta.model_fields[name].related_name or item.get_name() + "s"
|
||||||
if rel_name in item._orm:
|
)
|
||||||
relation_name = item.resolve_relation_name(related_model, item)
|
item._orm.remove(name, parent)
|
||||||
item._orm.remove(rel_name, related_model)
|
parent._orm.remove(relation_name, item)
|
||||||
related_model._orm.remove(relation_name, item)
|
|
||||||
|
|||||||
@ -89,6 +89,10 @@ async def test_working_with_aliases():
|
|||||||
first_name="Son", last_name="2", born_year=1995
|
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()
|
album = await Album.objects.select_related("artist").first()
|
||||||
assert album.artist.last_name == "Mosbey"
|
assert album.artist.last_name == "Mosbey"
|
||||||
|
|
||||||
@ -99,9 +103,10 @@ async def test_working_with_aliases():
|
|||||||
assert album.name == "Aunt Robin"
|
assert album.name == "Aunt Robin"
|
||||||
|
|
||||||
artist = await Artist.objects.select_related("children").get()
|
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[0].first_name == "Son"
|
||||||
assert artist.children[1].last_name == "2"
|
assert artist.children[1].last_name == "2"
|
||||||
|
assert artist.children[2].last_name == "3"
|
||||||
|
|
||||||
await artist.update(last_name="Bundy")
|
await artist.update(last_name="Bundy")
|
||||||
await Artist.objects.filter(pk=artist.pk).update(born_year=1974)
|
await Artist.objects.filter(pk=artist.pk).update(born_year=1974)
|
||||||
|
|||||||
@ -205,11 +205,11 @@ async def test_model_removal_from_relations():
|
|||||||
album = await Album.objects.select_related("tracks").get(name="Chichi")
|
album = await Album.objects.select_related("tracks").get(name="Chichi")
|
||||||
assert track1.album == album
|
assert track1.album == album
|
||||||
|
|
||||||
track1.remove(album)
|
track1.remove(album, name="album")
|
||||||
assert track1.album is None
|
assert track1.album is None
|
||||||
assert len(album.tracks) == 2
|
assert len(album.tracks) == 2
|
||||||
|
|
||||||
track2.remove(album)
|
track2.remove(album, name="album")
|
||||||
assert track2.album is None
|
assert track2.album is None
|
||||||
assert len(album.tracks) == 1
|
assert len(album.tracks) == 1
|
||||||
|
|
||||||
|
|||||||
@ -102,6 +102,7 @@ class Car(ormar.Model):
|
|||||||
name: str = ormar.String(max_length=50)
|
name: str = ormar.String(max_length=50)
|
||||||
owner: Person = ormar.ForeignKey(Person)
|
owner: Person = ormar.ForeignKey(Person)
|
||||||
co_owner: Person = ormar.ForeignKey(Person, related_name="coowned")
|
co_owner: Person = ormar.ForeignKey(Person, related_name="coowned")
|
||||||
|
created_date: datetime.datetime = ormar.DateTime(default=datetime.datetime.now)
|
||||||
|
|
||||||
|
|
||||||
class Truck(Car):
|
class Truck(Car):
|
||||||
@ -291,11 +292,22 @@ async def test_inheritance_with_relation():
|
|||||||
).get(name="Joe")
|
).get(name="Joe")
|
||||||
assert joe_check.pk == joe.pk
|
assert joe_check.pk == joe.pk
|
||||||
assert joe_check.coowned_trucks[0] == shelby
|
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] == unicorn
|
||||||
|
assert joe_check.coowned_buses[0].created_date is not None
|
||||||
|
|
||||||
joe_check = await Person.objects.prefetch_related(
|
joe_check = (
|
||||||
["coowned_trucks", "coowned_buses"]
|
await Person.objects.exclude_fields(
|
||||||
).get(name="Joe")
|
{
|
||||||
|
"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.pk == joe.pk
|
||||||
assert joe_check.coowned_trucks[0] == shelby
|
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] == unicorn
|
||||||
|
assert joe_check.coowned_buses[0].created_date is None
|
||||||
|
|||||||
@ -58,6 +58,16 @@ class User(ormar.Model):
|
|||||||
name: str = ormar.String(max_length=100, default="")
|
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 Product(ormar.Model):
|
||||||
class Meta:
|
class Meta:
|
||||||
tablename = "product"
|
tablename = "product"
|
||||||
@ -215,8 +225,9 @@ async def test_model_get():
|
|||||||
assert lookup == user
|
assert lookup == user
|
||||||
|
|
||||||
user = await User.objects.create(name="Jane")
|
user = await User.objects.create(name="Jane")
|
||||||
|
await User.objects.create(name="Jane")
|
||||||
with pytest.raises(ormar.MultipleMatches):
|
with pytest.raises(ormar.MultipleMatches):
|
||||||
await User.objects.get()
|
await User.objects.get(name="Jane")
|
||||||
|
|
||||||
same_user = await User.objects.get(pk=user.id)
|
same_user = await User.objects.get(pk=user.id)
|
||||||
assert same_user.id == 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()
|
users = await User.objects.filter(name__endswith="igo").all()
|
||||||
assert len(users) == 2
|
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"
|
||||||
|
|||||||
@ -266,7 +266,7 @@ async def test_prefetch_related_with_select_related():
|
|||||||
await Album.objects.select_related(["tracks", "shops"])
|
await Album.objects.select_related(["tracks", "shops"])
|
||||||
.filter(name="Malibu")
|
.filter(name="Malibu")
|
||||||
.prefetch_related(["cover_pictures", "shops__division"])
|
.prefetch_related(["cover_pictures", "shops__division"])
|
||||||
.get()
|
.first()
|
||||||
)
|
)
|
||||||
|
|
||||||
assert len(album.tracks) == 0
|
assert len(album.tracks) == 0
|
||||||
|
|||||||
@ -90,14 +90,65 @@ async def test_create_primary_models():
|
|||||||
await p1.keywords.add(keyword)
|
await p1.keywords.add(keyword)
|
||||||
else:
|
else:
|
||||||
await p2.keywords.add(keyword)
|
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) == 5
|
||||||
assert len(models[0].keywords) == 2
|
assert len(models[0].keywords) == 2
|
||||||
assert len(models[1].keywords) == 3
|
assert len(models[1].keywords) == 3
|
||||||
assert len(models[2].keywords) == 0
|
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")
|
@pytest.fixture(autouse=True, scope="module")
|
||||||
def create_test_database():
|
def create_test_database():
|
||||||
|
|||||||
Reference in New Issue
Block a user