TypeDAL 4.4.5__tar.gz → 4.4.6__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 (74) hide show
  1. {typedal-4.4.5 → typedal-4.4.6}/CHANGELOG.md +10 -0
  2. {typedal-4.4.5 → typedal-4.4.6}/PKG-INFO +44 -11
  3. {typedal-4.4.5 → typedal-4.4.6}/README.md +43 -10
  4. typedal-4.4.6/docs/10_advanced_apis.md +76 -0
  5. {typedal-4.4.5 → typedal-4.4.6}/docs/2_defining_tables.md +2 -2
  6. {typedal-4.4.5 → typedal-4.4.6}/docs/3_building_queries.md +8 -3
  7. {typedal-4.4.5 → typedal-4.4.6}/docs/4_relationships.md +24 -1
  8. {typedal-4.4.5 → typedal-4.4.6}/docs/7_configuration.md +1 -1
  9. {typedal-4.4.5 → typedal-4.4.6}/docs/9_memoization.md +5 -0
  10. {typedal-4.4.5 → typedal-4.4.6}/docs/index.md +1 -0
  11. {typedal-4.4.5 → typedal-4.4.6}/mkdocs.yml +2 -1
  12. {typedal-4.4.5 → typedal-4.4.6}/src/typedal/__about__.py +1 -1
  13. {typedal-4.4.5 → typedal-4.4.6}/src/typedal/query_builder.py +8 -3
  14. {typedal-4.4.5 → typedal-4.4.6}/tests/test_query_builder.py +8 -0
  15. typedal-4.4.5/.crush/.gitignore +0 -1
  16. typedal-4.4.5/.crush/crush.db-shm +0 -0
  17. typedal-4.4.5/.crush/crush.db-wal +0 -0
  18. typedal-4.4.5/.crush/init +0 -0
  19. typedal-4.4.5/.crush/logs/crush.log +0 -37
  20. {typedal-4.4.5 → typedal-4.4.6}/.github/workflows/su6.yml +0 -0
  21. {typedal-4.4.5 → typedal-4.4.6}/.gitignore +0 -0
  22. {typedal-4.4.5 → typedal-4.4.6}/.readthedocs.yml +0 -0
  23. {typedal-4.4.5 → typedal-4.4.6}/coverage.svg +0 -0
  24. {typedal-4.4.5 → typedal-4.4.6}/docs/1_getting_started.md +0 -0
  25. {typedal-4.4.5 → typedal-4.4.6}/docs/5_py4web.md +0 -0
  26. {typedal-4.4.5 → typedal-4.4.6}/docs/6_migrations.md +0 -0
  27. {typedal-4.4.5 → typedal-4.4.6}/docs/8_mixins.md +0 -0
  28. {typedal-4.4.5 → typedal-4.4.6}/docs/css/code_blocks.css +0 -0
  29. {typedal-4.4.5 → typedal-4.4.6}/docs/requirements.txt +0 -0
  30. {typedal-4.4.5 → typedal-4.4.6}/example_new.py +0 -0
  31. {typedal-4.4.5 → typedal-4.4.6}/example_old.py +0 -0
  32. {typedal-4.4.5 → typedal-4.4.6}/pyproject.toml +0 -0
  33. {typedal-4.4.5 → typedal-4.4.6}/src/typedal/__init__.py +0 -0
  34. {typedal-4.4.5 → typedal-4.4.6}/src/typedal/caching.py +0 -0
  35. {typedal-4.4.5 → typedal-4.4.6}/src/typedal/cli.py +0 -0
  36. {typedal-4.4.5 → typedal-4.4.6}/src/typedal/config.py +0 -0
  37. {typedal-4.4.5 → typedal-4.4.6}/src/typedal/constants.py +0 -0
  38. {typedal-4.4.5 → typedal-4.4.6}/src/typedal/core.py +0 -0
  39. {typedal-4.4.5 → typedal-4.4.6}/src/typedal/define.py +0 -0
  40. {typedal-4.4.5 → typedal-4.4.6}/src/typedal/fields.py +0 -0
  41. {typedal-4.4.5 → typedal-4.4.6}/src/typedal/for_py4web.py +0 -0
  42. {typedal-4.4.5 → typedal-4.4.6}/src/typedal/for_web2py.py +0 -0
  43. {typedal-4.4.5 → typedal-4.4.6}/src/typedal/helpers.py +0 -0
  44. {typedal-4.4.5 → typedal-4.4.6}/src/typedal/mixins.py +0 -0
  45. {typedal-4.4.5 → typedal-4.4.6}/src/typedal/py.typed +0 -0
  46. {typedal-4.4.5 → typedal-4.4.6}/src/typedal/relationships.py +0 -0
  47. {typedal-4.4.5 → typedal-4.4.6}/src/typedal/rows.py +0 -0
  48. {typedal-4.4.5 → typedal-4.4.6}/src/typedal/serializers/as_json.py +0 -0
  49. {typedal-4.4.5 → typedal-4.4.6}/src/typedal/tables.py +0 -0
  50. {typedal-4.4.5 → typedal-4.4.6}/src/typedal/types.py +0 -0
  51. {typedal-4.4.5 → typedal-4.4.6}/src/typedal/web2py_py4web_shared.py +0 -0
  52. {typedal-4.4.5 → typedal-4.4.6}/tasks.py +0 -0
  53. {typedal-4.4.5 → typedal-4.4.6}/tests/__init__.py +0 -0
  54. {typedal-4.4.5 → typedal-4.4.6}/tests/configs/simple.toml +0 -0
  55. {typedal-4.4.5 → typedal-4.4.6}/tests/configs/valid.env +0 -0
  56. {typedal-4.4.5 → typedal-4.4.6}/tests/configs/valid.toml +0 -0
  57. {typedal-4.4.5 → typedal-4.4.6}/tests/py314_tests.py +0 -0
  58. {typedal-4.4.5 → typedal-4.4.6}/tests/test_cli.py +0 -0
  59. {typedal-4.4.5 → typedal-4.4.6}/tests/test_config.py +0 -0
  60. {typedal-4.4.5 → typedal-4.4.6}/tests/test_docs_examples.py +0 -0
  61. {typedal-4.4.5 → typedal-4.4.6}/tests/test_helpers.py +0 -0
  62. {typedal-4.4.5 → typedal-4.4.6}/tests/test_json.py +0 -0
  63. {typedal-4.4.5 → typedal-4.4.6}/tests/test_main.py +0 -0
  64. {typedal-4.4.5 → typedal-4.4.6}/tests/test_mixins.py +0 -0
  65. {typedal-4.4.5 → typedal-4.4.6}/tests/test_mypy.py +0 -0
  66. {typedal-4.4.5 → typedal-4.4.6}/tests/test_orm.py +0 -0
  67. {typedal-4.4.5 → typedal-4.4.6}/tests/test_py4web.py +0 -0
  68. {typedal-4.4.5 → typedal-4.4.6}/tests/test_relationships.py +0 -0
  69. {typedal-4.4.5 → typedal-4.4.6}/tests/test_row.py +0 -0
  70. {typedal-4.4.5 → typedal-4.4.6}/tests/test_stats.py +0 -0
  71. {typedal-4.4.5 → typedal-4.4.6}/tests/test_table.py +0 -0
  72. {typedal-4.4.5 → typedal-4.4.6}/tests/test_web2py.py +0 -0
  73. {typedal-4.4.5 → typedal-4.4.6}/tests/test_xx_others.py +0 -0
  74. {typedal-4.4.5 → typedal-4.4.6}/tests/timings.py +0 -0
