Skip to content
16 changes: 16 additions & 0 deletions changelog.d/unreleased/3717.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
category: fixed
issues:
- 3717
affected:
- src/CodeIndex/Database/DbContext.cs
- tests/CodeIndex.Tests/DatabaseTests.cs
---

## English

- **Schema rebuild migrations now validate foreign keys before returning (#3717)** — migrations that temporarily disable SQLite FK enforcement now run bounded `foreign_key_check` diagnostics and fail with an actionable integrity-check hint if a violation is detected.

## 日本語

- **schema rebuild migration が復帰前に外部キーを検証するようになりました (#3717)** — SQLite の FK enforcement を一時的に無効化する migration は、上限付きの `foreign_key_check` 診断を実行し、違反検出時は integrity-check の復旧ヒント付きで失敗します。
18 changes: 18 additions & 0 deletions changelog.d/unreleased/3732.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
category: fixed
issues:
- 3732
affected:
- src/CodeIndex/Cli/DbCommandRunner.cs
- src/CodeIndex/Cli/ProgramRunner.cs
- tests/CodeIndex.Tests/DbCommandRunnerTests.cs
- tests/CodeIndex.Tests/ProgramRunnerTests.cs
---

## English

- **Temporary cleanup paths are revalidated and audited (#3732)** — recursive cleanup now rejects reparse/special targets immediately before deletion, and upgrade installer temporary-directory cleanup failures emit bounded warnings instead of being silently swallowed.

## 日本語

- **temporary cleanup path を再検証し監査可能にしました (#3732)** — recursive cleanup は削除直前に reparse/special target を拒否し、upgrade installer temporary directory の cleanup failure は黙殺せず上限付き warning として出力します。
17 changes: 17 additions & 0 deletions changelog.d/unreleased/3737.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
category: fixed
issues:
- 3737
affected:
- src/CodeIndex/Cli/QueryCommandRunner.cs
- src/CodeIndex/Database/DbContext.cs
- tests/CodeIndex.Tests/QueryCommandRunnerTests.cs
---

## English

- **`deps --workspace-db` now validates member CodeIndex databases before cross-database queries (#3737)** — non-CodeIndex databases, newer schema stamps, and read-only fallback cases now report clear validation results before dependency fan-out starts.

## 日本語

- **`deps --workspace-db` が cross-database query の前に member CodeIndex database を検証するようになりました (#3737)** — CodeIndex ではない DB、新しい schema stamp、read-only fallback が必要なケースを、dependency fan-out 開始前に明確な validation 結果として報告します。
16 changes: 16 additions & 0 deletions changelog.d/unreleased/3810.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
category: fixed
issues:
- 3810
affected:
- src/CodeIndex/Database/DbContext.cs
- tests/CodeIndex.Tests/LegacySchemaMigrationTests.cs
---

## English

- **Read-migration warnings no longer expose local database paths (#3810)** — partial read-migration failures now clear active migration state on every exit path and bound/redact SQLite warning text while keeping writable-storage remediation actionable.

## 日本語

- **read migration の警告がローカル DB パスを露出しないようになりました (#3810)** — 部分的な read migration 失敗時も active migration 状態を必ず解除し、SQLite 警告文を上限付きで赤字化しつつ writable storage での復旧案内を維持します。
17 changes: 17 additions & 0 deletions changelog.d/unreleased/3812.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
category: fixed
issues:
- 3812
affected:
- src/CodeIndex/Cli/DbCommandRunner.cs
- src/CodeIndex/Cli/JsonOutputContracts.cs
- tests/CodeIndex.Tests/DbCommandRunnerTests.cs
---

## English

- **DB checkpoint listing and restore rollback are more fault-tolerant (#3812)** — checkpoint file enumeration now reports bounded diagnostics instead of failing after a successful snapshot, and restore rollback moves backed-up files back with private permissions without deleting current targets first.

## 日本語

- **DB checkpoint listing と restore rollback の耐障害性を高めました (#3812)** — checkpoint file enumeration は snapshot 成功後に失敗させず上限付き診断として返し、restore rollback は現在の target を先に削除せず private permission を保ったまま backup を戻します。
22 changes: 22 additions & 0 deletions changelog.d/unreleased/3832.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
category: fixed
issues:
- 3832
affected:
- src/CodeIndex/Database/DbConnectionFactory.cs
- src/CodeIndex/Database/DbContext.cs
- src/CodeIndex/Database/DbReader.cs
- src/CodeIndex/Database/DbReader.FilesStatus.cs
- src/CodeIndex/Cli/JsonOutputContracts.cs
- src/CodeIndex/Cli/QueryCommandRunner.cs
- src/CodeIndex/Models/QueryResults.cs
- tests/CodeIndex.Tests/LegacySchemaMigrationTests.cs
---

## English

- **Read-only SQLite fallback now reports stale-WAL diagnostics (#3832)** — immutable fallback is limited to read-only/cantopen/io-style open failures, while status and query JSON expose checkpoint skip/failure reasons and stale snapshot risk when WAL freshness cannot be guaranteed.

## 日本語

- **read-only SQLite fallback が stale WAL 診断を返すようになりました (#3832)** — immutable fallback は read-only/cantopen/io 系の open 失敗に限定し、status と query JSON には WAL freshness を保証できない場合の checkpoint skip/failure reason と stale snapshot risk を出力します。
15 changes: 15 additions & 0 deletions changelog.d/unreleased/3859.internal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
category: internal
issues:
- 3859
affected:
- src/CodeIndex/Mcp/McpToolHandlers.cs
---

## English

- **MCP status builds now compile after the update-check token flow (#3859)** - removed a duplicate `requestToken` declaration that broke release builds and CodeQL extraction before tests could run.

## 日本語

- **update-check token flow 追加後も MCP status build がコンパイルできるようになりました (#3859)** - release build と CodeQL extraction がテスト前に失敗する原因だった重複 `requestToken` 宣言を削除しました。
86 changes: 60 additions & 26 deletions src/CodeIndex/Cli/DbCommandRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public static class DbCommandRunner
private static readonly char[] InvalidCheckpointNameChars = Path.GetInvalidFileNameChars();
internal static Action? RestoreFailureAfterBackupForTesting { get; set; }
internal static Action<string>? DeleteTemporaryDirectoryForTesting { get; set; }
internal static Func<string, IEnumerable<string>>? EnumerateCheckpointFilesForTesting { get; set; }
internal static Func<IEnumerable<string>>? IntegrityCheckRowsForTesting { get; set; }
internal static Func<string, IEnumerable<string>>? EnumerateCheckpointFileNamesForTesting { get; set; }

Expand Down Expand Up @@ -351,10 +352,11 @@ private static int RunCheckpoint(DbCommandOptions options, JsonSerializerOptions
Console.WriteLine($" name : {result.Name}");
Console.WriteLine($" checkpoint: {result.CheckpointPath}");
Console.WriteLine($" files : {ConsoleUi.Counted(result.Files.Count, "file")}{(result.FilesTruncated ? " (truncated)" : string.Empty)}");
foreach (var diagnostic in result.Diagnostics)
Console.Error.WriteLine($"Warning [{diagnostic.Code}]: {diagnostic.Message}");
}

foreach (var diagnostic in result.Diagnostics)
CommandErrorWriter.WriteStderr($"Warning [{diagnostic.Code}]: {diagnostic.Message}");

return CommandExitCodes.Success;
}
catch (Exception ex)
Expand Down Expand Up @@ -1164,23 +1166,42 @@ private static (List<string> Items, bool Truncated) EnumerateCheckpointFileNames
var truncated = false;
try
{
IEnumerable<string?> fileNames = EnumerateCheckpointFileNamesForTesting?.Invoke(checkpointPath)
?? Directory.EnumerateFiles(checkpointPath).Select(Path.GetFileName);
foreach (var name in fileNames)
if (EnumerateCheckpointFileNamesForTesting != null)
{
if (files.Count >= CheckpointFileInspectLimit)
foreach (var name in EnumerateCheckpointFileNamesForTesting(checkpointPath))
{
truncated = true;
break;
if (files.Count >= CheckpointFileInspectLimit)
{
truncated = true;
break;
}

if (name is not null)
files.Add(name);
}
}
else
{
var listedFiles = EnumerateCheckpointFiles(checkpointPath, diagnostics, CheckpointFileInspectLimit + 1);
foreach (var file in listedFiles.Items)
{
if (files.Count >= CheckpointFileInspectLimit)
{
truncated = true;
break;
}

var name = Path.GetFileName(file);
if (name is not null)
files.Add(name);
}

if (name is not null)
files.Add(name);
truncated = listedFiles.Truncated || listedFiles.Items.Count > CheckpointFileInspectLimit;
}
}
catch (Exception ex) when (IsRecoverableFilesystemException(ex))
{
diagnostics.Add(CreateCheckpointDiagnostic("checkpoint_file_enumeration_failed", "Unable to enumerate every checkpoint file after creation.", checkpointPath));
diagnostics.Add(CreateCheckpointDiagnostic("checkpoint_file_enumeration_failed", "Unable to enumerate every checkpoint file.", checkpointPath));
truncated = true;
}

Expand Down Expand Up @@ -1222,7 +1243,7 @@ private static (List<string> Items, bool Truncated) EnumerateCheckpointFiles(
var files = new List<string>();
try
{
foreach (var file in Directory.EnumerateFiles(checkpointPath))
foreach (var file in EnumerateCheckpointFilesForTesting?.Invoke(checkpointPath) ?? Directory.EnumerateFiles(checkpointPath))
{
if (files.Count >= limit)
return (files, Truncated: true);
Expand Down Expand Up @@ -1366,12 +1387,12 @@ private static void CopyIfExists(string source, string destination, bool private
DataDirectorySecurity.ApplyPrivateFileMode(destination);
}

private static void MoveIfExists(string source, string destination, bool privateDestination = false)
private static void MoveIfExists(string source, string destination, bool privateDestination = false, bool overwrite = false)
{
if (!TryGetRegularExistingFile(source, out var normalizedSource))
return;

File.Move(normalizedSource, LongPath.EnsureWindowsPrefix(destination));
File.Move(normalizedSource, LongPath.EnsureWindowsPrefix(destination), overwrite);
if (privateDestination)
DataDirectorySecurity.ApplyPrivateFileMode(destination);
}
Expand All @@ -1394,18 +1415,9 @@ private static void RestoreBackedUpFiles(string fullDbPath, string backupPath)
if (!Directory.Exists(backupPath))
return;

DeleteIfExists(fullDbPath);
DeleteIfExists(fullDbPath + "-wal");
DeleteIfExists(fullDbPath + "-shm");
MoveIfExists(Path.Combine(backupPath, Path.GetFileName(fullDbPath)), fullDbPath, privateDestination: true);
MoveIfExists(Path.Combine(backupPath, Path.GetFileName(fullDbPath) + "-wal"), fullDbPath + "-wal", privateDestination: true);
MoveIfExists(Path.Combine(backupPath, Path.GetFileName(fullDbPath) + "-shm"), fullDbPath + "-shm", privateDestination: true);
}

private static void DeleteIfExists(string path)
{
if (File.Exists(LongPath.EnsureWindowsPrefix(path)))
File.Delete(LongPath.EnsureWindowsPrefix(path));
MoveIfExists(Path.Combine(backupPath, Path.GetFileName(fullDbPath)), fullDbPath, privateDestination: true, overwrite: true);
MoveIfExists(Path.Combine(backupPath, Path.GetFileName(fullDbPath) + "-wal"), fullDbPath + "-wal", privateDestination: true, overwrite: true);
MoveIfExists(Path.Combine(backupPath, Path.GetFileName(fullDbPath) + "-shm"), fullDbPath + "-shm", privateDestination: true, overwrite: true);
}

internal static void TryDeleteTemporaryDirectory(string path, string cleanupDescription, string safeRoot, string expectedNamePrefix)
Expand All @@ -1421,6 +1433,12 @@ internal static void TryDeleteTemporaryDirectory(string path, string cleanupDesc
if (!Directory.Exists(LongPath.EnsureWindowsPrefix(fullPath)))
return;

if (!TryValidateTemporaryDirectoryCleanupTarget(fullPath, safeRoot, expectedNamePrefix, out fullPath, out validationFailure))
{
CommandErrorWriter.WriteStderr($"Warning: skipped deleting {cleanupDescription} {ConsoleUi.FormatBoundedValue(path)} ({validationFailure}).");
return;
}

if (DeleteTemporaryDirectoryForTesting != null)
DeleteTemporaryDirectoryForTesting(fullPath);
else
Expand Down Expand Up @@ -1458,6 +1476,22 @@ private static bool TryValidateTemporaryDirectoryCleanupTarget(
return false;
}

var longPath = LongPath.EnsureWindowsPrefix(fullPath);
if (Directory.Exists(longPath))
{
var attributes = File.GetAttributes(longPath);
if ((attributes & (FileAttributes.ReparsePoint | FileAttributes.Device)) != 0)
{
failureReason = "target is not a regular temporary directory";
return false;
}
}
else if (File.Exists(longPath))
{
failureReason = "target is not a directory";
return false;
}

return true;
}
catch (Exception ex) when (ex is ArgumentException or IOException or NotSupportedException or UnauthorizedAccessException or PathTooLongException)
Expand Down
18 changes: 17 additions & 1 deletion src/CodeIndex/Cli/JsonOutputContracts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,23 @@ internal sealed record QueryPathErrorJsonResult(
internal sealed record JsonStreamDoneResult(
[property: JsonPropertyName("done")] bool Done,
[property: JsonPropertyName("count")] int Count,
[property: JsonPropertyName("interrupted")] bool Interrupted);
[property: JsonPropertyName("interrupted")] bool Interrupted,
[property: JsonPropertyName("read_only_fallback")]
[property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] bool? ReadOnlyFallback = null,
[property: JsonPropertyName("wal_checkpoint_attempted")]
[property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] bool? WalCheckpointAttempted = null,
[property: JsonPropertyName("wal_checkpoint_succeeded")]
[property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] bool? WalCheckpointSucceeded = null,
[property: JsonPropertyName("read_only_immutable_fallback")]
[property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] bool? ReadOnlyImmutableFallback = null,
[property: JsonPropertyName("wal_checkpoint_skipped_reason")]
[property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] string? WalCheckpointSkippedReason = null,
[property: JsonPropertyName("wal_checkpoint_failure_reason")]
[property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] string? WalCheckpointFailureReason = null,
[property: JsonPropertyName("wal_stale_snapshot_risk")]
[property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] bool? WalStaleSnapshotRisk = null,
[property: JsonPropertyName("wal_stale_snapshot_reason")]
[property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] string? WalStaleSnapshotReason = null);

internal sealed record LanguageEntryJsonResult(
[property: JsonPropertyName("lang")] string Lang,
Expand Down
Loading
Loading