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}