Replace core database utcnow defaults (#1457)

Co-authored-by: ghreprimand <203024559+ghreprimand@users.noreply.github.com>
This commit is contained in:
ghreprimand
2026-06-03 20:50:19 -05:00
committed by GitHub
parent 6e66e69451
commit 82fcec6bb6
2 changed files with 53 additions and 14 deletions

View File

@@ -1,7 +1,7 @@
import os import os
import logging import logging
import sqlite3 import sqlite3
from datetime import datetime from datetime import datetime, timezone
from sqlalchemy import event, create_engine, Column, String, Text, Boolean, DateTime, Integer, ForeignKey, JSON, Index, func, text from sqlalchemy import event, create_engine, Column, String, Text, Boolean, DateTime, Integer, ForeignKey, JSON, Index, func, text
from sqlalchemy.engine import Engine from sqlalchemy.engine import Engine
from sqlalchemy.types import TypeDecorator from sqlalchemy.types import TypeDecorator
@@ -13,15 +13,21 @@ logger = logging.getLogger(__name__)
# Create base class for declarative models # Create base class for declarative models
Base = declarative_base() Base = declarative_base()
def utcnow_naive() -> datetime:
"""Return naive UTC for existing DateTime columns."""
return datetime.now(timezone.utc).replace(tzinfo=None)
class TimestampMixin: class TimestampMixin:
"""Mixin that adds timestamp fields to models""" """Mixin that adds timestamp fields to models"""
@declared_attr @declared_attr
def created_at(cls): def created_at(cls):
return Column(DateTime, default=datetime.utcnow, nullable=False) return Column(DateTime, default=utcnow_naive, nullable=False)
@declared_attr @declared_attr
def updated_at(cls): def updated_at(cls):
return Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False) return Column(DateTime, default=utcnow_naive, onupdate=utcnow_naive, nullable=False)
# Get database URL from environment, default to SQLite # Get database URL from environment, default to SQLite
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./data/app.db") DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./data/app.db")
@@ -171,7 +177,7 @@ class ChatMessage(Base):
meta_data = Column("metadata", Text, nullable=True) # JSON string for metrics etc. meta_data = Column("metadata", Text, nullable=True) # JSON string for metrics etc.
# Timestamp # Timestamp
timestamp = Column(DateTime, default=datetime.utcnow) timestamp = Column(DateTime, default=utcnow_naive)
# Relationship to Session # Relationship to Session
session = relationship("Session", back_populates="messages") session = relationship("Session", back_populates="messages")
@@ -224,7 +230,7 @@ class DocumentVersion(Base):
content = Column(Text, nullable=False) content = Column(Text, nullable=False)
summary = Column(String, nullable=True) # Edit description summary = Column(String, nullable=True) # Edit description
source = Column(String, default="ai") # "ai" or "user" source = Column(String, default="ai") # "ai" or "user"
created_at = Column(DateTime, default=datetime.utcnow) created_at = Column(DateTime, default=utcnow_naive)
document = relationship("Document", back_populates="versions") document = relationship("Document", back_populates="versions")
@@ -472,8 +478,8 @@ class UserToolData(Base):
tool_id = Column(String, ForeignKey("user_tools.id", ondelete="CASCADE"), nullable=False) tool_id = Column(String, ForeignKey("user_tools.id", ondelete="CASCADE"), nullable=False)
key = Column(String, nullable=False) key = Column(String, nullable=False)
value = Column(Text, nullable=True) value = Column(Text, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow) created_at = Column(DateTime, default=utcnow_naive)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) updated_at = Column(DateTime, default=utcnow_naive, onupdate=utcnow_naive)
tool = relationship("UserTool", backref=backref("data_entries", cascade="all, delete-orphan")) tool = relationship("UserTool", backref=backref("data_entries", cascade="all, delete-orphan"))
@@ -592,7 +598,7 @@ class TaskRun(Base):
id = Column(String, primary_key=True, index=True) id = Column(String, primary_key=True, index=True)
task_id = Column(String, ForeignKey("scheduled_tasks.id", ondelete="CASCADE"), nullable=False) task_id = Column(String, ForeignKey("scheduled_tasks.id", ondelete="CASCADE"), nullable=False)
started_at = Column(DateTime, nullable=False, default=datetime.utcnow) started_at = Column(DateTime, nullable=False, default=utcnow_naive)
finished_at = Column(DateTime, nullable=True) finished_at = Column(DateTime, nullable=True)
status = Column(String, default="running") # "running", "success", "error" status = Column(String, default="running") # "running", "success", "error"
result = Column(Text, nullable=True) result = Column(Text, nullable=True)
@@ -633,7 +639,7 @@ class Memory(Base):
session_id = Column(String, ForeignKey("sessions.id", ondelete="SET NULL"), nullable=True, index=True) session_id = Column(String, ForeignKey("sessions.id", ondelete="SET NULL"), nullable=True, index=True)
# Timestamp as Unix timestamp # Timestamp as Unix timestamp
timestamp = Column(Integer, default=lambda: int(datetime.utcnow().timestamp())) timestamp = Column(Integer, default=lambda: int(utcnow_naive().timestamp()))
# Relationship to Session # Relationship to Session
session = relationship("Session", backref="memories") session = relationship("Session", backref="memories")
@@ -1480,7 +1486,7 @@ def _migrate_seed_email_account():
if not imap_host and not smtp_host: if not imap_host and not smtp_host:
return # nothing to migrate return # nothing to migrate
now = datetime.utcnow() now = utcnow_naive()
with engine.begin() as conn: with engine.begin() as conn:
conn.execute(text(""" conn.execute(text("""
INSERT INTO email_accounts INSERT INTO email_accounts
@@ -1760,7 +1766,7 @@ def bulk_insert_messages(session_id: str, messages: list):
'session_id': session_id, 'session_id': session_id,
'role': msg['role'], 'role': msg['role'],
'content': msg['content'], 'content': msg['content'],
'timestamp': datetime.utcnow() 'timestamp': utcnow_naive()
} }
for msg in messages for msg in messages
] ]
@@ -1771,7 +1777,7 @@ def cleanup_old_sessions(days: int = 30):
from datetime import timedelta from datetime import timedelta
with get_db_session() as db: with get_db_session() as db:
cutoff_date = datetime.utcnow() - timedelta(days=days) cutoff_date = utcnow_naive() - timedelta(days=days)
deleted_count = db.query(Session).filter( deleted_count = db.query(Session).filter(
Session.archived == True, Session.archived == True,
@@ -1816,7 +1822,7 @@ def update_session_last_accessed(session_id: str):
with get_db_session() as db: with get_db_session() as db:
db_session = db.query(Session).filter(Session.id == session_id).first() db_session = db.query(Session).filter(Session.id == session_id).first()
if db_session: if db_session:
db_session.last_accessed = datetime.utcnow() db_session.last_accessed = utcnow_naive()
db.commit() db.commit()
return True return True
return False return False
@@ -1861,7 +1867,7 @@ def get_upcoming_events(owner, horizon_days: int = 60, limit: int = 40):
The autonomous email->calendar pass relies on this to avoid disclosing (and The autonomous email->calendar pass relies on this to avoid disclosing (and
acting on) other users' calendars.""" acting on) other users' calendars."""
from datetime import timedelta from datetime import timedelta
now = datetime.utcnow() now = utcnow_naive()
with get_db_session() as db: with get_db_session() as db:
q = db.query(CalendarEvent).join(CalendarCal).filter( q = db.query(CalendarEvent).join(CalendarCal).filter(
CalendarEvent.dtstart >= now, CalendarEvent.dtstart >= now,

View File

@@ -0,0 +1,33 @@
import types
import pytest
sqlalchemy = pytest.importorskip("sqlalchemy")
if not isinstance(sqlalchemy, types.ModuleType):
pytest.skip("sqlalchemy is stubbed in this environment", allow_module_level=True)
from core.database import ChatMessage, DocumentVersion, Session, TaskRun, UserToolData, utcnow_naive
def test_utcnow_naive_returns_naive_utc_datetime():
now = utcnow_naive()
assert now.tzinfo is None
assert abs((now - utcnow_naive()).total_seconds()) < 2
def test_database_timestamp_defaults_use_utcnow_naive():
defaults = (
Session.created_at.default.arg,
Session.updated_at.default.arg,
Session.updated_at.onupdate.arg,
ChatMessage.timestamp.default.arg,
DocumentVersion.created_at.default.arg,
UserToolData.created_at.default.arg,
UserToolData.updated_at.default.arg,
UserToolData.updated_at.onupdate.arg,
TaskRun.started_at.default.arg,
)
for fn in defaults:
assert fn.__name__ == "utcnow_naive"