llama-stack-mirror/tests/unit/utils/sqlstore/test_sqlstore.py
Ashwin Bharambe c2c5ab622c feat(storage): share sql/kv instances and add upsert support
Sql stores now share a single SqlAlchemySqlStoreImpl per backend, and kvstore_impl caches instances per (backend, namespace). This avoids spawning multiple SQLite connections for the same file, reducing lock contention and aligning the cache story for all backends.

Added an async upsert API (with SQLite/Postgres dialect inserts) and routed it through AuthorizedSqlStore, then switched conversations and responses to call it. Using native ON CONFLICT DO UPDATE eliminates the insert-then-update retry window that previously caused long WAL lock retries.

Introduced an opt-in conversation stress test that mirrors the recorded prompts from test_conversation_multi_turn_and_streaming while fanning them out across many threads. This gives us a fast local way to hammer the conversations/responses sync path when investigating lockups.
2025-11-12 11:29:32 -08:00

497 lines
17 KiB
Python

# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under the terms described in the LICENSE file in
# the root directory of this source tree.
import time
from tempfile import TemporaryDirectory
import pytest
from llama_stack.providers.utils.sqlstore.api import ColumnDefinition, ColumnType
from llama_stack.providers.utils.sqlstore.sqlalchemy_sqlstore import SqlAlchemySqlStoreImpl
from llama_stack.providers.utils.sqlstore.sqlstore import SqliteSqlStoreConfig
async def test_sqlite_sqlstore():
with TemporaryDirectory() as tmp_dir:
db_name = "test.db"
sqlstore = SqlAlchemySqlStoreImpl(
SqliteSqlStoreConfig(
db_path=tmp_dir + "/" + db_name,
)
)
await sqlstore.create_table(
table="test",
schema={
"id": ColumnType.INTEGER,
"name": ColumnType.STRING,
},
)
await sqlstore.insert("test", {"id": 1, "name": "test"})
await sqlstore.insert("test", {"id": 12, "name": "test12"})
result = await sqlstore.fetch_all("test")
assert result.data == [{"id": 1, "name": "test"}, {"id": 12, "name": "test12"}]
assert result.has_more is False
row = await sqlstore.fetch_one("test", {"id": 1})
assert row == {"id": 1, "name": "test"}
row = await sqlstore.fetch_one("test", {"name": "test12"})
assert row == {"id": 12, "name": "test12"}
# order by
result = await sqlstore.fetch_all("test", order_by=[("id", "asc")])
assert result.data == [{"id": 1, "name": "test"}, {"id": 12, "name": "test12"}]
result = await sqlstore.fetch_all("test", order_by=[("id", "desc")])
assert result.data == [{"id": 12, "name": "test12"}, {"id": 1, "name": "test"}]
# limit
result = await sqlstore.fetch_all("test", limit=1)
assert result.data == [{"id": 1, "name": "test"}]
assert result.has_more is True
# update
await sqlstore.update("test", {"name": "test123"}, {"id": 1})
row = await sqlstore.fetch_one("test", {"id": 1})
assert row == {"id": 1, "name": "test123"}
# delete
await sqlstore.delete("test", {"id": 1})
result = await sqlstore.fetch_all("test")
assert result.data == [{"id": 12, "name": "test12"}]
assert result.has_more is False
async def test_sqlstore_upsert_support():
with TemporaryDirectory() as tmp_dir:
db_path = tmp_dir + "/upsert.db"
store = SqlAlchemySqlStoreImpl(SqliteSqlStoreConfig(db_path=db_path))
await store.create_table(
"items",
{
"id": ColumnDefinition(type=ColumnType.STRING, primary_key=True),
"value": ColumnType.STRING,
"updated_at": ColumnType.INTEGER,
},
)
await store.upsert(
table="items",
data={"id": "item_1", "value": "first", "updated_at": 1},
conflict_columns=["id"],
)
row = await store.fetch_one("items", {"id": "item_1"})
assert row == {"id": "item_1", "value": "first", "updated_at": 1}
await store.upsert(
table="items",
data={"id": "item_1", "value": "second", "updated_at": 2},
conflict_columns=["id"],
update_columns=["value", "updated_at"],
)
row = await store.fetch_one("items", {"id": "item_1"})
assert row == {"id": "item_1", "value": "second", "updated_at": 2}
async def test_sqlstore_pagination_basic():
"""Test basic pagination functionality at the SQL store level."""
with TemporaryDirectory() as tmp_dir:
db_path = tmp_dir + "/test.db"
store = SqlAlchemySqlStoreImpl(SqliteSqlStoreConfig(db_path=db_path))
# Create test table
await store.create_table(
"test_records",
{
"id": ColumnType.STRING,
"created_at": ColumnType.INTEGER,
"name": ColumnType.STRING,
},
)
# Insert test data
base_time = int(time.time())
test_data = [
{"id": "zebra", "created_at": base_time + 1, "name": "First"},
{"id": "apple", "created_at": base_time + 2, "name": "Second"},
{"id": "moon", "created_at": base_time + 3, "name": "Third"},
{"id": "banana", "created_at": base_time + 4, "name": "Fourth"},
{"id": "car", "created_at": base_time + 5, "name": "Fifth"},
]
for record in test_data:
await store.insert("test_records", record)
# Test 1: First page (no cursor)
result = await store.fetch_all(
table="test_records",
order_by=[("created_at", "desc")],
limit=2,
)
assert len(result.data) == 2
assert result.data[0]["id"] == "car" # Most recent first
assert result.data[1]["id"] == "banana"
assert result.has_more is True
# Test 2: Second page using cursor
result2 = await store.fetch_all(
table="test_records",
order_by=[("created_at", "desc")],
cursor=("id", "banana"),
limit=2,
)
assert len(result2.data) == 2
assert result2.data[0]["id"] == "moon"
assert result2.data[1]["id"] == "apple"
assert result2.has_more is True
# Test 3: Final page
result3 = await store.fetch_all(
table="test_records",
order_by=[("created_at", "desc")],
cursor=("id", "apple"),
limit=2,
)
assert len(result3.data) == 1
assert result3.data[0]["id"] == "zebra"
assert result3.has_more is False
async def test_sqlstore_pagination_with_filter():
"""Test pagination with WHERE conditions."""
with TemporaryDirectory() as tmp_dir:
db_path = tmp_dir + "/test.db"
store = SqlAlchemySqlStoreImpl(SqliteSqlStoreConfig(db_path=db_path))
# Create test table
await store.create_table(
"test_records",
{
"id": ColumnType.STRING,
"created_at": ColumnType.INTEGER,
"category": ColumnType.STRING,
},
)
# Insert test data with categories
base_time = int(time.time())
test_data = [
{"id": "xyz", "created_at": base_time + 1, "category": "A"},
{"id": "def", "created_at": base_time + 2, "category": "B"},
{"id": "pqr", "created_at": base_time + 3, "category": "A"},
{"id": "abc", "created_at": base_time + 4, "category": "B"},
]
for record in test_data:
await store.insert("test_records", record)
# Test pagination with filter
result = await store.fetch_all(
table="test_records",
where={"category": "A"},
order_by=[("created_at", "desc")],
limit=1,
)
assert len(result.data) == 1
assert result.data[0]["id"] == "pqr" # Most recent category A
assert result.has_more is True
# Second page with filter
result2 = await store.fetch_all(
table="test_records",
where={"category": "A"},
order_by=[("created_at", "desc")],
cursor=("id", "pqr"),
limit=1,
)
assert len(result2.data) == 1
assert result2.data[0]["id"] == "xyz"
assert result2.has_more is False
async def test_sqlstore_pagination_ascending_order():
"""Test pagination with ascending order."""
with TemporaryDirectory() as tmp_dir:
db_path = tmp_dir + "/test.db"
store = SqlAlchemySqlStoreImpl(SqliteSqlStoreConfig(db_path=db_path))
# Create test table
await store.create_table(
"test_records",
{
"id": ColumnType.STRING,
"created_at": ColumnType.INTEGER,
},
)
# Insert test data
base_time = int(time.time())
test_data = [
{"id": "gamma", "created_at": base_time + 1},
{"id": "alpha", "created_at": base_time + 2},
{"id": "beta", "created_at": base_time + 3},
]
for record in test_data:
await store.insert("test_records", record)
# Test ascending order
result = await store.fetch_all(
table="test_records",
order_by=[("created_at", "asc")],
limit=1,
)
assert len(result.data) == 1
assert result.data[0]["id"] == "gamma" # Oldest first
assert result.has_more is True
# Second page with ascending order
result2 = await store.fetch_all(
table="test_records",
order_by=[("created_at", "asc")],
cursor=("id", "gamma"),
limit=1,
)
assert len(result2.data) == 1
assert result2.data[0]["id"] == "alpha"
assert result2.has_more is True
async def test_sqlstore_pagination_multi_column_ordering_error():
"""Test that multi-column ordering raises an error when using cursor pagination."""
with TemporaryDirectory() as tmp_dir:
db_path = tmp_dir + "/test.db"
store = SqlAlchemySqlStoreImpl(SqliteSqlStoreConfig(db_path=db_path))
# Create test table
await store.create_table(
"test_records",
{
"id": ColumnType.STRING,
"priority": ColumnType.INTEGER,
"created_at": ColumnType.INTEGER,
},
)
await store.insert("test_records", {"id": "task1", "priority": 1, "created_at": 12345})
# Test that multi-column ordering with cursor raises error
with pytest.raises(ValueError, match="Cursor pagination only supports single-column ordering, got 2 columns"):
await store.fetch_all(
table="test_records",
order_by=[("priority", "asc"), ("created_at", "desc")],
cursor=("id", "task1"),
limit=2,
)
# Test that multi-column ordering without cursor works fine
result = await store.fetch_all(
table="test_records",
order_by=[("priority", "asc"), ("created_at", "desc")],
limit=2,
)
assert len(result.data) == 1
assert result.data[0]["id"] == "task1"
async def test_sqlstore_pagination_cursor_requires_order_by():
"""Test that cursor pagination requires order_by parameter."""
with TemporaryDirectory() as tmp_dir:
db_path = tmp_dir + "/test.db"
store = SqlAlchemySqlStoreImpl(SqliteSqlStoreConfig(db_path=db_path))
await store.create_table("test_records", {"id": ColumnType.STRING})
await store.insert("test_records", {"id": "task1"})
# Test that cursor without order_by raises error
with pytest.raises(ValueError, match="order_by is required when using cursor pagination"):
await store.fetch_all(
table="test_records",
cursor=("id", "task1"),
)
async def test_sqlstore_pagination_error_handling():
"""Test error handling for invalid columns and cursor IDs."""
with TemporaryDirectory() as tmp_dir:
db_path = tmp_dir + "/test.db"
store = SqlAlchemySqlStoreImpl(SqliteSqlStoreConfig(db_path=db_path))
# Create test table
await store.create_table(
"test_records",
{
"id": ColumnType.STRING,
"name": ColumnType.STRING,
},
)
await store.insert("test_records", {"id": "test1", "name": "Test"})
# Test invalid cursor tuple format
with pytest.raises(ValueError, match="Cursor must be a tuple of"):
await store.fetch_all(
table="test_records",
order_by=[("name", "asc")],
cursor="invalid", # Should be tuple
)
# Test invalid cursor_key_column
with pytest.raises(ValueError, match="Cursor key column 'nonexistent' not found in table"):
await store.fetch_all(
table="test_records",
order_by=[("name", "asc")],
cursor=("nonexistent", "test1"),
)
# Test invalid order_by column
with pytest.raises(ValueError, match="Column 'invalid_col' not found in table"):
await store.fetch_all(
table="test_records",
order_by=[("invalid_col", "asc")],
)
# Test nonexistent cursor_id
with pytest.raises(ValueError, match="Record with id='nonexistent' not found in table"):
await store.fetch_all(
table="test_records",
order_by=[("name", "asc")],
cursor=("id", "nonexistent"),
)
async def test_where_operator_gt_and_update_delete():
with TemporaryDirectory() as tmp_dir:
db_path = tmp_dir + "/test.db"
store = SqlAlchemySqlStoreImpl(SqliteSqlStoreConfig(db_path=db_path))
await store.create_table(
"items",
{
"id": ColumnType.INTEGER,
"value": ColumnType.INTEGER,
"name": ColumnType.STRING,
},
)
await store.insert("items", {"id": 1, "value": 10, "name": "one"})
await store.insert("items", {"id": 2, "value": 20, "name": "two"})
await store.insert("items", {"id": 3, "value": 30, "name": "three"})
result = await store.fetch_all("items", where={"value": {">": 15}})
assert {r["id"] for r in result.data} == {2, 3}
row = await store.fetch_one("items", where={"value": {">=": 30}})
assert row["id"] == 3
await store.update("items", {"name": "small"}, {"value": {"<": 25}})
rows = (await store.fetch_all("items")).data
names = {r["id"]: r["name"] for r in rows}
assert names[1] == "small"
assert names[2] == "small"
assert names[3] == "three"
await store.delete("items", {"id": {"==": 2}})
rows_after = (await store.fetch_all("items")).data
assert {r["id"] for r in rows_after} == {1, 3}
async def test_batch_insert():
with TemporaryDirectory() as tmp_dir:
db_path = tmp_dir + "/test.db"
store = SqlAlchemySqlStoreImpl(SqliteSqlStoreConfig(db_path=db_path))
await store.create_table(
"batch_test",
{
"id": ColumnType.INTEGER,
"name": ColumnType.STRING,
"value": ColumnType.INTEGER,
},
)
batch_data = [
{"id": 1, "name": "first", "value": 10},
{"id": 2, "name": "second", "value": 20},
{"id": 3, "name": "third", "value": 30},
]
await store.insert("batch_test", batch_data)
result = await store.fetch_all("batch_test", order_by=[("id", "asc")])
assert result.data == batch_data
async def test_where_operator_edge_cases():
with TemporaryDirectory() as tmp_dir:
db_path = tmp_dir + "/test.db"
store = SqlAlchemySqlStoreImpl(SqliteSqlStoreConfig(db_path=db_path))
await store.create_table(
"events",
{"id": ColumnType.STRING, "ts": ColumnType.INTEGER},
)
base = 1024
await store.insert("events", {"id": "a", "ts": base - 10})
await store.insert("events", {"id": "b", "ts": base + 10})
row = await store.fetch_one("events", where={"id": "a"})
assert row["id"] == "a"
with pytest.raises(ValueError, match="Unsupported operator"):
await store.fetch_all("events", where={"ts": {"!=": base}})
async def test_sqlstore_pagination_custom_key_column():
"""Test pagination with custom primary key column (not 'id')."""
with TemporaryDirectory() as tmp_dir:
db_path = tmp_dir + "/test.db"
store = SqlAlchemySqlStoreImpl(SqliteSqlStoreConfig(db_path=db_path))
# Create test table with custom primary key
await store.create_table(
"custom_table",
{
"uuid": ColumnType.STRING,
"timestamp": ColumnType.INTEGER,
"data": ColumnType.STRING,
},
)
# Insert test data
base_time = int(time.time())
test_data = [
{"uuid": "uuid-alpha", "timestamp": base_time + 1, "data": "First"},
{"uuid": "uuid-beta", "timestamp": base_time + 2, "data": "Second"},
{"uuid": "uuid-gamma", "timestamp": base_time + 3, "data": "Third"},
]
for record in test_data:
await store.insert("custom_table", record)
# Test pagination with custom key column
result = await store.fetch_all(
table="custom_table",
order_by=[("timestamp", "desc")],
limit=2,
)
assert len(result.data) == 2
assert result.data[0]["uuid"] == "uuid-gamma" # Most recent
assert result.data[1]["uuid"] == "uuid-beta"
assert result.has_more is True
# Second page using custom key column
result2 = await store.fetch_all(
table="custom_table",
order_by=[("timestamp", "desc")],
cursor=("uuid", "uuid-beta"), # Use uuid as key column
limit=2,
)
assert len(result2.data) == 1
assert result2.data[0]["uuid"] == "uuid-alpha"
assert result2.has_more is False