Skip to main content

oxgraph_db/
value.rs

1//! Typed property and query values.
2
3use std::{fmt, sync::Arc};
4
5use serde::{Deserialize, Serialize};
6
7use crate::error::DbError;
8
9/// Supported scalar property types.
10///
11/// # Performance
12///
13/// Copying and comparing a type tag are `O(1)`.
14#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
15pub enum PropertyType {
16    /// Boolean property.
17    Boolean,
18    /// Signed 64-bit integer property.
19    Integer,
20    /// UTF-8 text property.
21    Text,
22}
23
24/// One typed property value.
25///
26/// Text is held as a shared `Arc<str>`, so a value cloned across overlay
27/// generations (the writer seeds from the parent's delta maps) shares one
28/// allocation instead of copying the bytes. Ordering, equality, hashing, and
29/// the serde representation are unchanged from the owned-`String` form: they
30/// all delegate to the underlying `str`.
31///
32/// # Performance
33///
34/// Cloning is `O(1)` (text clones an `Arc`); comparing and hashing are
35/// `O(value length)` for text and `O(1)` otherwise.
36#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
37pub enum PropertyValue {
38    /// Boolean value.
39    Boolean(bool),
40    /// Signed integer value.
41    Integer(i64),
42    /// UTF-8 text value, shared by reference count.
43    Text(Arc<str>),
44}
45
46impl PropertyValue {
47    /// Returns this value's type tag.
48    ///
49    /// # Performance
50    ///
51    /// This function is `O(1)`.
52    #[must_use]
53    pub const fn value_type(&self) -> PropertyType {
54        match self {
55            Self::Boolean(_value) => PropertyType::Boolean,
56            Self::Integer(_value) => PropertyType::Integer,
57            Self::Text(_value) => PropertyType::Text,
58        }
59    }
60
61    /// Returns the text payload, or `None` for a non-text value.
62    ///
63    /// # Performance
64    ///
65    /// This function is `O(1)`.
66    #[must_use]
67    pub fn as_text(&self) -> Option<&str> {
68        match self {
69            Self::Text(value) => Some(value.as_ref()),
70            Self::Boolean(_) | Self::Integer(_) => None,
71        }
72    }
73
74    /// Returns the integer payload, or `None` for a non-integer value.
75    ///
76    /// # Performance
77    ///
78    /// This function is `O(1)`.
79    #[must_use]
80    pub const fn as_int(&self) -> Option<i64> {
81        match self {
82            Self::Integer(value) => Some(*value),
83            Self::Boolean(_) | Self::Text(_) => None,
84        }
85    }
86
87    /// Returns the integer payload narrowed to `usize`, or `None` when the value
88    /// is not an integer or falls outside `usize` range.
89    ///
90    /// # Performance
91    ///
92    /// This function is `O(1)`.
93    #[must_use]
94    pub fn as_count(&self) -> Option<usize> {
95        match self {
96            Self::Integer(value) => usize::try_from(*value).ok(),
97            Self::Boolean(_) | Self::Text(_) => None,
98        }
99    }
100
101    /// Returns the boolean payload, or `None` for a non-boolean value.
102    ///
103    /// # Performance
104    ///
105    /// This function is `O(1)`.
106    #[must_use]
107    pub const fn as_bool(&self) -> Option<bool> {
108        match self {
109            Self::Boolean(value) => Some(*value),
110            Self::Integer(_) | Self::Text(_) => None,
111        }
112    }
113}
114
115impl From<bool> for PropertyValue {
116    /// Wraps a boolean value.
117    ///
118    /// # Performance
119    ///
120    /// This function is `O(1)`.
121    fn from(value: bool) -> Self {
122        Self::Boolean(value)
123    }
124}
125
126impl From<i64> for PropertyValue {
127    /// Wraps a signed 64-bit integer value.
128    ///
129    /// # Performance
130    ///
131    /// This function is `O(1)`.
132    fn from(value: i64) -> Self {
133        Self::Integer(value)
134    }
135}
136
137impl From<&str> for PropertyValue {
138    /// Copies a string slice into a shared text value.
139    ///
140    /// # Performance
141    ///
142    /// This function is `O(value length)`.
143    fn from(value: &str) -> Self {
144        Self::Text(Arc::from(value))
145    }
146}
147
148impl From<String> for PropertyValue {
149    /// Copies a string into a shared text value (the `Arc<str>` allocation
150    /// carries an inline reference count, so the string's buffer cannot be
151    /// reused).
152    ///
153    /// # Performance
154    ///
155    /// This function is `O(value length)`.
156    fn from(value: String) -> Self {
157        Self::Text(Arc::from(value))
158    }
159}
160
161impl From<Arc<str>> for PropertyValue {
162    /// Wraps an already-shared string as a text value.
163    ///
164    /// # Performance
165    ///
166    /// This function is `O(1)`.
167    fn from(value: Arc<str>) -> Self {
168        Self::Text(value)
169    }
170}
171
172impl TryFrom<u64> for PropertyValue {
173    type Error = DbError;
174
175    /// Narrows an unsigned 64-bit value into a signed [`PropertyValue::Integer`].
176    ///
177    /// # Errors
178    ///
179    /// Returns [`DbError::Query(crate::error::QueryError::ValueOutOfRange)`] when the value exceeds
180    /// `i64::MAX`.
181    ///
182    /// # Performance
183    ///
184    /// This function is `O(1)`.
185    fn try_from(value: u64) -> Result<Self, Self::Error> {
186        i64::try_from(value)
187            .map(Self::Integer)
188            .map_err(|_overflow| DbError::Query(crate::error::QueryError::ValueOutOfRange))
189    }
190}
191
192impl TryFrom<usize> for PropertyValue {
193    type Error = DbError;
194
195    /// Narrows a pointer-width unsigned value into a signed
196    /// [`PropertyValue::Integer`].
197    ///
198    /// # Errors
199    ///
200    /// Returns [`DbError::Query(crate::error::QueryError::ValueOutOfRange)`] when the value exceeds
201    /// `i64::MAX`.
202    ///
203    /// # Performance
204    ///
205    /// This function is `O(1)`.
206    fn try_from(value: usize) -> Result<Self, Self::Error> {
207        i64::try_from(value)
208            .map(Self::Integer)
209            .map_err(|_overflow| DbError::Query(crate::error::QueryError::ValueOutOfRange))
210    }
211}
212
213impl fmt::Display for PropertyValue {
214    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
215        match self {
216            Self::Boolean(value) => write!(formatter, "{value}"),
217            Self::Integer(value) => write!(formatter, "{value}"),
218            Self::Text(value) => formatter.write_str(value),
219        }
220    }
221}
222
223/// Property value parser used by CLI, HTTP, `OxQL`.
224///
225/// # Errors
226///
227/// Returns the original token when it cannot be parsed as a supported value.
228///
229/// # Performance
230///
231/// This function is `O(token.len())`.
232pub(crate) fn parse_value_token(token: &str) -> Result<PropertyValue, String> {
233    let trimmed = token.trim();
234    if trimmed == "true" {
235        return Ok(PropertyValue::Boolean(true));
236    }
237    if trimmed == "false" {
238        return Ok(PropertyValue::Boolean(false));
239    }
240    if let Ok(value) = trimmed.parse::<i64>() {
241        return Ok(PropertyValue::Integer(value));
242    }
243    parse_quoted(trimmed).map(PropertyValue::from)
244}
245
246/// Parses one single- or double-quoted token.
247fn parse_quoted(token: &str) -> Result<String, String> {
248    let single = token
249        .strip_prefix('\'')
250        .and_then(|text| text.strip_suffix('\''));
251    let double = token
252        .strip_prefix('"')
253        .and_then(|text| text.strip_suffix('"'));
254    single
255        .or(double)
256        .map_or_else(|| Err(token.to_owned()), |value| Ok(value.to_owned()))
257}
258
259#[cfg(test)]
260mod tests {
261    use proptest::prelude::*;
262
263    use super::*;
264
265    #[test]
266    fn from_scalars_roundtrip_through_accessors() {
267        assert_eq!(PropertyValue::from(true).as_bool(), Some(true));
268        assert_eq!(PropertyValue::from(7_i64).as_int(), Some(7));
269        assert_eq!(PropertyValue::from("hi").as_text(), Some("hi"));
270        assert_eq!(
271            PropertyValue::from(String::from("hi")).as_text(),
272            Some("hi")
273        );
274    }
275
276    #[test]
277    fn accessors_reject_mismatched_types() {
278        let text = PropertyValue::from("x");
279        assert_eq!(text.as_int(), None);
280        assert_eq!(text.as_bool(), None);
281        assert_eq!(text.as_count(), None);
282    }
283
284    proptest! {
285        #[test]
286        fn integer_roundtrips(value in any::<i64>()) {
287            prop_assert_eq!(PropertyValue::from(value).as_int(), Some(value));
288        }
289
290        #[test]
291        fn boolean_roundtrips(value in any::<bool>()) {
292            prop_assert_eq!(PropertyValue::from(value).as_bool(), Some(value));
293        }
294
295        #[test]
296        fn text_roundtrips(value in ".*") {
297            let parsed = PropertyValue::from(value.as_str());
298            prop_assert_eq!(parsed.as_text(), Some(value.as_str()));
299        }
300
301        #[test]
302        fn try_from_u64_matches_checked_narrowing(value in any::<u64>()) {
303            match i64::try_from(value) {
304                Ok(expected) => prop_assert_eq!(
305                    PropertyValue::try_from(value).ok().and_then(|parsed| parsed.as_int()),
306                    Some(expected)
307                ),
308                Err(_overflow) => prop_assert!(PropertyValue::try_from(value).is_err()),
309            }
310        }
311
312        #[test]
313        fn as_count_matches_checked_conversion(value in any::<i64>()) {
314            prop_assert_eq!(PropertyValue::Integer(value).as_count(), usize::try_from(value).ok());
315        }
316    }
317}