1use chrono::{Duration, NaiveDate, NaiveTime};
26use pylon_core::query::ShapeNode;
27use pylon_value::DecodedValue;
28
29use crate::value::Value;
30
31pub fn row_to_json(shape: &ShapeNode, row: &DecodedValue) -> String {
33 to_json(&crate::decode::decode(shape, row))
34}
35
36pub fn to_json(value: &Value) -> String {
37 let mut out = String::new();
38 write(value, &mut out);
39 out
40}
41
42fn write(value: &Value, out: &mut String) {
43 match value {
44 Value::Null => out.push_str("null"),
45 Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
46 Value::Int64(n) => out.push_str(&n.to_string()),
47 Value::Float64(f) => out.push_str(&serde_json::to_string(f).unwrap_or_else(|_| "null".to_string())),
48 Value::Str(s) => write_str(s, out),
49 Value::Bytes(bytes) => write_str(&base64(bytes), out),
50 Value::Uuid(u) => write_str(&u.hyphenated().to_string(), out),
51 Value::Decimal(s) if s.parse::<f64>().is_ok_and(f64::is_finite) => out.push_str(s),
54 Value::Decimal(s) => write_str(s, out),
55 Value::Duration {
56 months,
57 days,
58 microseconds,
59 } => write_str(&iso_duration(*months, *days, *microseconds), out),
60 Value::Date(days) => write_str(&date(*days).to_string(), out),
61 Value::Time(micros) => write_str(&time(*micros), out),
62 Value::Timestamp(micros) => write_str(×tamp(*micros), out),
63 Value::Timestamptz(micros) => write_str(&format!("{}+00:00", timestamp(*micros)), out),
64 Value::Range(range) => {
65 if range.empty {
66 out.push_str(r#"{"empty": true}"#);
67 return;
68 }
69 out.push_str(r#"{"lower": "#);
70 write(range.lower.as_ref().unwrap_or(&Value::Null), out);
71 out.push_str(&format!(r#", "inc_lower": {}, "upper": "#, range.inc_lower));
72 write(range.upper.as_ref().unwrap_or(&Value::Null), out);
73 out.push_str(&format!(r#", "inc_upper": {}}}"#, range.inc_upper));
74 }
75 Value::Array(items) | Value::Tuple(items) => write_list(items.iter(), out),
76 Value::Object(object) => write_fields(object.json_fields(), out),
77 Value::Enum { value, .. } => write_str(value, out),
78 Value::Group(group) => {
79 out.push_str(r#"{"key": "#);
80 write_fields(group.key.fields(), out);
81 out.push_str(r#", "grouping": "#);
82 out.push_str(&serde_json::to_string(&group.grouping).unwrap_or_else(|_| "[]".to_string()));
83 out.push_str(r#", "elements": "#);
84 write_list(group.elements.iter(), out);
85 out.push('}');
86 }
87 Value::VectorSearch { object, distance } => {
88 out.push_str(r#"{"object": "#);
89 write(object, out);
90 out.push_str(r#", "distance": "#);
91 write(&Value::Float64(*distance), out);
92 out.push('}');
93 }
94 Value::FtsSearch { object, score } => {
95 out.push_str(r#"{"object": "#);
96 write(object, out);
97 out.push_str(r#", "score": "#);
98 write(&Value::Float64(*score), out);
99 out.push('}');
100 }
101 }
102}
103
104fn write_str(s: &str, out: &mut String) {
105 out.push_str(&serde_json::to_string(s).unwrap_or_else(|_| "\"\"".to_string()));
106}
107
108fn write_list<'v>(items: impl Iterator<Item = &'v Value>, out: &mut String) {
109 out.push('[');
110 for (i, item) in items.enumerate() {
111 if i > 0 {
112 out.push_str(", ");
113 }
114 write(item, out);
115 }
116 out.push(']');
117}
118
119fn write_fields<'v>(fields: impl Iterator<Item = (&'v str, &'v Value)>, out: &mut String) {
120 out.push('{');
121 for (i, (name, value)) in fields.enumerate() {
122 if i > 0 {
123 out.push_str(", ");
124 }
125 write_str(name, out);
126 out.push_str(": ");
127 write(value, out);
128 }
129 out.push('}');
130}
131
132fn pg_epoch() -> NaiveDate {
133 NaiveDate::from_ymd_opt(2000, 1, 1).unwrap_or_default()
134}
135
136fn date(days: i32) -> NaiveDate {
137 pg_epoch() + Duration::days(i64::from(days))
138}
139
140fn time(micros: i64) -> String {
143 let seconds = micros.div_euclid(1_000_000);
144 let fraction = micros.rem_euclid(1_000_000);
145 let clock = NaiveTime::from_num_seconds_from_midnight_opt(seconds.rem_euclid(86_400) as u32, 0)
146 .unwrap_or_default()
147 .format("%H:%M:%S")
148 .to_string();
149 format!("{clock}{}", fraction_suffix(fraction))
150}
151
152fn timestamp(micros: i64) -> String {
153 let days = micros.div_euclid(86_400_000_000);
154 let within_day = micros.rem_euclid(86_400_000_000);
155 format!("{}T{}", date(days as i32), time(within_day))
156}
157
158fn fraction_suffix(fraction_micros: i64) -> String {
159 if fraction_micros == 0 {
160 return String::new();
161 }
162 format!(".{fraction_micros:06}").trim_end_matches('0').to_string()
163}
164
165fn iso_duration(months: i32, days: i32, micros: i64) -> String {
167 let mut out = String::from("P");
168 let (years, months) = (months / 12, months % 12);
169 for (amount, unit) in [
170 (i64::from(years), 'Y'),
171 (i64::from(months), 'M'),
172 (i64::from(days), 'D'),
173 ] {
174 if amount != 0 {
175 out.push_str(&format!("{amount}{unit}"));
176 }
177 }
178 let negative = micros < 0;
179 let magnitude = micros.unsigned_abs();
180 let hours = magnitude / 3_600_000_000;
181 let minutes = magnitude % 3_600_000_000 / 60_000_000;
182 let seconds = magnitude % 60_000_000 / 1_000_000;
183 let fraction = (magnitude % 1_000_000) as i64;
184 let sign = if negative { "-" } else { "" };
185 let mut clock = String::new();
186 if hours != 0 {
187 clock.push_str(&format!("{sign}{hours}H"));
188 }
189 if minutes != 0 {
190 clock.push_str(&format!("{sign}{minutes}M"));
191 }
192 if seconds != 0 || fraction != 0 {
193 clock.push_str(&format!("{sign}{seconds}{}S", fraction_suffix(fraction)));
194 }
195 if !clock.is_empty() {
196 out.push('T');
197 out.push_str(&clock);
198 }
199 if out == "P" {
200 out.push_str("T0S");
201 }
202 out
203}
204
205fn base64(bytes: &[u8]) -> String {
206 const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
207 let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
208 for chunk in bytes.chunks(3) {
209 let n = chunk
210 .iter()
211 .enumerate()
212 .fold(0u32, |acc, (i, b)| acc | u32::from(*b) << (16 - 8 * i));
213 for i in 0..4 {
214 if i <= chunk.len() {
215 out.push(ALPHABET[(n >> (18 - 6 * i) & 0x3f) as usize] as char);
216 } else {
217 out.push('=');
218 }
219 }
220 }
221 out
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227
228 #[test]
229 fn temporal_values_render_as_iso_8601() {
230 assert_eq!(date(9760).to_string(), "2026-09-21");
232 assert_eq!(time(71_076_000_000), "19:44:36");
233 assert_eq!(time(71_076_500_000), "19:44:36.5");
234 assert_eq!(
235 timestamp(9760 * 86_400_000_000 + 71_076_224_624),
236 "2026-09-21T19:44:36.224624"
237 );
238 }
239
240 #[test]
241 fn durations_render_as_iso_8601_durations() {
242 assert_eq!(iso_duration(0, 0, 5_402_500_000), "PT1H30M2.5S");
243 assert_eq!(iso_duration(1, 2, 10_800_000_000), "P1M2DT3H");
244 assert_eq!(iso_duration(0, 0, 0), "PT0S");
245 }
246
247 #[test]
248 fn bytes_are_base64() {
249 assert_eq!(base64(b"ab"), "YWI=");
250 assert_eq!(base64(b"abc"), "YWJj");
251 assert_eq!(base64(b"a"), "YQ==");
252 }
253
254 #[test]
255 fn decimals_keep_their_digits() {
256 assert_eq!(to_json(&Value::Decimal("12.3400".to_string())), "12.3400");
257 assert_eq!(to_json(&Value::Decimal("NaN".to_string())), "\"NaN\"");
258 }
259}