Report a stemless --db path as a usage error and separate the two lancedb.uri migrations

An empty lancedb.uri, which init-config used to emit, is told to remove the
key; only a non-empty value is told the databases spelling. haiku-rag --db
and haiku-ingester --db turn a path with no stem into typer.BadParameter.
This commit is contained in:
Yiorgis Gozadinos 2026-09-03 14:54:37 +03:00
parent a04a16c717
commit 3814b2bc78
No known key found for this signature in database
7 changed files with 52 additions and 6 deletions

View file

@ -24,7 +24,8 @@
- Every database has a name: the key in `lancedb.databases`, or the path's stem - Every database has a name: the key in `lancedb.databases`, or the path's stem
for `--db PATH`, `db_path=` and the default database, which is the entry for `--db PATH`, `db_path=` and the default database, which is the entry
`haiku.rag` under `storage.data_dir` and selectable by that name. `haiku.rag` under `storage.data_dir` and selectable by that name.
`SearchResult.source`, `Document.source` and `Citation.source` always carry it. `SearchResult.source`, `Document.source` and `Citation.source` carry it on
every value a database produces.
- `db_path=` beside a configured `lancedb.databases` raises - `db_path=` beside a configured `lancedb.databases` raises
`AmbiguousDatabaseError` (`HaikuRAG`, `create_capability`, `create_mcp_server`, `AmbiguousDatabaseError` (`HaikuRAG`, `create_capability`, `create_mcp_server`,
`Sandbox`). `haiku-rag --db PATH` and `haiku-ingester --db PATH` open that path `Sandbox`). `haiku-rag --db PATH` and `haiku-ingester --db PATH` open that path

View file

@ -102,7 +102,10 @@ def resolve_scope(
"pass --db or --db-name, not both: they name the same thing" "pass --db or --db-name, not both: they name the same thing"
) )
if db is not None: if db is not None:
try:
return DatabaseScope.at(db) return DatabaseScope.at(db)
except ValueError as error:
raise typer.BadParameter(str(error), param_hint="--db") from error
scope = DatabaseScope.resolve(get_config(), database_name=_db_name) scope = DatabaseScope.resolve(get_config(), database_name=_db_name)
if scope.covers_multiple and not covers_set: if scope.covers_multiple and not covers_set:
raise AmbiguousDatabaseError( raise AmbiguousDatabaseError(

View file

@ -120,10 +120,15 @@ class LanceDBConfig(ConfigModel):
@classmethod @classmethod
def _uri_names_its_replacement(cls, data: Any) -> Any: def _uri_names_its_replacement(cls, data: Any) -> Any:
if isinstance(data, dict) and "uri" in data: if isinstance(data, dict) and "uri" in data:
if str(data["uri"]).strip():
raise ValueError( raise ValueError(
"lancedb.uri was removed; write lancedb.databases: {NAME: " "lancedb.uri was removed; write lancedb.databases: {NAME: "
f"{data['uri']!r}}} instead" f"{data['uri']!r}}} instead"
) )
raise ValueError(
"lancedb.uri was removed; remove the empty key. With no "
"lancedb.databases the database is haiku.rag under storage.data_dir"
)
return data return data
@model_validator(mode="after") @model_validator(mode="after")

View file

@ -224,7 +224,12 @@ def _scope_for(db: Path | None) -> "DatabaseScope | None":
is configured. None leaves placement to the configuration.""" is configured. None leaves placement to the configuration."""
from haiku.rag.client.scope import DatabaseScope from haiku.rag.client.scope import DatabaseScope
return DatabaseScope.at(db) if db is not None else None if db is None:
return None
try:
return DatabaseScope.at(db)
except ValueError as error:
raise typer.BadParameter(str(error), param_hint="--db") from error
@_cli.command("serve") @_cli.command("serve")

View file

@ -547,6 +547,16 @@ class TestPlacingTheIngesterDatabase:
tmp_path / "local.lancedb" tmp_path / "local.lancedb"
) )
def test_a_path_without_a_stem_is_a_usage_error(self):
from pathlib import Path
import typer
from haiku.rag.ingester.cli import _scope_for
with pytest.raises(typer.BadParameter, match="no name"):
_scope_for(Path("/"))
def test_a_python_caller_cannot_pass_a_path(self): def test_a_python_caller_cannot_pass_a_path(self):
from haiku.rag.ingester.app import IngesterApp from haiku.rag.ingester.app import IngesterApp

View file

@ -169,6 +169,16 @@ class TestOneDatabaseCommands:
assert scope.names == ("other",) assert scope.names == ("other",)
assert not scope.covers_multiple assert not scope.covers_multiple
def test_a_path_without_a_stem_is_a_usage_error(self, monkeypatch):
"""A path that names no database is the operator's mistake, reported as
one."""
import typer
self._install(monkeypatch)
with pytest.raises(typer.BadParameter, match="no name"):
resolve_scope(Path("/"))
def test_no_configured_databases_is_allowed(self, monkeypatch, tmp_path): def test_no_configured_databases_is_allowed(self, monkeypatch, tmp_path):
import haiku.rag.config as config_module import haiku.rag.config as config_module

View file

@ -812,7 +812,19 @@ def test_lancedb_uri_is_refused_with_the_replacement_named():
message = str(raised.value) message = str(raised.value)
assert "lancedb.uri" in message assert "lancedb.uri" in message
assert "lancedb.databases" in message assert "lancedb.databases: {NAME: 's3://bucket/notes.lancedb'}" in message
with pytest.raises(ValidationError, match="lancedb.databases"): with pytest.raises(ValidationError, match="lancedb.databases"):
LanceDBConfig.model_validate({"uri": "/data/notes.lancedb"}) LanceDBConfig.model_validate({"uri": "/data/notes.lancedb"})
def test_an_empty_lancedb_uri_is_refused_with_removal_as_the_remedy():
"""Generated configs carried `uri: ""` for the local default. The remedy is
to delete the key, never a mapping with an empty location."""
with pytest.raises(ValidationError) as raised:
AppConfig.model_validate({"lancedb": {"uri": ""}})
message = str(raised.value)
assert "lancedb.uri" in message
assert "remove" in message
assert "{NAME" not in message