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}