Skip to content

F-04 (High) + F-14: Append-only triggers and row-validity checks#4

Merged
adamburmister merged 12 commits into
mainfrom
fix/f04-append-only-storage
Jul 18, 2026
Merged

F-04 (High) + F-14: Append-only triggers and row-validity checks#4
adamburmister merged 12 commits into
mainfrom
fix/f04-append-only-storage

Conversation

@adamburmister

Copy link
Copy Markdown
Owner

Findings

F-04 (High): append-only was convention, not enforcement. The library itself contains no UPDATE/DELETE — but the deployment model hands the consumer the raw SQL handle (migrate(ctx.storage.sql) runs in their DO), so a stray UPDATE pluts_amounts SET amount = ... in application code rewrites the financial record silently: no trace, no error, and if it keeps the entry balanced, no trial-balance signal. GAAP-grade auditability requires corrections via reversing entries that leave a trail.

F-14 (Low): pluts_amounts.type and amount had no CHECK constraints, and pluts_entries.date no format check. A row inserted with type = 'Debit' via non-library SQL is silently excluded from every WHERE type = 'debit' aggregate — quietly corrupting sums instead of failing loudly.

Fix

  • Append-only triggers on pluts_entries, pluts_amounts, pluts_entry_keys: BEFORE UPDATE/BEFORE DELETERAISE(ABORT). The entries UPDATE trigger is column-scoped to the financial fields (id, description, date, posted_at) so future non-financial housekeeping columns (e.g. a journal-sequence backfill) remain writable.
  • Row-validity INSERT triggers: kind must be debit|credit, amount must be a non-negative integer (typeof(amount) = 'integer' also blocks float contamination at the DB level), date must be yyyy-mm-dd. Triggers apply to already-provisioned databases, where SQLite cannot retrofit CHECKs; equivalent CHECK constraints added to the CREATE TABLE definitions for fresh databases.
  • README porting notes: plain SQLite defaults PRAGMA foreign_keys = OFF (DO storage enforces FKs already) — ports must enable it; and SCHEMA_SQL must not be split naively on ; now that trigger bodies contain semicolons (migrate() is unaffected — it executes each statement whole).

Testing

New schema-hardening.spec.ts runs the real DDL against SQLite via Node's built-in node:sqlite (same engine family as DO storage): 9 tests written first and watched fail — UPDATE/DELETE blocked on all three tables, invalid kind / negative / float amounts rejected, malformed dates rejected, valid rows still accepted, schema still idempotent. Includes a minimal ambient type declaration for node:sqlite since the project compiles against @cloudflare/workers-types, not @types/node.

Full suite: 89 passed. tsc --noEmit and biome check clean.

🤖 Generated with Claude Code

The library issues no UPDATE or DELETE, but nothing prevented them: the
consumer's Durable Object holds the raw SqlStorage handle by design, so
one stray UPDATE from application code could rewrite closed periods
with no trace and no error. Row validity was also unconstrained on
pluts_amounts (a row with type 'Debit' or a float amount would be
silently excluded from every sum) and on pluts_entries.date.

- BEFORE UPDATE/DELETE triggers on pluts_entries, pluts_amounts, and
  pluts_entry_keys RAISE(ABORT) — corrections must be reversing
  entries. The entries UPDATE trigger is column-scoped to the financial
  fields so future housekeeping columns stay backfillable.
- BEFORE INSERT validation triggers enforce kind in (debit, credit),
  integer non-negative amounts, and yyyy-mm-dd dates on existing
  databases; matching CHECK constraints added to the CREATE TABLE
  definitions for fresh databases.
- README porting notes: enable PRAGMA foreign_keys on plain SQLite
  (defaults OFF; Durable Objects enforce it already), and don't split
  SCHEMA_SQL on ';' now that trigger bodies contain semicolons.
- Schema tests run the real DDL against SQLite via node:sqlite, with a
  minimal ambient type declaration (the project compiles against
  workers-types, not @types/node).

Audit findings F-04 (High) and F-14 (Low).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 09c6e10801

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/db/schema.ts
adamburmister and others added 3 commits July 18, 2026 12:33
With SQLite's default recursive_triggers = OFF, INSERT OR REPLACE
resolves a primary-key conflict by deleting the existing row WITHOUT
firing the DELETE trigger, then inserting the replacement — so raw
consumer SQL could silently rewrite a posted amount, entry, or key
mapping despite the append-only triggers (caught by Codex review).

