Merge pull request #161 from collerek/fields_access

Direct fields access for filter and order_by
This commit is contained in:
collerek
2021-04-21 11:35:22 +02:00
committed by GitHub
20 changed files with 1058 additions and 120 deletions

View File

@ -220,7 +220,10 @@ async def create():
async def read(): async def read():
# Fetch an instance, without loading a foreign key relationship on it. # Fetch an instance, without loading a foreign key relationship on it.
# Django style
book = await Book.objects.get(title="The Hobbit") book = await Book.objects.get(title="The Hobbit")
# or python style
book = await Book.objects.get(Book.title == "The Hobbit")
book2 = await Book.objects.first() book2 = await Book.objects.first()
# first() fetch the instance with lower primary key value # first() fetch the instance with lower primary key value
@ -334,20 +337,30 @@ async def filter_and_sort():
# get(), all() etc. # get(), all() etc.
# to use special methods or access related model fields use double # to use special methods or access related model fields use double
# underscore like to filter by the name of the author use author__name # underscore like to filter by the name of the author use author__name
# Django style
books = await Book.objects.all(author__name="J.R.R. Tolkien") books = await Book.objects.all(author__name="J.R.R. Tolkien")
# python style
books = await Book.objects.all(Book.author.name == "J.R.R. Tolkien")
assert len(books) == 3 assert len(books) == 3
# filter can accept special methods also separated with double underscore # filter can accept special methods also separated with double underscore
# to issue sql query ` where authors.name like "%tolkien%"` that is not # to issue sql query ` where authors.name like "%tolkien%"` that is not
# case sensitive (hence small t in Tolkien) # case sensitive (hence small t in Tolkien)
# Django style
books = await Book.objects.filter(author__name__icontains="tolkien").all() books = await Book.objects.filter(author__name__icontains="tolkien").all()
# python style
books = await Book.objects.filter(Book.author.name.icontains("tolkien")).all()
assert len(books) == 3 assert len(books) == 3
# to sort use order_by() function of queryset # to sort use order_by() function of queryset
# to sort decreasing use hyphen before the field name # to sort decreasing use hyphen before the field name
# same as with filter you can use double underscores to access related fields # same as with filter you can use double underscores to access related fields
# Django style
books = await Book.objects.filter(author__name__icontains="tolkien").order_by( books = await Book.objects.filter(author__name__icontains="tolkien").order_by(
"-year").all() "-year").all()
# python style
books = await Book.objects.filter(Book.author.name.icontains("tolkien")).order_by(
Book.year.desc()).all()
assert len(books) == 3 assert len(books) == 3
assert books[0].title == "The Silmarillion" assert books[0].title == "The Silmarillion"
assert books[2].title == "The Hobbit" assert books[2].title == "The Hobbit"
@ -417,12 +430,24 @@ async def pagination():
async def aggregations(): async def aggregations():
# ormar currently supports count: # count:
assert 2 == await Author.objects.count() assert 2 == await Author.objects.count()
# and exists # exists:
assert await Book.objects.filter(title="The Hobbit").exists() assert await Book.objects.filter(title="The Hobbit").exists()
# max:
assert 1990 == await Book.objects.max(columns=["year"])
# min:
assert 1937 == await Book.objects.min(columns=["year"])
# avg:
assert 1964.75 == await Book.objects.avg(columns=["year"])
# sum:
assert 7859 == await Book.objects.sum(columns=["year"])
# to read more about aggregated functions # to read more about aggregated functions
# visit: https://collerek.github.io/ormar/queries/aggregations/ # visit: https://collerek.github.io/ormar/queries/aggregations/
@ -448,16 +473,16 @@ metadata.drop_all(engine)
### QuerySet methods ### QuerySet methods
* `create(**kwargs): -> Model` * `create(**kwargs): -> Model`
* `get(**kwargs): -> Model` * `get(*args, **kwargs): -> Model`
* `get_or_none(**kwargs): -> Optional[Model]` * `get_or_none(*args, **kwargs): -> Optional[Model]`
* `get_or_create(**kwargs) -> Model` * `get_or_create(*args, **kwargs) -> Model`
* `first(): -> Model` * `first(*args, **kwargs): -> 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`
* `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(*args, each: bool = False, **kwargs) -> int`
* `all(**kwargs) -> List[Optional[Model]]` * `all(*args, **kwargs) -> List[Optional[Model]]`
* `filter(*args, **kwargs) -> QuerySet` * `filter(*args, **kwargs) -> QuerySet`
* `exclude(*args, **kwargs) -> QuerySet` * `exclude(*args, **kwargs) -> QuerySet`
* `select_related(related: Union[List, str]) -> QuerySet` * `select_related(related: Union[List, str]) -> QuerySet`
@ -466,6 +491,10 @@ metadata.drop_all(engine)
* `offset(offset: int) -> QuerySet` * `offset(offset: int) -> QuerySet`
* `count() -> int` * `count() -> int`
* `exists() -> bool` * `exists() -> bool`
* `max(columns: List[str]) -> Any`
* `min(columns: List[str]) -> Any`
* `avg(columns: List[str]) -> Any`
* `sum(columns: List[str]) -> Any`
* `fields(columns: Union[List, str, set, dict]) -> QuerySet` * `fields(columns: Union[List, str, set, dict]) -> QuerySet`
* `exclude_fields(columns: Union[List, str, set, dict]) -> QuerySet` * `exclude_fields(columns: Union[List, str, set, dict]) -> QuerySet`
* `order_by(columns:Union[List, str]) -> QuerySet` * `order_by(columns:Union[List, str]) -> QuerySet`

View File

@ -220,7 +220,10 @@ async def create():
async def read(): async def read():
# Fetch an instance, without loading a foreign key relationship on it. # Fetch an instance, without loading a foreign key relationship on it.
# Django style
book = await Book.objects.get(title="The Hobbit") book = await Book.objects.get(title="The Hobbit")
# or python style
book = await Book.objects.get(Book.title == "The Hobbit")
book2 = await Book.objects.first() book2 = await Book.objects.first()
# first() fetch the instance with lower primary key value # first() fetch the instance with lower primary key value
@ -334,20 +337,30 @@ async def filter_and_sort():
# get(), all() etc. # get(), all() etc.
# to use special methods or access related model fields use double # to use special methods or access related model fields use double
# underscore like to filter by the name of the author use author__name # underscore like to filter by the name of the author use author__name
# Django style
books = await Book.objects.all(author__name="J.R.R. Tolkien") books = await Book.objects.all(author__name="J.R.R. Tolkien")
# python style
books = await Book.objects.all(Book.author.name == "J.R.R. Tolkien")
assert len(books) == 3 assert len(books) == 3
# filter can accept special methods also separated with double underscore # filter can accept special methods also separated with double underscore
# to issue sql query ` where authors.name like "%tolkien%"` that is not # to issue sql query ` where authors.name like "%tolkien%"` that is not
# case sensitive (hence small t in Tolkien) # case sensitive (hence small t in Tolkien)
# Django style
books = await Book.objects.filter(author__name__icontains="tolkien").all() books = await Book.objects.filter(author__name__icontains="tolkien").all()
# python style
books = await Book.objects.filter(Book.author.name.icontains("tolkien")).all()
assert len(books) == 3 assert len(books) == 3
# to sort use order_by() function of queryset # to sort use order_by() function of queryset
# to sort decreasing use hyphen before the field name # to sort decreasing use hyphen before the field name
# same as with filter you can use double underscores to access related fields # same as with filter you can use double underscores to access related fields
# Django style
books = await Book.objects.filter(author__name__icontains="tolkien").order_by( books = await Book.objects.filter(author__name__icontains="tolkien").order_by(
"-year").all() "-year").all()
# python style
books = await Book.objects.filter(Book.author.name.icontains("tolkien")).order_by(
Book.year.desc()).all()
assert len(books) == 3 assert len(books) == 3
assert books[0].title == "The Silmarillion" assert books[0].title == "The Silmarillion"
assert books[2].title == "The Hobbit" assert books[2].title == "The Hobbit"
@ -417,12 +430,24 @@ async def pagination():
async def aggregations(): async def aggregations():
# ormar currently supports count: # count:
assert 2 == await Author.objects.count() assert 2 == await Author.objects.count()
# and exists # exists:
assert await Book.objects.filter(title="The Hobbit").exists() assert await Book.objects.filter(title="The Hobbit").exists()
# max:
assert 1990 == await Book.objects.max(columns=["year"])
# min:
assert 1937 == await Book.objects.min(columns=["year"])
# avg:
assert 1964.75 == await Book.objects.avg(columns=["year"])
# sum:
assert 7859 == await Book.objects.sum(columns=["year"])
# to read more about aggregated functions # to read more about aggregated functions
# visit: https://collerek.github.io/ormar/queries/aggregations/ # visit: https://collerek.github.io/ormar/queries/aggregations/
@ -448,16 +473,16 @@ metadata.drop_all(engine)
### QuerySet methods ### QuerySet methods
* `create(**kwargs): -> Model` * `create(**kwargs): -> Model`
* `get(**kwargs): -> Model` * `get(*args, **kwargs): -> Model`
* `get_or_none(**kwargs): -> Optional[Model]` * `get_or_none(*args, **kwargs): -> Optional[Model]`
* `get_or_create(**kwargs) -> Model` * `get_or_create(*args, **kwargs) -> Model`
* `first(): -> Model` * `first(*args, **kwargs): -> 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`
* `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(*args, each: bool = False, **kwargs) -> int`
* `all(**kwargs) -> List[Optional[Model]]` * `all(*args, **kwargs) -> List[Optional[Model]]`
* `filter(*args, **kwargs) -> QuerySet` * `filter(*args, **kwargs) -> QuerySet`
* `exclude(*args, **kwargs) -> QuerySet` * `exclude(*args, **kwargs) -> QuerySet`
* `select_related(related: Union[List, str]) -> QuerySet` * `select_related(related: Union[List, str]) -> QuerySet`
@ -466,6 +491,10 @@ metadata.drop_all(engine)
* `offset(offset: int) -> QuerySet` * `offset(offset: int) -> QuerySet`
* `count() -> int` * `count() -> int`
* `exists() -> bool` * `exists() -> bool`
* `max(columns: List[str]) -> Any`
* `min(columns: List[str]) -> Any`
* `avg(columns: List[str]) -> Any`
* `sum(columns: List[str]) -> Any`
* `fields(columns: Union[List, str, set, dict]) -> QuerySet` * `fields(columns: Union[List, str, set, dict]) -> QuerySet`
* `exclude_fields(columns: Union[List, str, set, dict]) -> QuerySet` * `exclude_fields(columns: Union[List, str, set, dict]) -> QuerySet`
* `order_by(columns:Union[List, str]) -> QuerySet` * `order_by(columns:Union[List, str]) -> QuerySet`

