vibesql_executor/
debug_output.rs

1//! Structured debug output infrastructure
2//!
3//! This module provides a unified interface for debug/profiling output that can emit
4//! either human-readable text (default) or machine-parseable JSON.
5//!
6//! # Environment Variables
7//!
8//! - `VIBESQL_DEBUG_FORMAT=json` - Output JSON to stderr (for agents/CI)
9//! - `VIBESQL_DEBUG_FORMAT=text` - Output human-readable text (default)
10//!
11//! # JSON Schema
12//!
13//! All JSON output follows this structure:
14//! ```json
15//! {
16//!   "timestamp": "2024-01-15T10:30:00.123Z",
17//!   "category": "optimizer",
18//!   "event": "join_reorder",
19//!   "data": { ... }
20//! }
21//! ```
22//!
23//! # Categories
24//!
25//! - `optimizer` - Query optimization decisions (join reorder, table elimination, etc.)
26//! - `execution` - Execution timing and statistics
27//! - `index` - Index selection and usage
28//! - `dml` - DML operation timing (insert, update, delete)
29//! - `profile` - General profiling output
30
31use std::sync::atomic::{AtomicU8, Ordering};
32use std::time::{SystemTime, UNIX_EPOCH};
33
34/// Output format for debug messages
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
36#[repr(u8)]
37pub enum DebugFormat {
38    /// Human-readable text output (default)
39    #[default]
40    Text = 0,
41    /// Machine-parseable JSON output
42    Json = 1,
43}
44
45/// Global format setting (0 = Text, 1 = Json)
46static DEBUG_FORMAT: AtomicU8 = AtomicU8::new(0);
47
48/// Initialize debug format from environment variable.
49/// Call this once at program start.
50pub fn init() {
51    if let Ok(format) = std::env::var("VIBESQL_DEBUG_FORMAT") {
52        match format.to_lowercase().as_str() {
53            "json" => DEBUG_FORMAT.store(1, Ordering::Relaxed),
54            "text" | "" => DEBUG_FORMAT.store(0, Ordering::Relaxed),
55            _ => {
56                eprintln!(
57                    "[WARNING] Unknown VIBESQL_DEBUG_FORMAT='{}', using 'text'",
58                    format
59                );
60                DEBUG_FORMAT.store(0, Ordering::Relaxed);
61            }
62        }
63    }
64}
65
66/// Get the current debug output format
67pub fn get_format() -> DebugFormat {
68    match DEBUG_FORMAT.load(Ordering::Relaxed) {
69        1 => DebugFormat::Json,
70        _ => DebugFormat::Text,
71    }
72}
73
74/// Check if JSON output is enabled
75pub fn is_json() -> bool {
76    DEBUG_FORMAT.load(Ordering::Relaxed) == 1
77}
78
79/// Debug output categories
80#[derive(Debug, Clone, Copy)]
81pub enum Category {
82    /// Query optimization decisions
83    Optimizer,
84    /// Execution timing and statistics
85    Execution,
86    /// Index selection and usage
87    Index,
88    /// DML operation timing
89    Dml,
90    /// General profiling
91    Profile,
92}
93
94impl Category {
95    pub fn as_str(&self) -> &'static str {
96        match self {
97            Category::Optimizer => "optimizer",
98            Category::Execution => "execution",
99            Category::Index => "index",
100            Category::Dml => "dml",
101            Category::Profile => "profile",
102        }
103    }
104}
105
106/// Get current timestamp in ISO 8601 format
107fn iso_timestamp() -> String {
108    let now = SystemTime::now();
109    let duration = now.duration_since(UNIX_EPOCH).unwrap_or_default();
110    let secs = duration.as_secs();
111    let millis = duration.subsec_millis();
112
113    // Convert to date/time components (simplified UTC)
114    let days_since_epoch = secs / 86400;
115    let time_of_day = secs % 86400;
116    let hours = time_of_day / 3600;
117    let minutes = (time_of_day % 3600) / 60;
118    let seconds = time_of_day % 60;
119
120    // Simplified date calculation (good enough for debugging purposes)
121    // This handles dates from 1970-2099 reasonably well
122    let mut year = 1970;
123    let mut remaining_days = days_since_epoch as i64;
124
125    loop {
126        let days_in_year = if year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) {
127            366
128        } else {
129            365
130        };
131        if remaining_days < days_in_year {
132            break;
133        }
134        remaining_days -= days_in_year;
135        year += 1;
136    }
137
138    let is_leap = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
139    let days_in_months: [i64; 12] = if is_leap {
140        [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
141    } else {
142        [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
143    };
144
145    let mut month = 1;
146    for &days in &days_in_months {
147        if remaining_days < days {
148            break;
149        }
150        remaining_days -= days;
151        month += 1;
152    }
153    let day = remaining_days + 1;
154
155    format!(
156        "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:03}Z",
157        year, month, day, hours, minutes, seconds, millis
158    )
159}
160
161/// Escape a string for JSON output
162fn json_escape(s: &str) -> String {
163    let mut result = String::with_capacity(s.len() + 16);
164    for c in s.chars() {
165        match c {
166            '"' => result.push_str("\\\""),
167            '\\' => result.push_str("\\\\"),
168            '\n' => result.push_str("\\n"),
169            '\r' => result.push_str("\\r"),
170            '\t' => result.push_str("\\t"),
171            c if c.is_control() => {
172                result.push_str(&format!("\\u{:04x}", c as u32));
173            }
174            c => result.push(c),
175        }
176    }
177    result
178}
179
180/// A builder for constructing debug output with optional JSON fields
181pub struct DebugEvent {
182    category: Category,
183    event: &'static str,
184    tag: &'static str,
185    text_parts: Vec<String>,
186    json_fields: Vec<(String, JsonValue)>,
187}
188
189/// JSON value types (simplified, no external dependency)
190pub enum JsonValue {
191    String(String),
192    Number(f64),
193    Int(i64),
194    Bool(bool),
195    Array(Vec<JsonValue>),
196    Object(Vec<(String, JsonValue)>),
197    Null,
198}
199
200impl JsonValue {
201    /// Format as JSON string
202    pub fn to_json(&self) -> String {
203        match self {
204            JsonValue::String(s) => format!("\"{}\"", json_escape(s)),
205            JsonValue::Number(n) => {
206                if n.is_finite() {
207                    format!("{}", n)
208                } else {
209                    "null".to_string()
210                }
211            }
212            JsonValue::Int(n) => format!("{}", n),
213            JsonValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
214            JsonValue::Array(arr) => {
215                let items: Vec<String> = arr.iter().map(|v| v.to_json()).collect();
216                format!("[{}]", items.join(","))
217            }
218            JsonValue::Object(fields) => {
219                let items: Vec<String> = fields
220                    .iter()
221                    .map(|(k, v)| format!("\"{}\":{}", json_escape(k), v.to_json()))
222                    .collect();
223                format!("{{{}}}", items.join(","))
224            }
225            JsonValue::Null => "null".to_string(),
226        }
227    }
228}
229
230impl DebugEvent {
231    /// Create a new debug event
232    pub fn new(category: Category, event: &'static str, tag: &'static str) -> Self {
233        Self {
234            category,
235            event,
236            tag,
237            text_parts: Vec::new(),
238            json_fields: Vec::new(),
239        }
240    }
241
242    /// Add a text message (for human-readable output)
243    pub fn text(mut self, message: impl Into<String>) -> Self {
244        self.text_parts.push(message.into());
245        self
246    }
247
248    /// Add a JSON field (for machine-readable output)
249    pub fn field(mut self, name: impl Into<String>, value: JsonValue) -> Self {
250        self.json_fields.push((name.into(), value));
251        self
252    }
253
254    /// Add a string field
255    pub fn field_str(self, name: impl Into<String>, value: impl Into<String>) -> Self {
256        self.field(name, JsonValue::String(value.into()))
257    }
258
259    /// Add an integer field
260    pub fn field_int(self, name: impl Into<String>, value: i64) -> Self {
261        self.field(name, JsonValue::Int(value))
262    }
263
264    /// Add a float field
265    pub fn field_float(self, name: impl Into<String>, value: f64) -> Self {
266        self.field(name, JsonValue::Number(value))
267    }
268
269    /// Add a boolean field
270    pub fn field_bool(self, name: impl Into<String>, value: bool) -> Self {
271        self.field(name, JsonValue::Bool(value))
272    }
273
274    /// Add duration in microseconds
275    pub fn field_duration_us(self, name: impl Into<String>, duration: std::time::Duration) -> Self {
276        self.field(name, JsonValue::Int(duration.as_micros() as i64))
277    }
278
279    /// Add duration in milliseconds (as float)
280    pub fn field_duration_ms(self, name: impl Into<String>, duration: std::time::Duration) -> Self {
281        self.field(
282            name,
283            JsonValue::Number(duration.as_secs_f64() * 1000.0),
284        )
285    }
286
287    /// Add a string array field
288    pub fn field_str_array(self, name: impl Into<String>, values: &[String]) -> Self {
289        let arr: Vec<JsonValue> = values
290            .iter()
291            .map(|s| JsonValue::String(s.clone()))
292            .collect();
293        self.field(name, JsonValue::Array(arr))
294    }
295
296    /// Emit the debug event to stderr
297    pub fn emit(self) {
298        match get_format() {
299            DebugFormat::Text => {
300                // Traditional text output: [TAG] messages
301                let message = self.text_parts.join(" ");
302                eprintln!("[{}] {}", self.tag, message);
303            }
304            DebugFormat::Json => {
305                // JSON output
306                let timestamp = iso_timestamp();
307                let mut fields = vec![
308                    ("timestamp".to_string(), JsonValue::String(timestamp)),
309                    (
310                        "category".to_string(),
311                        JsonValue::String(self.category.as_str().to_string()),
312                    ),
313                    (
314                        "event".to_string(),
315                        JsonValue::String(self.event.to_string()),
316                    ),
317                ];
318
319                // Add data object with all custom fields
320                if !self.json_fields.is_empty() {
321                    fields.push(("data".to_string(), JsonValue::Object(self.json_fields)));
322                }
323
324                let json = JsonValue::Object(fields).to_json();
325                eprintln!("{}", json);
326            }
327        }
328    }
329}
330
331/// Convenience function to create a debug event
332pub fn debug_event(category: Category, event: &'static str, tag: &'static str) -> DebugEvent {
333    DebugEvent::new(category, event, tag)
334}
335
336/// Macro for creating debug events with both text and JSON output
337///
338/// # Example
339///
340/// ```text
341/// debug_emit!(
342///     optimizer, "join_reorder", "JOIN_REORDER",
343///     text: "Optimal order: {:?}", optimal_order,
344///     fields: {
345///         "original_order" => json_str_array(&original_order),
346///         "optimal_order" => json_str_array(&optimal_order),
347///         "optimizer_time_us" => JsonValue::Int(time.as_micros() as i64)
348///     }
349/// );
350/// ```
351#[macro_export]
352macro_rules! debug_emit {
353    (
354        $category:ident, $event:expr, $tag:expr,
355        text: $($text_fmt:expr),* $(,)?
356        $(, fields: { $($field_name:expr => $field_value:expr),* $(,)? })?
357    ) => {{
358        let mut event = $crate::debug_output::debug_event(
359            $crate::debug_output::Category::$category,
360            $event,
361            $tag
362        );
363        event = event.text(format!($($text_fmt),*));
364        $($(
365            event = event.field($field_name, $field_value);
366        )*)?
367        event.emit();
368    }};
369}
370
371// Re-export for use in macros
372pub use crate::debug_emit;
373
374#[cfg(test)]
375mod tests {
376    use super::*;
377
378    #[test]
379    fn test_json_escape() {
380        assert_eq!(json_escape("hello"), "hello");
381        assert_eq!(json_escape("hello\"world"), "hello\\\"world");
382        assert_eq!(json_escape("line\nbreak"), "line\\nbreak");
383        assert_eq!(json_escape("tab\there"), "tab\\there");
384    }
385
386    #[test]
387    fn test_json_value_formatting() {
388        assert_eq!(JsonValue::String("test".to_string()).to_json(), "\"test\"");
389        assert_eq!(JsonValue::Int(42).to_json(), "42");
390        assert_eq!(JsonValue::Number(3.5).to_json(), "3.5");
391        assert_eq!(JsonValue::Bool(true).to_json(), "true");
392        assert_eq!(JsonValue::Null.to_json(), "null");
393
394        let arr = JsonValue::Array(vec![JsonValue::Int(1), JsonValue::Int(2)]);
395        assert_eq!(arr.to_json(), "[1,2]");
396
397        let obj = JsonValue::Object(vec![
398            ("name".to_string(), JsonValue::String("test".to_string())),
399            ("value".to_string(), JsonValue::Int(42)),
400        ]);
401        assert_eq!(obj.to_json(), "{\"name\":\"test\",\"value\":42}");
402    }
403
404    #[test]
405    fn test_debug_event_builder() {
406        // Just test that the builder compiles and doesn't panic
407        let event = DebugEvent::new(Category::Optimizer, "test_event", "TEST")
408            .text("Test message")
409            .field_str("key", "value")
410            .field_int("count", 42)
411            .field_float("ratio", 0.5)
412            .field_bool("enabled", true);
413
414        // Don't emit in tests to avoid polluting output
415        drop(event);
416    }
417
418    #[test]
419    fn test_iso_timestamp_format() {
420        let ts = iso_timestamp();
421        // Should match pattern: YYYY-MM-DDTHH:MM:SS.mmmZ
422        assert!(ts.len() == 24, "Timestamp should be 24 chars: {}", ts);
423        assert!(ts.ends_with('Z'), "Timestamp should end with Z: {}", ts);
424        assert!(ts.contains('T'), "Timestamp should contain T: {}", ts);
425    }
426}