Skip to content

Commit ce64787

Browse files
wuliang229copybara-github
authored andcommitted
feat: Add database schema migration command and script
Final part of #3605. This change introduces: - A new `adk migrate session` CLI command to run database schema upgrades. - A migration script to upgrade from the old Pickle-based session schema (v0) to the new JSON-based schema (v1). - A migration runner that orchestrates the upgrade process, handling sequential migrations and using temporary SQLite databases for intermediate steps if needed. - Unit tests for the v0 to v1 migration. Co-authored-by: Liang Wu <wuliang@google.com> PiperOrigin-RevId: 852983323
1 parent 0827d12 commit ce64787

6 files changed

Lines changed: 640 additions & 15 deletions

File tree

src/google/adk/cli/cli_tools_click.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
from . import cli_deploy
3737
from .. import version
3838
from ..evaluation.constants import MISSING_EVAL_DEPENDENCIES_MESSAGE
39+
from ..sessions.migration import migration_runner
3940
from .cli import run_cli
4041
from .fast_api import get_fast_api_app
4142
from .utils import envs
@@ -1507,6 +1508,47 @@ def cli_deploy_cloud_run(
15071508
click.secho(f"Deploy failed: {e}", fg="red", err=True)
15081509

15091510

1511+
@main.group()
1512+
def migrate():
1513+
"""ADK migration commands."""
1514+
pass
1515+
1516+
1517+
@migrate.command("session", cls=HelpfulCommand)
1518+
@click.option(
1519+
"--source_db_url",
1520+
required=True,
1521+
help=(
1522+
"SQLAlchemy URL of source database in database session service, e.g."
1523+
" sqlite:///source.db."
1524+
),
1525+
)
1526+
@click.option(
1527+
"--dest_db_url",
1528+
required=True,
1529+
help=(
1530+
"SQLAlchemy URL of destination database in database session service,"
1531+
" e.g. sqlite:///dest.db."
1532+
),
1533+
)
1534+
@click.option(
1535+
"--log_level",
1536+
type=LOG_LEVELS,
1537+
default="INFO",
1538+
help="Optional. Set the logging level",
1539+
)
1540+
def cli_migrate_session(
1541+
*, source_db_url: str, dest_db_url: str, log_level: str
1542+
):
1543+
"""Migrates a session database to the latest schema version."""
1544+
logs.setup_adk_logger(getattr(logging, log_level.upper()))
1545+
try:
1546+
migration_runner.upgrade(source_db_url, dest_db_url)
1547+
click.secho("Migration check and upgrade process finished.", fg="green")
1548+
except Exception as e:
1549+
click.secho(f"Migration failed: {e}", fg="red", err=True)
1550+
1551+
15101552
@deploy.command("agent_engine")
15111553
@click.option(
15121554
"--api_key",

src/google/adk/sessions/database_session_service.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -178,12 +178,9 @@ async def _prepare_tables(self):
178178
self._db_schema_version = await conn.run_sync(
179179
_schema_check_utils.get_db_schema_version_from_connection
180180
)
181-
except Exception:
182-
# If inspection fails, assume the latest schema
183-
logger.warning(
184-
"Failed to inspect database tables, assuming the latest schema."
185-
)
186-
self._db_schema_version = _schema_check_utils.LATEST_SCHEMA_VERSION
181+
except Exception as e:
182+
logger.error("Failed to inspect database tables: %s", e)
183+
raise
187184

188185
# Check if tables are created and create them if not
189186
if self._tables_created:

src/google/adk/sessions/migration/_schema_check_utils.py

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import logging
1818

19+
from sqlalchemy import create_engine as create_sync_engine
1920
from sqlalchemy import inspect
2021
from sqlalchemy import text
2122

@@ -38,14 +39,16 @@ def _get_schema_version_impl(inspector, connection) -> str:
3839
if result:
3940
return result[0]
4041
else:
41-
return LATEST_SCHEMA_VERSION
42+
raise ValueError(
43+
"Schema version not found in adk_internal_metadata. The database"
44+
" might be malformed."
45+
)
4246
except Exception as e:
43-
logger.warning(
44-
"Failed to query schema version from adk_internal_metadata,"
45-
" assuming the latest schema: %s.",
47+
logger.error(
48+
"Failed to query schema version from adk_internal_metadata: %s.",
4649
e,
4750
)
48-
return LATEST_SCHEMA_VERSION
51+
raise
4952
# Metadata table doesn't exist, check for v0 schema.
5053
# V0 schema has an 'events' table with an 'actions' column.
5154
if inspector.has_table("events"):
@@ -57,17 +60,57 @@ def _get_schema_version_impl(inspector, connection) -> str:
5760
" serialize event actions. The v0 schema will not be supported"
5861
" going forward and will be deprecated in a few rollouts. Please"
5962
" migrate to the v1 schema which uses JSON serialization for event"
60-
" data. The migration command and script will be provided soon."
63+
" data. You can use `adk migrate session` command to migrate your"
64+
" database."
6165
)
6266
return SCHEMA_VERSION_0_PICKLE
6367
except Exception as e:
64-
logger.warning("Failed to inspect 'events' table columns: %s", e)
65-
return LATEST_SCHEMA_VERSION
66-
# New database, assume the latest schema.
68+
logger.error("Failed to inspect 'events' table columns: %s", e)
69+
raise
70+
# New database, use the latest schema.
6771
return LATEST_SCHEMA_VERSION
6872

6973

7074
def get_db_schema_version_from_connection(connection) -> str:
7175
"""Gets DB schema version from a DB connection."""
7276
inspector = inspect(connection)
7377
return _get_schema_version_impl(inspector, connection)
78+
79+
80+
def _to_sync_url(db_url: str) -> str:
81+
"""Removes '+driver' from SQLAlchemy URL."""
82+
if "://" in db_url:
83+
scheme, _, rest = db_url.partition("://")
84+
if "+" in scheme:
85+
dialect = scheme.split("+", 1)[0]
86+
return f"{dialect}://{rest}"
87+
return db_url
88+
89+
90+
def get_db_schema_version(db_url: str) -> str:
91+
"""Reads schema version from DB.
92+
93+
Checks metadata table first and then falls back to table structure.
94+
95+
Args:
96+
db_url: The database URL.
97+
98+
Returns:
99+
The detected schema version as a string. Returns `LATEST_SCHEMA_VERSION`
100+
if it's a new database.
101+
"""
102+
engine = None
103+
try:
104+
engine = create_sync_engine(_to_sync_url(db_url))
105+
with engine.connect() as connection:
106+
inspector = inspect(connection)
107+
return _get_schema_version_impl(inspector, connection)
108+
except Exception:
109+
logger.warning(
110+
"Failed to get schema version from database %s.",
111+
db_url,
112+
)
113+
raise
114+
finally:
115+
if engine:
116+
engine.dispose()

0 commit comments

Comments
 (0)