@@ -2,6 +2,16 @@
2
2
 
3
3
  <!--next-version-placeholder-->
4
4
 
5
+ ## v4.4.6 (2026-03-05)
6
+
7
+ ### Fix
8
+
9
+ * Support .first() when using querybuilder on pydal-style tables. ([`d75f254`](https://github.com/trialandsuccess/TypeDAL/commit/d75f25440f0dcca51991bc047bf7ce8d479f942b))
10
+
11
+ ### Documentation
12
+
13
+ * Make docs up-to-date with recently introduced features, improve reading flow for new users ([`64bb7be`](https://github.com/trialandsuccess/TypeDAL/commit/64bb7be220c5f1a5998940b320565b5a3dbca988))
14
+
5
15
  ## v4.4.5 (2026-02-27)
6
16
 
7
17
  ### Fix
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: TypeDAL
3
- Version: 4.4.5
3
+ Version: 4.4.6
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
@@ -81,13 +81,49 @@ the underlying `db.define_table` pydal Tables.
81
81
  e.g. `rows: TypedRows[SomeTable] = db(...).select()`. When using the QueryBuilder, a `TypedRows` instance is returned
82
82
  by `.collect()`.
83
83
 
84
- Version 2.0 also introduces more ORM-like funcionality.
84
+ Version 2.0 also introduces more ORM-like functionality.
85
85
  Most notably, a Typed Query Builder that sees your table classes as models with relationships to each other.
86
86
  See [3. Building Queries](https://typedal.readthedocs.io/en/stable/3_building_queries/) for more
87
87
  details.
88
88
 
89
+ ## Quickstart
90
+
91
+ ```bash
92
+ uv pip install typedal
93
+ # alternative:
94
+ pip install typedal
95
+ ```
96
+
97
+ ```python
98
+ from typedal import TypeDAL, TypedTable
99
+
100
+ db = TypeDAL("sqlite:memory")
101
+ # Alternatives:
102
+ # db = TypeDAL("sqlite://storage.sqlite")
103
+ # db = TypeDAL("postgres://user:password@localhost:5432/mydb")
104
+ # db = TypeDAL("mysql://user:password@localhost:3306/mydb")
105
+ # ...
106
+
107
+ @db.define()
108
+ class User(TypedTable):
109
+ name: str
110
+ age: int | None
111
+
112
+
113
+ User.insert(name="Alice", age=30)
114
+ adults = User.where(User.age >= 18).collect()
115
+ print(adults.column("name")) # ['Alice']
116
+ ```
117
+
118
+ If you are new to TypeDAL, start with:
119
+
120
+ 1. [Getting Started](https://typedal.readthedocs.io/en/stable/1_getting_started/)
121
+ 2. [Defining Tables](https://typedal.readthedocs.io/en/stable/2_defining_tables/)
122
+ 3. [Building Queries](https://typedal.readthedocs.io/en/stable/3_building_queries/)
123
+ 4. [Relationships](https://typedal.readthedocs.io/en/stable/4_relationships/)
124
+
89
125
  ## CLI
90
- The Typedal CLI provides a convenient interface for generating SQL migrations for [edwh-migrate](https://github.com/educationwarehouse/migrate#readme)
126
+ The TypeDAL CLI provides a convenient interface for generating SQL migrations for [edwh-migrate](https://github.com/educationwarehouse/migrate#readme)
91
127
  from PyDAL or TypeDAL configurations using [pydal2sql](https://github.com/robinvandernoord/pydal2sql).
92
128
  It offers various commands to streamline database management tasks.
93
129
 
@@ -252,7 +288,7 @@ row = db.table_name(id=1) # -> Any (Row)
252
288
  # all:
253
289
  all_rows = TableName.collect() # or .all()
254
290
  # some:
255
- # order of select and where is interchangable here
291
+ # order of select and where is interchangeable here
256
292
  rows = TableName.select(Tablename.id).where(TableName.id > 5).where(TableName.id < 50).collect()
257
293
  # one:
258
294
  row = TableName(id=1) # or .where(...).first()
@@ -330,13 +366,10 @@ db.commit() # this is usually done automatically but sometimes you want to manua
330
366
 
331
367
  ## Caveats
332
368
 
333
- - This package depends heavily on the current implementation of annotations (which are computed when the class is
334
- defined). PEP 563 (Postponed Evaluation of Annotations, accepted) aims to change this behavior (
335
- and `from __future__ import annotations` already does) in a way that this module currently can not handle: all
336
- annotations are converted to string representations. This makes it very hard to re-evaluate the annotation into the
337
- original type, since the variable scope is lost (and thus references to variables or other classes are ambiguous or
338
- simply impossible to find).
369
+ - Some editors (notably PyCharm) cannot always distinguish class-level and instance-level access on the same symbol.
370
+ For example, `Model.somefield` is a field descriptor (query operations like `.belongs()`), while
371
+ `model.somefield` is the runtime value (for example `list[str]`).
339
372
  - `TypedField` limitations; Since pydal implements some magic methods to perform queries, some features of typing will
340
373
  not work on a typed field: `typing.Optional` or a union (`Field() | None`) will result in errors. The only way to make
341
374
  a typedfield optional right now, would be to set `required=False` as an argument yourself. This is also a reason
342
- why `typing.get_type_hints` is not a solution for the first caveat.
375
+ why `typing.get_type_hints` is not a complete solution.
@@ -24,13 +24,49 @@ the underlying `db.define_table` pydal Tables.
24
24
  e.g. `rows: TypedRows[SomeTable] = db(...).select()`. When using the QueryBuilder, a `TypedRows` instance is returned
25
25
  by `.collect()`.
26
26
 
27
- Version 2.0 also introduces more ORM-like funcionality.
27
+ Version 2.0 also introduces more ORM-like functionality.
28
28
  Most notably, a Typed Query Builder that sees your table classes as models with relationships to each other.
29
29
  See [3. Building Queries](https://typedal.readthedocs.io/en/stable/3_building_queries/) for more
30
30
  details.
31
31
 
32
+ ## Quickstart
33
+
34
+ ```bash
35
+ uv pip install typedal
36
+ # alternative:
37
+ pip install typedal
38
+ ```
39
+
40
+ ```python
41
+ from typedal import TypeDAL, TypedTable
42
+
43
+ db = TypeDAL("sqlite:memory")
44
+ # Alternatives:
45
+ # db = TypeDAL("sqlite://storage.sqlite")
46
+ # db = TypeDAL("postgres://user:password@localhost:5432/mydb")
47
+ # db = TypeDAL("mysql://user:password@localhost:3306/mydb")
48
+ # ...
49
+
50
+ @db.define()
51
+ class User(TypedTable):
52
+ name: str
53
+ age: int | None
54
+
55
+
56
+ User.insert(name="Alice", age=30)
57
+ adults = User.where(User.age >= 18).collect()
58
+ print(adults.column("name")) # ['Alice']
59
+ ```
60
+
61
+ If you are new to TypeDAL, start with:
62
+
63
+ 1. [Getting Started](https://typedal.readthedocs.io/en/stable/1_getting_started/)
64
+ 2. [Defining Tables](https://typedal.readthedocs.io/en/stable/2_defining_tables/)
65
+ 3. [Building Queries](https://typedal.readthedocs.io/en/stable/3_building_queries/)
66
+ 4. [Relationships](https://typedal.readthedocs.io/en/stable/4_relationships/)
67
+
32
68
  ## CLI
33
- The Typedal CLI provides a convenient interface for generating SQL migrations for [edwh-migrate](https://github.com/educationwarehouse/migrate#readme)
69
+ The TypeDAL CLI provides a convenient interface for generating SQL migrations for [edwh-migrate](https://github.com/educationwarehouse/migrate#readme)
34
70
  from PyDAL or TypeDAL configurations using [pydal2sql](https://github.com/robinvandernoord/pydal2sql).
35
71
  It offers various commands to streamline database management tasks.
36
72
 
@@ -195,7 +231,7 @@ row = db.table_name(id=1) # -> Any (Row)
195
231
  # all:
196
232
  all_rows = TableName.collect() # or .all()
197
233
  # some:
198
- # order of select and where is interchangable here
234
+ # order of select and where is interchangeable here
199
235
  rows = TableName.select(Tablename.id).where(TableName.id > 5).where(TableName.id < 50).collect()
200
236
  # one:
201
237
  row = TableName(id=1) # or .where(...).first()
@@ -273,13 +309,10 @@ db.commit() # this is usually done automatically but sometimes you want to manua
273
309
 
274
310
  ## Caveats
275
311
 
276
- - This package depends heavily on the current implementation of annotations (which are computed when the class is
277
- defined). PEP 563 (Postponed Evaluation of Annotations, accepted) aims to change this behavior (
278
- and `from __future__ import annotations` already does) in a way that this module currently can not handle: all
279
- annotations are converted to string representations. This makes it very hard to re-evaluate the annotation into the
280
- original type, since the variable scope is lost (and thus references to variables or other classes are ambiguous or
281
- simply impossible to find).
312
+ - Some editors (notably PyCharm) cannot always distinguish class-level and instance-level access on the same symbol.
313
+ For example, `Model.somefield` is a field descriptor (query operations like `.belongs()`), while
314
+ `model.somefield` is the runtime value (for example `list[str]`).
282
315
  - `TypedField` limitations; Since pydal implements some magic methods to perform queries, some features of typing will
283
316
  not work on a typed field: `typing.Optional` or a union (`Field() | None`) will result in errors. The only way to make
284
317
  a typedfield optional right now, would be to set `required=False` as an argument yourself. This is also a reason
285
- why `typing.get_type_hints` is not a solution for the first caveat.
318
+ why `typing.get_type_hints` is not a complete solution.
@@ -0,0 +1,76 @@
1
+ # 10. Advanced APIs
2
+
3
+ This chapter documents a few public APIs that are useful in specific cases, but are not part of the default onboarding flow.
4
+
5
+ ## QueryBuilder on old-style pyDAL tables
6
+
7
+ If you are migrating incrementally, you can still use TypeDAL's query builder on existing pyDAL tables:
8
+
9
+ ```python
10
+ from typedal import QueryBuilder
11
+
12
+ rows = QueryBuilder(db.some_table).where(id=2).collect()
13
+ ```
14
+
15
+ This gives you partial builder ergonomics while keeping your existing table definitions.
16
+
17
+ > **Important:** support is intentionally limited for old-style tables.
18
+ > Internally, `.collect()` is effectively a passthrough to `.execute()` and returns regular pyDAL `Rows`.
19
+ > `.first()`/`.first_or_fail()` return a regular pyDAL `Row` in this mode.
20
+
21
+ ### Verified working methods
22
+
23
+ - Query composition: `.where(...)`, `.select(...)`, `.orderby(...)`, `.groupby(...)`, `.having(...)`
24
+ - Execution/introspection: `.execute()`, `.collect()`, `.to_sql()`
25
+ - Row access: `.first()`, `.first_or_fail()`
26
+ - Pagination helpers: `.paginate()`, `.chunk()`
27
+ - Basic set operations: `.count()`, `.exists()`, `.update(...)`, `.delete(...)`, `.collect_or_fail()`
28
+ - `.cache(...).collect()` runs (returns pyDAL `Rows`)
29
+
30
+ ### Verified unsupported methods
31
+
32
+ - `.join(...)`: **not supported** for old-style tables (depends on TypeDAL model relationship internals)
33
+
34
+ ### Behavioral caveat
35
+
36
+ Legacy mode does not perform typed model mapping. Expect pyDAL `Rows`/`Row` outputs rather than typed entities.
37
+
38
+ If you need full QueryBuilder behavior (typed entities, relationships, typed joins, cache integration),
39
+ migrate that table to `TypedTable`.
40
+
41
+ ## Upsert and validation helpers
42
+
43
+ `TypedTable` exposes convenience methods for common upsert/validation flows:
44
+
45
+ ```python
46
+ # update if found, otherwise insert
47
+ user = User.update_or_insert(User.email == "a@example.com", email="a@example.com", name="Alice")
48
+
49
+ # validate before insert
50
+ created, errors = User.validate_and_insert(email="a@example.com")
51
+
52
+ # validate before update
53
+ updated, errors = User.validate_and_update(User.id == 1, name="Alice Updated")
54
+
55
+ # validate before update-or-insert
56
+ row, errors = User.validate_and_update_or_insert(User.email == "a@example.com", name="Alice")
57
+ ```
58
+
59
+ Behavior notes:
60
+
61
+ - `update_or_insert(...)` returns the resulting instance.
62
+ - `validate_and_*` methods return `(instance_or_none, errors_or_none)`.
63
+
64
+ ## Reordering table fields
65
+
66
+ You can reorder fields on a defined table with `reorder_fields`:
67
+
68
+ ```python
69
+ # Keep listed fields first, keep all other fields after them
70
+ MyTable.reorder_fields(MyTable.id, MyTable.name)
71
+
72
+ # Keep only the listed fields
73
+ MyTable.reorder_fields(MyTable.id, MyTable.name, keep_others=False)
74
+ ```
75
+
76
+ This is useful when you want deterministic field order for SQL generation, inspection, or exports.
@@ -40,7 +40,7 @@ Any keyword arguments you would pass to `db.define_table`, you can also pass to
40
40
  | `Field('name', 'time')` | `name: datetime.time` | `name: TypedField[datetime.time]` | `name = TypedField(datetime.time)` | `name = TimeField()` |
41
41
  | `Field('name', 'datetime')` | `name: datetime.datetime` | `name: TypedField[datetime.datetime]` | `name = TypedField(datetime.datetime)` | `name = DatetimeField()` |
42
42
  | `Field('name', 'password')` | × | × | `name = TypedField(str, type="password")` | `name = PasswordField()` |
43
- | `Field('name', 'upload')` | × | × | `name = TypedField(str, type="upload)` | `name = UploadField()` |
43
+ | `Field('name', 'upload')` | × | × | `name = TypedField(str, type="upload")` | `name = UploadField()` |
44
44
  | `Field('name', 'reference <table>')` | `name: Table` | `name: TypedField[Table]` | `name = TypedField(Table)` | `name = ReferenceField('table')` |
45
45
  | `Field('name', 'list:string')` | `name: list[str]` | `name: TypedField[list[str]]` | `name = TypedField(list[str])` | `name = ListStringField()` |
46
46
  | `Field('name', 'list:integer')` | `name: list[int]` | `name: TypedField[list[int]]` | `name = TypedField()` | `name = ListIntegerField()` |
@@ -104,4 +104,4 @@ row.delete_record() # to trigger
104
104
  MyTable.where(...).delete() # to trigger
105
105
  ```
106
106
 
107
- "Now that we have some tables, it's time to actually query them! Let's go to [3. Building Queries](./3_building_queries.md) to learn how.
107
+ Now that we have some tables, it's time to actually query them! Let's go to [3. Building Queries](./3_building_queries.md) to learn how.
@@ -236,8 +236,8 @@ The Query Builder has a few operations that don't return a new builder instance:
236
236
  - first: get the first entity matching your query, possibly with relationships loaded (if .join was used)
237
237
  - first_or_fail: where `first` may return an empty result, this variant will raise an error if there are no results.
238
238
  - to_sql: get the SQL string that would run, useful for debugging, subqueries and other advanced SQL operations.
239
- - update: instead of selecting rows, update those matching the current query (see [Delete](#delete))
240
- - delete: instead of selecting rows, delete those matching the current query (see [Update](#update))
239
+ - update: instead of selecting rows, update those matching the current query (see [Update](#update))
240
+ - delete: instead of selecting rows, delete those matching the current query (see [Delete](#delete))
241
241
 
242
242
  Additionally, you can directly call `.all()`, `.collect()`, `.count()`, `.first()` on a model (e.g. `User.all()`).
243
243
 
@@ -264,7 +264,7 @@ person.update_record(name="New Name")
264
264
  db(db.person.name == "Old Name").delete()
265
265
 
266
266
  row = db.person(4)
267
- row.update_record(name="New Name")
267
+ row.delete_record()
268
268
 
269
269
  # typedal:
270
270
  Person.where(id="Old Name").delete() # via query builder
@@ -272,3 +272,8 @@ Person.where(id="Old Name").delete() # via query builder
272
272
  person = Person(4)
273
273
  person.delete_record()
274
274
  ```
275
+
276
+ ---
277
+
278
+ Need less-common query patterns (for example, using `QueryBuilder` on old-style pyDAL tables)?
279
+ See [10. Advanced APIs](./10_advanced_apis.md).
@@ -98,6 +98,30 @@ In this example, `Relationship["Sidekick"]` is added as an extra type hint, sinc
98
98
  after the superhero class.
99
99
  Adding the `Relationship["Sidekick"]` hint is optional, but recommended to improve editor support.
100
100
 
101
+ ### Forward References with `Ref[...]` (typing helper)
102
+
103
+ For relationship targets, you can use `Ref[...]` as a typed replacement for the string form:
104
+
105
+ ```python
106
+ from typedal.relationships import Ref
107
+
108
+ bestie = relationship(Ref["BestFriend"], lambda user, bestie: user.id == bestie.friend)
109
+ ```
110
+
111
+ This is equivalent at runtime to:
112
+
113
+ ```python
114
+ bestie = relationship("BestFriend", lambda user, bestie: user.id == bestie.friend)
115
+ ```
116
+
117
+ `Ref[...]` is primarily for type-checking/editor support. It does not change runtime behavior.
118
+
119
+ For normal table fields, `Ref[...]` is not needed. Use standard forward-reference annotations, for example:
120
+
121
+ ```python
122
+ owner: "User"
123
+ ```
124
+
101
125
  ## Many-to-Many
102
126
 
103
127
  Setting up a relationship that uses a junction/pivot table is slightly harder.
@@ -199,4 +223,3 @@ Depending on your setup:
199
223
  - **Using py4web or web2py?** → [5. py4web & web2py](./5_py4web.md)
200
224
  - **Ready to manage your database?** → [6. Migrations](./6_migrations.md)
201
225
  - **Dive deeper into functionality?** → [8.: Mixins](./8_mixins.md)
202
-
@@ -55,7 +55,7 @@ noop = false
55
55
  ### Generating Migrations (pydal2sql)
56
56
 
57
57
  - **`input`**: Path to your TypeDAL table definitions file
58
- - **`output`**: Path to the generated migration `.py` fil
58
+ - **`output`**: Path to the generated migration `.py` file
59
59
  - **`dialect`**: Database type: `sqlite`, `postgres`, `mysql`, etc. (if unclear from database uri)
60
60
  - **`magic`**: Insert missing variables to prevent crashes (default: `true`).
61
61
  See [pydal2sql docs](https://github.com/robinvandernoord/pydal2sql#configuration).
@@ -148,3 +148,8 @@ class SpecialTable(TypedTable):
148
148
  ```
149
149
 
150
150
  **Warning:** Disabling this may break caching functionality for queries involving this table.
151
+
152
+ ---
153
+
154
+ Want to explore less common but useful APIs (like old-style pyDAL `QueryBuilder`, validation/upsert helpers, and field reordering)?
155
+ Continue with [10. Advanced APIs](./10_advanced_apis.md).
@@ -9,3 +9,4 @@
9
9
  7. [Advanced Configuration](./7_configuration.md)
10
10
  8. [Mixins](./8_mixins.md)
11
11
  9. [Function Memoization](./9_memoization.md)
12
+ 10. [Advanced APIs](./10_advanced_apis.md)
@@ -11,6 +11,7 @@ nav:
11
11
  - 7. Configuration: 7_configuration.md
12
12
  - 8. Mixins: 8_mixins.md
13
13
  - 9. Function Memoization: 9_memoization.md
14
+ - 10. Advanced APIs: 10_advanced_apis.md
14
15
  extra:
15
16
  version:
16
17
  default: stable
@@ -40,4 +41,4 @@ theme:
40
41
 
41
42
  extra_css:
42
43
  - https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/tokyo-night-dark.min.css
43
- - css/code_blocks.css
44
+ - css/code_blocks.css
@@ -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.4.5"
8
+ __version__ = "4.4.6"
@@ -1133,11 +1133,16 @@ class QueryBuilder(t.Generic[T_MetaInstance]):
1133
1133
 
1134
1134
  Also adds paginate, since it would be a waste to select more rows than needed.
1135
1135
  """
1136
- if row := self.paginate(page=1, limit=1, verbose=verbose).first():
1137
- return self.model.from_row(row)
1138
- else:
1136
+ row = self.paginate(page=1, limit=1, verbose=verbose).first()
1137
+ if not row:
1139
1138
  return None
1140
1139
 
1140
+ if not isinstance(self.model, TableMeta):
1141
+ # old-style pydal table: keep pydal semantics and return raw Row
1142
+ return row
1143
+
1144
+ return self.model.from_row(row)
1145
+
1141
1146
  def _first(self) -> str:
1142
1147
  return self._paginate(page=1, limit=1)
1143
1148
 
@@ -526,6 +526,14 @@ def test_minimal_functionality_on_pydal_style_tables():
526
526
  assert qb2
527
527
  assert len(qb2) == 1
528
528
 
529
+ first = QueryBuilder(db.test_query_table).where(number=2).first()
530
+ assert first
531
+ assert first.id == qb1.first().id
532
+
533
+ first_or_fail = QueryBuilder(db.test_query_table).where(number=2).first_or_fail()
534
+ assert first_or_fail
535
+ assert first_or_fail.id == qb1.first().id
536
+
529
537
 
530
538
  def test_before_after_collect(capsys):
531
539
  _setup_data()
@@ -1 +0,0 @@
1
- *
Binary file
Binary file
typedal-4.4.5/.crush/init DELETED
File without changes
@@ -1,37 +0,0 @@
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"}
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