Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions DEVELOPER_GUIDE.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions TESTING_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,7 @@ Use `docs/test-doc-maintenance-plan.md` before moving oversized suites or adding
- `IndexCommandRunnerTests.RunOptimizeFts_DryRunPreviewsWithoutWritingThenOptimizeMutates_Issue4577`, `RunOptimizeFts_LockHeld_ReportsDbLocked`, and `RunOptimizeFts_ReadOnlyUri_ReturnsDbNotWritable`
share seeded databases to verify that optimize dry-run reports sizes, readiness, lock state, recommendations, and planned work while preserving the source bytes and creating no lock artifacts; the same fixtures retain the writable optimize and lock/read-only URI mutation guards.
`ConsoleUiTests.PrintUsage_WithoutBanner_HidesAsciiArtAndEasterEggFlags` keeps the optimize dry-run flag visible in full CLI usage.
- Issue #4889 backfill-checkpoint coverage keeps the mutation preflight, folded-value validation, index lock, and artifact decision in one contract. Reuse the completed fixture for default no-op, forced `--checkpoint`, `--no-checkpoint` mutation, human output, a non-NULL drifted fold that requires a protected full repair, and a zero-row pending graph refresh; keep WAL/SHM open during the forced snapshot so Windows sharing semantics and the complete DB/WAL/SHM/manifest file set are exercised. Separate dry-run, invalid-DB, and held-lock assertions must prove that no checkpoint directory is created, while a real pending rewrite or pending graph refresh must retain the automatic checkpoint.
- Issue #4856 maintenance-error coverage spans `CommandErrorWriterTests`, `QueryCommandRunnerTests`, `IndexCommandRunnerTests`, and `DbCommandRunnerTests`.
Keep the SQLite primary-code matrix (`5`, `6`, `8`, `11`, `26`), caller-spelled relative paths, whole-value Unix/Windows/file-URI redaction, missing and invalid-header files, directory/inaccessible preflight classification, integrity corruption rows, lock-holder metadata, the `index --optimize` human preamble, valid JSON, and category-specific recovery hints aligned with classifier version `1`.
- `IndexCommandRunnerTests.Run_DryRunWithRebuildAndMemoryTrace_SkipsConfirmationAndPreservesWorkspace_Issue4580`
Expand Down Expand Up @@ -1303,6 +1304,7 @@ dotnet test --filter "FullyQualifiedName~GitHelperTests"
- `IndexCommandRunnerTests.RunOptimizeFts_DryRunPreviewsWithoutWritingThenOptimizeMutates_Issue4577`、`RunOptimizeFts_LockHeld_ReportsDbLocked`、`RunOptimizeFts_ReadOnlyUri_ReturnsDbNotWritable`
seed 済み database を共有し、optimize dry-run が size、readiness、lock state、推奨、planned work を報告しつつ source byte を保持し、lock artifact を作成しないことを検証する。同じ fixture で、書き込み版 optimize と lock/read-only URI の mutation guard も維持する。
`ConsoleUiTests.PrintUsage_WithoutBanner_HidesAsciiArtAndEasterEggFlags` は、full CLI usage に optimize dry-run flag が表示され続けることを固定します。
- Issue #4889 の backfill-checkpoint coverage は、mutation preflight、folded値の検証、index lock、artifact 作成判断を1つの契約として固定します。完了済み fixture を既定no-op、明示 `--checkpoint`、`--no-checkpoint` mutation、human output、保護された全行修復を必要とする非NULLのfold drift、zero-rowのpending graph refreshで再利用し、明示snapshot中は WAL/SHM をopenしたままにしてWindowsの共有 semanticsとDB/WAL/SHM/manifest一式を検証してください。dry-run、無効DB、lock保持中のassertionではcheckpoint directoryが作られないことを個別に証明し、実際にpending rewriteまたはpending graph refreshがある場合はautomatic checkpointを維持してください。
- Issue #4856 の maintenance-error coverage は `CommandErrorWriterTests`、`QueryCommandRunnerTests`、`IndexCommandRunnerTests`、`DbCommandRunnerTests` にまたがります。
SQLite primary-code matrix (`5`、`6`、`8`、`11`、`26`)、呼び出し側表記を維持する relative path、Unix / Windows / file URI の値全体 redaction、missing / invalid-header file、directory / inaccessible preflight classification、integrity corruption row、lock-holder metadata、`index --optimize` の human preamble、有効な JSON、category 別 recovery hint を classifier version `1` と同期させてください。
- `IndexCommandRunnerTests.Run_DryRunWithRebuildAndMemoryTrace_SkipsConfirmationAndPreservesWorkspace_Issue4580`
Expand Down
17 changes: 17 additions & 0 deletions changelog.d/unreleased/4889.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
category: fixed
issues:
- 4889
affected:
- src/CodeIndex/Cli/IndexCommandRunner.Maintenance.cs
- src/CodeIndex/Cli/JsonOutputContracts.cs
- DEVELOPER_GUIDE.md
---

