Skip to main content

oxgraph_db/
typed.rs

1//! Compile-time typed handles over the catalog's plain ids.
2//!
3//! A [`Key<T>`] wraps a [`PropertyKeyId`] and carries its value type `T` in the
4//! type system, so assigning a value of the wrong type through the typed write
5//! surface is a compile error rather than a runtime
6//! [`DbError::PropertyTypeMismatch`](crate::DbError). The markers [`Text`],
7//! [`Int`], and [`Bool`] correspond to the three [`PropertyType`] variants.
8//!
9//! # Performance
10//!
11//! Every handle is a `Copy` newtype; constructing, copying, and unwrapping are
12//! `O(1)`. [`Assignable::into_value`] is `O(1)` except for text, which is
13//! `O(value length)`.
14
15use core::marker::PhantomData;
16
17use crate::{IndexId, PropertyKeyId, PropertyType, PropertyValue, error::DbError};
18
19/// Sealing module so [`ValueType`] cannot be implemented downstream.
20mod sealed {
21    /// Sealed supertrait; only this crate's markers implement it.
22    pub trait Sealed {}
23}
24
25/// A scalar value type usable as a typed-handle marker.
26///
27/// Implemented only by [`Text`], [`Int`], and [`Bool`]; sealed against
28/// downstream implementations.
29///
30/// # Performance
31///
32/// `perf: unspecified` — a compile-time marker trait with no runtime cost.
33pub trait ValueType: sealed::Sealed + Copy {
34    /// The catalog property type this marker corresponds to.
35    const TYPE: PropertyType;
36}
37
38/// Text value-type marker (corresponds to [`PropertyType::Text`]).
39#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
40pub struct Text;
41
42/// Integer value-type marker (corresponds to [`PropertyType::Integer`]).
43#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
44pub struct Int;
45
46/// Boolean value-type marker (corresponds to [`PropertyType::Boolean`]).
47#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
48pub struct Bool;
49
50impl sealed::Sealed for Text {}
51impl sealed::Sealed for Int {}
52impl sealed::Sealed for Bool {}
53
54impl ValueType for Text {
55    const TYPE: PropertyType = PropertyType::Text;
56}
57impl ValueType for Int {
58    const TYPE: PropertyType = PropertyType::Integer;
59}
60impl ValueType for Bool {
61    const TYPE: PropertyType = PropertyType::Boolean;
62}
63
64/// A property key that carries its value type `T` in the type system.
65///
66/// # Performance
67///
68/// Copying and unwrapping are `O(1)`.
69#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
70pub struct Key<T: ValueType> {
71    /// The underlying catalog property key id.
72    id: PropertyKeyId,
73    /// Phantom value-type tag carrying `T`.
74    _ty: PhantomData<T>,
75}
76
77impl<T: ValueType> Key<T> {
78    /// Wraps a plain property key id as a typed key. The caller asserts the key
79    /// was declared with type `T::TYPE`.
80    ///
81    /// # Performance
82    ///
83    /// This function is `O(1)`.
84    #[must_use]
85    pub const fn from_id(id: PropertyKeyId) -> Self {
86        Self {
87            id,
88            _ty: PhantomData,
89        }
90    }
91
92    /// Returns the underlying plain property key id.
93    ///
94    /// # Performance
95    ///
96    /// This function is `O(1)`.
97    #[must_use]
98    pub const fn id(self) -> PropertyKeyId {
99        self.id
100    }
101}
102
103/// An equality index whose indexed key has value type `T`.
104///
105/// # Performance
106///
107/// Copying and unwrapping are `O(1)`.
108#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
109pub struct EqualityIndex<T: ValueType> {
110    /// The underlying catalog index id.
111    id: IndexId,
112    /// Phantom value-type tag carrying `T`.
113    _ty: PhantomData<T>,
114}
115
116impl<T: ValueType> EqualityIndex<T> {
117    /// Wraps a plain index id as a typed equality index. The caller asserts the
118    /// index covers a key of type `T::TYPE`.
119    ///
120    /// # Performance
121    ///
122    /// This function is `O(1)`.
123    #[must_use]
124    pub const fn from_id(id: IndexId) -> Self {
125        Self {
126            id,
127            _ty: PhantomData,
128        }
129    }
130
131    /// Returns the underlying plain index id.
132    ///
133    /// # Performance
134    ///
135    /// This function is `O(1)`.
136    #[must_use]
137    pub const fn id(self) -> IndexId {
138        self.id
139    }
140}
141
142/// A range index whose indexed key has value type `T`.
143///
144/// # Performance
145///
146/// Copying and unwrapping are `O(1)`.
147#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
148pub struct RangeIndex<T: ValueType> {
149    /// The underlying catalog index id.
150    id: IndexId,
151    /// Phantom value-type tag carrying `T`.
152    _ty: PhantomData<T>,
153}
154
155impl<T: ValueType> RangeIndex<T> {
156    /// Wraps a plain index id as a typed range index. The caller asserts the
157    /// index covers a key of type `T::TYPE`.
158    ///
159    /// # Performance
160    ///
161    /// This function is `O(1)`.
162    #[must_use]
163    pub const fn from_id(id: IndexId) -> Self {
164        Self {
165            id,
166            _ty: PhantomData,
167        }
168    }
169
170    /// Returns the underlying plain index id.
171    ///
172    /// # Performance
173    ///
174    /// This function is `O(1)`.
175    #[must_use]
176    pub const fn id(self) -> IndexId {
177        self.id
178    }
179}
180
181/// A Rust value that may be assigned to a property of value type `T`.
182///
183/// This is the compile-time gate behind the typed write surface: a value
184/// implements `Assignable<T>` only for the `T` it can legally inhabit, so a
185/// type-mismatched assignment fails to compile.
186///
187/// # Performance
188///
189/// [`Assignable::into_value`] is `O(1)` except for text (`O(value length)`).
190pub trait Assignable<T: ValueType> {
191    /// Converts into a [`PropertyValue`], checking range where narrowing.
192    ///
193    /// # Errors
194    ///
195    /// Returns [`DbError::ValueOutOfRange`] when an unsigned value exceeds
196    /// `i64::MAX`.
197    ///
198    /// # Performance
199    ///
200    /// This function is `O(1)` except for text (`O(value length)`).
201    fn into_value(self) -> Result<PropertyValue, DbError>;
202}
203
204impl Assignable<Text> for &str {
205    fn into_value(self) -> Result<PropertyValue, DbError> {
206        Ok(PropertyValue::Text(self.to_owned()))
207    }
208}
209
210impl Assignable<Text> for String {
211    fn into_value(self) -> Result<PropertyValue, DbError> {
212        Ok(PropertyValue::Text(self))
213    }
214}
215
216impl Assignable<Int> for i64 {
217    fn into_value(self) -> Result<PropertyValue, DbError> {
218        Ok(PropertyValue::Integer(self))
219    }
220}
221
222impl Assignable<Int> for u64 {
223    fn into_value(self) -> Result<PropertyValue, DbError> {
224        PropertyValue::try_from(self)
225    }
226}
227
228impl Assignable<Int> for usize {
229    fn into_value(self) -> Result<PropertyValue, DbError> {
230        PropertyValue::try_from(self)
231    }
232}
233
234impl Assignable<Bool> for bool {
235    fn into_value(self) -> Result<PropertyValue, DbError> {
236        Ok(PropertyValue::Boolean(self))
237    }
238}
239
240/// A Rust value that can be read back from a property of value type `T`.
241///
242/// # Performance
243///
244/// [`Readable::read`] is `O(1)` except for text (`O(value length)` to copy).
245pub trait Readable<T: ValueType>: Sized {
246    /// Reads `Self` from a [`PropertyValue`], or `None` on a type mismatch or
247    /// out-of-range narrowing.
248    ///
249    /// # Performance
250    ///
251    /// This function is `O(1)` except for text (`O(value length)`).
252    fn read(value: &PropertyValue) -> Option<Self>;
253}
254
255impl Readable<Text> for String {
256    fn read(value: &PropertyValue) -> Option<Self> {
257        value.as_text().map(str::to_owned)
258    }
259}
260
261impl Readable<Int> for i64 {
262    fn read(value: &PropertyValue) -> Option<Self> {
263        value.as_int()
264    }
265}
266
267impl Readable<Int> for usize {
268    fn read(value: &PropertyValue) -> Option<Self> {
269        value.as_count()
270    }
271}
272
273impl Readable<Bool> for bool {
274    fn read(value: &PropertyValue) -> Option<Self> {
275        value.as_bool()
276    }
277}
278
279#[cfg(test)]
280mod tests {
281    use super::*;
282
283    #[test]
284    fn markers_map_to_property_types() {
285        assert_eq!(<Text as ValueType>::TYPE, PropertyType::Text);
286        assert_eq!(<Int as ValueType>::TYPE, PropertyType::Integer);
287        assert_eq!(<Bool as ValueType>::TYPE, PropertyType::Boolean);
288    }
289
290    #[test]
291    fn typed_keys_roundtrip_their_plain_ids() {
292        let raw = PropertyKeyId::new(7);
293        let key = Key::<Text>::from_id(raw);
294        assert_eq!(key.id(), raw);
295    }
296
297    #[test]
298    fn assignable_checks_range_for_unsigned() {
299        assert_eq!(
300            Assignable::<Int>::into_value(5_u64).ok(),
301            Some(PropertyValue::Integer(5))
302        );
303        assert!(Assignable::<Int>::into_value(u64::MAX).is_err());
304        assert_eq!(
305            Assignable::<Text>::into_value("hi").ok(),
306            Some(PropertyValue::Text("hi".to_owned()))
307        );
308    }
309
310    #[test]
311    fn readable_projects_matching_values() {
312        assert_eq!(
313            <i64 as Readable<Int>>::read(&PropertyValue::Integer(3)),
314            Some(3)
315        );
316        assert_eq!(
317            <String as Readable<Text>>::read(&PropertyValue::Integer(3)),
318            None
319        );
320    }
321}