Skip to content
Draft
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
14 changes: 12 additions & 2 deletions src/agents/extensions/memory/sqlalchemy_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
text as sql_text,
update,
)
from sqlalchemy.dialects.mysql import LONGTEXT
from sqlalchemy.exc import IntegrityError, OperationalError
from sqlalchemy.ext.asyncio import AsyncEngine, async_sessionmaker, create_async_engine

Expand All @@ -64,7 +65,12 @@


class SQLAlchemySession(SessionABC):
"""SQLAlchemy implementation of [`Session`][agents.memory.session.Session]."""
"""SQLAlchemy implementation of [`Session`][agents.memory.session.Session].

Newly created MySQL and MariaDB message tables use LONGTEXT for serialized items.
Existing tables are not migrated; applications must widen their message_data column
to store items larger than the existing column's limit.
"""

_table_init_locks: ClassVar[dict[tuple[str, str, str], threading.Lock]] = {}
_table_init_locks_guard: ClassVar[threading.Lock] = threading.Lock()
Expand Down Expand Up @@ -215,7 +221,11 @@ def __init__(
ForeignKey(f"{sessions_table}.session_id", ondelete="CASCADE"),
nullable=False,
),
Column("message_data", Text, nullable=False),
Column(
"message_data",
Text().with_variant(LONGTEXT(), "mysql", "mariadb"),
nullable=False,
),
Column(
"created_at",
TIMESTAMP(timezone=False),
Expand Down
25 changes: 25 additions & 0 deletions tests/extensions/memory/test_sqlalchemy_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@
Summary,
)
from sqlalchemy import event, insert, select, text, update
from sqlalchemy.dialects import mysql, postgresql, sqlite
from sqlalchemy.dialects.mysql.mariadb import MariaDBDialect
from sqlalchemy.engine import Dialect
from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine
from sqlalchemy.schema import CreateColumn
from sqlalchemy.sql import Select

pytest.importorskip("sqlalchemy") # Skip tests if SQLAlchemy is not installed
Expand Down Expand Up @@ -108,6 +112,27 @@ async def test_sqlalchemy_session_direct_ops(agent: Agent):
assert len(retrieved_after_clear) == 0


@pytest.mark.parametrize(
("dialect", "expected_type"),
[
pytest.param(mysql.dialect(), "LONGTEXT", id="mysql"),
pytest.param(MariaDBDialect(), "LONGTEXT", id="mariadb"),
pytest.param(postgresql.dialect(), "TEXT", id="postgresql"),
pytest.param(sqlite.dialect(), "TEXT", id="sqlite"),
],
)
async def test_message_data_column_type(dialect: Dialect, expected_type: str):
"""MySQL needs large text storage; other dialects retain the existing TEXT column."""
session = SQLAlchemySession.from_url("message_column", url=DB_URL)
try:
column = session._messages.c.message_data
assert str(CreateColumn(column).compile(dialect=dialect)) == (
f"message_data {expected_type} NOT NULL"
)
finally:
await session.engine.dispose()


async def test_sqlalchemy_session_defaults_to_escaped_non_ascii_storage():
"""Default storage keeps the historical escaped non-ASCII JSON representation."""
session = SQLAlchemySession.from_url("default_ascii_storage", url=DB_URL, create_tables=True)
Expand Down