feat: add SQLite database layer with SQLModel
Add shared database infrastructure to clide core for use by both the CLI and clide-web. SQLModel provides Pydantic-native models that double as SQLAlchemy table definitions. Models: Project, Session, UserPreference, ConnectionLog. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
"""Pydantic models for Clide."""
|
"""Pydantic models for Clide."""
|
||||||
|
|
||||||
from clide.models.config import ClideSettings, PanelConfig
|
from clide.models.config import ClideSettings, PanelConfig
|
||||||
|
from clide.models.db import ConnectionLog, Project, Session, UserPreference
|
||||||
from clide.models.diff import ChangeType, DiffContent, DiffHunk, DiffLine, DiffViewState
|
from clide.models.diff import ChangeType, DiffContent, DiffHunk, DiffLine, DiffViewState
|
||||||
from clide.models.editor import CursorPosition, EditorState, FileBuffer, Selection
|
from clide.models.editor import CursorPosition, EditorState, FileBuffer, Selection
|
||||||
from clide.models.git import (
|
from clide.models.git import (
|
||||||
@@ -56,4 +57,9 @@ __all__ = [
|
|||||||
"TabInfo",
|
"TabInfo",
|
||||||
"TabType",
|
"TabType",
|
||||||
"TAB_ICONS",
|
"TAB_ICONS",
|
||||||
|
# Database (SQLModel)
|
||||||
|
"Project",
|
||||||
|
"Session",
|
||||||
|
"UserPreference",
|
||||||
|
"ConnectionLog",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
"""SQLModel table models for persistent storage.
|
||||||
|
|
||||||
|
These models serve both the standalone Clide TUI and the clide-web server.
|
||||||
|
They ARE Pydantic models (SQLModel inherits from BaseModel).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from sqlmodel import Field, SQLModel
|
||||||
|
|
||||||
|
|
||||||
|
class Project(SQLModel, table=True):
|
||||||
|
"""A project (git repo) that can be opened in Clide."""
|
||||||
|
|
||||||
|
id: int | None = Field(default=None, primary_key=True)
|
||||||
|
name: str = Field(unique=True, index=True)
|
||||||
|
path: str
|
||||||
|
theme: str = "summer-night"
|
||||||
|
last_accessed: datetime | None = None
|
||||||
|
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||||
|
|
||||||
|
|
||||||
|
class Session(SQLModel, table=True):
|
||||||
|
"""A tmux session running a Clide instance (used by clide-web)."""
|
||||||
|
|
||||||
|
id: int | None = Field(default=None, primary_key=True)
|
||||||
|
project_name: str = Field(index=True)
|
||||||
|
tmux_session: str = Field(unique=True)
|
||||||
|
pid: int | None = None
|
||||||
|
status: str = "active"
|
||||||
|
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||||
|
last_activity: datetime = Field(default_factory=datetime.utcnow)
|
||||||
|
|
||||||
|
|
||||||
|
class UserPreference(SQLModel, table=True):
|
||||||
|
"""Key-value user preferences persisted across restarts."""
|
||||||
|
|
||||||
|
id: int | None = Field(default=None, primary_key=True)
|
||||||
|
key: str = Field(unique=True, index=True)
|
||||||
|
value: str
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectionLog(SQLModel, table=True):
|
||||||
|
"""Audit log of browser connections (used by clide-web)."""
|
||||||
|
|
||||||
|
id: int | None = Field(default=None, primary_key=True)
|
||||||
|
project_name: str
|
||||||
|
client_ip: str
|
||||||
|
connected_at: datetime = Field(default_factory=datetime.utcnow)
|
||||||
|
disconnected_at: datetime | None = None
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
"""SQLite database engine and session management.
|
||||||
|
|
||||||
|
Used by both standalone Clide and clide-web. The database file
|
||||||
|
defaults to ~/.clide/clide.db but is configurable.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Generator
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from sqlmodel import Session as DBSession
|
||||||
|
from sqlmodel import SQLModel, create_engine
|
||||||
|
|
||||||
|
_engine = None
|
||||||
|
|
||||||
|
DEFAULT_DB_PATH = Path.home() / ".clide" / "clide.db"
|
||||||
|
|
||||||
|
|
||||||
|
def get_engine(db_path: Path | None = None):
|
||||||
|
"""Create or return the SQLAlchemy engine."""
|
||||||
|
global _engine
|
||||||
|
if _engine is None:
|
||||||
|
path = db_path or DEFAULT_DB_PATH
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
_engine = create_engine(
|
||||||
|
f"sqlite:///{path}",
|
||||||
|
echo=False,
|
||||||
|
connect_args={"check_same_thread": False},
|
||||||
|
)
|
||||||
|
return _engine
|
||||||
|
|
||||||
|
|
||||||
|
def init_db(db_path: Path | None = None) -> None:
|
||||||
|
"""Create all tables if they don't exist."""
|
||||||
|
# Import models so SQLModel registers them
|
||||||
|
import clide.models.db # noqa: F401
|
||||||
|
|
||||||
|
engine = get_engine(db_path)
|
||||||
|
SQLModel.metadata.create_all(engine)
|
||||||
|
|
||||||
|
|
||||||
|
def get_db() -> Generator[DBSession, None, None]:
|
||||||
|
"""Yield a database session. Usable as a FastAPI dependency or context manager."""
|
||||||
|
if _engine is None:
|
||||||
|
raise RuntimeError("Database not initialized — call init_db() first")
|
||||||
|
with DBSession(_engine) as session:
|
||||||
|
yield session
|
||||||
@@ -32,6 +32,8 @@ dependencies = [
|
|||||||
"rich>=13.0.0",
|
"rich>=13.0.0",
|
||||||
"watchdog>=4.0.0",
|
"watchdog>=4.0.0",
|
||||||
"wcwidth>=0.2.0", # Required by vendored pyte
|
"wcwidth>=0.2.0", # Required by vendored pyte
|
||||||
|
"sqlmodel>=0.0.16",
|
||||||
|
"aiosqlite>=0.20",
|
||||||
# Tree-sitter for syntax highlighting
|
# Tree-sitter for syntax highlighting
|
||||||
"tree-sitter>=0.21.0",
|
"tree-sitter>=0.21.0",
|
||||||
"tree-sitter-python>=0.21.0",
|
"tree-sitter-python>=0.21.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user