diff --git a/changelog.d/unreleased/2932.fixed.md b/changelog.d/unreleased/2932.fixed.md new file mode 100644 index 0000000000..5a70f5fe7f --- /dev/null +++ b/changelog.d/unreleased/2932.fixed.md @@ -0,0 +1,16 @@ +--- +category: fixed +issues: + - 2932 +affected: + - src/CodeIndex/Database/DbContext.cs + - tests/CodeIndex.Tests/LegacySchemaMigrationTests.cs +--- + +## English + +- **Read-path schema migration now skips current databases without taking a writer lock (#2932)** — `TryMigrateForRead` now confirms the read-migration schema is already present before starting `BEGIN IMMEDIATE`, avoiding intermittent SQLite failures when status reads race with active writers on an up-to-date database. + +## 日本語 + +- **read path の schema migration が current DB では writer lock を取らずにスキップするようになりました (#2932)** — `TryMigrateForRead` は read migration 対象の schema が既に揃っていることを確認してから `BEGIN IMMEDIATE` に進むため、最新 DB に対する status 読み取りが active writer と競合した際の断続的な SQLite failure を避けます。 diff --git a/src/CodeIndex/Database/DbContext.cs b/src/CodeIndex/Database/DbContext.cs index 97f0e80806..fdfbbcbd57 100644 --- a/src/CodeIndex/Database/DbContext.cs +++ b/src/CodeIndex/Database/DbContext.cs @@ -27,6 +27,60 @@ public class DbContext : IDisposable "chunks", "symbols", ]; + private static readonly string[] ReadMigrationRequiredTables = + [ + "reference_lines", + "symbol_references", + "file_issues", + "codeindex_meta", + ]; + private static readonly (string Table, string Column)[] ReadMigrationRequiredColumns = + [ + ("symbol_references", "reference_line_id"), + ("symbol_references", "is_self_reference"), + ("symbol_references", "is_mutual_recursion"), + ("symbol_references", "symbol_name_folded"), + ("symbol_references", "container_name_folded"), + ("files", "checksum"), + ("files", "modified"), + ("files", "indexed_at"), + ("symbols", "start_line"), + ("symbols", "end_line"), + ("symbols", "body_start_line"), + ("symbols", "body_end_line"), + ("symbols", "signature"), + ("symbols", "container_kind"), + ("symbols", "container_name"), + ("symbols", "container_qualified_name"), + ("symbols", "family_key"), + ("symbols", "visibility"), + ("symbols", "return_type"), + ("symbols", "is_metadata_target"), + ("symbols", "name_folded"), + ]; + private static readonly string[] ReadMigrationRequiredIndexes = + [ + "idx_symbol_refs_name", + "idx_symbol_refs_file", + "idx_symbol_refs_container", + "idx_symbol_refs_container_kind", + "idx_symbol_refs_name_kind", + "idx_symbol_refs_name_file", + "idx_reference_lines_file_line", + "idx_symbol_refs_reference_line", + "idx_symbol_refs_name_nocase", + "idx_symbol_refs_container_nocase", + "idx_symbol_refs_name_nocase_kind", + "idx_symbol_refs_name_nocase_file", + "idx_symbol_refs_container_nocase_kind", + "idx_symbols_name_nocase", + "idx_symbols_name_folded", + "idx_symbol_refs_symbol_name_folded", + "idx_symbol_refs_container_name_folded", + "idx_symbol_refs_symbol_name_folded_kind", + "idx_symbol_refs_symbol_name_folded_file", + "idx_symbol_refs_container_name_folded_kind", + ]; private SqliteConnection _connection = null!; private bool _isReadOnly; @@ -2183,6 +2237,8 @@ public void TryMigrateForRead() if (_isReadOnly) return; LastMigrationFailure = null; + if (ReadMigrationSchemaIsCurrent()) + return; try { @@ -2412,6 +2468,29 @@ value TEXT )")); } + private bool ReadMigrationSchemaIsCurrent() + { + foreach (var table in ReadMigrationRequiredTables) + { + if (!TableExists(table)) + return false; + } + + foreach (var (table, column) in ReadMigrationRequiredColumns) + { + if (!ColumnExists(table, column)) + return false; + } + + foreach (var index in ReadMigrationRequiredIndexes) + { + if (!IndexExists(index)) + return false; + } + + return true; + } + private string BuildMigrationSuggestedAction(int sqliteErrorCode) { // 8 = SQLITE_READONLY, 10 = SQLITE_IOERR, 14 = SQLITE_CANTOPEN: classic restricted- @@ -2492,6 +2571,16 @@ private bool ColumnExists(string tableName, string columnName) return false; } + private bool IndexExists(string name) + { + using var cmd = _connection.CreateCommand(); + if (_activeMigrationTransaction != null) + cmd.Transaction = _activeMigrationTransaction; + cmd.CommandText = "SELECT 1 FROM sqlite_master WHERE type = 'index' AND name = @name"; + cmd.Parameters.AddWithValue("@name", name); + return cmd.ExecuteScalar() != null; + } + private string ExecuteScalar(string sql) { using var cmd = _connection.CreateCommand(); diff --git a/tests/CodeIndex.Tests/LegacySchemaMigrationTests.cs b/tests/CodeIndex.Tests/LegacySchemaMigrationTests.cs index 26ad8c04a5..c0d1dfc979 100644 --- a/tests/CodeIndex.Tests/LegacySchemaMigrationTests.cs +++ b/tests/CodeIndex.Tests/LegacySchemaMigrationTests.cs @@ -1562,6 +1562,45 @@ public void InitializeSchema_PrunesKnownDeprecatedNullMetaKeysAndStampsMetaSchem } } + [Fact] + public void TryMigrateForRead_CurrentSchema_DoesNotTakeWriterLockDuringActiveWrite_Issue2932() + { + var dir = Path.Combine(Path.GetTempPath(), $"codeindex_read_migration_current_{Guid.NewGuid():N}"); + Directory.CreateDirectory(dir); + var dbPath = Path.Combine(dir, "codeindex.db"); + try + { + using (var setup = new DbContext(dbPath)) + { + setup.InitializeSchema(); + } + + using var readDb = new DbContext(dbPath); + using var writeDb = new DbContext(dbPath); + var writer = new DbWriter(writeDb.Connection); + using var txn = writer.BeginTransaction(); + writer.UpsertFile(new FileRecord + { + Path = "src/active-writer.cs", + Lang = "csharp", + Size = 10, + Lines = 1, + Modified = ManualTimeProvider.FixtureUtcNow.UtcDateTime, + Checksum = "active-writer", + }); + + readDb.TryMigrateForRead(); + + Assert.Null(readDb.LastMigrationFailure); + txn.Commit(); + } + finally + { + SqliteConnection.ClearAllPools(); + try { Directory.Delete(dir, recursive: true); } catch { } + } + } + [Fact] public async Task TryMigrateForRead_ConcurrentLegacyMigrations_SerializeAndComplete() {