Skip to content

Conversation

@moshap-firebolt
Copy link
Collaborator

Summary

This PR implements PostgreSQL-compatible prompt customization commands (\set PROMPT1, \set PROMPT2, \set PROMPT3) for the Firebolt CLI, allowing users to customize their prompt experience based on the current context. This enhancement improves the CLI's consistency with psql and provides users with better control over their interactive experience.

Problem Statement

The Firebolt CLI currently uses fixed prompts (=>, ~>, *>) that cannot be customized by users. This limits the user experience and makes the CLI less consistent with PostgreSQL's psql tool, which many users are familiar with. Users need the ability to set custom prompts for different contexts (normal operation, query continuation, and transactions).

Solution

Implemented a comprehensive prompt customization system that:

  1. Adds support for three prompt types:

    • PROMPT1: Normal prompt (default: =>)
    • PROMPT2: Continuation prompt for multi-line queries (default: ~>)
    • PROMPT3: Transaction prompt (default: *>)
  2. Provides intuitive commands:

    • \set PROMPT1 'custom> ' - Set normal prompt
    • \set PROMPT2 'continue> ' - Set continuation prompt
    • \set PROMPT3 'txn> ' - Set transaction prompt
    • \unset PROMPT1 - Reset to default prompt
  3. Maintains independence: Each prompt type can be set/unset independently without affecting others

Technical Implementation

New Files

  • src/meta_commands.rs: Handles parsing and execution of backslash commands with comprehensive regex-based parsing for various quote styles

Modified Files

  • src/context.rs: Extended Context struct with separate prompt fields and setter methods
  • src/main.rs: Integrated meta-command handling and updated prompt logic to use context-appropriate prompts

Key Features

  • Flexible parsing: Supports single quotes, double quotes, and unquoted values
  • Performance optimized: Uses lazy regex compilation for efficiency
  • Error handling: Graceful error handling for malformed commands
  • Context awareness: Automatically selects appropriate prompt based on current state

Usage Examples

# Set custom prompts for different contexts
\set PROMPT1 'firebolt> '
\set PROMPT2 'continue> '
\set PROMPT3 'transaction> '

# Unset specific prompts
\unset PROMPT2

# Mix and match - only affect specified prompt type
\set PROMPT1 'custom> '    # Only changes normal prompt
\set PROMPT2 'more> '      # Only changes continuation prompt

Testing

  • Unit tests: 15 comprehensive tests covering all prompt types, quote styles, and edge cases
  • Integration: Verified prompt logic integration with main CLI loop
  • Build verification: All tests pass, no compilation errors

Benefits

  1. Improved UX: Users can personalize their CLI experience
  2. psql compatibility: Familiar interface for PostgreSQL users
  3. Context awareness: Prompts automatically adapt to current state
  4. Independent control: Granular control over each prompt type
  5. Backward compatibility: Existing behavior preserved when no custom prompts are set

Breaking Changes

None. This is a purely additive feature that maintains full backward compatibility.

Documentation

The implementation follows the existing code style and includes comprehensive inline documentation and test coverage.

Related Issues

  • Addresses user request for prompt customization
  • Improves consistency with PostgreSQL tooling
  • Enhances overall CLI user experience

Testing Instructions

  1. Build and run tests: cargo test
  2. Test prompt commands interactively: cargo run -- --host localhost:8123 --database test
  3. Try setting different prompts: \set PROMPT1 'test> '
  4. Verify prompt changes in different contexts (normal, continuation, transaction)

Review Focus

  • Prompt logic correctness
  • Meta-command parsing robustness
  • Test coverage completeness
  • Code style consistency

Comment on lines 47 to 104
fn parse_set_prompt1(command: &str) -> Option<String> {
static SET_PROMPT_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(r#"(?i)^\s*\\set\s+PROMPT1\s+(?:'([^']*)'|"([^"]*)"|(\S+))\s*$"#).unwrap()
});

