diff --git a/zeeguu/core/model/__init__.py b/zeeguu/core/model/__init__.py index e3868ba8..f23ef697 100644 --- a/zeeguu/core/model/__init__.py +++ b/zeeguu/core/model/__init__.py @@ -44,6 +44,7 @@ from .user_language import UserLanguage from .user_article import UserArticle +from .starred_article import StarredArticle from .article_difficulty_feedback import ArticleDifficultyFeedback from .feed import Feed diff --git a/zeeguu/core/model/context_type.py b/zeeguu/core/model/context_type.py index 3519fafe..530f6967 100644 --- a/zeeguu/core/model/context_type.py +++ b/zeeguu/core/model/context_type.py @@ -37,7 +37,11 @@ class ContextType(db.Model): EXAMPLE_SENTENCE, ] - __table_args__ = {"mysql_collate": "utf8_bin"} + # Named for the index production already has, so nobody generates a second one. + __table_args__ = ( + db.UniqueConstraint("type", name="unique_context_type"), + {"mysql_collate": "utf8_bin"}, + ) id = db.Column(db.Integer, primary_key=True) type = db.Column(db.String(45)) diff --git a/zeeguu/core/model/level_adapted_article_text.py b/zeeguu/core/model/level_adapted_article_text.py index 386856a6..fefa4fac 100644 --- a/zeeguu/core/model/level_adapted_article_text.py +++ b/zeeguu/core/model/level_adapted_article_text.py @@ -38,7 +38,13 @@ class LevelAdaptedArticleText(db.Model): added — renaming it would have churned the two context joins and every FK.) """ - __table_args__ = {"mysql_collate": "utf8_bin"} + # Named for the index production already has (added by + # tools/migrations/26-08-12--add_article_level_summary.sql), so nobody + # generates a second one. + __table_args__ = ( + db.UniqueConstraint("article_id", "cefr_level", name="uq_article_level"), + {"mysql_collate": "utf8_bin"}, + ) id = db.Column(db.Integer, primary_key=True) diff --git a/zeeguu/core/model/new_text.py b/zeeguu/core/model/new_text.py index 307562f2..9db8c4b5 100644 --- a/zeeguu/core/model/new_text.py +++ b/zeeguu/core/model/new_text.py @@ -8,7 +8,12 @@ class NewText(db.Model): - __table_args__ = {"mysql_collate": "utf8_bin"} + # HASH_INDEX is production's own name for this index; keep it so nobody + # generates a second one. + __table_args__ = ( + db.UniqueConstraint("content_hash", name="HASH_INDEX"), + {"mysql_collate": "utf8_bin"}, + ) id = db.Column(db.Integer, primary_key=True) diff --git a/zeeguu/core/model/source_text.py b/zeeguu/core/model/source_text.py index ab64c8fe..39753ce9 100644 --- a/zeeguu/core/model/source_text.py +++ b/zeeguu/core/model/source_text.py @@ -11,7 +11,12 @@ class SourceText(db.Model): - __table_args__ = {"mysql_collate": "utf8_bin"} + # HASH_INDEX is production's own name for this index; keep it so nobody + # generates a second one. It is what find_or_create's hash lookup relies on. + __table_args__ = ( + db.UniqueConstraint("content_hash", name="HASH_INDEX"), + {"mysql_collate": "utf8_bin"}, + ) id = db.Column(db.Integer, primary_key=True) # this mapes to TEXT in the mysql which can hold about 15K words diff --git a/zeeguu/core/model/starred_article.py b/zeeguu/core/model/starred_article.py index 62706a7e..9ad5c7ad 100644 --- a/zeeguu/core/model/starred_article.py +++ b/zeeguu/core/model/starred_article.py @@ -25,7 +25,15 @@ class StarredArticle(db.Model): """ - __table_args__ = {"mysql_collate": "utf8_bin"} + # url_id is production's own name for this index; keep it so nobody + # generates a second one. This was a bare UniqueConstraint further down the + # class body -- which does attach to the table -- but the module was missing + # from zeeguu.core.model's imports, so the table never reached + # db.create_all() and the test database had no starred_article at all. + __table_args__ = ( + UniqueConstraint("url_id", "user_id", name="url_id"), + {"mysql_collate": "utf8_bin"}, + ) id = Column(Integer, primary_key=True) @@ -43,9 +51,6 @@ class StarredArticle(db.Model): # Useful for ordering past read articles starred_date = Column(DateTime) - # Together an url_id and user_id identify an article - UniqueConstraint(url_id, user_id) - def __init__(self, user, url, _title: str, language): self.user = user self.url = url diff --git a/zeeguu/core/model/teacher_cohort_map.py b/zeeguu/core/model/teacher_cohort_map.py index b0b5fa59..22d6c5b4 100644 --- a/zeeguu/core/model/teacher_cohort_map.py +++ b/zeeguu/core/model/teacher_cohort_map.py @@ -7,7 +7,12 @@ class TeacherCohortMap(db.Model): - __table_args__ = {"mysql_collate": "utf8_bin"} + # user_id_2 is production's own (auto-generated) name for this index; keep it + # so nobody generates a second one under a tidier name. + __table_args__ = ( + db.UniqueConstraint("user_id", "cohort_id", name="user_id_2"), + {"mysql_collate": "utf8_bin"}, + ) id = Column(Integer, primary_key=True) diff --git a/zeeguu/core/model/text.py b/zeeguu/core/model/text.py index bb41e5df..6bafc0b6 100644 --- a/zeeguu/core/model/text.py +++ b/zeeguu/core/model/text.py @@ -13,7 +13,11 @@ class Text(db.Model): - __table_args__ = {"mysql_collate": "utf8_bin"} + # Named for the index production already has, so nobody generates a second one. + __table_args__ = ( + db.UniqueConstraint("content_hash", name="content_hash"), + {"mysql_collate": "utf8_bin"}, + ) id = db.Column(db.Integer, primary_key=True) diff --git a/zeeguu/core/model/user_avatar.py b/zeeguu/core/model/user_avatar.py index 7b741415..357c31c7 100644 --- a/zeeguu/core/model/user_avatar.py +++ b/zeeguu/core/model/user_avatar.py @@ -7,6 +7,11 @@ class UserAvatar(db.Model): __tablename__ = "user_avatar" + # One avatar per user. Named for the index production already has + # (tools/migrations/26-03-13--add_user_avatar.sql), so nobody generates a + # second one -- and so update_or_create's find-then-insert cannot quietly + # produce two avatars for a user in the test database. + __table_args__ = (db.UniqueConstraint("user_id", name="user_id"),) id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False) diff --git a/zeeguu/core/model/user_onboarding_message.py b/zeeguu/core/model/user_onboarding_message.py index 4e0b6f37..0734696e 100644 --- a/zeeguu/core/model/user_onboarding_message.py +++ b/zeeguu/core/model/user_onboarding_message.py @@ -13,7 +13,15 @@ class UserOnboardingMessage(db.Model): when that dismissal was performed. If not, this field will be null """ - __table_args__ = {"mysql_collate": "utf8_bin"} + # Named for the index production already has, so nobody generates a second one. + __table_args__ = ( + db.UniqueConstraint( + "user_id", + "onboarding_message_id", + name="ux_user_onboarding_message_user_message", + ), + {"mysql_collate": "utf8_bin"}, + ) id = db.Column(db.Integer, primary_key=True) diff --git a/zeeguu/core/model/user_word.py b/zeeguu/core/model/user_word.py index 62298803..63636669 100644 --- a/zeeguu/core/model/user_word.py +++ b/zeeguu/core/model/user_word.py @@ -14,7 +14,15 @@ class UserWord(db.Model): - __table_args__ = {"mysql_collate": "utf8_bin"} + # Named for the index production already has (added by + # tools/migrations/25-05-24--adding_the_user_word_table.sql), so nobody + # generates a second one under a different name. Declared here so the + # SQLite test database refuses the duplicate pairs MySQL refuses -- without + # it, a test exercising the find_or_create race passes against unfixed code. + __table_args__ = ( + db.UniqueConstraint("user_id", "meaning_id", name="unique_user_word"), + {"mysql_collate": "utf8_bin"}, + ) __tablename__ = "user_word" # Explicitly set table name for migration id = db.Column(db.Integer, primary_key=True)