add default exceptions to relations, test one argument, test querysetproxy, test deeply nested

This commit is contained in:
collerek
2021-03-09 10:13:51 +01:00
parent 472c8368e4
commit 0ea9b0952c
11 changed files with 223 additions and 22 deletions

View File

@ -3,6 +3,9 @@ checks:
method-complexity: method-complexity:
config: config:
threshold: 8 threshold: 8
argument-count:
config:
threshold: 6
file-lines: file-lines:
config: config:
threshold: 500 threshold: 500

View File

@ -437,8 +437,8 @@ metadata.drop_all(engine)
* `bulk_update(objects: List[Model], columns: List[str] = None) -> None` * `bulk_update(objects: List[Model], columns: List[str] = None) -> None`
* `delete(each: bool = False, **kwargs) -> int` * `delete(each: bool = False, **kwargs) -> int`
* `all(**kwargs) -> List[Optional[Model]]` * `all(**kwargs) -> List[Optional[Model]]`
* `filter(**kwargs) -> QuerySet` * `filter(*args, **kwargs) -> QuerySet`
* `exclude(**kwargs) -> QuerySet` * `exclude(*args, **kwargs) -> QuerySet`
* `select_related(related: Union[List, str]) -> QuerySet` * `select_related(related: Union[List, str]) -> QuerySet`
* `prefetch_related(related: Union[List, str]) -> QuerySet` * `prefetch_related(related: Union[List, str]) -> QuerySet`
* `limit(limit_count: int) -> QuerySet` * `limit(limit_count: int) -> QuerySet`
@ -453,7 +453,7 @@ metadata.drop_all(engine)
#### Relation types #### Relation types
* One to many - with `ForeignKey(to: Model)` * One to many - with `ForeignKey(to: Model)`
* Many to many - with `ManyToMany(to: Model, through: Model)` * Many to many - with `ManyToMany(to: Model, Optional[through]: Model)`
#### Model fields types #### Model fields types
@ -491,8 +491,8 @@ The following keyword arguments are supported on all field types.
All fields are required unless one of the following is set: All fields are required unless one of the following is set:
* `nullable` - Creates a nullable column. Sets the default to `None`. * `nullable` - Creates a nullable column. Sets the default to `None`.
* `default` - Set a default value for the field. * `default` - Set a default value for the field. **Not available for relation fields**
* `server_default` - Set a default value for the field on server side (like sqlalchemy's `func.now()`). * `server_default` - Set a default value for the field on server side (like sqlalchemy's `func.now()`). **Not available for relation fields**
* `primary key` with `autoincrement` - When a column is set to primary key and autoincrement is set on this column. * `primary key` with `autoincrement` - When a column is set to primary key and autoincrement is set on this column.
Autoincrement is set by default on int primary keys. Autoincrement is set by default on int primary keys.
* `pydantic_only` - Field is available only as normal pydantic field, not stored in the database. * `pydantic_only` - Field is available only as normal pydantic field, not stored in the database.

View File

@ -437,8 +437,8 @@ metadata.drop_all(engine)
* `bulk_update(objects: List[Model], columns: List[str] = None) -> None` * `bulk_update(objects: List[Model], columns: List[str] = None) -> None`
* `delete(each: bool = False, **kwargs) -> int` * `delete(each: bool = False, **kwargs) -> int`
* `all(**kwargs) -> List[Optional[Model]]` * `all(**kwargs) -> List[Optional[Model]]`
* `filter(**kwargs) -> QuerySet` * `filter(*args, **kwargs) -> QuerySet`
* `exclude(**kwargs) -> QuerySet` * `exclude(*args, **kwargs) -> QuerySet`
* `select_related(related: Union[List, str]) -> QuerySet` * `select_related(related: Union[List, str]) -> QuerySet`
* `prefetch_related(related: Union[List, str]) -> QuerySet` * `prefetch_related(related: Union[List, str]) -> QuerySet`
* `limit(limit_count: int) -> QuerySet` * `limit(limit_count: int) -> QuerySet`
@ -453,7 +453,7 @@ metadata.drop_all(engine)
#### Relation types #### Relation types
* One to many - with `ForeignKey(to: Model)` * One to many - with `ForeignKey(to: Model)`
* Many to many - with `ManyToMany(to: Model, through: Model)` * Many to many - with `ManyToMany(to: Model, Optional[through]: Model)`
#### Model fields types #### Model fields types
@ -491,8 +491,8 @@ The following keyword arguments are supported on all field types.
All fields are required unless one of the following is set: All fields are required unless one of the following is set:
* `nullable` - Creates a nullable column. Sets the default to `None`. * `nullable` - Creates a nullable column. Sets the default to `None`.
* `default` - Set a default value for the field. * `default` - Set a default value for the field. **Not available for relation fields**
* `server_default` - Set a default value for the field on server side (like sqlalchemy's `func.now()`). * `server_default` - Set a default value for the field on server side (like sqlalchemy's `func.now()`). **Not available for relation fields**
* `primary key` with `autoincrement` - When a column is set to primary key and autoincrement is set on this column. * `primary key` with `autoincrement` - When a column is set to primary key and autoincrement is set on this column.
Autoincrement is set by default on int primary keys. Autoincrement is set by default on int primary keys.
* `pydantic_only` - Field is available only as normal pydantic field, not stored in the database. * `pydantic_only` - Field is available only as normal pydantic field, not stored in the database.