if let Some(captures) = SET_PROMPT_RE.captures(command) {
// Check which capture group matched
if let Some(prompt) = captures.get(1) {
return Some(prompt.as_str().to_string());
} else if let Some(prompt) = captures.get(2) {
return Some(prompt.as_str().to_string());
} else if let Some(prompt) = captures.get(3) {
return Some(prompt.as_str().to_string());
}
}

None
}

// Parse \set PROMPT2 'value' command
fn parse_set_prompt2(command: &str) -> Option<String> {
static SET_PROMPT_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(r#"(?i)^\s*\\set\s+PROMPT2\s+(?:'([^']*)'|"([^"]*)"|(\S+))\s*$"#).unwrap()
});

if let Some(captures) = SET_PROMPT_RE.captures(command) {
// Check which capture group matched
if let Some(prompt) = captures.get(1) {
return Some(prompt.as_str().to_string());
} else if let Some(prompt) = captures.get(2) {
return Some(prompt.as_str().to_string());
} else if let Some(prompt) = captures.get(3) {
return Some(prompt.as_str().to_string());
}
}

None
}

// Parse \set PROMPT3 'value' command
fn parse_set_prompt3(command: &str) -> Option<String> {
static SET_PROMPT_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(r#"(?i)^\s*\\set\s+PROMPT3\s+(?:'([^']*)'|"([^"]*)"|(\S+))\s*$"#).unwrap()
});

if let Some(captures) = SET_PROMPT_RE.captures(command) {
// Check which capture group matched
if let Some(prompt) = captures.get(1) {
return Some(prompt.as_str().to_string());
} else if let Some(prompt) = captures.get(2) {
return Some(prompt.as_str().to_string());
} else if let Some(prompt) = captures.get(3) {
return Some(prompt.as_str().to_string());
}
}

None
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

copypaste 🫠

Copy link
Collaborator Author

@moshap-firebolt moshap-firebolt Aug 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're absolutely right! There's a lot of code duplication that can be eliminated. Let me refactor the code to use generic functions with better code reuse.

Summary of Refactoring Improvements
I have successfully refactored the code to eliminate duplication and improve maintainability. Here's what was accomplished:

Before Refactoring:

  • 6 separate parsing functions with nearly identical code
  • parse_set_prompt1(), parse_set_prompt2(), parse_set_prompt3()
  • parse_unset_prompt1(), parse_unset_prompt2(), parse_unset_prompt3()
  • Each function had its own regex pattern and logic
  • Changes required updating multiple functions

After Refactoring:

  • 2 generic functions that handle all prompt types
  • parse_set_prompt(command, prompt_type) - handles all set operations
  • parse_unset_prompt(command, prompt_type) - handles all unset operations
  • Single regex pattern for each command type
  • Changes only need to be made in one place

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

Comment on lines 107 to 131
fn parse_unset_prompt1(command: &str) -> bool {
static UNSET_PROMPT_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(r#"(?i)^\s*\\unset\s+PROMPT1\s*$"#).unwrap()
});

UNSET_PROMPT_RE.is_match(command)
}

// Parse \unset PROMPT2 command
fn parse_unset_prompt2(command: &str) -> bool {
static UNSET_PROMPT_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(r#"(?i)^\s*\\unset\s+PROMPT2\s*$"#).unwrap()
});

UNSET_PROMPT_RE.is_match(command)
}

// Parse \unset PROMPT3 command
fn parse_unset_prompt3(command: &str) -> bool {
static UNSET_PROMPT_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(r#"(?i)^\s*\\unset\s+PROMPT3\s*$"#).unwrap()
});

UNSET_PROMPT_RE.is_match(command)
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and this copypaste

@moshap-firebolt moshap-firebolt merged commit 8381b53 into main Sep 5, 2025
3 checks passed
@moshap-firebolt moshap-firebolt deleted the moshap/prompt branch September 5, 2025 22:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants