Skip to main content

mdcs_db/
document.rs

1//! Document Store - High-level API for managing CRDT documents.
2//!
3//! Provides a unified interface for:
4//! - Creating and managing documents
5//! - Path-based queries
6//! - Document versioning and snapshots
7//! - Prefix scans and queries
8
9use crate::error::DbError;
10use crate::json_crdt::{JsonCrdt, JsonCrdtDelta, JsonPath, JsonValue};
11use crate::rga_text::{RGAText, RGATextDelta};
12use crate::rich_text::{RichText, RichTextDelta};
13use mdcs_core::lattice::Lattice;
14use serde::{Deserialize, Serialize};
15use std::collections::{BTreeMap, HashMap};
16use ulid::Ulid;
17
18/// Unique identifier for a document.
19#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
20pub struct DocumentId(pub String);
21
22impl DocumentId {
23    pub fn new() -> Self {
24        Self(Ulid::new().to_string())
25    }
26
27    pub fn from_string(s: impl Into<String>) -> Self {
28        Self(s.into())
29    }
30}
31
32impl Default for DocumentId {
33    fn default() -> Self {
34        Self::new()
35    }
36}
37
38impl std::fmt::Display for DocumentId {
39    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        write!(f, "{}", self.0)
41    }
42}
43
44/// The type of a document.
45#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
46pub enum DocumentType {
47    /// Plain text document.
48    Text,
49    /// Rich text with formatting.
50    RichText,
51    /// JSON-like structured document.
52    Json,
53}
54
55/// A CRDT value that can be stored in a document.
56#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
57pub enum CrdtValue {
58    /// Plain text.
59    Text(RGAText),
60    /// Rich text with formatting.
61    RichText(RichText),
62    /// Structured JSON data.
63    Json(JsonCrdt),
64}
65
66impl CrdtValue {
67    pub fn document_type(&self) -> DocumentType {
68        match self {
69            CrdtValue::Text(_) => DocumentType::Text,
70            CrdtValue::RichText(_) => DocumentType::RichText,
71            CrdtValue::Json(_) => DocumentType::Json,
72        }
73    }
74
75    pub fn as_text(&self) -> Option<&RGAText> {
76        match self {
77            CrdtValue::Text(t) => Some(t),
78            _ => None,
79        }
80    }
81
82    pub fn as_text_mut(&mut self) -> Option<&mut RGAText> {
83        match self {
84            CrdtValue::Text(t) => Some(t),
85            _ => None,
86        }
87    }
88
89    pub fn as_rich_text(&self) -> Option<&RichText> {
90        match self {
91            CrdtValue::RichText(rt) => Some(rt),
92            _ => None,
93        }
94    }
95
96    pub fn as_rich_text_mut(&mut self) -> Option<&mut RichText> {
97        match self {
98            CrdtValue::RichText(rt) => Some(rt),
99            _ => None,
100        }
101    }
102
103    pub fn as_json(&self) -> Option<&JsonCrdt> {
104        match self {
105            CrdtValue::Json(j) => Some(j),
106            _ => None,
107        }
108    }
109
110    pub fn as_json_mut(&mut self) -> Option<&mut JsonCrdt> {
111        match self {
112            CrdtValue::Json(j) => Some(j),
113            _ => None,
114        }
115    }
116}
117
118impl Lattice for CrdtValue {
119    fn bottom() -> Self {
120        CrdtValue::Json(JsonCrdt::bottom())
121    }
122
123    fn join(&self, other: &Self) -> Self {
124        match (self, other) {
125            (CrdtValue::Text(a), CrdtValue::Text(b)) => CrdtValue::Text(a.join(b)),
126            (CrdtValue::RichText(a), CrdtValue::RichText(b)) => CrdtValue::RichText(a.join(b)),
127            (CrdtValue::Json(a), CrdtValue::Json(b)) => CrdtValue::Json(a.join(b)),
128            // Type mismatch - prefer self
129            _ => self.clone(),
130        }
131    }
132}
133
134/// Delta for document changes.
135#[derive(Clone, Debug, Serialize, Deserialize)]
136pub enum DocumentDelta {
137    Text(RGATextDelta),
138    RichText(RichTextDelta),
139    Json(JsonCrdtDelta),
140}
141
142/// A document with metadata.
143#[derive(Clone, Debug, Serialize, Deserialize)]
144pub struct Document {
145    /// Document ID.
146    pub id: DocumentId,
147    /// Document title/name.
148    pub title: String,
149    /// The CRDT value.
150    pub value: CrdtValue,
151    /// Creation timestamp.
152    pub created_at: u64,
153    /// Last modified timestamp.
154    pub modified_at: u64,
155    /// Document metadata.
156    pub metadata: HashMap<String, String>,
157}
158
159impl Document {
160    /// Create a new text document.
161    pub fn new_text(id: DocumentId, title: impl Into<String>, replica_id: &str) -> Self {
162        let now = std::time::SystemTime::now()
163            .duration_since(std::time::UNIX_EPOCH)
164            .unwrap_or_default()
165            .as_millis() as u64;
166
167        Self {
168            id,
169            title: title.into(),
170            value: CrdtValue::Text(RGAText::new(replica_id)),
171            created_at: now,
172            modified_at: now,
173            metadata: HashMap::new(),
174        }
175    }
176
177    /// Create a new rich text document.
178    pub fn new_rich_text(id: DocumentId, title: impl Into<String>, replica_id: &str) -> Self {
179        let now = std::time::SystemTime::now()
180            .duration_since(std::time::UNIX_EPOCH)
181            .unwrap_or_default()
182            .as_millis() as u64;
183
184        Self {
185            id,
186            title: title.into(),
187            value: CrdtValue::RichText(RichText::new(replica_id)),
188            created_at: now,
189            modified_at: now,
190            metadata: HashMap::new(),
191        }
192    }
193
194    /// Create a new JSON document.
195    pub fn new_json(id: DocumentId, title: impl Into<String>, replica_id: &str) -> Self {
196        let now = std::time::SystemTime::now()
197            .duration_since(std::time::UNIX_EPOCH)
198            .unwrap_or_default()
199            .as_millis() as u64;
200
201        Self {
202            id,
203            title: title.into(),
204            value: CrdtValue::Json(JsonCrdt::new(replica_id)),
205            created_at: now,
206            modified_at: now,
207            metadata: HashMap::new(),
208        }
209    }
210
211    /// Get the document type.
212    pub fn document_type(&self) -> DocumentType {
213        self.value.document_type()
214    }
215
216    /// Touch the modified timestamp.
217    pub fn touch(&mut self) {
218        self.modified_at = std::time::SystemTime::now()
219            .duration_since(std::time::UNIX_EPOCH)
220            .unwrap_or_default()
221            .as_millis() as u64;
222    }
223
224    /// Set metadata.
225    pub fn set_metadata(&mut self, key: impl Into<String>, value: impl Into<String>) {
226        self.metadata.insert(key.into(), value.into());
227    }
228
229    /// Get metadata.
230    pub fn get_metadata(&self, key: &str) -> Option<&String> {
231        self.metadata.get(key)
232    }
233}
234
235/// Options for querying documents.
236#[derive(Clone, Debug, Default)]
237pub struct QueryOptions {
238    /// Filter by document type.
239    pub document_type: Option<DocumentType>,
240    /// Filter by title prefix.
241    pub title_prefix: Option<String>,
242    /// Sort by field.
243    pub sort_by: Option<SortField>,
244    /// Sort direction.
245    pub sort_desc: bool,
246    /// Limit results.
247    pub limit: Option<usize>,
248    /// Skip results.
249    pub offset: Option<usize>,
250}
251
252#[derive(Clone, Debug)]
253pub enum SortField {
254    Title,
255    CreatedAt,
256    ModifiedAt,
257}
258
259/// A document store for managing multiple CRDT documents.
260#[derive(Clone, Debug)]
261pub struct DocumentStore {
262    /// The replica ID for this store.
263    replica_id: String,
264    /// All documents indexed by ID.
265    documents: BTreeMap<DocumentId, Document>,
266    /// Index by title for prefix queries.
267    title_index: BTreeMap<String, DocumentId>,
268    /// Pending changes for replication.
269    pending_changes: Vec<StoreChange>,
270}
271
272/// A change to the store.
273#[derive(Clone, Debug, Serialize, Deserialize)]
274pub enum StoreChange {
275    /// A new document was created.
276    Create {
277        id: DocumentId,
278        doc_type: DocumentType,
279        title: String,
280    },
281    /// A document was updated.
282    Update {
283        id: DocumentId,
284        delta: DocumentDelta,
285    },
286    /// A document was deleted.
287    Delete { id: DocumentId },
288    /// Document metadata changed.
289    MetadataChange {
290        id: DocumentId,
291        key: String,
292        value: Option<String>,
293    },
294}
295
296impl DocumentStore {
297    /// Create a new document store.
298    pub fn new(replica_id: impl Into<String>) -> Self {
299        Self {
300            replica_id: replica_id.into(),
301            documents: BTreeMap::new(),
302            title_index: BTreeMap::new(),
303            pending_changes: Vec::new(),
304        }
305    }
306
307    /// Get the replica ID.
308    pub fn replica_id(&self) -> &str {
309        &self.replica_id
310    }
311
312    // === Document CRUD ===
313
314    /// Create a new text document.
315    pub fn create_text(&mut self, title: impl Into<String>) -> DocumentId {
316        let id = DocumentId::new();
317        let title = title.into();
318        let doc = Document::new_text(id.clone(), &title, &self.replica_id);
319
320        self.title_index.insert(title.clone(), id.clone());
321        self.documents.insert(id.clone(), doc);
322
323        self.pending_changes.push(StoreChange::Create {
324            id: id.clone(),
325            doc_type: DocumentType::Text,
326            title,
327        });
328
329        id
330    }
331
332    /// Create a new rich text document.
333    pub fn create_rich_text(&mut self, title: impl Into<String>) -> DocumentId {
334        let id = DocumentId::new();
335        let title = title.into();
336        let doc = Document::new_rich_text(id.clone(), &title, &self.replica_id);
337
338        self.title_index.insert(title.clone(), id.clone());
339        self.documents.insert(id.clone(), doc);
340
341        self.pending_changes.push(StoreChange::Create {
342            id: id.clone(),
343            doc_type: DocumentType::RichText,
344            title,
345        });
346
347        id
348    }
349
350    /// Create a new JSON document.
351    pub fn create_json(&mut self, title: impl Into<String>) -> DocumentId {
352        let id = DocumentId::new();
353        let title = title.into();
354        let doc = Document::new_json(id.clone(), &title, &self.replica_id);
355
356        self.title_index.insert(title.clone(), id.clone());
357        self.documents.insert(id.clone(), doc);
358
359        self.pending_changes.push(StoreChange::Create {
360            id: id.clone(),
361            doc_type: DocumentType::Json,
362            title,
363        });
364
365        id
366    }
367
368    /// Get a document by ID.
369    pub fn get(&self, id: &DocumentId) -> Option<&Document> {
370        self.documents.get(id)
371    }
372
373    /// Get a mutable document by ID.
374    pub fn get_mut(&mut self, id: &DocumentId) -> Option<&mut Document> {
375        self.documents.get_mut(id)
376    }
377
378    /// Delete a document.
379    pub fn delete(&mut self, id: &DocumentId) -> Option<Document> {
380        if let Some(doc) = self.documents.remove(id) {
381            self.title_index.remove(&doc.title);
382            self.pending_changes
383                .push(StoreChange::Delete { id: id.clone() });
384            Some(doc)
385        } else {
386            None
387        }
388    }
389
390    /// Check if a document exists.
391    pub fn contains(&self, id: &DocumentId) -> bool {
392        self.documents.contains_key(id)
393    }
394
395    /// Get the number of documents.
396    pub fn len(&self) -> usize {
397        self.documents.len()
398    }
399
400    /// Check if the store is empty.
401    pub fn is_empty(&self) -> bool {
402        self.documents.is_empty()
403    }
404
405    // === Text Operations ===
406
407    /// Insert text into a text document.
408    pub fn text_insert(
409        &mut self,
410        id: &DocumentId,
411        position: usize,
412        text: &str,
413    ) -> Result<(), DbError> {
414        let doc = self
415            .documents
416            .get_mut(id)
417            .ok_or_else(|| DbError::DocumentNotFound(id.to_string()))?;
418
419        let doc_type = doc.value.document_type();
420        let rga_text = doc.value.as_text_mut().ok_or(DbError::TypeMismatch {
421            expected: "Text".to_string(),
422            found: format!("{:?}", doc_type),
423        })?;
424
425        rga_text.insert(position, text);
426        let delta = rga_text.take_delta();
427        doc.touch();
428
429        if let Some(delta) = delta {
430            self.pending_changes.push(StoreChange::Update {
431                id: id.clone(),
432                delta: DocumentDelta::Text(delta),
433            });
434        }
435
436        Ok(())
437    }
438
439    /// Delete text from a text document.
440    pub fn text_delete(
441        &mut self,
442        id: &DocumentId,
443        start: usize,
444        length: usize,
445    ) -> Result<(), DbError> {
446        let doc = self
447            .documents
448            .get_mut(id)
449            .ok_or_else(|| DbError::DocumentNotFound(id.to_string()))?;
450
451        let doc_type = doc.value.document_type();
452        let rga_text = doc.value.as_text_mut().ok_or(DbError::TypeMismatch {
453            expected: "Text".to_string(),
454            found: format!("{:?}", doc_type),
455        })?;
456
457        rga_text.delete(start, length);
458        let delta = rga_text.take_delta();
459        doc.touch();
460
461        if let Some(delta) = delta {
462            self.pending_changes.push(StoreChange::Update {
463                id: id.clone(),
464                delta: DocumentDelta::Text(delta),
465            });
466        }
467
468        Ok(())
469    }
470
471    /// Get text content.
472    pub fn text_content(&self, id: &DocumentId) -> Result<String, DbError> {
473        let doc = self
474            .documents
475            .get(id)
476            .ok_or_else(|| DbError::DocumentNotFound(id.to_string()))?;
477
478        let rga_text = doc.value.as_text().ok_or(DbError::TypeMismatch {
479            expected: "Text".to_string(),
480            found: format!("{:?}", doc.value.document_type()),
481        })?;
482
483        Ok(rga_text.to_string())
484    }
485
486    // === Rich Text Operations ===
487
488    /// Insert text into a rich text document.
489    pub fn rich_text_insert(
490        &mut self,
491        id: &DocumentId,
492        position: usize,
493        text: &str,
494    ) -> Result<(), DbError> {
495        let doc = self
496            .documents
497            .get_mut(id)
498            .ok_or_else(|| DbError::DocumentNotFound(id.to_string()))?;
499
500        let doc_type = doc.value.document_type();
501        let rich_text = doc.value.as_rich_text_mut().ok_or(DbError::TypeMismatch {
502            expected: "RichText".to_string(),
503            found: format!("{:?}", doc_type),
504        })?;
505
506        rich_text.insert(position, text);
507        let delta = rich_text.take_delta();
508        doc.touch();
509
510        if let Some(delta) = delta {
511            self.pending_changes.push(StoreChange::Update {
512                id: id.clone(),
513                delta: DocumentDelta::RichText(delta),
514            });
515        }
516
517        Ok(())
518    }
519
520    /// Apply bold formatting.
521    pub fn rich_text_bold(
522        &mut self,
523        id: &DocumentId,
524        start: usize,
525        end: usize,
526    ) -> Result<(), DbError> {
527        let doc = self
528            .documents
529            .get_mut(id)
530            .ok_or_else(|| DbError::DocumentNotFound(id.to_string()))?;
531
532        let doc_type = doc.value.document_type();
533        let rich_text = doc.value.as_rich_text_mut().ok_or(DbError::TypeMismatch {
534            expected: "RichText".to_string(),
535            found: format!("{:?}", doc_type),
536        })?;
537
538        rich_text.bold(start, end);
539        let delta = rich_text.take_delta();
540        doc.touch();
541
542        if let Some(delta) = delta {
543            self.pending_changes.push(StoreChange::Update {
544                id: id.clone(),
545                delta: DocumentDelta::RichText(delta),
546            });
547        }
548
549        Ok(())
550    }
551
552    /// Apply italic formatting.
553    pub fn rich_text_italic(
554        &mut self,
555        id: &DocumentId,
556        start: usize,
557        end: usize,
558    ) -> Result<(), DbError> {
559        let doc = self
560            .documents
561            .get_mut(id)
562            .ok_or_else(|| DbError::DocumentNotFound(id.to_string()))?;
563
564        let doc_type = doc.value.document_type();
565        let rich_text = doc.value.as_rich_text_mut().ok_or(DbError::TypeMismatch {
566            expected: "RichText".to_string(),
567            found: format!("{:?}", doc_type),
568        })?;
569
570        rich_text.italic(start, end);
571        let delta = rich_text.take_delta();
572        doc.touch();
573
574        if let Some(delta) = delta {
575            self.pending_changes.push(StoreChange::Update {
576                id: id.clone(),
577                delta: DocumentDelta::RichText(delta),
578            });
579        }
580
581        Ok(())
582    }
583
584    /// Get rich text as HTML.
585    pub fn rich_text_html(&self, id: &DocumentId) -> Result<String, DbError> {
586        let doc = self
587            .documents
588            .get(id)
589            .ok_or_else(|| DbError::DocumentNotFound(id.to_string()))?;
590
591        let doc_type = doc.value.document_type();
592        let rich_text = doc.value.as_rich_text().ok_or(DbError::TypeMismatch {
593            expected: "RichText".to_string(),
594            found: format!("{:?}", doc_type),
595        })?;
596
597        Ok(rich_text.to_html())
598    }
599
600    // === JSON Operations ===
601
602    /// Set a value in a JSON document.
603    pub fn json_set(
604        &mut self,
605        id: &DocumentId,
606        path: &str,
607        value: JsonValue,
608    ) -> Result<(), DbError> {
609        let doc = self
610            .documents
611            .get_mut(id)
612            .ok_or_else(|| DbError::DocumentNotFound(id.to_string()))?;
613
614        let doc_type = doc.value.document_type();
615        let json = doc.value.as_json_mut().ok_or(DbError::TypeMismatch {
616            expected: "Json".to_string(),
617            found: format!("{:?}", doc_type),
618        })?;
619
620        json.set(&JsonPath::parse(path), value)?;
621        let delta = json.take_delta();
622        doc.touch();
623
624        if let Some(delta) = delta {
625            self.pending_changes.push(StoreChange::Update {
626                id: id.clone(),
627                delta: DocumentDelta::Json(delta),
628            });
629        }
630
631        Ok(())
632    }
633
634    /// Get a value from a JSON document.
635    pub fn json_get(&self, id: &DocumentId, path: &str) -> Result<Option<&JsonValue>, DbError> {
636        let doc = self
637            .documents
638            .get(id)
639            .ok_or_else(|| DbError::DocumentNotFound(id.to_string()))?;
640
641        let doc_type = doc.value.document_type();
642        let json = doc.value.as_json().ok_or(DbError::TypeMismatch {
643            expected: "Json".to_string(),
644            found: format!("{:?}", doc_type),
645        })?;
646
647        Ok(json.get(&JsonPath::parse(path)))
648    }
649
650    /// Get JSON document as serde_json::Value.
651    pub fn json_to_value(&self, id: &DocumentId) -> Result<serde_json::Value, DbError> {
652        let doc = self
653            .documents
654            .get(id)
655            .ok_or_else(|| DbError::DocumentNotFound(id.to_string()))?;
656
657        let json = doc.value.as_json().ok_or(DbError::TypeMismatch {
658            expected: "Json".to_string(),
659            found: format!("{:?}", doc.value.document_type()),
660        })?;
661
662        Ok(json.to_json())
663    }
664
665    // === Query Operations ===
666
667    /// Find a document by title.
668    pub fn find_by_title(&self, title: &str) -> Option<&Document> {
669        self.title_index
670            .get(title)
671            .and_then(|id| self.documents.get(id))
672    }
673
674    /// List all documents.
675    pub fn list(&self) -> Vec<&Document> {
676        self.documents.values().collect()
677    }
678
679    /// Query documents with options.
680    pub fn query(&self, options: &QueryOptions) -> Vec<&Document> {
681        let mut results: Vec<_> = self
682            .documents
683            .values()
684            .filter(|doc| {
685                // Type filter
686                if let Some(ref doc_type) = options.document_type {
687                    if &doc.document_type() != doc_type {
688                        return false;
689                    }
690                }
691                // Title prefix filter
692                if let Some(ref prefix) = options.title_prefix {
693                    if !doc.title.starts_with(prefix) {
694                        return false;
695                    }
696                }
697                true
698            })
699            .collect();
700
701        // Sort
702        if let Some(ref sort_by) = options.sort_by {
703            match sort_by {
704                SortField::Title => {
705                    results.sort_by(|a, b| a.title.cmp(&b.title));
706                }
707                SortField::CreatedAt => {
708                    results.sort_by(|a, b| a.created_at.cmp(&b.created_at));
709                }
710                SortField::ModifiedAt => {
711                    results.sort_by(|a, b| a.modified_at.cmp(&b.modified_at));
712                }
713            }
714            if options.sort_desc {
715                results.reverse();
716            }
717        }
718
719        // Pagination
720        if let Some(offset) = options.offset {
721            results = results.into_iter().skip(offset).collect();
722        }
723        if let Some(limit) = options.limit {
724            results.truncate(limit);
725        }
726
727        results
728    }
729
730    /// Prefix scan for titles.
731    pub fn scan_prefix(&self, prefix: &str) -> Vec<&Document> {
732        self.title_index
733            .range(prefix.to_string()..)
734            .take_while(|(k, _)| k.starts_with(prefix))
735            .filter_map(|(_, id)| self.documents.get(id))
736            .collect()
737    }
738
739    // === Replication ===
740
741    /// Take pending changes for replication.
742    pub fn take_changes(&mut self) -> Vec<StoreChange> {
743        std::mem::take(&mut self.pending_changes)
744    }
745
746    /// Apply changes from another replica.
747    pub fn apply_changes(&mut self, changes: &[StoreChange]) {
748        for change in changes {
749            match change {
750                StoreChange::Create {
751                    id,
752                    doc_type,
753                    title,
754                } => {
755                    if !self.documents.contains_key(id) {
756                        let doc = match doc_type {
757                            DocumentType::Text => {
758                                Document::new_text(id.clone(), title, &self.replica_id)
759                            }
760                            DocumentType::RichText => {
761                                Document::new_rich_text(id.clone(), title, &self.replica_id)
762                            }
763                            DocumentType::Json => {
764                                Document::new_json(id.clone(), title, &self.replica_id)
765                            }
766                        };
767                        self.title_index.insert(title.clone(), id.clone());
768                        self.documents.insert(id.clone(), doc);
769                    }
770                }
771                StoreChange::Update { id, delta } => {
772                    if let Some(doc) = self.documents.get_mut(id) {
773                        match (delta, &mut doc.value) {
774                            (DocumentDelta::Text(d), CrdtValue::Text(t)) => {
775                                t.apply_delta(d);
776                            }
777                            (DocumentDelta::RichText(d), CrdtValue::RichText(rt)) => {
778                                rt.apply_delta(d);
779                            }
780                            (DocumentDelta::Json(d), CrdtValue::Json(j)) => {
781                                j.apply_delta(d);
782                            }
783                            _ => {} // Type mismatch, ignore
784                        }
785                        doc.touch();
786                    }
787                }
788                StoreChange::Delete { id } => {
789                    if let Some(doc) = self.documents.remove(id) {
790                        self.title_index.remove(&doc.title);
791                    }
792                }
793                StoreChange::MetadataChange { id, key, value } => {
794                    if let Some(doc) = self.documents.get_mut(id) {
795                        match value {
796                            Some(v) => {
797                                doc.metadata.insert(key.clone(), v.clone());
798                            }
799                            None => {
800                                doc.metadata.remove(key);
801                            }
802                        }
803                    }
804                }
805            }
806        }
807    }
808
809    /// Get all document IDs.
810    pub fn document_ids(&self) -> impl Iterator<Item = &DocumentId> + '_ {
811        self.documents.keys()
812    }
813}
814
815#[cfg(test)]
816mod tests {
817    use super::*;
818
819    #[test]
820    fn test_create_documents() {
821        let mut store = DocumentStore::new("r1");
822
823        let text_id = store.create_text("My Text");
824        let rich_id = store.create_rich_text("My Rich Text");
825        let json_id = store.create_json("My JSON");
826
827        assert_eq!(store.len(), 3);
828        assert!(store.contains(&text_id));
829        assert!(store.contains(&rich_id));
830        assert!(store.contains(&json_id));
831    }
832
833    #[test]
834    fn test_text_operations() {
835        let mut store = DocumentStore::new("r1");
836        let id = store.create_text("Test");
837
838        store.text_insert(&id, 0, "Hello").unwrap();
839        store.text_insert(&id, 5, " World").unwrap();
840
841        let content = store.text_content(&id).unwrap();
842        assert_eq!(content, "Hello World");
843
844        store.text_delete(&id, 5, 6).unwrap();
845        let content = store.text_content(&id).unwrap();
846        assert_eq!(content, "Hello");
847    }
848
849    #[test]
850    fn test_json_operations() {
851        let mut store = DocumentStore::new("r1");
852        let id = store.create_json("Config");
853
854        store
855            .json_set(&id, "name", JsonValue::String("Test".to_string()))
856            .unwrap();
857        store.json_set(&id, "count", JsonValue::Int(42)).unwrap();
858
859        let name = store.json_get(&id, "name").unwrap();
860        assert_eq!(name.unwrap().as_str(), Some("Test"));
861
862        let json = store.json_to_value(&id).unwrap();
863        assert_eq!(json["name"], "Test");
864        assert_eq!(json["count"], 42);
865    }
866
867    #[test]
868    fn test_find_by_title() {
869        let mut store = DocumentStore::new("r1");
870
871        store.create_text("Document A");
872        store.create_text("Document B");
873        store.create_text("Other");
874
875        let doc = store.find_by_title("Document A").unwrap();
876        assert_eq!(doc.title, "Document A");
877
878        assert!(store.find_by_title("Not Found").is_none());
879    }
880
881    #[test]
882    fn test_query() {
883        let mut store = DocumentStore::new("r1");
884
885        store.create_text("Text 1");
886        store.create_text("Text 2");
887        store.create_json("Json 1");
888
889        let options = QueryOptions {
890            document_type: Some(DocumentType::Text),
891            ..Default::default()
892        };
893
894        let results = store.query(&options);
895        assert_eq!(results.len(), 2);
896    }
897
898    #[test]
899    fn test_prefix_scan() {
900        let mut store = DocumentStore::new("r1");
901
902        store.create_text("project/doc1");
903        store.create_text("project/doc2");
904        store.create_text("other/doc1");
905
906        let results = store.scan_prefix("project/");
907        assert_eq!(results.len(), 2);
908    }
909
910    #[test]
911    fn test_delete() {
912        let mut store = DocumentStore::new("r1");
913
914        let id = store.create_text("To Delete");
915        assert!(store.contains(&id));
916
917        store.delete(&id);
918        assert!(!store.contains(&id));
919    }
920
921    #[test]
922    fn test_replication() {
923        let mut store1 = DocumentStore::new("r1");
924        let mut store2 = DocumentStore::new("r2");
925
926        // Create on store1
927        let id = store1.create_text("Shared Doc");
928        store1.text_insert(&id, 0, "Hello").unwrap();
929
930        // Replicate to store2
931        let changes = store1.take_changes();
932        store2.apply_changes(&changes);
933
934        // Verify
935        assert!(store2.contains(&id));
936        let content = store2.text_content(&id).unwrap();
937        assert_eq!(content, "Hello");
938    }
939
940    #[test]
941    fn test_metadata() {
942        let mut store = DocumentStore::new("r1");
943        let id = store.create_text("With Metadata");
944
945        let doc = store.get_mut(&id).unwrap();
946        doc.set_metadata("author", "Alice");
947        doc.set_metadata("version", "1.0");
948
949        let doc = store.get(&id).unwrap();
950        assert_eq!(doc.get_metadata("author"), Some(&"Alice".to_string()));
951        assert_eq!(doc.get_metadata("version"), Some(&"1.0".to_string()));
952    }
953}