Opening a database ran `list_tables` three times, opened the settings table
twice, and read and parsed the same settings row three times: once for the stored
vector dimension, once for the version behind the migration check, and once for
config validation. On object storage each of those is a round trip.
`_initialize` now reads both once and threads them down. `_init_tables` and
`_check_migrations` take what it read instead of fetching their own copy, and
`validate_config_compatibility` accepts the settings it should compare against,
still reading for itself when called directly.
Passing the pre-init read to validation is equivalent: nothing between the read
and the validation rewrites `embeddings`, which is all it compares.
The settings read no longer swallows every exception. It did before, when the
only consequence was falling back to the configured vector dimension; now the
same empty result feeds the migration check, where it would read as version
0.0.0 and declare every migration pending. Only decode failures are tolerated,
and a decoded non-object normalizes to {} rather than reaching callers that
expect a mapping.
71 lines
2.3 KiB
Python
71 lines
2.3 KiB
Python
import lancedb
|
|
import pytest
|
|
|
|
from haiku.rag.store.engine import Store
|
|
|
|
|
|
@pytest.fixture
|
|
def counts(monkeypatch):
|
|
"""Count the connection-level calls an open makes."""
|
|
tally: dict[str, int] = {"list_tables": 0, "open_settings": 0, "settings_query": 0}
|
|
|
|
list_tables = lancedb.AsyncConnection.list_tables
|
|
open_table = lancedb.AsyncConnection.open_table
|
|
query = lancedb.AsyncTable.query
|
|
|
|
async def counted_list_tables(self, *args, **kwargs):
|
|
tally["list_tables"] += 1
|
|
return await list_tables(self, *args, **kwargs)
|
|
|
|
async def counted_open_table(self, name, *args, **kwargs):
|
|
if name == "settings":
|
|
tally["open_settings"] += 1
|
|
return await open_table(self, name, *args, **kwargs)
|
|
|
|
def counted_query(self):
|
|
if self.name == "settings":
|
|
tally["settings_query"] += 1
|
|
return query(self)
|
|
|
|
monkeypatch.setattr(lancedb.AsyncConnection, "list_tables", counted_list_tables)
|
|
monkeypatch.setattr(lancedb.AsyncConnection, "open_table", counted_open_table)
|
|
monkeypatch.setattr(lancedb.AsyncTable, "query", counted_query)
|
|
return tally
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_reopening_reads_the_table_list_and_settings_once(temp_db_path, counts):
|
|
async with Store(temp_db_path, create=True):
|
|
pass
|
|
for key in counts:
|
|
counts[key] = 0
|
|
|
|
async with Store(temp_db_path):
|
|
pass
|
|
|
|
assert counts["list_tables"] == 1
|
|
assert counts["open_settings"] == 1
|
|
assert counts["settings_query"] == 1
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_storage_failures_propagate(temp_db_path):
|
|
"""A read failure must not read as empty settings: the migration check would
|
|
then see version 0.0.0 and declare every migration pending."""
|
|
async with Store(temp_db_path, create=True) as store:
|
|
|
|
def boom():
|
|
raise RuntimeError("s3 is having a day")
|
|
|
|
store.settings_table.query = boom
|
|
|
|
with pytest.raises(RuntimeError, match="s3 is having a day"):
|
|
await store._read_stored_settings()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_non_dict_settings_read_as_empty(temp_db_path):
|
|
async with Store(temp_db_path, create=True) as store:
|
|
await store.settings_table.update({"settings": "[]"}, where="id = 'settings'")
|
|
|
|
assert await store._read_stored_settings() == {}
|