From 430e1c546ca9aaa027b63245baee8ae345a4f2a4 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sat, 22 Aug 2026 12:34:03 +0900 Subject: [PATCH 1/3] Fix vacuum dry-run single-slash file URIs (#5123) --- DEVELOPER_GUIDE.md | 4 ++ changelog.d/unreleased/5123.fixed.md | 17 ++++++ .../Cli/QueryCommandRunner.Maintenance.cs | 26 +++++++- .../QueryCommandRunnerTests.cs | 59 +++++++++++++++++++ 4 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 changelog.d/unreleased/5123.fixed.md diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index 713d87a7f..f112df89f 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -1347,6 +1347,8 @@ Current stable codes and triggers: | Memory tracing | `index --json --memory-trace` adds a `memory_timeline` block to the CLI index result and persists peak working-set MB into `last_index_run`; dry-run results also emit live `start`, `snapshot`, `scan`, and `finalize` samples but never persist run metadata. `index --dry-run --rebuild` bypasses destructive confirmation because it does not delete or rewrite the index. `CDIDX_MEM_WARN_MB=` prints a warning when the sampled working set crosses that threshold. | | Newer schema protection | Writable opens reject databases whose `PRAGMA user_version` contains readiness bits outside the current binary's `CurrentSchemaVersion` mask. Read-only status/query paths may still surface `index_newer_than_reader=true` as a degraded audit signal, but write-capable paths must fail with `E003_SCHEMA_TOO_NEW` so an older cdidx cannot silently rewrite a DB stamped by a newer one. | +`vacuum --dry-run` accepts supported local SQLite URI spellings such as `file:/absolute/path/codeindex.db` and `file:///absolute/path/codeindex.db`. Filesystem validation uses the normalized local path; the query-only vacuum connection canonicalizes a single-slash path spelling while retaining its original query string, so an explicit `immutable=1` keeps its stale-snapshot semantics. + ### Data directory resolution When `--db ` is omitted, cdidx resolves the SQLite location from a data directory and appends `codeindex.db`. The precedence chain is: @@ -5226,6 +5228,8 @@ apply 時は `PRAGMA optimize` を実行します。 | memory tracing | `index --json --memory-trace` は CLI index 結果に `memory_timeline` block を追加し、peak working-set MB を `last_index_run` に保存します。dry-run 結果も live な `start`、`snapshot`、`scan`、`finalize` sample を返しますが、run metadata は保存しません。`index --dry-run --rebuild` は index を削除も rewrite もしないため destructive confirmation を bypass します。`CDIDX_MEM_WARN_MB=` は sampled working set がしきい値を超えたときに warning を出します。 | | newer schema protection | writable open は、`PRAGMA user_version` に current binary の `CurrentSchemaVersion` mask 外の readiness bit が含まれる database も拒否します。read-only status/query path は degraded audit signal として `index_newer_than_reader=true` を表示できますが、write-capable path は古い cdidx が新しい binary で stamp された DB を黙って rewrite しないよう `E003_SCHEMA_TOO_NEW` で失敗しなければなりません。 | +`vacuum --dry-run` は、`file:/absolute/path/codeindex.db` と `file:///absolute/path/codeindex.db` のような対応済みローカル SQLite URI 表記を受け付けます。filesystem 検証には正規化したローカル path を使います。query-only vacuum connection では single-slash の path 表記を canonicalize しつつ元の query string を維持するため、明示的な `immutable=1` の stale-snapshot semantics も維持されます。 + ### データディレクトリ解決 `--db ` が省略された場合、cdidx は data directory を解決し、その下に `codeindex.db` を置く。優先順位は次のとおりです。 diff --git a/changelog.d/unreleased/5123.fixed.md b/changelog.d/unreleased/5123.fixed.md new file mode 100644 index 000000000..809a0205b --- /dev/null +++ b/changelog.d/unreleased/5123.fixed.md @@ -0,0 +1,17 @@ +--- +category: fixed +issues: + - 5123 +affected: + - src/CodeIndex/Cli/QueryCommandRunner.Maintenance.cs + - tests/CodeIndex.Tests/QueryCommandRunnerTests.cs + - DEVELOPER_GUIDE.md +--- + +## English + +- **`vacuum --dry-run` now accepts single-slash local file URIs (#5123)** — filesystem validation normalizes supported `file:/absolute/path` spellings while the query-only connection canonicalizes the path form and preserves its original query string, including explicit `immutable=1` semantics. + +## 日本語 + +- **`vacuum --dry-run` が single-slash のローカル file URI を受け付けるようになりました (#5123)** — filesystem 検証では対応済みの `file:/absolute/path` 表記を正規化し、query-only connection では path 表記を canonicalize しつつ元の query string を維持することで、明示的な `immutable=1` semantics も維持します。 diff --git a/src/CodeIndex/Cli/QueryCommandRunner.Maintenance.cs b/src/CodeIndex/Cli/QueryCommandRunner.Maintenance.cs index e28cd5034..3ef59fb63 100644 --- a/src/CodeIndex/Cli/QueryCommandRunner.Maintenance.cs +++ b/src/CodeIndex/Cli/QueryCommandRunner.Maintenance.cs @@ -54,8 +54,23 @@ public static int RunVacuum(string[] cmdArgs, JsonSerializerOptions jsonOptions, MaintenanceDatabaseFailureKind.NotWritable)); } + var validationDbPath = options.DbPath; + var queryOnlyDbPath = options.DbPath; + if (options.DryRun + && SqliteFileUri.StartsWithFileScheme(options.DbPath) + && DbPathResolver.TryNormalizeDbPath(options.DbPath, out var normalizedDbPath, out _) + && !SqliteFileUri.StartsWithFileScheme(normalizedDbPath)) + { + validationDbPath = Path.GetFullPath(normalizedDbPath); + if (options.DbPath.StartsWith("file:/", StringComparison.OrdinalIgnoreCase) + && !options.DbPath.StartsWith("file://", StringComparison.OrdinalIgnoreCase)) + { + queryOnlyDbPath = CanonicalizeSingleSlashFileUri(options.DbPath, validationDbPath); + } + } + if (!DbContext.TryValidateExistingCodeIndexDb( - options.DbPath, + validationDbPath, requireWritable: !options.DryRun, requireSupportedUserVersion: false, out _, @@ -82,7 +97,7 @@ public static int RunVacuum(string[] cmdArgs, JsonSerializerOptions jsonOptions, DbContext.VacuumGenerationWitness? vacuumGenerationWitness; using (var db = DbContext.CreateUnpooled( options.DryRun ? DbOpenIntent.QueryOnly : DbOpenIntent.Repair, - options.DbPath, + options.DryRun ? queryOnlyDbPath : options.DbPath, cancellationToken)) { db.SuppressPlannerStatisticsMaintenanceOnClose(); @@ -163,4 +178,11 @@ private static void WriteVacuumByteTransition(string label, long? before, long? if (before.HasValue && after.HasValue) Console.WriteLine(ConsoleUi.FormatSummaryLine(label, $"{before.Value:N0} -> {after.Value:N0} bytes")); } + + private static string CanonicalizeSingleSlashFileUri(string originalDbUri, string normalizedDbPath) + { + var queryIndex = originalDbUri.IndexOf('?', StringComparison.Ordinal); + var querySuffix = queryIndex >= 0 ? originalDbUri[queryIndex..] : string.Empty; + return CodeIndex.FileUriPolicy.PathToFileUri(normalizedDbPath) + querySuffix; + } } diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs index 3e0f6d350..90c3e6361 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs @@ -1076,6 +1076,65 @@ INSERT INTO vacuum_payload (payload) DbWriter.DefaultFtsOptimizeIncrementalWriteThreshold, ftsOptimization.GetProperty("observed_writes").GetInt64()); Assert.Equal("current", ftsOptimization.GetProperty("state").GetString()); + + using (var walDb = new DbContext(DbOpenIntent.WriteIndex, dbPath)) + { + walDb.SuppressPlannerStatisticsMaintenanceOnClose(); + walDb.CheckpointWalTruncate(); + new DbWriter(walDb).SetMeta(DbWriter.FtsIncrementalWritesSinceOptimizeMetaKey, "0"); + } + + var canonicalDbUri = new Uri(dbPath).AbsoluteUri; + Assert.StartsWith("file:///", canonicalDbUri, StringComparison.OrdinalIgnoreCase); + var singleSlashDbUri = "file:/" + canonicalDbUri["file:///".Length..]; + foreach (var (canonicalUri, singleSlashUri) in new[] + { + (canonicalDbUri + "?immutable=1", singleSlashDbUri + "?immutable=1"), + (canonicalDbUri, singleSlashDbUri), + }) + { + var (canonicalExitCode, canonicalStdout, canonicalStderr) = CaptureVacuum( + ["--db", canonicalUri, "--dry-run", "--json"]); + var (uriExitCode, uriStdout, uriStderr) = CaptureVacuum( + ["--db", singleSlashUri, "--dry-run", "--json"]); + + Assert.Equal(CommandExitCodes.Success, canonicalExitCode); + Assert.Equal(string.Empty, canonicalStderr); + Assert.Equal(CommandExitCodes.Success, uriExitCode); + Assert.Equal(string.Empty, uriStderr); + using var canonicalDocument = ParseJsonOutput(canonicalStdout); + using var uriDocument = ParseJsonOutput(uriStdout); + var uriRoot = uriDocument.RootElement; + Assert.Equal("dry_run", uriRoot.GetProperty("status").GetString()); + Assert.True(uriRoot.GetProperty("dry_run").GetBoolean()); + var canonicalFtsOptimization = canonicalDocument.RootElement + .GetProperty("maintenance_guidance") + .GetProperty("fts_optimization"); + Assert.Equal( + canonicalUri.EndsWith("?immutable=1", StringComparison.Ordinal) ? "stale" : "current", + canonicalFtsOptimization.GetProperty("state").GetString()); + var uriFtsOptimization = uriRoot + .GetProperty("maintenance_guidance") + .GetProperty("fts_optimization"); + Assert.Equal( + canonicalFtsOptimization.GetProperty("recommended").GetBoolean(), + uriFtsOptimization.GetProperty("recommended").GetBoolean()); + Assert.Equal( + canonicalFtsOptimization.GetProperty("action").GetString(), + uriFtsOptimization.GetProperty("action").GetString()); + Assert.Equal( + canonicalFtsOptimization.GetProperty("reason").GetString(), + uriFtsOptimization.GetProperty("reason").GetString()); + Assert.Equal( + canonicalFtsOptimization.GetProperty("threshold_writes").GetInt32(), + uriFtsOptimization.GetProperty("threshold_writes").GetInt32()); + Assert.Equal( + canonicalFtsOptimization.GetProperty("observed_writes").GetInt64(), + uriFtsOptimization.GetProperty("observed_writes").GetInt64()); + Assert.Equal( + canonicalFtsOptimization.GetProperty("state").GetString(), + uriFtsOptimization.GetProperty("state").GetString()); + } } [Fact] From 776b5235b25cedc408a029f0d8828615b58e5697 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sat, 22 Aug 2026 12:54:09 +0900 Subject: [PATCH 2/3] Address file URI review findings (#5123) --- DEVELOPER_GUIDE.md | 4 ++-- changelog.d/unreleased/5123.fixed.md | 5 ++-- .../Cli/QueryCommandRunner.Maintenance.cs | 15 +++++------- src/CodeIndex/PathUriNormalizer.cs | 9 +++++++ .../QueryCommandRunnerTests.cs | 24 +++++++++++++++++++ 5 files changed, 44 insertions(+), 13 deletions(-) diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index f112df89f..86b6af6a4 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -1347,7 +1347,7 @@ Current stable codes and triggers: | Memory tracing | `index --json --memory-trace` adds a `memory_timeline` block to the CLI index result and persists peak working-set MB into `last_index_run`; dry-run results also emit live `start`, `snapshot`, `scan`, and `finalize` samples but never persist run metadata. `index --dry-run --rebuild` bypasses destructive confirmation because it does not delete or rewrite the index. `CDIDX_MEM_WARN_MB=` prints a warning when the sampled working set crosses that threshold. | | Newer schema protection | Writable opens reject databases whose `PRAGMA user_version` contains readiness bits outside the current binary's `CurrentSchemaVersion` mask. Read-only status/query paths may still surface `index_newer_than_reader=true` as a degraded audit signal, but write-capable paths must fail with `E003_SCHEMA_TOO_NEW` so an older cdidx cannot silently rewrite a DB stamped by a newer one. | -`vacuum --dry-run` accepts supported local SQLite URI spellings such as `file:/absolute/path/codeindex.db` and `file:///absolute/path/codeindex.db`. Filesystem validation uses the normalized local path; the query-only vacuum connection canonicalizes a single-slash path spelling while retaining its original query string, so an explicit `immutable=1` keeps its stale-snapshot semantics. +`vacuum --dry-run` accepts supported local SQLite URI spellings such as `file:/absolute/path/codeindex.db`, Windows `file:/C:/absolute/path/codeindex.db`, and canonical `file:///...` forms. Single-slash paths are canonicalized while retaining their original query string, and validation plus metric collection use that same query-only URI so an explicit `immutable=1` keeps its stale-snapshot semantics. ### Data directory resolution @@ -5228,7 +5228,7 @@ apply 時は `PRAGMA optimize` を実行します。 | memory tracing | `index --json --memory-trace` は CLI index 結果に `memory_timeline` block を追加し、peak working-set MB を `last_index_run` に保存します。dry-run 結果も live な `start`、`snapshot`、`scan`、`finalize` sample を返しますが、run metadata は保存しません。`index --dry-run --rebuild` は index を削除も rewrite もしないため destructive confirmation を bypass します。`CDIDX_MEM_WARN_MB=` は sampled working set がしきい値を超えたときに warning を出します。 | | newer schema protection | writable open は、`PRAGMA user_version` に current binary の `CurrentSchemaVersion` mask 外の readiness bit が含まれる database も拒否します。read-only status/query path は degraded audit signal として `index_newer_than_reader=true` を表示できますが、write-capable path は古い cdidx が新しい binary で stamp された DB を黙って rewrite しないよう `E003_SCHEMA_TOO_NEW` で失敗しなければなりません。 | -`vacuum --dry-run` は、`file:/absolute/path/codeindex.db` と `file:///absolute/path/codeindex.db` のような対応済みローカル SQLite URI 表記を受け付けます。filesystem 検証には正規化したローカル path を使います。query-only vacuum connection では single-slash の path 表記を canonicalize しつつ元の query string を維持するため、明示的な `immutable=1` の stale-snapshot semantics も維持されます。 +`vacuum --dry-run` は、`file:/absolute/path/codeindex.db`、Windows の `file:/C:/absolute/path/codeindex.db`、canonical な `file:///...` 形式を受け付けます。single-slash の path を canonicalize しつつ元の query string を維持し、validation と metric 収集に同じ query-only URI を使うため、明示的な `immutable=1` の stale-snapshot semantics も維持されます。 ### データディレクトリ解決 diff --git a/changelog.d/unreleased/5123.fixed.md b/changelog.d/unreleased/5123.fixed.md index 809a0205b..0ba6a0287 100644 --- a/changelog.d/unreleased/5123.fixed.md +++ b/changelog.d/unreleased/5123.fixed.md @@ -3,6 +3,7 @@ category: fixed issues: - 5123 affected: + - src/CodeIndex/PathUriNormalizer.cs - src/CodeIndex/Cli/QueryCommandRunner.Maintenance.cs - tests/CodeIndex.Tests/QueryCommandRunnerTests.cs - DEVELOPER_GUIDE.md @@ -10,8 +11,8 @@ affected: ## English -- **`vacuum --dry-run` now accepts single-slash local file URIs (#5123)** — filesystem validation normalizes supported `file:/absolute/path` spellings while the query-only connection canonicalizes the path form and preserves its original query string, including explicit `immutable=1` semantics. +- **`vacuum --dry-run` now accepts single-slash local file URIs (#5123)** — Unix `file:/absolute/path` and Windows `file:/C:/absolute/path` spellings are canonicalized while preserving the original query string, and validation reads the same snapshot used for metrics, including explicit `immutable=1` semantics. ## 日本語 -- **`vacuum --dry-run` が single-slash のローカル file URI を受け付けるようになりました (#5123)** — filesystem 検証では対応済みの `file:/absolute/path` 表記を正規化し、query-only connection では path 表記を canonicalize しつつ元の query string を維持することで、明示的な `immutable=1` semantics も維持します。 +- **`vacuum --dry-run` が single-slash のローカル file URI を受け付けるようになりました (#5123)** — Unix の `file:/absolute/path` と Windows の `file:/C:/absolute/path` を元の query string を維持したまま canonicalize し、validation と metric 収集で同じ snapshot を読むため、明示的な `immutable=1` semantics も維持します。 diff --git a/src/CodeIndex/Cli/QueryCommandRunner.Maintenance.cs b/src/CodeIndex/Cli/QueryCommandRunner.Maintenance.cs index 3ef59fb63..32e31d2f1 100644 --- a/src/CodeIndex/Cli/QueryCommandRunner.Maintenance.cs +++ b/src/CodeIndex/Cli/QueryCommandRunner.Maintenance.cs @@ -54,23 +54,20 @@ public static int RunVacuum(string[] cmdArgs, JsonSerializerOptions jsonOptions, MaintenanceDatabaseFailureKind.NotWritable)); } - var validationDbPath = options.DbPath; var queryOnlyDbPath = options.DbPath; if (options.DryRun - && SqliteFileUri.StartsWithFileScheme(options.DbPath) + && options.DbPath.StartsWith("file:/", StringComparison.OrdinalIgnoreCase) + && !options.DbPath.StartsWith("file://", StringComparison.OrdinalIgnoreCase) && DbPathResolver.TryNormalizeDbPath(options.DbPath, out var normalizedDbPath, out _) && !SqliteFileUri.StartsWithFileScheme(normalizedDbPath)) { - validationDbPath = Path.GetFullPath(normalizedDbPath); - if (options.DbPath.StartsWith("file:/", StringComparison.OrdinalIgnoreCase) - && !options.DbPath.StartsWith("file://", StringComparison.OrdinalIgnoreCase)) - { - queryOnlyDbPath = CanonicalizeSingleSlashFileUri(options.DbPath, validationDbPath); - } + queryOnlyDbPath = CanonicalizeSingleSlashFileUri( + options.DbPath, + Path.GetFullPath(normalizedDbPath)); } if (!DbContext.TryValidateExistingCodeIndexDb( - validationDbPath, + options.DryRun ? queryOnlyDbPath : options.DbPath, requireWritable: !options.DryRun, requireSupportedUserVersion: false, out _, diff --git a/src/CodeIndex/PathUriNormalizer.cs b/src/CodeIndex/PathUriNormalizer.cs index 6710e5283..ab4317584 100644 --- a/src/CodeIndex/PathUriNormalizer.cs +++ b/src/CodeIndex/PathUriNormalizer.cs @@ -82,6 +82,8 @@ internal static bool TryNormalizeFileUriPath(string fileUri, out string normaliz var relativePath = Uri.UnescapeDataString(pathText["file:".Length..]); if (string.IsNullOrWhiteSpace(relativePath)) return true; + if (OperatingSystem.IsWindows() && HasSingleSlashWindowsDrivePrefix(relativePath)) + relativePath = relativePath[1..]; normalizedPath = Path.GetFullPath(relativePath); return true; } @@ -126,6 +128,13 @@ internal static bool HasWindowsDrivePrefix(string path) && path[1] == ':' && ((path[0] >= 'A' && path[0] <= 'Z') || (path[0] >= 'a' && path[0] <= 'z')); + private static bool HasSingleSlashWindowsDrivePrefix(string path) + => path.Length >= 4 + && path[0] is '/' or '\\' + && path[2] == ':' + && path[3] is '/' or '\\' + && ((path[1] >= 'A' && path[1] <= 'Z') || (path[1] >= 'a' && path[1] <= 'z')); + private static string StripQuery(string uri) { var query = uri.IndexOf('?'); diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs index 90c3e6361..c32692188 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs @@ -1135,6 +1135,30 @@ INSERT INTO vacuum_payload (payload) canonicalFtsOptimization.GetProperty("state").GetString(), uriFtsOptimization.GetProperty("state").GetString()); } + + using (var invalidWalDb = new DbContext(DbOpenIntent.WriteIndex, dbPath)) + { + invalidWalDb.SuppressPlannerStatisticsMaintenanceOnClose(); + invalidWalDb.CheckpointWalTruncate(); + using var command = invalidWalDb.Connection.CreateCommand(); + command.CommandText = "PRAGMA application_id=0"; + command.ExecuteNonQuery(); + } + + foreach (var immutableUri in new[] + { + canonicalDbUri + "?immutable=1", + singleSlashDbUri + "?immutable=1", + }) + { + var (immutableExitCode, immutableStdout, immutableStderr) = CaptureVacuum( + ["--db", immutableUri, "--dry-run", "--json"]); + + Assert.Equal(CommandExitCodes.Success, immutableExitCode); + Assert.Equal(string.Empty, immutableStderr); + using var immutableDocument = ParseJsonOutput(immutableStdout); + Assert.Equal("dry_run", immutableDocument.RootElement.GetProperty("status").GetString()); + } } [Fact] From fcac8483346236b66461d8de80e952766d29c15e Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sat, 22 Aug 2026 13:19:51 +0900 Subject: [PATCH 3/3] Handle file URI fragments in vacuum dry-run (#5123) --- DEVELOPER_GUIDE.md | 4 +- changelog.d/unreleased/5123.fixed.md | 4 +- .../Cli/QueryCommandRunner.Maintenance.cs | 40 ++++++++++++++----- .../QueryCommandRunnerTests.cs | 10 +++-- 4 files changed, 40 insertions(+), 18 deletions(-) diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index 0c58ab67c..de051816b 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -1348,7 +1348,7 @@ Current stable codes and triggers: | Memory tracing | `index --json --memory-trace` adds a `memory_timeline` block to the CLI index result and persists peak working-set MB into `last_index_run`; dry-run results also emit live `start`, `snapshot`, `scan`, and `finalize` samples but never persist run metadata. `index --dry-run --rebuild` bypasses destructive confirmation because it does not delete or rewrite the index. `CDIDX_MEM_WARN_MB=` prints a warning when the sampled working set crosses that threshold. | | Newer schema protection | Writable opens reject databases whose `PRAGMA user_version` contains readiness bits outside the current binary's `CurrentSchemaVersion` mask. Read-only status/query paths may still surface `index_newer_than_reader=true` as a degraded audit signal, but write-capable paths must fail with `E003_SCHEMA_TOO_NEW` so an older cdidx cannot silently rewrite a DB stamped by a newer one. | -`vacuum --dry-run` accepts supported local SQLite URI spellings such as `file:/absolute/path/codeindex.db`, Windows `file:/C:/absolute/path/codeindex.db`, and canonical `file:///...` forms. Single-slash paths are canonicalized while retaining their original query string, and validation plus metric collection use that same query-only URI so an explicit `immutable=1` keeps its stale-snapshot semantics. +`vacuum --dry-run` accepts supported local SQLite URI spellings such as `file:/absolute/path/codeindex.db`, Windows `file:/C:/absolute/path/codeindex.db`, and canonical `file:///...` forms. Single-slash paths are canonicalized while retaining their original query string and ignoring URI fragments, and validation plus metric collection use that same query-only URI so an explicit `immutable=1` keeps its stale-snapshot semantics. ### Data directory resolution @@ -5230,7 +5230,7 @@ apply 時は `PRAGMA optimize` を実行します。 | memory tracing | `index --json --memory-trace` は CLI index 結果に `memory_timeline` block を追加し、peak working-set MB を `last_index_run` に保存します。dry-run 結果も live な `start`、`snapshot`、`scan`、`finalize` sample を返しますが、run metadata は保存しません。`index --dry-run --rebuild` は index を削除も rewrite もしないため destructive confirmation を bypass します。`CDIDX_MEM_WARN_MB=` は sampled working set がしきい値を超えたときに warning を出します。 | | newer schema protection | writable open は、`PRAGMA user_version` に current binary の `CurrentSchemaVersion` mask 外の readiness bit が含まれる database も拒否します。read-only status/query path は degraded audit signal として `index_newer_than_reader=true` を表示できますが、write-capable path は古い cdidx が新しい binary で stamp された DB を黙って rewrite しないよう `E003_SCHEMA_TOO_NEW` で失敗しなければなりません。 | -`vacuum --dry-run` は、`file:/absolute/path/codeindex.db`、Windows の `file:/C:/absolute/path/codeindex.db`、canonical な `file:///...` 形式を受け付けます。single-slash の path を canonicalize しつつ元の query string を維持し、validation と metric 収集に同じ query-only URI を使うため、明示的な `immutable=1` の stale-snapshot semantics も維持されます。 +`vacuum --dry-run` は、`file:/absolute/path/codeindex.db`、Windows の `file:/C:/absolute/path/codeindex.db`、canonical な `file:///...` 形式を受け付けます。single-slash の path を canonicalize しつつ元の query string を維持して URI fragment を無視し、validation と metric 収集に同じ query-only URI を使うため、明示的な `immutable=1` の stale-snapshot semantics も維持されます。 ### データディレクトリ解決 diff --git a/changelog.d/unreleased/5123.fixed.md b/changelog.d/unreleased/5123.fixed.md index 0ba6a0287..cac745aba 100644 --- a/changelog.d/unreleased/5123.fixed.md +++ b/changelog.d/unreleased/5123.fixed.md @@ -11,8 +11,8 @@ affected: ## English -- **`vacuum --dry-run` now accepts single-slash local file URIs (#5123)** — Unix `file:/absolute/path` and Windows `file:/C:/absolute/path` spellings are canonicalized while preserving the original query string, and validation reads the same snapshot used for metrics, including explicit `immutable=1` semantics. +- **`vacuum --dry-run` now accepts single-slash local file URIs (#5123)** — Unix `file:/absolute/path` and Windows `file:/C:/absolute/path` spellings are canonicalized while preserving the original query string and ignoring URI fragments, and validation reads the same snapshot used for metrics, including explicit `immutable=1` semantics. ## 日本語 -- **`vacuum --dry-run` が single-slash のローカル file URI を受け付けるようになりました (#5123)** — Unix の `file:/absolute/path` と Windows の `file:/C:/absolute/path` を元の query string を維持したまま canonicalize し、validation と metric 収集で同じ snapshot を読むため、明示的な `immutable=1` semantics も維持します。 +- **`vacuum --dry-run` が single-slash のローカル file URI を受け付けるようになりました (#5123)** — Unix の `file:/absolute/path` と Windows の `file:/C:/absolute/path` を、元の query string を維持して URI fragment を無視しつつ canonicalize し、validation と metric 収集で同じ snapshot を読むため、明示的な `immutable=1` semantics も維持します。 diff --git a/src/CodeIndex/Cli/QueryCommandRunner.Maintenance.cs b/src/CodeIndex/Cli/QueryCommandRunner.Maintenance.cs index 32e31d2f1..a78f9b662 100644 --- a/src/CodeIndex/Cli/QueryCommandRunner.Maintenance.cs +++ b/src/CodeIndex/Cli/QueryCommandRunner.Maintenance.cs @@ -54,16 +54,13 @@ public static int RunVacuum(string[] cmdArgs, JsonSerializerOptions jsonOptions, MaintenanceDatabaseFailureKind.NotWritable)); } - var queryOnlyDbPath = options.DbPath; + var queryOnlyDbPath = options.DryRun + ? StripFileUriFragment(options.DbPath) + : options.DbPath; if (options.DryRun - && options.DbPath.StartsWith("file:/", StringComparison.OrdinalIgnoreCase) - && !options.DbPath.StartsWith("file://", StringComparison.OrdinalIgnoreCase) - && DbPathResolver.TryNormalizeDbPath(options.DbPath, out var normalizedDbPath, out _) - && !SqliteFileUri.StartsWithFileScheme(normalizedDbPath)) + && TryCanonicalizeSingleSlashFileUri(queryOnlyDbPath, out var canonicalDbUri)) { - queryOnlyDbPath = CanonicalizeSingleSlashFileUri( - options.DbPath, - Path.GetFullPath(normalizedDbPath)); + queryOnlyDbPath = canonicalDbUri; } if (!DbContext.TryValidateExistingCodeIndexDb( @@ -176,10 +173,33 @@ private static void WriteVacuumByteTransition(string label, long? before, long? Console.WriteLine(ConsoleUi.FormatSummaryLine(label, $"{before.Value:N0} -> {after.Value:N0} bytes")); } - private static string CanonicalizeSingleSlashFileUri(string originalDbUri, string normalizedDbPath) + private static bool TryCanonicalizeSingleSlashFileUri(string originalDbUri, out string canonicalDbUri) { + canonicalDbUri = originalDbUri; + if (!originalDbUri.StartsWith("file:/", StringComparison.OrdinalIgnoreCase) + || originalDbUri.StartsWith("file://", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + if (!DbPathResolver.TryNormalizeDbPath(originalDbUri, out var normalizedDbPath, out _) + || SqliteFileUri.StartsWithFileScheme(normalizedDbPath)) + { + return false; + } + var queryIndex = originalDbUri.IndexOf('?', StringComparison.Ordinal); var querySuffix = queryIndex >= 0 ? originalDbUri[queryIndex..] : string.Empty; - return CodeIndex.FileUriPolicy.PathToFileUri(normalizedDbPath) + querySuffix; + canonicalDbUri = CodeIndex.FileUriPolicy.PathToFileUri(Path.GetFullPath(normalizedDbPath)) + querySuffix; + return true; + } + + private static string StripFileUriFragment(string dbPath) + { + if (!SqliteFileUri.StartsWithFileScheme(dbPath)) + return dbPath; + + var fragmentIndex = dbPath.IndexOf('#', StringComparison.Ordinal); + return fragmentIndex >= 0 ? dbPath[..fragmentIndex] : dbPath; } } diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs index c32692188..2dd3a0298 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs @@ -1087,10 +1087,12 @@ INSERT INTO vacuum_payload (payload) var canonicalDbUri = new Uri(dbPath).AbsoluteUri; Assert.StartsWith("file:///", canonicalDbUri, StringComparison.OrdinalIgnoreCase); var singleSlashDbUri = "file:/" + canonicalDbUri["file:///".Length..]; - foreach (var (canonicalUri, singleSlashUri) in new[] + foreach (var (canonicalUri, singleSlashUri, expectedFtsState) in new[] { - (canonicalDbUri + "?immutable=1", singleSlashDbUri + "?immutable=1"), - (canonicalDbUri, singleSlashDbUri), + (canonicalDbUri + "?immutable=1", singleSlashDbUri + "?immutable=1", "stale"), + (canonicalDbUri + "?immutable=1#ignored", singleSlashDbUri + "?immutable=1#ignored", "stale"), + (canonicalDbUri + "#ignored", singleSlashDbUri + "#ignored", "current"), + (canonicalDbUri, singleSlashDbUri, "current"), }) { var (canonicalExitCode, canonicalStdout, canonicalStderr) = CaptureVacuum( @@ -1111,7 +1113,7 @@ INSERT INTO vacuum_payload (payload) .GetProperty("maintenance_guidance") .GetProperty("fts_optimization"); Assert.Equal( - canonicalUri.EndsWith("?immutable=1", StringComparison.Ordinal) ? "stale" : "current", + expectedFtsState, canonicalFtsOptimization.GetProperty("state").GetString()); var uriFtsOptimization = uriRoot .GetProperty("maintenance_guidance")