Merge pull request #139 from ggozad/fix/create-db-only-on-write

Drop disable_auto_create flag, simply create only on write operations
This commit is contained in:
Yiorgis Gozadinos 2025-11-13 13:58:14 +02:00 committed by GitHub
commit 7140b3410f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 143 additions and 22 deletions

View file

@ -50,9 +50,11 @@
- **Document Creation**: Optimized `create_document` to skip unnecessary DoclingDocument conversion when chunks are pre-provided - **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 - **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 ### 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) - **BREAKING**: Removed legacy `ResearchStream` and `ResearchStreamEvent` classes (replaced by AG-UI event protocol)
## [0.15.0] - 2025-11-07 ## [0.15.0] - 2025-11-07

View file

@ -52,7 +52,6 @@ environment: production
storage: storage:
data_dir: "" # Empty = use default platform location data_dir: "" # Empty = use default platform location
disable_autocreate: false
vacuum_retention_seconds: 86400 vacuum_retention_seconds: 86400
monitor: 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. **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 - **Write operations** (add, add-src, delete, rebuild): Automatically create the database and required tables if they don't exist
storage: - **Read operations** (list, get, search, ask, research): Fail with a clear error if the database doesn't exist
disable_autocreate: true
```
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 ### Document Processing

View file

@ -137,7 +137,9 @@ class HaikuRAGApp:
) )
async def list_documents(self, filter: str | None = None): 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) documents = await self.client.list_documents(filter=filter)
for doc in documents: for doc in documents:
self._rich_print_document(doc, truncate=True) self._rich_print_document(doc, truncate=True)
@ -170,7 +172,9 @@ class HaikuRAGApp:
) )
async def get_document(self, doc_id: str): 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) doc = await self.client.get_document_by_id(doc_id)
if doc is None: if doc is None:
self.console.print(f"[red]Document with id {doc_id} not found.[/red]") 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 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) results = await self.client.search(query, limit=limit, filter=filter)
if not results: if not results:
self.console.print("[yellow]No results found.[/yellow]") self.console.print("[yellow]No results found.[/yellow]")
@ -213,7 +219,9 @@ class HaikuRAGApp:
deep: Use deep QA mode (multi-step reasoning) deep: Use deep QA mode (multi-step reasoning)
verbose: Show verbose output 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: try:
if deep: if deep:
from haiku.rag.graph.deep_qa.dependencies import DeepQAContext from haiku.rag.graph.deep_qa.dependencies import DeepQAContext
@ -258,7 +266,9 @@ class HaikuRAGApp:
question: The research question question: The research question
verbose: Show AG-UI event stream during execution 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: try:
self.console.print("[bold cyan]Starting research[/bold cyan]") self.console.print("[bold cyan]Starting research[/bold cyan]")
self.console.print(f"[bold blue]Question:[/bold blue] {question}") self.console.print(f"[bold blue]Question:[/bold blue] {question}")

View file

@ -28,6 +28,7 @@ class HaikuRAG:
db_path: Path | None = None, db_path: Path | None = None,
config: AppConfig = Config, config: AppConfig = Config,
skip_validation: bool = False, skip_validation: bool = False,
allow_create: bool = True,
): ):
"""Initialize the RAG client with a database path. """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. db_path: Path to the database file. If None, uses config.storage.data_dir.
config: Configuration to use. Defaults to global Config. config: Configuration to use. Defaults to global Config.
skip_validation: Whether to skip configuration validation on database load. 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 self._config = config
if db_path is None: if db_path is None:
db_path = self._config.storage.data_dir / "haiku.rag.lancedb" db_path = self._config.storage.data_dir / "haiku.rag.lancedb"
self.store = Store( 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.document_repository = DocumentRepository(self.store)
self.chunk_repository = ChunkRepository(self.store) self.chunk_repository = ChunkRepository(self.store)

View file

@ -53,7 +53,6 @@ def generate_default_config() -> dict:
"environment": "production", "environment": "production",
"storage": { "storage": {
"data_dir": "", "data_dir": "",
"disable_autocreate": False,
"vacuum_retention_seconds": 86400, "vacuum_retention_seconds": 86400,
}, },
"monitor": { "monitor": {

View file

@ -7,7 +7,6 @@ from haiku.rag.utils import get_default_data_dir
class StorageConfig(BaseModel): class StorageConfig(BaseModel):
data_dir: Path = Field(default_factory=get_default_data_dir) data_dir: Path = Field(default_factory=get_default_data_dir)
disable_autocreate: bool = False
vacuum_retention_seconds: int = 86400 vacuum_retention_seconds: int = 86400

View file

@ -50,7 +50,11 @@ class SettingsRecord(LanceModel):
class Store: class Store:
def __init__( 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.db_path: Path = db_path
self._config = config self._config = config
@ -62,14 +66,14 @@ class Store:
# Local filesystem handling for DB directory # Local filesystem handling for DB directory
if not self._has_cloud_config(): if not self._has_cloud_config():
if self._config.storage.disable_autocreate: if not allow_create:
# LanceDB uses a directory path for local databases; enforce presence # Read operations should not create the database
if not db_path.exists(): if not db_path.exists():
raise FileNotFoundError( 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: else:
# Ensure parent directories exist when autocreation allowed # Write operations - ensure parent directories exist
if not db_path.parent.exists(): if not db_path.parent.exists():
Path.mkdir(db_path.parent, parents=True) Path.mkdir(db_path.parent, parents=True)

View file

@ -40,7 +40,6 @@ def temp_yaml_config(tmp_path, monkeypatch):
"storage": { "storage": {
"data_dir": "", "data_dir": "",
"monitor_directories": [], "monitor_directories": [],
"disable_autocreate": False,
"vacuum_retention_seconds": 60, "vacuum_retention_seconds": 60,
}, },
"embeddings": { "embeddings": {

View file

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