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, Operand, OrderByTerm, PartiqlValue, ReturningVariant, SetValue, Statement, WhereClause,
WhereCondition,
};
use crate::partiql::{PathSegment, names_attribute, split_path_segments};
use crate::storage_backend::StorageBackend;
use crate::types::{AttributeValue, Item, ScalarAttributeType};
use std::collections::HashMap;
use std::sync::Arc;
#[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 key_string(value: &AttributeValue, key_name: &str) -> Result<String> {
value.to_key_string().ok_or_else(|| {
DynoxideError::ValidationException(format!(
"Key value must be of type S, N, or B. Key name: {key_name}, Key type: {}",
value.type_name()
))
})
}
#[derive(Debug)]
pub(crate) enum KeyFault {
NotAKeyType(String),
WrongType(String),
}
impl From<KeyFault> for DynoxideError {
fn from(fault: KeyFault) -> Self {
match fault {
KeyFault::NotAKeyType(message) | KeyFault::WrongType(message) => {
DynoxideError::ValidationException(message)
}
}
}
}
fn typed_key(
value: &AttributeValue,
key_name: &str,
declared: Option<&ScalarAttributeType>,
) -> std::result::Result<String, KeyFault> {
let Some(stored) = value.to_key_string() else {
return Err(KeyFault::NotAKeyType(format!(
"Key value must be of type S, N, or B. Key name: {key_name}, Key type: {}",
value.type_name()
)));
};
let matches = match declared {
None => true,
Some(ScalarAttributeType::S) => matches!(value, AttributeValue::S(_)),
Some(ScalarAttributeType::N) => matches!(value, AttributeValue::N(_)),
Some(ScalarAttributeType::B) => matches!(value, AttributeValue::B(_)),
};
if !matches {
return Err(KeyFault::WrongType(format!(
"Key attribute's data type should match its data type in table's schema: Key {key_name}"
)));
}
Ok(stored)
}
fn declared_attribute_type(
meta: &crate::storage::TableMetadata,
attr: &str,
) -> Option<ScalarAttributeType> {
let defs: Vec<crate::types::AttributeDefinition> =
serde_json::from_str(&meta.attribute_definitions).ok()?;
defs.into_iter()
.find(|d| d.attribute_name == attr)
.map(|d| d.attribute_type)
}
struct KeyCondition {
op: &'static str,
values: Vec<AttributeValue>,
}
impl PartialEq for KeyCondition {
fn eq(&self, other: &Self) -> bool {
self.op == other.op
&& if self.op == "=" {
self.values.len() == other.values.len()
&& self.values.iter().all(|v| other.values.contains(v))
} else {
self.values == other.values
}
}
}
fn key_conditions(
group: &[Arc<WhereCondition>],
key: &str,
sort_key: bool,
parameters: &[AttributeValue],
) -> Option<Vec<KeyCondition>> {
let resolve = |values: &[&PartiqlValue]| -> Option<Vec<AttributeValue>> {
values
.iter()
.map(|v| resolve_value(v, parameters).ok())
.collect()
};
let mut found: Vec<KeyCondition> = Vec::new();
for cond in group {
let (op, values): (&'static str, Vec<&PartiqlValue>) = match &**cond {
WhereCondition::Comparison(c) if names_attribute(&c.path, key) => match c.op {
CompOp::Eq => ("=", vec![&c.value]),
CompOp::Lt if sort_key => ("<", vec![&c.value]),
CompOp::Le if sort_key => ("<=", vec![&c.value]),
CompOp::Gt if sort_key => (">", vec![&c.value]),
CompOp::Ge if sort_key => (">=", vec![&c.value]),
_ => continue,
},
WhereCondition::In(path, values) if names_attribute(path, key) => {
("=", values.iter().collect())
}
WhereCondition::Between(path, lo, hi) if sort_key && names_attribute(path, key) => {
("BETWEEN", vec![lo, hi])
}
WhereCondition::BeginsWith(path, prefix) if sort_key && names_attribute(path, key) => {
("begins_with", vec![prefix])
}
_ => continue,
};
let mut resolved = resolve(&values)?;
if op == "=" {
resolved = first_of_each(resolved, |value| {
let mut key = Vec::new();
encode_scalar(value, &mut key).then_some(key)
});
}
found.push(KeyCondition {
op,
values: resolved,
});
}
Some(first_of_each(found, |condition| {
let mut encoded = Vec::with_capacity(condition.values.len());
for value in &condition.values {
let mut one = Vec::new();
if !encode_scalar(value, &mut one) {
return None;
}
encoded.push(one);
}
if condition.op == "=" {
encoded.sort_unstable();
}
let mut key = condition.op.as_bytes().to_vec();
for one in encoded {
key.extend_from_slice(&one);
}
Some(key)
}))
}
fn first_of_each<T: PartialEq>(
items: impl IntoIterator<Item = T>,
key: impl Fn(&T) -> Option<Vec<u8>>,
) -> Vec<T> {
let mut seen = std::collections::HashSet::new();
let mut out: Vec<T> = Vec::new();
let mut unkeyed: Vec<usize> = Vec::new();
for item in items {
let fresh = match key(&item) {
Some(k) => seen.insert(k),
None => !unkeyed.iter().any(|&i| out[i] == item),
};
if fresh {
if key(&item).is_none() {
unkeyed.push(out.len());
}
out.push(item);
}
}
out
}
fn encode_scalar(value: &AttributeValue, key: &mut Vec<u8>) -> bool {
let (tag, bytes) = match value {
AttributeValue::S(s) => (b'S', s.as_bytes()),
AttributeValue::N(n) => (b'N', n.as_bytes()),
AttributeValue::B(b) => (b'B', b.as_slice()),
_ => return false,
};
key.push(tag);
key.extend_from_slice(&(bytes.len() as u64).to_le_bytes());
key.extend_from_slice(bytes);
true
}
fn check_read_key_types(
where_clause: Option<&WhereClause>,
parameters: &[AttributeValue],
read_pk: &str,
read_sk: Option<&str>,
declared: &dyn Fn(&str) -> Option<ScalarAttributeType>,
) -> Result<()> {
let Some(wc) = where_clause else {
return Ok(());
};
for group in &wc.groups {
let Some(pk) = key_conditions(group, read_pk, false, parameters) else {
return Ok(());
};
let pk = match pk.as_slice() {
[one] => one,
_ => return Ok(()),
};
let sk = match read_sk {
Some(sk_name) => {
let Some(conditions) = key_conditions(group, sk_name, true, parameters) else {
return Ok(());
};
if conditions.len() > 1 {
return Ok(());
}
conditions.into_iter().next().map(|c| (sk_name, c.values))
}
None => None,
};
for (name, values) in std::iter::once((read_pk, pk.values.clone())).chain(sk) {
let declared = declared(name);
for value in &values {
typed_key(value, name, declared.as_ref())?;
}
}
}
Ok(())
}
pub(crate) enum KeyedSelect {
ByPrimaryKey,
NotByPrimaryKey,
KeyType(String),
}
pub(crate) fn keyed_select(
stmt: &Statement,
parameters: &[AttributeValue],
resolved: &ResolvedTable,
) -> KeyedSelect {
let Statement::Select {
where_clause: Some(wc),
..
} = stmt
else {
return KeyedSelect::NotByPrimaryKey;
};
let [group] = wc.groups.as_slice() else {
return KeyedSelect::NotByPrimaryKey;
};
let schema = &resolved.key_schema;
let keys: Vec<(&str, Option<&ScalarAttributeType>)> = std::iter::once((
schema.partition_key.as_str(),
Some(&schema.partition_key_type),
))
.chain(
schema
.sort_key
.as_deref()
.map(|sk| (sk, schema.sort_key_type.as_ref())),
)
.collect();
let names_a_key = |path: &str| keys.iter().any(|(name, _)| names_attribute(path, name));
let single = group.iter().all(|cond| match &**cond {
WhereCondition::Comparison(c) => c.op == CompOp::Eq && names_a_key(&c.path),
WhereCondition::In(path, values) => values.len() == 1 && names_a_key(path),
_ => false,
});
if !single {
return KeyedSelect::NotByPrimaryKey;
}
let mut values = Vec::with_capacity(keys.len());
for (name, declared) in &keys {
let Some(conditions) = key_conditions(group, name, false, parameters) else {
return KeyedSelect::NotByPrimaryKey;
};
let [KeyCondition { values: named, .. }] = conditions.as_slice() else {
return KeyedSelect::NotByPrimaryKey;
};
let [value] = named.as_slice() else {
return KeyedSelect::NotByPrimaryKey;
};
values.push((*name, *declared, value.clone()));
}
for (name, declared, value) in values {
if let Err(KeyFault::NotAKeyType(message) | KeyFault::WrongType(message)) =
typed_key(&value, name, declared)
{
return KeyedSelect::KeyType(message);
}
}
KeyedSelect::ByPrimaryKey
}
pub(crate) fn reject_overlapping_select(
stmt: &Statement,
parameters: &[AttributeValue],
resolved: &ResolvedTable,
) -> Result<()> {
let Statement::Select { where_clause, .. } = stmt else {
return Ok(());
};
reject_overlapping_key_conditions(
where_clause.as_ref(),
parameters,
&resolved.key_schema.partition_key,
resolved.key_schema.sort_key.as_deref(),
)
}
pub(crate) fn write_key_fault(
stmt: &Statement,
parameters: &[AttributeValue],
resolved: &ResolvedTable,
) -> Option<KeyFault> {
let (Statement::Update {
where_clause: Some(wc),
..
}
| Statement::Delete {
where_clause: Some(wc),
..
}) = stmt
else {
return None;
};
let schema = &resolved.key_schema;
let keys = std::iter::once((&schema.partition_key, Some(&schema.partition_key_type))).chain(
schema
.sort_key
.as_ref()
.map(|sk| (sk, schema.sort_key_type.as_ref())),
);
let keys: Vec<_> = keys.collect();
for group in &wc.groups {
for (name, declared) in &keys {
let Some(cond) = find_comparison(group, name) else {
continue;
};
let Ok(value) = resolve_value(&cond.value, parameters) else {
continue;
};
if let Err(fault) = typed_key(&value, name, *declared) {
return Some(fault);
}
}
}
None
}
pub(crate) fn check_transaction_insert_keys(
stmt: &Statement,
parameters: &[AttributeValue],
resolved: &ResolvedTable,
) -> Result<()> {
let Statement::Insert { item, .. } = stmt else {
return Ok(());
};
let item = resolve_insert_item(item, parameters)?;
crate::actions::helpers::validate_item_keys(&item, &resolved.key_schema, &resolved.meta)
}
pub(crate) fn delete_has_condition(stmt: &Statement, resolved: &ResolvedTable) -> bool {
let Statement::Delete {
where_clause: Some(wc),
..
} = stmt
else {
return false;
};
let schema = &resolved.key_schema;
let is_key = |path: &str| {
names_attribute(path, &schema.partition_key)
|| schema
.sort_key
.as_deref()
.is_some_and(|sk| names_attribute(path, sk))
};
let names_key = |condition: &WhereCondition| match condition {
WhereCondition::Comparison(c) => c.op == CompOp::Eq && is_key(&c.path),
WhereCondition::In(path, values) => values.len() == 1 && is_key(path),
_ => false,
};
!matches!(wc.groups.as_slice(), [group] if group.iter().all(|c| names_key(c)))
}
pub(crate) fn check_parameter_value(value: &AttributeValue) -> Result<()> {
crate::validation::validate_parameter_value(value)
}
#[derive(PartialEq)]
enum SortKeyAtom {
Points(Vec<String>),
Above(String, bool),
Below(String, bool),
Prefix(String),
}
fn reject_overlapping_key_conditions(
where_clause: Option<&WhereClause>,
parameters: &[AttributeValue],
read_pk: &str,
read_sk: Option<&str>,
) -> Result<()> {
let overlap = || {
DynoxideError::ValidationException(
"Overlapping conditions with range keys are not supported in where clause".to_string(),
)
};
let Some(wc) = where_clause else {
return Ok(());
};
let key = |value: &PartiqlValue| {
resolve_value(value, parameters)
.ok()
.and_then(|av| av.to_key_string())
};
let keys_of = |values: &[PartiqlValue]| -> Result<Vec<String>> {
let keys: Vec<String> = values.iter().filter_map(key).collect();
let mut seen = std::collections::HashSet::new();
if keys.iter().any(|k| !seen.insert(k)) {
return Err(overlap());
}
Ok(keys)
};
let groups = key_groups(wc, read_pk, read_sk);
let mut branches = Vec::with_capacity(groups.len());
for group in &groups {
let mut partitions: Option<Vec<String>> = None;
let mut narrow = |keys: Vec<String>| {
partitions = Some(match partitions.take() {
None => keys,
Some(held) => {
let wanted: std::collections::HashSet<&str> =
keys.iter().map(String::as_str).collect();
held.into_iter()
.filter(|k| wanted.contains(k.as_str()))
.collect()
}
});
};
let mut conditions: Vec<Vec<SortKeyAtom>> = Vec::new();
let mut seen_conditions = std::collections::HashSet::new();
let mut add = |atoms: Vec<SortKeyAtom>| {
if seen_conditions.insert(encode_atoms(&atoms)) {
conditions.push(atoms);
}
};
for cond in group {
match &**cond {
WhereCondition::Comparison(c)
if names_attribute(&c.path, read_pk) && c.op == CompOp::Eq =>
{
narrow(key(&c.value).into_iter().collect());
}
WhereCondition::In(path, values) if names_attribute(path, read_pk) => {
narrow(keys_of(values)?)
}
WhereCondition::Comparison(c)
if read_sk.is_some_and(|sk| names_attribute(&c.path, sk)) =>
{
if let Some(v) = key(&c.value) {
match c.op {
CompOp::Eq => add(vec![SortKeyAtom::Points(vec![v])]),
CompOp::Lt => add(vec![SortKeyAtom::Below(v, false)]),
CompOp::Le => add(vec![SortKeyAtom::Below(v, true)]),
CompOp::Gt => add(vec![SortKeyAtom::Above(v, false)]),
CompOp::Ge => add(vec![SortKeyAtom::Above(v, true)]),
CompOp::Ne => return Ok(()),
}
}
}
WhereCondition::Between(path, lo, hi)
if read_sk.is_some_and(|sk| names_attribute(path, sk)) =>
{
if let (Some(lo), Some(hi)) = (key(lo), key(hi)) {
add(vec![
SortKeyAtom::Above(lo, true),
SortKeyAtom::Below(hi, true),
]);
}
}
WhereCondition::In(path, values)
if read_sk.is_some_and(|sk| names_attribute(path, sk)) =>
{
add(vec![SortKeyAtom::Points(keys_of(values)?)]);
}
WhereCondition::BeginsWith(path, prefix)
if read_sk.is_some_and(|sk| names_attribute(path, sk)) =>
{
if let Some(prefix) = key(prefix) {
add(vec![SortKeyAtom::Prefix(prefix)]);
}
}
WhereCondition::NotBetween(path, _, _)
if read_sk.is_some_and(|sk| names_attribute(path, sk)) =>
{
return Ok(());
}
WhereCondition::NotComparison(c)
if read_sk.is_some_and(|sk| names_attribute(&c.path, sk)) =>
{
return Ok(());
}
WhereCondition::NotBeginsWith(path, _)
if read_sk.is_some_and(|sk| names_attribute(path, sk)) =>
{
return Ok(());
}
_ => {}
}
}
if conditions.len() > 1 {
return Ok(());
}
let atoms = conditions.into_iter().next().unwrap_or_default();
let Some(partitions) = partitions else {
return Ok(());
};
branches.push((partitions, atoms));
}
for (i, (partitions, atoms)) in branches.iter().enumerate() {
for (other_partitions, other_atoms) in &branches[i + 1..] {
let shared = partitions.iter().any(|p| other_partitions.contains(p));
let both: Vec<&SortKeyAtom> = atoms.iter().chain(other_atoms).collect();
if shared && sort_keys_meet(&both) {
return Err(overlap());
}
}
}
Ok(())
}
fn key_groups(
wc: &WhereClause,
read_pk: &str,
read_sk: Option<&str>,
) -> Vec<Vec<Arc<WhereCondition>>> {
use crate::partiql::parser::ConditionTree;
let is_key = |cond: &WhereCondition| {
condition_path(cond).is_some_and(|path| {
let root = root_attribute(path);
root == read_pk || read_sk == Some(&*root)
})
};
fn mentions_key(tree: &ConditionTree, is_key: &dyn Fn(&WhereCondition) -> bool) -> bool {
match tree {
ConditionTree::Leaf(cond) => is_key(cond),
ConditionTree::And(terms) | ConditionTree::Or(terms) => {
terms.iter().any(|t| mentions_key(t, is_key))
}
}
}
fn flatten(
tree: &ConditionTree,
is_key: &dyn Fn(&WhereCondition) -> bool,
) -> Vec<Vec<Arc<WhereCondition>>> {
match tree {
ConditionTree::Leaf(cond) => vec![vec![cond.clone()]],
ConditionTree::Or(terms) if !terms.iter().any(|t| mentions_key(t, is_key)) => {
vec![Vec::new()]
}
ConditionTree::Or(terms) => terms.iter().flat_map(|t| flatten(t, is_key)).collect(),
ConditionTree::And(terms) => {
let mut out = vec![Vec::new()];
for term in terms {
let term_groups = flatten(term, is_key);
out = out
.iter()
.flat_map(|held: &Vec<Arc<WhereCondition>>| {
term_groups.iter().map(move |group| {
let mut joined = held.clone();
joined.extend(group.iter().cloned());
joined
})
})
.collect();
}
out
}
}
}
match &wc.tree {
Some(tree) => flatten(tree, &is_key),
None => wc.groups.clone(),
}
}
fn encode_atoms(atoms: &[SortKeyAtom]) -> Vec<u8> {
fn text(tag: u8, s: &str, key: &mut Vec<u8>) {
key.push(tag);
key.extend_from_slice(&(s.len() as u64).to_le_bytes());
key.extend_from_slice(s.as_bytes());
}
let mut key = Vec::new();
for atom in atoms {
match atom {
SortKeyAtom::Points(values) => {
key.push(b'P');
key.extend_from_slice(&(values.len() as u64).to_le_bytes());
for v in values {
text(b'v', v, &mut key);
}
}
SortKeyAtom::Above(v, inclusive) => {
text(b'A', v, &mut key);
key.push(u8::from(*inclusive));
}
SortKeyAtom::Below(v, inclusive) => {
text(b'W', v, &mut key);
key.push(u8::from(*inclusive));
}
SortKeyAtom::Prefix(v) => text(b'X', v, &mut key),
}
}
key
}
fn sort_keys_meet(atoms: &[&SortKeyAtom]) -> bool {
let mut low: Option<(&str, bool)> = None;
let mut high: Option<(&str, bool)> = None;
let mut points: Option<Vec<&str>> = None;
let mut prefix: Option<&str> = None;
for atom in atoms {
match atom {
SortKeyAtom::Above(v, inclusive) => {
low = Some(match low {
Some((held, held_inclusive)) if held > v.as_str() => (held, held_inclusive),
Some((held, held_inclusive)) if held == v.as_str() => {
(held, held_inclusive && *inclusive)
}
_ => (v.as_str(), *inclusive),
});
}
SortKeyAtom::Below(v, inclusive) => {
high = Some(match high {
Some((held, held_inclusive)) if held < v.as_str() => (held, held_inclusive),
Some((held, held_inclusive)) if held == v.as_str() => {
(held, held_inclusive && *inclusive)
}
_ => (v.as_str(), *inclusive),
});
}
SortKeyAtom::Points(values) => {
points = Some(match points {
None => values.iter().map(String::as_str).collect(),
Some(held) => {
let wanted: std::collections::HashSet<&str> =
values.iter().map(String::as_str).collect();
held.into_iter().filter(|p| wanted.contains(p)).collect()
}
});
}
SortKeyAtom::Prefix(p) => {
prefix = match prefix {
None => Some(p.as_str()),
Some(held) if p.starts_with(held) => Some(p.as_str()),
Some(held) if held.starts_with(p.as_str()) => Some(held),
Some(_) => return false,
};
}
}
}
let below_high = |v: &str| high.is_none_or(|(h, inclusive)| v < h || (inclusive && v == h));
let within = |v: &str| {
low.is_none_or(|(l, inclusive)| v > l || (inclusive && v == l))
&& below_high(v)
&& prefix.is_none_or(|p| v.starts_with(p))
};
if let Some(points) = points {
return points.into_iter().any(within);
}
match prefix {
Some(p) => {
let smallest = match low {
None => p.to_string(),
Some((l, inclusive)) if l.starts_with(p) => {
if inclusive {
l.to_string()
} else {
format!("{l}\0")
}
}
Some((l, _)) if l < p => p.to_string(),
Some(_) => return false,
};
below_high(&smallest)
}
None => match (low, high) {
(Some((l, l_inclusive)), Some((h, h_inclusive))) => {
l < h || (l == h && l_inclusive && h_inclusive)
}
_ => true,
},
}
}
struct OrderPlan {
partitions: Vec<String>,
forward: bool,
}
fn check_order_terms(
order_by: &[OrderByTerm],
where_clause: Option<&WhereClause>,
read_pk: &str,
read_sk: Option<&str>,
) -> Result<()> {
let reject = |message: String| DynoxideError::ValidationException(message);
if where_clause.is_none() {
return Err(reject(
"Must have WHERE clause in the statement when using ORDER BY clause.".to_string(),
));
}
for term in order_by {
if let Some(rejection) = &term.rejection {
return Err(reject(rejection.clone()));
}
if !names_attribute(&term.path, read_pk)
&& !read_sk.is_some_and(|sk| names_attribute(&term.path, sk))
{
return Err(reject(format!(
"Attribute {} in ORDER BY clause must be part of the primary key",
term.path
)));
}
}
Ok(())
}
fn plan_order(
order_by: &[OrderByTerm],
where_clause: Option<&WhereClause>,
parameters: &[AttributeValue],
read_pk: &str,
read_sk: Option<&str>,
) -> Result<OrderPlan> {
let reject = |message: String| DynoxideError::ValidationException(message);
let Some(wc) = where_clause else {
return Err(reject(
"Must have WHERE clause in the statement when using ORDER BY clause.".to_string(),
));
};
let mut partition_values = Vec::new();
for group in &wc.groups {
let pinned: Vec<Vec<&PartiqlValue>> = group
.iter()
.filter_map(|cond| match &**cond {
WhereCondition::Comparison(c)
if names_attribute(&c.path, read_pk) && c.op == CompOp::Eq =>
{
Some(vec![&c.value])
}
WhereCondition::In(path, values) if names_attribute(path, read_pk) => {
Some(values.iter().collect())
}
_ => None,
})
.collect();
let [values] = pinned.as_slice() else {
return Err(reject(
"Must have at least one non-optional hash key condition in WHERE clause \
when using ORDER BY clause."
.to_string(),
));
};
partition_values.extend(values.iter().copied());
}
let pk_term = order_by
.iter()
.find(|term| names_attribute(&term.path, read_pk));
if partition_values.len() > 1 && pk_term.is_none() {
return Err(reject(
"Must have hash key in ORDER BY clause when more than one hash key condition \
specified in WHERE clause."
.to_string(),
));
}
let mut partitions = Vec::with_capacity(partition_values.len());
for value in partition_values {
partitions.push(key_string(&resolve_value(value, parameters)?, read_pk)?);
}
partitions.sort();
partitions.dedup();
if pk_term.is_some_and(|term| term.descending) {
partitions.reverse();
}
let forward = order_by
.iter()
.find(|term| read_sk.is_some_and(|sk| names_attribute(&term.path, sk)))
.is_some_and(|term| !term.descending);
Ok(OrderPlan {
partitions,
forward,
})
}
#[allow(clippy::too_many_arguments)]
async fn read_in_order<S: StorageBackend>(
storage: &S,
table_name: &str,
index: Option<&ResolvedIndex>,
where_clause: Option<&WhereClause>,
parameters: &[AttributeValue],
read_sk: Option<&str>,
cursor: Option<&Cursor>,
limit: Option<usize>,
plan: &OrderPlan,
) -> Result<Vec<(String, String, String)>> {
let sk_conditions = match (read_sk, where_clause) {
(Some(sk_name), Some(wc)) if wc.groups.len() == 1 => {
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 first = match cursor {
Some(c) => plan
.partitions
.iter()
.position(|pk| *pk == c.pk)
.ok_or_else(|| DynoxideError::ValidationException("Invalid NextToken".to_string()))?,
None => 0,
};
let mut rows = Vec::new();
for (i, pk_str) in plan.partitions.iter().enumerate().skip(first) {
let remaining = limit.map(|lim| lim.saturating_sub(rows.len()));
if remaining == Some(0) {
break;
}
let resume = cursor.filter(|_| i == first);
let params = crate::storage::QueryParams {
sk_condition: sk_condition_sql.as_deref(),
sk_params: &sk_params_refs,
forward: plan.forward,
limit: remaining,
exclusive_start_sk: resume.map(|c| c.sk.as_str()),
exclusive_start_base_pk: resume.and_then(|c| c.base_pk.as_deref()),
exclusive_start_base_sk: resume.and_then(|c| c.base_sk.as_deref()),
};
let page = 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?,
};
rows.extend(page);
}
Ok(rows)
}
fn reject_item_nested_to_the_limit(item: &Item) -> Result<()> {
if item_nested_to_the_limit(item) {
return Err(nested_to_the_limit());
}
Ok(())
}
fn item_nested_to_the_limit(item: &Item) -> bool {
fn deepest(value: &AttributeValue) -> usize {
let children: Box<dyn Iterator<Item = &AttributeValue>> = match value {
AttributeValue::M(map) => Box::new(map.values()),
AttributeValue::L(items) => Box::new(items.iter()),
_ => return 0,
};
children.map(|child| 1 + deepest(child)).max().unwrap_or(0)
}
item.values().map(deepest).max() == Some(32)
}
fn nested_to_the_limit() -> DynoxideError {
DynoxideError::ValidationException("Nesting Levels have exceeded supported limits".to_string())
}
fn bound_operand(value: &AttributeValue) -> String {
match value {
AttributeValue::S(s) => format!("S:{s}"),
AttributeValue::N(n) => format!("N:{n}"),
AttributeValue::B(b) => {
use base64::Engine;
format!(
"B:[{}]",
base64::engine::general_purpose::STANDARD.encode(b)
)
}
other => other.type_name().to_string(),
}
}
fn check_attribute_type_parameter(
kind: &PartiqlValue,
parameters: &[AttributeValue],
reject: fn(&str, &AttributeValue) -> DynoxideError,
) -> Result<()> {
match resolve_value(kind, parameters)? {
AttributeValue::S(name) => match crate::partiql::parser::attribute_type_name_refusal(&name)
{
Some(message) => Err(DynoxideError::ValidationException(message)),
None => Ok(()),
},
other => Err(reject("attribute_type", &other)),
}
}
fn validate_operand_types(
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()
))
}
fn ordering_op(op: &CompOp) -> Option<&'static str> {
match op {
CompOp::Lt => Some("<"),
CompOp::Le => Some("<="),
CompOp::Gt => Some(">"),
CompOp::Ge => Some(">="),
CompOp::Eq | CompOp::Ne => None,
}
}
fn check_bounds(low: &AttributeValue, high: &AttributeValue) -> Result<()> {
for value in [low, high] {
if !orderable(value) {
return Err(reject("BETWEEN", value));
}
}
let bounds = |rule: &str| {
DynoxideError::ValidationException(format!(
"The BETWEEN operator requires {rule}; lower bound operand: \
AttributeValue: {{{}}}, upper bound operand: AttributeValue: {{{}}}",
bound_operand(low),
bound_operand(high)
))
};
if std::mem::discriminant(low) != std::mem::discriminant(high) {
return Err(bounds("same data type for lower and upper bounds"));
}
if compare_values(low, &CompOp::Gt, high) {
return Err(bounds(
"upper bound to be greater than or equal to lower bound",
));
}
Ok(())
}
fn check(condition: &WhereCondition, parameters: &[AttributeValue]) -> Result<()> {
match condition {
WhereCondition::Comparison(c) | WhereCondition::NotComparison(c) => {
let Some(op) = ordering_op(&c.op) else {
return Ok(());
};
let value = resolve_value(&c.value, parameters)?;
if !orderable(&value) {
return Err(reject(op, &value));
}
}
WhereCondition::Between(_, low, high) | WhereCondition::NotBetween(_, low, high) => {
check_bounds(
&resolve_value(low, parameters)?,
&resolve_value(high, parameters)?,
)?;
}
WhereCondition::Compare(left, op, right) => {
for side in [left, right] {
if let Operand::Condition(inner) = side {
check(inner, parameters)?;
}
}
let Some(op) = ordering_op(op) else {
return Ok(());
};
for side in [left, right] {
if let Operand::Value(value) = side {
let value = resolve_value(value, parameters)?;
if !orderable(&value) {
return Err(reject(op, &value));
}
}
}
}
WhereCondition::CompareBetween(_, low, high) => match (low, high) {
(Operand::Value(low), Operand::Value(high)) => check_bounds(
&resolve_value(low, parameters)?,
&resolve_value(high, parameters)?,
)?,
_ => {
for bound in [low, high] {
if let Operand::Value(value) = bound {
let value = resolve_value(value, parameters)?;
if !orderable(&value) {
return Err(reject("BETWEEN", &value));
}
}
}
}
},
WhereCondition::CompareIn(operand, members) => {
for side in std::iter::once(operand).chain(members) {
if let Operand::Condition(inner) = side {
check(inner, parameters)?;
}
}
}
WhereCondition::AttributeType(_, kind @ PartiqlValue::Parameter(_)) => {
check_attribute_type_parameter(kind, parameters, reject)?;
}
WhereCondition::Not(inner) => check(inner, parameters)?,
WhereCondition::BeginsWith(_, prefix) | WhereCondition::NotBeginsWith(_, prefix) => {
let value = resolve_value(prefix, parameters)?;
if !matches!(value, AttributeValue::S(_) | AttributeValue::B(_)) {
return Err(reject("begins_with", &value));
}
}
_ => {}
}
Ok(())
}
let Some(wc) = where_clause else {
return Ok(());
};
for group in &wc.groups {
for condition in group {
check(condition, parameters)?;
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub(crate) async fn execute_written_page<S: StorageBackend>(
storage: &S,
written: &str,
stmt: &Statement,
parameters: &[AttributeValue],
limit: Option<usize>,
next_token: Option<&str>,
consistent_read: bool,
capacity_mode: Option<&str>,
resolved: Option<&ResolvedTable>,
old_on_condition_failure: bool,
) -> 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(),
));
}
let loaded;
let resolved = match (stmt, resolved) {
(
Statement::Select {
table_name,
index_name: Some(_),
..
},
None,
) => {
loaded = ResolvedTable::load(storage, table_name).await.ok();
loaded.as_ref()
}
(_, resolved) => resolved,
};
if let (
Statement::Select {
table_name,
index_name: Some(index_name),
..
},
Some(table),
) = (stmt, resolved)
&& table.table_name == *table_name
{
reject_multi_attribute_index(table, index_name)?;
}
validate_operand_types(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,
order_by,
..
} => {
let outcome = execute_select(
storage,
written,
table_name,
index_name.as_deref(),
projections,
where_clause.as_ref(),
order_by,
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,
old_on_condition_failure,
)
.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,
old_on_condition_failure,
)
.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) {
result.insert(projection_key(path), val);
}
fn projection_key(path: &str) -> String {
if !path.contains('"') {
return path.rsplit('.').next().unwrap_or(path).to_string();
}
let Some(segments) = split_path_segments(path) else {
return path.to_string();
};
let last_name = segments
.iter()
.rposition(|segment| matches!(segment, PathSegment::Key(_)))
.unwrap_or(0);
segments[last_name..]
.iter()
.map(|segment| match segment {
PathSegment::Key(name) => name.to_string(),
PathSegment::Index(index) => format!("[{index}]"),
})
.collect()
}
#[allow(clippy::too_many_arguments)]
async fn execute_select<S: StorageBackend>(
storage: &S,
written: &str,
table_name: &str,
index_name: Option<&str>,
projections: &[String],
where_clause: Option<&WhereClause>,
order_by: &[OrderByTerm],
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()?;
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(),
),
};
if !order_by.is_empty() {
check_order_terms(order_by, where_clause, read_pk, read_sk)?;
}
let declared = |attr: &str| match index.as_ref() {
None if attr == table_key_schema.partition_key => {
Some(table_key_schema.partition_key_type.clone())
}
None => table_key_schema.sort_key_type.clone(),
Some(_) => declared_attribute_type(meta, attr),
};
check_read_key_types(where_clause, parameters, read_pk, read_sk, &declared)?;
reject_overlapping_key_conditions(where_clause, parameters, read_pk, read_sk)?;
let order = if order_by.is_empty() {
None
} else {
Some(plan_order(
order_by,
where_clause,
parameters,
read_pk,
read_sk,
)?)
};
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, read_pk) {
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 fingerprint = statement_fingerprint(written, parameters);
let cursor = next_token
.map(|token| decode_next_token(token, table_name, fingerprint))
.transpose()?;
if cursor.is_some()
&& parameters
.iter()
.any(|p| matches!(p, AttributeValue::L(_) | AttributeValue::M(_)))
{
return Err(token_mismatch());
}
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,
order.as_ref(),
)
.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,
order: Option<&OrderPlan>,
) -> Result<Window> {
let pk_condition = where_clause.and_then(|wc| find_pk_condition(wc, read_pk));
let pk_key = match pk_condition {
Some(value) => {
let value = resolve_value(value, parameters)?;
if let Some(refusal) = crate::actions::helpers::key_size_refusal(&value, false) {
return Err(refusal);
}
if let (Some(sk_name), Some(wc)) = (read_sk, where_clause) {
let sk_conditions =
translate_sk_conditions(&wc.groups[0], sk_name, parameters).unwrap_or_default();
for sk_value in sk_conditions.iter().flat_map(|c| c.values()) {
if let Some(refusal) = crate::actions::helpers::key_size_refusal(sk_value, true)
{
return Err(refusal);
}
}
}
Some(value.to_key_string())
}
None => {
if let Some(value) = where_clause.and_then(|wc| pk_in_every_group(wc, read_pk)) {
let value = resolve_value(value, parameters)?;
if let Some(refusal) = crate::actions::helpers::key_size_refusal(&value, false) {
return Err(refusal);
}
}
None
}
};
let rows: Vec<(String, String, String)> = if let Some(plan) = order {
read_in_order(
storage,
table_name,
index,
where_clause,
parameters,
read_sk,
cursor,
limit,
plan,
)
.await?
} else if let Some(None) = pk_key {
Vec::new()
} else if let Some(Some(pk_str)) = pk_key {
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>,
}
pub(crate) const MULTI_ATTRIBUTE_INDEX_REFUSAL: &str = "PartiQL statements cannot be executed \
on tables with multi-attribute HASH keys or multi-attribute RANGE keys.";
pub(crate) fn reject_multi_attribute_index(
resolved: &ResolvedTable,
index_name: &str,
) -> Result<()> {
let multi_attribute = crate::actions::gsi::parse_gsi_defs(&resolved.meta)?
.iter()
.any(|g| g.index_name == index_name && (g.hash_attrs.len() > 1 || g.range_attrs.len() > 1));
if multi_attribute {
return Err(DynoxideError::ValidationException(
MULTI_ATTRIBUTE_INDEX_REFUSAL.to_string(),
));
}
Ok(())
}
pub(crate) async fn reject_multi_attribute_index_by_name<S: StorageBackend>(
storage: &S,
table_name: &str,
index_name: &str,
) -> Result<()> {
match ResolvedTable::load(storage, table_name).await {
Ok(resolved) => reject_multi_attribute_index(&resolved, index_name),
Err(_) => Ok(()),
}
}
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) -> std::borrow::Cow<'_, str> {
if path.starts_with('"')
&& let Some(segments) = split_path_segments(path)
&& let Some(PathSegment::Key(name)) = segments.into_iter().next()
{
return name;
}
let end = path.find(['.', '[']).unwrap_or(path.len());
std::borrow::Cow::Borrowed(&path[..end])
}
fn condition_path(condition: &WhereCondition) -> Option<&str> {
match condition {
WhereCondition::Comparison(c) | WhereCondition::NotComparison(c) => Some(c.path.as_str()),
WhereCondition::Exists(p)
| WhereCondition::NotExists(p)
| WhereCondition::BeginsWith(p, _)
| WhereCondition::NotBeginsWith(p, _)
| WhereCondition::Between(p, _, _)
| WhereCondition::NotBetween(p, _, _)
| WhereCondition::In(p, _)
| WhereCondition::Contains(p, _)
| WhereCondition::NotContains(p, _)
| WhereCondition::IsMissing(p)
| WhereCondition::IsNotMissing(p)
| WhereCondition::IsType(p, _)
| WhereCondition::IsNotType(p, _) => Some(p.as_str()),
_ => None,
}
}
fn condition_paths(condition: &WhereCondition) -> Vec<&str> {
fn operand_paths(operand: &Operand) -> Vec<&str> {
match operand {
Operand::Path(path) => vec![path.as_str()],
Operand::Size(inner) => operand_paths(inner),
Operand::Condition(inner) => condition_paths(inner),
_ => Vec::new(),
}
}
match condition {
WhereCondition::Compare(left, _, right) => {
[left, right].into_iter().flat_map(operand_paths).collect()
}
WhereCondition::CompareBetween(operand, low, high) => [operand, low, high]
.into_iter()
.flat_map(operand_paths)
.collect(),
WhereCondition::CompareIn(operand, list) => std::iter::once(operand)
.chain(list)
.flat_map(operand_paths)
.collect(),
WhereCondition::AttributeType(operand, _) => operand_paths(operand),
WhereCondition::Not(inner) => condition_paths(inner),
other => condition_path(other).into_iter().collect(),
}
}
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 {
for path in condition_paths(condition) {
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(written: &str, parameters: &[AttributeValue]) -> u64 {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
written.hash(&mut hasher);
hash_parameters(parameters, &mut hasher);
hasher.finish()
}
fn hash_parameters<H: std::hash::Hasher>(parameters: &[AttributeValue], hasher: &mut H) {
use std::hash::Hash;
parameters.len().hash(hasher);
for parameter in parameters {
match parameter {
AttributeValue::N(n) => {
"N".hash(hasher);
decimal_identity(n).hash(hasher);
}
AttributeValue::NS(members) => {
let mut values: Vec<String> = members
.iter()
.map(|n| crate::types::normalize_dynamo_number(n))
.collect();
values.sort_unstable();
"NS".hash(hasher);
values.hash(hasher);
}
AttributeValue::SS(members) => {
let mut members: Vec<&String> = members.iter().collect();
members.sort_unstable();
"SS".hash(hasher);
members.hash(hasher);
}
AttributeValue::BS(members) => {
let mut members: Vec<&Vec<u8>> = members.iter().collect();
members.sort_unstable();
"BS".hash(hasher);
members.hash(hasher);
}
other => crate::canonical_json(other).hash(hasher),
}
}
}
fn decimal_identity(n: &str) -> (bool, String, i64) {
let negative = n.starts_with('-');
let unsigned = n.trim_start_matches(['+', '-']);
let (coefficient, exponent) = match unsigned.find(['e', 'E']) {
Some(at) => match unsigned[at + 1..].parse::<i64>() {
Ok(exponent) => (&unsigned[..at], exponent),
Err(_) => return (false, n.to_string(), i64::MIN),
},
None => (unsigned, 0),
};
let (whole, fraction) = coefficient.split_once('.').unwrap_or((coefficient, ""));
if !whole
.chars()
.chain(fraction.chars())
.all(|c| c.is_ascii_digit())
{
return (false, n.to_string(), i64::MIN);
}
let digits: String = whole
.chars()
.chain(fraction.chars())
.skip_while(|&d| d == '0')
.collect();
let scale = i64::try_from(fraction.len())
.unwrap_or(i64::MAX)
.saturating_sub(exponent);
(negative && !digits.is_empty(), digits, scale)
}
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: &[Arc<WhereCondition>],
sk_name: &str,
parameters: &[AttributeValue],
) -> Option<Vec<ResolvedSortKeyCondition>> {
let mut resolved = Vec::new();
for cond in group {
match &**cond {
WhereCondition::Comparison(c) if names_attribute(&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 names_attribute(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 names_attribute(path, sk_name) => {
let prefix = resolve_value(prefix, parameters).ok()?;
prefix.to_key_string()?;
resolved.push(ResolvedSortKeyCondition::BeginsWith(
sk_name.to_string(),
prefix,
));
}
WhereCondition::In(path, values)
if names_attribute(path, sk_name) && values.len() == 1 =>
{
let value = resolve_value(&values[0], parameters).ok()?;
value.to_key_string()?;
resolved.push(ResolvedSortKeyCondition::Eq(sk_name.to_string(), value));
}
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)
| WhereCondition::IsType(path, _)
| WhereCondition::IsNotType(path, _)
if names_attribute(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) => {
names_attribute(&cond.path, pk_name) && cond.op == CompOp::Eq
}
WhereCondition::In(path, _) => names_attribute(path, pk_name),
_ => false,
})
}
fn find_pk_condition<'a>(wc: &'a WhereClause, pk_name: &str) -> Option<&'a PartiqlValue> {
let [group] = wc.groups.as_slice() else {
return None;
};
group_pk_value(group, pk_name)
}
fn pk_in_every_group<'a>(wc: &'a WhereClause, pk_name: &str) -> Option<&'a PartiqlValue> {
let (first, rest) = wc.groups.split_first()?;
let value = group_pk_value(first, pk_name)?;
rest.iter()
.all(|group| group_pk_value(group, pk_name) == Some(value))
.then_some(value)
}
fn group_pk_value<'a>(group: &'a [Arc<WhereCondition>], pk_name: &str) -> Option<&'a PartiqlValue> {
let mut pinning = group.iter().filter_map(|c| match &**c {
WhereCondition::Comparison(cond)
if names_attribute(&cond.path, pk_name) && cond.op == CompOp::Eq =>
{
Some(std::slice::from_ref(&cond.value))
}
WhereCondition::In(path, values) if names_attribute(path, pk_name) => {
Some(values.as_slice())
}
_ => None,
});
let values = pinning.next()?;
if pinning.any(|other| other != values) {
return None;
}
match values {
[value] => Some(value),
_ => 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_key_value_in_groups(&wc.groups, name)
.and_then(|value| resolve_value(value, parameters).ok())
})
};
let from_top_level = |where_clause: &Option<WhereClause>| -> Option<(String, String)> {
let wc = where_clause.as_ref()?;
key_of(&|name: &str| {
let mut values = wc
.top_level_equalities
.iter()
.filter(|cond| names_attribute(&cond.path, name))
.map(|cond| resolve_value(&cond.value, parameters).ok());
let first = values.next()??;
let first_key = first.to_key_string();
if values.any(|v| v.and_then(|v| v.to_key_string()) != first_key) {
return None;
}
Some(first)
})
};
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, .. } => {
from_top_level(where_clause)
}
Statement::Select { where_clause, .. } => from_where(where_clause),
}
}
fn resolve_insert_item(
item_template: &HashMap<String, PartiqlValue>,
parameters: &[AttributeValue],
) -> Result<Item> {
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);
}
Ok(item)
}
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 = resolve_insert_item(item_template, parameters)?;
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 keys = std::iter::once((
&key_schema.partition_key,
Some(&key_schema.partition_key_type),
))
.chain(
key_schema
.sort_key
.as_ref()
.map(|sk| (sk, key_schema.sort_key_type.as_ref())),
);
for (name, declared) in keys {
if let Some(value) = item.get(name)
&& typed_key(value, name, declared).is_err()
{
return Err(DynoxideError::ValidationException(format!(
"Key attribute's data type should match its data type in table's schema: Key {name}"
)));
}
}
crate::actions::helpers::validate_item_keys(&item, key_schema, meta)?;
let at_the_limit = item_nested_to_the_limit(&item);
if !at_the_limit {
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(),
));
}
if at_the_limit {
return Err(nested_to_the_limit());
}
let item_json = serde_json::to_string(&item)
.map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
if crate::types::item_size(&item) > crate::types::MAX_ITEM_SIZE {
return Err(DynoxideError::ValidationException(
"Item size has exceeded the maximum allowed size".to_string(),
));
}
let vector_defs = crate::actions::vector_index::parse_vector_defs(meta)?;
let item_size = crate::types::billed_item_size(&item, &vector_defs);
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(|old| crate::types::billed_item_size(old, &vector_defs)),
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>,
old_on_condition_failure: bool,
) -> 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(mandatory_key_equality)?;
let (pk_str, sk_str) = single_item_key(wc, key_schema, parameters)?;
let paths = set_clauses
.iter()
.map(|clause| clause.path.as_str())
.chain(remove_paths.iter().map(String::as_str));
for path in paths {
let key = std::iter::once(key_schema.partition_key.as_str())
.chain(key_schema.sort_key.as_deref())
.find(|key| names_attribute(path, key));
if let Some(key) = key {
return Err(crate::actions::helpers::key_attribute_update_refusal(key));
}
}
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() {
return Err(DynoxideError::ConditionalCheckFailedException(
"The conditional request failed".to_string(),
None,
));
}
if !matches_where(&old_item, where_clause, parameters) {
return Err(DynoxideError::ConditionalCheckFailedException(
"The conditional request failed".to_string(),
old_on_condition_failure.then_some(old_item),
));
}
let before_item = item.clone();
for clause in set_clauses {
match resolve_set_value(&clause.value, &item, parameters)? {
Some(val) => set_nested_value(&mut item, &clause.path, val)?,
None => remove_nested_value(&mut item, &clause.path),
}
}
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()),
));
}
reject_item_nested_to_the_limit(&item)?;
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()))?;
if crate::types::item_size(&item) > crate::types::MAX_ITEM_SIZE {
return Err(DynoxideError::ValidationException(
"Item size to update has exceeded the maximum allowed size".to_string(),
));
}
let vector_defs = crate::actions::vector_index::parse_vector_defs(meta)?;
let item_size = crate::types::billed_item_size(&item, &vector_defs);
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(|old| crate::types::billed_item_size(old, &vector_defs)),
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>,
old_on_condition_failure: bool,
) -> 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(mandatory_key_equality)?;
let (pk_str, sk_str) = single_item_key(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(),
old_on_condition_failure.then_some(existing),
));
}
}
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 vector_defs = crate::actions::vector_index::parse_vector_defs(meta)?;
let capacity = WriteCapacity::new(
table_name,
old_item
.as_ref()
.map(|old| crate::types::billed_item_size(old, &vector_defs)),
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_for_item_op(storage, table_name).await
}
fn single_item_key(
wc: &WhereClause,
key_schema: &crate::actions::helpers::KeySchema,
parameters: &[AttributeValue],
) -> Result<(String, String)> {
let pinned =
|name: &str, declared: Option<&ScalarAttributeType>| -> Result<(String, AttributeValue)> {
let mut key: Option<(String, AttributeValue)> = None;
for cond in wc
.top_level_equalities
.iter()
.filter(|c| names_attribute(&c.path, name))
{
let resolved = resolve_value(&cond.value, parameters)?;
let value = typed_key(&resolved, name, declared)?;
match key {
Some((ref already, _)) if *already != value => {
return Err(DynoxideError::ValidationException(format!(
"Multiple conditions on same key {name}. Only single item \
Update/Insert/Delete are supported"
)));
}
_ => key = Some((value, resolved)),
}
}
key.ok_or_else(mandatory_key_equality)
};
let (pk, pk_value) = pinned(
&key_schema.partition_key,
Some(&key_schema.partition_key_type),
)?;
let sk = match key_schema.sort_key {
Some(ref sk_name) => Some(pinned(sk_name, key_schema.sort_key_type.as_ref())?),
None => None,
};
if let Some(refusal) = crate::actions::helpers::key_size_refusal(&pk_value, false) {
return Err(refusal);
}
let sk = match sk {
Some((sk, sk_value)) => {
if let Some(refusal) = crate::actions::helpers::key_size_refusal(&sk_value, true) {
return Err(refusal);
}
sk
}
None => String::new(),
};
Ok((pk, sk))
}
fn mandatory_key_equality() -> DynoxideError {
DynoxideError::ValidationException(
"Where clause does not contain a mandatory equality on all key attributes".to_string(),
)
}
fn find_key_value_in_groups<'a>(
groups: &'a [Vec<Arc<WhereCondition>>],
path: &str,
) -> Option<&'a PartiqlValue> {
groups.iter().find_map(|group| {
group.iter().find_map(|c| match &**c {
WhereCondition::Comparison(cond)
if names_attribute(&cond.path, path) && cond.op == CompOp::Eq =>
{
Some(&cond.value)
}
WhereCondition::In(p, values) if names_attribute(p, path) && values.len() == 1 => {
Some(&values[0])
}
_ => None,
})
})
}
fn find_comparison<'a>(
conditions: &'a [Arc<WhereCondition>],
path: &str,
) -> Option<&'a crate::partiql::parser::Condition> {
conditions.iter().find_map(|c| match &**c {
WhereCondition::Comparison(cond)
if names_attribute(&cond.path, path) && cond.op == CompOp::Eq =>
{
Some(cond)
}
_ => None,
})
}
fn begins_with(value: &AttributeValue, prefix: &AttributeValue) -> bool {
match (value, prefix) {
(AttributeValue::S(s), AttributeValue::S(p)) => s.starts_with(p.as_str()),
(AttributeValue::B(b), AttributeValue::B(p)) => b.starts_with(p),
_ => false,
}
}
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<Option<AttributeValue>> {
if let SetValue::SetCall {
delete,
path,
members,
refusal_at,
} = val
{
return resolve_set_call(*delete, path, members, refusal_at, item, parameters);
}
resolve_set_operand(val, item, parameters).map(Some)
}
fn resolve_set_call(
delete: bool,
path: &str,
members: &PartiqlValue,
refusal_at: &str,
item: &Item,
parameters: &[AttributeValue],
) -> Result<Option<AttributeValue>> {
let name = if delete { "SET_DELETE" } else { "SET_ADD" };
let members = resolve_value(members, parameters)?;
if !matches!(
members,
AttributeValue::SS(_) | AttributeValue::NS(_) | AttributeValue::BS(_)
) {
return Err(DynoxideError::ValidationException(format!(
"The second argument to {name} must be a value with type SET at {refusal_at}"
)));
}
crate::validation::validate_parameter_value(&members)?;
let wrong_type = || {
DynoxideError::ValidationException(
"An operand in the update expression has an incorrect data type".to_string(),
)
};
let result = match (resolve_nested_path(item, path), &members) {
(None, _) => return Ok((!delete).then_some(members)),
(Some(AttributeValue::SS(held)), AttributeValue::SS(change)) => {
AttributeValue::SS(combine_set_members(held, change, delete, String::clone))
}
(Some(AttributeValue::NS(held)), AttributeValue::NS(change)) => {
AttributeValue::NS(combine_set_members(held, change, delete, |n: &String| {
crate::types::normalize_dynamo_number(n)
}))
}
(Some(AttributeValue::BS(held)), AttributeValue::BS(change)) => {
AttributeValue::BS(combine_set_members(held, change, delete, Vec::clone))
}
_ => return Err(wrong_type()),
};
let empty = match &result {
AttributeValue::SS(m) | AttributeValue::NS(m) => m.is_empty(),
AttributeValue::BS(m) => m.is_empty(),
_ => false,
};
Ok((!empty).then_some(result))
}
fn combine_set_members<T: Clone, K: Eq + std::hash::Hash>(
held: &[T],
change: &[T],
delete: bool,
key: impl Fn(&T) -> K,
) -> Vec<T> {
use std::collections::HashSet;
if delete {
let named: HashSet<K> = change.iter().map(&key).collect();
held.iter()
.filter(|m| !named.contains(&key(m)))
.cloned()
.collect()
} else {
let mut seen: HashSet<K> = held.iter().map(&key).collect();
let mut out = held.to_vec();
for m in change {
if seen.insert(key(m)) {
out.push(m.clone());
}
}
out
}
}
fn resolve_set_operand(
val: &SetValue,
item: &Item,
parameters: &[AttributeValue],
) -> Result<AttributeValue> {
match val {
SetValue::SetCall { .. } => unreachable!("resolve_set_value handles a set call"),
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.as_ref());
return;
}
if let Some(current) = item.get_mut(key.as_ref()) {
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.as_ref());
}
}
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.as_ref())
{
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,
};
if wc
.groups
.iter()
.flatten()
.any(|condition| has_unbound_parameter(condition, parameters.len()))
{
return false;
}
wc.groups
.iter()
.any(|group| matches_conditions(item, group, parameters))
}
fn has_unbound_parameter(condition: &WhereCondition, bound: usize) -> bool {
fn value(v: &PartiqlValue, bound: usize) -> bool {
matches!(v, PartiqlValue::Parameter(i) if *i >= bound)
}
fn operand(o: &Operand, bound: usize) -> bool {
match o {
Operand::Path(_) => false,
Operand::Value(v) => value(v, bound),
Operand::Size(inner) => operand(inner, bound),
Operand::Condition(inner) => has_unbound_parameter(inner, bound),
}
}
match condition {
WhereCondition::Comparison(c) | WhereCondition::NotComparison(c) => value(&c.value, bound),
WhereCondition::Exists(_)
| WhereCondition::NotExists(_)
| WhereCondition::IsMissing(_)
| WhereCondition::IsNotMissing(_)
| WhereCondition::IsType(..)
| WhereCondition::IsNotType(..) => false,
WhereCondition::BeginsWith(_, v)
| WhereCondition::NotBeginsWith(_, v)
| WhereCondition::Contains(_, v)
| WhereCondition::NotContains(_, v) => value(v, bound),
WhereCondition::Between(_, low, high) | WhereCondition::NotBetween(_, low, high) => {
value(low, bound) || value(high, bound)
}
WhereCondition::In(_, values) => values.iter().any(|v| value(v, bound)),
WhereCondition::Compare(left, _, right) => operand(left, bound) || operand(right, bound),
WhereCondition::CompareBetween(o, low, high) => {
operand(o, bound) || operand(low, bound) || operand(high, bound)
}
WhereCondition::CompareIn(o, list) => {
operand(o, bound) || list.iter().any(|m| operand(m, bound))
}
WhereCondition::AttributeType(o, kind) => operand(o, bound) || value(kind, bound),
WhereCondition::Not(inner) => has_unbound_parameter(inner, bound),
}
}
fn matches_conditions<C: std::borrow::Borrow<WhereCondition>>(
item: &Item,
conditions: &[C],
parameters: &[AttributeValue],
) -> bool {
for cond in conditions {
match cond.borrow() {
WhereCondition::Comparison(c) => {
let item_val = match resolve_nested_path(item, &c.path) {
Some(v) => v,
None if c.op == CompOp::Ne => continue,
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;
}
} else if c.op == CompOp::Ne {
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::IsType(path, test) => {
if !resolve_nested_path(item, path).is_some_and(|v| test.matches(v)) {
return false;
}
}
WhereCondition::IsNotType(path, test) => {
if resolve_nested_path(item, path).is_some_and(|v| test.matches(v)) {
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,
};
if !begins_with(item_val, &prefix) {
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 begins_with(item_val, &prefix) {
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::NotBetween(path, low, high) => {
if let Some(item_val) = resolve_nested_path(item, path) {
let (Ok(low_val), Ok(high_val)) = (
resolve_value(low, parameters),
resolve_value(high, parameters),
) else {
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;
}
}
WhereCondition::Not(inner) => {
if matches_conditions(item, std::slice::from_ref(inner), parameters) {
return false;
}
}
other => {
if !operand_condition_holds(item, other, parameters) {
return false;
}
}
}
}
true
}
fn operand_condition_holds(
item: &Item,
condition: &WhereCondition,
parameters: &[AttributeValue],
) -> bool {
let value = |operand: &Operand| operand_value(item, operand, parameters);
match condition {
WhereCondition::Compare(left, op, right) => match (value(left), value(right)) {
(Some(left), Some(right)) => compare_values(&left, op, &right),
_ => *op == CompOp::Ne,
},
WhereCondition::CompareBetween(operand, low, high) => {
match (value(operand), value(low), value(high)) {
(Some(v), Some(low), Some(high)) => {
compare_values(&v, &CompOp::Ge, &low) && compare_values(&v, &CompOp::Le, &high)
}
_ => false,
}
}
WhereCondition::CompareIn(operand, list) => value(operand).is_some_and(|v| {
list.iter()
.any(|member| value(member).is_some_and(|m| compare_values(&v, &CompOp::Eq, &m)))
}),
WhereCondition::AttributeType(operand, kind) => {
match (value(operand), resolve_value(kind, parameters)) {
(Some(v), Ok(AttributeValue::S(kind))) => v.type_name() == kind,
_ => false,
}
}
_ => false,
}
}
fn operand_value(
item: &Item,
operand: &Operand,
parameters: &[AttributeValue],
) -> Option<AttributeValue> {
match operand {
Operand::Path(path) => resolve_nested_path(item, path).cloned(),
Operand::Value(value) => resolve_value(value, parameters).ok(),
Operand::Condition(condition) => Some(AttributeValue::BOOL(matches_conditions(
item,
std::slice::from_ref(condition.as_ref()),
parameters,
))),
Operand::Size(inner) => {
let size = match operand_value(item, inner, parameters)? {
AttributeValue::S(s) => s.encode_utf16().count(),
AttributeValue::B(b) => b.len(),
AttributeValue::SS(set) => set.len(),
AttributeValue::NS(set) => set.len(),
AttributeValue::BS(set) => set.len(),
AttributeValue::L(list) => list.len(),
AttributeValue::M(map) => map.len(),
_ => return None,
};
Some(AttributeValue::N(size.to_string()))
}
}
}
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.starts_with('"') {
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.as_ref())?,
PathSegment::Index(_) => return None,
};
for seg in &segments[1..] {
current = match seg {
PathSegment::Key(k) => match current {
AttributeValue::M(map) => map.get(k.as_ref())?,
_ => return None,
},
PathSegment::Index(idx) => match current {
AttributeValue::L(list) => list.get(*idx)?,
_ => return None,
},
};
}
Some(current)
}
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()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::partiql::parser::parse;
#[test]
fn set_members_combine_once_per_member() {
let s = |v: &[&str]| v.iter().map(|m| m.to_string()).collect::<Vec<_>>();
let held = s(&["a", "b", "c"]);
assert_eq!(
combine_set_members(&held, &s(&["c", "d", "d", "e"]), false, String::clone),
s(&["a", "b", "c", "d", "e"])
);
assert_eq!(
combine_set_members(&held, &s(&["c", "a", "z"]), true, String::clone),
s(&["b"])
);
let numbers = s(&["1", "2.0", "30"]);
let by_value = |n: &String| crate::types::normalize_dynamo_number(n);
assert_eq!(
combine_set_members(&numbers, &s(&["2", "3e1", "4"]), false, by_value),
s(&["1", "2.0", "30", "4"])
);
assert_eq!(
combine_set_members(&numbers, &s(&["1.00", "30"]), true, by_value),
s(&["2.0"])
);
let held: Vec<String> = (0..2_000).map(|i| format!("h{i}")).collect();
let change: Vec<String> = (0..2_000).map(|i| format!("c{i}")).collect();
for delete in [false, true] {
let calls = std::cell::Cell::new(0usize);
let counted = |m: &String| {
calls.set(calls.get() + 1);
m.clone()
};
combine_set_members(&held, &change, delete, counted);
assert_eq!(
calls.get(),
held.len() + change.len(),
"set_{} worked out {} keys for {} members",
if delete { "delete" } else { "add" },
calls.get(),
held.len() + change.len()
);
}
}
fn where_of(statement: &str) -> WhereClause {
match parse(statement).unwrap() {
Statement::Select {
where_clause: Some(wc),
..
} => wc,
other => panic!("expected a SELECT with WHERE: {other:?}"),
}
}
#[test]
fn a_fingerprint_follows_the_text_as_written() {
let fingerprint = |statement: &str| statement_fingerprint(statement, &[]);
let a = r#"SELECT * FROM "t" WHERE pk = 'k' AND (a = 1 OR b = 2)"#;
let b = r#"SELECT * FROM "t" WHERE pk = 'k' AND (a = 1 OR b = 3)"#;
let c = r#"SELECT * FROM "t" WHERE pk = 'k' AND (a = 1 AND b = 2)"#;
let spaced = r#"SELECT * FROM "t" WHERE pk = 'k' AND (a = 1 OR b = 2)"#;
let lower = r#"select * FROM "t" WHERE pk = 'k' AND (a = 1 OR b = 2)"#;
assert_eq!(fingerprint(a), fingerprint(a));
for other in [b, c, spaced, lower] {
assert_ne!(fingerprint(a), fingerprint(other), "{other}");
}
let with =
|p: &[AttributeValue]| statement_fingerprint(r#"SELECT * FROM "t" WHERE pk = ?"#, p);
let one = [AttributeValue::S("1".to_string())];
let two = [AttributeValue::S("2".to_string())];
assert_eq!(with(&one), with(&one));
assert_ne!(with(&one), with(&two));
assert_ne!(with(&one), with(&[]));
}
#[test]
fn a_condition_short_of_its_parameters_never_matches() {
let item: Item = [
("pk".to_string(), AttributeValue::S("a".to_string())),
("tag".to_string(), AttributeValue::S("x".to_string())),
]
.into_iter()
.collect();
let matched: Vec<&str> = [
("Comparison", "flag <> ?"),
("NotComparison", "NOT flag = ?"),
("In", "tag IN [?, 'x']"),
("Compare", "size(tag) <> ?"),
("Compare, parameter first", "? <> size(tag)"),
]
.into_iter()
.filter(|(_, clause)| {
let wc = where_of(&format!(r#"SELECT * FROM "t" WHERE {clause}"#));
matches_where(&item, Some(&wc), &[])
})
.map(|(kind, _)| kind)
.collect();
assert!(
matched.is_empty(),
"matched with no parameters: {matched:?}"
);
let wc = where_of(r#"SELECT * FROM "t" WHERE tag IN [?, 'x']"#);
assert!(matches_where(
&item,
Some(&wc),
&[AttributeValue::S("y".to_string())]
));
}
}