feat(schema): CHECK constraints on the posts/comments flags - #31
Merged
Conversation
Migration [114] constrained the leaf tables and skipped these two as the awkward case. [116] finishes it: the boolean flags on posts (locked, edited, is_self, over_18, deleted, stickied, comments_locked, approved, is_question) and on comments (edited, deleted, is_submitter, stickied, approved). The interesting part is the rebuild, not the constraints. `posts` is referenced by six tables and by itself, carries the FTS5 sync triggers, and both tables feed the v_daily_activity view -- so the recipe used for the leaf tables is wrong here in three separate ways: - With foreign keys enabled, ALTER TABLE ... RENAME rewrites the REFERENCES clauses of child tables to follow the rename. Renaming `posts` out of the way would leave `comments` pointing at `posts_old`. rebuild_referenced_table uses SQLite's documented order instead -- build under a temporary name, copy, drop the original, rename into place -- so children keep naming the table they always named. A spec asserts PRAGMA foreign_key_list(comments) still says posts, and that it says neither posts_old nor posts_rebuild. - Foreign keys must be off for the drop, via a PRAGMA that is only effective outside a transaction. Lapis runs migrations without one unless asked, so this works, but it is a dependency on that default and the docs now say so. PRAGMA foreign_key_check runs afterwards and the migration asserts on orphans. - SQLite re-parses the whole schema during the rename, so a view still pointing at the dropped table fails that parse. The three FTS triggers and v_daily_activity are dropped up front and recreated after. The FTS index itself is never touched, so its contents still match the copied rows. Specs cover both halves of that: an existing post is still findable by search after a rebuild, and a post created afterwards is indexed by the reattached AFTER INSERT trigger. Rows are copied as-is rather than normalized. Every writer sets these from a Lua boolean, so a violation would mean data this schema should never have held -- same policy as [114]'s text enums. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0128hUpuk1spKzk4UHburdki
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Second of the two follow-ups. #26 constrained the leaf tables and deliberately
skipped these two; this is the awkward case it deferred.
What's constrained
Boolean flags that were only ever enforced in Lua:
locked,edited,is_self,over_18,deleted,stickied,comments_locked,approved,is_questionedited,deleted,is_submitter,stickied,approvedThe rebuild is the interesting part
postsis referenced by six tables and itself, carries the FTS5 sync triggers,and both tables feed the
v_daily_activityview. The recipe used for the leaftables is wrong here in three separate ways:
1. A referenced table must not be renamed aside. With foreign keys enabled,
ALTER TABLE ... RENAMErewrites theREFERENCESclauses of child tables tofollow the rename — so moving
postsout of the way would have leftcommentspointing at
posts_old. That's a silent corruption: the schema still looksplausible.
rebuild_referenced_tableuses SQLite's documented order — build under atemporary name, copy, drop the original, rename into place — so children keep
naming the table they always named. A spec asserts
PRAGMA foreign_key_list(comments)still saysposts, and says neitherposts_oldnorposts_rebuild.2. Foreign keys have to be off for the drop, via a
PRAGMAthat only takeseffect outside a transaction. Lapis runs migrations without one unless asked
(
transaction = "global" | "individual"), so this works — but it's a dependencyon that default, and the docs now say so.
PRAGMA foreign_key_checkrunsafterwards and the migration asserts on any orphan.
3. Triggers and views must be dropped and recreated. SQLite re-parses the
whole schema during the rename, and a view still pointing at the dropped table
fails that parse. The three FTS triggers and
v_daily_activitycome down firstand go back after.
The FTS index itself is never touched, so its contents still match the copied
rows. Both halves of that are tested: an existing post is still findable by
search after a rebuild, and a post created afterwards is indexed by the
reattached
AFTER INSERTtrigger — the second would fail silently if I'dforgotten to put the triggers back.
Data handling
Rows are copied as-is, not normalized. Every writer sets these from a Lua
boolean, so a violation would mean data this schema should never have held —
same policy as #26's text enums, where failing loudly beats quietly rewriting
someone's content.
Verification
posts_constraints_spec(8 cases): both tables reject a non-boolean flag, everyvalue the app writes is still accepted, child foreign keys still resolve, a
re-run of
[116]against real rows preserves posts/comments/votes and the view,FTS survives and stays live, all 15 indexes are back, and
PRAGMA foreign_key_checkis clean. Full suite 421 passing, coverage 86.2%,stylua and luacheck clean.
🤖 Generated with Claude Code
https://claude.ai/code/session_0128hUpuk1spKzk4UHburdki