notes

Log | Files | Refs | README

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.