use super::types::*;
use crate::api_request::{ApiRequest, Mutation, Payload, PreferResolution, QualifiedIdentifier};
use crate::error::{Error, Result};
use crate::schema_cache::Table;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum MutatePlan {
Insert {
target: QualifiedIdentifier,
columns: Vec<CoercibleField>,
body: Option<bytes::Bytes>,
on_conflict: Option<(PreferResolution, Vec<String>)>,
where_clauses: Vec<CoercibleLogicTree>,
returning: Vec<String>,
pk_cols: Vec<String>,
apply_defaults: bool,
#[serde(default)]
reports_inserted: bool,
},
Update {
target: QualifiedIdentifier,
columns: Vec<CoercibleField>,
body: Option<bytes::Bytes>,
where_clauses: Vec<CoercibleLogicTree>,
returning: Vec<String>,
apply_defaults: bool,
},
Delete {
target: QualifiedIdentifier,
where_clauses: Vec<CoercibleLogicTree>,
returning: Vec<String>,
},
}
impl MutatePlan {
pub fn from_request(
request: &ApiRequest,
table: &Table,
mutation: &Mutation,
schema_cache: &crate::schema_cache::SchemaCache,
) -> Result<Self> {
let qi = table.qualified_identifier();
match mutation {
Mutation::Create => Self::create_insert(request, table, qi, schema_cache),
Mutation::Update => Self::create_update(request, table, qi, schema_cache),
Mutation::Delete => Self::create_delete(request, table, qi),
Mutation::SingleUpsert => Self::create_upsert(request, table, qi, schema_cache),
}
}
fn create_insert(
request: &ApiRequest,
table: &Table,
qi: QualifiedIdentifier,
schema_cache: &crate::schema_cache::SchemaCache,
) -> Result<Self> {
let columns = get_payload_columns(request, table, schema_cache)?;
let body = get_body_bytes(request)?;
let returning = get_returning_columns(request, table, true);
let apply_defaults = asked_for_defaults(request);
let on_conflict = request
.preferences
.resolution
.clone()
.and_then(|resolution| {
let columns = request
.query_params
.on_conflict
.clone()
.unwrap_or_else(|| table.pk_cols.clone());
match columns.is_empty() {
true => None,
false => Some((resolution, columns)),
}
});
Ok(Self::Insert {
target: qi,
columns,
body,
on_conflict,
where_clauses: vec![],
returning,
pk_cols: table.pk_cols.clone(),
apply_defaults,
reports_inserted: !table.is_view && !table.is_partitioned,
})
}
fn create_update(
request: &ApiRequest,
table: &Table,
qi: QualifiedIdentifier,
schema_cache: &crate::schema_cache::SchemaCache,
) -> Result<Self> {
let columns = get_payload_columns(request, table, schema_cache)?;
let body = get_body_bytes(request)?;
let where_clauses = build_mutation_where(request, table)?;
let returning = get_returning_columns(request, table, false);
let apply_defaults = asked_for_defaults(request);
Ok(Self::Update {
target: qi,
columns,
body,
where_clauses,
returning,
apply_defaults,
})
}
fn create_delete(request: &ApiRequest, table: &Table, qi: QualifiedIdentifier) -> Result<Self> {
let where_clauses = build_mutation_where(request, table)?;
let returning = get_returning_columns(request, table, false);
Ok(Self::Delete {
target: qi,
where_clauses,
returning,
})
}
fn create_upsert(
request: &ApiRequest,
table: &Table,
qi: QualifiedIdentifier,
schema_cache: &crate::schema_cache::SchemaCache,
) -> Result<Self> {
validate_put(request, table)?;
let columns = get_payload_columns(request, table, schema_cache)?;
let body = get_body_bytes(request)?;
let returning = get_returning_columns(request, table, true);
let on_conflict = Some((PreferResolution::MergeDuplicates, table.pk_cols.clone()));
let where_clauses = build_mutation_where(request, table)?;
Ok(Self::Insert {
target: qi,
columns,
body,
on_conflict,
where_clauses,
returning,
pk_cols: table.pk_cols.clone(),
apply_defaults: asked_for_defaults(request),
reports_inserted: !table.is_view && !table.is_partitioned,
})
}
pub fn target(&self) -> &QualifiedIdentifier {
match self {
Self::Insert { target, .. } => target,
Self::Update { target, .. } => target,
Self::Delete { target, .. } => target,
}
}
pub fn has_body(&self) -> bool {
match self {
Self::Insert { body, .. } => body.is_some(),
Self::Update { body, .. } => body.is_some(),
Self::Delete { .. } => false,
}
}
}
fn get_payload_columns(
request: &ApiRequest,
table: &Table,
schema_cache: &crate::schema_cache::SchemaCache,
) -> Result<Vec<CoercibleField>> {
let keys = match &request.payload {
Some(Payload::ProcessedJson { keys, .. }) => keys,
Some(Payload::ProcessedUrlEncoded { keys, .. }) => keys,
_ => return Ok(vec![]),
};
let names: Vec<&String> = match &request.query_params.columns {
Some(columns) => columns.iter().collect(),
None => keys.iter().collect(),
};
let mut columns = Vec::new();
for key in names {
let column = table.get_column(key).ok_or_else(|| Error::UnknownColumn {
column: key.clone(),
relation: table.name.clone(),
})?;
let mut field = CoercibleField::simple(key, &column.nominal_type);
field.default = column.default.clone();
if let Some(function) = schema_cache.representation("json", column.representation_type()) {
field.ir_type = "json".to_string();
field.transform = Some(function.to_string());
}
columns.push(field);
}
Ok(columns)
}
fn asked_for_defaults(request: &ApiRequest) -> bool {
request
.preferences
.applied
.iter()
.any(|pref| pref == "missing=default")
}
fn validate_put(request: &ApiRequest, table: &Table) -> Result<()> {
use crate::api_request::{Operation, QuantOperator};
if request
.query_params
.ranges
.values()
.any(|range| range.limit.is_some() || range.offset != 0)
{
return Err(Error::PutLimitNotAllowed);
}
let mut keyed: std::collections::HashMap<&str, &str> = std::collections::HashMap::new();
for filter in &request.query_params.filters_root {
let Operation::Quant {
op: QuantOperator::Equal,
quantifier: None,
value,
} = &filter.op_expr.operation
else {
return Err(Error::InvalidPutFilters);
};
if filter.op_expr.negated {
return Err(Error::InvalidPutFilters);
}
keyed.insert(&filter.field.name, value);
}
if !request.query_params.logic.is_empty() || !request.query_params.filters.is_empty() {
return Err(Error::InvalidPutFilters);
}
if table.pk_cols.is_empty()
|| keyed.len() != table.pk_cols.len()
|| !table
.pk_cols
.iter()
.all(|key| keyed.contains_key(key.as_str()))
{
return Err(Error::InvalidPutFilters);
}
Ok(())
}
fn get_body_bytes(request: &ApiRequest) -> Result<Option<bytes::Bytes>> {
match &request.payload {
Some(Payload::ProcessedJson { raw, .. }) => Ok(Some(raw.clone())),
Some(Payload::RawJson(raw)) => Ok(Some(raw.clone())),
Some(Payload::RawPayload(raw)) => Ok(Some(raw.clone())),
Some(Payload::ProcessedUrlEncoded { data, .. }) => {
let json = serde_json::to_vec(
&data
.iter()
.cloned()
.collect::<std::collections::HashMap<_, _>>(),
)
.map_err(|e| Error::InvalidBody(e.to_string()))?;
Ok(Some(bytes::Bytes::from(json)))
}
None => Ok(None),
}
}
fn get_returning_columns(request: &ApiRequest, table: &Table, creates: bool) -> Vec<String> {
use crate::api_request::SelectItem;
let everything = || -> Vec<String> { table.column_names().map(str::to_string).collect() };
let mut wanted: Vec<String> = Vec::new();
if request.preferences.representation.needs_body() {
if request.query_params.select.is_empty() {
return everything();
}
for item in &request.query_params.select {
match item {
SelectItem::Field { field, .. } => {
if field.name == "*"
|| table
.get_computed_column(&field.name)
.is_some_and(|c| !c.takes_arguments)
{
return everything();
}
wanted.push(field.name.clone());
}
SelectItem::Relation { .. } | SelectItem::SpreadRelation { .. } => {}
}
}
for (path, terms) in &request.query_params.order {
if !path.is_empty() {
continue;
}
for term in terms {
if let crate::api_request::OrderTerm::Field { field, .. } = term {
wanted.push(field.name.clone());
}
}
}
}
if creates {
wanted.extend(table.pk_cols.iter().cloned());
}
wanted.retain(|name| table.get_column(name).is_some());
wanted.sort();
wanted.dedup();
wanted
}
fn build_mutation_where(request: &ApiRequest, table: &Table) -> Result<Vec<CoercibleLogicTree>> {
let type_resolver = |name: &str| -> String {
table
.get_column(name)
.map(|c| c.data_type.clone())
.unwrap_or_else(|| "text".to_string())
};
let mut clauses = Vec::new();
for filter in &request.query_params.filters_root {
let pg_type = type_resolver(&filter.field.name);
clauses.push(CoercibleLogicTree::Stmt(CoercibleFilter::from_filter(
filter, &pg_type,
)));
}
for (path, tree) in &request.query_params.logic {
if path.is_empty() {
clauses.push(CoercibleLogicTree::from_logic_tree(tree, type_resolver));
}
}
Ok(clauses)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mutate_plan_target() {
let qi = QualifiedIdentifier::new("public", "users");
let plan = MutatePlan::Delete {
target: qi.clone(),
where_clauses: vec![],
returning: vec!["id".into()],
};
assert_eq!(plan.target().name, "users");
}
#[test]
fn test_mutate_plan_has_body() {
let qi = QualifiedIdentifier::new("public", "users");
let insert = MutatePlan::Insert {
target: qi.clone(),
columns: vec![],
body: Some(bytes::Bytes::from("{}".as_bytes())),
on_conflict: None,
where_clauses: vec![],
returning: vec![],
pk_cols: vec![],
apply_defaults: true,
reports_inserted: false,
};
assert!(insert.has_body());
let delete = MutatePlan::Delete {
target: qi,
where_clauses: vec![],
returning: vec![],
};
assert!(!delete.has_body());
}
}