Add the unique keys the models have been promising - #744
Merged
Merged
Conversation
Five constraints the models declare and production has never had. Because the test database is built by db.create_all() from the models, each has been enforced in tests and unenforced in production -- the reverse of #741, and the reason a race the tests refuse can still duplicate rows in prod. Production data was checked first: all five have zero duplicate groups today, so the ALTERs apply with no dedupe step. level_adapted_article_summary_context is the clearest case. Its sibling level_adapted_article_title_context has uq_latc_bookmark_title in production, and this one was almost certainly lost when article_level_summary was renamed and the column became level_adapted_article_text_id. Its find_or_create already inserts inside a SAVEPOINT expecting the constraint to be there. The other four needed code first. Each find_or_create caught only NoResultFound: except sqlalchemy.orm.exc.NoResultFound: new = cls(user, topic) session.add(new) so adding the key would have converted a silent duplicate into a 500 on a user-facing path. They now use the same SAVEPOINT-and-requery pattern as UserLanguage.find_or_create (#733), which undoes just the losing insert rather than the caller's whole transaction. The bare floating UniqueConstraint in each class body -- which does attach, but unnamed -- moves into __table_args__ under the name the migration uses, so model and production agree. Two more from the same sweep are deliberately NOT here, because neither is a mechanical add: phrase (content, language_id) -- 55502 duplicate groups in production. The model has declared this unique since the index was dropped in 25-05-21--rename_user_word_to_phrase.sql. Enforcing it needs a data cleanup that rewrites bookmark and meaning references, not an ALTER. article_topic_user_feedback (article_id, user_id, topic_id) -- zero duplicates, but find_or_create keys on (article, user, topic, FEEDBACK), narrower than the constraint. Adding it would turn "user changes their mind about a topic" into an IntegrityError. Needs a decision about whether a changed feedback is a new row or an update. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
ArchLens - No architecturally relevant changes to the existing views |
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.
Follow-up to #741, and the last of the backwards sweep. Five constraints the models declare that production has never had — so each has been enforced in tests and unenforced in production, the exact reverse of #741. A race the test suite refuses can still duplicate rows in prod.
Production data was checked before writing any DDL: all five have zero duplicate groups today, so the
ALTERs apply with no dedupe step.level_adapted_article_summary_context— the clearest caseIts sibling
level_adapted_article_title_contexthasuq_latc_bookmark_titlein production. This one doesn't, and was almost certainly lost whenarticle_level_summarywas renamed and the column becamelevel_adapted_article_text_id. Itsfind_or_createalready inserts inside aSAVEPOINTexpecting the constraint to be there — so today that recovery path is dead code in production.Migration only; no code change needed.
The other four needed code first
search_filter,search_subscription,topic_filter,topic_subscriptioneach had afind_or_createcatching onlyNoResultFound:Adding the unique key to that would have converted a silent duplicate into a 500 on a user-facing path (subscribing to a topic, saving a search). So they now use the same
SAVEPOINT-and-requery pattern asUserLanguage.find_or_createfrom #733 — rolling back just the losing insert rather than the caller's whole transaction.The bare floating
UniqueConstraint(user_id, topic_id)in each class body — which does attach, but unnamed — moves into__table_args__under the name the migration uses, so model and production agree.Apply the migration together with this code, not ahead of it.
Two I deliberately did not fix
Both were in scope and both turned out to need a decision rather than an
ALTER:phrase (content, language_id)— 55502 duplicate groups. The model has declared this unique since the index was dropped in25-05-21--rename_user_word_to_phrase.sql. Enforcing it means a data cleanup that rewritesbookmarkandmeaningreferences, not a migration. I left the model declaration alone rather than dropping it: it encodes the correct intent, and removing it would quietly bless the duplicates.article_topic_user_feedback (article_id, user_id, topic_id)— zero duplicates, but a semantic conflict. Itsfind_or_createkeys on(article, user, topic, feedback)— narrower than the constraint. Adding the key would turn "user changes their mind about a topic" into anIntegrityError. Whether a changed feedback is a new row or an update to the existing one is a product decision, not one to guess at.Testing
498 passed, 12 subtests — unchanged from this branch point.
🤖 Generated with Claude Code