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}