BEFORE INSERT triggers fire before conflict resolution, so an
existence guard on the primary key closes the path on all three
tables. The entry-keys guard keeps "UNIQUE constraint failed" in its
message: the repository's concurrent-post recovery string-matches that
phrase when two posts race on the same idempotency key.

Reproduced first as three failing tests against node:sqlite; the tests
also pin that the original rows survive the attempt and that plain
duplicate-key inserts still surface as unique-constraint failures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 77f5f5a4b8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/db/schema.ts
adamburmister and others added 3 commits July 18, 2026 18:06
The table CHECK and validate-insert trigger only matched the digit
pattern, so raw SQL could store impossible dates like 2026-02-30 or
2026-13-01 — rows that lexicographic date-range filters then mis-bucket
or silently drop from period reports. Both guards now also require
date(x) round-tripping: SQLite normalizes 2026-02-30 to 2026-03-02
(breaking equality) and returns NULL for month 13, so only real
calendar dates pass.

Addresses PR #4 review (P2).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…g test

Main merged F-11 (repository test suite, PR #10) and F-16 (frozen entry
arrays, PR #11) independently; combined, the in-memory aliasing test's
push onto entry.debitAmounts now throws TypeError on the frozen array,
failing on main itself and on every PR's merge CI. The freeze strengthens
the invariant the test checks, so it now asserts the push throws and that
later reads still come back clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 71a4a619bd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/db/schema.ts
The no_replace triggers only checked the TEXT primary key, but all three
append-only tables are rowid tables: raw SQL could INSERT OR REPLACE
reusing an existing row's rowid with a different id, conflict on rowid
instead, and (with recursive_triggers OFF) silently rewrite the row past
the DELETE trigger. Each guard now also aborts when NEW.rowid matches an
existing row. Auto-assigned rowids surface as NEW.rowid = -1 inside a
BEFORE INSERT trigger, which matches no real row, so normal inserts are
unaffected — verified against node:sqlite.

Addresses PR #4 review (P2, rowid REPLACE).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 083f69fdcc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/db/schema.ts
Raw SQL could store an amount above Number.MAX_SAFE_INTEGER — fine as a
SQLite 64-bit integer, but unreadable across the SqlStorage JS-number
boundary: every later read or SUM would throw in fromStorageInt, taking
reports down. The fresh-table CHECK and the validate-insert trigger now
enforce amount <= 9007199254740991, matching the ceiling the write path
already guarantees via toStorageInt.

Addresses PR #4 review (P2, unsafe amount integers).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: eb2b96b8db

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/db/schema.ts
The rowid REPLACE guards read NEW.rowid = -1 as SQLite's auto-assigned
sentinel, but raw SQL can store a real row at rowid -1 — after which the
sentinel matches an existing row and every ordinary insert aborts as
append-only. AFTER INSERT triggers see the actually-assigned (always
positive when automatic) rowid, so each guarded table now aborts any
insert that lands on a negative rowid, making the poisoned state
unstorable without touching normal inserts.

Addresses PR #4 review (P2, rowid -1 poisoning).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3c741c09e5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/db/schema.ts
The entries UPDATE trigger is column-scoped (UPDATE OF ...) so future
migration columns stay backfillable — but UPDATE OF never matches a rowid
assignment, so raw SQL could run UPDATE pluts_entries SET rowid = -1,
bypassing every append-only trigger and poisoning the auto-rowid sentinel:
all subsequent ordinary posts aborted. A companion trigger now fires on
every UPDATE and aborts when the rowid changes. Amounts and entry_keys use
bare UPDATE triggers and were already covered (verified by probe).

Addresses PR #4 review (P2, rowid-only UPDATE).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2c83029cc1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/db/schema.ts
Databases provisioned before the append-only triggers can already hold
rows at rowid -1; the AFTER INSERT negative-rowid guard is not
retroactive, so such a row makes the no_replace sentinel match on every
ordinary insert — bricking posts — while the new UPDATE triggers block
any in-place repair. migrate() now reassigns negative rowids to fresh
positive ones before the DDL loop creates the triggers. Safe: nothing
references rowid (FKs use TEXT ids), and re-migrating a guarded database
finds no negatives so no guarded UPDATE ever runs.

Addresses PR #4 review (P2, legacy rowid -1).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@adamburmister

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Delightful!

Reviewed commit: e0f36e0d4f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@adamburmister
adamburmister merged commit 20bd75c into main Jul 18, 2026
1 check passed
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.

1 participant