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.
Files changed (80) hide show
  1. typedal-4.9.3/.crush/.gitignore +1 -0
  2. typedal-4.9.3/.crush/crush.db-shm +0 -0
  3. typedal-4.9.3/.crush/crush.db-wal +0 -0
  4. typedal-4.9.3/.crush/logs/crush.log +37 -0
  5. {typedal-4.9.1 → typedal-4.9.3}/CHANGELOG.md +12 -0
  6. {typedal-4.9.1 → typedal-4.9.3}/PKG-INFO +2 -1
  7. {typedal-4.9.1 → typedal-4.9.3}/pyproject.toml +1 -0
  8. {typedal-4.9.1 → typedal-4.9.3}/src/typedal/__about__.py +1 -1
  9. {typedal-4.9.1 → typedal-4.9.3}/src/typedal/fields.py +14 -0
  10. {typedal-4.9.1 → typedal-4.9.3}/src/typedal/query_builder.py +69 -10
  11. {typedal-4.9.1 → typedal-4.9.3}/src/typedal/types.py +4 -2
  12. typedal-4.9.3/tests/__init__.py +0 -0
  13. typedal-4.9.3/tests/conftest.py +30 -0
  14. {typedal-4.9.1 → typedal-4.9.3}/tests/test_config.py +0 -17
  15. {typedal-4.9.1 → typedal-4.9.3}/tests/test_query_builder.py +44 -1
  16. {typedal-4.9.1 → typedal-4.9.3}/tests/test_relationships.py +27 -0
  17. /typedal-4.9.1/src/typedal/py.typed → /typedal-4.9.3/.crush/init +0 -0
  18. {typedal-4.9.1 → typedal-4.9.3}/.github/workflows/su6.yml +0 -0
  19. {typedal-4.9.1 → typedal-4.9.3}/.gitignore +0 -0
  20. {typedal-4.9.1 → typedal-4.9.3}/.readthedocs.yml +0 -0
  21. {typedal-4.9.1 → typedal-4.9.3}/README.md +0 -0
  22. {typedal-4.9.1 → typedal-4.9.3}/coverage.svg +0 -0
  23. {typedal-4.9.1 → typedal-4.9.3}/docs/10_advanced_apis.md +0 -0
  24. {typedal-4.9.1 → typedal-4.9.3}/docs/1_getting_started.md +0 -0
  25. {typedal-4.9.1 → typedal-4.9.3}/docs/2_defining_tables.md +0 -0
  26. {typedal-4.9.1 → typedal-4.9.3}/docs/3_building_queries.md +0 -0
  27. {typedal-4.9.1 → typedal-4.9.3}/docs/4_relationships.md +0 -0
  28. {typedal-4.9.1 → typedal-4.9.3}/docs/5_py4web.md +0 -0
  29. {typedal-4.9.1 → typedal-4.9.3}/docs/6_migrations.md +0 -0
  30. {typedal-4.9.1 → typedal-4.9.3}/docs/7_configuration.md +0 -0
  31. {typedal-4.9.1 → typedal-4.9.3}/docs/8_mixins.md +0 -0
  32. {typedal-4.9.1 → typedal-4.9.3}/docs/9_memoization.md +0 -0
  33. {typedal-4.9.1 → typedal-4.9.3}/docs/css/code_blocks.css +0 -0
  34. {typedal-4.9.1 → typedal-4.9.3}/docs/index.md +0 -0
  35. {typedal-4.9.1 → typedal-4.9.3}/docs/requirements.txt +0 -0
  36. {typedal-4.9.1 → typedal-4.9.3}/example_new.py +0 -0
  37. {typedal-4.9.1 → typedal-4.9.3}/example_old.py +0 -0
  38. {typedal-4.9.1 → typedal-4.9.3}/mkdocs.yml +0 -0
  39. {typedal-4.9.1 → typedal-4.9.3}/src/typedal/__init__.py +0 -0
  40. {typedal-4.9.1 → typedal-4.9.3}/src/typedal/caching.py +0 -0
  41. {typedal-4.9.1 → typedal-4.9.3}/src/typedal/cli.py +0 -0
  42. {typedal-4.9.1 → typedal-4.9.3}/src/typedal/config.py +0 -0
  43. {typedal-4.9.1 → typedal-4.9.3}/src/typedal/constants.py +0 -0
  44. {typedal-4.9.1 → typedal-4.9.3}/src/typedal/core.py +0 -0
  45. {typedal-4.9.1 → typedal-4.9.3}/src/typedal/define.py +0 -0
  46. {typedal-4.9.1 → typedal-4.9.3}/src/typedal/enum_helpers.py +0 -0
  47. {typedal-4.9.1 → typedal-4.9.3}/src/typedal/for_py4web.py +0 -0
  48. {typedal-4.9.1 → typedal-4.9.3}/src/typedal/for_web2py.py +0 -0
  49. {typedal-4.9.1 → typedal-4.9.3}/src/typedal/helpers.py +0 -0
  50. {typedal-4.9.1 → typedal-4.9.3}/src/typedal/mixins.py +0 -0
  51. /typedal-4.9.1/tests/__init__.py → /typedal-4.9.3/src/typedal/py.typed +0 -0
  52. {typedal-4.9.1 → typedal-4.9.3}/src/typedal/relationships.py +0 -0
  53. {typedal-4.9.1 → typedal-4.9.3}/src/typedal/rows.py +0 -0
  54. {typedal-4.9.1 → typedal-4.9.3}/src/typedal/serializers/as_json.py +0 -0
  55. {typedal-4.9.1 → typedal-4.9.3}/src/typedal/serializers/typescript.py +0 -0
  56. {typedal-4.9.1 → typedal-4.9.3}/src/typedal/tables.py +0 -0
  57. {typedal-4.9.1 → typedal-4.9.3}/src/typedal/web2py_py4web_shared.py +0 -0
  58. {typedal-4.9.1 → typedal-4.9.3}/tasks.py +0 -0
  59. {typedal-4.9.1 → typedal-4.9.3}/tests/configs/simple.toml +0 -0
  60. {typedal-4.9.1 → typedal-4.9.3}/tests/configs/valid.env +0 -0
  61. {typedal-4.9.1 → typedal-4.9.3}/tests/configs/valid.toml +0 -0
  62. {typedal-4.9.1 → typedal-4.9.3}/tests/py314_tests.py +0 -0
  63. {typedal-4.9.1 → typedal-4.9.3}/tests/test_cli.py +0 -0
  64. {typedal-4.9.1 → typedal-4.9.3}/tests/test_docs_examples.py +0 -0
  65. {typedal-4.9.1 → typedal-4.9.3}/tests/test_helpers.py +0 -0
  66. {typedal-4.9.1 → typedal-4.9.3}/tests/test_json.py +0 -0
  67. {typedal-4.9.1 → typedal-4.9.3}/tests/test_main.py +0 -0
  68. {typedal-4.9.1 → typedal-4.9.3}/tests/test_mixins.py +0 -0
  69. {typedal-4.9.1 → typedal-4.9.3}/tests/test_mypy.py +0 -0
  70. {typedal-4.9.1 → typedal-4.9.3}/tests/test_orm.py +0 -0
  71. {typedal-4.9.1 → typedal-4.9.3}/tests/test_py4web.py +0 -0
  72. {typedal-4.9.1 → typedal-4.9.3}/tests/test_row.py +0 -0
  73. {typedal-4.9.1 → typedal-4.9.3}/tests/test_stats.py +0 -0
  74. {typedal-4.9.1 → typedal-4.9.3}/tests/test_table.py +0 -0
  75. {typedal-4.9.1 → typedal-4.9.3}/tests/test_typescript.py +0 -0
  76. {typedal-4.9.1 → typedal-4.9.3}/tests/test_typing_mypy.md +0 -0
  77. {typedal-4.9.1 → typedal-4.9.3}/tests/test_typing_pyright.md +0 -0
  78. {typedal-4.9.1 → typedal-4.9.3}/tests/test_web2py.py +0 -0
  79. {typedal-4.9.1 → typedal-4.9.3}/tests/test_xx_others.py +0 -0
  80. {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.1
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'
@@ -97,6 +97,7 @@ dev = [
97
97
  "contextlib-chdir",
98
98
  "testcontainers",
99
99
  "pydantic < 3",
100
+ "psycopg2-binary",
100
101
  # depends on ->
101
102
  "requests<2.32",
102
103
  # mypy:
@@ -5,4 +5,4 @@ This file contains the Version info for this package.
5
5
  # SPDX-FileCopyrightText: 2023-present Robin van der Noord <robinvandernoord@gmail.com>
6
6
  #
7
7
  # SPDX-License-Identifier: MIT
8
- __version__ = "4.9.1"
8
+ __version__ = "4.9.3"
@@ -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=t.cast(Permissions, 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, fields: t.Iterable[str | t.Type[TypedTable]], method: JOIN_OPTIONS = None, **update: t.Any
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, relation: Relationship[t.Any], parent_table: t.Type[_TypedTable], key: str, parent_key: str = ""
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, nested_name, select_args, left_joins, other, nested_key
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, select_args: list[t.Any], other: t.Type[TypedTable], select_fields: str
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, select_args: list[t.Any], pre_alias: str, other: t.Type[TypedTable]
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(self, db: TypeDAL, distinct: t.Optional[bool] = None) -> Query:
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 (not relation.condition or relation.join != "inner") and not distinct:
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
- query &= relation.condition(model, other)
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.count()
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