Skip to content

Fix edge cases in GRI levels caching - #59

Merged
mlin merged 1 commit into
mlin:mainfrom
Wid-L-Hacker:conrad/gri-level-cache-fix
Nov 10, 2026
Merged

mlin merged 1 commit into
mlin:mainfrom
Wid-L-Hacker:conrad/gri-level-cache-fix

Conversation

@mlin

@mlin mlin commented Nov 8, 2026

Copy link
Copy Markdown
Owner

The levels cache was keyed by the bare table name and validated only by counters that no rollback moves, so it could serve levels that stopped being true: levels computed over writes a rollback later undid, and temp/attached tables' levels filed under the main table's entry.

Unqualified names are now cached only when they resolve to the main database, under a schema-qualified key, compared with SQLite's own identifier comparison. Stores stay autocommit-only, and the cache is bypassed while a writable statement is executing, since trigger-consulted levels reflect its uncommitted snapshot. Eleven regression tests cover the failure modes.

@Wid-L-Hacker
Wid-L-Hacker force-pushed the conrad/gri-level-cache-fix branch from a12b05d to 7efd5ae Compare November 9, 2026 08:18
@mlin
mlin marked this pull request as ready for review November 9, 2026 08:32
The per-connection levels cache filed entries under the bare table name and
validated them only by the database data version and total change count.
Three gaps followed:

- Levels computed over uncommitted writes were memoized. No rollback (full
  or ROLLBACK TO a savepoint) moves either validity counter, so after a
  rollback the cache could serve levels for rows that no longer exist, or
  miss rows the rollback restored, silently skewing range queries fed by
  the cached bounds. Cache entries are now stored only in autocommit mode,
  and the cache is bypassed entirely while a writable statement is
  executing on the connection: levels consulted from inside such a
  statement (e.g. from a trigger of the DELETE in flight) reflect its
  uncommitted snapshot even in autocommit mode -- sqlite3_get_autocommit()
  stays 1 while a statement executes -- and a statement rollback is just
  as invisible to the counters.

- An unqualified table name was filed as the main database's even when
  SQLite's name resolution took it to a temp table or an attached
  database. An unqualified name is now cached only if it resolves to
  main -- it names a table or view in main and no temp table or view
  shadows it -- under a schema-qualified key, and the catalog names are
  compared with sqlite3_stricmp(), the comparison SQLite itself applies
  to identifiers, rather than a SQL collation a connection can override.

Adds regression tests: savepoint rollback, full rollback, bare savepoints,
cross-connection invalidation, temp-table shadowing in both directions and
after the cache is warmed, detach/reattach of a different file under one
schema name, a trigger that consults the levels and then aborts its own
statement (also via DELETE ... RETURNING and via a view over the
function), and a case-sensitive NOCASE collation override.
@Wid-L-Hacker
Wid-L-Hacker force-pushed the conrad/gri-level-cache-fix branch from 7efd5ae to ba490ef Compare November 9, 2026 23:53
@mlin mlin changed the title Fix GRI levels cache serving stale levels Fix edge cases in GRI levels caching Nov 10, 2026
@mlin
mlin merged commit b83a4c2 into mlin:main Nov 10, 2026
8 checks passed
@mlin
mlin deleted the conrad/gri-level-cache-fix branch November 10, 2026 05:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants