Skip to content

Commit

Permalink
Merge pull request #130 from collerek/default_order
Browse files Browse the repository at this point in the history
Default order
  • Loading branch information
collerek authored Mar 15, 2021
2 parents e306eec + 0fb5c6f commit 61c456a
Show file tree
Hide file tree
Showing 39 changed files with 2,028 additions and 78 deletions.
45 changes: 45 additions & 0 deletions docs/models/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,51 @@ You can set this parameter by providing `Meta` class `constraints` argument.
--8<-- "../docs_src/models/docs006.py"
```

## Model sort order

When querying the database with given model by default the Model is ordered by the `primary_key`
column ascending. If you wish to change the default behaviour you can do it by providing `orders_by`
parameter to model `Meta` class.

Sample default ordering:
```python
database = databases.Database(DATABASE_URL)
metadata = sqlalchemy.MetaData()


class BaseMeta(ormar.ModelMeta):
metadata = metadata
database = database

# default sort by column id ascending
class Author(ormar.Model):
class Meta(BaseMeta):
tablename = "authors"

id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
```
Modified
```python

database = databases.Database(DATABASE_URL)
metadata = sqlalchemy.MetaData()


class BaseMeta(ormar.ModelMeta):
metadata = metadata
database = database

# now default sort by name descending
class Author(ormar.Model):
class Meta(BaseMeta):
tablename = "authors"
orders_by = ["-name"]

id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
```

## Model Initialization

There are two ways to create and persist the `Model` instance in the database.
Expand Down
233 changes: 232 additions & 1 deletion docs/queries/aggregations.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
# Aggregation functions

Currently 2 aggregation functions are supported.
Currently 6 aggregation functions are supported.


* `count() -> int`
* `exists() -> bool`
* `sum(columns) -> Any`
* `avg(columns) -> Any`
* `min(columns) -> Any`
* `max(columns) -> Any`


* `QuerysetProxy`
* `QuerysetProxy.count()` method
* `QuerysetProxy.exists()` method
* `QuerysetProxy.sum(columns)` method
* `QuerysetProxy.avg(columns)` method
* `QuerysetProxy.min(column)` method
* `QuerysetProxy.max(columns)` method


## count
Expand Down Expand Up @@ -68,6 +76,209 @@ class Book(ormar.Model):
has_sample = await Book.objects.filter(title='Sample').exists()
```

## sum

`sum(columns) -> Any`

Returns sum value of columns for rows matching the given criteria (applied with `filter` and `exclude` if set before).

You can pass one or many column names including related columns.

As of now each column passed is aggregated separately (so `sum(col1+col2)` is not possible,
you can have `sum(col1, col2)` and later add 2 returned sums in python)

You cannot `sum` non numeric columns.

If you aggregate on one column, the single value is directly returned as a result
If you aggregate on multiple columns a dictionary with column: result pairs is returned

Given models like follows

```Python
--8<-- "../docs_src/aggregations/docs001.py"
```

A sample usage might look like following

```python
author = await Author(name="Author 1").save()
await Book(title="Book 1", year=1920, ranking=3, author=author).save()
await Book(title="Book 2", year=1930, ranking=1, author=author).save()
await Book(title="Book 3", year=1923, ranking=5, author=author).save()

assert await Book.objects.sum("year") == 5773
result = await Book.objects.sum(["year", "ranking"])
assert result == dict(year=5773, ranking=9)

try:
# cannot sum string column
await Book.objects.sum("title")
except ormar.QueryDefinitionError:
pass

assert await Author.objects.select_related("books").sum("books__year") == 5773
result = await Author.objects.select_related("books").sum(
["books__year", "books__ranking"]
)
assert result == dict(books__year=5773, books__ranking=9)

assert (
await Author.objects.select_related("books")
.filter(books__year__lt=1925)
.sum("books__year")
== 3843
)
```

## avg

`avg(columns) -> Any`

Returns avg value of columns for rows matching the given criteria (applied with `filter` and `exclude` if set before).

You can pass one or many column names including related columns.

As of now each column passed is aggregated separately (so `sum(col1+col2)` is not possible,
you can have `sum(col1, col2)` and later add 2 returned sums in python)

You cannot `avg` non numeric columns.

If you aggregate on one column, the single value is directly returned as a result
If you aggregate on multiple columns a dictionary with column: result pairs is returned

```Python
--8<-- "../docs_src/aggregations/docs001.py"
```

A sample usage might look like following

```python
author = await Author(name="Author 1").save()
await Book(title="Book 1", year=1920, ranking=3, author=author).save()
await Book(title="Book 2", year=1930, ranking=1, author=author).save()
await Book(title="Book 3", year=1923, ranking=5, author=author).save()

assert round(float(await Book.objects.avg("year")), 2) == 1924.33
result = await Book.objects.avg(["year", "ranking"])
assert round(float(result.get("year")), 2) == 1924.33
assert result.get("ranking") == 3.0

try:
# cannot avg string column
await Book.objects.avg("title")
except ormar.QueryDefinitionError:
pass

result = await Author.objects.select_related("books").avg("books__year")
assert round(float(result), 2) == 1924.33
result = await Author.objects.select_related("books").avg(
["books__year", "books__ranking"]
)
assert round(float(result.get("books__year")), 2) == 1924.33
assert result.get("books__ranking") == 3.0

assert (
await Author.objects.select_related("books")
.filter(books__year__lt=1925)
.avg("books__year")
== 1921.5
)
```

## min

`min(columns) -> Any`

