""" SQLite implementation of artifact repositories. This module provides SQLite-backed implementations of the repository interfaces for persistent storage of prompt artifacts. """ import sqlite3 import json from pathlib import Path from typing import List, Optional from datetime import datetime from markitect.prompts.repositories.interfaces import ( IArtifactRepository, ArtifactNotFoundError, DuplicateArtifactError, RepositoryError, ) from markitect.prompts.models import Artifact, ArtifactType, ArtifactMetadata # SQL Schema for artifact tables ARTIFACT_TABLES_SQL = """ -- Prompt artifacts table CREATE TABLE IF NOT EXISTS prompt_artifacts ( id TEXT PRIMARY KEY, space_id TEXT NOT NULL, name TEXT NOT NULL, artifact_type TEXT NOT NULL, content_digest TEXT NOT NULL, content_size INTEGER DEFAULT 0, metadata JSON, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(space_id, name) ); -- Indexes for performance CREATE INDEX IF NOT EXISTS idx_artifacts_space ON prompt_artifacts(space_id); CREATE INDEX IF NOT EXISTS idx_artifacts_digest ON prompt_artifacts(content_digest); CREATE INDEX IF NOT EXISTS idx_artifacts_type ON prompt_artifacts(artifact_type); CREATE INDEX IF NOT EXISTS idx_artifacts_name ON prompt_artifacts(space_id, name); """ def initialize_artifact_tables(db_path: str) -> None: """ Initialize the artifact-related database tables. Args: db_path: Path to the SQLite database file """ # Ensure directory exists db_dir = Path(db_path).parent if db_dir and not db_dir.exists(): db_dir.mkdir(parents=True, exist_ok=True) conn = sqlite3.connect(db_path) try: conn.executescript(ARTIFACT_TABLES_SQL) conn.commit() finally: conn.close() class SQLiteArtifactRepository(IArtifactRepository): """ SQLite implementation of artifact repository. Provides persistent storage for artifacts with content digest tracking. """ def __init__(self, db_path: str): """ Initialize repository with database path. Args: db_path: Path to SQLite database file """ self.db_path = db_path initialize_artifact_tables(db_path) def _get_connection(self) -> sqlite3.Connection: """Get a database connection.""" conn = sqlite3.connect(self.db_path) conn.row_factory = sqlite3.Row return conn def create(self, artifact: Artifact) -> Artifact: """ Persist a new artifact. Args: artifact: Artifact to create Returns: Created artifact Raises: DuplicateArtifactError: If artifact with same space_id+name exists RepositoryError: On other persistence errors """ conn = self._get_connection() try: conn.execute( """ INSERT INTO prompt_artifacts ( id, space_id, name, artifact_type, content_digest, content_size, metadata, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( artifact.id, artifact.space_id, artifact.name, artifact.artifact_type.value, artifact.content_digest, artifact.content_size, json.dumps(artifact.metadata.to_dict()), artifact.created_at.isoformat(), artifact.updated_at.isoformat(), ), ) conn.commit() return artifact except sqlite3.IntegrityError as e: if "UNIQUE constraint" in str(e): raise DuplicateArtifactError( f"Artifact '{artifact.name}' already exists in space '{artifact.space_id}'" ) raise RepositoryError(f"Database integrity error: {e}") except Exception as e: raise RepositoryError(f"Failed to create artifact: {e}") finally: conn.close() def get_by_id(self, artifact_id: str) -> Optional[Artifact]: """ Retrieve artifact by ID. Args: artifact_id: Artifact identifier Returns: Artifact if found, None otherwise """ conn = self._get_connection() try: cursor = conn.execute( "SELECT * FROM prompt_artifacts WHERE id = ?", (artifact_id,) ) row = cursor.fetchone() return self._row_to_artifact(row) if row else None finally: conn.close() def get_by_name(self, space_id: str, name: str) -> Optional[Artifact]: """ Retrieve artifact by space and name. Args: space_id: Space identifier name: Artifact name Returns: Artifact if found, None otherwise """ conn = self._get_connection() try: cursor = conn.execute( "SELECT * FROM prompt_artifacts WHERE space_id = ? AND name = ?", (space_id, name) ) row = cursor.fetchone() return self._row_to_artifact(row) if row else None finally: conn.close() def get_by_digest(self, content_digest: str) -> List[Artifact]: """ Find artifacts with matching content digest. Args: content_digest: SHA-256 digest to match Returns: List of artifacts with matching digest """ conn = self._get_connection() try: cursor = conn.execute( "SELECT * FROM prompt_artifacts WHERE content_digest = ?", (content_digest,) ) return [self._row_to_artifact(row) for row in cursor.fetchall()] finally: conn.close() def list_by_space( self, space_id: str, artifact_type: Optional[ArtifactType] = None, ) -> List[Artifact]: """ List all artifacts in a space. Args: space_id: Space identifier artifact_type: Optional type filter Returns: List of artifacts in space """ conn = self._get_connection() try: if artifact_type: cursor = conn.execute( "SELECT * FROM prompt_artifacts WHERE space_id = ? AND artifact_type = ? ORDER BY name", (space_id, artifact_type.value) ) else: cursor = conn.execute( "SELECT * FROM prompt_artifacts WHERE space_id = ? ORDER BY name", (space_id,) ) return [self._row_to_artifact(row) for row in cursor.fetchall()] finally: conn.close() def update(self, artifact: Artifact) -> Artifact: """ Update an existing artifact. Args: artifact: Artifact with updated data Returns: Updated artifact Raises: ArtifactNotFoundError: If artifact doesn't exist RepositoryError: On other persistence errors """ conn = self._get_connection() try: # Update timestamp artifact.updated_at = datetime.utcnow() cursor = conn.execute( """ UPDATE prompt_artifacts SET content_digest = ?, content_size = ?, metadata = ?, updated_at = ? WHERE id = ? """, ( artifact.content_digest, artifact.content_size, json.dumps(artifact.metadata.to_dict()), artifact.updated_at.isoformat(), artifact.id, ), ) if cursor.rowcount == 0: raise ArtifactNotFoundError(f"Artifact with ID '{artifact.id}' not found") conn.commit() return artifact except ArtifactNotFoundError: raise except Exception as e: raise RepositoryError(f"Failed to update artifact: {e}") finally: conn.close() def delete(self, artifact_id: str) -> bool: """ Delete an artifact. Args: artifact_id: Artifact identifier Returns: True if deleted, False if not found """ conn = self._get_connection() try: cursor = conn.execute( "DELETE FROM prompt_artifacts WHERE id = ?", (artifact_id,) ) conn.commit() return cursor.rowcount > 0 finally: conn.close() def exists(self, space_id: str, name: str) -> bool: """ Check if artifact exists. Args: space_id: Space identifier name: Artifact name Returns: True if artifact exists """ conn = self._get_connection() try: cursor = conn.execute( "SELECT COUNT(*) FROM prompt_artifacts WHERE space_id = ? AND name = ?", (space_id, name) ) count = cursor.fetchone()[0] return count > 0 finally: conn.close() def _row_to_artifact(self, row: sqlite3.Row) -> Artifact: """Convert database row to Artifact instance.""" metadata_dict = json.loads(row["metadata"]) if row["metadata"] else {} return Artifact( id=row["id"], space_id=row["space_id"], name=row["name"], artifact_type=ArtifactType(row["artifact_type"]), content_digest=row["content_digest"], content_size=row["content_size"], metadata=ArtifactMetadata.from_dict(metadata_dict), created_at=datetime.fromisoformat(row["created_at"]), updated_at=datetime.fromisoformat(row["updated_at"]), )