Files
basicmachines-co-basic-memory/src/basic_memory/db.py
T
2024-12-23 20:48:22 -06:00

108 lines
3.2 KiB
Python

import asyncio
from contextlib import asynccontextmanager
from enum import Enum, auto
from pathlib import Path
from typing import AsyncGenerator
from loguru import logger
from sqlalchemy import text
from sqlalchemy.ext.asyncio import (
create_async_engine,
async_sessionmaker,
AsyncSession,
AsyncEngine,
async_scoped_session,
)
from basic_memory.models import Base
class DatabaseType(Enum):
"""Types of supported databases."""
MEMORY = auto()
FILESYSTEM = auto()
@classmethod
def get_db_path(cls, project_path: Path, db_type: "DatabaseType") -> Path:
"""Get database path based on type."""
if db_type == cls.MEMORY:
return Path(":memory:")
else:
path = project_path / "data" / "memory.db"
logger.info(f"Using database path: {path}")
return path
@classmethod
def get_db_url(cls, db_path: Path) -> str:
"""Get SQLAlchemy URL for database path."""
if str(db_path) == ":memory:":
logger.info("Using in-memory SQLite database")
return "sqlite+aiosqlite://"
url = f"sqlite+aiosqlite:///{db_path}"
logger.info(f"Using SQLite database URL: {url}")
return url
def get_scoped_session_factory(
session_maker: async_sessionmaker[AsyncSession],
) -> async_scoped_session:
"""Create a scoped session factory scoped to current task."""
return async_scoped_session(session_maker, scopefunc=asyncio.current_task)
@asynccontextmanager
async def scoped_session(
session_maker: async_sessionmaker[AsyncSession],
) -> AsyncGenerator[AsyncSession, None]:
"""
Get a scoped session with proper lifecycle management.
Args:
session_maker: Session maker to create scoped sessions from
"""
factory = get_scoped_session_factory(session_maker)
session = factory()
try:
await session.execute(text("PRAGMA foreign_keys=ON"))
yield session
await session.commit()
except Exception:
await session.rollback()
raise
finally:
await session.close()
await factory.remove()
async def init_db(session: AsyncSession):
"""Initialize database with required tables."""
await session.execute(text("PRAGMA foreign_keys=ON"))
conn = await session.connection()
await conn.run_sync(Base.metadata.create_all)
await session.commit()
@asynccontextmanager
async def engine_session_factory(
project_path: Path,
db_type: DatabaseType = DatabaseType.FILESYSTEM,
init: bool = True,
) -> AsyncGenerator[tuple[AsyncEngine, async_sessionmaker[AsyncSession]], None]:
"""Create engine and session factory."""
logger.debug(f"Creating engine for project path: {project_path}")
db_path = DatabaseType.get_db_path(project_path, db_type)
db_url = DatabaseType.get_db_url(db_path)
engine = create_async_engine(db_url, connect_args={"check_same_thread": False})
try:
factory = async_sessionmaker(engine, expire_on_commit=False)
if init:
logger.debug("Initializing database...")
async with scoped_session(factory) as db_session:
await init_db(db_session)
yield engine, factory
finally:
await engine.dispose()