p2i: add Database Integration tutorial — all engines, query API, pool config, migrations, saturation handling
swadhinbiswas committed 3 months ago
67e97ec
1 parent c67d8ba
commit 67e97ec
1 file changed
+170 -0
Showing 1 changed file+170-0
▼docs_site/src/content/docs/tutorials/database-integration.md
+170-0
| @@ -0,0 +1,170 @@ | ||
1 | +--- | |
2 | +title: Database Integration | |
3 | +description: Connect to SQL and NoSQL databases with JustAPI's Rust-native connection pool. | |
4 | +--- | |
5 | + | |
6 | +JustAPI provides a built-in database connection pool manager (`app.db`) that executes queries in Rust via `sqlx`, keeping the GIL released for maximum throughput. | |
7 | + | |
8 | +## Quick Start: SQLite | |
9 | + | |
10 | +```python | |
11 | +from justapi import JustAPIApp | |
12 | + | |
13 | +app = JustAPIApp() | |
14 | + | |
15 | +app.set_database( | |
16 | + "sqlite:///app.db", | |
17 | + init_sql=""" | |
18 | + CREATE TABLE IF NOT EXISTS items ( | |
19 | + id INTEGER PRIMARY KEY AUTOINCREMENT, | |
20 | + name TEXT NOT NULL, | |
21 | + quantity INTEGER DEFAULT 0 | |
22 | + ) | |
23 | + """, | |
24 | +) | |
25 | + | |
26 | + | |
27 | +@app.get("/items") | |
28 | +def list_items(request): | |
29 | + return app.db.query("SELECT * FROM items ORDER BY id") | |
30 | + | |
31 | + | |
32 | +@app.post("/items") | |
33 | +def add_item(request): | |
34 | + data = request.json() | |
35 | + app.db.execute( | |
36 | + "INSERT INTO items (name, quantity) VALUES ($1, $2)", | |
37 | + [data["name"], data.get("quantity", 0)], | |
38 | + ) | |
39 | + return {"message": "Item created"} | |
40 | +``` | |
41 | + | |
42 | +## Supported Databases | |
43 | + | |
44 | +| Engine | URL Scheme | Driver | Use Case | | |
45 | +|---|---|---|---| | |
46 | +| **PostgreSQL** | `postgres://user:pass@host/db` | Rust `sqlx-postgres` | Production OLTP | | |
47 | +| **MySQL** | `mysql://user:pass@host/db` | Rust `sqlx-mysql` | Scalable relational | | |
48 | +| **SQLite** | `sqlite:///path/to/db.db` | Rust `sqlx-sqlite` | Embedded, dev, single-server | | |
49 | +| **DuckDB** | `duckdb:///path/to/db.duckdb` | Python `duckdb` | Analytical OLAP | | |
50 | +| **ClickHouse** | `clickhouse://host:9000/db` | Python `clickhouse-driver` | Column-store analytics | | |
51 | +| **MongoDB** | `mongodb://host:27017/db` | Python `pymongo` | Document store | | |
52 | +| **Redis** | `redis://host:6379/0` | Python `redis` | Caching, pub/sub | | |
53 | + | |
54 | +## PostgreSQL Example | |
55 | + | |
56 | +```python | |
57 | +app.set_database( | |
58 | + "postgres://postgres:password@localhost:5432/mydb", | |
59 | + init_sql=""" | |
60 | + CREATE TABLE IF NOT EXISTS users ( | |
61 | + id SERIAL PRIMARY KEY, | |
62 | + name TEXT NOT NULL, | |
63 | + email TEXT UNIQUE NOT NULL | |
64 | + ) | |
65 | + """, | |
66 | +) | |
67 | + | |
68 | + | |
69 | +@app.post("/users") | |
70 | +def create_user(request): | |
71 | + data = request.json() | |
72 | + result = app.db.query( | |
73 | + "INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id", | |
74 | + [data["name"], data["email"]], | |
75 | + ) | |
76 | + return {"user_id": result[0]["id"]} | |
77 | +``` | |
78 | + | |
79 | +## Query API | |
80 | + | |
81 | +The `app.db` object provides two methods: | |
82 | + | |
83 | +| Method | Description | Returns | | |
84 | +|---|---|---| | |
85 | +| `app.db.query(sql, params)` | Execute a SELECT query | `list[dict]` — rows as dicts | | |
86 | +| `app.db.execute(sql, params)` | Execute INSERT/UPDATE/DELETE | `int` — affected rows | | |
87 | + | |
88 | +## Connection Pool Configuration | |
89 | + | |
90 | +```python | |
91 | +app.set_database( | |
92 | + "postgres://localhost/mydb", | |
93 | + max_connections=20, # Pool size (default: 10) | |
94 | + request_acquire_timeout=3.0, # Seconds before 503 (default: 3.0) | |
95 | + init_sql="CREATE TABLE IF NOT EXISTS ...", | |
96 | +) | |
97 | +``` | |
98 | + | |
99 | +### SQLite-Specific Options | |
100 | + | |
101 | +```python | |
102 | +app.set_database( | |
103 | + "sqlite:///app.db", | |
104 | + wal=True, # Enable WAL mode for concurrent access | |
105 | + pragmas=["journal_mode=WAL", "synchronous=NORMAL"], | |
106 | +) | |
107 | +``` | |
108 | + | |
109 | +By default, JustAPI enables `busy_timeout=5000`, WAL journal mode, and `synchronous=NORMAL` for all SQLite connections to support concurrent reads and writes. | |
110 | + | |
111 | +## Pool Saturation Handling | |
112 | + | |
113 | +When the connection pool is exhausted, requests get **503 Service Unavailable** instead of waiting indefinitely: | |
114 | + | |
115 | +```python | |
116 | +# Default: request_acquire_timeout=3.0 seconds | |
117 | +# If no connection is available within 3s, returns 503 | |
118 | +app.set_database("sqlite:///app.db", request_acquire_timeout=1.0) | |
119 | +``` | |
120 | + | |
121 | +## Migration System | |
122 | + | |
123 | +JustAPI includes a simple migration runner: | |
124 | + | |
125 | +```bash | |
126 | +justapi db migrate # Apply pending migrations | |
127 | +justapi db rollback # Rollback last migration | |
128 | +justapi db list # List migration status | |
129 | +``` | |
130 | + | |
131 | +Migrations are SQL files in the `migrations/` directory: | |
132 | + | |
133 | +```sql | |
134 | +-- migrations/0002_add_email.sql | |
135 | +ALTER TABLE users ADD COLUMN email TEXT; | |
136 | +``` | |
137 | + | |
138 | +## Using Native Drivers (Python) | |
139 | + | |
140 | +For databases not supported by `sqlx`, use standard Python drivers: | |
141 | + | |
142 | +```python | |
143 | +import duckdb | |
144 | + | |
145 | + | |
146 | +app = JustAPIApp() | |
147 | +conn = duckdb.connect("analytics.duckdb") | |
148 | + | |
149 | + | |
150 | +@app.get("/analytics/sales") | |
151 | +def sales_summary(request): | |
152 | + result = conn.execute( | |
153 | + "SELECT region, SUM(amount) as total FROM sales GROUP BY region" | |
154 | + ).fetchall() | |
155 | + return {"summary": [{"region": r[0], "total": r[1]} for r in result]} | |
156 | +``` | |
157 | + | |
158 | +## How It Works Internally | |
159 | + | |
160 | +1. `app.set_database()` creates a Rust `sqlx` connection pool (`AnyPool`) | |
161 | +2. Queries are executed in Rust via `py.detach()` — zero GIL overhead | |
162 | +3. Results are returned as Python dicts via zero-copy serialization | |
163 | +4. Transactions are auto-managed: commit on 2xx, rollback on error | |
164 | +5. Pool saturation returns 503 with `Retry-After: 1` header | |
165 | + | |
166 | +## Next Steps | |
167 | + | |
168 | +- [Background Tasks](/tutorials/background-tasks/) — Async processing after DB writes | |
169 | +- [Dependency Injection](/tutorials/dependency-injection/) — DB session as a dependency | |
170 | +- [Performance Tuning](/advanced/performance-tuning/) — Optimize database queries | |
0 | 171 | |