use super::gsi::{IndexDef, build_index_item};
use crate::types::{
AttributeValue, Item, item_size, table_write_capacity_units, write_capacity_units,
};
use std::collections::HashMap;
pub(crate) struct IndexEntry {
pub(crate) key: (String, String),
pub(crate) projected: Item,
size: usize,
}
pub(crate) fn entry_for(
item: &Item,
index: &IndexDef,
table_pk: &str,
table_sk: Option<&str>,
) -> Option<IndexEntry> {
let key = index.index_key_strings(item)?;
let projected = build_index_item(item, index, table_pk, table_sk);
let size = crate::types::item_size(&projected);
Some(IndexEntry {
key,
projected,
size,
})
}
pub fn index_write_units(
old_item: Option<&Item>,
new_item: Option<&Item>,
index: &IndexDef,
table_pk: &str,
table_sk: Option<&str>,
) -> Option<f64> {
let new = new_item.and_then(|item| entry_for(item, index, table_pk, table_sk));
index_write_units_for(old_item, new, index, table_pk, table_sk)
}
pub(crate) fn index_write_units_for(
old_item: Option<&Item>,
new: Option<IndexEntry>,
index: &IndexDef,
table_pk: &str,
table_sk: Option<&str>,
) -> Option<f64> {
let old = old_item.and_then(|item| entry_for(item, index, table_pk, table_sk));
match (old, new) {
(None, None) => None,
(None, Some(new)) => Some(write_capacity_units(new.size)),
(Some(old), None) => Some(write_capacity_units(old.size)),
(Some(old), Some(new)) if old.key != new.key => {
Some(write_capacity_units(old.size) + write_capacity_units(new.size))
}
(Some(old), Some(new)) if unchanged(&old.projected, &new.projected) => None,
(Some(old), Some(new)) => {
Some(write_capacity_units(old.size).max(write_capacity_units(new.size)))
}
}
}
pub(crate) fn unchanged(old: &Item, new: &Item) -> bool {
old.len() == new.len()
&& old
.iter()
.all(|(name, a)| new.get(name).is_some_and(|b| value_unchanged(a, b)))
}
pub(crate) fn value_unchanged(a: &AttributeValue, b: &AttributeValue) -> bool {
match (a, b) {
(AttributeValue::SS(x), AttributeValue::SS(y)) => set_unchanged(x, y),
(AttributeValue::NS(x), AttributeValue::NS(y)) => set_unchanged(x, y),
(AttributeValue::BS(x), AttributeValue::BS(y)) => set_unchanged(x, y),
(AttributeValue::L(x), AttributeValue::L(y)) => {
x.len() == y.len() && x.iter().zip(y).all(|(p, q)| value_unchanged(p, q))
}
(AttributeValue::M(x), AttributeValue::M(y)) => unchanged(x, y),
_ => a == b,
}
}
fn set_unchanged<T: Ord>(x: &[T], y: &[T]) -> bool {
if x.len() != y.len() {
return false;
}
let mut x: Vec<&T> = x.iter().collect();
let mut y: Vec<&T> = y.iter().collect();
x.sort_unstable();
y.sort_unstable();
x == y
}
#[derive(Debug, Clone, Default)]
pub struct WriteCapacity {
pub table_name: String,
pub old_size: Option<usize>,
pub new_size: Option<usize>,
pub gsi_units: HashMap<String, f64>,
pub lsi_units: HashMap<String, f64>,
pub vector_bytes: HashMap<String, f64>,
}
impl WriteCapacity {
pub fn new(
table_name: &str,
old_size: Option<usize>,
new_size: Option<usize>,
gsi_units: HashMap<String, f64>,
lsi_units: HashMap<String, f64>,
) -> Self {
Self {
table_name: table_name.to_string(),
old_size,
new_size,
gsi_units,
lsi_units,
vector_bytes: HashMap::new(),
}
}
pub fn with_vector_bytes(mut self, vector_bytes: HashMap<String, f64>) -> Self {
self.vector_bytes = vector_bytes;
self
}
pub fn from_items(
table_name: &str,
old_item: Option<&Item>,
new_item: Option<&Item>,
gsi_units: HashMap<String, f64>,
lsi_units: HashMap<String, f64>,
) -> Self {
Self::new(
table_name,
old_item.map(item_size),
new_item.map(item_size),
gsi_units,
lsi_units,
)
}
pub fn condition_check(
table_name: &str,
item: Option<&Item>,
vector_indexes: &[crate::types::VectorIndex],
) -> Self {
Self::new(
table_name,
item.map(|i| crate::types::billed_item_size(i, vector_indexes)),
None,
HashMap::new(),
HashMap::new(),
)
}
}
#[derive(Default)]
pub struct TableCapacity {
pub table_units: f64,
pub gsi_units: HashMap<String, f64>,
pub lsi_units: HashMap<String, f64>,
pub vector_bytes: HashMap<String, f64>,
}
pub(crate) type CapacityBuilder = fn(
&str,
f64,
&HashMap<String, f64>,
&HashMap<String, f64>,
&Option<String>,
) -> Option<crate::types::ConsumedCapacity>;
pub(crate) fn transactional_read_units(sizes: &[(String, usize)]) -> HashMap<String, f64> {
let mut table_units: HashMap<String, f64> = HashMap::new();
for (table, size) in sizes {
*table_units.entry(table.clone()).or_default() +=
crate::types::TRANSACTIONAL_CAPACITY_FACTOR * crate::types::read_capacity_units(*size);
}
table_units
}
pub(crate) fn per_table_capacity(
by_table: &HashMap<String, TableCapacity>,
mode: &Option<String>,
builder: CapacityBuilder,
) -> Option<Vec<crate::types::ConsumedCapacity>> {
if !matches!(mode.as_deref(), Some("TOTAL") | Some("INDEXES")) {
return None;
}
let mut tables: Vec<&String> = by_table.keys().collect();
tables.sort();
Some(
tables
.into_iter()
.filter_map(|table| {
let units = by_table.get(table)?;
crate::types::attach_vector_index_capacity(
builder(
table,
units.table_units,
&units.gsi_units,
&units.lsi_units,
mode,
),
&units.vector_bytes,
mode,
)
})
.collect(),
)
}
pub fn aggregate_by_table(
records: &[WriteCapacity],
factor: f64,
) -> HashMap<String, TableCapacity> {
let mut by_table: HashMap<String, TableCapacity> = HashMap::new();
for record in records {
let entry = by_table.entry(record.table_name.clone()).or_default();
entry.table_units += factor * table_write_capacity_units(record.old_size, record.new_size);
for (name, units) in &record.gsi_units {
*entry.gsi_units.entry(name.clone()).or_default() += units;
}
for (name, units) in &record.lsi_units {
*entry.lsi_units.entry(name.clone()).or_default() += units;
}
for (name, bytes) in &record.vector_bytes {
*entry.vector_bytes.entry(name.clone()).or_default() += bytes;
}
}
by_table
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::ProjectionType;
fn gsi_include() -> IndexDef {
IndexDef {
index_name: "gsi-inc".to_string(),
pk_attr: "gsiPk".to_string(),
sk_attr: None,
hash_attrs: vec!["gsiPk".to_string()],
range_attrs: Vec::new(),
projection_type: ProjectionType::INCLUDE,
non_key_attributes: Some(vec!["proj".to_string()]),
}
}
fn gsi_all() -> IndexDef {
IndexDef {
index_name: "gsi-all".to_string(),
pk_attr: "gsiPk".to_string(),
sk_attr: None,
hash_attrs: vec!["gsiPk".to_string()],
range_attrs: Vec::new(),
projection_type: ProjectionType::ALL,
non_key_attributes: None,
}
}
fn gsi_keys_only() -> IndexDef {
IndexDef {
index_name: "gsi-keys".to_string(),
pk_attr: "gsiPk".to_string(),
sk_attr: None,
hash_attrs: vec!["gsiPk".to_string()],
range_attrs: Vec::new(),
projection_type: ProjectionType::KEYS_ONLY,
non_key_attributes: None,
}
}
fn lsi_include() -> IndexDef {
IndexDef {
index_name: "lsi-inc".to_string(),
pk_attr: "pk".to_string(),
sk_attr: Some("lsiSk".to_string()),
hash_attrs: vec!["pk".to_string()],
range_attrs: vec!["lsiSk".to_string()],
projection_type: ProjectionType::INCLUDE,
non_key_attributes: Some(vec!["proj".to_string()]),
}
}
fn s(v: &str) -> AttributeValue {
AttributeValue::S(v.to_string())
}
fn item(pairs: &[(&str, &str)]) -> Item {
let mut item: HashMap<String, AttributeValue> = HashMap::new();
for (k, v) in pairs {
item.insert((*k).to_string(), s(v));
}
item
}
fn pad(n: usize) -> String {
"x".repeat(n)
}
fn units(old: Option<&Item>, new: Option<&Item>, index: &IndexDef) -> Option<f64> {
index_write_units(old, new, index, "pk", Some("sk"))
}
#[test]
fn absent_on_both_sides_charges_nothing() {
let without = item(&[("pk", "a"), ("sk", "1"), ("other", "o")]);
assert_eq!(units(Some(&without), Some(&without), &gsi_include()), None);
assert_eq!(units(None, None, &gsi_include()), None);
}
#[test]
fn joining_the_index_charges_the_new_entry() {
let new = item(&[("pk", "v1"), ("sk", "1"), ("gsiPk", "g"), ("proj", "p")]);
assert_eq!(units(None, Some(&new), &gsi_include()), Some(1.0));
}
#[test]
fn leaving_the_index_charges_the_old_entry() {
let old = item(&[
("pk", "s7"),
("sk", "1"),
("gsiPk", "g"),
("proj", &pad(3000)),
]);
let new = item(&[("pk", "s7"), ("sk", "1"), ("proj", &pad(3000))]);
assert_eq!(units(Some(&old), Some(&new), &gsi_include()), Some(3.0));
}
#[test]
fn delete_charges_the_old_entry() {
let old = item(&[
("pk", "s6"),
("sk", "1"),
("gsiPk", "g"),
("proj", &pad(3000)),
]);
assert_eq!(units(Some(&old), None, &gsi_include()), Some(3.0));
}
#[test]
fn delete_of_a_non_member_charges_nothing() {
let old = item(&[("pk", "c"), ("sk", "1"), ("other", "o")]);
assert_eq!(units(Some(&old), None, &gsi_include()), None);
}
#[test]
fn identical_overwrite_charges_nothing() {
let same = item(&[("pk", "v1"), ("sk", "1"), ("gsiPk", "g"), ("proj", "p")]);
assert_eq!(units(Some(&same), Some(&same), &gsi_include()), None);
}
#[test]
fn a_reordered_set_is_not_a_change() {
let mut old = item(&[("pk", "v1"), ("sk", "1"), ("gsiPk", "g")]);
let mut new = old.clone();
old.insert(
"proj".to_string(),
AttributeValue::SS(vec!["a".to_string(), "b".to_string(), "c".to_string()]),
);
new.insert(
"proj".to_string(),
AttributeValue::SS(vec!["c".to_string(), "a".to_string(), "b".to_string()]),
);
assert_eq!(units(Some(&old), Some(&new), &gsi_include()), None);
}
#[test]
fn a_reordered_number_set_is_not_a_change() {
let mut old = item(&[("pk", "v1"), ("sk", "1"), ("gsiPk", "g")]);
let mut new = old.clone();
old.insert(
"proj".to_string(),
AttributeValue::NS(vec!["1".to_string(), "2".to_string(), "3".to_string()]),
);
new.insert(
"proj".to_string(),
AttributeValue::NS(vec!["3".to_string(), "1".to_string(), "2".to_string()]),
);
assert_eq!(units(Some(&old), Some(&new), &gsi_include()), None);
}
#[test]
fn a_reordered_binary_set_is_not_a_change() {
let mut old = item(&[("pk", "v1"), ("sk", "1"), ("gsiPk", "g")]);
let mut new = old.clone();
old.insert(
"proj".to_string(),
AttributeValue::BS(vec![vec![1, 2], vec![3], vec![4, 5, 6]]),
);
new.insert(
"proj".to_string(),
AttributeValue::BS(vec![vec![4, 5, 6], vec![1, 2], vec![3]]),
);
assert_eq!(units(Some(&old), Some(&new), &gsi_include()), None);
}
#[test]
fn renaming_an_attribute_is_a_change() {
let old = item(&[("pk", "v1"), ("sk", "1"), ("gsiPk", "g"), ("before", "v")]);
let new = item(&[("pk", "v1"), ("sk", "1"), ("gsiPk", "g"), ("after", "v")]);
assert_eq!(units(Some(&old), Some(&new), &gsi_all()), Some(1.0));
}
#[test]
fn a_changed_set_member_is_still_a_change() {
let mut old = item(&[("pk", "v1"), ("sk", "1"), ("gsiPk", "g")]);
let mut new = old.clone();
old.insert(
"proj".to_string(),
AttributeValue::SS(vec!["a".to_string(), "b".to_string()]),
);
new.insert(
"proj".to_string(),
AttributeValue::SS(vec!["b".to_string(), "z".to_string()]),
);
assert_eq!(units(Some(&old), Some(&new), &gsi_include()), Some(1.0));
}
#[test]
fn a_reordered_list_is_a_change() {
let mut old = item(&[("pk", "v1"), ("sk", "1"), ("gsiPk", "g")]);
let mut new = old.clone();
old.insert("proj".to_string(), AttributeValue::L(vec![s("a"), s("b")]));
new.insert("proj".to_string(), AttributeValue::L(vec![s("b"), s("a")]));
assert_eq!(units(Some(&old), Some(&new), &gsi_include()), Some(1.0));
}
#[test]
fn a_reordered_set_nested_in_a_map_is_not_a_change() {
let mut old = item(&[("pk", "v1"), ("sk", "1"), ("gsiPk", "g")]);
let mut new = old.clone();
let nest = |members: Vec<&str>| {
let mut m: HashMap<String, AttributeValue> = HashMap::new();
m.insert(
"tags".to_string(),
AttributeValue::SS(members.into_iter().map(str::to_string).collect()),
);
AttributeValue::M(m)
};
old.insert("proj".to_string(), nest(vec!["a", "b"]));
new.insert("proj".to_string(), nest(vec!["b", "a"]));
assert_eq!(units(Some(&old), Some(&new), &gsi_include()), None);
}
#[test]
fn change_outside_the_projection_charges_nothing() {
let old = item(&[("pk", "b"), ("sk", "1"), ("gsiPk", "g"), ("other", "o")]);
let new = item(&[("pk", "b"), ("sk", "1"), ("gsiPk", "g"), ("other", "o2")]);
assert_eq!(units(Some(&old), Some(&new), &gsi_include()), None);
assert_eq!(units(Some(&old), Some(&new), &gsi_keys_only()), None);
}
#[test]
fn change_outside_the_projection_still_charges_an_all_projection() {
let old = item(&[("pk", "b"), ("sk", "1"), ("gsiPk", "g"), ("other", "o")]);
let new = item(&[("pk", "b"), ("sk", "1"), ("gsiPk", "g"), ("other", "o2")]);
assert_eq!(units(Some(&old), Some(&new), &gsi_all()), Some(1.0));
}
#[test]
fn in_place_change_is_charged_on_the_larger_image() {
let big = item(&[
("pk", "s2"),
("sk", "1"),
("gsiPk", "g"),
("proj", &pad(3000)),
]);
let small = item(&[("pk", "s2"), ("sk", "1"), ("gsiPk", "g"), ("proj", "p")]);
assert_eq!(units(Some(&big), Some(&small), &gsi_include()), Some(3.0));
assert_eq!(units(Some(&small), Some(&big), &gsi_include()), Some(3.0));
}
#[test]
fn in_place_change_rounds_once_not_on_the_summed_bytes() {
let a = item(&[
("pk", "r1"),
("sk", "1"),
("gsiPk", "g"),
("proj", &pad(600)),
]);
let b = item(&[
("pk", "r1"),
("sk", "1"),
("gsiPk", "g"),
("proj", &"y".repeat(600)),
]);
assert_eq!(units(Some(&a), Some(&b), &gsi_include()), Some(1.0));
let c = item(&[
("pk", "r2"),
("sk", "1"),
("gsiPk", "g"),
("proj", &pad(1500)),
]);
let d = item(&[
("pk", "r2"),
("sk", "1"),
("gsiPk", "g"),
("proj", &"y".repeat(1500)),
]);
assert_eq!(units(Some(&c), Some(&d), &gsi_include()), Some(2.0));
}
#[test]
fn in_place_shrink_across_a_boundary_holds_the_larger_image() {
let old = item(&[
("pk", "r3"),
("sk", "1"),
("gsiPk", "g"),
("proj", &pad(2000)),
]);
let new = item(&[
("pk", "r3"),
("sk", "1"),
("gsiPk", "g"),
("proj", &pad(100)),
]);
assert_eq!(units(Some(&old), Some(&new), &gsi_include()), Some(2.0));
}
#[test]
fn key_move_charges_both_halves_separately() {
let old = item(&[
("pk", "s4"),
("sk", "1"),
("gsiPk", "A"),
("proj", &pad(1500)),
]);
let new = item(&[
("pk", "s4"),
("sk", "1"),
("gsiPk", "B"),
("proj", &pad(1500)),
]);
assert_eq!(units(Some(&old), Some(&new), &gsi_include()), Some(4.0));
}
#[test]
fn key_move_sizes_each_half_on_its_own_image() {
let old = item(&[
("pk", "s5"),
("sk", "1"),
("gsiPk", "A"),
("proj", &pad(3000)),
]);
let new = item(&[("pk", "s5"), ("sk", "1"), ("gsiPk", "B"), ("proj", "p")]);
assert_eq!(units(Some(&old), Some(&new), &gsi_include()), Some(4.0));
}
#[test]
fn sub_kilobyte_key_move_costs_two() {
let old = item(&[("pk", "b"), ("sk", "1"), ("gsiPk", "g")]);
let new = item(&[("pk", "b"), ("sk", "1"), ("gsiPk", "g2")]);
assert_eq!(units(Some(&old), Some(&new), &gsi_include()), Some(2.0));
}
#[test]
fn sizing_ignores_attributes_the_index_does_not_project() {
let new = item(&[
("pk", "s1"),
("sk", "1"),
("gsiPk", "g"),
("proj", "p"),
("other", &pad(3000)),
]);
assert_eq!(units(None, Some(&new), &gsi_include()), Some(1.0));
assert_eq!(units(None, Some(&new), &gsi_all()), Some(3.0));
}
#[test]
fn lsi_follows_the_same_rules() {
let small = item(&[("pk", "l1"), ("sk", "1"), ("lsiSk", "L"), ("proj", "p")]);
let big = item(&[
("pk", "l1"),
("sk", "1"),
("lsiSk", "L"),
("proj", &pad(3000)),
]);
assert_eq!(units(Some(&small), Some(&big), &lsi_include()), Some(3.0));
let moved_from = item(&[
("pk", "l2"),
("sk", "1"),
("lsiSk", "A"),
("proj", &pad(3000)),
]);
let moved_to = item(&[("pk", "l2"), ("sk", "1"), ("lsiSk", "B"), ("proj", "p")]);
assert_eq!(
units(Some(&moved_from), Some(&moved_to), &lsi_include()),
Some(4.0)
);
}
#[test]
fn sparse_membership_decides_presence_on_each_side() {
let member = item(&[("pk", "sp"), ("sk", "1"), ("lsiSk", "L")]);
let no_sort_key = item(&[("pk", "sp"), ("sk", "1")]);
assert_eq!(
units(Some(&no_sort_key), Some(&member), &lsi_include()),
Some(1.0)
);
assert_eq!(
units(Some(&member), Some(&no_sort_key), &lsi_include()),
Some(1.0)
);
let mut non_scalar = member.clone();
non_scalar.insert(
"lsiSk".to_string(),
AttributeValue::L(vec![AttributeValue::S("nope".to_string())]),
);
assert_eq!(units(Some(&non_scalar), None, &lsi_include()), None);
}
#[test]
fn numeric_rendering_change_is_a_real_change() {
let mut old = item(&[("pk", "n"), ("sk", "1"), ("gsiPk", "g")]);
old.insert("proj".to_string(), AttributeValue::N("1".to_string()));
let mut new = old.clone();
new.insert("proj".to_string(), AttributeValue::N("1.0".to_string()));
assert_eq!(units(Some(&old), Some(&new), &gsi_include()), Some(1.0));
}
fn units_map(pairs: &[(&str, f64)]) -> HashMap<String, f64> {
pairs
.iter()
.map(|(name, u)| ((*name).to_string(), *u))
.collect()
}
fn table_of<'a>(agg: &'a HashMap<String, TableCapacity>, name: &str) -> &'a TableCapacity {
agg.get(name).expect("table missing from aggregate")
}
#[test]
fn record_takes_its_sizes_from_the_images() {
let new = item(&[("pk", "a"), ("sk", "1"), ("gsiPk", "g")]);
let insert =
WriteCapacity::from_items("t", None, Some(&new), HashMap::new(), HashMap::new());
assert_eq!(insert.old_size, None);
assert_eq!(insert.new_size, Some(crate::types::item_size(&new)));
let delete =
WriteCapacity::from_items("t", Some(&new), None, HashMap::new(), HashMap::new());
assert_eq!(delete.old_size, Some(crate::types::item_size(&new)));
assert_eq!(delete.new_size, None);
}
#[test]
fn record_carries_one_arm_per_index_kind() {
let new = item(&[("pk", "a3"), ("sk", "1"), ("gsiPk", "g"), ("lsiSk", "L")]);
let record = WriteCapacity::from_items(
"t",
None,
Some(&new),
units_map(&[("gsi-inc", 1.0)]),
units_map(&[("lsi-all", 1.0)]),
);
assert_eq!(record.gsi_units.len(), 1);
assert_eq!(record.lsi_units.len(), 1);
}
#[test]
fn identical_overwrite_keeps_its_sizes_and_drops_its_arms() {
let same = item(&[
("pk", "k3"),
("sk", "1"),
("gsiPk", "g"),
("proj", &pad(3000)),
]);
let record = WriteCapacity::from_items(
"t",
Some(&same),
Some(&same),
HashMap::new(),
HashMap::new(),
);
assert!(record.gsi_units.is_empty());
assert!(record.lsi_units.is_empty());
let agg = aggregate_by_table(&[record], crate::types::TRANSACTIONAL_CAPACITY_FACTOR);
assert_eq!(table_of(&agg, "t").table_units, 6.0);
}
#[test]
fn transactional_factor_reaches_the_table_arm_and_not_the_index_arms() {
let new = item(&[("pk", "a1"), ("sk", "1"), ("gsiPk", "g")]);
let record = WriteCapacity::from_items(
"t",
None,
Some(&new),
units_map(&[("gsi-inc", 1.0)]),
units_map(&[("lsi-all", 1.0)]),
);
let agg = aggregate_by_table(&[record], crate::types::TRANSACTIONAL_CAPACITY_FACTOR);
let t = table_of(&agg, "t");
assert_eq!(t.table_units, 2.0);
assert_eq!(t.gsi_units.get("gsi-inc"), Some(&1.0));
assert_eq!(t.lsi_units.get("lsi-all"), Some(&1.0));
}
#[test]
fn factor_of_one_leaves_the_table_arm_undoubled() {
let new = item(&[("pk", "c1"), ("sk", "1"), ("gsiPk", "g")]);
let record = WriteCapacity::from_items(
"t",
None,
Some(&new),
units_map(&[("gsi-inc", 1.0)]),
HashMap::new(),
);
let agg = aggregate_by_table(&[record], 1.0);
assert_eq!(table_of(&agg, "t").table_units, 1.0);
assert_eq!(table_of(&agg, "t").gsi_units.get("gsi-inc"), Some(&1.0));
}
#[test]
fn two_actions_against_one_table_sum_every_arm() {
let a = item(&[("pk", "a13"), ("sk", "1"), ("gsiPk", "g"), ("lsiSk", "L")]);
let b = item(&[("pk", "a14"), ("sk", "1"), ("gsiPk", "g"), ("lsiSk", "L")]);
let records = vec![
WriteCapacity::from_items(
"t",
None,
Some(&a),
units_map(&[("gsi-inc", 1.0)]),
units_map(&[("lsi-all", 1.0)]),
),
WriteCapacity::from_items(
"t",
None,
Some(&b),
units_map(&[("gsi-inc", 1.0)]),
units_map(&[("lsi-all", 1.0)]),
),
];
let agg = aggregate_by_table(&records, crate::types::TRANSACTIONAL_CAPACITY_FACTOR);
let t = table_of(&agg, "t");
assert_eq!(t.table_units, 4.0);
assert_eq!(t.gsi_units.get("gsi-inc"), Some(&2.0));
assert_eq!(t.lsi_units.get("lsi-all"), Some(&2.0));
}
#[test]
fn actions_against_different_tables_aggregate_separately() {
let a = item(&[("pk", "g1"), ("sk", "1"), ("gsiPk", "g"), ("lsiSk", "L")]);
let b = item(&[("pk", "g1"), ("sk", "1"), ("gsiPk", "g")]);
let records = vec![
WriteCapacity::from_items(
"first",
None,
Some(&a),
units_map(&[("gsi-inc", 1.0)]),
units_map(&[("lsi-all", 1.0)]),
),
WriteCapacity::from_items(
"second",
None,
Some(&b),
units_map(&[("gsi-inc", 1.0)]),
HashMap::new(),
),
];
let agg = aggregate_by_table(&records, crate::types::TRANSACTIONAL_CAPACITY_FACTOR);
assert_eq!(agg.len(), 2);
assert_eq!(table_of(&agg, "first").table_units, 2.0);
assert!(table_of(&agg, "first").lsi_units.contains_key("lsi-all"));
assert_eq!(table_of(&agg, "second").table_units, 2.0);
assert!(table_of(&agg, "second").lsi_units.is_empty());
}
#[test]
fn an_action_with_no_image_either_side_still_costs_the_minimum() {
let record = WriteCapacity::from_items("t", None, None, HashMap::new(), HashMap::new());
let agg = aggregate_by_table(&[record], crate::types::TRANSACTIONAL_CAPACITY_FACTOR);
assert_eq!(table_of(&agg, "t").table_units, 2.0);
}
#[test]
fn condition_check_against_a_missing_target_costs_the_minimum() {
let record = WriteCapacity::condition_check("t", None, &[]);
assert_eq!(record.old_size, None);
assert_eq!(record.new_size, None);
let agg = aggregate_by_table(&[record], crate::types::TRANSACTIONAL_CAPACITY_FACTOR);
assert_eq!(table_of(&agg, "t").table_units, 2.0);
}
#[test]
fn condition_check_is_sized_on_the_image_it_reads() {
let existing = item(&[("pk", "f1"), ("sk", "1"), ("other", &pad(3000))]);
let record = WriteCapacity::condition_check("t", Some(&existing), &[]);
assert!(record.gsi_units.is_empty());
assert!(record.lsi_units.is_empty());
let agg = aggregate_by_table(&[record], crate::types::TRANSACTIONAL_CAPACITY_FACTOR);
assert_eq!(table_of(&agg, "t").table_units, 6.0);
}
#[test]
fn a_key_move_doubles_its_table_arm_and_leaves_its_index_arm_alone() {
let old = item(&[
("pk", "b7"),
("sk", "1"),
("gsiPk", "A"),
("proj", &pad(1500)),
]);
let new = item(&[
("pk", "b7"),
("sk", "1"),
("gsiPk", "B"),
("proj", &pad(1500)),
]);
let arm = units(Some(&old), Some(&new), &gsi_include()).unwrap();
assert_eq!(arm, 4.0);
let record = WriteCapacity::from_items(
"t",
Some(&old),
Some(&new),
units_map(&[("gsi-inc", arm)]),
HashMap::new(),
);
let agg = aggregate_by_table(&[record], crate::types::TRANSACTIONAL_CAPACITY_FACTOR);
let t = table_of(&agg, "t");
assert_eq!(t.table_units, 4.0);
assert_eq!(t.gsi_units.get("gsi-inc"), Some(&4.0));
}
#[test]
fn a_shrinking_write_is_sized_on_the_image_it_replaced() {
let old = item(&[("pk", "b3"), ("sk", "1"), ("other", &pad(3000))]);
let new = item(&[("pk", "b3"), ("sk", "1")]);
let record =
WriteCapacity::from_items("t", Some(&old), Some(&new), HashMap::new(), HashMap::new());
let agg = aggregate_by_table(&[record], crate::types::TRANSACTIONAL_CAPACITY_FACTOR);
assert_eq!(table_of(&agg, "t").table_units, 6.0);
}
#[test]
fn round_tripped_item_compares_equal() {
let original = item(&[("pk", "rt"), ("sk", "1"), ("gsiPk", "g"), ("proj", "p")]);
let json = serde_json::to_string(&original).unwrap();
let round_tripped: Item = serde_json::from_str(&json).unwrap();
assert_eq!(
units(Some(&round_tripped), Some(&original), &gsi_include()),
None
);
}
}