View File

@ -288,6 +288,37 @@ books = (
) )
``` ```
If you want or need to you can nest deeper conditions as deep as you want, in example to
acheive a query like this:
sql:
```
WHERE ( ( ( books.year > 1960 OR books.year < 1940 )
AND authors.name = 'J.R.R. Tolkien' ) OR
( books.year < 2000 AND authors.name = 'Andrzej Sapkowski' ) )
```
You can construct a query as follows:
```python
books = (
await Book.objects.select_related("author")
.filter(
ormar.or_(
ormar.and_(
ormar.or_(year__gt=1960, year__lt=1940),
author__name="J.R.R. Tolkien",
),
ormar.and_(year__lt=2000, author__name="Andrzej Sapkowski"),
)
)
.all()
)
assert len(books) == 3
assert books[0].title == "The Hobbit"
assert books[1].title == "The Silmarillion"
assert books[2].title == "The Witcher"
```
By now you should already have an idea how `ormar.or_` and `ormar.and_` works. By now you should already have an idea how `ormar.or_` and `ormar.and_` works.
Of course, you could chain them in any other methods of queryset, so in example a perfectly Of course, you could chain them in any other methods of queryset, so in example a perfectly
valid query can look like follows: valid query can look like follows:
@ -310,9 +341,48 @@ assert books[0].title == "The Witcher"
!!!note !!!note
Note that you cannot provide the same keyword argument several times so queries like `filter(ormar.or_(name='Jack', name='John'))` are not allowed. If you want to check the same Note that you cannot provide the same keyword argument several times so queries like `filter(ormar.or_(name='Jack', name='John'))` are not allowed. If you want to check the same
column for several values simply use `in` operator: `filter(name__in=['Jack','John'])`. column for several values simply use `in` operator: `filter(name__in=['Jack','John'])`.
Note that also that technically you can still do `filter(ormar.or_(name='Jack', name__exact='John'))`
but it's not recommended. The different operators can be used as long as they do not If you pass only one parameter to `or_` or `and_` functions it's simply wrapped in parenthesis and
repeat so `filter(ormar.or_(year__lt=1560, year__gt=2000))` is fine. has no effect on actual query, so in the end all 3 queries are identical:
```python
await Book.objects.filter(title='The Hobbit').get()
await Book.objects.filter(ormar.or_(title='The Hobbit')).get()
await Book.objects.filter(ormar.and_(title='The Hobbit')).get()
```
!!!note
Note that `or_` and `and_` queries will have `WHERE (title='The Hobbit')` but the parenthesis is redundant and has no real effect.
This feature can be used if you **really** need to use the same field name twice.
Remember that you cannot pass the same keyword arguments twice to the function, so
how you can query in example `WHERE (authors.name LIKE '%tolkien%') OR (authors.name LIKE '%sapkowski%'))`?
You cannot do:
```python
books = (
await Book.objects.select_related("author")
.filter(ormar.or_(
author__name__icontains="tolkien",
author__name__icontains="sapkowski" # you cannot use same keyword twice in or_!
)) # python syntax error
.all()
)
```
But you can do this:
```python
books = (
await Book.objects.select_related("author")
.filter(ormar.or_(
ormar.and_(author__name__icontains="tolkien"), # one argument == just wrapped in ()
ormar.and_(author__name__icontains="sapkowski")
))
.all()
)
assert len(books) == 5
```
## get ## get

View File

