TypeDAL 4.3.6__tar.gz → 4.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- typedal-4.4.0/.crush/.gitignore +1 -0
- typedal-4.4.0/.crush/crush.db-shm +0 -0
- typedal-4.4.0/.crush/crush.db-wal +0 -0
- typedal-4.4.0/.crush/logs/crush.log +37 -0
- {typedal-4.3.6 → typedal-4.4.0}/CHANGELOG.md +6 -0
- {typedal-4.3.6 → typedal-4.4.0}/PKG-INFO +1 -1
- {typedal-4.3.6 → typedal-4.4.0}/docs/3_building_queries.md +31 -0
- {typedal-4.3.6 → typedal-4.4.0}/src/typedal/__about__.py +1 -1
- {typedal-4.3.6 → typedal-4.4.0}/src/typedal/caching.py +6 -1
- {typedal-4.3.6 → typedal-4.4.0}/src/typedal/cli.py +2 -1
- {typedal-4.3.6 → typedal-4.4.0}/src/typedal/core.py +1 -0
- {typedal-4.3.6 → typedal-4.4.0}/src/typedal/query_builder.py +29 -5
- {typedal-4.3.6 → typedal-4.4.0}/src/typedal/tables.py +12 -0
- {typedal-4.3.6 → typedal-4.4.0}/src/typedal/types.py +4 -0
- typedal-4.4.0/tests/__init__.py +0 -0
- {typedal-4.3.6 → typedal-4.4.0}/tests/test_query_builder.py +128 -19
- /typedal-4.3.6/src/typedal/py.typed → /typedal-4.4.0/.crush/init +0 -0
- {typedal-4.3.6 → typedal-4.4.0}/.github/workflows/su6.yml +0 -0
- {typedal-4.3.6 → typedal-4.4.0}/.gitignore +0 -0
- {typedal-4.3.6 → typedal-4.4.0}/.readthedocs.yml +0 -0
- {typedal-4.3.6 → typedal-4.4.0}/README.md +0 -0
- {typedal-4.3.6 → typedal-4.4.0}/coverage.svg +0 -0
- {typedal-4.3.6 → typedal-4.4.0}/docs/1_getting_started.md +0 -0
- {typedal-4.3.6 → typedal-4.4.0}/docs/2_defining_tables.md +0 -0
- {typedal-4.3.6 → typedal-4.4.0}/docs/4_relationships.md +0 -0
- {typedal-4.3.6 → typedal-4.4.0}/docs/5_py4web.md +0 -0
- {typedal-4.3.6 → typedal-4.4.0}/docs/6_migrations.md +0 -0
- {typedal-4.3.6 → typedal-4.4.0}/docs/7_configuration.md +0 -0
- {typedal-4.3.6 → typedal-4.4.0}/docs/8_mixins.md +0 -0
- {typedal-4.3.6 → typedal-4.4.0}/docs/css/code_blocks.css +0 -0
- {typedal-4.3.6 → typedal-4.4.0}/docs/index.md +0 -0
- {typedal-4.3.6 → typedal-4.4.0}/docs/requirements.txt +0 -0
- {typedal-4.3.6 → typedal-4.4.0}/example_new.py +0 -0
- {typedal-4.3.6 → typedal-4.4.0}/example_old.py +0 -0
- {typedal-4.3.6 → typedal-4.4.0}/mkdocs.yml +0 -0
- {typedal-4.3.6 → typedal-4.4.0}/pyproject.toml +0 -0
- {typedal-4.3.6 → typedal-4.4.0}/src/typedal/__init__.py +0 -0
- {typedal-4.3.6 → typedal-4.4.0}/src/typedal/config.py +0 -0
- {typedal-4.3.6 → typedal-4.4.0}/src/typedal/constants.py +0 -0
- {typedal-4.3.6 → typedal-4.4.0}/src/typedal/define.py +0 -0
- {typedal-4.3.6 → typedal-4.4.0}/src/typedal/fields.py +0 -0
- {typedal-4.3.6 → typedal-4.4.0}/src/typedal/for_py4web.py +0 -0
- {typedal-4.3.6 → typedal-4.4.0}/src/typedal/for_web2py.py +0 -0
- {typedal-4.3.6 → typedal-4.4.0}/src/typedal/helpers.py +0 -0
- {typedal-4.3.6 → typedal-4.4.0}/src/typedal/mixins.py +0 -0
- /typedal-4.3.6/tests/__init__.py → /typedal-4.4.0/src/typedal/py.typed +0 -0
- {typedal-4.3.6 → typedal-4.4.0}/src/typedal/relationships.py +0 -0
- {typedal-4.3.6 → typedal-4.4.0}/src/typedal/rows.py +0 -0
- {typedal-4.3.6 → typedal-4.4.0}/src/typedal/serializers/as_json.py +0 -0
- {typedal-4.3.6 → typedal-4.4.0}/src/typedal/web2py_py4web_shared.py +0 -0
- {typedal-4.3.6 → typedal-4.4.0}/tests/configs/simple.toml +0 -0
- {typedal-4.3.6 → typedal-4.4.0}/tests/configs/valid.env +0 -0
- {typedal-4.3.6 → typedal-4.4.0}/tests/configs/valid.toml +0 -0
- {typedal-4.3.6 → typedal-4.4.0}/tests/py314_tests.py +0 -0
- {typedal-4.3.6 → typedal-4.4.0}/tests/test_cli.py +0 -0
- {typedal-4.3.6 → typedal-4.4.0}/tests/test_config.py +0 -0
- {typedal-4.3.6 → typedal-4.4.0}/tests/test_docs_examples.py +0 -0
- {typedal-4.3.6 → typedal-4.4.0}/tests/test_helpers.py +0 -0
- {typedal-4.3.6 → typedal-4.4.0}/tests/test_json.py +0 -0
- {typedal-4.3.6 → typedal-4.4.0}/tests/test_main.py +0 -0
- {typedal-4.3.6 → typedal-4.4.0}/tests/test_mixins.py +0 -0
- {typedal-4.3.6 → typedal-4.4.0}/tests/test_mypy.py +0 -0
- {typedal-4.3.6 → typedal-4.4.0}/tests/test_orm.py +0 -0
- {typedal-4.3.6 → typedal-4.4.0}/tests/test_py4web.py +0 -0
- {typedal-4.3.6 → typedal-4.4.0}/tests/test_relationships.py +0 -0
- {typedal-4.3.6 → typedal-4.4.0}/tests/test_row.py +0 -0
- {typedal-4.3.6 → typedal-4.4.0}/tests/test_stats.py +0 -0
- {typedal-4.3.6 → typedal-4.4.0}/tests/test_table.py +0 -0
- {typedal-4.3.6 → typedal-4.4.0}/tests/test_web2py.py +0 -0
- {typedal-4.3.6 → typedal-4.4.0}/tests/test_xx_others.py +0 -0
- {typedal-4.3.6 → typedal-4.4.0}/tests/timings.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
*
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{"time":"2026-01-20T14:19:21.359487106+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/config.(*catwalkSync).Get.func1","file":"github.com/charmbracelet/crush/internal/config/catwalk.go","line":55},"msg":"Fetching providers from Catwalk"}
|
|
2
|
+
{"time":"2026-01-20T14:19:21.518115889+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/config.(*catwalkSync).Get.func1","file":"github.com/charmbracelet/crush/internal/config/catwalk.go","line":63},"msg":"Catwalk providers not modified"}
|
|
3
|
+
{"time":"2026-01-20T14:19:21.52626179+01:00","level":"INFO","msg":"OK 20250424200609_initial.sql (1.52ms)"}
|
|
4
|
+
{"time":"2026-01-20T14:19:21.526651475+01:00","level":"INFO","msg":"OK 20250515105448_add_summary_message_id.sql (330.71µs)"}
|
|
5
|
+
{"time":"2026-01-20T14:19:21.526995425+01:00","level":"INFO","msg":"OK 20250624000000_add_created_at_indexes.sql (325.29µs)"}
|
|
6
|
+
{"time":"2026-01-20T14:19:21.527303588+01:00","level":"INFO","msg":"OK 20250627000000_add_provider_to_messages.sql (293.15µs)"}
|
|
7
|
+
{"time":"2026-01-20T14:19:21.52790648+01:00","level":"INFO","msg":"OK 20250810000000_add_is_summary_message.sql (495.71µs)"}
|
|
8
|
+
{"time":"2026-01-20T14:19:21.528316924+01:00","level":"INFO","msg":"OK 20250812000000_add_todos_to_sessions.sql (389.38µs)"}
|
|
9
|
+
{"time":"2026-01-20T14:19:21.528324939+01:00","level":"INFO","msg":"goose: successfully migrated database to version: 20250812000000"}
|
|
10
|
+
{"time":"2026-01-20T14:19:21.528356959+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/app.(*App).initLSPClients","file":"github.com/charmbracelet/crush/internal/app/lsp.go","line":21},"msg":"LSP clients initialization started in background"}
|
|
11
|
+
{"time":"2026-01-20T14:19:21.52843831+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/app.New.func1","file":"github.com/charmbracelet/crush/internal/app/app.go","line":102},"msg":"Initializing MCP clients"}
|
|
12
|
+
{"time":"2026-01-20T14:21:06.413595152+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/agent.(*sessionAgent).generateTitle","file":"github.com/charmbracelet/crush/internal/agent/agent.go","line":788},"msg":"generated title with small model"}
|
|
13
|
+
{"time":"2026-01-20T14:21:14.539289381+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/agent.(*sessionAgent).generateTitle","file":"github.com/charmbracelet/crush/internal/agent/agent.go","line":788},"msg":"generated title with small model"}
|
|
14
|
+
{"time":"2026-01-20T14:21:15.710688424+01:00","level":"WARN","source":{"function":"github.com/charmbracelet/crush/internal/agent/tools.init.func1","file":"github.com/charmbracelet/crush/internal/agent/tools/rg.go","line":18},"msg":"Ripgrep (rg) not found in $PATH. Some grep features might be limited or slower."}
|
|
15
|
+
{"time":"2026-01-20T14:21:17.08089755+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/agent.(*sessionAgent).generateTitle","file":"github.com/charmbracelet/crush/internal/agent/agent.go","line":788},"msg":"generated title with small model"}
|
|
16
|
+
{"time":"2026-01-20T14:21:17.151106375+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/agent.(*sessionAgent).generateTitle","file":"github.com/charmbracelet/crush/internal/agent/agent.go","line":788},"msg":"generated title with small model"}
|
|
17
|
+
{"time":"2026-01-20T14:22:05.503761296+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/agent.(*sessionAgent).generateTitle","file":"github.com/charmbracelet/crush/internal/agent/agent.go","line":788},"msg":"generated title with small model"}
|
|
18
|
+
{"time":"2026-01-20T14:26:05.030425805+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/agent.(*sessionAgent).generateTitle","file":"github.com/charmbracelet/crush/internal/agent/agent.go","line":788},"msg":"generated title with small model"}
|
|
19
|
+
{"time":"2026-01-20T14:36:26.778665189+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/agent.(*sessionAgent).generateTitle","file":"github.com/charmbracelet/crush/internal/agent/agent.go","line":788},"msg":"generated title with small model"}
|
|
20
|
+
{"time":"2026-01-20T14:38:51.314473736+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/agent.(*sessionAgent).generateTitle","file":"github.com/charmbracelet/crush/internal/agent/agent.go","line":788},"msg":"generated title with small model"}
|
|
21
|
+
{"time":"2026-01-20T15:02:22.602405814+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/app.(*App).Shutdown.func1","file":"github.com/charmbracelet/crush/internal/app/app.go","line":403},"msg":"Shutdown took 6.62949ms"}
|
|
22
|
+
{"time":"2026-01-20T15:02:23.719585648+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/config.(*catwalkSync).Get.func1","file":"github.com/charmbracelet/crush/internal/config/catwalk.go","line":55},"msg":"Fetching providers from Catwalk"}
|
|
23
|
+
{"time":"2026-01-20T15:02:23.881789126+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/config.(*catwalkSync).Get.func1","file":"github.com/charmbracelet/crush/internal/config/catwalk.go","line":63},"msg":"Catwalk providers not modified"}
|
|
24
|
+
{"time":"2026-01-20T15:02:23.884044691+01:00","level":"INFO","msg":"goose: no migrations to run. current version: 20250812000000"}
|
|
25
|
+
{"time":"2026-01-20T15:02:23.884082792+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/app.(*App).initLSPClients","file":"github.com/charmbracelet/crush/internal/app/lsp.go","line":21},"msg":"LSP clients initialization started in background"}
|
|
26
|
+
{"time":"2026-01-20T15:02:23.884273487+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/app.New.func1","file":"github.com/charmbracelet/crush/internal/app/app.go","line":102},"msg":"Initializing MCP clients"}
|
|
27
|
+
{"time":"2026-01-20T15:04:13.024457515+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/agent.(*sessionAgent).generateTitle","file":"github.com/charmbracelet/crush/internal/agent/agent.go","line":788},"msg":"generated title with small model"}
|
|
28
|
+
{"time":"2026-01-20T15:04:14.232072885+01:00","level":"WARN","source":{"function":"github.com/charmbracelet/crush/internal/agent/tools.init.func1","file":"github.com/charmbracelet/crush/internal/agent/tools/rg.go","line":18},"msg":"Ripgrep (rg) not found in $PATH. Some grep features might be limited or slower."}
|
|
29
|
+
{"time":"2026-01-20T15:11:24.612161916+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/app.(*App).Shutdown.func1","file":"github.com/charmbracelet/crush/internal/app/app.go","line":403},"msg":"Shutdown took 9.537141ms"}
|
|
30
|
+
{"time":"2026-01-20T15:11:25.263960575+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/config.(*catwalkSync).Get.func1","file":"github.com/charmbracelet/crush/internal/config/catwalk.go","line":55},"msg":"Fetching providers from Catwalk"}
|
|
31
|
+
{"time":"2026-01-20T15:11:25.431990051+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/config.(*catwalkSync).Get.func1","file":"github.com/charmbracelet/crush/internal/config/catwalk.go","line":63},"msg":"Catwalk providers not modified"}
|
|
32
|
+
{"time":"2026-01-20T15:11:25.434282035+01:00","level":"INFO","msg":"goose: no migrations to run. current version: 20250812000000"}
|
|
33
|
+
{"time":"2026-01-20T15:11:25.434366662+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/app.(*App).initLSPClients","file":"github.com/charmbracelet/crush/internal/app/lsp.go","line":21},"msg":"LSP clients initialization started in background"}
|
|
34
|
+
{"time":"2026-01-20T15:11:25.434579879+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/app.New.func1","file":"github.com/charmbracelet/crush/internal/app/app.go","line":102},"msg":"Initializing MCP clients"}
|
|
35
|
+
{"time":"2026-01-20T15:24:40.456010578+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/agent.(*sessionAgent).generateTitle","file":"github.com/charmbracelet/crush/internal/agent/agent.go","line":788},"msg":"generated title with small model"}
|
|
36
|
+
{"time":"2026-01-20T15:24:51.673808433+01:00","level":"WARN","source":{"function":"github.com/charmbracelet/crush/internal/agent/tools.init.func1","file":"github.com/charmbracelet/crush/internal/agent/tools/rg.go","line":18},"msg":"Ripgrep (rg) not found in $PATH. Some grep features might be limited or slower."}
|
|
37
|
+
{"time":"2026-01-20T15:26:03.868420566+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/agent.(*sessionAgent).Cancel","file":"github.com/charmbracelet/crush/internal/agent/agent.go","line":907},"msg":"Request cancellation initiated","session_id":"4c2d1684-8db2-4ffd-83bb-9c08fe850f29"}
|
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
<!--next-version-placeholder-->
|
|
4
4
|
|
|
5
|
+
## v4.4.0 (2026-01-20)
|
|
6
|
+
|
|
7
|
+
### Feature
|
|
8
|
+
|
|
9
|
+
* Add groupby and having methods to QueryBuilder ([`8bce7ec`](https://github.com/trialandsuccess/TypeDAL/commit/8bce7ec137db79ab1631f4504ae891b5bd42c1d8))
|
|
10
|
+
|
|
5
11
|
## v4.3.6 (2026-01-19)
|
|
6
12
|
|
|
7
13
|
### Fix
|
|
@@ -22,6 +22,8 @@ A Query Builder can be initialized by calling one of these methods on a TypedTab
|
|
|
22
22
|
- where
|
|
23
23
|
- select
|
|
24
24
|
- join
|
|
25
|
+
- groupby
|
|
26
|
+
- having
|
|
25
27
|
- cache
|
|
26
28
|
|
|
27
29
|
e.g. `Person.where(...)` -> `QueryBuilder[Person]`
|
|
@@ -155,6 +157,35 @@ Person.join('articles', method='inner') # will only yield persons that have rel
|
|
|
155
157
|
|
|
156
158
|
For more details about relationships and joins, see [4. Relationships](./4_relationships.md).
|
|
157
159
|
|
|
160
|
+
### groupby & having
|
|
161
|
+
|
|
162
|
+
Group query results by one or more fields, typically used with aggregate functions like `count()`, `sum()`, `avg()`, etc.
|
|
163
|
+
Use `having` to filter the grouped results based on aggregate conditions.
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
# Basic grouping: count articles per author
|
|
167
|
+
Article.select(Article.author, Article.id.count().with_alias("article_count"))
|
|
168
|
+
.groupby(Article.author)
|
|
169
|
+
.collect()
|
|
170
|
+
|
|
171
|
+
# Group by multiple fields
|
|
172
|
+
Sale.select(Sale.product, Sale.region, Sale.amount.sum().with_alias("total"))
|
|
173
|
+
.groupby(Sale.product, Sale.region)
|
|
174
|
+
.collect()
|
|
175
|
+
|
|
176
|
+
# Filter groups with having: only authors with more than 5 articles
|
|
177
|
+
Article.select(Article.author, Article.id.count().with_alias("article_count"))
|
|
178
|
+
.groupby(Article.author)
|
|
179
|
+
.having(Article.id.count() > 5)
|
|
180
|
+
.collect()
|
|
181
|
+
|
|
182
|
+
# Can be chained in any order
|
|
183
|
+
School.groupby(School.id)
|
|
184
|
+
.having(Team.id.count() > 0)
|
|
185
|
+
.select(School.id, Team.id.count())
|
|
186
|
+
.collect()
|
|
187
|
+
```
|
|
188
|
+
|
|
158
189
|
### cache
|
|
159
190
|
|
|
160
191
|
```python
|
|
@@ -12,6 +12,7 @@ import dill # nosec
|
|
|
12
12
|
from pydal.objects import Field, Rows, Set
|
|
13
13
|
|
|
14
14
|
from .fields import TypedField
|
|
15
|
+
from .helpers import throw
|
|
15
16
|
from .rows import TypedRows
|
|
16
17
|
from .tables import TypedTable
|
|
17
18
|
from .types import CacheStatus, Query
|
|
@@ -177,8 +178,12 @@ def clear_cache() -> None:
|
|
|
177
178
|
|
|
178
179
|
Immediately commits
|
|
179
180
|
"""
|
|
181
|
+
db: TypeDAL = _TypedalCache._db or throw(
|
|
182
|
+
RuntimeError("@define or db.define is not called on typedal caching classes yet!")
|
|
183
|
+
)
|
|
184
|
+
|
|
180
185
|
_TypedalCache.truncate("RESTART IDENTITY CASCADE")
|
|
181
|
-
|
|
186
|
+
db.commit()
|
|
182
187
|
|
|
183
188
|
|
|
184
189
|
def clear_expired() -> int:
|
|
@@ -392,7 +392,8 @@ def fake_migrations(
|
|
|
392
392
|
|
|
393
393
|
previously_migrated = (
|
|
394
394
|
db(
|
|
395
|
-
db.ewh_implemented_features.name.belongs(to_fake)
|
|
395
|
+
db.ewh_implemented_features.name.belongs(to_fake)
|
|
396
|
+
& (db.ewh_implemented_features.installed == True) # noqa E712
|
|
396
397
|
)
|
|
397
398
|
.select(db.ewh_implemented_features.name)
|
|
398
399
|
.column("name")
|
|
@@ -70,8 +70,7 @@ class QueryBuilder(t.Generic[T_MetaInstance]):
|
|
|
70
70
|
"""
|
|
71
71
|
self.model = model
|
|
72
72
|
table = self._ensure_table_defined()
|
|
73
|
-
|
|
74
|
-
default_query = table.id > 0
|
|
73
|
+
default_query: Query = table.id > 0
|
|
75
74
|
self.query = add_query or default_query
|
|
76
75
|
self.select_args = select_args or []
|
|
77
76
|
self.select_kwargs = select_kwargs or {}
|
|
@@ -111,7 +110,7 @@ class QueryBuilder(t.Generic[T_MetaInstance]):
|
|
|
111
110
|
Querybuilder is truthy if it has t.Any conditions.
|
|
112
111
|
"""
|
|
113
112
|
table = self._ensure_table_defined()
|
|
114
|
-
default_query = table.id > 0
|
|
113
|
+
default_query: Query = table.id > 0
|
|
115
114
|
return any(
|
|
116
115
|
[
|
|
117
116
|
self.query != default_query,
|
|
@@ -183,6 +182,31 @@ class QueryBuilder(t.Generic[T_MetaInstance]):
|
|
|
183
182
|
"""
|
|
184
183
|
return self.select(orderby=fields)
|
|
185
184
|
|
|
185
|
+
def groupby(self, *fields: t.Any) -> "QueryBuilder[T_MetaInstance]":
|
|
186
|
+
"""
|
|
187
|
+
Group the query results by specified fields.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
fields: Field(s) to group by (e.g., Table.column)
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
QueryBuilder: A new QueryBuilder instance with grouping applied.
|
|
194
|
+
"""
|
|
195
|
+
groupby_value = fields[0] if len(fields) == 1 else fields
|
|
196
|
+
return self.select(groupby=groupby_value)
|
|
197
|
+
|
|
198
|
+
def having(self, condition: t.Any) -> "QueryBuilder[T_MetaInstance]":
|
|
199
|
+
"""
|
|
200
|
+
Filter grouped query results based on aggregate conditions.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
condition: Query condition for filtering groups (e.g., Team.id.count() > 0)
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
QueryBuilder: A new QueryBuilder instance with having condition applied.
|
|
207
|
+
"""
|
|
208
|
+
return self.select(having=condition)
|
|
209
|
+
|
|
186
210
|
def where(
|
|
187
211
|
self,
|
|
188
212
|
*queries_or_lambdas: Query | t.Callable[[t.Type[T_MetaInstance]], Query] | dict[str, t.Any],
|
|
@@ -524,7 +548,7 @@ class QueryBuilder(t.Generic[T_MetaInstance]):
|
|
|
524
548
|
Raw version of .collect which only executes the SQL, without performing t.Any magic afterwards.
|
|
525
549
|
"""
|
|
526
550
|
db = self._get_db()
|
|
527
|
-
metadata = self.metadata.copy()
|
|
551
|
+
metadata: Metadata = self.metadata.copy()
|
|
528
552
|
|
|
529
553
|
query, select_args, select_kwargs = self._before_query(metadata, add_id=add_id)
|
|
530
554
|
|
|
@@ -552,7 +576,7 @@ class QueryBuilder(t.Generic[T_MetaInstance]):
|
|
|
552
576
|
for fn_before in db._before_collect:
|
|
553
577
|
fn_before(self)
|
|
554
578
|
|
|
555
|
-
metadata = self.metadata.copy()
|
|
579
|
+
metadata: Metadata = self.metadata.copy()
|
|
556
580
|
|
|
557
581
|
if metadata.get("cache", {}).get("enabled") and (result := self._collect_cached(metadata)):
|
|
558
582
|
return result
|
|
@@ -326,6 +326,18 @@ class TableMeta(type):
|
|
|
326
326
|
"""
|
|
327
327
|
return QueryBuilder(self).orderby(*fields)
|
|
328
328
|
|
|
329
|
+
def groupby(self: t.Type[T_MetaInstance], *fields: t.Any) -> "QueryBuilder[T_MetaInstance]":
|
|
330
|
+
"""
|
|
331
|
+
See QueryBuilder.groupby!
|
|
332
|
+
"""
|
|
333
|
+
return QueryBuilder(self).groupby(*fields)
|
|
334
|
+
|
|
335
|
+
def having(self: t.Type[T_MetaInstance], condition: t.Any) -> "QueryBuilder[T_MetaInstance]":
|
|
336
|
+
"""
|
|
337
|
+
See QueryBuilder.having!
|
|
338
|
+
"""
|
|
339
|
+
return QueryBuilder(self).having(condition)
|
|
340
|
+
|
|
329
341
|
def cache(self: t.Type[T_MetaInstance], *deps: t.Any, **kwargs: t.Any) -> "QueryBuilder[T_MetaInstance]":
|
|
330
342
|
"""
|
|
331
343
|
See QueryBuilder.cache!
|
|
@@ -220,6 +220,8 @@ class SelectKwargs(t.TypedDict, total=False):
|
|
|
220
220
|
join: t.Optional[list[Expression]]
|
|
221
221
|
left: t.Optional[list[Expression]]
|
|
222
222
|
orderby: "OrderBy | t.Iterable[OrderBy] | None"
|
|
223
|
+
groupby: "GroupBy | t.Iterable[GroupBy] | None"
|
|
224
|
+
having: "Having | None"
|
|
223
225
|
limitby: t.Optional[tuple[int, int]]
|
|
224
226
|
distinct: bool | Field | Expression
|
|
225
227
|
orderby_on_limitby: bool
|
|
@@ -323,5 +325,7 @@ CacheModel = t.Callable[[str, CacheFn, int], Rows]
|
|
|
323
325
|
CacheTuple = tuple[CacheModel, int]
|
|
324
326
|
|
|
325
327
|
OrderBy: t.TypeAlias = str | Expression
|
|
328
|
+
GroupBy: t.TypeAlias = Field | Expression
|
|
329
|
+
Having: t.TypeAlias = Query | Expression
|
|
326
330
|
|
|
327
331
|
T_annotation = t.Type[t.Any] | types.UnionType
|
|
File without changes
|
|
@@ -45,25 +45,6 @@ def test_query_type():
|
|
|
45
45
|
assert isinstance(TestQueryTable.number != 3, Query)
|
|
46
46
|
|
|
47
47
|
|
|
48
|
-
"""
|
|
49
|
-
SELECT "test_query_table"."id"
|
|
50
|
-
, "test_query_table"."number"
|
|
51
|
-
, "relations_8106139955393"."id"
|
|
52
|
-
, "relations_8106139955393"."name"
|
|
53
|
-
, "relations_8106139955393"."value"
|
|
54
|
-
, "relations_8106139955393"."querytable"
|
|
55
|
-
FROM "test_query_table"
|
|
56
|
-
LEFT JOIN "test_relationship" AS "relations_8106139955393"
|
|
57
|
-
ON ("relations_8106139955393"."querytable" = "test_query_table"."id")
|
|
58
|
-
WHERE ("test_query_table"."id" IN (SELECT "test_query_table"."id"
|
|
59
|
-
FROM "test_query_table"
|
|
60
|
-
WHERE ("test_query_table"."id" > 0)
|
|
61
|
-
ORDER BY "test_query_table"."id"
|
|
62
|
-
LIMIT 3 OFFSET 0))
|
|
63
|
-
ORDER BY "test_query_table"."number" DESC;
|
|
64
|
-
"""
|
|
65
|
-
|
|
66
|
-
|
|
67
48
|
def _setup_data():
|
|
68
49
|
TestQueryTable.truncate()
|
|
69
50
|
first = TestQueryTable.insert(number=0)
|
|
@@ -562,3 +543,131 @@ def test_before_after_collect():
|
|
|
562
543
|
db._after_collect.append(print_duration)
|
|
563
544
|
|
|
564
545
|
TestQueryTable.all()
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def test_groupby_basic():
|
|
549
|
+
"""Test basic groupby with count aggregation."""
|
|
550
|
+
_setup_data()
|
|
551
|
+
|
|
552
|
+
result = TestRelationship.select(
|
|
553
|
+
TestRelationship.querytable.with_alias("query_table"),
|
|
554
|
+
TestRelationship.querytable.count().with_alias("count"),
|
|
555
|
+
).groupby(TestRelationship.querytable).execute()
|
|
556
|
+
|
|
557
|
+
assert len(result) == 2
|
|
558
|
+
for row in result:
|
|
559
|
+
assert row["count"] == 4
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def test_groupby_multiple_fields():
|
|
563
|
+
"""Test grouping by multiple fields."""
|
|
564
|
+
_setup_data()
|
|
565
|
+
|
|
566
|
+
result = TestRelationship.select(
|
|
567
|
+
TestRelationship.querytable,
|
|
568
|
+
TestRelationship.value,
|
|
569
|
+
TestRelationship.id.count().with_alias("count"),
|
|
570
|
+
).groupby(TestRelationship.querytable, TestRelationship.value).execute()
|
|
571
|
+
|
|
572
|
+
# Should group by combination of querytable and value
|
|
573
|
+
assert len(result) > 0
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def test_groupby_with_having():
|
|
577
|
+
"""Test groupby with having to filter groups."""
|
|
578
|
+
_setup_data()
|
|
579
|
+
|
|
580
|
+
result = TestRelationship.select(
|
|
581
|
+
TestRelationship.querytable.with_alias("query_table"),
|
|
582
|
+
TestRelationship.querytable.count().with_alias("count"),
|
|
583
|
+
).groupby(TestRelationship.querytable).having(TestRelationship.querytable.count() > 3).execute()
|
|
584
|
+
|
|
585
|
+
# Only groups with count > 3
|
|
586
|
+
assert len(result) == 2
|
|
587
|
+
for row in result:
|
|
588
|
+
assert row["count"] > 3
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def test_having_filters_aggregates():
|
|
592
|
+
"""Test that having properly filters based on aggregate conditions."""
|
|
593
|
+
_setup_data()
|
|
594
|
+
|
|
595
|
+
# Get all groups
|
|
596
|
+
all_groups = TestRelationship.select(
|
|
597
|
+
TestRelationship.querytable,
|
|
598
|
+
TestRelationship.querytable.count().with_alias("count"),
|
|
599
|
+
).groupby(TestRelationship.querytable).execute()
|
|
600
|
+
|
|
601
|
+
# Filter with having
|
|
602
|
+
filtered = TestRelationship.select(
|
|
603
|
+
TestRelationship.querytable,
|
|
604
|
+
TestRelationship.querytable.count().with_alias("count"),
|
|
605
|
+
).groupby(TestRelationship.querytable).having(TestRelationship.querytable.count() > 10).execute()
|
|
606
|
+
|
|
607
|
+
# Should have fewer results (or zero if no groups have count > 10)
|
|
608
|
+
assert len(filtered) <= len(all_groups)
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def test_groupby_to_sql():
|
|
612
|
+
"""Verify SQL generation includes GROUP BY."""
|
|
613
|
+
sql = TestRelationship.select(
|
|
614
|
+
TestRelationship.querytable, TestRelationship.querytable.count()
|
|
615
|
+
).groupby(TestRelationship.querytable).to_sql()
|
|
616
|
+
|
|
617
|
+
assert "GROUP BY" in sql
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def test_having_to_sql():
|
|
621
|
+
"""Verify SQL generation includes HAVING."""
|
|
622
|
+
sql = (
|
|
623
|
+
TestRelationship.select(TestRelationship.querytable, TestRelationship.querytable.count())
|
|
624
|
+
.groupby(TestRelationship.querytable)
|
|
625
|
+
.having(TestRelationship.querytable.count() > 0)
|
|
626
|
+
.to_sql()
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
assert "GROUP BY" in sql
|
|
630
|
+
assert "HAVING" in sql
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def test_groupby_chaining():
|
|
634
|
+
"""Test that multiple groupby calls work (last one should win)."""
|
|
635
|
+
_setup_data()
|
|
636
|
+
|
|
637
|
+
# First groupby by querytable
|
|
638
|
+
builder1 = TestRelationship.select(
|
|
639
|
+
TestRelationship.querytable, TestRelationship.querytable.count().with_alias("count")
|
|
640
|
+
).groupby(TestRelationship.querytable)
|
|
641
|
+
|
|
642
|
+
# Then groupby by value (should override)
|
|
643
|
+
builder2 = builder1.groupby(TestRelationship.value)
|
|
644
|
+
|
|
645
|
+
sql = builder2.to_sql()
|
|
646
|
+
# Should only have the second groupby
|
|
647
|
+
assert "GROUP BY" in sql
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
def test_groupby_having_on_table_class():
|
|
651
|
+
"""Test calling .groupby() and .having() directly on table class in different orders."""
|
|
652
|
+
_setup_data()
|
|
653
|
+
|
|
654
|
+
builder1 = (
|
|
655
|
+
TestRelationship.groupby(TestRelationship.querytable)
|
|
656
|
+
.having(TestRelationship.querytable.count() > 0)
|
|
657
|
+
.select(TestRelationship.querytable, TestRelationship.querytable.count())
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
sql1 = builder1.to_sql()
|
|
661
|
+
|
|
662
|
+
builder2 = (
|
|
663
|
+
TestRelationship.having(TestRelationship.querytable.count() > 0)
|
|
664
|
+
.groupby(TestRelationship.querytable)
|
|
665
|
+
.select(TestRelationship.querytable, TestRelationship.querytable.count())
|
|
666
|
+
)
|
|
667
|
+
sql2 = builder2.to_sql()
|
|
668
|
+
|
|
669
|
+
assert sql1 == sql2
|
|
670
|
+
assert "GROUP BY" in sql1
|
|
671
|
+
assert "HAVING" in sql1
|
|
672
|
+
|
|
673
|
+
assert builder1.execute() == builder2.execute()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|