From 8d836dae349fba4a4d785dd895452827d02f3f06 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Mon, 2 Feb 2026 08:31:36 -0500 Subject: [PATCH] Make Alembic migrations idempotent for SQLite Check if columns/tables exist before adding them so migrations don't fail on re-run when schema was applied out of band. --- ...e_add_customizable_button_text_comment_.py | 22 +++++++-- ..._add_push_notifications_and_webmentions.py | 49 +++++++++++++------ 2 files changed, 51 insertions(+), 20 deletions(-) diff --git a/remarkbox/scripts/alembic/versions/896568b0752e_add_customizable_button_text_comment_.py b/remarkbox/scripts/alembic/versions/896568b0752e_add_customizable_button_text_comment_.py index dfe13d1..a6d7d3d 100644 --- a/remarkbox/scripts/alembic/versions/896568b0752e_add_customizable_button_text_comment_.py +++ b/remarkbox/scripts/alembic/versions/896568b0752e_add_customizable_button_text_comment_.py @@ -16,14 +16,26 @@ from alembic import op import sqlalchemy as sa +def _column_exists(table, column): + """Check if a column exists in a SQLite table.""" + conn = op.get_bind() + result = conn.execute(sa.text("PRAGMA table_info('{}')".format(table))) + return any(row[1] == column for row in result) + + def upgrade(): # T4: customizable button text and comment labels - op.add_column('rb_namespace', sa.Column('submit_button_text', sa.Unicode(length=256), nullable=True)) - op.add_column('rb_namespace', sa.Column('comment_label_singular', sa.Unicode(length=256), nullable=True)) - op.add_column('rb_namespace', sa.Column('comment_label_plural', sa.Unicode(length=256), nullable=True)) + if not _column_exists('rb_namespace', 'submit_button_text'): + op.add_column('rb_namespace', sa.Column('submit_button_text', sa.Unicode(length=256), nullable=True)) + if not _column_exists('rb_namespace', 'comment_label_singular'): + op.add_column('rb_namespace', sa.Column('comment_label_singular', sa.Unicode(length=256), nullable=True)) + if not _column_exists('rb_namespace', 'comment_label_plural'): + op.add_column('rb_namespace', sa.Column('comment_label_plural', sa.Unicode(length=256), nullable=True)) # T8: nesting depth settings - op.add_column('rb_namespace', sa.Column('max_nesting_depth', sa.Integer(), nullable=True)) - op.add_column('rb_namespace', sa.Column('collapse_depth', sa.Integer(), nullable=True)) + if not _column_exists('rb_namespace', 'max_nesting_depth'): + op.add_column('rb_namespace', sa.Column('max_nesting_depth', sa.Integer(), nullable=True)) + if not _column_exists('rb_namespace', 'collapse_depth'): + op.add_column('rb_namespace', sa.Column('collapse_depth', sa.Integer(), nullable=True)) def downgrade(): diff --git a/remarkbox/scripts/alembic/versions/b7f3a2d1e8c9_add_push_notifications_and_webmentions.py b/remarkbox/scripts/alembic/versions/b7f3a2d1e8c9_add_push_notifications_and_webmentions.py index 87f0e69..6660657 100644 --- a/remarkbox/scripts/alembic/versions/b7f3a2d1e8c9_add_push_notifications_and_webmentions.py +++ b/remarkbox/scripts/alembic/versions/b7f3a2d1e8c9_add_push_notifications_and_webmentions.py @@ -18,25 +18,44 @@ import sqlalchemy as sa from remarkbox.models.meta import UUIDType +def _column_exists(table, column): + """Check if a column exists in a SQLite table.""" + conn = op.get_bind() + result = conn.execute(sa.text("PRAGMA table_info('{}')".format(table))) + return any(row[1] == column for row in result) + + +def _table_exists(table): + """Check if a table exists in SQLite.""" + conn = op.get_bind() + result = conn.execute(sa.text( + "SELECT name FROM sqlite_master WHERE type='table' AND name='{}'".format(table) + )) + return result.fetchone() is not None + + def upgrade(): # T10: push notification preferences on user - op.add_column('rb_user', sa.Column('notification_preference', sa.Unicode(length=5), server_default='email', nullable=False)) - op.add_column('rb_user', sa.Column('push_subscriptions', sa.UnicodeText(), nullable=True)) + if not _column_exists('rb_user', 'notification_preference'): + op.add_column('rb_user', sa.Column('notification_preference', sa.Unicode(length=5), server_default='email', nullable=False)) + if not _column_exists('rb_user', 'push_subscriptions'): + op.add_column('rb_user', sa.Column('push_subscriptions', sa.UnicodeText(), nullable=True)) # T7: webmention table - op.create_table( - 'rb_webmention', - sa.Column('id', UUIDType, primary_key=True, index=True), - sa.Column('source', sa.Unicode(2048), nullable=False), - sa.Column('target', sa.Unicode(2048), nullable=False), - sa.Column('node_id', UUIDType, sa.ForeignKey('rb_node.id'), index=True, nullable=True), - sa.Column('verified', sa.Boolean(), default=False, nullable=False), - sa.Column('author_name', sa.Unicode(256), nullable=True), - sa.Column('author_url', sa.Unicode(2048), nullable=True), - sa.Column('content', sa.UnicodeText(), nullable=True), - sa.Column('created_timestamp', sa.BigInteger(), nullable=False), - sa.Column('updated_timestamp', sa.BigInteger(), nullable=False), - ) + if not _table_exists('rb_webmention'): + op.create_table( + 'rb_webmention', + sa.Column('id', UUIDType, primary_key=True, index=True), + sa.Column('source', sa.Unicode(2048), nullable=False), + sa.Column('target', sa.Unicode(2048), nullable=False), + sa.Column('node_id', UUIDType, sa.ForeignKey('rb_node.id'), index=True, nullable=True), + sa.Column('verified', sa.Boolean(), default=False, nullable=False), + sa.Column('author_name', sa.Unicode(256), nullable=True), + sa.Column('author_url', sa.Unicode(2048), nullable=True), + sa.Column('content', sa.UnicodeText(), nullable=True), + sa.Column('created_timestamp', sa.BigInteger(), nullable=False), + sa.Column('updated_timestamp', sa.BigInteger(), nullable=False), + ) def downgrade():