Files
lumi-re/backend/application/system_resources_orm_setup.md
T
2026-07-13 21:57:58 +02:00

11 KiB

System Resources ORM Setup - Diesel Implementation

This document provides all the code needed to convert the system_resource entity from raw SQL queries to Diesel ORM, following the same pattern used for the user entity.

Current State Analysis

The system_resources entity currently has:

  • Diesel model defined in infrastructure/src/system_resources/model.rs
  • No Diesel repository implementation (only SQL-based)
  • No Diesel adapter implementation (only SQL-based)
  • Missing table definition in schema.rs

Step 1: Add system_resources Table Definition to schema.rs

File: infrastructure/src/schema.rs

Add the following table definition after the user table:

// Define the system_resources table structure matching the current SQL schema
table! {
    system_resources (id) {
        id -> Text,
        datetime -> Integer,
        cpu_usage -> Float,
        total_memory -> Integer,
        used_memory -> Integer,
    }
}

Step 2: Create Diesel Repository Implementation

File: infrastructure/src/system_resources/diesel_repository.rs

use diesel::prelude::*;
use diesel::sqlite::SqliteConnection;
use std::sync::{Arc, Mutex};
use chrono::{DateTime, Utc};
use super::model::SystemResourcesEntity;

#[derive(Clone)]
pub struct SystemResourcesDieselRepository {
    pub database_connection: Arc<Mutex<SqliteConnection>>,
}

impl SystemResourcesDieselRepository {
    pub fn new(database_connection: Arc<Mutex<SqliteConnection>>) -> Self {
        Self {
            database_connection,
        }
    }

    fn get_connection(&self) -> std::sync::MutexGuard<'_, SqliteConnection> {
        self.database_connection.lock().unwrap()
    }

    pub fn create_table(&self) -> Result<(), diesel::result::Error> {
        let mut connection = self.get_connection();
        diesel::sql_query(
            "CREATE TABLE IF NOT EXISTS system_resources (
                id TEXT PRIMARY KEY,
                datetime INTEGER NOT NULL,
                cpu_usage REAL NOT NULL,
                total_memory INTEGER NOT NULL,
                used_memory INTEGER NOT NULL
            );"
        ).execute(&mut *connection)?;
        Ok(())
    }

    pub fn insert(&self, resource: &SystemResourcesEntity) -> Result<(), diesel::result::Error> {
        let mut connection = self.get_connection();
        
        diesel::insert_into(crate::schema::system_resources::table)
            .values(resource)
            .execute(&mut *connection)?;
        Ok(())
    }

    pub fn get_by_id(&self, id: &str) -> Result<Option<SystemResourcesEntity>, diesel::result::Error> {
        let mut connection = self.get_connection();
        
        let result: Result<SystemResourcesEntity, _> = crate::schema::system_resources::table
            .find(id)
            .first(&mut *connection);
        
        match result {
            Ok(resource_diesel) => Ok(Some(resource_diesel)),
            Err(diesel::result::Error::NotFound) => Ok(None),
            Err(e) => Err(e),
        }
    }

    pub fn get_between(&self, start: i64, end: i64) -> Result<Vec<SystemResourcesEntity>, diesel::result::Error> {
        let mut connection = self.get_connection();
        
        let resources: Vec<SystemResourcesEntity> = crate::schema::system_resources::table
            .filter(crate::schema::system_resources::datetime.between(start, end))
            .order(crate::schema::system_resources::datetime.asc())
            .load(&mut *connection)?;
        
        Ok(resources)
    }
}

Step 3: Create Diesel Adapter Implementation

File: infrastructure/src/system_resources/diesel_adapter.rs

use std::io::{Error, ErrorKind};
use chrono::{DateTime, Utc};
use surveillant_systeme_domain::system_resources::model::SystemResources;
use surveillant_systeme_domain::system_resources::port::SystemResourcesPort;
use crate::system_resources::diesel_repository::SystemResourcesDieselRepository;
use sysinfo::{CpuRefreshKind, MemoryRefreshKind, RefreshKind, System};

#[derive(Clone)]
pub struct SystemResourcesDieselAdapter {
    pub repository: SystemResourcesDieselRepository
}

impl SystemResourcesDieselAdapter {
    pub fn new(repository: SystemResourcesDieselRepository) -> Self {
        Self {
            repository
        }
    }

    fn get_cpu_usage_from_shell(&self, _: &System) -> f32 {
        use std::process::Command;
        use log::error;
        
        if cfg!(target_os = "linux") {
            self.get_cpu_usage_from_command("echo $[100-$(vmstat 1 2|tail -1|awk '{print $15}')]")
        } else if cfg!(target_os = "macos") {
            self.get_cpu_usage_from_command("top -l 1 | grep -E \"^CPU\" | cut -d ' ' -f3 | cut -d '%' -f1")
        } else {
            error!("Unhandled OS…");
            0f32
        }
    }

    fn get_cpu_usage_from_command(&self, command: &str) -> f32 {
        use std::process::Command;
        
        let command_output = Command::new("sh")
            .arg("-c")
            .arg(command)
            .output()
            .expect("Error while retrieving CPU usage from command…");

        if command_output.status.success() {
            let string = String::from_utf8(command_output.stdout).unwrap();
            string.trim().parse().unwrap()
        } else {
            let stderr = String::from_utf8(command_output.stderr).unwrap();
            error!("Command failed with error:\n{}", stderr);
            0f32
        }
    }
}

