From 19b607684f15ce2b6ffd60382211ba5600705743 Mon Sep 17 00:00:00 2001 From: Filipe Caixeta Date: Mon, 9 Feb 2026 15:27:13 -0800 Subject: [PATCH] fix: Strip timezone for PostgreSQL timestamps in DatabaseSessionService Merge https://github.com/google/adk-python/pull/4365 ## Summary - Fixes `DataError` when using PostgreSQL with `asyncpg` for session storage - PostgreSQL's default `TIMESTAMP` type is `WITHOUT TIME ZONE`, which cannot accept timezone-aware datetime objects - The existing code handled this for SQLite but not PostgreSQL - this fix applies the same timezone stripping ## Error When creating a session with PostgreSQL + asyncpg, the following error occurs: ``` sqlalchemy.dialects.postgresql.asyncpg.Error: : invalid input for query argument $5: datetime.datetime(2026, 2, 3, 21, 32, 50, 353909, tzinfo=datetime.timezone.utc) (can't subtract offset-naive and offset-aware datetimes) ``` During the INSERT: ```sql INSERT INTO sessions (app_name, user_id, id, state, create_time, update_time) VALUES ($1, $2, $3, $4, $5, $6) ``` Where `$5` and `$6` are timezone-aware datetimes being inserted into `TIMESTAMP WITHOUT TIME ZONE` columns. ## Root Cause Commit 1063fa53 changed from database-generated timestamps (`func.now()`) to explicit Python datetimes (`datetime.now(timezone.utc)`). The SQLite case was handled by stripping the timezone, but PostgreSQL was overlooked. ## Test plan - [x] Verified fix resolves the error when creating sessions with PostgreSQL + asyncpg - [ ] Existing unit tests pass Fixes regression from #1733 COPYBARA_INTEGRATE_REVIEW=https://github.com/google/adk-python/pull/4365 from filipecaixeta:fix-postgresql-timestamp-timezone 9d788ba99e7167a53962d93e59a80f78af091ca9 PiperOrigin-RevId: 867800330 --- .../adk/sessions/database_session_service.py | 3 +- .../sessions/test_session_service.py | 41 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/google/adk/sessions/database_session_service.py b/src/google/adk/sessions/database_session_service.py index da26dd25..c7c86e6e 100644 --- a/src/google/adk/sessions/database_session_service.py +++ b/src/google/adk/sessions/database_session_service.py @@ -374,7 +374,8 @@ class DatabaseSessionService(BaseSessionService): # Store the session now = datetime.now(timezone.utc) is_sqlite = self.db_engine.dialect.name == _SQLITE_DIALECT - if is_sqlite: + is_postgresql = self.db_engine.dialect.name == _POSTGRESQL_DIALECT + if is_sqlite or is_postgresql: now = now.replace(tzinfo=None) storage_session = schema.StorageSession( diff --git a/tests/unittests/sessions/test_session_service.py b/tests/unittests/sessions/test_session_service.py index e2b03f2d..29530d2e 100644 --- a/tests/unittests/sessions/test_session_service.py +++ b/tests/unittests/sessions/test_session_service.py @@ -87,6 +87,47 @@ def test_database_session_service_enables_pool_pre_ping_by_default(): assert captured_kwargs.get('pool_pre_ping') is True +@pytest.mark.parametrize('dialect_name', ['sqlite', 'postgresql']) +def test_database_session_service_strips_timezone_for_dialect(dialect_name): + """Verifies that timezone-aware datetimes are converted to naive datetimes + for SQLite and PostgreSQL to avoid 'can't subtract offset-naive and + offset-aware datetimes' errors. + + PostgreSQL's default TIMESTAMP type is WITHOUT TIME ZONE, which cannot + accept timezone-aware datetime objects when using asyncpg. SQLite also + requires naive datetimes. + """ + # Simulate the logic in create_session + is_sqlite = dialect_name == 'sqlite' + is_postgres = dialect_name == 'postgresql' + + now = datetime.now(timezone.utc) + assert now.tzinfo is not None # Starts with timezone + + if is_sqlite or is_postgres: + now = now.replace(tzinfo=None) + + # Both SQLite and PostgreSQL should have timezone stripped + assert now.tzinfo is None + + +def test_database_session_service_preserves_timezone_for_other_dialects(): + """Verifies that timezone info is preserved for dialects that support it.""" + # For dialects like MySQL with explicit timezone support, we don't strip + dialect_name = 'mysql' + is_sqlite = dialect_name == 'sqlite' + is_postgres = dialect_name == 'postgresql' + + now = datetime.now(timezone.utc) + assert now.tzinfo is not None + + if is_sqlite or is_postgres: + now = now.replace(tzinfo=None) + + # MySQL should preserve timezone (if the column type supports it) + assert now.tzinfo is not None + + def test_database_session_service_respects_pool_pre_ping_override(): captured_kwargs = {}