fix(lbug): bump @ladybugdb/core to 0.21.2 to stop doubled rows after delete and re-copy

LadybugDB 0.21.0 and 0.21.1 return every row of a table that spans more
than one native vector twice once the incremental writeback has DETACH
DELETEd it and a COPY into an earlier table has run. NodeGroup::scan let a
morsel that ended before the group's first live row wrap an unsigned
subtraction and rescan to the end of the group (LadybugDB #1085, fixed in
0.21.2). No primary key error fires. The identity guard from #3442 stops
the run with "unexpected or duplicate ID", so a Swift edit that pulls every
importer into the write set failed on repos with more than 2048 structs.
Only unreleased main was affected: 1.6.12 ships 0.18.3.

Pin 0.21.2 in package.json, update the six @ladybugdb/core entries in the
lockfile, and move coreVersion in the vendored FTS manifest. The FTS
extension stays at 0.21.0 because no v0.21.1 or v0.21.2 artifact is
published; the pairing assertion and its unit tests pass.

Add a regression to the delete-nodes-for-files suite: 2049 structs with
CONTAINS edges are loaded, every file is deleted, the graph is loaded
again, and each id and edge must appear exactly once.

Verification (Linux, Node 22.18):
- lbug-delete-nodes-for-files: 11/11 on 0.21.2. With the 0.21.1 native
  packages swapped in, the new test fails with 4098 rows instead of 2049.
- Reporter recipe end to end on the built CLI: incremental analyze exits 0
  with no duplicate ids on a Swift-heavy fixture and on a 70-file Python
  fixture, with and without FTS. On 0.21.1 the Swift fixture exits 1.
- A 0.21.1-written database opens and reads under 0.21.2.
- prettier, eslint and typecheck:tests are clean on the changed files.
- The full lbug-db project had not finished when this was committed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Gergo Magyar 2026-10-14 05:00:00 +00:00
parent 9572e71127
commit 0cdc3dfe39
4 changed files with 105 additions and 26 deletions

View file

