66copied into district_notes by b3d9f47a25c1; form comments/commenters/tags are
77dropped without migration by decision.
88
9- Irreversible: downgrade raises. Restore from a backup if this ever needs
10- undoing .
9+ Downgrade recreates the tables (final shape as of 0db008690d60 + da39a3ee5e6b)
10+ empty — the data is gone; restore from a backup if it's needed .
1111
1212Revision ID: d8f1b52c96e3
1313Revises: c7e2a94d81f5
1919
2020from alembic import op
2121import sqlalchemy as sa
22+ from sqlalchemy .dialects import postgresql
23+ import app .core .models
24+ from app .constants import SQL_DIR
2225
2326# revision identifiers, used by Alembic.
2427revision : str = "d8f1b52c96e3"
2528down_revision : Union [str , None ] = "c7e2a94d81f5"
2629branch_labels : Union [str , Sequence [str ], None ] = None
2730depends_on : Union [str , Sequence [str ], None ] = None
2831
32+ # Inlined (the app-side ReviewStatus enum was deleted with the legacy
33+ # comment tables; this migration must stay runnable). create_type=False so
34+ # create_table doesn't re-emit CREATE TYPE; downgrade creates it explicitly.
35+ review_status_enum = postgresql .ENUM (
36+ "REVIEWED" ,
37+ "APPROVED" ,
38+ "REJECTED" ,
39+ name = "review_status_enum" ,
40+ schema = "comments" ,
41+ create_type = False ,
42+ )
43+
44+
45+ def _timestamps ():
46+ return (
47+ sa .Column (
48+ "created_at" ,
49+ sa .TIMESTAMP (timezone = True ),
50+ server_default = sa .text ("CURRENT_TIMESTAMP" ),
51+ nullable = False ,
52+ ),
53+ sa .Column (
54+ "updated_at" ,
55+ sa .TIMESTAMP (timezone = True ),
56+ server_default = sa .text ("CURRENT_TIMESTAMP" ),
57+ nullable = False ,
58+ ),
59+ )
60+
2961
3062def upgrade () -> None :
3163 op .drop_table ("document_comment" , schema = "comments" )
@@ -40,6 +72,143 @@ def upgrade() -> None:
4072
4173
4274def downgrade () -> None :
43- raise NotImplementedError (
44- "The legacy comment tables are gone for good — restore from a backup."
75+ # Recreates the schema only; rows are unrecoverable without a backup.
76+ op .execute (
77+ sa .text (
78+ "CREATE TYPE comments.review_status_enum AS ENUM "
79+ "('REVIEWED', 'APPROVED', 'REJECTED')"
80+ )
81+ )
82+
83+ op .create_table (
84+ "commenter" ,
85+ * _timestamps (),
86+ sa .Column ("id" , sa .Integer (), autoincrement = True , nullable = False ),
87+ sa .Column ("first_name" , sa .String (length = 255 ), nullable = False ),
88+ sa .Column ("email" , sa .String (length = 320 ), nullable = False ),
89+ sa .Column ("salutation" , sa .String (length = 255 ), nullable = True ),
90+ sa .Column ("last_name" , sa .String (length = 255 ), nullable = True ),
91+ sa .Column ("place" , sa .String (length = 255 ), nullable = True ),
92+ sa .Column ("state" , sa .String (length = 255 ), nullable = True ),
93+ sa .Column ("zip_code" , sa .String (length = 255 ), nullable = True ),
94+ sa .Column ("moderation_score" , sa .Float (), nullable = True ),
95+ sa .Column ("review_status" , review_status_enum , nullable = True ),
96+ sa .CheckConstraint (
97+ "email ~* '^[a-zA-Z0-9.!#$%%&''*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\ .[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$'" ,
98+ name = "valid_email_format" ,
99+ ),
100+ sa .CheckConstraint ("LENGTH(TRIM(email)) > 0" , name = "email_not_empty" ),
101+ sa .CheckConstraint ("LENGTH(TRIM(first_name)) > 0" , name = "first_name_not_empty" ),
102+ sa .PrimaryKeyConstraint ("id" ),
103+ sa .UniqueConstraint (
104+ "first_name" , "email" , name = "commenter_unique_on_first_name_and_email"
105+ ),
106+ schema = "comments" ,
107+ )
108+ op .create_index (
109+ "idx_commenter_first_name_and_email" ,
110+ "commenter" ,
111+ [sa .text ("lower(trim(first_name))" ), sa .text ("lower(trim(email))" )],
112+ unique = False ,
113+ schema = "comments" ,
114+ )
115+ op .create_index (
116+ op .f ("ix_comments_commenter_id" ),
117+ "commenter" ,
118+ ["id" ],
119+ unique = True ,
120+ schema = "comments" ,
121+ )
122+
123+ op .create_table (
124+ "comment" ,
125+ * _timestamps (),
126+ sa .Column ("id" , sa .Integer (), autoincrement = True , nullable = False ),
127+ sa .Column ("title" , sa .String (length = 255 ), nullable = False ),
128+ sa .Column ("comment" , sa .String (length = 5000 ), nullable = False ),
129+ sa .Column ("commenter_id" , sa .Integer (), nullable = True ),
130+ sa .Column ("moderation_score" , sa .Float (), nullable = True ),
131+ sa .Column ("review_status" , review_status_enum , nullable = True ),
132+ sa .Column (
133+ "review_flagged" , sa .Boolean (), nullable = False , server_default = "false"
134+ ),
135+ sa .ForeignKeyConstraint (["commenter_id" ], ["comments.commenter.id" ]),
136+ sa .PrimaryKeyConstraint ("id" ),
137+ sa .CheckConstraint ("LENGTH(TRIM(title)) > 0" , name = "title_not_empty" ),
138+ sa .CheckConstraint ("LENGTH(TRIM(comment)) > 0" , name = "comment_not_empty" ),
139+ schema = "comments" ,
140+ )
141+ op .create_index (
142+ op .f ("ix_comments_comment_commenter_id" ),
143+ "comment" ,
144+ ["commenter_id" ],
145+ unique = False ,
146+ schema = "comments" ,
147+ )
148+ op .create_index (
149+ op .f ("ix_comments_comment_id" ),
150+ "comment" ,
151+ ["id" ],
152+ unique = True ,
153+ schema = "comments" ,
154+ )
155+
156+ op .create_table (
157+ "tag" ,
158+ * _timestamps (),
159+ sa .Column ("id" , sa .Integer (), autoincrement = True , nullable = False ),
160+ sa .Column ("slug" , sa .String (length = 255 ), nullable = False ),
161+ sa .Column ("moderation_score" , sa .Float (), nullable = True ),
162+ sa .Column ("review_status" , review_status_enum , nullable = True ),
163+ sa .PrimaryKeyConstraint ("id" ),
164+ sa .CheckConstraint ("LENGTH(slug) > 0" , name = "slug_not_empty" ),
165+ schema = "comments" ,
166+ )
167+ op .create_index (
168+ op .f ("ix_comments_tag_id" ), "tag" , ["id" ], unique = True , schema = "comments"
169+ )
170+ op .create_index (
171+ op .f ("ix_comments_tag_slug" ), "tag" , ["slug" ], unique = True , schema = "comments"
172+ )
173+
174+ op .create_table (
175+ "comment_tag" ,
176+ sa .Column ("comment_id" , sa .Integer (), nullable = False ),
177+ sa .Column ("tag_id" , sa .Integer (), nullable = False ),
178+ sa .ForeignKeyConstraint (["comment_id" ], ["comments.comment.id" ]),
179+ sa .ForeignKeyConstraint (["tag_id" ], ["comments.tag.id" ]),
180+ sa .PrimaryKeyConstraint ("comment_id" , "tag_id" ),
181+ sa .UniqueConstraint ("comment_id" , "tag_id" , name = "unique_comment_tag_link" ),
182+ schema = "comments" ,
183+ )
184+
185+ op .create_table (
186+ "document_comment" ,
187+ sa .Column ("comment_id" , sa .Integer (), nullable = False ),
188+ sa .Column ("document_id" , app .core .models .UUIDType (), nullable = False ),
189+ sa .Column ("zone" , sa .Integer (), nullable = True ),
190+ sa .ForeignKeyConstraint (["comment_id" ], ["comments.comment.id" ]),
191+ sa .ForeignKeyConstraint (["document_id" ], ["document.document.document_id" ]),
192+ sa .PrimaryKeyConstraint ("comment_id" ),
193+ sa .UniqueConstraint ("comment_id" ),
194+ schema = "comments" ,
195+ )
196+ op .create_index (
197+ op .f ("ix_comments_document_comment_document_id" ),
198+ "document_comment" ,
199+ ["document_id" ],
200+ unique = False ,
201+ schema = "comments" ,
202+ )
203+
204+ with open (SQL_DIR / "normalize_email.sql" , "r" ) as f :
205+ op .execute (sa .text (f .read ()))
206+ op .execute (
207+ sa .text ("""
208+ CREATE TRIGGER normalize_email_trigger
209+ BEFORE INSERT OR UPDATE ON comments.commenter
210+ FOR EACH ROW EXECUTE FUNCTION normalize_email();
211+ """ )
45212 )
213+ with open (SQL_DIR / "slugify_tag.sql" , "r" ) as f :
214+ op .execute (sa .text (f .read ()))
0 commit comments