Skip to content

Commit 90ce76c

Browse files
authored
[Fix] Comprehensive safety migration for all missing columns and tables (#125)
1 parent ac5ba41 commit 90ce76c

File tree

1 file changed

+108
-4
lines changed

1 file changed

+108
-4
lines changed

alembic/versions/fix_original_filename.py

Lines changed: 108 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1-
"""ensure original_ebook_filename exists
1+
"""ensure all expected columns and tables exist
2+
3+
This is a comprehensive safety migration that idempotently verifies
4+
the schema matches the current model definitions. It catches any columns
5+
or tables that may have been missed due to migration chain issues.
26
37
Revision ID: fix_original_filename
4-
Revises: d1e2f3a4b5c6
8+
Revises: add_hardcover_audio_seconds
59
Create Date: 2026-02-18
610
711
"""
@@ -15,14 +19,114 @@
1519
depends_on = None
1620

1721

22+
def _get_columns(inspector, table_name: str) -> set:
23+
"""Get the set of column names for a table, or empty set if table missing."""
24+
if table_name not in inspector.get_table_names():
25+
return set()
26+
return {c['name'] for c in inspector.get_columns(table_name)}
27+
28+
29+
def _get_indexes(inspector, table_name: str) -> set:
30+
"""Get the set of index names for a table."""
31+
if table_name not in inspector.get_table_names():
32+
return set()
33+
return {idx['name'] for idx in inspector.get_indexes(table_name)}
34+
35+
1836
def upgrade():
1937
conn = op.get_bind()
2038
inspector = sa.inspect(conn)
21-
columns = [c['name'] for c in inspector.get_columns('books')]
22-
if 'original_ebook_filename' not in columns:
39+
tables = inspector.get_table_names()
40+
41+
# ── books table ──────────────────────────────────────────────────
42+
books_cols = _get_columns(inspector, 'books')
43+
if 'sync_mode' not in books_cols:
44+
op.add_column('books', sa.Column('sync_mode', sa.String(20), nullable=True, server_default='audiobook'))
45+
if 'original_ebook_filename' not in books_cols:
2346
op.add_column('books', sa.Column('original_ebook_filename', sa.String(500), nullable=True))
47+
if 'storyteller_uuid' not in books_cols:
48+
op.add_column('books', sa.Column('storyteller_uuid', sa.String(36), nullable=True))
49+
if 'abs_ebook_item_id' not in books_cols:
50+
op.add_column('books', sa.Column('abs_ebook_item_id', sa.String(255), nullable=True))
51+
52+
books_indexes = _get_indexes(inspector, 'books')
53+
if 'storyteller_uuid' in _get_columns(inspector, 'books') and 'ix_books_storyteller_uuid' not in books_indexes:
54+
op.create_index(op.f('ix_books_storyteller_uuid'), 'books', ['storyteller_uuid'], unique=False)
55+
56+
# ── hardcover_details table ──────────────────────────────────────
57+
hc_cols = _get_columns(inspector, 'hardcover_details')
58+
if 'hardcover_slug' not in hc_cols:
59+
op.add_column('hardcover_details', sa.Column('hardcover_slug', sa.String(255), nullable=True))
60+
if 'hardcover_audio_seconds' not in hc_cols:
61+
op.add_column('hardcover_details', sa.Column('hardcover_audio_seconds', sa.Integer(), nullable=True))
62+
63+
# ── kosync_documents table ───────────────────────────────────────
64+
if 'kosync_documents' in tables:
65+
kd_cols = _get_columns(inspector, 'kosync_documents')
66+
if 'filename' not in kd_cols:
67+
op.add_column('kosync_documents', sa.Column('filename', sa.String(500), nullable=True))
68+
if 'source' not in kd_cols:
69+
op.add_column('kosync_documents', sa.Column('source', sa.String(50), nullable=True))
70+
if 'booklore_id' not in kd_cols:
71+
op.add_column('kosync_documents', sa.Column('booklore_id', sa.String(255), nullable=True))
72+
if 'mtime' not in kd_cols:
73+
op.add_column('kosync_documents', sa.Column('mtime', sa.Float(), nullable=True))
74+
75+
kd_indexes = _get_indexes(inspector, 'kosync_documents')
76+
if 'booklore_id' in _get_columns(inspector, 'kosync_documents') and 'ix_kosync_documents_booklore_id' not in kd_indexes:
77+
op.create_index(op.f('ix_kosync_documents_booklore_id'), 'kosync_documents', ['booklore_id'], unique=False)
78+
79+
# ── jobs table ───────────────────────────────────────────────────
80+
jobs_cols = _get_columns(inspector, 'jobs')
81+
if 'progress' not in jobs_cols:
82+
op.add_column('jobs', sa.Column('progress', sa.Float(), nullable=True, server_default='0.0'))
83+
84+
# ── settings table ───────────────────────────────────────────────
85+
if 'settings' not in tables:
86+
op.create_table('settings',
87+
sa.Column('key', sa.String(length=255), nullable=False),
88+
sa.Column('value', sa.Text(), nullable=True),
89+
sa.PrimaryKeyConstraint('key')
90+
)
91+
92+
# ── pending_suggestions table ────────────────────────────────────
93+
if 'pending_suggestions' not in tables:
94+
op.create_table('pending_suggestions',
95+
sa.Column('id', sa.Integer(), primary_key=True, autoincrement=True),
96+
sa.Column('source', sa.String(50), nullable=True, server_default='abs'),
97+
sa.Column('source_id', sa.String(255), nullable=True),
98+
sa.Column('title', sa.String(500), nullable=True),
99+
sa.Column('author', sa.String(500), nullable=True),
100+
sa.Column('cover_url', sa.String(500), nullable=True),
101+
sa.Column('matches_json', sa.Text(), nullable=True),
102+
sa.Column('status', sa.String(20), nullable=True, server_default='pending'),
103+
sa.Column('created_at', sa.DateTime(), nullable=True),
104+
)
105+
106+
# ── book_alignments table ────────────────────────────────────────
107+
if 'book_alignments' not in tables:
108+
op.create_table('book_alignments',
109+
sa.Column('abs_id', sa.String(255), sa.ForeignKey('books.abs_id', ondelete='CASCADE'), primary_key=True),
110+
sa.Column('alignment_map_json', sa.Text(), nullable=True),
111+
)
112+
113+
# ── booklore_books table ─────────────────────────────────────────
114+
if 'booklore_books' not in tables:
115+
op.create_table('booklore_books',
116+
sa.Column('id', sa.Integer(), primary_key=True, autoincrement=True),
117+
sa.Column('filename', sa.String(500), nullable=True),
118+
sa.Column('title', sa.String(500), nullable=True),
119+
sa.Column('authors', sa.String(500), nullable=True),
120+
sa.Column('raw_metadata', sa.Text(), nullable=True),
121+
)
24122

25123

26124
def downgrade():
27125
with op.batch_alter_table('books', schema=None) as batch_op:
28126
batch_op.drop_column('original_ebook_filename')
127+
batch_op.drop_column('storyteller_uuid')
128+
batch_op.drop_column('abs_ebook_item_id')
129+
batch_op.drop_column('sync_mode')
130+
with op.batch_alter_table('hardcover_details', schema=None) as batch_op:
131+
batch_op.drop_column('hardcover_slug')
132+
batch_op.drop_column('hardcover_audio_seconds')

0 commit comments

Comments
 (0)