DynamoDB library for Rust.
You can see more examples here
raiden uses aws-sdk-dynamodb or rusoto_dynamodb as internal client.
use raiden::*;
#[derive(Raiden)]
#[raiden(table_name = "user")]
struct User {
#[raiden(partition_key)]
id: String,
name: String,
}
#[tokio::main]
async fn main() {
// Simply, specify the region.
let client = User::client(config::Region::from_static("us-east-1"));
// You can also specify aws-sdk-dynamodb client.
let client = {
let sdk_config = aws_config::defaults(aws_config::BehaviorVersion::latest())
.region(raiden::config::Region::from_static("us-east-1"))
.load()
.await;
let sdk_client = raiden::Client::new(&sdk_config);
User::client_with(sdk_client)
};
// Run operations...
}use raiden::*;
#[derive(Raiden)]
#[raiden(table_name = "user")]
struct User {
#[raiden(partition_key)]
id: String,
name: String,
}
#[tokio::main]
async fn main() {
// Simply, specify the region.
let client = User::client(Region::UsEast1);
// You can also specify rusoto_core client.
let client = User::client_with(Client::shared(), Region::UsEast1);
// Run operations...
}use raiden::*;
#[derive(Raiden)]
#[raiden(table_name = "user")]
struct User {
#[raiden(partition_key)]
id: String,
name: String,
}
#[tokio::main]
async fn main() {
let client = User::client(config::Region::from_static("us-east-1"))
.table_prefix("prefix-")
.table_suffix("-suffix");
// Print `prefix-user-suffix`
println!("{}", client.table_name());
}NOTE: Default retry strategy differs between aws-sdk and rusoto ( or rusoto_rustls )
aws-sdk... Not retry in raiden by default. Because you can configure retry strategy usingaws_config. Or you can configure your own strategy like next example.rusotoorrusoto_rustls... Enabled retrying in raiden by default. See detail here.
use raiden::*;
#[derive(Raiden)]
#[raiden(table_name = "user")]
struct User {
#[raiden(partition_key)]
id: String,
name: String,
}
// Force retry 3 times.
struct MyRetryStrategy;
impl RetryStrategy for MyRetryStrategy {
fn should_retry(&self, _error: &RaidenError) -> bool {
true
}
fn policy(&self) -> Policy {
Policy::Limit(3)
}
}
#[tokio::main]
async fn main() {
let client = User::client(config::Region::from_static("us-east-1"))
.with_retries(Box::new(MyRetryStrategy));
// Run operations...
}use raiden::*;
#[derive(Raiden)]
#[raiden(table_name = "user")]
struct User {
#[raiden(partition_key)]
id: String,
name: String,
}
#[tokio::main]
async fn main() {
let client = /* generate client */;
let _res = client.get("user_primary_key").run().await;
}use raiden::*;
#[derive(Raiden)]
#[raiden(table_name = "user")]
pub struct User {
#[raiden(partition_key)]
id: String,
name: String,
}
#[tokio::main]
async fn main() {
let client = /* generate client */;
let input = User::put_item_builder()
.id("foo".to_owned())
.name("bokuweb".to_owned())
.build();
let _res = client.put(&input).run().await;
}use std::collections::{BTreeMap, HashMap};
use raiden::*;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, RaidenDocument)]
struct Profile {
display_name: String,
level: usize,
}
#[derive(Raiden)]
#[raiden(table_name = "user")]
struct User {
#[raiden(partition_key)]
id: String,
metadata: HashMap<String, usize>,
flags: BTreeMap<String, bool>,
profile: Profile,
profiles: HashMap<String, Profile>,
}
#[tokio::main]
async fn main() {
let client = /* generate client */;
let mut metadata = HashMap::new();
metadata.insert("score".to_owned(), 42);
let mut flags = BTreeMap::new();
flags.insert("active".to_owned(), true);
let profile = Profile {
display_name: "bokuweb".to_owned(),
level: 3,
};
let mut profiles = HashMap::new();
profiles.insert("primary".to_owned(), profile.clone());
let input = User::put_item_builder()
.id("user#1".to_owned())
.metadata(metadata)
.flags(flags)
.profile(profile)
.profiles(profiles)
.build();
let _res = client.put(input).run().await;
}Notes:
- map key is currently limited to
String - use
#[derive(RaidenDocument)]when you want to store a nested type directly as a field - enums can also derive
RaidenDocument; serde enum tagging such as#[serde(tag = "type")]is preserved when values are encoded into DynamoDB maps and decoded back Document<T>remains available as an explicit wrapper when you prefer opt-in at the field type level- empty maps are preserved as empty DynamoDB
Mvalues rather than being dropped
Tagged enums can be stored directly as nested document fields:
use std::collections::HashMap;
use raiden::*;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, RaidenDocument)]
#[serde(tag = "type")]
enum Message {
Request {
id: String,
method: String,
params: HashMap<String, usize>,
},
Response {
id: String,
result: String,
},
}
#[derive(Raiden)]
#[raiden(table_name = "event")]
struct Event {
#[raiden(partition_key)]
id: String,
message: Message,
}
fn input() -> EventPutItemInput {
let mut params = HashMap::new();
params.insert("attempt".to_owned(), 1);
Event::put_item_builder()
.id("event#1".to_owned())
.message(Message::Request {
id: "msg#1".to_owned(),
method: "send".to_owned(),
params,
})
.build()
}When a tagged enum is used as the item type itself, the DynamoDB item map is decoded through serde as a whole document. This allows projection reads such as query().project::<Message>() when the item contains the tag and variant fields at the top level.
When decoding an item fails, RaidenError::AttributeConvertError includes the attribute name and the original ConversionError in its source field. This also preserves custom messages returned by FromAttribute implementations:
if let Err(RaidenError::AttributeConvertError { attr_name, source }) = result {
eprintln!("could not decode {attr_name}: {source}");
}use std::collections::HashMap;
use raiden::*;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, RaidenDocument)]
struct Profile {
display_name: String,
level: usize,
}
#[derive(Raiden)]
#[raiden(table_name = "user")]
struct User {
#[raiden(partition_key)]
id: String,
admin_ids: Vec<String>,
metadata: HashMap<String, usize>,
profile: Profile,
}
#[tokio::main]
async fn main() {
let client = /* generate client */;
let key = User::key_condition(User::id()).eq("user#1");
let filter = User::filter_expression(User::metadata().key("score"))
.ge(40)
.and(User::filter_expression(User::profile().field(Profile::level())).eq(3));
let _res = client
.query()
.key_condition(key)
.filter(filter)
.run()
.await;
let condition = User::condition()
.attr_exists(User::metadata().key("score"))
.and(User::condition().attr(User::profile().field(Profile::level())).eq_value(3));
// Condition expressions also support typed function operands and logical
// composition. Values and names are bound through collision-free placeholders.
let preserve_last_admin = User::condition()
.not()
.contains(User::admin_ids(), "member#1")
.or(User::condition().size(User::admin_ids()).ge(2_usize));
let not_in_range = User::condition()
.between(User::metadata().key("score"), 10, 20)
.or(User::condition().in_values(User::metadata().key("score"), [30, 40]))
.not(); // NOT (the entire BETWEEN ... OR ... group)
// `condition` can be passed to `put`, `update`, `delete`,
// transaction writes, and other conditional operations.
}Notes:
- use
.key("...")for dynamic map keys such asmetadata.score - use
.field(...)with#[derive(RaidenDocument)]accessors for nested document fields such asprofile.level - document paths are supported in
filter_expressionandcondition - condition expressions support
contains, attribute comparisons (eq_attr/eq_value,ne_attr/ne_value,lt_attr/lt_value,le_attr/le_value,gt_attr/gt_value,ge_attr/ge_value),between,in_values(1–100 values), numericsizecomparisons (eq,ne,lt,le,gt, andge), andnot/and/orcomposition - call
.not()on a completed condition to negate the whole group sizecomparisons only accept values implementingIntoNumberAttribute; raw condition strings are not neededkey_conditionstill follows DynamoDB key rules, so nested map/document values are not valid partition or sort keys unless you project them to top-level attributes or an index.index(usize)is also available when you need to address list elements in a document path- path segments are emitted through expression attribute names, so reserved words remain escaped correctly
use raiden::*;
#[derive(Raiden, Debug, PartialEq)]
pub struct User {
#[raiden(partition_key)]
id: String,
#[raiden(sort_key)]
year: usize,
}
#[tokio::main]
async fn main() {
let client = /* generate client */;
let keys: Vec<(&str, usize)> = vec![("Alice", 1992), ("Bob", 1976), ("Charlie", 2002)];
let res = client.batch_get(keys).run().await;
}use raiden::*;
#[derive(Raiden, Debug, PartialEq)]
pub struct User {
#[raiden(partition_key)]
id: String,
#[raiden(sort_key)]
year: usize,
name: String,
}
#[tokio::main]
async fn main() {
let client = /* generate client */;
let items = vec![
User::put_item_builder()
.id("Alice".to_owned())
.year(1992)
.name("Alice".to_owned())
.build(),
User::put_item_builder()
.id("Bob".to_owned())
.year(1976)
.name("Bob".to_owned())
.build(),
];
let res = client.batch_put(items).run().await;
}use raiden::*;
#[derive(Raiden, Debug, PartialEq)]
pub struct User {
#[raiden(partition_key)]
id: String,
#[raiden(sort_key)]
year: usize,
name: String,
}
#[tokio::main]
async fn main() {
let client = /* generate client */;
let keys: Vec<(&str, usize)> = vec![("Alice", 1992), ("Bob", 1976)];
let res = client.transact_get(keys).run().await;
// Responses preserve the request order.
// Missing items are returned as `None`.
}use raiden::*;
#[derive(Raiden)]
#[raiden(table_name = "user")]
#[raiden(
gsi(
name = "userIndex",
partition_key = "org_id",
sort_key = "created_at",
sort_key = "status"
)
)]
struct User {
#[raiden(partition_key)]
id: String,
org_id: String,
created_at: String,
status: String,
#[raiden(omit_gsi = "userIndex")]
internal_note: String,
}
#[tokio::main]
async fn main() {
let client = /* generate client */;
let cond = UserIndexItem::user_index_key_condition()
.eq("org_1")
.and(UserIndexItem::user_index_sort_key_condition_1().eq("2026-03-28T00:00:00Z"))
.and(UserIndexItem::user_index_sort_key_condition_2().begins_with("active"));
let _res = client
.query()
.user_index()
.project::<UserIndexItem>()
.key_condition(cond)
.run()
.await;
let _res = UserIndexItem::query(&client)
.key_condition(cond)
.run()
.await;
}If you want to override the generated projection shape or type name, declare a
RaidenIndex explicitly:
use raiden::*;
#[derive(Raiden)]
#[raiden(table_name = "user")]
#[raiden(gsi(name = "userIndex", partition_key = "org_id"))]
struct User {
#[raiden(partition_key)]
id: String,
org_id: String,
display_name: String,
avatar_url: String,
#[raiden(omit_gsi = "userIndex")]
internal_note: String,
}
#[derive(RaidenIndex, Debug, PartialEq)]
#[raiden(source = "User", gsi = "userIndex")]
#[raiden(gsi(name = "userIndex", partition_key = "org_id"))]
struct PublicUserIndexItem {
org_id: String,
display_name: String,
}
#[tokio::main]
async fn main() {
let client = /* generate client */;
let cond = PublicUserIndexItem::user_index_key_condition().eq("org_1");
let _res = PublicUserIndexItem::query(&client)
.key_condition(cond)
.run()
.await;
}Composite GSIs with multiple sort-key segments are also supported:
use raiden::*;
#[derive(Raiden)]
#[raiden(table_name = "user")]
#[raiden(
gsi(
name = "activityIndex",
partition_key = "org_id",
sort_key = "created_at",
sort_key = "status"
)
)]
struct User {
#[raiden(partition_key)]
id: String,
org_id: String,
created_at: String,
status: String,
#[raiden(omit_gsi = "activityIndex")]
internal_note: String,
}
#[tokio::main]
async fn main() {
let client = /* generate client */;
let cond = UserActivityIndexItem::activity_index_key_condition()
.eq("org_1")
.and(UserActivityIndexItem::activity_index_sort_key_condition_1().eq("2026-03-28T00:00:00Z"))
.and(UserActivityIndexItem::activity_index_sort_key_condition_2().begins_with("active"));
let _res = UserActivityIndexItem::query(&client)
.key_condition(cond)
.run()
.await;
}The composite helper methods enforce DynamoDB's ordering rules:
- start with the partition key
- then chain sort key segment 1, sort key segment 2, and so on
- use range operators such as
gt,between, andbegins_withonly on the last sort key segment
Notes:
- typed GSI methods such as
user_index()are generated from#[raiden(gsi = "...")]or#[raiden(gsi(...))] #[raiden(omit_gsi = "userIndex")]also generates a default projection type such asUserIndexItem, so the common case does not require writing#[derive(RaidenIndex)]manually#[derive(RaidenIndex)]remains available when you want to override the generated projection shape or name, or when you prefer to declare the projection item explicitly#[derive(RaidenIndex)]also generatesYourIndexType::query(&client)andYourIndexType::scan(&client)helpers for projection-first access- add
#[raiden(gsi(name = "...", partition_key = "...", sort_key = "..."))]to theRaidenIndextype when you also want typed key condition helpers on the projection type itself - typed GSI query/scan keeps the base struct projection by default; switch to an index projection explicitly with
project::<...>() client.query().user_index().project::<UserIndexItem>()andUserIndexItem::query(&client)are equivalent entrypoints; choose whichever style is clearer for your call siteclient.scan().user_index().project::<UserIndexItem>()andUserIndexItem::scan(&client)are also equivalent entrypointsrun_with::<...>()remains available as a backward-compatible convenience wrapper- the legacy
.index("userIndex")API is still available for backward compatibility, and existing typed GSI builders still default to the source-item projection unless you opt into a projection item - composite GSI conditions must be chained in order: partition key -> sort key 1 -> sort key 2 ...
- range conditions such as
gt,between, andbegins_withare only allowed on the last sort key - the old
.index("userIndex")API is deprecated, but preserved for compatibility while migrating to typed GSI helpers
Declare an LSI with the table's alternate sort-key field. The table must have its own sort key, and the LSI shares the table partition key automatically:
use raiden::*;
#[derive(Raiden)]
#[raiden(table_name = "events")]
#[raiden(lsi(name = "createdIndex", sort_key = "created_at"))]
struct Event {
#[raiden(partition_key)]
account_id: String,
#[raiden(sort_key)]
event_id: String,
created_at: String,
#[raiden(omit_lsi = "createdIndex")]
private_note: String,
}
// `EventCreatedIndexItem` is generated from fields not marked `omit_lsi`.
let condition = Event::created_index_key_condition()
.eq("account-1")
.and(Event::created_index_sort_key_condition().begins_with("2026-"));
let result = client.query()
.created_index()
.project::<EventCreatedIndexItem>()
.consistent()
.key_condition(condition)
.run()
.await?;
let scan = client.scan()
.created_index()
.project::<EventCreatedIndexItem>()
.consistent()
.run()
.await?;#[derive(RaidenIndex)] also accepts #[raiden(source = "Event", lsi = "createdIndex")]
for a custom projection item. Add
#[raiden(lsi(name = "createdIndex", partition_key = "account_id", sort_key = "created_at"))]
on that projection type to generate its own typed key-condition methods. The
partition_key on a custom projection identifies the source table's key; on
the source Raiden type it is inferred and validated.
LSI queries and scans support .consistent(). Reading attributes that are not
projected into the LSI makes DynamoDB fetch them from the base table, increasing
read cost and latency. A GSI supports eventual consistency only. The library's
existing .consistent() method does not prevent selecting a GSI, so avoid that
combination.
raiden supports making span for Tracing ( span name is dynamodb::action with table name and api name in field ).
To activate this feature, you need to specify tracing feature in your Cargo.toml. And your crate needs tracing .
# Example
[dependencies]
raiden = {
tag = "0.0.76",
git = "https://github.com/raiden-rs/raiden-dynamo.git",
features = [ "tracing"]
}
tracing = "0.1"- Rust (1.76.0+)
- Deno (1.13.2+)
- GNU Make
- Docker Engine
make test
NOTE: Don't recommend to use cargo test because our test suite doesn't support running tests in parallel. Use cargo test -- --test-threads=1 instead of it.
make dynamo
AWS_ACCESS_KEY_ID=dummy AWS_SECRET_ACCESS_KEY=dummy cargo run --example EXAMPLE_NAME
dynamodb-admin is useful to check data in DynamoDB Local.
npx dynamodb-admin
Then open http://localhost:8001 in browser.
- BatchGetItem
- BatchWriteItem
- DeleteItem
- GetItem
- PutItem
- Query
- Scan
- TransactGetItems
- TransactWriteItems
- UpdateItem
Here is a list of unsupported features/behaviors in the actual implementation. We have a plan to resolve these issues in a future release.
This project is available under the terms of either the Apache 2.0 license or the MIT license.
