Skip to content

Commit 0181f08

Browse files
Running alembic check in cicd.
1 parent 4fae4ae commit 0181f08

File tree

15 files changed

+174
-43
lines changed

15 files changed

+174
-43
lines changed

.circleci/config.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,11 @@ jobs:
5858
paths:
5959
- "venv"
6060
- run:
61+
name: Run Alembic Migrations
6162
command: ./venv/bin/python ./main.py db upgrade
63+
- run:
64+
name: Check for Missing Migrations
65+
command: ./venv/bin/python ./main.py db check
6266
api-tests-3-12:
6367
docker:
6468
- image: cimg/python:3.12

alembic/env.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,20 @@
1515

1616
# add your model's MetaData object here
1717
# for 'autogenerate' support
18-
# from myapp import mymodel
19-
# target_metadata = mymodel.Base.metadata
20-
target_metadata = None
18+
from wrolpi.common import Base
19+
# Import all models so they are registered with the Base metadata
20+
from wrolpi.files.models import FileGroup, Directory # noqa: F401
21+
from wrolpi.files.ebooks import EBook # noqa: F401
22+
from wrolpi.tags import Tag # noqa: F401
23+
from wrolpi.flags import WROLPiFlag # noqa: F401
24+
from wrolpi.downloader import Download # noqa: F401
25+
from wrolpi.collections.models import Collection, CollectionItem # noqa: F401
26+
from modules.videos.models import Video, Channel # noqa: F401
27+
from modules.archive.models import Archive # noqa: F401
28+
from modules.zim.models import Zim, TagZimEntry, ZimSubscription # noqa: F401
29+
from modules.map.models import MapFile # noqa: F401
30+
from modules.inventory.models import Item, Inventory # noqa: F401
31+
target_metadata = Base.metadata
2132

2233

2334
# other values from the config, defined by the needs of env.py,
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
"""collections feature
2+
3+
Revision ID: f38cfe4b5cb2
4+
Revises: add_unique_collection_name_kind
5+
Create Date: 2025-11-29 16:51:34.222412
6+
7+
"""
8+
import sqlalchemy as sa
9+
from alembic import op
10+
from sqlalchemy.dialects import postgresql
11+
12+
# revision identifiers, used by Alembic.
13+
revision = 'f38cfe4b5cb2'
14+
down_revision = 'add_unique_collection_name_kind'
15+
branch_labels = None
16+
depends_on = None
17+
18+
19+
def upgrade():
20+
# ### commands auto generated by Alembic - please adjust! ###
21+
op.alter_column('archive', 'collection_id',
22+
existing_type=sa.INTEGER(),
23+
nullable=True)
24+
op.alter_column('file_group', 'mimetype',
25+
existing_type=sa.TEXT(),
26+
nullable=True)
27+
op.alter_column('tag_file', 'tag_id',
28+
existing_type=sa.INTEGER(),
29+
nullable=False)
30+
op.alter_column('tag_file', 'file_group_id',
31+
existing_type=sa.BIGINT(),
32+
nullable=False)
33+
op.alter_column('tag_zim', 'tag_id',
34+
existing_type=sa.INTEGER(),
35+
nullable=False)
36+
op.alter_column('tag_zim', 'zim_id',
37+
existing_type=sa.INTEGER(),
38+
nullable=False)
39+
op.drop_index('video_upload_date_idx', table_name='video')
40+
op.drop_column('video', 'duration')
41+
op.drop_column('video', 'upload_date')
42+
# ### end Alembic commands ###
43+
44+
45+
def downgrade():
46+
# ### commands auto generated by Alembic - please adjust! ###
47+
op.add_column('video', sa.Column('upload_date', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True))
48+
op.add_column('video', sa.Column('duration', sa.INTEGER(), autoincrement=False, nullable=True))
49+
op.create_index('video_upload_date_idx', 'video', ['upload_date'], unique=False)
50+
op.alter_column('tag_zim', 'zim_id',
51+
existing_type=sa.INTEGER(),
52+
nullable=True)
53+
op.alter_column('tag_zim', 'tag_id',
54+
existing_type=sa.INTEGER(),
55+
nullable=True)
56+
op.alter_column('tag_file', 'file_group_id',
57+
existing_type=sa.BIGINT(),
58+
nullable=True)
59+
op.alter_column('tag_file', 'tag_id',
60+
existing_type=sa.INTEGER(),
61+
nullable=True)
62+
op.alter_column('archive', 'collection_id',
63+
existing_type=sa.INTEGER(),
64+
nullable=False)
65+
op.alter_column('file_group', 'mimetype',
66+
existing_type=sa.TEXT(),
67+
nullable=False)
68+
# ### end Alembic commands ###

main.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232

