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.
- {typedal-4.4.5 → typedal-4.4.6}/CHANGELOG.md +10 -0
- {typedal-4.4.5 → typedal-4.4.6}/PKG-INFO +44 -11
- {typedal-4.4.5 → typedal-4.4.6}/README.md +43 -10
- typedal-4.4.6/docs/10_advanced_apis.md +76 -0
- {typedal-4.4.5 → typedal-4.4.6}/docs/2_defining_tables.md +2 -2
- {typedal-4.4.5 → typedal-4.4.6}/docs/3_building_queries.md +8 -3
- {typedal-4.4.5 → typedal-4.4.6}/docs/4_relationships.md +24 -1
- {typedal-4.4.5 → typedal-4.4.6}/docs/7_configuration.md +1 -1
- {typedal-4.4.5 → typedal-4.4.6}/docs/9_memoization.md +5 -0
- {typedal-4.4.5 → typedal-4.4.6}/docs/index.md +1 -0
- {typedal-4.4.5 → typedal-4.4.6}/mkdocs.yml +2 -1
- {typedal-4.4.5 → typedal-4.4.6}/src/typedal/__about__.py +1 -1
- {typedal-4.4.5 → typedal-4.4.6}/src/typedal/query_builder.py +8 -3
- {typedal-4.4.5 → typedal-4.4.6}/tests/test_query_builder.py +8 -0
- typedal-4.4.5/.crush/.gitignore +0 -1
- typedal-4.4.5/.crush/crush.db-shm +0 -0
- typedal-4.4.5/.crush/crush.db-wal +0 -0
- typedal-4.4.5/.crush/init +0 -0
- typedal-4.4.5/.crush/logs/crush.log +0 -37
- {typedal-4.4.5 → typedal-4.4.6}/.github/workflows/su6.yml +0 -0
- {typedal-4.4.5 → typedal-4.4.6}/.gitignore +0 -0
- {typedal-4.4.5 → typedal-4.4.6}/.readthedocs.yml +0 -0
- {typedal-4.4.5 → typedal-4.4.6}/coverage.svg +0 -0
- {typedal-4.4.5 → typedal-4.4.6}/docs/1_getting_started.md +0 -0
- {typedal-4.4.5 → typedal-4.4.6}/docs/5_py4web.md +0 -0
- {typedal-4.4.5 → typedal-4.4.6}/docs/6_migrations.md +0 -0
- {typedal-4.4.5 → typedal-4.4.6}/docs/8_mixins.md +0 -0
- {typedal-4.4.5 → typedal-4.4.6}/docs/css/code_blocks.css +0 -0
- {typedal-4.4.5 → typedal-4.4.6}/docs/requirements.txt +0 -0
- {typedal-4.4.5 → typedal-4.4.6}/example_new.py +0 -0
- {typedal-4.4.5 → typedal-4.4.6}/example_old.py +0 -0
- {typedal-4.4.5 → typedal-4.4.6}/pyproject.toml +0 -0
- {typedal-4.4.5 → typedal-4.4.6}/src/typedal/__init__.py +0 -0
- {typedal-4.4.5 → typedal-4.4.6}/src/typedal/caching.py +0 -0
- {typedal-4.4.5 → typedal-4.4.6}/src/typedal/cli.py +0 -0
- {typedal-4.4.5 → typedal-4.4.6}/src/typedal/config.py +0 -0
- {typedal-4.4.5 → typedal-4.4.6}/src/typedal/constants.py +0 -0
- {typedal-4.4.5 → typedal-4.4.6}/src/typedal/core.py +0 -0
- {typedal-4.4.5 → typedal-4.4.6}/src/typedal/define.py +0 -0
- {typedal-4.4.5 → typedal-4.4.6}/src/typedal/fields.py +0 -0
- {typedal-4.4.5 → typedal-4.4.6}/src/typedal/for_py4web.py +0 -0
- {typedal-4.4.5 → typedal-4.4.6}/src/typedal/for_web2py.py +0 -0
- {typedal-4.4.5 → typedal-4.4.6}/src/typedal/helpers.py +0 -0
- {typedal-4.4.5 → typedal-4.4.6}/src/typedal/mixins.py +0 -0
- {typedal-4.4.5 → typedal-4.4.6}/src/typedal/py.typed +0 -0
- {typedal-4.4.5 → typedal-4.4.6}/src/typedal/relationships.py +0 -0
- {typedal-4.4.5 → typedal-4.4.6}/src/typedal/rows.py +0 -0
- {typedal-4.4.5 → typedal-4.4.6}/src/typedal/serializers/as_json.py +0 -0
- {typedal-4.4.5 → typedal-4.4.6}/src/typedal/tables.py +0 -0
- {typedal-4.4.5 → typedal-4.4.6}/src/typedal/types.py +0 -0
- {typedal-4.4.5 → typedal-4.4.6}/src/typedal/web2py_py4web_shared.py +0 -0
- {typedal-4.4.5 → typedal-4.4.6}/tasks.py +0 -0
- {typedal-4.4.5 → typedal-4.4.6}/tests/__init__.py +0 -0
- {typedal-4.4.5 → typedal-4.4.6}/tests/configs/simple.toml +0 -0
- {typedal-4.4.5 → typedal-4.4.6}/tests/configs/valid.env +0 -0
- {typedal-4.4.5 → typedal-4.4.6}/tests/configs/valid.toml +0 -0
- {typedal-4.4.5 → typedal-4.4.6}/tests/py314_tests.py +0 -0
- {typedal-4.4.5 → typedal-4.4.6}/tests/test_cli.py +0 -0
- {typedal-4.4.5 → typedal-4.4.6}/tests/test_config.py +0 -0
- {typedal-4.4.5 → typedal-4.4.6}/tests/test_docs_examples.py +0 -0
- {typedal-4.4.5 → typedal-4.4.6}/tests/test_helpers.py +0 -0
- {typedal-4.4.5 → typedal-4.4.6}/tests/test_json.py +0 -0
- {typedal-4.4.5 → typedal-4.4.6}/tests/test_main.py +0 -0
- {typedal-4.4.5 → typedal-4.4.6}/tests/test_mixins.py +0 -0
- {typedal-4.4.5 → typedal-4.4.6}/tests/test_mypy.py +0 -0
- {typedal-4.4.5 → typedal-4.4.6}/tests/test_orm.py +0 -0
- {typedal-4.4.5 → typedal-4.4.6}/tests/test_py4web.py +0 -0
- {typedal-4.4.5 → typedal-4.4.6}/tests/test_relationships.py +0 -0
- {typedal-4.4.5 → typedal-4.4.6}/tests/test_row.py +0 -0
- {typedal-4.4.5 → typedal-4.4.6}/tests/test_stats.py +0 -0
- {typedal-4.4.5 → typedal-4.4.6}/tests/test_table.py +0 -0
- {typedal-4.4.5 → typedal-4.4.6}/tests/test_web2py.py +0 -0
- {typedal-4.4.5 → typedal-4.4.6}/tests/test_xx_others.py +0 -0
- {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.
|
|
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
|
|
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
|
|
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
|
|
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
|
-
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
|
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)`
|
|
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
|
-
|
|
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 [
|
|
240
|
-
- delete: instead of selecting rows, delete those matching the current query (see [
|
|
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.
|
|
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`
|
|
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).
|
|
@@ -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
|
|
@@ -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
|
-
|
|
1137
|
-
|
|
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()
|
typedal-4.4.5/.crush/.gitignore
DELETED
|
@@ -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
|
|
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
|