contextual-engine 0.1.0__py3-none-any.whl
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.
- contextual/__init__.py +18 -0
- contextual/__main__.py +11 -0
- contextual/cli.py +339 -0
- contextual/cli_docs.py +685 -0
- contextual/config.py +7 -0
- contextual/core/__init__.py +11 -0
- contextual/core/errors.py +470 -0
- contextual/core/models.py +590 -0
- contextual/docs/__init__.py +66 -0
- contextual/docs/chunker.py +550 -0
- contextual/docs/pipeline.py +513 -0
- contextual/docs/retrieval.py +654 -0
- contextual/docs/watcher.py +265 -0
- contextual/embedding/__init__.py +87 -0
- contextual/embedding/cache.py +455 -0
- contextual/embedding/embedder.py +414 -0
- contextual/embedding/helpers.py +252 -0
- contextual/git/__init__.py +22 -0
- contextual/git/blame.py +334 -0
- contextual/indexing/__init__.py +20 -0
- contextual/indexing/bug_sweep.py +119 -0
- contextual/indexing/chunker.py +691 -0
- contextual/indexing/embedder.py +271 -0
- contextual/indexing/file_watcher.py +154 -0
- contextual/indexing/incremental.py +260 -0
- contextual/indexing/index_writer.py +442 -0
- contextual/indexing/pipeline.py +438 -0
- contextual/indexing/processor.py +436 -0
- contextual/indexing/queries/readme.md +22 -0
- contextual/indexing/symbol_extractor.py +426 -0
- contextual/indexing/tokenizer.py +203 -0
- contextual/integrations/__init__.py +10 -0
- contextual/mcp/__init__.py +15 -0
- contextual/mcp/__main__.py +24 -0
- contextual/mcp/docs_tools.py +286 -0
- contextual/mcp/server.py +118 -0
- contextual/mcp/tools.py +443 -0
- contextual/observability/__init__.py +21 -0
- contextual/observability/logging.py +115 -0
- contextual/py.typed +0 -0
- contextual/retrieval/__init__.py +24 -0
- contextual/retrieval/context_assembler.py +372 -0
- contextual/retrieval/ranker.py +193 -0
- contextual/retrieval/search.py +548 -0
- contextual/security/__init__.py +52 -0
- contextual/security/paths.py +347 -0
- contextual/security/sanitize.py +349 -0
- contextual/security/workspace.py +348 -0
- contextual/storage/__init__.py +36 -0
- contextual/storage/fts_manager.py +273 -0
- contextual/storage/migration_v2.py +289 -0
- contextual/storage/migrations.py +316 -0
- contextual/storage/schema.py +210 -0
- contextual/storage/sqlite_pool.py +468 -0
- contextual/storage/vec0_manager.py +421 -0
- contextual_engine-0.1.0.dist-info/METADATA +297 -0
- contextual_engine-0.1.0.dist-info/RECORD +60 -0
- contextual_engine-0.1.0.dist-info/WHEEL +4 -0
- contextual_engine-0.1.0.dist-info/entry_points.txt +2 -0
- contextual_engine-0.1.0.dist-info/licenses/LICENSE +111 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""Phase 2 schema migration — Migration version 2.
|
|
2
|
+
|
|
3
|
+
Adds documentation-specific columns to the ``chunks`` table and
|
|
4
|
+
extends the FTS5 index to include ``heading_path`` for boosted BM25.
|
|
5
|
+
|
|
6
|
+
HOW TO WIRE INTO migrations.py
|
|
7
|
+
================================
|
|
8
|
+
Find the ``run_migrations`` function in ``contextual/storage/migrations.py``
|
|
9
|
+
and add ONE call inside the version dispatch block:
|
|
10
|
+
|
|
11
|
+
# Existing (version 1 was Phase 1 baseline):
|
|
12
|
+
if current_version < 1:
|
|
13
|
+
_apply_migration_1(conn)
|
|
14
|
+
record_migration(conn, 1)
|
|
15
|
+
applied += 1
|
|
16
|
+
|
|
17
|
+
# ADD THIS BLOCK:
|
|
18
|
+
if current_version < 2:
|
|
19
|
+
from contextual.storage.migration_v2 import apply_migration_2
|
|
20
|
+
apply_migration_2(conn)
|
|
21
|
+
record_migration(conn, 2)
|
|
22
|
+
applied += 1
|
|
23
|
+
|
|
24
|
+
return applied
|
|
25
|
+
|
|
26
|
+
That's it. The rest of this file is self-contained.
|
|
27
|
+
|
|
28
|
+
Migration strategy
|
|
29
|
+
------------------
|
|
30
|
+
SQLite supports ``ALTER TABLE ... ADD COLUMN`` for regular tables since 3.1.3.
|
|
31
|
+
We use it for all five new ``chunks`` columns (safe, idempotent via
|
|
32
|
+
``PRAGMA table_info`` guard).
|
|
33
|
+
|
|
34
|
+
FTS5 virtual tables do NOT support ALTER TABLE. The correct approach is:
|
|
35
|
+
1. Create ``fts_chunks_new`` with the extended schema.
|
|
36
|
+
2. Populate from existing ``chunks`` data (heading_path defaults to '').
|
|
37
|
+
3. Drop ``fts_chunks``.
|
|
38
|
+
4. Rename ``fts_chunks_new`` → ``fts_chunks``.
|
|
39
|
+
5. Recreate the three content-sync triggers.
|
|
40
|
+
|
|
41
|
+
SQLite's ``PRAGMA writable_schema`` is NOT used — we keep everything
|
|
42
|
+
through the public DDL surface for safety.
|
|
43
|
+
"""
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
import sqlite3
|
|
47
|
+
from sqlite3 import Connection
|
|
48
|
+
|
|
49
|
+
import structlog
|
|
50
|
+
|
|
51
|
+
logger = structlog.get_logger(__name__)
|
|
52
|
+
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
# Column definitions added to ``chunks``
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
_NEW_CHUNKS_COLUMNS: list[tuple[str, str]] = [
|
|
58
|
+
# (column_name, ALTER TABLE fragment)
|
|
59
|
+
("file_path", "ALTER TABLE chunks ADD COLUMN file_path TEXT NOT NULL DEFAULT ''"),
|
|
60
|
+
("source_type", "ALTER TABLE chunks ADD COLUMN source_type TEXT NOT NULL DEFAULT 'code'"),
|
|
61
|
+
("heading_path", "ALTER TABLE chunks ADD COLUMN heading_path TEXT NOT NULL DEFAULT ''"),
|
|
62
|
+
("heading_level", "ALTER TABLE chunks ADD COLUMN heading_level INTEGER NOT NULL DEFAULT 0"),
|
|
63
|
+
("chunk_type", "ALTER TABLE chunks ADD COLUMN chunk_type TEXT NOT NULL DEFAULT 'code'"),
|
|
64
|
+
("frontmatter_json","ALTER TABLE chunks ADD COLUMN frontmatter_json TEXT NOT NULL DEFAULT '{}'"),
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
# New FTS5 schema (extends Phase 1 schema with heading_path and symbol_name)
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
_FTS5_CREATE = """
|
|
72
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS fts_chunks_new USING fts5(
|
|
73
|
+
chunk_id UNINDEXED,
|
|
74
|
+
content,
|
|
75
|
+
file_path UNINDEXED,
|
|
76
|
+
symbol_name,
|
|
77
|
+
heading_path,
|
|
78
|
+
content='chunks',
|
|
79
|
+
content_rowid='id',
|
|
80
|
+
tokenize='unicode61 remove_diacritics 2 tokenchars "_-."'
|
|
81
|
+
)
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
# Triggers keep fts_chunks_new in sync with chunks (external content table)
|
|
85
|
+
_FTS5_TRIGGERS = [
|
|
86
|
+
# INSERT trigger
|
|
87
|
+
"""
|
|
88
|
+
CREATE TRIGGER IF NOT EXISTS fts_chunks_ai AFTER INSERT ON chunks BEGIN
|
|
89
|
+
INSERT INTO fts_chunks(rowid, chunk_id, content, file_path, symbol_name, heading_path)
|
|
90
|
+
VALUES (new.id, new.id, new.content, '',
|
|
91
|
+
COALESCE(new.symbol_name, ''), COALESCE(new.heading_path, ''));
|
|
92
|
+
END
|
|
93
|
+
""",
|
|
94
|
+
# DELETE trigger
|
|
95
|
+
"""
|
|
96
|
+
CREATE TRIGGER IF NOT EXISTS fts_chunks_ad AFTER DELETE ON chunks BEGIN
|
|
97
|
+
INSERT INTO fts_chunks(fts_chunks, rowid, chunk_id, content, file_path, symbol_name, heading_path)
|
|
98
|
+
VALUES ('delete', old.id, old.id, old.content, '',
|
|
99
|
+
COALESCE(old.symbol_name, ''), COALESCE(old.heading_path, ''));
|
|
100
|
+
END
|
|
101
|
+
""",
|
|
102
|
+
# UPDATE trigger
|
|
103
|
+
"""
|
|
104
|
+
CREATE TRIGGER IF NOT EXISTS fts_chunks_au AFTER UPDATE ON chunks BEGIN
|
|
105
|
+
INSERT INTO fts_chunks(fts_chunks, rowid, chunk_id, content, file_path, symbol_name, heading_path)
|
|
106
|
+
VALUES ('delete', old.id, old.id, old.content, '',
|
|
107
|
+
COALESCE(old.symbol_name, ''), COALESCE(old.heading_path, ''));
|
|
108
|
+
INSERT INTO fts_chunks(rowid, chunk_id, content, file_path, symbol_name, heading_path)
|
|
109
|
+
VALUES (new.id, new.id, new.content, '',
|
|
110
|
+
COALESCE(new.symbol_name, ''), COALESCE(new.heading_path, ''));
|
|
111
|
+
END
|
|
112
|
+
""",
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# ---------------------------------------------------------------------------
|
|
117
|
+
# Public migration function
|
|
118
|
+
# ---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
def apply_migration_2(conn: Connection) -> None:
|
|
121
|
+
"""Apply Phase 2 schema migration to an existing Contextual database.
|
|
122
|
+
|
|
123
|
+
Operations (all idempotent — safe to call on an already-migrated DB):
|
|
124
|
+
1. Add five new columns to ``chunks`` (ALTER TABLE ADD COLUMN, no-op if exists).
|
|
125
|
+
2. Rebuild ``fts_chunks`` virtual table to include ``heading_path``.
|
|
126
|
+
3. Backfill the new FTS table from existing code chunks.
|
|
127
|
+
4. Drop Phase 1 triggers; install Phase 2 three-trigger set.
|
|
128
|
+
5. Add missing performance indexes.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
conn: Open SQLite connection with write access.
|
|
132
|
+
Must have sqlite_vec and FTS5 available.
|
|
133
|
+
|
|
134
|
+
Raises:
|
|
135
|
+
sqlite3.Error: On any unrecoverable DDL failure.
|
|
136
|
+
"""
|
|
137
|
+
logger.info("Applying Phase 2 migration (version 2)")
|
|
138
|
+
|
|
139
|
+
# ------------------------------------------------------------------
|
|
140
|
+
# 1. Add new columns to chunks (idempotent)
|
|
141
|
+
# ------------------------------------------------------------------
|
|
142
|
+
existing_cols = {
|
|
143
|
+
row[1]
|
|
144
|
+
for row in conn.execute("PRAGMA table_info(chunks)").fetchall()
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
added: list[str] = []
|
|
148
|
+
for col_name, alter_sql in _NEW_CHUNKS_COLUMNS:
|
|
149
|
+
if col_name not in existing_cols:
|
|
150
|
+
conn.execute(alter_sql)
|
|
151
|
+
added.append(col_name)
|
|
152
|
+
logger.debug("Added column to chunks", column=col_name)
|
|
153
|
+
|
|
154
|
+
if added:
|
|
155
|
+
logger.info("chunks table columns added", columns=added)
|
|
156
|
+
else:
|
|
157
|
+
logger.info("chunks table columns already present — skipping")
|
|
158
|
+
|
|
159
|
+
# Backfill file_path from the files table for existing rows
|
|
160
|
+
conn.execute("""
|
|
161
|
+
UPDATE chunks
|
|
162
|
+
SET file_path = (
|
|
163
|
+
SELECT f.path FROM files f WHERE f.id = chunks.file_id
|
|
164
|
+
)
|
|
165
|
+
WHERE file_path = '' AND file_id IS NOT NULL
|
|
166
|
+
""")
|
|
167
|
+
logger.info("Backfilled file_path on chunks from files table")
|
|
168
|
+
|
|
169
|
+
# ------------------------------------------------------------------
|
|
170
|
+
# 2. Rebuild fts_chunks to include heading_path
|
|
171
|
+
# Skip if already rebuilt (detect by checking column count via
|
|
172
|
+
# fts5 info query — safest approach without touching writable_schema).
|
|
173
|
+
# ------------------------------------------------------------------
|
|
174
|
+
needs_fts_rebuild = _fts_needs_rebuild(conn)
|
|
175
|
+
|
|
176
|
+
if needs_fts_rebuild:
|
|
177
|
+
logger.info("Rebuilding fts_chunks with heading_path and symbol_name columns")
|
|
178
|
+
_rebuild_fts(conn)
|
|
179
|
+
else:
|
|
180
|
+
logger.info("fts_chunks already has heading_path — skipping rebuild")
|
|
181
|
+
|
|
182
|
+
# ------------------------------------------------------------------
|
|
183
|
+
# 3. Add performance indexes (I2)
|
|
184
|
+
# ------------------------------------------------------------------
|
|
185
|
+
conn.execute("""
|
|
186
|
+
CREATE INDEX IF NOT EXISTS idx_chunks_source_type
|
|
187
|
+
ON chunks(source_type)
|
|
188
|
+
""")
|
|
189
|
+
conn.execute("""
|
|
190
|
+
CREATE INDEX IF NOT EXISTS idx_chunks_heading_path
|
|
191
|
+
ON chunks(heading_path)
|
|
192
|
+
WHERE heading_path != ''
|
|
193
|
+
""")
|
|
194
|
+
conn.execute("""
|
|
195
|
+
CREATE INDEX IF NOT EXISTS idx_chunks_file_path_source
|
|
196
|
+
ON chunks(file_id, source_type)
|
|
197
|
+
""")
|
|
198
|
+
|
|
199
|
+
conn.commit()
|
|
200
|
+
logger.info("Phase 2 migration complete")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# ---------------------------------------------------------------------------
|
|
204
|
+
# FTS rebuild helpers
|
|
205
|
+
# ---------------------------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
def _fts_needs_rebuild(conn: Connection) -> bool:
|
|
208
|
+
"""Return True if fts_chunks does NOT have a heading_path column.
|
|
209
|
+
|
|
210
|
+
We probe by attempting a query that references heading_path.
|
|
211
|
+
If it raises OperationalError, the column doesn't exist yet.
|
|
212
|
+
"""
|
|
213
|
+
try:
|
|
214
|
+
conn.execute(
|
|
215
|
+
"SELECT heading_path FROM fts_chunks WHERE fts_chunks MATCH 'probe_migration_v2' LIMIT 1"
|
|
216
|
+
)
|
|
217
|
+
return False
|
|
218
|
+
except sqlite3.OperationalError:
|
|
219
|
+
return True
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _rebuild_fts(conn: Connection) -> None:
|
|
223
|
+
"""Rebuild fts_chunks virtual table with heading_path support.
|
|
224
|
+
|
|
225
|
+
Steps:
|
|
226
|
+
1. Create fts_chunks_new (extended schema).
|
|
227
|
+
2. Populate from chunks table (existing rows, heading_path='').
|
|
228
|
+
3. Drop fts_chunks (and its auto-generated shadow tables).
|
|
229
|
+
4. Rename fts_chunks_new → fts_chunks.
|
|
230
|
+
5. Drop Phase 1 triggers (if they exist).
|
|
231
|
+
6. Install Phase 2 three-trigger set.
|
|
232
|
+
"""
|
|
233
|
+
# Step 1: Create new FTS table
|
|
234
|
+
conn.execute(_FTS5_CREATE)
|
|
235
|
+
|
|
236
|
+
# Step 2: Backfill from existing chunks
|
|
237
|
+
# Existing code chunks have heading_path='' (DEFAULT); docs chunks
|
|
238
|
+
# will be re-indexed by DocsPipeline on first run.
|
|
239
|
+
conn.execute("""
|
|
240
|
+
INSERT INTO fts_chunks_new(rowid, chunk_id, content, file_path, symbol_name, heading_path)
|
|
241
|
+
SELECT id, id, content, '', COALESCE(symbol_name, ''), ''
|
|
242
|
+
FROM chunks
|
|
243
|
+
WHERE id NOT IN (SELECT rowid FROM fts_chunks_new)
|
|
244
|
+
""")
|
|
245
|
+
logger.debug("Backfilled fts_chunks_new from chunks table")
|
|
246
|
+
|
|
247
|
+
# Step 3: Drop old fts_chunks
|
|
248
|
+
# FTS5 drop cascades to shadow tables (_data, _idx, _content, _docsize, _config).
|
|
249
|
+
conn.execute("DROP TABLE IF EXISTS fts_chunks")
|
|
250
|
+
|
|
251
|
+
# Step 4: Rename — SQLite supports ALTER TABLE ... RENAME TO for virtual tables
|
|
252
|
+
conn.execute("ALTER TABLE fts_chunks_new RENAME TO fts_chunks")
|
|
253
|
+
logger.debug("Renamed fts_chunks_new → fts_chunks")
|
|
254
|
+
|
|
255
|
+
# Step 5: Drop Phase 1 triggers (they reference old schema)
|
|
256
|
+
for trigger in ("chunks_ai", "chunks_ad", "chunks_au",
|
|
257
|
+
"fts_chunks_ai", "fts_chunks_ad", "fts_chunks_au"):
|
|
258
|
+
conn.execute(f"DROP TRIGGER IF EXISTS {trigger}")
|
|
259
|
+
|
|
260
|
+
# Step 6: Install Phase 2 triggers
|
|
261
|
+
for trigger_sql in _FTS5_TRIGGERS:
|
|
262
|
+
conn.execute(trigger_sql)
|
|
263
|
+
|
|
264
|
+
logger.info("fts_chunks rebuilt with heading_path column, triggers reinstalled")
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
# ---------------------------------------------------------------------------
|
|
268
|
+
# Standalone CLI helper (python -m contextual.storage.migration_v2)
|
|
269
|
+
# ---------------------------------------------------------------------------
|
|
270
|
+
|
|
271
|
+
if __name__ == "__main__":
|
|
272
|
+
import sys
|
|
273
|
+
from pathlib import Path
|
|
274
|
+
|
|
275
|
+
if len(sys.argv) < 2:
|
|
276
|
+
print("Usage: python -m contextual.storage.migration_v2 <path/to/contextual.db>")
|
|
277
|
+
sys.exit(1)
|
|
278
|
+
|
|
279
|
+
db_path = Path(sys.argv[1])
|
|
280
|
+
if not db_path.exists():
|
|
281
|
+
print(f"Error: {db_path} not found")
|
|
282
|
+
sys.exit(1)
|
|
283
|
+
|
|
284
|
+
import sqlite3 as _sqlite3
|
|
285
|
+
conn = _sqlite3.connect(str(db_path))
|
|
286
|
+
conn.execute("PRAGMA journal_mode=WAL")
|
|
287
|
+
apply_migration_2(conn)
|
|
288
|
+
conn.close()
|
|
289
|
+
print(f"✓ Migration v2 applied to {db_path}")
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"""SQLite migration runner for schema version management.
|
|
2
|
+
|
|
3
|
+
Tracks schema versions and applies pending migrations in order.
|
|
4
|
+
Migrations:
|
|
5
|
+
- v1: Core bi-temporal schema (entities, facts, episodes)
|
|
6
|
+
- v2: Phase 1 codebase indexing (code_symbols, indexing_state, vec0, FTS5)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import sqlite3
|
|
12
|
+
import time
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
from contextual.core.errors import ErrorCode, StorageError, storage_context
|
|
16
|
+
from contextual.observability.logging import get_logger
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from sqlite3 import Connection
|
|
21
|
+
|
|
22
|
+
logger = get_logger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_current_version(conn: Connection) -> int:
|
|
26
|
+
"""Get current schema version.
|
|
27
|
+
|
|
28
|
+
Reads from schema_version table. Returns 0 if table doesn't exist
|
|
29
|
+
or is empty (indicating fresh database).
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
conn: SQLite connection.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Current schema version (0 if no version recorded).
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
StorageError: If version query fails.
|
|
39
|
+
"""
|
|
40
|
+
try:
|
|
41
|
+
cursor = conn.cursor()
|
|
42
|
+
|
|
43
|
+
# Check if schema_version table exists.
|
|
44
|
+
cursor.execute(
|
|
45
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='schema_version'",
|
|
46
|
+
)
|
|
47
|
+
if not cursor.fetchone():
|
|
48
|
+
return 0
|
|
49
|
+
|
|
50
|
+
# Get the latest recorded version.
|
|
51
|
+
cursor.execute("SELECT MAX(version) FROM schema_version")
|
|
52
|
+
result = cursor.fetchone()
|
|
53
|
+
|
|
54
|
+
if result is None or result[0] is None:
|
|
55
|
+
return 0
|
|
56
|
+
|
|
57
|
+
return int(result[0])
|
|
58
|
+
|
|
59
|
+
except sqlite3.Error as e:
|
|
60
|
+
msg = f"Failed to get schema version: {e}"
|
|
61
|
+
raise StorageError(
|
|
62
|
+
code=ErrorCode.STORAGE_MIGRATION_FAILED,
|
|
63
|
+
message=msg,
|
|
64
|
+
context=storage_context(query="get_current_version"),
|
|
65
|
+
) from e
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def record_migration(conn: Connection, version: int) -> None:
|
|
69
|
+
"""Record that a migration was applied.
|
|
70
|
+
|
|
71
|
+
Inserts version and timestamp into schema_version table.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
conn: SQLite connection.
|
|
75
|
+
version: Migration version number.
|
|
76
|
+
|
|
77
|
+
Raises:
|
|
78
|
+
StorageError: If recording fails.
|
|
79
|
+
"""
|
|
80
|
+
try:
|
|
81
|
+
cursor = conn.cursor()
|
|
82
|
+
now_ms = int(time.time() * 1000)
|
|
83
|
+
|
|
84
|
+
cursor.execute(
|
|
85
|
+
"INSERT INTO schema_version (version, applied_at) VALUES (?, ?)",
|
|
86
|
+
(version, now_ms),
|
|
87
|
+
)
|
|
88
|
+
conn.commit()
|
|
89
|
+
|
|
90
|
+
except sqlite3.Error as e:
|
|
91
|
+
msg = f"Failed to record migration version {version}: {e}"
|
|
92
|
+
raise StorageError(
|
|
93
|
+
code=ErrorCode.STORAGE_MIGRATION_FAILED,
|
|
94
|
+
message=msg,
|
|
95
|
+
context=storage_context(query=f"record_migration({version})"),
|
|
96
|
+
) from e
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def apply_initial_schema(conn: Connection) -> None:
|
|
100
|
+
"""Apply initial schema to a fresh database.
|
|
101
|
+
|
|
102
|
+
Creates all tables, indexes, and FTS5 structures.
|
|
103
|
+
Records this as version 1.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
conn: SQLite connection.
|
|
107
|
+
|
|
108
|
+
Raises:
|
|
109
|
+
StorageError: If schema application fails.
|
|
110
|
+
"""
|
|
111
|
+
from contextual.storage.schema import apply_schema
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
# Apply complete schema for version 1.
|
|
115
|
+
apply_schema(conn)
|
|
116
|
+
|
|
117
|
+
# Record the schema as version 1.
|
|
118
|
+
record_migration(conn, 1)
|
|
119
|
+
|
|
120
|
+
except StorageError:
|
|
121
|
+
# Re-raise storage errors as-is.
|
|
122
|
+
raise
|
|
123
|
+
except Exception as e:
|
|
124
|
+
msg = f"Failed to apply initial schema: {e}"
|
|
125
|
+
raise StorageError(
|
|
126
|
+
code=ErrorCode.STORAGE_MIGRATION_FAILED,
|
|
127
|
+
message=msg,
|
|
128
|
+
context=storage_context(),
|
|
129
|
+
) from e
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def apply_migration_v2(conn: Connection) -> None:
|
|
133
|
+
"""Apply migration v2: Phase 1 codebase indexing tables.
|
|
134
|
+
|
|
135
|
+
Adds:
|
|
136
|
+
- code_symbols table (function/class/method metadata)
|
|
137
|
+
- indexing_state table (tracks repo indexing status)
|
|
138
|
+
- chunks table (code chunks with embeddings)
|
|
139
|
+
- files table (indexed files)
|
|
140
|
+
- vec0 virtual table (768-dim vectors for jina-v2-code)
|
|
141
|
+
- fts_chunks virtual table (FTS5 full-text search)
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
conn: SQLite connection.
|
|
145
|
+
|
|
146
|
+
Raises:
|
|
147
|
+
StorageError: If migration fails.
|
|
148
|
+
"""
|
|
149
|
+
try:
|
|
150
|
+
cursor = conn.cursor()
|
|
151
|
+
|
|
152
|
+
# FILES TABLE - tracks indexed files
|
|
153
|
+
cursor.execute("""
|
|
154
|
+
CREATE TABLE IF NOT EXISTS files (
|
|
155
|
+
id INTEGER PRIMARY KEY,
|
|
156
|
+
path TEXT NOT NULL UNIQUE,
|
|
157
|
+
language TEXT NOT NULL,
|
|
158
|
+
mtime INTEGER NOT NULL,
|
|
159
|
+
content_hash TEXT NOT NULL,
|
|
160
|
+
indexed_at INTEGER NOT NULL
|
|
161
|
+
) STRICT
|
|
162
|
+
""")
|
|
163
|
+
|
|
164
|
+
# CHUNKS TABLE - code chunks with metadata
|
|
165
|
+
cursor.execute("""
|
|
166
|
+
CREATE TABLE IF NOT EXISTS chunks (
|
|
167
|
+
id INTEGER PRIMARY KEY,
|
|
168
|
+
file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
|
|
169
|
+
start_line INTEGER NOT NULL,
|
|
170
|
+
end_line INTEGER NOT NULL,
|
|
171
|
+
symbol_name TEXT,
|
|
172
|
+
symbol_type TEXT,
|
|
173
|
+
content TEXT NOT NULL,
|
|
174
|
+
content_hash TEXT NOT NULL UNIQUE,
|
|
175
|
+
embedding_id INTEGER,
|
|
176
|
+
indexed_at INTEGER NOT NULL
|
|
177
|
+
) STRICT
|
|
178
|
+
""")
|
|
179
|
+
|
|
180
|
+
# CODE_SYMBOLS TABLE - extracted symbols with metadata
|
|
181
|
+
cursor.execute("""
|
|
182
|
+
CREATE TABLE IF NOT EXISTS code_symbols (
|
|
183
|
+
id INTEGER PRIMARY KEY,
|
|
184
|
+
chunk_id INTEGER NOT NULL REFERENCES chunks(id) ON DELETE CASCADE,
|
|
185
|
+
name TEXT NOT NULL,
|
|
186
|
+
kind TEXT NOT NULL CHECK (
|
|
187
|
+
kind IN ('function','class','method','interface','struct',
|
|
188
|
+
'enum','type_alias','variable','import')
|
|
189
|
+
),
|
|
190
|
+
signature TEXT,
|
|
191
|
+
docstring TEXT,
|
|
192
|
+
UNIQUE(chunk_id, name, kind)
|
|
193
|
+
) STRICT
|
|
194
|
+
""")
|
|
195
|
+
|
|
196
|
+
# INDEXING_STATE TABLE - repo-level indexing metadata
|
|
197
|
+
cursor.execute("""
|
|
198
|
+
CREATE TABLE IF NOT EXISTS indexing_state (
|
|
199
|
+
repo_root TEXT PRIMARY KEY,
|
|
200
|
+
last_full_index_at INTEGER NOT NULL,
|
|
201
|
+
last_incremental_at INTEGER,
|
|
202
|
+
total_files INTEGER NOT NULL,
|
|
203
|
+
total_chunks INTEGER NOT NULL,
|
|
204
|
+
total_symbols INTEGER NOT NULL
|
|
205
|
+
) STRICT
|
|
206
|
+
""")
|
|
207
|
+
|
|
208
|
+
# Indexes for performance
|
|
209
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_chunks_file ON chunks(file_id)")
|
|
210
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_chunks_content_hash ON chunks(content_hash)")
|
|
211
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_symbols_name ON code_symbols(name)")
|
|
212
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_symbols_kind ON code_symbols(kind)")
|
|
213
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_symbols_chunk ON code_symbols(chunk_id)")
|
|
214
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_files_path ON files(path)")
|
|
215
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_files_content_hash ON files(content_hash)")
|
|
216
|
+
|
|
217
|
+
# VEC0 VIRTUAL TABLE - 768-dim vectors (jina-v2-code)
|
|
218
|
+
# Note: vec0 extension must be loaded before this runs
|
|
219
|
+
try:
|
|
220
|
+
cursor.execute("""
|
|
221
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS vec_chunks
|
|
222
|
+
USING vec0(
|
|
223
|
+
chunk_id INTEGER PRIMARY KEY,
|
|
224
|
+
embedding FLOAT[768]
|
|
225
|
+
)
|
|
226
|
+
""")
|
|
227
|
+
except sqlite3.OperationalError as e:
|
|
228
|
+
# vec0 not available - log warning but don't fail migration
|
|
229
|
+
logger.warning(
|
|
230
|
+
"vec0 extension not available - vector search disabled",
|
|
231
|
+
error=str(e)
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
# FTS5 VIRTUAL TABLE - full-text search on chunks
|
|
235
|
+
# Using manual sync approach instead of triggers to avoid FTS5 transaction issues
|
|
236
|
+
cursor.execute("""
|
|
237
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS fts_chunks USING fts5(
|
|
238
|
+
chunk_id UNINDEXED,
|
|
239
|
+
content,
|
|
240
|
+
symbol_name,
|
|
241
|
+
tokenize='unicode61 remove_diacritics 2 tokenchars "_"'
|
|
242
|
+
)
|
|
243
|
+
""")
|
|
244
|
+
|
|
245
|
+
# EMBEDDING CACHE TABLE
|
|
246
|
+
from contextual.embedding.cache import initialize_cache
|
|
247
|
+
try:
|
|
248
|
+
initialize_cache(conn)
|
|
249
|
+
logger.info("Embedding cache initialized")
|
|
250
|
+
except Exception as e:
|
|
251
|
+
logger.warning("Failed to initialize embedding cache", error=str(e))
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
# NOTE: Triggers disabled to avoid "unsafe use of virtual table" errors with FTS5
|
|
255
|
+
# FTS5 sync is now handled manually in index_writer.py after chunk insertion
|
|
256
|
+
|
|
257
|
+
conn.commit()
|
|
258
|
+
|
|
259
|
+
logger.info("Migration v2 applied successfully")
|
|
260
|
+
|
|
261
|
+
except sqlite3.Error as e:
|
|
262
|
+
conn.rollback()
|
|
263
|
+
msg = f"Failed to apply migration v2: {e}"
|
|
264
|
+
raise StorageError(
|
|
265
|
+
code=ErrorCode.STORAGE_MIGRATION_FAILED,
|
|
266
|
+
message=msg,
|
|
267
|
+
context=storage_context(query="apply_migration_v2"),
|
|
268
|
+
) from e
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def run_migrations(conn: Connection) -> int:
|
|
272
|
+
"""Run any pending migrations.
|
|
273
|
+
|
|
274
|
+
Checks current version and applies necessary migrations.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
conn: SQLite connection.
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
Number of migrations applied.
|
|
281
|
+
|
|
282
|
+
Raises:
|
|
283
|
+
StorageError: If migrations fail.
|
|
284
|
+
"""
|
|
285
|
+
current_version = get_current_version(conn)
|
|
286
|
+
migrations_applied = 0
|
|
287
|
+
|
|
288
|
+
# Version 0 -> 1: initial schema.
|
|
289
|
+
if current_version == 0:
|
|
290
|
+
apply_initial_schema(conn)
|
|
291
|
+
migrations_applied += 1
|
|
292
|
+
current_version = 1
|
|
293
|
+
|
|
294
|
+
# Version 1 -> 2: Phase 1 codebase indexing
|
|
295
|
+
if current_version == 1:
|
|
296
|
+
apply_migration_v2(conn)
|
|
297
|
+
record_migration(conn, 2)
|
|
298
|
+
migrations_applied += 1
|
|
299
|
+
current_version = 2
|
|
300
|
+
|
|
301
|
+
# Version 2 -> 3: Phase 2 docs schema (heading_path columns, FTS rebuild)
|
|
302
|
+
if current_version == 2:
|
|
303
|
+
from contextual.storage.migration_v2 import apply_migration_2
|
|
304
|
+
apply_migration_2(conn)
|
|
305
|
+
record_migration(conn, 3)
|
|
306
|
+
migrations_applied += 1
|
|
307
|
+
current_version = 3
|
|
308
|
+
|
|
309
|
+
if migrations_applied > 0:
|
|
310
|
+
logger.info(
|
|
311
|
+
"Migrations complete",
|
|
312
|
+
applied=migrations_applied,
|
|
313
|
+
current_version=current_version
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
return migrations_applied
|