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.
- package/.devcontainer/.env +22 -0
- package/.devcontainer/CHANGELOG.md +197 -0
- package/.devcontainer/CLAUDE.md +117 -0
- package/.devcontainer/README.md +222 -0
- package/.devcontainer/config/main-system-prompt.md +502 -0
- package/.devcontainer/config/settings.json +47 -0
- package/.devcontainer/devcontainer.json +94 -0
- package/.devcontainer/features/README.md +113 -0
- package/.devcontainer/features/agent-browser/README.md +65 -0
- package/.devcontainer/features/agent-browser/devcontainer-feature.json +23 -0
- package/.devcontainer/features/agent-browser/install.sh +79 -0
- package/.devcontainer/features/ast-grep/README.md +24 -0
- package/.devcontainer/features/ast-grep/devcontainer-feature.json +24 -0
- package/.devcontainer/features/ast-grep/install.sh +51 -0
- package/.devcontainer/features/ccstatusline/README.md +296 -0
- package/.devcontainer/features/ccstatusline/devcontainer-feature.json +19 -0
- package/.devcontainer/features/ccstatusline/install.sh +290 -0
- package/.devcontainer/features/ccusage/README.md +205 -0
- package/.devcontainer/features/ccusage/devcontainer-feature.json +38 -0
- package/.devcontainer/features/ccusage/install.sh +132 -0
- package/.devcontainer/features/claude-code/README.md +498 -0
- package/.devcontainer/features/claude-code/config/settings.json +36 -0
- package/.devcontainer/features/claude-code/config/system-prompt.md +118 -0
- package/.devcontainer/features/claude-code/config/world-building-sp.md +1432 -0
- package/.devcontainer/features/claude-code/devcontainer-feature.json +42 -0
- package/.devcontainer/features/claude-code/install.sh +466 -0
- package/.devcontainer/features/claude-monitor/README.md +74 -0
- package/.devcontainer/features/claude-monitor/devcontainer-feature.json +38 -0
- package/.devcontainer/features/claude-monitor/install.sh +99 -0
- package/.devcontainer/features/lsp-servers/README.md +85 -0
- package/.devcontainer/features/lsp-servers/devcontainer-feature.json +40 -0
- package/.devcontainer/features/lsp-servers/install.sh +116 -0
- package/.devcontainer/features/mcp-qdrant/CHANGES.md +399 -0
- package/.devcontainer/features/mcp-qdrant/README.md +474 -0
- package/.devcontainer/features/mcp-qdrant/devcontainer-feature.json +57 -0
- package/.devcontainer/features/mcp-qdrant/install.sh +295 -0
- package/.devcontainer/features/mcp-qdrant/poststart-hook.sh +129 -0
- package/.devcontainer/features/mcp-reasoner/README.md +177 -0
- package/.devcontainer/features/mcp-reasoner/devcontainer-feature.json +20 -0
- package/.devcontainer/features/mcp-reasoner/install.sh +177 -0
- package/.devcontainer/features/mcp-reasoner/poststart-hook.sh +67 -0
- package/.devcontainer/features/notify-hook/README.md +86 -0
- package/.devcontainer/features/notify-hook/devcontainer-feature.json +23 -0
- package/.devcontainer/features/notify-hook/install.sh +38 -0
- package/.devcontainer/features/splitrail/README.md +140 -0
- package/.devcontainer/features/splitrail/devcontainer-feature.json +34 -0
- package/.devcontainer/features/splitrail/install.sh +129 -0
- package/.devcontainer/features/tree-sitter/README.md +138 -0
- package/.devcontainer/features/tree-sitter/devcontainer-feature.json +52 -0
- package/.devcontainer/features/tree-sitter/install.sh +173 -0
- package/.devcontainer/plugins/devs-marketplace/.claude-plugin/marketplace.json +106 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/hooks/hooks.json +17 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/scripts/format-file.py +101 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/hooks/hooks.json +17 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/scripts/lint-file.py +137 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/.claude-plugin/plugin.json +8 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/claude-code-headless/SKILL.md +387 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/claude-code-headless/references/cli-flags-and-output.md +312 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/claude-code-headless/references/sdk-and-mcp.md +569 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker/SKILL.md +309 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker/references/compose-services.md +438 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker/references/dockerfile-patterns.md +340 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker-py/SKILL.md +412 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker-py/references/container-lifecycle.md +388 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker-py/references/resources-and-security.md +444 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/fastapi/SKILL.md +344 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/fastapi/references/middleware-and-lifespan.md +254 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/fastapi/references/pydantic-models.md +245 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/fastapi/references/routing-and-dependencies.md +255 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/fastapi/references/sse-and-streaming.md +318 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/pydantic-ai/SKILL.md +345 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/pydantic-ai/references/agents-and-tools.md +271 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/pydantic-ai/references/models-and-streaming.md +422 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/skill-building/SKILL.md +220 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/skill-building/references/cross-vendor-principles.md +139 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/skill-building/references/patterns-and-antipatterns.md +376 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/skill-building/references/skill-authoring-patterns.md +356 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/sqlite/SKILL.md +329 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/sqlite/references/advanced-queries.md +314 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/sqlite/references/javascript-patterns.md +323 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/sqlite/references/python-patterns.md +354 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/sqlite/references/schema-and-pragmas.md +326 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/SKILL.md +356 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/ai-sdk-svelte.md +128 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/component-patterns.md +332 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/layercake.md +203 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/migration-guide.md +350 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/runes-and-reactivity.md +328 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/spa-and-routing.md +262 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/svelte-dnd-action.md +181 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/testing/SKILL.md +414 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/testing/references/fastapi-testing.md +411 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/testing/references/svelte-testing.md +538 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codeforge-lsp/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/dangerous-command-blocker/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/dangerous-command-blocker/hooks/hooks.json +17 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/dangerous-command-blocker/scripts/block-dangerous.py +110 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/notify-hook/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/notify-hook/hooks/hooks.json +17 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/planning-reminder/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/planning-reminder/hooks/hooks.json +17 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/hooks/hooks.json +17 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/scripts/guard-protected.py +108 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/commands/ticket/357/200/272create-pr.md +337 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/commands/ticket/357/200/272new.md +166 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/commands/ticket/357/200/272review-commit.md +290 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/commands/ticket/357/200/272work.md +257 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/plugin.json +8 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/system-prompt.md +184 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/.claude-plugin/plugin.json +6 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/config/planning-instructions.md +14 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/functional-conjuring-map.md +989 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/hooks/hooks.json +33 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/__pycache__/post-enhance-task.cpython-314.pyc +0 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/enhance-planning.py +71 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/enhancers/enhance-plan.sh +68 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/enhancers/enhance-task.sh +120 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/post-enhance-plan.py +133 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/post-enhance-task.py +253 -0
- package/.devcontainer/scripts/setup-aliases.sh +80 -0
- package/.devcontainer/scripts/setup-config.sh +28 -0
- package/.devcontainer/scripts/setup-irie-claude.sh +32 -0
- package/.devcontainer/scripts/setup-plugins.sh +80 -0
- package/.devcontainer/scripts/setup.sh +58 -0
- package/LICENSE.txt +674 -0
- package/README.md +267 -0
- package/package.json +44 -0
- 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.
|