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: <class 'asyncpg.exceptions.DataError'>:
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
This commit is contained in:
Filipe Caixeta
2026-02-09 15:27:13 -08:00
committed by Copybara-Service
parent 8d0279251c
commit 19b607684f
2 changed files with 43 additions and 1 deletions
@@ -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(
@@ -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 = {}