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:
commit
7140b3410f
9 changed files with 143 additions and 22 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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}")
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
105
tests/test_database_autocreate.py
Normal file
105
tests/test_database_autocreate.py
Normal 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()
|
||||||
Loading…
Reference in a new issue