3333
def db_main(args):
3434
"""
35-
Handle database migrations. This uses Alembic, supported commands are "upgrade" and "downgrade".
35+
Handle database migrations. This uses Alembic, supported commands are "upgrade", "downgrade", and "check".
3636
"""
3737
from alembic.config import Config
3838
from alembic import command
@@ -48,6 +48,11 @@ def db_main(args):
4848
command.upgrade(config, 'head')
4949
elif args.command == 'downgrade':
5050
command.downgrade(config, '-1')
51+
elif args.command == 'check':
52+
command.check(config)
53+
elif args.command == 'revision':
54+
message = getattr(args, 'message', None) or 'auto migration'
55+
command.revision(config, message=message, autogenerate=True)
5156
else:
5257
print(f'Unknown DB command: {args.command}')
5358
return 2
@@ -100,7 +105,8 @@ def main():
100105

101106
# DB Parser for running Alembic migrations
102107
db_parser = sub_commands.add_parser('db')
103-
db_parser.add_argument('command', help='Supported commands: upgrade, downgrade')
108+
db_parser.add_argument('command', help='Supported commands: upgrade, downgrade, check, revision')
109+
db_parser.add_argument('-m', '--message', help='Migration message (for revision command)')
104110

105111
args = parser.parse_args()
106112

modules/archive/__init__.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ def model_archive(file_group: FileGroup, session: Session = None) -> Archive:
190190

191191
# Set collection_id BEFORE adding to session to avoid autoflush constraint violation
192192
from modules.archive.lib import get_or_create_domain_collection
193+
193194
if file_group.url:
194195
# URL is already set on the FileGroup
195196
collection = get_or_create_domain_collection(session, file_group.url)
@@ -202,12 +203,8 @@ def model_archive(file_group: FileGroup, session: Session = None) -> Archive:
202203
collection = get_or_create_domain_collection(session, url)
203204
archive.collection_id = collection.id if collection else None
204205
except (RuntimeError, ValueError) as e:
205-
# Could not extract URL from singlefile
206-
if not PYTEST:
207-
# In production, archives must have a URL/collection
208-
raise InvalidArchive(f'Archive has no URL and could not extract from singlefile: {e}') from e
209-
# In tests, allow archives without collections (factory will set it later)
210-
logger.debug(f'Could not extract URL from singlefile (test mode): {e}')
206+
# Could not extract URL from singlefile - archive will have no collection
207+
logger.debug(f'Could not extract URL from singlefile: {e}')
211208
archive.collection_id = None
212209

213210
session.add(archive)

modules/inventory/models.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class Item(Base, ModelHelper):
2323
subcategory = Column(String)
2424
unit = Column(String)
2525

26-
inventory_id = Column(Integer, ForeignKey('inventory.id'))
26+
inventory_id = Column(Integer, ForeignKey('inventory.id', ondelete='CASCADE'))
2727
inventory = relationship('Inventory', primaryjoin="Item.inventory_id==Inventory.id", back_populates='items')
2828

2929
def __repr__(self):
@@ -35,7 +35,7 @@ class Inventory(Base, ModelHelper):
3535
__tablename__ = 'inventory'
3636
id = Column(Integer, primary_key=True)
3737

38-
name = Column(String, unique=True)
38+
name = Column(String, unique=True, nullable=False)
3939
viewed_at = Column(TZDateTime)
4040
created_at = Column(TZDateTime, default=now)
4141
deleted_at = Column(TZDateTime)
@@ -61,6 +61,3 @@ def find_by_name(session: Session, name: str) -> 'Inventory':
6161
return session.query(Inventory).filter_by(name=name).one()
6262

6363

64-
class InventoriesVersion(Base, ModelHelper):
65-
__tablename__ = 'inventories_version'
66-
version = Column(Integer, primary_key=True)

modules/map/models.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ class MapFile(Base, ModelHelper):
1010
__tablename__ = 'map_file'
1111
id = Column(Integer, primary_key=True)
1212

13-
path = Column(MediaPathType, unique=True, nullable=False)
14-
imported = Column(Boolean, default=False, nullable=False)
13+
path = Column(MediaPathType, nullable=False)
14+
imported = Column(Boolean, default=False)
1515
size = Column(BigInteger)
1616

1717
def __repr__(self):

modules/videos/models.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import pathlib
33
from typing import Optional, Dict, List
44

5-
from sqlalchemy import Column, Integer, String, Boolean, JSON, Date, ForeignKey, BigInteger
5+
from sqlalchemy import Column, Integer, String, Boolean, JSON, Date, ForeignKey, BigInteger, Index, text
66
from sqlalchemy.orm import relationship, Session, deferred
77
from sqlalchemy.orm.collections import InstrumentedList
88

@@ -28,6 +28,11 @@
2828

2929
class Video(ModelHelper, Base):
3030
__tablename__ = 'video'
31+
__table_args__ = (
32+
Index('video_channel_id_idx', 'channel_id'),
33+
Index('video_source_id_idx', 'source_id'),
34+
Index('video_view_count_idx', 'view_count'),
35+
)
3136
id = Column(Integer, primary_key=True)
3237

