Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
47ac538
build: add asyncio optional dependency
seapagan Apr 18, 2026
46641a6
feat: add core asyncio database support
seapagan Apr 18, 2026
87df13e
feat: add async prefetch and shared query helpers
seapagan Apr 18, 2026
3161d28
update pytest-mock
seapagan Apr 19, 2026
5a7fc22
test: restore full coverage for async support
seapagan Apr 19, 2026
aad6cf1
feat: add async ORM support
seapagan Apr 19, 2026
63f918d
docs: add async API reference
seapagan Apr 19, 2026
8e11ee3
add markdown linting config
seapagan Apr 19, 2026
ec2b3f0
perf: batch async bulk_insert into a single transaction
seapagan Apr 19, 2026
a617d6e
refactor: extract shared cache helpers to reduce sync/async duplication
seapagan Apr 19, 2026
1954ecd
refactor: clean up async db_context binding and transaction flag
seapagan Apr 19, 2026
9163e04
delete old .markdownlintrc
seapagan Apr 19, 2026
ba43501
docs: release-readiness pass for async support
seapagan Apr 19, 2026
39b1ea1
feat: add async support demos to TUI and documentation
seapagan Apr 19, 2026
6925646
docs: document mypy type limitations for async FK and reverse accessors
seapagan Apr 19, 2026
b55ec77
chore: remove async demo icon to match other categories; add TODO for…
seapagan Apr 19, 2026
99ae1a9
fix list style in guide orm section
seapagan Apr 19, 2026
8af1978
fix: resolve Codacy issues with type checks and unused import
seapagan Apr 19, 2026
179511c
test: update type-check tests and add comprehensive async demo tests
seapagan Apr 19, 2026
057f326
fix: eliminate test ordering and thread leaks in async demo tests
seapagan Apr 19, 2026
cd82c33
fix: align async API parity
seapagan Apr 19, 2026
e62e22b
refactor: share CRUD SQL plans
seapagan Apr 19, 2026
76c0720
refactor: share schema SQL builders
seapagan Apr 19, 2026
ea53c9c
refactor: share prefetch query helpers
seapagan Apr 19, 2026
36d4075
refactor: share bulk insert preparation
seapagan Apr 19, 2026
89df2d2
fix: align async cache API and docs
seapagan Apr 19, 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
4 changes: 4 additions & 0 deletions .markdownlint.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
MD013:
line_length: 80
tables: false
MD028: false
3 changes: 0 additions & 3 deletions .markdownlintrc

This file was deleted.

1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ repos:
# we need to force python 3.10 or it chokes on pytest v9+
args: ["--strict", "--python-version", "3.10"]
additional_dependencies:
- aiosqlite
- pydantic
- sqladmin
- types-redis
Expand Down
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ is Pydantic itself.

It does not aim to be a full-fledged ORM like SQLAlchemy, but rather a simple
and easy-to-use library for basic database operations, especially for small
projects. It is NOT asynchronous (at this time, though that is planned).
projects. It supports both synchronous and asynchronous APIs.

