Skip to content

Commit e743f10

Browse files
committed
test(adk): strengthen consolidation contracts
1 parent c2dd2d5 commit e743f10

3 files changed

Lines changed: 295 additions & 1 deletion

File tree

sqlspec/adapters/oracledb/adk/store.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2147,7 +2147,7 @@ def _sessions_table_ddl_for_type(self, storage_type: JSONStorageType) -> str:
21472147
self._config,
21482148
"session",
21492149
in_memory=self._in_memory,
2150-
hash_partition_key="session_id",
2150+
hash_partition_key="id",
21512151
range_partition_key="create_time",
21522152
)
21532153

tests/unit/extensions/test_adk/test_sql_template_hoist.py

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,67 @@
1+
# pyright: reportPrivateUsage=false
12
"""Regression tests for adapter ADK SQL template ownership."""
23

34
import ast
45
import importlib
56
import inspect
7+
from unittest.mock import MagicMock
68

79
import pytest
810

11+
from sqlspec.adapters.cockroach_psycopg.adk import (
12+
CockroachPsycopgAsyncADKMemoryStore,
13+
CockroachPsycopgAsyncADKStore,
14+
CockroachPsycopgSyncADKMemoryStore,
15+
CockroachPsycopgSyncADKStore,
16+
)
17+
from sqlspec.adapters.mysqlconnector.adk import (
18+
MysqlConnectorAsyncADKMemoryStore,
19+
MysqlConnectorAsyncADKStore,
20+
MysqlConnectorSyncADKMemoryStore,
21+
MysqlConnectorSyncADKStore,
22+
)
23+
from sqlspec.adapters.oracledb.adk import (
24+
JSONStorageType,
25+
OracleAsyncADKMemoryStore,
26+
OracleAsyncADKStore,
27+
OracleSyncADKMemoryStore,
28+
OracleSyncADKStore,
29+
)
30+
from sqlspec.adapters.psycopg.adk import (
31+
PsycopgAsyncADKMemoryStore,
32+
PsycopgAsyncADKStore,
33+
PsycopgSyncADKMemoryStore,
34+
PsycopgSyncADKStore,
35+
)
36+
37+
38+
def _mock_config(adk_config: dict[str, object]) -> MagicMock:
39+
config = MagicMock()
40+
config.extension_config = {"adk": adk_config}
41+
return config
42+
43+
44+
async def _session_ddl(store: object) -> dict[str, str]:
45+
return {
46+
"sessions": await store._sessions_table_ddl(), # type: ignore[attr-defined]
47+
"events": await store._events_table_ddl(), # type: ignore[attr-defined]
48+
"app_state": await store._app_states_table_ddl(), # type: ignore[attr-defined]
49+
"user_state": await store._user_states_table_ddl(), # type: ignore[attr-defined]
50+
"metadata": await store._metadata_table_ddl(), # type: ignore[attr-defined]
51+
"seed": await store._metadata_seed_sql(), # type: ignore[attr-defined]
52+
}
53+
54+
55+
def _sync_session_ddl(store: object) -> dict[str, str]:
56+
return {
57+
"sessions": store._sessions_table_ddl(), # type: ignore[attr-defined]
58+
"events": store._events_table_ddl(), # type: ignore[attr-defined]
59+
"app_state": store._app_states_table_ddl(), # type: ignore[attr-defined]
60+
"user_state": store._user_states_table_ddl(), # type: ignore[attr-defined]
61+
"metadata": store._metadata_table_ddl(), # type: ignore[attr-defined]
62+
"seed": store._metadata_seed_sql(), # type: ignore[attr-defined]
63+
}
64+
965

