Skip to main content

storage_kit/core/
record.rs

1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use uuid::Uuid;
6
7use super::vector::DenseVector;
8use crate::StorageError;
9
10/// Logical record identifier preserved by implementations supporting logical IDs.
11///
12/// Constructing an identifier does not enable ID persistence or lookup on every
13/// backend. Consult the backend contract and handle authoritative operation errors. The supplied LanceDB adapter does not support
14/// logical IDs; store an application key as a normal field and use a filter,
15/// as shown in the [`crate::database`] walkthrough.
16///
17/// # Examples
18///
19/// ```
20/// let id = storage_kit::database::data::RecordId::String("doc-1".into());
21/// assert_eq!(id.as_string_lossy(), "doc-1");
22/// ```
23#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
24pub enum RecordId {
25    /// UTF-8 string id.
26    String(String),
27    /// Unsigned 64-bit id.
28    U64(u64),
29    /// Signed 64-bit id.
30    I64(i64),
31    /// UUID id.
32    Uuid(Uuid),
33    /// Opaque application-defined id that should round-trip as text.
34    Opaque(String),
35}
36
37impl RecordId {
38    /// Converts the identifier into a human-readable string.
39    ///
40    /// # Examples
41    ///
42    /// ```
43    /// let id = storage_kit::database::data::RecordId::U64(42);
44    /// assert_eq!(id.as_string_lossy(), "42");
45    /// ```
46    pub fn as_string_lossy(&self) -> String {
47        match self {
48            Self::String(v) => v.clone(),
49            Self::U64(v) => v.to_string(),
50            Self::I64(v) => v.to_string(),
51            Self::Uuid(v) => v.to_string(),
52            Self::Opaque(v) => v.clone(),
53        }
54    }
55}
56
57impl From<&str> for RecordId {
58    fn from(value: &str) -> Self {
59        Self::String(value.into())
60    }
61}
62
63impl From<String> for RecordId {
64    fn from(value: String) -> Self {
65        Self::String(value)
66    }
67}
68
69impl From<u64> for RecordId {
70    fn from(value: u64) -> Self {
71        Self::U64(value)
72    }
73}
74
75impl From<i64> for RecordId {
76    fn from(value: i64) -> Self {
77        Self::I64(value)
78    }
79}
80
81impl From<Uuid> for RecordId {
82    fn from(value: Uuid) -> Self {
83        Self::Uuid(value)
84    }
85}
86
87/// Portable record payload used for inserts, updates, and query responses.
88///
89/// Use [`Self::without_id`] with the default LanceDB backend. [`Self::new`]
90/// supplies a logical ID only for implementations that support it; it does not
91/// enable logical-ID persistence on an unsupported backend.
92///
93/// # Examples
94///
95/// ```
96/// use storage_kit::database::data::DataRecord;
97/// let record = DataRecord::without_id().field("title", "Storage Kit")?;
98/// assert_eq!(record.fields["title"], "Storage Kit");
99/// assert!(record.id.is_none());
100/// # Ok::<(), storage_kit::StorageError>(())
101/// ```
102#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
103pub struct DataRecord {
104    /// Logical record identifier, when the selected backend supports ID persistence.
105    /// Unsupported backends reject a populated ID rather than synthesizing one.
106    pub id: Option<RecordId>,
107    /// Named vector payloads stored with the record.
108    pub vectors: BTreeMap<String, DenseVector>,
109    /// JSON document fields stored alongside vectors and the optional logical id.
110    pub fields: serde_json::Map<String, Value>,
111}
112
113impl DataRecord {
114    /// Creates a record with a logical id.
115    ///
116    /// The selected backend must support logical IDs. For LanceDB, use
117    /// [`Self::without_id`] and put the application identity in a normal field.
118    ///
119    /// # Examples
120    ///
121    /// ```
122    /// let record = storage_kit::database::data::DataRecord::new("doc-1");
123    /// assert_eq!(record.id.unwrap().as_string_lossy(), "doc-1");
124    /// ```
125    #[must_use]
126    pub fn new(id: impl Into<RecordId>) -> Self {
127        Self {
128            id: Some(id.into()),
129            ..Self::default()
130        }
131    }
132
133    /// Creates a record without a logical id.
134    ///
135    /// # Examples
136    ///
137    /// ```
138    /// let record = storage_kit::database::data::DataRecord::without_id();
139    /// assert!(record.id.is_none());
140    /// ```
141    #[must_use]
142    pub fn without_id() -> Self {
143        Self::default()
144    }
145
146    /// Adds one serializable JSON field.
147    ///
148    /// # Examples
149    ///
150    /// ```
151    /// let record = storage_kit::database::data::DataRecord::without_id()
152    ///     .field("title", "Storage Kit")?;
153    /// assert_eq!(record.fields["title"], serde_json::json!("Storage Kit"));
154    /// # Ok::<(), storage_kit::StorageError>(())
155    /// ```
156    ///
157    /// # Errors
158    ///
159    /// Returns an error when the value cannot be serialized to JSON.
160    pub fn field<T>(mut self, key: impl Into<String>, value: T) -> Result<Self, StorageError>
161    where
162        T: Serialize,
163    {
164        let value = serde_json::to_value(value).map_err(|error| {
165            StorageError::InvalidConfig(format!("failed to serialize record field: {error}"))
166        })?;
167        self.fields.insert(key.into(), value);
168        Ok(self)
169    }
170
171    /// Adds one already-materialized JSON field.
172    ///
173    /// # Examples
174    ///
175    /// ```
176    /// let record = storage_kit::database::data::DataRecord::without_id()
177    ///     .json_field("title", serde_json::json!("Storage Kit"));
178    /// assert_eq!(record.fields["title"], serde_json::json!("Storage Kit"));
179    /// ```
180    #[must_use]
181    pub fn json_field(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
182        self.fields.insert(key.into(), value);
183        self
184    }
185
186    /// Adds one named vector field.
187    ///
188    /// # Examples
189    ///
190    /// ```
191    /// let record = storage_kit::database::data::DataRecord::without_id().vector(
192    ///     "embedding",
193    ///     storage_kit::database::data::DenseVector::F32(vec![0.1, 0.2]),
194    /// );
195    /// assert_eq!(record.vectors.len(), 1);
196    /// ```
197    #[must_use]
198    pub fn vector(mut self, key: impl Into<String>, vector: DenseVector) -> Self {
199        self.vectors.insert(key.into(), vector);
200        self
201    }
202}