codeforge-dev 1.4.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 (131) hide show
  1. package/.devcontainer/.env +22 -0
  2. package/.devcontainer/CHANGELOG.md +197 -0
  3. package/.devcontainer/CLAUDE.md +117 -0
  4. package/.devcontainer/README.md +222 -0
  5. package/.devcontainer/config/main-system-prompt.md +502 -0
  6. package/.devcontainer/config/settings.json +47 -0
  7. package/.devcontainer/devcontainer.json +94 -0
  8. package/.devcontainer/features/README.md +113 -0
  9. package/.devcontainer/features/agent-browser/README.md +65 -0
  10. package/.devcontainer/features/agent-browser/devcontainer-feature.json +23 -0
  11. package/.devcontainer/features/agent-browser/install.sh +79 -0
  12. package/.devcontainer/features/ast-grep/README.md +24 -0
  13. package/.devcontainer/features/ast-grep/devcontainer-feature.json +24 -0
  14. package/.devcontainer/features/ast-grep/install.sh +51 -0
  15. package/.devcontainer/features/ccstatusline/README.md +296 -0
  16. package/.devcontainer/features/ccstatusline/devcontainer-feature.json +19 -0
  17. package/.devcontainer/features/ccstatusline/install.sh +290 -0
  18. package/.devcontainer/features/ccusage/README.md +205 -0
  19. package/.devcontainer/features/ccusage/devcontainer-feature.json +38 -0
  20. package/.devcontainer/features/ccusage/install.sh +132 -0
  21. package/.devcontainer/features/claude-code/README.md +498 -0
  22. package/.devcontainer/features/claude-code/config/settings.json +36 -0
  23. package/.devcontainer/features/claude-code/config/system-prompt.md +118 -0
  24. package/.devcontainer/features/claude-code/config/world-building-sp.md +1432 -0
  25. package/.devcontainer/features/claude-code/devcontainer-feature.json +42 -0
  26. package/.devcontainer/features/claude-code/install.sh +466 -0
  27. package/.devcontainer/features/claude-monitor/README.md +74 -0
  28. package/.devcontainer/features/claude-monitor/devcontainer-feature.json +38 -0
  29. package/.devcontainer/features/claude-monitor/install.sh +99 -0
  30. package/.devcontainer/features/lsp-servers/README.md +85 -0
  31. package/.devcontainer/features/lsp-servers/devcontainer-feature.json +40 -0
  32. package/.devcontainer/features/lsp-servers/install.sh +116 -0
  33. package/.devcontainer/features/mcp-qdrant/CHANGES.md +399 -0
  34. package/.devcontainer/features/mcp-qdrant/README.md +474 -0
  35. package/.devcontainer/features/mcp-qdrant/devcontainer-feature.json +57 -0
  36. package/.devcontainer/features/mcp-qdrant/install.sh +295 -0
  37. package/.devcontainer/features/mcp-qdrant/poststart-hook.sh +129 -0
  38. package/.devcontainer/features/mcp-reasoner/README.md +177 -0
  39. package/.devcontainer/features/mcp-reasoner/devcontainer-feature.json +20 -0
  40. package/.devcontainer/features/mcp-reasoner/install.sh +177 -0
  41. package/.devcontainer/features/mcp-reasoner/poststart-hook.sh +67 -0
  42. package/.devcontainer/features/notify-hook/README.md +86 -0
  43. package/.devcontainer/features/notify-hook/devcontainer-feature.json +23 -0
  44. package/.devcontainer/features/notify-hook/install.sh +38 -0
  45. package/.devcontainer/features/splitrail/README.md +140 -0
  46. package/.devcontainer/features/splitrail/devcontainer-feature.json +34 -0
  47. package/.devcontainer/features/splitrail/install.sh +129 -0
  48. package/.devcontainer/features/tree-sitter/README.md +138 -0
  49. package/.devcontainer/features/tree-sitter/devcontainer-feature.json +52 -0
  50. package/.devcontainer/features/tree-sitter/install.sh +173 -0
  51. package/.devcontainer/plugins/devs-marketplace/.claude-plugin/marketplace.json +106 -0
  52. package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/.claude-plugin/plugin.json +7 -0
  53. package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/hooks/hooks.json +17 -0
  54. package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/scripts/format-file.py +101 -0
  55. package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/.claude-plugin/plugin.json +7 -0
  56. package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/hooks/hooks.json +17 -0
  57. package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/scripts/lint-file.py +137 -0
  58. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/.claude-plugin/plugin.json +8 -0
  59. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/claude-code-headless/SKILL.md +387 -0
  60. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/claude-code-headless/references/cli-flags-and-output.md +312 -0
  61. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/claude-code-headless/references/sdk-and-mcp.md +569 -0
  62. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker/SKILL.md +309 -0
  63. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker/references/compose-services.md +438 -0
  64. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker/references/dockerfile-patterns.md +340 -0
  65. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker-py/SKILL.md +412 -0
  66. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker-py/references/container-lifecycle.md +388 -0
  67. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker-py/references/resources-and-security.md +444 -0
  68. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/fastapi/SKILL.md +344 -0
  69. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/fastapi/references/middleware-and-lifespan.md +254 -0
  70. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/fastapi/references/pydantic-models.md +245 -0
  71. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/fastapi/references/routing-and-dependencies.md +255 -0
  72. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/fastapi/references/sse-and-streaming.md +318 -0
  73. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/pydantic-ai/SKILL.md +345 -0
  74. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/pydantic-ai/references/agents-and-tools.md +271 -0
  75. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/pydantic-ai/references/models-and-streaming.md +422 -0
  76. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/skill-building/SKILL.md +220 -0
  77. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/skill-building/references/cross-vendor-principles.md +139 -0
  78. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/skill-building/references/patterns-and-antipatterns.md +376 -0
  79. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/skill-building/references/skill-authoring-patterns.md +356 -0
  80. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/sqlite/SKILL.md +329 -0
  81. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/sqlite/references/advanced-queries.md +314 -0
  82. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/sqlite/references/javascript-patterns.md +323 -0
  83. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/sqlite/references/python-patterns.md +354 -0
  84. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/sqlite/references/schema-and-pragmas.md +326 -0
  85. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/SKILL.md +356 -0
  86. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/ai-sdk-svelte.md +128 -0
  87. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/component-patterns.md +332 -0
  88. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/layercake.md +203 -0
  89. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/migration-guide.md +350 -0
  90. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/runes-and-reactivity.md +328 -0
  91. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/spa-and-routing.md +262 -0
  92. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/svelte-dnd-action.md +181 -0
  93. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/testing/SKILL.md +414 -0
  94. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/testing/references/fastapi-testing.md +411 -0
  95. package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/testing/references/svelte-testing.md +538 -0
  96. package/.devcontainer/plugins/devs-marketplace/plugins/codeforge-lsp/.claude-plugin/plugin.json +7 -0
  97. package/.devcontainer/plugins/devs-marketplace/plugins/dangerous-command-blocker/.claude-plugin/plugin.json +7 -0
  98. package/.devcontainer/plugins/devs-marketplace/plugins/dangerous-command-blocker/hooks/hooks.json +17 -0
  99. package/.devcontainer/plugins/devs-marketplace/plugins/dangerous-command-blocker/scripts/block-dangerous.py +110 -0
  100. package/.devcontainer/plugins/devs-marketplace/plugins/notify-hook/.claude-plugin/plugin.json +7 -0
  101. package/.devcontainer/plugins/devs-marketplace/plugins/notify-hook/hooks/hooks.json +17 -0
  102. package/.devcontainer/plugins/devs-marketplace/plugins/planning-reminder/.claude-plugin/plugin.json +7 -0
  103. package/.devcontainer/plugins/devs-marketplace/plugins/planning-reminder/hooks/hooks.json +17 -0
  104. package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/.claude-plugin/plugin.json +7 -0
  105. package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/hooks/hooks.json +17 -0
  106. package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/scripts/guard-protected.py +108 -0
  107. package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/commands/ticket/357/200/272create-pr.md +337 -0
  108. package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/commands/ticket/357/200/272new.md +166 -0
  109. package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/commands/ticket/357/200/272review-commit.md +290 -0
  110. package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/commands/ticket/357/200/272work.md +257 -0
  111. package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/plugin.json +8 -0
  112. package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/system-prompt.md +184 -0
  113. package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/.claude-plugin/plugin.json +6 -0
  114. package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/config/planning-instructions.md +14 -0
  115. package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/functional-conjuring-map.md +989 -0
  116. package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/hooks/hooks.json +33 -0
  117. package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/__pycache__/post-enhance-task.cpython-314.pyc +0 -0
  118. package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/enhance-planning.py +71 -0
  119. package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/enhancers/enhance-plan.sh +68 -0
  120. package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/enhancers/enhance-task.sh +120 -0
  121. package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/post-enhance-plan.py +133 -0
  122. package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/post-enhance-task.py +253 -0
  123. package/.devcontainer/scripts/setup-aliases.sh +80 -0
  124. package/.devcontainer/scripts/setup-config.sh +28 -0
  125. package/.devcontainer/scripts/setup-irie-claude.sh +32 -0
  126. package/.devcontainer/scripts/setup-plugins.sh +80 -0
  127. package/.devcontainer/scripts/setup.sh +58 -0
  128. package/LICENSE.txt +674 -0
  129. package/README.md +267 -0
  130. package/package.json +44 -0
  131. package/setup.js +83 -0
