diff --git a/haiku_rag_slim/haiku/rag/client/__init__.py b/haiku_rag_slim/haiku/rag/client/__init__.py index 70a7ad21..c69240c7 100644 --- a/haiku_rag_slim/haiku/rag/client/__init__.py +++ b/haiku_rag_slim/haiku/rag/client/__init__.py @@ -127,8 +127,12 @@ class HaikuRAG: @property def is_read_only(self) -> bool: - """Whether the client is in read-only mode.""" - return self.store.is_read_only + """Whether the client is in read-only mode. + + The mode the client was opened with, which is the mode every database it + covers is opened with. A client covering a set has no store to ask. + """ + return self._read_only @property def embedder(self) -> "EmbedderWrapper": diff --git a/haiku_rag_slim/haiku/rag/config/models.py b/haiku_rag_slim/haiku/rag/config/models.py index 06074531..83e05eee 100644 --- a/haiku_rag_slim/haiku/rag/config/models.py +++ b/haiku_rag_slim/haiku/rag/config/models.py @@ -123,6 +123,13 @@ class LanceDBConfig(ConfigModel): "lancedb.uri and lancedb.databases are mutually exclusive: " "use uri for one database, databases for several" ) + for name, location in self.databases.items(): + # A blank name is falsy, so source routing reads it as absent; a + # blank location resolves to the working directory. + if not name.strip(): + raise ValueError("lancedb.databases has an entry with no name") + if not location.strip(): + raise ValueError(f"lancedb.databases[{name}] has no location") return self diff --git a/tests/test_multi_db.py b/tests/test_multi_db.py index b0a7d135..86b5dda9 100644 --- a/tests/test_multi_db.py +++ b/tests/test_multi_db.py @@ -28,6 +28,23 @@ class TestConfig: assert LanceDBConfig(uri="s3://b/one.lancedb").databases == {} +class TestNamingIsRequired: + def test_a_blank_name_is_rejected(self): + """An unnamed database is unreachable: every source check reads the + empty name as no name at all.""" + with pytest.raises(ValidationError, match="entry with no name"): + LanceDBConfig(databases={"": "/tmp/a.lancedb"}) + with pytest.raises(ValidationError, match="entry with no name"): + LanceDBConfig(databases={" ": "/tmp/a.lancedb"}) + + def test_a_blank_location_is_rejected(self): + """A blank location resolves to the working directory.""" + with pytest.raises( + ValidationError, match=r"databases\[alpha\] has no location" + ): + LanceDBConfig(databases={"alpha": ""}) + + def _config(tmp_path, names) -> AppConfig: return AppConfig( lancedb=LanceDBConfig( @@ -279,6 +296,20 @@ class TestLookupByIdentifier: assert await rag.get_document_by_uri("test://nowhere") is None +class TestReadOnlyMode: + @pytest.mark.asyncio + async def test_a_client_covering_a_set_reports_its_mode(self, tmp_path): + """A client covering a set has no store of its own to ask.""" + config = _config(tmp_path, ["alpha", "beta"]) + await _seed(config, "alpha", ["alpha one"]) + await _seed(config, "beta", ["beta one"]) + + async with HaikuRAG(config=config, read_only=True) as rag: + assert rag.is_read_only is True + async with HaikuRAG(config=config) as rag: + assert rag.is_read_only is False + + class TestDocumentsNameTheirDatabase: """A listing that spans databases is unreadable when the documents do not say which one they came from, the same reason a search result carries one."""