@ -172,7 +172,7 @@ await post.categories.filter(name="Test category3").update(
### filter ### filter
`filter(**kwargs) -> QuerySet` `filter(*args, **kwargs) -> QuerySet`
Allows you to filter by any Model attribute/field as well as to fetch instances, with a filter across an FK relationship. Allows you to filter by any Model attribute/field as well as to fetch instances, with a filter across an FK relationship.
@ -181,7 +181,7 @@ Allows you to filter by any Model attribute/field as well as to fetch instances,
### exclude ### exclude
`exclude(**kwargs) -> QuerySet` `exclude(*args, **kwargs) -> QuerySet`
Works exactly the same as filter and all modifiers (suffixes) are the same, but returns a not condition. Works exactly the same as filter and all modifiers (suffixes) are the same, but returns a not condition.

View File

@ -23,6 +23,9 @@
``` ```
Check the updated docs in Queries -> Filtering and sorting -> Complex filters Check the updated docs in Queries -> Filtering and sorting -> Complex filters
## Other
* Setting default on `ForeignKey` or `ManyToMany` raises and `ModelDefinition` exception as it is (and was) not supported
# 0.9.6 # 0.9.6
##Important ##Important

View File

@ -11,7 +11,7 @@ from pydantic.typing import ForwardRef, evaluate_forwardref
from sqlalchemy import UniqueConstraint from sqlalchemy import UniqueConstraint
import ormar # noqa I101 import ormar # noqa I101
from ormar.exceptions import RelationshipInstanceError from ormar.exceptions import ModelDefinitionError, RelationshipInstanceError
from ormar.fields.base import BaseField from ormar.fields.base import BaseField
if TYPE_CHECKING: # pragma no cover if TYPE_CHECKING: # pragma no cover
@ -184,6 +184,11 @@ def ForeignKey( # noqa CFQ002
owner = kwargs.pop("owner", None) owner = kwargs.pop("owner", None)
self_reference = kwargs.pop("self_reference", False) self_reference = kwargs.pop("self_reference", False)
default = kwargs.pop("default", None)
if default is not None:
raise ModelDefinitionError(
"Argument 'default' is not supported " "on relation fields!"
)
if to.__class__ == ForwardRef: if to.__class__ == ForwardRef:
__type__ = to if not nullable else Optional[to] __type__ = to if not nullable else Optional[to]

View File

@ -96,6 +96,12 @@ def ManyToMany(
if through is not None and through.__class__ != ForwardRef: if through is not None and through.__class__ != ForwardRef:
forbid_through_relations(cast(Type["Model"], through)) forbid_through_relations(cast(Type["Model"], through))
default = kwargs.pop("default", None)
if default is not None:
raise ModelDefinitionError(
"Argument 'default' is not supported " "on relation fields!"
)
if to.__class__ == ForwardRef: if to.__class__ == ForwardRef:
__type__ = to if not nullable else Optional[to] __type__ = to if not nullable else Optional[to]
column_type = None column_type = None

View File

@ -374,7 +374,7 @@ class QuerysetProxy:
model = await self.queryset.get(pk=kwargs[pk_name]) model = await self.queryset.get(pk=kwargs[pk_name])
return await model.update(**kwargs) return await model.update(**kwargs)
def filter(self, **kwargs: Any) -> "QuerysetProxy": # noqa: A003, A001 def filter(self, *args: Any, **kwargs: Any) -> "QuerysetProxy": # noqa: A003, A001
""" """
Allows you to filter by any `Model` attribute/field Allows you to filter by any `Model` attribute/field
as well as to fetch instances, with a filter across an FK relationship. as well as to fetch instances, with a filter across an FK relationship.
@ -404,10 +404,10 @@ class QuerysetProxy:
:return: filtered QuerysetProxy :return: filtered QuerysetProxy
:rtype: QuerysetProxy :rtype: QuerysetProxy
""" """
queryset = self.queryset.filter(**kwargs) queryset = self.queryset.filter(*args, **kwargs)
return self.__class__(relation=self.relation, type_=self.type_, qryset=queryset) return self.__class__(relation=self.relation, type_=self.type_, qryset=queryset)
def exclude(self, **kwargs: Any) -> "QuerysetProxy": # noqa: A003, A001 def exclude(self, *args: Any, **kwargs: Any) -> "QuerysetProxy": # noqa: A003, A001
""" """
Works exactly the same as filter and all modifiers (suffixes) are the same, Works exactly the same as filter and all modifiers (suffixes) are the same,
but returns a *not* condition. but returns a *not* condition.
@ -428,7 +428,7 @@ class QuerysetProxy:
:return: filtered QuerysetProxy :return: filtered QuerysetProxy
:rtype: QuerysetProxy :rtype: QuerysetProxy
""" """
queryset = self.queryset.exclude(**kwargs) queryset = self.queryset.exclude(*args, **kwargs)
return self.__class__(relation=self.relation, type_=self.type_, qryset=queryset) return self.__class__(relation=self.relation, type_=self.type_, qryset=queryset)
def select_related(self, related: Union[List, str]) -> "QuerysetProxy": def select_related(self, related: Union[List, str]) -> "QuerysetProxy":

View File

@ -110,6 +110,24 @@ async def test_or_filters():
assert books[0].title == "The Silmarillion" assert books[0].title == "The Silmarillion"
assert books[1].title == "The Witcher" assert books[1].title == "The Witcher"
books = (
await Book.objects.select_related("author")
.filter(
ormar.or_(
ormar.and_(
ormar.or_(year__gt=1960, year__lt=1940),
author__name="J.R.R. Tolkien",
),
ormar.and_(year__lt=2000, author__name="Andrzej Sapkowski"),
)
)
.all()
)
assert len(books) == 3
assert books[0].title == "The Hobbit"
assert books[1].title == "The Silmarillion"
assert books[2].title == "The Witcher"
books = ( books = (
await Book.objects.select_related("author") await Book.objects.select_related("author")
.exclude( .exclude(
@ -187,6 +205,38 @@ async def test_or_filters():
with pytest.raises(QueryDefinitionError): with pytest.raises(QueryDefinitionError):
await Book.objects.select_related("author").filter("wrong").all() await Book.objects.select_related("author").filter("wrong").all()
books = await tolkien.books.filter(
ormar.or_(year__lt=1940, year__gt=1960)
).all()
assert len(books) == 2
books = await tolkien.books.filter(
ormar.and_(
ormar.or_(year__lt=1940, year__gt=1960), title__icontains="hobbit"
)
).all()
assert len(books) == 1
assert tolkien.books[0].title == "The Hobbit"
books = (
await Book.objects.select_related("author")
.filter(ormar.or_(author__name="J.R.R. Tolkien"))
.all()
)
assert len(books) == 3
books = (
await Book.objects.select_related("author")
.filter(
ormar.or_(
ormar.and_(author__name__icontains="tolkien"),
ormar.and_(author__name__icontains="sapkowski"),
)
)
.all()
)
assert len(books) == 5
# TODO: Check / modify # TODO: Check / modify
# process and and or into filter groups (V) # process and and or into filter groups (V)
@ -196,5 +246,4 @@ async def test_or_filters():
# finish docstrings (V) # finish docstrings (V)
# fix types for FilterAction and FilterGroup (X) # fix types for FilterAction and FilterGroup (X)
# add docs (V) # add docs (V)
# fix querysetproxy (V)
# fix querysetproxy

View File

@ -0,0 +1,65 @@
# type: ignore
from typing import List, Optional
import databases
import pytest
import sqlalchemy
import ormar
from ormar.exceptions import ModelDefinitionError
from tests.settings import DATABASE_URL
database = databases.Database(DATABASE_URL, force_rollback=True)
metadata = sqlalchemy.MetaData()
class Author(ormar.Model):
class Meta:
tablename = "authors"
database = database
metadata = metadata
id: int = ormar.Integer(primary_key=True)
first_name: str = ormar.String(max_length=80)
last_name: str = ormar.String(max_length=80)
class Category(ormar.Model):
class Meta:
tablename = "categories"
database = database
metadata = metadata
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=40)
def test_fk_error():
with pytest.raises(ModelDefinitionError):
class Post(ormar.Model):
class Meta:
tablename = "posts"
database = database
metadata = metadata
id: int = ormar.Integer(primary_key=True)
title: str = ormar.String(max_length=200)
categories: Optional[List[Category]] = ormar.ManyToMany(Category)
author: Optional[Author] = ormar.ForeignKey(Author, default="aa")
def test_m2m_error():
with pytest.raises(ModelDefinitionError):
class Post(ormar.Model):
class Meta:
tablename = "posts"
database = database
metadata = metadata
id: int = ormar.Integer(primary_key=True)
title: str = ormar.String(max_length=200)
categories: Optional[List[Category]] = ormar.ManyToMany(
Category, default="aa"
)