1066
@pytest.mark.parametrize(
1167
"module_name",
@@ -28,3 +84,149 @@ def test_adk_ddl_methods_reference_module_templates(module_name: str) -> None:
2884
inline_templates = [node for owner in sql_owners for node in ast.walk(owner) if isinstance(node, ast.JoinedStr)]
2985

3086
assert not inline_templates
87+
88+
89+
@pytest.mark.anyio
90+
async def test_psycopg_templates_bind_identically_for_sync_and_async_stores() -> None:
91+
config = _mock_config({
92+
"session_table": "agent_session",
93+
"events_table": "agent_event",
94+
"memory_table": "agent_memory",
95+
"owner_id_column": "tenant_id UUID REFERENCES tenant(id)",
96+
"enable_event_generated_columns": True,
97+
"enable_covering_indexes": True,
98+
})
99+
async_store = PsycopgAsyncADKStore(config)
100+
sync_store = PsycopgSyncADKStore(config)
101+
async_memory = PsycopgAsyncADKMemoryStore(config)
102+
sync_memory = PsycopgSyncADKMemoryStore(config)
103+
104+
async_ddl = await _session_ddl(async_store)
105+
sync_ddl = _sync_session_ddl(sync_store)
106+
async_memory_ddl = await async_memory._memory_table_ddl()
107+
sync_memory_ddl = sync_memory._memory_table_ddl()
108+
109+
assert async_ddl == sync_ddl
110+
assert async_memory_ddl == sync_memory_ddl
111+
assert "tenant_id UUID REFERENCES tenant(id)," in async_ddl["sessions"]
112+
assert "tenant_id UUID REFERENCES tenant(id)," in async_memory_ddl
113+
assert "author_gc VARCHAR(256) GENERATED ALWAYS AS (event_data->>'author') STORED" in async_ddl["events"]
114+
assert "node_path_gc TEXT GENERATED ALWAYS AS (event_data->'node_info'->>'path') STORED" in async_ddl["events"]
115+
assert "ON agent_event(session_id, timestamp ASC) INCLUDE (invocation_id)" in async_ddl["events"]
116+
117+
118+
@pytest.mark.anyio
119+
async def test_cockroach_templates_bind_identically_for_sync_and_async_stores() -> None:
120+
config = _mock_config({
121+
"session_table": "agent_session",
122+
"events_table": "agent_event",
123+
"memory_table": "agent_memory",
124+
"owner_id_column": "tenant_id UUID",
125+
"table_locality": "LOCALITY GLOBAL",
126+
"enable_hash_sharded_indexes": True,
127+
"hash_shard_bucket_count": 8,
128+
"enable_storing_indexes": True,
129+
"enable_memory_trigram_index": True,
130+
})
131+
async_store = CockroachPsycopgAsyncADKStore(config)
132+
sync_store = CockroachPsycopgSyncADKStore(config)
133+
async_memory = CockroachPsycopgAsyncADKMemoryStore(config)
134+
sync_memory = CockroachPsycopgSyncADKMemoryStore(config)
135+
136+
async_ddl = await _session_ddl(async_store)
137+
sync_ddl = _sync_session_ddl(sync_store)
138+
async_memory_ddl = await async_memory._memory_table_ddl()
139+
sync_memory_ddl = sync_memory._memory_table_ddl()
140+
141+
assert async_ddl == sync_ddl
142+
assert async_memory_ddl == sync_memory_ddl
143+
assert "tenant_id UUID," in async_ddl["sessions"]
144+
assert "LOCALITY GLOBAL" in async_ddl["sessions"]
145+
assert "USING HASH WITH (bucket_count = 8)" in async_ddl["events"]
146+
assert "STORING (invocation_id, event_data)" in async_ddl["events"]
147+
assert "ON agent_memory USING GIN (content_text gin_trgm_ops)" in async_memory_ddl
148+
149+
150+
@pytest.mark.anyio
151+
async def test_mysqlconnector_templates_bind_identically_for_sync_and_async_stores() -> None:
152+
config = _mock_config({
153+
"session_table": "agent_session",
154+
"events_table": "agent_event",
155+
"memory_table": "agent_memory",
156+
"owner_id_column": "tenant_id BIGINT",
157+
"enable_event_generated_columns": True,
158+
"enable_covering_indexes": True,
159+
"events_table_options": "COMMENT='agent-events'",
160+
"memory_table_options": "COMMENT='agent-memory'",
161+
})
162+
async_store = MysqlConnectorAsyncADKStore(config)
163+
sync_store = MysqlConnectorSyncADKStore(config)
164+
async_memory = MysqlConnectorAsyncADKMemoryStore(config)
165+
sync_memory = MysqlConnectorSyncADKMemoryStore(config)
166+
167+
async_ddl = await _session_ddl(async_store)
168+
sync_ddl = _sync_session_ddl(sync_store)
169+
async_memory_ddl = await async_memory._memory_table_ddl()
170+
sync_memory_ddl = sync_memory._memory_table_ddl()
171+
172+
assert async_ddl == sync_ddl
173+
assert async_memory_ddl == sync_memory_ddl
174+
assert "tenant_id BIGINT," in async_ddl["sessions"]
175+
assert "tenant_id BIGINT," in async_memory_ddl
176+
assert "author_gc VARCHAR(256) GENERATED ALWAYS AS" in async_ddl["events"]
177+
assert "INDEX idx_agent_event_session (session_id, timestamp ASC, invocation_id)" in async_ddl["events"]
178+
assert "COMMENT='agent-events'" in async_ddl["events"]
179+
assert "COMMENT='agent-memory'" in async_memory_ddl
180+
181+
182+
@pytest.mark.anyio
183+
async def test_oracle_templates_bind_identically_for_every_json_storage_type() -> None:
184+
config = _mock_config({
185+
"session_table": "agent_session",
186+
"events_table": "agent_event",
187+
"memory_table": "agent_memory",
188+
"owner_id_column": "tenant_id NUMBER",
189+
"partitioning": {"strategy": "hash", "partition_count": 8},
190+
})
191+
async_store = OracleAsyncADKStore(config)
192+
sync_store = OracleSyncADKStore(config)
193+
async_memory = OracleAsyncADKMemoryStore(config)
194+
sync_memory = OracleSyncADKMemoryStore(config)
195+
expected_columns = {
196+
JSONStorageType.JSON_NATIVE: ("state JSON NOT NULL", "event_data JSON NOT NULL", "content_json JSON"),
197+
JSONStorageType.BLOB_JSON: (
198+
"state BLOB CHECK (state IS JSON) NOT NULL",
199+
"event_data BLOB CHECK (event_data IS JSON) NOT NULL",
200+
"content_json BLOB CHECK (content_json IS JSON)",
201+
),
202+
JSONStorageType.BLOB_PLAIN: ("state BLOB NOT NULL", "event_data BLOB NOT NULL", "content_json BLOB"),
203+
}
204+
205+
for storage_type, (state_column, event_column, memory_column) in expected_columns.items():
206+
async_session_ddl = async_store._sessions_table_ddl_for_type(storage_type)
207+
sync_session_ddl = sync_store._sessions_table_ddl_for_type(storage_type)
208+
async_events_ddl = async_store._events_table_ddl_for_type(storage_type)
209+
sync_events_ddl = sync_store._events_table_ddl_for_type(storage_type)
210+
async_memory_ddl = async_memory._memory_table_ddl_for_type(storage_type)
211+
sync_memory_ddl = sync_memory._memory_table_ddl_for_type(storage_type)
212+
213+
assert async_session_ddl == sync_session_ddl
214+
assert async_events_ddl == sync_events_ddl
215+
assert async_store._app_states_table_ddl_for_type(storage_type) == sync_store._app_states_table_ddl_for_type(
216+
storage_type
217+
)
218+
assert async_store._user_states_table_ddl_for_type(storage_type) == sync_store._user_states_table_ddl_for_type(
219+
storage_type
220+
)
221+
assert async_memory_ddl == sync_memory_ddl
222+
assert state_column in async_session_ddl
223+
assert event_column in async_events_ddl
224+
assert memory_column in async_memory_ddl
225+
assert (
226+
"update_time TIMESTAMP WITH TIME ZONE DEFAULT SYSTIMESTAMP NOT NULL, tenant_id NUMBER" in async_session_ddl
227+
)
228+
assert "tenant_id NUMBER," in async_memory_ddl
229+
assert "PARTITION BY HASH (id) PARTITIONS 8" in async_session_ddl
230+
231+
assert await async_store._metadata_table_ddl() == sync_store._metadata_table_ddl()
232+
assert await async_store._metadata_seed_sql() == sync_store._metadata_seed_sql()

tests/unit/extensions/test_adk/test_store_common_mixins.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
"""Regression tests for common ADK store state mixins."""
22

3+
import pytest
4+
5+
from sqlspec.adapters.sqlite import SqliteConfig
6+
from sqlspec.adapters.sqlite.adk import SqliteADKMemoryStore, SqliteADKStore
7+
from sqlspec.extensions.adk.artifact._types import ArtifactRecord
38
from sqlspec.extensions.adk.artifact.store import (
49
BaseAsyncADKArtifactStore,
510
BaseSyncADKArtifactStore,
@@ -9,6 +14,55 @@
914
from sqlspec.extensions.adk.store import BaseAsyncADKStore, BaseSyncADKStore, _ADKStoreCommon
1015

1116

17+
class _ConcreteArtifactStore(BaseSyncADKArtifactStore[SqliteConfig]):
18+
__slots__ = ()
19+
20+
def insert_artifact(self, record: ArtifactRecord) -> None:
21+
return None
22+
23+
def get_artifact(
24+
self, app_name: str, user_id: str, filename: str, session_id: str | None = None, version: int | None = None
25+
) -> ArtifactRecord | None:
26+
return None
27+
28+
def list_artifact_keys(self, app_name: str, user_id: str, session_id: str | None = None) -> list[str]:
29+
return []
30+
31+
def list_artifact_versions(
32+
self, app_name: str, user_id: str, filename: str, session_id: str | None = None
33+
) -> list[ArtifactRecord]:
34+
return []
35+
36+
def delete_artifact(
37+
self, app_name: str, user_id: str, filename: str, session_id: str | None = None
38+
) -> list[ArtifactRecord]:
39+
return []
40+
41+
def get_next_version(self, app_name: str, user_id: str, filename: str, session_id: str | None = None) -> int:
42+
return 0
43+
44+
def create_table(self) -> None:
45+
return None
46+
47+
48+
def _sqlite_config() -> SqliteConfig:
49+
return SqliteConfig(
50+
connection_config={"database": ":memory:"},
51+
extension_config={
52+
"adk": {
53+
"session_table": "agent_sessions",
54+
"events_table": "agent_events",
55+
"app_state_table": "agent_app_state",
56+
"user_state_table": "agent_user_state",
57+
"metadata_table": "agent_metadata",
58+
"memory_table": "agent_memory",
59+
"artifact_table": "agent_artifact",
60+
"owner_id_column": "tenant_id INTEGER",
61+
}
62+
},
63+
)
64+
65+
1266
def test_session_store_bases_share_slotted_common_state() -> None:
1367
assert issubclass(BaseAsyncADKStore, _ADKStoreCommon)
1468
assert issubclass(BaseSyncADKStore, _ADKStoreCommon)
@@ -28,3 +82,41 @@ def test_artifact_store_bases_share_slotted_common_state() -> None:
2882
assert issubclass(BaseSyncADKArtifactStore, _ADKArtifactStoreCommon)
2983
assert BaseAsyncADKArtifactStore.__slots__ == ()
3084
assert BaseSyncADKArtifactStore.__slots__ == ()
85+
86+
87+
@pytest.mark.parametrize(
88+
"store",
89+
[
90+
pytest.param(SqliteADKStore(_sqlite_config()), id="session"),
91+
pytest.param(SqliteADKMemoryStore(_sqlite_config()), id="memory"),
92+
pytest.param(_ConcreteArtifactStore(_sqlite_config()), id="artifact"),
93+
],
94+
)
95+
def test_concrete_adk_stores_reject_undeclared_attributes(store: object) -> None:
96+
with pytest.raises(AttributeError):
97+
setattr(store, "undeclared", True)
98+
99+
100+
def test_concrete_session_store_exposes_all_shared_properties() -> None:
101+
config = _sqlite_config()
102+
store = SqliteADKStore(config)
103+
104+
assert {
105+
"config": store.config,
106+
"session_table": store.session_table,
107+
"events_table": store.events_table,
108+
"app_state_table": store.app_state_table,
109+
"user_state_table": store.user_state_table,
110+
"metadata_table": store.metadata_table,
111+
"owner_id_column_ddl": store.owner_id_column_ddl,
112+
"owner_id_column_name": store.owner_id_column_name,
113+
} == {
114+
"config": config,
115+
"session_table": "agent_sessions",
116+
"events_table": "agent_events",
117+
"app_state_table": "agent_app_state",
118+
"user_state_table": "agent_user_state",
119+
"metadata_table": "agent_metadata",
120+
"owner_id_column_ddl": "tenant_id INTEGER",
121+
"owner_id_column_name": "tenant_id",
122+
}

0 commit comments

Comments
 (0)