qop 0.5.1

A simple and powerful database migration tool.
# ADR-0008: CLI Command Pattern Implementation

## Status

Accepted

## Date

2025-01-27T23:00:00Z

## Context

The `qop` migration tool has a complex command-line interface with:
1. Top-level commands (manual generation, autocomplete)
2. Subsystem-specific commands with different database backends
3. Nested command hierarchies (e.g., `subsystem postgres history sync`)
4. Feature-conditional availability of subsystems
5. Validation requirements across different command combinations

The CLI needs to be:
- Extensible for new commands and subsystems
- Type-safe to prevent invalid command combinations
- Maintainable with clear separation between parsing and execution
- User-friendly with comprehensive help and error messages
- Compatible with the existing subsystem architecture

The Command pattern provides a clean way to encapsulate command logic while maintaining type safety and extensibility.

## Decision

The codebase MUST implement a structured Command pattern that separates argument parsing, command validation, and command execution, with type-safe representations of all command variants.

### Command Architecture

1. **Command Types**: Strongly-typed enums representing all possible commands
2. **Argument Parsing**: Centralized parsing logic using Clap with builders
3. **Validation Layer**: Command validation separate from parsing
4. **Dispatch Layer**: Route commands to appropriate subsystem handlers
5. **Execution Layer**: Business logic execution via services and repositories

### Implementation Requirements

1. **Type Safety**: All commands MUST be represented as strongly-typed enums
2. **Feature Integration**: Command availability MUST respect feature flags
3. **Validation**: Command validation MUST be separate from parsing
4. **Extensibility**: New commands MUST follow established patterns
5. **Error Handling**: Consistent error handling across all command paths

## Consequences

### Positive

- **Type Safety**: Invalid command combinations are caught at compile time
- **Extensibility**: New commands and subsystems can be added systematically
- **Maintainability**: Clear separation between parsing, validation, and execution
- **Consistency**: All commands follow the same architectural patterns
- **Testing**: Command logic can be tested independently of CLI parsing
- **User Experience**: Comprehensive help and error messages

### Negative

- **Boilerplate**: Each new command requires enum variants and parsing code
- **Complexity**: Multi-level command hierarchies create complex enum structures
- **Compilation Time**: Large command structures may increase compilation time
- **Code Duplication**: Similar commands across subsystems require similar patterns

## Implementation

### Command Type Hierarchy

```rust
#[derive(Debug)]
pub struct CallArgs {
    pub privileges: Privilege,
    pub command: Command,
}

#[derive(Debug)]
pub enum Command {
    Manual {
        path: PathBuf,
        format: ManualFormat,
    },
    Autocomplete {
        path: PathBuf,
        shell: clap_complete::Shell,
    },
    Subsystem(Subsystem),
}

#[derive(Debug)]
pub enum Subsystem {
    #[cfg(feature = "sub+postgres")]
    Postgres {
        path: PathBuf,
        config: SubsystemPostgres,
        command: crate::subsystem::postgres::commands::Command,
    },
    #[cfg(feature = "sub+sqlite")]
    Sqlite {
        path: PathBuf,
        config: SubsystemSqlite,
        command: crate::subsystem::sqlite::commands::Command,
    },
}
```

### Subsystem Command Structure

```rust
// In subsystem::postgres::commands
#[derive(Debug)]
pub enum Command {
    Init,
    New { comment: Option<String>, locked: bool },
    Up { timeout: Option<u64>, count: Option<usize>, diff: bool, dry: bool, yes: bool },
    Down { timeout: Option<u64>, count: usize, remote: bool, diff: bool, dry: bool, yes: bool, unlock: bool },
    List { output: Output },
    Config(ConfigCommand),
    History(HistoryCommand),
    Diff,
    Apply(MigrationApply),
}

#[derive(Debug)]
pub enum ConfigCommand {
    Init { connection: String },
}

#[derive(Debug)]  
pub enum HistoryCommand {
    Sync,
    Fix,
}

#[derive(Debug)]
pub enum MigrationApply {
    Up { id: String, timeout: Option<u64>, dry: bool, yes: bool },
    Down { id: String, timeout: Option<u64>, remote: bool, dry: bool, yes: bool, unlock: bool },
}
```

