seam-code 0.3.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.
- seam/__init__.py +3 -0
- seam/_data/schema.sql +225 -0
- seam/_web/assets/index-BL_tqprR.js +216 -0
- seam/_web/assets/index-GTKUhVyD.css +1 -0
- seam/_web/index.html +13 -0
- seam/analysis/__init__.py +14 -0
- seam/analysis/affected.py +254 -0
- seam/analysis/builtins.py +966 -0
- seam/analysis/byte_budget.py +217 -0
- seam/analysis/changes.py +709 -0
- seam/analysis/cluster_naming.py +260 -0
- seam/analysis/clustering.py +216 -0
- seam/analysis/confidence.py +699 -0
- seam/analysis/embeddings.py +195 -0
- seam/analysis/flows.py +708 -0
- seam/analysis/impact.py +444 -0
- seam/analysis/imports.py +994 -0
- seam/analysis/imports_ext.py +780 -0
- seam/analysis/imports_resolve.py +176 -0
- seam/analysis/processes.py +453 -0
- seam/analysis/relevance.py +155 -0
- seam/analysis/rwr.py +129 -0
- seam/analysis/staleness.py +328 -0
- seam/analysis/steer.py +282 -0
- seam/analysis/synthesis.py +253 -0
- seam/analysis/synthesis_channels.py +433 -0
- seam/analysis/testpaths.py +103 -0
- seam/analysis/traversal.py +470 -0
- seam/cli/__init__.py +0 -0
- seam/cli/install.py +232 -0
- seam/cli/main.py +2602 -0
- seam/cli/output.py +137 -0
- seam/cli/read.py +244 -0
- seam/cli/serve.py +145 -0
- seam/config.py +551 -0
- seam/indexer/__init__.py +0 -0
- seam/indexer/cluster_index.py +425 -0
- seam/indexer/db.py +496 -0
- seam/indexer/embedding_index.py +183 -0
- seam/indexer/field_access.py +536 -0
- seam/indexer/field_access_c_cpp.py +643 -0
- seam/indexer/field_access_ext.py +708 -0
- seam/indexer/field_access_ext2.py +408 -0
- seam/indexer/field_access_go_rust.py +737 -0
- seam/indexer/field_access_php_swift.py +888 -0
- seam/indexer/field_access_ts.py +626 -0
- seam/indexer/graph.py +321 -0
- seam/indexer/graph_c.py +562 -0
- seam/indexer/graph_c_cpp.py +39 -0
- seam/indexer/graph_common.py +644 -0
- seam/indexer/graph_cpp.py +615 -0
- seam/indexer/graph_csharp.py +651 -0
- seam/indexer/graph_go.py +723 -0
- seam/indexer/graph_go_rust.py +39 -0
- seam/indexer/graph_java.py +689 -0
- seam/indexer/graph_java_csharp.py +38 -0
- seam/indexer/graph_php.py +914 -0
- seam/indexer/graph_python.py +628 -0
- seam/indexer/graph_ruby.py +748 -0
- seam/indexer/graph_rust.py +653 -0
- seam/indexer/graph_scope_infer.py +902 -0
- seam/indexer/graph_scope_infer_ext.py +723 -0
- seam/indexer/graph_scope_infer_ext2.py +992 -0
- seam/indexer/graph_swift.py +1014 -0
- seam/indexer/graph_swift_infer.py +515 -0
- seam/indexer/graph_typescript.py +663 -0
- seam/indexer/migrations.py +816 -0
- seam/indexer/parser.py +204 -0
- seam/indexer/pipeline.py +197 -0
- seam/indexer/signatures.py +634 -0
- seam/indexer/signatures_ext.py +780 -0
- seam/indexer/sync.py +287 -0
- seam/indexer/synthesis_index.py +291 -0
- seam/indexer/tokenize.py +79 -0
- seam/installer/__init__.py +67 -0
- seam/installer/claude.py +97 -0
- seam/installer/codex.py +94 -0
- seam/installer/core.py +127 -0
- seam/installer/cursor.py +61 -0
- seam/installer/guide.py +110 -0
- seam/installer/jsonfile.py +85 -0
- seam/installer/markdownfile.py +146 -0
- seam/installer/tomlfile.py +72 -0
- seam/query/__init__.py +0 -0
- seam/query/clusters.py +206 -0
- seam/query/comments.py +217 -0
- seam/query/context.py +293 -0
- seam/query/engine.py +940 -0
- seam/query/fts.py +328 -0
- seam/query/names.py +470 -0
- seam/query/pack.py +433 -0
- seam/query/semantic.py +339 -0
- seam/query/structure.py +727 -0
- seam/server/__init__.py +0 -0
- seam/server/graph_api.py +437 -0
- seam/server/handler_common.py +323 -0
- seam/server/impact_handler.py +615 -0
- seam/server/mcp.py +556 -0
- seam/server/tools.py +697 -0
- seam/server/trace_handler.py +184 -0
- seam/server/web.py +922 -0
- seam/watcher/__init__.py +0 -0
- seam/watcher/__main__.py +56 -0
- seam/watcher/daemon.py +237 -0
- seam_code-0.3.0.dist-info/METADATA +318 -0
- seam_code-0.3.0.dist-info/RECORD +109 -0
- seam_code-0.3.0.dist-info/WHEEL +4 -0
- seam_code-0.3.0.dist-info/entry_points.txt +2 -0
- seam_code-0.3.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,816 @@
|
|
|
1
|
+
"""Schema migration functions for the Seam SQLite index.
|
|
2
|
+
|
|
3
|
+
Each function is a guarded, idempotent migration that advances the DB schema
|
|
4
|
+
by one version. All take an open sqlite3.Connection and commit their own work.
|
|
5
|
+
|
|
6
|
+
Design rules (shared with db.py):
|
|
7
|
+
- Each migration is version-guarded: reads schema_version and short-circuits
|
|
8
|
+
if already at or beyond the target version.
|
|
9
|
+
- Each migration commits its own transaction (BEGIN IMMEDIATE / COMMIT).
|
|
10
|
+
- Each migration raises RuntimeError on failure so the caller knows the DB is
|
|
11
|
+
in a bad state rather than silently continuing.
|
|
12
|
+
- No external dependencies: stdlib only (sqlite3, logging). Leaf module.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
import sqlite3
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _run_migration_v1_to_v2(conn: sqlite3.Connection) -> None:
|
|
22
|
+
"""Guarded migration: add edges.confidence column if this is a v1 database.
|
|
23
|
+
|
|
24
|
+
Idempotent: if the column already exists (v2+ db or fresh db), PRAGMA table_info
|
|
25
|
+
detects it and the migration is skipped silently.
|
|
26
|
+
|
|
27
|
+
When the column is absent (v1 db), the ALTER TABLE runs exactly once, the
|
|
28
|
+
schema_version is bumped to '2', and ONE info log is emitted recommending a
|
|
29
|
+
re-index (old edges carry DEFAULT 'INFERRED' — conservative, not high-trust).
|
|
30
|
+
|
|
31
|
+
Raises RuntimeError on failure so the caller knows the DB is in a bad state
|
|
32
|
+
rather than silently continuing with a broken schema.
|
|
33
|
+
"""
|
|
34
|
+
try:
|
|
35
|
+
col_names = {row["name"] for row in conn.execute("PRAGMA table_info(edges)").fetchall()}
|
|
36
|
+
if "confidence" not in col_names:
|
|
37
|
+
# v1 db: add column. Legacy edges are conservatively INFERRED (not high-trust);
|
|
38
|
+
# extract under the new resolver via a re-index for accurate tags.
|
|
39
|
+
conn.execute("ALTER TABLE edges ADD COLUMN confidence TEXT NOT NULL DEFAULT 'INFERRED'")
|
|
40
|
+
conn.execute("UPDATE metadata SET value = '2' WHERE key = 'schema_version'")
|
|
41
|
+
# Commit this migration's own work so the chain is order-independent and
|
|
42
|
+
# the ALTER does not rely on a later migration's commit to persist.
|
|
43
|
+
conn.commit()
|
|
44
|
+
logger.info(
|
|
45
|
+
"Migrated Seam index v1->v2 (added edges.confidence). Existing edges marked "
|
|
46
|
+
"INFERRED; run 'seam init' to re-index for accurate confidence tags."
|
|
47
|
+
)
|
|
48
|
+
except Exception as exc: # noqa: BLE001
|
|
49
|
+
raise RuntimeError(
|
|
50
|
+
"Seam DB migration v1->v2 failed; run 'seam init' to rebuild the index"
|
|
51
|
+
) from exc
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _run_migration_v2_to_v3(conn: sqlite3.Connection) -> None:
|
|
55
|
+
"""Guarded migration: bump schema_version from 2 to 3 (adds comments table).
|
|
56
|
+
|
|
57
|
+
The comments table itself is created by the schema script's CREATE TABLE IF NOT EXISTS,
|
|
58
|
+
which runs on every init_db call before this function is reached. This guard only
|
|
59
|
+
bumps schema_version exactly once, and logs an info message telling the user to
|
|
60
|
+
re-index to populate the new table.
|
|
61
|
+
|
|
62
|
+
Idempotent: if schema_version >= 3 already, the migration is skipped silently.
|
|
63
|
+
Fail-loud: raises RuntimeError on any error so the caller knows the DB is bad.
|
|
64
|
+
"""
|
|
65
|
+
try:
|
|
66
|
+
row = conn.execute("SELECT value FROM metadata WHERE key = 'schema_version'").fetchone()
|
|
67
|
+
version = int(row["value"]) if row else 0
|
|
68
|
+
|
|
69
|
+
if version < 3:
|
|
70
|
+
# The comments table was already created by the schema script above.
|
|
71
|
+
# Just bump the version and inform the operator.
|
|
72
|
+
conn.execute("UPDATE metadata SET value = '3' WHERE key = 'schema_version'")
|
|
73
|
+
conn.commit()
|
|
74
|
+
logger.info(
|
|
75
|
+
"Migrated Seam index v%d->v3 (added comments table). Existing indexes "
|
|
76
|
+
"have no comments; run 'seam init' to populate semantic comment data.",
|
|
77
|
+
version,
|
|
78
|
+
)
|
|
79
|
+
except Exception as exc: # noqa: BLE001
|
|
80
|
+
raise RuntimeError(
|
|
81
|
+
"Seam DB migration v2->v3 failed; run 'seam init' to rebuild the index"
|
|
82
|
+
) from exc
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _run_migration_v3_to_v4(conn: sqlite3.Connection) -> None:
|
|
86
|
+
"""Guarded migration: add clusters table + symbols.cluster_id column (v3 → v4).
|
|
87
|
+
|
|
88
|
+
The clusters table is created by the schema script's CREATE TABLE IF NOT EXISTS,
|
|
89
|
+
which runs on every init_db call before this function is reached.
|
|
90
|
+
This guard handles the additive ALTER TABLE for cluster_id on existing symbols
|
|
91
|
+
tables, and bumps schema_version to '4' exactly once.
|
|
92
|
+
|
|
93
|
+
WHY: symbols tables from v1-v3 were created WITHOUT cluster_id. Since
|
|
94
|
+
CREATE TABLE IF NOT EXISTS is idempotent (it skips if the table exists),
|
|
95
|
+
we must ALTER the existing table to add the column. The schema script's
|
|
96
|
+
CREATE TABLE includes cluster_id for new databases, but existing databases
|
|
97
|
+
need the ALTER TABLE path here.
|
|
98
|
+
|
|
99
|
+
Idempotent: if schema_version >= 4 already, skipped silently.
|
|
100
|
+
Fail-loud: raises RuntimeError on any error so the caller knows the DB is bad.
|
|
101
|
+
"""
|
|
102
|
+
try:
|
|
103
|
+
row = conn.execute("SELECT value FROM metadata WHERE key = 'schema_version'").fetchone()
|
|
104
|
+
version = int(row["value"]) if row else 0
|
|
105
|
+
|
|
106
|
+
if version < 4:
|
|
107
|
+
# Add cluster_id column to symbols if absent (existing DBs won't have it).
|
|
108
|
+
col_names = {r["name"] for r in conn.execute("PRAGMA table_info(symbols)").fetchall()}
|
|
109
|
+
if "cluster_id" not in col_names:
|
|
110
|
+
conn.execute("ALTER TABLE symbols ADD COLUMN cluster_id INTEGER")
|
|
111
|
+
|
|
112
|
+
# The clusters table is already created by the schema script above;
|
|
113
|
+
# just bump the version and inform the operator.
|
|
114
|
+
conn.execute("UPDATE metadata SET value = '4' WHERE key = 'schema_version'")
|
|
115
|
+
conn.commit()
|
|
116
|
+
logger.info(
|
|
117
|
+
"Migrated Seam index v%d->v4 (added clusters table + symbols.cluster_id). "
|
|
118
|
+
"Run 'seam init' to compute cluster assignments.",
|
|
119
|
+
version,
|
|
120
|
+
)
|
|
121
|
+
except Exception as exc: # noqa: BLE001
|
|
122
|
+
raise RuntimeError(
|
|
123
|
+
"Seam DB migration v3->v4 failed; run 'seam init' to rebuild the index"
|
|
124
|
+
) from exc
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _run_migration_v4_to_v5(conn: sqlite3.Connection) -> None:
|
|
128
|
+
"""Guarded migration: add Phase 4 node-enrichment columns + rebuild FTS (v4 → v5).
|
|
129
|
+
|
|
130
|
+
Steps (all guarded — idempotent):
|
|
131
|
+
1. ALTER TABLE symbols ADD COLUMN for the 5 new fields (signature, decorators,
|
|
132
|
+
is_exported, visibility, qualified_name). All nullable; existing rows default NULL.
|
|
133
|
+
2. DROP symbols_fts virtual table and its 3 sync triggers. FTS5 columns cannot
|
|
134
|
+
be altered, so the only way to add "signature" is to drop and recreate.
|
|
135
|
+
3. CREATE the new symbols_fts virtual table with columns (name, docstring, signature).
|
|
136
|
+
4. Recreate the 3 sync triggers (symbols_ai, symbols_ad, symbols_au) for the new
|
|
137
|
+
FTS column set.
|
|
138
|
+
5. Repopulate FTS from the symbols content table (INSERT INTO symbols_fts).
|
|
139
|
+
6. Parity check: assert count(symbols_fts) == count(symbols) after repopulation.
|
|
140
|
+
7. Bump schema_version to '5'.
|
|
141
|
+
|
|
142
|
+
ATOMICITY: all structural changes (ALTERs, DROP, CREATE, INSERT) and the version bump
|
|
143
|
+
run inside a SINGLE explicit transaction using BEGIN IMMEDIATE / COMMIT / ROLLBACK.
|
|
144
|
+
Every DDL statement uses conn.execute(), NOT conn.executescript — the batch method
|
|
145
|
+
issues an implicit COMMIT before running, which would make each DDL non-transactional.
|
|
146
|
+
|
|
147
|
+
WHY explicit BEGIN IMMEDIATE:
|
|
148
|
+
Python's sqlite3 module with the default isolation_level wraps DML in implicit
|
|
149
|
+
transactions, but DDL statements (DROP TABLE, CREATE TABLE) cause implicit commits
|
|
150
|
+
of the current Python-managed transaction. To make DDL transactional we must issue
|
|
151
|
+
an explicit BEGIN before any DDL and ROLLBACK on failure. This guarantees that if
|
|
152
|
+
the process dies or raises mid-migration, the DROP TABLE is rolled back and the
|
|
153
|
+
DB is left at v4 with the old FTS intact — not in a half-migrated state.
|
|
154
|
+
|
|
155
|
+
Guarded: runs only when the stored version is < 5.
|
|
156
|
+
Idempotent: each ALTER TABLE is guarded by a PRAGMA table_info check; DROP IF EXISTS
|
|
157
|
+
prevents double-drop; re-running is safe (no-op when already at v5).
|
|
158
|
+
|
|
159
|
+
Raises RuntimeError on any failure so the caller knows the DB is in a bad state.
|
|
160
|
+
"""
|
|
161
|
+
row = conn.execute("SELECT value FROM metadata WHERE key = 'schema_version'").fetchone()
|
|
162
|
+
version = int(row["value"]) if row else 0
|
|
163
|
+
|
|
164
|
+
if version >= 5:
|
|
165
|
+
return # Already at v5 or newer — skip migration entirely.
|
|
166
|
+
|
|
167
|
+
# Capture symbol count BEFORE starting the transaction for observability log.
|
|
168
|
+
symbol_count = conn.execute("SELECT COUNT(*) FROM symbols").fetchone()[0]
|
|
169
|
+
logger.info(
|
|
170
|
+
"Starting v%d->v5 migration — rebuilding FTS5 for %d symbols "
|
|
171
|
+
"(adding signature column). DO NOT interrupt.",
|
|
172
|
+
version,
|
|
173
|
+
symbol_count,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# Start an explicit transaction BEFORE any DDL (DROP/CREATE TABLE/triggers).
|
|
177
|
+
# Python sqlite3's implicit transaction management commits pending state on DDL;
|
|
178
|
+
# an explicit BEGIN makes the DDL itself transactional so ROLLBACK reverts it.
|
|
179
|
+
conn.execute("BEGIN IMMEDIATE")
|
|
180
|
+
try:
|
|
181
|
+
# Step 1: Add the 5 new columns if they don't already exist.
|
|
182
|
+
# Each ALTER is individually guarded so repeated calls don't fail on "duplicate column name".
|
|
183
|
+
col_names = {r["name"] for r in conn.execute("PRAGMA table_info(symbols)").fetchall()}
|
|
184
|
+
|
|
185
|
+
new_cols = [
|
|
186
|
+
("signature", "TEXT"),
|
|
187
|
+
("decorators", "TEXT"), # JSON-encoded list of strings
|
|
188
|
+
("is_exported", "INTEGER"), # 0/1/NULL — SQLite stores bool as integer
|
|
189
|
+
("visibility", "TEXT"),
|
|
190
|
+
("qualified_name", "TEXT"),
|
|
191
|
+
]
|
|
192
|
+
for col, col_type in new_cols:
|
|
193
|
+
if col not in col_names:
|
|
194
|
+
conn.execute(f"ALTER TABLE symbols ADD COLUMN {col} {col_type}")
|
|
195
|
+
|
|
196
|
+
# Step 2: Drop the old FTS5 table and its triggers.
|
|
197
|
+
# FTS5 virtual tables cannot be altered (no ALTER VIRTUAL TABLE support in SQLite),
|
|
198
|
+
# so adding the 'signature' column requires a full DROP + CREATE cycle. Each DDL
|
|
199
|
+
# uses conn.execute() individually — the batch alternative would issue an implicit
|
|
200
|
+
# COMMIT, breaking the surrounding BEGIN IMMEDIATE transaction.
|
|
201
|
+
conn.execute("DROP TRIGGER IF EXISTS symbols_ai")
|
|
202
|
+
conn.execute("DROP TRIGGER IF EXISTS symbols_ad")
|
|
203
|
+
conn.execute("DROP TRIGGER IF EXISTS symbols_au")
|
|
204
|
+
conn.execute("DROP TABLE IF EXISTS symbols_fts")
|
|
205
|
+
|
|
206
|
+
# Step 3: Recreate symbols_fts with the new column set (name, docstring, signature).
|
|
207
|
+
conn.execute("""
|
|
208
|
+
CREATE VIRTUAL TABLE symbols_fts USING fts5(
|
|
209
|
+
name,
|
|
210
|
+
docstring,
|
|
211
|
+
signature,
|
|
212
|
+
content='symbols',
|
|
213
|
+
content_rowid='id'
|
|
214
|
+
)
|
|
215
|
+
""")
|
|
216
|
+
|
|
217
|
+
# Step 4: Recreate the 3 sync triggers (INSERT/DELETE/UPDATE) for the new column set.
|
|
218
|
+
# Individual execute() calls for the same reason as step 2: the batch alternative
|
|
219
|
+
# commits implicitly, breaking the surrounding BEGIN IMMEDIATE transaction.
|
|
220
|
+
conn.execute("""
|
|
221
|
+
CREATE TRIGGER symbols_ai AFTER INSERT ON symbols BEGIN
|
|
222
|
+
INSERT INTO symbols_fts(rowid, name, docstring, signature)
|
|
223
|
+
VALUES (new.id, new.name, new.docstring, new.signature);
|
|
224
|
+
END
|
|
225
|
+
""")
|
|
226
|
+
conn.execute("""
|
|
227
|
+
CREATE TRIGGER symbols_ad AFTER DELETE ON symbols BEGIN
|
|
228
|
+
INSERT INTO symbols_fts(symbols_fts, rowid, name, docstring, signature)
|
|
229
|
+
VALUES ('delete', old.id, old.name, old.docstring, old.signature);
|
|
230
|
+
END
|
|
231
|
+
""")
|
|
232
|
+
conn.execute("""
|
|
233
|
+
CREATE TRIGGER symbols_au AFTER UPDATE ON symbols BEGIN
|
|
234
|
+
INSERT INTO symbols_fts(symbols_fts, rowid, name, docstring, signature)
|
|
235
|
+
VALUES ('delete', old.id, old.name, old.docstring, old.signature);
|
|
236
|
+
INSERT INTO symbols_fts(rowid, name, docstring, signature)
|
|
237
|
+
VALUES (new.id, new.name, new.docstring, new.signature);
|
|
238
|
+
END
|
|
239
|
+
""")
|
|
240
|
+
|
|
241
|
+
# Step 5: Repopulate FTS from the symbols content table.
|
|
242
|
+
# Pre-migration rows have signature=NULL — FTS5 treats NULL as an empty token
|
|
243
|
+
# list for that column, so existing symbols are still searchable by name/docstring.
|
|
244
|
+
conn.execute("""
|
|
245
|
+
INSERT INTO symbols_fts(rowid, name, docstring, signature)
|
|
246
|
+
SELECT id, name, docstring, signature FROM symbols
|
|
247
|
+
""")
|
|
248
|
+
|
|
249
|
+
# Step 6: Parity check — abort and roll back if FTS wasn't fully populated.
|
|
250
|
+
# A partial repopulation (e.g. interrupted INSERT) silently drops search hits:
|
|
251
|
+
# FTS would return results but miss any symbol whose row was never inserted.
|
|
252
|
+
# Failing here ensures the ROLLBACK restores the old FTS rather than leaving
|
|
253
|
+
# the index in a silently degraded state.
|
|
254
|
+
fts_count = conn.execute("SELECT COUNT(*) FROM symbols_fts").fetchone()[0]
|
|
255
|
+
if fts_count != symbol_count:
|
|
256
|
+
raise RuntimeError(
|
|
257
|
+
f"FTS repopulation parity mismatch: symbols={symbol_count}, "
|
|
258
|
+
f"symbols_fts={fts_count}. Run 'seam init' to rebuild the index."
|
|
259
|
+
)
|
|
260
|
+
logger.info(
|
|
261
|
+
"v4->v5 migration: FTS repopulated with %d rows (parity OK).",
|
|
262
|
+
fts_count,
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
# Step 7: Bump schema_version to '5' as the very last step before commit.
|
|
266
|
+
# Placing this last ensures the version is only advanced when all structural
|
|
267
|
+
# changes have succeeded — a process crash between step 6 and here is safe
|
|
268
|
+
# because the ROLLBACK in the except block reverts everything, leaving the DB
|
|
269
|
+
# at v4 so this migration reruns cleanly on the next connect().
|
|
270
|
+
conn.execute("UPDATE metadata SET value = '5' WHERE key = 'schema_version'")
|
|
271
|
+
conn.execute("COMMIT")
|
|
272
|
+
|
|
273
|
+
logger.info(
|
|
274
|
+
"Migrated Seam index v%d->v5 (added Phase 4 node enrichment fields: "
|
|
275
|
+
"signature, decorators, is_exported, visibility, qualified_name; "
|
|
276
|
+
"FTS5 rebuilt to index signature). Run 'seam init' to populate new fields.",
|
|
277
|
+
version,
|
|
278
|
+
)
|
|
279
|
+
except Exception as exc: # noqa: BLE001
|
|
280
|
+
# Roll back all structural changes (DROP, CREATE, ALTER, INSERT) so the DB
|
|
281
|
+
# is left at v4 with the old FTS intact — not in a half-migrated state.
|
|
282
|
+
try:
|
|
283
|
+
conn.execute("ROLLBACK")
|
|
284
|
+
except Exception: # noqa: BLE001
|
|
285
|
+
pass # Best-effort rollback; if this fails the DB may need a full reinit.
|
|
286
|
+
raise RuntimeError(
|
|
287
|
+
"Seam DB migration v4->v5 failed; run 'seam init' to rebuild the index"
|
|
288
|
+
) from exc
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _run_migration_v5_to_v6(conn: sqlite3.Connection) -> None:
|
|
292
|
+
"""Guarded migration: add import_mappings table (v5 → v6).
|
|
293
|
+
|
|
294
|
+
Additive-only: creates import_mappings table and two indexes if absent.
|
|
295
|
+
Does NOT backfill existing rows — only `seam init` / watcher re-index
|
|
296
|
+
populates mappings. Until then, resolution silently falls back to the
|
|
297
|
+
name-count rule (documented gotcha, mirrors the Phase 4 backfill caveat).
|
|
298
|
+
|
|
299
|
+
Steps:
|
|
300
|
+
1. CREATE TABLE IF NOT EXISTS import_mappings (idempotent).
|
|
301
|
+
2. CREATE INDEX IF NOT EXISTS for file_id and local_name (idempotent).
|
|
302
|
+
3. Bump schema_version to '6'.
|
|
303
|
+
|
|
304
|
+
Guarded: runs only when stored version < 6.
|
|
305
|
+
Idempotent: CREATE TABLE IF NOT EXISTS / CREATE INDEX IF NOT EXISTS are safe
|
|
306
|
+
on repeated calls. The version guard prevents double-bumping.
|
|
307
|
+
Fresh-DB-safe: a brand-new DB seeded with schema_version='6' returns early.
|
|
308
|
+
|
|
309
|
+
Uses BEGIN IMMEDIATE / COMMIT for atomicity (consistent with v4→v5 pattern).
|
|
310
|
+
Raises RuntimeError on failure so caller knows the DB is in a bad state.
|
|
311
|
+
"""
|
|
312
|
+
try:
|
|
313
|
+
row = conn.execute("SELECT value FROM metadata WHERE key='schema_version'").fetchone()
|
|
314
|
+
version = int(row["value"]) if row else 0
|
|
315
|
+
|
|
316
|
+
if version >= 6:
|
|
317
|
+
return # Already at v6 or newer — no-op.
|
|
318
|
+
|
|
319
|
+
conn.execute("BEGIN IMMEDIATE")
|
|
320
|
+
try:
|
|
321
|
+
# Step 1: Create import_mappings table (CREATE TABLE IF NOT EXISTS is idempotent).
|
|
322
|
+
conn.execute("""
|
|
323
|
+
CREATE TABLE IF NOT EXISTS import_mappings (
|
|
324
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
325
|
+
file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
|
|
326
|
+
local_name TEXT NOT NULL,
|
|
327
|
+
exported_name TEXT NOT NULL,
|
|
328
|
+
source_module TEXT NOT NULL,
|
|
329
|
+
is_default INTEGER NOT NULL DEFAULT 0,
|
|
330
|
+
is_namespace INTEGER NOT NULL DEFAULT 0,
|
|
331
|
+
is_wildcard INTEGER NOT NULL DEFAULT 0,
|
|
332
|
+
line INTEGER NOT NULL
|
|
333
|
+
)
|
|
334
|
+
""")
|
|
335
|
+
|
|
336
|
+
# Step 2: Create indexes (CREATE INDEX IF NOT EXISTS is idempotent).
|
|
337
|
+
conn.execute("""
|
|
338
|
+
CREATE INDEX IF NOT EXISTS idx_import_mappings_file_id
|
|
339
|
+
ON import_mappings(file_id)
|
|
340
|
+
""")
|
|
341
|
+
conn.execute("""
|
|
342
|
+
CREATE INDEX IF NOT EXISTS idx_import_mappings_local_name
|
|
343
|
+
ON import_mappings(local_name)
|
|
344
|
+
""")
|
|
345
|
+
|
|
346
|
+
# Step 3: Bump schema_version to '6' as the last step before commit.
|
|
347
|
+
conn.execute("UPDATE metadata SET value = '6' WHERE key = 'schema_version'")
|
|
348
|
+
conn.execute("COMMIT")
|
|
349
|
+
|
|
350
|
+
logger.info(
|
|
351
|
+
"Migrated Seam index v%d->v6 (added import_mappings table). "
|
|
352
|
+
"Run 'seam init' to populate import mappings for existing files.",
|
|
353
|
+
version,
|
|
354
|
+
)
|
|
355
|
+
except Exception as exc: # noqa: BLE001
|
|
356
|
+
try:
|
|
357
|
+
conn.execute("ROLLBACK")
|
|
358
|
+
except Exception: # noqa: BLE001
|
|
359
|
+
pass
|
|
360
|
+
raise RuntimeError(
|
|
361
|
+
"Seam DB migration v5->v6 failed; run 'seam init' to rebuild the index"
|
|
362
|
+
) from exc
|
|
363
|
+
except RuntimeError:
|
|
364
|
+
raise
|
|
365
|
+
except Exception as exc: # noqa: BLE001
|
|
366
|
+
raise RuntimeError(
|
|
367
|
+
"Seam DB migration v5->v6 failed; run 'seam init' to rebuild the index"
|
|
368
|
+
) from exc
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _run_migration_v6_to_v7(conn: sqlite3.Connection) -> None:
|
|
372
|
+
"""Guarded migration: add embeddings table (v6 → v7).
|
|
373
|
+
|
|
374
|
+
Additive-only: creates the embeddings table if absent.
|
|
375
|
+
Does NOT backfill — the table is populated ONLY by `seam init --semantic`.
|
|
376
|
+
Until then, read paths that check for embeddings will find an empty table
|
|
377
|
+
and degrade gracefully to FTS5-only (documented gotcha).
|
|
378
|
+
|
|
379
|
+
Steps:
|
|
380
|
+
1. CREATE TABLE IF NOT EXISTS embeddings (idempotent).
|
|
381
|
+
2. Bump schema_version to '7'.
|
|
382
|
+
|
|
383
|
+
Guarded: runs only when stored version < 7.
|
|
384
|
+
Idempotent: CREATE TABLE IF NOT EXISTS is safe on repeated calls.
|
|
385
|
+
The version guard prevents double-bumping.
|
|
386
|
+
Fresh-DB-safe: a brand-new DB seeded with schema_version='7' returns early.
|
|
387
|
+
|
|
388
|
+
Uses BEGIN IMMEDIATE / COMMIT for atomicity (consistent with v5→v6 pattern).
|
|
389
|
+
Raises RuntimeError on failure so caller knows the DB is in a bad state.
|
|
390
|
+
"""
|
|
391
|
+
try:
|
|
392
|
+
row = conn.execute("SELECT value FROM metadata WHERE key='schema_version'").fetchone()
|
|
393
|
+
version = int(row["value"]) if row else 0
|
|
394
|
+
|
|
395
|
+
if version >= 7:
|
|
396
|
+
return # Already at v7 or newer — no-op.
|
|
397
|
+
|
|
398
|
+
conn.execute("BEGIN IMMEDIATE")
|
|
399
|
+
try:
|
|
400
|
+
# Step 1: Create embeddings table (CREATE TABLE IF NOT EXISTS is idempotent).
|
|
401
|
+
# symbol_id is PRIMARY KEY (one embedding per symbol row) and a FK to
|
|
402
|
+
# symbols(id) with ON DELETE CASCADE so re-indexing a file automatically
|
|
403
|
+
# removes stale embeddings for deleted/replaced symbol rows.
|
|
404
|
+
# Vector stored as float32 bytes (numpy.array(..., dtype=np.float32).tobytes()).
|
|
405
|
+
conn.execute("""
|
|
406
|
+
CREATE TABLE IF NOT EXISTS embeddings (
|
|
407
|
+
symbol_id INTEGER PRIMARY KEY REFERENCES symbols(id) ON DELETE CASCADE,
|
|
408
|
+
model TEXT NOT NULL,
|
|
409
|
+
dim INTEGER NOT NULL,
|
|
410
|
+
vector BLOB NOT NULL
|
|
411
|
+
)
|
|
412
|
+
""")
|
|
413
|
+
|
|
414
|
+
# Step 2: Bump schema_version to '7' as the last step before commit.
|
|
415
|
+
conn.execute("UPDATE metadata SET value = '7' WHERE key = 'schema_version'")
|
|
416
|
+
conn.execute("COMMIT")
|
|
417
|
+
|
|
418
|
+
logger.info(
|
|
419
|
+
"Migrated Seam index v%d->v7 (added embeddings table for semantic search). "
|
|
420
|
+
"Run 'seam init --semantic' to populate embeddings.",
|
|
421
|
+
version,
|
|
422
|
+
)
|
|
423
|
+
except Exception as exc: # noqa: BLE001
|
|
424
|
+
try:
|
|
425
|
+
conn.execute("ROLLBACK")
|
|
426
|
+
except Exception: # noqa: BLE001
|
|
427
|
+
pass
|
|
428
|
+
raise RuntimeError(
|
|
429
|
+
"Seam DB migration v6->v7 failed; run 'seam init' to rebuild the index"
|
|
430
|
+
) from exc
|
|
431
|
+
except RuntimeError:
|
|
432
|
+
raise
|
|
433
|
+
except Exception as exc: # noqa: BLE001
|
|
434
|
+
raise RuntimeError(
|
|
435
|
+
"Seam DB migration v6->v7 failed; run 'seam init' to rebuild the index"
|
|
436
|
+
) from exc
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _run_migration_v7_to_v8(conn: sqlite3.Connection) -> None:
|
|
440
|
+
"""Guarded migration: add clusters.cohesion column (v7 → v8).
|
|
441
|
+
|
|
442
|
+
Additive-only: adds a nullable REAL column to the clusters table if absent.
|
|
443
|
+
Does NOT backfill — cohesion stays NULL on existing cluster rows until the
|
|
444
|
+
next `seam init` recomputes clusters (which writes the value). Until then,
|
|
445
|
+
the search rescore path treats NULL cohesion as "no bonus" (byte-identical
|
|
446
|
+
ranking), so the additive migration changes nothing for existing indexes.
|
|
447
|
+
|
|
448
|
+
Steps:
|
|
449
|
+
1. ALTER TABLE clusters ADD COLUMN cohesion REAL (guarded by table_info).
|
|
450
|
+
2. Bump schema_version to '8'.
|
|
451
|
+
|
|
452
|
+
Guarded: runs only when stored version < 8.
|
|
453
|
+
Idempotent: the PRAGMA table_info check skips the ALTER when the column
|
|
454
|
+
already exists; the version guard prevents double-bumping.
|
|
455
|
+
Fresh-DB-safe: a brand-new DB seeded with schema_version='8' returns early.
|
|
456
|
+
|
|
457
|
+
Uses BEGIN IMMEDIATE / COMMIT for atomicity (consistent with v6→v7 pattern).
|
|
458
|
+
Raises RuntimeError on failure so caller knows the DB is in a bad state.
|
|
459
|
+
"""
|
|
460
|
+
try:
|
|
461
|
+
row = conn.execute("SELECT value FROM metadata WHERE key='schema_version'").fetchone()
|
|
462
|
+
version = int(row["value"]) if row else 0
|
|
463
|
+
|
|
464
|
+
if version >= 8:
|
|
465
|
+
return # Already at v8 or newer — no-op.
|
|
466
|
+
|
|
467
|
+
conn.execute("BEGIN IMMEDIATE")
|
|
468
|
+
try:
|
|
469
|
+
# Step 1: add the nullable cohesion column if it does not already exist.
|
|
470
|
+
col_names = {
|
|
471
|
+
r["name"] for r in conn.execute("PRAGMA table_info(clusters)").fetchall()
|
|
472
|
+
}
|
|
473
|
+
if "cohesion" not in col_names:
|
|
474
|
+
conn.execute("ALTER TABLE clusters ADD COLUMN cohesion REAL")
|
|
475
|
+
|
|
476
|
+
# Step 2: bump schema_version to '8' as the last step before commit.
|
|
477
|
+
conn.execute("UPDATE metadata SET value = '8' WHERE key = 'schema_version'")
|
|
478
|
+
conn.execute("COMMIT")
|
|
479
|
+
|
|
480
|
+
logger.info(
|
|
481
|
+
"Migrated Seam index v%d->v8 (added clusters.cohesion column). "
|
|
482
|
+
"Run 'seam init' to populate cohesion for existing clusters.",
|
|
483
|
+
version,
|
|
484
|
+
)
|
|
485
|
+
except Exception as exc: # noqa: BLE001
|
|
486
|
+
try:
|
|
487
|
+
conn.execute("ROLLBACK")
|
|
488
|
+
except Exception: # noqa: BLE001
|
|
489
|
+
pass
|
|
490
|
+
raise RuntimeError(
|
|
491
|
+
"Seam DB migration v7->v8 failed; run 'seam init' to rebuild the index"
|
|
492
|
+
) from exc
|
|
493
|
+
except RuntimeError:
|
|
494
|
+
raise
|
|
495
|
+
except Exception as exc: # noqa: BLE001
|
|
496
|
+
raise RuntimeError(
|
|
497
|
+
"Seam DB migration v7->v8 failed; run 'seam init' to rebuild the index"
|
|
498
|
+
) from exc
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def _run_migration_v8_to_v9(conn: sqlite3.Connection) -> None:
|
|
502
|
+
"""Guarded migration: add symbols.entry_score column (v8 → v9).
|
|
503
|
+
|
|
504
|
+
Additive-only: adds a nullable REAL column to the symbols table if absent.
|
|
505
|
+
Does NOT backfill — entry_score stays NULL on existing symbol rows until the
|
|
506
|
+
next `seam init` re-indexes (upsert_file computes the score from the file
|
|
507
|
+
path pattern + decorator text). Until then, list_entry_points() treats NULL
|
|
508
|
+
entry_score as the neutral baseline (1.0), so the additive migration changes
|
|
509
|
+
nothing for existing indexes (byte-identical ranking).
|
|
510
|
+
|
|
511
|
+
Steps:
|
|
512
|
+
1. ALTER TABLE symbols ADD COLUMN entry_score REAL (guarded by table_info).
|
|
513
|
+
2. Bump schema_version to '9'.
|
|
514
|
+
|
|
515
|
+
Guarded: runs only when stored version < 9.
|
|
516
|
+
Idempotent: the PRAGMA table_info check skips the ALTER when the column
|
|
517
|
+
already exists; the version guard prevents double-bumping.
|
|
518
|
+
Fresh-DB-safe: a brand-new DB seeded with schema_version='9' returns early.
|
|
519
|
+
|
|
520
|
+
Uses BEGIN IMMEDIATE / COMMIT for atomicity (consistent with v7→v8 pattern).
|
|
521
|
+
Raises RuntimeError on failure so caller knows the DB is in a bad state.
|
|
522
|
+
"""
|
|
523
|
+
try:
|
|
524
|
+
row = conn.execute("SELECT value FROM metadata WHERE key='schema_version'").fetchone()
|
|
525
|
+
version = int(row["value"]) if row else 0
|
|
526
|
+
|
|
527
|
+
if version >= 9:
|
|
528
|
+
return # Already at v9 or newer — no-op.
|
|
529
|
+
|
|
530
|
+
conn.execute("BEGIN IMMEDIATE")
|
|
531
|
+
try:
|
|
532
|
+
# Step 1: add the nullable entry_score column if it does not already exist.
|
|
533
|
+
col_names = {
|
|
534
|
+
r["name"] for r in conn.execute("PRAGMA table_info(symbols)").fetchall()
|
|
535
|
+
}
|
|
536
|
+
if "entry_score" not in col_names:
|
|
537
|
+
conn.execute("ALTER TABLE symbols ADD COLUMN entry_score REAL")
|
|
538
|
+
|
|
539
|
+
# Step 2: bump schema_version to '9' as the last step before commit.
|
|
540
|
+
conn.execute("UPDATE metadata SET value = '9' WHERE key = 'schema_version'")
|
|
541
|
+
conn.execute("COMMIT")
|
|
542
|
+
|
|
543
|
+
logger.info(
|
|
544
|
+
"Migrated Seam index v%d->v9 (added symbols.entry_score column). "
|
|
545
|
+
"Run 'seam init' to populate entry_score for framework entry-point ranking.",
|
|
546
|
+
version,
|
|
547
|
+
)
|
|
548
|
+
except Exception as exc: # noqa: BLE001
|
|
549
|
+
try:
|
|
550
|
+
conn.execute("ROLLBACK")
|
|
551
|
+
except Exception: # noqa: BLE001
|
|
552
|
+
pass
|
|
553
|
+
raise RuntimeError(
|
|
554
|
+
"Seam DB migration v8->v9 failed; run 'seam init' to rebuild the index"
|
|
555
|
+
) from exc
|
|
556
|
+
except RuntimeError:
|
|
557
|
+
raise
|
|
558
|
+
except Exception as exc: # noqa: BLE001
|
|
559
|
+
raise RuntimeError(
|
|
560
|
+
"Seam DB migration v8->v9 failed; run 'seam init' to rebuild the index"
|
|
561
|
+
) from exc
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def _run_migration_v9_to_v10(conn: sqlite3.Connection) -> None:
|
|
565
|
+
"""Guarded migration: add edges.receiver column (v9 → v10).
|
|
566
|
+
|
|
567
|
+
Tier B B1 addition: captures the raw receiver expression text from attribute-call
|
|
568
|
+
edges (e.g., `recv.method()` → receiver='recv'). This enables later receiver-type
|
|
569
|
+
inference (Tier B slices B2+) without requiring another schema change.
|
|
570
|
+
|
|
571
|
+
Additive-only: adds a nullable TEXT column to the edges table if absent.
|
|
572
|
+
Does NOT backfill — receiver stays NULL on existing rows until re-index.
|
|
573
|
+
The null-contract mirrors Phase 4/5 fields: NULL means "not yet captured" or
|
|
574
|
+
"not applicable" (import edges, bare calls), not "has no receiver".
|
|
575
|
+
|
|
576
|
+
Steps:
|
|
577
|
+
1. ALTER TABLE edges ADD COLUMN receiver TEXT (guarded by table_info).
|
|
578
|
+
2. Bump schema_version to '10'.
|
|
579
|
+
|
|
580
|
+
Guarded: runs only when stored version < 10.
|
|
581
|
+
Idempotent: the PRAGMA table_info check skips the ALTER when the column
|
|
582
|
+
already exists; the version guard prevents double-bumping.
|
|
583
|
+
Fresh-DB-safe: a brand-new DB seeded with schema_version='10' returns early.
|
|
584
|
+
Uses BEGIN IMMEDIATE / COMMIT for atomicity (consistent with v8→v9 pattern).
|
|
585
|
+
Raises RuntimeError on failure so caller knows the DB is in a bad state.
|
|
586
|
+
"""
|
|
587
|
+
try:
|
|
588
|
+
row = conn.execute("SELECT value FROM metadata WHERE key='schema_version'").fetchone()
|
|
589
|
+
version = int(row["value"]) if row else 0
|
|
590
|
+
|
|
591
|
+
if version >= 10:
|
|
592
|
+
return # Already at v10 or newer — no-op.
|
|
593
|
+
|
|
594
|
+
conn.execute("BEGIN IMMEDIATE")
|
|
595
|
+
try:
|
|
596
|
+
# Step 1: add the nullable receiver column if it does not already exist.
|
|
597
|
+
# Guarded by table_info to make repeated calls safe (idempotent).
|
|
598
|
+
col_names = {
|
|
599
|
+
r["name"] for r in conn.execute("PRAGMA table_info(edges)").fetchall()
|
|
600
|
+
}
|
|
601
|
+
if "receiver" not in col_names:
|
|
602
|
+
conn.execute("ALTER TABLE edges ADD COLUMN receiver TEXT")
|
|
603
|
+
|
|
604
|
+
# Step 2: bump schema_version to '10' as the last step before commit.
|
|
605
|
+
# Placing this last guarantees the version is only advanced when the
|
|
606
|
+
# structural change has succeeded.
|
|
607
|
+
conn.execute("UPDATE metadata SET value = '10' WHERE key = 'schema_version'")
|
|
608
|
+
conn.execute("COMMIT")
|
|
609
|
+
|
|
610
|
+
logger.info(
|
|
611
|
+
"Migrated Seam index v%d->v10 (added edges.receiver column for Tier B "
|
|
612
|
+
"receiver-type inference). Run 'seam init' to populate receiver for "
|
|
613
|
+
"existing Python attribute calls.",
|
|
614
|
+
version,
|
|
615
|
+
)
|
|
616
|
+
except Exception as exc: # noqa: BLE001
|
|
617
|
+
try:
|
|
618
|
+
conn.execute("ROLLBACK")
|
|
619
|
+
except Exception: # noqa: BLE001
|
|
620
|
+
pass
|
|
621
|
+
raise RuntimeError(
|
|
622
|
+
"Seam DB migration v9->v10 failed; run 'seam init' to rebuild the index"
|
|
623
|
+
) from exc
|
|
624
|
+
except RuntimeError:
|
|
625
|
+
raise
|
|
626
|
+
except Exception as exc: # noqa: BLE001
|
|
627
|
+
raise RuntimeError(
|
|
628
|
+
"Seam DB migration v9->v10 failed; run 'seam init' to rebuild the index"
|
|
629
|
+
) from exc
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def _run_migration_v11_to_v12(conn: sqlite3.Connection) -> None:
|
|
633
|
+
"""Guarded migration: add edges.synthesized_by column (v11 → v12).
|
|
634
|
+
|
|
635
|
+
Edge-synthesis post-pass addition: captures which synthesis channel produced an
|
|
636
|
+
edge (e.g. 'interface-override'). NULL = statically extracted by a parser;
|
|
637
|
+
a channel name string = synthesized by the post-pass. Provenance is derived:
|
|
638
|
+
synthesized_by IS NOT NULL ⟹ heuristic. No separate provenance column needed.
|
|
639
|
+
|
|
640
|
+
Additive-only: adds a nullable TEXT column to the edges table if absent.
|
|
641
|
+
Does NOT backfill — synthesized_by stays NULL on existing rows. Synthesized
|
|
642
|
+
edges appear only after the next full `seam init` (explicit backfill, same
|
|
643
|
+
null-contract as prior enrichment columns). Toggling SEAM_EDGE_SYNTHESIS
|
|
644
|
+
requires a re-index to take effect.
|
|
645
|
+
|
|
646
|
+
Steps:
|
|
647
|
+
1. ALTER TABLE edges ADD COLUMN synthesized_by TEXT (guarded by table_info).
|
|
648
|
+
2. Bump schema_version to '12'.
|
|
649
|
+
|
|
650
|
+
Guarded: runs only when stored version < 12.
|
|
651
|
+
Idempotent: the PRAGMA table_info check skips the ALTER when the column
|
|
652
|
+
already exists; the version guard prevents double-bumping.
|
|
653
|
+
Fresh-DB-safe: a brand-new DB seeded with schema_version='12' returns early.
|
|
654
|
+
Uses BEGIN IMMEDIATE / COMMIT for atomicity (consistent with v9→v10 pattern).
|
|
655
|
+
Raises RuntimeError on failure so caller knows the DB is in a bad state.
|
|
656
|
+
"""
|
|
657
|
+
try:
|
|
658
|
+
row = conn.execute("SELECT value FROM metadata WHERE key='schema_version'").fetchone()
|
|
659
|
+
version = int(row["value"]) if row else 0
|
|
660
|
+
|
|
661
|
+
if version >= 12:
|
|
662
|
+
return # Already at v12 or newer — no-op.
|
|
663
|
+
|
|
664
|
+
conn.execute("BEGIN IMMEDIATE")
|
|
665
|
+
try:
|
|
666
|
+
# Step 1: add the nullable synthesized_by column if it does not already exist.
|
|
667
|
+
# Guarded by table_info to make repeated calls safe (idempotent).
|
|
668
|
+
col_names = {
|
|
669
|
+
r["name"] for r in conn.execute("PRAGMA table_info(edges)").fetchall()
|
|
670
|
+
}
|
|
671
|
+
if "synthesized_by" not in col_names:
|
|
672
|
+
conn.execute("ALTER TABLE edges ADD COLUMN synthesized_by TEXT")
|
|
673
|
+
|
|
674
|
+
# Step 2: bump schema_version to '12' as the last step before commit.
|
|
675
|
+
# Placing this last guarantees the version is only advanced when the
|
|
676
|
+
# structural change has succeeded.
|
|
677
|
+
conn.execute("UPDATE metadata SET value = '12' WHERE key = 'schema_version'")
|
|
678
|
+
conn.execute("COMMIT")
|
|
679
|
+
|
|
680
|
+
logger.info(
|
|
681
|
+
"Migrated Seam index v%d->v12 (added edges.synthesized_by column for "
|
|
682
|
+
"edge-synthesis post-pass provenance). Run 'seam init' to populate "
|
|
683
|
+
"synthesized_by for synthesized edges.",
|
|
684
|
+
version,
|
|
685
|
+
)
|
|
686
|
+
except Exception as exc: # noqa: BLE001
|
|
687
|
+
try:
|
|
688
|
+
conn.execute("ROLLBACK")
|
|
689
|
+
except Exception: # noqa: BLE001
|
|
690
|
+
pass
|
|
691
|
+
raise RuntimeError(
|
|
692
|
+
"Seam DB migration v11->v12 failed; run 'seam init' to rebuild the index"
|
|
693
|
+
) from exc
|
|
694
|
+
except RuntimeError:
|
|
695
|
+
raise
|
|
696
|
+
except Exception as exc: # noqa: BLE001
|
|
697
|
+
raise RuntimeError(
|
|
698
|
+
"Seam DB migration v11->v12 failed; run 'seam init' to rebuild the index"
|
|
699
|
+
) from exc
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
def _run_migration_v10_to_v11(conn: sqlite3.Connection) -> None:
|
|
703
|
+
"""Guarded migration: add symbols.search_text + a 4th symbols_fts column (v10 → v11).
|
|
704
|
+
|
|
705
|
+
Tier D #12: identifier compound-split tokenization. Adds a nullable search_text column
|
|
706
|
+
holding camelCase/snake_case-split tokens, surfaced as a 4th FTS5 column so that a
|
|
707
|
+
natural-language query matches a camelCase identifier (the unicode61 tokenizer does not
|
|
708
|
+
split camelCase). Changing the FTS column set requires DROPping and recreating the
|
|
709
|
+
symbols_fts virtual table + its 3 sync triggers, then a 'rebuild'.
|
|
710
|
+
|
|
711
|
+
Additive-only / null-contract: search_text is NULL on existing rows after the rebuild —
|
|
712
|
+
full split-token recall arrives only after a `seam init` re-index. Until then, search
|
|
713
|
+
behaves exactly as before (the name/docstring/signature columns are unchanged).
|
|
714
|
+
|
|
715
|
+
Steps:
|
|
716
|
+
1. ALTER TABLE symbols ADD COLUMN search_text TEXT (guarded by table_info).
|
|
717
|
+
2. DROP the 3 sync triggers + symbols_fts; recreate symbols_fts with 4 columns and
|
|
718
|
+
the 3 triggers; INSERT ... VALUES('rebuild') to repopulate from the content table.
|
|
719
|
+
3. Bump schema_version to '11'.
|
|
720
|
+
|
|
721
|
+
Guarded (version < 11), idempotent (table_info guard + version guard), fresh-DB-safe
|
|
722
|
+
(a new DB seeded with '11' returns early). BEGIN IMMEDIATE / COMMIT for atomicity.
|
|
723
|
+
Raises RuntimeError on failure.
|
|
724
|
+
"""
|
|
725
|
+
try:
|
|
726
|
+
row = conn.execute("SELECT value FROM metadata WHERE key='schema_version'").fetchone()
|
|
727
|
+
version = int(row["value"]) if row else 0
|
|
728
|
+
|
|
729
|
+
if version >= 11:
|
|
730
|
+
return # Already at v11 or newer — no-op.
|
|
731
|
+
|
|
732
|
+
conn.execute("BEGIN IMMEDIATE")
|
|
733
|
+
try:
|
|
734
|
+
# Step 1: add the nullable search_text column if absent (idempotent).
|
|
735
|
+
col_names = {
|
|
736
|
+
r["name"] for r in conn.execute("PRAGMA table_info(symbols)").fetchall()
|
|
737
|
+
}
|
|
738
|
+
if "search_text" not in col_names:
|
|
739
|
+
conn.execute("ALTER TABLE symbols ADD COLUMN search_text TEXT")
|
|
740
|
+
|
|
741
|
+
# Step 2: rebuild the FTS table with the new column set. The FTS5 column list
|
|
742
|
+
# is fixed at creation, so a column addition requires a full drop+recreate.
|
|
743
|
+
# Triggers must be dropped first (they reference the old column list).
|
|
744
|
+
# The 'rebuild' below re-tokenizes every symbol; on a large index this is a
|
|
745
|
+
# multi-second write under BEGIN IMMEDIATE, and it fires on the FIRST process to
|
|
746
|
+
# open the DB after upgrade (often a read command). Log up front so a momentarily
|
|
747
|
+
# slow `seam query`/`start` right after upgrading is explainable, not mysterious.
|
|
748
|
+
logger.info(
|
|
749
|
+
"Migrating Seam index v%d->v11: rebuilding FTS index for camelCase search "
|
|
750
|
+
"(one-time; may take a moment on a large index)...",
|
|
751
|
+
version,
|
|
752
|
+
)
|
|
753
|
+
conn.execute("DROP TRIGGER IF EXISTS symbols_ai")
|
|
754
|
+
conn.execute("DROP TRIGGER IF EXISTS symbols_ad")
|
|
755
|
+
conn.execute("DROP TRIGGER IF EXISTS symbols_au")
|
|
756
|
+
conn.execute("DROP TABLE IF EXISTS symbols_fts")
|
|
757
|
+
conn.execute(
|
|
758
|
+
"""
|
|
759
|
+
CREATE VIRTUAL TABLE symbols_fts USING fts5(
|
|
760
|
+
name, docstring, signature, search_text,
|
|
761
|
+
content='symbols', content_rowid='id'
|
|
762
|
+
)
|
|
763
|
+
"""
|
|
764
|
+
)
|
|
765
|
+
conn.execute(
|
|
766
|
+
"""
|
|
767
|
+
CREATE TRIGGER symbols_ai AFTER INSERT ON symbols BEGIN
|
|
768
|
+
INSERT INTO symbols_fts(rowid, name, docstring, signature, search_text)
|
|
769
|
+
VALUES (new.id, new.name, new.docstring, new.signature, new.search_text);
|
|
770
|
+
END
|
|
771
|
+
"""
|
|
772
|
+
)
|
|
773
|
+
conn.execute(
|
|
774
|
+
"""
|
|
775
|
+
CREATE TRIGGER symbols_ad AFTER DELETE ON symbols BEGIN
|
|
776
|
+
INSERT INTO symbols_fts(symbols_fts, rowid, name, docstring, signature, search_text)
|
|
777
|
+
VALUES ('delete', old.id, old.name, old.docstring, old.signature, old.search_text);
|
|
778
|
+
END
|
|
779
|
+
"""
|
|
780
|
+
)
|
|
781
|
+
conn.execute(
|
|
782
|
+
"""
|
|
783
|
+
CREATE TRIGGER symbols_au AFTER UPDATE ON symbols BEGIN
|
|
784
|
+
INSERT INTO symbols_fts(symbols_fts, rowid, name, docstring, signature, search_text)
|
|
785
|
+
VALUES ('delete', old.id, old.name, old.docstring, old.signature, old.search_text);
|
|
786
|
+
INSERT INTO symbols_fts(rowid, name, docstring, signature, search_text)
|
|
787
|
+
VALUES (new.id, new.name, new.docstring, new.signature, new.search_text);
|
|
788
|
+
END
|
|
789
|
+
"""
|
|
790
|
+
)
|
|
791
|
+
# Repopulate the FTS index from the content table (search_text = NULL on old rows).
|
|
792
|
+
conn.execute("INSERT INTO symbols_fts(symbols_fts) VALUES('rebuild')")
|
|
793
|
+
|
|
794
|
+
# Step 3: bump version last — only advance once the rebuild succeeded.
|
|
795
|
+
conn.execute("UPDATE metadata SET value = '11' WHERE key = 'schema_version'")
|
|
796
|
+
conn.execute("COMMIT")
|
|
797
|
+
|
|
798
|
+
logger.info(
|
|
799
|
+
"Migrated Seam index v%d->v11 (added symbols.search_text + 4th symbols_fts "
|
|
800
|
+
"column for camelCase search recall). Run 'seam init' to populate search_text.",
|
|
801
|
+
version,
|
|
802
|
+
)
|
|
803
|
+
except Exception as exc: # noqa: BLE001
|
|
804
|
+
try:
|
|
805
|
+
conn.execute("ROLLBACK")
|
|
806
|
+
except Exception: # noqa: BLE001
|
|
807
|
+
pass
|
|
808
|
+
raise RuntimeError(
|
|
809
|
+
"Seam DB migration v10->v11 failed; run 'seam init' to rebuild the index"
|
|
810
|
+
) from exc
|
|
811
|
+
except RuntimeError:
|
|
812
|
+
raise
|
|
813
|
+
except Exception as exc: # noqa: BLE001
|
|
814
|
+
raise RuntimeError(
|
|
815
|
+
"Seam DB migration v10->v11 failed; run 'seam init' to rebuild the index"
|
|
816
|
+
) from exc
|