Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
6ad47cc
feat(async): add asynchronous execution scaffolding
robinvandernoord Aug 14, 2026
49b1fe1
chore(docs): reformat Python examples
robinvandernoord Aug 14, 2026
49f42be
feat(async): add async query execution for SQLite and Postgres
robinvandernoord Aug 14, 2026
a4c15b0
feat(async): implement async CRUD and query execution
robinvandernoord Aug 14, 2026
570c24e
test: add tests for the remaining async stubs
robinvandernoord Aug 14, 2026
784f46c
feat(async): implement remaining async query and table operations
robinvandernoord Aug 14, 2026
c0dbd4c
test(async): expand async execution parity and defect coverage
robinvandernoord Aug 14, 2026
24e96e2
chore(testing): configure source-only coverage
robinvandernoord Aug 14, 2026
c0252fa
fix(async): serialize SQLite execution and align async operations wit…
robinvandernoord Aug 14, 2026
9b4502b
chore(typing): configure ty and tighten project annotations
robinvandernoord Aug 14, 2026
23aac69
test: get rid of monkeypatch.setattr slop
robinvandernoord Aug 14, 2026
6c799b7
refactor(async-execution): extract async pool lifecycle into manager
robinvandernoord Aug 14, 2026
4db8d7d
refactor(typescript): simplify registry world access
robinvandernoord Aug 14, 2026
f64b9fc
docs(typedal): remove brittle upstream line references
robinvandernoord Aug 15, 2026
3ad5667
test(async-execution): add regression coverage for transaction and pa…
robinvandernoord Aug 15, 2026
59c9667
fix(async): preserve transaction boundaries across async and sync paths
robinvandernoord Aug 15, 2026
381f2db
fix(tables): keep async model operations non-blocking and compatible
robinvandernoord Aug 15, 2026
1b367b5
test(fixtures): close PostgreSQL databases after each test
robinvandernoord Aug 15, 2026
0ed6c4e
fix(async): track pending writes per task
robinvandernoord Aug 15, 2026
b37f873
test(async): cover transaction isolation and pool cleanup
robinvandernoord Aug 15, 2026
ef94da3
docs: remove stale rfc file
robinvandernoord Aug 15, 2026
6d65098
test(async): cover sqlite memory transaction ownership
robinvandernoord Aug 15, 2026
48aa449
fix(async): guard transaction ownership and cleanup
robinvandernoord Aug 15, 2026
95d3b7a
docs(async-execution): clarify non-owner transaction settlement
robinvandernoord Aug 15, 2026
31d22a5
test(async-execution): add non-owner commit isolation regression
robinvandernoord Aug 15, 2026
d8ac3da
test(imports): use src-prefixed typedal imports in remaining tests
robinvandernoord Aug 15, 2026
872883a
fix(async): release connections after read-only queries
robinvandernoord Aug 15, 2026
e8bc0fe
fix(async): guard transaction ownership during cleanup
robinvandernoord Aug 15, 2026
a9e19ba
fix(async): preserve transaction state and reclaim held connections
robinvandernoord Aug 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ db = TypeDAL("sqlite:memory")
# db = TypeDAL("mysql://user:password@localhost:3306/mydb")
# ...


@db.define()
class User(TypedTable):
name: str
Expand Down Expand Up @@ -143,11 +144,12 @@ db = TypeDAL(...)
<td>

```python
db.define_table("table_name",
Field("fieldname", "string", required=True),
Field("otherfield", "float"),
Field("yet_another", "text", default="Something")
)
db.define_table(
"table_name",
Field("fieldname", "string", required=True),
Field("otherfield", "float"),
Field("yet_another", "text", default="Something"),
)
```

</td>
Expand Down Expand Up @@ -236,7 +238,6 @@ all_rows = TableName.collect() # or .all()
rows = TableName.select(Tablename.id).where(TableName.id > 5).where(TableName.id < 50).collect()
# one:
row = TableName(id=1) # or .where(...).first()

```

<td>
Expand Down Expand Up @@ -301,11 +302,11 @@ These helpers are useful for scenarios where direct access to the PyDAL objects
An example of this is when you need to do a `db.commit()` but you can't import `db` directly:

```python
from typedal.helpers import get_db #, get_table, get_field
from typedal.helpers import get_db # , get_table, get_field

MyTable.insert(...)
db = get_db(MyTable)
db.commit() # this is usually done automatically but sometimes you want to manually commit.
db.commit() # this is usually done automatically but sometimes you want to manually commit.
```

## Caveats
Expand Down
16 changes: 5 additions & 11 deletions docs/1_getting_started.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pip install typedal[py4web]

```python
from typedal import TypeDAL

# or, if in py4web:
from typedal.for_py4web import TypeDAL

