1use std::sync::atomic::{AtomicU8, Ordering};
32use std::time::{SystemTime, UNIX_EPOCH};
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
36#[repr(u8)]
37pub enum DebugFormat {
38 #[default]
40 Text = 0,
41 Json = 1,
43}
44
45static DEBUG_FORMAT: AtomicU8 = AtomicU8::new(0);
47
48pub 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
66pub fn get_format() -> DebugFormat {
68 match DEBUG_FORMAT.load(Ordering::Relaxed) {
69 1 => DebugFormat::Json,
70 _ => DebugFormat::Text,
71 }
72}
73
74pub fn is_json() -> bool {
76 DEBUG_FORMAT.load(Ordering::Relaxed) == 1
77}
78
79#[derive(Debug, Clone, Copy)]
81pub enum Category {
82 Optimizer,
84 Execution,
86 Index,
88 Dml,
90 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
106fn 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 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 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
161fn 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
180pub 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
189pub 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 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 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 pub fn text(mut self, message: impl Into<String>) -> Self {
244 self.text_parts.push(message.into());
245 self
246 }
247
248 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 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 pub fn field_int(self, name: impl Into<String>, value: i64) -> Self {
261 self.field(name, JsonValue::Int(value))
262 }
263
264 pub fn field_float(self, name: impl Into<String>, value: f64) -> Self {
266 self.field(name, JsonValue::Number(value))
267 }
268
269 pub fn field_bool(self, name: impl Into<String>, value: bool) -> Self {
271 self.field(name, JsonValue::Bool(value))
272 }
273
274 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 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 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 pub fn emit(self) {
298 match get_format() {
299 DebugFormat::Text => {
300 let message = self.text_parts.join(" ");
302 eprintln!("[{}] {}", self.tag, message);
303 }
304 DebugFormat::Json => {
305 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 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
331pub fn debug_event(category: Category, event: &'static str, tag: &'static str) -> DebugEvent {
333 DebugEvent::new(category, event, tag)
334}
335
336#[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
371pub 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 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 drop(event);
416 }
417
418 #[test]
419 fn test_iso_timestamp_format() {
420 let ts = iso_timestamp();
421 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}