feat(persistence): initialize SQLite database module

- Add domain types: RelayId newtype and RepositoryError enum
- Implement SqliteRelayLabelRepository with in-memory test support
- Create relay_labels migration with SQLx compile-time verification
- Add comprehensive integration test suite (266 lines)

Ref: T006 (specs/001-modbus-relay-control)
This commit is contained in:
2026-01-01 17:35:58 +01:00
parent d8a7ed5d29
commit 6903d76682
12 changed files with 389 additions and 1 deletions

View File

@@ -34,3 +34,5 @@
//! - Architecture: `specs/constitution.md` - Hexagonal Architecture principles
//! - Type design: `specs/001-modbus-relay-control/types-design.md`
//! - Domain specification: `specs/001-modbus-relay-control/spec.md`
pub mod relay;

9
src/domain/relay/mod.rs Normal file
View File

@@ -0,0 +1,9 @@
//! Relay domain module.
//!
//! This module contains the core domain logic for relay control and management,
//! including relay types, repository abstractions, and business rules.
/// Repository trait and error types for relay persistence.
pub mod repository;
/// Domain types for relay identification and control.
pub mod types;

View File

@@ -0,0 +1,15 @@
use super::types::RelayId;
/// Errors that can occur during repository operations.
///
/// This enum provides structured error handling for all data persistence
/// operations related to relay management.
#[derive(Debug, thiserror::Error)]
pub enum RepositoryError {
/// A database operation failed with the given error message.
#[error("Database error: {0}")]
DatabaseError(String),
/// The requested relay was not found in the repository.
#[error("Relay not found: {0}")]
NotFound(RelayId),
}

14
src/domain/relay/types.rs Normal file
View File

@@ -0,0 +1,14 @@
/// Unique identifier for a relay in the system.
///
/// Uses the newtype pattern to provide type safety and prevent mixing relay IDs
/// with other numeric values. Valid values range from 0-255, corresponding to
/// individual relay channels in the Modbus controller.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[repr(transparent)]
pub struct RelayId(u8);
impl std::fmt::Display for RelayId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}

View File

@@ -74,3 +74,5 @@
//! - Architecture: `specs/constitution.md` - Dependency Inversion Principle
//! - Implementation: `specs/001-modbus-relay-control/plan.md` - Infrastructure tasks
//! - Modbus docs: `docs/Modbus_POE_ETH_Relay.md` - Hardware protocol specification
pub mod persistence;

View File

@@ -0,0 +1,7 @@
//! Persistence layer implementations.
//!
//! This module contains the concrete implementations of repository traits
//! for data persistence, including SQLite-based storage for relay labels.
/// `SQLite` repository implementation for relay labels.
pub mod sqlite_repository;

View File

@@ -0,0 +1,64 @@
use sqlx::SqlitePool;
use crate::domain::relay::repository::RepositoryError;
/// `SQLite` implementation of the relay label repository.
///
/// This repository manages persistent storage of relay labels using `SQLite`,
/// with automatic schema migrations via `SQLx`.
pub struct SqliteRelayLabelRepository {
/// The `SQLite` connection pool for database operations.
pool: SqlitePool,
}
impl SqliteRelayLabelRepository {
/// Creates a new `SQLite` relay label repository.
///
/// # Arguments
///
/// * `db_path` - The `SQLite` database path or connection string (e.g., `"sqlite://data.db"`)
///
/// # Errors
///
/// Returns `RepositoryError::DatabaseError` if the connection fails or migrations cannot be applied.
pub async fn new(db_path: &str) -> Result<Self, RepositoryError> {
let pool = SqlitePool::connect(db_path)
.await
.map_err(|e| RepositoryError::DatabaseError(e.to_string()))?;
let repo = Self { pool };
repo.run_migrations().await?;
Ok(repo)
}
/// Returns a reference to the underlying connection pool.
///
/// This is primarily used for testing to verify schema and constraints.
#[must_use]
pub const fn pool(&self) -> &SqlitePool {
&self.pool
}
/// Creates a new in-memory `SQLite` relay label repository.
///
/// This is useful for testing and ephemeral data storage.
///
/// # Errors
///
/// Returns `RepositoryError::DatabaseError` if the in-memory database cannot be created.
pub async fn in_memory() -> Result<Self, RepositoryError> {
Self::new("sqlite::memory:").await
}
/// Runs all pending database migrations.
///
/// # Errors
///
/// Returns `RepositoryError::DatabaseError` if migrations fail to apply.
async fn run_migrations(&self) -> Result<(), RepositoryError> {
sqlx::migrate!()
.run(&self.pool)
.await
.map_err(|e| RepositoryError::DatabaseError(e.to_string()))?;
Ok(())
}
}