mirror of
https://github.com/283375/arcaea-offline.git
synced 2025-04-19 06:00:18 +00:00
wip: v5 database models and tests
This commit is contained in:
parent
43be27bd4a
commit
88201e2ca4
38
src/arcaea_offline/database/models/v5/__init__.py
Normal file
38
src/arcaea_offline/database/models/v5/__init__.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
from .arcaea import (
|
||||||
|
Chart,
|
||||||
|
ChartInfo,
|
||||||
|
Difficulty,
|
||||||
|
DifficultyLocalized,
|
||||||
|
Pack,
|
||||||
|
PackLocalized,
|
||||||
|
Song,
|
||||||
|
SongLocalized,
|
||||||
|
SongSearchWord,
|
||||||
|
)
|
||||||
|
from .base import ModelsV5Base, ModelsV5ViewBase
|
||||||
|
from .config import Property
|
||||||
|
from .play_results import (
|
||||||
|
CalculatedPotential,
|
||||||
|
PlayResult,
|
||||||
|
PlayResultBest,
|
||||||
|
PlayResultCalculated,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"CalculatedPotential",
|
||||||
|
"Chart",
|
||||||
|
"ChartInfo",
|
||||||
|
"Difficulty",
|
||||||
|
"DifficultyLocalized",
|
||||||
|
"ModelsV5Base",
|
||||||
|
"ModelsV5ViewBase",
|
||||||
|
"Pack",
|
||||||
|
"PackLocalized",
|
||||||
|
"PlayResult",
|
||||||
|
"PlayResultBest",
|
||||||
|
"PlayResultCalculated",
|
||||||
|
"Property",
|
||||||
|
"Song",
|
||||||
|
"SongLocalized",
|
||||||
|
"SongSearchWord",
|
||||||
|
]
|
275
src/arcaea_offline/database/models/v5/arcaea.py
Normal file
275
src/arcaea_offline/database/models/v5/arcaea.py
Normal file
@ -0,0 +1,275 @@
|
|||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from sqlalchemy import ForeignKey, and_, func, select
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
from sqlalchemy_utils import create_view
|
||||||
|
|
||||||
|
from arcaea_offline.constants.enums import ArcaeaRatingClass, ArcaeaSongSide
|
||||||
|
|
||||||
|
from .base import ModelsV5Base, ModelsV5ViewBase, ReprHelper
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"Chart",
|
||||||
|
"ChartInfo",
|
||||||
|
"Difficulty",
|
||||||
|
"DifficultyLocalized",
|
||||||
|
"Pack",
|
||||||
|
"PackLocalized",
|
||||||
|
"Song",
|
||||||
|
"SongLocalized",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class Pack(ModelsV5Base, ReprHelper):
|
||||||
|
__tablename__ = "packs"
|
||||||
|
|
||||||
|
id: Mapped[str] = mapped_column(primary_key=True)
|
||||||
|
name: Mapped[str]
|
||||||
|
description: Mapped[Optional[str]]
|
||||||
|
|
||||||
|
songs: Mapped[List["Song"]] = relationship(back_populates="pack", viewonly=True)
|
||||||
|
localized_objects: Mapped[List["PackLocalized"]] = relationship(
|
||||||
|
back_populates="parent", viewonly=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PackLocalized(ModelsV5Base, ReprHelper):
|
||||||
|
__tablename__ = "packs_localized"
|
||||||
|
|
||||||
|
pkid: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
id: Mapped[str] = mapped_column(
|
||||||
|
ForeignKey(Pack.id, onupdate="CASCADE", ondelete="NO ACTION")
|
||||||
|
)
|
||||||
|
lang: Mapped[str]
|
||||||
|
name: Mapped[Optional[str]]
|
||||||
|
description: Mapped[Optional[str]]
|
||||||
|
|
||||||
|
parent: Mapped[Pack] = relationship(viewonly=True)
|
||||||
|
|
||||||
|
|
||||||
|
class Song(ModelsV5Base, ReprHelper):
|
||||||
|
__tablename__ = "songs"
|
||||||
|
|
||||||
|
idx: Mapped[int]
|
||||||
|
id: Mapped[str] = mapped_column(primary_key=True)
|
||||||
|
title: Mapped[str]
|
||||||
|
artist: Mapped[str]
|
||||||
|
pack_id: Mapped[str] = mapped_column(
|
||||||
|
ForeignKey(Pack.id, onupdate="CASCADE", ondelete="NO ACTION")
|
||||||
|
)
|
||||||
|
bpm: Mapped[Optional[str]]
|
||||||
|
bpm_base: Mapped[Optional[float]]
|
||||||
|
audio_preview: Mapped[Optional[int]]
|
||||||
|
audio_preview_end: Mapped[Optional[int]]
|
||||||
|
side: Mapped[Optional[ArcaeaSongSide]]
|
||||||
|
version: Mapped[Optional[str]]
|
||||||
|
date: Mapped[Optional[int]]
|
||||||
|
bg: Mapped[Optional[str]]
|
||||||
|
bg_inverse: Mapped[Optional[str]]
|
||||||
|
bg_day: Mapped[Optional[str]]
|
||||||
|
bg_night: Mapped[Optional[str]]
|
||||||
|
source: Mapped[Optional[str]]
|
||||||
|
source_copyright: Mapped[Optional[str]]
|
||||||
|
|
||||||
|
pack: Mapped[Pack] = relationship(viewonly=True)
|
||||||
|
difficulties: Mapped[List["Difficulty"]] = relationship(
|
||||||
|
back_populates="song", viewonly=True
|
||||||
|
)
|
||||||
|
localized_objects: Mapped[List["SongLocalized"]] = relationship(
|
||||||
|
back_populates="parent", viewonly=True
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def charts_info(self):
|
||||||
|
return [d.chart_info for d in self.difficulties if d.chart_info is not None]
|
||||||
|
|
||||||
|
|
||||||
|
class SongLocalized(ModelsV5Base, ReprHelper):
|
||||||
|
__tablename__ = "songs_localized"
|
||||||
|
|
||||||
|
pkid: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
id: Mapped[str] = mapped_column(
|
||||||
|
ForeignKey(Song.id, onupdate="CASCADE", ondelete="NO ACTION")
|
||||||
|
)
|
||||||
|
lang: Mapped[str]
|
||||||
|
title: Mapped[Optional[str]]
|
||||||
|
source: Mapped[Optional[str]]
|
||||||
|
|
||||||
|
parent: Mapped[Song] = relationship(
|
||||||
|
back_populates="localized_objects", viewonly=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SongSearchWord(ModelsV5Base, ReprHelper):
|
||||||
|
__tablename__ = "songs_search_words"
|
||||||
|
|
||||||
|
pkid: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
id: Mapped[str] = mapped_column(
|
||||||
|
ForeignKey(Song.id, onupdate="CASCADE", ondelete="NO ACTION")
|
||||||
|
)
|
||||||
|
lang: Mapped[str]
|
||||||
|
type: Mapped[int] = mapped_column(comment="1: title, 2: artist")
|
||||||
|
value: Mapped[str]
|
||||||
|
|
||||||
|
|
||||||
|
class Difficulty(ModelsV5Base, ReprHelper):
|
||||||
|
__tablename__ = "difficulties"
|
||||||
|
|
||||||
|
song_id: Mapped[str] = mapped_column(
|
||||||
|
ForeignKey(Song.id, onupdate="CASCADE", ondelete="NO ACTION"),
|
||||||
|
primary_key=True,
|
||||||
|
)
|
||||||
|
rating_class: Mapped[ArcaeaRatingClass] = mapped_column(primary_key=True)
|
||||||
|
rating: Mapped[int]
|
||||||
|
rating_plus: Mapped[bool]
|
||||||
|
chart_designer: Mapped[Optional[str]]
|
||||||
|
jacket_desginer: Mapped[Optional[str]]
|
||||||
|
audio_override: Mapped[bool] = mapped_column(default=False)
|
||||||
|
jacket_override: Mapped[bool] = mapped_column(default=False)
|
||||||
|
jacket_night: Mapped[Optional[str]]
|
||||||
|
title: Mapped[Optional[str]]
|
||||||
|
artist: Mapped[Optional[str]]
|
||||||
|
bg: Mapped[Optional[str]]
|
||||||
|
bg_inverse: Mapped[Optional[str]]
|
||||||
|
bpm: Mapped[Optional[str]]
|
||||||
|
bpm_base: Mapped[Optional[float]]
|
||||||
|
version: Mapped[Optional[str]]
|
||||||
|
date: Mapped[Optional[int]]
|
||||||
|
|
||||||
|
song: Mapped[Song] = relationship(back_populates="difficulties", viewonly=True)
|
||||||
|
chart_info: Mapped[Optional["ChartInfo"]] = relationship(
|
||||||
|
primaryjoin=(
|
||||||
|
"and_(Difficulty.song_id==ChartInfo.song_id, "
|
||||||
|
"Difficulty.rating_class==ChartInfo.rating_class)"
|
||||||
|
),
|
||||||
|
viewonly=True,
|
||||||
|
)
|
||||||
|
localized_objects: Mapped[List["DifficultyLocalized"]] = relationship(
|
||||||
|
primaryjoin=(
|
||||||
|
"and_(Difficulty.song_id==DifficultyLocalized.song_id, "
|
||||||
|
"Difficulty.rating_class==DifficultyLocalized.rating_class)"
|
||||||
|
),
|
||||||
|
viewonly=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DifficultyLocalized(ModelsV5Base, ReprHelper):
|
||||||
|
__tablename__ = "difficulties_localized"
|
||||||
|
|
||||||
|
pkid: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
song_id: Mapped[str] = mapped_column(
|
||||||
|
ForeignKey(Difficulty.song_id, onupdate="CASCADE", ondelete="NO ACTION")
|
||||||
|
)
|
||||||
|
rating_class: Mapped[ArcaeaRatingClass] = mapped_column(
|
||||||
|
ForeignKey(Difficulty.rating_class, onupdate="CASCADE", ondelete="NO ACTION")
|
||||||
|
)
|
||||||
|
lang: Mapped[str]
|
||||||
|
title: Mapped[Optional[str]]
|
||||||
|
artist: Mapped[Optional[str]]
|
||||||
|
|
||||||
|
parent: Mapped[Difficulty] = relationship(
|
||||||
|
primaryjoin=and_(
|
||||||
|
Difficulty.song_id == song_id, Difficulty.rating_class == rating_class
|
||||||
|
),
|
||||||
|
viewonly=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ChartInfo(ModelsV5Base, ReprHelper):
|
||||||
|
__tablename__ = "charts_info"
|
||||||
|
|
||||||
|
song_id: Mapped[str] = mapped_column(
|
||||||
|
ForeignKey(Difficulty.song_id, onupdate="CASCADE", ondelete="NO ACTION"),
|
||||||
|
primary_key=True,
|
||||||
|
)
|
||||||
|
rating_class: Mapped[ArcaeaRatingClass] = mapped_column(
|
||||||
|
ForeignKey(Difficulty.rating_class, onupdate="CASCADE", ondelete="NO ACTION"),
|
||||||
|
primary_key=True,
|
||||||
|
)
|
||||||
|
constant: Mapped[int] = mapped_column(
|
||||||
|
comment="real_constant * 10. For example, Crimson Throne [FTR] is 10.4, then store 104."
|
||||||
|
)
|
||||||
|
notes: Mapped[Optional[int]]
|
||||||
|
|
||||||
|
difficulty: Mapped[Difficulty] = relationship(
|
||||||
|
primaryjoin=and_(
|
||||||
|
Difficulty.song_id == song_id, Difficulty.rating_class == rating_class
|
||||||
|
),
|
||||||
|
viewonly=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Chart(ModelsV5ViewBase, ReprHelper):
|
||||||
|
__tablename__ = "charts"
|
||||||
|
|
||||||
|
song_idx: Mapped[int]
|
||||||
|
song_id: Mapped[str]
|
||||||
|
rating_class: Mapped[ArcaeaRatingClass]
|
||||||
|
rating: Mapped[int]
|
||||||
|
rating_plus: Mapped[bool]
|
||||||
|
title: Mapped[str]
|
||||||
|
artist: Mapped[str]
|
||||||
|
pack_id: Mapped[str]
|
||||||
|
bpm: Mapped[Optional[str]]
|
||||||
|
bpm_base: Mapped[Optional[float]]
|
||||||
|
audio_preview: Mapped[Optional[int]]
|
||||||
|
audio_preview_end: Mapped[Optional[int]]
|
||||||
|
side: Mapped[Optional[int]]
|
||||||
|
version: Mapped[Optional[str]]
|
||||||
|
date: Mapped[Optional[int]]
|
||||||
|
bg: Mapped[Optional[str]]
|
||||||
|
bg_inverse: Mapped[Optional[str]]
|
||||||
|
bg_day: Mapped[Optional[str]]
|
||||||
|
bg_night: Mapped[Optional[str]]
|
||||||
|
source: Mapped[Optional[str]]
|
||||||
|
source_copyright: Mapped[Optional[str]]
|
||||||
|
chart_designer: Mapped[Optional[str]]
|
||||||
|
jacket_desginer: Mapped[Optional[str]]
|
||||||
|
audio_override: Mapped[bool]
|
||||||
|
jacket_override: Mapped[bool]
|
||||||
|
jacket_night: Mapped[Optional[str]]
|
||||||
|
constant: Mapped[int]
|
||||||
|
notes: Mapped[Optional[int]]
|
||||||
|
|
||||||
|
__table__ = create_view(
|
||||||
|
name=__tablename__,
|
||||||
|
selectable=select(
|
||||||
|
Song.idx.label("song_idx"),
|
||||||
|
Difficulty.song_id,
|
||||||
|
Difficulty.rating_class,
|
||||||
|
Difficulty.rating,
|
||||||
|
Difficulty.rating_plus,
|
||||||
|
func.coalesce(Difficulty.title, Song.title).label("title"),
|
||||||
|
func.coalesce(Difficulty.artist, Song.artist).label("artist"),
|
||||||
|
Song.pack_id,
|
||||||
|
func.coalesce(Difficulty.bpm, Song.bpm).label("bpm"),
|
||||||
|
func.coalesce(Difficulty.bpm_base, Song.bpm_base).label("bpm_base"),
|
||||||
|
Song.audio_preview,
|
||||||
|
Song.audio_preview_end,
|
||||||
|
Song.side,
|
||||||
|
func.coalesce(Difficulty.version, Song.version).label("version"),
|
||||||
|
func.coalesce(Difficulty.date, Song.date).label("date"),
|
||||||
|
func.coalesce(Difficulty.bg, Song.bg).label("bg"),
|
||||||
|
func.coalesce(Difficulty.bg_inverse, Song.bg_inverse).label("bg_inverse"),
|
||||||
|
Song.bg_day,
|
||||||
|
Song.bg_night,
|
||||||
|
Song.source,
|
||||||
|
Song.source_copyright,
|
||||||
|
Difficulty.chart_designer,
|
||||||
|
Difficulty.jacket_desginer,
|
||||||
|
Difficulty.audio_override,
|
||||||
|
Difficulty.jacket_override,
|
||||||
|
Difficulty.jacket_night,
|
||||||
|
ChartInfo.constant,
|
||||||
|
ChartInfo.notes,
|
||||||
|
)
|
||||||
|
.select_from(Difficulty)
|
||||||
|
.join(
|
||||||
|
ChartInfo,
|
||||||
|
(Difficulty.song_id == ChartInfo.song_id)
|
||||||
|
& (Difficulty.rating_class == ChartInfo.rating_class),
|
||||||
|
)
|
||||||
|
.join(Song, Difficulty.song_id == Song.id),
|
||||||
|
metadata=ModelsV5ViewBase.metadata,
|
||||||
|
cascade_on_drop=False,
|
||||||
|
)
|
62
src/arcaea_offline/database/models/v5/base.py
Normal file
62
src/arcaea_offline/database/models/v5/base.py
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from sqlalchemy.orm import DeclarativeBase
|
||||||
|
from sqlalchemy.orm.exc import DetachedInstanceError
|
||||||
|
|
||||||
|
from arcaea_offline.constants.enums import (
|
||||||
|
ArcaeaPlayResultClearType,
|
||||||
|
ArcaeaPlayResultModifier,
|
||||||
|
ArcaeaRatingClass,
|
||||||
|
ArcaeaSongSide,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .._custom_types import DbIntEnum, TZDateTime
|
||||||
|
|
||||||
|
TYPE_ANNOTATION_MAP = {
|
||||||
|
datetime: TZDateTime,
|
||||||
|
ArcaeaRatingClass: DbIntEnum(ArcaeaRatingClass),
|
||||||
|
ArcaeaSongSide: DbIntEnum(ArcaeaSongSide),
|
||||||
|
ArcaeaPlayResultClearType: DbIntEnum(ArcaeaPlayResultClearType),
|
||||||
|
ArcaeaPlayResultModifier: DbIntEnum(ArcaeaPlayResultModifier),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ModelsV5Base(DeclarativeBase):
|
||||||
|
type_annotation_map = TYPE_ANNOTATION_MAP
|
||||||
|
|
||||||
|
|
||||||
|
class ModelsV5ViewBase(DeclarativeBase):
|
||||||
|
type_annotation_map = TYPE_ANNOTATION_MAP
|
||||||
|
|
||||||
|
|
||||||
|
class ReprHelper:
|
||||||
|
# pylint: disable=no-member
|
||||||
|
|
||||||
|
def _repr(self, **kwargs) -> str:
|
||||||
|
"""
|
||||||
|
Helper for __repr__
|
||||||
|
|
||||||
|
https://stackoverflow.com/a/55749579/16484891
|
||||||
|
|
||||||
|
CC BY-SA 4.0
|
||||||
|
"""
|
||||||
|
field_strings = []
|
||||||
|
at_least_one_attached_attribute = False
|
||||||
|
for key, field in kwargs.items():
|
||||||
|
try:
|
||||||
|
field_strings.append(f"{key}={field!r}")
|
||||||
|
except DetachedInstanceError:
|
||||||
|
field_strings.append(f"{key}=DetachedInstanceError")
|
||||||
|
else:
|
||||||
|
at_least_one_attached_attribute = True
|
||||||
|
|
||||||
|
if at_least_one_attached_attribute:
|
||||||
|
return f"<{self.__class__.__name__}({', '.join(field_strings)})>"
|
||||||
|
return f"<{self.__class__.__name__} {id(self)}>"
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
if isinstance(self, DeclarativeBase):
|
||||||
|
return self._repr(
|
||||||
|
**{c.key: getattr(self, c.key) for c in self.__table__.columns}
|
||||||
|
)
|
||||||
|
return super().__repr__()
|
12
src/arcaea_offline/database/models/v5/config.py
Normal file
12
src/arcaea_offline/database/models/v5/config.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from .base import ModelsV5Base, ReprHelper
|
||||||
|
|
||||||
|
__all__ = ["Property"]
|
||||||
|
|
||||||
|
|
||||||
|
class Property(ModelsV5Base, ReprHelper):
|
||||||
|
__tablename__ = "properties"
|
||||||
|
|
||||||
|
key: Mapped[str] = mapped_column(primary_key=True)
|
||||||
|
value: Mapped[str] = mapped_column()
|
197
src/arcaea_offline/database/models/v5/play_results.py
Normal file
197
src/arcaea_offline/database/models/v5/play_results.py
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
from datetime import datetime, timezone
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from sqlalchemy import ForeignKey, and_, case, func, inspect, select, text
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
from sqlalchemy_utils import create_view
|
||||||
|
|
||||||
|
from arcaea_offline.constants.enums import (
|
||||||
|
ArcaeaPlayResultClearType,
|
||||||
|
ArcaeaPlayResultModifier,
|
||||||
|
ArcaeaRatingClass,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .arcaea import ChartInfo, Difficulty
|
||||||
|
from .base import ModelsV5Base, ModelsV5ViewBase, ReprHelper
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"CalculatedPotential",
|
||||||
|
"PlayResult",
|
||||||
|
"PlayResultBest",
|
||||||
|
"PlayResultCalculated",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class PlayResult(ModelsV5Base, ReprHelper):
|
||||||
|
__tablename__ = "play_results"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(autoincrement=True, primary_key=True)
|
||||||
|
song_id: Mapped[str] = mapped_column(
|
||||||
|
ForeignKey(Difficulty.song_id, onupdate="CASCADE", ondelete="NO ACTION"),
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
rating_class: Mapped[ArcaeaRatingClass] = mapped_column(
|
||||||
|
ForeignKey(Difficulty.rating_class, onupdate="CASCADE", ondelete="NO ACTION"),
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
score: Mapped[int]
|
||||||
|
pure: Mapped[Optional[int]]
|
||||||
|
far: Mapped[Optional[int]]
|
||||||
|
lost: Mapped[Optional[int]]
|
||||||
|
date: Mapped[Optional[datetime]] = mapped_column(
|
||||||
|
default=lambda: datetime.now(timezone.utc)
|
||||||
|
)
|
||||||
|
max_recall: Mapped[Optional[int]]
|
||||||
|
modifier: Mapped[Optional[ArcaeaPlayResultModifier]]
|
||||||
|
clear_type: Mapped[Optional[ArcaeaPlayResultClearType]]
|
||||||
|
comment: Mapped[Optional[str]]
|
||||||
|
|
||||||
|
difficulty: Mapped[Difficulty] = relationship(
|
||||||
|
primaryjoin=and_(
|
||||||
|
song_id == Difficulty.song_id,
|
||||||
|
rating_class == Difficulty.rating_class,
|
||||||
|
),
|
||||||
|
viewonly=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# How to create an SQL View with SQLAlchemy?
|
||||||
|
# https://stackoverflow.com/a/53253105/16484891
|
||||||
|
# CC BY-SA 4.0
|
||||||
|
|
||||||
|
|
||||||
|
class PlayResultCalculated(ModelsV5ViewBase, ReprHelper):
|
||||||
|
__tablename__ = "play_results_calculated"
|
||||||
|
|
||||||
|
id: Mapped[int]
|
||||||
|
song_id: Mapped[str]
|
||||||
|
rating_class: Mapped[ArcaeaRatingClass]
|
||||||
|
score: Mapped[int]
|
||||||
|
pure: Mapped[Optional[int]]
|
||||||
|
shiny_pure: Mapped[Optional[int]]
|
||||||
|
far: Mapped[Optional[int]]
|
||||||
|
lost: Mapped[Optional[int]]
|
||||||
|
date: Mapped[Optional[datetime]]
|
||||||
|
max_recall: Mapped[Optional[int]]
|
||||||
|
modifier: Mapped[Optional[ArcaeaPlayResultModifier]]
|
||||||
|
clear_type: Mapped[Optional[ArcaeaPlayResultClearType]]
|
||||||
|
potential: Mapped[float]
|
||||||
|
comment: Mapped[Optional[str]]
|
||||||
|
|
||||||
|
__table__ = create_view(
|
||||||
|
name=__tablename__,
|
||||||
|
selectable=select(
|
||||||
|
PlayResult.id,
|
||||||
|
Difficulty.song_id,
|
||||||
|
Difficulty.rating_class,
|
||||||
|
PlayResult.score,
|
||||||
|
PlayResult.pure,
|
||||||
|
(
|
||||||
|
case(
|
||||||
|
(
|
||||||
|
(
|
||||||
|
ChartInfo.notes.is_not(None)
|
||||||
|
& PlayResult.pure.is_not(None)
|
||||||
|
& PlayResult.far.is_not(None)
|
||||||
|
& (ChartInfo.notes != 0)
|
||||||
|
),
|
||||||
|
PlayResult.score
|
||||||
|
- func.floor(
|
||||||
|
(PlayResult.pure * 10000000.0 / ChartInfo.notes)
|
||||||
|
+ (PlayResult.far * 0.5 * 10000000.0 / ChartInfo.notes)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
else_=text("NULL"),
|
||||||
|
)
|
||||||
|
).label("shiny_pure"),
|
||||||
|
PlayResult.far,
|
||||||
|
PlayResult.lost,
|
||||||
|
PlayResult.date,
|
||||||
|
PlayResult.max_recall,
|
||||||
|
PlayResult.modifier,
|
||||||
|
PlayResult.clear_type,
|
||||||
|
case(
|
||||||
|
(PlayResult.score >= 10000000, ChartInfo.constant / 10.0 + 2), # noqa: PLR2004
|
||||||
|
(
|
||||||
|
PlayResult.score >= 9800000, # noqa: PLR2004
|
||||||
|
ChartInfo.constant / 10.0
|
||||||
|
+ 1
|
||||||
|
+ (PlayResult.score - 9800000) / 200000.0,
|
||||||
|
),
|
||||||
|
else_=func.max(
|
||||||
|
(ChartInfo.constant / 10.0)
|
||||||
|
+ (PlayResult.score - 9500000) / 300000.0,
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
).label("potential"),
|
||||||
|
PlayResult.comment,
|
||||||
|
)
|
||||||
|
.select_from(Difficulty)
|
||||||
|
.join(
|
||||||
|
ChartInfo,
|
||||||
|
(Difficulty.song_id == ChartInfo.song_id)
|
||||||
|
& (Difficulty.rating_class == ChartInfo.rating_class),
|
||||||
|
)
|
||||||
|
.join(
|
||||||
|
PlayResult,
|
||||||
|
(Difficulty.song_id == PlayResult.song_id)
|
||||||
|
& (Difficulty.rating_class == PlayResult.rating_class),
|
||||||
|
),
|
||||||
|
metadata=ModelsV5ViewBase.metadata,
|
||||||
|
cascade_on_drop=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PlayResultBest(ModelsV5ViewBase, ReprHelper):
|
||||||
|
__tablename__ = "play_results_best"
|
||||||
|
|
||||||
|
id: Mapped[int]
|
||||||
|
song_id: Mapped[str]
|
||||||
|
rating_class: Mapped[int]
|
||||||
|
score: Mapped[int]
|
||||||
|
pure: Mapped[Optional[int]]
|
||||||
|
shiny_pure: Mapped[Optional[int]]
|
||||||
|
far: Mapped[Optional[int]]
|
||||||
|
lost: Mapped[Optional[int]]
|
||||||
|
date: Mapped[Optional[datetime]]
|
||||||
|
max_recall: Mapped[Optional[int]]
|
||||||
|
modifier: Mapped[Optional[ArcaeaPlayResultModifier]]
|
||||||
|
clear_type: Mapped[Optional[ArcaeaPlayResultClearType]]
|
||||||
|
potential: Mapped[float]
|
||||||
|
comment: Mapped[Optional[str]]
|
||||||
|
|
||||||
|
__table__ = create_view(
|
||||||
|
name=__tablename__,
|
||||||
|
selectable=select(
|
||||||
|
*[
|
||||||
|
col
|
||||||
|
for col in inspect(PlayResultCalculated).columns
|
||||||
|
if col.name != "potential"
|
||||||
|
],
|
||||||
|
func.max(PlayResultCalculated.potential).label("potential"),
|
||||||
|
)
|
||||||
|
.select_from(PlayResultCalculated)
|
||||||
|
.group_by(PlayResultCalculated.song_id, PlayResultCalculated.rating_class)
|
||||||
|
.order_by(PlayResultCalculated.potential.desc()),
|
||||||
|
metadata=ModelsV5ViewBase.metadata,
|
||||||
|
cascade_on_drop=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CalculatedPotential(ModelsV5ViewBase, ReprHelper):
|
||||||
|
__tablename__ = "calculated_potential"
|
||||||
|
|
||||||
|
b30: Mapped[float]
|
||||||
|
|
||||||
|
_select_bests_subquery = (
|
||||||
|
select(PlayResultBest.potential.label("b30_sum"))
|
||||||
|
.order_by(PlayResultBest.potential.desc())
|
||||||
|
.limit(30)
|
||||||
|
.subquery()
|
||||||
|
)
|
||||||
|
__table__ = create_view(
|
||||||
|
name=__tablename__,
|
||||||
|
selectable=select(func.avg(_select_bests_subquery.c.b30_sum).label("b30")),
|
||||||
|
metadata=ModelsV5ViewBase.metadata,
|
||||||
|
cascade_on_drop=False,
|
||||||
|
)
|
0
tests/db/models/v5/__init__.py
Normal file
0
tests/db/models/v5/__init__.py
Normal file
0
tests/db/models/v5/relationships/__init__.py
Normal file
0
tests/db/models/v5/relationships/__init__.py
Normal file
143
tests/db/models/v5/relationships/test_common.py
Normal file
143
tests/db/models/v5/relationships/test_common.py
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
"""
|
||||||
|
Database model v5 common relationships
|
||||||
|
|
||||||
|
┌──────┐ ┌──────┐ ┌────────────┐ ┌────────────┐
|
||||||
|
│ Pack ◄───► Song ◄───► Difficulty ◄───┤ PlayResult │
|
||||||
|
└──────┘ └──┬───┘ └─────▲──────┘ └────────────┘
|
||||||
|
│ │
|
||||||
|
│ ┌─────▼─────┐
|
||||||
|
└───────► ChartInfo │
|
||||||
|
└───────────┘
|
||||||
|
"""
|
||||||
|
|
||||||
|
from arcaea_offline.constants.enums import ArcaeaRatingClass
|
||||||
|
from arcaea_offline.database.models.v5 import (
|
||||||
|
ChartInfo,
|
||||||
|
Difficulty,
|
||||||
|
ModelsV5Base,
|
||||||
|
Pack,
|
||||||
|
PlayResult,
|
||||||
|
Song,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestSongRelationships:
|
||||||
|
@staticmethod
|
||||||
|
def init_db(session):
|
||||||
|
ModelsV5Base.metadata.create_all(session.bind)
|
||||||
|
|
||||||
|
def test_relationships(self, db_session):
|
||||||
|
self.init_db(db_session)
|
||||||
|
|
||||||
|
song_id = "test_song"
|
||||||
|
title_en = "Test Lorem Ipsum"
|
||||||
|
artist_en = "Test Artist"
|
||||||
|
|
||||||
|
pack = Pack(
|
||||||
|
id="test_pack",
|
||||||
|
name="Test Pack",
|
||||||
|
description="This is a test pack.",
|
||||||
|
)
|
||||||
|
|
||||||
|
song = Song(
|
||||||
|
idx=1,
|
||||||
|
id=song_id,
|
||||||
|
title=title_en,
|
||||||
|
artist=artist_en,
|
||||||
|
pack_id=pack.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
difficulty_pst = Difficulty(
|
||||||
|
song_id=song.id,
|
||||||
|
rating_class=ArcaeaRatingClass.PAST,
|
||||||
|
rating=2,
|
||||||
|
rating_plus=False,
|
||||||
|
)
|
||||||
|
chart_info_pst = ChartInfo(
|
||||||
|
song_id=song.id,
|
||||||
|
rating_class=ArcaeaRatingClass.PAST,
|
||||||
|
constant=20,
|
||||||
|
notes=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
difficulty_prs = Difficulty(
|
||||||
|
song_id=song.id,
|
||||||
|
rating_class=ArcaeaRatingClass.PRESENT,
|
||||||
|
rating=7,
|
||||||
|
rating_plus=True,
|
||||||
|
)
|
||||||
|
chart_info_prs = ChartInfo(
|
||||||
|
song_id=song.id,
|
||||||
|
rating_class=ArcaeaRatingClass.PRESENT,
|
||||||
|
constant=78,
|
||||||
|
notes=780,
|
||||||
|
)
|
||||||
|
|
||||||
|
difficulty_ftr = Difficulty(
|
||||||
|
song_id=song.id,
|
||||||
|
rating_class=ArcaeaRatingClass.FUTURE,
|
||||||
|
rating=10,
|
||||||
|
rating_plus=True,
|
||||||
|
)
|
||||||
|
chart_info_ftr = ChartInfo(
|
||||||
|
song_id=song.id,
|
||||||
|
rating_class=ArcaeaRatingClass.FUTURE,
|
||||||
|
constant=109,
|
||||||
|
notes=1090,
|
||||||
|
)
|
||||||
|
|
||||||
|
difficulty_etr = Difficulty(
|
||||||
|
song_id=song.id,
|
||||||
|
rating_class=ArcaeaRatingClass.ETERNAL,
|
||||||
|
rating=9,
|
||||||
|
rating_plus=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
play_result_ftr = PlayResult(
|
||||||
|
song_id=song.id,
|
||||||
|
rating_class=ArcaeaRatingClass.FUTURE,
|
||||||
|
score=123456,
|
||||||
|
)
|
||||||
|
|
||||||
|
db_session.add_all(
|
||||||
|
[
|
||||||
|
pack,
|
||||||
|
song,
|
||||||
|
difficulty_pst,
|
||||||
|
chart_info_pst,
|
||||||
|
difficulty_prs,
|
||||||
|
chart_info_prs,
|
||||||
|
difficulty_ftr,
|
||||||
|
chart_info_ftr,
|
||||||
|
difficulty_etr,
|
||||||
|
play_result_ftr,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
db_session.commit()
|
||||||
|
|
||||||
|
assert pack.songs == [song]
|
||||||
|
|
||||||
|
assert song.pack == pack
|
||||||
|
assert song.difficulties == [
|
||||||
|
difficulty_pst,
|
||||||
|
difficulty_prs,
|
||||||
|
difficulty_ftr,
|
||||||
|
difficulty_etr,
|
||||||
|
]
|
||||||
|
assert song.charts_info == [chart_info_pst, chart_info_prs, chart_info_ftr]
|
||||||
|
|
||||||
|
assert difficulty_pst.song == song
|
||||||
|
assert difficulty_prs.song == song
|
||||||
|
assert difficulty_ftr.song == song
|
||||||
|
assert difficulty_etr.song == song
|
||||||
|
|
||||||
|
assert difficulty_pst.chart_info == chart_info_pst
|
||||||
|
assert difficulty_prs.chart_info == chart_info_prs
|
||||||
|
assert difficulty_ftr.chart_info == chart_info_ftr
|
||||||
|
assert difficulty_etr.chart_info is None
|
||||||
|
|
||||||
|
assert chart_info_pst.difficulty == difficulty_pst
|
||||||
|
assert chart_info_prs.difficulty == difficulty_prs
|
||||||
|
assert chart_info_ftr.difficulty == difficulty_ftr
|
||||||
|
|
||||||
|
assert play_result_ftr.difficulty == difficulty_ftr
|
69
tests/db/models/v5/relationships/test_pack.py
Normal file
69
tests/db/models/v5/relationships/test_pack.py
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
"""
|
||||||
|
Database model v5 relationships
|
||||||
|
|
||||||
|
Pack <> PackLocalized
|
||||||
|
"""
|
||||||
|
|
||||||
|
from arcaea_offline.constants.enums import ArcaeaLanguage
|
||||||
|
from arcaea_offline.database.models.v5 import ModelsV5Base, Pack, PackLocalized
|
||||||
|
|
||||||
|
|
||||||
|
class TestPackRelationships:
|
||||||
|
@staticmethod
|
||||||
|
def init_db(session):
|
||||||
|
ModelsV5Base.metadata.create_all(session.bind)
|
||||||
|
|
||||||
|
def test_localized_objects(self, db_session):
|
||||||
|
self.init_db(db_session)
|
||||||
|
|
||||||
|
pack_id = "test_pack"
|
||||||
|
name_en = "Test Pack"
|
||||||
|
description_en = "Travel through common database models\nfrom the unpopular framework 'Arcaea Offline'\ntogether with an ordinary partner '∅'."
|
||||||
|
|
||||||
|
pack = Pack(
|
||||||
|
id=pack_id,
|
||||||
|
name=name_en,
|
||||||
|
description=description_en,
|
||||||
|
)
|
||||||
|
|
||||||
|
pkid_ja = 1
|
||||||
|
description_ja = "普通のパートナー「∅」と一緒に、\n不人気フレームワーク「Arcaea Offline」より、\n一般的なデータベース・モデルを通過する。"
|
||||||
|
pack_localized_ja = PackLocalized(
|
||||||
|
pkid=pkid_ja,
|
||||||
|
id=pack_id,
|
||||||
|
lang=ArcaeaLanguage.JA.value,
|
||||||
|
name=None,
|
||||||
|
description=description_ja,
|
||||||
|
)
|
||||||
|
|
||||||
|
pkid_zh_hans = 2
|
||||||
|
description_zh_hans = "与平凡的「∅」一起,\n在没人用的「Arcaea Offline」框架里,\n一同探索随处可见的数据库模型。"
|
||||||
|
pack_localized_zh_hans = PackLocalized(
|
||||||
|
pkid=pkid_zh_hans,
|
||||||
|
id=pack_id,
|
||||||
|
lang=ArcaeaLanguage.ZH_HANS.value,
|
||||||
|
name=None,
|
||||||
|
description=description_zh_hans,
|
||||||
|
)
|
||||||
|
|
||||||
|
db_session.add_all([pack, pack_localized_ja])
|
||||||
|
db_session.commit()
|
||||||
|
|
||||||
|
assert len(pack.localized_objects) == len([pack_localized_ja])
|
||||||
|
|
||||||
|
assert pack_localized_ja.parent.description == pack.description
|
||||||
|
|
||||||
|
# relationships should be viewonly
|
||||||
|
new_pack = Pack(
|
||||||
|
id=f"{pack_id}_new",
|
||||||
|
name="NEW",
|
||||||
|
description="new new pack",
|
||||||
|
)
|
||||||
|
db_session.add(new_pack)
|
||||||
|
|
||||||
|
pack_localized_ja.parent = new_pack
|
||||||
|
pack.localized_objects.append(pack_localized_zh_hans)
|
||||||
|
db_session.commit()
|
||||||
|
|
||||||
|
assert pack_localized_ja.parent == pack
|
||||||
|
assert len(pack.localized_objects) == 1
|
110
tests/db/models/v5/test_chart.py
Normal file
110
tests/db/models/v5/test_chart.py
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
"""
|
||||||
|
Database models v5
|
||||||
|
|
||||||
|
Chart functionalities
|
||||||
|
- basic data handling
|
||||||
|
- Difficulty song info overriding
|
||||||
|
"""
|
||||||
|
|
||||||
|
from arcaea_offline.constants.enums.arcaea import ArcaeaRatingClass
|
||||||
|
from arcaea_offline.database.models.v5 import (
|
||||||
|
Chart,
|
||||||
|
ChartInfo,
|
||||||
|
Difficulty,
|
||||||
|
ModelsV5Base,
|
||||||
|
ModelsV5ViewBase,
|
||||||
|
Pack,
|
||||||
|
Song,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestChart:
|
||||||
|
def init_db(self, session):
|
||||||
|
ModelsV5Base.metadata.create_all(session.bind)
|
||||||
|
ModelsV5ViewBase.metadata.create_all(session.bind)
|
||||||
|
|
||||||
|
def test_basic(self, db_session):
|
||||||
|
self.init_db(db_session)
|
||||||
|
|
||||||
|
pack_id = "test_pack"
|
||||||
|
song_id = "test_song"
|
||||||
|
rating_class = ArcaeaRatingClass.FUTURE
|
||||||
|
|
||||||
|
pack = Pack(id=pack_id, name="Test Pack")
|
||||||
|
song = Song(
|
||||||
|
idx=2,
|
||||||
|
id=song_id,
|
||||||
|
title="~TEST~",
|
||||||
|
artist="~test~",
|
||||||
|
pack_id=pack_id,
|
||||||
|
)
|
||||||
|
difficulty = Difficulty(
|
||||||
|
song_id=song_id,
|
||||||
|
rating_class=rating_class,
|
||||||
|
rating=9,
|
||||||
|
rating_plus=True,
|
||||||
|
)
|
||||||
|
chart_info = ChartInfo(
|
||||||
|
song_id=song_id,
|
||||||
|
rating_class=rating_class,
|
||||||
|
constant=98,
|
||||||
|
notes=980,
|
||||||
|
)
|
||||||
|
db_session.add_all([pack, song, difficulty, chart_info])
|
||||||
|
|
||||||
|
chart: Chart = (
|
||||||
|
db_session.query(Chart)
|
||||||
|
.where((Chart.song_id == song_id) & (Chart.rating_class == rating_class))
|
||||||
|
.one()
|
||||||
|
)
|
||||||
|
|
||||||
|
# `song_id` and `rating_class` are guarded by the WHERE clause above
|
||||||
|
assert chart.song_idx == song.idx
|
||||||
|
assert chart.title == song.title
|
||||||
|
assert chart.artist == song.artist
|
||||||
|
assert chart.pack_id == song.pack_id
|
||||||
|
assert chart.rating == difficulty.rating
|
||||||
|
assert chart.rating_plus == difficulty.rating_plus
|
||||||
|
assert chart.constant == chart_info.constant
|
||||||
|
assert chart.notes == chart_info.notes
|
||||||
|
|
||||||
|
def test_difficulty_override(self, db_session):
|
||||||
|
self.init_db(db_session)
|
||||||
|
|
||||||
|
pack_id = "test_pack"
|
||||||
|
song_id = "test_song"
|
||||||
|
rating_class = ArcaeaRatingClass.FUTURE
|
||||||
|
|
||||||
|
pack = Pack(id=pack_id, name="Test Pack")
|
||||||
|
song = Song(
|
||||||
|
idx=2,
|
||||||
|
id=song_id,
|
||||||
|
title="~TEST~",
|
||||||
|
artist="~test~",
|
||||||
|
pack_id=pack_id,
|
||||||
|
)
|
||||||
|
difficulty = Difficulty(
|
||||||
|
song_id=song_id,
|
||||||
|
rating_class=rating_class,
|
||||||
|
rating=9,
|
||||||
|
rating_plus=True,
|
||||||
|
title="~TEST DIFF~",
|
||||||
|
artist="~diff~",
|
||||||
|
)
|
||||||
|
chart_info = ChartInfo(
|
||||||
|
song_id=song_id,
|
||||||
|
rating_class=rating_class,
|
||||||
|
constant=98,
|
||||||
|
notes=980,
|
||||||
|
)
|
||||||
|
db_session.add_all([pack, song, difficulty, chart_info])
|
||||||
|
|
||||||
|
chart: Chart = (
|
||||||
|
db_session.query(Chart)
|
||||||
|
.where((Chart.song_id == song_id) & (Chart.rating_class == rating_class))
|
||||||
|
.one()
|
||||||
|
)
|
||||||
|
|
||||||
|
assert chart.song_idx == song.idx
|
||||||
|
assert chart.title == difficulty.title
|
||||||
|
assert chart.artist == difficulty.artist
|
Loading…
x
Reference in New Issue
Block a user