fill part of queryset docstrings

This commit is contained in:
collerek
2021-01-03 17:54:09 +01:00
parent a32a3b9d59
commit e4b4d9451d
10 changed files with 524 additions and 47 deletions

View File

@ -149,6 +149,7 @@ assert len(tracks) == 1
* `create(**kwargs): -> Model` * `create(**kwargs): -> Model`
* `get(**kwargs): -> Model` * `get(**kwargs): -> Model`
* `get_or_create(**kwargs) -> Model` * `get_or_create(**kwargs) -> Model`
* `first(): -> Model`
* `update(each: bool = False, **kwargs) -> int` * `update(each: bool = False, **kwargs) -> int`
* `update_or_create(**kwargs) -> Model` * `update_or_create(**kwargs) -> Model`
* `bulk_create(objects: List[Model]) -> None` * `bulk_create(objects: List[Model]) -> None`

View File

@ -149,6 +149,7 @@ assert len(tracks) == 1
* `create(**kwargs): -> Model` * `create(**kwargs): -> Model`
* `get(**kwargs): -> Model` * `get(**kwargs): -> Model`
* `get_or_create(**kwargs) -> Model` * `get_or_create(**kwargs) -> Model`
* `first(): -> Model`
* `update(each: bool = False, **kwargs) -> int` * `update(each: bool = False, **kwargs) -> int`
* `update_or_create(**kwargs) -> Model` * `update_or_create(**kwargs) -> Model`
* `bulk_create(objects: List[Model]) -> None` * `bulk_create(objects: List[Model]) -> None`

View File

@ -2,14 +2,55 @@ from typing import Dict
import ormar import ormar
from ormar.exceptions import ModelPersistenceError from ormar.exceptions import ModelPersistenceError
from ormar.models.mixins import AliasMixin
from ormar.models.mixins.relation_mixin import RelationMixin from ormar.models.mixins.relation_mixin import RelationMixin
class SavePrepareMixin(RelationMixin): class SavePrepareMixin(RelationMixin, AliasMixin):
""" """
Used to prepare models to be saved in database Used to prepare models to be saved in database
""" """
@classmethod
def _prepare_model_to_save(cls, new_kwargs: dict) -> dict:
"""
Combines all preparation methods before saving.
Removes primary key for if it's nullable or autoincrement pk field,
and it's set to None.
Substitute related models with their primary key values as fk column.
Populates the default values for field with default set and no value.
Translate columns into aliases (db names).
:param new_kwargs: dictionary of model that is about to be saved
:type new_kwargs: Dict[str, str]
:return: dictionary of model that is about to be saved
:rtype: Dict[str, str]
"""
new_kwargs = cls._remove_pk_from_kwargs(new_kwargs)
new_kwargs = cls.substitute_models_with_pks(new_kwargs)
new_kwargs = cls.populate_default_values(new_kwargs)
new_kwargs = cls.translate_columns_to_aliases(new_kwargs)
return new_kwargs
@classmethod
def _remove_pk_from_kwargs(cls, new_kwargs: dict) -> dict:
"""
Removes primary key for if it's nullable or autoincrement pk field,
and it's set to None.
:param new_kwargs: dictionary of model that is about to be saved
:type new_kwargs: Dict[str, str]
:return: dictionary of model that is about to be saved
:rtype: Dict[str, str]
"""
pkname = cls.Meta.pkname
pk = cls.Meta.model_fields[pkname]
if new_kwargs.get(pkname, ormar.Undefined) is None and (
pk.nullable or pk.autoincrement
):
del new_kwargs[pkname]
return new_kwargs
@classmethod @classmethod
def substitute_models_with_pks(cls, model_dict: Dict) -> Dict: # noqa CCR001 def substitute_models_with_pks(cls, model_dict: Dict) -> Dict: # noqa CCR001
""" """

View File