### Clap Builder Pattern

```rust
impl ClapArgumentLoader {
    pub fn root_command() -> clap::Command {
        let mut root = clap::Command::new("qop")
            .version(env!("CARGO_PKG_VERSION"))
            .about("Database migrations for savages.")
            .subcommand_required(false)
            .args([Arg::new("experimental").short('e').long("experimental")]);

        // Feature-conditional subsystem registration
        #[cfg(any(feature = "sub+postgres", feature = "sub+sqlite"))]
        {
            let mut subsystem = clap::Command::new("subsystem")
                .aliases(["sub", "s"])
                .subcommand_required(true);

            #[cfg(feature = "sub+postgres")]
            {
                let pg = clap::Command::new("postgres")
                    .aliases(["pg"])
                    .arg(clap::Arg::new("path").short('p').long("path").default_value("qop.toml"))
                    .subcommand_required(true)
                    .subcommand(clap::Command::new("init"))
                    .subcommand(
                        clap::Command::new("up")
                            .arg(clap::Arg::new("timeout").short('t').long("timeout"))
                            .arg(clap::Arg::new("count").short('c').long("count"))
                            .arg(clap::Arg::new("dry").long("dry").num_args(0))
                            .arg(clap::Arg::new("yes").short('y').long("yes").num_args(0))
                    );
                subsystem = subsystem.subcommand(pg);
            }
            
            root = root.subcommand(subsystem);
        }

        root
    }
}
```

### Command Parsing Pattern

```rust
impl ClapArgumentLoader {
    pub fn load() -> Result<CallArgs> {
        let matches = Self::root_command().get_matches();

        let privileges = if matches.get_flag("experimental") {
            Privilege::Experimental
        } else {
            Privilege::Normal
        };

        let cmd = if let Some(subsystem_matches) = matches.subcommand_matches("subsystem") {
            #[cfg(feature = "sub+postgres")]
            {
                if let Some(pg_matches) = subsystem_matches.subcommand_matches("postgres") {
                    let path = Self::get_absolute_path(pg_matches, "path")?;
                    
                    // Load and validate configuration
                    let config: Config = toml::from_str(&std::fs::read_to_string(&path)?)?;
                    let pg_cfg = match config.subsystem { 
                        Subsystem::Postgres(c) => c, 
                        _ => anyhow::bail!("config is not postgres") 
                    };
                    
                    // Parse postgres-specific commands
                    let postgres_cmd = if pg_matches.subcommand_matches("init").is_some() {
                        postgres::commands::Command::Init
                    } else if let Some(up_matches) = pg_matches.subcommand_matches("up") {
                        postgres::commands::Command::Up {
                            timeout: up_matches.get_one::<String>("timeout").map(|s| s.parse().unwrap()),
                            count: up_matches.get_one::<String>("count").map(|s| s.parse().unwrap()),
                            diff: up_matches.get_flag("diff"),
                            dry: up_matches.get_flag("dry"),
                            yes: up_matches.get_flag("yes"),
                        }
                    } else {
                        unreachable!();
                    };
                    
                    return Ok(CallArgs { 
                        privileges, 
                        command: Command::Subsystem(Subsystem::Postgres { 
                            path, 
                            config: pg_cfg, 
                            command: postgres_cmd 
                        })
                    });
                }
            }
            // Similar pattern for other subsystems...
        } else {
            // Handle other top-level commands...
        };

        let callargs = CallArgs { privileges, command: cmd };
        callargs.validate()?;
        Ok(callargs)
    }
}
```

### Validation Rules

The validation layer MUST ensure commands are structurally valid, but the `diff` capability is considered stable and MUST NOT require experimental privileges.

### Command Dispatch Pattern

