DynamoDB Rust Query Examples
This cheat sheet should help you understand how to perform a variety of operations starting from simple queries ending with complex transactions using the AWS Rust SDK.
The AWS Rust SDK doesn't currently have a high level DocumentClient like Node.js does, so you'll see more low-level DynamoDB concepts like defining types using AttributeValue
s when using the SDK.
Table of Contents
Setup
You'll want to include aws_config
and aws_sdk_dynamodb
in your Cargo.toml. Using cargo-edit:
cargo add aws_config aws_sdk_dynamodb
Typically you'll load the region and other configuration from the environment using aws_config
and then pass that to the dynamodb Client::new
method to create a new dynamodb client.
use aws_sdk_dynamodb::Client;
...
let shared_config = aws_config::load_from_env().await;
let client = Client::new(&shared_config);
The AWS Rust SDK takes advantage of async support with Tokio - an asynchronous runtime for Rust.
If you aren't comfortable with Rust, check out Rust Adventure by Chris Biscardi, an author of this blogpost where he teaches Rust the practical way by building Serverless Functions, interacting with Planetscale SQL databases, and even building a small game!
Create Table
DynamoDB structures data in tables, so if you want to save some data to DynamoDB, first you need to create a table. You can do that using AWS Console, AWS CLI, CDK, or using the aws-sdk-rust
, like this:
use aws_sdk_dynamodb::{
model::{
AttributeDefinition, BillingMode, KeySchemaElement,
KeyType, ScalarAttributeType,
},
Client, Error,
};
#[tokio::main]
async fn main() -> Result<(), Error> {
let shared_config = aws_config::load_from_env().await;
let client = Client::new(&shared_config);
let key = "name";
let pk = AttributeDefinition::builder()
.attribute_name(key)
.attribute_type(ScalarAttributeType::S)
.build();
let ks = KeySchemaElement::builder()
.attribute_name(key)
.key_type(KeyType::Hash)
.build();
client
.create_table()
.table_name(String::from("my-table"))
.key_schema(ks)
.attribute_definitions(pk)
.billing_mode(BillingMode::PayPerRequest)
.send()
.await?;
Ok(())
}
The create_table
call succeeding does not mean the database is ready to be used. Before we start manipulating items in it, we should check if it's in ACTIVE
state first using the describe_table
function ran every 5 seconds until it is ready:
use aws_sdk_dynamodb::{
error::{DescribeTableError, DescribeTableErrorKind},
model::{
AttributeDefinition, BillingMode, KeySchemaElement,
KeyType, ScalarAttributeType, TableDescription,
TableStatus,
},
output::DescribeTableOutput,
Client, Error,
};
use tokio::time;
#[tokio::main]
async fn main() -> Result<(), Error> {
let shared_config = aws_config::load_from_env().await;
let client = Client::new(&shared_config);
let key = "name";
let pk = AttributeDefinition::builder()
.attribute_name(key)
.attribute_type(ScalarAttributeType::S)
.build();
let ks = KeySchemaElement::builder()
.attribute_name(key)
.key_type(KeyType::Hash)
.build();
client
.create_table()
.table_name(String::from("my-table"))
.key_schema(ks)
.attribute_definitions(pk)
.billing_mode(BillingMode::PayPerRequest)
.send()
.await?;
let mut interval =
time::interval(time::Duration::from_secs(5));
loop {
println!(
"Waiting for database {} to become available",
"my-table"
);
interval.tick().await;
let resp = client
.describe_table()
.table_name("my-table")
.send()
.await;
match resp {
Ok(DescribeTableOutput {
table:
Some(TableDescription {
table_status: Some(TableStatus::Active),
..
}),
..
}) => {
break;
}
Err(aws_sdk_dynamodb::SdkError::ServiceError {
err:
DescribeTableError {
kind: DescribeTableErrorKind::ResourceNotFoundException(_),
..
},
raw: _,
}) => {
}
e => {
e?;
}
}
}
Ok(())
}
Delete Table
If you changed your mind and need to remove DynamoDB table, don't worry, it's simple:
use aws_sdk_dynamodb::{Client, Error};
#[tokio::main]
async fn main() -> Result<(), Error> {
let shared_config = aws_config::load_from_env().await;
let client = Client::new(&shared_config);
client
.delete_table()
.table_name("my-table")
.send()
.await?;
println!("Deleted 'my-table'");
Ok(())
}
Keep in mind that Dynobase is capable of removing tables too.
List Tables
If you want to check what tables are available at your disposal in current region, use the list_tables
call. Keep in mind that if selected region has more than 100 tables you'll have to paginate through them to fetch a complete list.
use aws_sdk_dynamodb::{Client, Error};
#[tokio::main]
async fn main() -> Result<(), Error> {
let shared_config = aws_config::load_from_env().await;
let client = Client::new(&shared_config);
let res = client.list_tables().send().await?;
dbg!(res.table_names);
Ok(())
}
Get All Items / Scan in DynamoDB
After our table is provisioned and it's in ACTIVE
state, first thing that we probably would like to do is get all items in it aka use (DynamoDB Scan operation):
use aws_sdk_dynamodb::{Client, Error};
#[tokio::main]
async fn main() -> Result<(), Error> {
let shared_config = aws_config::load_from_env().await;
let client = Client::new(&shared_config);
let resp =
client.scan().table_name("my-table").send().await?;
if let Some(item) = resp.items {
dbg!(item);
}
Ok(())
}
If you want to narrow your search results, use FilterExpressions
combined with ExpressionAttributeNames
object like so:
use aws_sdk_dynamodb::{
model::AttributeValue, Client, Error,
};
#[tokio::main]
async fn main() -> Result<(), Error> {
let shared_config = aws_config::load_from_env().await;
let client = Client::new(&shared_config);
let resp = client
.scan()
.filter_expression(
"lengthInSeconds <
:seconds",
)
.expression_attribute_values(
":seconds",
AttributeValue::N(100.to_string()),
)
.table_name("my-table")
.send()
.await?;
if let Some(item) = resp.items {
dbg!(item);
}
Ok(())
}
You can find full reference how to write FilterExpressions
in this post by Alex Debrie.
The snippet above will in fact return all the items in the table under one condition - you have less than 1MB of data inside it. If your table is bigger than that, you'll have to run Scan command a few times in a loop using pagination.
Get Item
If you know the exact Partition Key (and Sort Key if using composite key) of the item that you want to retrieve from the DynamoDB table, you can use get operation:
use aws_sdk_dynamodb::{
model::AttributeValue, Client, Error,
};
#[tokio::main]
async fn main() -> Result<(), Error> {
let shared_config = aws_config::load_from_env().await;
let client = Client::new(&shared_config);
let item = client
.get_item()
.table_name("my-table")
.key(
"name",
AttributeValue::S("bulbasaur".to_string()),
)
.send()
.await?;
dbg!(item.item);
Ok(())
}
Batch Get Item
aws-sdk-rust
is also capable of running bunch of get operations in a single call to the DynamoDB service:
use std::collections::HashMap;
use aws_sdk_dynamodb::{
model::{AttributeValue, KeysAndAttributes},
Client, Error,
};
#[tokio::main]
async fn main() -> Result<(), Error> {
let shared_config = aws_config::load_from_env().await;
let client = Client::new(&shared_config);
let items = client
.batch_get_item()
.request_items(
"my-table",
KeysAndAttributes::builder()
.keys(HashMap::from([(
"name".to_string(),
AttributeValue::S(
"bulbasaur".to_string(),
),
)]))
.keys(HashMap::from([(
"name".to_string(),
AttributeValue::S(
"charmander".to_string(),
),
)]))
.build(),
)
.send()
.await?;
dbg!(items);
Ok(())
}
As you can see, the RequestItems objects can accept multiple table names and can fetch multiple items from multiple tables in a single call. Keep in mind that number of items retrieved using batch_get_item
is limited to 100 items or 16MB of data.
Moreover, if you exceed table capacity, this call will return UnprocessedKeys
attribute containing a map of keys which weren't fetched.
Put Item aka Write
put_item
operation creates a new item, or replaces an old item with a new item if it's using the same key(s):
use aws_sdk_dynamodb::{
model::AttributeValue, Client, Error,
};
#[tokio::main]
async fn main() -> Result<(), Error> {
let shared_config = aws_config::load_from_env().await;
let client = Client::new(&shared_config);
let request = client
.put_item()
.table_name("my-table")
.item(
"name",
AttributeValue::S(String::from(
"bulbasaur".to_string(),
)),
)
.item(
"pokemon_type",
AttributeValue::S(String::from(
"grass".to_string(),
)),
);
request.send().await?;
Ok(())
}
Batch Write / Put Item
If you need to insert, update or delete multiple items in a single API call, use the batch_write_item
operation. It bundles multiple database requests against multiple tables into a single SDK call. It decreases amount of network calls needed to be made, reduces the overall latency and makes your application faster.
Example request writing two different items.
use std::collections::HashMap;
use aws_sdk_dynamodb::{
model::{AttributeValue, PutRequest, WriteRequest},
Client, Error,
};
#[tokio::main]
async fn main() -> Result<(), Error> {
let shared_config = aws_config::load_from_env().await;
let client = Client::new(&shared_config);
client
.batch_write_item()
.request_items(
"my-table",
vec![
WriteRequest::builder()
.put_request(
PutRequest::builder()
.set_item(Some(HashMap::from(
[
(
"name".to_string(),
AttributeValue::S(
"bulbasaur"
.to_string(
),
),
),
(
"pokemon_type"
.to_string(),
AttributeValue::S(
"grass"
.to_string(
),
),
),
],
)))
.build(),
)
.build(),
WriteRequest::builder()
.put_request(
PutRequest::builder()
.set_item(Some(HashMap::from(
[
(
"name".to_string(),
AttributeValue::S(
"charmander"
.to_string(
),
),
),
(
"pokemon_type"
.to_string(),
AttributeValue::S(
"fire"
.to_string(
),
),
),
],
)))
.build(),
)
.build(),
],
)
.send()
.await?;
Ok(())
}
If you are curious about performance of batch_write_item
, head to dynamodb-performance-testing repo by Alex DeBrie.
Query for a Set of Items
If your table has composite key (which is the best practice), in order to get a collection of items sharing the same Parition Key, use Query method. It also allows to use multiple operators for SortKey such as begins_with
or mathematical ones like >
, =
, >=
and so on.
use aws_sdk_dynamodb::{
model::AttributeValue, Client, Error,
};
#[tokio::main]
async fn main() -> Result<(), Error> {
let shared_config = aws_config::load_from_env().await;
let client = Client::new(&shared_config);
let req = client
.query()
.table_name("my-other-table")
.key_condition_expression(
"id = :hashKey and createdAt > :rangeKey",
)
.expression_attribute_values(
":hashKey",
AttributeValue::S("123".to_string()),
)
.expression_attribute_values(
":rangeKey",
AttributeValue::N(20150101.to_string()),
)
.send()
.await?;
dbg!(req.items);
Ok(())
}
Keep in mind that Query can return up to 1MB of data and you can also use FilterExpressions here to narrow the results on non-key attributes.
Query an Index
DynamoDB allows querying not only on the main table index, but also on LSIs (Local Secondary Indexes) and GSIs (Global Secondary Indexes).
To do that in SDK, you need to change two things:
- Specify the index you want to query using
index_name
parameter - Provide correct
KeyConditionExpression
with corresponding ExpressionAttributeValues
and ExpressionAttributeNames
So, to give you an example. Imagine your table is having a Global Secondary Index called GSI1
with attributes gsi1pk
and gsi1sk
. If you'd like to query that index in Rust, it will look like this:
use aws_sdk_dynamodb::{
model::AttributeValue, Client, Error,
};
#[tokio::main]
async fn main() -> Result<(), Error> {
let shared_config = aws_config::load_from_env().await;
let client = Client::new(&shared_config);
let req = client
.query()
.table_name("my-other-table")
.index_name("GSI1")
.key_condition_expression(
"gsi1pk = :gsi1pk and gsi1sk > :gsi1sk",
)
.expression_attribute_values(
":hashKey",
AttributeValue::S("123".to_string()),
)
.expression_attribute_values(
":rangeKey",
AttributeValue::N(20150101.to_string()),
)
.send()
.await?;
dbg!(req.items);
Ok(())
}
Simple Transaction
DynamoDB also support transactions - they allow to run multiple write operations atomically meaning that either all of operations are executed succesfully or none of them. It is especially useful when dealing with applications where data integrity is essential, e.g. in e-commerce - adding an item to a cart and decrementing count of items still available to buy.
Such flow should:
Should happen atomically - these two operations should be treated as one, we don't want to have a single moment in time where there's a discrepancy in items count
Should succeed only if count of items available to buy is greated than zero
Described flow can be modelled in Rust like this:
use aws_sdk_dynamodb::{
model::{
AttributeValue, Put, TransactWriteItem, Update,
},
Client, Error,
};
#[tokio::main]
async fn main() -> Result<(), Error> {
let shared_config = aws_config::load_from_env().await;
let client = Client::new(&shared_config);
let req = client
.transact_write_items()
.transact_items(
TransactWriteItem::builder()
.put(
Put::builder()
.item(
"id",
AttributeValue::S(
"1".to_string(),
),
)
.item(
"count",
AttributeValue::N(
1.to_string(),
),
)
.build(),
)
.build(),
)
.transact_items(
TransactWriteItem::builder()
.update(
Update::builder()
.condition_expression(
"#count > :zeroValue",
)
.expression_attribute_names(
"#count", "count",
)
.expression_attribute_values(
"value",
AttributeValue::N(
1.to_string(),
),
)
.expression_attribute_values(
":zeroValue",
AttributeValue::N(
0.to_string(),
),
)
.key(
"id",
AttributeValue::S(
"123".to_string(),
),
)
.table_name("ItemsTable")
.update_expression(
"SET #count = :count - :value",
)
.build(),
)
.build(),
)
.send()
.await?;
dbg!(req);
Ok(())
}
If you want to learn more about transactions, head to our DynamoDB Transactions Guide.
Read Transaction
Transactions can be also used for reading data atomically. Like in batch_get_item
, you can retrieve the data from multiple tables in a single call:
use aws_sdk_dynamodb::{
model::{AttributeValue, Get, TransactGetItem},
Client, Error,
};
#[tokio::main]
async fn main() -> Result<(), Error> {
let shared_config = aws_config::load_from_env().await;
let client = Client::new(&shared_config);
let req = client
.transact_get_items()
.transact_items(
TransactGetItem::builder()
.get(
Get::builder()
.table_name("my-table")
.key(
"name",
AttributeValue::S(
"bulbasaur".to_string(),
),
)
.build(),
)
.build(),
)
.transact_items(
TransactGetItem::builder()
.get(
Get::builder()
.table_name("my-table")
.key(
"name",
AttributeValue::S(
"charmander".to_string(),
),
)
.build(),
)
.build(),
)
.send()
.await?;
dbg!(req.responses);
Ok(())
}
If you want to learn more about transactions, head to our DynamoDB Transactions Guide.
Query with Sorting
Unfortunately, DynamoDB offers only one way of sorting the results on the database side - using the sort key. If your table does not have one, your sorting capabilities are limited to sorting items in application code after fetching the results. However, if you need to sort DynamoDB results on sort key descending or ascending, you can use following syntax:
use aws_sdk_dynamodb::{
model::AttributeValue, Client, Error,
};
#[tokio::main]
async fn main() -> Result<(), Error> {
let shared_config = aws_config::load_from_env().await;
let client = Client::new(&shared_config);
let req = client
.query()
.table_name("my-table")
.index_name("Index")
.key_condition_expression(
"id = :hashKey and createdAt > :rangeKey",
)
.expression_attribute_values(
":hashKey",
AttributeValue::S("123".to_string()),
)
.expression_attribute_values(
":rangeKey",
AttributeValue::N(20150101.to_string()),
)
.send()
.await?;
dbg!(req);
Ok(())
}
Query (and Scan) DynamoDB Pagination
Both Query and Scan operations return results with up to 1MB of items. If you need to fetch more records, you need to invoke a second call to fetch the next page of results. If LastEvaluatedKey
is present in response object, this table has more items like requested and another call with ExclusiveStartKey
should be sent to fetch more of them:
use aws_sdk_dynamodb::{
model::AttributeValue, Client, Error,
};
#[tokio::main]
async fn main() -> Result<(), Error> {
let shared_config = aws_config::load_from_env().await;
let client = Client::new(&shared_config);
let mut results = vec![];
let mut exclusive_start_key = None;
loop {
let req = client
.query()
.table_name("my-table")
.limit(10)
.set_exclusive_start_key(exclusive_start_key)
.key_condition_expression(
"id = :hashKey and createdAt > :rangeKey",
)
.expression_attribute_values(
":hashKey",
AttributeValue::S("123".to_string()),
)
.expression_attribute_values(
":rangeKey",
AttributeValue::N(20150101.to_string()),
)
.send()
.await?;
if let Some(items) = req.items {
results.extend(items);
match req.last_evaluated_key {
Some(last_evaluated_key) => {
exclusive_start_key =
Some(last_evaluated_key.clone());
}
None => {
break;
}
}
} else {
break;
}
}
dbg!(results);
Ok(())
}
Learn more about Pagination in DynamoDB.
Update Item
DynamoDB update operation in Node.js consists of two main parts:
- Part which item to update (Key), similar to get
- Part what in the selected item should be updated (
UpdateExpression
and ExpressionAttributeValues
)
use aws_sdk_dynamodb::{
model::AttributeValue, Client, Error,
};
#[tokio::main]
async fn main() -> Result<(), Error> {
let shared_config = aws_config::load_from_env().await;
let client = Client::new(&shared_config);
let request = client
.update_item()
.table_name("my-table")
.key(
"name",
AttributeValue::S("joe".to_string()),
)
.update_expression("set firstName = :firstName")
.expression_attribute_values(
":firstName",
AttributeValue::S("John McNewname".to_string()),
);
request.send().await?;
Ok(())
}
Conditionally Update Item
Sometimes we want to update our record only if some condition is met, e.g. item is not soft-deleted (does not have deletedAt
attribute set). To do that, use ConditionExpression
which has similar syntax to the FilterExpression
:
use aws_sdk_dynamodb::{
model::AttributeValue, Client, Error,
};
#[tokio::main]
async fn main() -> Result<(), Error> {
let shared_config = aws_config::load_from_env().await;
let client = Client::new(&shared_config);
let request = client
.update_item()
.table_name("my-table")
.key(
"name",
AttributeValue::S("joe".to_string()),
)
.update_expression("set firstName = :firstName")
.expression_attribute_values(
":firstName",
AttributeValue::S("John McNewname".to_string()),
)
.expression_attribute_values( ":company",
AttributeValue::S("Apple".to_string())
).condition_expression("attribute_not_exists(deletedAt) and company = :company");
request.send().await?;
Ok(())
}
In this example the name attribute of the record with partition key name = joe
in table my-table
will be only updated if this item does not have attribute deletedAt
and its attribute company has value Apple
.
Increment Item Attribute
Incrementing a Number value in DynamoDB item can be achieved in two ways:
- Get item, update the value in the application code and send a put request back to DDB overwriting item
- Using update operation
While it might be tempting to use first method because Update syntax is unfriendly, I strongly recommend using second one because of the fact it's much faster (requires only one request) and atomic:
use aws_sdk_dynamodb::{
model::AttributeValue, Client, Error,
};
#[tokio::main]
async fn main() -> Result<(), Error> {
let shared_config = aws_config::load_from_env().await;
let client = Client::new(&shared_config);
let request = client
.update_item()
.table_name("my-table")
.key(
"name",
AttributeValue::S("bulbasaur".to_string()),
)
.update_expression("set score = :score + :value")
.expression_attribute_values( ":value",
AttributeValue::N("1".to_string())
).condition_expression("attribute_not_exists(deletedAt) and company = :company");
request.send().await?;
Ok(())
}
Delete Item
Removing single item from table is very similar to Get Item operation. The parameters of the call are actually exactly the same, the only difference is that we call delete instead of get:
use aws_sdk_dynamodb::{
model::AttributeValue, Client, Error,
};
#[tokio::main]
async fn main() -> Result<(), Error> {
let shared_config = aws_config::load_from_env().await;
let client = Client::new(&shared_config);
client
.delete_item()
.table_name("my-table")
.key(
"pk",
AttributeValue::S("bulbasaur".to_string()),
)
.send()
.await?;
Ok(())
}
Uplevel Your Rust Skills
This article was brought to you by Chris Biscardi. Chris is an author of Rust Adventure, one of the best Rust courses you can get on the market right now.
In Rust Adventure, you will experience the versatility of the Rust language through building games, powerful CLI apps, and serverless functions. Whether your goal is to jumpstart the next stretch of your development career or just to tinker, Rust Adventure is what you need.
I highly recommend it! - Rafal from Dynobase