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:
Yiorgis Gozadinos 2026-08-25 17:38:32 +03:00
parent d9ac221ca0
commit 9a3e2b86ca
No known key found for this signature in database
5 changed files with 79 additions and 27 deletions

View file

@ -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))

View file

@ -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)

View file

@ -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

View file

@ -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):

View file

@ -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