vibesql_types/sql_value/
display.rs

1//! Display implementation for SqlValue
2
3use std::fmt;
4
5use crate::sql_value::SqlValue;
6
7/// Format a f64 value like SQLite does:
8/// - Use minimal representation (shortest round-trip safe string)
9/// - KEEP ".0" for whole numbers to distinguish REAL from INTEGER (SQLite behavior)
10/// - Use scientific notation for very small or very large values
11/// - Normalize -0.0 to 0.0 (SQLite behavior)
12fn format_f64(n: f64) -> String {
13    if n.is_nan() {
14        return "NaN".to_string();
15    }
16    // Normal display uses Inf/-Inf (capital I, matching SQLite)
17    // The quote() function formats as 9.0e+999 instead
18    if n.is_infinite() {
19        return if n > 0.0 { "Inf".to_string() } else { "-Inf".to_string() };
20    }
21
22    // Normalize negative zero to positive zero (SQLite behavior)
23    let n = if n == 0.0 { 0.0 } else { n };
24
25    let abs_n = n.abs();
26
27    // Use scientific notation for very large or very small numbers (like SQLite)
28    // SQLite uses ~15 significant figures (IEEE 754 double precision)
29    // Format: 1.0e+15, 1.0e-05 (lowercase e, explicit +/-, 2-digit exponent)
30    if abs_n >= 1e15 || (abs_n < 1e-4 && abs_n != 0.0) {
31        // Use 14 decimal places in mantissa (15 total significant figures)
32        let s = format!("{:.14e}", n);
33        return format_scientific_sqlite(&s);
34    }
35
36    // SQLite uses 15 significant digits for floating-point display (like printf's %.15g)
37    // We need to format with at most 15 significant digits, then strip trailing zeros
38    format_with_significant_digits(n, 15)
39}
40
41/// Format a f64 with a specified number of significant digits, SQLite-style
42/// - Strip trailing zeros (but keep at least one decimal place for whole numbers)
43/// - Handle the distinction between REAL and INTEGER types
44fn format_with_significant_digits(n: f64, sig_digits: usize) -> String {
45    if n == 0.0 {
46        return "0.0".to_string();
47    }
48
49    // Calculate the number of decimal places needed for sig_digits significant digits
50    let log10_abs = n.abs().log10();
51    let integer_digits = if log10_abs >= 0.0 { log10_abs.floor() as i32 + 1 } else { 0 };
52    let decimal_places = (sig_digits as i32 - integer_digits).max(0) as usize;
53
54    // Format with calculated decimal places
55    let formatted = format!("{:.prec$}", n, prec = decimal_places);
56
57    // Strip trailing zeros after decimal point, but keep at least one digit
58    if formatted.contains('.') {
59        let trimmed = formatted.trim_end_matches('0');
60        if trimmed.ends_with('.') {
61            format!("{}0", trimmed)
62        } else {
63            trimmed.to_string()
64        }
65    } else {
66        // Add .0 suffix for whole numbers (SQLite REAL distinction)
67        format!("{}.0", formatted)
68    }
69}
70
71/// Format scientific notation like SQLite: 1.0e+15, 1.0e-05
72/// - Lowercase 'e'
73/// - Explicit + or - sign for exponent
74/// - Two-digit exponent (padded with leading zero if needed)
75/// - Strip trailing zeros from mantissa (but keep at least one decimal place)
76fn format_scientific_sqlite(s: &str) -> String {
77    // Input format from Rust: "1.50000000000000e10" or "1.5e-5"
78    // Output format for SQLite: "1.5e+10" or "1.5e-05"
79    if let Some(e_pos) = s.find('e') {
80        let (mantissa, exp_part) = s.split_at(e_pos);
81        let exp_str = &exp_part[1..]; // Skip the 'e'
82
83        // Strip trailing zeros from mantissa and trailing decimal point
84        let mantissa = mantissa.trim_end_matches('0').trim_end_matches('.');
85
86        let (sign, exp_digits) = if let Some(stripped) = exp_str.strip_prefix('-') {
87            ("-", stripped)
88        } else if let Some(stripped) = exp_str.strip_prefix('+') {
89            ("+", stripped)
90        } else {
91            ("+", exp_str)
92        };
93
94        // Pad exponent to 2 digits
95        let exp_num: i32 = exp_digits.parse().unwrap_or(0);
96        format!("{}e{}{:02}", mantissa, sign, exp_num.abs())
97    } else {
98        s.to_string()
99    }
100}
101
102/// Format a f32 value like SQLite does.
103/// IMPORTANT: Format at f32 precision, not f64, to avoid exposing
104/// representation differences (e.g., 1.1f32 becomes 1.100000023841858 as f64)
105/// - Normalize -0.0 to 0.0 (SQLite behavior)
106fn format_f32(n: f32) -> String {
107    if n.is_nan() {
108        return "NaN".to_string();
109    }
110    // Normal display uses Inf/-Inf (capital I, matching SQLite)
111    // The quote() function formats as 9.0e+999 instead
112    if n.is_infinite() {
113        return if n > 0.0 { "Inf".to_string() } else { "-Inf".to_string() };
114    }
115
116    // Normalize negative zero to positive zero (SQLite behavior)
117    let n = if n == 0.0 { 0.0 } else { n };
118
119    let abs_n = n.abs();
120
121    // Use scientific notation for very large or very small numbers (like SQLite)
122    // SQLite uses ~15 significant figures (IEEE 754 double precision)
123    // Format: 1.0e+15, 1.0e-05 (lowercase e, explicit +/-, 2-digit exponent)
124    if abs_n >= 1e15 || (abs_n < 1e-4 && abs_n != 0.0) {
125        // Use 14 decimal places in mantissa (15 total significant figures)
126        let s = format!("{:.14e}", n);
127        return format_scientific_sqlite(&s);
128    }
129
130    // Use ryu for shortest round-trip representation at f32 precision
131    let mut buffer = ryu::Buffer::new();
132    let s = buffer.format(n);
133
134    // SQLite behavior: KEEP ".0" suffix for whole numbers to distinguish REAL from INTEGER
135    s.to_string()
136}
137
138/// Display implementation for SqlValue (how values are shown to users)
139impl fmt::Display for SqlValue {
140    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
141        match self {
142            // SQLite displays ALL integers as exact integers, regardless of magnitude
143            // Scientific notation is only for REAL values, not INTEGER values
144            SqlValue::Integer(i) => write!(f, "{}", i),
145            SqlValue::Smallint(i) => write!(f, "{}", i),
146            SqlValue::Bigint(i) => write!(f, "{}", i),
147            SqlValue::Unsigned(u) => write!(f, "{}", u),
148            // Format floating point types like SQLite: minimal representation
149            // Use f32-specific formatting for Float/Real to avoid precision artifacts
150            SqlValue::Numeric(n) => write!(f, "{}", format_f64(*n)),
151            SqlValue::Float(n) => write!(f, "{}", format_f32(*n)),
152            SqlValue::Real(n) => write!(f, "{}", format_f64(*n)),
153            SqlValue::Double(n) => write!(f, "{}", format_f64(*n)),
154            SqlValue::Character(s) => write!(f, "{}", s),
155            SqlValue::Varchar(s) => write!(f, "{}", s),
156            SqlValue::Boolean(true) => write!(f, "TRUE"),
157            SqlValue::Boolean(false) => write!(f, "FALSE"),
158            SqlValue::Date(s) => write!(f, "{}", s),
159            SqlValue::Time(s) => write!(f, "{}", s),
160            SqlValue::Timestamp(s) => write!(f, "{}", s),
161            SqlValue::Interval(s) => write!(f, "{}", s),
162            SqlValue::Vector(v) => {
163                // Format vector as space-separated f32 values
164                let formatted: Vec<String> = v.iter().map(|x| x.to_string()).collect();
165                write!(f, "[{}]", formatted.join(", "))
166            }
167            SqlValue::Blob(b) => {
168                // SQLite compatibility: Display BLOB as text if it contains valid UTF-8,
169                // otherwise display as hex string (without x'' prefix)
170                if let Ok(s) = std::str::from_utf8(b) {
171                    write!(f, "{}", s)
172                } else {
173                    // Not valid UTF-8, display as hex
174                    for byte in b {
175                        write!(f, "{:02X}", byte)?;
176                    }
177                    Ok(())
178                }
179            }
180            SqlValue::Null => write!(f, "NULL"),
181        }
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    #[test]
190    fn test_format_f64_helper() {
191        // SQLite-style formatting: minimal representation with at least one decimal place
192        assert_eq!(format_f64(1.1), "1.1");
193        assert_eq!(format_f64(2.2), "2.2");
194        assert_eq!(format_f64(1.0), "1.0");
195        assert_eq!(format_f64(2.0), "2.0");
196        assert_eq!(format_f64(0.0), "0.0");
197        assert_eq!(format_f64(123.456), "123.456");
198        assert_eq!(format_f64(0.5), "0.5");
199        assert_eq!(format_f64(100.0), "100.0");
200        assert_eq!(format_f64(-4373.0), "-4373.0");
201        assert_eq!(format_f64(-4373.123), "-4373.123");
202    }
203
204    #[test]
205    fn test_format_f32_helper() {
206        // f32 formatting: minimal representation at f32 precision
207        // This is the key fix: 1.1f32 should display as "1.1", not "1.100000023841858"
208        assert_eq!(format_f32(1.1f32), "1.1");
209        assert_eq!(format_f32(2.2f32), "2.2");
210        assert_eq!(format_f32(1.0f32), "1.0");
211        assert_eq!(format_f32(0.0f32), "0.0");
212        assert_eq!(format_f32(123.456f32), "123.456");
213        assert_eq!(format_f32(0.5f32), "0.5");
214        assert_eq!(format_f32(100.0f32), "100.0");
215        assert_eq!(format_f32(-4373.0f32), "-4373.0");
216    }
217
218    #[test]
219    fn test_format_f64_scientific() {
220        // Very large numbers use scientific notation (SQLite format: e+XX or e-XX)
221        assert_eq!(format_f64(1e15), "1e+15");
222        assert_eq!(format_f64(1e16), "1e+16");
223        // Very small numbers use scientific notation
224        assert_eq!(format_f64(0.00001), "1e-05");
225        assert_eq!(format_f64(1e-10), "1e-10");
226    }
227
228    #[test]
229    fn test_format_scientific_sqlite() {
230        // Test the SQLite-compatible scientific notation formatter
231        assert_eq!(format_scientific_sqlite("1e15"), "1e+15");
232        assert_eq!(format_scientific_sqlite("1e5"), "1e+05");
233        assert_eq!(format_scientific_sqlite("1.5e-5"), "1.5e-05");
234        assert_eq!(format_scientific_sqlite("1.5e-15"), "1.5e-15");
235        assert_eq!(format_scientific_sqlite("9.22337203685478e18"), "9.22337203685478e+18");
236    }
237
238    #[test]
239    fn test_numeric_display_whole_numbers() {
240        // SQLite-style: whole numbers display with .0 suffix
241        assert_eq!(format!("{}", SqlValue::Numeric(32.0)), "32.0");
242        assert_eq!(format!("{}", SqlValue::Numeric(-4373.0)), "-4373.0");
243        assert_eq!(format!("{}", SqlValue::Numeric(0.0)), "0.0");
244        assert_eq!(format!("{}", SqlValue::Numeric(164.0)), "164.0");
245    }
246
247    #[test]
248    fn test_numeric_display_fractional() {
249        // Fractional values display without trailing zeros
250        assert_eq!(format!("{}", SqlValue::Numeric(32.5)), "32.5");
251        assert_eq!(format!("{}", SqlValue::Numeric(-4373.123)), "-4373.123");
252        assert_eq!(format!("{}", SqlValue::Numeric(0.5)), "0.5");
253        assert_eq!(format!("{}", SqlValue::Numeric(1.1)), "1.1");
254    }
255
256    #[test]
257    fn test_numeric_display_special_values() {
258        // Special values
259        assert_eq!(format!("{}", SqlValue::Numeric(f64::NAN)), "NaN");
260        // Normal display uses Inf/-Inf (capital I, matching SQLite)
261        assert_eq!(format!("{}", SqlValue::Numeric(f64::INFINITY)), "Inf");
262        assert_eq!(format!("{}", SqlValue::Numeric(f64::NEG_INFINITY)), "-Inf");
263    }
264
265    #[test]
266    fn test_float_display_whole_numbers() {
267        // SQLite-style: Float type displays with minimal representation
268        assert_eq!(format!("{}", SqlValue::Float(32.0)), "32.0");
269        assert_eq!(format!("{}", SqlValue::Float(-4373.0)), "-4373.0");
270        assert_eq!(format!("{}", SqlValue::Float(0.0)), "0.0");
271        assert_eq!(format!("{}", SqlValue::Float(127.75)), "127.75");
272    }
273
274    #[test]
275    fn test_real_display_fractional() {
276        // Real type (now f64) displays with minimal representation
277        // SQLite REAL is an 8-byte IEEE float (same as f64)
278        assert_eq!(format!("{}", SqlValue::Real(32.5)), "32.5");
279        assert_eq!(format!("{}", SqlValue::Real(0.5)), "0.5");
280        assert_eq!(format!("{}", SqlValue::Real(1.1)), "1.1");
281        assert_eq!(format!("{}", SqlValue::Real(2.2)), "2.2");
282    }
283
284    #[test]
285    fn test_double_display_special_values() {
286        // Double type handles special values
287        assert_eq!(format!("{}", SqlValue::Double(f64::NAN)), "NaN");
288        // Normal display uses Inf/-Inf (capital I, matching SQLite)
289        assert_eq!(format!("{}", SqlValue::Double(f64::INFINITY)), "Inf");
290        assert_eq!(format!("{}", SqlValue::Double(f64::NEG_INFINITY)), "-Inf");
291        assert_eq!(format!("{}", SqlValue::Double(123.45)), "123.45");
292    }
293
294    #[test]
295    fn test_format_f64_whole_numbers() {
296        // SQLite behavior: whole numbers formatted WITH ".0" to distinguish from INTEGER
297        assert_eq!(format_f64(45.0), "45.0");
298        assert_eq!(format_f64(100.0), "100.0");
299        assert_eq!(format_f64(0.0), "0.0");
300        assert_eq!(format_f64(45.5), "45.5");
301        assert_eq!(format_f64(123.456), "123.456");
302    }
303
304    #[test]
305    fn test_format_negative_zero() {
306        // SQLite behavior: -0.0 should be normalized to 0.0
307        assert_eq!(format_f64(-0.0), "0.0");
308        assert_eq!(format_f32(-0.0f32), "0.0");
309        // Result of 0.0 * -1.0 should be "0.0", not "-0.0"
310        assert_eq!(format_f64(0.0 * -1.0), "0.0");
311    }
312
313    #[test]
314    fn test_blob_display_utf8() {
315        // SQLite compatibility: BLOBs containing valid UTF-8 display as text
316        // x'616263' = "abc"
317        assert_eq!(
318            format!("{}", SqlValue::Blob(vec![0x61, 0x62, 0x63])),
319            "abc"
320        );
321        // x'68617265' = "hare"
322        assert_eq!(
323            format!("{}", SqlValue::Blob(vec![0x68, 0x61, 0x72, 0x65])),
324            "hare"
325        );
326        // x'68656c6c6f' = "hello"
327        assert_eq!(
328            format!("{}", SqlValue::Blob(vec![0x68, 0x65, 0x6c, 0x6c, 0x6f])),
329            "hello"
330        );
331    }
332
333    #[test]
334    fn test_blob_display_invalid_utf8() {
335        // Non-UTF8 bytes display as hex
336        assert_eq!(
337            format!("{}", SqlValue::Blob(vec![0xFF, 0xFE])),
338            "FFFE"
339        );
340        // Invalid UTF-8 sequence
341        assert_eq!(
342            format!("{}", SqlValue::Blob(vec![0x80, 0x81, 0x82])),
343            "808182"
344        );
345    }
346
347    #[test]
348    fn test_blob_display_empty() {
349        // Empty blob displays as empty string
350        assert_eq!(format!("{}", SqlValue::Blob(vec![])), "");
351    }
352
353    #[test]
354    fn test_integer_display_no_scientific_notation() {
355        // SQLite displays ALL integers as exact integers, regardless of magnitude
356        // Scientific notation is only for REAL values, not INTEGER values
357        assert_eq!(format!("{}", SqlValue::Integer(i64::MAX)), "9223372036854775807");
358        assert_eq!(format!("{}", SqlValue::Bigint(i64::MAX)), "9223372036854775807");
359        assert_eq!(format!("{}", SqlValue::Integer(i64::MIN)), "-9223372036854775808");
360        assert_eq!(format!("{}", SqlValue::Integer(1_000_000_000_000_000)), "1000000000000000");
361        assert_eq!(format!("{}", SqlValue::Integer(999_999_999_999_999)), "999999999999999");
362        assert_eq!(format!("{}", SqlValue::Integer(10_000_000_000_000_000)), "10000000000000000");
363    }
364
365    #[test]
366    fn test_small_integer_display() {
367        // Standard integer display
368        assert_eq!(format!("{}", SqlValue::Integer(0)), "0");
369        assert_eq!(format!("{}", SqlValue::Integer(42)), "42");
370        assert_eq!(format!("{}", SqlValue::Integer(-100)), "-100");
371        assert_eq!(format!("{}", SqlValue::Integer(1_000_000)), "1000000");
372        assert_eq!(format!("{}", SqlValue::Bigint(123456789)), "123456789");
373    }
374
375    #[test]
376    fn test_unsigned_integer_display() {
377        // Unsigned integers also display as exact values
378        assert_eq!(format!("{}", SqlValue::Unsigned(u64::MAX)), "18446744073709551615");
379        assert_eq!(format!("{}", SqlValue::Unsigned(1_000_000_000_000_000)), "1000000000000000");
380        assert_eq!(format!("{}", SqlValue::Unsigned(999_999_999_999_999)), "999999999999999");
381    }
382}