diff --git a/docs/models/inheritance.md b/docs/models/inheritance.md index dc39dec..cf22fb7 100644 --- a/docs/models/inheritance.md +++ b/docs/models/inheritance.md @@ -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. + + diff --git a/docs/releases.md b/docs/releases.md index 44f09c9..0e78de7 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -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]) diff --git a/ormar/models/helpers/relations.py b/ormar/models/helpers/relations.py index b98243d..7c5461c 100644 --- a/ormar/models/helpers/relations.py +++ b/ormar/models/helpers/relations.py @@ -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: diff --git a/ormar/models/modelproxy.py b/ormar/models/modelproxy.py index 5ecea9a..1be9625 100644 --- a/ormar/models/modelproxy.py +++ b/ormar/models/modelproxy.py @@ -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(): diff --git a/ormar/models/newbasemodel.py b/ormar/models/newbasemodel.py index 569ce98..3fbe614 100644 --- a/ormar/models/newbasemodel.py +++ b/ormar/models/newbasemodel.py @@ -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) diff --git a/ormar/queryset/clause.py b/ormar/queryset/clause.py index 6212b8d..e92df41 100644 --- a/ormar/queryset/clause.py +++ b/ormar/queryset/clause.py @@ -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) diff --git a/ormar/queryset/prefetch_query.py b/ormar/queryset/prefetch_query.py index 2e857a1..8ec9051 100644 --- a/ormar/queryset/prefetch_query.py +++ b/ormar/queryset/prefetch_query.py @@ -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})) diff --git a/ormar/queryset/query.py b/ormar/queryset/query.py index dc08b99..b8bee37 100644 --- a/ormar/queryset/query.py +++ b/ormar/queryset/query.py @@ -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 diff --git a/ormar/queryset/queryset.py b/ormar/queryset/queryset.py index 63bbf1f..68448a4 100644 --- a/ormar/queryset/queryset.py +++ b/ormar/queryset/queryset.py @@ -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) diff --git a/ormar/relations/alias_manager.py b/ormar/relations/alias_manager.py index 5ab750a..46c68c9 100644 --- a/ormar/relations/alias_manager.py +++ b/ormar/relations/alias_manager.py @@ -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: diff --git a/ormar/relations/querysetproxy.py b/ormar/relations/querysetproxy.py index 1bbfdf2..10c6f2d 100644 --- a/ormar/relations/querysetproxy.py +++ b/ormar/relations/querysetproxy.py @@ -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: diff --git a/ormar/relations/relation_manager.py b/ormar/relations/relation_manager.py index 12f175d..211068d 100644 --- a/ormar/relations/relation_manager.py +++ b/ormar/relations/relation_manager.py @@ -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) diff --git a/tests/test_aliases.py b/tests/test_aliases.py index df472cc..239c182 100644 --- a/tests/test_aliases.py +++ b/tests/test_aliases.py @@ -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) diff --git a/tests/test_foreign_keys.py b/tests/test_foreign_keys.py index c4b9a4c..dbcf87f 100644 --- a/tests/test_foreign_keys.py +++ b/tests/test_foreign_keys.py @@ -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 diff --git a/tests/test_inheritance_concrete.py b/tests/test_inheritance_concrete.py index a8031c7..cde626b 100644 --- a/tests/test_inheritance_concrete.py +++ b/tests/test_inheritance_concrete.py @@ -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 diff --git a/tests/test_models.py b/tests/test_models.py index 24a81ad..0601f4d 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -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" diff --git a/tests/test_prefetch_related.py b/tests/test_prefetch_related.py index d7ccea9..9347c3a 100644 --- a/tests/test_prefetch_related.py +++ b/tests/test_prefetch_related.py @@ -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 diff --git a/tests/test_select_related_with_limit.py b/tests/test_select_related_with_limit.py index 7fd801e..df20930 100644 --- a/tests/test_select_related_with_limit.py +++ b/tests/test_select_related_with_limit.py @@ -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():