eidetica/store/docstore.rs
1use std::str::FromStr;
2
3use crate::{
4 Error, Result, Store, Transaction,
5 crdt::{
6 CRDT, CRDTError, Doc,
7 doc::{List, Path, PathBuf, PathError, Value},
8 },
9 store::{Registered, errors::StoreError},
10};
11use async_trait::async_trait;
12
13/// A document-oriented Store providing ergonomic access to Doc CRDT data.
14///
15/// DocStore wraps the [`Doc`](crate::crdt::Doc) CRDT to provide path-based access to nested
16/// document structures. It supports string values and deletions via tombstones.
17///
18/// # API Overview
19///
20/// - **Basic operations**: [`get`](Self::get), [`set`](Self::set), [`delete`](Self::delete),
21/// [`get_all`](Self::get_all), [`contains_key`](Self::contains_key)
22/// - **Path operations**: [`get_path`](Self::get_path), [`set_path`](Self::set_path),
23/// [`contains_path`](Self::contains_path)
24/// - **Path mutation**: [`modify_path`](Self::modify_path),
25/// [`get_or_insert_path`](Self::get_or_insert_path),
26/// [`modify_or_insert_path`](Self::modify_or_insert_path)
27pub struct DocStore {
28 pub(crate) name: String,
29 pub(crate) txn: Transaction,
30}
31
32impl Registered for DocStore {
33 fn type_id() -> &'static str {
34 "docstore:v0"
35 }
36}
37
38#[async_trait]
39impl Store for DocStore {
40 type Data = Doc;
41
42 async fn new(txn: &Transaction, subtree_name: String) -> Result<Self> {
43 Ok(Self {
44 name: subtree_name,
45 txn: txn.clone(),
46 })
47 }
48
49 fn name(&self) -> &str {
50 &self.name
51 }
52
53 fn transaction(&self) -> &Transaction {
54 &self.txn
55 }
56}
57
58impl DocStore {
59 /// Gets a value associated with a key from the Store.
60 ///
61 /// This method prioritizes returning data staged within the current `Transaction`.
62 /// If the key is not found in the staged data it retrieves the fully merged historical
63 /// state from the backend up to the point defined by the `Transaction`'s parents and
64 /// returns the value from there.
65 ///
66 /// # Arguments
67 /// * `key` - The key to retrieve the value for.
68 ///
69 /// # Returns
70 /// A `Result` containing the MapValue if found, or `Error::NotFound`.
71 pub async fn get(&self, key: impl AsRef<str>) -> Result<Value> {
72 let key = key.as_ref();
73 // First check if there's any data in the transaction itself
74 if let Some(data) = self.local_data()? {
75 match data.get(key) {
76 Some(Value::Deleted) => {
77 return Err(StoreError::KeyNotFound {
78 store: self.name.clone(),
79 key: key.to_string(),
80 }
81 .into());
82 }
83 Some(value) => return Ok(value.clone()),
84 None => {
85 // Key not in local data, continue to backend
86 }
87 }
88 }
89
90 // Otherwise, get the full state from the backend
91 let data: Doc = self.txn.get_full_state(&self.name).await?;
92
93 // Return the value from the full state
94 match data.get(key) {
95 Some(value) => Ok(value.clone()),
96 None => Err(StoreError::KeyNotFound {
97 store: self.name.clone(),
98 key: key.to_string(),
99 }
100 .into()),
101 }
102 }
103
104 /// Gets a value associated with a key from the Store (HashMap-like API).
105 ///
106 /// This method returns an Option for compatibility with std::HashMap.
107 /// Returns `None` if the key is not found or is deleted.
108 ///
109 /// # Arguments
110 /// * `key` - The key to retrieve the value for.
111 ///
112 /// # Returns
113 /// An `Option` containing the cloned Value if found, or `None`.
114 pub async fn get_option(&self, key: impl AsRef<str>) -> Option<Value> {
115 self.get(key).await.ok()
116 }
117
118 /// Gets a value associated with a key from the Store (Result-based API for backward compatibility).
119 ///
120 /// This method prioritizes returning data staged within the current `Transaction`.
121 /// If the key is not found in the staged data it retrieves the fully merged historical
122 /// state from the backend up to the point defined by the `Transaction`'s parents and
123 /// returns the value from there.
124 ///
125 /// # Arguments
126 /// * `key` - The key to retrieve the value for.
127 ///
128 /// # Returns
129 /// A `Result` containing the MapValue if found, or `Error::NotFound`.
130 pub async fn get_result(&self, key: impl AsRef<str>) -> Result<Value> {
131 let key = key.as_ref();
132 // First check if there's any data in the transaction itself
133 if let Some(data) = self.local_data()?
134 && let Some(value) = data.get(key)
135 {
136 return Ok(value.clone());
137 }
138
139 // Otherwise, get the full state from the backend
140 let data: Doc = self.txn.get_full_state(&self.name).await?;
141
142 // Get the value
143 match data.get(key) {
144 Some(value) => Ok(value.clone()),
145 None => Err(StoreError::KeyNotFound {
146 store: self.name.clone(),
147 key: key.to_string(),
148 }
149 .into()),
150 }
151 }
152
153 /// Gets a string value associated with a key from the Store.
154 ///
155 /// This is a convenience method that calls `get()` and expects the value to be a string.
156 ///
157 /// # Arguments
158 /// * `key` - The key to retrieve the value for.
159 ///
160 /// # Returns
161 /// A `Result` containing the string value if found, or an error if the key is not found
162 /// or if the value is not a string.
163 pub async fn get_string(&self, key: impl AsRef<str>) -> Result<String> {
164 let key_ref = key.as_ref();
165 match self.get_result(key_ref).await? {
166 Value::Text(value) => Ok(value),
167 Value::Doc(_) => Err(StoreError::TypeMismatch {
168 store: self.name.clone(),
169 expected: "String".to_string(),
170 actual: "Doc".to_string(),
171 }
172 .into()),
173 Value::List(_) => Err(StoreError::TypeMismatch {
174 store: self.name.clone(),
175 expected: "String".to_string(),
176 actual: "list".to_string(),
177 }
178 .into()),
179 Value::Deleted => Err(StoreError::KeyNotFound {
180 store: self.name.clone(),
181 key: key_ref.to_string(),
182 }
183 .into()),
184 _ => Err(StoreError::TypeMismatch {
185 store: self.name.clone(),
186 expected: "String".to_string(),
187 actual: "Other".to_string(),
188 }
189 .into()),
190 }
191 }
192
193 /// Stages the setting of a key-value pair within the associated `Transaction`.
194 ///
195 /// This method updates the `Map` data held within the `Transaction` for this
196 /// `Doc` instance's subtree name. The change is **not** persisted to the backend
197 /// until the `Transaction::commit()` method is called.
198 ///
199 /// # Arguments
200 /// * `key` - The key to set.
201 /// * `value` - The value to associate with the key (can be &str, String, Value, etc.)
202 ///
203 /// # Returns
204 /// A `Result<()>` indicating success or an error during serialization or staging.
205 pub async fn set(&self, key: impl Into<String>, value: impl Into<Value>) -> Result<()> {
206 let key = key.into();
207 let value = value.into();
208
209 // Get current data from the transaction, or create new if not existing
210 let mut data = self.local_data()?.unwrap_or_default();
211
212 // Update the data using unified path interface
213 data.set(&key, value);
214
215 // Serialize and update the transaction
216 let serialized = serde_json::to_string(&data)?;
217 self.txn.update_subtree(&self.name, &serialized).await
218 }
219
220 /// Sets a key-value pair (HashMap-like API).
221 ///
222 /// Returns the previous value if one existed, or None if the key was not present.
223 /// This follows std::HashMap::insert() semantics.
224 ///
225 /// # Arguments
226 /// * `key` - The key to set.
227 /// * `value` - The value to associate with the key (can be &str, String, Value, etc.)
228 ///
229 /// # Returns
230 /// A `Result<Option<Value>>` containing the previous value, or `None` if no previous value.
231 pub async fn insert(
232 &self,
233 key: impl Into<String>,
234 value: impl Into<Value>,
235 ) -> Result<Option<Value>> {
236 let key = key.into();
237 let value = value.into();
238
239 // Get current data from the transaction, or create new if not existing
240 let mut data = self.local_data()?.unwrap_or_default();
241
242 // Get the previous value (if any) before setting
243 let previous = data
244 .get(&key)
245 .cloned()
246 .filter(|v| !matches!(v, Value::Deleted));
247
248 // Update the data
249 data.set(&key, value);
250
251 // Serialize and update the transaction
252 let serialized = serde_json::to_string(&data)?;
253 self.txn.update_subtree(&self.name, &serialized).await?;
254
255 Ok(previous)
256 }
257
258 /// Sets a key-value pair (Result-based API for backward compatibility).
259 ///
260 /// This method updates the `Map` data held within the `Transaction` for this
261 /// `Doc` instance's subtree name. The change is **not** persisted to the backend
262 /// until the `Transaction::commit()` method is called.
263 ///
264 /// # Arguments
265 /// * `key` - The key to set.
266 /// * `value` - The value to associate with the key (can be &str, String, Value, etc.)
267 ///
268 /// # Returns
269 /// A `Result<()>` indicating success or an error during serialization or staging.
270 pub async fn set_result(&self, key: impl Into<String>, value: impl Into<Value>) -> Result<()> {
271 let key = key.into();
272 let value = value.into();
273
274 // Get current data from the transaction, or create new if not existing
275 let mut data = self.local_data()?.unwrap_or_default();
276
277 // Update the data
278 data.set(&key, value);
279
280 // Serialize and update the transaction
281 let serialized = serde_json::to_string(&data)?;
282 self.txn.update_subtree(&self.name, &serialized).await
283 }
284
285 /// Convenience method to set a string value.
286 pub async fn set_string(&self, key: impl Into<String>, value: impl Into<String>) -> Result<()> {
287 self.set(key, Value::Text(value.into())).await
288 }
289
290 /// Stages the setting of a nested value within the associated `Transaction`.
291 ///
292 /// This method allows setting any valid Value type (String, Map, or Deleted).
293 ///
294 /// # Arguments
295 /// * `key` - The key to set.
296 /// * `value` - The Value to associate with the key.
297 ///
298 /// # Returns
299 /// A `Result<()>` indicating success or an error during serialization or staging.
300 /// Convenience method to get a List value.
301 pub async fn get_list(&self, key: impl AsRef<str>) -> Result<List> {
302 match self.get(key).await? {
303 Value::List(list) => Ok(list),
304 _ => Err(StoreError::TypeMismatch {
305 store: self.name.clone(),
306 expected: "list".to_string(),
307 actual: "Other".to_string(),
308 }
309 .into()),
310 }
311 }
312
313 /// Convenience method to get a nested Doc value.
314 pub async fn get_node(&self, key: impl AsRef<str>) -> Result<Doc> {
315 match self.get(key).await? {
316 Value::Doc(node) => Ok(node),
317 _ => Err(StoreError::TypeMismatch {
318 store: self.name.clone(),
319 expected: "Doc".to_string(),
320 actual: "Other".to_string(),
321 }
322 .into()),
323 }
324 }
325
326 /// Convenience method to set a list value.
327 pub async fn set_list(&self, key: impl Into<String>, list: impl Into<List>) -> Result<()> {
328 self.set(key, Value::List(list.into())).await
329 }
330
331 /// Convenience method to set a nested Doc value.
332 pub async fn set_node(&self, key: impl Into<String>, node: impl Into<Doc>) -> Result<()> {
333 self.set(key, Value::Doc(node.into())).await
334 }
335
336 /// Legacy method for backward compatibility - now just an alias to set
337 pub async fn set_value(&self, key: impl Into<String>, value: impl Into<Value>) -> Result<()> {
338 self.set(key, value).await
339 }
340
341 /// Legacy method for backward compatibility - now just an alias to get
342 pub async fn get_value(&self, key: impl AsRef<str>) -> Result<Value> {
343 self.get(key).await
344 }
345
346 /// Enhanced access methods with type inference
347 ///
348 /// These methods provide cleaner access with automatic type conversion,
349 /// similar to the CRDT Doc interface but adapted for the DocStore transaction model.
350 ///
351 /// Gets a value by path using dot notation (e.g., "user.profile.name")
352 ///
353 /// Traverses the DocStore data structure following the path segments separated by dots.
354 /// This method follows the DocStore staging model by checking local staged data first,
355 /// then falling back to historical data from the backend.
356 ///
357 /// # Path Syntax
358 ///
359 /// - **Docs**: Navigate by key name (e.g., "user.profile.name")
360 /// - **Lists**: Navigate by index (e.g., "items.0.title")
361 /// - **Mixed**: Combine both (e.g., "users.0.tags.1")
362 ///
363 /// # Examples
364 ///
365 /// ```rust,no_run
366 /// # use eidetica::Database;
367 /// # use eidetica::store::DocStore;
368 /// # use eidetica::crdt::doc::path;
369 /// # async fn example(database: Database) -> eidetica::Result<()> {
370 /// let txn = database.new_transaction().await?;
371 /// let store = txn.get_store::<DocStore>("data").await?;
372 ///
373 /// store.set_path(path!("user.profile.name"), "Alice").await?;
374 ///
375 /// // Navigate nested structure
376 /// let name = store.get_path(path!("user.profile.name")).await?;
377 /// # Ok(())
378 /// # }
379 /// ```
380 ///
381 /// # Returns
382 /// A `Result<Value>` containing the value if found, or an error if not found.
383 pub async fn get_path(&self, path: impl AsRef<Path>) -> Result<Value> {
384 // First check if there's any local staged data
385 if let Some(data) = self.local_data()?
386 && let Some(value) = data.get(&path)
387 {
388 return Ok(value.clone());
389 }
390
391 // Otherwise, get the full state from the backend
392 let data: Doc = self.txn.get_full_state(&self.name).await?;
393
394 // Get the path from the full state
395 match data.get(&path) {
396 Some(value) => Ok(value.clone()),
397 None => Err(StoreError::KeyNotFound {
398 store: self.name.clone(),
399 key: path.as_ref().as_str().to_string(),
400 }
401 .into()),
402 }
403 }
404
405 /// Gets a value by path using dot notation (HashMap-like API).
406 ///
407 /// # Returns
408 /// An `Option<Value>` containing the value if found, or `None` if not found.
409 pub async fn get_path_option(&self, path: impl AsRef<Path>) -> Option<Value> {
410 self.get_path(path).await.ok()
411 }
412
413 /// Gets a value by path using dot notation (Result-based API for backward compatibility).
414 ///
415 /// # Returns
416 /// A `Result<Value>` containing the value if found, or an error if not found.
417 pub async fn get_path_result(&self, path: impl AsRef<Path>) -> Result<Value> {
418 self.get_path(path).await
419 }
420}
421
422impl From<PathError> for Error {
423 fn from(err: PathError) -> Self {
424 // Convert PathError to CRDTError first, then to main Error
425 Error::CRDT(err.into())
426 }
427}
428
429impl DocStore {
430 /// Gets a value with automatic type conversion using TryFrom.
431 ///
432 /// This provides a generic interface that can convert to any type that implements
433 /// `TryFrom<&Value>`, making the API more ergonomic by reducing type specification.
434 ///
435 /// # Examples
436 ///
437 /// ```rust,no_run
438 /// # use eidetica::Database;
439 /// # use eidetica::store::DocStore;
440 /// # async fn example(database: Database) -> eidetica::Result<()> {
441 /// let txn = database.new_transaction().await?;
442 /// let store = txn.get_store::<DocStore>("data").await?;
443 ///
444 /// store.set("name", "Alice").await?;
445 /// store.set("age", 30).await?;
446 ///
447 /// // Type inference makes this clean
448 /// let name: String = store.get_as("name").await?;
449 /// let age: i64 = store.get_as("age").await?;
450 ///
451 /// assert_eq!(name, "Alice");
452 /// assert_eq!(age, 30);
453 /// # Ok(())
454 /// # }
455 /// ```
456 pub async fn get_as<T>(&self, key: impl AsRef<str>) -> Result<T>
457 where
458 T: for<'a> TryFrom<&'a Value, Error = CRDTError>,
459 {
460 let value = self.get(key).await?;
461 T::try_from(&value).map_err(Into::into)
462 }
463
464 /// Gets a value by path with automatic type conversion using TryFrom
465 ///
466 /// Similar to `get_as()` but works with dot-notation paths for nested access.
467 /// This method follows the DocStore staging model by checking local staged data first,
468 /// then falling back to historical data from the backend.
469 ///
470 /// # Examples
471 ///
472 /// ```rust,no_run
473 /// # use eidetica::Database;
474 /// # use eidetica::store::DocStore;
475 /// # use eidetica::crdt::doc::path;
476 /// # async fn example(database: Database) -> eidetica::Result<()> {
477 /// let txn = database.new_transaction().await?;
478 /// let store = txn.get_store::<DocStore>("data").await?;
479 ///
480 /// // Assuming nested structure exists
481 /// // Type inference with path access
482 /// let name: String = store.get_path_as(path!("user.profile.name")).await?;
483 /// let age: i64 = store.get_path_as(path!("user.profile.age")).await?;
484 ///
485 /// assert_eq!(name, "Alice");
486 /// assert_eq!(age, 30);
487 /// # Ok(())
488 /// # }
489 /// ```
490 ///
491 /// # Errors
492 ///
493 /// Returns an error if:
494 /// - The path doesn't exist (`SubtreeError::KeyNotFound`)
495 /// - The value cannot be converted to type T (`CRDTError::TypeMismatch`)
496 /// - The DocStore operation fails
497 pub async fn get_path_as<T>(&self, path: impl AsRef<Path>) -> Result<T>
498 where
499 T: for<'a> TryFrom<&'a Value, Error = CRDTError>,
500 {
501 let value = self.get_path(path).await?;
502 T::try_from(&value).map_err(Into::into)
503 }
504
505 /// Mutable access methods for transaction-based modification
506 ///
507 /// These methods work with DocStore's staging model, where changes are staged
508 /// in the Transaction transaction rather than modified in-place.
509 ///
510 /// Get or insert a value with a default.
511 ///
512 /// If the key exists (in either local staging area or historical data),
513 /// returns the existing value. If the key doesn't exist, sets it to the
514 /// default value and returns that.
515 ///
516 /// # Examples
517 ///
518 /// ```rust,no_run
519 /// # use eidetica::Database;
520 /// # use eidetica::store::DocStore;
521 /// # async fn example(database: Database) -> eidetica::Result<()> {
522 /// let txn = database.new_transaction().await?;
523 /// let store = txn.get_store::<DocStore>("data").await?;
524 ///
525 /// // Key doesn't exist - will set default
526 /// let count1: i64 = store.get_or_insert("counter", 0).await?;
527 /// assert_eq!(count1, 0);
528 ///
529 /// // Key exists - will return existing value
530 /// store.set("counter", 5).await?;
531 /// let count2: i64 = store.get_or_insert("counter", 100).await?;
532 /// assert_eq!(count2, 5);
533 /// # Ok(())
534 /// # }
535 /// ```
536 pub async fn get_or_insert<T>(&self, key: impl AsRef<str>, default: T) -> Result<T>
537 where
538 T: Into<Value> + for<'a> TryFrom<&'a Value, Error = CRDTError> + Clone,
539 {
540 let key_str = key.as_ref();
541
542 // Try to get existing value first
543 match self.get_as::<T>(key_str).await {
544 Ok(existing) => Ok(existing),
545 Err(_) => {
546 // Key doesn't exist or wrong type - set default and return it
547 self.set_result(key_str, default.clone()).await?;
548 Ok(default)
549 }
550 }
551 }
552
553 /// Modifies a value in-place using a closure
554 ///
555 /// If the key exists and can be converted to type T, the closure is called
556 /// with the value. After the closure returns, the modified value is staged
557 /// back to the DocStore.
558 ///
559 /// This method handles the DocStore staging model by:
560 /// 1. Getting the current value (from local staging or historical data)
561 /// 2. Converting it to the desired type
562 /// 3. Applying the modification closure
563 /// 4. Staging the result back to the Transaction
564 ///
565 /// # Errors
566 ///
567 /// Returns an error if:
568 /// - The key doesn't exist (`SubtreeError::KeyNotFound`)
569 /// - The value cannot be converted to type T (`CRDTError::TypeMismatch`)
570 /// - Setting the value fails
571 ///
572 /// # Examples
573 ///
574 /// ```rust,no_run
575 /// # use eidetica::Database;
576 /// # use eidetica::store::DocStore;
577 /// # async fn example(database: Database) -> eidetica::Result<()> {
578 /// let txn = database.new_transaction().await?;
579 /// let store = txn.get_store::<DocStore>("data").await?;
580 ///
581 /// store.set("count", 5).await?;
582 /// store.set("text", "hello").await?;
583 ///
584 /// // Modify counter
585 /// store.modify::<i64, _>("count", |count| {
586 /// *count += 10;
587 /// }).await?;
588 /// assert_eq!(store.get_as::<i64>("count").await?, 15);
589 ///
590 /// // Modify string
591 /// store.modify::<String, _>("text", |text| {
592 /// text.push_str(" world");
593 /// }).await?;
594 /// assert_eq!(store.get_as::<String>("text").await?, "hello world");
595 /// # Ok(())
596 /// # }
597 /// ```
598 pub async fn modify<T, F>(&self, key: impl AsRef<str>, f: F) -> Result<()>
599 where
600 T: for<'a> TryFrom<&'a Value, Error = CRDTError> + Into<Value>,
601 F: FnOnce(&mut T),
602 {
603 let key = key.as_ref();
604
605 // Try to get and convert the current value
606 let mut value = self.get_as::<T>(key).await?;
607
608 // Apply the modification
609 f(&mut value);
610
611 // Stage the modified value back
612 self.set(key, value).await?;
613 Ok(())
614 }
615
616 /// Modify a value or insert a default if it doesn't exist.
617 ///
618 /// This is a combination of `get_or_insert` and `modify` that ensures
619 /// the key exists before modification.
620 ///
621 /// # Examples
622 ///
623 /// ```rust,no_run
624 /// # use eidetica::Database;
625 /// # use eidetica::store::DocStore;
626 /// # async fn example(database: Database) -> eidetica::Result<()> {
627 /// let txn = database.new_transaction().await?;
628 /// let store = txn.get_store::<DocStore>("data").await?;
629 ///
630 /// // Key doesn't exist - will create with default then modify
631 /// store.modify_or_insert::<i64, _>("counter", 0, |count| {
632 /// *count += 5;
633 /// }).await?;
634 /// assert_eq!(store.get_as::<i64>("counter").await?, 5);
635 ///
636 /// // Key exists - will just modify
637 /// store.modify_or_insert::<i64, _>("counter", 100, |count| {
638 /// *count *= 2;
639 /// }).await?;
640 /// assert_eq!(store.get_as::<i64>("counter").await?, 10);
641 /// # Ok(())
642 /// # }
643 /// ```
644 pub async fn modify_or_insert<T, F>(&self, key: impl AsRef<str>, default: T, f: F) -> Result<()>
645 where
646 T: Into<Value> + for<'a> TryFrom<&'a Value, Error = CRDTError> + Clone,
647 F: FnOnce(&mut T),
648 {
649 let key = key.as_ref();
650
651 // Get existing value or insert default
652 let mut value = self.get_or_insert(key, default).await?;
653
654 // Apply the modification
655 f(&mut value);
656
657 // Stage the modified value back
658 self.set(key, value).await?;
659
660 Ok(())
661 }
662
663 /// Get or insert a value at a path with a default, similar to get_or_insert but for paths
664 ///
665 /// If the path exists (in either local staging area or historical data),
666 /// returns the existing value. If the path doesn't exist, sets it to the
667 /// default value and returns that. Intermediate nodes are created as needed.
668 ///
669 /// # Examples
670 ///
671 /// ```rust,no_run
672 /// # use eidetica::Database;
673 /// # use eidetica::store::DocStore;
674 /// # use eidetica::crdt::doc::path;
675 /// # async fn example(database: Database) -> eidetica::Result<()> {
676 /// let txn = database.new_transaction().await?;
677 /// let store = txn.get_store::<DocStore>("data").await?;
678 ///
679 /// // Path doesn't exist - will create structure and set default
680 /// let count1: i64 = store.get_or_insert_path(path!("user.stats.score"), 0).await?;
681 /// assert_eq!(count1, 0);
682 ///
683 /// // Path exists - will return existing value
684 /// store.set_path(path!("user.stats.score"), 42).await?;
685 /// let count2: i64 = store.get_or_insert_path(path!("user.stats.score"), 100).await?;
686 /// assert_eq!(count2, 42);
687 /// # Ok(())
688 /// # }
689 /// ```
690 pub async fn get_or_insert_path<T>(&self, path: impl AsRef<Path>, default: T) -> Result<T>
691 where
692 T: Into<Value> + for<'a> TryFrom<&'a Value, Error = CRDTError> + Clone,
693 {
694 // Try to get existing value first
695 match self.get_path_as(path.as_ref()).await {
696 Ok(existing) => Ok(existing),
697 Err(_) => {
698 // Path doesn't exist or wrong type - set default and return it
699 self.set_path(path, default.clone()).await?;
700 Ok(default)
701 }
702 }
703 }
704
705 /// Get or insert a value at a path with string paths for runtime normalization
706 pub async fn get_or_insert_path_str<T>(&self, path: &str, default: T) -> Result<T>
707 where
708 T: Into<Value> + for<'a> TryFrom<&'a Value, Error = CRDTError> + Clone,
709 {
710 let pathbuf = PathBuf::from_str(path).unwrap(); // Infallible
711 self.get_or_insert_path(&pathbuf, default).await
712 }
713
714 /// Modify a value at a path or insert a default if it doesn't exist.
715 ///
716 /// This is a combination of `get_or_insert_path` and `modify_path` that ensures
717 /// the path exists before modification, creating intermediate structure as needed.
718 ///
719 /// # Examples
720 ///
721 /// ```rust,no_run
722 /// # use eidetica::Database;
723 /// # use eidetica::store::DocStore;
724 /// # use eidetica::crdt::doc::path;
725 /// # async fn example(database: Database) -> eidetica::Result<()> {
726 /// let txn = database.new_transaction().await?;
727 /// let store = txn.get_store::<DocStore>("data").await?;
728 ///
729 /// // Path doesn't exist - will create structure with default then modify
730 /// store.modify_or_insert_path::<i64, _>(path!("user.stats.score"), 0, |score| {
731 /// *score += 10;
732 /// }).await?;
733 /// assert_eq!(store.get_path_as::<i64>(path!("user.stats.score")).await?, 10);
734 ///
735 /// // Path exists - will just modify
736 /// store.modify_or_insert_path::<i64, _>(path!("user.stats.score"), 100, |score| {
737 /// *score *= 2;
738 /// }).await?;
739 /// assert_eq!(store.get_path_as::<i64>(path!("user.stats.score")).await?, 20);
740 /// # Ok(())
741 /// # }
742 /// ```
743 pub async fn modify_or_insert_path<T, F>(
744 &self,
745 path: impl AsRef<Path>,
746 default: T,
747 f: F,
748 ) -> Result<()>
749 where
750 T: Into<Value> + for<'a> TryFrom<&'a Value, Error = CRDTError> + Clone,
751 F: FnOnce(&mut T),
752 {
753 // Get existing value or insert default
754 let mut value = self.get_or_insert_path(path.as_ref(), default).await?;
755
756 // Apply the modification
757 f(&mut value);
758
759 // Stage the modified value back
760 self.set_path(path, value).await?;
761
762 Ok(())
763 }
764
765 /// Modify a value or insert a default with string paths for runtime normalization
766 pub async fn modify_or_insert_path_str<T, F>(&self, path: &str, default: T, f: F) -> Result<()>
767 where
768 T: Into<Value> + for<'a> TryFrom<&'a Value, Error = CRDTError> + Clone,
769 F: FnOnce(&mut T),
770 {
771 let pathbuf = PathBuf::from_str(path).unwrap(); // Infallible
772 self.modify_or_insert_path(&pathbuf, default, f).await
773 }
774
775 /// Sets a value at the given path, creating intermediate nodes as needed
776 ///
777 /// This method stages a path-based set operation in the Transaction transaction.
778 /// The path uses dot notation to navigate and create **nested map structures**.
779 /// Intermediate maps are created automatically where necessary.
780 ///
781 /// # Important: Creates Nested Maps, Not Flat Keys
782 ///
783 /// Using dots in the path creates a **hierarchy of nested maps**, not flat keys with dots.
784 /// For example, `set_path("user.name", "Alice")` creates:
785 /// ```json
786 /// {
787 /// "user": {
788 /// "name": "Alice"
789 /// }
790 /// }
791 /// ```
792 /// NOT: `{ "user.name": "Alice" }`
793 ///
794 /// # Path Syntax
795 ///
796 /// - **Docs**: Navigate by key name (e.g., "user.profile.name")
797 /// - **Creating structure**: Intermediate nodes are created automatically
798 /// - **Overwriting**: If a path segment points to a non-node value, it will be overwritten
799 ///
800 /// # Examples
801 ///
802 /// ```rust,no_run
803 /// # use eidetica::Database;
804 /// # use eidetica::store::DocStore;
805 /// # use eidetica::crdt::doc::path;
806 /// # use eidetica::crdt::doc::Value;
807 /// # async fn example(database: Database) -> eidetica::Result<()> {
808 /// let txn = database.new_transaction().await?;
809 /// let store = txn.get_store::<DocStore>("data").await?;
810 ///
811 /// // Set nested values, creating structure as needed
812 /// store.set_path(path!("user.profile.name"), "Alice").await?;
813 /// store.set_path(path!("user.profile.age"), 30).await?;
814 /// store.set_path(path!("user.settings.theme"), "dark").await?;
815 ///
816 /// // This creates nested structure:
817 /// // {
818 /// // "user": {
819 /// // "profile": { "name": "Alice", "age": 30 },
820 /// // "settings": { "theme": "dark" }
821 /// // }
822 /// // }
823 ///
824 /// // Access with get_path methods
825 /// assert_eq!(store.get_path_as::<String>(path!("user.profile.name")).await?, "Alice");
826 ///
827 /// // Or navigate the nested structure manually from get_all()
828 /// let all = store.get_all().await?;
829 /// // all.get("user") returns a Doc, NOT all.get("user.profile.name")
830 /// if let Some(Value::Doc(user)) = all.get("user") {
831 /// if let Some(Value::Doc(profile)) = user.get("profile") {
832 /// assert_eq!(profile.get("name"), Some(&Value::Text("Alice".to_string())));
833 /// }
834 /// }
835 /// # Ok(())
836 /// # }
837 /// ```
838 ///
839 /// # Errors
840 ///
841 /// Returns an error if:
842 /// - The path is empty
843 /// - A non-final segment contains a non-node value that cannot be navigated through
844 /// - The DocStore operation fails
845 pub async fn set_path(&self, path: impl AsRef<Path>, value: impl Into<Value>) -> Result<()> {
846 let value = value.into();
847
848 // Get current data from the transaction, or create new if not existing
849 let mut data = self.local_data()?.unwrap_or_default();
850
851 // Use Doc's set method to handle the path logic
852 data.set(&path, value);
853
854 // Serialize and update the transaction
855 let serialized = serde_json::to_string(&data)?;
856 self.txn.update_subtree(&self.name, &serialized).await
857 }
858
859 /// Sets a value at the given path with string paths for runtime normalization
860 pub async fn set_path_str(&self, path: &str, value: impl Into<Value>) -> Result<()> {
861 let pathbuf = PathBuf::from_str(path).unwrap(); // Infallible
862 self.set_path(&pathbuf, value).await
863 }
864
865 /// Modifies a value at a path in-place using a closure
866 ///
867 /// Similar to `modify()` but works with dot-notation paths for nested access.
868 /// This method follows the DocStore staging model by checking local staged data
869 /// first, then falling back to historical data from the backend.
870 ///
871 /// # Errors
872 ///
873 /// Returns an error if:
874 /// - The path doesn't exist (`SubtreeError::KeyNotFound`)
875 /// - The value cannot be converted to type T (`CRDTError::TypeMismatch`)
876 /// - Setting the path fails (`CRDTError::InvalidPath`)
877 ///
878 /// # Examples
879 ///
880 /// ```rust,no_run
881 /// # use eidetica::Database;
882 /// # use eidetica::store::DocStore;
883 /// # use eidetica::crdt::doc::path;
884 /// # async fn example(database: Database) -> eidetica::Result<()> {
885 /// let txn = database.new_transaction().await?;
886 /// let store = txn.get_store::<DocStore>("data").await?;
887 ///
888 /// store.set_path(path!("user.score"), 100).await?;
889 ///
890 /// store.modify_path::<i64, _>(path!("user.score"), |score| {
891 /// *score += 50;
892 /// }).await?;
893 ///
894 /// assert_eq!(store.get_path_as::<i64>(path!("user.score")).await?, 150);
895 /// # Ok(())
896 /// # }
897 /// ```
898 pub async fn modify_path<T, F>(&self, path: impl AsRef<Path>, f: F) -> Result<()>
899 where
900 T: for<'a> TryFrom<&'a Value, Error = CRDTError> + Into<Value>,
901 F: FnOnce(&mut T),
902 {
903 // Try to get and convert the current value
904 let mut value = self.get_path_as(path.as_ref()).await?;
905
906 // Apply the modification
907 f(&mut value);
908
909 // Stage the modified value back
910 self.set_path(path, value).await?;
911 Ok(())
912 }
913
914 /// Modify a value at a path with string paths for runtime normalization
915 pub async fn modify_path_str<T, F>(&self, path: &str, f: F) -> Result<()>
916 where
917 T: for<'a> TryFrom<&'a Value, Error = CRDTError> + Into<Value>,
918 F: FnOnce(&mut T),
919 {
920 let pathbuf = PathBuf::from_str(path).unwrap(); // Infallible
921 self.modify_path(&pathbuf, f).await
922 }
923
924 /// Stages the deletion of a key within the associated `Transaction`.
925 ///
926 /// This method removes the key-value pair from the `Map` data held within
927 /// the `Transaction` for this `Doc` instance's subtree name. A tombstone is created,
928 /// which will propagate the deletion when merged with other data. The change is **not**
929 /// persisted to the backend until the `Transaction::commit()` method is called.
930 ///
931 /// When using the `get` method, deleted keys will return `Error::NotFound`. However,
932 /// the deletion is still tracked internally as a tombstone, which ensures that the
933 /// deletion propagates correctly when merging with other versions of the data.
934 ///
935 /// # Examples
936 /// ```rust,no_run
937 /// # use eidetica::Database;
938 /// # use eidetica::store::DocStore;
939 /// # async fn example(database: Database) -> eidetica::Result<()> {
940 /// let txn = database.new_transaction().await?;
941 /// let store = txn.get_store::<DocStore>("my_data").await?;
942 ///
943 /// // First set a value
944 /// store.set("user1", "Alice").await?;
945 ///
946 /// // Later delete the value
947 /// store.delete("user1").await?;
948 ///
949 /// // Attempting to get the deleted key will return NotFound
950 /// assert!(store.get("user1").await.is_err());
951 ///
952 /// // You can verify the tombstone exists by checking the full state
953 /// let all_data = store.get_all().await?;
954 /// assert!(all_data.is_tombstone("user1"));
955 /// # Ok(())
956 /// # }
957 /// ```
958 ///
959 /// # Arguments
960 /// * `key` - The key to delete.
961 ///
962 /// # Returns
963 /// - `Ok(true)` if the key existed and was deleted
964 /// - `Ok(false)` if the key did not exist (no-op)
965 /// - `Err` on serialization or staging errors
966 pub async fn delete(&self, key: impl AsRef<str>) -> Result<bool> {
967 let key_str = key.as_ref();
968
969 // Check if key exists in full merged state
970 let full_state = self.get_all().await?;
971 if full_state.get(key_str).is_none() {
972 return Ok(false); // Key doesn't exist, no-op
973 }
974
975 // Get current data from the transaction, or create new if not existing
976 let mut data = self.local_data()?.unwrap_or_default();
977
978 // Remove the key (creates a tombstone)
979 data.remove(key_str);
980
981 // Serialize and update the transaction
982 let serialized = serde_json::to_string(&data)?;
983 self.txn.update_subtree(&self.name, &serialized).await?;
984 Ok(true)
985 }
986
987 /// Retrieves all key-value pairs as a Doc, merging staged and historical state.
988 ///
989 /// This method combines the data staged within the current `Transaction` with the
990 /// fully merged historical state from the backend, providing a complete view
991 /// of the document as it would appear if the transaction were committed.
992 /// The staged data takes precedence in case of conflicts (overwrites).
993 ///
994 /// # Important: Understanding Nested Structure
995 ///
996 /// When using `set_path()` with dot-notation paths, the data is stored as **nested maps**.
997 /// The returned Doc will contain the top-level keys, with nested structures as `Value::Doc` values.
998 ///
999 /// ## Example:
1000 /// ```rust,no_run
1001 /// # use eidetica::Database;
1002 /// # use eidetica::store::DocStore;
1003 /// # use eidetica::crdt::doc::path;
1004 /// # use eidetica::crdt::doc::Value;
1005 /// # async fn example(database: Database) -> eidetica::Result<()> {
1006 /// let txn = database.new_transaction().await?;
1007 /// let store = txn.get_store::<DocStore>("data").await?;
1008 ///
1009 /// // Using set_path creates nested structure
1010 /// store.set_path(path!("user.name"), "Alice").await?;
1011 /// store.set_path(path!("user.age"), 30).await?;
1012 /// store.set_path(path!("config.theme"), "dark").await?;
1013 ///
1014 /// let all_data = store.get_all().await?;
1015 ///
1016 /// // The top-level map has keys "user" and "config", NOT "user.name", "user.age", etc.
1017 /// assert_eq!(all_data.len(), 2); // Only 2 top-level keys
1018 ///
1019 /// // To access nested data from get_all():
1020 /// if let Some(Value::Doc(user_node)) = all_data.get("user") {
1021 /// // user_node contains "name" and "age" as its children
1022 /// assert_eq!(user_node.get("name"), Some(&Value::Text("Alice".to_string())));
1023 /// assert_eq!(user_node.get("age"), Some(&Value::Text("30".to_string())));
1024 /// }
1025 ///
1026 /// // For direct access, use get_path() or get_path_as() instead:
1027 /// assert_eq!(store.get_path_as::<String>(path!("user.name")).await?, "Alice");
1028 /// # Ok(())
1029 /// # }
1030 /// ```
1031 ///
1032 /// # Returns
1033 /// A `Result` containing the merged `Doc` data structure with nested maps for path-based data.
1034 pub async fn get_all(&self) -> Result<Doc> {
1035 let mut data = self.txn.get_full_state::<Doc>(&self.name).await?;
1036
1037 // Merge with local staged data if any
1038 if let Some(local) = self.local_data()? {
1039 data = data.merge(&local)?;
1040 }
1041
1042 Ok(data)
1043 }
1044
1045 /// Returns true if the DocStore contains the given key
1046 ///
1047 /// This method checks both local staged data and historical backend data
1048 /// following the DocStore staging model. A key is considered to exist if:
1049 /// - It exists in local staged data (and is not deleted)
1050 /// - It exists in backend data (and is not deleted)
1051 ///
1052 /// Deleted keys (tombstones) are treated as non-existent.
1053 ///
1054 /// # Examples
1055 ///
1056 /// ```rust,no_run
1057 /// # use eidetica::Database;
1058 /// # use eidetica::store::DocStore;
1059 /// # async fn example(database: Database) -> eidetica::Result<()> {
1060 /// let txn = database.new_transaction().await?;
1061 /// let store = txn.get_store::<DocStore>("data").await?;
1062 ///
1063 /// assert!(!store.contains_key("missing").await); // Key doesn't exist
1064 ///
1065 /// store.set("name", "Alice").await?;
1066 /// assert!(store.contains_key("name").await); // Key exists in staging
1067 ///
1068 /// store.delete("name").await?;
1069 /// assert!(!store.contains_key("name").await); // Key deleted (tombstone)
1070 /// # Ok(())
1071 /// # }
1072 /// ```
1073 pub async fn contains_key(&self, key: impl AsRef<str>) -> bool {
1074 let key = key.as_ref();
1075
1076 // Check local staged data first
1077 if let Ok(Some(data)) = self.local_data()
1078 && data.contains_key(key)
1079 {
1080 return true;
1081 }
1082
1083 // Check backend data
1084 if let Ok(backend_data) = self.txn.get_full_state::<Doc>(&self.name).await {
1085 backend_data.contains_key(key)
1086 } else {
1087 false
1088 }
1089 }
1090
1091 /// Returns true if the DocStore contains the given path
1092 ///
1093 /// This method checks both local staged data and historical backend data
1094 /// following the DocStore staging model. A path is considered to exist if:
1095 /// - The complete path exists and points to a non-deleted value
1096 /// - All intermediate segments are navigable (nodes or lists)
1097 ///
1098 /// # Path Syntax
1099 ///
1100 /// Uses the same dot notation as other path methods:
1101 /// - **Docs**: Navigate by key name (e.g., "user.profile.name")
1102 /// - **Lists**: Navigate by index (e.g., "items.0.title")
1103 /// - **Mixed**: Combine both (e.g., "users.0.tags.1")
1104 ///
1105 /// # Examples
1106 ///
1107 /// ```rust,no_run
1108 /// # use eidetica::Database;
1109 /// # use eidetica::store::DocStore;
1110 /// # use eidetica::crdt::doc::path;
1111 /// # async fn example(database: Database) -> eidetica::Result<()> {
1112 /// let txn = database.new_transaction().await?;
1113 /// let store = txn.get_store::<DocStore>("data").await?;
1114 ///
1115 /// assert!(!store.contains_path(path!("user.name")).await); // Path doesn't exist
1116 ///
1117 /// store.set_path(path!("user.profile.name"), "Alice").await?;
1118 /// assert!(store.contains_path(path!("user")).await); // Intermediate path exists
1119 /// assert!(store.contains_path(path!("user.profile")).await); // Intermediate path exists
1120 /// assert!(store.contains_path(path!("user.profile.name")).await); // Full path exists
1121 /// assert!(!store.contains_path(path!("user.profile.age")).await); // Path doesn't exist
1122 /// # Ok(())
1123 /// # }
1124 /// ```
1125 pub async fn contains_path(&self, path: impl AsRef<Path>) -> bool {
1126 // Check local staged data first
1127 if let Ok(Some(data)) = self.local_data()
1128 && data.get(&path).is_some()
1129 {
1130 return true;
1131 }
1132
1133 // Check backend data
1134 if let Ok(backend_data) = self.txn.get_full_state::<Doc>(&self.name).await {
1135 backend_data.get(&path).is_some()
1136 } else {
1137 false
1138 }
1139 }
1140
1141 /// Returns true if the DocStore contains the given path with string paths for runtime normalization
1142 pub async fn contains_path_str(&self, path: &str) -> bool {
1143 let pathbuf = PathBuf::from_str(path).unwrap(); // Infallible
1144 self.contains_path(&pathbuf).await
1145 }
1146}