Skip to content

Commit

Permalink
feat(queries): сount query
Browse files Browse the repository at this point in the history
  • Loading branch information
Pentusha committed Jan 10, 2025
1 parent 91ade1d commit b0a3aaa
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 1 deletion.
43 changes: 42 additions & 1 deletion docs/queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,9 +173,50 @@ as context's variable `limit_N` and will pass to EdgeDB dynamically, which will
Similar to how it works in SQLAlchemy's `unsafe_text` wrapper make this expression hardcoded into final query as is,
without dynamic contexts.

Please note that `offset` by design producing not optional execution plan
Please note that `offset` by design producing not optional execution plan,
and you have to avoid to use this keyword and method as far as you can.

### Counting

The count method allows you to calculate the total number of objects returned by a query.
It supports the application of filters and other query modifiers such as where.

#### Count Without Filters

To count all objects of a model:

```python
Movie.count.build()
```

<details>
<summary>generated query</summary>

```
select count(Movie)
```
</details>

#### Count with Filters

You can apply filters using the where method to count a subset of objects. For example:

```python
Movie.count.where(Movie.c.year <= int16(2000)).build()
```

<details>
<summary>generated query</summary>

```
select count((select Movie filter .year <= <int16>$filter_0))
{'filter_0': 2000}
```
</details>

Please note that `select count(Movie) filter .year < 2000`
and `select count((select Movie filter .year < 2000))` have different semantics in EdgeDB.
This library implements only the second one.

## Insert

Expand Down
19 changes: 19 additions & 0 deletions edgeql_qb/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from edgeql_qb.func import FuncInvocation
from edgeql_qb.operators import BinaryOp, SortedExpression, UnaryOp
from edgeql_qb.render.condition import render_conditions
from edgeql_qb.render.count import render_count, render_count_inner
from edgeql_qb.render.delete import render_delete
from edgeql_qb.render.group import (
render_group,
Expand Down Expand Up @@ -47,6 +48,10 @@ def select(self, *selectables: SelectExpressions, **kwargs: SubQuery) -> 'Select
_select=(*select_args, *select_kwargs),
)

@property
def count(self):
return CountQuery(_model=self)

def group(self, *selectables: SelectExpressions) -> 'GroupQuery':
return GroupQuery(
_model=self,
Expand Down Expand Up @@ -127,6 +132,20 @@ def build(self, generator: Iterator[int] | None = None) -> RenderedQuery:
)


@dataclass(slots=True, frozen=True)
class CountQuery:
_model: EdgeDBModel
_filters: tuple[Expression, ...] = field(default_factory=tuple)

def where(self, compared: BinaryOp | UnaryOp | FuncInvocation) -> 'SelectQuery':
return replace(self, _filters=(*self._filters, Expression(compared)))

def build(self, generator: Iterator[int] | None = None) -> RenderedQuery:
gen = generator or count()
inner = render_count_inner(self._model.name, self._filters, gen)
return render_count(inner)


@dataclass(slots=True, frozen=True)
class GroupQuery:
_model: EdgeDBModel
Expand Down
34 changes: 34 additions & 0 deletions edgeql_qb/render/count.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from collections.abc import Iterator

from mypy.nodes import Expression

from edgeql_qb.render.condition import render_conditions
from edgeql_qb.render.select import render_select
from edgeql_qb.render.tools import combine_many_renderers
from edgeql_qb.render.types import RenderedQuery


def render_count(inner: RenderedQuery) -> RenderedQuery:
return combine_many_renderers(
RenderedQuery('select count('),
inner,
RenderedQuery(')'),
)

def render_count_inner(model_name, filters: tuple[Expression, ...], gen: Iterator[int]):
match filters:
case ():
return RenderedQuery(model_name)
case conditions:
rendered_select = render_select(
model_name,
select=(),
generator=gen,
)
rendered_filters = render_conditions(conditions, gen)
return combine_many_renderers(
RenderedQuery('('),
rendered_select,
rendered_filters,
RenderedQuery(')'),
)
26 changes: 26 additions & 0 deletions tests/test_renderer/test_count_renderer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from multiprocessing.connection import Client

from edgeql_qb import EdgeDBModel
from edgeql_qb.frozendict import FrozenDict
from edgeql_qb.types import int16

A = EdgeDBModel('A')


def test_count_wo_filters() -> None:
rendered = A.count.build()
assert rendered.query == 'select count(A)'


def test_count_filter(client: Client) -> None:
insert1 = A.insert.values(p_int16=int16(1)).build()
insert2 = A.insert.values(p_int16=int16(2)).build()
insert3 = A.insert.values(p_int16=int16(3)).build()
client.query(insert1.query, **insert1.context)
client.query(insert2.query, **insert2.context)
client.query(insert3.query, **insert3.context)
rendered = A.count.where(A.c.p_int16 <= int16(2)).build()
assert rendered.query == 'select count((select A filter .p_int16 <= <int16>$filter_0))'
assert rendered.context == FrozenDict(filter_0=2)
result = client.query(rendered.query, **rendered.context)
assert result == [2]

0 comments on commit b0a3aaa

Please sign in to comment.