Skip to content

Commit ed77ad9

Browse files
authored
Fix stale reference cleanup (#2700)
* Fix dangling reference line cleanup (#1781) * Batch stale file deletes (#1826) * Clean stale symbol references (#1785)
2 parents 78406ab + cd4b3c1 commit ed77ad9

6 files changed

Lines changed: 303 additions & 41 deletions

File tree

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
category: fixed
3+
issues:
4+
- 1781
5+
affected:
6+
- src/CodeIndex/Database/DbContext.cs
7+
- tests/CodeIndex.Tests/DatabaseTests.cs
8+
---
9+
10+
## English
11+
12+
- **Reference context links are cleared instead of dangling after reference-line deletion (#1781)**`symbol_references.reference_line_id` now uses `ON DELETE SET NULL`, and existing indexes are migrated by nulling already-dangling line-context pointers before rebuilding the table constraint.
13+
14+
## 日本語
15+
16+
- **reference-line 削除後の参照コンテキストリンクが dangling ではなく NULL 化されるようになりました (#1781)**`symbol_references.reference_line_id``ON DELETE SET NULL` を使い、既存 index は既に dangling になっている line-context pointer を NULL 化してからテーブル制約を再構築します。
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
category: fixed
3+
issues:
4+
- 1785
5+
affected:
6+
- src/CodeIndex/Database/DbWriter.cs
7+
- tests/CodeIndex.Tests/DatabaseTests.cs
8+
---
9+
10+
## English
11+
12+
- **Stale file purges remove cross-file references to symbols defined only by the purged files (#1785)** — cleanup now drops phantom symbol edges when a deleted file was the only remaining definition for the referenced name.
13+
14+
## 日本語
15+
16+
- **stale file purge が purge 対象ファイルにしか定義が残っていないシンボルへの cross-file reference を削除するようになりました (#1785)** — 削除済みファイルが参照名の唯一の定義だった場合、phantom symbol edge を cleanup で取り除きます。
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
category: fixed
3+
issues:
4+
- 1826
5+
affected:
6+
- src/CodeIndex/Database/DbWriter.cs
7+
---
8+
9+
## English
10+
11+
- **Stale file purges delete file rows in chunked batches (#1826)** — purge paths now issue chunked `DELETE ... IN (...)` statements instead of one delete statement per stale file.
12+
13+
## 日本語
14+
15+
- **stale file purge が file row を chunked batch で削除するようになりました (#1826)** — purge 経路は stale file ごとに個別の DELETE を発行せず、chunked `DELETE ... IN (...)` を使います。

src/CodeIndex/Database/DbContext.cs

Lines changed: 91 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1354,7 +1354,7 @@ reference_kind TEXT CHECK (reference_kind IN (" + referenceKindCheck + @")),
13541354
line INTEGER,
13551355
column_number INTEGER,
13561356
context TEXT,
1357-
reference_line_id INTEGER REFERENCES reference_lines(id),
1357+
reference_line_id INTEGER REFERENCES reference_lines(id) ON DELETE SET NULL,
13581358
container_kind TEXT CHECK (container_kind IS NULL OR container_kind IN (" + symbolKindCheck + @")),
13591359
container_name TEXT
13601360
)");
@@ -1403,7 +1403,7 @@ value TEXT
14031403
EnsureColumn(
14041404
"symbol_references",
14051405
"reference_line_id",
1406-
rebuildsSymbolReferences ? "INTEGER" : "INTEGER REFERENCES reference_lines(id)");
1406+
rebuildsSymbolReferences ? "INTEGER" : "INTEGER REFERENCES reference_lines(id) ON DELETE SET NULL");
14071407
// #86: Unicode-aware folded name columns for `--exact` name matching across all
14081408
// `--exact` command variants. Populated by the writer via NameFold.Fold; NULL on
14091409
// legacy rows until a full reindex, in which case the reader falls back to the
@@ -1415,6 +1415,7 @@ value TEXT
14151415
EnsureColumn("symbol_references", "is_self_reference", "INTEGER NOT NULL DEFAULT 0");
14161416
EnsureColumn("symbol_references", "is_mutual_recursion", "INTEGER NOT NULL DEFAULT 0");
14171417
EnforceRequiredFileIdConstraints();
1418+
EnforceReferenceLineSetNullConstraint();
14181419
EnsureReferenceLinesContextKey();
14191420

14201421
// Indexes / インデックス
@@ -1513,6 +1514,7 @@ CREATE TRIGGER IF NOT EXISTS fts_chunks_au AFTER UPDATE ON chunks BEGIN
15131514

15141515
private void EnforceRequiredFileIdConstraints()
15151516
{
1517+
var symbolKindCheck = SymbolKindCatalog.ToSqlCheckInList(SymbolKindCatalog.SymbolKinds);
15161518
Execute("PRAGMA foreign_keys=OFF");
15171519
var legacyAlterTable = ExecuteScalar("PRAGMA legacy_alter_table");
15181520
Execute("PRAGMA legacy_alter_table=ON");
@@ -1534,11 +1536,11 @@ file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
15341536
"id, file_id, chunk_index, start_line, end_line, content");
15351537
RebuildTableWithRequiredFileId(
15361538
"symbols",
1537-
"""
1539+
$"""
15381540
CREATE TABLE symbols (
15391541
id INTEGER PRIMARY KEY AUTOINCREMENT,
15401542
file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
1541-
kind TEXT,
1543+
kind TEXT CHECK (kind IN ({symbolKindCheck})),
15421544
sub_kind TEXT,
15431545
name TEXT,
15441546
line INTEGER,
@@ -1548,7 +1550,7 @@ file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
15481550
body_start_line INTEGER,
15491551
body_end_line INTEGER,
15501552
signature TEXT,
1551-
container_kind TEXT,
1553+
container_kind TEXT CHECK (container_kind IS NULL OR container_kind IN ({symbolKindCheck})),
15521554
container_name TEXT,
15531555
container_qualified_name TEXT,
15541556
family_key TEXT,
@@ -1588,6 +1590,8 @@ private void RebuildReferenceLineTablesWithRequiredFileId()
15881590
return;
15891591
}
15901592

1593+
var symbolKindCheck = SymbolKindCatalog.ToSqlCheckInList(SymbolKindCatalog.SymbolKinds);
1594+
var referenceKindCheck = SymbolKindCatalog.ToSqlCheckInList(SymbolKindCatalog.ReferenceKinds);
15911595
const string referenceLinesCreateSql =
15921596
"""
15931597
CREATE TABLE reference_lines (
@@ -1599,18 +1603,18 @@ file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
15991603
)
16001604
""";
16011605
const string referenceLinesColumns = "id, file_id, line, context";
1602-
const string symbolReferencesCreateSql =
1603-
"""
1606+
var symbolReferencesCreateSql =
1607+
$"""
16041608
CREATE TABLE symbol_references (
16051609
id INTEGER PRIMARY KEY AUTOINCREMENT,
16061610
file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
16071611
symbol_name TEXT,
1608-
reference_kind TEXT,
1612+
reference_kind TEXT CHECK (reference_kind IN ({referenceKindCheck})),
16091613
line INTEGER,
16101614
column_number INTEGER,
16111615
context TEXT,
1612-
reference_line_id INTEGER REFERENCES reference_lines(id),
1613-
container_kind TEXT,
1616+
reference_line_id INTEGER REFERENCES reference_lines(id) ON DELETE SET NULL,
1617+
container_kind TEXT CHECK (container_kind IS NULL OR container_kind IN ({symbolKindCheck})),
16141618
container_name TEXT,
16151619
symbol_name_folded TEXT,
16161620
container_name_folded TEXT,
@@ -1636,11 +1640,81 @@ is_mutual_recursion INTEGER NOT NULL DEFAULT 0
16361640
Execute($"DROP TABLE {oldReferenceLines}");
16371641
}
16381642

1643+
private void EnforceReferenceLineSetNullConstraint()
1644+
{
1645+
if (SymbolReferencesReferenceLineDeletesSetNull())
1646+
return;
1647+
1648+
var symbolKindCheck = SymbolKindCatalog.ToSqlCheckInList(SymbolKindCatalog.SymbolKinds);
1649+
var referenceKindCheck = SymbolKindCatalog.ToSqlCheckInList(SymbolKindCatalog.ReferenceKinds);
1650+
var symbolReferencesCreateSql =
1651+
$"""
1652+
CREATE TABLE symbol_references (
1653+
id INTEGER PRIMARY KEY AUTOINCREMENT,
1654+
file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
1655+
symbol_name TEXT,
1656+
reference_kind TEXT CHECK (reference_kind IN ({referenceKindCheck})),
1657+
line INTEGER,
1658+
column_number INTEGER,
1659+
context TEXT,
1660+
reference_line_id INTEGER REFERENCES reference_lines(id) ON DELETE SET NULL,
1661+
container_kind TEXT CHECK (container_kind IS NULL OR container_kind IN ({symbolKindCheck})),
1662+
container_name TEXT,
1663+
symbol_name_folded TEXT,
1664+
container_name_folded TEXT,
1665+
is_self_reference INTEGER NOT NULL DEFAULT 0,
1666+
is_mutual_recursion INTEGER NOT NULL DEFAULT 0
1667+
)
1668+
""";
1669+
const string symbolReferencesColumns = "id, file_id, symbol_name, reference_kind, line, column_number, context, reference_line_id, container_kind, container_name, symbol_name_folded, container_name_folded, is_self_reference, is_mutual_recursion";
1670+
const string oldSymbolReferences = "_symbol_references_reference_line_delete";
1671+
1672+
Execute($"DROP TABLE IF EXISTS {oldSymbolReferences}");
1673+
Execute(@"
1674+
UPDATE symbol_references
1675+
SET reference_line_id = NULL
1676+
WHERE reference_line_id IS NOT NULL
1677+
AND NOT EXISTS (
1678+
SELECT 1
1679+
FROM reference_lines
1680+
WHERE reference_lines.id = symbol_references.reference_line_id
1681+
)");
1682+
Execute($"ALTER TABLE symbol_references RENAME TO {oldSymbolReferences}");
1683+
Execute(symbolReferencesCreateSql);
1684+
Execute($"INSERT INTO symbol_references ({symbolReferencesColumns}) SELECT {symbolReferencesColumns} FROM {oldSymbolReferences}");
1685+
Execute($"DROP TABLE {oldSymbolReferences}");
1686+
}
1687+
1688+
private bool SymbolReferencesReferenceLineDeletesSetNull()
1689+
{
1690+
using var cmd = _connection.CreateCommand();
1691+
if (_activeMigrationTransaction != null)
1692+
cmd.Transaction = _activeMigrationTransaction;
1693+
cmd.CommandText = "PRAGMA foreign_key_list('symbol_references')";
1694+
1695+
using var reader = cmd.ExecuteTrackedReader();
1696+
while (reader.TrackedRead())
1697+
{
1698+
var table = reader.GetString(2);
1699+
var from = reader.GetString(3);
1700+
var onDelete = reader.GetString(6);
1701+
if (string.Equals(table, "reference_lines", StringComparison.OrdinalIgnoreCase)
1702+
&& string.Equals(from, "reference_line_id", StringComparison.OrdinalIgnoreCase))
1703+
{
1704+
return string.Equals(onDelete, "SET NULL", StringComparison.OrdinalIgnoreCase);
1705+
}
1706+
}
1707+
1708+
return false;
1709+
}
1710+
16391711
private void EnsureReferenceLinesContextKey()
16401712
{
16411713
if (ReferenceLinesHasContextUniqueKey())
16421714
return;
16431715

1716+
var symbolKindCheck = SymbolKindCatalog.ToSqlCheckInList(SymbolKindCatalog.SymbolKinds);
1717+
var referenceKindCheck = SymbolKindCatalog.ToSqlCheckInList(SymbolKindCatalog.ReferenceKinds);
16441718
const string referenceLinesCreateSql =
16451719
"""
16461720
CREATE TABLE reference_lines (
@@ -1652,18 +1726,18 @@ file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
16521726
)
16531727
""";
16541728
const string referenceLinesColumns = "id, file_id, line, context";
1655-
const string symbolReferencesCreateSql =
1656-
"""
1729+
var symbolReferencesCreateSql =
1730+
$"""
16571731
CREATE TABLE symbol_references (
16581732
id INTEGER PRIMARY KEY AUTOINCREMENT,
16591733
file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
16601734
symbol_name TEXT,
1661-
reference_kind TEXT,
1735+
reference_kind TEXT CHECK (reference_kind IN ({referenceKindCheck})),
16621736
line INTEGER,
16631737
column_number INTEGER,
16641738
context TEXT,
1665-
reference_line_id INTEGER REFERENCES reference_lines(id),
1666-
container_kind TEXT,
1739+
reference_line_id INTEGER REFERENCES reference_lines(id) ON DELETE SET NULL,
1740+
container_kind TEXT CHECK (container_kind IS NULL OR container_kind IN ({symbolKindCheck})),
16671741
container_name TEXT,
16681742
symbol_name_folded TEXT,
16691743
container_name_folded TEXT,
@@ -1969,14 +2043,14 @@ file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
19692043
line INTEGER,
19702044
column_number INTEGER,
19712045
context TEXT,
1972-
reference_line_id INTEGER REFERENCES reference_lines(id),
2046+
reference_line_id INTEGER REFERENCES reference_lines(id) ON DELETE SET NULL,
19732047
container_kind TEXT,
19742048
container_name TEXT,
19752049
is_self_reference INTEGER NOT NULL DEFAULT 0,
19762050
is_mutual_recursion INTEGER NOT NULL DEFAULT 0
19772051
)"));
19782052
yield return ("EnsureColumn symbol_references.reference_line_id",
1979-
() => EnsureColumn("symbol_references", "reference_line_id", "INTEGER REFERENCES reference_lines(id)"));
2053+
() => EnsureColumn("symbol_references", "reference_line_id", "INTEGER REFERENCES reference_lines(id) ON DELETE SET NULL"));
19802054
yield return ("EnsureColumn symbol_references.is_self_reference",
19812055
() => EnsureColumn("symbol_references", "is_self_reference", "INTEGER NOT NULL DEFAULT 0"));
19822056
yield return ("EnsureColumn symbol_references.is_mutual_recursion",

src/CodeIndex/Database/DbWriter.cs

Lines changed: 68 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public class DbWriter
2828
private readonly SemaphoreSlim _transactionGate = new(1, 1);
2929
private readonly AsyncLocal<Guid?> _currentTransactionGateToken = new();
3030
private const int BatchSize = 500;
31+
private const int DeleteFilesBatchSize = 500;
3132
private const int MaxSqlVariables = 999;
3233
private const int SqliteConstraintErrorCode = 19;
3334
private int _rowSkipSavepointCounter;
@@ -617,22 +618,7 @@ public int PurgeStaleFilesSharingChecksum(string projectRoot, string retainedRel
617618
ReleaseCommand(cmd);
618619
}
619620

620-
if (staleIds.Count == 0)
621-
return 0;
622-
623-
using var txn = !IsInTransaction() ? BeginTransaction() : null;
624-
using var deleteCmd = _conn.CreateCommand();
625-
deleteCmd.CommandText = "DELETE FROM files WHERE id = @id";
626-
var pId = deleteCmd.Parameters.Add("@id", SqliteType.Integer);
627-
deleteCmd.Prepare();
628-
foreach (var id in staleIds)
629-
{
630-
pId.Value = id;
631-
deleteCmd.ExecuteNonQuery();
632-
}
633-
txn?.Commit();
634-
635-
return staleIds.Count;
621+
return DeleteStaleFileIds(staleIds);
636622
}
637623

638624
/// <summary>
@@ -677,18 +663,76 @@ private int DeleteStaleFileIds(IReadOnlyCollection<long> staleIds)
677663
return 0;
678664

679665
using var txn = !IsInTransaction() ? BeginTransaction() : null;
666+
DeleteFilesByIdBatched(staleIds);
667+
txn?.Commit();
668+
669+
return staleIds.Count;
670+
}
671+
672+
private void DeleteFilesByIdBatched(IEnumerable<long> fileIds, int batchSize = DeleteFilesBatchSize)
673+
{
674+
var batch = new List<long>(batchSize);
675+
foreach (var id in fileIds)
676+
{
677+
batch.Add(id);
678+
if (batch.Count == batchSize)
679+
{
680+
DeleteFileIdBatch(batch);
681+
batch.Clear();
682+
}
683+
}
684+
685+
if (batch.Count > 0)
686+
DeleteFileIdBatch(batch);
687+
}
688+
689+
private void DeleteFileIdBatch(IReadOnlyList<long> fileIds)
690+
{
691+
DeleteCrossFileReferencesToSymbolsDefinedOnlyByFiles(fileIds);
692+
680693
using var deleteCmd = _conn.CreateCommand();
681-
deleteCmd.CommandText = "DELETE FROM files WHERE id = @id";
682-
var pId = deleteCmd.Parameters.Add("@id", SqliteType.Integer);
683-
deleteCmd.Prepare();
684-
foreach (var id in staleIds)
694+
var parameters = new List<string>(fileIds.Count);
695+
for (var i = 0; i < fileIds.Count; i++)
685696
{
686-
pId.Value = id;
687-
deleteCmd.ExecuteNonQuery();
697+
var parameterName = $"@id{i}";
698+
parameters.Add(parameterName);
699+
deleteCmd.Parameters.Add(parameterName, SqliteType.Integer).Value = fileIds[i];
688700
}
689-
txn?.Commit();
690701

691-
return staleIds.Count;
702+
deleteCmd.CommandText = $"DELETE FROM files WHERE id IN ({string.Join(", ", parameters)})";
703+
deleteCmd.ExecuteNonQuery();
704+
}
705+
706+
private void DeleteCrossFileReferencesToSymbolsDefinedOnlyByFiles(IReadOnlyList<long> fileIds)
707+
{
708+
using var deleteCmd = _conn.CreateCommand();
709+
var parameters = new List<string>(fileIds.Count);
710+
for (var i = 0; i < fileIds.Count; i++)
711+
{
712+
var parameterName = $"@id{i}";
713+
parameters.Add(parameterName);
714+
deleteCmd.Parameters.Add(parameterName, SqliteType.Integer).Value = fileIds[i];
715+
}
716+
717+
var idList = string.Join(", ", parameters);
718+
deleteCmd.CommandText = $@"
719+
DELETE FROM symbol_references
720+
WHERE file_id NOT IN ({idList})
721+
AND symbol_name IS NOT NULL
722+
AND symbol_name <> ''
723+
AND EXISTS (
724+
SELECT 1
725+
FROM symbols deleted_symbols
726+
WHERE deleted_symbols.file_id IN ({idList})
727+
AND deleted_symbols.name = symbol_references.symbol_name
728+
)
729+
AND NOT EXISTS (
730+
SELECT 1
731+
FROM symbols retained_symbols
732+
WHERE retained_symbols.file_id NOT IN ({idList})
733+
AND retained_symbols.name = symbol_references.symbol_name
734+
)";
735+
deleteCmd.ExecuteNonQuery();
692736
}
693737

694738
private static string GetRelativeDirectory(string relativePath)

0 commit comments

Comments
 (0)