Read a database's stored settings once
`Store` already parses the settings blob on open, so keep it: the inspector's info modal was opening the settings table, querying it and parsing the JSON a second time, and `doctor` was asking `SettingsRepository` for it on a store it already held. Creating a database refreshes it, so a new one reports the version init wrote. `gather_database_info` keeps its own parse: it goes around `Store` so a pre-migration database still reports what it can.
This commit is contained in:
parent
d9ac221ca0
commit
9a3e2b86ca
5 changed files with 79 additions and 27 deletions
|
|
@ -17,7 +17,6 @@ from haiku.rag.config.models import (
|
|||
)
|
||||
from haiku.rag.store.engine import Store, connect_lancedb
|
||||
from haiku.rag.store.info import get_database_stats
|
||||
from haiku.rag.store.repositories.settings import SettingsRepository
|
||||
from haiku.rag.store.schema import REQUIRED_TABLES
|
||||
from haiku.rag.store.upgrades import get_pending_upgrades
|
||||
|
||||
|
|
@ -725,7 +724,7 @@ async def run_db_checks(
|
|||
.select(["id", "vector", "document_id"])
|
||||
.to_arrow()
|
||||
)
|
||||
stored = await SettingsRepository(store).get_current_settings()
|
||||
stored = store.stored_settings
|
||||
stored_dim = stored.get("embeddings", {}).get("model", {}).get("vector_dim")
|
||||
actual_dim = arrow.schema.field("vector").type.list_size
|
||||
results.append(_check_vector_dimension(stored_dim, actual_dim))
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import asyncio
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
|
|
@ -51,24 +50,14 @@ async def database_lines(client: "HaikuRAG") -> list[str]:
|
|||
except Exception as e:
|
||||
return [f"[red]Failed to open database: {e}[/red]"]
|
||||
|
||||
stored_version = "unknown"
|
||||
embed_provider: str | None = None
|
||||
embed_model: str | None = None
|
||||
vector_dim: int | None = None
|
||||
|
||||
if stats["settings"]["exists"]:
|
||||
settings_tbl = await db.open_table("settings")
|
||||
arrow = await settings_tbl.query().where("id = 'settings'").limit(1).to_arrow()
|
||||
rows = arrow.to_pylist() if arrow is not None else []
|
||||
if rows:
|
||||
raw = rows[0].get("settings") or "{}"
|
||||
data = json.loads(raw) if isinstance(raw, str) else (raw or {})
|
||||
stored_version = str(data.get("version", stored_version))
|
||||
embeddings = data.get("embeddings", {})
|
||||
embed_model_obj = embeddings.get("model", {})
|
||||
embed_provider = embed_model_obj.get("provider")
|
||||
embed_model = embed_model_obj.get("name")
|
||||
vector_dim = embed_model_obj.get("vector_dim")
|
||||
# The store read these on open; a second query would re-read and re-parse
|
||||
# the same blob.
|
||||
settings = client.store.stored_settings
|
||||
stored_version = str(settings.get("version", "unknown"))
|
||||
embed_model_obj = settings.get("embeddings", {}).get("model", {})
|
||||
embed_provider = embed_model_obj.get("provider")
|
||||
embed_model = embed_model_obj.get("name")
|
||||
vector_dim = embed_model_obj.get("vector_dim")
|
||||
|
||||
num_docs = stats["documents"].get("num_rows", 0)
|
||||
num_chunks = stats["chunks"].get("num_rows", 0)
|
||||
|
|
|
|||
|
|
@ -215,6 +215,10 @@ class Store:
|
|||
# Create embedder (sync — no LanceDB needed)
|
||||
self.embedder = get_embedder(config=self._config)
|
||||
self.stored_embedding: tuple[str | None, str | None, int | None] | None = None
|
||||
# The settings blob as of open, so reporting on a database costs no
|
||||
# second read of it. Not refreshed by a later write, the same as
|
||||
# `stored_embedding`.
|
||||
self.stored_settings: dict = {}
|
||||
|
||||
async def _initialize(self):
|
||||
"""Perform async initialization: connect to LanceDB, init tables, validate."""
|
||||
|
|
@ -228,30 +232,30 @@ class Store:
|
|||
existing_tables = (await self.db.list_tables()).tables
|
||||
is_new_db = self._is_new_db or not existing_tables
|
||||
|
||||
stored_settings: dict = {}
|
||||
if not is_new_db and "settings" in existing_tables:
|
||||
self.settings_table = await self.db.open_table("settings")
|
||||
stored_settings = await self._read_stored_settings()
|
||||
self.stored_settings = await self._read_stored_settings()
|
||||
|
||||
# An existing database's chunks can only be read with the dimension they
|
||||
# were written at.
|
||||
stored_vector_dim = _stored_vector_dim(stored_settings)
|
||||
self.stored_embedding = _stored_embedding(stored_settings)
|
||||
stored_vector_dim = _stored_vector_dim(self.stored_settings)
|
||||
self.stored_embedding = _stored_embedding(self.stored_settings)
|
||||
chunk_vector_dim = stored_vector_dim or self.embedder._vector_dim
|
||||
self.ChunkRecord: type[ChunkRecordBase] = create_chunk_model(chunk_vector_dim)
|
||||
|
||||
# Initialize tables (creates them if they don't exist). For an existing
|
||||
# DB this raises MigrationRequiredError up front when migrations are
|
||||
# pending, before creating any newly-introduced table.
|
||||
await self._init_tables(is_new_db, existing_tables, stored_settings)
|
||||
await self._init_tables(is_new_db, existing_tables, self.stored_settings)
|
||||
|
||||
# Set version for new databases.
|
||||
if is_new_db and not self._read_only:
|
||||
await self._set_initial_version()
|
||||
self.stored_settings = await self._read_stored_settings()
|
||||
|
||||
# Validate config compatibility after connection is established
|
||||
if not self._skip_validation:
|
||||
await self._validate_configuration(stored_settings)
|
||||
await self._validate_configuration(self.stored_settings)
|
||||
|
||||
async def __aenter__(self):
|
||||
# If _initialize connects to LanceDB but then fails (e.g. migration
|
||||
|
|
|
|||
|
|
@ -4,6 +4,32 @@ from haiku.rag.store.engine import Store
|
|||
from haiku.rag.store.info import get_database_stats
|
||||
|
||||
|
||||
class TestStoredSettings:
|
||||
@pytest.mark.asyncio
|
||||
async def test_a_new_database_carries_the_version_it_was_created_with(
|
||||
self, temp_db_path
|
||||
):
|
||||
"""Creating writes the settings row, so the store has to report it
|
||||
rather than the emptiness it opened on."""
|
||||
from importlib import metadata
|
||||
|
||||
async with Store(temp_db_path, create=True) as store:
|
||||
assert store.stored_settings["version"] == metadata.version(
|
||||
"haiku.rag-slim"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_an_existing_database_carries_its_stored_settings(self, temp_db_path):
|
||||
"""Read once on open: reporting on a database reads them from here
|
||||
instead of querying the settings table again."""
|
||||
async with Store(temp_db_path, create=True) as store:
|
||||
written = store.stored_settings
|
||||
|
||||
async with Store(temp_db_path) as store:
|
||||
assert store.stored_settings == written
|
||||
assert store.stored_settings["embeddings"]["model"]["vector_dim"] > 0
|
||||
|
||||
|
||||
class TestGetDatabaseStats:
|
||||
@pytest.mark.asyncio
|
||||
async def test_empty_database_stats(self, temp_db_path):
|
||||
|
|
|
|||
|
|
@ -403,6 +403,7 @@ class TestReportingReusesTheConnection:
|
|||
client = MagicMock()
|
||||
client.store.db = connection
|
||||
client.store.db_path = tmp_path
|
||||
client.store.stored_settings = {}
|
||||
client.store._connection_mode = ConnectionMode.LOCAL
|
||||
|
||||
lines = await database_lines(client)
|
||||
|
|
@ -410,6 +411,39 @@ class TestReportingReusesTheConnection:
|
|||
assert asked == [connection]
|
||||
assert any("documents" in line for line in lines)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_settings_come_from_the_store_that_parsed_them(
|
||||
self, tmp_path, monkeypatch
|
||||
):
|
||||
"""The store read and parsed the settings blob on open, so reporting
|
||||
reads it from there instead of querying the settings table again."""
|
||||
from haiku.rag.inspector.widgets.info_modal import database_lines
|
||||
from haiku.rag.store.engine import ConnectionMode
|
||||
|
||||
async def fake_stats(db): # noqa: ARG001
|
||||
return {
|
||||
"documents": {"num_rows": 1},
|
||||
"document_meta": {"num_rows": 1},
|
||||
"chunks": {"num_rows": 1},
|
||||
}
|
||||
|
||||
monkeypatch.setattr("haiku.rag.store.info.get_database_stats", fake_stats)
|
||||
|
||||
client = MagicMock()
|
||||
client.store.db_path = tmp_path
|
||||
client.store._connection_mode = ConnectionMode.LOCAL
|
||||
client.store.stored_settings = {
|
||||
"version": "1.2.3",
|
||||
"embeddings": {
|
||||
"model": {"provider": "ollama", "name": "embed", "vector_dim": 7}
|
||||
},
|
||||
}
|
||||
|
||||
lines = await database_lines(client)
|
||||
|
||||
assert any("1.2.3" in line for line in lines)
|
||||
assert any("ollama/embed (dim: 7)" in line for line in lines)
|
||||
|
||||
|
||||
class TestReportingEachDatabase:
|
||||
@pytest.mark.asyncio
|
||||
|
|
|
|||
Loading…
Reference in a new issue