```rust
// In main.rs
#[tokio::main]
async fn main() -> Result<()> {
    let cmd = crate::args::ClapArgumentLoader::load()?;

    match cmd.command {
        Command::Manual { path, format } => {
            std::fs::create_dir_all(&path)?;
            match format {
                ManualFormat::Manpages => reference::build_manpages(&path)?,
                ManualFormat::Markdown => reference::build_markdown(&path)?,
            }
            Ok(())
        },
        Command::Autocomplete { path, shell } => {
            std::fs::create_dir_all(&path)?;
            reference::build_shell_completion(&path, &shell)?;
            Ok(())
        },
        Command::Subsystem(subsystem) => {
            crate::subsystem::driver::dispatch(subsystem).await
        },
    }
}
```

### Subsystem Dispatch Pattern

```rust
pub async fn dispatch(subsystem: Subsystem) -> Result<()> {
    match subsystem {
        #[cfg(feature = "sub+postgres")]
        Subsystem::Postgres { path, config, command } => {
            match command {
                postgres::commands::Command::Init => {
                    let repo = postgres::repo::PostgresRepo::from_config(&path, config, false).await?;
                    let svc = MigrationService::new(repo);
                    svc.init().await
                }
                postgres::commands::Command::Up { timeout, count, dry, yes, .. } => {
                    let repo = postgres::repo::PostgresRepo::from_config(&path, config, true).await?;
                    let svc = MigrationService::new(repo);
                    svc.up(&path, timeout, count, yes, dry).await
                }
                // ... other command handlers
            }
        }
        // ... other subsystem handlers
    }
}
```

## Design Guidelines

### Command Structure Principles

1. **Hierarchical Organization**: Commands MUST follow logical hierarchies (`subsystem > backend > operation`)
2. **Type Safety**: All command variants MUST be strongly typed with appropriate parameters
3. **Feature Integration**: Commands MUST be conditionally compiled based on feature flags
4. **Validation Separation**: Command parsing and validation MUST be separate concerns
5. **Consistent Patterns**: All subsystems MUST follow the same command structure patterns

### Argument Handling

```rust
// Good: Strongly typed with validation
Command::Up { 
    timeout: Option<u64>,     // Parsed and validated
    count: Option<usize>,     // Optional with type safety  
    dry: bool,                // Boolean flags
    yes: bool                 // Clear intent
}

// Bad: Stringly typed
Command::Up {
    args: HashMap<String, String>  // Loses type safety
}
```

### Error Messages and Help

1. **Contextual Help**: Each command level MUST provide relevant help information
2. **Error Context**: Invalid commands MUST provide suggestions for correction
3. **Feature Awareness**: Help messages MUST reflect enabled features
4. **Consistent Formatting**: All command output MUST follow consistent formatting

### Extensibility Patterns

```rust
// Adding a new command to existing subsystem
#[derive(Debug)]
pub enum Command {
    // ... existing commands
    NewCommand { param1: String, param2: Option<u64> },
}

// Adding parsing support
let postgres_cmd = if let Some(new_matches) = pg_matches.subcommand_matches("new-command") {
    Command::NewCommand {
        param1: new_matches.get_one::<String>("param1").unwrap().clone(),
        param2: new_matches.get_one::<String>("param2").map(|s| s.parse().unwrap()),
    }
} else {
    // ... existing command parsing
};

// Adding dispatch handling  
match command {
    Command::NewCommand { param1, param2 } => {
        // Implementation
    },
    // ... existing command handling
}
```

## Anti-Patterns

```rust
// Don't: String-based command handling
match command_str {
    "up" => handle_up(),
    "down" => handle_down(),
    _ => Err("Unknown command"),
}

// Don't: Mixed parsing and execution
fn handle_command(matches: &ArgMatches) -> Result<()> {
    let timeout = matches.get_one::<String>("timeout");
    // BAD: Business logic mixed with parsing
    let repo = PostgresRepo::new()?;
    repo.apply_migrations()?;
}

// Don't: Inconsistent command structures across subsystems
enum PostgresCommand { Up { dry: bool } }
enum SqliteCommand { Up { dry_run: bool } }  // Inconsistent naming
```

## References

- [Command Pattern]https://refactoring.guru/design-patterns/command
- [Clap Documentation]https://docs.rs/clap/
- [Rust CLI Working Group Guide]https://rust-cli.github.io/book/