diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b5f14b2..0560d197 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,8 @@ ### Removed - `lancedb.uri`. Write `lancedb.databases: {NAME: }`; a config carrying - `uri` fails to load with that message. + `uri` fails to load with that message. Configurations generated by + `init-config` through 0.81 carry `uri: ""` and must drop the key. - `HAIKU_RAG_DB`. Capabilities cover the databases the configuration places, or the `db_path` argument. - `DB_PATH` in the `app/` backend and `examples/custom_agent_agui.py`. Both load @@ -33,6 +34,12 @@ - `DatabaseRef(name, location, given)` replaces `DatabaseRef(name, uri, db_path)`; `DatabaseScope.at(path)` added; `locate_database` returns `Path | str`. `IngesterApp(config, scope)` takes a resolved scope in place of `db_path`. + `SingleDatabaseSession(ref, config)` replaces `SingleDatabaseSession(db_path, + config, source=)`. `Store.db_path` is `None` for a database behind a URI. +- Opening a configured or default database that does not exist raises + `SourceUnavailableError` naming the database and the remedy (`haiku-rag init` + or `create=True`), where the default database raised `FileNotFoundError` with + its path. A database given as a path still raises `FileNotFoundError`. - `Store(location, config)`, `connect_lancedb(location, config)`, `gather_database_info(location, config)` and `run_doctor(config, location, ...)` take the database location, a path or a URI. `ConnectionMode.of(location)` diff --git a/docs/configuration/storage.md b/docs/configuration/storage.md index 99986cec..9c0659b5 100644 --- a/docs/configuration/storage.md +++ b/docs/configuration/storage.md @@ -110,7 +110,7 @@ async with HaikuRAG(create=True) as client: The [default location](index.md#configuration-file-locations) is platform-specific (e.g., `~/Library/Application Support/haiku.rag/` on macOS). -Opening a nonexistent local database given as a path raises `FileNotFoundError`, naming the path. This prevents accidental database creation from typos or misconfigured paths. A database placed by `lancedb.databases` raises `SourceUnavailableError` instead, naming the database and not its location. +Opening a nonexistent local database given as a path raises `FileNotFoundError`, naming the path. This prevents accidental database creation from typos or misconfigured paths. A configured or default database raises `SourceUnavailableError` instead, naming the database and not its location. ## Remote Storage diff --git a/docs/python.md b/docs/python.md index c075ea47..43622aba 100644 --- a/docs/python.md +++ b/docs/python.md @@ -27,7 +27,7 @@ async with HaikuRAG("path/to/database.lancedb", read_only=True) as client: `async with` is the lifecycle. A caller that owns the client some other way releases it with `await client.aclose()`, which does the same work for every client shape. `client.close()` closes the connection to one database and nothing else, since draining the background vacuum and releasing the embedder and reranker are awaitable; it refuses a client covering several. !!! note - Databases must be explicitly created with `create=True` or via `haiku-rag init` before use. Opening a nonexistent local database given as a path raises `FileNotFoundError`, naming the path; one placed by `lancedb.databases` raises `SourceUnavailableError`, which names the database rather than its location. A path beside a configured `lancedb.databases` raises `AmbiguousDatabaseError`. + Databases must be explicitly created with `create=True` or via `haiku-rag init` before use. Opening a nonexistent local database given as a path raises `FileNotFoundError`, naming the path; a configured or default database raises `SourceUnavailableError`, which names the database rather than its location. A path beside a configured `lancedb.databases` raises `AmbiguousDatabaseError`. !!! note Read-only mode is useful for safely accessing databases without risk of modification. It blocks all write operations and downgrades an embedding provider/name mismatch to a warning instead of raising `ConfigMismatchError`. diff --git a/haiku_rag_slim/haiku/rag/client/scope.py b/haiku_rag_slim/haiku/rag/client/scope.py index 1c487d24..d52efacf 100644 --- a/haiku_rag_slim/haiku/rag/client/scope.py +++ b/haiku_rag_slim/haiku/rag/client/scope.py @@ -126,7 +126,7 @@ class DatabaseScope: if database_name is not None: if database_name not in declared: raise UnknownDatabaseError( - f"unknown database {database_name!r}; lancedb.databases names " + f"unknown database {database_name!r}; the databases are " f"{', '.join(sorted(declared))}" ) return cls( diff --git a/haiku_rag_slim/haiku/rag/client/session.py b/haiku_rag_slim/haiku/rag/client/session.py index 5b3a83b3..50f9516a 100644 --- a/haiku_rag_slim/haiku/rag/client/session.py +++ b/haiku_rag_slim/haiku/rag/client/session.py @@ -113,13 +113,15 @@ class SingleDatabaseSession: # A path the caller gave may be named: the caller knows it already. if self.ref.given: raise - failure = type(error).__name__ + failure = ( + "does not exist; create it with `haiku-rag init` or `create=True`" + if isinstance(error, FileNotFoundError) + else f"could not be opened: {type(error).__name__}" + ) if failure is not None: # Raised outside the handler: the exception carries neither a cause # nor a location-bearing context. - raise SourceUnavailableError( - f"database {self.source!r} could not be opened: {failure}" - ) + raise SourceUnavailableError(f"database {self.source!r} {failure}") self.document_repository = DocumentRepository(self.store) self.chunk_repository = ChunkRepository(self.store) self.document_item_repository = DocumentItemRepository(self.store) diff --git a/tests/ingester/test_cli.py b/tests/ingester/test_cli.py index bdad46b6..71578de5 100644 --- a/tests/ingester/test_cli.py +++ b/tests/ingester/test_cli.py @@ -557,12 +557,6 @@ class TestPlacingTheIngesterDatabase: with pytest.raises(typer.BadParameter, match="no name"): _scope_for(Path("/")) - def test_a_python_caller_cannot_pass_a_path(self): - from haiku.rag.ingester.app import IngesterApp - - with pytest.raises(TypeError): - IngesterApp(config=AppConfig(), db_path="/db/other.lancedb") # type: ignore[call-arg] # ty: ignore[unknown-argument] - def test_one_configured_database_is_accepted(self, tmp_path): """A one-entry mapping names which database to write.""" config = AppConfig( diff --git a/tests/multi_db/test_lifecycle.py b/tests/multi_db/test_lifecycle.py index f3b46f4e..05294351 100644 --- a/tests/multi_db/test_lifecycle.py +++ b/tests/multi_db/test_lifecycle.py @@ -474,6 +474,25 @@ class TestFailureNaming: assert str(tmp_path) not in str(caught.value) assert caught.value.__cause__ is None + @pytest.mark.asyncio + async def test_a_missing_default_database_names_the_remedy(self, tmp_path): + """The location stays out of the message; the way to create the + database does not.""" + from haiku.rag.config.models import AppConfig, StorageConfig + + config = AppConfig(storage=StorageConfig(data_dir=tmp_path / "empty")) + + with pytest.raises(SourceUnavailableError) as caught: + async with HaikuRAG(config=config): + pass + + message = str(caught.value) + assert "database 'haiku.rag' does not exist" in message + assert "haiku-rag init" in message + assert "create=True" in message + assert str(tmp_path) not in message + assert caught.value.__cause__ is None + @pytest.mark.asyncio async def test_a_database_given_as_a_path_keeps_its_error(self, tmp_path): """The caller gave the path, so the error may name it.""" diff --git a/tests/test_database_scope.py b/tests/test_database_scope.py index 9516ab08..210a77da 100644 --- a/tests/test_database_scope.py +++ b/tests/test_database_scope.py @@ -54,11 +54,21 @@ class TestResolution: assert scope.names == ("beta",) def test_an_unknown_name_is_refused(self): + """The message lists the databases there are, configured or default.""" config = _config(databases={"alpha": "/data/alpha.lancedb"}) - with pytest.raises(UnknownDatabaseError, match="unknown database 'nope'"): + with pytest.raises( + UnknownDatabaseError, + match="unknown database 'nope'.*the databases are alpha", + ): DatabaseScope.resolve(config, database_name="nope") + with pytest.raises( + UnknownDatabaseError, + match="unknown database 'nope'.*the databases are haiku.rag", + ): + DatabaseScope.resolve(_config(), database_name="nope") + def test_no_selector_covers_the_configured_set_in_order(self): config = _config( databases={"beta": "/data/b.lancedb", "alpha": "/data/a.lancedb"}