@ -23,7 +23,7 @@
"hasInstallScript": true,
"license": "PolyForm-Noncommercial-1.0.0",
"dependencies": {
"@ladybugdb/core": "0.21.1",
"@ladybugdb/core": "0.21.2",
"@modelcontextprotocol/sdk": "^1.0.0",
"@scarf/scarf": "^1.4.0",
"busboy": "^1.6.0",
@ -737,9 +737,9 @@
}
},
"node_modules/@ladybugdb/core": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@ladybugdb/core/-/core-0.21.1.tgz",
"integrity": "sha512-G0LWyn/dIqX9yHlyjpR12Rvou2o1dvCB9EkuzGa5ykCYbzur8EQhRUbi4fw3FlbckWvR6rK/tVbnFpBcRLwiHg==",
"version": "0.21.2",
"resolved": "https://registry.npmjs.org/@ladybugdb/core/-/core-0.21.2.tgz",
"integrity": "sha512-pnD1p5AaxEdmqJsLtF6bYuSmbe+pHzmaxDPNhROFOx+f7eE/AUPJFhW+rebMD6PQxK40B2NhqXbdsPCJ+FLuxg==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@ -748,17 +748,17 @@
"node-addon-api": "^6.0.0"
},
"optionalDependencies": {
"@ladybugdb/core-darwin-arm64": "0.21.1",
"@ladybugdb/core-darwin-x64": "0.21.1",
"@ladybugdb/core-linux-arm64": "0.21.1",
"@ladybugdb/core-linux-x64": "0.21.1",
"@ladybugdb/core-win32-x64": "0.21.1"
"@ladybugdb/core-darwin-arm64": "0.21.2",
"@ladybugdb/core-darwin-x64": "0.21.2",
"@ladybugdb/core-linux-arm64": "0.21.2",
"@ladybugdb/core-linux-x64": "0.21.2",
"@ladybugdb/core-win32-x64": "0.21.2"
}
},
"node_modules/@ladybugdb/core-darwin-arm64": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@ladybugdb/core-darwin-arm64/-/core-darwin-arm64-0.21.1.tgz",
"integrity": "sha512-TEFYNqBdbIojf1t29p3esgRhMDm56lD5eK91dwWFZJTVWAZoGXn+yLKs0NUQgj3lW2ZquuRTLZGglPhFEsDL1Q==",
"version": "0.21.2",
"resolved": "https://registry.npmjs.org/@ladybugdb/core-darwin-arm64/-/core-darwin-arm64-0.21.2.tgz",
"integrity": "sha512-xJrPBfdh+i51SVuPlj0qgve2HQloCqPomMh01RHKpRKl/pmrwj7dU+wnK+PDVb1OCAq5MAjt8SOiChvY89Wfiw==",
"cpu": [
"arm64"
],
@ -769,9 +769,9 @@
]
},
"node_modules/@ladybugdb/core-darwin-x64": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@ladybugdb/core-darwin-x64/-/core-darwin-x64-0.21.1.tgz",
"integrity": "sha512-w9p3oKrqeMOeWpX3xdCc/hQJHfrZtpB9We+Ir+0qyls68dSQoeXXR1GZYzzTElOtxr4OM5USwYbVyxADx+CMZg==",
"version": "0.21.2",
"resolved": "https://registry.npmjs.org/@ladybugdb/core-darwin-x64/-/core-darwin-x64-0.21.2.tgz",
"integrity": "sha512-zF+iONU8WOtzj9OLNr1CewQMcoJ+gwO3TJ0H4Lz+TA9D7JEdcTiHcdIQD4HQoJKEnKOwhL33MkWBdst8SfcV0g==",
"cpu": [
"x64"
],
@ -782,9 +782,9 @@
]
},
"node_modules/@ladybugdb/core-linux-arm64": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@ladybugdb/core-linux-arm64/-/core-linux-arm64-0.21.1.tgz",
"integrity": "sha512-TodWmzOHxmmKvXXo9buBXGeyolxf/NqBUbo/LcR/tuMViKhGH8TBzIXi8DfVVwmsJ+CQCU065TvXgv2JoHAlZg==",
"version": "0.21.2",
"resolved": "https://registry.npmjs.org/@ladybugdb/core-linux-arm64/-/core-linux-arm64-0.21.2.tgz",
"integrity": "sha512-t/Z2ESuwx6RaEruZzGFrPHX19PEz5vKRVCm5fmSr85DqLd8/W5KWcw4f/RUpQtHdU2gdYLKkBur8yEpn36wNZA==",
"cpu": [
"arm64"
],
@ -795,9 +795,9 @@
]
},
"node_modules/@ladybugdb/core-linux-x64": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@ladybugdb/core-linux-x64/-/core-linux-x64-0.21.1.tgz",
"integrity": "sha512-hCcLYv8ds0x4fkTaRkuc4f75KLdH9e0EV51ne6uBNdQSY7kyc0RgSoUG3utUGfFDMMkfgIJAeIxQY4ck9jQiXg==",
"version": "0.21.2",
"resolved": "https://registry.npmjs.org/@ladybugdb/core-linux-x64/-/core-linux-x64-0.21.2.tgz",
"integrity": "sha512-U4cAfkmPjixDURzyrkYG9NTzjEJpitaTOOATGWLTl78coc3oLpIil71wN+y2e0UuM4yd7450iLTyzL2QFzUazg==",
"cpu": [
"x64"
],
@ -808,9 +808,9 @@
]
},
"node_modules/@ladybugdb/core-win32-x64": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@ladybugdb/core-win32-x64/-/core-win32-x64-0.21.1.tgz",
"integrity": "sha512-nBS1XSRJfpexdlhkezmEiRtkUt2qEDp4zBjIhH+oxgRdD7UsRz1Jyit8foJuZAmUsoQGukN5LO+YeyDU2WZTyA==",
"version": "0.21.2",
"resolved": "https://registry.npmjs.org/@ladybugdb/core-win32-x64/-/core-win32-x64-0.21.2.tgz",
"integrity": "sha512-O4rQve2RcXPbEPv222N2OwM5p6TgWYeQ0vT+BlhBfo8cOxvFrXkI45gl6qbH5tKlBdGDLV2ijKyrb1hoAFx4Zg==",
"cpu": [
"x64"
],

View file

