diff --git a/app/dbmanager.py b/app/dbmanager.py index 9f8c2ca..e80d6fd 100644 --- a/app/dbmanager.py +++ b/app/dbmanager.py @@ -18,7 +18,8 @@ class DatabaseManager: def __init__(self, database_url: str, **kwargs: dict) -> None: if DatabaseManager.__instance is None: self.db = Alchemical(database_url, **kwargs) - self.db.create_all() + # Database managed by Alembic so no create_all() required + # self.db.create_all() DatabaseManager.__instance = self else: raise Exception("Attempted to create a second DatabaseManager instance") diff --git a/app/dbtables.py b/app/dbtables.py index 14116e0..45e927b 100644 --- a/app/dbtables.py +++ b/app/dbtables.py @@ -1,6 +1,8 @@ # Standard library imports from typing import Optional +from dataclasses import asdict import datetime as dt +import json # PyQt imports @@ -13,13 +15,37 @@ from sqlalchemy import ( String, ) from sqlalchemy.ext.associationproxy import association_proxy +from sqlalchemy.ext.hybrid import hybrid_property +from sqlalchemy.engine.interfaces import Dialect from sqlalchemy.orm import ( Mapped, mapped_column, relationship, ) +from sqlalchemy.types import TypeDecorator, TEXT # App imports +from classes import Filter + + +class JSONEncodedDict(TypeDecorator): + """ + Custom JSON Type for MariaDB (since native JSON type is just LONGTEXT) + """ + + impl = TEXT + + def process_bind_param(self, value: dict | None, dialect: Dialect) -> str | None: + """Convert Python dictionary to JSON string before saving.""" + if value is None: + return None + return json.dumps(value, default=lambda o: o.__dict__) + + def process_result_value(self, value: str | None, dialect: Dialect) -> dict | None: + """Convert JSON string back to Python dictionary after retrieval.""" + if value is None: + return None + return json.loads(value) # Database classes @@ -128,15 +154,24 @@ class QueriesTable(Model): id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) name: Mapped[str] = mapped_column(String(128), nullable=False) - sql: Mapped[str] = mapped_column( - String(2048), index=False, default="", nullable=False - ) - favourite: Mapped[bool] = mapped_column( - Boolean, nullable=False, index=False, default=False - ) + _filter_data: Mapped[dict | None] = mapped_column("filter_data", JSONEncodedDict, nullable=True) + favourite: Mapped[bool] = mapped_column(Boolean, nullable=False, index=False, default=False) + + def _get_filter(self) -> Filter: + """Convert stored JSON dictionary to a Filter object.""" + if isinstance(self._filter_data, dict): + return Filter(**self._filter_data) + return Filter() # Default object if None or invalid data + + def _set_filter(self, value: Filter | None) -> None: + """Convert a Filter object to JSON before storing.""" + self._filter_data = asdict(value) if isinstance(value, Filter) else None + + # Single definition of `filter` + filter = property(_get_filter, _set_filter) def __repr__(self) -> str: - return f"" + return f"" class SettingsTable(Model): diff --git a/app/models.py b/app/models.py index 535bf53..eed3241 100644 --- a/app/models.py +++ b/app/models.py @@ -608,6 +608,25 @@ class PlaylistRows(dbtables.PlaylistRowsTable): session.connection().execute(stmt, sqla_map) +class Queries(dbtables.QueriesTable): + def __init__( + self, session: Session, name: str, filter: dbtables.Filter, favourite: bool = False + ) -> None: + """Create new query""" + + self.name = name + self.filter = filter + self.favourite = favourite + session.add(self) + session.commit() + + @classmethod + def get_all_queries(cls, session: Session) -> Sequence["Queries"]: + """Returns a list of all queries ordered by name""" + + return session.scalars(select(cls).order_by(cls.name)).all() + + class Settings(dbtables.SettingsTable): def __init__(self, session: Session, name: str) -> None: self.name = name diff --git a/migrations/versions/4fc2a9a82ab0_create_queries_table.py b/migrations/versions/4fc2a9a82ab0_create_queries_table.py new file mode 100644 index 0000000..dc71142 --- /dev/null +++ b/migrations/versions/4fc2a9a82ab0_create_queries_table.py @@ -0,0 +1,47 @@ +"""create queries table + +Revision ID: 4fc2a9a82ab0 +Revises: ab475332d873 +Create Date: 2025-02-26 13:13:25.118489 + +""" +from alembic import op +import sqlalchemy as sa +import dbtables + + +# revision identifiers, used by Alembic. +revision = '4fc2a9a82ab0' +down_revision = 'ab475332d873' +branch_labels = None +depends_on = None + + +def upgrade(engine_name: str) -> None: + globals()["upgrade_%s" % engine_name]() + + +def downgrade(engine_name: str) -> None: + globals()["downgrade_%s" % engine_name]() + + + + + +def upgrade_() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('queries', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('name', sa.String(length=128), nullable=False), + sa.Column('filter_data', dbtables.JSONEncodedDict(), nullable=True), + sa.Column('favourite', sa.Boolean(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade_() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('queries') + # ### end Alembic commands ### +