Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions docs/mkdocs/en/session.md
Original file line number Diff line number Diff line change
Expand Up @@ -479,8 +479,8 @@ CREATE TABLE sessions (
id VARCHAR(255) NOT NULL,
state JSON,
conversation_count INT DEFAULT 0,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
create_time DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6),
update_time DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
PRIMARY KEY (app_name, user_id, id),
INDEX idx_update_time (update_time) -- Used for cleanup task
);
Expand Down Expand Up @@ -523,6 +523,8 @@ async def _cleanup_expired_async(self) -> None:
- `pool_recycle=3600` sets the connection recycle time to avoid long-lived connections
- The cleanup task uses batch SQL DELETE for performance optimization
- Foreign key cascade delete: deleting a session automatically deletes associated events
- **MySQL timestamp precision**: `sessions.create_time` and `sessions.update_time` use microsecond precision. The framework detects the SQLAlchemy dialect automatically from the database URL; MySQL uses `CURRENT_TIMESTAMP(6)` to match `DATETIME(6)`, while PostgreSQL, SQLite, and other databases retain their native current-time expressions. Application code does not need to detect the database type
- No migration is needed for tables created automatically by the current framework. When reusing an older MySQL table, verify that both columns are `DATETIME(6)` so fractional seconds are not truncated

**Related Examples**:
- 📁 [`examples/session_service_with_sql/run_agent.py`](../../../examples/session_service_with_sql/run_agent.py) - Complete SQL Session Service usage example
Expand Down
6 changes: 4 additions & 2 deletions docs/mkdocs/zh/session.md
Original file line number Diff line number Diff line change
Expand Up @@ -479,8 +479,8 @@ CREATE TABLE sessions (
id VARCHAR(255) NOT NULL,
state JSON,
conversation_count INT DEFAULT 0,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
create_time DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6),
update_time DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
PRIMARY KEY (app_name, user_id, id),
INDEX idx_update_time (update_time) -- 用于清理任务
);
Expand Down Expand Up @@ -523,6 +523,8 @@ async def _cleanup_expired_async(self) -> None:
- `pool_recycle=3600` 设置连接回收时间,避免长时间连接
- 清理任务使用批量 SQL DELETE,性能优化
- 外键级联删除:删除会话时自动删除关联事件
- **MySQL 时间精度**:`sessions.create_time` 和 `sessions.update_time` 使用微秒精度。框架会根据数据库连接 URL 自动识别 SQLAlchemy dialect;MySQL 使用 `CURRENT_TIMESTAMP(6)` 与 `DATETIME(6)` 对齐,PostgreSQL、SQLite 等数据库继续使用各自原生的当前时间表达式,无需业务代码手动判断数据库类型
- 如果使用框架自动建表,无需额外迁移;如果复用旧表,请确认 MySQL 中上述两个字段为 `DATETIME(6)`,避免更新时间微秒被截断

**相关示例**:
- 📁 [`examples/session_service_with_sql/run_agent.py`](../../../examples/session_service_with_sql/run_agent.py) - 完整的 SQL Session Service 使用示例
Expand Down
24 changes: 24 additions & 0 deletions tests/storage/test_sql_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from sqlalchemy import Text
from sqlalchemy.dialects import mysql
from sqlalchemy.dialects import postgresql
from sqlalchemy.dialects import sqlite
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.types import DateTime
from sqlalchemy.types import PickleType
Expand All @@ -29,6 +30,7 @@
DynamicJSON,
DynamicJSONOptions,
DynamicPickleType,
PreciseNow,
PreciseTimestamp,
SpannerPickleType,
StorageData,
Expand Down Expand Up @@ -277,6 +279,26 @@ def test_load_dialect_impl_postgresql(self):
assert isinstance(result, String)


# ---------------------------------------------------------------------------
# PreciseNow SQL expression
# ---------------------------------------------------------------------------


class TestPreciseNow:

