-
Notifications
You must be signed in to change notification settings - Fork 10
fix(native): enable bulkInsertNodes native path — null-visibility serialisation #737
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
0fcafe9
ec903cc
32358c0
4058f91
38014fb
40d4df3
3f7f8ac
7c8e8c6
2009ecb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,10 +11,12 @@ | |
| import path from 'node:path'; | ||
| import { performance } from 'node:perf_hooks'; | ||
| import { bulkNodeIdsByFile } from '../../../../db/index.js'; | ||
| import { loadNative } from '../../../../infrastructure/native.js'; | ||
| import type { | ||
| BetterSqlite3Database, | ||
| ExtractorOutput, | ||
| MetadataUpdate, | ||
| NativeDatabase, | ||
| SqliteStatement, | ||
| } from '../../../../types.js'; | ||
| import type { PipelineContext } from '../context.js'; | ||
|
|
@@ -39,15 +41,28 @@ interface PrecomputedFileData { | |
| // ── Native fast-path ───────────────────────────────────────────────── | ||
|
|
||
| function tryNativeInsert(ctx: PipelineContext): boolean { | ||
| // Disabled: bulkInsertNodes corrupts the DB when both the JS (better-sqlite3) | ||
| // and Rust (rusqlite) connections are open to the same WAL-mode file. | ||
| // The native path was never operational before — it always crashed on null | ||
| // visibility serialisation. See #696 for the dual-connection fix. | ||
| if (ctx.db) return false; | ||
|
|
||
| // Use NativeDatabase persistent connection (Phase 6.15+). | ||
| // Standalone napi functions were removed in 6.17 — falls through to JS if nativeDb unavailable. | ||
| if (!ctx.nativeDb?.bulkInsertNodes) return false; | ||
| // Open a temporary native connection for the insert. The pipeline closes | ||
| // ctx.nativeDb before pipeline stages to prevent dual-connection WAL | ||
| // corruption (#696). We open our own connection and coordinate with | ||
| // suspendJsDb / resumeJsDb WAL checkpoints (#709, same pattern as the | ||
| // analysis stages in features/). | ||
| const native = loadNative(); | ||
| if (!native?.NativeDatabase) return false; | ||
|
|
||
| let nativeDb: NativeDatabase | undefined; | ||
| try { | ||
| nativeDb = native.NativeDatabase.openReadWrite(ctx.dbPath); | ||
| } catch { | ||
| return false; | ||
| } | ||
| if (!nativeDb?.bulkInsertNodes) { | ||
| try { | ||
| nativeDb?.close(); | ||
| } catch { | ||
| /* ignore */ | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| const { allSymbols, filesToParse, metadataUpdates, rootDir, removed } = ctx; | ||
|
|
||
|
|
@@ -144,7 +159,24 @@ function tryNativeInsert(ctx: PipelineContext): boolean { | |
| fileHashes.push({ file: item.relPath, hash: item.hash, mtime, size }); | ||
| } | ||
|
|
||
| return ctx.nativeDb!.bulkInsertNodes(batches, fileHashes, removed); | ||
| // WAL checkpoint dance: flush JS WAL so the native connection starts clean, | ||
| // do the write, then flush the native WAL so better-sqlite3 sees the changes. | ||
| try { | ||
| ctx.db.pragma('wal_checkpoint(TRUNCATE)'); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The same unconditional call exists in const cpResult = ctx.db.pragma('wal_checkpoint(TRUNCATE)') as { busy: number }[] | undefined;
if (cpResult?.[0]?.busy) {
// WAL not fully flushed — fall back to JS to avoid mixed-writer WAL state
return false;
}
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in ec903cc. The pre-write checkpoint result is now inspected: const cpResult = ctx.db.pragma('wal_checkpoint(TRUNCATE)') as { busy: number }[] | undefined;
if (cpResult?.[0]?.busy) {
// WAL not fully flushed — fall back to JS to avoid mixed-writer WAL state
return false;
}When |
||
| const ok = nativeDb!.bulkInsertNodes(batches, fileHashes, removed); | ||
| try { | ||
| nativeDb!.exec('PRAGMA wal_checkpoint(TRUNCATE)'); | ||
| } catch { | ||
| /* ignore — checkpoint failure is non-fatal */ | ||
| } | ||
| return ok; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The post-write native checkpoint ( A more defensive approach would be to issue the post-write checkpoint through // In finally, after nativeDb.close():
try { ctx.db.pragma('wal_checkpoint(TRUNCATE)'); } catch { /* ignore */ }This would guarantee that the native WAL frames are visible to better-sqlite3 before the next stage reads from
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in ec903cc. The post-write checkpoint is now issued through } finally {
try {
nativeDb!.close();
} catch {
/* ignore close errors */
}
// Post-write checkpoint through ctx.db *after* nativeDb is closed, so
// rusqlite holds no locks and better-sqlite3 can acquire the exclusive
// WAL lock needed for TRUNCATE (#709).
try {
ctx.db.pragma('wal_checkpoint(TRUNCATE)');
} catch {
/* ignore — checkpoint failure is non-fatal */
}
}This ensures rusqlite holds no locks during the TRUNCATE, so better-sqlite3 can acquire the exclusive WAL lock and fully compact the WAL frames into the main DB file.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Follow-up fix in 32358c0: After further analysis of the CI SQLITE_CORRUPT failures on Linux/macOS, I identified the root cause more precisely. The post-write checkpoint must go through The corrected sequence:
This matches the established suspend/resume pattern and should resolve the SQLITE_CORRUPT errors.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Follow-up fix in 38014fb: The earlier fix (32358c0) correctly checkpointed through rusqlite before close, but missed a crucial detail: better-sqlite3 keeps a cached SHM/WAL mapping that doesn't automatically refresh when another SQLite library modifies the WAL. The final WAL coordination sequence is now:
This resolves the CI failures on Linux/macOS. |
||
| } finally { | ||
| try { | ||
| nativeDb!.close(); | ||
| } catch { | ||
| /* ignore close errors */ | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // ── JS fallback: Phase 1 ──────────────────────────────────────────── | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment says the function "coordinate[s] with
suspendJsDb/resumeJsDbWAL checkpoints… same pattern as the analysis stages." But those callbacks are not called here — the equivalent logic is inlined directly. Readers who then look atdataflow.tsorast.tswill findsuspendJsDb?.()/resumeJsDb?.()called onengineOpts, which is a different mechanism.The inline approach is fine, but the comment overstates the similarity and may mislead future authors into thinking they can safely call
ctx.engineOpts.suspendJsDbfrom here (which would not make sense since that callback usesctx.nativeDb, already closed at this point).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in ec903cc. Updated the comment to accurately describe what happens — the logic is inlined, not delegated to suspendJsDb/resumeJsDb:
This matches the suggested wording and avoids misleading future contributors into thinking suspendJsDb/resumeJsDb are relevant here.