Skip to content

Commit

Permalink
Filter refactoring. Exclude and Q objects negation. Filtering by None
Browse files Browse the repository at this point in the history
  • Loading branch information
abondar committed Dec 22, 2018
1 parent 98c37cd commit c6fd94d
Show file tree
Hide file tree
Showing 18 changed files with 862 additions and 260 deletions.
8 changes: 7 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
Changelog
=========

0.11.0
------
- Added `.exclude()` method for QuerySet
- Q objects can now be negated for `NOT` query (`~Q(...)`)
- Numerous bug fixes

0.10.11
-------
- Pre-build some query & filters statically, 15-30% speed up for smaller queries.
Expand Down Expand Up @@ -152,7 +158,7 @@ Changelog
0.5.0
-----
- Added ``contains`` and other filter modifiers.
- Field kwarg ``default`` not accepts functions.
- Field kwarg ``default`` now accepts functions.

0.4.0
-----
Expand Down
13 changes: 9 additions & 4 deletions docs/query.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
Query API
=========

Be sure to check `examples <https://github.com/Zeliboba5/tortoise-orm/tree/master/examples>`_ for better understanding
Be sure to check `examples <https://github.com/tortoise/tortoise-orm/tree/master/examples>`_ for better understanding

You start your query from your model class:

Expand All @@ -14,7 +14,8 @@ You start your query from your model class:
There are several method on model itself to start query:

- ``first(*args, **kwargs)`` - create QuerySet with given filters
- ``filter(*args, **kwargs)`` - create QuerySet with given filters
- ``exclude(*args, **kwargs)`` - create QuerySet with given excluding filters
- ``all()`` - create QuerySet without filters
- ``first()`` - create QuerySet limited to one object and returning instance instead of list

Expand Down Expand Up @@ -85,7 +86,11 @@ When you need to make ``OR`` query or something a little more challenging you co
Q(id__in=[event_first.id, event_second.id]) | Q(name='3')
)
Also, Q objects support negated to generate `NOT` clause in your query

.. code-block:: python3
not_third_events = await Event.filter(~Q(name='3'))
Filtering
=========
Expand All @@ -104,7 +109,7 @@ When using ``.filter()`` method you can use number of modifiers to field names t
- ``lte`` - lower or equals than passed value
- ``lt`` - lower than passed value
- ``isnull`` - field is null
- ``not_isnull``
- ``not_isnull`` - field is not null
- ``contains`` - field contains specified substring
- ``icontains`` - case insensitive ``contains``
- ``startswith`` - if field starts with value
Expand All @@ -124,4 +129,4 @@ Sometimes it is required to fetch only certain related records. You can achieve
Prefetch('events', queryset=Event.filter(name='First'))
).first()
You can view full example here: `complex_prefetching <https://github.com/Zeliboba5/tortoise-orm/tree/master/examples/complex_prefetching.py>`_
You can view full example here: `complex_prefetching <https://github.com/tortoise/tortoise-orm/tree/master/examples/complex_prefetching.py>`_
11 changes: 5 additions & 6 deletions examples/complex_filtering.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,11 @@ async def run():

event_first = Event(name='1', tournament=tournament)
await event_first.save()
event_second = Event(name='2', tournament=second_tournament)
await event_second.save()
event_third = Event(name='3', tournament=tournament)
await event_third.save()
event_forth = Event(name='4', tournament=second_tournament)
await event_forth.save()
event_second = await Event.create(name='2', tournament=second_tournament)
await Event.create(name='3', tournament=tournament)
await Event.create(name='4', tournament=second_tournament)

await Event.filter(tournament=tournament)

team_first = Team(name='First')
await team_first.save()
Expand Down
7 changes: 1 addition & 6 deletions examples/config.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
{
"connections": {
"default": {
"engine": "tortoise.backends.sqlite",
"credentials": {
"file_path": "example.sqlite3"
}
}
"default": "sqlite://:memory:"
},
"apps": {
"models": {
Expand Down
2 changes: 1 addition & 1 deletion tortoise/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -386,4 +386,4 @@ async def do_stuff():
loop.run_until_complete(Tortoise.close_connections())


__version__ = "0.10.11"
__version__ = "0.11.0"
2 changes: 1 addition & 1 deletion tortoise/aggregation.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def _resolve_field_for_model(self, field: str, model) -> dict:
aggregation['joins'].append(join)
return aggregation

def resolve_for_model(self, model) -> dict:
def resolve(self, model) -> dict:
aggregation = self._resolve_field_for_model(self.field, model)
aggregation['joins'] = reversed(aggregation['joins'])
return aggregation
Expand Down
26 changes: 25 additions & 1 deletion tortoise/backends/base/executor.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Type # noqa

from pypika import Table
from pypika import JoinType, Table

from tortoise import fields
from tortoise.exceptions import OperationalError
from tortoise.query_utils import QueryModifier

INSERT_CACHE = {} # type: Dict[str, Tuple[list, list, str]]

Expand Down Expand Up @@ -136,6 +137,29 @@ async def _prefetch_m2m_relation(self, instance_list: list, field: str, related_
subquery._backward_relation_key.as_('_backward_relation_key'),
*[getattr(related_query_table, field).as_(field) for field in related_query.fields]
)

if related_query._q_objects:
joined_tables = [] # type: List[Table]
modifier = QueryModifier()
for node in related_query._q_objects:
modifier &= node.resolve(
model=related_query.model,
annotations=related_query._annotations,
custom_filters=related_query._custom_filters,
)

where_criterion, joins, having_criterion = modifier.get_query_modifiers()
for join in joins:
if join[0] not in joined_tables:
query = query.join(join[0], how=JoinType.left_outer).on(join[1])
joined_tables.append(join[0])

if where_criterion:
query = query.where(where_criterion)

if having_criterion:
query = query.having(having_criterion)

raw_results = await self.db.execute_query(str(query))
relations = {(e['_backward_relation_key'], e['id']) for e in raw_results}
related_object_list = [related_query.model(**e) for e in raw_results]
Expand Down
10 changes: 5 additions & 5 deletions tortoise/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -494,15 +494,15 @@ async def remove(self, *instances, using_db=None) -> None:

if len(instances) == 1:
condition = (
getattr(through_table, self.field.forward_key) == instances[0].id
& getattr(through_table, self.field.backward_key) == self.instance.id
(getattr(through_table, self.field.forward_key) == instances[0].id)
& (getattr(through_table, self.field.backward_key) == self.instance.id)
)
else:
condition = (
getattr(through_table, self.field.backward_key) == self.instance.id
& getattr(through_table, self.field.forward_key).isin([
(getattr(through_table, self.field.backward_key) == self.instance.id)
& (getattr(through_table, self.field.forward_key).isin([
i.id for i in instances
])
]))
)
query = db.query_class.from_(through_table).where(condition).delete()
await db.execute_query(str(query))
8 changes: 8 additions & 0 deletions tortoise/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,14 @@ def first(cls) -> QuerySet:
def filter(cls, *args, **kwargs) -> QuerySet:
return QuerySet(cls).filter(*args, **kwargs)

@classmethod
def exclude(cls, *args, **kwargs) -> QuerySet:
return QuerySet(cls).exclude(*args, **kwargs)

@classmethod
def annotate(cls, **kwargs) -> QuerySet:
return QuerySet(cls).annotate(**kwargs)

@classmethod
def all(cls) -> QuerySet:
return QuerySet(cls)
Expand Down
Loading

0 comments on commit c6fd94d

Please sign in to comment.