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/// # Examples
90///
91/// ```
92/// let mut record = storage_kit::database::data::DataRecord {
93/// id: Some(storage_kit::database::data::RecordId::String("doc-1".into())),
94/// ..Default::default()
95/// };
96/// record
97/// .fields
98/// .insert("title".into(), serde_json::json!("Storage Kit"));
99/// assert_eq!(record.id.as_ref().unwrap().as_string_lossy(), "doc-1");
100/// ```
101#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
102pub struct DataRecord {
103 /// Logical record identifier, when the selected backend supports ID persistence.
104 /// Unsupported backends reject a populated ID rather than synthesizing one.
105 pub id: Option<RecordId>,
106 /// Named vector payloads stored with the record.
107 pub vectors: BTreeMap<String, DenseVector>,
108 /// JSON document fields stored alongside vectors and the optional logical id.
109 pub fields: serde_json::Map<String, Value>,
110}
111
112impl DataRecord {
113 /// Creates a record with a logical id.
114 ///
115 /// # Examples
116 ///
117 /// ```
118 /// let record = storage_kit::database::data::DataRecord::new("doc-1");
119 /// assert_eq!(record.id.unwrap().as_string_lossy(), "doc-1");
120 /// ```
121 #[must_use]
122 pub fn new(id: impl Into<RecordId>) -> Self {
123 Self {
124 id: Some(id.into()),
125 ..Self::default()
126 }
127 }
128
129 /// Creates a record without a logical id.
130 ///
131 /// # Examples
132 ///
133 /// ```
134 /// let record = storage_kit::database::data::DataRecord::without_id();
135 /// assert!(record.id.is_none());
136 /// ```
137 #[must_use]
138 pub fn without_id() -> Self {
139 Self::default()
140 }
141
142 /// Adds one serializable JSON field.
143 ///
144 /// # Examples
145 ///
146 /// ```
147 /// let record = storage_kit::database::data::DataRecord::new("doc-1")
148 /// .field("title", "Storage Kit")?;
149 /// assert_eq!(record.fields["title"], serde_json::json!("Storage Kit"));
150 /// # Ok::<(), storage_kit::StorageError>(())
151 /// ```
152 ///
153 /// # Errors
154 ///
155 /// Returns an error when the value cannot be serialized to JSON.
156 pub fn field<T>(mut self, key: impl Into<String>, value: T) -> Result<Self, StorageError>
157 where
158 T: Serialize,
159 {
160 let value = serde_json::to_value(value).map_err(|error| {
161 StorageError::InvalidConfig(format!("failed to serialize record field: {error}"))
162 })?;
163 self.fields.insert(key.into(), value);
164 Ok(self)
165 }
166
167 /// Adds one already-materialized JSON field.
168 ///
169 /// # Examples
170 ///
171 /// ```
172 /// let record = storage_kit::database::data::DataRecord::new("doc-1")
173 /// .json_field("title", serde_json::json!("Storage Kit"));
174 /// assert_eq!(record.fields["title"], serde_json::json!("Storage Kit"));
175 /// ```
176 #[must_use]
177 pub fn json_field(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
178 self.fields.insert(key.into(), value);
179 self
180 }
181
182 /// Adds one named vector field.
183 ///
184 /// # Examples
185 ///
186 /// ```
187 /// let record = storage_kit::database::data::DataRecord::new("doc-1").vector(
188 /// "embedding",
189 /// storage_kit::database::data::DenseVector::F32(vec![0.1, 0.2]),
190 /// );
191 /// assert_eq!(record.vectors.len(), 1);
192 /// ```
193 #[must_use]
194 pub fn vector(mut self, key: impl Into<String>, vector: DenseVector) -> Self {
195 self.vectors.insert(key.into(), vector);
196 self
197 }
198}