define_dal_transactions_macro.md (10387B)
1 # define_dal_transactions! Macro Overview 2 3 ## What It Is 4 5 The `define_dal_transactions!` macro is a **declarative procedural macro** in Rust that automates the creation of trait signatures for database access layer (DAL) operations. It's designed to provide an abstract interface for database operations while allowing SQL-specific implementations to be swapped underneath. 6 7 At its core, it's a **macro-templating mechanism** that: 8 9 1. **Takes trait and function names** as input 10 2. **Generates trait definitions** with properly typed async methods 11 3. **Returns `sqlx::Result` futures** indicating database operations 12 4. **Supports generics** for flexible implementation 13 14 Think of it as a "trait factory macro" - given a pattern like `CreateProject => create_project`, it generates a trait that says "implement `create_project` that returns a Future which resolves to a database query result." 15 16 ## How It Works 17 18 The macro uses Rust's `macro_rules!` syntax with complex pattern matching to parse the input and generate matching output. Here's the breakdown: 19 20 ### Input Pattern 21 22 ```rust 23 define_dal_transactions!( 24 CreateProject => create_project<Id, Name>(id: Id, name: Name) -> Project, 25 ListProjects => list_projects<Department>(department: Department) -> Vec<Project>, 26 ) 27 ``` 28 29 ### Generated Output 30 31 The macro transforms the input declarative style into actual trait definitions with async method signatures. 32 33 ## Technical Details 34 35 ### Internal Structure 36 37 ```rust 38 #[macro_export] 39 macro_rules! define_dal_transactions { 40 ( 41 $( $trait:ident => $func_name:ident $(< $($generic:tt),* >)? ($($param:ident : $ptype:ty),*) -> $rtype:ty ),* $(,)? 42 ) => { 43 $( 44 pub trait $trait { 45 fn $func_name $(< $($generic),* >)? ($($param : $ptype),*) -> impl std::future::Future<Output = sqlx::Result<$rtype>> + Send; 46 } 47 )* 48 }; 49 } 50 ``` 51 52 ### Pattern Components 53 54 | Token | Meaning | 55 |-------|---------| 56 | `$trait:ident` | Captures a trait name (e.g., `CreateProject`) | 57 | `$func_name:ident` | Captures a function name (e.g., `create_project`) | 58 | `< $($generic:tt),* >` | Supports optional generic parameters `<T, U>` | 59 | `$($param:ident : $ptype:ty),*` | List of parameters with types | 60 | `$rtype:ty` | Return type (must be `sqlx::Result<T>`) | 61 | `$()*` | Repetition pattern for multiple entries | 62 63 ### The Return Expression 64 65 ```rust 66 impl std::future::Future<Output = sqlx::Result<$rtype>> + Send 67 ``` 68 69 This uses Rust's trait system to indicate: 70 71 1. **Returns a `Future`** - The function is async 72 2. **Future resolves to `sqlx::Result<T>`** - Handles SQL query errors/success 73 3. **`+ Send` bound** - Allows the future to be moved across thread boundaries when used in concurrent contexts 74 75 ## When You Use It 76 77 The macro is used in data access layer implementations to establish a **contract** between: 78 79 1. **The trait definition** (DAL abstraction - what operations exist) 80 2. **The trait implementation** (SQL execution - what the operations do) 81 3. **The core business logic** (uses the abstraction without knowing SQL) 82 83 ## Example: The DAL Context 84 85 In the DAL architecture pattern, this macro bridges the gap between high-level business logic and low-level SQL execution: 86 87 ```rust 88 // User writes: define_dal_transactions! macro with trait name 89 define_dal_transactions!( 90 CreateProject => create_project(project: NewProject) -> Project 91 ); 92 93 // The macro generates: trait definition in compile-time 94 pub trait CreateProject { 95 fn create_project(project: NewProject) -> impl std::future::Future<Output = sqlx::Result<Project>> + Send; 96 } 97 ``` 98 99 Then, a concrete implementation provides the SQL: 100 101 ```rust 102 // SqlxPostGresDescriptor implements CreateProject (generated by #[db_transaction] macro impl) 103 #[db_transaction(SqlxPostGresDescriptor, CreateProject)] 104 async fn create_project(project: NewProject) -> Project { 105 // ... SQL execution code ... 106 } 107 ``` 108 109 ## Usage Examples 110 111 ### Example 1: Basic CRUD 112 113 ```rust 114 define_dal_transactions!( 115 GetProjectById => get_project(id: i32) -> Option<Project>, 116 CreateProject => create_project(project: NewProject) -> Project, 117 UpdateProject => update_project(id: i32, update: ProjectUpdate) -> Option<Project>, 118 DeleteProject => delete_project(id: i32) -> Result<(), sqlx::Error>, 119 ListProjects => list_projects(limit: i32) -> Vec<Project>, 120 ); 121 ``` 122 123 Generates: 124 125 ```rust 126 pub trait GetProjectById { 127 fn get_project(id: i32) -> impl std::future::Future<Output = sqlx::Result<Option<Project>>> + Send; 128 } 129 130 pub trait CreateProject { 131 fn create_project(project: NewProject) -> impl std::future::Future<Output = sqlx::Result<Project>> + Send; 132 } 133 134 pub trait UpdateProject { 135 fn update_project(id: i32, update: ProjectUpdate) -> impl std::future::Future<Output = sqlx::Result<Option<Project>>> + Send; 136 } 137 138 pub trait DeleteProject { 139 fn delete_project(id: i32) -> impl std::future::Future<Output = sqlx::Result<(), sqlx::Error>> + Send; 140 } 141 142 pub trait ListProjects { 143 fn list_projects(limit: i32) -> impl std::future::Future<Output = sqlx::Result<Vec<Project>>> + Send; 144 } 145 ``` 146 147 ### Example 2: With Generics 148 149 ```rust 150 define_dal_transactions!( 151 GetUserById<Uid> => get_user<Uid>(id: Uid) -> Option<User<Uid>>, 152 GetUserProjects<Uid> => get_user_projects<Uid>(user_id: Uid) -> Vec<Project>, 153 CreateProjectWithOwner<Uid> => create_project_with_owner<Uid>(owner_id: Uid, project: NewProject) -> Project, 154 ); 155 ``` 156 157 Generates: 158 159 ```rust 160 pub trait GetUserById<Uid> { 161 fn get_user<Uid>(id: Uid) -> impl std::future::Future<Output = sqlx::Result<Option<User<Uid>>>> + Send; 162 } 163 164 pub trait GetUserProjects<Uid> { 165 fn get_user_projects<Uid>(user_id: Uid) -> impl std::future::Future<Output = sqlx::Result<Vec<Project>>> + Send; 166 } 167 168 pub trait CreateProjectWithOwner<Uid> { 169 fn create_project_with_owner<Uid>(owner_id: Uid, project: NewProject) -> impl std::future::Future<Output = sqlx::Result<Project>> + Send; 170 } 171 ``` 172 173 ### Example 3: Multiple Parameters 174 175 ```rust 176 define_dal_transactions!( 177 CreateMultipleTickets => create_tickets<Id>(ticket_params: Vec<TicketParams>) -> Result<Vec<Ticket>, CreateError>, 178 GetTicketsByRange => get_tickets_by_range(start_id: i64, end_id: i64) -> Vec<Ticket>, 179 GetTicketsByStatus => get_tickets_by_status(status: TicketStatus) -> Vec<Ticket>, 180 ); 181 ``` 182 183 Generates proper async signatures with multiple parameters. 184 185 ## Benefits 186 187 ### 1. **Abstraction** 188 The macro-defined traits provide a clear contract for database operations without exposing SQL details. 189 190 ### 2. **Testing** 191 Core business logic can use type parameters to swap implementations: 192 193 ```rust 194 // In core layer: 195 pub async fn create_project<X: CreateProject>(...) 196 where 197 X: CreateProject, 198 { 199 X::create_project(...).await 200 } 201 202 // With real DB: 203 create_project::<SqlxPostGresDescriptor>(...).await 204 205 // With mock for tests: 206 create_project::<MockDbHandle<MockDeadPostGresPool>>(...).await 207 ``` 208 209 ### 3. **Type Safety** 210 Compile-time checking of trait bounds ensures operations match expectations. 211 212 ### 4. **Consistency** 213 All database operations follow the same async pattern using `sqlx::Result`. 214 215 ### 5. **Reusability** 216 Core business logic doesn't know about SQL, making it testable with mock implementations. 217 218 ## Limitations 219 220 ### 1. **No Implementation** 221 This macro only defines **trait signatures**. It doesn't provide implementations. Those must be added separately, typically via `#[db_transaction]` proc-macros or manual implementations. 222 223 ### 2. **No Type Inference** 224 You must explicitly specify return types (they must be `sqlx::Result<...>`). 225 226 ### 3. **Macro Complexity** 227 The pattern matching requires careful syntax to avoid parsing errors. Incorrectly formed input will fail compilation with macro-specific errors. 228 229 ### 4. **Hidden in Macros** 230 Code generated by macros can be harder to debug compared to regular Rust code, as you can't "go to definition" on macro-generated code easily. 231 232 ## Comparison: Without vs. With Macro 233 234 ### Without Macro (Manual) 235 236 ```rust 237 pub trait CreateProject { 238 fn create_project(project: NewProject) -> impl std::future::Future<Output = sqlx::Result<Project>> + Send; 239 } 240 241 pub trait ListProjects { 242 fn list_projects() -> impl std::future::Future<Output = sqlx::Result<Vec<Project>>> + Send; 243 } 244 245 pub trait DeleteProject { 246 fn delete_project(id: i32) -> impl std::future::Future<Output = sqlx::Result<Project>> + Send; 247 } 248 249 // Repeat for hundreds of operations... 250 ``` 251 252 ### With Macro (DRY) 253 254 ```rust 255 define_dal_transactions!( 256 CreateProject => create_project(project: NewProject) -> Project, 257 ListProjects => list_projects() -> Vec<Project>, 258 DeleteProject => delete_project(id: i32) -> Project, 259 // Add more operations... 260 ); 261 ``` 262 263 ### Key Improvement 264 265 The macro provides **declarative templating** - you describe the operation once instead of writing the trait boilerplate manually for every database operation. 266 267 ## Real-World Example: From DAL Architecture 268 269 You'll typically see this combined with a procedural macro for implementation: 270 271 ```rust 272 // File: layers/dal/src/models/projects/tx_definitions.rs 273 define_dal_transactions!( 274 CreateProject => create_project(project: NewProject) -> Project, 275 CreateBranch => create_branch(branch: ProjectBranch) -> ProjectBranch, 276 GetProjectsByDepartmentId => get_projects_by_department_id(department_id: i32) -> Vec<Project>, 277 ); 278 279 // File: layers/dal/src/models/projects/postgres_txs.rs 280 #[db_transaction(SqlxPostGresDescriptor, CreateProject)] 281 async fn create_project(project: NewProject) -> Project { 282 // Implementation 283 } 284 285 #[db_transaction(SqlxPostGresDescriptor, CreateBranch)] 286 async fn create_branch(branch: ProjectBranch) -> ProjectBranch { 287 // Implementation 288 } 289 290 #[db_transaction(SqlxPostGresDescriptor, GetProjectsByDepartmentId)] 291 async fn get_projects_by_department_id(department_id: i32) -> Vec<Project> { 292 // Implementation 293 } 294 ``` 295 296 ## Summary 297 298 The `define_dal_transactions!` macro is a powerful tool for the Data Access Layer pattern that: 299 300 - **Generates** trait signatures for database operations 301 - **Standardizes** the async SQL pattern across all operations 302 - **Enables** testing by allowing trait-based implementations 303 - **Reduces** boilerplate when defining many CRUD operations 304 305 It's a classic example of using **compiler-generated code** to move declarative description into imperative behavior, allowing you to focus on what operations exist rather than how they're implemented.