From adc358335881389dd0e9f7508f9a4a650c343186 Mon Sep 17 00:00:00 2001 From: sepehr Date: Sun, 14 Jun 2026 19:01:07 +0200 Subject: [PATCH] fix(db): make migrations and glossary index SQLite-compatible --- .../versions/005_add_reset_token_to_users.py | 13 +++-- .../versions/006_fix_tier_check_constraint.py | 46 ++++++++++++----- ...e0f1a2_set_multilingual_target_language.py | 51 +++++++++++++------ ...e0f1a2b3_rename_multilingual_glossaries.py | 8 +++ ...e1f2a3b4c5_cleanup_duplicate_glossaries.py | 8 +++ database/models.py | 2 +- 6 files changed, 93 insertions(+), 35 deletions(-) diff --git a/alembic/versions/005_add_reset_token_to_users.py b/alembic/versions/005_add_reset_token_to_users.py index 64f8e68..efd7b23 100644 --- a/alembic/versions/005_add_reset_token_to_users.py +++ b/alembic/versions/005_add_reset_token_to_users.py @@ -21,11 +21,14 @@ depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: op.add_column("users", sa.Column("reset_token", sa.String(length=255), nullable=True)) op.add_column("users", sa.Column("reset_token_expires", sa.DateTime(), nullable=True)) - # Make legacy password_hash column nullable (ORM uses hashed_password now) - op.alter_column("users", "password_hash", nullable=True) + # Make legacy password_hash column nullable (ORM uses hashed_password now). + # Use batch_alter_table for SQLite compatibility. + with op.batch_alter_table("users") as batch_op: + batch_op.alter_column("password_hash", nullable=True) def downgrade() -> None: - op.drop_column("users", "reset_token_expires") - op.drop_column("users", "reset_token") - op.alter_column("users", "password_hash", nullable=False) + with op.batch_alter_table("users") as batch_op: + batch_op.drop_column("reset_token_expires") + batch_op.drop_column("reset_token") + batch_op.alter_column("password_hash", nullable=False) diff --git a/alembic/versions/006_fix_tier_check_constraint.py b/alembic/versions/006_fix_tier_check_constraint.py index 9613de2..4ba1346 100644 --- a/alembic/versions/006_fix_tier_check_constraint.py +++ b/alembic/versions/006_fix_tier_check_constraint.py @@ -12,20 +12,38 @@ depends_on = None def upgrade() -> None: - op.execute( - "ALTER TABLE users DROP CONSTRAINT IF EXISTS ck_users_tier" - ) - op.execute( - "ALTER TABLE users ADD CONSTRAINT ck_users_tier " - "CHECK (tier IN ('free', 'starter', 'pro', 'business', 'enterprise'))" - ) + conn = op.get_bind() + dialect = conn.dialect.name + + if dialect == "sqlite": + # Migration 002 skipped the CHECK constraint on SQLite; just create it now. + with op.batch_alter_table("users") as batch_op: + batch_op.create_check_constraint( + "ck_users_tier", + "tier IN ('free', 'starter', 'pro', 'business', 'enterprise')", + ) + else: + op.execute("ALTER TABLE users DROP CONSTRAINT IF EXISTS ck_users_tier") + op.execute( + "ALTER TABLE users ADD CONSTRAINT ck_users_tier " + "CHECK (tier IN ('free', 'starter', 'pro', 'business', 'enterprise'))" + ) def downgrade() -> None: - op.execute( - "ALTER TABLE users DROP CONSTRAINT IF EXISTS ck_users_tier" - ) - op.execute( - "ALTER TABLE users ADD CONSTRAINT ck_users_tier " - "CHECK (tier IN ('free', 'pro'))" - ) + conn = op.get_bind() + dialect = conn.dialect.name + + if dialect == "sqlite": + with op.batch_alter_table("users") as batch_op: + batch_op.drop_constraint("ck_users_tier", type_="check") + batch_op.create_check_constraint( + "ck_users_tier", + "tier IN ('free', 'pro')", + ) + else: + op.execute("ALTER TABLE users DROP CONSTRAINT IF EXISTS ck_users_tier") + op.execute( + "ALTER TABLE users ADD CONSTRAINT ck_users_tier " + "CHECK (tier IN ('free', 'pro'))" + ) diff --git a/alembic/versions/b7c8d9e0f1a2_set_multilingual_target_language.py b/alembic/versions/b7c8d9e0f1a2_set_multilingual_target_language.py index 4cc864c..df5be2e 100644 --- a/alembic/versions/b7c8d9e0f1a2_set_multilingual_target_language.py +++ b/alembic/versions/b7c8d9e0f1a2_set_multilingual_target_language.py @@ -26,23 +26,44 @@ depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: + conn = op.get_bind() + dialect = conn.dialect.name + # Glossaries with terms containing 5+ translation keys are multilingual templates # (enriched glossaries have 11 translations: de, es, it, pt, nl, ru, ja, ko, zh, ar, fa) - op.execute(""" - UPDATE glossaries - SET target_language = 'multi' - WHERE id IN ( - SELECT DISTINCT g.id - FROM glossaries g - JOIN glossary_terms gt ON gt.glossary_id = g.id - WHERE gt.translations IS NOT NULL - AND jsonb_typeof(gt.translations) = 'object' - AND ( - SELECT count(*) - FROM jsonb_object_keys(gt.translations) - ) >= 5 - ) - """) + if dialect == "sqlite": + # SQLite does not have jsonb_object_keys. Use json_each for compatibility. + op.execute(""" + UPDATE glossaries + SET target_language = 'multi' + WHERE id IN ( + SELECT DISTINCT g.id + FROM glossaries g + JOIN glossary_terms gt ON gt.glossary_id = g.id + WHERE gt.translations IS NOT NULL + AND json_type(gt.translations, '$') = 'object' + AND ( + SELECT count(*) + FROM json_each(gt.translations) + ) >= 5 + ) + """) + else: + op.execute(""" + UPDATE glossaries + SET target_language = 'multi' + WHERE id IN ( + SELECT DISTINCT g.id + FROM glossaries g + JOIN glossary_terms gt ON gt.glossary_id = g.id + WHERE gt.translations IS NOT NULL + AND jsonb_typeof(gt.translations) = 'object' + AND ( + SELECT count(*) + FROM jsonb_object_keys(gt.translations) + ) >= 5 + ) + """) # Also rename glossaries: replace "Anglais" with "Multilingue" in the name op.execute(""" diff --git a/alembic/versions/c8d9e0f1a2b3_rename_multilingual_glossaries.py b/alembic/versions/c8d9e0f1a2b3_rename_multilingual_glossaries.py index 5af8dbf..1b84f81 100644 --- a/alembic/versions/c8d9e0f1a2b3_rename_multilingual_glossaries.py +++ b/alembic/versions/c8d9e0f1a2b3_rename_multilingual_glossaries.py @@ -22,6 +22,14 @@ depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: + conn = op.get_bind() + dialect = conn.dialect.name + + if dialect == "sqlite": + # SQLite test databases are created fresh and have no legacy glossaries to clean up. + # The PostgreSQL-specific jsonb_object_keys logic is not needed here. + return + # 1. Delete old FR→EN glossaries that don't have multilingual translations # (imported before enrichment, they are stale duplicates) op.execute(""" diff --git a/alembic/versions/d0e1f2a3b4c5_cleanup_duplicate_glossaries.py b/alembic/versions/d0e1f2a3b4c5_cleanup_duplicate_glossaries.py index e627ab6..57ea0d8 100644 --- a/alembic/versions/d0e1f2a3b4c5_cleanup_duplicate_glossaries.py +++ b/alembic/versions/d0e1f2a3b4c5_cleanup_duplicate_glossaries.py @@ -21,10 +21,18 @@ depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: + conn = op.get_bind() + dialect = conn.dialect.name + # Step 1: Delete ALL glossaries with target_language='en' (old stale imports) op.execute("DELETE FROM glossary_terms WHERE glossary_id IN (SELECT id FROM glossaries WHERE target_language = 'en')") op.execute("DELETE FROM glossaries WHERE target_language = 'en'") + if dialect == "sqlite": + # SQLite test databases are created fresh and have no duplicate multilingual glossaries. + # The PostgreSQL-specific DISTINCT ON logic is not needed here. + return + # Step 2: Deduplicate multilingual glossaries — keep only the newest per name # Delete terms for duplicates first, then the duplicates themselves op.execute(""" diff --git a/database/models.py b/database/models.py index 3897c3f..5e4b137 100644 --- a/database/models.py +++ b/database/models.py @@ -334,7 +334,7 @@ class Glossary(Base): name = Column(String(255), nullable=False) source_language = Column(String(10), nullable=False, default="fr") target_language = Column(String(10), nullable=True, default="en") - template_id = Column(String(50), nullable=True, index=True) + template_id = Column(String(50), nullable=True) created_at = Column(DateTime, default=_utcnow) updated_at = Column(DateTime, default=_utcnow, onupdate=_utcnow)