red64-cli 0.1.0 → 0.2.0
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.
- package/dist/cli/parseArgs.d.ts.map +1 -1
- package/dist/cli/parseArgs.js +5 -0
- package/dist/cli/parseArgs.js.map +1 -1
- package/dist/components/init/CompleteStep.d.ts.map +1 -1
- package/dist/components/init/CompleteStep.js +2 -2
- package/dist/components/init/CompleteStep.js.map +1 -1
- package/dist/components/init/TestCheckStep.d.ts +16 -0
- package/dist/components/init/TestCheckStep.d.ts.map +1 -0
- package/dist/components/init/TestCheckStep.js +120 -0
- package/dist/components/init/TestCheckStep.js.map +1 -0
- package/dist/components/init/index.d.ts +1 -0
- package/dist/components/init/index.d.ts.map +1 -1
- package/dist/components/init/index.js +1 -0
- package/dist/components/init/index.js.map +1 -1
- package/dist/components/init/types.d.ts +9 -0
- package/dist/components/init/types.d.ts.map +1 -1
- package/dist/components/screens/InitScreen.d.ts.map +1 -1
- package/dist/components/screens/InitScreen.js +69 -6
- package/dist/components/screens/InitScreen.js.map +1 -1
- package/dist/components/screens/StartScreen.d.ts.map +1 -1
- package/dist/components/screens/StartScreen.js +89 -3
- package/dist/components/screens/StartScreen.js.map +1 -1
- package/dist/services/ConfigService.d.ts +1 -0
- package/dist/services/ConfigService.d.ts.map +1 -1
- package/dist/services/ConfigService.js.map +1 -1
- package/dist/services/ProjectDetector.d.ts +28 -0
- package/dist/services/ProjectDetector.d.ts.map +1 -0
- package/dist/services/ProjectDetector.js +236 -0
- package/dist/services/ProjectDetector.js.map +1 -0
- package/dist/services/TestRunner.d.ts +46 -0
- package/dist/services/TestRunner.d.ts.map +1 -0
- package/dist/services/TestRunner.js +85 -0
- package/dist/services/TestRunner.js.map +1 -0
- package/dist/services/index.d.ts +2 -0
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +2 -0
- package/dist/services/index.js.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/framework/agents/claude/.claude/agents/red64/spec-impl.md +131 -2
- package/framework/agents/claude/.claude/commands/red64/spec-impl.md +24 -0
- package/framework/agents/codex/.codex/agents/red64/spec-impl.md +131 -2
- package/framework/agents/codex/.codex/commands/red64/spec-impl.md +24 -0
- package/framework/stacks/generic/feedback.md +80 -0
- package/framework/stacks/nextjs/accessibility.md +437 -0
- package/framework/stacks/nextjs/api.md +431 -0
- package/framework/stacks/nextjs/coding-style.md +282 -0
- package/framework/stacks/nextjs/commenting.md +226 -0
- package/framework/stacks/nextjs/components.md +411 -0
- package/framework/stacks/nextjs/conventions.md +333 -0
- package/framework/stacks/nextjs/css.md +310 -0
- package/framework/stacks/nextjs/error-handling.md +442 -0
- package/framework/stacks/nextjs/feedback.md +124 -0
- package/framework/stacks/nextjs/migrations.md +332 -0
- package/framework/stacks/nextjs/models.md +362 -0
- package/framework/stacks/nextjs/queries.md +410 -0
- package/framework/stacks/nextjs/responsive.md +338 -0
- package/framework/stacks/nextjs/tech-stack.md +177 -0
- package/framework/stacks/nextjs/test-writing.md +475 -0
- package/framework/stacks/nextjs/validation.md +467 -0
- package/framework/stacks/python/api.md +468 -0
- package/framework/stacks/python/authentication.md +342 -0
- package/framework/stacks/python/code-quality.md +283 -0
- package/framework/stacks/python/code-refactoring.md +315 -0
- package/framework/stacks/python/coding-style.md +462 -0
- package/framework/stacks/python/conventions.md +399 -0
- package/framework/stacks/python/error-handling.md +512 -0
- package/framework/stacks/python/feedback.md +92 -0
- package/framework/stacks/python/implement-ai-llm.md +468 -0
- package/framework/stacks/python/migrations.md +388 -0
- package/framework/stacks/python/models.md +399 -0
- package/framework/stacks/python/python.md +232 -0
- package/framework/stacks/python/queries.md +451 -0
- package/framework/stacks/python/structure.md +245 -58
- package/framework/stacks/python/tech.md +92 -35
- package/framework/stacks/python/testing.md +380 -0
- package/framework/stacks/python/validation.md +471 -0
- package/framework/stacks/rails/authentication.md +176 -0
- package/framework/stacks/rails/code-quality.md +287 -0
- package/framework/stacks/rails/code-refactoring.md +299 -0
- package/framework/stacks/rails/feedback.md +130 -0
- package/framework/stacks/rails/implement-ai-llm-with-rubyllm.md +342 -0
- package/framework/stacks/rails/rails.md +301 -0
- package/framework/stacks/rails/rails8-best-practices.md +498 -0
- package/framework/stacks/rails/rails8-css.md +573 -0
- package/framework/stacks/rails/structure.md +140 -0
- package/framework/stacks/rails/tech.md +108 -0
- package/framework/stacks/react/code-quality.md +521 -0
- package/framework/stacks/react/components.md +625 -0
- package/framework/stacks/react/data-fetching.md +586 -0
- package/framework/stacks/react/feedback.md +110 -0
- package/framework/stacks/react/forms.md +694 -0
- package/framework/stacks/react/performance.md +640 -0
- package/framework/stacks/react/product.md +22 -9
- package/framework/stacks/react/state-management.md +472 -0
- package/framework/stacks/react/structure.md +351 -44
- package/framework/stacks/react/tech.md +219 -30
- package/framework/stacks/react/testing.md +690 -0
- package/package.json +1 -1
- package/framework/stacks/node/product.md +0 -27
- package/framework/stacks/node/structure.md +0 -82
- package/framework/stacks/node/tech.md +0 -63
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
# SQLAlchemy Query Patterns
|
|
2
|
+
|
|
3
|
+
Best practices for safe, performant database queries with SQLAlchemy 2.0 and async support.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Philosophy
|
|
8
|
+
|
|
9
|
+
- **Parameterized always**: Never interpolate user input into SQL
|
|
10
|
+
- **Explicit loading**: Choose eager/lazy loading per query, not per model
|
|
11
|
+
- **Select what you need**: Avoid `SELECT *` when a subset suffices
|
|
12
|
+
- **Transactions are explicit**: Wrap related operations, commit intentionally
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## SQL Injection Prevention
|
|
17
|
+
|
|
18
|
+
### Parameterized Queries (Always)
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
# GOOD: SQLAlchemy handles parameterization
|
|
22
|
+
stmt = select(User).where(User.email == email)
|
|
23
|
+
|
|
24
|
+
# GOOD: Parameterized raw SQL
|
|
25
|
+
stmt = text("SELECT * FROM users WHERE email = :email")
|
|
26
|
+
result = await db.execute(stmt, {"email": email})
|
|
27
|
+
|
|
28
|
+
# GOOD: Filter with bound parameters
|
|
29
|
+
stmt = select(User).where(User.name.ilike(f"%{search_term}%"))
|
|
30
|
+
# SQLAlchemy binds search_term as a parameter internally
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
# BAD: String interpolation -- SQL INJECTION RISK
|
|
35
|
+
stmt = text(f"SELECT * FROM users WHERE email = '{email}'")
|
|
36
|
+
|
|
37
|
+
# BAD: f-string in raw SQL
|
|
38
|
+
query = f"DELETE FROM users WHERE id = {user_id}"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Rule**: If you see an f-string or `.format()` inside a SQL query, it is a bug.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## N+1 Query Prevention
|
|
46
|
+
|
|
47
|
+
### The Problem
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
# BAD: N+1 -- one query for posts, then one query PER post for author
|
|
51
|
+
posts = (await db.execute(select(Post))).scalars().all()
|
|
52
|
+
for post in posts:
|
|
53
|
+
print(post.author.name) # Each access triggers a lazy load query
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### selectinload (Collections)
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from sqlalchemy.orm import selectinload
|
|
60
|
+
|
|
61
|
+
# GOOD: Two queries total (posts + authors via SELECT IN)
|
|
62
|
+
stmt = select(Post).options(selectinload(Post.comments))
|
|
63
|
+
posts = (await db.execute(stmt)).scalars().all()
|
|
64
|
+
|
|
65
|
+
# Nested loading
|
|
66
|
+
stmt = (
|
|
67
|
+
select(User)
|
|
68
|
+
.options(
|
|
69
|
+
selectinload(User.posts)
|
|
70
|
+
.selectinload(Post.comments)
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### joinedload (Single Relations)
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from sqlalchemy.orm import joinedload
|
|
79
|
+
|
|
80
|
+
# GOOD: Single JOIN query for to-one relationships
|
|
81
|
+
stmt = select(Post).options(joinedload(Post.author))
|
|
82
|
+
posts = (await db.execute(stmt)).unique().scalars().all()
|
|
83
|
+
# Note: .unique() is required when using joinedload with collections
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### When to Use Each
|
|
87
|
+
|
|
88
|
+
| Strategy | Use Case | Queries |
|
|
89
|
+
|---|---|---|
|
|
90
|
+
| `selectinload` | Collections (one-to-many, many-to-many) | 2 (base + IN) |
|
|
91
|
+
| `joinedload` | Single relations (many-to-one) | 1 (JOIN) |
|
|
92
|
+
| `subqueryload` | Large collections with complex base query | 2 (base + subquery) |
|
|
93
|
+
| `raiseload` | Prevent accidental lazy loading | Raises error |
|
|
94
|
+
|
|
95
|
+
### Prevent Accidental Lazy Loading
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from sqlalchemy.orm import raiseload
|
|
99
|
+
|
|
100
|
+
# Raise error if any unloaded relationship is accessed
|
|
101
|
+
stmt = select(Post).options(
|
|
102
|
+
joinedload(Post.author),
|
|
103
|
+
raiseload("*"), # All other relationships will raise
|
|
104
|
+
)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Select Only Needed Columns
|
|
110
|
+
|
|
111
|
+
### Partial Selects
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
# Full model (all columns)
|
|
115
|
+
stmt = select(User)
|
|
116
|
+
|
|
117
|
+
# Only specific columns (returns Row objects, not models)
|
|
118
|
+
stmt = select(User.id, User.email, User.name)
|
|
119
|
+
rows = (await db.execute(stmt)).all()
|
|
120
|
+
for row in rows:
|
|
121
|
+
print(row.id, row.email)
|
|
122
|
+
|
|
123
|
+
# Hybrid: load model but defer heavy columns
|
|
124
|
+
from sqlalchemy.orm import defer
|
|
125
|
+
|
|
126
|
+
stmt = select(User).options(defer(User.bio), defer(User.hashed_password))
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### When to Use Partial Selects
|
|
130
|
+
|
|
131
|
+
| Scenario | Approach |
|
|
132
|
+
|---|---|
|
|
133
|
+
| List/index pages | Select only displayed columns |
|
|
134
|
+
| Detail pages | Full model load |
|
|
135
|
+
| Autocomplete/search | `select(Model.id, Model.name)` |
|
|
136
|
+
| Counting | `select(func.count()).select_from(Model)` |
|
|
137
|
+
| Existence check | `select(Model.id).where(...).limit(1)` |
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Proper Indexing
|
|
142
|
+
|
|
143
|
+
### Index Strategy
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from sqlalchemy import Index
|
|
147
|
+
|
|
148
|
+
class Post(Base):
|
|
149
|
+
__tablename__ = "posts"
|
|
150
|
+
__table_args__ = (
|
|
151
|
+
# Composite index for common query patterns
|
|
152
|
+
Index("ix_posts_user_status", "user_id", "status"),
|
|
153
|
+
# Partial index (PostgreSQL)
|
|
154
|
+
Index(
|
|
155
|
+
"ix_posts_published",
|
|
156
|
+
"published_at",
|
|
157
|
+
postgresql_where=text("status = 'published'"),
|
|
158
|
+
),
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
162
|
+
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), index=True)
|
|
163
|
+
status: Mapped[str] = mapped_column(String(20), index=True)
|
|
164
|
+
published_at: Mapped[datetime | None] = mapped_column(index=True)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Index Rules
|
|
168
|
+
|
|
169
|
+
- Index every foreign key column
|
|
170
|
+
- Index columns used in `WHERE`, `ORDER BY`, and `JOIN`
|
|
171
|
+
- Composite indexes: put high-cardinality columns first
|
|
172
|
+
- Partial indexes for filtered queries (e.g., only active records)
|
|
173
|
+
- Do not over-index: each index slows writes
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Transactions and Isolation
|
|
178
|
+
|
|
179
|
+
### Explicit Transactions
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
# Default: auto-begin, explicit commit
|
|
183
|
+
async with db.begin():
|
|
184
|
+
user = User(email="a@b.com", name="Test", hashed_password="hash")
|
|
185
|
+
db.add(user)
|
|
186
|
+
# Commits automatically at end of block
|
|
187
|
+
# Rolls back on exception
|
|
188
|
+
|
|
189
|
+
# Manual commit pattern
|
|
190
|
+
db.add(user)
|
|
191
|
+
await db.flush() # Generate ID without committing
|
|
192
|
+
# ... use user.id for related records ...
|
|
193
|
+
await db.commit()
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Isolation Levels
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
from sqlalchemy.ext.asyncio import create_async_engine
|
|
200
|
+
|
|
201
|
+
# Set default isolation level
|
|
202
|
+
engine = create_async_engine(
|
|
203
|
+
settings.database_url,
|
|
204
|
+
isolation_level="READ COMMITTED", # PostgreSQL default
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# Per-transaction isolation (for critical operations)
|
|
208
|
+
async with db.begin():
|
|
209
|
+
await db.connection(execution_options={"isolation_level": "SERIALIZABLE"})
|
|
210
|
+
# Critical operation here
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Savepoints (Nested Transactions)
|
|
214
|
+
|
|
215
|
+
```python
|
|
216
|
+
async with db.begin():
|
|
217
|
+
db.add(order)
|
|
218
|
+
await db.flush()
|
|
219
|
+
|
|
220
|
+
try:
|
|
221
|
+
async with db.begin_nested(): # Savepoint
|
|
222
|
+
await charge_payment(order)
|
|
223
|
+
except PaymentError:
|
|
224
|
+
# Savepoint rolled back, outer transaction continues
|
|
225
|
+
order.status = "payment_failed"
|
|
226
|
+
|
|
227
|
+
await db.commit()
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## Query Timeouts
|
|
233
|
+
|
|
234
|
+
### Statement Timeout
|
|
235
|
+
|
|
236
|
+
```python
|
|
237
|
+
# Per-engine default (PostgreSQL)
|
|
238
|
+
engine = create_async_engine(
|
|
239
|
+
settings.database_url,
|
|
240
|
+
connect_args={"options": "-c statement_timeout=30000"}, # 30 seconds
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
# Per-query timeout
|
|
244
|
+
from sqlalchemy import text
|
|
245
|
+
|
|
246
|
+
await db.execute(text("SET LOCAL statement_timeout = '5s'"))
|
|
247
|
+
result = await db.execute(expensive_query)
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Application-Level Timeout
|
|
251
|
+
|
|
252
|
+
```python
|
|
253
|
+
import asyncio
|
|
254
|
+
|
|
255
|
+
async def get_report_data(db: AsyncSession) -> list[Row]:
|
|
256
|
+
try:
|
|
257
|
+
result = await asyncio.wait_for(
|
|
258
|
+
db.execute(complex_report_query),
|
|
259
|
+
timeout=10.0,
|
|
260
|
+
)
|
|
261
|
+
return result.all()
|
|
262
|
+
except asyncio.TimeoutError:
|
|
263
|
+
raise QueryTimeoutError("Report query exceeded 10s timeout")
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## Bulk Operations
|
|
269
|
+
|
|
270
|
+
### Bulk Insert
|
|
271
|
+
|
|
272
|
+
```python
|
|
273
|
+
# GOOD: Bulk insert with executemany
|
|
274
|
+
users = [
|
|
275
|
+
User(email=f"user{i}@example.com", name=f"User {i}", hashed_password="hash")
|
|
276
|
+
for i in range(1000)
|
|
277
|
+
]
|
|
278
|
+
db.add_all(users)
|
|
279
|
+
await db.commit()
|
|
280
|
+
|
|
281
|
+
# BETTER: Core-level insert for large volumes (bypasses ORM overhead)
|
|
282
|
+
from sqlalchemy import insert
|
|
283
|
+
|
|
284
|
+
await db.execute(
|
|
285
|
+
insert(User),
|
|
286
|
+
[
|
|
287
|
+
{"email": f"user{i}@example.com", "name": f"User {i}", "hashed_password": "hash"}
|
|
288
|
+
for i in range(10000)
|
|
289
|
+
],
|
|
290
|
+
)
|
|
291
|
+
await db.commit()
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Bulk Update
|
|
295
|
+
|
|
296
|
+
```python
|
|
297
|
+
from sqlalchemy import update
|
|
298
|
+
|
|
299
|
+
# Update matching rows in a single statement
|
|
300
|
+
stmt = (
|
|
301
|
+
update(User)
|
|
302
|
+
.where(User.is_active == False)
|
|
303
|
+
.where(User.last_login < cutoff_date)
|
|
304
|
+
.values(status="inactive")
|
|
305
|
+
)
|
|
306
|
+
result = await db.execute(stmt)
|
|
307
|
+
print(f"Updated {result.rowcount} rows")
|
|
308
|
+
await db.commit()
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### Bulk Delete
|
|
312
|
+
|
|
313
|
+
```python
|
|
314
|
+
from sqlalchemy import delete
|
|
315
|
+
|
|
316
|
+
stmt = delete(Session).where(Session.expires_at < datetime.now(timezone.utc))
|
|
317
|
+
result = await db.execute(stmt)
|
|
318
|
+
await db.commit()
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
## Raw SQL (When Appropriate)
|
|
324
|
+
|
|
325
|
+
### When to Use Raw SQL
|
|
326
|
+
|
|
327
|
+
- Complex reporting queries with CTEs, window functions
|
|
328
|
+
- Database-specific features (PostgreSQL `LATERAL`, `DISTINCT ON`)
|
|
329
|
+
- Performance-critical paths where ORM overhead matters
|
|
330
|
+
- One-off data migrations
|
|
331
|
+
|
|
332
|
+
```python
|
|
333
|
+
from sqlalchemy import text
|
|
334
|
+
|
|
335
|
+
# Named parameters (always)
|
|
336
|
+
stmt = text("""
|
|
337
|
+
WITH active_users AS (
|
|
338
|
+
SELECT id, name, created_at,
|
|
339
|
+
ROW_NUMBER() OVER (ORDER BY created_at DESC) as rn
|
|
340
|
+
FROM users
|
|
341
|
+
WHERE is_active = :is_active
|
|
342
|
+
)
|
|
343
|
+
SELECT * FROM active_users WHERE rn <= :limit
|
|
344
|
+
""")
|
|
345
|
+
|
|
346
|
+
result = await db.execute(stmt, {"is_active": True, "limit": 10})
|
|
347
|
+
rows = result.all()
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
## Async Queries with asyncpg
|
|
353
|
+
|
|
354
|
+
### Engine Setup
|
|
355
|
+
|
|
356
|
+
```python
|
|
357
|
+
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
|
|
358
|
+
|
|
359
|
+
engine = create_async_engine(
|
|
360
|
+
"postgresql+asyncpg://user:pass@localhost/db",
|
|
361
|
+
pool_size=20,
|
|
362
|
+
max_overflow=10,
|
|
363
|
+
pool_timeout=30,
|
|
364
|
+
pool_recycle=3600,
|
|
365
|
+
echo=settings.debug, # Log SQL in development
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
async_session = async_sessionmaker(engine, expire_on_commit=False)
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### Session Dependency (FastAPI)
|
|
372
|
+
|
|
373
|
+
```python
|
|
374
|
+
from collections.abc import AsyncIterator
|
|
375
|
+
|
|
376
|
+
async def get_db() -> AsyncIterator[AsyncSession]:
|
|
377
|
+
async with async_session() as session:
|
|
378
|
+
try:
|
|
379
|
+
yield session
|
|
380
|
+
finally:
|
|
381
|
+
await session.close()
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
### Connection Pool Monitoring
|
|
385
|
+
|
|
386
|
+
```python
|
|
387
|
+
from sqlalchemy import event
|
|
388
|
+
|
|
389
|
+
@event.listens_for(engine.sync_engine, "checkout")
|
|
390
|
+
def log_checkout(dbapi_conn, connection_record, connection_proxy):
|
|
391
|
+
logger.debug("Connection checked out from pool", pool_size=engine.pool.size())
|
|
392
|
+
|
|
393
|
+
@event.listens_for(engine.sync_engine, "checkin")
|
|
394
|
+
def log_checkin(dbapi_conn, connection_record):
|
|
395
|
+
logger.debug("Connection returned to pool")
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
## Pagination
|
|
401
|
+
|
|
402
|
+
### Cursor-Based (Preferred)
|
|
403
|
+
|
|
404
|
+
```python
|
|
405
|
+
async def get_posts_cursor(
|
|
406
|
+
db: AsyncSession,
|
|
407
|
+
after_id: int | None = None,
|
|
408
|
+
limit: int = 20,
|
|
409
|
+
) -> list[Post]:
|
|
410
|
+
stmt = select(Post).order_by(Post.id.desc()).limit(limit + 1)
|
|
411
|
+
if after_id:
|
|
412
|
+
stmt = stmt.where(Post.id < after_id)
|
|
413
|
+
|
|
414
|
+
results = (await db.execute(stmt)).scalars().all()
|
|
415
|
+
has_next = len(results) > limit
|
|
416
|
+
return results[:limit], has_next
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
### Offset-Based (Simple Cases)
|
|
420
|
+
|
|
421
|
+
```python
|
|
422
|
+
async def get_posts_page(
|
|
423
|
+
db: AsyncSession,
|
|
424
|
+
page: int = 1,
|
|
425
|
+
per_page: int = 20,
|
|
426
|
+
) -> tuple[list[Post], int]:
|
|
427
|
+
count_stmt = select(func.count()).select_from(Post)
|
|
428
|
+
total = (await db.execute(count_stmt)).scalar_one()
|
|
429
|
+
|
|
430
|
+
stmt = select(Post).offset((page - 1) * per_page).limit(per_page)
|
|
431
|
+
items = (await db.execute(stmt)).scalars().all()
|
|
432
|
+
return items, total
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
---
|
|
436
|
+
|
|
437
|
+
## Anti-Patterns
|
|
438
|
+
|
|
439
|
+
| Anti-Pattern | Problem | Correct Approach |
|
|
440
|
+
|---|---|---|
|
|
441
|
+
| f-strings in SQL | SQL injection | Use parameterized queries |
|
|
442
|
+
| No eager loading | N+1 queries | Use `selectinload`/`joinedload` |
|
|
443
|
+
| `SELECT *` for lists | Wasted bandwidth | Select needed columns |
|
|
444
|
+
| No indexes on FK columns | Slow joins | Always index foreign keys |
|
|
445
|
+
| Implicit transactions | Unexpected behavior | Explicit `begin()`/`commit()` |
|
|
446
|
+
| No query timeout | Runaway queries | Set statement timeout |
|
|
447
|
+
| ORM for bulk operations | Slow inserts/updates | Use core `insert()`/`update()` |
|
|
448
|
+
|
|
449
|
+
---
|
|
450
|
+
|
|
451
|
+
_Queries are the most performance-sensitive part of the application. Profile before optimizing, but prevent N+1 and injection from the start._
|