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
16 changes: 16 additions & 0 deletions changelog.d/unreleased/2932.fixed.md
Original file line number Diff line number Diff line change
@@ -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 を避けます。
89 changes: 89 additions & 0 deletions src/CodeIndex/Database/DbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -2183,6 +2237,8 @@ public void TryMigrateForRead()
if (_isReadOnly) return;

LastMigrationFailure = null;
if (ReadMigrationSchemaIsCurrent())
return;

try
{
Expand Down Expand Up @@ -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-
Expand Down Expand Up @@ -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();
Expand Down
39 changes: 39 additions & 0 deletions tests/CodeIndex.Tests/LegacySchemaMigrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
Loading