Skip to main content

storage_kit/core/
data.rs

1use std::collections::{BTreeSet, HashSet};
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6use crate::{DataRecord, FilterExpr, StorageError, StorageLocator};
7
8use super::{
9    ApplyDefaultLocator, HasLocator,
10    query::{PageToken, Projection},
11};
12
13/// Behavior to apply when writing rows to an existing collection.
14///
15/// # Examples
16///
17/// ```
18/// let mode = storage_kit::database::data::WriteMode::Append;
19/// assert!(matches!(mode, storage_kit::database::data::WriteMode::Append));
20/// ```
21#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
22pub enum WriteMode {
23    /// Append new rows to the existing collection.
24    Append,
25    /// Replace all collection records, including clearing an empty replacement.
26    ///
27    /// Preserves the collection schema and indexes. Catalog overwrite instead
28    /// recreates the collection. Ordinary Database writes do not provide
29    /// rollback across multiple native requests; a failure can leave an
30    /// uncertain or partial outcome.
31    Overwrite,
32}
33
34/// Request to insert records.
35///
36/// # Examples
37///
38/// ```
39/// let request = storage_kit::database::data::InsertRequest {
40///     locator: storage_kit::database::StorageLocator::new("documents"),
41///     records: vec![storage_kit::database::data::DataRecord::default()],
42///     mode: storage_kit::database::data::WriteMode::Append,
43/// };
44/// assert_eq!(request.records.len(), 1);
45/// ```
46#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
47pub struct InsertRequest {
48    /// Collection that should receive the inserted records.
49    pub locator: StorageLocator,
50    /// Records to write.
51    pub records: Vec<DataRecord>,
52    /// Write behavior to apply.
53    pub mode: WriteMode,
54}
55
56impl HasLocator for InsertRequest {
57    fn locator_mut(&mut self) -> &mut StorageLocator {
58        &mut self.locator
59    }
60}
61
62impl InsertRequest {
63    /// Creates an append insert request.
64    ///
65    /// # Examples
66    ///
67    /// ```
68    /// let request = storage_kit::database::data::InsertRequest::append(
69    ///     storage_kit::database::StorageLocator::new("documents"),
70    ///     vec![storage_kit::database::data::DataRecord::without_id()],
71    /// );
72    /// assert!(matches!(request.mode, storage_kit::database::data::WriteMode::Append));
73    /// ```
74    #[must_use]
75    pub fn append(locator: StorageLocator, records: Vec<DataRecord>) -> Self {
76        Self {
77            locator,
78            records,
79            mode: WriteMode::Append,
80        }
81    }
82
83    /// Creates an overwrite insert request.
84    ///
85    /// The complete supplied set replaces existing records; an empty set clears
86    /// them while preserving schema and indexes. This constructor only builds
87    /// the request. Execution can reject unsupported combinations before
88    /// mutation, and native execution failures may leave an uncertain outcome.
89    ///
90    /// # Examples
91    ///
92    /// ```
93    /// let request = storage_kit::database::data::InsertRequest::overwrite(
94    ///     storage_kit::database::StorageLocator::new("documents"),
95    ///     vec![storage_kit::database::data::DataRecord::without_id()],
96    /// );
97    /// assert!(matches!(request.mode, storage_kit::database::data::WriteMode::Overwrite));
98    /// ```
99    #[must_use]
100    pub fn overwrite(locator: StorageLocator, records: Vec<DataRecord>) -> Self {
101        Self {
102            locator,
103            records,
104            mode: WriteMode::Overwrite,
105        }
106    }
107}
108
109/// Request to scan a collection with optional filtering and pagination.
110///
111/// # Examples
112///
113/// ```
114/// let request = storage_kit::database::data::ScanRequest {
115///     locator: storage_kit::database::StorageLocator::new("documents"),
116///     filter: None,
117///     projection: storage_kit::database::query::Projection::All,
118///     limit: Some(10),
119///     offset: None,
120///     page_token: None,
121/// };
122/// assert_eq!(request.limit, Some(10));
123/// ```
124#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
125pub struct ScanRequest {
126    /// Collection that should be scanned.
127    pub locator: StorageLocator,
128    /// Filter predicate used when the scan should exclude some rows.
129    pub filter: Option<FilterExpr>,
130    /// Projection to apply to returned records.
131    pub projection: Projection,
132    /// Page size to request when the backend supports paginated scans.
133    pub limit: Option<usize>,
134    /// Row offset to apply when the backend supports offset-based scans.
135    pub offset: Option<usize>,
136    /// Continuation token returned by a previous scan page.
137    ///
138    /// Return it unchanged with the same applicable scope; do not construct a
139    /// numeric offset by inspecting a token from another backend or query.
140    pub page_token: Option<PageToken>,
141}
142
143impl HasLocator for ScanRequest {
144    fn locator_mut(&mut self) -> &mut StorageLocator {
145        &mut self.locator
146    }
147}
148
149impl ScanRequest {
150    /// Creates a scan request with all fields projected.
151    ///
152    /// # Examples
153    ///
154    /// ```
155    /// let request = storage_kit::database::data::ScanRequest::new(storage_kit::database::StorageLocator::new("documents"));
156    /// assert_eq!(request.limit, None);
157    /// ```
158    #[must_use]
159    pub fn new(locator: StorageLocator) -> Self {
160        Self {
161            locator,
162            filter: None,
163            projection: Projection::All,
164            limit: None,
165            offset: None,
166            page_token: None,
167        }
168    }
169
170    /// Applies a filter.
171    ///
172    /// # Examples
173    ///
174    /// ```
175    /// let filter = storage_kit::database::data::FilterExpr::Compare {
176    ///     field: "kind".to_owned(),
177    ///     op: storage_kit::database::data::ComparisonOp::Eq,
178    ///     value: serde_json::json!("note"),
179    /// };
180    /// let request = storage_kit::database::data::ScanRequest::new(storage_kit::database::StorageLocator::new("documents"))
181    ///     .filter(filter);
182    /// assert!(request.filter.is_some());
183    /// ```
184    #[must_use]
185    pub fn filter(mut self, filter: FilterExpr) -> Self {
186        self.filter = Some(filter);
187        self
188    }
189
190    /// Replaces the projection.
191    ///
192    /// # Examples
193    ///
194    /// ```
195    /// let request = storage_kit::database::data::ScanRequest::new(storage_kit::database::StorageLocator::new("documents"))
196    ///     .projection(storage_kit::database::query::Projection::Columns(vec!["title".to_owned()]));
197    /// assert!(matches!(request.projection, storage_kit::database::query::Projection::Columns(_)));
198    /// ```
199    #[must_use]
200    pub fn projection(mut self, projection: Projection) -> Self {
201        self.projection = projection;
202        self
203    }
204
205    /// Applies a result limit.
206    ///
207    /// # Examples
208    ///
209    /// ```
210    /// let request = storage_kit::database::data::ScanRequest::new(storage_kit::database::StorageLocator::new("documents"))
211    ///     .limit(5);
212    /// assert_eq!(request.limit, Some(5));
213    /// ```
214    #[must_use]
215    pub fn limit(mut self, limit: usize) -> Self {
216        self.limit = Some(limit);
217        self
218    }
219
220    /// Applies an offset.
221    ///
222    /// # Examples
223    ///
224    /// ```
225    /// let request = storage_kit::database::data::ScanRequest::new(storage_kit::database::StorageLocator::new("documents"))
226    ///     .offset(10);
227    /// assert_eq!(request.offset, Some(10));
228    /// ```
229    #[must_use]
230    pub fn offset(mut self, offset: usize) -> Self {
231        self.offset = Some(offset);
232        self
233    }
234
235    /// Applies a continuation page token.
236    ///
237    /// # Examples
238    ///
239    /// ```
240    /// let request = storage_kit::database::data::ScanRequest::new(storage_kit::database::StorageLocator::new("documents"))
241    ///     .page_token("page-2");
242    /// assert_eq!(request.page_token.as_deref(), Some("page-2"));
243    /// ```
244    #[must_use]
245    pub fn page_token(mut self, page_token: impl Into<PageToken>) -> Self {
246        self.page_token = Some(page_token.into());
247        self
248    }
249
250    pub(crate) fn effective_offset(&self) -> Result<Option<usize>, StorageError> {
251        PageToken::effective_offset(self.page_token.as_deref(), self.offset)
252    }
253}
254
255/// Single assignment used by update-style mutations.
256///
257/// # Examples
258///
259/// ```
260/// let assignment = storage_kit::database::data::UpdateAssignment {
261///     column: "status".into(),
262///     value: serde_json::json!("published"),
263/// };
264/// assert_eq!(assignment.value, serde_json::json!("published"));
265/// ```
266#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
267pub struct UpdateAssignment {
268    /// Column name to assign.
269    pub column: String,
270    /// JSON literal assigned to the column.
271    pub value: Value,
272}
273
274/// Request to update rows that match a filter.
275///
276/// # Examples
277///
278/// ```
279/// let request = storage_kit::database::data::UpdateRequest {
280///     locator: storage_kit::database::StorageLocator::new("documents"),
281///     filter: Some(storage_kit::database::data::FilterExpr::eq("id", "doc-1")),
282///     assignments: vec![storage_kit::database::data::UpdateAssignment {
283///         column: "status".into(),
284///         value: serde_json::json!("published"),
285///     }],
286/// };
287/// assert_eq!(request.assignments.len(), 1);
288/// ```
289#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
290pub struct UpdateRequest {
291    /// Collection whose rows should be updated.
292    pub locator: StorageLocator,
293    /// Filter predicate used when the update should only affect matching rows.
294    pub filter: Option<FilterExpr>,
295    /// Assignments to apply.
296    pub assignments: Vec<UpdateAssignment>,
297}
298
299impl HasLocator for UpdateRequest {
300    fn locator_mut(&mut self) -> &mut StorageLocator {
301        &mut self.locator
302    }
303}
304
305/// Request to delete rows that match a filter.
306///
307/// # Examples
308///
309/// ```
310/// let request = storage_kit::database::data::DeleteRequest {
311///     locator: storage_kit::database::StorageLocator::new("documents"),
312///     filter: storage_kit::database::data::FilterExpr::eq("status", "archived"),
313/// };
314/// assert_eq!(request.locator.collection, "documents");
315/// ```
316#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
317pub struct DeleteRequest {
318    /// Collection whose rows should be deleted.
319    pub locator: StorageLocator,
320    /// Filter predicate that selects rows to delete.
321    pub filter: FilterExpr,
322}
323
324impl HasLocator for DeleteRequest {
325    fn locator_mut(&mut self) -> &mut StorageLocator {
326        &mut self.locator
327    }
328}
329
330/// Atomically replaces all records selected by one bounded partition.
331///
332/// The identity field is a collection-global unique scalar payload field used
333/// to match replacement records to existing rows. Partition fields and the
334/// identity field must be ordinary payload fields rather than the reserved
335/// `id` field.
336///
337/// # Examples
338///
339/// ```
340/// let record = storage_kit::database::data::DataRecord::without_id()
341///     .json_field("tenant", serde_json::json!("acme"))
342///     .json_field("record_key", serde_json::json!("chunk-0"));
343/// let request = storage_kit::database::data::ReplacePartitionRequest::new(
344///     storage_kit::database::StorageLocator::new("managed_knowledge"),
345///     storage_kit::database::data::FilterExpr::eq("tenant", "acme"),
346///     "record_key",
347///     vec![record],
348/// )?;
349/// assert_eq!(request.records.len(), 1);
350/// # Ok::<(), storage_kit::StorageError>(())
351/// ```
352#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
353pub struct ReplacePartitionRequest {
354    /// Collection whose selected partition should be replaced.
355    pub locator: StorageLocator,
356    /// Bounded equality predicate selecting the complete target partition.
357    pub partition: FilterExpr,
358    /// Collection-global unique scalar payload field used to match records.
359    pub identity_field: String,
360    /// Complete replacement set for the selected partition.
361    pub records: Vec<DataRecord>,
362}
363
364impl HasLocator for ReplacePartitionRequest {
365    fn locator_mut(&mut self) -> &mut StorageLocator {
366        &mut self.locator
367    }
368}
369
370impl ReplacePartitionRequest {
371    /// Creates and validates an atomic partition replacement request.
372    ///
373    /// # Examples
374    ///
375    /// ```
376    /// let request = storage_kit::database::data::ReplacePartitionRequest::new(
377    ///     storage_kit::database::StorageLocator::new("documents"),
378    ///     storage_kit::database::data::FilterExpr::eq("tenant", "acme"),
379    ///     "record_key",
380    ///     Vec::new(),
381    /// )?;
382    /// assert!(request.records.is_empty());
383    /// # Ok::<(), storage_kit::StorageError>(())
384    /// ```
385    ///
386    /// # Errors
387    ///
388    /// Returns [`StorageError::InvalidRequest`] when the partition, identity
389    /// field, or replacement records do not satisfy the bounded replacement
390    /// contract.
391    pub fn new(
392        locator: StorageLocator,
393        partition: FilterExpr,
394        identity_field: impl Into<String>,
395        records: Vec<DataRecord>,
396    ) -> Result<Self, StorageError> {
397        let request = Self {
398            locator,
399            partition,
400            identity_field: identity_field.into(),
401            records,
402        };
403        request.validate()?;
404        Ok(request)
405    }
406
407    /// Validates the bounded partition and complete replacement record set.
408    ///
409    /// This method is also applied after default-locator normalization at the
410    /// common database boundary, so direct construction and deserialization
411    /// cannot bypass validation before backend I/O.
412    ///
413    /// # Examples
414    ///
415    /// ```
416    /// let request = storage_kit::database::data::ReplacePartitionRequest::new(
417    ///     storage_kit::database::StorageLocator::new("documents"),
418    ///     storage_kit::database::data::FilterExpr::eq("tenant", "acme"),
419    ///     "record_key",
420    ///     Vec::new(),
421    /// )?;
422    /// request.validate()?;
423    /// # Ok::<(), storage_kit::StorageError>(())
424    /// ```
425    ///
426    /// # Errors
427    ///
428    /// Returns [`StorageError::InvalidRequest`] for an unbounded or nested
429    /// predicate, invalid or reserved field names, non-scalar values,
430    /// mismatched records, or duplicate replacement identities.
431    pub fn validate(&self) -> Result<(), StorageError> {
432        let mut partition_fields = BTreeSet::new();
433        match &self.partition {
434            FilterExpr::Compare { .. } | FilterExpr::In { .. } => {
435                self.validate_partition_term(&self.partition, &mut partition_fields)?;
436            }
437            FilterExpr::And(terms) if !terms.is_empty() => {
438                for term in terms {
439                    self.validate_partition_term(term, &mut partition_fields)?;
440                }
441            }
442            FilterExpr::And(_) => {
443                return Err(StorageError::InvalidRequest(
444                    "replace partition requires a nonempty conjunction".to_owned(),
445                ));
446            }
447            _ => {
448                return Err(StorageError::InvalidRequest(
449                    "replace partition accepts only equality, nonempty membership, or a flat nonempty conjunction of those predicates"
450                        .to_owned(),
451                ));
452            }
453        }
454
455        self.validate_field_name(&self.identity_field, "identity")?;
456        if partition_fields.contains(&self.identity_field) {
457            return Err(StorageError::InvalidRequest(format!(
458                "replace partition identity field `{}` must differ from every partition field",
459                self.identity_field
460            )));
461        }
462
463        let mut identities = HashSet::new();
464        for (index, record) in self.records.iter().enumerate() {
465            if record.id.is_some() {
466                return Err(StorageError::InvalidRequest(format!(
467                    "replace partition record {index} must not carry DataRecord.id"
468                )));
469            }
470            if record.fields.contains_key("id") || record.vectors.contains_key("id") {
471                return Err(StorageError::InvalidRequest(format!(
472                    "replace partition record {index} must not contain a field or vector named `id`"
473                )));
474            }
475            if record.fields.contains_key(&self.identity_field)
476                && record.vectors.contains_key(&self.identity_field)
477            {
478                return Err(StorageError::InvalidRequest(format!(
479                    "replace partition record {index} must not contain identity field `{}` as both a scalar field and a vector",
480                    self.identity_field
481                )));
482            }
483            if let Some(field) = partition_fields.iter().find(|field| {
484                record.fields.contains_key(field.as_str())
485                    && record.vectors.contains_key(field.as_str())
486            }) {
487                return Err(StorageError::InvalidRequest(format!(
488                    "replace partition record {index} must not contain partition field `{field}` as both a scalar field and a vector"
489                )));
490            }
491            if !self.record_matches_partition(record) {
492                return Err(StorageError::InvalidRequest(format!(
493                    "replace partition record {index} does not contain and satisfy every partition field"
494                )));
495            }
496
497            let identity = record.fields.get(&self.identity_field).ok_or_else(|| {
498                StorageError::InvalidRequest(format!(
499                    "replace partition record {index} is missing identity field `{}`",
500                    self.identity_field
501                ))
502            })?;
503            self.validate_scalar(identity, "replacement identity")?;
504            if !identities.insert(identity) {
505                return Err(StorageError::InvalidRequest(format!(
506                    "replace partition identity field `{}` must be unique within the replacement set",
507                    self.identity_field
508                )));
509            }
510        }
511
512        Ok(())
513    }
514
515    fn validate_partition_term(
516        &self,
517        term: &FilterExpr,
518        partition_fields: &mut BTreeSet<String>,
519    ) -> Result<(), StorageError> {
520        match term {
521            FilterExpr::Compare {
522                field,
523                op: super::ComparisonOp::Eq,
524                value,
525            } => {
526                self.validate_partition_field(field, partition_fields)?;
527                self.validate_scalar(value, "partition value")
528            }
529            FilterExpr::In { field, values } if !values.is_empty() => {
530                self.validate_partition_field(field, partition_fields)?;
531                let mut unique_values = HashSet::new();
532                for value in values {
533                    self.validate_scalar(value, "partition membership value")?;
534                    if !unique_values.insert(value) {
535                        return Err(StorageError::InvalidRequest(format!(
536                            "replace partition membership values for `{field}` must be duplicate-free"
537                        )));
538                    }
539                }
540                Ok(())
541            }
542            FilterExpr::In { .. } => Err(StorageError::InvalidRequest(
543                "replace partition membership values must be nonempty".to_owned(),
544            )),
545            FilterExpr::Compare { .. } => Err(StorageError::InvalidRequest(
546                "replace partition comparisons must use equality".to_owned(),
547            )),
548            FilterExpr::And(_) => Err(StorageError::InvalidRequest(
549                "replace partition conjunctions must be flat".to_owned(),
550            )),
551            _ => Err(StorageError::InvalidRequest(
552                "replace partition conjunction terms must be equality or nonempty membership predicates"
553                    .to_owned(),
554            )),
555        }
556    }
557
558    fn validate_partition_field(
559        &self,
560        field: &str,
561        partition_fields: &mut BTreeSet<String>,
562    ) -> Result<(), StorageError> {
563        self.validate_field_name(field, "partition")?;
564        if !partition_fields.insert(field.to_owned()) {
565            return Err(StorageError::InvalidRequest(format!(
566                "replace partition field `{field}` must appear only once"
567            )));
568        }
569        Ok(())
570    }
571
572    fn validate_field_name(&self, field: &str, role: &str) -> Result<(), StorageError> {
573        let mut bytes = field.bytes();
574        let valid_first = bytes
575            .next()
576            .is_some_and(|byte| byte == b'_' || byte.is_ascii_alphabetic());
577        let valid_rest = bytes.all(|byte| byte == b'_' || byte.is_ascii_alphanumeric());
578        if !valid_first || !valid_rest || field == "id" {
579            return Err(StorageError::InvalidRequest(format!(
580                "replace partition {role} field `{field}` must match [A-Za-z_][A-Za-z0-9_]* and must not be `id`"
581            )));
582        }
583        Ok(())
584    }
585
586    fn validate_scalar(&self, value: &Value, role: &str) -> Result<(), StorageError> {
587        if matches!(value, Value::Bool(_) | Value::Number(_) | Value::String(_)) {
588            Ok(())
589        } else {
590            Err(StorageError::InvalidRequest(format!(
591                "replace partition {role} must be a non-null JSON scalar"
592            )))
593        }
594    }
595
596    fn record_matches_partition(&self, record: &DataRecord) -> bool {
597        match &self.partition {
598            FilterExpr::And(terms) => terms
599                .iter()
600                .all(|term| self.record_matches_partition_term(record, term)),
601            term => self.record_matches_partition_term(record, term),
602        }
603    }
604
605    fn record_matches_partition_term(&self, record: &DataRecord, term: &FilterExpr) -> bool {
606        match term {
607            FilterExpr::Compare { field, value, .. } => record
608                .fields
609                .get(field)
610                .is_some_and(|actual| actual == value),
611            FilterExpr::In { field, values } => record
612                .fields
613                .get(field)
614                .is_some_and(|actual| values.iter().any(|candidate| actual == candidate)),
615            _ => false,
616        }
617    }
618}
619
620/// Request to count rows matching an optional filter.
621///
622/// # Examples
623///
624/// ```
625/// let request = storage_kit::database::data::CountRequest {
626///     locator: storage_kit::database::StorageLocator::new("documents"),
627///     filter: None,
628/// };
629/// assert!(request.filter.is_none());
630/// ```
631#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
632pub struct CountRequest {
633    /// Collection whose rows should be counted.
634    pub locator: StorageLocator,
635    /// Filter predicate used when only matching rows should be counted.
636    pub filter: Option<FilterExpr>,
637}
638
639impl HasLocator for CountRequest {
640    fn locator_mut(&mut self) -> &mut StorageLocator {
641        &mut self.locator
642    }
643}
644
645/// Page of records returned by lookup or scan flows.
646///
647/// # Examples
648///
649/// ```
650/// let page = storage_kit::database::data::RecordsPage {
651///     records: vec![storage_kit::database::data::DataRecord::default()],
652///     total: Some(1),
653///     next_page: None,
654/// };
655/// assert_eq!(page.total, Some(1));
656/// ```
657#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
658pub struct RecordsPage {
659    /// Records returned in this page.
660    pub records: Vec<DataRecord>,
661    /// Total matching row count when the backend reports it alongside the page.
662    pub total: Option<u64>,
663    /// Continuation token for fetching the next page of records.
664    pub next_page: Option<PageToken>,
665}
666
667/// Portable mutation counts.
668///
669/// Count fields are populated only when the backend returns data that proves
670/// them. If the backend only acknowledges a write or returns non-count
671/// metadata, the count fields remain `None` and `counts_exact` is `false`.
672///
673/// # Examples
674///
675/// ```
676/// let result = storage_kit::database::data::MutationResult {
677///     inserted: Some(2),
678///     counts_exact: true,
679///     ..Default::default()
680/// };
681/// assert_eq!(result.inserted, Some(2));
682/// ```
683#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
684pub struct MutationResult {
685    /// Inserted row count.
686    pub inserted: Option<u64>,
687    /// Updated row count.
688    pub updated: Option<u64>,
689    /// Deleted row count.
690    pub deleted: Option<u64>,
691    /// Upserted row count.
692    pub upserted: Option<u64>,
693    /// Matched row count.
694    pub matched: Option<u64>,
695    /// Whether the populated counts are exact backend-reported counts.
696    pub counts_exact: bool,
697}
698
699impl MutationResult {
700    #[must_use]
701    pub(crate) fn counts(
702        inserted: Option<u64>,
703        updated: Option<u64>,
704        deleted: Option<u64>,
705        upserted: Option<u64>,
706        matched: Option<u64>,
707        counts_exact: bool,
708    ) -> Self {
709        Self {
710            inserted,
711            updated,
712            deleted,
713            upserted,
714            matched,
715            counts_exact,
716        }
717    }
718
719    #[must_use]
720    pub(crate) fn no_counts() -> Self {
721        Self::counts(None, None, None, None, None, false)
722    }
723
724    #[must_use]
725    #[cfg(feature = "surrealdb")]
726    pub(crate) fn matched_operation(matched: u64, deleted: bool, counts_exact: bool) -> Self {
727        Self::counts(
728            Some(0),
729            Some(if deleted { 0 } else { matched }),
730            Some(if deleted { matched } else { 0 }),
731            Some(0),
732            Some(matched),
733            counts_exact,
734        )
735    }
736}
737
738/// Portable data request.
739///
740/// # Examples
741///
742/// ```
743/// let request = storage_kit::database::data::DataRequest::Count(storage_kit::database::data::CountRequest {
744///     locator: storage_kit::database::StorageLocator::new("documents"),
745///     filter: None,
746/// });
747/// assert!(matches!(request, storage_kit::database::data::DataRequest::Count(_)));
748/// ```
749#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
750pub enum DataRequest {
751    /// Insert records.
752    Insert(InsertRequest),
753    /// Scan a collection.
754    Scan(ScanRequest),
755    /// Update matching rows.
756    Update(UpdateRequest),
757    /// Delete matching rows.
758    Delete(DeleteRequest),
759    /// Atomically replace all records in one bounded partition.
760    ReplacePartition(ReplacePartitionRequest),
761    /// Count matching rows.
762    Count(CountRequest),
763}
764
765impl DataRequest {
766    pub(crate) fn validate(&self) -> Result<(), StorageError> {
767        match self {
768            Self::ReplacePartition(request) => request.validate(),
769            _ => Ok(()),
770        }
771    }
772}
773
774impl ApplyDefaultLocator for DataRequest {
775    fn apply_default_locator(self, default: &StorageLocator) -> Self {
776        match self {
777            Self::Insert(req) => Self::Insert(req.apply_default_locator(default)),
778            Self::Scan(req) => Self::Scan(req.apply_default_locator(default)),
779            Self::Update(req) => Self::Update(req.apply_default_locator(default)),
780            Self::Delete(req) => Self::Delete(req.apply_default_locator(default)),
781            Self::ReplacePartition(req) => {
782                Self::ReplacePartition(req.apply_default_locator(default))
783            }
784            Self::Count(req) => Self::Count(req.apply_default_locator(default)),
785        }
786    }
787}
788
789/// Portable data response.
790///
791/// # Examples
792///
793/// ```
794/// let response = storage_kit::database::data::DataResponse::Count(3);
795/// assert!(matches!(response, storage_kit::database::data::DataResponse::Count(3)));
796/// ```
797#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
798pub enum DataResponse {
799    /// Returns portable mutation counts.
800    Mutation(MutationResult),
801    /// Returns one page of records.
802    Records(RecordsPage),
803    /// Returns only the matching row count.
804    Count(u64),
805}