Skip to main content

reifydb_export/render/
value.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use reifydb_value::value::Value;
5
6use crate::error::RenderError;
7
8pub fn render_value(value: &Value) -> Result<String, RenderError> {
9	match value {
10		Value::None {
11			..
12		} => Ok("none".to_string()),
13		Value::Boolean(b) => Ok(if *b {
14			"true"
15		} else {
16			"false"
17		}
18		.to_string()),
19		Value::Float4(f) => {
20			if f.value().is_finite() {
21				Ok(f.to_string())
22			} else {
23				Err(RenderError::NonFiniteFloat)
24			}
25		}
26		Value::Float8(f) => {
27			if f.value().is_finite() {
28				Ok(f.to_string())
29			} else {
30				Err(RenderError::NonFiniteFloat)
31			}
32		}
33		Value::Int1(v) => Ok(v.to_string()),
34		Value::Int2(v) => Ok(v.to_string()),
35		Value::Int4(v) => Ok(v.to_string()),
36		Value::Int8(v) => Ok(v.to_string()),
37		Value::Int16(v) => Ok(v.to_string()),
38		Value::Uint1(v) => Ok(v.to_string()),
39		Value::Uint2(v) => Ok(v.to_string()),
40		Value::Uint4(v) => Ok(v.to_string()),
41		Value::Uint8(v) => Ok(v.to_string()),
42		Value::Uint16(v) => Ok(v.to_string()),
43		Value::Int(v) => Ok(v.to_string()),
44		Value::Uint(v) => Ok(v.to_string()),
45		Value::Decimal(d) => Ok(format!("'{}'", d)),
46		Value::Utf8(s) => render_text(s),
47		Value::Date(d) => Ok(format!("@{}", d)),
48		Value::Time(t) => Ok(format!("@{}", t)),
49		Value::DateTime(dt) => Ok(format!("@{}", dt)),
50		Value::Duration(dur) => Ok(format!("@{}", dur.to_iso_string())),
51		Value::Blob(b) => {
52			let display = b.to_string();
53			let hex = display.strip_prefix("0x").unwrap_or(&display);
54			Ok(format!("blob::hex('{}')", hex))
55		}
56		Value::Uuid4(u) => Ok(format!("'{}'", u)),
57		Value::Uuid7(u) => Ok(format!("'{}'", u)),
58		Value::IdentityId(id) => Ok(format!("'{}'", id)),
59		Value::DictionaryId(_) => Err(RenderError::Unsupported("DictionaryId")),
60		Value::Any(_) => Err(RenderError::Unsupported("Any")),
61		Value::Type(_) => Err(RenderError::Unsupported("Type")),
62		Value::List(_) => Err(RenderError::Unsupported("List")),
63		Value::Record(_) => Err(RenderError::Unsupported("Record")),
64		Value::Tuple(_) => Err(RenderError::Unsupported("Tuple")),
65	}
66}
67
68pub fn render_text(s: &str) -> Result<String, RenderError> {
69	let has_single = s.contains('\'');
70	let has_double = s.contains('"');
71	if has_single && has_double {
72		return Err(RenderError::UnrepresentableText);
73	}
74	if has_single {
75		Ok(format!("\"{}\"", s))
76	} else {
77		Ok(format!("'{}'", s))
78	}
79}
80
81#[cfg(test)]
82mod tests {
83	use reifydb_value::value::{
84		blob::Blob, date::Date, datetime::DateTime, duration::Duration, time::Time, value_type::ValueType,
85	};
86
87	use super::*;
88
89	#[test]
90	fn none_renders_as_none() {
91		assert_eq!(render_value(&Value::none()).unwrap(), "none");
92	}
93
94	#[test]
95	fn booleans_render_unquoted() {
96		assert_eq!(render_value(&Value::Boolean(true)).unwrap(), "true");
97		assert_eq!(render_value(&Value::Boolean(false)).unwrap(), "false");
98	}
99
100	#[test]
101	fn integers_render_as_decimal_including_negatives() {
102		assert_eq!(render_value(&Value::Int4(-100)).unwrap(), "-100");
103		assert_eq!(render_value(&Value::Int1(i8::MIN)).unwrap(), "-128");
104		assert_eq!(render_value(&Value::Uint8(u64::MAX)).unwrap(), "18446744073709551615");
105	}
106
107	#[test]
108	fn finite_floats_render_and_non_finite_fails_loud() {
109		assert_eq!(render_value(&Value::float8(0.0)).unwrap(), "0");
110		assert_eq!(render_value(&Value::float8(-2.5)).unwrap(), "-2.5");
111		assert_eq!(render_value(&Value::float8(f64::INFINITY)), Err(RenderError::NonFiniteFloat));
112		assert_eq!(render_value(&Value::float4(f32::NEG_INFINITY)), Err(RenderError::NonFiniteFloat));
113	}
114
115	#[test]
116	fn text_quote_selection_and_unrepresentable() {
117		assert_eq!(render_value(&Value::Utf8("Alice".to_string())).unwrap(), "'Alice'");
118		assert_eq!(render_value(&Value::Utf8(String::new())).unwrap(), "''");
119		assert_eq!(render_value(&Value::Utf8("it's".to_string())).unwrap(), "\"it's\"");
120		assert_eq!(render_value(&Value::Utf8("say \"hi\"".to_string())).unwrap(), "'say \"hi\"'");
121		assert_eq!(
122			render_value(&Value::Utf8("both ' and \"".to_string())),
123			Err(RenderError::UnrepresentableText)
124		);
125	}
126
127	#[test]
128	fn temporals_render_with_at_prefix() {
129		assert_eq!(render_value(&Value::Date(Date::from_ymd(2024, 3, 15).unwrap())).unwrap(), "@2024-03-15");
130		assert_eq!(
131			render_value(&Value::Time(Time::from_hms_nano(14, 30, 15, 123_456_789).unwrap())).unwrap(),
132			"@14:30:15.123456789"
133		);
134		let dt = render_value(&Value::DateTime(DateTime::from_epoch_secs(1_700_000_000).unwrap())).unwrap();
135		assert!(dt.starts_with("@") && dt.contains('T') && dt.ends_with('Z'), "got {dt}");
136	}
137
138	#[test]
139	fn durations_render_as_iso_with_at_prefix() {
140		assert_eq!(render_value(&Value::Duration(Duration::from_seconds(90).unwrap())).unwrap(), "@PT1M30S");
141		assert_eq!(
142			render_value(&Value::Duration(Duration::new(14, 3, 3_661_000_000_000).unwrap())).unwrap(),
143			"@P1Y2M3DT1H1M1S"
144		);
145		assert_eq!(render_value(&Value::Duration(Duration::zero()).clone()).unwrap(), "@PT0S");
146	}
147
148	#[test]
149	fn blob_renders_as_hex_constructor() {
150		assert_eq!(
151			render_value(&Value::Blob(Blob::from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]))).unwrap(),
152			"blob::hex('deadbeef')"
153		);
154		assert_eq!(render_value(&Value::Blob(Blob::empty())).unwrap(), "blob::hex('')");
155	}
156
157	#[test]
158	fn dictionary_id_value_is_rejected_since_scans_decode() {
159		let _ = ValueType::DictionaryId;
160		assert!(matches!(
161			render_value(&Value::DictionaryId(Default::default())),
162			Err(RenderError::Unsupported(_))
163		));
164	}
165}