Skip to content

Commit 21dfee7

Browse files
committed
Make SetMeta transactional for #1753
1 parent 7c49bb8 commit 21dfee7

4 files changed

Lines changed: 97 additions & 0 deletions

File tree

DEVELOPER_GUIDE.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,16 @@ Incremental refreshes that mutate `fts_chunks` increment `codeindex_meta.fts_inc
157157

158158
Successful writer sessions attempt `PRAGMA wal_checkpoint(TRUNCATE)` before closing a writable `DbContext`, so large WAL files are reclaimed after index, backfill, optimize, prune, and other DB-writing commands. `cdidx db schema [--json]` dumps `sqlite_master` entries plus `PRAGMA user_version` for schema inspection, and `cdidx db prune --dry-run|--apply [--json]` counts or deletes orphaned `symbol_references`, `reference_lines`, and `symbols` rows before running `PRAGMA optimize` on apply.
159159

160+
### Metadata invariants
161+
162+
`DbWriter.SetMeta` participates in the caller's writer transaction when one is
163+
active. When no writer transaction is active, it wraps the metadata UPSERT in a
164+
SQLite savepoint so standalone stamps still have a commit boundary and calls
165+
from raw SQL transactions do not attempt a nested `BEGIN`. Dependent metadata
166+
and row rewrites that must succeed or fail together should be placed inside the
167+
same `DbWriter.BeginTransaction()` scope; do not stamp readiness or schema
168+
trust metadata before the dependent rows are written.
169+
160170
### Extending the indexer
161171

162172
Out-of-tree post-extraction hooks can implement `CodeIndex.Indexer.Hooks.IPostExtractionHook` in a `.dll` placed under `~/.config/cdidx/hooks/` (or the directory named by `CDIDX_HOOKS_DIR`). Hook assemblies are discovered in path order. Each concrete hook type is instantiated with a public parameterless constructor, then called after built-in symbol extraction and again after built-in reference extraction, before rows are persisted. Hooks receive a `FileContext` plus mutable `IList<SymbolRecord>` / `IList<ReferenceRecord>` values, so they can annotate extracted records, add synthetic symbols, or add domain-specific references.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
category: fixed
3+
issues:
4+
- 1753
5+
affected:
6+
- src/CodeIndex/Database/DbWriter.cs
7+
- tests/CodeIndex.Tests/DatabaseTests.cs
8+
- DEVELOPER_GUIDE.md
9+
---
10+
11+
## English
12+
13+
- **Made metadata stamps participate in transaction boundaries (#1753)**`SetMeta` now joins writer transactions or uses a SQLite savepoint for standalone writes so metadata and dependent rows can roll back together.
14+
15+
## 日本語
16+
17+
- **metadata stamp が transaction 境界に参加するようにしました (#1753)**`SetMeta` は writer transaction に参加し、単独書き込みでは SQLite savepoint を使うため、metadata と依存 row をまとめて rollback できます。

src/CodeIndex/Database/DbWriter.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3025,7 +3025,30 @@ public void SetMeta(string key, string? value)
30253025
if (!HasMetaTable())
30263026
return;
30273027

3028+
if (!IsInTransaction())
3029+
{
3030+
Execute("SAVEPOINT set_meta_atomic");
3031+
try
3032+
{
3033+
SetMetaCore(key, value);
3034+
Execute("RELEASE SAVEPOINT set_meta_atomic");
3035+
}
3036+
catch
3037+
{
3038+
try { Execute("ROLLBACK TO SAVEPOINT set_meta_atomic"); } catch (SqliteException) { /* best effort */ }
3039+
try { Execute("RELEASE SAVEPOINT set_meta_atomic"); } catch (SqliteException) { /* best effort */ }
3040+
throw;
3041+
}
3042+
return;
3043+
}
3044+
3045+
SetMetaCore(key, value);
3046+
}
3047+
3048+
private void SetMetaCore(string key, string? value)
3049+
{
30283050
using var cmd = _conn.CreateCommand();
3051+
cmd.Transaction = _activeTransaction;
30293052
cmd.CommandText = @"INSERT INTO codeindex_meta (key, value) VALUES (@key, @value)
30303053
ON CONFLICT(key) DO UPDATE SET value = excluded.value";
30313054
cmd.Parameters.AddWithValue("@key", key);

tests/CodeIndex.Tests/DatabaseTests.cs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2429,6 +2429,45 @@ public void DbContext_NewDatabaseRestrictsFileModeOnPosix()
24292429
Assert.Equal("0600", DbContext.GetUnixFileModeString(_dbPath));
24302430
}
24312431

2432+
[Fact]
2433+
public void SetMeta_InsideWriterTransaction_RollsBackWithDependentRows_Issue1753()
2434+
{
2435+
using (var transaction = _writer.BeginTransaction())
2436+
{
2437+
_writer.SetMeta("schema_phase", "new");
2438+
_writer.UpsertFile(new FileRecord
2439+
{
2440+
Path = "src/partial.cs",
2441+
Lang = "csharp",
2442+
Size = 12,
2443+
Lines = 1,
2444+
Modified = new DateTime(2026, 5, 31, 0, 0, 0, DateTimeKind.Utc),
2445+
Checksum = "partial",
2446+
});
2447+
}
2448+
2449+
Assert.Null(ReadMeta("schema_phase"));
2450+
Assert.False(_writer.HasFileAtPath("src/partial.cs"));
2451+
}
2452+
2453+
[Fact]
2454+
public void SetMeta_InsideRawSqlTransaction_UsesSavepointWithoutNestedBegin_Issue1753()
2455+
{
2456+
ExecuteNonQuery(_db.Connection, "BEGIN IMMEDIATE");
2457+
try
2458+
{
2459+
_writer.SetMeta("raw_phase", "new");
2460+
ExecuteNonQuery(_db.Connection, "ROLLBACK");
2461+
}
2462+
catch
2463+
{
2464+
ExecuteNonQuery(_db.Connection, "ROLLBACK");
2465+
throw;
2466+
}
2467+
2468+
Assert.Null(ReadMeta("raw_phase"));
2469+
}
2470+
24322471
private void DeleteDbPath()
24332472
{
24342473
DeleteDbFiles(_dbPath);
@@ -2476,6 +2515,14 @@ private string ExecuteScalarString(string sql)
24762515
private long ExecuteScalarLong(string sql)
24772516
=> ExecuteScalarLong(_db.Connection, sql);
24782517

2518+
private string? ReadMeta(string key)
2519+
{
2520+
using var cmd = _db.Connection.CreateCommand();
2521+
cmd.CommandText = "SELECT value FROM codeindex_meta WHERE key = @key";
2522+
cmd.Parameters.AddWithValue("@key", key);
2523+
return cmd.ExecuteScalar() as string;
2524+
}
2525+
24792526
private static long ExecuteScalarLong(SqliteConnection connection, string sql)
24802527
{
24812528
using var cmd = connection.CreateCommand();

0 commit comments

Comments
 (0)