@@ -0,0 +1,354 @@
1
+ # Python SQLite Patterns -- Deep Dive
2
+
3
+ ## 1. Connection Management for Multi-Threaded Apps
4
+
5
+ SQLite connections are not thread-safe by default. For multi-threaded applications, create one connection per thread or use a connection pool:
6
+
7
+ ```python
8
+ import sqlite3
9
+ import threading
10
+ from contextlib import contextmanager
11
+
12
+ class ConnectionPool:
13
+ def __init__(self, db_path: str, max_connections: int = 5):
14
+ self.db_path = db_path
15
+ self.semaphore = threading.Semaphore(max_connections)
16
+ self.local = threading.local()
17
+
18
+ @contextmanager
19
+ def get_connection(self):
20
+ self.semaphore.acquire()
21
+ try:
22
+ if not hasattr(self.local, "conn"):
23
+ self.local.conn = self._create_connection()
24
+ yield self.local.conn
25
+ finally:
26
+ self.semaphore.release()
27
+
28
+ def _create_connection(self) -> sqlite3.Connection:
29
+ conn = sqlite3.connect(self.db_path)
30
+ conn.row_factory = sqlite3.Row
31
+ conn.execute("PRAGMA journal_mode=WAL")
32
+ conn.execute("PRAGMA foreign_keys=ON")
33
+ conn.execute("PRAGMA busy_timeout=5000")
34
+ return conn
35
+ ```
36
+
37
+ Thread-local storage ensures each thread reuses its own connection. The semaphore limits total concurrent connections, preventing resource exhaustion.
38
+
39
+ For `check_same_thread=False` (sharing a connection across threads), external locking is required:
40
+
41
+ ```python
42
+ conn = sqlite3.connect("app.db", check_same_thread=False)
43
+ lock = threading.Lock()
44
+
45
+ def execute_safely(sql, params=()):
46
+ with lock:
47
+ return conn.execute(sql, params).fetchall()
48
+ ```
49
+
50
+ This pattern is simpler but serializes all database access. Prefer per-thread connections for concurrent workloads.
51
+
52
+ ---
53
+
54
+ ## 2. Custom Aggregates and Collations
55
+
56
+ ### Custom Aggregate Functions
57
+
58
+ Register Python functions as SQL aggregate functions:
59
+
60
+ ```python
61
+ class MedianAggregate:
62
+ def __init__(self):
63
+ self.values = []
64
+
65
+ def step(self, value):
66
+ if value is not None:
67
+ self.values.append(value)
68
+
69
+ def finalize(self):
70
+ if not self.values:
71
+ return None
72
+ self.values.sort()
73
+ n = len(self.values)
74
+ mid = n // 2
75
+ if n % 2 == 0:
76
+ return (self.values[mid - 1] + self.values[mid]) / 2
77
+ return self.values[mid]
78
+
79
+ conn.create_aggregate("median", 1, MedianAggregate)
80
+ # SELECT median(price) FROM products;
81
+ ```
82
+
83
+ ### Custom Collations
84
+
85
+ Define custom sorting logic for text comparisons:
86
+
87
+ ```python
88
+ import unicodedata
89
+
90
+ def collate_normalized(a: str, b: str) -> int:
91
+ na = unicodedata.normalize("NFKD", a).casefold()
92
+ nb = unicodedata.normalize("NFKD", b).casefold()
93
+ return (na > nb) - (na < nb)
94
+
95
+ conn.create_collation("NORMALIZED", collate_normalized)
96
+ # SELECT * FROM users ORDER BY name COLLATE NORMALIZED;
97
+ ```
98
+
99
+ ### Scalar Functions
100
+
101
+ ```python
102
+ import json
103
+
104
+ def json_array_length(value):
105
+ if value is None:
106
+ return 0
107
+ return len(json.loads(value))
108
+
109
+ conn.create_function("json_arr_len", 1, json_array_length)
110
+ # SELECT json_arr_len(tags) FROM posts;
111
+ ```
112
+
113
+ ---
114
+
115
+ ## 3. Backup API
116
+
117
+ Copy a live database without locking writes:
118
+
119
+ ```python
120
+ import sqlite3
121
+
122
+ def backup_database(source_path: str, backup_path: str):
123
+ source = sqlite3.connect(source_path)
124
+ dest = sqlite3.connect(backup_path)
125
+ with dest:
126
+ source.backup(dest, pages=100, progress=backup_progress)
127
+ dest.close()
128
+ source.close()
129
+
130
+ def backup_progress(status, remaining, total):
131
+ print(f"Backup: {total - remaining}/{total} pages copied")
132
+ ```
133
+
134
+ The `pages` parameter controls how many pages are copied per step. Between steps, other connections can write. This makes `backup()` suitable for hot backups of production databases.
135
+
136
+ ### In-Memory to Disk (and Vice Versa)
137
+
138
+ ```python
139
+ # Load disk database into memory for fast operations
140
+ disk_db = sqlite3.connect("app.db")
141
+ mem_db = sqlite3.connect(":memory:")
142
+ disk_db.backup(mem_db)
143
+
144
+ # Save in-memory database to disk
145
+ mem_db.backup(disk_db)
146
+ ```
147
+
148
+ ---
149
+
150
+ ## 4. Datetime Adapters
151
+
152
+ SQLite has no native datetime type. Store timestamps as ISO 8601 text and register adapters for automatic conversion:
153
+
154
+ ```python
155
+ import sqlite3
156
+ from datetime import datetime, timezone
157
+
158
+ def adapt_datetime(dt: datetime) -> str:
159
+ return dt.isoformat()
160
+
161
+ def convert_datetime(value: bytes) -> datetime:
162
+ return datetime.fromisoformat(value.decode())
163
+
164
+ sqlite3.register_adapter(datetime, adapt_datetime)
165
+ sqlite3.register_converter("TIMESTAMP", convert_datetime)
166
+
167
+ conn = sqlite3.connect("app.db", detect_types=sqlite3.PARSE_DECLTYPES)
168
+ ```
169
+
170
+ With `PARSE_DECLTYPES`, a column declared as `TIMESTAMP` automatically uses the registered converter. This keeps datetime handling transparent to application code.
171
+
172
+ ---
173
+
174
+ ## 5. Blob I/O
175
+
176
+ For large binary data, use incremental blob I/O instead of loading the entire value into memory:
177
+
178
+ ```python
179
+ import sqlite3
180
+
181
+ # Write a blob
182
+ with open("image.png", "rb") as f:
183
+ data = f.read()
184
+ conn.execute("INSERT INTO files (name, data) VALUES (?, ?)", ("image.png", data))
185
+
186
+ # Read a blob incrementally
187
+ row = conn.execute("SELECT rowid, length(data) FROM files WHERE name = ?",
188
+ ("image.png",)).fetchone()
189
+ rowid, size = row
190
+
191
+ blob = conn.blobopen("main", "files", "data", rowid, readonly=True)
192
+ chunk_size = 65536
193
+ with open("output.png", "wb") as f:
194
+ while True:
195
+ chunk = blob.read(chunk_size)
196
+ if not chunk:
197
+ break
198
+ f.write(chunk)
199
+ blob.close()
200
+ ```
201
+
202
+ Incremental blob I/O avoids loading multi-megabyte files into Python memory. The `blobopen()` method returns a file-like object that reads directly from the database page cache.
203
+
204
+ ---
205
+
206
+ ## 6. aiosqlite + FastAPI Integration
207
+
208
+ ### Dependency Pattern
209
+
210
+ ```python
211
+ import aiosqlite
212
+ from contextlib import asynccontextmanager
213
+ from fastapi import FastAPI, Depends, Request
214
+
215
+ @asynccontextmanager
216
+ async def lifespan(app: FastAPI):
217
+ app.state.db = await aiosqlite.connect("app.db")
218
+ app.state.db.row_factory = aiosqlite.Row
219
+ await app.state.db.execute("PRAGMA journal_mode=WAL")
220
+ await app.state.db.execute("PRAGMA foreign_keys=ON")
221
+ yield
222
+ await app.state.db.close()
223
+
224
+ app = FastAPI(lifespan=lifespan)
225
+
226
+ async def get_db(request: Request) -> aiosqlite.Connection:
227
+ return request.app.state.db
228
+
229
+ @app.get("/users")
230
+ async def list_users(db: aiosqlite.Connection = Depends(get_db)):
231
+ async with db.execute("SELECT * FROM users") as cursor:
232
+ rows = await cursor.fetchall()
233
+ return [dict(row) for row in rows]
234
+ ```
235
+
236
+ ### Transaction Pattern
237
+
238
+ ```python
239
+ async def create_user_with_profile(db: aiosqlite.Connection, user_data: dict):
240
+ try:
241
+ await db.execute("BEGIN")
242
+ cursor = await db.execute(
243
+ "INSERT INTO users (email, name) VALUES (?, ?) RETURNING id",
244
+ (user_data["email"], user_data["name"]),
245
+ )
246
+ row = await cursor.fetchone()
247
+ user_id = row[0]
248
+ await db.execute(
249
+ "INSERT INTO profiles (user_id, bio) VALUES (?, ?)",
250
+ (user_id, user_data.get("bio", "")),
251
+ )
252
+ await db.commit()
253
+ return user_id
254
+ except Exception:
255
+ await db.rollback()
256
+ raise
257
+ ```
258
+
259
+ ---
260
+
261
+ ## 7. Migration Patterns
262
+
263
+ ### Simple Version Tracking
264
+
265
+ ```python
266
+ import sqlite3
267
+
268
+ MIGRATIONS = [
269
+ # Version 1
270
+ """
271
+ CREATE TABLE users (
272
+ id INTEGER PRIMARY KEY,
273
+ email TEXT NOT NULL UNIQUE,
274
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
275
+ ) STRICT;
276
+ """,
277
+ # Version 2
278
+ """
279
+ ALTER TABLE users ADD COLUMN display_name TEXT;
280
+ CREATE INDEX idx_users_email ON users(email);
281
+ """,
282
+ # Version 3
283
+ """
284
+ CREATE TABLE posts (
285
+ id INTEGER PRIMARY KEY,
286
+ user_id INTEGER NOT NULL REFERENCES users(id),
287
+ title TEXT NOT NULL,
288
+ body TEXT NOT NULL
289
+ ) STRICT;
290
+ """,
291
+ ]
292
+
293
+ def migrate(conn: sqlite3.Connection):
294
+ conn.execute("CREATE TABLE IF NOT EXISTS schema_version (version INTEGER)")
295
+ row = conn.execute("SELECT MAX(version) FROM schema_version").fetchone()
296
+ current = row[0] if row[0] is not None else 0
297
+
298
+ for i, sql in enumerate(MIGRATIONS[current:], start=current + 1):
299
+ conn.executescript(sql)
300
+ conn.execute("INSERT INTO schema_version (version) VALUES (?)", (i,))
301
+ conn.commit()
302
+ ```
303
+
304
+ This pattern works for small-to-medium applications.
305
+
306
+ ### Reversible Migrations
307
+
308
+ Structure migrations as `(up_sql, down_sql)` tuples to support rollback during development:
309
+
310
+ ```python
311
+ MIGRATIONS = [
312
+ # Version 1
313
+ (
314
+ """
315
+ CREATE TABLE users (
316
+ id INTEGER PRIMARY KEY,
317
+ email TEXT NOT NULL UNIQUE,
318
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
319
+ ) STRICT;
320
+ """,
321
+ """
322
+ DROP TABLE users;
323
+ """,
324
+ ),
325
+ # Version 2
326
+ (
327
+ """
328
+ ALTER TABLE users ADD COLUMN display_name TEXT;
329
+ CREATE INDEX idx_users_email ON users(email);
330
+ """,
331
+ """
332
+ DROP INDEX idx_users_email;
333
+ ALTER TABLE users DROP COLUMN display_name;
334
+ """,
335
+ ),
336
+ ]
337
+
338
+ def rollback(conn: sqlite3.Connection, target_version: int):
339
+ row = conn.execute("SELECT MAX(version) FROM schema_version").fetchone()
340
+ current = row[0] if row[0] is not None else 0
341
+
342
+ if target_version >= current:
343
+ return
344
+
345
+ for i in range(current, target_version, -1):
346
+ _up, down_sql = MIGRATIONS[i - 1]
347
+ conn.executescript(down_sql)
348
+ conn.execute("DELETE FROM schema_version WHERE version = ?", (i,))
349
+ conn.commit()
350
+ ```
351
+
352
+ **Safety constraints:** `DROP COLUMN` is only available in SQLite 3.35.0+. Data-destructive operations (dropping columns, changing types, deleting rows) are not safely reversible — the down migration can undo the schema change but not restore lost data. Prefer forward-fix migrations in production; use rollback as a development convenience for iterating on schema changes.
353
+
354
+ For larger projects, use a dedicated migration tool (Alembic, yoyo-migrations) that provides migration file management and dependency tracking.
@@ -0,0 +1,326 @@
1
+ # Schema and PRAGMAs -- Deep Dive
2
+
3
+ ## 1. Recommended PRAGMAs
4
+
5
+ Apply these at connection startup for production workloads. Each PRAGMA has a specific purpose and tradeoff:
6
+
7
+ | PRAGMA | Value | Rationale |
8
+ |--------|-------|-----------|
9
+ | `journal_mode` | `WAL` | Concurrent reads during writes, better performance for mixed workloads |
10
+ | `foreign_keys` | `ON` | Enforce referential integrity (off by default for backward compatibility) |
11
+ | `busy_timeout` | `5000` | Wait 5 seconds for locks instead of failing immediately with SQLITE_BUSY |
12
+ | `synchronous` | `NORMAL` | Safe with WAL -- fsync on checkpoint only. `FULL` fsyncs every commit |
13
+ | `cache_size` | `-64000` | 64MB page cache in memory. Negative value = kilobytes. Default is ~2MB |
14
+ | `temp_store` | `MEMORY` | Temp tables and indexes in RAM instead of disk |
15
+ | `mmap_size` | `268435456` | Memory-map up to 256MB of the database file for faster reads |
16
+ | `page_size` | `4096` | Match filesystem block size (set before creating the database) |
17
+
18
+ ### Persistence Rules
19
+
20
+ | PRAGMA | Persists in DB file? | Set per connection? |
21
+ |--------|---------------------|-------------------|
22
+ | `journal_mode` | Yes (WAL persists) | Set once, all connections inherit |
23
+ | `foreign_keys` | No | Must set every connection |
24
+ | `busy_timeout` | No | Must set every connection |
25
+ | `synchronous` | No | Must set every connection |
26
+ | `cache_size` | No | Must set every connection |
27
+ | `page_size` | Yes | Set before first table creation |
28
+
29
+ ### Connection Initialization Template
30
+
31
+ ```sql
32
+ -- Apply in this order at connection startup
33
+ PRAGMA journal_mode = WAL;
34
+ PRAGMA foreign_keys = ON;
35
+ PRAGMA busy_timeout = 5000;
36
+ PRAGMA synchronous = NORMAL;
37
+ PRAGMA cache_size = -64000;
38
+ PRAGMA temp_store = MEMORY;
39
+ PRAGMA mmap_size = 268435456;
40
+ ```
41
+
42
+ ---
43
+
44
+ ## 2. WAL Internals
45
+
46
+ ### How WAL Works
47
+
48
+ In WAL mode, writes append to a separate WAL file (`database.db-wal`) instead of modifying the main database file. Readers see a consistent snapshot of the database as of the start of their transaction, even while a writer is appending new data.
49
+
50
+ The WAL file grows until a **checkpoint** transfers committed pages back to the main database file. Checkpointing is automatic by default (after 1000 pages, ~4MB with 4KB page size).
51
+
52
+ ### WAL File Lifecycle
53
+
54
+ ```
55
+ database.db -- main database file (read by readers via mmap)
56
+ database.db-wal -- write-ahead log (appended by writers)
57
+ database.db-shm -- shared memory for WAL index (mmap'd by all connections)
58
+ ```
59
+
60
+ The `-shm` file is a shared-memory index that allows readers to find pages in the WAL file efficiently. Both `-wal` and `-shm` files are automatically created and managed.
61
+
62
+ ### Checkpoint Modes
63
+
64
+ ```sql
65
+ -- Passive: checkpoint pages not currently locked by readers
66
+ PRAGMA wal_checkpoint(PASSIVE);
67
+
68
+ -- Full: wait for readers to finish, then checkpoint all pages
69
+ PRAGMA wal_checkpoint(FULL);
70
+
71
+ -- Truncate: like FULL, then truncate WAL file to zero bytes
72
+ PRAGMA wal_checkpoint(TRUNCATE);
73
+ ```
74
+
75
+ Use `PASSIVE` for regular maintenance (non-blocking). Use `TRUNCATE` before backups or when disk space is a concern.
76
+
77
+ ### WAL Limitations
78
+
79
+ - Only one writer at a time (multiple concurrent readers are fine).
80
+ - WAL does not work on network filesystems (NFS, SMB) -- the shared memory file requires POSIX locking.
81
+ - The WAL file can grow large under sustained write pressure without checkpointing.
82
+
83
+ ---
84
+
85
+ ## 3. FTS5 Tokenizers
86
+
87
+ ### Built-In Tokenizers
88
+
89
+ | Tokenizer | Behavior | Best For |
90
+ |-----------|----------|----------|
91
+ | `unicode61` | Unicode-aware, folds diacritics, lowercases | General text in any language |
92
+ | `porter unicode61` | Unicode61 + Porter stemming | English-language search |
93
+ | `ascii` | ASCII-only tokenization | ASCII-only data, slightly faster |
94
+ | `trigram` | 3-character substrings | Substring matching, autocomplete |
95
+
96
+ ### Configuring Tokenizers
97
+
98
+ ```sql
99
+ -- Porter stemming with Unicode folding (default for English)
100
+ CREATE VIRTUAL TABLE docs_fts USING fts5(
101
+ title, body,
102
+ tokenize='porter unicode61'
103
+ );
104
+
105
+ -- Trigram for substring search
106
+ CREATE VIRTUAL TABLE names_fts USING fts5(
107
+ name,
108
+ tokenize='trigram'
109
+ );
110
+
111
+ -- Case-sensitive trigram
112
+ CREATE VIRTUAL TABLE code_fts USING fts5(
113
+ source,
114
+ tokenize='trigram case_sensitive 1'
115
+ );
116
+ ```
117
+
118
+ ### Prefix Indexes
119
+
120
+ Enable prefix search (autocomplete) by configuring `prefix`:
121
+
122
+ ```sql
123
+ CREATE VIRTUAL TABLE search_fts USING fts5(
124
+ title, body,
125
+ tokenize='porter unicode61',
126
+ prefix='2,3' -- index 2- and 3-character prefixes
127
+ );
128
+
129
+ -- Query with prefix
130
+ SELECT * FROM search_fts WHERE search_fts MATCH 'sqli*';
131
+ ```
132
+
133
+ Prefix indexes increase FTS index size but make prefix queries instant instead of scanning all tokens.
134
+
135
+ ---
136
+
137
+ ## 4. External Content Tables
138
+
139
+ An external-content FTS table stores no content of its own -- it reads from a regular table. This avoids data duplication:
140
+
141
+ ```sql
142
+ CREATE TABLE posts (
143
+ id INTEGER PRIMARY KEY,
144
+ title TEXT NOT NULL,
145
+ body TEXT NOT NULL,
146
+ created_at TEXT NOT NULL
147
+ );
148
+
149
+ CREATE VIRTUAL TABLE posts_fts USING fts5(
150
+ title, body,
151
+ content='posts',
152
+ content_rowid='id'
153
+ );
154
+ ```
155
+
156
+ ### Keeping External Content in Sync
157
+
158
+ External-content tables do not update automatically. Use triggers to maintain synchronization:
159
+
160
+ ```sql
161
+ -- After INSERT: add to FTS
162
+ CREATE TRIGGER posts_fts_insert AFTER INSERT ON posts BEGIN
163
+ INSERT INTO posts_fts(rowid, title, body)
164
+ VALUES (new.id, new.title, new.body);
165
+ END;
166
+
167
+ -- After DELETE: remove from FTS
168
+ CREATE TRIGGER posts_fts_delete AFTER DELETE ON posts BEGIN
169
+ INSERT INTO posts_fts(posts_fts, rowid, title, body)
170
+ VALUES ('delete', old.id, old.title, old.body);
171
+ END;
172
+
173
+ -- After UPDATE: remove old, add new
174
+ CREATE TRIGGER posts_fts_update AFTER UPDATE ON posts BEGIN
175
+ INSERT INTO posts_fts(posts_fts, rowid, title, body)
176
+ VALUES ('delete', old.id, old.title, old.body);
177
+ INSERT INTO posts_fts(rowid, title, body)
178
+ VALUES (new.id, new.title, new.body);
179
+ END;
180
+ ```
181
+
182
+ The `INSERT INTO fts_table(fts_table, ...)` syntax with the table name as the first value is FTS5's delete command. This is not a regular insert -- it instructs FTS5 to remove the specified row from the index.
183
+
184
+ ### Rebuilding the FTS Index
185
+
186
+ If the FTS index becomes out of sync with the content table, rebuild it:
187
+
188
+ ```sql
189
+ INSERT INTO posts_fts(posts_fts) VALUES ('rebuild');
190
+ ```
191
+
192
+ This scans the entire content table and rebuilds the FTS index from scratch.
193
+
194
+ ---
195
+
196
+ ## 5. FTS5 Query Syntax
197
+
198
+ ### Boolean Operators
199
+
200
+ ```sql
201
+ -- AND (implicit or explicit)
202
+ SELECT * FROM posts_fts WHERE posts_fts MATCH 'sqlite database';
203
+ SELECT * FROM posts_fts WHERE posts_fts MATCH 'sqlite AND database';
204
+
205
+ -- OR
206
+ SELECT * FROM posts_fts WHERE posts_fts MATCH 'sqlite OR postgres';
207
+
208
+ -- NOT
209
+ SELECT * FROM posts_fts WHERE posts_fts MATCH 'sqlite NOT tutorial';
210
+
211
+ -- Phrase search
212
+ SELECT * FROM posts_fts WHERE posts_fts MATCH '"full text search"';
213
+
214
+ -- Column filter
215
+ SELECT * FROM posts_fts WHERE posts_fts MATCH 'title:sqlite';
216
+
217
+ -- NEAR (within N tokens)
218
+ SELECT * FROM posts_fts WHERE posts_fts MATCH 'NEAR(sqlite database, 5)';
219
+ ```
220
+
221
+ ### Ranking with bm25()
222
+
223
+ ```sql
224
+ SELECT *, bm25(posts_fts) AS relevance
225
+ FROM posts_fts
226
+ WHERE posts_fts MATCH 'search query'
227
+ ORDER BY relevance;
228
+ ```
229
+
230
+ `bm25()` returns negative values where more negative means more relevant. Pass column weights to prioritize title matches over body matches:
231
+
232
+ ```sql
233
+ -- Weight title 10x more than body
234
+ SELECT *, bm25(posts_fts, 10.0, 1.0) AS relevance
235
+ FROM posts_fts
236
+ WHERE posts_fts MATCH 'search query'
237
+ ORDER BY relevance;
238
+ ```
239
+
240
+ ### Highlight and Snippet
241
+
242
+ ```sql
243
+ SELECT
244
+ highlight(posts_fts, 0, '<mark>', '</mark>') AS title,
245
+ snippet(posts_fts, 1, '<mark>', '</mark>', '...', 64) AS body_preview
246
+ FROM posts_fts
247
+ WHERE posts_fts MATCH 'sqlite';
248
+ ```
249
+
250
+ `highlight()` wraps all matching terms in the specified markers. `snippet()` extracts a relevant fragment with context, truncated to the specified token count.
251
+
252
+ ---
253
+
254
+ ## 6. Partial and Expression Indexes
255
+
256
+ ### Partial Indexes
257
+
258
+ Reduce index size by indexing only rows that meet a condition:
259
+
260
+ ```sql
261
+ -- Index only active products
262
+ CREATE INDEX idx_products_active ON products(name, price)
263
+ WHERE status = 'active';
264
+
265
+ -- Index only future events
266
+ CREATE INDEX idx_events_upcoming ON events(start_date)
267
+ WHERE start_date > date('now');
268
+ ```
269
+
270
+ The query planner uses a partial index only when the query's `WHERE` clause implies the index condition. Add the same condition to queries:
271
+
272
+ ```sql
273
+ -- Uses idx_products_active
274
+ SELECT * FROM products WHERE status = 'active' AND name LIKE 'A%';
275
+
276
+ -- Does NOT use idx_products_active (missing status condition)
277
+ SELECT * FROM products WHERE name LIKE 'A%';
278
+ ```
279
+
280
+ ### Expression Indexes
281
+
282
+ ```sql
283
+ -- Index on lowercase email for case-insensitive lookup
284
+ CREATE INDEX idx_users_email_lower ON users(lower(email));
285
+
286
+ -- Index on JSON field
287
+ CREATE INDEX idx_users_theme ON users(json_extract(metadata, '$.theme'));
288
+
289
+ -- Index on date extracted from timestamp
290
+ CREATE INDEX idx_orders_date ON orders(date(created_at));
291
+ ```
292
+
293
+ ---
294
+
295
+ ## 7. ANALYZE
296
+
297
+ `ANALYZE` collects statistics about table and index contents, stored in the `sqlite_stat1` table. The query planner uses these statistics to choose optimal execution plans:
298
+
299
+ ```sql
300
+ -- Analyze all tables and indexes
301
+ ANALYZE;
302
+
303
+ -- Analyze a specific table
304
+ ANALYZE users;
305
+
306
+ -- Analyze a specific index
307
+ ANALYZE idx_users_email;
308
+ ```
309
+
310
+ ### When to Run ANALYZE
311
+
312
+ - After bulk data loads that significantly change data distribution.
313
+ - After creating indexes on populated tables.
314
+ - Periodically in long-running applications as data distribution shifts.
315
+
316
+ Without `ANALYZE`, the query planner assumes uniform distribution and may choose suboptimal plans for skewed data.
317
+
318
+ ### Inspecting Statistics
319
+
320
+ ```sql
321
+ SELECT * FROM sqlite_stat1;
322
+ -- tbl: table name, idx: index name, stat: "nrow n1 n2 ..."
323
+ -- nrow = rows in table, n1 = avg rows per first index column value
324
+ ```
325
+
326
+ Low selectivity values (high n1 relative to nrow) indicate the index is not selective enough for certain queries. Consider a composite index or a different query strategy.