diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b0ddd47..7dad500f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,9 +50,11 @@ - **Document Creation**: Optimized `create_document` to skip unnecessary DoclingDocument conversion when chunks are pre-provided - **FileReader**: Error messages now include both original exception details and file path for easier debugging +- **Database Auto-creation**: Read operations (search, list, get, ask, research) no longer auto-create empty databases. Write operations (add, add-src, delete, rebuild) still create the database as needed. This prevents the confusing scenario where a search query creates an empty database. Fixes issue #137. ### Removed +- **BREAKING**: Removed `disable_autocreate` config option - the behavior is now automatic based on operation type - **BREAKING**: Removed legacy `ResearchStream` and `ResearchStreamEvent` classes (replaced by AG-UI event protocol) ## [0.15.0] - 2025-11-07 diff --git a/docs/configuration.md b/docs/configuration.md index b2e91cb4..cc34c9e6 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -52,7 +52,6 @@ environment: production storage: data_dir: "" # Empty = use default platform location - disable_autocreate: false vacuum_retention_seconds: 86400 monitor: @@ -561,16 +560,14 @@ Authentication is handled through standard cloud provider credentials (AWS CLI, **Note:** Table optimization is automatically handled by LanceDB Cloud (`db://` URIs) and is disabled for better performance. For object storage backends (S3, Azure, GCS), optimization is still performed locally. -#### Disable database auto-creation +#### Database Auto-creation -By default, haiku.rag creates the local LanceDB directory and required tables on first use. To prevent accidental database creation and fail fast if a database hasn't been set up yet: +haiku.rag intelligently handles database creation based on operation type: -```yaml -storage: - disable_autocreate: true -``` +- **Write operations** (add, add-src, delete, rebuild): Automatically create the database and required tables if they don't exist +- **Read operations** (list, get, search, ask, research): Fail with a clear error if the database doesn't exist -When enabled, for local paths, haiku.rag errors if the LanceDB directory does not exist, and it will not create parent directories. +This prevents the common mistake where a search query accidentally creates an empty database. To initialize your database, simply add your first document using `haiku-rag add` or `haiku-rag add-src`. ### Document Processing diff --git a/haiku_rag_slim/haiku/rag/app.py b/haiku_rag_slim/haiku/rag/app.py index ba2e77c3..01bf2401 100644 --- a/haiku_rag_slim/haiku/rag/app.py +++ b/haiku_rag_slim/haiku/rag/app.py @@ -137,7 +137,9 @@ class HaikuRAGApp: ) async def list_documents(self, filter: str | None = None): - async with HaikuRAG(db_path=self.db_path, config=self.config) as self.client: + async with HaikuRAG( + db_path=self.db_path, config=self.config, allow_create=False + ) as self.client: documents = await self.client.list_documents(filter=filter) for doc in documents: self._rich_print_document(doc, truncate=True) @@ -170,7 +172,9 @@ class HaikuRAGApp: ) async def get_document(self, doc_id: str): - async with HaikuRAG(db_path=self.db_path, config=self.config) as self.client: + async with HaikuRAG( + db_path=self.db_path, config=self.config, allow_create=False + ) as self.client: doc = await self.client.get_document_by_id(doc_id) if doc is None: self.console.print(f"[red]Document with id {doc_id} not found.[/red]") @@ -190,7 +194,9 @@ class HaikuRAGApp: ) async def search(self, query: str, limit: int = 5, filter: str | None = None): - async with HaikuRAG(db_path=self.db_path, config=self.config) as self.client: + async with HaikuRAG( + db_path=self.db_path, config=self.config, allow_create=False + ) as self.client: results = await self.client.search(query, limit=limit, filter=filter) if not results: self.console.print("[yellow]No results found.[/yellow]") @@ -213,7 +219,9 @@ class HaikuRAGApp: deep: Use deep QA mode (multi-step reasoning) verbose: Show verbose output """ - async with HaikuRAG(db_path=self.db_path, config=self.config) as self.client: + async with HaikuRAG( + db_path=self.db_path, config=self.config, allow_create=False + ) as self.client: try: if deep: from haiku.rag.graph.deep_qa.dependencies import DeepQAContext @@ -258,7 +266,9 @@ class HaikuRAGApp: question: The research question verbose: Show AG-UI event stream during execution """ - async with HaikuRAG(db_path=self.db_path, config=self.config) as client: + async with HaikuRAG( + db_path=self.db_path, config=self.config, allow_create=False + ) as client: try: self.console.print("[bold cyan]Starting research[/bold cyan]") self.console.print(f"[bold blue]Question:[/bold blue] {question}") diff --git a/haiku_rag_slim/haiku/rag/client.py b/haiku_rag_slim/haiku/rag/client.py index f4cb1c9b..664d23f3 100644 --- a/haiku_rag_slim/haiku/rag/client.py +++ b/haiku_rag_slim/haiku/rag/client.py @@ -28,6 +28,7 @@ class HaikuRAG: db_path: Path | None = None, config: AppConfig = Config, skip_validation: bool = False, + allow_create: bool = True, ): """Initialize the RAG client with a database path. @@ -35,12 +36,17 @@ class HaikuRAG: db_path: Path to the database file. If None, uses config.storage.data_dir. config: Configuration to use. Defaults to global Config. skip_validation: Whether to skip configuration validation on database load. + allow_create: Whether to allow database creation. If False, will raise error + if database doesn't exist (for read operations). """ self._config = config if db_path is None: db_path = self._config.storage.data_dir / "haiku.rag.lancedb" self.store = Store( - db_path, config=self._config, skip_validation=skip_validation + db_path, + config=self._config, + skip_validation=skip_validation, + allow_create=allow_create, ) self.document_repository = DocumentRepository(self.store) self.chunk_repository = ChunkRepository(self.store) diff --git a/haiku_rag_slim/haiku/rag/config/loader.py b/haiku_rag_slim/haiku/rag/config/loader.py index 946d9571..53ca6c3f 100644 --- a/haiku_rag_slim/haiku/rag/config/loader.py +++ b/haiku_rag_slim/haiku/rag/config/loader.py @@ -53,7 +53,6 @@ def generate_default_config() -> dict: "environment": "production", "storage": { "data_dir": "", - "disable_autocreate": False, "vacuum_retention_seconds": 86400, }, "monitor": { diff --git a/haiku_rag_slim/haiku/rag/config/models.py b/haiku_rag_slim/haiku/rag/config/models.py index a0542a89..d29d969d 100644 --- a/haiku_rag_slim/haiku/rag/config/models.py +++ b/haiku_rag_slim/haiku/rag/config/models.py @@ -7,7 +7,6 @@ from haiku.rag.utils import get_default_data_dir class StorageConfig(BaseModel): data_dir: Path = Field(default_factory=get_default_data_dir) - disable_autocreate: bool = False vacuum_retention_seconds: int = 86400 diff --git a/haiku_rag_slim/haiku/rag/store/engine.py b/haiku_rag_slim/haiku/rag/store/engine.py index fc97c0bf..4c2a54e3 100644 --- a/haiku_rag_slim/haiku/rag/store/engine.py +++ b/haiku_rag_slim/haiku/rag/store/engine.py @@ -50,7 +50,11 @@ class SettingsRecord(LanceModel): class Store: def __init__( - self, db_path: Path, config: AppConfig = Config, skip_validation: bool = False + self, + db_path: Path, + config: AppConfig = Config, + skip_validation: bool = False, + allow_create: bool = True, ): self.db_path: Path = db_path self._config = config @@ -62,14 +66,14 @@ class Store: # Local filesystem handling for DB directory if not self._has_cloud_config(): - if self._config.storage.disable_autocreate: - # LanceDB uses a directory path for local databases; enforce presence + if not allow_create: + # Read operations should not create the database if not db_path.exists(): raise FileNotFoundError( - f"LanceDB path does not exist: {db_path}. Auto-creation is disabled." + f"Database does not exist: {db_path}. Use a write operation (add, add-src) to create it." ) else: - # Ensure parent directories exist when autocreation allowed + # Write operations - ensure parent directories exist if not db_path.parent.exists(): Path.mkdir(db_path.parent, parents=True) diff --git a/tests/conftest.py b/tests/conftest.py index 0c2c3437..133d8a23 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -40,7 +40,6 @@ def temp_yaml_config(tmp_path, monkeypatch): "storage": { "data_dir": "", "monitor_directories": [], - "disable_autocreate": False, "vacuum_retention_seconds": 60, }, "embeddings": { diff --git a/tests/test_database_autocreate.py b/tests/test_database_autocreate.py new file mode 100644 index 00000000..75359ab5 --- /dev/null +++ b/tests/test_database_autocreate.py @@ -0,0 +1,105 @@ +import tempfile +from pathlib import Path + +import pytest + +from haiku.rag.client import HaikuRAG +from haiku.rag.config import AppConfig + + +def test_read_operations_do_not_create_database(): + """Test that read operations fail if database doesn't exist.""" + with tempfile.TemporaryDirectory() as tmpdir: + db_path = Path(tmpdir) / "test.lancedb" + + config = AppConfig() + + # Read operation with allow_create=False should fail + with pytest.raises( + FileNotFoundError, + match="Database does not exist.*Use a write operation", + ): + HaikuRAG(db_path=db_path, config=config, allow_create=False) + + +def test_write_operations_create_database(): + """Test that write operations create the database.""" + with tempfile.TemporaryDirectory() as tmpdir: + db_path = Path(tmpdir) / "test.lancedb" + + config = AppConfig() + + # Write operation with allow_create=True (default) should succeed + client = HaikuRAG(db_path=db_path, config=config, allow_create=True) + assert db_path.exists() + client.close() + + +async def test_add_document_creates_database(): + """Test that add operations create the database.""" + with tempfile.TemporaryDirectory() as tmpdir: + db_path = Path(tmpdir) / "test.lancedb" + + config = AppConfig() + + # Create a document (write operation) should work and create DB + async with HaikuRAG( + db_path=db_path, config=config, allow_create=True + ) as client: + doc = await client.create_document("Test content") + assert doc.id is not None + assert doc.content == "Test content" + assert db_path.exists() + + +async def test_search_fails_if_database_does_not_exist(): + """Test that search operations fail if DB doesn't exist.""" + with tempfile.TemporaryDirectory() as tmpdir: + db_path = Path(tmpdir) / "test.lancedb" + + config = AppConfig() + + # Read operation (search) should fail if DB doesn't exist + with pytest.raises( + FileNotFoundError, + match="Database does not exist.*Use a write operation", + ): + async with HaikuRAG( + db_path=db_path, config=config, allow_create=False + ) as client: + await client.search("test query") + + +async def test_read_operations_work_after_database_created(): + """Test that read operations work after DB is created via write operation.""" + with tempfile.TemporaryDirectory() as tmpdir: + db_path = Path(tmpdir) / "test.lancedb" + + config = AppConfig() + + # First, create DB via write operation + async with HaikuRAG( + db_path=db_path, config=config, allow_create=True + ) as client: + await client.create_document("Test content", uri="test://doc1") + + # Now read operations should work since DB exists + async with HaikuRAG( + db_path=db_path, config=config, allow_create=False + ) as client: + docs = await client.list_documents() + assert len(docs) == 1 + assert docs[0].content == "Test content" + + +def test_default_allow_create_is_true(): + """Test that allow_create defaults to True for backward compatibility.""" + with tempfile.TemporaryDirectory() as tmpdir: + db_path = Path(tmpdir) / "test.lancedb" + + config = AppConfig() + + # Without specifying allow_create, it should default to True + client = HaikuRAG(db_path=db_path, config=config) + assert db_path.exists() + client.close()