Development Documentation (main branch) - For stable release docs, see docs.rs/eidetica

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}