Skip to content

sqlite-shared cleanup: dedupe, lost-write fix, index split, comment pass - #445

Merged
cuibonobo merged 9 commits into
mainfrom
claude/intelligent-lovelace-5l0m4g
Oct 11, 2026
Merged

cuibonobo merged 9 commits into
mainfrom
claude/intelligent-lovelace-5l0m4g

Conversation

@cuibonobo

@cuibonobo cuibonobo commented Oct 11, 2026 •

Copy link
Copy Markdown
Member

Summary

A review pass over @haverstack/sqlite-shared, one commit per concern:

  • Remove redundant code (af5bc5c): trims the barrels to what consumers import; drops the SortField/SORT_FIELDS aliases of core's NativeSortField/NATIVE_SORT_FIELDS; replaces repeated inline code in record-logic.ts with shared helpers; removes a redundant existsSync before readLock().
  • Keep NOT inside a search phrase (d8ef10d): the bare-NOT strip ran over the whole search string, so "cats (not dogs" lost its not. Phrases are now matched first and kept whole.
  • Export the association authority partition (c427f43): isAuthorityAssociation from @haverstack/core (apps splitting a journal entry's edits between amendAssociations() and amendAccess() need it; the spec's own example called a predicate nothing exported), and partitionAssociations from @haverstack/core/adapter. sqlite-shared and MemoryAdapter drop their own copies.
  • Move the derived indexes into indexes.ts (2d9da43): the FTS5 writes plus a ContentIndex class that owns the per-type field cache and every content_index write. fts5.ts is now only the sanitizer.
  • Bring comments in line with AGENTS.md (cc64114): every block is at most five lines outside file-top comments, linking to the spec where the reasoning lives there.
  • Read and check inside the write transaction (dd7cba0): mutateRecord applied a content patch to a record read before an await, so two concurrent unguarded patches lost one of them, and content_index could be rebuilt under a stale typeId. mutateRecord, restoreVersion and commitMigration now read (version, type, content) inside their transaction and build from it.
  • One list of authority kinds (b7d358d): core exports AUTHORITY_ASSOCIATION_KINDS; isAuthorityAssociation reads it, and the SQL deletes the data half as "not an authority kind", replacing record-logic.ts's own kind lists.
  • One style for the indexes and the executor (e68f4d4): SearchIndex class beside ContentIndex; exec stored at construction; import and barrel tidy-ups.
  • Name both bundling adapters on sqlite-shared changesets (2237353): record-adapter-sqlite and record-adapter-do-sqlite each inline sqlite-shared, so both changelogs owe the entry. AGENTS.md, CONTRIBUTING.md and expand-changesets' error message say so.

Spec

  • docs/spec/journal.md § The inverse: the example now imports isAuthorityAssociation from @haverstack/core.
  • docs/spec/adapters.md: the SQLite-specific prose sat under § Conformance; it now has its own § SQLite-backed adapters heading, and events.md and schema.ts link there.
  • docs/spec/data-model.md § Capability-gated filters: the allow-list paragraph no longer narrates the strip-list it replaced.

Behavior changes, both within existing spec: a NOT inside a search phrase is preserved (the spec already says phrase text is left alone), and concurrent writes to one record no longer lose updates.

Verification

All six checks pass locally: format:check, check:refs, lint, test, build, typecheck.

  • Phrase fix: compared sanitizer output before and after on 300k random inputs; identical for every input without quotes.
  • Lost write: reproduced with two concurrent mutateRecord patches to different fields (one was lost), now pinned by concurrent unguarded patches both land.

Changesets: minor on @haverstack/core (new exports); patch on both @haverstack/record-adapter-sqlite and @haverstack/record-adapter-do-sqlite for the sqlite-shared changes. The comment-only commit carries none.

Notes for reviewers

  • The read before the transaction (readForMutation()) is gone rather than kept as an early check: the search-index removal it ran ahead of is rolled back with the transaction on any failure. Three tests in record-adapter-sqlite simulated a writer in the await that read created; they are replaced by tests of the same guarantees from the outside (racing guarded writes: one wins, the other conflicts; a non-bumping write racing a purge: StackNotFoundError).
  • Left as-is: query.ts's ~200-line recordConditions could split its JSON-path walk into its own module if it grows.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DsiVnSgkrjGjpzpqaSr5xg

- Trim the barrels to what consumers import; everything else was
  re-exported but used only inside the package.
- Drop the SortField/SORT_FIELDS aliases of core's NativeSortField and
  NATIVE_SORT_FIELDS.
- record-logic: one recordNotFound(), currentVersion(), readForMutation()
  and hydrate() instead of repeated inline copies.
- lock: acquireLock no longer checks existsSync before readLock(), which
  already treats a missing lock as absent.