def test_compile_mysql_uses_microsecond_precision(self):
sql = str(PreciseNow().compile(dialect=mysql.dialect()))
assert sql == "CURRENT_TIMESTAMP(6)"

def test_compile_postgresql_preserves_default_now(self):
sql = str(PreciseNow().compile(dialect=postgresql.dialect()))
assert sql == "now()"

def test_compile_sqlite_preserves_default_now(self):
sql = str(PreciseNow().compile(dialect=sqlite.dialect()))
Comment on lines +282 to +298

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

问题: 新增的 TestPreciseNow 仅断言 PreciseNow().compile(dialect=...) 的独立字符串输出(CURRENT_TIMESTAMP(6)/now()/CURRENT_TIMESTAMP),未验证其在 mapped_column(default=, onupdate=) 中实际渲染出的 DDL,也未验证作为属性赋值时 UPDATE 语句的 SET 片段。

触发条件: SQLAlchemy 升级或 @compiles 实现调整后,独立编译输出可能仍正确,但列默认值/onupdate 的 DDL 渲染路径发生回归时无测试拦截。

实际影响: 本次变更的核心契约——MySQL 下 create_time/update_time 列 DDL 为 DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6)——缺少回归保护,精度回归可能在不被察觉时引入。

修正方向:TestPreciseNow 中增加用例,对 StorageSession.__table__ 执行 CreateTable(...).compile(dialect=mysql.dialect()),断言 DDL 包含 DEFAULT CURRENT_TIMESTAMP(6)ON UPDATE CURRENT_TIMESTAMP(6)

assert sql == "CURRENT_TIMESTAMP"


# ---------------------------------------------------------------------------
# PreciseTimestamp TypeDecorator
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -464,6 +486,7 @@ def test_all_symbols_reexported(self):
DynamicJSON as _DJ,
DynamicJSONOptions as _DJO,
DynamicPickleType as _DPT,
PreciseNow as _PN,
PreciseTimestamp as _PT,
SpannerPickleType as _SPT,
StorageData as _SD,
Expand All @@ -475,6 +498,7 @@ def test_all_symbols_reexported(self):
assert _DJ is DynamicJSON
assert _DJO is DynamicJSONOptions
assert _DPT is DynamicPickleType
assert _PN is PreciseNow
assert _PT is PreciseTimestamp
assert _SPT is SpannerPickleType
assert _SD is StorageData
Expand Down
7 changes: 4 additions & 3 deletions trpc_agent_sdk/sessions/_sql_session_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
from trpc_agent_sdk.storage import DEFAULT_MAX_VARCHAR_LENGTH
from trpc_agent_sdk.storage import DynamicJSON
from trpc_agent_sdk.storage import DynamicPickleType
from trpc_agent_sdk.storage import PreciseNow
from trpc_agent_sdk.storage import PreciseTimestamp
from trpc_agent_sdk.storage import SqlCondition
from trpc_agent_sdk.storage import SqlKey
Expand Down Expand Up @@ -155,8 +156,8 @@ class StorageSession(SessionStorageBase):
nullable=True)
conversation_count: Mapped[int] = mapped_column(Integer, default=0)

create_time: Mapped[datetime] = mapped_column(PreciseTimestamp, default=func.now())
update_time: Mapped[datetime] = mapped_column(PreciseTimestamp, default=func.now(), onupdate=func.now())
create_time: Mapped[datetime] = mapped_column(PreciseTimestamp, default=PreciseNow())
update_time: Mapped[datetime] = mapped_column(PreciseTimestamp, default=PreciseNow(), onupdate=PreciseNow())

Comment on lines +159 to 161

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

