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:
2026-02-24 17:01:55 +01:00
co-authored by Claude Opus 4.6
parent a14ee82883
commit 29a858d7a8
4 changed files with 106 additions and 0 deletions
+6
View File
@@ -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",
] ]
+50
View File
@@ -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
+48
View File
@@ -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
+2
View File
@@ -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",