3338
source_id = Column(String) # The id from yt-dlp
@@ -530,18 +535,25 @@ def replace_info_json(self, info_json: dict, clean: bool = True, format_: bool =
530535

531536
class Channel(ModelHelper, Base):
532537
__tablename__ = 'channel'
538+
__table_args__ = (
539+
Index('channel_minimum_frequency_idx', 'minimum_frequency'),
540+
Index('channel_source_id', 'source_id'),
541+
Index('channel_total_size_idx', 'total_size'),
542+
Index('channel_video_count_idx', 'video_count'),
543+
Index('channel_url_key', 'url', unique=True, postgresql_where=text('url IS NOT NULL')),
544+
)
533545
id = Column(Integer, primary_key=True)
534546
# name and directory are stored in Collection, accessed via properties
535-
url = Column(String, unique=True) # will only be downloaded if related Download exists.
547+
url = Column(String) # will only be downloaded if related Download exists. Partial unique index on non-NULL values.
536548
generate_posters = Column(Boolean, default=False) # generating posters may delete files, and can be slow.
537549
calculate_duration = Column(Boolean, default=True) # use ffmpeg to extract duration (slower than info json).
538550
download_missing_data = Column(Boolean, default=True) # fetch missing data like `source_id` and video comments.
539551
source_id = Column(String) # the ID from the source website.
540552
refreshed = Column(Boolean, default=False) # The files in the Channel have been refreshed.
541553

542554
# Columns updated by triggers
543-
video_count = Column(Integer, default=0) # update_channel_video_count
544-
total_size = Column(Integer, default=0) # update_channel_size
555+
video_count = Column(Integer, default=0, nullable=False) # update_channel_video_count
556+
total_size = Column(BigInteger, default=0, nullable=False) # update_channel_size
545557
minimum_frequency = Column(Integer) # update_channel_minimum_frequency
546558

547559
info_json = deferred(Column(JSON))

modules/zim/models.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from typing import List, Tuple, OrderedDict as OrderedDictType, Dict, Optional, Set
77

88
from libzim import Archive, Searcher, Query, Entry, SuggestionSearcher
9-
from sqlalchemy import Column, Integer, BigInteger, ForeignKey, Text, tuple_, Boolean
9+
from sqlalchemy import Column, Integer, BigInteger, ForeignKey, Text, tuple_, Boolean, UniqueConstraint
1010
from sqlalchemy.orm import relationship, Session
1111
from sqlalchemy.orm.exc import NoResultFound # noqa
1212

@@ -67,12 +67,15 @@ class Zim(Base, ModelHelper):
6767
"""
6868

6969
__tablename__ = 'zim'
70+
__table_args__ = (
71+
UniqueConstraint('path', name='zim_path_key'),
72+
)
7073
id = Column(Integer, primary_key=True)
7174
path: pathlib.Path = Column(MediaPathType, nullable=False)
7275

73-
file_group_id = Column(BigInteger, ForeignKey('file_group.id', ondelete='CASCADE'), unique=True, nullable=False)
76+
file_group_id = Column(BigInteger, ForeignKey('file_group.id', ondelete='CASCADE'), nullable=False)
7477
file_group: FileGroup = relationship('FileGroup')
75-
auto_search = Column(Boolean, nullable=False, default=True)
78+
auto_search = Column(Boolean, default=True)
7679

7780
def __repr__(self):
7881
return f'<Zim id={self.id} file_group_id={self.file_group_id} path={self.path}>'
@@ -353,8 +356,11 @@ def entries_with_tags(cls, tag_names: List[str], session: Session = None) -> Ord
353356

354357
class TagZimEntry(Base):
355358
__tablename__ = 'tag_zim'
359+
__table_args__ = (
360+
UniqueConstraint('tag_id', 'zim_id', 'zim_entry', name='tag_zim_tag_id_zim_id_zim_entry_key'),
361+
)
356362

357-
tag_id = Column(Integer, ForeignKey('tag.id', ondelete='CASCADE'), primary_key=True)
363+
tag_id = Column(Integer, ForeignKey('tag.id'), primary_key=True)
358364
tag: Tag = relationship('Tag')
359365
zim_id = Column(Integer, ForeignKey('zim.id', ondelete='CASCADE'), primary_key=True)
360366
zim: Zim = relationship('Zim')

modules/zim/test/test_lib.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -161,10 +161,9 @@ async def test_zim_tags_config(async_client, test_session, test_directory, test_
161161
['Tag1', test_zim.path.name, 'two', '2000-01-01T00:00:00.000001+00:00'],
162162
]
163163

164-
# Delete all Tags so they are recreated.
165-
test_session.query(tags.Tag).delete()
166-
# Delete all TagFileEntry(s), import them again.
164+
# Delete all TagZimEntry(s) first (due to FK constraint), then Tags so they are recreated.
167165
test_session.query(TagZimEntry).delete()
166+
test_session.query(tags.Tag).delete()
168167
test_session.commit()
169168

170169
tags.import_tags_config()

0 commit comments

Comments
 (0)