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 .pack import Pack, PackLocalization
from .song import Song, SongLocalization
from .version_date import VersionDate
from .chart import Chart # isort: skip
from .play_result import (
@ -29,4 +30,5 @@ __all__ = [
"Property",
"Song",
"SongLocalization",
"VersionDate",
]

View File

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

View File

@ -1,9 +1,11 @@
from datetime import datetime, timezone
from typing import Optional
from sqlalchemy import DateTime
from sqlalchemy import DateTime, String
from sqlalchemy.types import TypeDecorator
from arcaea_offline.utils import Version
class ForceTimezoneDateTime(TypeDecorator):
"""
@ -26,3 +28,23 @@ class ForceTimezoneDateTime(TypeDecorator):
if value is not None:
value = value.replace(tzinfo=timezone.utc)
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.orm import Mapped, mapped_column, relationship
from arcaea_offline.utils import Version
from ._base import ModelBase, ReprHelper
from ._types import ForceTimezoneDateTime
@ -28,6 +30,6 @@ class ChartInfo(ModelBase, ReprHelper):
song_id: Mapped[str] = mapped_column(String, primary_key=True)
rating_class: Mapped[int] = mapped_column(Integer, primary_key=True)
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)
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 arcaea_offline.utils import Version
from ._base import ModelBase, ReprHelper
from ._types import ForceTimezoneDateTime
@ -65,7 +67,7 @@ class Difficulty(ModelBase, ReprHelper):
bpm: Mapped[Optional[str]] = mapped_column(String)
bpm_base: Mapped[Optional[Decimal]] = mapped_column(Numeric(asdecimal=True))
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(
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.orm import Mapped, mapped_column, relationship
from arcaea_offline.utils import Version
from ._base import ModelBase, ReprHelper
from ._types import ForceTimezoneDateTime
@ -42,7 +44,7 @@ class Song(ModelBase, ReprHelper):
added_at: Mapped[datetime] = mapped_column(
ForceTimezoneDateTime, nullable=False, index=True
)
version: Mapped[Optional[str]] = mapped_column(String)
version: Mapped[Optional[Version]]
bpm: Mapped[Optional[str]] = mapped_column(String)
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}"