Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions src/basic_memory/alembic/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,14 +98,45 @@ def run_migrations_offline() -> None:
context.run_migrations()


def get_version_table_schema(connection) -> str | None:
"""Determine the schema for Alembic's version table.

Args:
connection: Database connection

Returns:
Schema name if Postgres with explicit search_path, else None

Why: When using schema isolation, Alembic's alembic_version table should
be in the same schema as the application tables.
"""
if connection.dialect.name != "postgresql":
return None

if not app_config.database_url:
return None

from basic_memory.db import extract_search_path_from_url

_, search_path = extract_search_path_from_url(app_config.database_url)
return search_path


def do_run_migrations(connection):
"""Execute migrations with the given connection."""
# --- Schema-Aware Migration Tracking ---
# Trigger: Postgres with non-public search_path in database_url
# Why: Alembic version table should be in same schema as application tables
# Outcome: version_table_schema passed to context.configure()
version_table_schema = get_version_table_schema(connection)

context.configure(
connection=connection,
target_metadata=target_metadata,
include_object=include_object,
render_as_batch=True,
compare_type=True,
version_table_schema=version_table_schema,
)
with context.begin_transaction():
context.run_migrations()
Expand Down
42 changes: 27 additions & 15 deletions src/basic_memory/cli/commands/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,23 +61,35 @@ def reset(
logger.info("Resetting database...")
config_manager = ConfigManager()
app_config = config_manager.config
# Get database path
# Get database path (None for Postgres)
db_path = app_config.app_database_path

# Delete the database file and WAL files if they exist
for suffix in ["", "-shm", "-wal"]:
path = db_path.parent / f"{db_path.name}{suffix}"
if path.exists():
try:
path.unlink()
logger.info(f"Deleted: {path}")
except OSError as e:
console.print(
f"[red]Error:[/red] Cannot delete {path.name}: {e}\n"
"The database may be in use by another process (e.g., MCP server).\n"
"Please close Claude Desktop or any other Basic Memory clients and try again."
)
raise typer.Exit(1)
# --- SQLite vs Postgres Handling ---
# Trigger: db_path is None when using Postgres backend
# Why: Postgres doesn't use a local file, so file deletion doesn't apply
# Outcome: skip file deletion for Postgres, only run migrations
if db_path is not None:
# Delete the database file and WAL files if they exist (SQLite only)
for suffix in ["", "-shm", "-wal"]:
path = db_path.parent / f"{db_path.name}{suffix}"
if path.exists():
try:
path.unlink()
logger.info(f"Deleted: {path}")
except OSError as e:
console.print(
f"[red]Error:[/red] Cannot delete {path.name}: {e}\n"
"The database may be in use by another process (e.g., MCP server).\n"
"Please close Claude Desktop or any other Basic Memory clients and try again."
)
raise typer.Exit(1)
else:
# Postgres: drop and recreate schema/tables
console.print(
"[yellow]Note:[/yellow] Using Postgres backend. "
"Dropping and recreating tables..."
)
run_with_cleanup(db.reset_postgres_database(app_config))

# Create a new empty database (preserves project configuration)
try:
Expand Down
33 changes: 29 additions & 4 deletions src/basic_memory/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,24 +316,49 @@ def model_post_init(self, __context: Any) -> None:
self.default_project = next(iter(self.projects.keys()))

@property
def app_database_path(self) -> Path:
def app_database_path(self) -> Optional[Path]:
"""Get the path to the app-level database.

This is the single database that will store all knowledge data
across all projects.

Returns:
Path to SQLite database file, or None for Postgres backends.
"""
# --- SQLite URL Handling ---
# Trigger: database_url is set and starts with "sqlite"
# Why: allows project-local SQLite databases via URL configuration
# Outcome: extracts path from URL (e.g., sqlite+aiosqlite:///.basic-memory/memory.db)
if self.database_url:
if self.database_url.startswith("sqlite"):
from urllib.parse import urlparse

parsed = urlparse(self.database_url)
# parsed.path will be "/.basic-memory/memory.db" - strip leading /
path = parsed.path[1:] if parsed.path.startswith("/") else parsed.path
resolved_path = Path(path).resolve()
# Ensure parent directory exists
if not resolved_path.parent.exists(): # pragma: no cover
resolved_path.parent.mkdir(parents=True, exist_ok=True)
return resolved_path
else:
# Postgres or other backend - no file path
return None

# --- Default SQLite Path ---
# No database_url set - use default ~/.basic-memory/memory.db
database_path = Path.home() / DATA_DIR_NAME / APP_DATABASE_NAME
if not database_path.exists(): # pragma: no cover
database_path.parent.mkdir(parents=True, exist_ok=True)
database_path.touch()
return database_path

@property
def database_path(self) -> Path:
def database_path(self) -> Optional[Path]:
"""Get SQLite database path.

Rreturns the app-level database path
for backward compatibility in the codebase.
Returns the app-level database path for backward compatibility.
Returns None when using Postgres backend.
"""

# Load the app-level database path from the global config
Expand Down
Loading
Loading