impl SystemResourcesPort for SystemResourcesDieselAdapter {
    fn insert_system_resources(&self, system_resources: &SystemResources) -> Result<(), Error> {
        let resource_entity = crate::system_resources::model::SystemResourcesEntity::from(system_resources);
        self.repository.insert(&resource_entity)
            .map_err(|error| Error::new(
                ErrorKind::Other,
                format!("Unable to insert the system resources… Cause: {}", error)
            ))
    }

    fn get_system_resources_lines_between(&self, start: &DateTime<Utc>, end: &DateTime<Utc>) -> Result<Vec<SystemResources>, Error> {
        self.repository.get_between(start.timestamp(), end.timestamp())
            .map_err(|error| Error::new(
                ErrorKind::Other,
                format!("Unable to retrieve the system resources in the given period… Cause: {}", error)
            ))
            .map(|resource_entities| resource_entities.into_iter().map(|resource_entity| resource_entity.to_domain()).collect())
    }

    fn get_system_resources(&self) -> Result<SystemResources, Error> {
        let system = System::new_with_specifics(
            RefreshKind::nothing().with_cpu(CpuRefreshKind::everything()).with_memory(MemoryRefreshKind::everything()),
        );
        let cpu_usage = self.get_cpu_usage_from_shell(&system);
        let total_memory = system.total_memory();
        let used_memory = system.used_memory();

        let result = SystemResources::new(
            cpu_usage,
            total_memory,
            used_memory,
        );
        Ok(result)
    }

    fn clone_box(&self) -> Box<dyn SystemResourcesPort> {
        Box::new(self.clone())
    }
}

Step 4: Update Module Files

File: infrastructure/src/system_resources/mod.rs

Add the new Diesel implementations to the module exports:

// Add these lines to expose the new Diesel implementations
pub mod diesel_repository;
pub mod diesel_adapter;

// Re-export the types for convenience
pub use diesel_repository::SystemResourcesDieselRepository;
pub use diesel_adapter::SystemResourcesDieselAdapter;

Step 5: Update Cargo.toml Dependencies

File: infrastructure/Cargo.toml

Ensure Diesel dependencies are properly configured. The file should include:

diesel = { version = "2.1", features = ["sqlite"] }
diesel_migrations = "2.1"

Step 6: Update Database Initialization

File: infrastructure/src/common/database_initialisation.rs

Update the initialization code to use Diesel for creating tables:

// Replace the existing system_resources table creation with Diesel version
impl DatabaseInitialisation {
    pub fn create_tables(&self) -> Result<(), Error> {
        // User table (already using Diesel)
        let user_repository = crate::user::diesel_repository::UserDieselRepository::new(self.database_connection.clone());
        user_repository.create_table()?;
        
        // System resources table (now using Diesel)
        let system_resources_repository = crate::system_resources::diesel_repository::SystemResourcesDieselRepository::new(self.database_connection.clone());
        system_resources_repository.create_table()?;
        
        Ok(())
    }
}

Step 7: Update Application Context

File: launcher/src/application_context.rs

Update the application context to use Diesel adapters:

// Replace system resources adapter initialization
let system_resources_repository = SystemResourcesDieselRepository::new(database_connection.clone());
let system_resources_adapter = SystemResourcesDieselAdapter::new(system_resources_repository);

Migration Strategy

Phase 1: Parallel Implementation (Current Step)

  1. Create Diesel model (already done)
  2. Add table definition to schema.rs
  3. Implement SystemResourcesDieselRepository
  4. Implement SystemResourcesDieselAdapter
  5. Update module exports

Phase 2: Integration

  1. Update database initialization to use Diesel
  2. Update application context to use Diesel adapters
  3. Test Diesel implementation alongside existing SQL version

Phase 3: Validation

  1. Verify identical behavior through manual testing
  2. Check performance characteristics
  3. Ensure all system resources API endpoints work correctly

Phase 4: Cleanup (Future)

  1. Remove old SQL-based adapter implementation
  2. Remove old SQL-based repository implementation
  3. Update all imports to use Diesel versions

Key Differences from User Implementation

  1. Model Differences: SystemResourcesEntity uses different field types:

    • datetime as i64 (Unix timestamp) instead of DateTime
    • cpu_usage as f32 instead of String
    • Memory fields as i64 instead of other types
  2. Repository Method Differences:

    • No equivalent to get_by_username (system resources use datetime ranges)
    • get_between method for time-based queries instead of ID/username lookups
    • Different query patterns for temporal data
  3. Adapter Method Differences:

    • Additional get_system_resources() method that collects live system data
    • Time-based range queries instead of single entity lookups

Success Criteria

  1. SystemResourcesDieselRepository implements all necessary methods
  2. SystemResourcesDieselAdapter provides same functionality as SQL version
  3. All system resources API endpoints work identically with Diesel backend
  4. No data migration required - existing system resources continue to work
  5. Performance characteristics meet or exceed current implementation
  6. Error handling produces same error types and messages