- query: compute the native sort field once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DsiVnSgkrjGjpzpqaSr5xg
The bare-NOT strip ran over the whole search string, so a NOT at the
start of a phrase or after a "(" inside one was deleted from the text
the user searched for. Phrases are now matched first and kept whole.
Output outside phrases is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DsiVnSgkrjGjpzpqaSr5xg
isAuthorityAssociation joins the root entry point: an app undoing a
journal entry has to route each edit to amendAssociations() or
amendAccess(), and the spec's own example called a predicate no package
exported. partitionAssociations joins @haverstack/core/adapter for
engines that store both halves in one table.

sqlite-shared and MemoryAdapter now use these instead of their own
copies of the kind check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DsiVnSgkrjGjpzpqaSr5xg
record-logic.ts carried the content_index subsystem (its per-type field
cache, sync, and kind lookup) inline, and fts5.ts mixed the search-text
sanitizer with the FTS5 write strategy. Both indexes are rebuildable
from records and are maintained the same way, so they now live together
in indexes.ts: fts5Strategy unchanged, and a ContentIndex class that
owns the cache and every content_index write. fts5.ts is now only the
sanitizer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DsiVnSgkrjGjpzpqaSr5xg
Every comment block in the package is now at most five lines, outside
file-top module comments. Where the reasoning is spec material the
comment links to the spec instead; the sanitizer's regex-backtracking
rationale, which is not, moves into fts5.ts's module comment.

Also removes comments describing earlier implementations (a test's
before/after timings, purgeRecord's reference to a write() call),
fixes a doc comment attached to the wrong declaration in fts5.ts, and
corrects a version-conflict comment that miscounted its call sites.

Spec: the SQLite-specific prose in adapters.md sat under § Conformance;
it now has its own § SQLite-backed adapters heading, and the two
references that meant it point there. data-model.md's allow-list
paragraph no longer narrates the strip-list it replaced.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DsiVnSgkrjGjpzpqaSr5xg
@changeset-bot

changeset-bot Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 2237353

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 10 packages
Name Type
@haverstack/core Minor
@haverstack/record-adapter-sqlite Patch
@haverstack/record-adapter-do-sqlite Patch
@haverstack/adapter-api Patch
@haverstack/adapter-conformance Patch
@haverstack/adapter-local Patch
@haverstack/blob-adapter-disk Patch
@haverstack/blob-adapter-s3 Patch
@haverstack/commons Patch
@haverstack/wire-types Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

mutateRecord read the record, awaited, then wrote: a content patch was
applied to that earlier read, so two concurrent unguarded patches lost
one of them, and content_index could be rebuilt under a stale typeId.
mutateRecord, restoreVersion and commitMigration now take a narrow read
(version, type, content) inside their transaction and build from it.

The pre-transaction readForMutation() is gone: the search-index removal
it used to precede is rolled back with the transaction on any failure,
so there is no work for an early check to save. amendAssociations'
existence check reads the version instead of hydrating the record.

Tests that simulated a writer in the old await gap now pin the same
guarantees from the outside: racing guarded writes, one wins and the
other conflicts; racing unguarded patches, both land.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DsiVnSgkrjGjpzpqaSr5xg
replaceAssociationHalves deleted by its own DATA_KINDS/AUTHORITY_KINDS
lists, a third spelling of the partition beside core's predicate. Core
now exports AUTHORITY_ASSOCIATION_KINDS from @haverstack/core/adapter,
isAuthorityAssociation reads it, and the SQL deletes the data half as
"not an authority kind", so a new authority kind added to core is
covered in both places at once.

isAuthorityAssociation also drops its `a?.` guards: its parameter is a
typed Association, and every caller passes one already validated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DsiVnSgkrjGjpzpqaSr5xg
- fts5Strategy becomes a SearchIndex class holding its executor, like
  ContentIndex, instead of taking `exec` on every call.
- SharedSqlRecordLogic and SharedTokenLogic store `exec` at
  construction rather than reading it through a getter, so the logic and
  the indexes it builds are bound to the same handle.
- query.ts imports from @haverstack/core once.
- PRAGMA_JOURNAL_MODE_WAL moves from the record-only barrel to the full
  one: its only consumer is the token store.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DsiVnSgkrjGjpzpqaSr5xg
record-adapter-sqlite and record-adapter-do-sqlite each inline their
own copy of sqlite-shared, so a change to it ships in both. Naming only
record-adapter-sqlite left the DO adapter's changelog without the entry,
even when the DO adapter was released for an unrelated reason.

AGENTS.md, CONTRIBUTING.md and expand-changesets' error message now say
to name both, and this branch's sqlite-shared changesets do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DsiVnSgkrjGjpzpqaSr5xg
@cuibonobo cuibonobo changed the title sqlite-shared cleanup: dedupe, index split, phrase fix, comment pass sqlite-shared cleanup: dedupe, lost-write fix, index split, comment pass Oct 11, 2026
@cuibonobo
cuibonobo merged commit 0ae448b into main Oct 11, 2026
9 checks passed
@cuibonobo
cuibonobo deleted the claude/intelligent-lovelace-5l0m4g branch October 11, 2026 13:42
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