diff --git a/README.md b/README.md index 13a7823..582f2f5 100644 --- a/README.md +++ b/README.md @@ -220,7 +220,10 @@ async def create(): async def read(): # Fetch an instance, without loading a foreign key relationship on it. + # Django style 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() # first() fetch the instance with lower primary key value @@ -334,20 +337,30 @@ async def filter_and_sort(): # get(), all() etc. # to use special methods or access related model fields use double # 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") + # python style + books = await Book.objects.all(Book.author.name == "J.R.R. Tolkien") assert len(books) == 3 # filter can accept special methods also separated with double underscore # to issue sql query ` where authors.name like "%tolkien%"` that is not # case sensitive (hence small t in Tolkien) + # Django style 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 # to sort use order_by() function of queryset # to sort decreasing use hyphen before the field name # 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( "-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 books[0].title == "The Silmarillion" assert books[2].title == "The Hobbit" @@ -417,12 +430,24 @@ async def pagination(): async def aggregations(): - # ormar currently supports count: + # count: assert 2 == await Author.objects.count() - # and exists + # 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 # visit: https://collerek.github.io/ormar/queries/aggregations/ @@ -448,16 +473,16 @@ metadata.drop_all(engine) ### QuerySet methods * `create(**kwargs): -> Model` -* `get(**kwargs): -> Model` -* `get_or_none(**kwargs): -> Optional[Model]` -* `get_or_create(**kwargs) -> Model` -* `first(): -> Model` +* `get(*args, **kwargs): -> Model` +* `get_or_none(*args, **kwargs): -> Optional[Model]` +* `get_or_create(*args, **kwargs) -> Model` +* `first(*args, **kwargs): -> Model` * `update(each: bool = False, **kwargs) -> int` * `update_or_create(**kwargs) -> Model` * `bulk_create(objects: List[Model]) -> None` * `bulk_update(objects: List[Model], columns: List[str] = None) -> None` -* `delete(each: bool = False, **kwargs) -> int` -* `all(**kwargs) -> List[Optional[Model]]` +* `delete(*args, each: bool = False, **kwargs) -> int` +* `all(*args, **kwargs) -> List[Optional[Model]]` * `filter(*args, **kwargs) -> QuerySet` * `exclude(*args, **kwargs) -> QuerySet` * `select_related(related: Union[List, str]) -> QuerySet` @@ -466,6 +491,10 @@ metadata.drop_all(engine) * `offset(offset: int) -> QuerySet` * `count() -> int` * `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` * `exclude_fields(columns: Union[List, str, set, dict]) -> QuerySet` * `order_by(columns:Union[List, str]) -> QuerySet` diff --git a/docs/index.md b/docs/index.md index 13a7823..582f2f5 100644 --- a/docs/index.md +++ b/docs/index.md @@ -220,7 +220,10 @@ async def create(): async def read(): # Fetch an instance, without loading a foreign key relationship on it. + # Django style 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() # first() fetch the instance with lower primary key value @@ -334,20 +337,30 @@ async def filter_and_sort(): # get(), all() etc. # to use special methods or access related model fields use double # 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") + # python style + books = await Book.objects.all(Book.author.name == "J.R.R. Tolkien") assert len(books) == 3 # filter can accept special methods also separated with double underscore # to issue sql query ` where authors.name like "%tolkien%"` that is not # case sensitive (hence small t in Tolkien) + # Django style 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 # to sort use order_by() function of queryset # to sort decreasing use hyphen before the field name # 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( "-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 books[0].title == "The Silmarillion" assert books[2].title == "The Hobbit" @@ -417,12 +430,24 @@ async def pagination(): async def aggregations(): - # ormar currently supports count: + # count: assert 2 == await Author.objects.count() - # and exists + # 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 # visit: https://collerek.github.io/ormar/queries/aggregations/ @@ -448,16 +473,16 @@ metadata.drop_all(engine) ### QuerySet methods * `create(**kwargs): -> Model` -* `get(**kwargs): -> Model` -* `get_or_none(**kwargs): -> Optional[Model]` -* `get_or_create(**kwargs) -> Model` -* `first(): -> Model` +* `get(*args, **kwargs): -> Model` +* `get_or_none(*args, **kwargs): -> Optional[Model]` +* `get_or_create(*args, **kwargs) -> Model` +* `first(*args, **kwargs): -> Model` * `update(each: bool = False, **kwargs) -> int` * `update_or_create(**kwargs) -> Model` * `bulk_create(objects: List[Model]) -> None` * `bulk_update(objects: List[Model], columns: List[str] = None) -> None` -* `delete(each: bool = False, **kwargs) -> int` -* `all(**kwargs) -> List[Optional[Model]]` +* `delete(*args, each: bool = False, **kwargs) -> int` +* `all(*args, **kwargs) -> List[Optional[Model]]` * `filter(*args, **kwargs) -> QuerySet` * `exclude(*args, **kwargs) -> QuerySet` * `select_related(related: Union[List, str]) -> QuerySet` @@ -466,6 +491,10 @@ metadata.drop_all(engine) * `offset(offset: int) -> QuerySet` * `count() -> int` * `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` * `exclude_fields(columns: Union[List, str, set, dict]) -> QuerySet` * `order_by(columns:Union[List, str]) -> QuerySet` diff --git a/docs/queries/filter-and-sort.md b/docs/queries/filter-and-sort.md index ab2625b..4045f10 100644 --- a/docs/queries/filter-and-sort.md +++ b/docs/queries/filter-and-sort.md @@ -2,27 +2,27 @@ You can use following methods to filter the data (sql where clause). -* `filter(**kwargs) -> QuerySet` -* `exclude(**kwargs) -> QuerySet` -* `get(**kwargs) -> Model` -* `get_or_none(**kwargs) -> Optional[Model]` -* `get_or_create(**kwargs) -> Model` -* `all(**kwargs) -> List[Optional[Model]]` +* `filter(*args, **kwargs) -> QuerySet` +* `exclude(*args, **kwargs) -> QuerySet` +* `get(*args, **kwargs) -> Model` +* `get_or_none(*args, **kwargs) -> Optional[Model]` +* `get_or_create(*args, **kwargs) -> Model` +* `all(*args, **kwargs) -> List[Optional[Model]]` * `QuerysetProxy` - * `QuerysetProxy.filter(**kwargs)` method - * `QuerysetProxy.exclude(**kwargs)` method - * `QuerysetProxy.get(**kwargs)` method - * `QuerysetProxy.get_or_none(**kwargs)` method - * `QuerysetProxy.get_or_create(**kwargs)` method - * `QuerysetProxy.all(**kwargs)` method + * `QuerysetProxy.filter(*args, **kwargs)` method + * `QuerysetProxy.exclude(*args, **kwargs)` method + * `QuerysetProxy.get(*args, **kwargs)` method + * `QuerysetProxy.get_or_none(*args, **kwargs)` method + * `QuerysetProxy.get_or_create(*args, **kwargs)` method + * `QuerysetProxy.all(*args, **kwargs)` method 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.order_by(columns:Union[List, str])` method + * `QuerysetProxy.order_by(columns:Union[List, str, OrderAction])` method ## Filtering @@ -65,24 +65,107 @@ tracks = Track.objects.filter(album__name="Fantasies").all() # will return all tracks where the columns album name = 'Fantasies' ``` +### Django style filters + You can use special filter suffix to change the filter operands: -* exact - like `album__name__exact='Malibu'` (exact match) -* iexact - like `album__name__iexact='malibu'` (exact match case insensitive) -* contains - like `album__name__contains='Mal'` (sql like) -* icontains - like `album__name__icontains='mal'` (sql like case insensitive) -* in - like `album__name__in=['Malibu', 'Barclay']` (sql in) -* isnull - like `album__name__isnull=True` (sql is null) - (isnotnull `album__name__isnull=False` (sql is not null)) -* gt - like `position__gt=3` (sql >) -* gte - like `position__gte=3` (sql >=) -* lt - like `position__lt=3` (sql <) -* lte - like `position__lte=3` (sql <=) -* startswith - like `album__name__startswith='Mal'` (exact start match) -* istartswith - like `album__name__istartswith='mal'` (exact start match case - insensitive) -* endswith - like `album__name__endswith='ibu'` (exact end match) -* iendswith - like `album__name__iendswith='IBU'` (exact end match case insensitive) +* exact - exact match to value, sql `column = ` + * can be written as`album__name__exact='Malibu'` +* iexact - exact match sql `column = ` (case insensitive) + * can be written as`album__name__iexact='malibu'` +* contains - sql `column LIKE '%%'` + * can be written as`album__name__contains='Mal'` +* icontains - sql `column LIKE '%%'` (case insensitive) + * can be written as`album__name__icontains='mal'` +* in - sql ` column IN (, , ...)` + * can be written as`album__name__in=['Malibu', 'Barclay']` +* isnull - sql `column IS NULL` (and sql `column IS NOT NULL`) + * can be written as`album__name__isnull=True` (isnotnull `album__name__isnull=False`) +* gt - sql `column > ` (greater than) + * can be written as`position__gt=3` +* gte - sql `column >= ` (greater or equal than) + * can be written as`position__gte=3` +* lt - sql `column < ` (lower than) + * can be written as`position__lt=3` +* lte - sql `column <= ` (lower equal than) + * can be written as`position__lte=3` +* startswith - sql `column LIKE '%'` (exact start match) + * can be written as`album__name__startswith='Mal'` +* istartswith - sql `column LIKE '%'` (case insensitive) + * can be written as`album__name__istartswith='mal'` +* endswith - sql `column LIKE '%'` (exact end match) + * can be written as`album__name__endswith='ibu'` +* iendswith - sql `column LIKE '%'` (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 = ` + * can be written as `Track.album.name == 'Malibu` +* iexact - exact match sql `column = ` (case insensitive) + * can be written as `Track.album.name.iexact('malibu')` +* contains - sql `column LIKE '%%'` + * can be written as `Track.album.name % 'Mal')` + * can be written as `Track.album.name.contains('Mal')` +* icontains - sql `column LIKE '%%'` (case insensitive) + * can be written as `Track.album.name.icontains('mal')` +* in - sql ` column IN (, , ...)` + * 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 > ` (greater than) + * can be written as `Track.album.name > 3` +* gte - sql `column >= ` (greater or equal than) + * can be written as `Track.album.name >= 3` +* lt - sql `column < ` (lower than) + * can be written as `Track.album.name < 3` +* lte - sql `column <= ` (lower equal than) + * can be written as `Track.album.name <= 3` +* startswith - sql `column LIKE '%'` (exact start match) + * can be written as `Track.album.name.startswith('Mal')` +* istartswith - sql `column LIKE '%'` (case insensitive) + * can be written as `Track.album.name.istartswith('mal')` +* endswith - sql `column LIKE '%'` (exact end match) + * can be written as `Track.album.name.endswith('ibu')` +* iendswith - sql `column LIKE '%'` (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 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`. !!!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. 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: `WHERE ( authors.name = 'J.R.R. Tolkien' OR books.year > 1970 )` +### Django style ```python books = ( await Book.objects.select_related("author") @@ -217,11 +301,22 @@ books = ( 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. sql: `WHERE ( books.year > 1960 OR books.year < 1940 ) AND authors.name = 'J.R.R. Tolkien'` +### Django style ```python # OPTION 1 - split and into separate call books = ( @@ -249,11 +344,38 @@ assert books[0].title == "The Hobbit" 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 sql: `WHERE ( ( books.year > 1960 AND authors.name = 'J.R.R. Tolkien' ) OR ( books.year < 2000 AND authors.name = 'Andrzej Sapkowski' ) ) ` +### Django style ```python books = ( await Book.objects.select_related("author") @@ -268,7 +390,20 @@ books = ( 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': sql: @@ -276,6 +411,7 @@ sql: ( books.year < 2000 AND os0cec_authors.name = 'Andrzej Sapkowski' ) OR books.title LIKE '%hobbit%' )` +### Django style ```python books = ( 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 achieve a query like this: @@ -301,6 +450,28 @@ AND authors.name = 'J.R.R. Tolkien' ) OR ``` 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 books = ( await Book.objects.select_related("author") @@ -339,10 +510,12 @@ assert len(books) == 1 assert books[0].title == "The Witcher" ``` +Same applies to python style chaining and nesting. -!!!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 - column for several values simply use `in` operator: `filter(name__in=['Jack','John'])`. +### Django style + +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'])`. If you pass only one parameter to `or_` or `and_` functions it's simply wrapped in parenthesis and has no effect on actual query, so in the end all 3 queries are identical: @@ -386,13 +559,28 @@ books = ( 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(**kwargs) -> Model` +`get(*args, **kwargs) -> Model` 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 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. - ## get_or_create -`get_or_create(**kwargs) -> Model` +`get_or_create(*args, **kwargs) -> Model` 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 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(**kwargs) -> List[Optional["Model"]]` +`all(*args, **kwargs) -> List[Optional["Model"]]` 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 To read more about `filter` go to [filter](./#filter). @@ -493,7 +680,7 @@ objects from other side of the relation. ### 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 fields. @@ -534,6 +721,7 @@ Given sample Models like following: To order by main model field just provide a field name +### Django style ```python toys = await Toy.objects.select_related("owner").order_by("name").all() assert [x.name.replace("Toy ", "") for x in toys] == [ @@ -543,11 +731,23 @@ assert toys[0].owner == zeus 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 '__'. You can sort this way across all relation types -> `ForeignKey`, reverse virtual FK and `ManyToMany` fields. +### Django style ```python toys = await Toy.objects.select_related("owner").order_by("owner__name").all() 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" ``` +### 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 +### Django style ```python owner = ( await Owner.objects.select_related("toys") @@ -568,6 +777,18 @@ assert owner.toys[0].name == "Toy 4" 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 All methods that do not return the rows explicitly returns a QueySet instance so you can chain them together diff --git a/docs/queries/read.md b/docs/queries/read.md index 40b84bb..17972b5 100644 --- a/docs/queries/read.md +++ b/docs/queries/read.md @@ -2,10 +2,10 @@ Following methods allow you to load data from the database. -* `get(**kwargs) -> Model` -* `get_or_create(**kwargs) -> Model` -* `first() -> Model` -* `all(**kwargs) -> List[Optional[Model]]` +* `get(*args, **kwargs) -> Model` +* `get_or_create(*args, **kwargs) -> Model` +* `first(*args, **kwargs) -> Model` +* `all(*args, **kwargs) -> List[Optional[Model]]` * `Model` @@ -13,20 +13,20 @@ Following methods allow you to load data from the database. * `QuerysetProxy` - * `QuerysetProxy.get(**kwargs)` method - * `QuerysetProxy.get_or_create(**kwargs)` method - * `QuerysetProxy.first()` method - * `QuerysetProxy.all(**kwargs)` method + * `QuerysetProxy.get(*args, **kwargs)` method + * `QuerysetProxy.get_or_create(*args, **kwargs)` method + * `QuerysetProxy.first(*args, **kwargs)` method + * `QuerysetProxy.all(*args, **kwargs)` method ## get -`get(**kwargs) -> Model` +`get(*args, **kwargs) -> Model` Get's the first row from the db meeting the criteria set by kwargs. If no criteria set it will return the last row in db sorted by pk 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 class Track(ormar.Model): @@ -57,14 +57,14 @@ track == track2 ## 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. ## get_or_create -`get_or_create(**kwargs) -> Model` +`get_or_create(*args, **kwargs) -> Model` Combination of create and get methods. @@ -102,7 +102,7 @@ assert album == album2 ## first -`first() -> Model` +`first(*args, **kwargs) -> Model` Gets the first row from the db ordered by primary key column ascending. @@ -127,11 +127,11 @@ assert album.name == 'The Cat' ## 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. -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. diff --git a/docs/releases.md b/docs/releases.md index 2fc282d..6b9a4a5 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -2,7 +2,7 @@ ## ✨ Features -* Add possibility to `filter` and `order_by` with field access instead of dunder separated strings. [#51](https://github.com/collerek/ormar/issues/51) +* 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 = ` @@ -102,7 +102,7 @@ (Product.categories.name << ['Toys', 'Books']) ).get() ``` -* Now you can alos use field access to provide OrderActions to `order_by()` +* 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()` @@ -112,6 +112,12 @@ * 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 ## ✨ Features diff --git a/examples/db.sqlite b/examples/db.sqlite new file mode 100644 index 0000000..e9f6c42 Binary files /dev/null and b/examples/db.sqlite differ diff --git a/examples/script_from_readme.py b/examples/script_from_readme.py index b0a0f82..ce4ee92 100644 --- a/examples/script_from_readme.py +++ b/examples/script_from_readme.py @@ -87,7 +87,10 @@ async def create(): async def read(): # Fetch an instance, without loading a foreign key relationship on it. + # Django style 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() # first() fetch the instance with lower primary key value @@ -193,7 +196,7 @@ async def joins(): # visit: https://collerek.github.io/ormar/relations/ # 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(): @@ -201,20 +204,30 @@ async def filter_and_sort(): # get(), all() etc. # to use special methods or access related model fields use double # 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") + # python style + books = await Book.objects.all(Book.author.name == "J.R.R. Tolkien") assert len(books) == 3 # filter can accept special methods also separated with double underscore # to issue sql query ` where authors.name like "%tolkien%"` that is not # case sensitive (hence small t in Tolkien) + # Django style 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 # to sort use order_by() function of queryset # to sort decreasing use hyphen before the field name # 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( "-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 books[0].title == "The Silmarillion" assert books[2].title == "The Hobbit" @@ -284,12 +297,24 @@ async def pagination(): async def aggregations(): - # ormar currently supports count: + # count: assert 2 == await Author.objects.count() - # and exists + # 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 # visit: https://collerek.github.io/ormar/queries/aggregations/ @@ -307,4 +332,4 @@ for func in [create, read, update, delete, joins, asyncio.run(func()) # drop the database tables -metadata.drop_all(engine) +metadata.drop_all(engine) \ No newline at end of file diff --git a/ormar/queryset/actions/filter_action.py b/ormar/queryset/actions/filter_action.py index 379c9e7..cbb1745 100644 --- a/ormar/queryset/actions/filter_action.py +++ b/ormar/queryset/actions/filter_action.py @@ -56,7 +56,7 @@ class FilterAction(QueryAction): Extracted in order to easily change table prefixes on complex relations. """ - def __init__(self, filter_str: str, value: Any, model_cls: Type["Model"], ) -> None: + def __init__(self, filter_str: str, value: Any, model_cls: Type["Model"],) -> None: super().__init__(query_str=filter_str, model_cls=model_cls) self.filter_value = value self._escape_characters_in_clause() @@ -149,7 +149,7 @@ class FilterAction(QueryAction): return clause def _compile_clause( - self, clause: sqlalchemy.sql.expression.BinaryExpression, modifiers: Dict, + self, clause: sqlalchemy.sql.expression.BinaryExpression, modifiers: Dict, ) -> sqlalchemy.sql.expression.TextClause: """ Compiles the clause to str using appropriate database dialect, replace columns @@ -177,7 +177,7 @@ class FilterAction(QueryAction): 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 + if dialect_name != "sqlite": # pragma: no cover clause_text = clause_text.replace("%%", "%") # remove %% in some dialects clause = text(clause_text) return clause diff --git a/ormar/queryset/field_accessor.py b/ormar/queryset/field_accessor.py index 52cc755..2701454 100644 --- a/ormar/queryset/field_accessor.py +++ b/ormar/queryset/field_accessor.py @@ -10,11 +10,11 @@ if TYPE_CHECKING: # pragma: no cover class FieldAccessor: def __init__( - self, - source_model: Type["Model"], - field: "BaseField" = None, - model: Type["Model"] = None, - access_chain: str = "", + self, + source_model: Type["Model"], + field: "BaseField" = None, + model: Type["Model"] = None, + access_chain: str = "", ) -> None: self._source_model = source_model self._field = field diff --git a/ormar/queryset/queryset.py b/ormar/queryset/queryset.py index 0024a7e..3cf4211 100644 --- a/ormar/queryset/queryset.py +++ b/ormar/queryset/queryset.py @@ -7,6 +7,7 @@ from typing import ( Sequence, Set, TYPE_CHECKING, + Tuple, Type, TypeVar, Union, @@ -180,16 +181,19 @@ class QuerySet(Generic[T]): return self.model.merge_instances_list(result_rows) # type: ignore 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. :param groups: tuple of FilterGroups :type groups: Any :return: list of resolver groups - :rtype: List[FilterGroup] + :rtype: Tuple[List[FilterGroup], List[str]] """ filter_groups = [] + select_related = self._select_related if groups: for group in groups: if not isinstance(group, FilterGroup): @@ -200,13 +204,13 @@ class QuerySet(Generic[T]): "other values need to be passed by" "keyword arguments" ) - group.resolve( + _, select_related = group.resolve( model_cls=self.model, select_related=self._select_related, filter_clauses=self.filter_clauses, ) filter_groups.append(group) - return filter_groups + return filter_groups, select_related @staticmethod def check_single_result_rows_count(rows: Sequence[Optional["T"]]) -> None: @@ -304,10 +308,10 @@ class QuerySet(Generic[T]): :return: filtered QuerySet :rtype: QuerySet """ - filter_groups = self._resolve_filter_groups(groups=args) + filter_groups, select_related = self._resolve_filter_groups(groups=args) qryclause = QueryClause( model_cls=self.model, - select_related=self._select_related, + select_related=select_related, filter_clauses=self.filter_clauses, ) filter_clauses, select_related = qryclause.prepare_filter(**kwargs)