## English

- **Completed fold backfills no longer create automatic checkpoint artifacts (#4889)** — `backfill-fold` now preflights pending rows, persisted folded values, and readiness under the index lock, creates an automatic DB/WAL/SHM/manifest checkpoint only before a real mutation, reports bounded checkpoint-skip reasons in JSON and human output, and accepts `--checkpoint` when an operator explicitly wants a snapshot of an already-complete database.

## 日本語

- **完了済みの fold backfill が automatic checkpoint artifact を作成しなくなりました (#4889)** — `backfill-fold` は index lock 内で pending row、永続化されたfolded値、readiness を事前確認し、実際の mutation 前にだけ DB/WAL/SHM/manifest checkpoint を自動作成します。JSON と human output には上限付きの checkpoint skip reason を表示し、完了済みDBでもoperatorが明示的にsnapshotを残したい場合は `--checkpoint` を利用できます。
3 changes: 2 additions & 1 deletion src/CodeIndex/Cli/CliFlagSchema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -625,7 +625,8 @@ private static IReadOnlyList<CliFlag> BuildAll()
},
new() { Name = "--show-paths", Description = "Show resolved local paths in maintenance diagnostics, recovery commands, or status --config output", PrimaryCommands = Set(RecoveryPathCommands.Concat(["index", "backfill-fold", "optimize", "vacuum", "db", "status"]).ToArray()) },
new() { Name = "--dry-run-path-limit", ValuePlaceholder = "<n>", Description = "Dry run only: candidate path processing limit before truncated lower-bound estimates", PrimaryCommands = Set("index") },
new() { Name = "--no-checkpoint", Description = "Skip the automatic DB checkpoint before maintenance", PrimaryCommands = Set("backfill-fold") },
new() { Name = "--checkpoint", Description = "Create a DB checkpoint even when backfill preflight is already complete", PrimaryCommands = Set("backfill-fold") },
new() { Name = "--no-checkpoint", Description = "Skip the automatic DB checkpoint before a required backfill mutation", PrimaryCommands = Set("backfill-fold") },
new()
{
Name = "--force",
Expand Down
2 changes: 1 addition & 1 deletion src/CodeIndex/Cli/ConsoleUi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ private static readonly (string Command, string Usage)[] CommandUsageLines =
[
("index", "cdidx index <projectPath> [--db <path>] [--rebuild [--yes]] [--optimize [--show-paths]] [--symbols-only] [--verbose] [--dry-run [--dry-run-path-limit <n>]] [--force] [--quiet] [--json] [--allow-partial] [--memory-trace] [--duration-format <auto|seconds|hms>] [--notify <auto|bell|osc9|desktop|none>] [--max-file-bytes <bytes>] [--max-symbols-per-file <n>] [--max-references-per-file <n>] [--follow-symlinks <none|internal|all>] [--include-symbol-kind <kind>[,<kind>]] [--exclude-symbol-kind <kind>[,<kind>]] [--watch [--debounce <ms>] [--watch-pending-path-limit <n>]]"),
("hooks", "cdidx hooks <install|uninstall|status> [--project <path>] [--force] [--dry-run] [--json]"),
("backfill-fold", "cdidx backfill-fold [--db <path>] [--dry-run] [--no-checkpoint] [--show-paths] [--json]"),
("backfill-fold", "cdidx backfill-fold [--db <path>] [--dry-run] [--checkpoint|--no-checkpoint] [--show-paths] [--json]"),
("optimize", "cdidx optimize [--db <path>] [--dry-run] [--show-paths] [--json]"),
("vacuum", "cdidx vacuum [--db <path>] [--dry-run] [--show-paths] [--json]"),
("index-commits", "cdidx index <projectPath> --commits <commit-ref> [commit-ref ...] [--db <path>] [--verbose] [--dry-run [--dry-run-path-limit <n>]] [--json] [--allow-partial] [--memory-trace] [--duration-format <auto|seconds|hms>] [--max-file-bytes <bytes>] [--include-symbol-kind <kind>[,<kind>]] [--exclude-symbol-kind <kind>[,<kind>]]"),
Expand Down
134 changes: 108 additions & 26 deletions src/CodeIndex/Cli/IndexCommandRunner.Maintenance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public static partial class IndexCommandRunner
private static readonly string[] AcceptedBackfillFoldFlags =
[
"--db", "--json", "--dry-run", "--help",
"--no-checkpoint", "--show-paths",
"--checkpoint", "--no-checkpoint", "--show-paths",
];
private static readonly (string Name, string[] Columns)[] OptimizeObjectDefinitions =
[
Expand Down Expand Up @@ -564,6 +564,12 @@ internal static int RunBackfillFold(

try
{
var normalizedDbPath = Path.GetFullPath(DbPathResolver.NormalizeDbPath(options.DbPath));
using var indexLock = options.DryRun
? null
: IndexLock.Acquire(
IndexLock.GetLockPath(normalizedDbPath),
Path.GetDirectoryName(normalizedDbPath) ?? Environment.CurrentDirectory);
using var db = new DbContext(
options.DryRun ? DbOpenIntent.QueryOnly : DbOpenIntent.Migration,
options.DbPath);
Expand Down Expand Up @@ -617,45 +623,89 @@ internal static int RunBackfillFold(
var symbolReferences = 0;
var verified = false;
var userVersionAfter = userVersionBefore;
var pendingRows = writer.CountBackfillFoldedColumns(rewriteAll);
if (!rewriteAll
&& pendingRows.Symbols == 0
&& pendingRows.SymbolReferences == 0
&& !writer.AllFoldedColumnsBackfilled(requireCurrentFoldKeys: true))
{
// A current metadata stamp plus non-NULL folded values is not sufficient:
// verify the persisted values before declaring a no-op. If they drifted,
// repair every row under the same checkpoint protection as stale metadata.
// current metadata と非 NULL 値だけでは no-op と断定せず実値を検証する。
// drift があれば stale metadata と同様に checkpoint 保護下で全行を修復する。
rewriteAll = true;
pendingRows = writer.CountBackfillFoldedColumns(rewriteAll);
}
var graphRefreshPending = writer.IsFoldBackfillGraphRefreshPending();
var symbolExtractorVersionsCurrent = writer.SymbolExtractorVersionsMatchCurrent();
var mutationRequired = pendingRows.Symbols > 0
|| pendingRows.SymbolReferences > 0
|| !foldReadyBefore
|| csharpSymbolNameContractUpgradeRequired
|| graphRefreshPending
|| !symbolExtractorVersionsCurrent;
var wasAlreadyComplete = foldReadyBefore
&& !rewriteAll
&& !csharpSymbolNameContractUpgradeRequired
&& !graphRefreshPending
&& symbolExtractorVersionsCurrent
&& pendingRows.Symbols == 0
&& pendingRows.SymbolReferences == 0;
var checkpointSkippedReason = options.DryRun
? "dry_run"
: options.NoCheckpoint
? "disabled_by_option"
: !mutationRequired && !options.Checkpoint
? "already_complete"
: null;
var checkpointSkipped = checkpointSkippedReason != null;

if (options.DryRun)
{
(symbols, symbolReferences) = writer.CountBackfillFoldedColumns(rewriteAll);
(symbols, symbolReferences) = pendingRows;
}
else
{
if (!options.NoCheckpoint)
backfillCancellation.Token.ThrowIfCancellationRequested();
if (!checkpointSkipped)
DbCommandRunner.CreateAutomaticCheckpoint(options.DbPath);

(symbols, symbolReferences) = writer.BackfillFoldedColumns(
rewriteAll,
backfillCancellation.Token);
// Row rewrites commit before the final FoldReady stamp so interrupted
// backfills can resume from the remaining rows.
// 行更新は FoldReady stamp より前に永続化し、中断後に残り行から再開できるようにする。
using var transaction = writer.BeginTransaction(backfillCancellation.Token, "backfill fold readiness stamp");
verified = writer.MarkFoldReady();
if (!verified)
if (mutationRequired)
{
return WriteCommandError(
options.Json,
jsonOptions,
"folded-name backfill verification failed: some rows still have NULL folded values",
CommandExitCodes.DatabaseError,
"Retry `cdidx backfill-fold`. If the DB still does not verify, rebuild it with `cdidx index <projectPath> --rebuild`.",
CommandErrorCodes.DbError);
}
writer.MarkCSharpSymbolNameContractReady();
(symbols, symbolReferences) = writer.BackfillFoldedColumns(
rewriteAll,
backfillCancellation.Token);
// Row rewrites commit before the final FoldReady stamp so interrupted
// backfills can resume from the remaining rows.
// 行更新は FoldReady stamp より前に永続化し、中断後に残り行から再開できるようにする。
using var transaction = writer.BeginTransaction(backfillCancellation.Token, "backfill fold readiness stamp");
verified = writer.MarkFoldReady();
if (!verified)
{
return WriteCommandError(
options.Json,
jsonOptions,
"folded-name backfill verification failed: some rows still have NULL folded values",
CommandExitCodes.DatabaseError,
"Retry `cdidx backfill-fold`. If the DB still does not verify, rebuild it with `cdidx index <projectPath> --rebuild`.",
CommandErrorCodes.DbError);
}
writer.MarkCSharpSymbolNameContractReady();

transaction.Commit();
userVersionAfter = db.GetUserVersion();
transaction.Commit();
userVersionAfter = db.GetUserVersion();
}
else
{
verified = true;
}
}
var foldMetadataCurrentAfter = options.DryRun
? foldMetadataCurrentBefore
: true;
var foldReadyAfter = (userVersionAfter & DbContext.FoldReadyFlag) != 0
&& foldMetadataCurrentAfter;
var wasAlreadyComplete = foldReadyBefore && !rewriteAll && symbols == 0 && symbolReferences == 0;

if (options.Json)
{
Expand All @@ -670,7 +720,9 @@ internal static int RunBackfillFold(
verified,
userVersionBefore,
userVersionAfter,
foldReadyAfter), jsonContext.BackfillFoldJsonResult));
foldReadyAfter,
checkpointSkipped,
checkpointSkippedReason), jsonContext.BackfillFoldJsonResult));
}
else
{
Expand All @@ -683,6 +735,13 @@ internal static int RunBackfillFold(
if (rewriteAll)
CommandOutputWriter.WriteLine(" mode: full folded-key refresh (fold metadata missing or mismatched)");
CommandOutputWriter.WriteLine($" already complete: {(wasAlreadyComplete ? "yes" : "no")}");
CommandOutputWriter.WriteLine(checkpointSkippedReason switch
{
"dry_run" => " checkpoint: skipped (dry run)",
"disabled_by_option" => " checkpoint: skipped (--no-checkpoint)",
"already_complete" => " checkpoint: skipped (already complete)",
_ => " checkpoint: created",
});
CommandOutputWriter.WriteLine($" fold_ready: {foldReadyBefore} -> {foldReadyAfter}");
if (!options.DryRun)
{
Expand All @@ -693,6 +752,21 @@ internal static int RunBackfillFold(

return CommandExitCodes.Success;
}
catch (IndexLockConflictException ex)
{
var holderDescription = DescribeLockHolder(ex.Holder);
return MaintenanceDatabaseErrorWriter.Write(
options.Json,
jsonOptions,
MaintenanceDatabaseErrorClassifier.Create(
"backfill-fold",
options.DbPath,
options.ShowPaths,
MaintenanceDatabaseFailureKind.Locked,
details: string.IsNullOrEmpty(holderDescription)
? null
: [holderDescription]));
}
catch (OperationCanceledException)
{
return WriteCommandError(
Expand Down Expand Up @@ -769,6 +843,7 @@ private static BackfillFoldCommandOptions ParseBackfillFoldArgs(string[] args)
var dbPath = Path.Combine(".cdidx", "codeindex.db");
var json = false;
var dryRun = false;
var checkpoint = false;
var noCheckpoint = false;
var showPaths = false;
string? parseError = null;
Expand All @@ -786,14 +861,17 @@ private static BackfillFoldCommandOptions ParseBackfillFoldArgs(string[] args)
case "--dry-run":
dryRun = true;
break;
case "--checkpoint":
checkpoint = true;
break;
case "--no-checkpoint":
noCheckpoint = true;
break;
case "--show-paths":
showPaths = true;
break;
case "--help" or "-h":
return new BackfillFoldCommandOptions { ShowHelp = true, DbPath = dbPath, Json = json, DryRun = dryRun, NoCheckpoint = noCheckpoint, ShowPaths = showPaths };
return new BackfillFoldCommandOptions { ShowHelp = true, DbPath = dbPath, Json = json, DryRun = dryRun, Checkpoint = checkpoint, NoCheckpoint = noCheckpoint, ShowPaths = showPaths };
default:
if (args[i].StartsWith("-", StringComparison.Ordinal))
{
Expand All @@ -807,11 +885,15 @@ private static BackfillFoldCommandOptions ParseBackfillFoldArgs(string[] args)
}
}

if (checkpoint && noCheckpoint)
parseError ??= "--checkpoint and --no-checkpoint cannot be used together";

return new BackfillFoldCommandOptions
{
DbPath = dbPath,
Json = json,
DryRun = dryRun,
Checkpoint = checkpoint,
NoCheckpoint = noCheckpoint,
ShowPaths = showPaths,
ParseError = parseError,
Expand Down
1 change: 1 addition & 0 deletions src/CodeIndex/Cli/IndexCommandRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,7 @@ public sealed class BackfillFoldCommandOptions
public bool Json { get; init; }
public bool DryRun { get; init; }
public bool ShowPaths { get; init; }
public bool Checkpoint { get; init; }
public bool NoCheckpoint { get; init; }
public string? ParseError { get; init; }
}
Expand Down
Loading
Loading