diff --git a/api/models/model.py b/api/models/model.py index ac26f0cd80b771..67949f95a85a7d 100644 --- a/api/models/model.py +++ b/api/models/model.py @@ -24,6 +24,7 @@ from graphon.enums import WorkflowExecutionStatus from graphon.file import FILE_MODEL_IDENTITY, File, FileTransferMethod, FileType from graphon.file import helpers as file_helpers +from libs.datetime_utils import naive_utc_now from libs.helper import generate_string # type: ignore[import-not-found] from libs.url_utils import normalize_api_base_url from libs.uuid_utils import uuidv7 @@ -1144,9 +1145,20 @@ class Conversation(Base): read_at = mapped_column(sa.DateTime) read_account_id = mapped_column(StringUUID) dialogue_count: Mapped[int] = mapped_column(default=0) - created_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp()) + # Keep conversation timestamps on the same naive-UTC convention as the + # application write paths instead of mixing DB-local current_timestamp(). + created_at = mapped_column( + sa.DateTime, + nullable=False, + default=naive_utc_now, + server_default=func.current_timestamp(), + ) updated_at = mapped_column( - sa.DateTime, nullable=False, server_default=func.current_timestamp(), onupdate=func.current_timestamp() + sa.DateTime, + nullable=False, + default=naive_utc_now, + server_default=func.current_timestamp(), + onupdate=naive_utc_now, ) messages = db.relationship("Message", backref="conversation", lazy="select", passive_deletes="all") @@ -1491,9 +1503,19 @@ class Message(Base): ) from_end_user_id: Mapped[str | None] = mapped_column(StringUUID) from_account_id: Mapped[str | None] = mapped_column(StringUUID) - created_at: Mapped[datetime] = mapped_column(sa.DateTime, server_default=func.current_timestamp()) + # Messages share the same timestamp contract as conversations so MySQL + # DATETIME rows do not mix DB-local creation times with UTC updates. + created_at: Mapped[datetime] = mapped_column( + sa.DateTime, + default=naive_utc_now, + server_default=func.current_timestamp(), + ) updated_at: Mapped[datetime] = mapped_column( - sa.DateTime, nullable=False, server_default=func.current_timestamp(), onupdate=func.current_timestamp() + sa.DateTime, + nullable=False, + default=naive_utc_now, + server_default=func.current_timestamp(), + onupdate=naive_utc_now, ) agent_based: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, server_default=sa.text("false")) workflow_run_id: Mapped[str | None] = mapped_column(StringUUID) diff --git a/api/tests/unit_tests/models/test_model.py b/api/tests/unit_tests/models/test_model.py index a87dd7f15ade2d..f011aae2172036 100644 --- a/api/tests/unit_tests/models/test_model.py +++ b/api/tests/unit_tests/models/test_model.py @@ -5,6 +5,7 @@ from core.workflow.file_reference import build_file_reference from graphon.file import FILE_MODEL_IDENTITY, FileTransferMethod +from libs.datetime_utils import naive_utc_now from models.model import Conversation, Message @@ -121,3 +122,19 @@ def test_inputs_restore_external_remote_url_file_mappings(owner_cls: type[Conver assert restored_file.transfer_method == FileTransferMethod.REMOTE_URL assert restored_file.remote_url == "https://example.com/report.pdf" + + +@pytest.mark.parametrize("owner_cls", [Conversation, Message]) +def test_timestamp_columns_use_naive_utc_defaults(owner_cls: type[Conversation] | type[Message]) -> None: + created_at_column = owner_cls.__table__.c.created_at + updated_at_column = owner_cls.__table__.c.updated_at + + assert created_at_column.default is not None + assert created_at_column.default.arg.__name__ == naive_utc_now.__name__ + assert created_at_column.default.arg.__module__ == naive_utc_now.__module__ + assert updated_at_column.default is not None + assert updated_at_column.default.arg.__name__ == naive_utc_now.__name__ + assert updated_at_column.default.arg.__module__ == naive_utc_now.__module__ + assert updated_at_column.onupdate is not None + assert updated_at_column.onupdate.arg.__name__ == naive_utc_now.__name__ + assert updated_at_column.onupdate.arg.__module__ == naive_utc_now.__module__