Skip to content

Extract shared SQLite layer; close deleteAttachment race; enforce FKs (#46 phase 1)#84

Open
cuibonobo wants to merge 1 commit into
mainfrom
claude/issue-45-design-review-1jgkcj
Open

Extract shared SQLite layer; close deleteAttachment race; enforce FKs (#46 phase 1)#84
cuibonobo wants to merge 1 commit into
mainfrom
claude/issue-45-design-review-1jgkcj

Conversation

@cuibonobo

Copy link
Copy Markdown
Member

Summary

First phase of #46 (SQLite record-adapter split), plus the two follow-up work items filed as comments on that issue. #46's own suggested order puts "§3 shared layer extraction (pure refactor, current tests keep passing)" first — this PR does that, plus the two comment items that fit naturally on top of it:

  • New internal package @haverstack/sqlite-shared (not a StackAdapter — not published/public API): schema DDL, WHERE/ORDER building, cursor codec, row mappers, and the FTS4 sanitizer, extracted verbatim from record-adapter-sqljs. This is what a future native SQLite adapter would consume too, so a cursor minted by one SQLite-backed adapter stays decodable by another instead of the two copies drifting. Cursor encoding moves from Node's Buffer to btoa/atob so the codec is portable to a browser adapter without change.
  • FK enforcement (comment item 1): record-adapter-sqljs now runs PRAGMA foreign_keys = ON and maps constraint violations to StackNotFoundError in associate(), so associating a nonexistent record fails loudly instead of silently creating an orphan row. This surfaced an sql.js quirk along the way — db.export() (called by persist() after every write) silently resets connection-level pragmas, which was quietly turning FK enforcement back off after the very first write. Fixed by reapplying the pragma inside persist(), with a regression test.
  • deleteAttachment()'s check-then-act race (comment item 3): StackRecordAdapter gains an optional deleteUnreferencedAttachmentRecords(fileId, metadataTypeId), implemented in record-adapter-sqljs as a single atomic SQL transaction. Stack.deleteAttachment() uses it when the adapter provides it, closing the race where a concurrent associate() could add a reference between the reference check and the metadata delete; adapters without it fall back to the previous non-atomic sequence. Byte deletion still runs only after the metadata step commits — a crash in between leaves orphaned bytes (harmless, GC-reclaimable later) rather than a dangling reference (harmful).
  • MemoryAdapter (core's test double) gains attachmentFileId filtering — needed to actually exercise deleteAttachment()'s reference check, which had no test coverage before this PR.

Left for a follow-up on #46

Native adapter creation (record-adapter-sqlite on node:sqlite, FTS5, WAL), sqljs's browser-only cleanup, the adapter-local swap, and moving the default token store out of the portable stack file (comment item 2) — these are larger, separable chunks of #46 and are better done once this shared layer is in place to build on.

Test plan

  • pnpm typecheck / pnpm lint / pnpm test — full monorepo, clean, 492 tests passing (no regressions)
  • New @haverstack/sqlite-shared unit tests: cursor codec roundtrip + malformed-input cases, FTS4 sanitizer
  • New record-adapter-sqljs tests: FK violation on associate()StackNotFoundError; FK enforcement survives multiple persist() calls (regression test for the sql.js pragma-reset quirk); deleteUnreferencedAttachmentRecords — referenced throws StackConflictError, unreferenced deletes and returns ids, dedup case deletes every metadata record sharing a fileId, rollback leaves records intact on conflict, version history is cleaned up too
  • New core tests: deleteAttachment() fallback path (conflict, success, not-found) and confirmation that the atomic adapter path is preferred when available

Refs #46.

https://claude.ai/code/session_01RRLiGUZC344E3pgNEGAe5W


Generated by Claude Code

…ce; enforce FKs

First phase of #46 (SQLite split), plus the two follow-up work items
from its comments:

- New internal package @haverstack/sqlite-shared: schema DDL, WHERE/
  ORDER building, cursor codec, row mappers, and the FTS4 sanitizer,
  extracted verbatim from record-adapter-sqljs so a future native
  SQLite adapter shares this SQL instead of drifting from a second
  copy. Cursor encoding moves from Node's Buffer to btoa/atob so the
  codec is portable to a browser adapter without change.
- record-adapter-sqljs enables PRAGMA foreign_keys = ON and maps FK
  constraint violations to StackNotFoundError in associate(), so
  associating a nonexistent record fails loudly instead of silently
  creating an orphan row. Also fixes an sql.js quirk where db.export()
  (called by persist() after every write) silently resets connection
  pragmas, which was quietly turning FK enforcement back off after the
  first write.
- StackRecordAdapter gains an optional
  deleteUnreferencedAttachmentRecords(fileId, metadataTypeId), and
  record-adapter-sqljs implements it as a single atomic SQL
  transaction. Stack.deleteAttachment() uses it when available,
  closing the check-then-act race where a concurrent associate()
  could add a reference between the reference check and the metadata
  delete; adapters without it fall back to the previous non-atomic
  sequence. Byte deletion still runs after the metadata step commits,
  so a crash in between leaves orphaned bytes (GC-reclaimable) rather
  than a dangling reference.
- MemoryAdapter gains attachmentFileId filtering, needed to actually
  exercise deleteAttachment()'s reference check in core's test suite
  (previously untested).

Native adapter creation, sqljs's browser-only cleanup, the
adapter-local swap, and the token-store-out-of-file default are left
for a follow-up pass on #46.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RRLiGUZC344E3pgNEGAe5W
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