use std::collections::{BTreeSet, HashSet};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::{DataRecord, FilterExpr, StorageError, StorageLocator};
use super::{
ApplyDefaultLocator, HasLocator,
query::{PageToken, Projection},
};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum WriteMode {
Append,
Overwrite,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct InsertRequest {
pub locator: StorageLocator,
pub records: Vec<DataRecord>,
pub mode: WriteMode,
}
impl HasLocator for InsertRequest {
fn locator_mut(&mut self) -> &mut StorageLocator {
&mut self.locator
}
}
impl InsertRequest {
#[must_use]
pub fn append(locator: StorageLocator, records: Vec<DataRecord>) -> Self {
Self {
locator,
records,
mode: WriteMode::Append,
}
}
#[must_use]
pub fn overwrite(locator: StorageLocator, records: Vec<DataRecord>) -> Self {
Self {
locator,
records,
mode: WriteMode::Overwrite,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ScanRequest {
pub locator: StorageLocator,
pub filter: Option<FilterExpr>,
pub projection: Projection,
pub limit: Option<usize>,
pub offset: Option<usize>,
pub page_token: Option<PageToken>,
}
impl HasLocator for ScanRequest {
fn locator_mut(&mut self) -> &mut StorageLocator {
&mut self.locator
}
}
impl ScanRequest {
#[must_use]
pub fn new(locator: StorageLocator) -> Self {
Self {
locator,
filter: None,
projection: Projection::All,
limit: None,
offset: None,
page_token: None,
}
}
#[must_use]
pub fn filter(mut self, filter: FilterExpr) -> Self {
self.filter = Some(filter);
self
}
#[must_use]
pub fn projection(mut self, projection: Projection) -> Self {
self.projection = projection;
self
}
#[must_use]
pub fn limit(mut self, limit: usize) -> Self {
self.limit = Some(limit);
self
}
#[must_use]
pub fn offset(mut self, offset: usize) -> Self {
self.offset = Some(offset);
self
}
#[must_use]
pub fn page_token(mut self, page_token: impl Into<PageToken>) -> Self {
self.page_token = Some(page_token.into());
self
}
pub(crate) fn effective_offset(&self) -> Result<Option<usize>, StorageError> {
PageToken::effective_offset(self.page_token.as_deref(), self.offset)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct UpdateAssignment {
pub column: String,
pub value: Value,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct UpdateRequest {
pub locator: StorageLocator,
pub filter: Option<FilterExpr>,
pub assignments: Vec<UpdateAssignment>,
}
impl HasLocator for UpdateRequest {
fn locator_mut(&mut self) -> &mut StorageLocator {
&mut self.locator
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct DeleteRequest {
pub locator: StorageLocator,
pub filter: FilterExpr,
}
impl HasLocator for DeleteRequest {
fn locator_mut(&mut self) -> &mut StorageLocator {
&mut self.locator
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ReplacePartitionRequest {
pub locator: StorageLocator,
pub partition: FilterExpr,
pub identity_field: String,
pub records: Vec<DataRecord>,
}
impl HasLocator for ReplacePartitionRequest {
fn locator_mut(&mut self) -> &mut StorageLocator {
&mut self.locator
}
}
impl ReplacePartitionRequest {
pub fn new(
locator: StorageLocator,
partition: FilterExpr,
identity_field: impl Into<String>,
records: Vec<DataRecord>,
) -> Result<Self, StorageError> {
let request = Self {
locator,
partition,
identity_field: identity_field.into(),
records,
};
request.validate()?;
Ok(request)
}
pub fn validate(&self) -> Result<(), StorageError> {
let mut partition_fields = BTreeSet::new();
match &self.partition {
FilterExpr::Compare { .. } | FilterExpr::In { .. } => {
self.validate_partition_term(&self.partition, &mut partition_fields)?;
}
FilterExpr::And(terms) if !terms.is_empty() => {
for term in terms {
self.validate_partition_term(term, &mut partition_fields)?;
}
}
FilterExpr::And(_) => {
return Err(StorageError::InvalidRequest(
"replace partition requires a nonempty conjunction".to_owned(),
));
}
_ => {
return Err(StorageError::InvalidRequest(
"replace partition accepts only equality, nonempty membership, or a flat nonempty conjunction of those predicates"
.to_owned(),
));
}
}
self.validate_field_name(&self.identity_field, "identity")?;
if partition_fields.contains(&self.identity_field) {
return Err(StorageError::InvalidRequest(format!(
"replace partition identity field `{}` must differ from every partition field",
self.identity_field
)));
}
let mut identities = HashSet::new();
for (index, record) in self.records.iter().enumerate() {
if record.id.is_some() {
return Err(StorageError::InvalidRequest(format!(
"replace partition record {index} must not carry DataRecord.id"
)));
}
if record.fields.contains_key("id") || record.vectors.contains_key("id") {
return Err(StorageError::InvalidRequest(format!(
"replace partition record {index} must not contain a field or vector named `id`"
)));
}
if record.fields.contains_key(&self.identity_field)
&& record.vectors.contains_key(&self.identity_field)
{
return Err(StorageError::InvalidRequest(format!(
"replace partition record {index} must not contain identity field `{}` as both a scalar field and a vector",
self.identity_field
)));
}
if let Some(field) = partition_fields.iter().find(|field| {
record.fields.contains_key(field.as_str())
&& record.vectors.contains_key(field.as_str())
}) {
return Err(StorageError::InvalidRequest(format!(
"replace partition record {index} must not contain partition field `{field}` as both a scalar field and a vector"
)));
}
if !self.record_matches_partition(record) {
return Err(StorageError::InvalidRequest(format!(
"replace partition record {index} does not contain and satisfy every partition field"
)));
}
let identity = record.fields.get(&self.identity_field).ok_or_else(|| {
StorageError::InvalidRequest(format!(
"replace partition record {index} is missing identity field `{}`",
self.identity_field
))
})?;
self.validate_scalar(identity, "replacement identity")?;
if !identities.insert(identity) {
return Err(StorageError::InvalidRequest(format!(
"replace partition identity field `{}` must be unique within the replacement set",
self.identity_field
)));
}
}
Ok(())
}
fn validate_partition_term(
&self,
term: &FilterExpr,
partition_fields: &mut BTreeSet<String>,
) -> Result<(), StorageError> {
match term {
FilterExpr::Compare {
field,
op: super::ComparisonOp::Eq,
value,
} => {
self.validate_partition_field(field, partition_fields)?;
self.validate_scalar(value, "partition value")
}
FilterExpr::In { field, values } if !values.is_empty() => {
self.validate_partition_field(field, partition_fields)?;
let mut unique_values = HashSet::new();
for value in values {
self.validate_scalar(value, "partition membership value")?;
if !unique_values.insert(value) {
return Err(StorageError::InvalidRequest(format!(
"replace partition membership values for `{field}` must be duplicate-free"
)));
}
}
Ok(())
}
FilterExpr::In { .. } => Err(StorageError::InvalidRequest(
"replace partition membership values must be nonempty".to_owned(),
)),
FilterExpr::Compare { .. } => Err(StorageError::InvalidRequest(
"replace partition comparisons must use equality".to_owned(),
)),
FilterExpr::And(_) => Err(StorageError::InvalidRequest(
"replace partition conjunctions must be flat".to_owned(),
)),
_ => Err(StorageError::InvalidRequest(
"replace partition conjunction terms must be equality or nonempty membership predicates"
.to_owned(),
)),
}
}
fn validate_partition_field(
&self,
field: &str,
partition_fields: &mut BTreeSet<String>,
) -> Result<(), StorageError> {
self.validate_field_name(field, "partition")?;
if !partition_fields.insert(field.to_owned()) {
return Err(StorageError::InvalidRequest(format!(
"replace partition field `{field}` must appear only once"
)));
}
Ok(())
}
fn validate_field_name(&self, field: &str, role: &str) -> Result<(), StorageError> {
let mut bytes = field.bytes();
let valid_first = bytes
.next()
.is_some_and(|byte| byte == b'_' || byte.is_ascii_alphabetic());
let valid_rest = bytes.all(|byte| byte == b'_' || byte.is_ascii_alphanumeric());
if !valid_first || !valid_rest || field == "id" {
return Err(StorageError::InvalidRequest(format!(
"replace partition {role} field `{field}` must match [A-Za-z_][A-Za-z0-9_]* and must not be `id`"
)));
}
Ok(())
}
fn validate_scalar(&self, value: &Value, role: &str) -> Result<(), StorageError> {
if matches!(value, Value::Bool(_) | Value::Number(_) | Value::String(_)) {
Ok(())
} else {
Err(StorageError::InvalidRequest(format!(
"replace partition {role} must be a non-null JSON scalar"
)))
}
}
fn record_matches_partition(&self, record: &DataRecord) -> bool {
match &self.partition {
FilterExpr::And(terms) => terms
.iter()
.all(|term| self.record_matches_partition_term(record, term)),
term => self.record_matches_partition_term(record, term),
}
}
fn record_matches_partition_term(&self, record: &DataRecord, term: &FilterExpr) -> bool {
match term {
FilterExpr::Compare { field, value, .. } => record
.fields
.get(field)
.is_some_and(|actual| actual == value),
FilterExpr::In { field, values } => record
.fields
.get(field)
.is_some_and(|actual| values.iter().any(|candidate| actual == candidate)),
_ => false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct CountRequest {
pub locator: StorageLocator,
pub filter: Option<FilterExpr>,
}
impl HasLocator for CountRequest {
fn locator_mut(&mut self) -> &mut StorageLocator {
&mut self.locator
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct RecordsPage {
pub records: Vec<DataRecord>,
pub total: Option<u64>,
pub next_page: Option<PageToken>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct MutationResult {
pub inserted: Option<u64>,
pub updated: Option<u64>,
pub deleted: Option<u64>,
pub upserted: Option<u64>,
pub matched: Option<u64>,
pub counts_exact: bool,
}
impl MutationResult {
#[must_use]
pub(crate) fn counts(
inserted: Option<u64>,
updated: Option<u64>,
deleted: Option<u64>,
upserted: Option<u64>,
matched: Option<u64>,
counts_exact: bool,
) -> Self {
Self {
inserted,
updated,
deleted,
upserted,
matched,
counts_exact,
}
}
#[must_use]
pub(crate) fn no_counts() -> Self {
Self::counts(None, None, None, None, None, false)
}
#[must_use]
#[cfg(feature = "surrealdb")]
pub(crate) fn matched_operation(matched: u64, deleted: bool, counts_exact: bool) -> Self {
Self::counts(
Some(0),
Some(if deleted { 0 } else { matched }),
Some(if deleted { matched } else { 0 }),
Some(0),
Some(matched),
counts_exact,
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum DataRequest {
Insert(InsertRequest),
Scan(ScanRequest),
Update(UpdateRequest),
Delete(DeleteRequest),
ReplacePartition(ReplacePartitionRequest),
Count(CountRequest),
}
impl DataRequest {
pub(crate) fn validate(&self) -> Result<(), StorageError> {
match self {
Self::ReplacePartition(request) => request.validate(),
_ => Ok(()),
}
}
}
impl ApplyDefaultLocator for DataRequest {
fn apply_default_locator(self, default: &StorageLocator) -> Self {
match self {
Self::Insert(req) => Self::Insert(req.apply_default_locator(default)),
Self::Scan(req) => Self::Scan(req.apply_default_locator(default)),
Self::Update(req) => Self::Update(req.apply_default_locator(default)),
Self::Delete(req) => Self::Delete(req.apply_default_locator(default)),
Self::ReplacePartition(req) => {
Self::ReplacePartition(req.apply_default_locator(default))
}
Self::Count(req) => Self::Count(req.apply_default_locator(default)),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum DataResponse {
Mutation(MutationResult),
Records(RecordsPage),
Count(u64),
}