Summary
LibsqlQuadStore.createTransaction().commit() calls commitPatchToLibsql() (which persists quads to the database) before searchIndexProjector.projectNovelQuads() (which updates the search index). If the search projector throws, quads are already committed to the database with no rollback, leaving the search index stale.
Location
src/libsql/quad-store/libsql-quad-store.ts:71-104
` s
commit: async (patch, context) => {
const { novelInsertions, novelQuadIds, labelTouchedSubjects } =
await commitPatchToLibsql(patch, this.options, context);
// ^ quads are now in the DB, no turning back
if (!skipSearchIndexProjection && this.options.searchIndexProjector) {
await this.options.searchIndexProjector.projectNovelQuads(
novelInsertions, novelQuadIds, labelTouchedSubjects,
);
// ^ if this throws, the quads from commitPatchToLibsql are orphaned
}
}
`
Evidence
Test "LibsqlQuadStore commit - quads persist even when searchProjector throws" in src/libsql/failure-point-tests.test.ts demonstrates this: a mock projector that throws still leaves the quad persisted in the database.
Impact
- Low-medium severity in practice. The quads table is the source of truth;
ebuildLibsqlSearchIndexFromQuads() exists to fix a stale index. A projector failure on one commit leaves a temporary search inconsistency that the next successful commit or explicit rebuild resolves.
- Mitigation exists: users can call
eindex() or the next successful commit will restore search consistency.
- No data loss — only a temporary blind spot in search results.
Suggested Fix
Option A (simplest): Move the search projector call before the quad flush, or defer the quad write until after the projector succeeds. Risk: reversed gap — quads are lost if projector succeeds but flush fails.
Option B (more robust): Wrap commitPatchToLibsql + projectNovelQuads in a single SQL transaction. This requires the transaction to span two concerns (SQL write + projector function call), which may be impractical if the projector is an external service.
Related
Summary
LibsqlQuadStore.createTransaction().commit() calls commitPatchToLibsql() (which persists quads to the database) before searchIndexProjector.projectNovelQuads() (which updates the search index). If the search projector throws, quads are already committed to the database with no rollback, leaving the search index stale.
Location
src/libsql/quad-store/libsql-quad-store.ts:71-104
` s
commit: async (patch, context) => {
const { novelInsertions, novelQuadIds, labelTouchedSubjects } =
await commitPatchToLibsql(patch, this.options, context);
// ^ quads are now in the DB, no turning back
if (!skipSearchIndexProjection && this.options.searchIndexProjector) {
await this.options.searchIndexProjector.projectNovelQuads(
novelInsertions, novelQuadIds, labelTouchedSubjects,
);
// ^ if this throws, the quads from commitPatchToLibsql are orphaned
}
}
`
Evidence
Test "LibsqlQuadStore commit - quads persist even when searchProjector throws" in src/libsql/failure-point-tests.test.ts demonstrates this: a mock projector that throws still leaves the quad persisted in the database.
Impact
ebuildLibsqlSearchIndexFromQuads() exists to fix a stale index. A projector failure on one commit leaves a temporary search inconsistency that the next successful commit or explicit rebuild resolves.
eindex() or the next successful commit will restore search consistency.
Suggested Fix
Option A (simplest): Move the search projector call before the quad flush, or defer the quad write until after the projector succeeds. Risk: reversed gap — quads are lost if projector succeeds but flush fails.
Option B (more robust): Wrap commitPatchToLibsql + projectNovelQuads in a single SQL transaction. This requires the transaction to span two concerns (SQL write + projector function call), which may be impractical if the projector is an external service.
Related