From 435cb37618f0d416212c8a682c1831830a52a033 Mon Sep 17 00:00:00 2001 From: Eric Minish Date: Thu, 15 Oct 2026 11:02:32 -0400 Subject: [PATCH 1/4] fix(sync): record only indexed files as synthesis inputs (#2565) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A lazy value that loads a workspace package by name, like `dynamic(() => import('icons').then((mod) => mod.EdgeFunctions))`, records the module it loads as a synthesis input. The import resolves a workspace package to its directory (`packages/icons`), which is no indexed file, and `synthesis_inputs.file_path` references `files(path)` — so the commit failed with "FOREIGN KEY constraint failed" and every sync after an edit to that file rolled back. Both writers (SynthesisStage.publish and replaceSynthesisInputs) now keep only paths that are in `files`. --- CHANGELOG.md | 1 + .../sync-synthesis-input-package.test.ts | 56 +++++++++++++++++++ src/db/queries.ts | 5 +- src/db/synthesis-stage.ts | 7 ++- 4 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 __tests__/sync-synthesis-input-package.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 4eb9e73572..d191cdc661 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Fixes +- `codegraph sync` no longer fails with "FOREIGN KEY constraint failed" in a React or Next.js project where a component lazily loads a workspace package by its name, like `dynamic(() => import('icons').then((mod) => mod.EdgeFunctions))`; before, every sync after an edit to that file failed and left the index unchanged (#2565). - In VB.NET, every member of a `Structure` is now indexed, including its fields, properties, methods, constructors and nested enums. Before, only the first member was, so the rest could not be found and their callers looked empty. - In VB.NET and C#, what a property's `Get` and `Set` code calls, creates and reads now belongs to that property, as do C#'s `get => …` accessors and `=> …` property bodies. Before, it was dropped, so a method used only from a property looked unused. - In VB.NET, a field or property initializer like `= Compute()` or `As New List(Of Order)` now links what it calls and creates, and so do a `Custom Event`'s `AddHandler`, `RemoveHandler` and `RaiseEvent` blocks. Re-index VB.NET and C# projects after upgrading. diff --git a/__tests__/sync-synthesis-input-package.test.ts b/__tests__/sync-synthesis-input-package.test.ts new file mode 100644 index 0000000000..416632a5b9 --- /dev/null +++ b/__tests__/sync-synthesis-input-package.test.ts @@ -0,0 +1,56 @@ +/** + * A lazy value that loads a workspace package by its name — `dynamic(() => + * import('icons').then((mod) => mod.EdgeFunctions))` — records the module it + * loads as a synthesis input, and the import resolves a workspace package to + * its directory, `packages/icons`, which is no indexed file. + * `synthesis_inputs.file_path` references `files(path)`, so recording it failed + * the commit with "FOREIGN KEY constraint failed" and rolled back every sync + * after an edit to that file (#2565). A full index commits inputs the same + * way, so it is pinned too. + */ +import { describe, it, expect, afterEach } from 'vitest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import CodeGraph from '../src/index'; + +const APP = `import dynamic from 'next/dynamic'; +const Chart = dynamic(() => import('icons').then((mod) => mod.EdgeFunctions)); +export function App() { return ; } +`; + +const FILES: Record = { + 'pnpm-workspace.yaml': "packages:\n - 'packages/*'\n", + 'package.json': JSON.stringify({ name: 'app', workspaces: ['packages/*'], dependencies: { react: '^18', next: '^14' } }), + 'packages/icons/package.json': JSON.stringify({ name: 'icons', main: './src/icons/index.ts' }), + 'packages/icons/src/icons/index.ts': 'export function EdgeFunctions() { return null; }\n', + 'src/App.tsx': APP, +}; + +let root: string | undefined; +let cg: CodeGraph | undefined; + +afterEach(() => { + cg?.close(); + cg = undefined; + if (root) fs.rmSync(root, { recursive: true, force: true }); + root = undefined; +}); + +describe('synthesis inputs from a lazily loaded workspace package (#2565)', () => { + it('a sync after an edit to the loading file succeeds', async () => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-2565-')); + for (const [file, text] of Object.entries(FILES)) { + fs.mkdirSync(path.dirname(path.join(root, file)), { recursive: true }); + fs.writeFileSync(path.join(root, file), text); + } + cg = await CodeGraph.init(root, { index: true }); + + fs.appendFileSync(path.join(root, 'src/App.tsx'), '// touched\n'); + await expect(cg.sync()).resolves.toBeDefined(); + + const indexed = new Set(cg.getFiles().map((f) => f.path)); + expect(indexed.has('src/App.tsx')).toBe(true); + expect(indexed.has('packages/icons')).toBe(false); + }); +}); diff --git a/src/db/queries.ts b/src/db/queries.ts index 6421eec360..f7eceb134f 100644 --- a/src/db/queries.ts +++ b/src/db/queries.ts @@ -2112,7 +2112,10 @@ export class QueryBuilder { replaceSynthesisInputs(files: string[]): void { this.db.transaction(() => { this.db.exec('DELETE FROM synthesis_inputs'); - const insert = this.db.prepare('INSERT INTO synthesis_inputs(file_path) VALUES (?)'); + // Indexed files only, as SynthesisStage.publish keeps them (#2565). + const insert = this.db.prepare( + 'INSERT INTO synthesis_inputs(file_path) SELECT path FROM files WHERE path = ?' + ); for (const file of files) insert.run(file); })(); } diff --git a/src/db/synthesis-stage.ts b/src/db/synthesis-stage.ts index 23c914d2a3..828b9e996b 100644 --- a/src/db/synthesis-stage.ts +++ b/src/db/synthesis-stage.ts @@ -70,8 +70,13 @@ export class SynthesisStage { insert.run(i, i + 2000); await yieldToLoop(); } + // An input is a path the passes read, and the resolver can hand them a + // path that is no indexed file — a workspace package's directory for + // `import('icons')` (#2565). Only indexed files are kept: the column + // references files(path), and one stray path rolled back the sync. this.db.exec(`DELETE FROM main.synthesis_inputs; - INSERT INTO main.synthesis_inputs SELECT * FROM temp.synthesis_inputs`); + INSERT INTO main.synthesis_inputs SELECT file_path FROM temp.synthesis_inputs + WHERE file_path IN (SELECT path FROM main.files)`); this.db.exec('COMMIT'); } catch (error) { this.db.exec('ROLLBACK'); From 160e5efee07e268756186b5ec74e8354ae36a1e2 Mon Sep 17 00:00:00 2001 From: Eric Minish Date: Thu, 15 Oct 2026 11:18:56 -0400 Subject: [PATCH 2/4] test(sync): pin the inputs a full index records too, which it left empty (#2565) --- CHANGELOG.md | 2 +- __tests__/sync-synthesis-input-package.test.ts | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d191cdc661..73715a4bc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Fixes -- `codegraph sync` no longer fails with "FOREIGN KEY constraint failed" in a React or Next.js project where a component lazily loads a workspace package by its name, like `dynamic(() => import('icons').then((mod) => mod.EdgeFunctions))`; before, every sync after an edit to that file failed and left the index unchanged (#2565). +- `codegraph sync` no longer fails with "FOREIGN KEY constraint failed" in a React or Next.js monorepo where a component lazily loads a workspace package by its name, like `dynamic(() => import('icons').then((mod) => mod.EdgeFunctions))`; before, every sync after an edit to that file failed and left the index unchanged (#2565). - In VB.NET, every member of a `Structure` is now indexed, including its fields, properties, methods, constructors and nested enums. Before, only the first member was, so the rest could not be found and their callers looked empty. - In VB.NET and C#, what a property's `Get` and `Set` code calls, creates and reads now belongs to that property, as do C#'s `get => …` accessors and `=> …` property bodies. Before, it was dropped, so a method used only from a property looked unused. - In VB.NET, a field or property initializer like `= Compute()` or `As New List(Of Order)` now links what it calls and creates, and so do a `Custom Event`'s `AddHandler`, `RemoveHandler` and `RaiseEvent` blocks. Re-index VB.NET and C# projects after upgrading. diff --git a/__tests__/sync-synthesis-input-package.test.ts b/__tests__/sync-synthesis-input-package.test.ts index 416632a5b9..a9fb90f6e5 100644 --- a/__tests__/sync-synthesis-input-package.test.ts +++ b/__tests__/sync-synthesis-input-package.test.ts @@ -5,8 +5,9 @@ * its directory, `packages/icons`, which is no indexed file. * `synthesis_inputs.file_path` references `files(path)`, so recording it failed * the commit with "FOREIGN KEY constraint failed" and rolled back every sync - * after an edit to that file (#2565). A full index commits inputs the same - * way, so it is pinned too. + * after an edit to that file (#2565). A full index writes its inputs through + * the same call but swallows the failure (synthesis is best-effort there), so + * it only left the inputs empty; both are pinned. */ import { describe, it, expect, afterEach } from 'vitest'; import * as fs from 'fs'; @@ -37,14 +38,21 @@ afterEach(() => { root = undefined; }); +/** The files recorded as synthesis inputs. */ +function inputs(graph: CodeGraph): string[] { + const db = (graph as any).db.db; + return db.prepare('SELECT file_path FROM synthesis_inputs ORDER BY file_path').all().map((r: any) => r.file_path); +} + describe('synthesis inputs from a lazily loaded workspace package (#2565)', () => { - it('a sync after an edit to the loading file succeeds', async () => { + it('records the loading file as an input, and a sync after an edit to it succeeds', async () => { root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-2565-')); for (const [file, text] of Object.entries(FILES)) { fs.mkdirSync(path.dirname(path.join(root, file)), { recursive: true }); fs.writeFileSync(path.join(root, file), text); } cg = await CodeGraph.init(root, { index: true }); + expect(inputs(cg)).toEqual(['src/App.tsx']); fs.appendFileSync(path.join(root, 'src/App.tsx'), '// touched\n'); await expect(cg.sync()).resolves.toBeDefined(); @@ -52,5 +60,6 @@ describe('synthesis inputs from a lazily loaded workspace package (#2565)', () = const indexed = new Set(cg.getFiles().map((f) => f.path)); expect(indexed.has('src/App.tsx')).toBe(true); expect(indexed.has('packages/icons')).toBe(false); + expect(inputs(cg)).toEqual(['src/App.tsx']); }); }); From 65a80781348ab983c5b08d80440c1b8eccf0cc7d Mon Sep 17 00:00:00 2001 From: Eric Minish Date: Thu, 15 Oct 2026 12:03:26 -0400 Subject: [PATCH 3/4] fix(sync): store synthesis inputs under the indexed path, in one place (#2565) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review round 1 on #2574: - An input spelled the way its import wrote it (`import('@/widgets/Chart/')` → `src/widgets/Chart//index.ts`, or a `./` segment) named a real indexed file but missed the exact-match filter, so the barrel was dropped and rewiring it never redrew the tag. Inputs are now normalized to the index's spelling before the lookup. - The publish-time filter was redundant: on the stage connection replaceSynthesisInputs already writes the temp table through the filter. It is removed, so the rule lives in one writer. - The test no longer pins the exact input set (a resolver that finds the package's entry file would rightly add it), and covers the issue's own trigger: an edit to another file with markup. - CHANGELOG: the failure hit every sync once a redraw was needed, not only edits to the loading file, and extraction was committed; add the upgrade note. --- CHANGELOG.md | 2 +- .../sync-synthesis-input-package.test.ts | 78 ++++++++++++++++++- src/db/queries.ts | 21 ++++- src/db/synthesis-stage.ts | 7 +- 4 files changed, 96 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73715a4bc6..31f6159d19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Fixes -- `codegraph sync` no longer fails with "FOREIGN KEY constraint failed" in a React or Next.js monorepo where a component lazily loads a workspace package by its name, like `dynamic(() => import('icons').then((mod) => mod.EdgeFunctions))`; before, every sync after an edit to that file failed and left the index unchanged (#2565). +- `codegraph sync` no longer fails with "FOREIGN KEY constraint failed" in a React or Next.js monorepo where a component lazily loads a workspace package by its name, like `dynamic(() => import('icons').then((mod) => mod.EdgeFunctions))`. Before, once a sync had to redraw links between components, that sync and every later one failed, so links between components stopped updating; run `codegraph sync` once after upgrading (#2565). - In VB.NET, every member of a `Structure` is now indexed, including its fields, properties, methods, constructors and nested enums. Before, only the first member was, so the rest could not be found and their callers looked empty. - In VB.NET and C#, what a property's `Get` and `Set` code calls, creates and reads now belongs to that property, as do C#'s `get => …` accessors and `=> …` property bodies. Before, it was dropped, so a method used only from a property looked unused. - In VB.NET, a field or property initializer like `= Compute()` or `As New List(Of Order)` now links what it calls and creates, and so do a `Custom Event`'s `AddHandler`, `RemoveHandler` and `RaiseEvent` blocks. Re-index VB.NET and C# projects after upgrading. diff --git a/__tests__/sync-synthesis-input-package.test.ts b/__tests__/sync-synthesis-input-package.test.ts index a9fb90f6e5..df969465b0 100644 --- a/__tests__/sync-synthesis-input-package.test.ts +++ b/__tests__/sync-synthesis-input-package.test.ts @@ -44,6 +44,38 @@ function inputs(graph: CodeGraph): string[] { return db.prepare('SELECT file_path FROM synthesis_inputs ORDER BY file_path').all().map((r: any) => r.file_path); } + +function write(dir: string, files: Record): void { + for (const [file, text] of Object.entries(files)) { + fs.mkdirSync(path.dirname(path.join(dir, file)), { recursive: true }); + fs.writeFileSync(path.join(dir, file), text); + } +} + +/** ` -> ::` for every synthesized JSX render edge. */ +function renders(graph: CodeGraph): string[] { + const out: string[] = []; + for (const file of graph.getFiles()) { + for (const node of graph.getNodesInFile(file.path)) { + for (const e of graph.getOutgoingEdges(node.id)) { + if (e.metadata?.synthesizedBy !== 'jsx-render') continue; + const child = graph.getNode(e.target); + out.push(`${node.name} -> ${child?.filePath}::${child?.name}`); + } + } + } + return out.sort(); +} + +let fresh: CodeGraph | undefined; +let freshRoot: string | undefined; +afterEach(() => { + fresh?.close(); + fresh = undefined; + if (freshRoot) fs.rmSync(freshRoot, { recursive: true, force: true }); + freshRoot = undefined; +}); + describe('synthesis inputs from a lazily loaded workspace package (#2565)', () => { it('records the loading file as an input, and a sync after an edit to it succeeds', async () => { root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-2565-')); @@ -52,7 +84,8 @@ describe('synthesis inputs from a lazily loaded workspace package (#2565)', () = fs.writeFileSync(path.join(root, file), text); } cg = await CodeGraph.init(root, { index: true }); - expect(inputs(cg)).toEqual(['src/App.tsx']); + expect(inputs(cg)).toContain('src/App.tsx'); + expect(inputs(cg)).not.toContain('packages/icons'); fs.appendFileSync(path.join(root, 'src/App.tsx'), '// touched\n'); await expect(cg.sync()).resolves.toBeDefined(); @@ -60,6 +93,47 @@ describe('synthesis inputs from a lazily loaded workspace package (#2565)', () = const indexed = new Set(cg.getFiles().map((f) => f.path)); expect(indexed.has('src/App.tsx')).toBe(true); expect(indexed.has('packages/icons')).toBe(false); - expect(inputs(cg)).toEqual(['src/App.tsx']); + expect(inputs(cg)).toContain('src/App.tsx'); }); + // The issue's own trigger: the edit is to ANOTHER file with markup, not the + // one that loads the package, and every such sync failed. + it('a sync after an edit to another file with markup succeeds', async () => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-2565-')); + write(root, { ...FILES, 'src/Other.tsx': 'export function Other() { return
; }\n' }); + cg = await CodeGraph.init(root, { index: true }); + + fs.appendFileSync(path.join(root, 'src/Other.tsx'), '// touched\n'); + await expect(cg.sync()).resolves.toBeDefined(); + }); + + // An input is matched to the indexed file by its path, so a module spelled + // with a trailing slash or a `.` segment (`import('@/widgets/Chart/')`) must + // still be recorded under the file's own path: dropping it left the barrel + // out of the inputs, and rewiring the barrel never redrew the tag. + it.each([['@/widgets/Chart/'], ['src/widgets/./Chart']])( + 'records a module spelled %s under its indexed path, so rewiring it redraws the tag', + async (spec) => { + const files: Record = { + 'package.json': JSON.stringify({ name: 'app', dependencies: { react: '^18', next: '^14' } }), + 'src/App.tsx': `import dynamic from 'next/dynamic';\nconst Chart = dynamic(() => import('${spec}'));\nexport function App() { return ; }\n`, + 'src/widgets/Chart/index.ts': "export { default } from './Impl';\n", + 'src/widgets/Chart/Impl.tsx': 'export default function ImplA() { return null; }\n', + 'src/widgets/Chart/Other.tsx': 'export default function Other() { return null; }\n', + }; + const rewired = { ...files, 'src/widgets/Chart/index.ts': "export { default } from './Other';\n" }; + root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-2565-')); + write(root, files); + cg = await CodeGraph.init(root, { index: true }); + expect(inputs(cg)).toContain('src/widgets/Chart/index.ts'); + + write(root, rewired); + await cg.sync(); + + freshRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-2565-fresh-')); + write(freshRoot, rewired); + fresh = await CodeGraph.init(freshRoot, { index: true }); + expect(renders(fresh)).toEqual(['App -> src/widgets/Chart/Other.tsx::Other']); + expect(renders(cg)).toEqual(renders(fresh)); + } + ); }); diff --git a/src/db/queries.ts b/src/db/queries.ts index f7eceb134f..9eabce7224 100644 --- a/src/db/queries.ts +++ b/src/db/queries.ts @@ -4,6 +4,7 @@ * Prepared statements for CRUD operations on the knowledge graph. */ +import * as path from 'path'; import { SqliteDatabase, SqliteStatement } from './sqlite-adapter'; import { Node, @@ -2112,11 +2113,14 @@ export class QueryBuilder { replaceSynthesisInputs(files: string[]): void { this.db.transaction(() => { this.db.exec('DELETE FROM synthesis_inputs'); - // Indexed files only, as SynthesisStage.publish keeps them (#2565). + // The column references files(path), so an input is stored under the + // indexed file's own spelling, and a path that is no indexed file is + // skipped rather than failing the write (#2565). On a SynthesisStage + // connection this writes the temp table that publish copies to main. const insert = this.db.prepare( - 'INSERT INTO synthesis_inputs(file_path) SELECT path FROM files WHERE path = ?' + 'INSERT OR IGNORE INTO synthesis_inputs(file_path) SELECT path FROM files WHERE path = ?' ); - for (const file of files) insert.run(file); + for (const file of files) insert.run(indexedPathOf(file)); })(); } @@ -4703,3 +4707,14 @@ function foldModuleRows( return { links: [...links.values()], pairs }; } + +/** + * A path as the index stores it: forward slashes, no `.` or empty segments, + * no trailing slash. A synthesis pass can be handed a module path spelled the + * way its import wrote it (`import('@/widgets/Chart/')` → + * `src/widgets/Chart//index.ts`), which names an indexed file under another + * spelling. + */ +function indexedPathOf(file: string): string { + return path.posix.normalize(file.replace(/\\/g, '/')).replace(/\/+$/, ''); +} diff --git a/src/db/synthesis-stage.ts b/src/db/synthesis-stage.ts index 828b9e996b..23c914d2a3 100644 --- a/src/db/synthesis-stage.ts +++ b/src/db/synthesis-stage.ts @@ -70,13 +70,8 @@ export class SynthesisStage { insert.run(i, i + 2000); await yieldToLoop(); } - // An input is a path the passes read, and the resolver can hand them a - // path that is no indexed file — a workspace package's directory for - // `import('icons')` (#2565). Only indexed files are kept: the column - // references files(path), and one stray path rolled back the sync. this.db.exec(`DELETE FROM main.synthesis_inputs; - INSERT INTO main.synthesis_inputs SELECT file_path FROM temp.synthesis_inputs - WHERE file_path IN (SELECT path FROM main.files)`); + INSERT INTO main.synthesis_inputs SELECT * FROM temp.synthesis_inputs`); this.db.exec('COMMIT'); } catch (error) { this.db.exec('ROLLBACK'); From f525cb5f0e4fadea3041cfb11214500905b9e2f8 Mon Sep 17 00:00:00 2001 From: Eric Minish Date: Thu, 15 Oct 2026 12:23:23 -0400 Subject: [PATCH 4/4] fix(sync): re-check synthesis inputs against files inside publish's write lock (#2565) Review round 2 on #2574: with the publish-time filter removed, the indexed-files check ran only when inputs were staged, outside the write lock; a file deleted before publish would fail the FK and roll back the whole refresh. The filter is back, naming its column rather than copying SELECT *. CHANGELOG: a full index on an affected version could leave the inputs empty, which only a re-index repairs. Test: one fixture writer, and the package directory is checked after the sync too. --- CHANGELOG.md | 2 +- __tests__/sync-synthesis-input-package.test.ts | 6 ++---- src/db/synthesis-stage.ts | 6 +++++- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 31f6159d19..61d70e39e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Fixes -- `codegraph sync` no longer fails with "FOREIGN KEY constraint failed" in a React or Next.js monorepo where a component lazily loads a workspace package by its name, like `dynamic(() => import('icons').then((mod) => mod.EdgeFunctions))`. Before, once a sync had to redraw links between components, that sync and every later one failed, so links between components stopped updating; run `codegraph sync` once after upgrading (#2565). +- `codegraph sync` no longer fails with "FOREIGN KEY constraint failed" in a React or Next.js monorepo where a component lazily loads a workspace package by its name, like `dynamic(() => import('icons').then((mod) => mod.EdgeFunctions))`. Before, once a sync had to redraw links between components, that sync and every later one failed, so links between components stopped updating. A full index of such a project could also miss which files to watch for those links; run `codegraph index` once after upgrading (#2565). - In VB.NET, every member of a `Structure` is now indexed, including its fields, properties, methods, constructors and nested enums. Before, only the first member was, so the rest could not be found and their callers looked empty. - In VB.NET and C#, what a property's `Get` and `Set` code calls, creates and reads now belongs to that property, as do C#'s `get => …` accessors and `=> …` property bodies. Before, it was dropped, so a method used only from a property looked unused. - In VB.NET, a field or property initializer like `= Compute()` or `As New List(Of Order)` now links what it calls and creates, and so do a `Custom Event`'s `AddHandler`, `RemoveHandler` and `RaiseEvent` blocks. Re-index VB.NET and C# projects after upgrading. diff --git a/__tests__/sync-synthesis-input-package.test.ts b/__tests__/sync-synthesis-input-package.test.ts index df969465b0..51833e62fd 100644 --- a/__tests__/sync-synthesis-input-package.test.ts +++ b/__tests__/sync-synthesis-input-package.test.ts @@ -79,10 +79,7 @@ afterEach(() => { describe('synthesis inputs from a lazily loaded workspace package (#2565)', () => { it('records the loading file as an input, and a sync after an edit to it succeeds', async () => { root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-2565-')); - for (const [file, text] of Object.entries(FILES)) { - fs.mkdirSync(path.dirname(path.join(root, file)), { recursive: true }); - fs.writeFileSync(path.join(root, file), text); - } + write(root, FILES); cg = await CodeGraph.init(root, { index: true }); expect(inputs(cg)).toContain('src/App.tsx'); expect(inputs(cg)).not.toContain('packages/icons'); @@ -94,6 +91,7 @@ describe('synthesis inputs from a lazily loaded workspace package (#2565)', () = expect(indexed.has('src/App.tsx')).toBe(true); expect(indexed.has('packages/icons')).toBe(false); expect(inputs(cg)).toContain('src/App.tsx'); + expect(inputs(cg)).not.toContain('packages/icons'); }); // The issue's own trigger: the edit is to ANOTHER file with markup, not the // one that loads the package, and every such sync failed. diff --git a/src/db/synthesis-stage.ts b/src/db/synthesis-stage.ts index 23c914d2a3..8ee4f16877 100644 --- a/src/db/synthesis-stage.ts +++ b/src/db/synthesis-stage.ts @@ -70,8 +70,12 @@ export class SynthesisStage { insert.run(i, i + 2000); await yieldToLoop(); } + // Re-checked against files inside the write lock: the inputs were staged + // before it, and a file deleted since would fail the FK and roll back + // every synthesized edge with it (#2565). this.db.exec(`DELETE FROM main.synthesis_inputs; - INSERT INTO main.synthesis_inputs SELECT * FROM temp.synthesis_inputs`); + INSERT INTO main.synthesis_inputs(file_path) SELECT file_path FROM temp.synthesis_inputs + WHERE file_path IN (SELECT path FROM main.files)`); this.db.exec('COMMIT'); } catch (error) { this.db.exec('ROLLBACK');