Expand Down Expand Up @@ -55,15 +56,11 @@ Or use the `placeholders` argument with positional or named parameters:

```python
# Positional
rows = db.executesql(
"SELECT * FROM some_table WHERE name = %s AND age > %s",
placeholders=[name, 18]
)
rows = db.executesql("SELECT * FROM some_table WHERE name = %s AND age > %s", placeholders=[name, 18])

# Named
rows = db.executesql(
"SELECT * FROM some_table WHERE name = %(name)s AND age > %(age)s",
placeholders={"name": name, "age": 18}
"SELECT * FROM some_table WHERE name = %(name)s AND age > %(age)s", placeholders={"name": name, "age": 18}
)
```

Expand All @@ -73,14 +70,11 @@ By default, `executesql()` returns rows as tuples. To map results to specific fi
Field/TypedField objects) or `colnames` (takes column name strings):

```python
rows = db.executesql(
"SELECT id, name FROM some_table",
colnames=["id", "name"]
)
rows = db.executesql("SELECT id, name FROM some_table", colnames=["id", "name"])

rows = db.executesql(
"SELECT id, name FROM some_table",
fields=[some_table.id, some_table.name] # Requires table definition
fields=[some_table.id, some_table.name], # Requires table definition
)
```

Expand Down
5 changes: 2 additions & 3 deletions docs/2_defining_tables.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ The syntax for creating a table is very different, but built on the same princip
from pydal import Field

# pydal:
db.define_table('my_table', Field('my_field'))
db.define_table("my_table", Field("my_field"))
```

```python
Expand Down Expand Up @@ -121,8 +121,7 @@ from typedal import TypedTable
from typedal.types import OpRow, Reference, Set


class MyTable(TypedTable):
...
class MyTable(TypedTable): ...