@ -67,7 +67,7 @@
"version": "node scripts/sync-plugin-manifests.mjs"
},
"dependencies": {
"@ladybugdb/core": "0.21.1",
"@ladybugdb/core": "0.21.2",
"@modelcontextprotocol/sdk": "^1.0.0",
"@scarf/scarf": "^1.4.0",
"busboy": "^1.6.0",

View file

@ -566,3 +566,82 @@ withTestLbugDB('embedding-row-dml-vector-gate', (handle) => {
}, 120_000);
});
});
/**
* Delete-all-then-recopy of a table that spans more than one native vector.
*
* @ladybugdb/core 0.21.0 and 0.21.1 returned every row of such a table twice
* once the incremental writeback had DETACH DELETEd it and a COPY into an
* earlier table had run: `NodeGroup::scan` let a morsel that ended before the
* group's first live row wrap an unsigned subtraction and rescan to the end of
* the group (LadybugDB #1085, fixed in 0.21.2). No primary-key error fires, so
* the only symptom was `Graph identity reconciliation failed … duplicate ID`
* on repos with more than 2048 symbols of one label (Swift structs in the
* report that found it), and 2×-inflated counts for anything that scanned.
*
* The statement order below is the one `runFullAnalysisInner` issues: delete the
* write set's rows, then COPY File before the symbol tables.
*/
withTestLbugDB('delete-recopy-multi-vector-table', (handle) => {
describe('deleteNodesForFiles then loadGraphToLbug on a multi-vector table', () => {
// One native vector holds 2048 rows; 2049 is the smallest table that spans two.
const STRUCT_COUNT = 2049;
const STRUCTS_PER_FILE = 64;
const fileOf = (i: number): string => `Sources/S${Math.floor(i / STRUCTS_PER_FILE)}.swift`;
const buildStructGraph = () => {
const nodes: TestNodeInput[] = [];
const rels: TestRelInput[] = [];
const files = new Set<string>();
for (let i = 0; i < STRUCT_COUNT; i++) {
const fp = fileOf(i);
if (!files.has(fp)) {
files.add(fp);
nodes.push({ id: `File:${fp}`, label: 'File', name: path.basename(fp), filePath: fp });
}
const id = `Struct:${fp}:S${i}`;
nodes.push({
id,
label: 'Struct',
name: `S${i}`,
filePath: fp,
startLine: 1,
endLine: 3,
});
rels.push({ sourceId: `File:${fp}`, targetId: id, type: 'CONTAINS' });
}
return { graph: buildTestGraph(nodes, rels), filePaths: [...files] };
};
it('returns each struct and its edge exactly once after the rows were deleted and re-copied', async () => {
const { loadGraphToLbug, deleteNodesForFiles, executeQuery } =
await import('../../src/core/lbug/lbug-adapter.js');
const storagePath = path.dirname(handle.dbPath);
const structIds = async (): Promise<string[]> =>
((await executeQuery('MATCH (n:Struct) RETURN n.id AS id')) as Array<{ id: string }>).map(
(r) => String(r.id),
);
const containsEdges = async (): Promise<number> => {
const rows = (await executeQuery(
`MATCH (:File)-[r:CodeRelation]->(:Struct) WHERE r.type = 'CONTAINS' RETURN count(r) AS c`,
)) as Array<{ c: number | bigint }>;
return Number(rows[0]?.c ?? 0);
};
const { graph, filePaths } = buildStructGraph();
await loadGraphToLbug(graph, '/tmp/repo', storagePath);
expect(new Set(await structIds()).size).toBe(STRUCT_COUNT);
// Every file is in the write set, as when a Swift edit pulls in all importers.
await deleteNodesForFiles(filePaths);
expect(await structIds()).toEqual([]);
await loadGraphToLbug(graph, '/tmp/repo', storagePath);
const ids = await structIds();
expect(ids).toHaveLength(STRUCT_COUNT);
expect(new Set(ids).size).toBe(STRUCT_COUNT);
expect(await containsEdges()).toBe(STRUCT_COUNT);
}, 120_000);
});
});

View file

@ -1,5 +1,5 @@
{
"coreVersion": "0.21.1",
"coreVersion": "0.21.2",
"extensionVersion": "0.21.0",
"filename": "libfts.lbug_extension",
"officialRepo": "https://extension.ladybugdb.com/",