use crate::actions::index_capacity::WriteCapacity;
use crate::errors::{DynoxideError, Result};
use crate::expressions::condition::compare_values;
use crate::expressions::key_condition::{ResolvedSortKeyCondition, sk_conditions_to_sql};
use crate::partiql::parser::{
CompOp, PartiqlValue, ReturningVariant, SetValue, Statement, WhereClause, WhereCondition,
};
use crate::storage_backend::StorageBackend;
use crate::types::{AttributeValue, Item};
use std::collections::HashMap;
pub async fn execute<S: StorageBackend>(
storage: &S,
stmt: &Statement,
parameters: &[AttributeValue],
limit: Option<usize>,
) -> Result<Option<Vec<Item>>> {
Ok(execute_measured(storage, stmt, parameters, limit).await?.0)
}
pub async fn execute_measured<S: StorageBackend>(
storage: &S,
stmt: &Statement,
parameters: &[AttributeValue],
limit: Option<usize>,
) -> Result<(Option<Vec<Item>>, usize)> {
let page = execute_page(storage, stmt, parameters, limit, None, false, None, None).await?;
Ok((page.items, page.size))
}
#[derive(Debug, Default)]
#[non_exhaustive]
pub struct StatementPage {
pub items: Option<Vec<Item>>,
pub size: usize,
pub capacity: Option<WriteCapacity>,
pub next_token: Option<String>,
pub read_index: Option<ReadIndex>,
pub base_read_units: f64,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct ReadIndex {
pub name: String,
pub is_lsi: bool,
}
fn statement_where_clause(stmt: &Statement) -> Option<&WhereClause> {
match stmt {
Statement::Select { where_clause, .. }
| Statement::Update { where_clause, .. }
| Statement::Delete { where_clause, .. } => where_clause.as_ref(),
Statement::Insert { .. } => None,
}
}
fn validate_ordering_operands(
where_clause: Option<&WhereClause>,
parameters: &[AttributeValue],
) -> Result<()> {
fn orderable(v: &AttributeValue) -> bool {
matches!(
v,
AttributeValue::S(_) | AttributeValue::N(_) | AttributeValue::B(_)
)
}
fn reject(op: &str, v: &AttributeValue) -> DynoxideError {
DynoxideError::ValidationException(format!(
"Incorrect operand type for operator or function; \
operator or function: {op}, operand type: {}",
v.type_name()
))
}
let Some(wc) = where_clause else {
return Ok(());
};
for group in &wc.groups {
for condition in group {
match condition {
WhereCondition::Comparison(c) | WhereCondition::NotComparison(c) => {
let op = match c.op {
CompOp::Lt => "<",
CompOp::Le => "<=",
CompOp::Gt => ">",
CompOp::Ge => ">=",
CompOp::Eq | CompOp::Ne => continue,
};
let value = resolve_value(&c.value, parameters)?;
if !orderable(&value) {
return Err(reject(op, &value));
}
}
WhereCondition::Between(_, low, high) => {
for operand in [low, high] {
let value = resolve_value(operand, parameters)?;
if !orderable(&value) {
return Err(reject("BETWEEN", &value));
}
}
}
_ => {}
}
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub async fn execute_page<S: StorageBackend>(
storage: &S,
stmt: &Statement,
parameters: &[AttributeValue],
limit: Option<usize>,
next_token: Option<&str>,
consistent_read: bool,
capacity_mode: Option<&str>,
resolved: Option<&ResolvedTable>,
) -> Result<StatementPage> {
if next_token.is_some() && !matches!(stmt, Statement::Select { .. }) {
return Err(DynoxideError::ValidationException(
"NextToken is only valid on a SELECT statement".to_string(),
));
}
validate_ordering_operands(statement_where_clause(stmt), parameters)?;
if !matches!(stmt, Statement::Select { .. })
&& crate::partiql::parser::index_name(stmt).is_some()
{
return Err(DynoxideError::ValidationException(
"This operation is not supported on an index".to_string(),
));
}
let resolved = resolved.filter(|r| {
crate::partiql::parser::table_name(stmt).is_some_and(|name| r.table_name == name)
});
match stmt {
Statement::Select {
table_name,
index_name,
projections,
where_clause,
..
} => {
let outcome = execute_select(
storage,
table_name,
index_name.as_deref(),
projections,
where_clause.as_ref(),
parameters,
limit,
next_token,
consistent_read,
resolved,
)
.await?;
Ok(StatementPage {
items: Some(outcome.items),
size: outcome.evaluated_bytes,
capacity: None,
next_token: outcome.next_token,
read_index: outcome.index.map(|i| ReadIndex {
name: i.def.index_name,
is_lsi: i.is_lsi,
}),
base_read_units: outcome.base_read_units,
})
}
Statement::Insert {
table_name,
item,
if_not_exists,
} => {
let capacity = execute_insert(
storage,
table_name,
item,
parameters,
*if_not_exists,
capacity_mode,
resolved,
)
.await?;
Ok(StatementPage {
items: None,
size: capacity.new_size.unwrap_or(0),
capacity: Some(capacity),
..Default::default()
})
}
Statement::Update {
table_name,
set_clauses,
remove_paths,
where_clause,
returning,
..
} => {
let (projection, capacity) = execute_update(
storage,
table_name,
set_clauses,
remove_paths,
where_clause.as_ref(),
parameters,
*returning,
capacity_mode,
resolved,
)
.await?;
let items = projection.map(|item| {
if item.is_empty() {
Vec::new()
} else {
vec![item]
}
});
Ok(StatementPage {
items,
size: capacity.new_size.unwrap_or(0),
capacity: Some(capacity),
..Default::default()
})
}
Statement::Delete {
table_name,
where_clause,
returning,
..
} => {
if let Some(variant) = returning
&& *variant != ReturningVariant::AllOld
{
return Err(DynoxideError::ValidationException(format!(
"Invalid returning clause: RETURNING {} *. Only RETURNING ALL OLD * is allowed in DELETE statements.",
variant.as_sql()
)));
}
let (old_item, capacity) = execute_delete(
storage,
table_name,
where_clause.as_ref(),
parameters,
capacity_mode,
resolved,
)
.await?;
let items = if returning.is_some() {
Some(old_item.map(|item| vec![item]).unwrap_or_default())
} else {
None
};
Ok(StatementPage {
items,
size: capacity.old_size.unwrap_or(0),
capacity: Some(capacity),
..Default::default()
})
}
}
}
fn insert_nested_projection(result: &mut Item, path: &str, val: AttributeValue) {
let parts: Vec<&str> = path.split('.').collect();
let key = parts.last().unwrap();
result.insert(key.to_string(), val);
}
#[allow(clippy::too_many_arguments)]
async fn execute_select<S: StorageBackend>(
storage: &S,
table_name: &str,
index_name: Option<&str>,
projections: &[String],
where_clause: Option<&WhereClause>,
parameters: &[AttributeValue],
limit: Option<usize>,
next_token: Option<&str>,
consistent_read: bool,
resolved: Option<&ResolvedTable>,
) -> Result<SelectOutcome> {
let owned;
let resolved = match resolved {
Some(r) => r,
None => {
owned = ResolvedTable::load(storage, table_name).await?;
&owned
}
};
let meta = &resolved.meta;
let table_key_schema = &resolved.key_schema;
let index = index_name
.map(|name| resolve_index(meta, name))
.transpose()?;
if consistent_read && index.as_ref().is_some_and(|i| !i.is_lsi) {
return Err(DynoxideError::ValidationException(
"Strongly consistent read is not supported on Global Secondary Indexes".to_string(),
));
}
if let Some(idx) = index.as_ref() {
if !idx.is_lsi {
let missing: Vec<String> = projections
.iter()
.map(|p| root_attribute(p).to_string())
.filter(|attr| !idx.projects(attr, table_key_schema))
.collect();
if !missing.is_empty() {
return Err(DynoxideError::ValidationException(format!(
"One or more parameter values were invalid: Global secondary index {} \
does not project [{}]",
idx.name(),
missing.join(", ")
)));
}
}
if keys_index(where_clause, idx.pk_attr()) {
let missing: Vec<String> = where_attributes(where_clause)
.into_iter()
.filter(|attr| !idx.projects(attr, table_key_schema))
.collect();
if !missing.is_empty() {
return Err(DynoxideError::ValidationException(format!(
"One or more parameter values were invalid: Secondary index {} \
does not project one or more filter attributes: [{}]",
idx.name(),
missing.join(", ")
)));
}
}
}
let (read_pk, read_sk) = match index.as_ref() {
Some(idx) => (idx.pk_attr(), idx.sk_attr()),
None => (
table_key_schema.partition_key.as_str(),
table_key_schema.sort_key.as_deref(),
),
};
let fingerprint = statement_fingerprint(where_clause, parameters, index_name);
let cursor = next_token
.map(|token| decode_next_token(token, table_name, fingerprint))
.transpose()?;
if index.is_some()
&& cursor
.as_ref()
.is_some_and(|c| c.base_pk.is_none() || c.base_sk.is_none())
{
return Err(DynoxideError::ValidationException(
"Invalid NextToken".to_string(),
));
}
let reach_back = index.as_ref().is_some_and(|idx| {
idx.is_lsi
&& projections
.iter()
.any(|p| !idx.projects(root_attribute(p), table_key_schema))
});
let window = evaluate_window(
storage,
table_name,
index.as_ref(),
where_clause,
parameters,
read_pk,
read_sk,
table_key_schema,
cursor.as_ref(),
limit,
reach_back,
consistent_read,
)
.await?;
let token = match (limit, &window.last_evaluated) {
(Some(lim), Some(stop)) if window.evaluated >= lim => {
Some(encode_next_token(table_name, fingerprint, stop))
}
_ => None,
};
let base_read_units = window.base_read_units;
let items = window
.matched
.into_iter()
.map(|item| {
if projections.is_empty() {
item
} else {
let mut projected = HashMap::new();
for proj in projections {
if let Some(val) = resolve_nested_path(&item, proj) {
insert_nested_projection(&mut projected, proj, val.clone());
}
}
projected
}
})
.collect();
Ok(SelectOutcome {
items,
evaluated_bytes: window.evaluated_bytes,
next_token: token,
index,
base_read_units,
})
}
struct SelectOutcome {
items: Vec<Item>,
evaluated_bytes: usize,
next_token: Option<String>,
index: Option<ResolvedIndex>,
base_read_units: f64,
}
struct Window {
matched: Vec<Item>,
last_evaluated: Option<Cursor>,
evaluated: usize,
evaluated_bytes: usize,
base_read_units: f64,
}
#[allow(clippy::too_many_arguments)]
async fn evaluate_window<S: StorageBackend>(
storage: &S,
table_name: &str,
index: Option<&ResolvedIndex>,
where_clause: Option<&WhereClause>,
parameters: &[AttributeValue],
read_pk: &str,
read_sk: Option<&str>,
table_key_schema: &crate::actions::helpers::KeySchema,
cursor: Option<&Cursor>,
limit: Option<usize>,
reach_back: bool,
consistent_read: bool,
) -> Result<Window> {
let pk_condition = where_clause.and_then(|wc| find_pk_condition(wc, read_pk));
let rows: Vec<(String, String, String)> = if let Some(pk_cond) = pk_condition {
let pk_val = resolve_value(&pk_cond.value, parameters)?;
let pk_str = pk_val
.to_key_string()
.ok_or_else(|| DynoxideError::ValidationException("Invalid key value".to_string()))?;
let sk_conditions = match (read_sk, where_clause) {
(Some(sk_name), Some(wc)) => {
translate_sk_conditions(&wc.groups[0], sk_name, parameters)
}
_ => None,
}
.unwrap_or_default();
let (sk_condition_sql, sk_param_values) = sk_conditions_to_sql(&sk_conditions);
let sk_params_refs: Vec<&str> = sk_param_values.iter().map(|s| s.as_str()).collect();
let params = crate::storage::QueryParams {
sk_condition: sk_condition_sql.as_deref(),
sk_params: &sk_params_refs,
forward: true,
limit,
exclusive_start_sk: cursor.map(|c| c.sk.as_str()),
exclusive_start_base_pk: cursor.and_then(|c| c.base_pk.as_deref()),
exclusive_start_base_sk: cursor.and_then(|c| c.base_sk.as_deref()),
};
match index {
Some(idx) if idx.is_lsi => {
storage
.query_lsi_items(table_name, idx.name(), &pk_str, ¶ms)
.await?
}
Some(idx) => {
storage
.query_gsi_items(table_name, idx.name(), &pk_str, ¶ms)
.await?
}
None => storage.query_items(table_name, &pk_str, ¶ms).await?,
}
} else {
let params = crate::storage::ScanParams {
limit,
exclusive_start_pk: cursor.map(|c| c.pk.as_str()),
exclusive_start_sk: cursor.map(|c| c.sk.as_str()),
exclusive_start_base_pk: cursor.and_then(|c| c.base_pk.as_deref()),
exclusive_start_base_sk: cursor.and_then(|c| c.base_sk.as_deref()),
..Default::default()
};
match index {
Some(idx) if idx.is_lsi => {
storage
.scan_lsi_items(table_name, idx.name(), ¶ms)
.await?
}
Some(idx) => {
storage
.scan_gsi_items(table_name, idx.name(), ¶ms)
.await?
}
None => storage.scan_items(table_name, ¶ms).await?,
}
};
let evaluated = rows.len();
let last_evaluated = rows.last().map(|(pk, sk, json)| {
let (base_pk, base_sk) = if index.is_some() {
let item: Option<Item> = serde_json::from_str(json).ok();
let base_pk = item
.as_ref()
.and_then(|i| i.get(&table_key_schema.partition_key))
.and_then(|v| v.to_key_string());
let base_sk = match table_key_schema.sort_key.as_deref() {
Some(name) => item
.as_ref()
.and_then(|i| i.get(name))
.and_then(|v| v.to_key_string()),
None => Some(String::new()),
};
(base_pk, base_sk)
} else {
(None, None)
};
Cursor {
pk: pk.clone(),
sk: sk.clone(),
base_pk,
base_sk,
}
});
let mut evaluated_bytes = 0usize;
let mut base_read_units = 0f64;
let mut matched = Vec::new();
for (_, _, json) in rows {
let item: Item = serde_json::from_str(&json).map_err(|e| {
DynoxideError::InternalServerError(format!("Bad item JSON in storage: {e}"))
})?;
evaluated_bytes += crate::types::item_size(&item);
let full = if reach_back {
fetch_base_item(storage, table_name, table_key_schema, &item).await?
} else {
None
};
if let Some(ref full) = full {
base_read_units += crate::types::read_capacity_units_with_consistency(
crate::types::item_size(full),
consistent_read,
);
}
if matches_where(&item, where_clause, parameters) {
matched.push(full.unwrap_or(item));
}
}
Ok(Window {
matched,
last_evaluated,
evaluated,
evaluated_bytes,
base_read_units,
})
}
async fn fetch_base_item<S: StorageBackend>(
storage: &S,
table_name: &str,
table_key_schema: &crate::actions::helpers::KeySchema,
index_row: &Item,
) -> Result<Option<Item>> {
let pk = index_row
.get(&table_key_schema.partition_key)
.and_then(|v| v.to_key_string())
.unwrap_or_default();
let sk = match table_key_schema.sort_key.as_deref() {
Some(name) => index_row
.get(name)
.and_then(|v| v.to_key_string())
.unwrap_or_default(),
None => String::new(),
};
match storage.get_item(table_name, &pk, &sk).await? {
Some(json) => Ok(Some(serde_json::from_str::<Item>(&json).map_err(|e| {
DynoxideError::InternalServerError(format!("Bad item JSON in storage: {e}"))
})?)),
None => Ok(None),
}
}
struct Cursor {
pk: String,
sk: String,
base_pk: Option<String>,
base_sk: Option<String>,
}
struct ResolvedIndex {
def: crate::actions::gsi::IndexDef,
is_lsi: bool,
}
impl ResolvedIndex {
fn name(&self) -> &str {
&self.def.index_name
}
fn pk_attr(&self) -> &str {
&self.def.pk_attr
}
fn sk_attr(&self) -> Option<&str> {
self.def.sk_attr.as_deref()
}
}
impl ResolvedIndex {
fn projects(&self, attr: &str, table_keys: &crate::actions::helpers::KeySchema) -> bool {
self.def.projects(
attr,
&table_keys.partition_key,
table_keys.sort_key.as_deref(),
)
}
}
fn root_attribute(path: &str) -> &str {
let end = path.find(['.', '[']).unwrap_or(path.len());
&path[..end]
}
fn where_attributes(where_clause: Option<&WhereClause>) -> Vec<String> {
let Some(wc) = where_clause else {
return Vec::new();
};
let mut out = Vec::new();
for group in &wc.groups {
for condition in group {
let path = match condition {
WhereCondition::Comparison(c) | WhereCondition::NotComparison(c) => c.path.as_str(),
WhereCondition::Exists(p)
| WhereCondition::NotExists(p)
| WhereCondition::BeginsWith(p, _)
| WhereCondition::NotBeginsWith(p, _)
| WhereCondition::Between(p, _, _)
| WhereCondition::In(p, _)
| WhereCondition::Contains(p, _)
| WhereCondition::NotContains(p, _)
| WhereCondition::IsMissing(p)
| WhereCondition::IsNotMissing(p) => p.as_str(),
};
let root = root_attribute(path).to_string();
if !out.contains(&root) {
out.push(root);
}
}
}
out
}
fn resolve_index(meta: &crate::storage::TableMetadata, index_name: &str) -> Result<ResolvedIndex> {
if let Some(lsi) = crate::actions::lsi::parse_lsi_defs(meta)?
.into_iter()
.find(|l| l.index_name == index_name)
{
return Ok(ResolvedIndex {
def: lsi,
is_lsi: true,
});
}
if let Some(gsi) = crate::actions::gsi::parse_gsi_defs(meta)?
.into_iter()
.find(|g| g.index_name == index_name)
{
return Ok(ResolvedIndex {
def: gsi,
is_lsi: false,
});
}
if crate::actions::vector_index::parse_vector_defs(meta)?
.iter()
.any(|v| v.index_name == index_name)
{
return Err(DynoxideError::ValidationException(
"Scan operation not supported on this index type".to_string(),
));
}
Err(DynoxideError::ValidationException(
"The table does not have the specified index".to_string(),
))
}
fn statement_fingerprint(
where_clause: Option<&WhereClause>,
parameters: &[AttributeValue],
index_name: Option<&str>,
) -> u64 {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
match where_clause {
Some(wc) => format!("{:?}", wc.groups).hash(&mut hasher),
None => "no-where".hash(&mut hasher),
}
index_name.hash(&mut hasher);
serde_json::to_string(parameters)
.unwrap_or_default()
.hash(&mut hasher);
hasher.finish()
}
fn encode_next_token(table_name: &str, fingerprint: u64, stop: &Cursor) -> String {
use base64::Engine;
let payload = serde_json::json!({
"t": table_name,
"f": fingerprint,
"pk": stop.pk,
"sk": stop.sk,
"bpk": stop.base_pk,
"bsk": stop.base_sk,
})
.to_string();
base64::engine::general_purpose::STANDARD.encode(payload)
}
fn token_mismatch() -> DynoxideError {
DynoxideError::ValidationException("NextToken does not match request".to_string())
}
fn decode_next_token(token: &str, table_name: &str, fingerprint: u64) -> Result<Cursor> {
use base64::Engine;
let invalid = || DynoxideError::ValidationException("Invalid NextToken".to_string());
let raw = base64::engine::general_purpose::STANDARD
.decode(token)
.map_err(|_| invalid())?;
let value: serde_json::Value = serde_json::from_slice(&raw).map_err(|_| invalid())?;
if value["t"].as_str() != Some(table_name) || value["f"].as_u64() != Some(fingerprint) {
return Err(token_mismatch());
}
Ok(Cursor {
pk: value["pk"].as_str().ok_or_else(invalid)?.to_string(),
sk: value["sk"].as_str().ok_or_else(invalid)?.to_string(),
base_pk: value["bpk"].as_str().map(str::to_string),
base_sk: value["bsk"].as_str().map(str::to_string),
})
}
fn translate_sk_conditions(
group: &[WhereCondition],
sk_name: &str,
parameters: &[AttributeValue],
) -> Option<Vec<ResolvedSortKeyCondition>> {
let mut resolved = Vec::new();
for cond in group {
match cond {
WhereCondition::Comparison(c) if c.path == sk_name => {
let value = resolve_value(&c.value, parameters).ok()?;
value.to_key_string()?;
let sk = sk_name.to_string();
resolved.push(match c.op {
CompOp::Eq => ResolvedSortKeyCondition::Eq(sk, value),
CompOp::Lt => ResolvedSortKeyCondition::Lt(sk, value),
CompOp::Le => ResolvedSortKeyCondition::Le(sk, value),
CompOp::Gt => ResolvedSortKeyCondition::Gt(sk, value),
CompOp::Ge => ResolvedSortKeyCondition::Ge(sk, value),
CompOp::Ne => return None,
});
}
WhereCondition::Between(path, lo, hi) if path == sk_name => {
let lo = resolve_value(lo, parameters).ok()?;
let hi = resolve_value(hi, parameters).ok()?;
lo.to_key_string()?;
hi.to_key_string()?;
resolved.push(ResolvedSortKeyCondition::Between(
sk_name.to_string(),
lo,
hi,
));
}
WhereCondition::BeginsWith(path, prefix) if path == sk_name => {
let prefix = resolve_value(prefix, parameters).ok()?;
prefix.to_key_string()?;
resolved.push(ResolvedSortKeyCondition::BeginsWith(
sk_name.to_string(),
prefix,
));
}
WhereCondition::NotComparison(crate::partiql::parser::Condition { path, .. })
| WhereCondition::NotBeginsWith(path, _)
| WhereCondition::In(path, _)
| WhereCondition::Contains(path, _)
| WhereCondition::Exists(path)
| WhereCondition::NotExists(path)
| WhereCondition::IsMissing(path)
| WhereCondition::IsNotMissing(path)
if path == sk_name =>
{
return None;
}
_ => {}
}
}
Some(resolved)
}
fn keys_index(where_clause: Option<&WhereClause>, pk_name: &str) -> bool {
let Some(wc) = where_clause else {
return false;
};
if wc.groups.len() != 1 {
return false;
}
wc.groups[0].iter().any(|c| match c {
WhereCondition::Comparison(cond) => cond.path == pk_name && cond.op == CompOp::Eq,
WhereCondition::In(path, _) => path == pk_name,
_ => false,
})
}
fn find_pk_condition<'a>(
wc: &'a WhereClause,
pk_name: &str,
) -> Option<&'a crate::partiql::parser::Condition> {
if wc.groups.len() == 1 {
wc.groups[0].iter().find_map(|c| match c {
WhereCondition::Comparison(cond) if cond.path == pk_name && cond.op == CompOp::Eq => {
Some(cond)
}
_ => None,
})
} else {
None
}
}
#[non_exhaustive]
pub struct ResolvedTable {
pub meta: crate::storage::TableMetadata,
pub key_schema: crate::actions::helpers::KeySchema,
pub table_name: String,
}
impl ResolvedTable {
pub async fn load<S: StorageBackend>(storage: &S, table_name: &str) -> Result<Self> {
let meta = require_table(storage, table_name).await?;
let key_schema = crate::actions::helpers::parse_key_schema(&meta)?;
Ok(Self {
meta,
key_schema,
table_name: table_name.to_string(),
})
}
}
pub fn statement_target_in(
stmt: &Statement,
parameters: &[AttributeValue],
resolved: &ResolvedTable,
) -> Option<(String, String, String)> {
statement_key(stmt, parameters, &resolved.key_schema)
.map(|(pk, sk)| (resolved.table_name.clone(), pk, sk))
}
fn statement_key(
stmt: &Statement,
parameters: &[AttributeValue],
key_schema: &crate::actions::helpers::KeySchema,
) -> Option<(String, String)> {
let key_of = |source: &dyn Fn(&str) -> Option<AttributeValue>| -> Option<(String, String)> {
let pk = source(&key_schema.partition_key)?.to_key_string()?;
let sk = match key_schema.sort_key {
Some(ref name) => source(name)?.to_key_string()?,
None => String::new(),
};
Some((pk, sk))
};
let from_where = |where_clause: &Option<WhereClause>| -> Option<(String, String)> {
let wc = where_clause.as_ref()?;
key_of(&|name: &str| {
find_comparison_in_groups(&wc.groups, name)
.and_then(|cond| resolve_value(&cond.value, parameters).ok())
})
};
match stmt {
Statement::Insert { item, .. } => key_of(&|name: &str| {
item.get(name)
.and_then(|v| resolve_value(v, parameters).ok())
}),
Statement::Update { where_clause, .. }
| Statement::Delete { where_clause, .. }
| Statement::Select { where_clause, .. } => from_where(where_clause),
}
}
async fn execute_insert<S: StorageBackend>(
storage: &S,
table_name: &str,
item_template: &HashMap<String, PartiqlValue>,
parameters: &[AttributeValue],
if_not_exists: bool,
capacity_mode: Option<&str>,
resolved: Option<&ResolvedTable>,
) -> Result<WriteCapacity> {
let mut item = HashMap::new();
for (k, v) in item_template {
let resolved = match v {
PartiqlValue::Literal(av) => av.clone(),
PartiqlValue::Parameter(idx) => parameters.get(*idx).cloned().ok_or_else(|| {
DynoxideError::ValidationException(format!(
"Parameter index {idx} out of range (have {} parameters)",
parameters.len()
))
})?,
};
item.insert(k.clone(), resolved);
}
let owned;
let resolved = match resolved {
Some(r) => r,
None => {
owned = ResolvedTable::load(storage, table_name).await?;
&owned
}
};
let meta = &resolved.meta;
let key_schema = &resolved.key_schema;
crate::actions::helpers::validate_item_keys(&item, key_schema, meta)?;
crate::validation::validate_item_attribute_values(&item)?;
crate::validation::normalize_item_sets(&mut item);
let (pk, sk) = crate::actions::helpers::extract_key_strings(&item, key_schema)?;
let existing = storage.get_item(table_name, &pk, &sk).await?;
if existing.is_some() {
if if_not_exists {
return Ok(WriteCapacity::new(
table_name,
None,
None,
HashMap::new(),
HashMap::new(),
));
}
return Err(DynoxideError::DuplicateItemException(
"Duplicate primary key exists in table".to_string(),
));
}
let item_json = serde_json::to_string(&item)
.map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
let item_size = crate::types::item_size(&item);
if item_size > crate::types::MAX_ITEM_SIZE {
return Err(DynoxideError::ValidationException(
"Item size has exceeded the maximum allowed size".to_string(),
));
}
let hash_prefix = item
.get(&key_schema.partition_key)
.map(crate::storage::compute_hash_prefix)
.unwrap_or_default();
let old_json = storage
.put_item_with_hash(table_name, &pk, &sk, &item_json, item_size, &hash_prefix)
.await?;
let old_item: Option<Item> = old_json.as_ref().and_then(|j| serde_json::from_str(j).ok());
let target = crate::actions::gsi::IndexWrite {
table_name,
pk: &pk,
sk: &sk,
pk_attr: &key_schema.partition_key,
sk_attr: key_schema.sort_key.as_deref(),
old_item: old_item.as_ref(),
capacity_mode,
};
let gsi_units =
crate::actions::gsi::maintain_gsis_after_write(storage, meta, &target, &item).await?;
let lsi_units =
crate::actions::lsi::maintain_lsis_after_write(storage, meta, &target, &item).await?;
let vector_bytes = crate::actions::vector_index::maintain_vector_indexes_after_write(
storage, meta, &target, &item,
)
.await?;
crate::streams::record_stream_event(storage, meta, old_item.as_ref(), Some(&item)).await?;
Ok(WriteCapacity::new(
table_name,
old_item.as_ref().map(crate::types::item_size),
Some(item_size),
gsi_units,
lsi_units,
)
.with_vector_bytes(vector_bytes))
}
#[allow(clippy::too_many_arguments)]
async fn execute_update<S: StorageBackend>(
storage: &S,
table_name: &str,
set_clauses: &[crate::partiql::parser::SetClause],
remove_paths: &[String],
where_clause: Option<&WhereClause>,
parameters: &[AttributeValue],
returning: Option<ReturningVariant>,
capacity_mode: Option<&str>,
resolved: Option<&ResolvedTable>,
) -> Result<(Option<Item>, WriteCapacity)> {
let owned;
let resolved = match resolved {
Some(r) => r,
None => {
owned = ResolvedTable::load(storage, table_name).await?;
&owned
}
};
let meta = &resolved.meta;
let key_schema = &resolved.key_schema;
let wc = where_clause.ok_or_else(|| {
DynoxideError::ValidationException("UPDATE requires a WHERE clause".to_string())
})?;
let (pk_str, sk_str) = single_item_key("UPDATE", wc, key_schema, parameters)?;
let existing_json = storage.get_item(table_name, &pk_str, &sk_str).await?;
let mut item: Item = existing_json
.as_ref()
.and_then(|j| serde_json::from_str(j).ok())
.unwrap_or_default();
let old_item = item.clone();
if existing_json.is_none() || !matches_where(&old_item, where_clause, parameters) {
return Err(DynoxideError::ConditionalCheckFailedException(
"The conditional request failed".to_string(),
None,
));
}
let before_item = item.clone();
for clause in set_clauses {
let val = resolve_set_value(&clause.value, &item, parameters)?;
set_nested_value(&mut item, &clause.path, val)?;
}
for path in remove_paths {
remove_nested_value(&mut item, path);
}
if item.is_empty() {
return Ok((
None,
WriteCapacity::new(table_name, None, None, HashMap::new(), HashMap::new()),
));
}
crate::validation::validate_item_attribute_values(&item)?;
crate::validation::normalize_item_sets(&mut item);
crate::actions::helpers::validate_updated_index_keys(&before_item, &item, meta)?;
let item_json = serde_json::to_string(&item)
.map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
let item_size = crate::types::item_size(&item);
if item_size > crate::types::MAX_ITEM_SIZE {
return Err(DynoxideError::ValidationException(
"Item size to update has exceeded the maximum allowed size".to_string(),
));
}
let hash_prefix = item
.get(&key_schema.partition_key)
.map(crate::storage::compute_hash_prefix)
.unwrap_or_default();
storage
.put_item_with_hash(
table_name,
&pk_str,
&sk_str,
&item_json,
item_size,
&hash_prefix,
)
.await?;
let old_ref = existing_json.is_some().then_some(&old_item);
let target = crate::actions::gsi::IndexWrite {
table_name,
pk: &pk_str,
sk: &sk_str,
pk_attr: &key_schema.partition_key,
sk_attr: key_schema.sort_key.as_deref(),
old_item: old_ref,
capacity_mode,
};
let gsi_units =
crate::actions::gsi::maintain_gsis_after_write(storage, meta, &target, &item).await?;
let lsi_units =
crate::actions::lsi::maintain_lsis_after_write(storage, meta, &target, &item).await?;
let vector_bytes = crate::actions::vector_index::maintain_vector_indexes_after_write(
storage, meta, &target, &item,
)
.await?;
crate::streams::record_stream_event(storage, meta, old_ref, Some(&item)).await?;
let projection = returning.map(|variant| {
let modified: std::collections::BTreeSet<String> = set_clauses
.iter()
.map(|c| c.path.clone())
.chain(remove_paths.iter().cloned())
.collect();
project_returning(variant, &old_item, &item, &modified)
});
Ok((
projection,
WriteCapacity::new(
table_name,
old_ref.map(crate::types::item_size),
Some(item_size),
gsi_units,
lsi_units,
)
.with_vector_bytes(vector_bytes),
))
}
fn project_returning(
variant: ReturningVariant,
old_item: &Item,
new_item: &Item,
modified: &std::collections::BTreeSet<String>,
) -> Item {
match variant {
ReturningVariant::AllOld => old_item.clone(),
ReturningVariant::AllNew => new_item.clone(),
ReturningVariant::ModifiedOld => project_modified(modified, old_item),
ReturningVariant::ModifiedNew => project_modified(modified, new_item),
}
}
enum ProjNode {
Leaf(AttributeValue),
Map(HashMap<String, ProjNode>),
List(std::collections::BTreeMap<usize, ProjNode>),
}
fn project_modified(paths: &std::collections::BTreeSet<String>, source: &Item) -> Item {
let mut root: HashMap<String, ProjNode> = HashMap::new();
for path in paths {
if let (Some(val), Some(segments)) =
(resolve_nested_path(source, path), split_path_segments(path))
{
if let Some((PathSegment::Key(key), rest)) = segments.split_first() {
let node = root
.entry((*key).to_string())
.or_insert_with(|| fresh_proj_node(rest));
insert_proj_node(node, rest, val.clone());
}
}
}
root.into_iter()
.map(|(k, node)| (k, proj_node_to_value(node)))
.collect()
}
fn fresh_proj_node(segments: &[PathSegment]) -> ProjNode {
match segments.first() {
Some(PathSegment::Index(_)) => ProjNode::List(std::collections::BTreeMap::new()),
_ => ProjNode::Map(HashMap::new()),
}
}
fn insert_proj_node(node: &mut ProjNode, segments: &[PathSegment], val: AttributeValue) {
let Some((seg, rest)) = segments.split_first() else {
*node = ProjNode::Leaf(val);
return;
};
match seg {
PathSegment::Key(k) => {
if let ProjNode::Map(map) = node {
let child = map
.entry((*k).to_string())
.or_insert_with(|| fresh_proj_node(rest));
insert_proj_node(child, rest, val);
}
}
PathSegment::Index(i) => {
if let ProjNode::List(list) = node {
let child = list.entry(*i).or_insert_with(|| fresh_proj_node(rest));
insert_proj_node(child, rest, val);
}
}
}
}
fn proj_node_to_value(node: ProjNode) -> AttributeValue {
match node {
ProjNode::Leaf(v) => v,
ProjNode::Map(map) => AttributeValue::M(
map.into_iter()
.map(|(k, n)| (k, proj_node_to_value(n)))
.collect(),
),
ProjNode::List(list) => {
AttributeValue::L(list.into_values().map(proj_node_to_value).collect())
}
}
}
async fn execute_delete<S: StorageBackend>(
storage: &S,
table_name: &str,
where_clause: Option<&WhereClause>,
parameters: &[AttributeValue],
capacity_mode: Option<&str>,
resolved: Option<&ResolvedTable>,
) -> Result<(Option<Item>, WriteCapacity)> {
let owned;
let resolved = match resolved {
Some(r) => r,
None => {
owned = ResolvedTable::load(storage, table_name).await?;
&owned
}
};
let meta = &resolved.meta;
let key_schema = &resolved.key_schema;
let wc = where_clause.ok_or_else(|| {
DynoxideError::ValidationException("DELETE requires a WHERE clause".to_string())
})?;
let (pk_str, sk_str) = single_item_key("DELETE", wc, key_schema, parameters)?;
if let Some(json) = storage.get_item(table_name, &pk_str, &sk_str).await? {
let existing: Item = serde_json::from_str(&json)
.map_err(|e| DynoxideError::InternalServerError(format!("Bad item JSON: {e}")))?;
if !matches_where(&existing, where_clause, parameters) {
return Err(DynoxideError::ConditionalCheckFailedException(
"The conditional request failed".to_string(),
None,
));
}
}
let old_json = storage.delete_item(table_name, &pk_str, &sk_str).await?;
let old_item: Option<Item> = old_json.as_ref().and_then(|j| serde_json::from_str(j).ok());
let target = crate::actions::gsi::IndexWrite {
table_name,
pk: &pk_str,
sk: &sk_str,
pk_attr: &key_schema.partition_key,
sk_attr: key_schema.sort_key.as_deref(),
old_item: old_item.as_ref(),
capacity_mode,
};
let gsi_units = crate::actions::gsi::maintain_gsis_after_delete(storage, meta, &target).await?;
let lsi_units = crate::actions::lsi::maintain_lsis_after_delete(storage, meta, &target).await?;
let vector_bytes =
crate::actions::vector_index::maintain_vector_indexes_after_delete(storage, meta, &target)
.await?;
if old_item.is_some() {
crate::streams::record_stream_event(storage, meta, old_item.as_ref(), None).await?;
}
let capacity =
WriteCapacity::from_items(table_name, old_item.as_ref(), None, gsi_units, lsi_units)
.with_vector_bytes(vector_bytes);
Ok((old_item, capacity))
}
async fn require_table<S: StorageBackend>(
storage: &S,
table_name: &str,
) -> Result<crate::storage::TableMetadata> {
crate::actions::helpers::require_table(storage, table_name).await
}
fn single_item_key(
verb: &str,
wc: &WhereClause,
key_schema: &crate::actions::helpers::KeySchema,
parameters: &[AttributeValue],
) -> Result<(String, String)> {
let unpinned = || {
DynoxideError::ValidationException(
"Where clause does not contain a mandatory equality on all key attributes".to_string(),
)
};
let expands = || {
DynoxideError::ValidationException(if wc.wrote_or {
format!("{verb} does not support OR conditions in WHERE clause")
} else {
format!("{verb} requires a WHERE clause that identifies a single item")
})
};
let refusal = || {
if wc.groups.len() > 1 {
expands()
} else {
unpinned()
}
};
let mut pinned: Option<(String, String)> = None;
for group in &wc.groups {
let pk_cond = find_comparison(group, &key_schema.partition_key).ok_or_else(refusal)?;
let pk = resolve_value(&pk_cond.value, parameters)?
.to_key_string()
.ok_or_else(|| DynoxideError::ValidationException("Invalid key value".to_string()))?;
let sk = match key_schema.sort_key {
Some(ref sk_name) => {
let sk_cond = find_comparison(group, sk_name).ok_or_else(refusal)?;
resolve_value(&sk_cond.value, parameters)?
.to_key_string()
.unwrap_or_default()
}
None => String::new(),
};
let key = (pk, sk);
match pinned {
Some(ref already) => {
if *already != key {
return Err(expands());
}
}
None => pinned = Some(key),
}
}
pinned.ok_or_else(unpinned)
}
fn find_comparison_in_groups<'a>(
groups: &'a [Vec<WhereCondition>],
path: &str,
) -> Option<&'a crate::partiql::parser::Condition> {
for group in groups {
if let Some(cond) = find_comparison(group, path) {
return Some(cond);
}
}
None
}
fn find_comparison<'a>(
conditions: &'a [WhereCondition],
path: &str,
) -> Option<&'a crate::partiql::parser::Condition> {
conditions.iter().find_map(|c| match c {
WhereCondition::Comparison(cond) if cond.path == path && cond.op == CompOp::Eq => {
Some(cond)
}
_ => None,
})
}
fn resolve_value(val: &PartiqlValue, parameters: &[AttributeValue]) -> Result<AttributeValue> {
match val {
PartiqlValue::Literal(av) => Ok(av.clone()),
PartiqlValue::Parameter(idx) => parameters.get(*idx).cloned().ok_or_else(|| {
DynoxideError::ValidationException(format!(
"Parameter index {idx} out of range (have {} parameters)",
parameters.len()
))
}),
}
}
fn resolve_set_value(
val: &SetValue,
item: &Item,
parameters: &[AttributeValue],
) -> Result<AttributeValue> {
match val {
SetValue::Simple(pv) => resolve_value(pv, parameters),
SetValue::Add(attr, pv) => {
let current = resolve_nested_path(item, attr);
let operand = resolve_value(pv, parameters)?;
match (current, &operand) {
(Some(AttributeValue::N(cur)), AttributeValue::N(add)) => {
use bigdecimal::BigDecimal;
use std::str::FromStr;
let a = BigDecimal::from_str(cur).map_err(|e| {
DynoxideError::ValidationException(format!("Invalid number: {e}"))
})?;
let b = BigDecimal::from_str(add).map_err(|e| {
DynoxideError::ValidationException(format!("Invalid number: {e}"))
})?;
let result = a + b;
Ok(AttributeValue::N(format_bigdecimal(&result)))
}
(None, AttributeValue::N(_)) => {
Ok(operand)
}
_ => Err(DynoxideError::ValidationException(
"SET expression add requires numeric attribute and operand".to_string(),
)),
}
}
SetValue::Sub(attr, pv) => {
let current = resolve_nested_path(item, attr);
let operand = resolve_value(pv, parameters)?;
match (current, &operand) {
(Some(AttributeValue::N(cur)), AttributeValue::N(sub)) => {
use bigdecimal::BigDecimal;
use std::str::FromStr;
let a = BigDecimal::from_str(cur).map_err(|e| {
DynoxideError::ValidationException(format!("Invalid number: {e}"))
})?;
let b = BigDecimal::from_str(sub).map_err(|e| {
DynoxideError::ValidationException(format!("Invalid number: {e}"))
})?;
let result = a - b;
Ok(AttributeValue::N(format_bigdecimal(&result)))
}
(None, AttributeValue::N(sub)) => {
use bigdecimal::BigDecimal;
use std::str::FromStr;
let b = BigDecimal::from_str(sub).map_err(|e| {
DynoxideError::ValidationException(format!("Invalid number: {e}"))
})?;
let result = -b;
Ok(AttributeValue::N(format_bigdecimal(&result)))
}
_ => Err(DynoxideError::ValidationException(
"SET expression subtract requires numeric attribute and operand".to_string(),
)),
}
}
SetValue::ListAppend(first, second) => {
let a = resolve_value(first, parameters)?;
let b = resolve_value(second, parameters)?;
let list_a = match &a {
AttributeValue::S(name) => resolve_nested_path(item, name)
.cloned()
.unwrap_or(AttributeValue::L(Vec::new())),
other => other.clone(),
};
let list_b = match &b {
AttributeValue::S(name) => resolve_nested_path(item, name)
.cloned()
.unwrap_or(AttributeValue::L(Vec::new())),
other => other.clone(),
};
match (list_a, list_b) {
(AttributeValue::L(mut la), AttributeValue::L(lb)) => {
la.extend(lb);
Ok(AttributeValue::L(la))
}
_ => Err(DynoxideError::ValidationException(
"list_append requires list operands".to_string(),
)),
}
}
}
}
fn invalid_update_path() -> DynoxideError {
DynoxideError::ValidationException(
"The document path provided in the update expression is invalid for update".to_string(),
)
}
fn set_nested_value(item: &mut Item, path: &str, val: AttributeValue) -> Result<()> {
let segments = split_path_segments(path).ok_or_else(invalid_update_path)?;
let (first, rest) = segments.split_first().ok_or_else(invalid_update_path)?;
let key = match first {
PathSegment::Key(k) => (*k).to_string(),
PathSegment::Index(_) => return Err(invalid_update_path()),
};
if rest.is_empty() {
item.insert(key, val);
return Ok(());
}
let entry = item
.entry(key)
.or_insert_with(|| AttributeValue::M(HashMap::new()));
set_into_value(entry, rest, val)
}
fn set_into_value(
current: &mut AttributeValue,
segments: &[PathSegment],
val: AttributeValue,
) -> Result<()> {
let (seg, rest) = segments.split_first().expect("segments is non-empty");
if rest.is_empty() {
return match seg {
PathSegment::Key(k) => match current {
AttributeValue::M(map) => {
map.insert((*k).to_string(), val);
Ok(())
}
_ => Err(invalid_update_path()),
},
PathSegment::Index(i) => match current {
AttributeValue::L(list) => {
if *i < list.len() {
list[*i] = val;
} else {
list.push(val);
}
Ok(())
}
_ => Err(invalid_update_path()),
},
};
}
match seg {
PathSegment::Key(k) => match current {
AttributeValue::M(map) => {
let next = map
.entry((*k).to_string())
.or_insert_with(|| AttributeValue::M(HashMap::new()));
set_into_value(next, rest, val)
}
_ => Err(invalid_update_path()),
},
PathSegment::Index(i) => match current {
AttributeValue::L(list) => match list.get_mut(*i) {
Some(next) => set_into_value(next, rest, val),
None => Err(invalid_update_path()),
},
_ => Err(invalid_update_path()),
},
}
}
fn remove_nested_value(item: &mut Item, path: &str) {
let Some(segments) = split_path_segments(path) else {
return;
};
let Some((first, rest)) = segments.split_first() else {
return;
};
let PathSegment::Key(key) = first else {
return; };
if rest.is_empty() {
item.remove(*key);
return;
}
if let Some(current) = item.get_mut(*key) {
remove_from_value(current, rest);
}
}
fn remove_from_value(current: &mut AttributeValue, segments: &[PathSegment]) {
let (seg, rest) = segments.split_first().expect("segments is non-empty");
if rest.is_empty() {
match seg {
PathSegment::Key(k) => {
if let AttributeValue::M(map) = current {
map.remove(*k);
}
}
PathSegment::Index(i) => {
if let AttributeValue::L(list) = current
&& *i < list.len()
{
list.remove(*i);
}
}
}
return;
}
match seg {
PathSegment::Key(k) => {
if let AttributeValue::M(map) = current
&& let Some(next) = map.get_mut(*k)
{
remove_from_value(next, rest);
}
}
PathSegment::Index(i) => {
if let AttributeValue::L(list) = current
&& let Some(next) = list.get_mut(*i)
{
remove_from_value(next, rest);
}
}
}
}
fn matches_where(
item: &Item,
where_clause: Option<&WhereClause>,
parameters: &[AttributeValue],
) -> bool {
let wc = match where_clause {
Some(wc) => wc,
None => return true,
};
wc.groups
.iter()
.any(|group| matches_conditions(item, group, parameters))
}
fn matches_conditions(
item: &Item,
conditions: &[WhereCondition],
parameters: &[AttributeValue],
) -> bool {
for cond in conditions {
match cond {
WhereCondition::Comparison(c) => {
let item_val = match resolve_nested_path(item, &c.path) {
Some(v) => v,
None => return false,
};
let target = match resolve_value(&c.value, parameters) {
Ok(v) => v,
Err(_) => return false,
};
if !compare_values(item_val, &c.op, &target) {
return false;
}
}
WhereCondition::NotComparison(c) => {
if let Some(item_val) = resolve_nested_path(item, &c.path) {
let target = match resolve_value(&c.value, parameters) {
Ok(v) => v,
Err(_) => return false,
};
if compare_values(item_val, &c.op, &target) {
return false;
}
}
}
WhereCondition::Exists(path) | WhereCondition::IsNotMissing(path) => {
if resolve_nested_path(item, path).is_none() {
return false;
}
}
WhereCondition::NotExists(path) | WhereCondition::IsMissing(path) => {
if resolve_nested_path(item, path).is_some() {
return false;
}
}
WhereCondition::BeginsWith(path, prefix_val) => {
let item_val = match resolve_nested_path(item, path) {
Some(v) => v,
None => return false,
};
let prefix = match resolve_value(prefix_val, parameters) {
Ok(v) => v,
Err(_) => return false,
};
match (item_val, &prefix) {
(AttributeValue::S(s), AttributeValue::S(p)) => {
if !s.starts_with(p.as_str()) {
return false;
}
}
_ => return false,
}
}
WhereCondition::NotBeginsWith(path, prefix_val) => {
if let Some(item_val) = resolve_nested_path(item, path) {
let prefix = match resolve_value(prefix_val, parameters) {
Ok(v) => v,
Err(_) => return false,
};
if let (AttributeValue::S(s), AttributeValue::S(p)) = (item_val, &prefix)
&& s.starts_with(p.as_str())
{
return false;
}
}
}
WhereCondition::Between(path, low, high) => {
let item_val = match resolve_nested_path(item, path) {
Some(v) => v,
None => return false,
};
let low_val = match resolve_value(low, parameters) {
Ok(v) => v,
Err(_) => return false,
};
let high_val = match resolve_value(high, parameters) {
Ok(v) => v,
Err(_) => return false,
};
if !compare_values(item_val, &CompOp::Ge, &low_val)
|| !compare_values(item_val, &CompOp::Le, &high_val)
{
return false;
}
}
WhereCondition::In(path, values) => {
let item_val = match resolve_nested_path(item, path) {
Some(v) => v,
None => return false,
};
let matched = values.iter().any(|v| {
resolve_value(v, parameters)
.map(|target| compare_values(item_val, &CompOp::Eq, &target))
.unwrap_or(false)
});
if !matched {
return false;
}
}
WhereCondition::Contains(path, substr_val) => {
if !contains_value(item, path, substr_val, parameters) {
return false;
}
}
WhereCondition::NotContains(path, substr_val) => {
if contains_value(item, path, substr_val, parameters) {
return false;
}
}
}
}
true
}
fn contains_value(
item: &Item,
path: &str,
substr_val: &PartiqlValue,
parameters: &[AttributeValue],
) -> bool {
let Some(item_val) = resolve_nested_path(item, path) else {
return false;
};
let Ok(substr) = resolve_value(substr_val, parameters) else {
return false;
};
match (item_val, &substr) {
(AttributeValue::S(s), AttributeValue::S(sub)) => s.contains(sub.as_str()),
(AttributeValue::SS(set), AttributeValue::S(val)) => set.contains(val),
(AttributeValue::NS(set), AttributeValue::N(val)) => set.contains(val),
(AttributeValue::L(list), target) => list.contains(target),
_ => false,
}
}
fn resolve_nested_path<'a>(item: &'a Item, path: &str) -> Option<&'a AttributeValue> {
if !path.contains('.') && !path.contains('[') {
return item.get(path);
}
let segments = split_path_segments(path)?;
if segments.is_empty() {
return None;
}
let mut current = match &segments[0] {
PathSegment::Key(k) => item.get(*k)?,
PathSegment::Index(_) => return None,
};
for seg in &segments[1..] {
current = match seg {
PathSegment::Key(k) => match current {
AttributeValue::M(map) => map.get(*k)?,
_ => return None,
},
PathSegment::Index(idx) => match current {
AttributeValue::L(list) => list.get(*idx)?,
_ => return None,
},
};
}
Some(current)
}
enum PathSegment<'a> {
Key(&'a str),
Index(usize),
}
fn split_path_segments(path: &str) -> Option<Vec<PathSegment<'_>>> {
let mut segments = Vec::new();
let bytes = path.as_bytes();
let mut start = 0;
let mut i = 0;
while i < bytes.len() {
match bytes[i] {
b'.' => {
if start < i {
segments.push(PathSegment::Key(&path[start..i]));
}
i += 1;
start = i;
}
b'[' => {
if start < i {
segments.push(PathSegment::Key(&path[start..i]));
}
i += 1;
let idx_start = i;
while i < bytes.len() && bytes[i] != b']' {
i += 1;
}
let idx = path[idx_start..i].parse::<usize>().ok()?;
segments.push(PathSegment::Index(idx));
if i < bytes.len() {
i += 1; }
start = i;
if i < bytes.len() && bytes[i] == b'.' {
i += 1;
start = i;
}
}
_ => {
i += 1;
}
}
}
if start < bytes.len() {
segments.push(PathSegment::Key(&path[start..]));
}
Some(segments)
}
fn format_bigdecimal(n: &bigdecimal::BigDecimal) -> String {
let normalized = n.normalized();
if normalized.as_bigint_and_exponent().1 < 0 {
normalized.with_scale(0).to_string()
} else {
normalized.to_string()
}
}