def my_before_insert(row: MyTable):
Expand Down
12 changes: 6 additions & 6 deletions docs/3_building_queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ Here you can enter any number of fields as arguments: database columns by name (
other (e.g. Table.ALL), or Expression objects.

```python
Person.select('id', Person.name, Person.ALL) # defaults to Person.ALL if select is omitted.
Person.select("id", Person.name, Person.ALL) # defaults to Person.ALL if select is omitted.
```

You can also specify extra options as keyword arguments. Supported options are: `orderby`, `groupby`, `limitby`,
Expand Down Expand Up @@ -131,10 +131,7 @@ Person.where(expr).select()

# Named arguments
expr = db.sql_expression(
"EXTRACT(year FROM %(date_col)s) = %(year)s",
date_col="created_at",
year=2023,
output_type="boolean"
"EXTRACT(year FROM %(date_col)s) = %(year)s", date_col="created_at", year=2023, output_type="boolean"
)
Person.where(expr).select()
```
Expand All @@ -152,7 +149,7 @@ By default, the `method` defined in the relationship is used.
This can be overwritten with the `method` keyword argument (left or inner)

```python
Person.join('articles', method='inner') # will only yield persons that have related articles
Person.join("articles", method="inner") # will only yield persons that have related articles
```

For more details about relationships and joins, see [4. Relationships](./4_relationships.md).
Expand Down Expand Up @@ -277,15 +274,18 @@ class User(TypedTable):
password_hash: str
is_active: bool


class PublicUser(TypedTable):
id: int
email: str
is_active: bool
profile_url: str | None = None


def enrich_profile_url(row: PublicUser, _raw):
row.profile_url = f"/users/{row.id}"


rows = User.where(is_active=True).collect_into(
PublicUser,
# note: `init` is optional:
Expand Down
35 changes: 22 additions & 13 deletions docs/4_relationships.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ class Post(TypedTable):
author: Author


authors_with_roles = Author.join('roles').collect()
posts_with_author = Post.join().collect() # join can be called without arguments to join all relationships (in this case only 'author')
authors_with_roles = Author.join("roles").collect()
posts_with_author = (
Post.join().collect()
) # join can be called without arguments to join all relationships (in this case only 'author')
post_deep = Post.join("author.roles").collect() # nested relationship, accessible via post.author.roles
```

Expand Down Expand Up @@ -127,32 +129,39 @@ owner: "User"
Setting up a relationship that uses a junction/pivot table is slightly harder.

```python

# with `unique_alias()` which is better if you have multiple joins:


@db.define()
class Post(TypedTable):
title: str
author: Author

tags = relationship(list["Tag"], on=lambda post, tag: [
# post and tag already have a unique alias, create one for tagged here:
tagged := Tagged.unique_alias(),
tagged.on(tagged.post == post.id),
tag.on(tag.id == tagged.tag),
])
tags = relationship(
list["Tag"],
on=lambda post, tag: [
# post and tag already have a unique alias, create one for tagged here:
tagged := Tagged.unique_alias(),
tagged.on(tagged.post == post.id),
tag.on(tag.id == tagged.tag),
],
)


# without unique alias:


@db.define()
class Tag(TypedTable):
name: str

posts = relationship(list["Post"], on=lambda tag, posts: [
Tagged.on(Tagged.tag == tag.id),
posts.on(posts.id == Tagged.post),
])
posts = relationship(
list["Post"],
on=lambda tag, posts: [
Tagged.on(Tagged.tag == tag.id),
posts.on(posts.id == Tagged.post),
],
)


@db.define()
Expand Down
8 changes: 3 additions & 5 deletions docs/5_py4web.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,7 @@ This library also has some py4web/web2py-specific enhancements.
# common.py
from typedal.for_py4web import DAL

db = DAL(
settings.DB_URI,
...
)
db = DAL(settings.DB_URI, ...)
```

This version of the `DAL` is also a py4web Fixture that manages database connections `on_request`, just as py4web's own
Expand All @@ -27,6 +24,7 @@ from .common import db

# you can now customize auth user:


class AuthUser(_AuthUser):
bookmarks = relationship(list["Bookmark"], ...)

Expand All @@ -35,7 +33,6 @@ db.define(AuthUser, redefine=True)

# or if you don't want to customize auth user:
setup_py4web_tables(db)

```

TypeDAL also provides an `AuthUser` class based on `db.auth_user`.
Expand All @@ -53,6 +50,7 @@ from .common import db

# you can now customize auth user:


class AuthUser(_AuthUser):
bookmarks = relationship(list["Bookmark"], ...)

Expand Down
13 changes: 7 additions & 6 deletions docs/8_mixins.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class MyTable(TypedTable, TimestampsMixin):
# Define your table fields here
pass


# Now, whenever you create or update a record in MyTable, the 'created_at' and 'updated_at' timestamps will be automatically managed.
```

Expand All @@ -44,6 +45,7 @@ class MyTable(TypedTable, SlugMixin, slug_field="title"):
title: str # Assuming 'title' is a field in your table
# Define other fields here


# Now, whenever you insert a record into MyTable, the 'slug' field will be automatically generated based on the 'title' field.
```

Expand Down Expand Up @@ -101,6 +103,7 @@ from fastapi import FastAPI

app = FastAPI()


@app.get("/books/{book_id}")
def get_book(book_id: int) -> Book:
return Book.where(id=book_id).join("author").first()
Expand Down Expand Up @@ -154,26 +157,24 @@ class HasImageMixin(Mixin):

# Now you can use HasImageMixin in your table definitions along with other mixins or base classes.


class Article(TypedTable, TimestampsMixin, HasImageMixin):
title: str

# this could also be a class method of Timestamps Mixin:
@classmethod
def recently_updated(cls, hours: int = 24) -> QueryBuilder[t.Self]:
"""Return records updated in the last N hours."""
cutoff = dt.datetime.now() - dt.timedelta(hours=hours)
return QueryBuilder(cls).where(cls.updated_at >= cutoff)


# Retrieve a record and use the custom method
article = Article(id=1)
article.img() # -> <img src=... />

# Use the classmethod to get recently updated articles
recent_articles = (
Article.recently_updated(hours=12)
.where(published=True)
.collect()
)
recent_articles = Article.recently_updated(hours=12).where(published=True).collect()
```

> **Note:** The `img()` example uses py4web utilities (URL, IMG), but the mixin itself works identically in any setup.
Expand Down
9 changes: 6 additions & 3 deletions docs/9_memoization.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ def process_articles(articles: TypedRows[Article]) -> dict:
# dummy example, normally you'd use .join() of course
for article in articles:
comments = Comment.where(article=article).collect()
result[article.id] = comments
result[article.id] = comments
return result


articles = Article.where(published=True).collect()

result, status = db.memoize(process_articles, articles)
Expand Down Expand Up @@ -53,6 +54,7 @@ When any tracked row is updated, inserted, or deleted, the cached result is inva
def something_slow():
return list(User.join())


result, status = db.memoize(something_slow)
assert status == "fresh"

Expand Down Expand Up @@ -114,9 +116,11 @@ TypeDAL provides `before_collect`/`before_execute` and `after_collect`/`after_ex
def print_query(qb: QueryBuilder):
print("going to run", qb.to_sql())


def print_duration(_qb: QueryBuilder, rows, _raw):
print("took", rows.metadata["select_duration"])


db.before_collect.append(print_query)
db.after_collect.append(print_duration)

Expand All @@ -143,8 +147,7 @@ If you need to disable cache invalidation hooks for a specific table:

```python
@db.define(cache_dependency=False)
class SpecialTable(TypedTable):
...
class SpecialTable(TypedTable): ...
```

**Warning:** Disabling this may break caching functionality for queries involving this table.
Expand Down
Loading
Loading