# sqlite-serve Architecture This document explains the design principles and architecture of the sqlite-serve NGINX module. ## Design Principles ### 1. Parse, Don't Validate Instead of validating data and returning booleans, we **parse** data into refined types that encode invariants. **Bad (Validate):** ```rust fn is_valid_query(q: &str) -> bool { q.starts_with("SELECT") } let query = "DELETE FROM users"; // Oops! if is_valid_query(&query) { // false, but query is still a String // Now we have to check again... } ``` **Good (Parse):** ```rust pub struct SqlQuery(String); impl SqlQuery { pub fn parse(q: String) -> Result { if !q.trim().to_uppercase().starts_with("SELECT") { return Err("only SELECT queries allowed"); } Ok(SqlQuery(q)) } } let query = SqlQuery::parse("DELETE...")?; // Compile error! // If we have a SqlQuery, we KNOW it's valid ``` **Benefits:** - Type system enforces constraints - Can't accidentally use invalid data - Validation happens once at construction - Impossible states are unrepresentable ### 2. Correctness by Construction Use the type system to make invalid states impossible to represent. **Our Types:** | Type | Invariant | Enforced By | |------|-----------|-------------| | `SqlQuery` | Must be SELECT | `parse()` validation | | `TemplatePath` | Must end in .hbs | `parse()` validation | | `NginxVariable` | Must start with $ | `parse()` validation | | `ParamName` | Must start with : or be empty | `parse()` validation | | `DatabasePath` | Must not be empty | `parse()` validation | **Example:** ```rust // This won't compile - can't create SqlQuery with DELETE: let query = SqlQuery::parse("DELETE FROM x")?; // Returns Err // This compiles - valid SELECT: let query = SqlQuery::parse("SELECT * FROM x")?; // Returns Ok // Now query.as_str() is GUARANTEED to be a SELECT statement ``` ### 3. Dependency Injection Separate deterministic logic from non-deterministic I/O by injecting dependencies. **Functional Core, Imperative Shell:** ``` ┌─────────────────────────────────────────┐ │ Imperative Shell │ │ (lib.rs - NGINX FFI, actual I/O) │ │ │ │ ┌────────────────────────────────────┐ │ │ │ Functional Core │ │ │ │ (domain.rs - pure logic) │ │ │ │ │ │ │ │ • No I/O │ │ │ │ • No NGINX API calls │ │ │ │ • 100% testable │ │ │ │ • Deterministic │ │ │ └────────────────────────────────────┘ │ └─────────────────────────────────────────┘ ``` **Traits for Dependency Injection:** ```rust // Abstract over variable resolution trait VariableResolver { fn resolve(&self, var_name: &str) -> Result; } // Abstract over database access trait QueryExecutor { fn execute(&self, ...) -> Result>, String>; } // Abstract over template operations trait TemplateLoader { ... } trait TemplateRenderer { ... } ``` **Benefits:** - Core logic testable with mocks (no nginx needed) - Easy to test error scenarios - Clear separation of concerns - Can swap implementations ### 4. Ghost of Departed Proofs Use types to carry compile-time guarantees about data provenance. **Example - ParameterBinding:** ```rust enum ParameterBinding { Positional { variable: NginxVariable }, // PROOF: is a valid variable Named { name: ParamName, variable: NginxVariable }, // PROOF: both valid } ``` By requiring `NginxVariable` instead of `String`, we **prove** at compile time that: - The variable name was validated - It starts with `$` - It's not empty ## Module Structure ### Core Modules (1,727 lines total) ``` src/ ├── lib.rs (389 lines) # NGINX module registration (imperative shell) ├── domain.rs (306 lines) # Pure business logic (functional core) ├── types.rs (303 lines) # Type-safe wrappers (parse, don't validate) ├── config.rs (133 lines) # Configuration structures ├── query.rs (327 lines) # SQL execution (I/O boundary) ├── template.rs (160 lines) # Template loading (I/O boundary) ├── variable.rs (106 lines) # Variable resolution (nginx API boundary) └── main.rs (3 lines) # Unused entry point ``` ### Layering **Layer 1: Types (types.rs)** - Validated domain types - Zero dependencies on other modules - Pure validation logic - 18 validation tests **Layer 2: Domain (domain.rs)** - Pure business logic - Depends only on types - Dependency injection via traits - 100% testable with mocks - 4 integration tests **Layer 3: I/O Boundaries (query.rs, template.rs, variable.rs)** - Implement domain traits - Actual I/O operations - SQLite access, file system, nginx API - 23 I/O tests **Layer 4: NGINX Integration (lib.rs)** - Module registration - Directive handlers - Request handler (glue code) - Calls domain layer with real implementations ### Data Flow ``` 1. NGINX Config Parse ↓ 2. Directive Handlers (lib.rs) - Validate and parse into types (types.rs) - Store in ModuleConfig ↓ 3. HTTP Request ↓ 4. Request Handler (lib.rs) - Extract config - Resolve variables (variable.rs - I/O) - Call domain layer (domain.rs - pure) ↓ 5. Domain Layer (domain.rs) - Uses injected QueryExecutor (query.rs) - Uses injected TemplateLoader (template.rs) - Uses injected TemplateRenderer (template.rs) - Returns rendered HTML (deterministic) ↓ 6. Response (lib.rs) - Create nginx buffer - Send response ``` ## Testing Strategy ### Unit Tests (45 tests) **Type Validation (18 tests):** - Database path validation - SQL query validation (SELECT-only enforcement) - Template path validation (.hbs requirement) - Variable name validation ($ prefix) - Parameter name validation (: prefix) **Pure Logic (4 tests):** - Template path resolution - Parameter resolution with mocks - Request processing with mocks - Dependency injection integration **I/O Operations (23 tests):** - SQL execution (positional & named params) - Data type conversion - Template loading and discovery - Template rendering - Error handling ### Test Independence All tests are independent: - Use temporary files (`/tmp/test_*`) - Clean up after themselves - No shared state - Can run in parallel ### Mocking Core business logic uses trait-based dependency injection: ```rust struct MockQueryExecutor; impl QueryExecutor for MockQueryExecutor { fn execute(...) -> Result<...> { // Return test data without touching a real database Ok(vec![test_row()]) } } // Test pure logic without I/O: let processor = RequestProcessor::new( MockQueryExecutor, // Mock database MockTemplateLoader, // Mock file system MockTemplateRenderer, // Mock rendering ); ``` ## Type Safety Guarantees ### Compile-Time Guarantees 1. **Read-Only Queries**: `SqlQuery` type can only be constructed from SELECT statements 2. **Template Safety**: `TemplatePath` ensures .hbs extension 3. **Variable Safety**: `NginxVariable` ensures $ prefix 4. **Parameter Safety**: `ParamName` ensures : prefix for named params ### Runtime Safety - UTF-8 validation on all nginx variables - SQL injection protection via prepared statements - Path traversal protection (paths validated) - Error propagation with Result types ## Design Patterns ### 1. Newtype Pattern Wrap primitives in single-field structs to add type safety: ```rust pub struct SqlQuery(String); // Not just any String! pub struct DatabasePath(PathBuf); // Not just any PathBuf! ``` ### 2. Builder Pattern (via Parsing) ```rust SqlQuery::parse(input)? // Validates and constructs .as_str() // Access validated data ``` ### 3. Strategy Pattern (via Traits) ```rust trait QueryExecutor { fn execute(...) -> Result<...>; } // Different strategies: struct RealQueryExecutor; // Uses actual SQLite struct MockQueryExecutor; // Returns test data struct CachingQueryExecutor; // Adds caching layer ``` ### 4. Functional Core, Imperative Shell - **Core** (domain.rs): Pure functions, dependency injection, fully testable - **Shell** (lib.rs, query.rs, template.rs, variable.rs): I/O, nginx API, side effects ## Future Enhancements With this architecture, we can easily add: ### Type-Level State Machines ```rust struct Unvalidated; struct Validated; struct Config { query: String, _phantom: PhantomData, } impl Config { fn validate(self) -> Result, Error> { ... } } // Can only execute with validated config: fn execute(config: Config) { ... } ``` ### Phantom Types for Data Provenance ```rust struct FromNginx; struct FromLiteral; struct ParameterValue { value: String, _phantom: PhantomData, } // Track where data came from at compile time ``` ### Session Types ```rust struct TemplateNotLoaded; struct TemplateLoaded; struct Renderer { _phantom: PhantomData, } impl Renderer { fn load(self) -> Renderer { ... } } impl Renderer { fn render(&self) { ... } // Only available after loading! } ``` ## Performance Considerations ### Type Safety Has Zero Cost All validation and type wrapping happens at: - **Compile time**: Type checking - **Parse time**: Config validation (once per config reload) - **Runtime**: Zero overhead (newtype pattern compiles away) ### Memory Layout ```rust #[repr(transparent)] pub struct SqlQuery(String); // Same size as String ``` Newtypes have the same memory layout as their wrapped type. ## Summary The refactored architecture provides: ✅ **Type Safety**: Invalid configurations rejected at parse time ✅ **Testability**: Pure core testable without nginx ✅ **Maintainability**: Clear module boundaries ✅ **Correctness**: Impossible states unrepresentable ✅ **Flexibility**: Easy to add features via traits ✅ **Performance**: Zero-cost abstractions **Test Results:** 45/45 tests passing **Lines of Code:** 1,727 (well-organized across 8 modules) **Production Status:** ✅ Verified working