From 9e8a7dd979e22dd265d30059d3852cdacded1b0e Mon Sep 17 00:00:00 2001 From: Widthdom Date: Mon, 1 Jun 2026 00:51:09 +0900 Subject: [PATCH 1/4] Fix resumable backfill fold progress for #1461 --- DEVELOPER_GUIDE.md | 6 +- README.md | 4 +- changelog.d/unreleased/1461.fixed.md | 18 +++++ .../Cli/IndexCommandRunner.Maintenance.cs | 8 +-- src/CodeIndex/Database/DbWriter.cs | 67 +++++++++++++++++-- src/CodeIndex/Mcp/McpToolHandlers.cs | 28 ++++++-- .../IndexCommandRunnerTests.cs | 54 +++++++++++++-- tests/CodeIndex.Tests/McpServerTests.cs | 5 ++ 8 files changed, 166 insertions(+), 24 deletions(-) create mode 100644 changelog.d/unreleased/1461.fixed.md diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index 1ff665da3e..6c871be584 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -103,7 +103,11 @@ without mutating the DB or stamping FoldReady. The MCP `backfill_fold` tool accepts the same preview as `dry_run: true`, and also accepts `force: true` to rewrite all folded keys when an operator needs to recover from suspicious fold metadata or row state even though the stored version/fingerprint appears -current. +current. Non-dry-run row rewrites are resumable after interruption: completed +row updates remain durable, and the final FoldReady metadata is stamped only +after verification succeeds. MCP responses include `progress.rows_done`, +`progress.rows_total`, and `progress.fraction` so clients can report and retry +long backfills. ## Filesystem Permissions diff --git a/README.md b/README.md index d284865d9f..baf47d0d56 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ After the first command, use these cues and follow-up commands: | Edits or branch switches | Refresh incrementally with `--files`, `--commits`, or `--changed-between ` instead of rebuilding. See [Quick Start](USER_GUIDE.md#quick-start) and [Incremental update reliability](USER_GUIDE.md#incremental-update-reliability). | | Intentional rebuilds | Interactive terminals ask before deleting the DB. Scripts and CI must pass `--yes` or `--force`. | | Long-lived DB compaction | Run `cdidx optimize` or `cdidx index --optimize` to compact FTS5 segments immediately. Incremental refreshes also optimize opportunistically. | -| Maintenance rollback | Run `cdidx db checkpoint ` before risky DB maintenance and `cdidx db restore ` to roll back. `backfill-fold` creates an automatic checkpoint unless `--no-checkpoint` is passed. | +| Maintenance rollback | Run `cdidx db checkpoint ` before risky DB maintenance and `cdidx db restore ` to roll back. `backfill-fold` creates an automatic checkpoint unless `--no-checkpoint` is passed, and interrupted folded-key rewrites resume from remaining rows. | | Permission or I/O scan errors | `cdidx` records the scan error, continues other directories, and writes `.cdidx/scan-checkpoint.json` so same-HEAD retries can skip completed directories. | Output controls: @@ -335,7 +335,7 @@ extractor fixture を確認できます。詳細は | 編集後やブランチ切り替え後 | 再構築ではなく `--files`、`--commits`、`--changed-between ` で差分更新します。詳細は [クイックスタート](USER_GUIDE.md#クイックスタート) と [インクリメンタル更新の信頼性](USER_GUIDE.md#インクリメンタル更新の信頼性) を参照してください。 | | 意図的な再構築 | interactive terminal では既存 DB 削除前に確認を求めます。script / CI では `--yes` または `--force` が必要です。 | | 長期間使っている DB の compact | `cdidx optimize` または `cdidx index --optimize` で FTS5 segment をすぐに compact できます。差分更新中も必要に応じて自動 optimize します。 | -| 保守作業の rollback | risky な DB 保守の前に `cdidx db checkpoint `、戻す場合は `cdidx db restore ` を使います。`backfill-fold` は `--no-checkpoint` を渡さない限り自動 checkpoint を作成します。 | +| 保守作業の rollback | risky な DB 保守の前に `cdidx db checkpoint `、戻す場合は `cdidx db restore ` を使います。`backfill-fold` は `--no-checkpoint` を渡さない限り自動 checkpoint を作成し、中断された folded-key rewrite は残り行から再開します。 | | 権限や I/O の scan error | `cdidx` は scan error を記録し、他のディレクトリの走査を続けます。同じ HEAD の再実行では `.cdidx/scan-checkpoint.json` により成功済みディレクトリを読み飛ばせます。 | 出力を整える option: diff --git a/changelog.d/unreleased/1461.fixed.md b/changelog.d/unreleased/1461.fixed.md new file mode 100644 index 0000000000..26580e0fee --- /dev/null +++ b/changelog.d/unreleased/1461.fixed.md @@ -0,0 +1,18 @@ +--- +category: fixed +issues: + - 1461 +affected: + - src/CodeIndex/Database/DbWriter.cs + - src/CodeIndex/Mcp/McpToolHandlers.cs + - tests/CodeIndex.Tests/IndexCommandRunnerTests.cs + - tests/CodeIndex.Tests/McpServerTests.cs +--- + +## English + +- **MCP `backfill_fold` can report progress and resume interrupted row rewrites (#1461)** — folded-key row updates are durable before the final FoldReady stamp, and MCP responses now include `progress.rows_done`, `progress.rows_total`, and `progress.fraction`. + +## 日本語 + +- **MCP `backfill_fold` が進捗を返し、中断された行 rewrite を再開できるようになりました (#1461)** — folded-key 行更新は最終 FoldReady stamp の前に永続化され、MCP 応答に `progress.rows_done`、`progress.rows_total`、`progress.fraction` を含めます。 diff --git a/src/CodeIndex/Cli/IndexCommandRunner.Maintenance.cs b/src/CodeIndex/Cli/IndexCommandRunner.Maintenance.cs index ae3df7e4d8..bed3afc602 100644 --- a/src/CodeIndex/Cli/IndexCommandRunner.Maintenance.cs +++ b/src/CodeIndex/Cli/IndexCommandRunner.Maintenance.cs @@ -195,13 +195,13 @@ internal static int RunBackfillFold( if (!options.NoCheckpoint) DbCommandRunner.CreateAutomaticCheckpoint(options.DbPath); - using var transaction = writer.BeginTransaction(); (symbols, symbolReferences) = writer.BackfillFoldedColumns( rewriteAll, backfillCancellation.Token); - // MarkFoldReady re-verifies in the same transaction so rows, metadata, and - // FoldReady stamp commit or roll back together. - // 同一 transaction 内で再検証し、行・metadata・FoldReady stamp を原子的に扱う。 + // Row rewrites commit before the final FoldReady stamp so interrupted + // backfills can resume from the remaining rows. + // 行更新は FoldReady stamp より前に永続化し、中断後に残り行から再開できるようにする。 + using var transaction = writer.BeginTransaction(); verified = writer.MarkFoldReady(); if (!verified) { diff --git a/src/CodeIndex/Database/DbWriter.cs b/src/CodeIndex/Database/DbWriter.cs index d09d9573f9..e9c7bed6f5 100644 --- a/src/CodeIndex/Database/DbWriter.cs +++ b/src/CodeIndex/Database/DbWriter.cs @@ -15,6 +15,10 @@ namespace CodeIndex.Database; /// public class DbWriter { + private const string FoldBackfillPhaseMetaKey = "fold_backfill_phase"; + private const string FoldBackfillLastSymbolIdMetaKey = "fold_backfill_last_symbol_id"; + private const string FoldBackfillLastReferenceIdMetaKey = "fold_backfill_last_reference_id"; + public const string FtsIncrementalWritesSinceOptimizeMetaKey = "fts_incremental_writes_since_optimize"; public const string FtsLastOptimizedAtMetaKey = "fts_last_optimized_at"; public const int DefaultFtsOptimizeIncrementalWriteThreshold = 25; @@ -3270,28 +3274,46 @@ private bool SymbolExtractorVersionMatchesCurrent(string? lang) CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); - using var txn = !IsInTransaction() ? BeginTransaction() : null; var symbols = BackfillSymbolFoldedRows(rewriteAll, cancellationToken); + if (rewriteAll) + { + SetMeta(FoldBackfillPhaseMetaKey, "references"); + SetMeta(FoldBackfillLastReferenceIdMetaKey, "0"); + } + var symbolReferences = BackfillReferenceFoldedRows(rewriteAll, cancellationToken); cancellationToken.ThrowIfCancellationRequested(); - txn?.Commit(); + if (rewriteAll) + ClearFoldBackfillCheckpoint(); + return (symbols, symbolReferences); } public (int Symbols, int SymbolReferences) CountBackfillFoldedColumns(bool rewriteAll = false) { + var phase = rewriteAll ? GetMetaString(FoldBackfillPhaseMetaKey) : null; + var lastSymbolId = rewriteAll ? GetFoldBackfillCheckpoint(FoldBackfillLastSymbolIdMetaKey) : 0; + var lastReferenceId = rewriteAll ? GetFoldBackfillCheckpoint(FoldBackfillLastReferenceIdMetaKey) : 0; + using var symbols = _conn.CreateCommand(); - symbols.CommandText = rewriteAll - ? "SELECT COUNT(*) FROM symbols WHERE name IS NOT NULL" + symbols.CommandText = rewriteAll && phase != "references" + ? "SELECT COUNT(*) FROM symbols WHERE name IS NOT NULL AND id > @lastSymbolId" + : rewriteAll + ? "SELECT 0" : "SELECT COUNT(*) FROM symbols WHERE name IS NOT NULL AND name_folded IS NULL"; + symbols.Parameters.AddWithValue("@lastSymbolId", lastSymbolId); using var references = _conn.CreateCommand(); references.CommandText = rewriteAll - ? "SELECT COUNT(*) FROM symbol_references WHERE symbol_name IS NOT NULL OR container_name IS NOT NULL" + ? @"SELECT COUNT(*) + FROM symbol_references + WHERE id > @lastReferenceId + AND (symbol_name IS NOT NULL OR container_name IS NOT NULL)" : @"SELECT COUNT(*) FROM symbol_references WHERE (symbol_name IS NOT NULL AND symbol_name_folded IS NULL) OR (container_name IS NOT NULL AND container_name_folded IS NULL)"; + references.Parameters.AddWithValue("@lastReferenceId", phase == "references" ? lastReferenceId : 0); return (ToInt32Count(symbols.ExecuteScalar()), ToInt32Count(references.ExecuteScalar())); } @@ -3304,12 +3326,18 @@ private static int ToInt32Count(object? value) private int BackfillSymbolFoldedRows(bool rewriteAll, CancellationToken cancellationToken) { + var phase = rewriteAll ? GetMetaString(FoldBackfillPhaseMetaKey) : null; + if (phase == "references") + return 0; + + var lastSymbolId = rewriteAll ? GetFoldBackfillCheckpoint(FoldBackfillLastSymbolIdMetaKey) : 0; var rows = new List<(long Id, string Name)>(); using (var cmd = _conn.CreateCommand()) { cmd.CommandText = rewriteAll - ? "SELECT id, name FROM symbols WHERE name IS NOT NULL" + ? "SELECT id, name FROM symbols WHERE name IS NOT NULL AND id > @lastSymbolId ORDER BY id" : "SELECT id, name FROM symbols WHERE name IS NOT NULL AND name_folded IS NULL"; + cmd.Parameters.AddWithValue("@lastSymbolId", lastSymbolId); using var reader = cmd.ExecuteTrackedReader(); while (reader.TrackedRead()) { @@ -3333,6 +3361,8 @@ private int BackfillSymbolFoldedRows(bool rewriteAll, CancellationToken cancella pFolded.Value = (object?)NameFold.Fold(row.Name) ?? DBNull.Value; pId.Value = row.Id; update.ExecuteNonQuery(); + if (rewriteAll) + SetMeta(FoldBackfillLastSymbolIdMetaKey, row.Id.ToString(System.Globalization.CultureInfo.InvariantCulture)); FoldBackfillRowUpdatedForTesting?.Invoke(); } @@ -3341,15 +3371,21 @@ private int BackfillSymbolFoldedRows(bool rewriteAll, CancellationToken cancella private int BackfillReferenceFoldedRows(bool rewriteAll, CancellationToken cancellationToken) { + var lastReferenceId = rewriteAll ? GetFoldBackfillCheckpoint(FoldBackfillLastReferenceIdMetaKey) : 0; var rows = new List<(long Id, string? SymbolName, string? ContainerName)>(); using (var cmd = _conn.CreateCommand()) { cmd.CommandText = rewriteAll - ? "SELECT id, symbol_name, container_name FROM symbol_references WHERE symbol_name IS NOT NULL OR container_name IS NOT NULL" + ? @"SELECT id, symbol_name, container_name + FROM symbol_references + WHERE id > @lastReferenceId + AND (symbol_name IS NOT NULL OR container_name IS NOT NULL) + ORDER BY id" : @"SELECT id, symbol_name, container_name FROM symbol_references WHERE (symbol_name IS NOT NULL AND symbol_name_folded IS NULL) OR (container_name IS NOT NULL AND container_name_folded IS NULL)"; + cmd.Parameters.AddWithValue("@lastReferenceId", lastReferenceId); using var reader = cmd.ExecuteTrackedReader(); while (reader.TrackedRead()) { @@ -3381,12 +3417,29 @@ FROM symbol_references pContainerNameFolded.Value = (object?)NameFold.Fold(row.ContainerName) ?? DBNull.Value; pId.Value = row.Id; update.ExecuteNonQuery(); + if (rewriteAll) + SetMeta(FoldBackfillLastReferenceIdMetaKey, row.Id.ToString(System.Globalization.CultureInfo.InvariantCulture)); FoldBackfillRowUpdatedForTesting?.Invoke(); } return rows.Count; } + private long GetFoldBackfillCheckpoint(string key) + { + var value = GetMetaString(key); + return long.TryParse(value, System.Globalization.NumberStyles.Integer, System.Globalization.CultureInfo.InvariantCulture, out var parsed) + ? parsed + : 0; + } + + private void ClearFoldBackfillCheckpoint() + { + SetMeta(FoldBackfillPhaseMetaKey, null); + SetMeta(FoldBackfillLastSymbolIdMetaKey, null); + SetMeta(FoldBackfillLastReferenceIdMetaKey, null); + } + private static object FoldedNameDbValue(string? name, Dictionary cache) { if (name == null) diff --git a/src/CodeIndex/Mcp/McpToolHandlers.cs b/src/CodeIndex/Mcp/McpToolHandlers.cs index d8a76e0273..7bd0c10ce1 100644 --- a/src/CodeIndex/Mcp/McpToolHandlers.cs +++ b/src/CodeIndex/Mcp/McpToolHandlers.cs @@ -3817,22 +3817,26 @@ private JsonNode ExecuteBackfillFold(JsonNode? id, JsonNode? args, JsonNode? pro || !foldMetadataCurrentBefore; var symbols = 0; var symbolReferences = 0; + var totalSymbols = 0; + var totalSymbolReferences = 0; var verified = false; var userVersionAfter = userVersionBefore; + (totalSymbols, totalSymbolReferences) = writer.CountBackfillFoldedColumns(rewriteAll); if (dryRun) { - (symbols, symbolReferences) = writer.CountBackfillFoldedColumns(rewriteAll); + symbols = totalSymbols; + symbolReferences = totalSymbolReferences; } else { EmitProgressNotification(progressToken, 0, null, "Backfilling folded-name keys."); - using var transaction = writer.BeginTransaction(); (symbols, symbolReferences) = writer.BackfillFoldedColumns(rewriteAll); - EmitProgressNotification(progressToken, symbols + symbolReferences, null, "Verifying folded-name keys."); - // Verify and stamp in the same transaction as the row rewrite so crash recovery - // never leaves current fold metadata without a matching FoldReady stamp. - // 行の再生成と同じ transaction で検証・stamp し、metadata だけが先に残らないようにする。 + EmitProgressNotification(progressToken, symbols + symbolReferences, totalSymbols + totalSymbolReferences, "Verifying folded-name keys."); + // Row rewrites are intentionally committed before the final FoldReady stamp so + // interrupted MCP backfills can resume from the remaining rows. + // 行更新は FoldReady stamp より前に永続化し、中断後に残り行から再開できるようにする。 + using var transaction = writer.BeginTransaction(); verified = writer.MarkFoldReady(); if (!verified) return CreateToolErrorResponse(id, "Folded-name backfill verification failed: some rows still have NULL folded values. Re-run backfill_fold."); @@ -3867,6 +3871,7 @@ private JsonNode ExecuteBackfillFold(JsonNode? id, JsonNode? args, JsonNode? pro ["fold_key_version_after"] = dryRun ? storedFoldVersion : currentFoldVersion, ["fold_key_fingerprint_before"] = storedFoldFingerprint, ["fold_key_fingerprint_after"] = dryRun ? storedFoldFingerprint : currentFoldFingerprint, + ["progress"] = BuildBackfillProgressJson(symbols + symbolReferences, totalSymbols + totalSymbolReferences), }; var summary = dryRun @@ -3882,6 +3887,17 @@ private JsonNode ExecuteBackfillFold(JsonNode? id, JsonNode? args, JsonNode? pro } } + private static JsonObject BuildBackfillProgressJson(int rowsDone, int rowsTotal) + { + var fraction = rowsTotal <= 0 ? 1.0 : Math.Min(1.0, rowsDone / (double)rowsTotal); + return new JsonObject + { + ["rows_done"] = rowsDone, + ["rows_total"] = rowsTotal, + ["fraction"] = fraction, + }; + } + /// /// Maximum length for suggestion description text. /// 提案説明テキストの最大長。 diff --git a/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs b/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs index c875bc4b11..bed8b0c475 100644 --- a/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs +++ b/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs @@ -3261,7 +3261,7 @@ public void RunBackfillFold_RewritesAllWhenOnlyFingerprintDrifted() } [Fact] - public void BackfillFoldedColumns_CancelledDuringSymbolLoop_RollsBackTransaction() + public void BackfillFoldedColumns_CancelledDuringSymbolLoop_KeepsCompletedRowsForResume() { var dbPath = Path.Combine(Path.GetTempPath(), $"cdidx_backfill_cancel_symbols_{Guid.NewGuid():N}.db"); var cts = new CancellationTokenSource(); @@ -3296,7 +3296,7 @@ public void BackfillFoldedColumns_CancelledDuringSymbolLoop_RollsBackTransaction using var count = db.Connection.CreateCommand(); count.CommandText = "SELECT COUNT(*) FROM symbols WHERE name_folded IS NOT NULL"; - Assert.Equal(0L, (long)count.ExecuteScalar()!); + Assert.Equal(1L, (long)count.ExecuteScalar()!); } } finally @@ -3310,7 +3310,7 @@ public void BackfillFoldedColumns_CancelledDuringSymbolLoop_RollsBackTransaction } [Fact] - public void BackfillFoldedColumns_CancelledDuringReferenceLoop_RollsBackTransaction() + public void BackfillFoldedColumns_CancelledDuringReferenceLoop_KeepsCompletedRowsForResume() { var dbPath = Path.Combine(Path.GetTempPath(), $"cdidx_backfill_cancel_refs_{Guid.NewGuid():N}.db"); var cts = new CancellationTokenSource(); @@ -3345,7 +3345,53 @@ public void BackfillFoldedColumns_CancelledDuringReferenceLoop_RollsBackTransact using var count = db.Connection.CreateCommand(); count.CommandText = "SELECT COUNT(*) FROM symbol_references WHERE symbol_name_folded IS NOT NULL OR container_name_folded IS NOT NULL"; - Assert.Equal(0L, (long)count.ExecuteScalar()!); + Assert.Equal(1L, (long)count.ExecuteScalar()!); + } + } + finally + { + DbWriter.FoldBackfillRowUpdatedForTesting = null; + cts.Dispose(); + SqliteConnection.ClearAllPools(); + if (File.Exists(dbPath)) + File.Delete(dbPath); + } + } + + [Fact] + public void BackfillFoldedColumns_RewriteAllResumesAfterCheckpoint() + { + var dbPath = Path.Combine(Path.GetTempPath(), $"cdidx_backfill_rewrite_resume_{Guid.NewGuid():N}.db"); + var cts = new CancellationTokenSource(); + try + { + using (var db = new DbContext(dbPath)) + { + db.InitializeSchema(); + var writer = new DbWriter(db.Connection); + var fileId = writer.UpsertFile(new FileRecord + { + Path = "src/app.py", + Lang = "python", + Size = 64, + Lines = 2, + Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), + }); + writer.InsertSymbols([ + new SymbolRecord { FileId = fileId, Kind = "function", Name = "first", Line = 1, StartLine = 1, EndLine = 1 }, + new SymbolRecord { FileId = fileId, Kind = "function", Name = "second", Line = 2, StartLine = 2, EndLine = 2 }, + ]); + + DbWriter.FoldBackfillRowUpdatedForTesting = cts.Cancel; + Assert.Throws(() => writer.BackfillFoldedColumns(rewriteAll: true, cts.Token)); + + DbWriter.FoldBackfillRowUpdatedForTesting = null; + var resumed = writer.BackfillFoldedColumns(rewriteAll: true); + + Assert.Equal(1, resumed.Symbols); + using var count = db.Connection.CreateCommand(); + count.CommandText = "SELECT COUNT(*) FROM symbols WHERE name_folded IS NOT NULL"; + Assert.Equal(2L, (long)count.ExecuteScalar()!); } } finally diff --git a/tests/CodeIndex.Tests/McpServerTests.cs b/tests/CodeIndex.Tests/McpServerTests.cs index 4dc6e7bfe7..e833c078bc 100644 --- a/tests/CodeIndex.Tests/McpServerTests.cs +++ b/tests/CodeIndex.Tests/McpServerTests.cs @@ -6993,6 +6993,9 @@ public void ToolsCall_BackfillFold_StampsFoldReady() Assert.Equal(3, structured["user_version_before"]!.GetValue()); Assert.Equal(7, structured["user_version_after"]!.GetValue()); Assert.True(structured["fold_ready"]!.GetValue()); + Assert.Equal(2, structured["progress"]!["rows_done"]!.GetValue()); + Assert.Equal(2, structured["progress"]!["rows_total"]!.GetValue()); + Assert.Equal(1.0, structured["progress"]!["fraction"]!.GetValue()); using var verifyDb = new DbContext(_dbPath); verifyDb.TryMigrateForRead(); @@ -7015,6 +7018,7 @@ public void ToolsCall_BackfillFold_DryRunDoesNotWrite() var structured = response["result"]!["structuredContent"]!; Assert.True(structured["dry_run"]!.GetValue()); Assert.Equal(2, structured["symbols"]!.GetValue()); + Assert.Equal(2, structured["progress"]!["rows_total"]!.GetValue()); Assert.False(structured["verified"]!.GetValue()); Assert.False(structured["fold_ready_before"]!.GetValue()); Assert.False(structured["fold_ready_after"]!.GetValue()); @@ -7037,6 +7041,7 @@ public void ToolsCall_BackfillFold_SecondRunSignalsAlreadyComplete() var structured = response["result"]!["structuredContent"]!; Assert.Equal(0, structured["symbols"]!.GetValue()); Assert.Equal(0, structured["symbol_references"]!.GetValue()); + Assert.Equal(0, structured["progress"]!["rows_total"]!.GetValue()); Assert.False(structured["rewrite_all"]!.GetValue()); Assert.True(structured["was_already_complete"]!.GetValue()); Assert.True(structured["fold_ready_before"]!.GetValue()); From f71812deb72233b9826edbcb3d311307dc8f3aa2 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Mon, 1 Jun 2026 01:02:30 +0900 Subject: [PATCH 2/4] Fix backfill fold reference checkpoint for #1461 --- src/CodeIndex/Database/DbWriter.cs | 3 +- .../IndexCommandRunnerTests.cs | 47 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/CodeIndex/Database/DbWriter.cs b/src/CodeIndex/Database/DbWriter.cs index e9c7bed6f5..de6d141c28 100644 --- a/src/CodeIndex/Database/DbWriter.cs +++ b/src/CodeIndex/Database/DbWriter.cs @@ -3274,8 +3274,9 @@ private bool SymbolExtractorVersionMatchesCurrent(string? lang) CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); + var foldBackfillPhase = rewriteAll ? GetMetaString(FoldBackfillPhaseMetaKey) : null; var symbols = BackfillSymbolFoldedRows(rewriteAll, cancellationToken); - if (rewriteAll) + if (rewriteAll && foldBackfillPhase != "references") { SetMeta(FoldBackfillPhaseMetaKey, "references"); SetMeta(FoldBackfillLastReferenceIdMetaKey, "0"); diff --git a/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs b/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs index bed8b0c475..1f17758754 100644 --- a/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs +++ b/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs @@ -3404,6 +3404,53 @@ public void BackfillFoldedColumns_RewriteAllResumesAfterCheckpoint() } } + [Fact] + public void BackfillFoldedColumns_RewriteAllResumesReferencePhaseCheckpoint() + { + var dbPath = Path.Combine(Path.GetTempPath(), $"cdidx_backfill_rewrite_refs_resume_{Guid.NewGuid():N}.db"); + var cts = new CancellationTokenSource(); + try + { + using (var db = new DbContext(dbPath)) + { + db.InitializeSchema(); + var writer = new DbWriter(db.Connection); + var fileId = writer.UpsertFile(new FileRecord + { + Path = "src/app.py", + Lang = "python", + Size = 64, + Lines = 2, + Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), + }); + writer.InsertReferences([ + new ReferenceRecord { FileId = fileId, SymbolName = "first", ReferenceKind = "call", Line = 1, Column = 1, Context = "first()" }, + new ReferenceRecord { FileId = fileId, SymbolName = "second", ReferenceKind = "call", Line = 2, Column = 1, Context = "second()" }, + ]); + + DbWriter.FoldBackfillRowUpdatedForTesting = cts.Cancel; + Assert.Throws(() => writer.BackfillFoldedColumns(rewriteAll: true, cts.Token)); + + DbWriter.FoldBackfillRowUpdatedForTesting = null; + var resumed = writer.BackfillFoldedColumns(rewriteAll: true); + + Assert.Equal(0, resumed.Symbols); + Assert.Equal(1, resumed.SymbolReferences); + using var count = db.Connection.CreateCommand(); + count.CommandText = "SELECT COUNT(*) FROM symbol_references WHERE symbol_name_folded IS NOT NULL"; + Assert.Equal(2L, (long)count.ExecuteScalar()!); + } + } + finally + { + DbWriter.FoldBackfillRowUpdatedForTesting = null; + cts.Dispose(); + SqliteConnection.ClearAllPools(); + if (File.Exists(dbPath)) + File.Delete(dbPath); + } + } + [Fact] public void RunBackfillFold_Cancelled_ReturnsInterruptedErrorCode() { From 089dbaa8765462a30c1fc56bfb59f2e24725f617 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Mon, 1 Jun 2026 01:35:18 +0900 Subject: [PATCH 3/4] Fix CI AOT analyzer warning policy for #1461 --- .github/workflows/dotnet.yml | 2 +- Directory.Build.props | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index b7ec61c36d..7d6caa88bb 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -179,7 +179,7 @@ jobs: } - name: Verify formatting - run: dotnet format CodeIndex.sln --verify-no-changes --no-restore --verbosity minimal + run: dotnet format whitespace CodeIndex.sln --verify-no-changes --no-restore --verbosity minimal - name: Verify developer task wrapper if: matrix.os == 'ubuntu-latest' && matrix.test-framework == 'net8.0' diff --git a/Directory.Build.props b/Directory.Build.props index a2ba2e8677..c144863b70 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -14,7 +14,7 @@ --> true true - $(WarningsNotAsErrors);IL2026;IL2067;IL2072;IL2075 + $(WarningsNotAsErrors);IL2026;IL3050;IL2067;IL2072;IL2075 false From d062a8f707b131294efc76f2e5dd1167c83530e0 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Mon, 1 Jun 2026 01:51:53 +0900 Subject: [PATCH 4/4] Fix lint wrapper format verification for #1461 --- dev.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev.sh b/dev.sh index 43bf32ff6f..eaa738e5d8 100755 --- a/dev.sh +++ b/dev.sh @@ -37,7 +37,7 @@ case "$task" in --blame-hang-timeout 5m ;; lint) - dotnet format CodeIndex.sln --verify-no-changes --verbosity minimal + dotnet format whitespace CodeIndex.sln --verify-no-changes --verbosity minimal ;; format) dotnet format CodeIndex.sln --verbosity minimal