4 Commits

Author SHA1 Message Date
2dc96e06aa refactor(db)!: replace version column using Version 2025-08-04 01:07:16 +08:00
308b087b94 feat(db): new model VersionDate 2025-08-04 01:04:57 +08:00
a8164f37e2 feat: add Version 2025-08-04 00:58:11 +08:00
ab03b27730 refactor(db)!: ChartInfo.notes can be NULL 2025-07-19 00:49:40 +08:00
10 changed files with 117 additions and 6 deletions

View File

@ -0,0 +1,31 @@
"""version date
Revision ID: f82ac2f445a5
Revises: 0ca6733e40dc
Create Date: 2025-07-19 17:11:27.448574
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
revision: str = "f82ac2f445a5"
down_revision: Union[str, None] = "0ca6733e40dc"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.create_table(
"version_date",
sa.Column("version", sa.String(), nullable=False),
sa.Column("songlist_at", sa.DateTime(), nullable=False),
sa.Column("published_at", sa.DateTime(), nullable=False),
sa.PrimaryKeyConstraint("version", name=op.f("pk_version_date")),
)
def downgrade() -> None:
op.drop_table("version_date")

View File

@ -4,6 +4,7 @@ from .config import Property
from .difficulty import Difficulty, DifficultyLocalization from .difficulty import Difficulty, DifficultyLocalization
from .pack import Pack, PackLocalization from .pack import Pack, PackLocalization
from .song import Song, SongLocalization from .song import Song, SongLocalization
from .version_date import VersionDate
from .chart import Chart # isort: skip from .chart import Chart # isort: skip
from .play_result import ( from .play_result import (
@ -29,4 +30,5 @@ __all__ = [
"Property", "Property",
"Song", "Song",
"SongLocalization", "SongLocalization",
"VersionDate",
] ]

View File

@ -4,10 +4,13 @@ from sqlalchemy import MetaData
from sqlalchemy.orm import DeclarativeBase from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm.exc import DetachedInstanceError from sqlalchemy.orm.exc import DetachedInstanceError
from ._types import ForceTimezoneDateTime from arcaea_offline.utils import Version
from ._types import ForceTimezoneDateTime, VersionDatabaseType
TYPE_ANNOTATION_MAP = { TYPE_ANNOTATION_MAP = {
datetime: ForceTimezoneDateTime, datetime: ForceTimezoneDateTime,
Version: VersionDatabaseType,
} }

View File

@ -1,9 +1,11 @@
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Optional from typing import Optional
from sqlalchemy import DateTime from sqlalchemy import DateTime, String
from sqlalchemy.types import TypeDecorator from sqlalchemy.types import TypeDecorator
from arcaea_offline.utils import Version
class ForceTimezoneDateTime(TypeDecorator): class ForceTimezoneDateTime(TypeDecorator):
""" """
@ -26,3 +28,23 @@ class ForceTimezoneDateTime(TypeDecorator):
if value is not None: if value is not None:
value = value.replace(tzinfo=timezone.utc) value = value.replace(tzinfo=timezone.utc)
return value return value
class VersionDatabaseType(TypeDecorator):
impl = String
cache_ok = True
def process_bind_param(self, value: Optional[Version], dialect):
if value is None:
return None
if not isinstance(value, Version):
raise ValueError("Input is not a Version instance.")
return str(f"{value.first}.{value.second}.{value.third}")
def process_result_value(self, value: Optional[str], dialect):
if value is None:
return None
return Version(*(map(int, value.split("."))))

View File

@ -5,6 +5,8 @@ from typing import TYPE_CHECKING, Optional
from sqlalchemy import ForeignKeyConstraint, Integer, Numeric, String from sqlalchemy import ForeignKeyConstraint, Integer, Numeric, String
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
from arcaea_offline.utils import Version
from ._base import ModelBase, ReprHelper from ._base import ModelBase, ReprHelper
from ._types import ForceTimezoneDateTime from ._types import ForceTimezoneDateTime
@ -28,6 +30,6 @@ class ChartInfo(ModelBase, ReprHelper):
song_id: Mapped[str] = mapped_column(String, primary_key=True) song_id: Mapped[str] = mapped_column(String, primary_key=True)
rating_class: Mapped[int] = mapped_column(Integer, primary_key=True) rating_class: Mapped[int] = mapped_column(Integer, primary_key=True)
constant: Mapped[Decimal] = mapped_column(Numeric, nullable=False) constant: Mapped[Decimal] = mapped_column(Numeric, nullable=False)
notes: Mapped[int] = mapped_column(Integer) notes: Mapped[Optional[int]] = mapped_column(Integer)
added_at: Mapped[datetime] = mapped_column(ForceTimezoneDateTime, primary_key=True) added_at: Mapped[datetime] = mapped_column(ForceTimezoneDateTime, primary_key=True)
version: Mapped[Optional[str]] = mapped_column(String) version: Mapped[Optional[Version]]

View File

@ -13,6 +13,8 @@ from sqlalchemy import (
) )
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
from arcaea_offline.utils import Version
from ._base import ModelBase, ReprHelper from ._base import ModelBase, ReprHelper
from ._types import ForceTimezoneDateTime from ._types import ForceTimezoneDateTime
@ -65,7 +67,7 @@ class Difficulty(ModelBase, ReprHelper):
bpm: Mapped[Optional[str]] = mapped_column(String) bpm: Mapped[Optional[str]] = mapped_column(String)
bpm_base: Mapped[Optional[Decimal]] = mapped_column(Numeric(asdecimal=True)) bpm_base: Mapped[Optional[Decimal]] = mapped_column(Numeric(asdecimal=True))
added_at: Mapped[Optional[datetime]] = mapped_column(ForceTimezoneDateTime) added_at: Mapped[Optional[datetime]] = mapped_column(ForceTimezoneDateTime)
version: Mapped[Optional[str]] = mapped_column(String) version: Mapped[Optional[Version]]
is_legacy11: Mapped[bool] = mapped_column( is_legacy11: Mapped[bool] = mapped_column(
Boolean, nullable=False, insert_default=False, server_default=text("0") Boolean, nullable=False, insert_default=False, server_default=text("0")
) )

View File

@ -5,6 +5,8 @@ from typing import TYPE_CHECKING, Optional
from sqlalchemy import Boolean, ForeignKey, Integer, Numeric, String, text from sqlalchemy import Boolean, ForeignKey, Integer, Numeric, String, text
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
from arcaea_offline.utils import Version
from ._base import ModelBase, ReprHelper from ._base import ModelBase, ReprHelper
from ._types import ForceTimezoneDateTime from ._types import ForceTimezoneDateTime
@ -42,7 +44,7 @@ class Song(ModelBase, ReprHelper):
added_at: Mapped[datetime] = mapped_column( added_at: Mapped[datetime] = mapped_column(
ForceTimezoneDateTime, nullable=False, index=True ForceTimezoneDateTime, nullable=False, index=True
) )
version: Mapped[Optional[str]] = mapped_column(String) version: Mapped[Optional[Version]]
bpm: Mapped[Optional[str]] = mapped_column(String) bpm: Mapped[Optional[str]] = mapped_column(String)
bpm_base: Mapped[Optional[Decimal]] = mapped_column(Numeric(asdecimal=True)) bpm_base: Mapped[Optional[Decimal]] = mapped_column(Numeric(asdecimal=True))

View File

@ -0,0 +1,15 @@
from datetime import datetime
from sqlalchemy.orm import Mapped, mapped_column
from arcaea_offline.utils import Version
from ._base import ModelBase, ReprHelper
class VersionDate(ModelBase, ReprHelper):
__tablename__ = "version_date"
version: Mapped[Version] = mapped_column(primary_key=True)
songlist_at: Mapped[datetime]
published_at: Mapped[datetime]

View File

@ -0,0 +1,5 @@
from .version import Version
__all__ = [
"Version",
]

View File

@ -0,0 +1,27 @@
from typing import NamedTuple
class Version(NamedTuple):
first: int
second: int
third: int
@classmethod
def from_string(cls, version_str: str):
version_str = version_str.removesuffix("c")
parts = version_str.split(".")
if len(parts) not in {2, 3}:
raise ValueError(f"Invalid version string {version_str}")
try:
if len(parts) == 2: # noqa: PLR2004
parts.append("0")
first, second, third = map(int, parts)
except ValueError as e:
raise ValueError(f"Invalid version string {version_str}") from e
return cls(first, second, third)
def __str__(self):
return f"{self.first}.{self.second}.{self.third}"