TypeDAL 4.0.2__tar.gz → 4.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {typedal-4.0.2 → typedal-4.1.0}/CHANGELOG.md +17 -0
- {typedal-4.0.2 → typedal-4.1.0}/PKG-INFO +1 -1
- typedal-4.1.0/docs/1_getting_started.md +91 -0
- {typedal-4.0.2 → typedal-4.1.0}/docs/2_defining_tables.md +2 -1
- {typedal-4.0.2 → typedal-4.1.0}/docs/3_building_queries.md +89 -24
- {typedal-4.0.2 → typedal-4.1.0}/docs/4_relationships.md +81 -11
- typedal-4.1.0/docs/6_migrations.md +90 -0
- typedal-4.1.0/docs/7_configuration.md +225 -0
- typedal-4.0.2/docs/7_mixins.md → typedal-4.1.0/docs/8_mixins.md +30 -12
- typedal-4.1.0/docs/index.md +10 -0
- {typedal-4.0.2 → typedal-4.1.0}/mkdocs.yml +2 -1
- {typedal-4.0.2 → typedal-4.1.0}/pyproject.toml +2 -0
- {typedal-4.0.2 → typedal-4.1.0}/src/typedal/__about__.py +1 -1
- {typedal-4.0.2 → typedal-4.1.0}/src/typedal/caching.py +1 -1
- {typedal-4.0.2 → typedal-4.1.0}/src/typedal/cli.py +2 -1
- {typedal-4.0.2 → typedal-4.1.0}/src/typedal/config.py +3 -0
- {typedal-4.0.2 → typedal-4.1.0}/src/typedal/core.py +4 -2
- {typedal-4.0.2 → typedal-4.1.0}/src/typedal/define.py +3 -3
- {typedal-4.0.2 → typedal-4.1.0}/src/typedal/for_py4web.py +1 -1
- {typedal-4.0.2 → typedal-4.1.0}/src/typedal/helpers.py +1 -1
- {typedal-4.0.2 → typedal-4.1.0}/src/typedal/query_builder.py +80 -34
- {typedal-4.0.2 → typedal-4.1.0}/src/typedal/relationships.py +145 -19
- {typedal-4.0.2 → typedal-4.1.0}/src/typedal/rows.py +1 -1
- {typedal-4.0.2 → typedal-4.1.0}/src/typedal/tables.py +10 -1
- {typedal-4.0.2 → typedal-4.1.0}/src/typedal/types.py +9 -9
- {typedal-4.0.2 → typedal-4.1.0}/tests/test_relationships.py +98 -34
- typedal-4.0.2/docs/1_getting_started.md +0 -34
- typedal-4.0.2/docs/6_migrations.md +0 -125
- typedal-4.0.2/docs/index.md +0 -9
- {typedal-4.0.2 → typedal-4.1.0}/.github/workflows/su6.yml +0 -0
- {typedal-4.0.2 → typedal-4.1.0}/.gitignore +0 -0
- {typedal-4.0.2 → typedal-4.1.0}/.readthedocs.yml +0 -0
- {typedal-4.0.2 → typedal-4.1.0}/README.md +0 -0
- {typedal-4.0.2 → typedal-4.1.0}/coverage.svg +0 -0
- {typedal-4.0.2 → typedal-4.1.0}/docs/5_py4web.md +0 -0
- {typedal-4.0.2 → typedal-4.1.0}/docs/css/code_blocks.css +0 -0
- {typedal-4.0.2 → typedal-4.1.0}/docs/requirements.txt +0 -0
- {typedal-4.0.2 → typedal-4.1.0}/example_new.py +0 -0
- {typedal-4.0.2 → typedal-4.1.0}/example_old.py +0 -0
- {typedal-4.0.2 → typedal-4.1.0}/src/typedal/__init__.py +0 -0
- {typedal-4.0.2 → typedal-4.1.0}/src/typedal/constants.py +0 -0
- {typedal-4.0.2 → typedal-4.1.0}/src/typedal/fields.py +0 -0
- {typedal-4.0.2 → typedal-4.1.0}/src/typedal/for_web2py.py +0 -0
- {typedal-4.0.2 → typedal-4.1.0}/src/typedal/mixins.py +0 -0
- {typedal-4.0.2 → typedal-4.1.0}/src/typedal/py.typed +0 -0
- {typedal-4.0.2 → typedal-4.1.0}/src/typedal/serializers/as_json.py +0 -0
- {typedal-4.0.2 → typedal-4.1.0}/src/typedal/web2py_py4web_shared.py +0 -0
- {typedal-4.0.2 → typedal-4.1.0}/tests/__init__.py +0 -0
- {typedal-4.0.2 → typedal-4.1.0}/tests/configs/simple.toml +0 -0
- {typedal-4.0.2 → typedal-4.1.0}/tests/configs/valid.env +0 -0
- {typedal-4.0.2 → typedal-4.1.0}/tests/configs/valid.toml +0 -0
- {typedal-4.0.2 → typedal-4.1.0}/tests/py314_tests.py +0 -0
- {typedal-4.0.2 → typedal-4.1.0}/tests/test_cli.py +0 -0
- {typedal-4.0.2 → typedal-4.1.0}/tests/test_config.py +0 -0
- {typedal-4.0.2 → typedal-4.1.0}/tests/test_docs_examples.py +0 -0
- {typedal-4.0.2 → typedal-4.1.0}/tests/test_helpers.py +0 -0
- {typedal-4.0.2 → typedal-4.1.0}/tests/test_json.py +0 -0
- {typedal-4.0.2 → typedal-4.1.0}/tests/test_main.py +0 -0
- {typedal-4.0.2 → typedal-4.1.0}/tests/test_mixins.py +0 -0
- {typedal-4.0.2 → typedal-4.1.0}/tests/test_mypy.py +0 -0
- {typedal-4.0.2 → typedal-4.1.0}/tests/test_orm.py +0 -0
- {typedal-4.0.2 → typedal-4.1.0}/tests/test_py4web.py +0 -0
- {typedal-4.0.2 → typedal-4.1.0}/tests/test_query_builder.py +0 -0
- {typedal-4.0.2 → typedal-4.1.0}/tests/test_row.py +0 -0
- {typedal-4.0.2 → typedal-4.1.0}/tests/test_stats.py +0 -0
- {typedal-4.0.2 → typedal-4.1.0}/tests/test_table.py +0 -0
- {typedal-4.0.2 → typedal-4.1.0}/tests/test_web2py.py +0 -0
- {typedal-4.0.2 → typedal-4.1.0}/tests/test_xx_others.py +0 -0
- {typedal-4.0.2 → typedal-4.1.0}/tests/timings.py +0 -0
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
<!--next-version-placeholder-->
|
|
4
4
|
|
|
5
|
+
## v4.1.0 (2025-11-26)
|
|
6
|
+
|
|
7
|
+
### Feature
|
|
8
|
+
|
|
9
|
+
* **relationships:** Improved join features such as lazy=LazyPolicy, explicit=bool ([`f612d43`](https://github.com/trialandsuccess/TypeDAL/commit/f612d43b3a8f474a39bbcc68c1ce9b688939b13a))
|
|
10
|
+
|
|
11
|
+
### Fix
|
|
12
|
+
|
|
13
|
+
* Move `LazyPolicy` to config so TypeDALConfig works on < 3.14 again ([`7ad2b19`](https://github.com/trialandsuccess/TypeDAL/commit/7ad2b1934e129307f7aca9d20055e10df5636c92))
|
|
14
|
+
|
|
15
|
+
### Documentation
|
|
16
|
+
|
|
17
|
+
* Implemented outstanding todo's into documenation ([`6acb6d8`](https://github.com/trialandsuccess/TypeDAL/commit/6acb6d8ec0ae0ce5b92533e607e4d9a1b65600bc))
|
|
18
|
+
* Continued working on docs todo list ([`022c95d`](https://github.com/trialandsuccess/TypeDAL/commit/022c95dc276ca356cf7e2c884e4d6d6b179aa108))
|
|
19
|
+
* Started on the todo list of doc changes ([`2ad04c4`](https://github.com/trialandsuccess/TypeDAL/commit/2ad04c4de619af9ec1335c645d3e0ad11b25f9a0))
|
|
20
|
+
* Added todo for what to update in docs ([`00eba3e`](https://github.com/trialandsuccess/TypeDAL/commit/00eba3e5d0b347a05d13bfa9dee36054cb261648))
|
|
21
|
+
|
|
5
22
|
## v4.0.2 (2025-10-14)
|
|
6
23
|
|
|
7
24
|
### Fix
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# 1. Getting Started
|
|
2
|
+
|
|
3
|
+
TypeDAL is built on top of pyDAL, which has excellent documentation. It is recommended to be familiar with at least the
|
|
4
|
+
basics of this abstraction, before using this library. TypeDAL uses many of the same concepts, but with slightly
|
|
5
|
+
different wording and syntax in some cases.
|
|
6
|
+
|
|
7
|
+
[web2py - Chapter 6: The database abstraction layer](http://www.web2py.com/books/default/chapter/29/06/the-database-abstraction-layer)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
### Installation
|
|
12
|
+
|
|
13
|
+
```shell
|
|
14
|
+
pip install typedal
|
|
15
|
+
# or, if you're using py4web:
|
|
16
|
+
pip install typedal[py4web]
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### First Steps
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from typedal import TypeDAL
|
|
23
|
+
# or, if in py4web:
|
|
24
|
+
from typedal.for_py4web import TypeDAL
|
|
25
|
+
|
|
26
|
+
db = TypeDAL("sqlite:memory")
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
TypeDAL accepts the same connection string format and other arguments as `pydal.DAL`.
|
|
30
|
+
Again, see
|
|
31
|
+
[their documentation](http://www.web2py.com/books/default/chapter/29/06/the-database-abstraction-layer#The-DAL-A-quick-tour)
|
|
32
|
+
for more info about this. For additional configuration options specific to TypeDAL, see the [7. Advanced Configuration](./7_configuration.md) page.
|
|
33
|
+
|
|
34
|
+
When using py4web, it is recommended to import the py4web-specific TypeDAL, which is a Fixture that handles database
|
|
35
|
+
connections on request (just like the py4web specific DAL class does). More information about this can be found on [5. py4web](./5_py4web.md)
|
|
36
|
+
|
|
37
|
+
### Simple Queries
|
|
38
|
+
|
|
39
|
+
For direct SQL access, use `executesql()`:
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
rows = db.executesql("SELECT * FROM some_table")
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
#### Safely Injecting Variables
|
|
46
|
+
|
|
47
|
+
Use t-strings (Python 3.14+) for automatic SQL escaping:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
name = "Robert'); DROP TABLE Students;--"
|
|
51
|
+
rows = db.executesql(t"SELECT * FROM some_table WHERE name = {name}")
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Or use the `placeholders` argument with positional or named parameters:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
# Positional
|
|
58
|
+
rows = db.executesql(
|
|
59
|
+
"SELECT * FROM some_table WHERE name = %s AND age > %s",
|
|
60
|
+
placeholders=[name, 18]
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Named
|
|
64
|
+
rows = db.executesql(
|
|
65
|
+
"SELECT * FROM some_table WHERE name = %(name)s AND age > %(age)s",
|
|
66
|
+
placeholders={"name": name, "age": 18}
|
|
67
|
+
)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
#### Result Formatting
|
|
71
|
+
|
|
72
|
+
By default, `executesql()` returns rows as tuples. To map results to specific fields, use `fields` (takes
|
|
73
|
+
Field/TypedField objects) or `colnames` (takes column name strings):
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
rows = db.executesql(
|
|
77
|
+
"SELECT id, name FROM some_table",
|
|
78
|
+
colnames=["id", "name"]
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
rows = db.executesql(
|
|
82
|
+
"SELECT id, name FROM some_table",
|
|
83
|
+
fields=[some_table.id, some_table.name] # Requires table definition
|
|
84
|
+
)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
You can also use `as_dict` or `as_ordered_dict` to return dictionaries instead of tuples.
|
|
88
|
+
|
|
89
|
+
Most of the time, you probably don't want to write raw queries. For that, you'll need to define some tables!
|
|
90
|
+
Head to [2. Defining Tables](./2_defining_tables.md) to learn how.
|
|
91
|
+
|
|
@@ -102,5 +102,6 @@ def my_after_delete(query: Set):
|
|
|
102
102
|
|
|
103
103
|
row.delete_record() # to trigger
|
|
104
104
|
MyTable.where(...).delete() # to trigger
|
|
105
|
-
|
|
106
105
|
```
|
|
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.
|
|
@@ -30,27 +30,33 @@ The query builder uses the builder pattern, so you can keep adding to it (in any
|
|
|
30
30
|
data:
|
|
31
31
|
|
|
32
32
|
```python
|
|
33
|
-
Person
|
|
33
|
+
builder = Person
|
|
34
34
|
.where(Person.id > 0)
|
|
35
|
-
.where(Person.id < 99, Person.id == 101)
|
|
35
|
+
.where(Person.id < 99, Person.id == 101) # id < 99 OR id == 101
|
|
36
36
|
.select(Reference.id, Reference.title)
|
|
37
37
|
.join('reference')
|
|
38
38
|
.select(Person.ALL)
|
|
39
|
-
|
|
39
|
+
|
|
40
|
+
# final step: actually runs the query:
|
|
41
|
+
builder.paginate(limit=5, page=2)
|
|
42
|
+
|
|
43
|
+
# to get the SQL (for debugging or subselects), you can use:
|
|
44
|
+
builder.to_sql()
|
|
40
45
|
```
|
|
41
46
|
|
|
42
47
|
```sql
|
|
43
|
-
SELECT "person"
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
FROM "person"
|
|
47
|
-
|
|
48
|
-
WHERE (("person"."id" IN (SELECT "person"."id"
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
48
|
+
SELECT "person".*
|
|
49
|
+
, "reference"."id"
|
|
50
|
+
, "reference"."title"
|
|
51
|
+
FROM "person"
|
|
52
|
+
, "reference"
|
|
53
|
+
WHERE (("person"."id" IN (SELECT "person"."id"
|
|
54
|
+
FROM "person"
|
|
55
|
+
WHERE ((("person"."id" > 0)) AND
|
|
56
|
+
(("person"."id" < 99) OR ("person"."id" = 101)))
|
|
57
|
+
ORDER BY "person"."id"
|
|
58
|
+
LIMIT 1 OFFSET 0))
|
|
59
|
+
AND ("person"."reference" = "reference"."id"));
|
|
54
60
|
```
|
|
55
61
|
|
|
56
62
|
### where
|
|
@@ -58,27 +64,81 @@ WHERE (("person"."id" IN (SELECT "person"."id"
|
|
|
58
64
|
In pydal, this is the part that would be in `db(...)`.
|
|
59
65
|
Can be used in multiple ways:
|
|
60
66
|
|
|
61
|
-
- `.where(
|
|
67
|
+
- `.where(query)` -> with a direct query such as `query = (Table.id == 5)`
|
|
62
68
|
- `.where(lambda table: table.id == 5)` -> with a query via a lambda
|
|
63
69
|
- `.where(id=5)` -> via keyword arguments
|
|
70
|
+
- `.where({"id": 5})` -> via a dictionary (equivalent to keyword args)
|
|
71
|
+
- `.where(Table.field)` -> with a Field directly, checks if field is not null
|
|
72
|
+
|
|
73
|
+
When using multiple `.where()` calls, they will be ANDed together:
|
|
74
|
+
`.where(lambda table: table.id == 5).where(active=True)` equals `(table.id == 5) & (table.active == True)`
|
|
64
75
|
|
|
65
|
-
When
|
|
66
|
-
`.where(
|
|
67
|
-
When passing multiple queries to a single .where, they will be ORed:
|
|
68
|
-
`.where(lambda table: table.id == 5, lambda table: table.id == 6)` equals `(table.id == 5) | (table.id=6)`
|
|
76
|
+
When passing multiple arguments to a single `.where()`, they will be ORed:
|
|
77
|
+
`.where({"id": 5}, {"id": 6})` equals `(table.id == 5) | (table.id == 6)`
|
|
69
78
|
|
|
70
79
|
### select
|
|
71
80
|
|
|
72
|
-
Here you can enter any number of fields as arguments: database columns by name ('id'), by field reference (
|
|
73
|
-
other (e.g.
|
|
81
|
+
Here you can enter any number of fields as arguments: database columns by name ('id'), by field reference (Table.id),
|
|
82
|
+
other (e.g. Table.ALL), or Expression objects.
|
|
74
83
|
|
|
75
84
|
```python
|
|
76
85
|
Person.select('id', Person.name, Person.ALL) # defaults to Person.ALL if select is omitted.
|
|
77
86
|
```
|
|
78
87
|
|
|
79
|
-
You can also specify extra options
|
|
88
|
+
You can also specify extra options as keyword arguments. Supported options are: `orderby`, `groupby`, `limitby`,
|
|
89
|
+
`distinct`, `having`, `orderby_on_limitby`, `join`, `left`, `cache`, see also
|
|
80
90
|
the [web2py docs](http://www.web2py.com/books/default/chapter/29/06/the-database-abstraction-layer#orderby-groupby-limitby-distinct-having-orderby_on_limitby-join-left-cache).
|
|
81
91
|
|
|
92
|
+
```python
|
|
93
|
+
Person.select(Person.name, distinct=True)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
If you only want a list of name strings (in this example) instead of Person instances, you could use the column() method
|
|
97
|
+
instead:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
Person.column(Person.name, distinct=True)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
You can use `.orderby(*fields)` as an alternative to `select(orderby=...)`:
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
Person.where(...).orderby(~Person.name, "age")
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`.orderby()` accepts field references (`Table.field` or `"field_name""`), reverse ordering (`~Table.field`), or the
|
|
110
|
+
literal `"<random>"`. Multiple field references can be passed (except when using `<random>`).
|
|
111
|
+
|
|
112
|
+
#### Raw SQL Expressions
|
|
113
|
+
|
|
114
|
+
For complex SQL that can't be expressed with field references, use `sql_expression()`:
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
# Simple arithmetic
|
|
118
|
+
expr = db.sql_expression("age * 2")
|
|
119
|
+
Person.select(expr)
|
|
120
|
+
|
|
121
|
+
# Safe parameter injection with t-strings (Python 3.14+)
|
|
122
|
+
min_age = 21
|
|
123
|
+
expr = db.sql_expression(t"age >= {min_age}", output_type="boolean")
|
|
124
|
+
Person.where(expr).select()
|
|
125
|
+
|
|
126
|
+
# Positional arguments
|
|
127
|
+
expr = db.sql_expression("age > %s AND status = %s", 18, "active", output_type="boolean")
|
|
128
|
+
Person.where(expr).select()
|
|
129
|
+
|
|
130
|
+
# Named arguments
|
|
131
|
+
expr = db.sql_expression(
|
|
132
|
+
"EXTRACT(year FROM %(date_col)s) = %(year)s",
|
|
133
|
+
date_col="created_at",
|
|
134
|
+
year=2023,
|
|
135
|
+
output_type="boolean"
|
|
136
|
+
)
|
|
137
|
+
Person.where(expr).select()
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Expressions can be used in `where()`, `select()`, `orderby()`, and other query methods.
|
|
141
|
+
|
|
82
142
|
### join
|
|
83
143
|
|
|
84
144
|
Include relationship fields in the result.
|
|
@@ -93,6 +153,8 @@ This can be overwritten with the `method` keyword argument (left or inner)
|
|
|
93
153
|
Person.join('articles', method='inner') # will only yield persons that have related articles
|
|
94
154
|
```
|
|
95
155
|
|
|
156
|
+
For more details about relationships and joins, see [4. Relationships](./4_relationships.md).
|
|
157
|
+
|
|
96
158
|
### cache
|
|
97
159
|
|
|
98
160
|
```python
|
|
@@ -120,7 +182,7 @@ time.
|
|
|
120
182
|
class ...
|
|
121
183
|
```
|
|
122
184
|
|
|
123
|
-
In order to enable this functionality, TypeDAL adds a `before update` and `before delete` hook to your tables,
|
|
185
|
+
In order to enable this functionality, TypeDAL adds a `before update` and `before delete` hook to your tables,
|
|
124
186
|
which manages the dependencies. You can disable this behavior by passing `cache_dependency=False` to `db.define`.
|
|
125
187
|
Be aware doing this might break some caching functionality!
|
|
126
188
|
|
|
@@ -136,12 +198,15 @@ The Query Builder has a few operations that don't return a new builder instance:
|
|
|
136
198
|
- paginate: this works similarly to `collect`, but returns a PaginatedRows instead, which has a `.next()`
|
|
137
199
|
and `.previous()` method to easily load more pages.
|
|
138
200
|
- collect_or_fail: where `collect` may return an empty result, this variant will raise an error if there are no results.
|
|
201
|
+
- execute: get the raw rows matching your query as returned by pydal, without entity mapping or relationship loading.
|
|
202
|
+
Useful for subqueries or when you need lower-level control.
|
|
139
203
|
- first: get the first entity matching your query, possibly with relationships loaded (if .join was used)
|
|
140
204
|
- first_or_fail: where `first` may return an empty result, this variant will raise an error if there are no results.
|
|
205
|
+
- to_sql: get the SQL string that would run, useful for debugging, subqueries and other advanced SQL operations.
|
|
141
206
|
- update: instead of selecting rows, update those matching the current query (see [Delete](#delete))
|
|
142
207
|
- delete: instead of selecting rows, delete those matching the current query (see [Update](#update))
|
|
143
208
|
|
|
144
|
-
Additionally, you can directly call `.all()`, `.collect()`, `.count()`, `.first()` on a model.
|
|
209
|
+
Additionally, you can directly call `.all()`, `.collect()`, `.count()`, `.first()` on a model (e.g. `User.all()`).
|
|
145
210
|
|
|
146
211
|
## Update
|
|
147
212
|
|
|
@@ -20,25 +20,37 @@ class Post(TypedTable):
|
|
|
20
20
|
author: Author
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
authors_with_roles = Author.join('
|
|
23
|
+
authors_with_roles = Author.join('roles').collect()
|
|
24
24
|
posts_with_author = Post.join().collect() # join can be called without arguments to join all relationships (in this case only 'author')
|
|
25
|
+
post_deep = Post.join("author.roles").collect() # nested relationship, accessible via post.author.roles
|
|
25
26
|
```
|
|
26
27
|
|
|
27
28
|
In this example, the `Post` table contains a reference to the `Author` table. In that case, the `Author` `id` is stored
|
|
28
|
-
in the
|
|
29
|
-
`Post`'s `author` column.
|
|
29
|
+
in the `Post`'s `author` column.
|
|
30
30
|
Furthermore, the `Author` table contains a `list:reference` to `list[Role]`. This means multiple `id`s from the `Role`
|
|
31
|
-
table
|
|
32
|
-
can be stored in the `roles` column of `Author`.
|
|
31
|
+
table can be stored in the `roles` column of `Author`.
|
|
33
32
|
|
|
34
33
|
For these two cases, a Relationship is set-up automatically, which means `.join()` can work with those.
|
|
35
34
|
|
|
35
|
+
### Alternative Join Syntax
|
|
36
|
+
|
|
37
|
+
You can pass the relationship object directly instead of its name as a string:
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
posts_with_author = Post.join(Post.author).collect()
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
This works, but note that `Post.author` is typed as `Relationship[Author]` at the class level, while `row.author` is
|
|
44
|
+
typed as `Author` at the instance level. Some editors may complain about type mismatches when using this syntax (e.g.,
|
|
45
|
+
reporting that `list[Tag]` isn't a `Relationship`). If you encounter type checking issues, use the string syntax
|
|
46
|
+
instead.
|
|
47
|
+
|
|
36
48
|
## Other Relationships
|
|
37
49
|
|
|
38
50
|
To get the reverse relationship, you'll have to tell TypeDAL how the two tables relate to each other (since guessing is
|
|
39
51
|
complex and unreliable).
|
|
40
52
|
|
|
41
|
-
For example, to set up the reverse
|
|
53
|
+
For example, to set up the reverse relationship from author to posts:
|
|
42
54
|
|
|
43
55
|
```python
|
|
44
56
|
@db.define()
|
|
@@ -46,7 +58,6 @@ class Author(TypedTable):
|
|
|
46
58
|
name: str
|
|
47
59
|
|
|
48
60
|
posts = relationship(list["Post"], condition=lambda author, post: author.id == post.author, join="left")
|
|
49
|
-
|
|
50
61
|
```
|
|
51
62
|
|
|
52
63
|
Note that `"Post"` is in quotes. This is because the `Post` class is defined later, so a reference to it is not
|
|
@@ -62,7 +73,7 @@ class Role(TypedTable):
|
|
|
62
73
|
authors = relationship(list["Author"], condition=lambda role, author: author.roles.contains(role.id), join="left")
|
|
63
74
|
```
|
|
64
75
|
|
|
65
|
-
Here, contains is used since `Author.roles` is a `list:reference`.
|
|
76
|
+
Here, `contains` is used since `Author.roles` is a `list:reference`.
|
|
66
77
|
See [the web2py docs](http://www.web2py.com/books/default/chapter/29/06/the-database-abstraction-layer#list-type-and-contains)
|
|
67
78
|
for more details.
|
|
68
79
|
|
|
@@ -82,8 +93,8 @@ class Sidekick(TypedTable):
|
|
|
82
93
|
superhero: SuperHero
|
|
83
94
|
```
|
|
84
95
|
|
|
85
|
-
In this example, `Relationship["Sidekick"]` is added as an extra type hint, since the reference to the table
|
|
86
|
-
|
|
96
|
+
In this example, `Relationship["Sidekick"]` is added as an extra type hint, since the reference to the table in
|
|
97
|
+
`relationship("Sidekick", ...)` is a string. This has to be passed as a string, since the Sidekick class is defined
|
|
87
98
|
after the superhero class.
|
|
88
99
|
Adding the `Relationship["Sidekick"]` hint is optional, but recommended to improve editor support.
|
|
89
100
|
|
|
@@ -107,6 +118,7 @@ class Post(TypedTable):
|
|
|
107
118
|
tag.on(tag.id == tagged.tag),
|
|
108
119
|
])
|
|
109
120
|
|
|
121
|
+
|
|
110
122
|
# without unique alias:
|
|
111
123
|
|
|
112
124
|
@db.define()
|
|
@@ -126,7 +138,65 @@ class Tagged(TypedTable):
|
|
|
126
138
|
```
|
|
127
139
|
|
|
128
140
|
Instead of a `condition`, it is recommended to define an `on`. Using a condition is possible, but could lead to pydal
|
|
129
|
-
|
|
141
|
+
generating a `CROSS JOIN` instead of a `LEFT JOIN`, which is bad for performance.
|
|
130
142
|
In this example, `Tag` is connected to `Post` and vice versa via the `Tagged` table.
|
|
131
143
|
It is recommended to use the tables received as arguments from the lambda (e.g. `tag.on` instead of `Tag.on` directly),
|
|
132
144
|
since these use aliases under the hood, which prevents conflicts when joining the same table multiple times.
|
|
145
|
+
|
|
146
|
+
## Lazy Loading and Explicit Relationships
|
|
147
|
+
|
|
148
|
+
### Lazy Policy
|
|
149
|
+
|
|
150
|
+
The `lazy` parameter on a relationship controls what happens when you access relationship data without explicitly
|
|
151
|
+
joining it first:
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
@db.define()
|
|
155
|
+
class User(TypedTable):
|
|
156
|
+
name: str
|
|
157
|
+
posts = relationship(list["Post"], condition=lambda user, post: user.id == post.author, lazy="forbid")
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Available policies:
|
|
161
|
+
|
|
162
|
+
- **`"forbid"`**: Raises an error. Prevents N+1 query problems by making them fail fast.
|
|
163
|
+
- **`"warn"`**: Returns an empty value (empty list or `None`) with a console warning.
|
|
164
|
+
- **`"ignore"`**: Returns an empty value silently.
|
|
165
|
+
- **`"tolerate"`**: Fetches the data but logs a warning about potential performance issues.
|
|
166
|
+
- **`"allow"`**: Fetches the data silently.
|
|
167
|
+
|
|
168
|
+
If `lazy=None` (the default), the relationship uses the database's default lazy policy. You can set this globally via
|
|
169
|
+
`TypeDAL`'s `lazy_policy` option (see [7. Advanced Configuration](./7_configuration.md) for configuration details),
|
|
170
|
+
which defaults to `"tolerate"`.
|
|
171
|
+
|
|
172
|
+
### Explicit Relationships
|
|
173
|
+
|
|
174
|
+
Use `explicit=True` for relationships that are expensive to join or rarely needed:
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
@db.define()
|
|
178
|
+
class User(TypedTable):
|
|
179
|
+
name: str
|
|
180
|
+
audit_logs = relationship(list["AuditLog"], condition=lambda user, log: user.id == log.user, explicit=True)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
When you call `.join()` without arguments, explicit relationships are skipped:
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
user = User.join().first() # user.audit_logs follows the lazy policy (empty/error/warning depending on setting)
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
To include an explicit relationship, reference it by name:
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
user = User.join("audit_logs").first() # now user.audit_logs is populated
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## What's Next?
|
|
196
|
+
|
|
197
|
+
Depending on your setup:
|
|
198
|
+
|
|
199
|
+
- **Using py4web or web2py?** → [5. py4web & web2py](./5_py4web.md)
|
|
200
|
+
- **Ready to manage your database?** → [6. Migrations](./6_migrations.md)
|
|
201
|
+
- **Dive deeper into functionality?** → [8.: Mixins](./8_mixins.md)
|
|
202
|
+
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# 6. Migrations
|
|
2
|
+
|
|
3
|
+
By default, pydal manages migrations in your database schema
|
|
4
|
+
automatically ([See also: the web2py docs](http://www.web2py.com/books/default/chapter/29/06/the-database-abstraction-layer#Migrations)).
|
|
5
|
+
This can be problematic in a production environment where you want to disable automatic table changes and manage these
|
|
6
|
+
by hand.
|
|
7
|
+
|
|
8
|
+
TypeDAL integrates with [`edwh-migrate`](https://pypi.org/project/edwh-migrate/) to make this easier. With this tool,
|
|
9
|
+
you write migrations (`CREATE`, `ALTER`, `DROP` statements) in SQL and it keeps track of which actions have already been
|
|
10
|
+
executed on your database.
|
|
11
|
+
|
|
12
|
+
To make this process easier, TypeDAL also integrates with [`pydal2sql`](https://pypi.org/project/pydal2sql/), which can
|
|
13
|
+
convert your pydal/TypeDAL table definitions into `CREATE` statements for new tables, or `ALTER` statements for existing
|
|
14
|
+
tables.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
To enable the migrations functionality within TypeDAL, install it with the migrations extra:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install typedal[migrations] # also included in typedal[all]
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Minimal Configuration
|
|
25
|
+
|
|
26
|
+
To use migrations, you need to configure TypeDAL in your `pyproject.toml`. At minimum, you must set:
|
|
27
|
+
|
|
28
|
+
- `database`: Your database URI
|
|
29
|
+
- `dialect`: The database type (e.g., `sqlite`, `postgres`)
|
|
30
|
+
- `migrate`: Set to `false` to disable pydal's automatic migrations
|
|
31
|
+
- `flag_location`: Where edwh-migrate stores its migration tracking
|
|
32
|
+
- `input`: Path to your table definitions (e.g., `data_model.py`)
|
|
33
|
+
- `output`: Where generated migrations are written (e.g., `migrations/`)
|
|
34
|
+
|
|
35
|
+
Optionally:
|
|
36
|
+
|
|
37
|
+
- `database_to_restore`: Path to a SQL file to restore before running migrations on a fresh database.
|
|
38
|
+
|
|
39
|
+
Here's a minimal example:
|
|
40
|
+
|
|
41
|
+
```toml
|
|
42
|
+
[tool.typedal]
|
|
43
|
+
database = "sqlite://"
|
|
44
|
+
dialect = "sqlite"
|
|
45
|
+
migrate = false
|
|
46
|
+
flag_location = "migrations/.flags"
|
|
47
|
+
input = "path/to/data_model.py"
|
|
48
|
+
output = "path/to/migrations.py"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
For dynamic properties or secrets (like a database with credentials),
|
|
52
|
+
add them to your `.env` file or set them as environment variables (optionally prefixed with `TYPEDAL_`):
|
|
53
|
+
|
|
54
|
+
```env
|
|
55
|
+
TYPEDAL_DATABASE = "psql://user:password@host:5432/database"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
> **Full configuration reference**: For all available options, multiple connections, environment overrides, and other
|
|
59
|
+
> settings, see [7. Configuration](./7_configuration.md).
|
|
60
|
+
|
|
61
|
+
You can generate a config interactively with `typedal setup`, or view your current config with `typedal --show-config`.
|
|
62
|
+
|
|
63
|
+
## Generate Migrations (pydal2sql)
|
|
64
|
+
|
|
65
|
+
With your config in place, generate migrations from your table definitions:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
typedal migrations.generate
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
You can override config values with CLI flags. See all options:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
typedal migrations.generate --help
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Run Migrations (edwh-migrate)
|
|
78
|
+
|
|
79
|
+
Apply your migrations to the database:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
typedal migrations.run
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
With a correctly configured setup, this should function without extra arguments.
|
|
86
|
+
You can however overwrite the behavior as defined in the config. See all options:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
typedal migrations.run --help
|
|
90
|
+
```
|