From 29a858d7a82db83d8174a8185e7834216b0d2c90 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Tue, 24 Feb 2026 17:01:55 +0100 Subject: [PATCH] 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 --- clide/models/__init__.py | 6 +++++ clide/models/db.py | 50 ++++++++++++++++++++++++++++++++++++++ clide/services/database.py | 48 ++++++++++++++++++++++++++++++++++++ pyproject.toml | 2 ++ 4 files changed, 106 insertions(+) create mode 100644 clide/models/db.py create mode 100644 clide/services/database.py diff --git a/clide/models/__init__.py b/clide/models/__init__.py index 97dcb029..7fa43fe2 100644 --- a/clide/models/__init__.py +++ b/clide/models/__init__.py @@ -1,6 +1,7 @@ """Pydantic models for Clide.""" 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.editor import CursorPosition, EditorState, FileBuffer, Selection from clide.models.git import ( @@ -56,4 +57,9 @@ __all__ = [ "TabInfo", "TabType", "TAB_ICONS", + # Database (SQLModel) + "Project", + "Session", + "UserPreference", + "ConnectionLog", ] diff --git a/clide/models/db.py b/clide/models/db.py new file mode 100644 index 00000000..9de745a8 --- /dev/null +++ b/clide/models/db.py @@ -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 diff --git a/clide/services/database.py b/clide/services/database.py new file mode 100644 index 00000000..2496c7ca --- /dev/null +++ b/clide/services/database.py @@ -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 diff --git a/pyproject.toml b/pyproject.toml index 13539e98..c58df55b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,8 @@ dependencies = [ "rich>=13.0.0", "watchdog>=4.0.0", "wcwidth>=0.2.0", # Required by vendored pyte + "sqlmodel>=0.0.16", + "aiosqlite>=0.20", # Tree-sitter for syntax highlighting "tree-sitter>=0.21.0", "tree-sitter-python>=0.21.0",