问题: 本次变更新增 PreciseNow 并仅将其应用于 sessions 表的 create_time/update_time(第 159-160 行)及 _get_session 的手动刷新(第 735 行),但同文件中其余同样使用 PreciseTimestamp(MySQL 下为 DATETIME(6))列的时间戳仍使用 func.now()SessionStorageEvent.timestamp(第 225 行)、StorageAppState.update_time(第 342 行)、StorageUserState.update_time(第 359 行),以及 _update_app_state/_get_app_state/_update_user_state/_get_user_state 中四处 storage_*_state.update_time = func.now()(第 676、694、706、719 行)。在 MySQL 上 func.now() 编译为 now(),只具备秒级精度,写入 DATETIME(6) 列时小数秒被截断为 .000000,与已修复的 sessions 表微秒精度再次形成不一致。

触发条件: 在 MySQL 场景下,对 app_statesuser_states 表执行访问/更新(触发 update_time 刷新),或向 events 表写入事件时,时间由 func.now()/now() 生成。

实际影响: 计划标题“修改 mysql 场景下的时间精度不一致的问题”仅对 sessions 表生效,app_statesuser_statesevents 三张表仍为秒级精度,跨表时间精度不一致持续存在;同一秒内多条事件按 timestamp 排序将出现并列,且与 sessions 表的微秒时间无法对齐比较。

修正方向:events.timestampStorageAppState.update_timeStorageUserState.update_timedefault/onupdate 以及第 676、694、706、719 行的手动 func.now() 赋值统一替换为 PreciseNow(),使全部 PreciseTimestamp 列在 MySQL 下都使用 CURRENT_TIMESTAMP(6),彻底消除精度不一致。

storage_events: Mapped[list[SessionStorageEvent]] = relationship(
"SessionStorageEvent",
Expand Down Expand Up @@ -731,7 +732,7 @@ async def _get_session(self, sql_session: SqlSession, app_name: str, user_id: st
logger.debug("Session %s is expired", session_id)
return None

storage_session.update_time = func.now()
storage_session.update_time = PreciseNow()
await self._sql_storage.commit(sql_session)

return storage_session
Expand Down
2 changes: 2 additions & 0 deletions trpc_agent_sdk/storage/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from ._sql_common import DynamicJSON
from ._sql_common import DynamicJSONOptions
from ._sql_common import DynamicPickleType
from ._sql_common import PreciseNow
from ._sql_common import PreciseTimestamp
from ._sql_common import SpannerPickleType
from ._sql_common import StorageData
Expand Down Expand Up @@ -59,6 +60,7 @@
"DynamicJSON",
"DynamicJSONOptions",
"DynamicPickleType",
"PreciseNow",
"PreciseTimestamp",
"SpannerPickleType",
"StorageData",
Expand Down
24 changes: 24 additions & 0 deletions trpc_agent_sdk/storage/_sql_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,12 @@

from sqlalchemy import Dialect
from sqlalchemy import Text
from sqlalchemy import func
from sqlalchemy.dialects import mysql
from sqlalchemy.dialects import postgresql
from sqlalchemy.ext.compiler import compiles
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.sql.functions import FunctionElement
from sqlalchemy.types import DateTime
from sqlalchemy.types import PickleType
from sqlalchemy.types import String
Expand Down Expand Up @@ -303,6 +306,27 @@ def process_result_value(self, value: Any, dialect: Dialect) -> Any:
return value


class PreciseNow(FunctionElement):
"""Return the current database timestamp with MySQL microsecond precision."""

type = DateTime()
inherit_cache = True


@compiles(PreciseNow)
def _compile_precise_now(element: PreciseNow, compiler: Any, **kwargs: Any) -> str:
"""Preserve SQLAlchemy's dialect-specific ``now()`` behavior by default."""
del element
return compiler.process(func.now(), **kwargs)


@compiles(PreciseNow, "mysql")
def _compile_precise_now_mysql(element: PreciseNow, compiler: Any, **kwargs: Any) -> str:
"""Use the precision declared by MySQL ``DATETIME(6)`` columns."""
del element, compiler, kwargs
return "CURRENT_TIMESTAMP(6)"


class DynamicPickleType(TypeDecorator):
"""Represents a type that can be pickled."""

Expand Down
Loading