@ -1,5 +1,4 @@
from ormar.models.mixins import ( from ormar.models.mixins import (
AliasMixin,
ExcludableMixin, ExcludableMixin,
MergeModelMixin, MergeModelMixin,
PrefetchQueryMixin, PrefetchQueryMixin,
@ -8,7 +7,7 @@ from ormar.models.mixins import (
class ModelTableProxy( class ModelTableProxy(
PrefetchQueryMixin, MergeModelMixin, AliasMixin, SavePrepareMixin, ExcludableMixin PrefetchQueryMixin, MergeModelMixin, SavePrepareMixin, ExcludableMixin
): ):
""" """
Used to combine all mixins with different set of functionalities. Used to combine all mixins with different set of functionalities.

View File

@ -4,11 +4,23 @@ import sqlalchemy
class FilterQuery: class FilterQuery:
"""
Modifies the select query with given list of where/filter clauses.
"""
def __init__(self, filter_clauses: List, exclude: bool = False) -> None: def __init__(self, filter_clauses: List, exclude: bool = False) -> None:
self.exclude = exclude self.exclude = exclude
self.filter_clauses = filter_clauses self.filter_clauses = filter_clauses
def apply(self, expr: sqlalchemy.sql.select) -> sqlalchemy.sql.select: def apply(self, expr: sqlalchemy.sql.select) -> sqlalchemy.sql.select:
"""
Applies all filter clauses if set.
:param expr: query to modify
:type expr: sqlalchemy.sql.selectable.Select
:return: modified query
:rtype: sqlalchemy.sql.selectable.Select
"""
if self.filter_clauses: if self.filter_clauses:
if len(self.filter_clauses) == 1: if len(self.filter_clauses) == 1:
clause = self.filter_clauses[0] clause = self.filter_clauses[0]

View File

@ -4,10 +4,22 @@ import sqlalchemy
class LimitQuery: class LimitQuery:
"""
Modifies the select query with limit clause.
"""
def __init__(self, limit_count: Optional[int]) -> None: def __init__(self, limit_count: Optional[int]) -> None:
self.limit_count = limit_count self.limit_count = limit_count
def apply(self, expr: sqlalchemy.sql.select) -> sqlalchemy.sql.select: def apply(self, expr: sqlalchemy.sql.select) -> sqlalchemy.sql.select:
"""
Applies the limit clause.
:param expr: query to modify
:type expr: sqlalchemy.sql.selectable.Select
:return: modified query
:rtype: sqlalchemy.sql.selectable.Select
"""
if self.limit_count: if self.limit_count:
expr = expr.limit(self.limit_count) expr = expr.limit(self.limit_count)
return expr return expr

View File

@ -4,10 +4,22 @@ import sqlalchemy
class OffsetQuery: class OffsetQuery:
"""
Modifies the select query with offset if set
"""
def __init__(self, query_offset: Optional[int]) -> None: def __init__(self, query_offset: Optional[int]) -> None:
self.query_offset = query_offset self.query_offset = query_offset
def apply(self, expr: sqlalchemy.sql.select) -> sqlalchemy.sql.select: def apply(self, expr: sqlalchemy.sql.select) -> sqlalchemy.sql.select:
"""
Applies the offset clause.
:param expr: query to modify
:type expr: sqlalchemy.sql.selectable.Select
:return: modified query
:rtype: sqlalchemy.sql.selectable.Select
"""
if self.query_offset: if self.query_offset:
expr = expr.offset(self.query_offset) expr = expr.offset(self.query_offset)
return expr return expr

View File

@ -4,10 +4,22 @@ import sqlalchemy
class OrderQuery: class OrderQuery:
"""
Modifies the select query with given list of order_by clauses.
"""
def __init__(self, sorted_orders: Dict) -> None: def __init__(self, sorted_orders: Dict) -> None:
self.sorted_orders = sorted_orders self.sorted_orders = sorted_orders
def apply(self, expr: sqlalchemy.sql.select) -> sqlalchemy.sql.select: def apply(self, expr: sqlalchemy.sql.select) -> sqlalchemy.sql.select:
"""
Applies all order_by clauses if set.
:param expr: query to modify
:type expr: sqlalchemy.sql.selectable.Select
:return: modified query
:rtype: sqlalchemy.sql.selectable.Select
"""
if self.sorted_orders: if self.sorted_orders:
for order in list(self.sorted_orders.values()): for order in list(self.sorted_orders.values()):
if order is not None: if order is not None:

View File

@ -20,6 +20,10 @@ if TYPE_CHECKING: # pragma no cover
class QuerySet: class QuerySet:
"""
Main class to perform database queries, exposed on each model as objects attribute.
"""
def __init__( # noqa CFQ002 def __init__( # noqa CFQ002
self, self,
model_cls: Type["Model"] = None, model_cls: Type["Model"] = None,
@ -57,12 +61,24 @@ class QuerySet:
@property @property
def model_meta(self) -> "ModelMeta": def model_meta(self) -> "ModelMeta":
"""
Shortcut to model class Meta set on QuerySet model.
:return: Meta class of the model
:rtype: model Meta class
"""
if not self.model_cls: # pragma nocover if not self.model_cls: # pragma nocover
raise ValueError("Model class of QuerySet is not initialized") raise ValueError("Model class of QuerySet is not initialized")
return self.model_cls.Meta return self.model_cls.Meta
@property @property
def model(self) -> Type["Model"]: def model(self) -> Type["Model"]:
"""
Shortcut to model class set on QuerySet.
:return: model class
:rtype: Type[Model]
"""
if not self.model_cls: # pragma nocover if not self.model_cls: # pragma nocover
raise ValueError("Model class of QuerySet is not initialized") raise ValueError("Model class of QuerySet is not initialized")
return self.model_cls return self.model_cls
@ -70,6 +86,16 @@ class QuerySet:
async def _prefetch_related_models( async def _prefetch_related_models(
self, models: Sequence[Optional["Model"]], rows: List self, models: Sequence[Optional["Model"]], rows: List
) -> Sequence[Optional["Model"]]: ) -> Sequence[Optional["Model"]]:
"""
Performs prefetch query for selected models names.
:param models: list of already parsed main Models from main query
:type models: List[Model]
:param rows: database rows from main query
:type rows: List[sqlalchemy.engine.result.RowProxy]
:return: list of models with prefetch models populated
:rtype: List[Model]
"""
query = PrefetchQuery( query = PrefetchQuery(
model_cls=self.model, model_cls=self.model,
fields=self._columns, fields=self._columns,
@ -81,6 +107,14 @@ class QuerySet:
return await query.prefetch_related(models=models, rows=rows) # type: ignore return await query.prefetch_related(models=models, rows=rows) # type: ignore
def _process_query_result_rows(self, rows: List) -> Sequence[Optional["Model"]]: def _process_query_result_rows(self, rows: List) -> Sequence[Optional["Model"]]:
"""
Process database rows and initialize ormar Model from each of the rows.
:param rows: list of database rows from query result
:type rows: List[sqlalchemy.engine.result.RowProxy]
:return: list of models
:rtype: List[Model]
"""
result_rows = [ result_rows = [
self.model.from_row( self.model.from_row(
row=row, row=row,
@ -94,24 +128,14 @@ class QuerySet:
return self.model.merge_instances_list(result_rows) # type: ignore return self.model.merge_instances_list(result_rows) # type: ignore
return result_rows return result_rows
def _prepare_model_to_save(self, new_kwargs: dict) -> dict:
new_kwargs = self._remove_pk_from_kwargs(new_kwargs)
new_kwargs = self.model.substitute_models_with_pks(new_kwargs)
new_kwargs = self.model.populate_default_values(new_kwargs)
new_kwargs = self.model.translate_columns_to_aliases(new_kwargs)
return new_kwargs
def _remove_pk_from_kwargs(self, new_kwargs: dict) -> dict:
pkname = self.model_meta.pkname
pk = self.model_meta.model_fields[pkname]
if new_kwargs.get(pkname, ormar.Undefined) is None and (
pk.nullable or pk.autoincrement
):
del new_kwargs[pkname]
return new_kwargs
@staticmethod @staticmethod
def check_single_result_rows_count(rows: Sequence[Optional["Model"]]) -> None: def check_single_result_rows_count(rows: Sequence[Optional["Model"]]) -> None:
"""
Verifies if the result has one and only one row.
:param rows: one element list of Models
:type rows: List[Model]
"""
if not rows or rows[0] is None: if not rows or rows[0] is None:
raise NoMatch() raise NoMatch()
if len(rows) > 1: if len(rows) > 1:
@ -119,15 +143,40 @@ class QuerySet:
@property @property
def database(self) -> databases.Database: def database(self) -> databases.Database:
"""
Shortcut to models database from Meta class.
:return: database
:rtype: databases.Database
"""
return self.model_meta.database return self.model_meta.database
@property @property
def table(self) -> sqlalchemy.Table: def table(self) -> sqlalchemy.Table:
"""
Shortcut to models table from Meta class.
:return: database table
:rtype: sqlalchemy.Table
"""
return self.model_meta.table return self.model_meta.table
def build_select_expression( def build_select_expression(
self, limit: int = None, offset: int = None, order_bys: List = None, self, limit: int = None, offset: int = None, order_bys: List = None,
) -> sqlalchemy.sql.select: ) -> sqlalchemy.sql.select:
"""
Constructs the actual database query used in the QuerySet.
If any of the params is not passed the QuerySet own value is used.
:param limit: number to limit the query
:type limit: int
:param offset: number to offset by
:type offset: int
:param order_bys: list of order-by fields names
:type order_bys: List
:return: built sqlalchemy select expression
:rtype: sqlalchemy.sql.selectable.Select
"""
qry = Query( qry = Query(
model_cls=self.model, model_cls=self.model,
select_related=self._select_related, select_related=self._select_related,
@ -145,6 +194,33 @@ class QuerySet:
return exp return exp
def filter(self, _exclude: bool = False, **kwargs: Any) -> "QuerySet": # noqa: A003 def filter(self, _exclude: bool = False, **kwargs: Any) -> "QuerySet": # noqa: A003
"""
Allows you to filter by any `Model` attribute/field
as well as to fetch instances, with a filter across an FK relationship.
You can use special filter suffix to change the filter operands:
* exact - like `album__name__exact='Malibu'` (exact match)
* iexact - like `album__name__iexact='malibu'` (exact match case insensitive)
* contains - like `album__name__contains='Mal'` (sql like)
* icontains - like `album__name__icontains='mal'` (sql like case insensitive)
* in - like `album__name__in=['Malibu', 'Barclay']` (sql in)
* gt - like `position__gt=3` (sql >)
* gte - like `position__gte=3` (sql >=)
* lt - like `position__lt=3` (sql <)
* lte - like `position__lte=3` (sql <=)
* startswith - like `album__name__startswith='Mal'` (exact start match)
* istartswith - like `album__name__istartswith='mal'` (case insensitive)
* endswith - like `album__name__endswith='ibu'` (exact end match)
* iendswith - like `album__name__iendswith='IBU'` (case insensitive)
:param _exclude: flag if it should be exclude or filter
:type _exclude: bool
:param kwargs: fields names and proper value types
:type kwargs: Any
:return: filtered QuerySet
:rtype: QuerySet
"""
qryclause = QueryClause( qryclause = QueryClause(
model_cls=self.model, model_cls=self.model,
select_related=self._select_related, select_related=self._select_related,
@ -173,9 +249,43 @@ class QuerySet:
) )
def exclude(self, **kwargs: Any) -> "QuerySet": # noqa: A003 def exclude(self, **kwargs: Any) -> "QuerySet": # noqa: A003
"""
Works exactly the same as filter and all modifiers (suffixes) are the same,
but returns a *not* condition.
So if you use `filter(name='John')` which is `where name = 'John'` in SQL,
the `exclude(name='John')` equals to `where name <> 'John'`
Note that all conditions are joined so if you pass multiple values it
becomes a union of conditions.
`exclude(name='John', age>=35)` will become
`where not (name='John' and age>=35)`
:param kwargs: fields names and proper value types
:type kwargs: Any
:return: filtered QuerySet
:rtype: QuerySet
"""
return self.filter(_exclude=True, **kwargs) return self.filter(_exclude=True, **kwargs)
def select_related(self, related: Union[List, str]) -> "QuerySet": def select_related(self, related: Union[List, str]) -> "QuerySet":
"""
Allows to prefetch related models during the same query.
**With `select_related` always only one query is run against the database**,
meaning that one (sometimes complicated) join is generated and later nested
models are processed in python.
To fetch related model use `ForeignKey` names.
To chain related `Models` relation use double underscores between names.
:param related: list of relation field names, can be linked by '__' to nest
:type related: Union[List, str]
:return: QuerySet
:rtype: QuerySet
"""
if not isinstance(related, list): if not isinstance(related, list):
related = [related] related = [related]
@ -195,6 +305,23 @@ class QuerySet:
) )
def prefetch_related(self, related: Union[List, str]) -> "QuerySet": def prefetch_related(self, related: Union[List, str]) -> "QuerySet":
"""
Allows to prefetch related models during query - but opposite to
`select_related` each subsequent model is fetched in a separate database query.
**With `prefetch_related` always one query per Model is run against the
database**, meaning that you will have multiple queries executed one
after another.
To fetch related model use `ForeignKey` names.
To chain related `Models` relation use double underscores between names.
:param related: list of relation field names, can be linked by '__' to nest
:type related: Union[List, str]
:return: QuerySet
:rtype: QuerySet
"""
if not isinstance(related, list): if not isinstance(related, list):
related = [related] related = [related]
@ -213,31 +340,49 @@ class QuerySet:
limit_raw_sql=self.limit_sql_raw, limit_raw_sql=self.limit_sql_raw,
) )
def exclude_fields(self, columns: Union[List, str, Set, Dict]) -> "QuerySet":
if isinstance(columns, str):
columns = [columns]
current_excluded = self._exclude_columns
if not isinstance(columns, dict):
current_excluded = update_dict_from_list(current_excluded, columns)
else:
current_excluded = update(current_excluded, columns)
return self.__class__(
model_cls=self.model,
filter_clauses=self.filter_clauses,
exclude_clauses=self.exclude_clauses,
select_related=self._select_related,
limit_count=self.limit_count,
offset=self.query_offset,
columns=self._columns,
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": def fields(self, columns: Union[List, str, Set, Dict]) -> "QuerySet":
"""
With `fields()` you can select subset of model columns to limit the data load.
Note that `fields()` and `exclude_fields()` works both for main models
(on normal queries like `get`, `all` etc.)
as well as `select_related` and `prefetch_related`
models (with nested notation).
You can select specified fields by passing a `str, List[str], Set[str] or
dict` with nested definition.
To include related models use notation
`{related_name}__{column}[__{optional_next} etc.]`.
`fields()` can be called several times, building up the columns to select.
If you include related models into `select_related()` call but you won't specify
columns for those models in fields - implies a list of all fields for
those nested models.
Mandatory fields cannot be excluded as it will raise `ValidationError`,
to exclude a field it has to be nullable.
Pk column cannot be excluded - it's always auto added even if
not explicitly included.
You can also pass fields to include as dictionary or set.
To mark a field as included in a dictionary use it's name as key
and ellipsis as value.
To traverse nested models use nested dictionaries.
To include fields at last level instead of nested dictionary a set can be used.
To include whole nested model specify model related field name and ellipsis.
:param columns: columns to include
:type columns: Union[List, str, Set, Dict]
:return: QuerySet
:rtype: QuerySet
"""
if isinstance(columns, str): if isinstance(columns, str):
columns = [columns] columns = [columns]
@ -261,7 +406,88 @@ class QuerySet:
limit_raw_sql=self.limit_sql_raw, limit_raw_sql=self.limit_sql_raw,
) )
def exclude_fields(self, columns: Union[List, str, Set, Dict]) -> "QuerySet":
"""
With `exclude_fields()` you can select subset of model columns that will
be excluded to limit the data load.
It's the opposite of `fields()` method so check documentation above
to see what options are available.
Especially check above how you can pass also nested dictionaries
and sets as a mask to exclude fields from whole hierarchy.
Note that `fields()` and `exclude_fields()` works both for main models
(on normal queries like `get`, `all` etc.)
as well as `select_related` and `prefetch_related` models
(with nested notation).
Mandatory fields cannot be excluded as it will raise `ValidationError`,
to exclude a field it has to be nullable.
Pk column cannot be excluded - it's always auto added even
if explicitly excluded.
:param columns: columns to exclude
:type columns: Union[List, str, Set, Dict]
:return: QuerySet
:rtype: QuerySet
"""
if isinstance(columns, str):
columns = [columns]
current_excluded = self._exclude_columns
if not isinstance(columns, dict):
current_excluded = update_dict_from_list(current_excluded, columns)
else:
current_excluded = update(current_excluded, columns)
return self.__class__(
model_cls=self.model,
filter_clauses=self.filter_clauses,
exclude_clauses=self.exclude_clauses,
select_related=self._select_related,
limit_count=self.limit_count,
offset=self.query_offset,
columns=self._columns,
exclude_columns=current_excluded,
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": def order_by(self, columns: Union[List, str]) -> "QuerySet":
"""
With `order_by()` you can order the results from database based on your
choice of fields.
You can provide a string with field name or list of strings with fields names.
Ordering in sql will be applied in order of names you provide in order_by.
By default if you do not provide ordering `ormar` explicitly orders by
all primary keys
If you are sorting by nested models that causes that the result rows are
unsorted by the main model `ormar` will combine those children rows into
one main model.
The main model will never duplicate in the result
To order by main model field just provide a field name
To sort on nested models separate field names with dunder '__'.
You can sort this way across all relation types -> `ForeignKey`,
reverse virtual FK and `ManyToMany` fields.
To sort in descending order provide a hyphen in front of the field name
:param columns: columns by which models should be sorted
:type columns: Union[List, str]
:return: QuerySet
:rtype: QuerySet
"""
if not isinstance(columns, list): if not isinstance(columns, list):
columns = [columns] columns = [columns]
@ -281,16 +507,43 @@ class QuerySet:
) )
async def exists(self) -> bool: async def exists(self) -> bool:
"""
Returns a bool value to confirm if there are rows matching the given criteria
(applied with `filter` and `exclude` if set).
:return: result of the check
:rtype: bool
"""
expr = self.build_select_expression() expr = self.build_select_expression()
expr = sqlalchemy.exists(expr).select() expr = sqlalchemy.exists(expr).select()
return await self.database.fetch_val(expr) return await self.database.fetch_val(expr)
async def count(self) -> int: async def count(self) -> int:
"""
Returns number of rows matching the given criteria
(applied with `filter` and `exclude` if set before).
:return: number of rows
:rtype: int
"""
expr = self.build_select_expression().alias("subquery_for_count") expr = self.build_select_expression().alias("subquery_for_count")
expr = sqlalchemy.func.count().select().select_from(expr) expr = sqlalchemy.func.count().select().select_from(expr)
return await self.database.fetch_val(expr) return await self.database.fetch_val(expr)
async def update(self, each: bool = False, **kwargs: Any) -> int: async def update(self, each: bool = False, **kwargs: Any) -> int:
"""
Updates the model table after applying the filters from kwargs.
You have to either pass a filter to narrow down a query or explicitly pass
each=True flag to affect whole table.
:param each: flag if whole table should be affected if no filter is passed
:type each: bool
:param kwargs: fields names and proper value types
:type kwargs: Any
:return: number of updated rows
:rtype: int
"""
self_fields = self.model.extract_db_own_fields().union( self_fields = self.model.extract_db_own_fields().union(
self.model.extract_related_names() self.model.extract_related_names()
) )
@ -307,6 +560,19 @@ class QuerySet:
return await self.database.execute(expr) return await self.database.execute(expr)
async def delete(self, each: bool = False, **kwargs: Any) -> int: async def delete(self, each: bool = False, **kwargs: Any) -> int:
"""
Deletes from the model table after applying the filters from kwargs.
You have to either pass a filter to narrow down a query or explicitly pass
each=True flag to affect whole table.
:param each: flag if whole table should be affected if no filter is passed
:type each: bool
:param kwargs: fields names and proper value types
:type kwargs: Any
:return: number of deleted rows
:rtype:int
"""
if kwargs: if kwargs:
return await self.filter(**kwargs).delete() return await self.filter(**kwargs).delete()
if not each and not self.filter_clauses: if not each and not self.filter_clauses:
@ -320,6 +586,19 @@ class QuerySet:
return await self.database.execute(expr) return await self.database.execute(expr)
def limit(self, limit_count: int, limit_raw_sql: bool = None) -> "QuerySet": def limit(self, limit_count: int, limit_raw_sql: bool = None) -> "QuerySet":
"""
You can limit the results to desired number of parent models.
To limit the actual number of database query rows instead of number of main
models use the `limit_raw_sql` parameter flag, and set it to `True`.
:param limit_raw_sql: flag if raw sql should be limited
:type limit_raw_sql: bool
:param limit_count: number of models to limit
:type limit_count: int
:return: QuerySet
:rtype: QuerySet
"""
limit_raw_sql = self.limit_sql_raw if limit_raw_sql is None else limit_raw_sql 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,
@ -336,6 +615,19 @@ class QuerySet:
) )
def offset(self, offset: int, limit_raw_sql: bool = None) -> "QuerySet": def offset(self, offset: int, limit_raw_sql: bool = None) -> "QuerySet":
"""
You can also offset the results by desired number of main models.
To offset the actual number of database query rows instead of number of main
models use the `limit_raw_sql` parameter flag, and set it to `True`.
:param limit_raw_sql: flag if raw sql should be offset
:type limit_raw_sql: bool
:param offset: numbers of models to offset
:type offset: int
:return: QuerySet
:rtype: QuerySet
"""
limit_raw_sql = self.limit_sql_raw if limit_raw_sql is None else limit_raw_sql 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,
@ -352,6 +644,16 @@ class QuerySet:
) )
async def first(self, **kwargs: Any) -> "Model": async def first(self, **kwargs: Any) -> "Model":
"""
Gets the first row from the db ordered by primary key column ascending.
:raises: NoMatch if no rows are returned
:raises: MultipleMatches if more than 1 row is returned.
:param kwargs: fields names and proper value types
:type kwargs: Any
:return: returned model
:rtype: Model
"""
if kwargs: if kwargs:
return await self.filter(**kwargs).first() return await self.filter(**kwargs).first()
@ -366,6 +668,20 @@ class QuerySet:
return processed_rows[0] # type: ignore return processed_rows[0] # type: ignore
async def get(self, **kwargs: Any) -> "Model": async def get(self, **kwargs: Any) -> "Model":
"""
Get's the first row from the db meeting the criteria set by kwargs.
If no criteria set it will return the last row in db sorted by pk.
Passing a criteria is actually calling filter(**kwargs) method described below.
:raises: NoMatch if no rows are returned
:raises: MultipleMatches if more than 1 row is returned.
:param kwargs: fields names and proper value types
:type kwargs: Any
:return: returned model
:rtype: Model
"""
if kwargs: if kwargs:
return await self.filter(**kwargs).get() return await self.filter(**kwargs).get()
@ -384,12 +700,32 @@ class QuerySet:
return processed_rows[0] # type: ignore return processed_rows[0] # type: ignore
async def get_or_create(self, **kwargs: Any) -> "Model": async def get_or_create(self, **kwargs: Any) -> "Model":
"""
Combination of create and get methods.
Tries to get a row meeting the criteria fro kwargs
and if `NoMatch` exception is raised
it creates a new one with given kwargs.
:param kwargs: fields names and proper value types
:type kwargs: Any
:return: returned or created Model
:rtype: Model
"""
try: try:
return await self.get(**kwargs) return await self.get(**kwargs)
except NoMatch: except NoMatch:
return await self.create(**kwargs) return await self.create(**kwargs)
async def update_or_create(self, **kwargs: Any) -> "Model": async def update_or_create(self, **kwargs: Any) -> "Model":
"""
Updates the model, or in case there is no match in database creates a new one.
:param kwargs: fields names and proper value types
:type kwargs: Any
:return: updated or created model
:rtype: Model
"""
pk_name = self.model_meta.pkname pk_name = self.model_meta.pkname
if "pk" in kwargs: if "pk" in kwargs:
kwargs[pk_name] = kwargs.pop("pk") kwargs[pk_name] = kwargs.pop("pk")
@ -399,6 +735,18 @@ class QuerySet:
return await model.update(**kwargs) return await model.update(**kwargs)
async def all(self, **kwargs: Any) -> Sequence[Optional["Model"]]: # noqa: A003 async def all(self, **kwargs: Any) -> Sequence[Optional["Model"]]: # noqa: A003
"""
Returns all rows from a database for given model for set filter options.
Passing kwargs is a shortcut and equals to calling `filter(**kwrags).all()`.
If there are no rows meeting the criteria an empty list is returned.
:param kwargs: fields names and proper value types
:type kwargs: Any
:return: list of returned models
:rtype: List[Model]
"""
if kwargs: if kwargs:
return await self.filter(**kwargs).all() return await self.filter(**kwargs).all()
@ -411,9 +759,19 @@ class QuerySet:
return result_rows return result_rows
async def create(self, **kwargs: Any) -> "Model": async def create(self, **kwargs: Any) -> "Model":
"""
Creates the model instance, saves it in a database and returns the updates model
(with pk populated if not passed and autoincrement is set).
The allowed kwargs are `Model` fields names and proper value types.
:param kwargs: fields names and proper value types
:type kwargs: Any
:return: created model
:rtype: Model
"""
new_kwargs = dict(**kwargs) new_kwargs = dict(**kwargs)
new_kwargs = self._prepare_model_to_save(new_kwargs) new_kwargs = self.model._prepare_model_to_save(new_kwargs)
expr = self.table.insert() expr = self.table.insert()
expr = expr.values(**new_kwargs) expr = expr.values(**new_kwargs)
@ -444,10 +802,22 @@ class QuerySet:
return instance return instance
async def bulk_create(self, objects: List["Model"]) -> None: async def bulk_create(self, objects: List["Model"]) -> None:
"""
Performs a bulk update in one database session to speed up the process.
Allows you to create multiple objects at once.
A valid list of `Model` objects needs to be passed.
Bulk operations do not send signals.
:param objects: list of ormar models already initialized and ready to save.
:type objects: List[Model]
"""
ready_objects = [] ready_objects = []
for objt in objects: for objt in objects:
new_kwargs = objt.dict() new_kwargs = objt.dict()
new_kwargs = self._prepare_model_to_save(new_kwargs) new_kwargs = objt._prepare_model_to_save(new_kwargs)
ready_objects.append(new_kwargs) ready_objects.append(new_kwargs)
expr = self.table.insert() expr = self.table.insert()
@ -459,6 +829,23 @@ class QuerySet:
async def bulk_update( # noqa: CCR001 async def bulk_update( # noqa: CCR001
self, objects: List["Model"], columns: List[str] = None self, objects: List["Model"], columns: List[str] = None
) -> None: ) -> None:
"""
Performs bulk update in one database session to speed up the process.
Allows to update multiple instance at once.
All `Models` passed need to have primary key column populated.
You can also select which fields to update by passing `columns` list
as a list of string names.
Bulk operations do not send signals.
:param objects: list of ormar models
:type objects: List[Model]
:param columns: list of columns to update
:type columns: List[str]
"""
ready_objects = [] ready_objects = []
pk_name = self.model_meta.pkname pk_name = self.model_meta.pkname
if not columns: if not columns:

View File

@ -376,7 +376,7 @@ class QuerysetProxy(ormar.QuerySetProtocol):
Actual call delegated to QuerySet. Actual call delegated to QuerySet.
:param related: list of relation field names, can be linked by '__' to nest :param related: list of relation field names, can be linked by '__' to nest
:type related: str :type related: Union[List, str]
:return: QuerysetProxy :return: QuerysetProxy
:rtype: QuerysetProxy :rtype: QuerysetProxy
""" """
@ -399,7 +399,7 @@ class QuerysetProxy(ormar.QuerySetProtocol):
Actual call delegated to QuerySet. Actual call delegated to QuerySet.
:param related: list of relation field names, can be linked by '__' to nest :param related: list of relation field names, can be linked by '__' to nest
:type related: str :type related: Union[List, str]
:return: QuerysetProxy :return: QuerysetProxy
:rtype: QuerysetProxy :rtype: QuerysetProxy
""" """