sqrrl 0.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.
- sqrrl-0.1.0/.gitignore +16 -0
- sqrrl-0.1.0/LICENSE +21 -0
- sqrrl-0.1.0/MANIFEST.in +5 -0
- sqrrl-0.1.0/PKG-INFO +289 -0
- sqrrl-0.1.0/README.md +256 -0
- sqrrl-0.1.0/benchmarks/bench_imports.py +163 -0
- sqrrl-0.1.0/benchmarks/bench_sqrrl.py +177 -0
- sqrrl-0.1.0/examples/__init__.py +0 -0
- sqrrl-0.1.0/examples/main.py +23 -0
- sqrrl-0.1.0/examples/migrations/000001_initial.json +195 -0
- sqrrl-0.1.0/examples/migrations/000001_initial.sql +25 -0
- sqrrl-0.1.0/examples/models.py +195 -0
- sqrrl-0.1.0/examples/schema.py +36 -0
- sqrrl-0.1.0/examples/sqrrl.json +5 -0
- sqrrl-0.1.0/pyproject.toml +77 -0
- sqrrl-0.1.0/setup.cfg +4 -0
- sqrrl-0.1.0/sqrrl/__init__.py +13 -0
- sqrrl-0.1.0/sqrrl/__main__.py +3 -0
- sqrrl-0.1.0/sqrrl/cli.py +193 -0
- sqrrl-0.1.0/sqrrl/errors.py +17 -0
- sqrrl-0.1.0/sqrrl/generate.py +226 -0
- sqrrl-0.1.0/sqrrl/migrate.py +548 -0
- sqrrl-0.1.0/sqrrl/py.typed +0 -0
- sqrrl-0.1.0/sqrrl/runtime.py +557 -0
- sqrrl-0.1.0/sqrrl/schema.py +450 -0
- sqrrl-0.1.0/sqrrl.egg-info/PKG-INFO +289 -0
- sqrrl-0.1.0/sqrrl.egg-info/SOURCES.txt +38 -0
- sqrrl-0.1.0/sqrrl.egg-info/dependency_links.txt +1 -0
- sqrrl-0.1.0/sqrrl.egg-info/entry_points.txt +2 -0
- sqrrl-0.1.0/sqrrl.egg-info/requires.txt +11 -0
- sqrrl-0.1.0/sqrrl.egg-info/top_level.txt +1 -0
- sqrrl-0.1.0/tests/conftest.py +66 -0
- sqrrl-0.1.0/tests/test_async.py +258 -0
- sqrrl-0.1.0/tests/test_cli.py +84 -0
- sqrrl-0.1.0/tests/test_generate.py +155 -0
- sqrrl-0.1.0/tests/test_migrate.py +270 -0
- sqrrl-0.1.0/tests/test_pass_regressions.py +119 -0
- sqrrl-0.1.0/tests/test_regressions.py +377 -0
- sqrrl-0.1.0/tests/test_runtime.py +142 -0
- sqrrl-0.1.0/tests/test_schema.py +39 -0
sqrrl-0.1.0/.gitignore
ADDED
sqrrl-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Caprine Logic
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
sqrrl-0.1.0/MANIFEST.in
ADDED
sqrrl-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sqrrl
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Generated typed async data access and checked migrations for SQLite
|
|
5
|
+
Author: depthbomb
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Documentation, https://github.com/depthbomb/sqrrl#readme
|
|
8
|
+
Project-URL: Source, https://github.com/depthbomb/sqrrl
|
|
9
|
+
Project-URL: Issues, https://github.com/depthbomb/sqrrl/issues
|
|
10
|
+
Project-URL: Releases, https://github.com/depthbomb/sqrrl/releases
|
|
11
|
+
Keywords: sqlite,async,database,migrations,code-generation,typing
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.14
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: aiosqlite>=0.22
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
25
|
+
Requires-Dist: mypy>=1.18.2; extra == "dev"
|
|
26
|
+
Requires-Dist: pyright>=1.1.407; extra == "dev"
|
|
27
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest-asyncio>=1; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest-cov>=6; extra == "dev"
|
|
30
|
+
Requires-Dist: ruff>=0.14; extra == "dev"
|
|
31
|
+
Requires-Dist: twine>=6; extra == "dev"
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
# sqrrl (pronounced squirrel)
|
|
35
|
+
|
|
36
|
+
Typed async SQLite access, generated from a Python schema, with easy migration management.
|
|
37
|
+
|
|
38
|
+
Describe your tables once, and sqrrl generates dataclasses, repositories, and
|
|
39
|
+
column helpers with real type annotations. Your editor knows which fields a
|
|
40
|
+
query returns and which arguments a write accepts. Database calls run through
|
|
41
|
+
`aiosqlite`.
|
|
42
|
+
|
|
43
|
+
Requires **Python 3.14+** and **SQLite 3.37+**. This is an early release, and the
|
|
44
|
+
API is still taking shape.
|
|
45
|
+
|
|
46
|
+
## Getting started
|
|
47
|
+
|
|
48
|
+
With your virtual environment active:
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
python -m pip install sqrrl
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
In your app's directory, create a starter schema and configuration:
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
sqrrl init
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
That gives you `schema.py` with a small notes table and a `sqrrl.json` file:
|
|
61
|
+
|
|
62
|
+
```json
|
|
63
|
+
{
|
|
64
|
+
"schema": "schema:schema",
|
|
65
|
+
"output": "models.py",
|
|
66
|
+
"migrations": "migrations"
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Generate the Python code and your first migration, then create the database:
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
sqrrl generate
|
|
74
|
+
sqrrl migrate diff initial
|
|
75
|
+
# Review migrations/000001_initial.sql before applying it.
|
|
76
|
+
sqrrl migrate up --db app.db
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Save this as `main.py` next to the generated `models.py`:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from asyncio import run
|
|
83
|
+
from sqrrl import Database
|
|
84
|
+
from models import Client, NoteColumns
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
async def main() -> None:
|
|
88
|
+
async with await Database.open("app.db") as database:
|
|
89
|
+
client = Client(database)
|
|
90
|
+
note = await client.notes.create(title="Try sqrrl")
|
|
91
|
+
print(note.id, note.title, note.done)
|
|
92
|
+
|
|
93
|
+
pending = await client.notes.query().where(NoteColumns.done.eq(False)).all()
|
|
94
|
+
print([item.title for item in pending])
|
|
95
|
+
|
|
96
|
+
await client.notes.update(note.id, done=True)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
run(main())
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Run it with `python main.py`. `Database.open()` requires an existing file;
|
|
103
|
+
`Database.create()` creates one if it's missing. Neither applies migrations
|
|
104
|
+
automatically. `sqrrl migrate up` creates the database when needed and applies
|
|
105
|
+
pending migrations.
|
|
106
|
+
|
|
107
|
+
You can use `python -m sqrrl` anywhere you'd use `sqrrl`.
|
|
108
|
+
|
|
109
|
+
## Defining a schema
|
|
110
|
+
|
|
111
|
+
A schema is a regular Python object. Here's the notes table from the starter:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from sqrrl.schema import Schema, Table, boolean, integer, text
|
|
115
|
+
|
|
116
|
+
schema = Schema(
|
|
117
|
+
tables=(
|
|
118
|
+
Table(
|
|
119
|
+
"notes",
|
|
120
|
+
model="Note",
|
|
121
|
+
fields=(
|
|
122
|
+
integer("id").primary_key(),
|
|
123
|
+
text("title"),
|
|
124
|
+
boolean("done").default("0"),
|
|
125
|
+
),
|
|
126
|
+
),
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The field helpers are `integer`, `text`, `boolean`, `real`, and `blob`. Fields
|
|
132
|
+
are required unless you add `.nullable()`. You can also declare unique fields,
|
|
133
|
+
foreign keys, immutable fields, indexes, checks, and composite primary keys.
|
|
134
|
+
Tables use SQLite's `STRICT` mode.
|
|
135
|
+
|
|
136
|
+
Defaults are **SQL expressions**, so `.default('0')` stores zero and
|
|
137
|
+
`.default("'draft'")` stores the text `draft`. `.immutable()` leaves a field out
|
|
138
|
+
of the generated update method; it doesn't prevent changes through raw SQL.
|
|
139
|
+
|
|
140
|
+
The [example schema](https://github.com/depthbomb/sqrrl/blob/master/examples/schema.py)
|
|
141
|
+
includes users, tasks, foreign keys, an index, and a composite key. Its
|
|
142
|
+
[generated models](https://github.com/depthbomb/sqrrl/blob/master/examples/models.py)
|
|
143
|
+
show what sqrrl produces.
|
|
144
|
+
|
|
145
|
+
Configuration paths are relative to the configuration file. `schema` names an
|
|
146
|
+
importable module and its exported object, such as `myapp.schema:schema`.
|
|
147
|
+
Use `--config path/to/sqrrl.json` on a command to select another configuration.
|
|
148
|
+
The output file and migrations directory must stay inside that configuration's
|
|
149
|
+
directory. Schema modules are imported during generation and schema checks, so
|
|
150
|
+
keep them free of side effects and stdout output.
|
|
151
|
+
|
|
152
|
+
## Reading and writing
|
|
153
|
+
|
|
154
|
+
Each generated repository has `create`, `get`, `update`, `delete`, and `query`.
|
|
155
|
+
Using the starter's `client`:
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
note = await client.notes.create(title="Ship something small")
|
|
159
|
+
note = await client.notes.get(note.id)
|
|
160
|
+
note = await client.notes.update(note.id, title="Ship sqrrl")
|
|
161
|
+
await client.notes.delete(note.id)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Rows are frozen dataclasses. Writes return new rows instead of changing the
|
|
165
|
+
objects you already have. A single integer primary key can be omitted on
|
|
166
|
+
creation. For composite keys, the generator provides a key dataclass, such as
|
|
167
|
+
`SettingKey(user_id=1, key='theme')` in the example.
|
|
168
|
+
|
|
169
|
+
Omitting an optional write argument uses `UNSET`: on creation, SQLite gets to
|
|
170
|
+
apply its default; on update, that field stays unchanged. Passing `None`
|
|
171
|
+
explicitly writes SQL `NULL` and requires a nullable field.
|
|
172
|
+
|
|
173
|
+
Build queries with the generated column helpers:
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
query = client.notes.query().where(NoteColumns.done.eq(False))
|
|
177
|
+
latest = await query.order_by(NoteColumns.id.desc()).limit(10).all()
|
|
178
|
+
first = await query.order_by(NoteColumns.id.asc()).first()
|
|
179
|
+
count = await query.count()
|
|
180
|
+
has_notes = await query.exists()
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Query builders return new queries, so you can reuse a base query. Values are
|
|
184
|
+
bound as SQL parameters. Column helpers support `eq`, `ne`, `gt`, `lt`, `in_`,
|
|
185
|
+
`is_null`, and `is_not_null`. Combine predicates with `&`, `|`, and `~`:
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
matching = await client.notes.query().where(NoteColumns.done.eq(False) & NoteColumns.id.gt(10)).all()
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
`first()` returns `None` when nothing matches. `only()` requires exactly one row:
|
|
192
|
+
it raises `NotFoundError` for no matches and `NotSingularError` for multiple
|
|
193
|
+
matches. `get()` and `update()` also raise `NotFoundError` for a missing key;
|
|
194
|
+
deleting a missing row is fine. These errors are available from `sqrrl`.
|
|
195
|
+
|
|
196
|
+
## Transactions and connections
|
|
197
|
+
|
|
198
|
+
Group writes with a transaction:
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
async with client.transaction() as transaction:
|
|
202
|
+
first = await transaction.notes.create(title="Write the README")
|
|
203
|
+
await transaction.notes.create(title="Publish the package")
|
|
204
|
+
await transaction.notes.update(first.id, done=True)
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
The transaction commits on success and rolls back on errors. Nested
|
|
208
|
+
transactions use savepoints. Cancellation waits for queued SQLite work to
|
|
209
|
+
settle; a commit that has already started can finish before cancellation arrives.
|
|
210
|
+
|
|
211
|
+
Each `Database` owns one connection and serializes repository access between
|
|
212
|
+
tasks. Keep a transaction's work in the task that opened it. Awaiting another
|
|
213
|
+
task that needs the same connection while holding a transaction can deadlock.
|
|
214
|
+
Use separate connections for concurrent work inside that scope.
|
|
215
|
+
|
|
216
|
+
Foreign keys are enabled. `Database.open()` and `Database.create()` also accept
|
|
217
|
+
`wal=True`, `timeout=5.0` (seconds), and `immediate=True` for `BEGIN IMMEDIATE`
|
|
218
|
+
transactions. WAL is opt-in.
|
|
219
|
+
|
|
220
|
+
For SQL beyond the query builder, `database.connection` exposes the underlying
|
|
221
|
+
`aiosqlite` connection. Use `database.transaction()` to coordinate raw SQL with
|
|
222
|
+
repository operations, close your cursors, and keep the connection in its owning
|
|
223
|
+
task during a transaction.
|
|
224
|
+
|
|
225
|
+
## Changing the database
|
|
226
|
+
|
|
227
|
+
After editing `schema.py`, regenerate the models and create a migration:
|
|
228
|
+
|
|
229
|
+
```sh
|
|
230
|
+
sqrrl generate
|
|
231
|
+
sqrrl migrate diff add_description
|
|
232
|
+
sqrrl migrate check
|
|
233
|
+
sqrrl migrate status --db app.db
|
|
234
|
+
sqrrl migrate up --db app.db
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Each migration has a readable `.sql` file and a matching `.json` file containing
|
|
238
|
+
schema metadata and checksums. Commit both, along with your generated models.
|
|
239
|
+
Review the SQL before applying it. Don't edit migration history in place:
|
|
240
|
+
sqrrl checks that the SQL, metadata, and checksum chain agree.
|
|
241
|
+
|
|
242
|
+
`migrate check` replays history in a scratch database and compares the result
|
|
243
|
+
with your declared schema. Applying migrations checks the live schema for drift
|
|
244
|
+
and applies the pending batch in one transaction, including foreign key checks.
|
|
245
|
+
|
|
246
|
+
Some changes need a little planning:
|
|
247
|
+
|
|
248
|
+
- Dropping tables or fields requires `migrate diff NAME --allow-drop`.
|
|
249
|
+
- For a field rename, keep its old identity with
|
|
250
|
+
`text('new_name').identity('old_name')`. For a table rename, preserve its `key`.
|
|
251
|
+
- A new required field needs a default or a staged backfill. Automatic primary
|
|
252
|
+
key changes and field representation changes are unsupported.
|
|
253
|
+
- `sqrrl migrate custom backfill --sql backfill.sql` records data-only SQL.
|
|
254
|
+
Custom schema objects, such as views and triggers, aren't supported.
|
|
255
|
+
- `sqrrl migrate baseline --db existing.db --version 1` adopts a database only
|
|
256
|
+
when its schema matches that migration exactly. Equivalent DDL written
|
|
257
|
+
differently can still be rejected.
|
|
258
|
+
|
|
259
|
+
There is no downgrade command. Make further changes with new migrations.
|
|
260
|
+
|
|
261
|
+
For an app's CI, these commands catch stale models and missing migrations:
|
|
262
|
+
|
|
263
|
+
```sh
|
|
264
|
+
sqrrl generate --check
|
|
265
|
+
sqrrl migrate check
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
## Development
|
|
269
|
+
|
|
270
|
+
From a checkout, create a Python 3.14+ virtual environment with
|
|
271
|
+
`python -m venv .venv`, then activate it (`.venv\Scripts\Activate.ps1` in
|
|
272
|
+
PowerShell, or `source .venv/bin/activate` in bash).
|
|
273
|
+
|
|
274
|
+
```sh
|
|
275
|
+
python -m pip install -e ".[dev]"
|
|
276
|
+
python -m pytest --cov=sqrrl --cov-branch
|
|
277
|
+
python -m ruff check .
|
|
278
|
+
python -m mypy
|
|
279
|
+
python -m pyright
|
|
280
|
+
python -m sqrrl generate --config examples/sqrrl.json --check
|
|
281
|
+
python -m sqrrl migrate check --config examples/sqrrl.json
|
|
282
|
+
python -m examples.main
|
|
283
|
+
python -m build --outdir dist/release
|
|
284
|
+
python -m twine check --strict dist/release/*
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
The example uses a temporary database and cleans up after itself. The
|
|
288
|
+
`benchmarks/` directory has separate runners for database operations and import
|
|
289
|
+
overhead.
|
sqrrl-0.1.0/README.md
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# sqrrl (pronounced squirrel)
|
|
2
|
+
|
|
3
|
+
Typed async SQLite access, generated from a Python schema, with easy migration management.
|
|
4
|
+
|
|
5
|
+
Describe your tables once, and sqrrl generates dataclasses, repositories, and
|
|
6
|
+
column helpers with real type annotations. Your editor knows which fields a
|
|
7
|
+
query returns and which arguments a write accepts. Database calls run through
|
|
8
|
+
`aiosqlite`.
|
|
9
|
+
|
|
10
|
+
Requires **Python 3.14+** and **SQLite 3.37+**. This is an early release, and the
|
|
11
|
+
API is still taking shape.
|
|
12
|
+
|
|
13
|
+
## Getting started
|
|
14
|
+
|
|
15
|
+
With your virtual environment active:
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
python -m pip install sqrrl
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
In your app's directory, create a starter schema and configuration:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
sqrrl init
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
That gives you `schema.py` with a small notes table and a `sqrrl.json` file:
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"schema": "schema:schema",
|
|
32
|
+
"output": "models.py",
|
|
33
|
+
"migrations": "migrations"
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Generate the Python code and your first migration, then create the database:
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
sqrrl generate
|
|
41
|
+
sqrrl migrate diff initial
|
|
42
|
+
# Review migrations/000001_initial.sql before applying it.
|
|
43
|
+
sqrrl migrate up --db app.db
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Save this as `main.py` next to the generated `models.py`:
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from asyncio import run
|
|
50
|
+
from sqrrl import Database
|
|
51
|
+
from models import Client, NoteColumns
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
async def main() -> None:
|
|
55
|
+
async with await Database.open("app.db") as database:
|
|
56
|
+
client = Client(database)
|
|
57
|
+
note = await client.notes.create(title="Try sqrrl")
|
|
58
|
+
print(note.id, note.title, note.done)
|
|
59
|
+
|
|
60
|
+
pending = await client.notes.query().where(NoteColumns.done.eq(False)).all()
|
|
61
|
+
print([item.title for item in pending])
|
|
62
|
+
|
|
63
|
+
await client.notes.update(note.id, done=True)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
run(main())
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Run it with `python main.py`. `Database.open()` requires an existing file;
|
|
70
|
+
`Database.create()` creates one if it's missing. Neither applies migrations
|
|
71
|
+
automatically. `sqrrl migrate up` creates the database when needed and applies
|
|
72
|
+
pending migrations.
|
|
73
|
+
|
|
74
|
+
You can use `python -m sqrrl` anywhere you'd use `sqrrl`.
|
|
75
|
+
|
|
76
|
+
## Defining a schema
|
|
77
|
+
|
|
78
|
+
A schema is a regular Python object. Here's the notes table from the starter:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from sqrrl.schema import Schema, Table, boolean, integer, text
|
|
82
|
+
|
|
83
|
+
schema = Schema(
|
|
84
|
+
tables=(
|
|
85
|
+
Table(
|
|
86
|
+
"notes",
|
|
87
|
+
model="Note",
|
|
88
|
+
fields=(
|
|
89
|
+
integer("id").primary_key(),
|
|
90
|
+
text("title"),
|
|
91
|
+
boolean("done").default("0"),
|
|
92
|
+
),
|
|
93
|
+
),
|
|
94
|
+
)
|
|
95
|
+
)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The field helpers are `integer`, `text`, `boolean`, `real`, and `blob`. Fields
|
|
99
|
+
are required unless you add `.nullable()`. You can also declare unique fields,
|
|
100
|
+
foreign keys, immutable fields, indexes, checks, and composite primary keys.
|
|
101
|
+
Tables use SQLite's `STRICT` mode.
|
|
102
|
+
|
|
103
|
+
Defaults are **SQL expressions**, so `.default('0')` stores zero and
|
|
104
|
+
`.default("'draft'")` stores the text `draft`. `.immutable()` leaves a field out
|
|
105
|
+
of the generated update method; it doesn't prevent changes through raw SQL.
|
|
106
|
+
|
|
107
|
+
The [example schema](https://github.com/depthbomb/sqrrl/blob/master/examples/schema.py)
|
|
108
|
+
includes users, tasks, foreign keys, an index, and a composite key. Its
|
|
109
|
+
[generated models](https://github.com/depthbomb/sqrrl/blob/master/examples/models.py)
|
|
110
|
+
show what sqrrl produces.
|
|
111
|
+
|
|
112
|
+
Configuration paths are relative to the configuration file. `schema` names an
|
|
113
|
+
importable module and its exported object, such as `myapp.schema:schema`.
|
|
114
|
+
Use `--config path/to/sqrrl.json` on a command to select another configuration.
|
|
115
|
+
The output file and migrations directory must stay inside that configuration's
|
|
116
|
+
directory. Schema modules are imported during generation and schema checks, so
|
|
117
|
+
keep them free of side effects and stdout output.
|
|
118
|
+
|
|
119
|
+
## Reading and writing
|
|
120
|
+
|
|
121
|
+
Each generated repository has `create`, `get`, `update`, `delete`, and `query`.
|
|
122
|
+
Using the starter's `client`:
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
note = await client.notes.create(title="Ship something small")
|
|
126
|
+
note = await client.notes.get(note.id)
|
|
127
|
+
note = await client.notes.update(note.id, title="Ship sqrrl")
|
|
128
|
+
await client.notes.delete(note.id)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Rows are frozen dataclasses. Writes return new rows instead of changing the
|
|
132
|
+
objects you already have. A single integer primary key can be omitted on
|
|
133
|
+
creation. For composite keys, the generator provides a key dataclass, such as
|
|
134
|
+
`SettingKey(user_id=1, key='theme')` in the example.
|
|
135
|
+
|
|
136
|
+
Omitting an optional write argument uses `UNSET`: on creation, SQLite gets to
|
|
137
|
+
apply its default; on update, that field stays unchanged. Passing `None`
|
|
138
|
+
explicitly writes SQL `NULL` and requires a nullable field.
|
|
139
|
+
|
|
140
|
+
Build queries with the generated column helpers:
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
query = client.notes.query().where(NoteColumns.done.eq(False))
|
|
144
|
+
latest = await query.order_by(NoteColumns.id.desc()).limit(10).all()
|
|
145
|
+
first = await query.order_by(NoteColumns.id.asc()).first()
|
|
146
|
+
count = await query.count()
|
|
147
|
+
has_notes = await query.exists()
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Query builders return new queries, so you can reuse a base query. Values are
|
|
151
|
+
bound as SQL parameters. Column helpers support `eq`, `ne`, `gt`, `lt`, `in_`,
|
|
152
|
+
`is_null`, and `is_not_null`. Combine predicates with `&`, `|`, and `~`:
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
matching = await client.notes.query().where(NoteColumns.done.eq(False) & NoteColumns.id.gt(10)).all()
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
`first()` returns `None` when nothing matches. `only()` requires exactly one row:
|
|
159
|
+
it raises `NotFoundError` for no matches and `NotSingularError` for multiple
|
|
160
|
+
matches. `get()` and `update()` also raise `NotFoundError` for a missing key;
|
|
161
|
+
deleting a missing row is fine. These errors are available from `sqrrl`.
|
|
162
|
+
|
|
163
|
+
## Transactions and connections
|
|
164
|
+
|
|
165
|
+
Group writes with a transaction:
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
async with client.transaction() as transaction:
|
|
169
|
+
first = await transaction.notes.create(title="Write the README")
|
|
170
|
+
await transaction.notes.create(title="Publish the package")
|
|
171
|
+
await transaction.notes.update(first.id, done=True)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
The transaction commits on success and rolls back on errors. Nested
|
|
175
|
+
transactions use savepoints. Cancellation waits for queued SQLite work to
|
|
176
|
+
settle; a commit that has already started can finish before cancellation arrives.
|
|
177
|
+
|
|
178
|
+
Each `Database` owns one connection and serializes repository access between
|
|
179
|
+
tasks. Keep a transaction's work in the task that opened it. Awaiting another
|
|
180
|
+
task that needs the same connection while holding a transaction can deadlock.
|
|
181
|
+
Use separate connections for concurrent work inside that scope.
|
|
182
|
+
|
|
183
|
+
Foreign keys are enabled. `Database.open()` and `Database.create()` also accept
|
|
184
|
+
`wal=True`, `timeout=5.0` (seconds), and `immediate=True` for `BEGIN IMMEDIATE`
|
|
185
|
+
transactions. WAL is opt-in.
|
|
186
|
+
|
|
187
|
+
For SQL beyond the query builder, `database.connection` exposes the underlying
|
|
188
|
+
`aiosqlite` connection. Use `database.transaction()` to coordinate raw SQL with
|
|
189
|
+
repository operations, close your cursors, and keep the connection in its owning
|
|
190
|
+
task during a transaction.
|
|
191
|
+
|
|
192
|
+
## Changing the database
|
|
193
|
+
|
|
194
|
+
After editing `schema.py`, regenerate the models and create a migration:
|
|
195
|
+
|
|
196
|
+
```sh
|
|
197
|
+
sqrrl generate
|
|
198
|
+
sqrrl migrate diff add_description
|
|
199
|
+
sqrrl migrate check
|
|
200
|
+
sqrrl migrate status --db app.db
|
|
201
|
+
sqrrl migrate up --db app.db
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Each migration has a readable `.sql` file and a matching `.json` file containing
|
|
205
|
+
schema metadata and checksums. Commit both, along with your generated models.
|
|
206
|
+
Review the SQL before applying it. Don't edit migration history in place:
|
|
207
|
+
sqrrl checks that the SQL, metadata, and checksum chain agree.
|
|
208
|
+
|
|
209
|
+
`migrate check` replays history in a scratch database and compares the result
|
|
210
|
+
with your declared schema. Applying migrations checks the live schema for drift
|
|
211
|
+
and applies the pending batch in one transaction, including foreign key checks.
|
|
212
|
+
|
|
213
|
+
Some changes need a little planning:
|
|
214
|
+
|
|
215
|
+
- Dropping tables or fields requires `migrate diff NAME --allow-drop`.
|
|
216
|
+
- For a field rename, keep its old identity with
|
|
217
|
+
`text('new_name').identity('old_name')`. For a table rename, preserve its `key`.
|
|
218
|
+
- A new required field needs a default or a staged backfill. Automatic primary
|
|
219
|
+
key changes and field representation changes are unsupported.
|
|
220
|
+
- `sqrrl migrate custom backfill --sql backfill.sql` records data-only SQL.
|
|
221
|
+
Custom schema objects, such as views and triggers, aren't supported.
|
|
222
|
+
- `sqrrl migrate baseline --db existing.db --version 1` adopts a database only
|
|
223
|
+
when its schema matches that migration exactly. Equivalent DDL written
|
|
224
|
+
differently can still be rejected.
|
|
225
|
+
|
|
226
|
+
There is no downgrade command. Make further changes with new migrations.
|
|
227
|
+
|
|
228
|
+
For an app's CI, these commands catch stale models and missing migrations:
|
|
229
|
+
|
|
230
|
+
```sh
|
|
231
|
+
sqrrl generate --check
|
|
232
|
+
sqrrl migrate check
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## Development
|
|
236
|
+
|
|
237
|
+
From a checkout, create a Python 3.14+ virtual environment with
|
|
238
|
+
`python -m venv .venv`, then activate it (`.venv\Scripts\Activate.ps1` in
|
|
239
|
+
PowerShell, or `source .venv/bin/activate` in bash).
|
|
240
|
+
|
|
241
|
+
```sh
|
|
242
|
+
python -m pip install -e ".[dev]"
|
|
243
|
+
python -m pytest --cov=sqrrl --cov-branch
|
|
244
|
+
python -m ruff check .
|
|
245
|
+
python -m mypy
|
|
246
|
+
python -m pyright
|
|
247
|
+
python -m sqrrl generate --config examples/sqrrl.json --check
|
|
248
|
+
python -m sqrrl migrate check --config examples/sqrrl.json
|
|
249
|
+
python -m examples.main
|
|
250
|
+
python -m build --outdir dist/release
|
|
251
|
+
python -m twine check --strict dist/release/*
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
The example uses a temporary database and cleans up after itself. The
|
|
255
|
+
`benchmarks/` directory has separate runners for database operations and import
|
|
256
|
+
overhead.
|