Returns min value of columns for rows matching the given criteria (applied with `filter` and `exclude` if set before).

You can pass one or many column names including related columns.

As of now each column passed is aggregated separately (so `sum(col1+col2)` is not possible,
you can have `sum(col1, col2)` and later add 2 returned sums in python)

If you aggregate on one column, the single value is directly returned as a result
If you aggregate on multiple columns a dictionary with column: result pairs is returned

```Python
--8<-- "../docs_src/aggregations/docs001.py"
```

A sample usage might look like following

```python
author = await Author(name="Author 1").save()
await Book(title="Book 1", year=1920, ranking=3, author=author).save()
await Book(title="Book 2", year=1930, ranking=1, author=author).save()
await Book(title="Book 3", year=1923, ranking=5, author=author).save()

assert await Book.objects.min("year") == 1920
result = await Book.objects.min(["year", "ranking"])
assert result == dict(year=1920, ranking=1)

assert await Book.objects.min("title") == "Book 1"

assert await Author.objects.select_related("books").min("books__year") == 1920
result = await Author.objects.select_related("books").min(
["books__year", "books__ranking"]
)
assert result == dict(books__year=1920, books__ranking=1)

assert (
await Author.objects.select_related("books")
.filter(books__year__gt=1925)
.min("books__year")
== 1930
)
```

## max

`max(columns) -> Any`

Returns max value of columns for rows matching the given criteria (applied with `filter` and `exclude` if set before).

Returns min value of columns for rows matching the given criteria (applied with `filter` and `exclude` if set before).

You can pass one or many column names including related columns.

As of now each column passed is aggregated separately (so `sum(col1+col2)` is not possible,
you can have `sum(col1, col2)` and later add 2 returned sums in python)

If you aggregate on one column, the single value is directly returned as a result
If you aggregate on multiple columns a dictionary with column: result pairs is returned

```Python
--8<-- "../docs_src/aggregations/docs001.py"
```

A sample usage might look like following

```python
author = await Author(name="Author 1").save()
await Book(title="Book 1", year=1920, ranking=3, author=author).save()
await Book(title="Book 2", year=1930, ranking=1, author=author).save()
await Book(title="Book 3", year=1923, ranking=5, author=author).save()

assert await Book.objects.max("year") == 1930
result = await Book.objects.max(["year", "ranking"])
assert result == dict(year=1930, ranking=5)

assert await Book.objects.max("title") == "Book 3"

assert await Author.objects.select_related("books").max("books__year") == 1930
result = await Author.objects.select_related("books").max(
["books__year", "books__ranking"]
)
assert result == dict(books__year=1930, books__ranking=5)

assert (
await Author.objects.select_related("books")
.filter(books__year__lt=1925)
.max("books__year")
== 1923
)
```

## QuerysetProxy methods

When access directly the related `ManyToMany` field as well as `ReverseForeignKey`
Expand All @@ -89,6 +300,26 @@ objects from other side of the relation.
Works exactly the same as [exists](./#exists) function above but allows you to select columns from related
objects from other side of the relation.

### sum

Works exactly the same as [sum](./#sum) function above but allows you to sum columns from related
objects from other side of the relation.

### avg

Works exactly the same as [avg](./#avg) function above but allows you to average columns from related
objects from other side of the relation.

### min

Works exactly the same as [min](./#min) function above but allows you to select minimum of columns from related
objects from other side of the relation.

### max

Works exactly the same as [max](./#max) function above but allows you to select maximum of columns from related
objects from other side of the relation.

!!!tip
To read more about `QuerysetProxy` visit [querysetproxy][querysetproxy] section

34 changes: 33 additions & 1 deletion docs/queries/filter-and-sort.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ books = (
```

If you want or need to you can nest deeper conditions as deep as you want, in example to
acheive a query like this:
achieve a query like this:

sql:
```
Expand Down Expand Up @@ -564,6 +564,38 @@ assert owner.toys[1].name == "Toy 1"

Something like `Track.object.select_related("album").filter(album__name="Malibu").offset(1).limit(1).all()`

### Default sorting in ormar

Since order of rows in a database is not guaranteed, `ormar` **always** issues an `order by` sql clause to each (part of) query even if you do not provide order yourself.

When querying the database with given model by default the `Model` is ordered by the `primary_key`
column ascending. If you wish to change the default behaviour you can do it by providing `orders_by`
parameter to model `Meta` class.

!!!tip
To read more about models sort order visit [models](../models/index.md#model-sort-order) section of documentation

By default the relations follow the same ordering, but you can modify the order in which related models are loaded during query by providing `orders_by` and `related_orders_by`
parameters to relations.

!!!tip
To read more about models sort order visit [relations](../relations/index.md#relationship-default-sort-order) section of documentation

Order in which order_by clauses are applied is as follows:

* Explicitly passed `order_by()` calls in query
* Relation passed `orders_by` and `related_orders_by` if exists
* Model `Meta` class `orders_by`
* Model `primary_key` column ascending (fallback, used if none of above provided)

**Order from only one source is applied to each `Model` (so that you can always overwrite it in a single query).**

That means that if you provide explicit `order_by` for a model in a query, the `Relation` and `Model` sort orders are skipped.

If you provide a `Relation` one, the `Model` sort is skipped.

Finally, if you provide one for `Model` the default one by `primary_key` is skipped.

### QuerysetProxy methods

When access directly the related `ManyToMany` field as well as `ReverseForeignKey`
Expand Down
Loading

0 comments on commit 61c456a

Please sign in to comment.