1
0
Fork 0
mirror of https://github.com/mealie-recipes/mealie.git synced 2025-08-02 20:15:24 +02:00

fix: Strip Timezone from Timestamps in DB (#4310)

Co-authored-by: Kuchenpirat <24235032+Kuchenpirat@users.noreply.github.com>
This commit is contained in:
Michael Genson 2024-10-06 01:30:30 -05:00 committed by GitHub
parent b5c0104aba
commit b0ed242ff2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 64 additions and 21 deletions

View file

@ -1 +1,2 @@
from mealie.db.models._model_utils.datetime import NaiveDateTime # noqa: F401
from mealie.db.models._model_utils.guid import GUID # noqa: F401

View file

@ -1,16 +1,16 @@
from datetime import datetime
from sqlalchemy import DateTime, Integer
from sqlalchemy import Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, declared_attr, mapped_column, synonym
from text_unidecode import unidecode
from ._model_utils.datetime import get_utc_now
from ._model_utils.datetime import NaiveDateTime, get_utc_now
class SqlAlchemyBase(DeclarativeBase):
id: Mapped[int] = mapped_column(Integer, primary_key=True)
created_at: Mapped[datetime | None] = mapped_column(DateTime, default=get_utc_now, index=True)
update_at: Mapped[datetime | None] = mapped_column(DateTime, default=get_utc_now, onupdate=get_utc_now)
created_at: Mapped[datetime | None] = mapped_column(NaiveDateTime, default=get_utc_now, index=True)
update_at: Mapped[datetime | None] = mapped_column(NaiveDateTime, default=get_utc_now, onupdate=get_utc_now)
@declared_attr
def updated_at(cls) -> Mapped[datetime | None]:

View file

@ -1,5 +1,7 @@
from datetime import datetime, timezone
from sqlalchemy.types import DateTime, TypeDecorator
def get_utc_now():
"""
@ -13,3 +15,36 @@ def get_utc_today():
Returns the current date in UTC.
"""
return datetime.now(timezone.utc).date()
class NaiveDateTime(TypeDecorator):
"""
Mealie uses naive date times since the app handles timezones explicitly.
All timezones are generated, stored, and retrieved as UTC.
This class strips the timezone from a datetime object when storing it so the database (i.e. postgres)
doesn't do any timezone conversion when storing the datetime, then re-inserts UTC when retrieving it.
"""
impl = DateTime
cache_ok = True
def process_bind_param(self, value: datetime | None, dialect):
if value is None:
return value
try:
if value.tzinfo is not None:
value = value.astimezone(timezone.utc)
return value.replace(tzinfo=None)
except Exception:
return value
def process_result_value(self, value: datetime | None, dialect):
try:
if value is not None:
value = value.replace(tzinfo=timezone.utc)
except Exception:
pass
return value

View file

@ -4,12 +4,12 @@ from typing import TYPE_CHECKING
from pydantic import ConfigDict
from sqlalchemy import ForeignKey, orm
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.sql.sqltypes import Boolean, DateTime, String
from sqlalchemy.sql.sqltypes import Boolean, String
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
from .._model_utils.auto_init import auto_init
from .._model_utils.datetime import get_utc_now
from .._model_utils.datetime import NaiveDateTime, get_utc_now
from .._model_utils.guid import GUID
if TYPE_CHECKING:
@ -23,7 +23,7 @@ class ReportEntryModel(SqlAlchemyBase, BaseMixins):
success: Mapped[bool | None] = mapped_column(Boolean, default=False)
message: Mapped[str] = mapped_column(String, nullable=True)
exception: Mapped[str] = mapped_column(String, nullable=True)
timestamp: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=get_utc_now)
timestamp: Mapped[datetime] = mapped_column(NaiveDateTime, nullable=False, default=get_utc_now)
report_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("group_reports.id"), nullable=False, index=True)
report: Mapped["ReportModel"] = orm.relationship("ReportModel", back_populates="entries")
@ -40,7 +40,7 @@ class ReportModel(SqlAlchemyBase, BaseMixins):
name: Mapped[str] = mapped_column(String, nullable=False)
status: Mapped[str] = mapped_column(String, nullable=False)
category: Mapped[str] = mapped_column(String, index=True, nullable=False)
timestamp: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=get_utc_now)
timestamp: Mapped[datetime] = mapped_column(NaiveDateTime, nullable=False, default=get_utc_now)
entries: Mapped[list[ReportEntryModel]] = orm.relationship(
ReportEntryModel, back_populates="report", cascade="all, delete-orphan"

View file

@ -12,7 +12,7 @@ from sqlalchemy.orm.attributes import get_history
from sqlalchemy.orm.session import object_session
from mealie.db.models._model_utils.auto_init import auto_init
from mealie.db.models._model_utils.datetime import get_utc_today
from mealie.db.models._model_utils.datetime import NaiveDateTime, get_utc_today
from mealie.db.models._model_utils.guid import GUID
from .._model_base import BaseMixins, SqlAlchemyBase
@ -135,8 +135,8 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
# Time Stamp Properties
date_added: Mapped[date | None] = mapped_column(sa.Date, default=get_utc_today)
date_updated: Mapped[datetime | None] = mapped_column(sa.DateTime)
last_made: Mapped[datetime | None] = mapped_column(sa.DateTime)
date_updated: Mapped[datetime | None] = mapped_column(NaiveDateTime)
last_made: Mapped[datetime | None] = mapped_column(NaiveDateTime)
# Shopping List Refs
shopping_list_refs: Mapped[list["ShoppingListRecipeReference"]] = orm.relationship(

View file

@ -1,10 +1,12 @@
from datetime import datetime, timezone
from typing import TYPE_CHECKING
from sqlalchemy import DateTime, ForeignKey, String
from sqlalchemy import ForeignKey, String
from sqlalchemy.ext.associationproxy import AssociationProxy, association_proxy
from sqlalchemy.orm import Mapped, mapped_column, relationship
from mealie.db.models._model_utils.datetime import NaiveDateTime
from .._model_base import BaseMixins, SqlAlchemyBase
from .._model_utils.auto_init import auto_init
from .._model_utils.guid import GUID
@ -38,7 +40,7 @@ class RecipeTimelineEvent(SqlAlchemyBase, BaseMixins):
image: Mapped[str | None] = mapped_column(String)
# Timestamps
timestamp: Mapped[datetime | None] = mapped_column(DateTime, index=True)
timestamp: Mapped[datetime | None] = mapped_column(NaiveDateTime, index=True)
@auto_init()
def __init__(

View file

@ -7,6 +7,7 @@ from sqlalchemy.orm import Mapped, mapped_column
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
from mealie.db.models._model_utils.auto_init import auto_init
from mealie.db.models._model_utils.datetime import NaiveDateTime
from mealie.db.models._model_utils.guid import GUID
if TYPE_CHECKING:
@ -26,7 +27,7 @@ class RecipeShareTokenModel(SqlAlchemyBase, BaseMixins):
recipe_id: Mapped[GUID] = mapped_column(GUID, sa.ForeignKey("recipes.id"), nullable=False, index=True)
recipe: Mapped["RecipeModel"] = sa.orm.relationship("RecipeModel", back_populates="share_tokens", uselist=False)
expires_at: Mapped[datetime] = mapped_column(sa.DateTime, nullable=False)
expires_at: Mapped[datetime] = mapped_column(NaiveDateTime, nullable=False)
@auto_init()
def __init__(self, **_) -> None:

View file

@ -1,10 +1,11 @@
from datetime import datetime
from typing import TYPE_CHECKING
from sqlalchemy import DateTime, ForeignKey, String, orm
from sqlalchemy import ForeignKey, String, orm
from sqlalchemy.orm import Mapped, mapped_column
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
from mealie.db.models._model_utils.datetime import NaiveDateTime
from mealie.db.models._model_utils.guid import GUID
from .._model_utils.auto_init import auto_init
@ -18,7 +19,7 @@ class ServerTaskModel(SqlAlchemyBase, BaseMixins):
__tablename__ = "server_tasks"
name: Mapped[str] = mapped_column(String, nullable=False)
completed_date: Mapped[datetime] = mapped_column(DateTime, nullable=True)
completed_date: Mapped[datetime] = mapped_column(NaiveDateTime, nullable=True)
status: Mapped[str] = mapped_column(String, nullable=False)
log: Mapped[str] = mapped_column(String, nullable=True)

View file

@ -3,13 +3,14 @@ from datetime import datetime
from typing import TYPE_CHECKING, Optional
from pydantic import ConfigDict
from sqlalchemy import Boolean, DateTime, Enum, ForeignKey, Integer, String, orm, select
from sqlalchemy import Boolean, Enum, ForeignKey, Integer, String, orm, select
from sqlalchemy.ext.associationproxy import AssociationProxy, association_proxy
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import Mapped, Session, mapped_column
from mealie.core.config import get_app_settings
from mealie.db.models._model_utils.auto_init import auto_init
from mealie.db.models._model_utils.datetime import NaiveDateTime
from mealie.db.models._model_utils.guid import GUID
from .._model_base import BaseMixins, SqlAlchemyBase
@ -65,7 +66,7 @@ class User(SqlAlchemyBase, BaseMixins):
cache_key: Mapped[str | None] = mapped_column(String, default="1234")
login_attemps: Mapped[int | None] = mapped_column(Integer, default=0)
locked_at: Mapped[datetime | None] = mapped_column(DateTime, default=None)
locked_at: Mapped[datetime | None] = mapped_column(NaiveDateTime, default=None)
# Group Permissions
can_manage_household: Mapped[bool | None] = mapped_column(Boolean, default=False)

View file

@ -15,6 +15,7 @@ from sqlalchemy.orm import InstrumentedAttribute, Mapper
from sqlalchemy.sql import sqltypes
from mealie.db.models._model_base import SqlAlchemyBase
from mealie.db.models._model_utils.datetime import NaiveDateTime
from mealie.db.models._model_utils.guid import GUID
Model = TypeVar("Model", bound=SqlAlchemyBase)
@ -177,7 +178,7 @@ class QueryFilterComponent:
except ValueError as e:
raise ValueError(f"invalid query string: invalid UUID '{v}'") from e
if isinstance(model_attr_type, sqltypes.Date | sqltypes.DateTime):
if isinstance(model_attr_type, sqltypes.Date | sqltypes.DateTime | NaiveDateTime):
try:
dt = date_parser.parse(v)
sanitized_values[i] = dt.date() if isinstance(model_attr_type, sqltypes.Date) else dt

View file

@ -1,11 +1,12 @@
import datetime
from pathlib import Path
from sqlalchemy import DateTime, cast, select
from sqlalchemy import cast, select
from mealie.core import root_logger
from mealie.core.config import get_app_dirs
from mealie.db.db_setup import session_context
from mealie.db.models._model_utils.datetime import NaiveDateTime
from mealie.db.models.group.exports import GroupDataExportsModel
ONE_DAY_AS_MINUTES = 1440
@ -19,7 +20,7 @@ def purge_group_data_exports(max_minutes_old=ONE_DAY_AS_MINUTES):
limit = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(minutes=max_minutes_old)
with session_context() as session:
stmt = select(GroupDataExportsModel).filter(cast(GroupDataExportsModel.expires, DateTime) <= limit)
stmt = select(GroupDataExportsModel).filter(cast(GroupDataExportsModel.expires, NaiveDateTime) <= limit)
results = session.execute(stmt).scalars().all()
total_removed = 0