Skip to content

feat(sessions): add a JDBC DatabaseSessionService for PostgreSQL, MySQL, MariaDB and SQLite - #1793

Open
voborl00 wants to merge 1 commit into
google:mainfrom
appsatori:feat/database-session-service
Open

voborl00 wants to merge 1 commit into
google:mainfrom
appsatori:feat/database-session-service

Conversation

@voborl00

@voborl00 voborl00 commented Nov 12, 2026 •

Copy link
Copy Markdown

Adds contrib/database-session-service (artifact google-adk-database-session-service): SessionService that keeps sessions, events and app/user state in a relational database through a caller-supplied javax.sql.DataSource. It supports PostgreSQL, MySQL, MariaDB and SQLite and uses the ADK Python table layout.

This is the new PR agreed in #647. It follows the review in #700 and the schema listed in #647. Fixes #665.

Contains:

  • DatabaseSessionService(DataSource) and a builder (dialect, InstantSource, UuidProvider, opt-in auto-migration, statement timeout).

  • Schema creation through an explicit migrate(), with Flyway embedded: it runs over the caller's DataSource, keeps its history in adk_schema_history, and reads its scripts from com/google/adk/sessions/migration/<dialect>/.

  • One contract suite that runs against InMemorySessionService (the part both share), SQLite, PostgreSQL, MySQL and MariaDB.

  • README with setup steps, and sql/ scripts that create a dedicated database and accounts.

  • Constructor and builder as requested. There are no URL or properties constructors and no connection pool of its own.

  • Migration runs only through migrate() or autoMigrate(true). The module reads no system properties or environment variables.

  • DatabaseSessionService is the only public type; everything else is package-private.

  • There is no getAppState, setAppState, getUserState or setUserState.

  • Only SessionException and SessionNotFoundException are thrown, without driver or Flyway exceptions and without caller data in messages. A duplicate ID gives SESSION_ALREADY_EXISTS.

  • The dialect can be set on the builder; otherwise it is taken from the connection's metadata.

Differences from the Python v1 schema

The five tables, their columns and keys, and the index idx_events_app_user_session_ts_id are as listed in #647, with JSON and TIMESTAMP(6) mapped as described there. The exceptions are below. Each can be changed if you prefer strict parity:

  1. events has an extra event_sequence BIGINT, unique per session, and history is read in that order because Java event timestamps are caller-set milliseconds that can tie or go backwards, and InMemorySessionService returns history in append order. (timestamp, id) cannot reproduce that.
    For Python parity: drop the column and order by (timestamp, id). Append order is then lost when timestamps tie.

  2. PostgreSQL uses JSON, not JSONB. becaus JSONB rejects strings with an escaped NUL and rewrites numbers, so an event would not always be stored, or read back, as it was appended. The service never queries inside the documents.
    For Python parity: switch to JSONB and reject such payloads on append.

  3. A stale writer is detected by the last event ID, not by update_time. The caller's last event ID is compared with the stored tail while the session row is locked. sessions.update_time is still the last event's timestamp. The reason is that Python compares a private marker kept on its Session; Java's Session has no such field, and a millisecond update_time does not change when two events share a timestamp.
    For Python parity: add a revision marker to core Session in a separate change and compare that.

  4. adk_internal_metadata has a second row, adk_java_storage_format = jdbc-v1, and the service refuses a schema without it because the layout matches Python but the payloads do not. For example, Java stores event time in milliseconds and Python in seconds. Sharing a database across SDKs was not a goal (feat: Implementation of MySQL-backed session service for ADK #647).
    For Python parity: remove the row and the check.

Questions:

  1. Are differences 1–4 acceptable, or do you want strict Python parity for any of them?
  2. The Python index is created as requested, but Java orders by event_sequence. Should the index stay for parity?
  3. The PR adds .github/workflows/database-session-service.yml, which runs the contract suite against real PostgreSQL, MySQL and MariaDB through Testcontainers when the module or the root pom.xml changes. The regular validation build covers the unit tests and SQLite. Is a separate workflow fine?
  4. A session loaded with a filter that leaves out the last stored event has to be reloaded before appending. Is that acceptable for a first version?
  5. Is the module README the right place for the setup guide and the sql/ scripts, or should they go to adk-docs?

Smaller additions:

  • Key columns have an exact collation (C on PostgreSQL, utf8mb4_0900_bin on MySQL, utf8mb4_nopad_bin on MariaDB, BINARY on SQLite), so identifiers compare the way Java strings do. Python uses the database default.
  • CHECK constraints require the JSON columns to hold a valid JSON object.
  • events.event_data stays nullable as in Python, but Java always writes it. invocation_id is NOT NULL, so an event without one is stored with an empty string.
  • Flyway's history table adk_schema_history is a sixth table.
  • App and user state are one JSON document per app and per user, as in Python. The per-key storage I asked about in feat: Implementation of MySQL-backed session service for ADK #647 is not used.

Outside the module the PR changes the root pom.xml (module entry and Flyway entries), release-please-config.json (the module README, so its version snippet is updated) and adds the workflow above.

…QL, MariaDB and SQLite

Add the contrib module google-adk-database-session-service: a
BaseSessionService that keeps sessions, events and app/user state in a
relational database through a caller-supplied javax.sql.DataSource, in the
ADK Python v1 table layout.

- DatabaseSessionService(DataSource) and a builder (dialect, InstantSource,
  UuidProvider, opt-in auto-migration, statement timeout); the module's only
  public type.
- The schema is created by an explicit migrate() with embedded Flyway, using
  its own adk_schema_history table and migration location.
- One transaction per operation. An append locks app and user state only
  when the event changes them and rejects a stale writer; the caller's
  session is updated only after the commit.
- One contract suite runs against InMemorySessionService, SQLite,
  PostgreSQL, MySQL and MariaDB; a workflow runs it on real databases.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement Database-Backed Session Storage (DatabaseSessionService) for ADK Java

1 participant