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/1988.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
category: fixed
issues:
- 1988
affected:
- src/CodeIndex/Database/DbContext.cs
- tests/CodeIndex.Tests/LegacySchemaMigrationTests.cs
---

## English

- **Column migrations re-check after taking the write lock (#1988)** — standalone `ALTER TABLE ADD COLUMN` migrations now re-read column state after `BEGIN IMMEDIATE`, avoiding duplicate concurrent DDL attempts when another process completed the migration first.

## 日本語

- **列 migration が write lock 取得後に再確認するようになりました (#1988)** — 単独の `ALTER TABLE ADD COLUMN` migration は `BEGIN IMMEDIATE` 後に列状態を再確認し、別プロセスが先に migration を完了した場合の重複 DDL 試行を避けます。
16 changes: 16 additions & 0 deletions changelog.d/unreleased/2025.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
category: fixed
issues:
- 2025
affected:
- src/CodeIndex/Database/DbWriter.cs
- tests/CodeIndex.Tests/LegacySchemaMigrationTests.cs
---

## English

- **Metadata stamps now skip DBs without `codeindex_meta` (#2025)** — writer metadata updates are best-effort when the metadata table is absent, preventing missing-table failures from turning indexing cleanup into an unhelpful crash.

## 日本語

- **`codeindex_meta` が無い DB では metadata stamp をスキップするようになりました (#2025)** — metadata table が存在しない場合の writer metadata 更新を best-effort にし、欠落テーブルによる indexing cleanup の分かりにくいクラッシュを防ぎました。
16 changes: 16 additions & 0 deletions changelog.d/unreleased/2026.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
category: fixed
issues:
- 2026
affected:
- src/CodeIndex/Database/DbContext.cs
- tests/CodeIndex.Tests/LegacySchemaMigrationTests.cs
---

## English

- **`codeindex_meta` now has a metadata-policy schema stamp (#2026)** — schema initialization records the metadata-key policy version and prunes known deprecated null keys while preserving unknown future contract stamps for forward-compatibility checks.

## 日本語

- **`codeindex_meta` に metadata policy の schema stamp を追加しました (#2026)** — schema 初期化時に metadata key policy version を記録し、既知の廃止済み null key を削除しつつ、forward-compatibility check 用の未知の将来 contract stamp は保持します。
17 changes: 17 additions & 0 deletions changelog.d/unreleased/2037.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
category: fixed
issues:
- 2037
affected:
- src/CodeIndex/Cli/QueryCommandRunner.cs
- src/CodeIndex/Database/DbContext.cs
- tests/CodeIndex.Tests/QueryCommandRunnerTests.cs
---

## English

- **Query commands now reject non-CodeIndex SQLite DBs early (#2037)** — `--db` query paths validate the minimal CodeIndex table set before opening a reader and print a direct rebuild hint for empty or wrong-schema SQLite files.

## 日本語

- **query command が CodeIndex ではない SQLite DB を早期に拒否するようになりました (#2037)** — `--db` query 経路は reader を開く前に最小限の CodeIndex table set を検証し、空または別 schema の SQLite file に対して直接的な rebuild hint を表示します。
12 changes: 12 additions & 0 deletions src/CodeIndex/Cli/QueryCommandRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,9 @@ public static int RunBatch(string[] cmdArgs, JsonSerializerOptions jsonOptions)
try
{
using var db = new DbContext(dbPath);
if (!db.TryValidateIsCodeIndexDb(out var validationReason))
return WriteInvalidCodeIndexDbError(dbPath, validationReason);

db.TryMigrateForRead();
s_batchReader = new DbReader(db);
var firstFailure = CommandExitCodes.Success;
Expand Down Expand Up @@ -4683,6 +4686,8 @@ private static int WithDb(QueryCommandOptions options, JsonSerializerOptions jso
else
{
db = new DbContext(dbPath);
if (!db.TryValidateIsCodeIndexDb(out var validationReason))
return WriteInvalidCodeIndexDbError(dbPath, validationReason);
db.TryMigrateForRead();
reader = new DbReader(db);
}
Expand Down Expand Up @@ -4752,6 +4757,13 @@ private static int WithDb(QueryCommandOptions options, JsonSerializerOptions jso
}
}

private static int WriteInvalidCodeIndexDbError(string dbPath, string? validationReason)
{
Console.Error.WriteLine($"Error [{CommandErrorCodes.DbError}]: {dbPath} does not appear to be a valid CodeIndex database ({validationReason}).");
Console.Error.WriteLine("Hint: rebuild with `cdidx index <projectPath> --db <path>` to create a fresh database.");
return CommandExitCodes.DatabaseError;
}

private static string? GetDataDirectoryPath(string? dbPath)
{
if (string.IsNullOrWhiteSpace(dbPath) ||
Expand Down
120 changes: 115 additions & 5 deletions src/CodeIndex/Database/DbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public class DbContext : IDisposable
private readonly bool _isReadOnly;
private readonly string? _schemaCacheKey;
private SqliteTransaction? _activeMigrationTransaction;
private bool _readMigrationInsideExternalTransaction;
private DbSchemaCache? _schemaCache;
private PreparedCommandCache? _preparedCommands;
private bool _suppressWriteWorkTracking = true;
Expand Down Expand Up @@ -1035,6 +1036,8 @@ private static void RegisterConnectionFunctionsWithRetry(
// bit 2 (FoldReadyFlag, #86): name_folded 列の完全バックフィル完了を示す。
public const int FoldReadyFlag = 4;
public const int CurrentSchemaVersion = GraphReadyFlag | IssuesReadyFlag | FoldReadyFlag; // 7 — full CLI readiness
public const int CodeIndexMetaSchemaVersion = 1;
public const string CodeIndexMetaSchemaVersionMetaKey = "codeindex_meta_schema_version";
// Query-semantic readiness for hotspot family grouping. Stored in codeindex_meta instead of
// PRAGMA user_version because this guards a higher-level interpretation contract
// (`family_key` / `container_qualified_name` are authoritative for the whole DB), not
Expand Down Expand Up @@ -1243,6 +1246,22 @@ public void ClearReadyFlags()
return raw is string s ? s : null;
}

public bool TryValidateIsCodeIndexDb(out string? reason)
{
var requiredTables = new[] { "files", "symbols" };
foreach (var table in requiredTables)
{
if (!TableExists(table))
{
reason = $"missing required table `{table}`";
return false;
}
}

reason = null;
return true;
}

private bool TableExists(string name)
{
using var cmd = _connection.CreateCommand();
Expand Down Expand Up @@ -1355,6 +1374,7 @@ CREATE TABLE IF NOT EXISTS codeindex_meta (
key TEXT PRIMARY KEY NOT NULL,
value TEXT
)");
NormalizeCodeIndexMetaKeys();

// Schema migrations for existing DBs / 既存DB向けスキーマ移行
EnsureColumn("files", "checksum", "TEXT");
Expand Down Expand Up @@ -1818,14 +1838,12 @@ public void TryMigrateForRead()
}
catch (SqliteException ex) when (IsNestedTransactionError(ex))
{
if (RunReadMigrationSteps())
EnsureForeignKeysEnabled();
RunReadMigrationStepsInsideExternalTransaction();
return;
}
catch (InvalidOperationException ex) when (IsNestedTransactionError(ex))
{
if (RunReadMigrationSteps())
EnsureForeignKeysEnabled();
RunReadMigrationStepsInsideExternalTransaction();
return;
}
catch (SqliteException ex) when (IsReadOnlyOpenError(ex))
Expand Down Expand Up @@ -1855,6 +1873,20 @@ public void TryMigrateForRead()
}
}

private void RunReadMigrationStepsInsideExternalTransaction()
{
_readMigrationInsideExternalTransaction = true;
try
{
if (RunReadMigrationSteps())
EnsureForeignKeysEnabled();
}
finally
{
_readMigrationInsideExternalTransaction = false;
}
}

private bool RunReadMigrationSteps()
{
try
Expand Down Expand Up @@ -2072,8 +2104,19 @@ private static void EmitMigrationFailureWarning(DbMigrationFailure failure)

private void EnsureColumn(string tableName, string columnName, string definition)
{
if (_activeMigrationTransaction != null || _readMigrationInsideExternalTransaction)
{
DbColumnEnsurer.EnsureColumn(
() => ColumnExists(tableName, columnName),
() => Execute($"ALTER TABLE {tableName} ADD COLUMN {columnName} {definition}"));
return;
}

DbColumnEnsurer.EnsureColumn(
() => ColumnExists(tableName, columnName),
beginImmediate: () => Execute("BEGIN IMMEDIATE"),
commit: () => Execute("COMMIT"),
rollback: () => Execute("ROLLBACK"),
() => Execute($"ALTER TABLE {tableName} ADD COLUMN {columnName} {definition}"));
}

Expand Down Expand Up @@ -2102,6 +2145,33 @@ private string ExecuteScalar(string sql)
return cmd.ExecuteScalar()?.ToString() ?? "";
}

private void NormalizeCodeIndexMetaKeys()
{
if (!TableExists("codeindex_meta"))
return;

using (var delete = _connection.CreateCommand())
{
if (_activeMigrationTransaction != null)
delete.Transaction = _activeMigrationTransaction;

delete.CommandText = @"
DELETE FROM codeindex_meta
WHERE key IN ('hotspot_family_version', 'hotspot_family_marker_fingerprint')
AND value IS NULL";
delete.ExecuteNonQuery();
}

using var stamp = _connection.CreateCommand();
if (_activeMigrationTransaction != null)
stamp.Transaction = _activeMigrationTransaction;
stamp.CommandText = @"
INSERT INTO codeindex_meta (key, value) VALUES ('codeindex_meta_schema_version', @version)
ON CONFLICT(key) DO UPDATE SET value = excluded.value";
stamp.Parameters.AddWithValue("@version", CodeIndexMetaSchemaVersion.ToString(CultureInfo.InvariantCulture));
stamp.ExecuteNonQuery();
}

internal void MarkWriteWork()
{
if (!_isReadOnly && !_suppressWriteWorkTracking)
Expand Down Expand Up @@ -2167,14 +2237,38 @@ public sealed record DbMigrationFailure(

internal static class DbColumnEnsurer
{
internal static void EnsureColumn(Func<bool> columnExists, Action alterColumn)
internal static void EnsureColumn(
Func<bool> columnExists,
Action? beginImmediate,
Action? commit,
Action? rollback,
Action alterColumn)
{
if (columnExists())
return;

var hasTransactionHooks = beginImmediate != null && commit != null && rollback != null;
var transactionStarted = false;
try
{
if (hasTransactionHooks)
{
beginImmediate!();
transactionStarted = true;
if (columnExists())
{
commit!();
transactionStarted = false;
return;
}
}

alterColumn();
if (transactionStarted)
{
commit!();
transactionStarted = false;
}
}
catch (SqliteException ex) when (IsDuplicateColumnRace(ex, columnExists))
{
Expand All @@ -2184,9 +2278,25 @@ internal static void EnsureColumn(Func<bool> columnExists, Action alterColumn)
// or future wording changes still recover (#1532, #1690).
// 列存在を PRAGMA 相当の状態で再確認し、SQLite の英語メッセージに依存せず
// 「移行済み」を判定する (#1532)。
if (transactionStarted)
{
try { rollback!(); } catch (SqliteException) { }
transactionStarted = false;
}
}
catch
{
if (transactionStarted)
{
try { rollback!(); } catch (SqliteException) { }
}
throw;
}
}

internal static void EnsureColumn(Func<bool> columnExists, Action alterColumn)
=> EnsureColumn(columnExists, beginImmediate: null, commit: null, rollback: null, alterColumn);

private static bool IsDuplicateColumnRace(SqliteException exception, Func<bool> columnExists)
{
if (!IsDuplicateColumnAddError(exception))
Expand Down
5 changes: 5 additions & 0 deletions src/CodeIndex/Database/DbWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2827,6 +2827,9 @@ private bool ColumnExists(string table, string column)
/// </summary>
public void SetMeta(string key, string? value)
{
if (!HasMetaTable())
return;

using var cmd = _conn.CreateCommand();
cmd.CommandText = @"INSERT INTO codeindex_meta (key, value) VALUES (@key, @value)
ON CONFLICT(key) DO UPDATE SET value = excluded.value";
Expand All @@ -2844,6 +2847,8 @@ public void SetMeta(string key, string? value)
}
public void ClearReadyFlags() => Execute("PRAGMA user_version = 0");

public bool HasMetaTable() => TableExists("codeindex_meta");

private bool TableExists(string name)
{
using var cmd = _conn.CreateCommand();
Expand Down
Loading
Loading