feat(spaces): implement Phase 0-1 of Information Space Service
Phase 0 - Project Organization: - Create docs/PROJECT_STRUCTURE.md documenting codebase layout - Create markitect/core/ with parser, serializer, document_manager, workspace - Create markitect/schema/ consolidating 6 schema_*.py modules - Create markitect/storage/ with database module - Maintain backward compatibility via re-exports from original locations - Add docs/roadmap/information-space-service/ with README and WORKPLAN Phase 1 - Foundation (Weeks 1-3): - Week 1: Core domain models (InformationSpace, SpaceDocument, SpaceConfig, SpaceMetadata, SpaceVariable, TransclusionReference, SpaceStatus) - Week 2: Repository layer with interfaces (ISpaceRepository, IDocumentAssociationRepository, IVariableRepository, IReferenceRepository) and SQLite implementations with foreign key cascade deletes - Week 3: SpaceService orchestration layer with full CRUD, document, variable, and reference tracking operations Test coverage: 124 tests (25 model + 63 repository + 36 integration) Capabilities delivered: - CAP-001: InformationSpace entity with lifecycle management - CAP-002: SpaceRepository CRUD with SQLite backing - CAP-003: Document-Space associations with path-based organization - CAP-004: Space metadata and configuration schemas - CAP-005: Database schema with migrations Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
713
markitect/spaces/repositories/sqlite.py
Normal file
713
markitect/spaces/repositories/sqlite.py
Normal file
@@ -0,0 +1,713 @@
|
||||
"""
|
||||
SQLite implementation of space repositories.
|
||||
|
||||
This module provides SQLite-backed implementations of the repository
|
||||
interfaces for persistent storage of Information Spaces.
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from .interfaces import (
|
||||
ISpaceRepository,
|
||||
IDocumentAssociationRepository,
|
||||
IVariableRepository,
|
||||
IReferenceRepository,
|
||||
)
|
||||
from ..models import (
|
||||
InformationSpace,
|
||||
SpaceDocument,
|
||||
SpaceVariable,
|
||||
TransclusionReference,
|
||||
SpaceStatus,
|
||||
SpaceMetadata,
|
||||
SpaceConfig,
|
||||
)
|
||||
|
||||
|
||||
# SQL Schema for space tables
|
||||
SPACE_TABLES_SQL = """
|
||||
-- Information Spaces table
|
||||
CREATE TABLE IF NOT EXISTS spaces (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT UNIQUE NOT NULL,
|
||||
description TEXT,
|
||||
metadata JSON,
|
||||
config JSON,
|
||||
parent_space_id TEXT REFERENCES spaces(id),
|
||||
status TEXT DEFAULT 'draft',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Space documents association table
|
||||
CREATE TABLE IF NOT EXISTS space_documents (
|
||||
id TEXT PRIMARY KEY,
|
||||
space_id TEXT NOT NULL REFERENCES spaces(id) ON DELETE CASCADE,
|
||||
document_id TEXT NOT NULL,
|
||||
space_path TEXT NOT NULL,
|
||||
order_index INTEGER DEFAULT 0,
|
||||
metadata JSON,
|
||||
content_hash TEXT,
|
||||
added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(space_id, space_path)
|
||||
);
|
||||
|
||||
-- Space variables for transclusion context
|
||||
CREATE TABLE IF NOT EXISTS space_variables (
|
||||
space_id TEXT NOT NULL REFERENCES spaces(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
value JSON,
|
||||
scope TEXT DEFAULT 'space',
|
||||
PRIMARY KEY(space_id, name)
|
||||
);
|
||||
|
||||
-- Transclusion reference tracking for cache invalidation
|
||||
CREATE TABLE IF NOT EXISTS transclusion_references (
|
||||
source_doc_id TEXT NOT NULL,
|
||||
target_doc_id TEXT NOT NULL,
|
||||
space_id TEXT NOT NULL REFERENCES spaces(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY(source_doc_id, target_doc_id, space_id)
|
||||
);
|
||||
|
||||
-- Indexes for performance
|
||||
CREATE INDEX IF NOT EXISTS idx_spaces_name ON spaces(name);
|
||||
CREATE INDEX IF NOT EXISTS idx_spaces_parent ON spaces(parent_space_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_spaces_status ON spaces(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_space_documents_space ON space_documents(space_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_space_documents_path ON space_documents(space_id, space_path);
|
||||
CREATE INDEX IF NOT EXISTS idx_transclusion_refs_source ON transclusion_references(source_doc_id, space_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_transclusion_refs_target ON transclusion_references(target_doc_id, space_id);
|
||||
"""
|
||||
|
||||
|
||||
def initialize_space_tables(db_path: str) -> None:
|
||||
"""
|
||||
Initialize the space-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:
|
||||
cursor = conn.cursor()
|
||||
cursor.executescript(SPACE_TABLES_SQL)
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
class SqliteSpaceRepository(ISpaceRepository):
|
||||
"""
|
||||
SQLite implementation of the space repository.
|
||||
|
||||
Provides persistent storage for InformationSpace entities
|
||||
using SQLite as the backend.
|
||||
"""
|
||||
|
||||
def __init__(self, db_path: str):
|
||||
"""
|
||||
Initialize the repository.
|
||||
|
||||
Args:
|
||||
db_path: Path to the SQLite database file
|
||||
"""
|
||||
self.db_path = db_path
|
||||
initialize_space_tables(db_path)
|
||||
|
||||
def _get_connection(self) -> sqlite3.Connection:
|
||||
"""Get a database connection with foreign keys enabled."""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
conn.execute("PRAGMA foreign_keys = ON")
|
||||
return conn
|
||||
|
||||
def _row_to_space(self, row: sqlite3.Row) -> InformationSpace:
|
||||
"""Convert a database row to an InformationSpace."""
|
||||
metadata_dict = json.loads(row["metadata"]) if row["metadata"] else {}
|
||||
config_dict = json.loads(row["config"]) if row["config"] else {}
|
||||
|
||||
return InformationSpace(
|
||||
id=row["id"],
|
||||
name=row["name"],
|
||||
description=row["description"],
|
||||
metadata=SpaceMetadata.from_dict(metadata_dict),
|
||||
config=SpaceConfig.from_dict(config_dict),
|
||||
parent_space_id=row["parent_space_id"],
|
||||
status=SpaceStatus(row["status"]),
|
||||
created_at=datetime.fromisoformat(row["created_at"]) if row["created_at"] else datetime.now(),
|
||||
updated_at=datetime.fromisoformat(row["updated_at"]) if row["updated_at"] else datetime.now(),
|
||||
)
|
||||
|
||||
def create(self, space: InformationSpace) -> InformationSpace:
|
||||
"""Create a new space."""
|
||||
conn = self._get_connection()
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Check if name already exists
|
||||
cursor.execute("SELECT id FROM spaces WHERE name = ?", (space.name,))
|
||||
if cursor.fetchone():
|
||||
raise ValueError(f"Space with name '{space.name}' already exists")
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO spaces (id, name, description, metadata, config, parent_space_id, status, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
space.id,
|
||||
space.name,
|
||||
space.description,
|
||||
json.dumps(space.metadata.to_dict() if isinstance(space.metadata, SpaceMetadata) else space.metadata),
|
||||
json.dumps(space.config.to_dict() if isinstance(space.config, SpaceConfig) else space.config),
|
||||
space.parent_space_id,
|
||||
space.status.value if isinstance(space.status, SpaceStatus) else space.status,
|
||||
space.created_at.isoformat(),
|
||||
space.updated_at.isoformat(),
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
return space
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def get_by_id(self, space_id: str) -> Optional[InformationSpace]:
|
||||
"""Get a space by ID."""
|
||||
conn = self._get_connection()
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT * FROM spaces WHERE id = ?", (space_id,))
|
||||
row = cursor.fetchone()
|
||||
return self._row_to_space(row) if row else None
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def get_by_name(self, name: str) -> Optional[InformationSpace]:
|
||||
"""Get a space by name."""
|
||||
conn = self._get_connection()
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT * FROM spaces WHERE name = ?", (name,))
|
||||
row = cursor.fetchone()
|
||||
return self._row_to_space(row) if row else None
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def list_all(self, include_archived: bool = False) -> List[InformationSpace]:
|
||||
"""List all spaces."""
|
||||
conn = self._get_connection()
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
if include_archived:
|
||||
cursor.execute("SELECT * FROM spaces WHERE status != 'deleted' ORDER BY name")
|
||||
else:
|
||||
cursor.execute("SELECT * FROM spaces WHERE status NOT IN ('archived', 'deleted') ORDER BY name")
|
||||
return [self._row_to_space(row) for row in cursor.fetchall()]
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def update(self, space: InformationSpace) -> InformationSpace:
|
||||
"""Update a space."""
|
||||
conn = self._get_connection()
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Check if space exists
|
||||
cursor.execute("SELECT id FROM spaces WHERE id = ?", (space.id,))
|
||||
if not cursor.fetchone():
|
||||
raise ValueError(f"Space with id '{space.id}' does not exist")
|
||||
|
||||
space.touch() # Update timestamp
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
UPDATE spaces SET
|
||||
name = ?,
|
||||
description = ?,
|
||||
metadata = ?,
|
||||
config = ?,
|
||||
parent_space_id = ?,
|
||||
status = ?,
|
||||
updated_at = ?
|
||||
WHERE id = ?
|
||||
""",
|
||||
(
|
||||
space.name,
|
||||
space.description,
|
||||
json.dumps(space.metadata.to_dict() if isinstance(space.metadata, SpaceMetadata) else space.metadata),
|
||||
json.dumps(space.config.to_dict() if isinstance(space.config, SpaceConfig) else space.config),
|
||||
space.parent_space_id,
|
||||
space.status.value if isinstance(space.status, SpaceStatus) else space.status,
|
||||
space.updated_at.isoformat(),
|
||||
space.id,
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
return space
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def delete(self, space_id: str) -> bool:
|
||||
"""Delete a space."""
|
||||
conn = self._get_connection()
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("DELETE FROM spaces WHERE id = ?", (space_id,))
|
||||
conn.commit()
|
||||
return cursor.rowcount > 0
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def exists(self, space_id: str) -> bool:
|
||||
"""Check if a space exists."""
|
||||
conn = self._get_connection()
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT 1 FROM spaces WHERE id = ?", (space_id,))
|
||||
return cursor.fetchone() is not None
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def get_children(self, parent_space_id: str) -> List[InformationSpace]:
|
||||
"""Get child spaces."""
|
||||
conn = self._get_connection()
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"SELECT * FROM spaces WHERE parent_space_id = ? ORDER BY name",
|
||||
(parent_space_id,),
|
||||
)
|
||||
return [self._row_to_space(row) for row in cursor.fetchall()]
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
class SqliteDocumentRepository(IDocumentAssociationRepository):
|
||||
"""
|
||||
SQLite implementation of the document association repository.
|
||||
"""
|
||||
|
||||
def __init__(self, db_path: str):
|
||||
"""Initialize the repository."""
|
||||
self.db_path = db_path
|
||||
initialize_space_tables(db_path)
|
||||
|
||||
def _get_connection(self) -> sqlite3.Connection:
|
||||
"""Get a database connection with foreign keys enabled."""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
conn.execute("PRAGMA foreign_keys = ON")
|
||||
return conn
|
||||
|
||||
def _row_to_document(self, row: sqlite3.Row) -> SpaceDocument:
|
||||
"""Convert a database row to a SpaceDocument."""
|
||||
metadata_dict = json.loads(row["metadata"]) if row["metadata"] else {}
|
||||
|
||||
return SpaceDocument(
|
||||
id=row["id"],
|
||||
space_id=row["space_id"],
|
||||
document_id=row["document_id"],
|
||||
space_path=row["space_path"],
|
||||
order_index=row["order_index"],
|
||||
metadata=metadata_dict,
|
||||
content_hash=row["content_hash"],
|
||||
added_at=datetime.fromisoformat(row["added_at"]) if row["added_at"] else datetime.now(),
|
||||
)
|
||||
|
||||
def add_document(self, document: SpaceDocument) -> SpaceDocument:
|
||||
"""Add a document to a space."""
|
||||
conn = self._get_connection()
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Check if path already exists in space
|
||||
cursor.execute(
|
||||
"SELECT id FROM space_documents WHERE space_id = ? AND space_path = ?",
|
||||
(document.space_id, document.space_path),
|
||||
)
|
||||
if cursor.fetchone():
|
||||
raise ValueError(f"Document path '{document.space_path}' already exists in space")
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO space_documents (id, space_id, document_id, space_path, order_index, metadata, content_hash, added_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
document.id,
|
||||
document.space_id,
|
||||
document.document_id,
|
||||
document.space_path,
|
||||
document.order_index,
|
||||
json.dumps(document.metadata),
|
||||
document.content_hash,
|
||||
document.added_at.isoformat(),
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
return document
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def get_document(self, document_id: str) -> Optional[SpaceDocument]:
|
||||
"""Get a document by ID."""
|
||||
conn = self._get_connection()
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT * FROM space_documents WHERE id = ?", (document_id,))
|
||||
row = cursor.fetchone()
|
||||
return self._row_to_document(row) if row else None
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def get_by_space_path(self, space_id: str, space_path: str) -> Optional[SpaceDocument]:
|
||||
"""Get a document by its path within a space."""
|
||||
conn = self._get_connection()
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"SELECT * FROM space_documents WHERE space_id = ? AND space_path = ?",
|
||||
(space_id, space_path),
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
return self._row_to_document(row) if row else None
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def list_by_space(self, space_id: str) -> List[SpaceDocument]:
|
||||
"""List all documents in a space."""
|
||||
conn = self._get_connection()
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"SELECT * FROM space_documents WHERE space_id = ? ORDER BY order_index, space_path",
|
||||
(space_id,),
|
||||
)
|
||||
return [self._row_to_document(row) for row in cursor.fetchall()]
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def update_document(self, document: SpaceDocument) -> SpaceDocument:
|
||||
"""Update a document."""
|
||||
conn = self._get_connection()
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("SELECT id FROM space_documents WHERE id = ?", (document.id,))
|
||||
if not cursor.fetchone():
|
||||
raise ValueError(f"Document with id '{document.id}' does not exist")
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
UPDATE space_documents SET
|
||||
document_id = ?,
|
||||
space_path = ?,
|
||||
order_index = ?,
|
||||
metadata = ?,
|
||||
content_hash = ?
|
||||
WHERE id = ?
|
||||
""",
|
||||
(
|
||||
document.document_id,
|
||||
document.space_path,
|
||||
document.order_index,
|
||||
json.dumps(document.metadata),
|
||||
document.content_hash,
|
||||
document.id,
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
return document
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def remove_document(self, document_id: str) -> bool:
|
||||
"""Remove a document from a space."""
|
||||
conn = self._get_connection()
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("DELETE FROM space_documents WHERE id = ?", (document_id,))
|
||||
conn.commit()
|
||||
return cursor.rowcount > 0
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def move_document(self, document_id: str, new_space_path: str) -> SpaceDocument:
|
||||
"""Move a document to a new path."""
|
||||
conn = self._get_connection()
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Get current document
|
||||
cursor.execute("SELECT * FROM space_documents WHERE id = ?", (document_id,))
|
||||
row = cursor.fetchone()
|
||||
if not row:
|
||||
raise ValueError(f"Document with id '{document_id}' does not exist")
|
||||
|
||||
# Check if new path already exists
|
||||
cursor.execute(
|
||||
"SELECT id FROM space_documents WHERE space_id = ? AND space_path = ? AND id != ?",
|
||||
(row["space_id"], new_space_path, document_id),
|
||||
)
|
||||
if cursor.fetchone():
|
||||
raise ValueError(f"Document path '{new_space_path}' already exists")
|
||||
|
||||
cursor.execute(
|
||||
"UPDATE space_documents SET space_path = ? WHERE id = ?",
|
||||
(new_space_path, document_id),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
document = self._row_to_document(row)
|
||||
document.space_path = new_space_path
|
||||
return document
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def reorder_documents(self, space_id: str, document_ids: List[str]) -> None:
|
||||
"""Reorder documents within a space."""
|
||||
conn = self._get_connection()
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
for index, doc_id in enumerate(document_ids):
|
||||
cursor.execute(
|
||||
"UPDATE space_documents SET order_index = ? WHERE id = ? AND space_id = ?",
|
||||
(index, doc_id, space_id),
|
||||
)
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def update_content_hash(self, document_id: str, content_hash: str) -> None:
|
||||
"""Update the content hash."""
|
||||
conn = self._get_connection()
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"UPDATE space_documents SET content_hash = ? WHERE id = ?",
|
||||
(content_hash, document_id),
|
||||
)
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
class SqliteVariableRepository(IVariableRepository):
|
||||
"""
|
||||
SQLite implementation of the variable repository.
|
||||
"""
|
||||
|
||||
def __init__(self, db_path: str):
|
||||
"""Initialize the repository."""
|
||||
self.db_path = db_path
|
||||
initialize_space_tables(db_path)
|
||||
|
||||
def _get_connection(self) -> sqlite3.Connection:
|
||||
"""Get a database connection with foreign keys enabled."""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
conn.execute("PRAGMA foreign_keys = ON")
|
||||
return conn
|
||||
|
||||
def set_variable(self, variable: SpaceVariable) -> SpaceVariable:
|
||||
"""Set a variable value."""
|
||||
conn = self._get_connection()
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT OR REPLACE INTO space_variables (space_id, name, value, scope)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
variable.space_id,
|
||||
variable.name,
|
||||
json.dumps(variable.value),
|
||||
variable.scope,
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
return variable
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def get_variable(self, space_id: str, name: str) -> Optional[SpaceVariable]:
|
||||
"""Get a variable by name."""
|
||||
conn = self._get_connection()
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"SELECT * FROM space_variables WHERE space_id = ? AND name = ?",
|
||||
(space_id, name),
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
if not row:
|
||||
return None
|
||||
return SpaceVariable(
|
||||
space_id=row["space_id"],
|
||||
name=row["name"],
|
||||
value=json.loads(row["value"]) if row["value"] else None,
|
||||
scope=row["scope"],
|
||||
)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def list_variables(self, space_id: str, scope: Optional[str] = None) -> List[SpaceVariable]:
|
||||
"""List variables in a space."""
|
||||
conn = self._get_connection()
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
if scope:
|
||||
cursor.execute(
|
||||
"SELECT * FROM space_variables WHERE space_id = ? AND scope = ?",
|
||||
(space_id, scope),
|
||||
)
|
||||
else:
|
||||
cursor.execute(
|
||||
"SELECT * FROM space_variables WHERE space_id = ?",
|
||||
(space_id,),
|
||||
)
|
||||
return [
|
||||
SpaceVariable(
|
||||
space_id=row["space_id"],
|
||||
name=row["name"],
|
||||
value=json.loads(row["value"]) if row["value"] else None,
|
||||
scope=row["scope"],
|
||||
)
|
||||
for row in cursor.fetchall()
|
||||
]
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def delete_variable(self, space_id: str, name: str) -> bool:
|
||||
"""Delete a variable."""
|
||||
conn = self._get_connection()
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"DELETE FROM space_variables WHERE space_id = ? AND name = ?",
|
||||
(space_id, name),
|
||||
)
|
||||
conn.commit()
|
||||
return cursor.rowcount > 0
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
class SqliteReferenceRepository(IReferenceRepository):
|
||||
"""
|
||||
SQLite implementation of the reference repository.
|
||||
"""
|
||||
|
||||
def __init__(self, db_path: str):
|
||||
"""Initialize the repository."""
|
||||
self.db_path = db_path
|
||||
initialize_space_tables(db_path)
|
||||
|
||||
def _get_connection(self) -> sqlite3.Connection:
|
||||
"""Get a database connection with foreign keys enabled."""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
conn.execute("PRAGMA foreign_keys = ON")
|
||||
return conn
|
||||
|
||||
def add_reference(self, reference: TransclusionReference) -> TransclusionReference:
|
||||
"""Add a transclusion reference."""
|
||||
conn = self._get_connection()
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT OR REPLACE INTO transclusion_references (source_doc_id, target_doc_id, space_id, created_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
reference.source_doc_id,
|
||||
reference.target_doc_id,
|
||||
reference.space_id,
|
||||
reference.created_at.isoformat(),
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
return reference
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def get_references_from(self, source_doc_id: str, space_id: str) -> List[TransclusionReference]:
|
||||
"""Get references from a source document."""
|
||||
conn = self._get_connection()
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"SELECT * FROM transclusion_references WHERE source_doc_id = ? AND space_id = ?",
|
||||
(source_doc_id, space_id),
|
||||
)
|
||||
return [
|
||||
TransclusionReference(
|
||||
source_doc_id=row["source_doc_id"],
|
||||
target_doc_id=row["target_doc_id"],
|
||||
space_id=row["space_id"],
|
||||
created_at=datetime.fromisoformat(row["created_at"]) if row["created_at"] else datetime.now(),
|
||||
)
|
||||
for row in cursor.fetchall()
|
||||
]
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def get_references_to(self, target_doc_id: str, space_id: str) -> List[TransclusionReference]:
|
||||
"""Get references to a target document."""
|
||||
conn = self._get_connection()
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"SELECT * FROM transclusion_references WHERE target_doc_id = ? AND space_id = ?",
|
||||
(target_doc_id, space_id),
|
||||
)
|
||||
return [
|
||||
TransclusionReference(
|
||||
source_doc_id=row["source_doc_id"],
|
||||
target_doc_id=row["target_doc_id"],
|
||||
space_id=row["space_id"],
|
||||
created_at=datetime.fromisoformat(row["created_at"]) if row["created_at"] else datetime.now(),
|
||||
)
|
||||
for row in cursor.fetchall()
|
||||
]
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def clear_references_from(self, source_doc_id: str, space_id: str) -> int:
|
||||
"""Clear references from a source document."""
|
||||
conn = self._get_connection()
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"DELETE FROM transclusion_references WHERE source_doc_id = ? AND space_id = ?",
|
||||
(source_doc_id, space_id),
|
||||
)
|
||||
conn.commit()
|
||||
return cursor.rowcount
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def get_dependents(self, document_id: str, space_id: str) -> List[str]:
|
||||
"""Get documents that depend on this document."""
|
||||
conn = self._get_connection()
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"SELECT DISTINCT source_doc_id FROM transclusion_references WHERE target_doc_id = ? AND space_id = ?",
|
||||
(document_id, space_id),
|
||||
)
|
||||
return [row["source_doc_id"] for row in cursor.fetchall()]
|
||||
finally:
|
||||
conn.close()
|
||||
Reference in New Issue
Block a user