use crate::errors::{DynoxideError, Result};
use crate::storage::TableMetadata;
use crate::storage_backend::{IndexWriteOp, StorageBackend};
use crate::types::{GlobalSecondaryIndex, Item, KeyType, ProjectionType};
use std::collections::HashMap;
pub struct IndexWrite<'a> {
pub table_name: &'a str,
pub pk: &'a str,
pub sk: &'a str,
pub pk_attr: &'a str,
pub sk_attr: Option<&'a str>,
pub old_item: Option<&'a Item>,
pub capacity_mode: Option<&'a str>,
}
pub struct IndexDef {
pub index_name: String,
pub pk_attr: String,
pub sk_attr: Option<String>,
pub projection_type: ProjectionType,
pub non_key_attributes: Option<Vec<String>>,
}
pub type GsiDef = IndexDef;
impl IndexDef {
pub fn projects(&self, attr: &str, table_pk: &str, table_sk: Option<&str>) -> bool {
if attr == self.pk_attr
|| self.sk_attr.as_deref() == Some(attr)
|| attr == table_pk
|| table_sk == Some(attr)
{
return true;
}
match self.projection_type {
ProjectionType::ALL => true,
ProjectionType::KEYS_ONLY => false,
ProjectionType::INCLUDE => self
.non_key_attributes
.as_ref()
.is_some_and(|names| names.iter().any(|n| n == attr)),
}
}
pub fn index_key_strings(&self, item: &Item) -> Option<(String, String)> {
let pk = item.get(&self.pk_attr)?.to_key_string()?;
let sk = match self.sk_attr {
Some(ref sk_attr) => item.get(sk_attr)?.to_key_string()?,
None => String::new(),
};
Some((pk, sk))
}
}
pub fn gsi_to_def(gsi: &GlobalSecondaryIndex) -> Result<GsiDef> {
let pk_attr = gsi
.key_schema
.iter()
.find(|k| k.key_type == KeyType::HASH)
.map(|k| k.attribute_name.clone())
.ok_or_else(|| DynoxideError::InternalServerError("GSI missing HASH key".to_string()))?;
let sk_attr = gsi
.key_schema
.iter()
.find(|k| k.key_type == KeyType::RANGE)
.map(|k| k.attribute_name.clone());
Ok(GsiDef {
index_name: gsi.index_name.clone(),
pk_attr,
sk_attr,
projection_type: gsi.projection.projection_type.clone().unwrap_or_default(),
non_key_attributes: gsi.projection.non_key_attributes.clone(),
})
}
pub fn parse_gsi_defs(meta: &TableMetadata) -> Result<Vec<GsiDef>> {
if meta.gsi_definitions.is_some() {
crate::bench_counters::record(&crate::bench_counters::INDEX_DEFS_PARSES);
}
let gsis: Vec<GlobalSecondaryIndex> = match meta.gsi_definitions.as_ref() {
Some(json) => serde_json::from_str(json)
.map_err(|e| DynoxideError::InternalServerError(format!("Bad GSI JSON: {e}")))?,
None => return Ok(Vec::new()),
};
gsis.iter().map(gsi_to_def).collect()
}
pub fn build_index_item(
item: &Item,
index: &IndexDef,
table_pk: &str,
table_sk: Option<&str>,
) -> Item {
crate::bench_counters::record(&crate::bench_counters::INDEX_ENTRIES_BUILT);
if index.projection_type == ProjectionType::ALL {
return item.clone();
}
item.iter()
.filter(|(name, _)| index.projects(name, table_pk, table_sk))
.map(|(name, value)| (name.clone(), value.clone()))
.collect()
}
pub async fn maintain_gsis_after_write<S: StorageBackend>(
storage: &S,
meta: &TableMetadata,
target: &IndexWrite<'_>,
item: &Item,
) -> Result<HashMap<String, f64>> {
let gsi_defs = parse_gsi_defs(meta)?;
maintain_gsis_after_write_with_defs(storage, &gsi_defs, target, item).await
}
pub async fn maintain_gsis_after_write_with_defs<S: StorageBackend>(
storage: &S,
gsi_defs: &[GsiDef],
target: &IndexWrite<'_>,
item: &Item,
) -> Result<HashMap<String, f64>> {
let want_capacity = crate::types::capacity_wanted(target.capacity_mode);
let mut gsi_units: HashMap<String, f64> = HashMap::new();
let mut ops: Vec<IndexWriteOp> = Vec::new();
for gsi in gsi_defs {
ops.push(IndexWriteOp::DeleteGsi {
table_name: target.table_name.to_string(),
index_name: gsi.index_name.clone(),
table_pk: target.pk.to_string(),
table_sk: target.sk.to_string(),
});
let entry = super::index_capacity::entry_for(item, gsi, target.pk_attr, target.sk_attr);
if let Some(ref entry) = entry {
let (gsi_pk, gsi_sk) = entry.key.clone();
let item_json = serde_json::to_string(&entry.projected)
.map_err(|e| DynoxideError::InternalServerError(e.to_string()))?;
ops.push(IndexWriteOp::InsertGsi {
table_name: target.table_name.to_string(),
index_name: gsi.index_name.clone(),
gsi_pk,
gsi_sk,
table_pk: target.pk.to_string(),
table_sk: target.sk.to_string(),
item_json,
});
}
if want_capacity
&& let Some(units) = super::index_capacity::index_write_units_for(
target.old_item,
entry,
gsi,
target.pk_attr,
target.sk_attr,
)
{
gsi_units.insert(gsi.index_name.clone(), units);
}
}
storage.apply_index_writes(&ops).await?;
Ok(gsi_units)
}
pub async fn maintain_gsis_after_delete<S: StorageBackend>(
storage: &S,
meta: &TableMetadata,
target: &IndexWrite<'_>,
) -> Result<HashMap<String, f64>> {
let gsi_defs = parse_gsi_defs(meta)?;
maintain_gsis_after_delete_with_defs(storage, &gsi_defs, target).await
}
pub async fn maintain_gsis_after_delete_with_defs<S: StorageBackend>(
storage: &S,
gsi_defs: &[GsiDef],
target: &IndexWrite<'_>,
) -> Result<HashMap<String, f64>> {
let want_capacity = crate::types::capacity_wanted(target.capacity_mode);
let mut gsi_units: HashMap<String, f64> = HashMap::new();
let mut ops: Vec<IndexWriteOp> = Vec::new();
for gsi in gsi_defs {
ops.push(IndexWriteOp::DeleteGsi {
table_name: target.table_name.to_string(),
index_name: gsi.index_name.clone(),
table_pk: target.pk.to_string(),
table_sk: target.sk.to_string(),
});
if want_capacity
&& let Some(units) = super::index_capacity::index_write_units(
target.old_item,
None,
gsi,
target.pk_attr,
target.sk_attr,
)
{
gsi_units.insert(gsi.index_name.clone(), units);
}
}
storage.apply_index_writes(&ops).await?;
Ok(gsi_units)
}
pub fn parse_gsi_key_schema(
meta: &TableMetadata,
index_name: &str,
) -> Result<(String, Option<String>)> {
let gsi_defs = parse_gsi_defs(meta)?;
let gsi = gsi_defs
.into_iter()
.find(|g| g.index_name == index_name)
.ok_or_else(|| {
DynoxideError::ValidationException(format!(
"The table does not have the specified index: {index_name}"
))
})?;
Ok((gsi.pk_attr, gsi.sk_attr))
}