View File

@ -2,27 +2,27 @@
You can use following methods to filter the data (sql where clause). You can use following methods to filter the data (sql where clause).
* `filter(**kwargs) -> QuerySet` * `filter(*args, **kwargs) -> QuerySet`
* `exclude(**kwargs) -> QuerySet` * `exclude(*args, **kwargs) -> QuerySet`
* `get(**kwargs) -> Model` * `get(*args, **kwargs) -> Model`
* `get_or_none(**kwargs) -> Optional[Model]` * `get_or_none(*args, **kwargs) -> Optional[Model]`
* `get_or_create(**kwargs) -> Model` * `get_or_create(*args, **kwargs) -> Model`
* `all(**kwargs) -> List[Optional[Model]]` * `all(*args, **kwargs) -> List[Optional[Model]]`
* `QuerysetProxy` * `QuerysetProxy`
* `QuerysetProxy.filter(**kwargs)` method * `QuerysetProxy.filter(*args, **kwargs)` method
* `QuerysetProxy.exclude(**kwargs)` method * `QuerysetProxy.exclude(*args, **kwargs)` method
* `QuerysetProxy.get(**kwargs)` method * `QuerysetProxy.get(*args, **kwargs)` method
* `QuerysetProxy.get_or_none(**kwargs)` method * `QuerysetProxy.get_or_none(*args, **kwargs)` method
* `QuerysetProxy.get_or_create(**kwargs)` method * `QuerysetProxy.get_or_create(*args, **kwargs)` method
* `QuerysetProxy.all(**kwargs)` method * `QuerysetProxy.all(*args, **kwargs)` method
And following methods to sort the data (sql order by clause). And following methods to sort the data (sql order by clause).
* `order_by(columns:Union[List, str]) -> QuerySet` * `order_by(columns:Union[List, str, OrderAction]) -> QuerySet`
* `QuerysetProxy` * `QuerysetProxy`
* `QuerysetProxy.order_by(columns:Union[List, str])` method * `QuerysetProxy.order_by(columns:Union[List, str, OrderAction])` method
## Filtering ## Filtering
@ -65,24 +65,107 @@ tracks = Track.objects.filter(album__name="Fantasies").all()
# will return all tracks where the columns album name = 'Fantasies' # will return all tracks where the columns album name = 'Fantasies'
``` ```
### Django style filters
You can use special filter suffix to change the filter operands: You can use special filter suffix to change the filter operands:
* exact - like `album__name__exact='Malibu'` (exact match) * exact - exact match to value, sql `column = <VALUE>`
* iexact - like `album__name__iexact='malibu'` (exact match case insensitive) * can be written as`album__name__exact='Malibu'`
* contains - like `album__name__contains='Mal'` (sql like) * iexact - exact match sql `column = <VALUE>` (case insensitive)
* icontains - like `album__name__icontains='mal'` (sql like case insensitive) * can be written as`album__name__iexact='malibu'`
* in - like `album__name__in=['Malibu', 'Barclay']` (sql in) * contains - sql `column LIKE '%<VALUE>%'`
* isnull - like `album__name__isnull=True` (sql is null) * can be written as`album__name__contains='Mal'`
(isnotnull `album__name__isnull=False` (sql is not null)) * icontains - sql `column LIKE '%<VALUE>%'` (case insensitive)
* gt - like `position__gt=3` (sql >) * can be written as`album__name__icontains='mal'`
* gte - like `position__gte=3` (sql >=) * in - sql ` column IN (<VALUE1>, <VALUE2>, ...)`
* lt - like `position__lt=3` (sql <) * can be written as`album__name__in=['Malibu', 'Barclay']`
* lte - like `position__lte=3` (sql <=) * isnull - sql `column IS NULL` (and sql `column IS NOT NULL`)
* startswith - like `album__name__startswith='Mal'` (exact start match) * can be written as`album__name__isnull=True` (isnotnull `album__name__isnull=False`)
* istartswith - like `album__name__istartswith='mal'` (exact start match case * gt - sql `column > <VALUE>` (greater than)
insensitive) * can be written as`position__gt=3`
* endswith - like `album__name__endswith='ibu'` (exact end match) * gte - sql `column >= <VALUE>` (greater or equal than)
* iendswith - like `album__name__iendswith='IBU'` (exact end match case insensitive) * can be written as`position__gte=3`
* lt - sql `column < <VALUE>` (lower than)
* can be written as`position__lt=3`
* lte - sql `column <= <VALUE>` (lower equal than)
* can be written as`position__lte=3`
* startswith - sql `column LIKE '<VALUE>%'` (exact start match)
* can be written as`album__name__startswith='Mal'`
* istartswith - sql `column LIKE '<VALUE>%'` (case insensitive)
* can be written as`album__name__istartswith='mal'`
* endswith - sql `column LIKE '%<VALUE>'` (exact end match)
* can be written as`album__name__endswith='ibu'`
* iendswith - sql `column LIKE '%<VALUE>'` (case insensitive)
* can be written as`album__name__iendswith='IBU'`
Some samples:
```python
# sql: ( product.name = 'Test' AND product.rating >= 3.0 )
Product.objects.filter(name='Test', rating__gte=3.0).get()
# sql: ( product.name = 'Test' AND product.rating >= 3.0 )
# OR (categories.name IN ('Toys', 'Books'))
Product.objects.filter(
ormar.or_(
ormar.and_(name='Test', rating__gte=3.0),
categories__name__in=['Toys', 'Books'])
).get()
# note: to read more about and_ and or_ read complex filters section below
```
### Python style filters
* exact - exact match to value, sql `column = <VALUE>`
* can be written as `Track.album.name == 'Malibu`
* iexact - exact match sql `column = <VALUE>` (case insensitive)
* can be written as `Track.album.name.iexact('malibu')`
* contains - sql `column LIKE '%<VALUE>%'`
* can be written as `Track.album.name % 'Mal')`
* can be written as `Track.album.name.contains('Mal')`
* icontains - sql `column LIKE '%<VALUE>%'` (case insensitive)
* can be written as `Track.album.name.icontains('mal')`
* in - sql ` column IN (<VALUE1>, <VALUE2>, ...)`
* can be written as `Track.album.name << ['Malibu', 'Barclay']`
* can be written as `Track.album.name.in_(['Malibu', 'Barclay'])`
* isnull - sql `column IS NULL` (and sql `column IS NOT NULL`)
* can be written as `Track.album.name >> None`
* can be written as `Track.album.name.is_null(True)`
* not null can be written as `Track.album.name.is_null(False)`
* not null can be written as `~(Track.album.name >> None)`
* not null can be written as `~(Track.album.name.is_null(True))`
* gt - sql `column > <VALUE>` (greater than)
* can be written as `Track.album.name > 3`
* gte - sql `column >= <VALUE>` (greater or equal than)
* can be written as `Track.album.name >= 3`
* lt - sql `column < <VALUE>` (lower than)
* can be written as `Track.album.name < 3`
* lte - sql `column <= <VALUE>` (lower equal than)
* can be written as `Track.album.name <= 3`
* startswith - sql `column LIKE '<VALUE>%'` (exact start match)
* can be written as `Track.album.name.startswith('Mal')`
* istartswith - sql `column LIKE '<VALUE>%'` (case insensitive)
* can be written as `Track.album.name.istartswith('mal')`
* endswith - sql `column LIKE '%<VALUE>'` (exact end match)
* can be written as `Track.album.name.endswith('ibu')`
* iendswith - sql `column LIKE '%<VALUE>'` (case insensitive)
* can be written as `Track.album.name.iendswith('IBU')`
Some samples:
```python
# sql: ( product.name = 'Test' AND product.rating >= 3.0 )
Product.objects.filter(
(Product.name == 'Test') & (Product.rating >=3.0)
).get()
# sql: ( product.name = 'Test' AND product.rating >= 3.0 )
# OR (categories.name IN ('Toys', 'Books'))
Product.objects.filter(
((Product.name='Test') & (Product.rating >= 3.0)) |
(Product.categories.name << ['Toys', 'Books'])
).get()
```
!!!note !!!note
All methods that do not return the rows explicitly returns a QueySet instance so All methods that do not return the rows explicitly returns a QueySet instance so
@ -155,7 +238,7 @@ In order to build `OR` and nested conditions ormar provides two functions that c
`filter()` and `exclude()` in `QuerySet` and `QuerysetProxy`. `filter()` and `exclude()` in `QuerySet` and `QuerysetProxy`.
!!!note !!!note
Note that you cannot provide those methods in any other method like `get()` or `all()` which accepts only keyword arguments. Note that you can provide those methods in any other method like `get()` or `all()` that accepts `*args`.
Call to `or_` and `and_` can be nested in each other, as well as combined with keyword arguments. Call to `or_` and `and_` can be nested in each other, as well as combined with keyword arguments.
Since it sounds more complicated than it is, let's look at some examples. Since it sounds more complicated than it is, let's look at some examples.
@ -208,6 +291,7 @@ Let's select books of Tolkien **OR** books written after 1970
sql: sql:
`WHERE ( authors.name = 'J.R.R. Tolkien' OR books.year > 1970 )` `WHERE ( authors.name = 'J.R.R. Tolkien' OR books.year > 1970 )`
### Django style
```python ```python
books = ( books = (
await Book.objects.select_related("author") await Book.objects.select_related("author")
@ -217,11 +301,22 @@ books = (
assert len(books) == 5 assert len(books) == 5
``` ```
### Python style
```python
books = (
await Book.objects.select_related("author")
.filter((Book.author.name=="J.R.R. Tolkien") | (Book.year > 1970))
.all()
)
assert len(books) == 5
```
Now let's select books written after 1960 or before 1940 which were written by Tolkien. Now let's select books written after 1960 or before 1940 which were written by Tolkien.
sql: sql:
`WHERE ( books.year > 1960 OR books.year < 1940 ) AND authors.name = 'J.R.R. Tolkien'` `WHERE ( books.year > 1960 OR books.year < 1940 ) AND authors.name = 'J.R.R. Tolkien'`
### Django style
```python ```python
# OPTION 1 - split and into separate call # OPTION 1 - split and into separate call
books = ( books = (
@ -249,11 +344,38 @@ assert books[0].title == "The Hobbit"
assert books[1].title == "The Silmarillion" assert books[1].title == "The Silmarillion"
``` ```
### Python style
```python
books = (
await Book.objects.select_related("author")
.filter((Book.year > 1960) | (Book.year < 1940))
.filter(Book.author.name == "J.R.R. Tolkien")
.all()
)
assert len(books) == 2
# OPTION 2 - all in one
books = (
await Book.objects.select_related("author")
.filter(
(
(Book.year > 1960) | (Book.year < 1940)
) & (Book.author.name == "J.R.R. Tolkien")
)
.all()
)
assert len(books) == 2
assert books[0].title == "The Hobbit"
assert books[1].title == "The Silmarillion"
```
Books of Sapkowski from before 2000 or books of Tolkien written after 1960 Books of Sapkowski from before 2000 or books of Tolkien written after 1960
sql: sql:
`WHERE ( ( books.year > 1960 AND authors.name = 'J.R.R. Tolkien' ) OR ( books.year < 2000 AND authors.name = 'Andrzej Sapkowski' ) ) ` `WHERE ( ( books.year > 1960 AND authors.name = 'J.R.R. Tolkien' ) OR ( books.year < 2000 AND authors.name = 'Andrzej Sapkowski' ) ) `
### Django style
```python ```python
books = ( books = (
await Book.objects.select_related("author") await Book.objects.select_related("author")
@ -268,7 +390,20 @@ books = (
assert len(books) == 2 assert len(books) == 2
``` ```
Of course those functions can have more than 2 conditions, so if we for example want also ### Python style
```python
books = (
await Book.objects.select_related("author")
.filter(
((Book.year > 1960) & (Book.author.name == "J.R.R. Tolkien")) |
((Book.year < 2000) & (Book.author.name == "Andrzej Sapkowski"))
)
.all()
)
assert len(books) == 2
```
Of course those functions can have more than 2 conditions, so if we for example want
books that contains 'hobbit': books that contains 'hobbit':
sql: sql:
@ -276,6 +411,7 @@ sql:
( books.year < 2000 AND os0cec_authors.name = 'Andrzej Sapkowski' ) OR ( books.year < 2000 AND os0cec_authors.name = 'Andrzej Sapkowski' ) OR
books.title LIKE '%hobbit%' )` books.title LIKE '%hobbit%' )`
### Django style
```python ```python
books = ( books = (
await Book.objects.select_related("author") await Book.objects.select_related("author")
@ -290,6 +426,19 @@ books = (
) )
``` ```
### Python style
```python
books = (
await Book.objects.select_related("author")
.filter(
((Book.year > 1960) & (Book.author.name == "J.R.R. Tolkien")) |
((Book.year < 2000) & (Book.author.name == "Andrzej Sapkowski")) |
(Book.title.icontains("hobbit"))
)
.all()
)
```
If you want or need to you can nest deeper conditions as deep as you want, in example to If you want or need to you can nest deeper conditions as deep as you want, in example to
achieve a query like this: achieve a query like this:
@ -301,6 +450,28 @@ AND authors.name = 'J.R.R. Tolkien' ) OR
``` ```
You can construct a query as follows: You can construct a query as follows:
### Django style
```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"
```
```python ```python
books = ( books = (
await Book.objects.select_related("author") await Book.objects.select_related("author")
@ -339,9 +510,11 @@ assert len(books) == 1
assert books[0].title == "The Witcher" assert books[0].title == "The Witcher"
``` ```
Same applies to python style chaining and nesting.
!!!note ### Django style
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 with django style 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'])`.
If you pass only one parameter to `or_` or `and_` functions it's simply wrapped in parenthesis and If you pass only one parameter to `or_` or `and_` functions it's simply wrapped in parenthesis and
@ -386,13 +559,28 @@ books = (
assert len(books) == 5 assert len(books) == 5
``` ```
### Python style
Note that with python style you can perfectly use the same fields as many times as you want.
```python
books = (
await Book.objects.select_related("author")
.filter(
(Book.author.name.icontains("tolkien")) |
(Book.author.name.icontains("sapkowski"))
))
.all()
)
```
## get ## get
`get(**kwargs) -> Model` `get(*args, **kwargs) -> Model`
Get's the first row from the db meeting the criteria set by kwargs. Get's the first row from the db meeting the criteria set by kwargs.
When any kwargs are passed it's a shortcut equivalent to calling `filter(**kwargs).get()` When any args and/or kwargs are passed it's a shortcut equivalent to calling `filter(*args, **kwargs).get()`
!!!tip !!!tip
To read more about `filter` go to [filter](./#filter). To read more about `filter` go to [filter](./#filter).
@ -403,14 +591,13 @@ When any kwargs are passed it's a shortcut equivalent to calling `filter(**kwarg
Exact equivalent of get described above but instead of raising the exception returns `None` if no db record matching the criteria is found. Exact equivalent of get described above but instead of raising the exception returns `None` if no db record matching the criteria is found.
## get_or_create ## get_or_create
`get_or_create(**kwargs) -> Model` `get_or_create(*args, **kwargs) -> Model`
Combination of create and get methods. Combination of create and get methods.
When any kwargs are passed it's a shortcut equivalent to calling `filter(**kwargs).get_or_create()` When any args and/or kwargs are passed it's a shortcut equivalent to calling `filter(*args, **kwargs).get_or_create()`
!!!tip !!!tip
To read more about `filter` go to [filter](./#filter). To read more about `filter` go to [filter](./#filter).
@ -423,11 +610,11 @@ When any kwargs are passed it's a shortcut equivalent to calling `filter(**kwarg
## all ## all
`all(**kwargs) -> List[Optional["Model"]]` `all(*args, **kwargs) -> List[Optional["Model"]]`
Returns all rows from a database for given model for set filter options. Returns all rows from a database for given model for set filter options.
When any kwargs are passed it's a shortcut equivalent to calling `filter(**kwargs).all()` When any kwargs are passed it's a shortcut equivalent to calling `filter(*args, **kwargs).all()`
!!!tip !!!tip
To read more about `filter` go to [filter](./#filter). To read more about `filter` go to [filter](./#filter).
@ -493,7 +680,7 @@ objects from other side of the relation.
### order_by ### order_by
`order_by(columns: Union[List, str]) -> QuerySet` `order_by(columns: Union[List, str, OrderAction]) -> QuerySet`
With `order_by()` you can order the results from database based on your choice of With `order_by()` you can order the results from database based on your choice of
fields. fields.
@ -534,6 +721,7 @@ Given sample Models like following:
To order by main model field just provide a field name To order by main model field just provide a field name
### Django style
```python ```python
toys = await Toy.objects.select_related("owner").order_by("name").all() toys = await Toy.objects.select_related("owner").order_by("name").all()
assert [x.name.replace("Toy ", "") for x in toys] == [ assert [x.name.replace("Toy ", "") for x in toys] == [
@ -543,11 +731,23 @@ assert toys[0].owner == zeus
assert toys[1].owner == aphrodite assert toys[1].owner == aphrodite
``` ```
### Python style
```python
toys = await Toy.objects.select_related("owner").order_by(Toy.name.asc()).all()
assert [x.name.replace("Toy ", "") for x in toys] == [
str(x + 1) for x in range(6)
]
assert toys[0].owner == zeus
assert toys[1].owner == aphrodite
```
To sort on nested models separate field names with dunder '__'. To sort on nested models separate field names with dunder '__'.
You can sort this way across all relation types -> `ForeignKey`, reverse virtual FK You can sort this way across all relation types -> `ForeignKey`, reverse virtual FK
and `ManyToMany` fields. and `ManyToMany` fields.
### Django style
```python ```python
toys = await Toy.objects.select_related("owner").order_by("owner__name").all() toys = await Toy.objects.select_related("owner").order_by("owner__name").all()
assert toys[0].owner.name == toys[1].owner.name == "Aphrodite" assert toys[0].owner.name == toys[1].owner.name == "Aphrodite"
@ -555,8 +755,17 @@ assert toys[2].owner.name == toys[3].owner.name == "Hermes"
assert toys[4].owner.name == toys[5].owner.name == "Zeus" assert toys[4].owner.name == toys[5].owner.name == "Zeus"
``` ```
### Python style
```python
toys = await Toy.objects.select_related("owner").order_by(Toy.owner.name.asc()).all()
assert toys[0].owner.name == toys[1].owner.name == "Aphrodite"
assert toys[2].owner.name == toys[3].owner.name == "Hermes"
assert toys[4].owner.name == toys[5].owner.name == "Zeus"
```
To sort in descending order provide a hyphen in front of the field name To sort in descending order provide a hyphen in front of the field name
### Django style
```python ```python
owner = ( owner = (
await Owner.objects.select_related("toys") await Owner.objects.select_related("toys")
@ -568,6 +777,18 @@ assert owner.toys[0].name == "Toy 4"
assert owner.toys[1].name == "Toy 1" assert owner.toys[1].name == "Toy 1"
``` ```
### Python style
```python
owner = (
await Owner.objects.select_related("toys")
.order_by(Owner.toys.name.desc())
.filter(Owner.name == "Zeus")
.get()
)
assert owner.toys[0].name == "Toy 4"
assert owner.toys[1].name == "Toy 1"
```
!!!note !!!note
All methods that do not return the rows explicitly returns a QueySet instance so All methods that do not return the rows explicitly returns a QueySet instance so
you can chain them together you can chain them together

View File

@ -2,10 +2,10 @@
Following methods allow you to load data from the database. Following methods allow you to load data from the database.
* `get(**kwargs) -> Model` * `get(*args, **kwargs) -> Model`
* `get_or_create(**kwargs) -> Model` * `get_or_create(*args, **kwargs) -> Model`
* `first() -> Model` * `first(*args, **kwargs) -> Model`
* `all(**kwargs) -> List[Optional[Model]]` * `all(*args, **kwargs) -> List[Optional[Model]]`
* `Model` * `Model`
@ -13,20 +13,20 @@ Following methods allow you to load data from the database.
* `QuerysetProxy` * `QuerysetProxy`
* `QuerysetProxy.get(**kwargs)` method * `QuerysetProxy.get(*args, **kwargs)` method
* `QuerysetProxy.get_or_create(**kwargs)` method * `QuerysetProxy.get_or_create(*args, **kwargs)` method
* `QuerysetProxy.first()` method * `QuerysetProxy.first(*args, **kwargs)` method
* `QuerysetProxy.all(**kwargs)` method * `QuerysetProxy.all(*args, **kwargs)` method
## get ## get
`get(**kwargs) -> Model` `get(*args, **kwargs) -> Model`
Get's the first row from the db meeting the criteria set by kwargs. 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 column. If no criteria set it will return the last row in db sorted by pk column.
Passing a criteria is actually calling filter(**kwargs) method described below. Passing a criteria is actually calling filter(*args, **kwargs) method described below.
```python ```python
class Track(ormar.Model): class Track(ormar.Model):
@ -57,14 +57,14 @@ track == track2
## get_or_none ## get_or_none
`get_or_none(**kwargs) -> Model` `get_or_none(*args, **kwargs) -> Model`
Exact equivalent of get described above but instead of raising the exception returns `None` if no db record matching the criteria is found. Exact equivalent of get described above but instead of raising the exception returns `None` if no db record matching the criteria is found.
## get_or_create ## get_or_create
`get_or_create(**kwargs) -> Model` `get_or_create(*args, **kwargs) -> Model`
Combination of create and get methods. Combination of create and get methods.
@ -102,7 +102,7 @@ assert album == album2
## first ## first
`first() -> Model` `first(*args, **kwargs) -> Model`
Gets the first row from the db ordered by primary key column ascending. Gets the first row from the db ordered by primary key column ascending.
@ -127,11 +127,11 @@ assert album.name == 'The Cat'
## all ## all
`all(**kwargs) -> List[Optional["Model"]]` `all(*args, **kwargs) -> List[Optional["Model"]]`
Returns all rows from a database for given model for set filter options. 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()`. Passing kwargs is a shortcut and equals to calling `filter(*args, **kwargs).all()`.
If there are no rows meeting the criteria an empty list is returned. If there are no rows meeting the criteria an empty list is returned.

View File

@ -1,3 +1,123 @@
# 0.10.4
## ✨ Features
* Add **Python style** to `filter` and `order_by` with field access instead of dunder separated strings. [#51](https://github.com/collerek/ormar/issues/51)
* Accessing a field with attribute access (chain of dot notation) can be used to construct `FilterGroups` (`ormar.and_` and `ormar.or_`)
* Field access overloads set of python operators and provide a set of functions to allow same functionality as with dunder separated param names in `**kwargs`, that means that querying from sample model `Track` related to model `Album` now you have more options:
* exact - exact match to value, sql `column = <VALUE>`
* OLD: `album__name__exact='Malibu'`
* NEW: can be also written as `Track.album.name == 'Malibu`
* iexact - exact match sql `column = <VALUE>` (case insensitive)
* OLD: `album__name__iexact='malibu'`
* NEW: can be also written as `Track.album.name.iexact('malibu')`
* contains - sql `column LIKE '%<VALUE>%'`
* OLD: `album__name__contains='Mal'`
* NEW: can be also written as `Track.album.name % 'Mal')`
* NEW: can be also written as `Track.album.name.contains('Mal')`
* icontains - sql `column LIKE '%<VALUE>%'` (case insensitive)
* OLD: `album__name__icontains='mal'`
* NEW: can be also written as `Track.album.name.icontains('mal')`
* in - sql ` column IN (<VALUE1>, <VALUE2>, ...)`
* OLD: `album__name__in=['Malibu', 'Barclay']`
* NEW: can be also written as `Track.album.name << ['Malibu', 'Barclay']`
* NEW: can be also written as `Track.album.name.in_(['Malibu', 'Barclay'])`
* isnull - sql `column IS NULL` (and sql `column IS NOT NULL`)
* OLD: `album__name__isnull=True` (isnotnull `album__name__isnull=False`)
* NEW: can be also written as `Track.album.name >> None`
* NEW: can be also written as `Track.album.name.is_null(True)`
* NEW: not null can be also written as `Track.album.name.is_null(False)`
* NEW: not null can be also written as `~(Track.album.name >> None)`
* NEW: not null can be also written as `~(Track.album.name.is_null(True))`
* gt - sql `column > <VALUE>` (greater than)
* OLD: `position__gt=3`
* NEW: can be also written as `Track.album.name > 3`
* gte - sql `column >= <VALUE>` (greater or equal than)
* OLD: `position__gte=3`
* NEW: can be also written as `Track.album.name >= 3`
* lt - sql `column < <VALUE>` (lower than)
* OLD: `position__lt=3`
* NEW: can be also written as `Track.album.name < 3`
* lte - sql `column <= <VALUE>` (lower equal than)
* OLD: `position__lte=3`
* NEW: can be also written as `Track.album.name <= 3`
* startswith - sql `column LIKE '<VALUE>%'` (exact start match)
* OLD: `album__name__startswith='Mal'`
* NEW: can be also written as `Track.album.name.startswith('Mal')`
* istartswith - sql `column LIKE '<VALUE>%'` (case insensitive)
* OLD: `album__name__istartswith='mal'`
* NEW: can be also written as `Track.album.name.istartswith('mal')`
* endswith - sql `column LIKE '%<VALUE>'` (exact end match)
* OLD: `album__name__endswith='ibu'`
* NEW: can be also written as `Track.album.name.endswith('ibu')`
* iendswith - sql `column LIKE '%<VALUE>'` (case insensitive)
* OLD: `album__name__iendswith='IBU'`
* NEW: can be also written as `Track.album.name.iendswith('IBU')`
* You can provide `FilterGroups` not only in `filter()` and `exclude()` but also in:
* `get()`
* `get_or_none()`
* `get_or_create()`
* `first()`
* `all()`
* `delete()`
* With `FilterGroups` (`ormar.and_` and `ormar.or_`) you can now use:
* `&` - as `and_` instead of next level of nesting
* `|` - as `or_' instead of next level of nesting
* `~` - as negation of the filter group
* To combine groups of filters into one set of conditions use `&` (sql `AND`) and `|` (sql `OR`)
```python
# Following queries are equivalent:
# sql: ( product.name = 'Test' AND product.rating >= 3.0 )
# ormar OPTION 1 - OLD one
Product.objects.filter(name='Test', rating__gte=3.0).get()
# ormar OPTION 2 - OLD one
Product.objects.filter(ormar.and_(name='Test', rating__gte=3.0)).get()
# ormar OPTION 3 - NEW one (field access)
Product.objects.filter((Product.name == 'Test') & (Product.rating >=3.0)).get()
```
* Same applies to nested complicated filters
```python
# Following queries are equivalent:
# sql: ( product.name = 'Test' AND product.rating >= 3.0 )
# OR (categories.name IN ('Toys', 'Books'))
# ormar OPTION 1 - OLD one
Product.objects.filter(ormar.or_(
ormar.and_(name='Test', rating__gte=3.0),
categories__name__in=['Toys', 'Books'])
).get()
# ormar OPTION 2 - NEW one (instead of nested or use `|`)
Product.objects.filter(
ormar.and_(name='Test', rating__gte=3.0) |
ormar.and_(categories__name__in=['Toys', 'Books'])
).get()
# ormar OPTION 3 - NEW one (field access)
Product.objects.filter(
((Product.name='Test') & (Product.rating >= 3.0)) |
(Product.categories.name << ['Toys', 'Books'])
).get()
```
* Now you can also use field access to provide OrderActions to `order_by()`
* Order ascending:
* OLD: `Product.objects.order_by("name").all()`
* NEW: `Product.objects.order_by(Product.name.asc()).all()`
* Order descending:
* OLD: `Product.objects.order_by("-name").all()`
* NEW: `Product.objects.order_by(Product.name.desc()).all()`
* You can of course also combine different models and many order_bys:
`Product.objects.order_by([Product.category.name.asc(), Product.name.desc()]).all()`
## 🐛 Fixes
* Not really a bug but rather inconsistency. Providing a filter with nested model i.e. `album__category__name = 'AA'`
is checking if album and category models are included in `select_related()` and if not it's auto-adding them there.
The same functionality was not working for `FilterGroups` (`and_` and `or_`), now it works (also for python style filters which return `FilterGroups`).
# 0.10.3 # 0.10.3
## ✨ Features ## ✨ Features

BIN
examples/db.sqlite Normal file

Binary file not shown.

View File

@ -87,7 +87,10 @@ async def create():
async def read(): async def read():
# Fetch an instance, without loading a foreign key relationship on it. # Fetch an instance, without loading a foreign key relationship on it.
# Django style
book = await Book.objects.get(title="The Hobbit") book = await Book.objects.get(title="The Hobbit")
# or python style
book = await Book.objects.get(Book.title == "The Hobbit")
book2 = await Book.objects.first() book2 = await Book.objects.first()
# first() fetch the instance with lower primary key value # first() fetch the instance with lower primary key value
@ -193,7 +196,7 @@ async def joins():
# visit: https://collerek.github.io/ormar/relations/ # visit: https://collerek.github.io/ormar/relations/
# to read more about joins and subqueries # to read more about joins and subqueries
# visit: https://collerek.github.io/ormar/queries/delete/ # visit: https://collerek.github.io/ormar/queries/joins-and-subqueries/
async def filter_and_sort(): async def filter_and_sort():
@ -201,20 +204,30 @@ async def filter_and_sort():
# get(), all() etc. # get(), all() etc.
# to use special methods or access related model fields use double # to use special methods or access related model fields use double
# underscore like to filter by the name of the author use author__name # underscore like to filter by the name of the author use author__name
# Django style
books = await Book.objects.all(author__name="J.R.R. Tolkien") books = await Book.objects.all(author__name="J.R.R. Tolkien")
# python style
books = await Book.objects.all(Book.author.name == "J.R.R. Tolkien")
assert len(books) == 3 assert len(books) == 3
# filter can accept special methods also separated with double underscore # filter can accept special methods also separated with double underscore
# to issue sql query ` where authors.name like "%tolkien%"` that is not # to issue sql query ` where authors.name like "%tolkien%"` that is not
# case sensitive (hence small t in Tolkien) # case sensitive (hence small t in Tolkien)
# Django style
books = await Book.objects.filter(author__name__icontains="tolkien").all() books = await Book.objects.filter(author__name__icontains="tolkien").all()
# python style
books = await Book.objects.filter(Book.author.name.icontains("tolkien")).all()
assert len(books) == 3 assert len(books) == 3
# to sort use order_by() function of queryset # to sort use order_by() function of queryset
# to sort decreasing use hyphen before the field name # to sort decreasing use hyphen before the field name
# same as with filter you can use double underscores to access related fields # same as with filter you can use double underscores to access related fields
# Django style
books = await Book.objects.filter(author__name__icontains="tolkien").order_by( books = await Book.objects.filter(author__name__icontains="tolkien").order_by(
"-year").all() "-year").all()
# python style
books = await Book.objects.filter(Book.author.name.icontains("tolkien")).order_by(
Book.year.desc()).all()
assert len(books) == 3 assert len(books) == 3
assert books[0].title == "The Silmarillion" assert books[0].title == "The Silmarillion"
assert books[2].title == "The Hobbit" assert books[2].title == "The Hobbit"
@ -284,12 +297,24 @@ async def pagination():
async def aggregations(): async def aggregations():
# ormar currently supports count: # count:
assert 2 == await Author.objects.count() assert 2 == await Author.objects.count()
# and exists # exists
assert await Book.objects.filter(title="The Hobbit").exists() assert await Book.objects.filter(title="The Hobbit").exists()
# max
assert 1990 == await Book.objects.max(columns=["year"])
# min
assert 1937 == await Book.objects.min(columns=["year"])
# avg
assert 1964.75 == await Book.objects.avg(columns=["year"])
# sum
assert 7859 == await Book.objects.sum(columns=["year"])
# to read more about aggregated functions # to read more about aggregated functions
# visit: https://collerek.github.io/ormar/queries/aggregations/ # visit: https://collerek.github.io/ormar/queries/aggregations/

View File

@ -75,7 +75,7 @@ class UndefinedType: # pragma no cover
Undefined = UndefinedType() Undefined = UndefinedType()
__version__ = "0.10.3" __version__ = "0.10.4"
__all__ = [ __all__ = [
"Integer", "Integer",
"BigInteger", "BigInteger",

View File

@ -39,7 +39,7 @@ from ormar.models.helpers import (
sqlalchemy_columns_from_model_fields, sqlalchemy_columns_from_model_fields,
) )
from ormar.models.quick_access_views import quick_access_set from ormar.models.quick_access_views import quick_access_set
from ormar.queryset import QuerySet from ormar.queryset import FieldAccessor, QuerySet
from ormar.relations.alias_manager import AliasManager from ormar.relations.alias_manager import AliasManager
from ormar.signals import Signal, SignalEmitter from ormar.signals import Signal, SignalEmitter
@ -561,3 +561,20 @@ class ModelMetaclass(pydantic.main.ModelMetaclass):
f"need to call update_forward_refs()." f"need to call update_forward_refs()."
) )
return QuerySet(model_cls=cls) return QuerySet(model_cls=cls)
def __getattr__(self, item: str) -> Any:
if item in object.__getattribute__(self, "Meta").model_fields:
field = self.Meta.model_fields.get(item)
if field.is_relation:
return FieldAccessor(
source_model=cast(Type["Model"], self),
model=field.to,
access_chain=item,
)
else:
return FieldAccessor(
source_model=cast(Type["Model"], self),
field=field,
access_chain=item,
)
return object.__getattribute__(self, item)

View File

@ -3,6 +3,7 @@ Contains QuerySet and different Query classes to allow for constructing of sql q
""" """
from ormar.queryset.actions import FilterAction, OrderAction, SelectAction from ormar.queryset.actions import FilterAction, OrderAction, SelectAction
from ormar.queryset.clause import and_, or_ from ormar.queryset.clause import and_, or_
from ormar.queryset.field_accessor import FieldAccessor
from ormar.queryset.filter_query import FilterQuery from ormar.queryset.filter_query import FilterQuery
from ormar.queryset.limit_query import LimitQuery from ormar.queryset.limit_query import LimitQuery
from ormar.queryset.offset_query import OffsetQuery from ormar.queryset.offset_query import OffsetQuery
@ -20,4 +21,5 @@ __all__ = [
"SelectAction", "SelectAction",
"and_", "and_",
"or_", "or_",
"FieldAccessor",
] ]

View File

@ -26,6 +26,23 @@ FILTER_OPERATORS = {
"lt": "__lt__", "lt": "__lt__",
"lte": "__le__", "lte": "__le__",
} }
METHODS_TO_OPERATORS = {
"__eq__": "exact",
"__mod__": "contains",
"__gt__": "gt",
"__ge__": "gte",
"__lt__": "lt",
"__le__": "lte",
"iexact": "iexact",
"contains": "contains",
"icontains": "icontains",
"startswith": "startswith",
"istartswith": "istartswith",
"endswith": "endswith",
"iendswith": "iendswith",
"isnull": "isnull",
"in": "in",
}
ESCAPE_CHARACTERS = ["%", "_"] ESCAPE_CHARACTERS = ["%", "_"]
@ -159,5 +176,8 @@ class FilterAction(QueryAction):
clause_text = clause_text.replace( clause_text = clause_text.replace(
f"{self.table.name}.{self.column.name}", aliased_name f"{self.table.name}.{self.column.name}", aliased_name
) )
dialect_name = self.target_model.Meta.database._backend._dialect.name
if dialect_name != "sqlite": # pragma: no cover
clause_text = clause_text.replace("%%", "%") # remove %% in some dialects
clause = text(clause_text) clause = text(clause_text)
return clause return clause

View File

@ -25,16 +25,30 @@ class FilterGroup:
""" """
def __init__( def __init__(
self, *args: Any, _filter_type: FilterType = FilterType.AND, **kwargs: Any, self,
*args: Any,
_filter_type: FilterType = FilterType.AND,
_exclude: bool = False,
**kwargs: Any,
) -> None: ) -> None:
self.filter_type = _filter_type self.filter_type = _filter_type
self.exclude = False self.exclude = _exclude
self._nested_groups: List["FilterGroup"] = list(args) self._nested_groups: List["FilterGroup"] = list(args)
self._resolved = False self._resolved = False
self.is_source_model_filter = False self.is_source_model_filter = False
self._kwargs_dict = kwargs self._kwargs_dict = kwargs
self.actions: List[FilterAction] = [] self.actions: List[FilterAction] = []
def __and__(self, other: "FilterGroup") -> "FilterGroup":
return FilterGroup(self, other)
def __or__(self, other: "FilterGroup") -> "FilterGroup":
return FilterGroup(self, other, _filter_type=FilterType.OR)
def __invert__(self) -> "FilterGroup":
self.exclude = not self.exclude
return self
def resolve( def resolve(
self, self,
model_cls: Type["Model"], model_cls: Type["Model"],
@ -107,13 +121,18 @@ class FilterGroup:
:return: complied and escaped clause :return: complied and escaped clause
:rtype: sqlalchemy.sql.elements.TextClause :rtype: sqlalchemy.sql.elements.TextClause
""" """
prefix = " NOT " if self.exclude else ""
if self.filter_type == FilterType.AND: if self.filter_type == FilterType.AND:
clause = sqlalchemy.text( clause = sqlalchemy.text(
"( " + str(sqlalchemy.sql.and_(*self._get_text_clauses())) + " )" f"{prefix}( "
+ str(sqlalchemy.sql.and_(*self._get_text_clauses()))
+ " )"
) )
else: else:
clause = sqlalchemy.text( clause = sqlalchemy.text(
"( " + str(sqlalchemy.sql.or_(*self._get_text_clauses())) + " )" f"{prefix}( "
+ str(sqlalchemy.sql.or_(*self._get_text_clauses()))
+ " )"
) )
return clause return clause

View File

@ -0,0 +1,116 @@
from typing import Any, TYPE_CHECKING, Type
from ormar.queryset.actions import OrderAction
from ormar.queryset.actions.filter_action import METHODS_TO_OPERATORS
from ormar.queryset.clause import FilterGroup
if TYPE_CHECKING: # pragma: no cover
from ormar import BaseField, Model
class FieldAccessor:
def __init__(
self,
source_model: Type["Model"],
field: "BaseField" = None,
model: Type["Model"] = None,
access_chain: str = "",
) -> None:
self._source_model = source_model
self._field = field
self._model = model
self._access_chain = access_chain
def __bool__(self) -> bool:
# hack to avoid pydantic name check from parent model
return False
def __getattr__(self, item: str) -> Any:
if self._field and item == self._field.name:
return self._field
if self._model and item in self._model.Meta.model_fields:
field = self._model.Meta.model_fields[item]
if field.is_relation:
return FieldAccessor(
source_model=self._source_model,
model=field.to,
access_chain=self._access_chain + f"__{item}",
)
else:
return FieldAccessor(
source_model=self._source_model,
field=field,
access_chain=self._access_chain + f"__{item}",
)
return object.__getattribute__(self, item) # pragma: no cover
def _check_field(self) -> None:
if not self._field:
raise AttributeError(
"Cannot filter by Model, you need to provide model name"
)
def _select_operator(self, op: str, other: Any) -> FilterGroup:
self._check_field()
filter_kwg = {self._access_chain + f"__{METHODS_TO_OPERATORS[op]}": other}
return FilterGroup(**filter_kwg)
def __eq__(self, other: Any) -> FilterGroup: # type: ignore
return self._select_operator(op="__eq__", other=other)
def __ge__(self, other: Any) -> FilterGroup:
return self._select_operator(op="__ge__", other=other)
def __gt__(self, other: Any) -> FilterGroup:
return self._select_operator(op="__gt__", other=other)
def __le__(self, other: Any) -> FilterGroup:
return self._select_operator(op="__le__", other=other)
def __lt__(self, other: Any) -> FilterGroup:
return self._select_operator(op="__lt__", other=other)
def __mod__(self, other: Any) -> FilterGroup:
return self._select_operator(op="__mod__", other=other)
def __lshift__(self, other: Any) -> FilterGroup:
return self._select_operator(op="in", other=other)
def __rshift__(self, other: Any) -> FilterGroup:
return self._select_operator(op="isnull", other=True)
def in_(self, other: Any) -> FilterGroup:
return self._select_operator(op="in", other=other)
def iexact(self, other: Any) -> FilterGroup:
return self._select_operator(op="iexact", other=other)
def contains(self, other: Any) -> FilterGroup:
return self._select_operator(op="contains", other=other)
def icontains(self, other: Any) -> FilterGroup:
return self._select_operator(op="icontains", other=other)
def startswith(self, other: Any) -> FilterGroup:
return self._select_operator(op="startswith", other=other)
def istartswith(self, other: Any) -> FilterGroup:
return self._select_operator(op="istartswith", other=other)
def endswith(self, other: Any) -> FilterGroup:
return self._select_operator(op="endswith", other=other)
def iendswith(self, other: Any) -> FilterGroup:
return self._select_operator(op="iendswith", other=other)
def isnull(self, other: Any) -> FilterGroup:
return self._select_operator(op="isnull", other=other)
def asc(self) -> OrderAction:
return OrderAction(order_str=self._access_chain, model_cls=self._source_model)
def desc(self) -> OrderAction:
return OrderAction(
order_str="-" + self._access_chain, model_cls=self._source_model
)

View File

@ -7,6 +7,7 @@ from typing import (
Sequence, Sequence,
Set, Set,
TYPE_CHECKING, TYPE_CHECKING,
Tuple,
Type, Type,
TypeVar, TypeVar,
Union, Union,
@ -180,16 +181,19 @@ class QuerySet(Generic[T]):
return self.model.merge_instances_list(result_rows) # type: ignore return self.model.merge_instances_list(result_rows) # type: ignore
return cast(List[Optional["T"]], result_rows) return cast(List[Optional["T"]], result_rows)
def _resolve_filter_groups(self, groups: Any) -> List[FilterGroup]: def _resolve_filter_groups(
self, groups: Any
) -> Tuple[List[FilterGroup], List[str]]:
""" """
Resolves filter groups to populate FilterAction params in group tree. Resolves filter groups to populate FilterAction params in group tree.
:param groups: tuple of FilterGroups :param groups: tuple of FilterGroups
:type groups: Any :type groups: Any
:return: list of resolver groups :return: list of resolver groups
:rtype: List[FilterGroup] :rtype: Tuple[List[FilterGroup], List[str]]
""" """
filter_groups = [] filter_groups = []
select_related = self._select_related
if groups: if groups:
for group in groups: for group in groups:
if not isinstance(group, FilterGroup): if not isinstance(group, FilterGroup):
@ -200,13 +204,13 @@ class QuerySet(Generic[T]):
"other values need to be passed by" "other values need to be passed by"
"keyword arguments" "keyword arguments"
) )
group.resolve( _, select_related = group.resolve(
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,
) )
filter_groups.append(group) filter_groups.append(group)
return filter_groups return filter_groups, select_related
@staticmethod @staticmethod
def check_single_result_rows_count(rows: Sequence[Optional["T"]]) -> None: def check_single_result_rows_count(rows: Sequence[Optional["T"]]) -> None:
@ -304,10 +308,10 @@ class QuerySet(Generic[T]):
:return: filtered QuerySet :return: filtered QuerySet
:rtype: QuerySet :rtype: QuerySet
""" """
filter_groups = self._resolve_filter_groups(groups=args) filter_groups, select_related = self._resolve_filter_groups(groups=args)
qryclause = QueryClause( qryclause = QueryClause(
model_cls=self.model, model_cls=self.model,
select_related=self._select_related, select_related=select_related,
filter_clauses=self.filter_clauses, filter_clauses=self.filter_clauses,
) )
filter_clauses, select_related = qryclause.prepare_filter(**kwargs) filter_clauses, select_related = qryclause.prepare_filter(**kwargs)
@ -504,7 +508,7 @@ class QuerySet(Generic[T]):
""" """
return self.fields(columns=columns, _is_exclude=True) return self.fields(columns=columns, _is_exclude=True)
def order_by(self, columns: Union[List, str]) -> "QuerySet[T]": def order_by(self, columns: Union[List, str, OrderAction]) -> "QuerySet[T]":
""" """
With `order_by()` you can order the results from database based on your With `order_by()` you can order the results from database based on your
choice of fields. choice of fields.
@ -541,6 +545,8 @@ class QuerySet(Generic[T]):
orders_by = [ orders_by = [
OrderAction(order_str=x, model_cls=self.model_cls) # type: ignore OrderAction(order_str=x, model_cls=self.model_cls) # type: ignore
if not isinstance(x, OrderAction)
else x
for x in columns for x in columns
] ]
@ -671,7 +677,7 @@ class QuerySet(Generic[T]):
) )
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, *args: Any, each: bool = False, **kwargs: Any) -> int:
""" """
Deletes from the model table after applying the filters from kwargs. Deletes from the model table after applying the filters from kwargs.
@ -685,8 +691,8 @@ class QuerySet(Generic[T]):
:return: number of deleted rows :return: number of deleted rows
:rtype:int :rtype:int
""" """
if kwargs: if kwargs or args:
return await self.filter(**kwargs).delete() return await self.filter(*args, **kwargs).delete()
if not each and not (self.filter_clauses or self.exclude_clauses): if not each and not (self.filter_clauses or self.exclude_clauses):
raise QueryDefinitionError( raise QueryDefinitionError(
"You cannot delete without filtering the queryset first. " "You cannot delete without filtering the queryset first. "
@ -753,7 +759,7 @@ class QuerySet(Generic[T]):
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.rebuild_self(offset=offset, limit_raw_sql=limit_raw_sql,) return self.rebuild_self(offset=offset, limit_raw_sql=limit_raw_sql,)
async def first(self, **kwargs: Any) -> "T": async def first(self, *args: Any, **kwargs: Any) -> "T":
""" """
Gets the first row from the db ordered by primary key column ascending. Gets the first row from the db ordered by primary key column ascending.
@ -764,8 +770,8 @@ class QuerySet(Generic[T]):
:return: returned model :return: returned model
:rtype: Model :rtype: Model
""" """
if kwargs: if kwargs or args:
return await self.filter(**kwargs).first() return await self.filter(*args, **kwargs).first()
expr = self.build_select_expression( expr = self.build_select_expression(
limit=1, limit=1,
@ -784,7 +790,7 @@ class QuerySet(Generic[T]):
self.check_single_result_rows_count(processed_rows) self.check_single_result_rows_count(processed_rows)
return processed_rows[0] # type: ignore return processed_rows[0] # type: ignore
async def get_or_none(self, **kwargs: Any) -> Optional["T"]: async def get_or_none(self, *args: Any, **kwargs: Any) -> Optional["T"]:
""" """
Get's the first row from the db meeting the criteria set by kwargs. Get's the first row from the db meeting the criteria set by kwargs.
@ -800,11 +806,11 @@ class QuerySet(Generic[T]):
:rtype: Model :rtype: Model
""" """
try: try:
return await self.get(**kwargs) return await self.get(*args, **kwargs)
except ormar.NoMatch: except ormar.NoMatch:
return None return None
async def get(self, **kwargs: Any) -> "T": async def get(self, *args: Any, **kwargs: Any) -> "T":
""" """
Get's the first row from the db meeting the criteria set by kwargs. Get's the first row from the db meeting the criteria set by kwargs.
@ -819,8 +825,8 @@ class QuerySet(Generic[T]):
:return: returned model :return: returned model
:rtype: Model :rtype: Model
""" """
if kwargs: if kwargs or args:
return await self.filter(**kwargs).get() return await self.filter(*args, **kwargs).get()
if not self.filter_clauses: if not self.filter_clauses:
expr = self.build_select_expression( expr = self.build_select_expression(
@ -843,7 +849,7 @@ class QuerySet(Generic[T]):
self.check_single_result_rows_count(processed_rows) self.check_single_result_rows_count(processed_rows)
return processed_rows[0] # type: ignore return processed_rows[0] # type: ignore
async def get_or_create(self, **kwargs: Any) -> "T": async def get_or_create(self, *args: Any, **kwargs: Any) -> "T":
""" """
Combination of create and get methods. Combination of create and get methods.
@ -857,7 +863,7 @@ class QuerySet(Generic[T]):
:rtype: Model :rtype: Model
""" """
try: try:
return await self.get(**kwargs) return await self.get(*args, **kwargs)
except NoMatch: except NoMatch:
return await self.create(**kwargs) return await self.create(**kwargs)
@ -878,7 +884,7 @@ class QuerySet(Generic[T]):
model = await self.get(pk=kwargs[pk_name]) model = await self.get(pk=kwargs[pk_name])
return await model.update(**kwargs) return await model.update(**kwargs)
async def all(self, **kwargs: Any) -> List[Optional["T"]]: # noqa: A003 async def all(self, *args: Any, **kwargs: Any) -> List[Optional["T"]]: # noqa: A003
""" """
Returns all rows from a database for given model for set filter options. Returns all rows from a database for given model for set filter options.
@ -891,8 +897,8 @@ class QuerySet(Generic[T]):
:return: list of returned models :return: list of returned models
:rtype: List[Model] :rtype: List[Model]
""" """
if kwargs: if kwargs or args:
return await self.filter(**kwargs).all() return await self.filter(*args, **kwargs).all()
expr = self.build_select_expression() expr = self.build_select_expression()
rows = await self.database.fetch_all(expr) rows = await self.database.fetch_all(expr)

View File

@ -22,7 +22,7 @@ if TYPE_CHECKING: # pragma no cover
from ormar.relations import Relation from ormar.relations import Relation
from ormar.models import Model, T from ormar.models import Model, T
from ormar.queryset import QuerySet from ormar.queryset import QuerySet
from ormar import RelationType from ormar import OrderAction, RelationType
else: else:
T = TypeVar("T", bound="Model") T = TypeVar("T", bound="Model")
@ -276,7 +276,7 @@ class QuerysetProxy(Generic[T]):
) )
return await queryset.delete(**kwargs) # type: ignore return await queryset.delete(**kwargs) # type: ignore
async def first(self, **kwargs: Any) -> "T": async def first(self, *args: Any, **kwargs: Any) -> "T":
""" """
Gets the first row from the db ordered by primary key column ascending. Gets the first row from the db ordered by primary key column ascending.
@ -289,12 +289,12 @@ class QuerysetProxy(Generic[T]):
:return: :return:
:rtype: _asyncio.Future :rtype: _asyncio.Future
""" """
first = await self.queryset.first(**kwargs) first = await self.queryset.first(*args, **kwargs)
self._clean_items_on_load() self._clean_items_on_load()
self._register_related(first) self._register_related(first)
return first return first
async def get_or_none(self, **kwargs: Any) -> Optional["T"]: async def get_or_none(self, *args: Any, **kwargs: Any) -> Optional["T"]:
""" """
Get's the first row from the db meeting the criteria set by kwargs. Get's the first row from the db meeting the criteria set by kwargs.
@ -310,7 +310,7 @@ class QuerysetProxy(Generic[T]):
:rtype: Model :rtype: Model
""" """
try: try:
get = await self.queryset.get(**kwargs) get = await self.queryset.get(*args, **kwargs)
except ormar.NoMatch: except ormar.NoMatch:
return None return None
@ -318,7 +318,7 @@ class QuerysetProxy(Generic[T]):
self._register_related(get) self._register_related(get)
return get return get
async def get(self, **kwargs: Any) -> "T": async def get(self, *args: Any, **kwargs: Any) -> "T":
""" """
Get's the first row from the db meeting the criteria set by kwargs. Get's the first row from the db meeting the criteria set by kwargs.
@ -337,12 +337,12 @@ class QuerysetProxy(Generic[T]):
:return: returned model :return: returned model
:rtype: Model :rtype: Model
""" """
get = await self.queryset.get(**kwargs) get = await self.queryset.get(*args, **kwargs)
self._clean_items_on_load() self._clean_items_on_load()
self._register_related(get) self._register_related(get)
return get return get
async def all(self, **kwargs: Any) -> List[Optional["T"]]: # noqa: A003 async def all(self, *args: Any, **kwargs: Any) -> List[Optional["T"]]: # noqa: A003
""" """
Returns all rows from a database for given model for set filter options. Returns all rows from a database for given model for set filter options.
@ -359,7 +359,7 @@ class QuerysetProxy(Generic[T]):
:return: list of returned models :return: list of returned models
:rtype: List[Model] :rtype: List[Model]
""" """
all_items = await self.queryset.all(**kwargs) all_items = await self.queryset.all(*args, **kwargs)
self._clean_items_on_load() self._clean_items_on_load()
self._register_related(all_items) self._register_related(all_items)
return all_items return all_items
@ -425,7 +425,7 @@ class QuerysetProxy(Generic[T]):
) )
return len(children) return len(children)
async def get_or_create(self, **kwargs: Any) -> "T": async def get_or_create(self, *args: Any, **kwargs: Any) -> "T":
""" """
Combination of create and get methods. Combination of create and get methods.
@ -439,7 +439,7 @@ class QuerysetProxy(Generic[T]):
:rtype: Model :rtype: Model
""" """
try: try:
return await self.get(**kwargs) return await self.get(*args, **kwargs)
except ormar.NoMatch: except ormar.NoMatch:
return await self.create(**kwargs) return await self.create(**kwargs)
@ -739,7 +739,7 @@ class QuerysetProxy(Generic[T]):
relation=self.relation, type_=self.type_, to=self.to, qryset=queryset relation=self.relation, type_=self.type_, to=self.to, qryset=queryset
) )
def order_by(self, columns: Union[List, str]) -> "QuerysetProxy[T]": def order_by(self, columns: Union[List, str, "OrderAction"]) -> "QuerysetProxy[T]":
""" """
With `order_by()` you can order the results from database based on your With `order_by()` you can order the results from database based on your
choice of fields. choice of fields.

View File

@ -1,5 +1,5 @@
import json import json
from typing import Optional from typing import Any, Dict, Optional, Set, Type, Union, cast
import databases import databases
import pytest import pytest
@ -8,6 +8,7 @@ from fastapi import FastAPI
from starlette.testclient import TestClient from starlette.testclient import TestClient
import ormar import ormar
from ormar.queryset.utils import translate_list_to_dict
from tests.settings import DATABASE_URL from tests.settings import DATABASE_URL
app = FastAPI() app = FastAPI()
@ -84,6 +85,24 @@ to_exclude_ormar = {
} }
def auto_exclude_id_field(to_exclude: Any) -> Union[Dict, Set]:
if isinstance(to_exclude, dict):
for key in to_exclude.keys():
to_exclude[key] = auto_exclude_id_field(to_exclude[key])
to_exclude["id"] = Ellipsis
return to_exclude
else:
return {"id"}
def generate_exclude_for_ids(model: Type[ormar.Model]) -> Dict:
to_exclude_base = translate_list_to_dict(model._iterate_related_models())
return cast(Dict, auto_exclude_id_field(to_exclude=to_exclude_base))
to_exclude_auto = generate_exclude_for_ids(model=Department)
@app.post("/departments/", response_model=Department) @app.post("/departments/", response_model=Department)
async def create_department(department: Department): async def create_department(department: Department):
await department.save_related(follow=True, save_all=True) await department.save_related(follow=True, save_all=True)

View File

@ -0,0 +1,208 @@
import databases
import pytest
import sqlalchemy
import ormar
from ormar import BaseField
from tests.settings import DATABASE_URL
database = databases.Database(DATABASE_URL, force_rollback=True)
metadata = sqlalchemy.MetaData()
class BaseMeta(ormar.ModelMeta):
metadata = metadata
database = database
class PriceList(ormar.Model):
class Meta(BaseMeta):
tablename = "price_lists"
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
class Category(ormar.Model):
class Meta(BaseMeta):
tablename = "categories"
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
price_lists = ormar.ManyToMany(PriceList, related_name="categories")
class Product(ormar.Model):
class Meta(BaseMeta):
tablename = "product"
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
rating: float = ormar.Float(minimum=1, maximum=5)
category = ormar.ForeignKey(Category)
@pytest.fixture(autouse=True, scope="module")
def create_test_database():
engine = sqlalchemy.create_engine(DATABASE_URL)
metadata.drop_all(engine)
metadata.create_all(engine)
yield
metadata.drop_all(engine)
def test_fields_access():
# basic access
assert Product.id._field == Product.Meta.model_fields["id"]
assert Product.id.id == Product.Meta.model_fields["id"]
assert isinstance(Product.id._field, BaseField)
assert Product.id._access_chain == "id"
assert Product.id._source_model == Product
# nested models
curr_field = Product.category.name
assert curr_field._field == Category.Meta.model_fields["name"]
assert curr_field._access_chain == "category__name"
assert curr_field._source_model == Product
# deeper nesting
curr_field = Product.category.price_lists.name
assert curr_field._field == PriceList.Meta.model_fields["name"]
assert curr_field._access_chain == "category__price_lists__name"
assert curr_field._source_model == Product
# reverse nesting
curr_field = PriceList.categories.products.rating
assert curr_field._field == Product.Meta.model_fields["rating"]
assert curr_field._access_chain == "categories__products__rating"
assert curr_field._source_model == PriceList
with pytest.raises(AttributeError):
assert Product.category >= 3
@pytest.mark.parametrize(
"method, expected, expected_value",
[
("__eq__", "exact", "Test"),
("__lt__", "lt", "Test"),
("__le__", "lte", "Test"),
("__ge__", "gte", "Test"),
("__gt__", "gt", "Test"),
("iexact", "iexact", "Test"),
("contains", "contains", "Test"),
("icontains", "icontains", "Test"),
("startswith", "startswith", "Test"),
("istartswith", "istartswith", "Test"),
("endswith", "endswith", "Test"),
("iendswith", "iendswith", "Test"),
("isnull", "isnull", "Test"),
("in_", "in", "Test"),
("__lshift__", "in", "Test"),
("__rshift__", "isnull", True),
("__mod__", "contains", "Test"),
],
)
def test_operator_return_proper_filter_action(method, expected, expected_value):
group_ = getattr(Product.name, method)("Test")
assert group_._kwargs_dict == {f"name__{expected}": expected_value}
group_ = getattr(Product.category.name, method)("Test")
assert group_._kwargs_dict == {f"category__name__{expected}": expected_value}
group_ = getattr(PriceList.categories.products.rating, method)("Test")
assert group_._kwargs_dict == {
f"categories__products__rating__{expected}": expected_value
}
@pytest.mark.parametrize("method, expected_direction", [("asc", ""), ("desc", "desc"),])
def test_operator_return_proper_order_action(method, expected_direction):
action = getattr(Product.name, method)()
assert action.source_model == Product
assert action.target_model == Product
assert action.direction == expected_direction
assert action.is_source_model_order
action = getattr(Product.category.name, method)()
assert action.source_model == Product
assert action.target_model == Category
assert action.direction == expected_direction
assert not action.is_source_model_order
action = getattr(PriceList.categories.products.rating, method)()
assert action.source_model == PriceList
assert action.target_model == Product
assert action.direction == expected_direction
assert not action.is_source_model_order
def test_combining_groups_together():
group = (Product.name == "Test") & (Product.rating >= 3.0)
group.resolve(model_cls=Product)
assert len(group._nested_groups) == 2
assert str(group.get_text_clause()) == (
"( ( product.name = 'Test' ) AND" " ( product.rating >= 3.0 ) )"
)
group = ~((Product.name == "Test") & (Product.rating >= 3.0))
group.resolve(model_cls=Product)
assert len(group._nested_groups) == 2
assert str(group.get_text_clause()) == (
" NOT ( ( product.name = 'Test' ) AND" " ( product.rating >= 3.0 ) )"
)
group = ((Product.name == "Test") & (Product.rating >= 3.0)) | (
Product.category.name << (["Toys", "Books"])
)
group.resolve(model_cls=Product)
assert len(group._nested_groups) == 2
assert len(group._nested_groups[0]._nested_groups) == 2
group_str = str(group.get_text_clause())
category_prefix = group._nested_groups[1].actions[0].table_prefix
assert group_str == (
"( ( ( product.name = 'Test' ) AND ( product.rating >= 3.0 ) ) "
f"OR ( {category_prefix}_categories.name IN ('Toys', 'Books') ) )"
)
group = (Product.name % "Test") | (
(Product.category.price_lists.name.startswith("Aa"))
| (Product.category.name << (["Toys", "Books"]))
)
group.resolve(model_cls=Product)
assert len(group._nested_groups) == 2
assert len(group._nested_groups[1]._nested_groups) == 2
group_str = str(group.get_text_clause())
price_list_prefix = (
group._nested_groups[1]._nested_groups[0].actions[0].table_prefix
)
category_prefix = group._nested_groups[1]._nested_groups[1].actions[0].table_prefix
assert group_str == (
f"( ( product.name LIKE '%Test%' ) "
f"OR ( ( {price_list_prefix}_price_lists.name LIKE 'Aa%' ) "
f"OR ( {category_prefix}_categories.name IN ('Toys', 'Books') ) ) )"
)
# @pytest.mark.asyncio
# async def test_filtering_by_field_access():
# async with database:
# async with database.transaction(force_rollback=True):
# category = await Category(name='Toys').save()
# product1 = await Product(name="G.I Joe",
# rating=4.7,
# category=category).save()
# product2 = await Product(name="My Little Pony",
# rating=3.8,
# category=category).save()
#
# check = Product.object.get(Product.name == "My Little Pony")
# assert check == product2
# TODO: Finish implementation
# * overload operators and add missing functions that return FilterAction (V)
# * return OrderAction for desc() and asc() (V)
# * create filter groups for & and | (and ~ - NOT?) (V)
# * accept args in all functions that accept filters? or only filter and exclude? (V)
# all functions: delete, first, get, get_or_none, get_or_create, all, filter, exclude
# * accept OrderActions in order_by (V)

View File

@ -0,0 +1,61 @@
from typing import Optional
import databases
import pytest
import sqlalchemy
from pydantic import HttpUrl
import ormar
from tests.settings import DATABASE_URL
database = databases.Database(DATABASE_URL)
metadata = sqlalchemy.MetaData()
class BaseMeta(ormar.ModelMeta):
metadata = metadata
database = database
class Test(ormar.Model):
class Meta(BaseMeta):
pass
def __init__(self, **kwargs):
# you need to pop non - db fields as ormar will complain that it's unknown field
url = kwargs.pop("url", self.__fields__["url"].get_default())
super().__init__(**kwargs)
self.url = url
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=200)
url: HttpUrl = "www.example.com" # field with default
@pytest.fixture(autouse=True, scope="module")
def create_test_database():
engine = sqlalchemy.create_engine(DATABASE_URL)
metadata.drop_all(engine)
metadata.create_all(engine)
yield
metadata.drop_all(engine)
@pytest.mark.asyncio
async def test_working_with_pydantic_fields():
async with database:
test = Test(name="Test")
assert test.name == "Test"
assert test.url == "www.example.com"
test.url = "www.sdta.ada.pt"
assert test.url == "www.sdta.ada.pt"
await test.save()
test_check = await Test.objects.get()
assert test_check.name == "Test"
assert test_check.url == "www.example.com"
# TODO add validate assignment to pydantic config
# test_check.email = 1

View File

@ -70,6 +70,14 @@ async def test_or_filters():
assert len(books) == 4 assert len(books) == 4
assert not any([x.title == "The Tower of Fools" for x in books]) assert not any([x.title == "The Tower of Fools" for x in books])
books = (
await Book.objects.select_related("author")
.filter((Book.author.name == "J.R.R. Tolkien") | (Book.year < 1995))
.all()
)
assert len(books) == 4
assert not any([x.title == "The Tower of Fools" for x in books])
books = ( books = (
await Book.objects.select_related("author") await Book.objects.select_related("author")
.filter(ormar.or_(year__gt=1960, year__lt=1940)) .filter(ormar.or_(year__gt=1960, year__lt=1940))
@ -110,6 +118,26 @@ 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(
(
(
(Book.year > 1960) & (Book.author.name == "J.R.R. Tolkien")
| (
(Book.year < 2000)
& (Book.author.name == "Andrzej Sapkowski")
)
)
& (Book.title.startswith("The"))
),
)
.all()
)
assert len(books) == 2
assert books[0].title == "The Silmarillion"
assert books[1].title == "The Witcher"
books = ( books = (
await Book.objects.select_related("author") await Book.objects.select_related("author")
.filter( .filter(

View File

@ -122,11 +122,21 @@ async def test_sort_order_on_main_model():
assert songs[1].name == "Song 2" assert songs[1].name == "Song 2"
assert songs[2].name == "Song 1" assert songs[2].name == "Song 1"
songs = await Song.objects.order_by(Song.sort_order.desc()).all()
assert songs[0].name == "Song 3"
assert songs[1].name == "Song 2"
assert songs[2].name == "Song 1"
songs = await Song.objects.order_by("sort_order").all() songs = await Song.objects.order_by("sort_order").all()
assert songs[0].name == "Song 1" assert songs[0].name == "Song 1"
assert songs[1].name == "Song 2" assert songs[1].name == "Song 2"
assert songs[2].name == "Song 3" assert songs[2].name == "Song 3"
songs = await Song.objects.order_by(Song.sort_order.asc()).all()
assert songs[0].name == "Song 1"
assert songs[1].name == "Song 2"
assert songs[2].name == "Song 3"
songs = await Song.objects.order_by("name").all() songs = await Song.objects.order_by("name").all()
assert songs[0].name == "Song 1" assert songs[0].name == "Song 1"
assert songs[1].name == "Song 2" assert songs[1].name == "Song 2"
@ -145,6 +155,14 @@ async def test_sort_order_on_main_model():
assert songs[2].name == "Song 2" assert songs[2].name == "Song 2"
assert songs[3].name == "Song 3" assert songs[3].name == "Song 3"
songs = await Song.objects.order_by(
[Song.sort_order.asc(), Song.name.asc()]
).all()
assert songs[0].name == "Song 1"
assert songs[1].name == "Song 4"
assert songs[2].name == "Song 2"
assert songs[3].name == "Song 3"
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_sort_order_on_related_model(): async def test_sort_order_on_related_model():