The ideal use case is more for Python CLI tools that need to store data in a
database-like format without needing to learn SQL or use a full ORM.
Expand Down Expand Up @@ -51,6 +51,7 @@ Full documentation is available on the [Website](https://sqliter.grantramsay.dev
- Foreign key relationships with referential integrity and CASCADE actions
- ORM mode with lazy loading, reverse relationships, many-to-many support, and
eager loading
- Optional async support for database, query, and ORM usage
- Chained Query building with filtering, ordering, and pagination
- Projection and aggregation queries with grouping, aggregate helpers, and
dictionary-based results
Expand Down Expand Up @@ -94,9 +95,10 @@ Currently by default, the only external dependency is Pydantic. However, there
are some optional dependencies that can be installed to enable additional
features:

- `async`: Installs `aiosqlite` for `sqliter.asyncio` support
- `demo`: Installs the Textual TUI framework for the interactive demo
- `extras`: Installs Inflect for better pluralization of table names
- `full`: Installs all optional dependencies (Textual and Inflect)
- `full`: Installs all optional dependencies

See [Installing Optional
Dependencies](https://sqliter.grantramsay.dev/installation#optional-dependencies)
Expand Down Expand Up @@ -145,6 +147,10 @@ See the [Guide](https://sqliter.grantramsay.dev/guide/guide/) section of the
documentation for more detailed information on how to use SQLiter, and advanced
features.

Async usage is available through `sqliter.asyncio` and `sqliter.asyncio.orm`.
See the [async guide](https://sqliter.grantramsay.dev/guide/asyncio/) for the
async API and async ORM relationship patterns.

You can also run the interactive TUI demo to explore SQLiter features hands-on:

```bash
Expand Down
2 changes: 2 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ Items marked with :fire: are high priority.

- Tidy up the test suite - remove any duplicates, sort them into logical files
(many already are), try to reduce and centralize fixtures.
- Add suitable icons to all TUI demo categories (currently all use `icon=""`)
for a more visually scannable tree view.

## Documentation

Expand Down
229 changes: 229 additions & 0 deletions docs/api-reference/async-orm.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
# Async ORM

Async ORM support lives under `sqliter.asyncio.orm`.

```python
from sqliter.asyncio.orm import (
AsyncBaseDBModel,
AsyncForeignKey,
AsyncManyToMany,
)
```

**Sources:** `sqliter/asyncio/orm/model.py`, `sqliter/asyncio/orm/fields.py`,
`sqliter/asyncio/orm/query.py`, `sqliter/asyncio/orm/m2m.py`

See also: [Guide -- Asyncio Support](../guide/asyncio.md)

> [!NOTE]
> Async ORM intentionally differs from sync ORM for lazy FK access:
> sync uses `book.author.name`, while async lazy loading uses
> `author = await book.author.fetch()`.

---

## `AsyncBaseDBModel`

Base model for async ORM usage.

```python
from sqliter.asyncio.orm import AsyncBaseDBModel
```

Use this instead of `sqliter.orm.BaseDBModel` when the model is meant to be
used with `AsyncSqliterDB`.

### Behavior

- forward FK fields return async-aware values
- reverse FK accessors return async reverse query wrappers
- many-to-many accessors return async managers
- eager-loaded relations are returned directly from caches

---

## `AsyncForeignKey[T]`

Async FK descriptor.

```python
class AsyncForeignKey(Generic[T]):
def __init__(
self,
to_model: type[T],
*,
on_delete: FKAction = "RESTRICT",
on_update: FKAction = "RESTRICT",
null: bool = False,
unique: bool = False,
related_name: str | None = None,
db_column: str | None = None,
) -> None:
```

### Lazy access

Accessing a forward FK on an instance returns either:

- `None` for null FK values
- the eager-loaded related object if already cached
- an `AsyncLazyLoader[T]` otherwise

Example:

```python
book = await db.get(Book, 1)
loader = book.author
author = await loader.fetch()
```

---

## `AsyncLazyLoader[T]`

Explicit async FK loader.

### `fetch()`

```python
async def fetch(self) -> T | None:
```

Loads the related object and caches it on the loader.

### `db_context`

Read-only property exposing the current DB context.

### `__getattr__()`

Raises `AttributeError` with guidance to call `await relation.fetch()` first.

---

## `AsyncReverseQuery`

Returned by reverse FK accessors such as `author.books`.

### Query-building methods

- `filter(**kwargs)`
- `limit(count)`
- `offset(count)`

### Async terminal methods

- `await fetch_all()`
- `await fetch_one()`
- `await count()`
- `await exists()`

Example:

```python
author = await db.get(Author, 1)
books = await author.books.filter(title="Guide").fetch_all()
```

---

## `AsyncPrefetchedResult`

Returned when a reverse FK relation was loaded through `prefetch_related()`.

Read methods:

- `await fetch_all()`
- `await fetch_one()`
- `await count()`
- `await exists()`

`filter(**kwargs)` falls back to a real `AsyncReverseQuery`.

---

## `AsyncManyToMany[T]`

Async many-to-many descriptor.

```python
class AsyncManyToMany(Generic[T]):
def __init__(
self,
to_model: type[T] | str,
*,
through: str | None = None,
related_name: str | None = None,
symmetrical: bool = False,
) -> None:
```

Accessing the descriptor from an instance returns an async manager or a
prefetched wrapper.

---

## `AsyncManyToManyManager[T]`

Returned by many-to-many accessors such as `article.tags`.

### Read/query methods

- `await fetch_all()`
- `await fetch_one()`
- `await count()`
- `await exists()`
- `await filter(**kwargs)`

### Write methods

- `await add(*instances)`
- `await remove(*instances)`
- `await clear()`
- `await set(*instances)`

### Property

- `sql_metadata`

Example:

```python
await article.tags.add(tag)
tags = await article.tags.fetch_all()
```

---

## `AsyncPrefetchedM2MResult[T]`

Returned when an M2M relation was loaded through `prefetch_related()`.

Read methods are served from cache:

- `await fetch_all()`
- `await fetch_one()`
- `await count()`
- `await exists()`

Write methods delegate to the real manager:

- `await add(*instances)`
- `await remove(*instances)`
- `await clear()`
- `await set(*instances)`

`await filter(**kwargs)` falls back to a real async query.

---

## `AsyncReverseManyToMany`

Reverse-side async many-to-many descriptor installed on the related model when
`related_name` is defined.

Example:

```python
articles = await tag.articles.fetch_all()
```
Loading