use std::{
sync::atomic::{AtomicU8, Ordering},
time::{SystemTime, UNIX_EPOCH},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[repr(u8)]
pub enum DebugFormat {
#[default]
Text = 0,
Json = 1,
}
static DEBUG_FORMAT: AtomicU8 = AtomicU8::new(0);
pub fn init() {
if let Ok(format) = std::env::var("VIBESQL_DEBUG_FORMAT") {
match format.to_lowercase().as_str() {
"json" => DEBUG_FORMAT.store(1, Ordering::Relaxed),
"text" | "" => DEBUG_FORMAT.store(0, Ordering::Relaxed),
_ => {
eprintln!("[WARNING] Unknown VIBESQL_DEBUG_FORMAT='{}', using 'text'", format);
DEBUG_FORMAT.store(0, Ordering::Relaxed);
}
}
}
}
pub fn get_format() -> DebugFormat {
match DEBUG_FORMAT.load(Ordering::Relaxed) {
1 => DebugFormat::Json,
_ => DebugFormat::Text,
}
}
pub fn is_json() -> bool {
DEBUG_FORMAT.load(Ordering::Relaxed) == 1
}
#[derive(Debug, Clone, Copy)]
pub enum Category {
Optimizer,
Execution,
Index,
Dml,
Profile,
}
impl Category {
pub fn as_str(&self) -> &'static str {
match self {
Category::Optimizer => "optimizer",
Category::Execution => "execution",
Category::Index => "index",
Category::Dml => "dml",
Category::Profile => "profile",
}
}
}
fn iso_timestamp() -> String {
let now = SystemTime::now();
let duration = now.duration_since(UNIX_EPOCH).unwrap_or_default();
let secs = duration.as_secs();
let millis = duration.subsec_millis();
let days_since_epoch = secs / 86400;
let time_of_day = secs % 86400;
let hours = time_of_day / 3600;
let minutes = (time_of_day % 3600) / 60;
let seconds = time_of_day % 60;
let mut year = 1970;
let mut remaining_days = days_since_epoch as i64;
loop {
let days_in_year =
if year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) { 366 } else { 365 };
if remaining_days < days_in_year {
break;
}
remaining_days -= days_in_year;
year += 1;
}
let is_leap = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
let days_in_months: [i64; 12] = if is_leap {
[31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
} else {
[31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
};
let mut month = 1;
for &days in &days_in_months {
if remaining_days < days {
break;
}
remaining_days -= days;
month += 1;
}
let day = remaining_days + 1;
format!(
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:03}Z",
year, month, day, hours, minutes, seconds, millis
)
}
fn json_escape(s: &str) -> String {
let mut result = String::with_capacity(s.len() + 16);
for c in s.chars() {
match c {
'"' => result.push_str("\\\""),
'\\' => result.push_str("\\\\"),
'\n' => result.push_str("\\n"),
'\r' => result.push_str("\\r"),
'\t' => result.push_str("\\t"),
c if c.is_control() => {
result.push_str(&format!("\\u{:04x}", c as u32));
}
c => result.push(c),
}
}
result
}
pub struct DebugEvent {
category: Category,
event: &'static str,
tag: &'static str,
text_parts: Vec<String>,
json_fields: Vec<(String, JsonValue)>,
}
pub enum JsonValue {
String(String),
Number(f64),
Int(i64),
Bool(bool),
Array(Vec<JsonValue>),
Object(Vec<(String, JsonValue)>),
Null,
}
impl JsonValue {
pub fn to_json(&self) -> String {
match self {
JsonValue::String(s) => format!("\"{}\"", json_escape(s)),
JsonValue::Number(n) => {
if n.is_finite() {
format!("{}", n)
} else {
"null".to_string()
}
}
JsonValue::Int(n) => format!("{}", n),
JsonValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
JsonValue::Array(arr) => {
let items: Vec<String> = arr.iter().map(|v| v.to_json()).collect();
format!("[{}]", items.join(","))
}
JsonValue::Object(fields) => {
let items: Vec<String> = fields
.iter()
.map(|(k, v)| format!("\"{}\":{}", json_escape(k), v.to_json()))
.collect();
format!("{{{}}}", items.join(","))
}
JsonValue::Null => "null".to_string(),
}
}
}
impl DebugEvent {
pub fn new(category: Category, event: &'static str, tag: &'static str) -> Self {
Self { category, event, tag, text_parts: Vec::new(), json_fields: Vec::new() }
}
pub fn text(mut self, message: impl Into<String>) -> Self {
self.text_parts.push(message.into());
self
}
pub fn field(mut self, name: impl Into<String>, value: JsonValue) -> Self {
self.json_fields.push((name.into(), value));
self
}
pub fn field_str(self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.field(name, JsonValue::String(value.into()))
}
pub fn field_int(self, name: impl Into<String>, value: i64) -> Self {
self.field(name, JsonValue::Int(value))
}
pub fn field_float(self, name: impl Into<String>, value: f64) -> Self {
self.field(name, JsonValue::Number(value))
}
pub fn field_bool(self, name: impl Into<String>, value: bool) -> Self {
self.field(name, JsonValue::Bool(value))
}
pub fn field_duration_us(self, name: impl Into<String>, duration: std::time::Duration) -> Self {
self.field(name, JsonValue::Int(duration.as_micros() as i64))
}
pub fn field_duration_ms(self, name: impl Into<String>, duration: std::time::Duration) -> Self {
self.field(name, JsonValue::Number(duration.as_secs_f64() * 1000.0))
}
pub fn field_str_array(self, name: impl Into<String>, values: &[String]) -> Self {
let arr: Vec<JsonValue> = values.iter().map(|s| JsonValue::String(s.clone())).collect();
self.field(name, JsonValue::Array(arr))
}
pub fn emit(self) {
match get_format() {
DebugFormat::Text => {
let message = self.text_parts.join(" ");
eprintln!("[{}] {}", self.tag, message);
}
DebugFormat::Json => {
let timestamp = iso_timestamp();
let mut fields = vec![
("timestamp".to_string(), JsonValue::String(timestamp)),
("category".to_string(), JsonValue::String(self.category.as_str().to_string())),
("event".to_string(), JsonValue::String(self.event.to_string())),
];
if !self.json_fields.is_empty() {
fields.push(("data".to_string(), JsonValue::Object(self.json_fields)));
}
let json = JsonValue::Object(fields).to_json();
eprintln!("{}", json);
}
}
}
}
pub fn debug_event(category: Category, event: &'static str, tag: &'static str) -> DebugEvent {
DebugEvent::new(category, event, tag)
}
#[macro_export]
macro_rules! debug_emit {
(
$category:ident, $event:expr, $tag:expr,
text: $($text_fmt:expr),* $(,)?
$(, fields: { $($field_name:expr => $field_value:expr),* $(,)? })?
) => {{
let mut event = $crate::debug_output::debug_event(
$crate::debug_output::Category::$category,
$event,
$tag
);
event = event.text(format!($($text_fmt),*));
$($(
event = event.field($field_name, $field_value);
)*)?
event.emit();
}};
}
pub use crate::debug_emit;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_json_escape() {
assert_eq!(json_escape("hello"), "hello");
assert_eq!(json_escape("hello\"world"), "hello\\\"world");
assert_eq!(json_escape("line\nbreak"), "line\\nbreak");
assert_eq!(json_escape("tab\there"), "tab\\there");
}
#[test]
fn test_json_value_formatting() {
assert_eq!(JsonValue::String("test".to_string()).to_json(), "\"test\"");
assert_eq!(JsonValue::Int(42).to_json(), "42");
assert_eq!(JsonValue::Number(3.5).to_json(), "3.5");
assert_eq!(JsonValue::Bool(true).to_json(), "true");
assert_eq!(JsonValue::Null.to_json(), "null");
let arr = JsonValue::Array(vec![JsonValue::Int(1), JsonValue::Int(2)]);
assert_eq!(arr.to_json(), "[1,2]");
let obj = JsonValue::Object(vec![
("name".to_string(), JsonValue::String("test".to_string())),
("value".to_string(), JsonValue::Int(42)),
]);
assert_eq!(obj.to_json(), "{\"name\":\"test\",\"value\":42}");
}
#[test]
fn test_debug_event_builder() {
let event = DebugEvent::new(Category::Optimizer, "test_event", "TEST")
.text("Test message")
.field_str("key", "value")
.field_int("count", 42)
.field_float("ratio", 0.5)
.field_bool("enabled", true);
drop(event);
}
#[test]
fn test_iso_timestamp_format() {
let ts = iso_timestamp();
assert!(ts.len() == 24, "Timestamp should be 24 chars: {}", ts);
assert!(ts.ends_with('Z'), "Timestamp should end with Z: {}", ts);
assert!(ts.contains('T'), "Timestamp should contain T: {}", ts);
}
}