TypeDAL 4.9.1__tar.gz → 4.9.3__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.9.3/.crush/.gitignore +1 -0
- typedal-4.9.3/.crush/crush.db-shm +0 -0
- typedal-4.9.3/.crush/crush.db-wal +0 -0
- typedal-4.9.3/.crush/logs/crush.log +37 -0
- {typedal-4.9.1 → typedal-4.9.3}/CHANGELOG.md +12 -0
- {typedal-4.9.1 → typedal-4.9.3}/PKG-INFO +2 -1
- {typedal-4.9.1 → typedal-4.9.3}/pyproject.toml +1 -0
- {typedal-4.9.1 → typedal-4.9.3}/src/typedal/__about__.py +1 -1
- {typedal-4.9.1 → typedal-4.9.3}/src/typedal/fields.py +14 -0
- {typedal-4.9.1 → typedal-4.9.3}/src/typedal/query_builder.py +69 -10
- {typedal-4.9.1 → typedal-4.9.3}/src/typedal/types.py +4 -2
- typedal-4.9.3/tests/__init__.py +0 -0
- typedal-4.9.3/tests/conftest.py +30 -0
- {typedal-4.9.1 → typedal-4.9.3}/tests/test_config.py +0 -17
- {typedal-4.9.1 → typedal-4.9.3}/tests/test_query_builder.py +44 -1
- {typedal-4.9.1 → typedal-4.9.3}/tests/test_relationships.py +27 -0
- /typedal-4.9.1/src/typedal/py.typed → /typedal-4.9.3/.crush/init +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/.github/workflows/su6.yml +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/.gitignore +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/.readthedocs.yml +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/README.md +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/coverage.svg +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/docs/10_advanced_apis.md +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/docs/1_getting_started.md +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/docs/2_defining_tables.md +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/docs/3_building_queries.md +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/docs/4_relationships.md +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/docs/5_py4web.md +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/docs/6_migrations.md +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/docs/7_configuration.md +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/docs/8_mixins.md +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/docs/9_memoization.md +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/docs/css/code_blocks.css +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/docs/index.md +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/docs/requirements.txt +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/example_new.py +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/example_old.py +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/mkdocs.yml +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/src/typedal/__init__.py +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/src/typedal/caching.py +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/src/typedal/cli.py +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/src/typedal/config.py +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/src/typedal/constants.py +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/src/typedal/core.py +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/src/typedal/define.py +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/src/typedal/enum_helpers.py +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/src/typedal/for_py4web.py +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/src/typedal/for_web2py.py +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/src/typedal/helpers.py +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/src/typedal/mixins.py +0 -0
- /typedal-4.9.1/tests/__init__.py → /typedal-4.9.3/src/typedal/py.typed +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/src/typedal/relationships.py +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/src/typedal/rows.py +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/src/typedal/serializers/as_json.py +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/src/typedal/serializers/typescript.py +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/src/typedal/tables.py +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/src/typedal/web2py_py4web_shared.py +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/tasks.py +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/tests/configs/simple.toml +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/tests/configs/valid.env +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/tests/configs/valid.toml +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/tests/py314_tests.py +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/tests/test_cli.py +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/tests/test_docs_examples.py +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/tests/test_helpers.py +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/tests/test_json.py +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/tests/test_main.py +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/tests/test_mixins.py +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/tests/test_mypy.py +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/tests/test_orm.py +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/tests/test_py4web.py +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/tests/test_row.py +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/tests/test_stats.py +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/tests/test_table.py +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/tests/test_typescript.py +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/tests/test_typing_mypy.md +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/tests/test_typing_pyright.md +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/tests/test_web2py.py +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/tests/test_xx_others.py +0 -0
- {typedal-4.9.1 → typedal-4.9.3}/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,18 @@
|
|
|
2
2
|
|
|
3
3
|
<!--next-version-placeholder-->
|
|
4
4
|
|
|
5
|
+
## v4.9.3 (2026-07-03)
|
|
6
|
+
|
|
7
|
+
### Fix
|
|
8
|
+
|
|
9
|
+
* Pagination count with joined relationships ([`2eaefc0`](https://github.com/trialandsuccess/TypeDAL/commit/2eaefc051a94d4a4204164e0773a8d7691aae63e))
|
|
10
|
+
|
|
11
|
+
## v4.9.2 (2026-07-02)
|
|
12
|
+
|
|
13
|
+
### Fix
|
|
14
|
+
|
|
15
|
+
* **query-builder:** Normalize field rnames in select options ([`5fe46d9`](https://github.com/trialandsuccess/TypeDAL/commit/5fe46d9b4acd3081ff6e87cf7f9a45b8f4aa876b))
|
|
16
|
+
|
|
5
17
|
## v4.9.1 (2026-07-02)
|
|
6
18
|
|
|
7
19
|
### Fix
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: TypeDAL
|
|
3
|
-
Version: 4.9.
|
|
3
|
+
Version: 4.9.3
|
|
4
4
|
Summary: Typing support for PyDAL
|
|
5
5
|
Project-URL: Documentation, https://typedal.readthedocs.io/
|
|
6
6
|
Project-URL: Issues, https://github.com/trialandsuccess/TypeDAL/issues
|
|
@@ -43,6 +43,7 @@ Requires-Dist: ewok; extra == 'dev'
|
|
|
43
43
|
Requires-Dist: hatch; extra == 'dev'
|
|
44
44
|
Requires-Dist: mkdocs; extra == 'dev'
|
|
45
45
|
Requires-Dist: mkdocs-dracula-theme; extra == 'dev'
|
|
46
|
+
Requires-Dist: psycopg2-binary; extra == 'dev'
|
|
46
47
|
Requires-Dist: pydantic<3; extra == 'dev'
|
|
47
48
|
Requires-Dist: pyright<1.1.400; extra == 'dev'
|
|
48
49
|
Requires-Dist: pytest-typing; extra == 'dev'
|
|
@@ -268,6 +268,20 @@ class TypedField[T_Value](Expression): # pragma: no cover
|
|
|
268
268
|
return t.cast(Expression, self._field.lower())
|
|
269
269
|
|
|
270
270
|
|
|
271
|
+
def rname(field: TypedField[t.Any] | Field) -> str:
|
|
272
|
+
"""
|
|
273
|
+
Return the full rname (table and field).
|
|
274
|
+
"""
|
|
275
|
+
|
|
276
|
+
table = field._table
|
|
277
|
+
inner_field = field._field if isinstance(field, TypedField) else field
|
|
278
|
+
|
|
279
|
+
if not (table and inner_field):
|
|
280
|
+
raise ValueError("missing table or inner field on this 'field'")
|
|
281
|
+
|
|
282
|
+
return "%s.%s" % (table._rname, inner_field._rname)
|
|
283
|
+
|
|
284
|
+
|
|
271
285
|
def is_typed_field(cls: t.Any) -> t.TypeGuard["TypedField[t.Any]"]:
|
|
272
286
|
"""
|
|
273
287
|
Is `cls` an instance or subclass of TypedField?
|
|
@@ -152,7 +152,22 @@ class QueryBuilder[T_MetaInstance: _TypedTable]:
|
|
|
152
152
|
"""
|
|
153
153
|
Return a clone of this builder with permission overrides merged in.
|
|
154
154
|
"""
|
|
155
|
-
return self._extend(permissions=
|
|
155
|
+
return self._extend(permissions=permissions)
|
|
156
|
+
|
|
157
|
+
def _normalize_select_option(
|
|
158
|
+
self, value: str | Field | Expression | bool | t.Iterable[str | Field]
|
|
159
|
+
) -> str | bool | list[str]:
|
|
160
|
+
# currently only used for 'distinct' since orderby, ... are patched by pydal itself in select()
|
|
161
|
+
if isinstance(value, bool):
|
|
162
|
+
return value
|
|
163
|
+
|
|
164
|
+
if isinstance(value, (list, tuple, set)):
|
|
165
|
+
return list(self._normalize_select_option(val) for val in value)
|
|
166
|
+
|
|
167
|
+
if rname := getattr(value, "_rname", None):
|
|
168
|
+
return str(rname)
|
|
169
|
+
|
|
170
|
+
return str(value)
|
|
156
171
|
|
|
157
172
|
def select(self, *fields: t.Any, **options: t.Unpack[SelectKwargs]) -> "QueryBuilder[T_MetaInstance]":
|
|
158
173
|
"""
|
|
@@ -179,6 +194,11 @@ class QueryBuilder[T_MetaInstance: _TypedTable]:
|
|
|
179
194
|
left: othertable.on(query) - do a LEFT JOIN. Using TypeDAL relationships with .join() is recommended!
|
|
180
195
|
cache: cache the query result to speed up repeated queries; e.g. (cache=(cache.ram, 3600), cacheable=True)
|
|
181
196
|
"""
|
|
197
|
+
|
|
198
|
+
for key in ("distinct",):
|
|
199
|
+
if options.get(key):
|
|
200
|
+
options[key] = self._normalize_select_option(options[key])
|
|
201
|
+
|
|
182
202
|
return self._extend(select_args=list(fields), select_kwargs=options)
|
|
183
203
|
|
|
184
204
|
def orderby(self, *fields: OrderBy) -> "QueryBuilder[T_MetaInstance]":
|
|
@@ -272,7 +292,10 @@ class QueryBuilder[T_MetaInstance: _TypedTable]:
|
|
|
272
292
|
return self._extend(overwrite_query=new_query)
|
|
273
293
|
|
|
274
294
|
def _parse_relationships(
|
|
275
|
-
self,
|
|
295
|
+
self,
|
|
296
|
+
fields: t.Iterable[str | t.Type[TypedTable]],
|
|
297
|
+
method: JOIN_OPTIONS = None,
|
|
298
|
+
**update: t.Any,
|
|
276
299
|
) -> dict[str, Relationship[t.Any]]:
|
|
277
300
|
"""
|
|
278
301
|
Parse relationship fields into a dict of base relationships with nested relationships.
|
|
@@ -766,7 +789,11 @@ class QueryBuilder[T_MetaInstance: _TypedTable]:
|
|
|
766
789
|
return joins
|
|
767
790
|
|
|
768
791
|
def _build_inner_joins_recursive(
|
|
769
|
-
self,
|
|
792
|
+
self,
|
|
793
|
+
relation: Relationship[t.Any],
|
|
794
|
+
parent_table: t.Type[_TypedTable],
|
|
795
|
+
key: str,
|
|
796
|
+
parent_key: str = "",
|
|
770
797
|
) -> list[t.Any]:
|
|
771
798
|
"""Recursively build inner joins for a relationship and its nested relationships."""
|
|
772
799
|
db = self._get_db()
|
|
@@ -810,6 +837,7 @@ class QueryBuilder[T_MetaInstance: _TypedTable]:
|
|
|
810
837
|
|
|
811
838
|
if joins:
|
|
812
839
|
kwargs["join"] = joins
|
|
840
|
+
kwargs["distinct"] = True
|
|
813
841
|
|
|
814
842
|
ids = db(query)._select(model.id, **kwargs)
|
|
815
843
|
query = model.id.belongs(ids)
|
|
@@ -876,13 +904,21 @@ class QueryBuilder[T_MetaInstance: _TypedTable]:
|
|
|
876
904
|
# todo: add additional test, deduplicate
|
|
877
905
|
nested_key = f"{parent_key}.{nested_name}" if parent_key else f"{key}.{nested_name}"
|
|
878
906
|
select_args = self._process_relationship_for_left_join(
|
|
879
|
-
nested,
|
|
907
|
+
nested,
|
|
908
|
+
nested_name,
|
|
909
|
+
select_args,
|
|
910
|
+
left_joins,
|
|
911
|
+
other,
|
|
912
|
+
nested_key,
|
|
880
913
|
)
|
|
881
914
|
|
|
882
915
|
return select_args
|
|
883
916
|
|
|
884
917
|
def _ensure_relationship_fields(
|
|
885
|
-
self,
|
|
918
|
+
self,
|
|
919
|
+
select_args: list[t.Any],
|
|
920
|
+
other: t.Type[TypedTable],
|
|
921
|
+
select_fields: str,
|
|
886
922
|
) -> list[t.Any]:
|
|
887
923
|
"""Ensure required fields from relationship table are selected."""
|
|
888
924
|
if f"{other}." not in select_fields:
|
|
@@ -895,7 +931,10 @@ class QueryBuilder[T_MetaInstance: _TypedTable]:
|
|
|
895
931
|
return select_args
|
|
896
932
|
|
|
897
933
|
def _update_select_args_with_alias(
|
|
898
|
-
self,
|
|
934
|
+
self,
|
|
935
|
+
select_args: list[t.Any],
|
|
936
|
+
pre_alias: str,
|
|
937
|
+
other: t.Type[TypedTable],
|
|
899
938
|
) -> list[t.Any]:
|
|
900
939
|
"""Update select_args to use aliased table names."""
|
|
901
940
|
post_alias = str(other).split(" AS ")[-1]
|
|
@@ -1090,19 +1129,31 @@ class QueryBuilder[T_MetaInstance: _TypedTable]:
|
|
|
1090
1129
|
"""
|
|
1091
1130
|
yield from self.collect()
|
|
1092
1131
|
|
|
1093
|
-
def __count(
|
|
1132
|
+
def __count(
|
|
1133
|
+
self,
|
|
1134
|
+
db: TypeDAL,
|
|
1135
|
+
distinct: t.Optional[bool] = None,
|
|
1136
|
+
*,
|
|
1137
|
+
include_left_for_distinct: bool = True,
|
|
1138
|
+
) -> Query:
|
|
1094
1139
|
# internal, shared logic between .count and ._count
|
|
1095
1140
|
model = self.model
|
|
1096
1141
|
query = self.query
|
|
1097
1142
|
for key, relation in self.relationships.items():
|
|
1098
|
-
if
|
|
1143
|
+
if not relation.condition:
|
|
1144
|
+
continue
|
|
1145
|
+
|
|
1146
|
+
include_left_join = distinct and include_left_for_distinct
|
|
1147
|
+
if relation.join != "inner" and not include_left_join:
|
|
1099
1148
|
continue
|
|
1100
1149
|
|
|
1101
1150
|
other = relation.get_table(db)
|
|
1102
1151
|
if not distinct:
|
|
1103
1152
|
# todo: can this lead to other issues?
|
|
1104
1153
|
other = other.with_alias(f"{key}_{hash(relation)}")
|
|
1105
|
-
|
|
1154
|
+
|
|
1155
|
+
if relation.condition is not None:
|
|
1156
|
+
query &= relation.condition(model, other)
|
|
1106
1157
|
|
|
1107
1158
|
return query
|
|
1108
1159
|
|
|
@@ -1137,12 +1188,20 @@ class QueryBuilder[T_MetaInstance: _TypedTable]:
|
|
|
1137
1188
|
require_permission(self._permissions, "read")
|
|
1138
1189
|
return bool(self.count())
|
|
1139
1190
|
|
|
1191
|
+
def __pagination_count(self) -> int:
|
|
1192
|
+
if not self.relationships:
|
|
1193
|
+
return self.count()
|
|
1194
|
+
|
|
1195
|
+
db = self._get_db()
|
|
1196
|
+
query = self.__count(db, distinct=self.model.id, include_left_for_distinct=False)
|
|
1197
|
+
return db(query).count(self.model.id)
|
|
1198
|
+
|
|
1140
1199
|
def __paginate(
|
|
1141
1200
|
self,
|
|
1142
1201
|
limit: int,
|
|
1143
1202
|
page: int = 1,
|
|
1144
1203
|
) -> "QueryBuilder[T_MetaInstance]":
|
|
1145
|
-
available = self.
|
|
1204
|
+
available = self.__pagination_count()
|
|
1146
1205
|
|
|
1147
1206
|
_from = limit * (page - 1)
|
|
1148
1207
|
_to = (limit * page) if limit else available
|
|
@@ -71,7 +71,7 @@ def merge_permissions(*permission_sets: Permissions | None) -> Permissions:
|
|
|
71
71
|
|
|
72
72
|
for key in permission_types:
|
|
73
73
|
if key in permission_set:
|
|
74
|
-
merged[key] = merged[key] and bool(permission_set[key])
|
|
74
|
+
merged[key] = merged[key] and bool(permission_set[key]) # type: ignore
|
|
75
75
|
|
|
76
76
|
return t.cast(Permissions, merged)
|
|
77
77
|
|
|
@@ -188,6 +188,8 @@ class Reference(_Reference):
|
|
|
188
188
|
class Field(_Field):
|
|
189
189
|
"""Pydal Field object. Make mypy happy."""
|
|
190
190
|
|
|
191
|
+
_rname: str
|
|
192
|
+
|
|
191
193
|
|
|
192
194
|
class Rows(_Rows):
|
|
193
195
|
"""Pydal Rows object. Make mypy happy."""
|
|
@@ -283,7 +285,7 @@ class SelectKwargs(t.TypedDict, total=False):
|
|
|
283
285
|
groupby: "GroupBy | t.Iterable[GroupBy] | None"
|
|
284
286
|
having: "Having | None"
|
|
285
287
|
limitby: t.Optional[tuple[int, int]]
|
|
286
|
-
distinct: bool | Field | Expression | str
|
|
288
|
+
distinct: bool | Field | Expression | str | t.Iterable[str]
|
|
287
289
|
orderby_on_limitby: bool
|
|
288
290
|
cacheable: bool
|
|
289
291
|
cache: "CacheTuple"
|
|
File without changes
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import tempfile
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from testcontainers.postgres import PostgresContainer
|
|
5
|
+
|
|
6
|
+
from src.typedal import TypeDAL
|
|
7
|
+
|
|
8
|
+
postgres = PostgresContainer(
|
|
9
|
+
dbname="postgres",
|
|
10
|
+
username="someuser",
|
|
11
|
+
password="somepass",
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.fixture(scope="module", autouse=True)
|
|
16
|
+
def psql(request):
|
|
17
|
+
postgres.ports = {
|
|
18
|
+
5432: 9631, # as set in valid.env
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
request.addfinalizer(postgres.stop)
|
|
22
|
+
postgres.start()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@pytest.fixture
|
|
26
|
+
def dal_psql(psql):
|
|
27
|
+
conn_str = postgres.get_connection_url()
|
|
28
|
+
uri = "postgres://" + conn_str.split("://")[-1]
|
|
29
|
+
with tempfile.TemporaryDirectory() as d:
|
|
30
|
+
yield TypeDAL(uri, attempts=1, migrate=True, enable_typedal_caching=False, folder=d)
|
|
@@ -9,7 +9,6 @@ from pathlib import Path
|
|
|
9
9
|
|
|
10
10
|
import pytest
|
|
11
11
|
from contextlib_chdir import chdir
|
|
12
|
-
from testcontainers.postgres import PostgresContainer
|
|
13
12
|
|
|
14
13
|
from src.typedal import TypeDAL, TypedField, TypedTable
|
|
15
14
|
from src.typedal.config import (
|
|
@@ -20,22 +19,6 @@ from src.typedal.config import (
|
|
|
20
19
|
)
|
|
21
20
|
from src.typedal.fields import PointField, TimestampField, UUIDField
|
|
22
21
|
|
|
23
|
-
postgres = PostgresContainer(
|
|
24
|
-
dbname="postgres",
|
|
25
|
-
username="someuser",
|
|
26
|
-
password="somepass",
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
@pytest.fixture(scope="module", autouse=True)
|
|
31
|
-
def psql(request):
|
|
32
|
-
postgres.ports = {
|
|
33
|
-
5432: 9631, # as set in valid.env
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
request.addfinalizer(postgres.stop)
|
|
37
|
-
postgres.start()
|
|
38
|
-
|
|
39
22
|
|
|
40
23
|
@pytest.fixture
|
|
41
24
|
def at_temp_dir():
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import pytest
|
|
2
|
-
from pydal.objects import Query
|
|
2
|
+
from pydal.objects import Field, Query
|
|
3
3
|
|
|
4
4
|
from src.typedal import TypeDAL, TypedField, TypedTable, relationship
|
|
5
5
|
from typedal import QueryBuilder
|
|
6
|
+
from typedal.fields import rname
|
|
6
7
|
|
|
7
8
|
db = TypeDAL("sqlite:memory")
|
|
8
9
|
|
|
@@ -559,6 +560,48 @@ def test_orderby():
|
|
|
559
560
|
)
|
|
560
561
|
|
|
561
562
|
|
|
563
|
+
def test_select_kwargs_use_rname_psql(dal_psql: TypeDAL):
|
|
564
|
+
db = dal_psql
|
|
565
|
+
|
|
566
|
+
@db.define(rname="some_table")
|
|
567
|
+
class Sometable(TypedTable):
|
|
568
|
+
name = TypedField(str, rname="some_name")
|
|
569
|
+
|
|
570
|
+
Sometable.insert(name="B")
|
|
571
|
+
Sometable.insert(name="A")
|
|
572
|
+
Sometable.insert(name="A")
|
|
573
|
+
db.commit()
|
|
574
|
+
|
|
575
|
+
# default orderby/distinct
|
|
576
|
+
|
|
577
|
+
orderby_sql = Sometable.select(orderby=Sometable.name).to_sql().lower()
|
|
578
|
+
|
|
579
|
+
assert "sometable.name" not in orderby_sql
|
|
580
|
+
assert "some_table.some_name" in orderby_sql
|
|
581
|
+
|
|
582
|
+
distinct_sql1 = Sometable.select(Sometable.name, distinct=Sometable.name).to_sql().lower()
|
|
583
|
+
distinct_sql2 = Sometable.select(Sometable.name, distinct=(Sometable.name, Sometable.id)).to_sql().lower()
|
|
584
|
+
distinct_sql3 = Sometable.select(Sometable.name, distinct=("name", "id")).to_sql().lower()
|
|
585
|
+
|
|
586
|
+
for sql in (distinct_sql1, distinct_sql2, distinct_sql3):
|
|
587
|
+
assert "sometable.name" not in sql
|
|
588
|
+
assert "some_table.some_name" in sql
|
|
589
|
+
|
|
590
|
+
builder = Sometable.select(orderby=Sometable.name)
|
|
591
|
+
existing_orderby: Field = builder.select_kwargs.get("orderby")
|
|
592
|
+
|
|
593
|
+
existing_orderby_name = rname(existing_orderby)
|
|
594
|
+
sometable_name = rname(Sometable.name)
|
|
595
|
+
sometable_name_pydal = rname(db.sometable.name)
|
|
596
|
+
|
|
597
|
+
with pytest.raises(ValueError):
|
|
598
|
+
rname(TypedField(str, rname="some_name"))
|
|
599
|
+
|
|
600
|
+
for sql in (existing_orderby_name, sometable_name, sometable_name_pydal):
|
|
601
|
+
assert "sometable.name" not in sql
|
|
602
|
+
assert "some_table.some_name" in sql
|
|
603
|
+
|
|
604
|
+
|
|
562
605
|
def test_execute():
|
|
563
606
|
_setup_data()
|
|
564
607
|
|
|
@@ -888,6 +888,33 @@ def test_join_with_select():
|
|
|
888
888
|
assert not hasattr(user.articles[0], "title")
|
|
889
889
|
|
|
890
890
|
|
|
891
|
+
def test_paginate_with_inner_join_uses_root_entities():
|
|
892
|
+
_setup_data()
|
|
893
|
+
|
|
894
|
+
page1 = User.join("articles", method="inner").orderby(~User.id).paginate(limit=2, page=1)
|
|
895
|
+
page2 = page1.next()
|
|
896
|
+
page1_again = page2.previous()
|
|
897
|
+
|
|
898
|
+
assert [user.id for user in page1] == [4, 3]
|
|
899
|
+
assert [user.id for user in page2] == [2]
|
|
900
|
+
assert [user.id for user in page1_again] == [4, 3]
|
|
901
|
+
|
|
902
|
+
assert sorted(article.title for article in page1.first().articles) == [
|
|
903
|
+
"Untagged Article 1",
|
|
904
|
+
"Untagged Article 2",
|
|
905
|
+
]
|
|
906
|
+
assert [article.title for article in page2.first().articles] == ["Article 1"]
|
|
907
|
+
|
|
908
|
+
assert page1.pagination["total_items"] == 3
|
|
909
|
+
assert page1.pagination["total_pages"] == 2
|
|
910
|
+
assert page1.pagination["has_next_page"] is True
|
|
911
|
+
assert page2.pagination["has_next_page"] is False
|
|
912
|
+
assert page2.pagination["has_prev_page"] is True
|
|
913
|
+
|
|
914
|
+
with pytest.raises(StopIteration):
|
|
915
|
+
page2.next()
|
|
916
|
+
|
|
917
|
+
|
|
891
918
|
def test_count_with_join():
|
|
892
919
|
_setup_data()
|
|
893
920
|
|
|
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
|
|
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
|