sql-cli 1.73.1

SQL query tool for CSV/JSON with both interactive TUI and non-interactive CLI modes - perfect for exploration and automation
Documentation
# Unified State Architecture - Buffer-Centric Redux Pattern

## Current State Mess
We have THREE competing state systems:
1. **AppStateContainer** - Holds DataView, filters, column state, etc.
2. **ShadowStateManager** - Observes/coordinates mode transitions
3. **Buffer** - Should eventually hold ALL state for buffer switching

This is unsustainable and will lead to more synchronization bugs.

## The Goal: Buffer-Centric State
When we switch buffers, we should restore EVERYTHING:
- Query text and cursor position
- DataView with current query results  
- All filters and search states
- Column visibility, sorting, pinning
- Viewport position and locks
- Current mode
- Search patterns and matches

## Proposed Architecture

### 1. Buffer Becomes the Redux Store
```rust
pub struct Buffer {
    // Core state slices
    pub query_state: QueryState,
    pub data_state: DataState,        // DataView lives here
    pub ui_state: UIState,            // Mode, viewport, etc.
    pub search_state: SearchState,    // All search types
    pub column_state: ColumnState,    // Hide, pin, sort
    
    // State version for change detection
    version: u64,
    
    // Subscribers interested in state changes
    subscribers: Vec<StateSubscriberId>,
}
```

### 2. AppStateContainer Becomes the Dispatcher
Instead of holding state, it coordinates:
```rust
pub struct AppStateContainer {
    // Reference to current buffer
    current_buffer: Rc<RefCell<Buffer>>,
    
    // All available buffers
    buffers: HashMap<BufferId, Rc<RefCell<Buffer>>>,
    
    // The reducer that processes actions
    reducer: StateReducer,
    
    // Component registry for side effects
    components: ComponentRegistry,
}

impl AppStateContainer {
    pub fn dispatch(&mut self, action: Action) -> Result<()> {
        // 1. Get current buffer state
        let buffer = self.current_buffer.borrow();
        
        // 2. Process action through reducer (pure function)
        let (new_state, side_effects) = self.reducer.process(&buffer, action);
        
        // 3. Update buffer with new state
        buffer.apply_state_change(new_state);
        
        // 4. Notify components about side effects
        for effect in side_effects {
            self.components.handle_effect(effect);
        }
        
        Ok(())
    }
}
```

### 3. Shadow State Becomes the Reducer
Transform shadow state into a pure function reducer:
```rust
pub struct StateReducer {
    // No state! Just logic
}

impl StateReducer {
    pub fn process(&self, state: &Buffer, action: Action) 
        -> (StateChange, Vec<SideEffect>) {
        
        match action {
            Action::ExitSearchMode => {
                let change = StateChange {
                    ui_state: Some(UIState { mode: AppMode::Results }),
                    search_state: Some(SearchState::cleared()),
                    ..Default::default()
                };
                
                let effects = vec![
                    SideEffect::RestoreNavigationKeys,
                    SideEffect::ClearSearchHighlights,
                ];
                
                (change, effects)
            }
            // ... other actions
        }
    }
}
```

### 4. Avoiding Clones with Smart References

#### Option A: State Versioning
```rust
pub struct DataState {
    data_view: Arc<DataView>,  // Immutable, shared
    version: u64,               // Increment on change
}

// Components keep version and check if stale
pub struct TableRenderer {
    data_version: u64,
    cached_render: Option<RenderOutput>,
}

impl TableRenderer {
    fn render(&mut self, data_state: &DataState) -> RenderOutput {
        if self.data_version != data_state.version {
            // Re-render only if data changed
            self.cached_render = Some(self.do_render(&data_state.data_view));
            self.data_version = data_state.version;
        }
        self.cached_render.clone().unwrap()
    }
}
```

#### Option B: Slice References
```rust
// Components get references to specific slices they care about
pub trait Component {
    type StateSlice;
    
    fn get_slice<'a>(&self, buffer: &'a Buffer) -> &'a Self::StateSlice;
    fn handle_change(&mut self, old: &Self::StateSlice, new: &Self::StateSlice);
}

impl Component for VimSearchManager {
    type StateSlice = SearchState;
    
    fn get_slice<'a>(&self, buffer: &'a Buffer) -> &'a SearchState {
        &buffer.search_state
    }
    
    fn handle_change(&mut self, old: &SearchState, new: &SearchState) {
        if old.vim_pattern != new.vim_pattern {
            self.update_pattern(&new.vim_pattern);
        }
    }
}
```

### 5. Pub-Sub Without Cloning

Use indices and weak references:
```rust
pub struct EventBus {
    // Events are just indices into a ring buffer
    events: RingBuffer<Event>,
    
    // Subscribers get notified with event index
    subscribers: HashMap<EventType, Vec<SubscriberId>>,
}

pub struct Event {
    action: Action,
    state_before: StateSnapshot,  // Just version numbers
    state_after: StateSnapshot,
}

pub struct StateSnapshot {
    query_version: u64,
    data_version: u64,
    ui_version: u64,
    // ... other version numbers
}

// Components check if they care about the change
impl VimSearchManager {
    fn on_event(&mut self, event_id: EventId, bus: &EventBus) {
        let event = bus.get_event(event_id);
        
        // Only react if search state changed
        if event.state_before.search_version != event.state_after.search_version {
            self.handle_search_change();
        }
    }
}
```

## Migration Path

### Phase 1: Move DataView to Buffer
```rust
// Move from AppStateContainer to Buffer
impl Buffer {
    pub fn get_data_view(&self) -> &DataView {
        &self.data_state.data_view
    }
}
```

### Phase 2: Consolidate Search State
```rust
// Unify all search types in Buffer
pub struct SearchState {
    vim_search: Option<VimSearchData>,
    column_search: Option<ColumnSearchData>,
    fuzzy_filter: Option<String>,
    data_filter: Option<String>,
}
```

### Phase 3: Make AppStateContainer a Dispatcher
- Remove all state storage from AppStateContainer
- Add dispatch() method
- Convert all mutations to actions

### Phase 4: Implement State Versioning
- Add version numbers to each state slice
- Components cache based on version
- Only re-compute when version changes

## Benefits

1. **Single Source of Truth**: Everything in Buffer
2. **Buffer Switching Works**: Can save/restore entire UI state
3. **No Redundant Clones**: Version checking and smart references
4. **Clear Data Flow**: Action → Reducer → State → Side Effects
5. **Testable**: Pure reducer functions
6. **Debuggable**: Can replay actions

## Specific Solution for N Key Issue

With this architecture:
```rust
// User presses Escape in search mode
dispatch(Action::ExitSearchMode)
  → Reducer returns (StateChange { mode: Results }, [ClearSearches])
  → Buffer updates search_state
  → VimSearchManager notified via side effect
  → VimSearchManager clears itself
  → N key now works properly
```

## Next Steps

1. **Don't add more state systems** - We have enough!
2. **Start moving state to Buffer** - DataView first
3. **Convert ShadowStateManager to Reducer** - Pure functions
4. **Make AppStateContainer a dispatcher** - Not a state holder
5. **Implement versioning** - Avoid unnecessary clones

## Key Principles

1. **Buffer owns all state** - It's the Redux store
2. **AppStateContainer dispatches** - It's the event bus
3. **Reducer is pure** - No state, just logic
4. **Components subscribe to slices** - Not whole state
5. **Use versions not clones** - Check if changed before reacting