|
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. |
2 | 6 |
|
3 | 7 | Revision ID: fix_original_filename |
4 | | -Revises: d1e2f3a4b5c6 |
| 8 | +Revises: add_hardcover_audio_seconds |
5 | 9 | Create Date: 2026-02-18 |
6 | 10 |
|
7 | 11 | """ |
|
15 | 19 | depends_on = None |
16 | 20 |
|
17 | 21 |
|
| 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 | + |
18 | 36 | def upgrade(): |
19 | 37 | conn = op.get_bind() |
20 | 38 | 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: |
23 | 46 | 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 | + ) |
24 | 122 |
|
25 | 123 |
|
26 | 124 | def downgrade(): |
27 | 125 | with op.batch_alter_table('books', schema=None) as batch_op: |
28 | 126 | 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