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.
Files changed (103) hide show
  1. package/dist/cli/parseArgs.d.ts.map +1 -1
  2. package/dist/cli/parseArgs.js +5 -0
  3. package/dist/cli/parseArgs.js.map +1 -1
  4. package/dist/components/init/CompleteStep.d.ts.map +1 -1
  5. package/dist/components/init/CompleteStep.js +2 -2
  6. package/dist/components/init/CompleteStep.js.map +1 -1
  7. package/dist/components/init/TestCheckStep.d.ts +16 -0
  8. package/dist/components/init/TestCheckStep.d.ts.map +1 -0
  9. package/dist/components/init/TestCheckStep.js +120 -0
  10. package/dist/components/init/TestCheckStep.js.map +1 -0
  11. package/dist/components/init/index.d.ts +1 -0
  12. package/dist/components/init/index.d.ts.map +1 -1
  13. package/dist/components/init/index.js +1 -0
  14. package/dist/components/init/index.js.map +1 -1
  15. package/dist/components/init/types.d.ts +9 -0
  16. package/dist/components/init/types.d.ts.map +1 -1
  17. package/dist/components/screens/InitScreen.d.ts.map +1 -1
  18. package/dist/components/screens/InitScreen.js +69 -6
  19. package/dist/components/screens/InitScreen.js.map +1 -1
  20. package/dist/components/screens/StartScreen.d.ts.map +1 -1
  21. package/dist/components/screens/StartScreen.js +89 -3
  22. package/dist/components/screens/StartScreen.js.map +1 -1
  23. package/dist/services/ConfigService.d.ts +1 -0
  24. package/dist/services/ConfigService.d.ts.map +1 -1
  25. package/dist/services/ConfigService.js.map +1 -1
  26. package/dist/services/ProjectDetector.d.ts +28 -0
  27. package/dist/services/ProjectDetector.d.ts.map +1 -0
  28. package/dist/services/ProjectDetector.js +236 -0
  29. package/dist/services/ProjectDetector.js.map +1 -0
  30. package/dist/services/TestRunner.d.ts +46 -0
  31. package/dist/services/TestRunner.d.ts.map +1 -0
  32. package/dist/services/TestRunner.js +85 -0
  33. package/dist/services/TestRunner.js.map +1 -0
  34. package/dist/services/index.d.ts +2 -0
  35. package/dist/services/index.d.ts.map +1 -1
  36. package/dist/services/index.js +2 -0
  37. package/dist/services/index.js.map +1 -1
  38. package/dist/types/index.d.ts +1 -0
  39. package/dist/types/index.d.ts.map +1 -1
  40. package/dist/types/index.js.map +1 -1
  41. package/framework/agents/claude/.claude/agents/red64/spec-impl.md +131 -2
  42. package/framework/agents/claude/.claude/commands/red64/spec-impl.md +24 -0
  43. package/framework/agents/codex/.codex/agents/red64/spec-impl.md +131 -2
  44. package/framework/agents/codex/.codex/commands/red64/spec-impl.md +24 -0
  45. package/framework/stacks/generic/feedback.md +80 -0
  46. package/framework/stacks/nextjs/accessibility.md +437 -0
  47. package/framework/stacks/nextjs/api.md +431 -0
  48. package/framework/stacks/nextjs/coding-style.md +282 -0
  49. package/framework/stacks/nextjs/commenting.md +226 -0
  50. package/framework/stacks/nextjs/components.md +411 -0
  51. package/framework/stacks/nextjs/conventions.md +333 -0
  52. package/framework/stacks/nextjs/css.md +310 -0
  53. package/framework/stacks/nextjs/error-handling.md +442 -0
  54. package/framework/stacks/nextjs/feedback.md +124 -0
  55. package/framework/stacks/nextjs/migrations.md +332 -0
  56. package/framework/stacks/nextjs/models.md +362 -0
  57. package/framework/stacks/nextjs/queries.md +410 -0
  58. package/framework/stacks/nextjs/responsive.md +338 -0
  59. package/framework/stacks/nextjs/tech-stack.md +177 -0
  60. package/framework/stacks/nextjs/test-writing.md +475 -0
  61. package/framework/stacks/nextjs/validation.md +467 -0
  62. package/framework/stacks/python/api.md +468 -0
  63. package/framework/stacks/python/authentication.md +342 -0
  64. package/framework/stacks/python/code-quality.md +283 -0
  65. package/framework/stacks/python/code-refactoring.md +315 -0
  66. package/framework/stacks/python/coding-style.md +462 -0
  67. package/framework/stacks/python/conventions.md +399 -0
  68. package/framework/stacks/python/error-handling.md +512 -0
  69. package/framework/stacks/python/feedback.md +92 -0
  70. package/framework/stacks/python/implement-ai-llm.md +468 -0
  71. package/framework/stacks/python/migrations.md +388 -0
  72. package/framework/stacks/python/models.md +399 -0
  73. package/framework/stacks/python/python.md +232 -0
  74. package/framework/stacks/python/queries.md +451 -0
  75. package/framework/stacks/python/structure.md +245 -58
  76. package/framework/stacks/python/tech.md +92 -35
  77. package/framework/stacks/python/testing.md +380 -0
  78. package/framework/stacks/python/validation.md +471 -0
  79. package/framework/stacks/rails/authentication.md +176 -0
  80. package/framework/stacks/rails/code-quality.md +287 -0
  81. package/framework/stacks/rails/code-refactoring.md +299 -0
  82. package/framework/stacks/rails/feedback.md +130 -0
  83. package/framework/stacks/rails/implement-ai-llm-with-rubyllm.md +342 -0
  84. package/framework/stacks/rails/rails.md +301 -0
  85. package/framework/stacks/rails/rails8-best-practices.md +498 -0
  86. package/framework/stacks/rails/rails8-css.md +573 -0
  87. package/framework/stacks/rails/structure.md +140 -0
  88. package/framework/stacks/rails/tech.md +108 -0
  89. package/framework/stacks/react/code-quality.md +521 -0
  90. package/framework/stacks/react/components.md +625 -0
  91. package/framework/stacks/react/data-fetching.md +586 -0
  92. package/framework/stacks/react/feedback.md +110 -0
  93. package/framework/stacks/react/forms.md +694 -0
  94. package/framework/stacks/react/performance.md +640 -0
  95. package/framework/stacks/react/product.md +22 -9
  96. package/framework/stacks/react/state-management.md +472 -0
  97. package/framework/stacks/react/structure.md +351 -44
  98. package/framework/stacks/react/tech.md +219 -30
  99. package/framework/stacks/react/testing.md +690 -0
  100. package/package.json +1 -1
  101. package/framework/stacks/node/product.md +0 -27
  102. package/framework/stacks/node/structure.md +0 -82
  103. 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._