syntaxis/docs/databases/surrealdb/surrealdb-example.rs
Jesús Pérez 9cef9b8d57 refactor: consolidate configuration directories
Merge _configs/ into config/ for single configuration directory.
Update all path references.

Changes:
- Move _configs/* to config/
- Update .gitignore for new patterns
- No code references to _configs/ found

Impact: -1 root directory (layout_conventions.md compliance)
2025-12-26 18:36:23 +00:00

473 lines
15 KiB
Rust

// Example: SurrealDB Integration for syntaxis
// This demonstrates the trait-based abstraction pattern
// ============================================================================
// Step 1: Define Database Trait (workspace-core/src/persistence/mod.rs)
// ============================================================================
use async_trait::async_trait;
use crate::error::Result;
use crate::types::*;
/// Unified database trait - implementation agnostic
#[async_trait]
pub trait Database: Send + Sync + Clone {
// ---- Project Management ----
/// Create a new project
async fn create_project(&self, name: &str, description: &str, project_type: &str) -> Result<Project>;
/// Get project by ID
async fn get_project(&self, id: &str) -> Result<Project>;
/// List all projects
async fn list_projects(&self) -> Result<Vec<Project>>;
/// Update project
async fn update_project(&self, id: &str, name: &str, description: &str) -> Result<Project>;
/// Delete project
async fn delete_project(&self, id: &str) -> Result<()>;
// ---- Checklist Management ----
/// Create checklist item
async fn create_checklist_item(&self, project_id: &str, item: &ChecklistItem) -> Result<ChecklistItem>;
/// Get checklist items for phase
async fn list_checklist_items(&self, project_id: &str, phase: &str) -> Result<Vec<ChecklistItem>>;
/// Mark checklist item complete
async fn complete_checklist_item(&self, id: &str) -> Result<ChecklistItem>;
// ---- Phase Management ----
/// Record phase transition (with graph edge)
async fn record_phase_transition(&self, transition: &PhaseTransition) -> Result<()>;
/// Get phase history for project
async fn get_phase_history(&self, project_id: &str) -> Result<Vec<PhaseTransition>>;
// ---- Security Assessments ----
/// Store security assessment result
async fn create_assessment(&self, assessment: &SecurityAssessment) -> Result<SecurityAssessment>;
/// Get latest assessment for project
async fn get_latest_assessment(&self, project_id: &str) -> Result<Option<SecurityAssessment>>;
// ---- Tool Configuration ----
/// Save tool configuration
async fn set_tool_config(&self, project_id: &str, tool_name: &str, config: &serde_json::Value) -> Result<()>;
/// Get tool configuration
async fn get_tool_config(&self, project_id: &str, tool_name: &str) -> Result<Option<serde_json::Value>>;
// ---- Audit Trail ----
/// Log activity
async fn log_activity(&self, activity: &Activity) -> Result<()>;
/// Get activity history
async fn get_activities(&self, project_id: &str, limit: u32) -> Result<Vec<Activity>>;
}
// ============================================================================
// Step 2: SQLite Implementation (workspace-core/src/persistence/sqlite_impl.rs)
// ============================================================================
use sqlx::{SqlitePool, Row};
#[derive(Clone)]
pub struct SqliteDatabase {
pool: SqlitePool,
}
impl SqliteDatabase {
pub async fn new(db_path: &str) -> Result<Self> {
let path = std::path::Path::new(db_path);
if let Some(parent) = path.parent() {
if !parent.as_os_str().is_empty() {
tokio::fs::create_dir_all(parent).await?;
}
}
let database_url = if db_path.starts_with('/') {
format!("sqlite://{}", db_path)
} else {
format!("sqlite:{}", db_path)
};
let database_url = format!("{}?mode=rwc", database_url);
let pool = sqlx::SqlitePoolOptions::new()
.max_connections(5)
.connect(&database_url)
.await?;
Self::run_migrations(&pool).await?;
Ok(Self { pool })
}
async fn run_migrations(pool: &SqlitePool) -> Result<()> {
// Enable foreign keys
sqlx::query("PRAGMA foreign_keys = ON")
.execute(pool)
.await?;
// Create tables (existing code)
sqlx::query(
"CREATE TABLE IF NOT EXISTS projects (
id TEXT PRIMARY KEY NOT NULL,
name TEXT NOT NULL,
version TEXT NOT NULL,
description TEXT NOT NULL,
project_type TEXT NOT NULL DEFAULT 'MultiLang',
current_phase TEXT NOT NULL DEFAULT 'Creation',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)",
)
.execute(pool)
.await?;
// ... more CREATE TABLE statements
Ok(())
}
}
#[async_trait]
impl Database for SqliteDatabase {
async fn create_project(&self, name: &str, description: &str, project_type: &str) -> Result<Project> {
let id = uuid::Uuid::new_v4().to_string();
let now = chrono::Utc::now().to_rfc3339();
sqlx::query(
"INSERT INTO projects (id, name, version, description, project_type, current_phase, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
)
.bind(&id)
.bind(name)
.bind("0.1.0")
.bind(description)
.bind(project_type)
.bind("Creation")
.bind(&now)
.bind(&now)
.execute(&self.pool)
.await?;
Ok(Project {
id,
name: name.to_string(),
version: "0.1.0".to_string(),
description: description.to_string(),
project_type: project_type.to_string(),
current_phase: "Creation".to_string(),
created_at: now.clone(),
updated_at: now,
})
}
async fn get_project(&self, id: &str) -> Result<Project> {
let row = sqlx::query_as::<_, Project>(
"SELECT id, name, version, description, project_type, current_phase, created_at, updated_at
FROM projects WHERE id = ?",
)
.bind(id)
.fetch_optional(&self.pool)
.await?
.ok_or_else(|| crate::error::LifecycleError::ProjectNotFound(id.to_string()))?;
Ok(row)
}
// ... implement all other trait methods
}
// ============================================================================
// Step 3: SurrealDB Implementation (workspace-core/src/persistence/surrealdb_impl.rs)
// ============================================================================
use surrealdb::{Client, Surreal};
use surrealdb::opt::auth::Root;
#[derive(Clone)]
pub struct SurrealDatabase {
db: Surreal<Client>,
}
impl SurrealDatabase {
pub async fn new(url: &str, namespace: &str, database: &str) -> Result<Self> {
// Connect to SurrealDB
let db = Surreal::new::<Ws>(url)
.await?;
// Authenticate (if needed)
db.signin(Root {
username: "root",
password: "root",
})
.await?;
// Select namespace and database
db.use_ns(namespace).use_db(database).await?;
// Initialize schema
Self::init_schema(&db).await?;
Ok(Self { db })
}
async fn init_schema(db: &Surreal<Client>) -> Result<()> {
// Define collection for projects
db.query(
"DEFINE TABLE projects SCHEMAFULL
COMMENT = 'Project collection with typed schema';"
)
.await?;
// Define fields
db.query("DEFINE FIELD projects.id AS string")
.await?;
db.query("DEFINE FIELD projects.name AS string")
.await?;
db.query("DEFINE FIELD projects.version AS string")
.await?;
db.query("DEFINE FIELD projects.current_phase AS enum<Creation,Development,Testing,Publishing,Archived>")
.await?;
// Define graph relation for phase transitions
db.query(
"DEFINE TABLE transitioned_to SCHEMALESS
COMMENT = 'Phase transition relationships';"
)
.await?;
// Define indices
db.query("DEFINE INDEX idx_projects_created ON TABLE projects COLUMNS created_at DESC")
.await?;
db.query("DEFINE INDEX idx_checklist_project ON TABLE checklist_items COLUMNS project_id")
.await?;
Ok(())
}
}
#[async_trait]
impl Database for SurrealDatabase {
async fn create_project(&self, name: &str, description: &str, project_type: &str) -> Result<Project> {
let id = uuid::Uuid::new_v4().to_string();
let now = chrono::Utc::now().to_rfc3339();
let project = Project {
id: id.clone(),
name: name.to_string(),
version: "0.1.0".to_string(),
description: description.to_string(),
project_type: project_type.to_string(),
current_phase: "Creation".to_string(),
created_at: now.clone(),
updated_at: now,
};
// Create project in SurrealDB (document-style)
self.db.create::<Project>(("projects", &id))
.content(&project)
.await?;
Ok(project)
}
async fn get_project(&self, id: &str) -> Result<Project> {
let project: Option<Project> = self.db.select(("projects", id))
.await?;
project.ok_or_else(|| crate::error::LifecycleError::ProjectNotFound(id.to_string()))
}
async fn list_projects(&self) -> Result<Vec<Project>> {
// SurrealDB uses SQL-like syntax
let projects: Vec<Project> = self.db
.query("SELECT * FROM projects WHERE archived = false")
.await?
.take(0)?;
Ok(projects)
}
async fn record_phase_transition(&self, transition: &PhaseTransition) -> Result<()> {
// Create graph edge from project to phase
self.db.query(
"LET $project = type::thing('projects', $project_id);
LET $to_phase = type::thing('phases', $to_phase);
RELATE $project->transitioned_to->$to_phase
SET
reason = $reason,
timestamp = $timestamp,
from_phase = $from_phase;"
)
.bind(("project_id", &transition.project_id))
.bind(("to_phase", &transition.to_phase))
.bind(("from_phase", &transition.from_phase))
.bind(("reason", &transition.reason))
.bind(("timestamp", &transition.timestamp))
.await?;
Ok(())
}
async fn get_phase_history(&self, project_id: &str) -> Result<Vec<PhaseTransition>> {
// Query graph edges for this project
let transitions: Vec<PhaseTransition> = self.db
.query("SELECT * FROM type::thing('projects', $id) -> transitioned_to ->")
.bind(("id", project_id))
.await?
.take(0)?;
Ok(transitions)
}
// ... implement remaining trait methods
}
// ============================================================================
// Step 4: Enum-based Runtime Selection
// ============================================================================
pub enum DatabaseBackend {
Sqlite(SqliteDatabase),
SurrealDB(SurrealDatabase),
}
impl DatabaseBackend {
pub async fn from_config(config: &DatabaseConfig) -> Result<Self> {
match config.engine.as_str() {
"sqlite" => {
let db = SqliteDatabase::new(&config.sqlite_path).await?;
Ok(DatabaseBackend::Sqlite(db))
}
"surrealdb" => {
let db = SurrealDatabase::new(
&config.surrealdb.url,
&config.surrealdb.namespace,
&config.surrealdb.database,
)
.await?;
Ok(DatabaseBackend::SurrealDB(db))
}
_ => Err(crate::error::LifecycleError::Config(
format!("Unknown database engine: {}", config.engine)
)),
}
}
}
// Make enum implement Database trait (delegate pattern)
#[async_trait]
impl Database for DatabaseBackend {
async fn create_project(&self, name: &str, description: &str, project_type: &str) -> Result<Project> {
match self {
DatabaseBackend::Sqlite(db) => db.create_project(name, description, project_type).await,
DatabaseBackend::SurrealDB(db) => db.create_project(name, description, project_type).await,
}
}
async fn get_project(&self, id: &str) -> Result<Project> {
match self {
DatabaseBackend::Sqlite(db) => db.get_project(id).await,
DatabaseBackend::SurrealDB(db) => db.get_project(id).await,
}
}
// ... delegate all trait methods
}
// ============================================================================
// Step 5: Usage in Application Code (unchanged!)
// ============================================================================
pub async fn main_application(db: impl Database) -> Result<()> {
// Create project (works with both SQLite AND SurrealDB!)
let project = db.create_project(
"My Project",
"A workspace project",
"MultiLang",
).await?;
println!("Created project: {}", project.id);
// Fetch project
let fetched = db.get_project(&project.id).await?;
println!("Fetched: {}", fetched.name);
// Record phase transition
db.record_phase_transition(&PhaseTransition {
project_id: project.id.clone(),
from_phase: "Creation".to_string(),
to_phase: "Development".to_string(),
timestamp: chrono::Utc::now().to_rfc3339(),
reason: Some("Starting development phase".to_string()),
}).await?;
Ok(())
}
// ============================================================================
// Configuration Example
// ============================================================================
#[derive(serde::Deserialize)]
pub struct DatabaseConfig {
pub engine: String, // "sqlite" or "surrealdb"
pub sqlite_path: String,
pub surrealdb: SurrealDBConfig,
}
#[derive(serde::Deserialize)]
pub struct SurrealDBConfig {
pub url: String,
pub namespace: String,
pub database: String,
pub username: String,
pub password: String,
}
// ============================================================================
// Key Benefits of This Approach
// ============================================================================
/*
1. ✅ ZERO BREAKING CHANGES
- Existing code using `Database` trait works with both implementations
- Can switch backends at runtime via configuration
2. ✅ GRADUAL MIGRATION
- Deploy with SQLite (default)
- Add SurrealDB support incrementally
- Let users choose which backend to use
3. ✅ TESTABLE
- Create mock implementation for testing
- Test business logic without database
- Each backend tested independently
4. ✅ PERFORMANCE
- Can benchmark both backends
- Keep SQLite for simple deployments
- Use SurrealDB for advanced features (graph queries, live subscriptions)
5. ✅ FUTURE-PROOF
- Easy to add PostgreSQL support later
- Easy to add MongoDB support later
- Any database can implement the trait
6. ✅ OPERATIONAL FLEXIBILITY
- Run same code with different databases
- Migrate data at your own pace
- Rollback easily if needed
*/