Skip to content

Commit

Permalink
Implement utcnow() function for consistent UTC timestamps and update …
Browse files Browse the repository at this point in the history
…timestamp defaults in models
  • Loading branch information
Aleksandr Movchan committed Dec 13, 2024
1 parent 6a411fa commit f41e8fa
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 14 deletions.
21 changes: 11 additions & 10 deletions aana/alembic/versions/5ad873484aa3_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from sqlalchemy.schema import CreateSequence, Sequence

from aana.storage.types import JSON
from aana.storage.utcnow import utcnow

# revision identifiers, used by Alembic.
revision: str = "5ad873484aa3"
Expand Down Expand Up @@ -57,14 +58,14 @@ def upgrade() -> None:
sa.Column(
"created_at",
sa.DateTime(timezone=True),
server_default=sa.text("(CURRENT_TIMESTAMP)"),
server_default=utcnow(),
nullable=False,
comment="Timestamp when row is inserted",
),
sa.Column(
"updated_at",
sa.DateTime(timezone=True),
server_default=sa.text("(CURRENT_TIMESTAMP)"),
server_default=utcnow(),
nullable=False,
comment="Timestamp when row is updated",
),
Expand All @@ -84,14 +85,14 @@ def upgrade() -> None:
sa.Column(
"created_at",
sa.DateTime(timezone=True),
server_default=sa.text("(CURRENT_TIMESTAMP)"),
server_default=utcnow(),
nullable=False,
comment="Timestamp when row is inserted",
),
sa.Column(
"updated_at",
sa.DateTime(timezone=True),
server_default=sa.text("(CURRENT_TIMESTAMP)"),
server_default=utcnow(),
nullable=False,
comment="Timestamp when row is updated",
),
Expand Down Expand Up @@ -135,14 +136,14 @@ def upgrade() -> None:
sa.Column(
"assigned_at",
sa.DateTime(timezone=True),
server_default=sa.text("(CURRENT_TIMESTAMP)"),
server_default=utcnow(),
nullable=True,
comment="Timestamp when the task was assigned",
),
sa.Column(
"completed_at",
sa.DateTime(timezone=True),
server_default=sa.text("(CURRENT_TIMESTAMP)"),
server_default=utcnow(),
nullable=True,
comment="Timestamp when the task was completed",
),
Expand All @@ -161,14 +162,14 @@ def upgrade() -> None:
sa.Column(
"created_at",
sa.DateTime(timezone=True),
server_default=sa.text("(CURRENT_TIMESTAMP)"),
server_default=utcnow(),
nullable=False,
comment="Timestamp when row is inserted",
),
sa.Column(
"updated_at",
sa.DateTime(timezone=True),
server_default=sa.text("(CURRENT_TIMESTAMP)"),
server_default=utcnow(),
nullable=False,
comment="Timestamp when row is updated",
),
Expand Down Expand Up @@ -216,14 +217,14 @@ def upgrade() -> None:
sa.Column(
"created_at",
sa.DateTime(timezone=True),
server_default=sa.text("(CURRENT_TIMESTAMP)"),
server_default=utcnow(),
nullable=False,
comment="Timestamp when row is inserted",
),
sa.Column(
"updated_at",
sa.DateTime(timezone=True),
server_default=sa.text("(CURRENT_TIMESTAMP)"),
server_default=utcnow(),
nullable=False,
comment="Timestamp when row is updated",
),
Expand Down
9 changes: 5 additions & 4 deletions aana/storage/models/base.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import datetime
from typing import Annotated, Any, TypeVar

from sqlalchemy import DateTime, MetaData, String, func
from sqlalchemy import DateTime, MetaData, String
from sqlalchemy.orm import (
DeclarativeBase,
Mapped,
Expand All @@ -11,6 +11,7 @@
)

from aana.core.models.media import MediaId
from aana.storage.utcnow import utcnow

timestamp = Annotated[
datetime.datetime,
Expand Down Expand Up @@ -79,11 +80,11 @@ class TimeStampEntity:
"""Mixin for database entities that will have create/update timestamps."""

created_at: Mapped[timestamp] = mapped_column(
server_default=func.now(),
server_default=utcnow(),
comment="Timestamp when row is inserted",
)
updated_at: Mapped[timestamp] = mapped_column(
onupdate=func.now(),
server_default=func.now(),
onupdate=utcnow(),
server_default=utcnow(),
comment="Timestamp when row is updated",
)
35 changes: 35 additions & 0 deletions aana/storage/utcnow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from sqlalchemy import DateTime
from sqlalchemy.ext.compiler import compiles
from sqlalchemy.sql.functions import FunctionElement


class utcnow(FunctionElement):
"""UTCNOW() expression for multiple dialects."""

inherit_cache = True
type = DateTime()


@compiles(utcnow)
def default_sql_utcnow(element, compiler, **kw):
"""Assume, by default, time zones work correctly.
Note:
This is a valid assumption for PostgreSQL and Oracle.
"""
return "CURRENT_TIMESTAMP"


@compiles(utcnow, "sqlite")
def sqlite_sql_utcnow(element, compiler, **kw):
"""SQLite DATETIME('NOW') returns a correct `datetime.datetime` but does not add milliseconds to it.
Directly call STRFTIME with the final %f modifier in order to get those.
"""
return r"(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW'))"


@compiles(utcnow, "snowflake")
def snowflake_sql_utcnow(element, compiler, **kw):
"""In Snowflake, SYSDATE() returns the current timestamp for the system in the UTC time zone."""
return "SYSDATE()"
13 changes: 13 additions & 0 deletions aana/tests/db/test_utcnow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# ruff: noqa: S101
from datetime import datetime, timedelta, timezone

from aana.storage.utcnow import utcnow


def test_utcnow(db_session):
"""Tests the utcnow() function."""
current_time_utc = datetime.now(tz=timezone.utc)
result = db_session.execute(utcnow()).scalar()
result = result.replace(tzinfo=timezone.utc) # Make result offset-aware
assert isinstance(result, datetime)
assert current_time_utc - result < timedelta(seconds=1)

0 comments on commit f41e8fa

Please sign in to comment.