# TxMap
[](https://crates.io/crates/txmap)
[](https://docs.rs/txmap)
[](LICENSE)
A concurrent transactional hash map for Rust with fine-grained locking and internal mutability.
`TxMap` partitions stored key-value pairs across multiple shards, each protected by its own `parking_lot::Mutex` and backed by its own `hashbrown::HashMap`. Read and write operations acquire locks only on the shards they need, maximizing concurrency. Transactional operations group multiple operations into atomic units, and support parameterized closures.
## Features
- **Concurrent access** Fine-grained shard-level locking; operations lock only the shards they touch
- **Transactions** Atomic, composable batches of modifications
- **Optional parameterized transactions** Optionally define a parameter type to pass into transaction closures
- **Guards/conditions** Declarative preconditions that must hold before a transaction runs
- **Flexible operations** Modify, map, insert, remove, swap, move, retain, and more
- **Builder API** Chain operations to build transactions with a fluent interface
- **No `unsafe`** 100% safe Rust
## License
Licensed under the [MIT License](LICENSE).
## Usage
Add `txmap` to your `Cargo.toml`:
```toml
[dependencies]
txmap = "0.1.0"
```
### Creating a `TxMap`
```rust
use txmap::prelude::*;
// Choose a shard count, a power of two between 8 to 128 inclusive
let map: TxMap<String, u64> = TxMap::new(ShardCount::_8);
```
Larger shard counts reduce lock contention at the cost of slightly more memory. Choose the smallest count that gives adequate concurrency for your workload.
### Key type requirements
The map key type `K` must implement `Hash` and `Eq`. Some functions also require `Clone`.
The value type `V` has no trait bounds by default. Operations that create default values (e.g., `insert_default`) require `V: Default`.
### Transactions
Transactions group multiple operations into an atomic unit. They are built using a fluent builder API. Use `.into_transaction()` to produce a reusable transaction, then `.execute()` to run it.
#### Simple transaction (no result)
```rust
use txmap::prelude::*;
let db: TxMap<String, u64> = TxMap::new(ShardCount::_8);
db.insert("alice".to_string(), 100);
// Transfer 50 from alice to bob in one transaction
db.transaction()
.modify("alice".to_string(), |_name, balance| {
*balance -= 50;
})
.modify("bob".to_string(), |_name, balance| {
*balance += 50;
})
.into_transaction()
.execute();
assert_eq!(db.get_with(&"alice".to_string(), |v| *v), Some(50));
assert_eq!(db.get_with(&"bob".to_string(), |v| *v), Some(50));
```
#### Transaction with guards (preconditions)
Guards are checked before any mutations take place. If any guard fails, the transaction is **not executed**, all locks are dropped, no mutations occur, and `TxResult::RequirementNotMet` is returned.
```rust
use txmap::prelude::*;
let db: TxMap<String, u64> = TxMap::new(ShardCount::_8);
db.insert("alice".to_string(), 100);
db.insert("bob".to_string(), 0);
let tx = db
.transaction()
.require(
"Alice has sufficient funds",
["alice".to_string()],
|[alice_balance]| alice_balance.is_some_and(|b| *b >= 250),
)
.modify("alice".to_string(), |_name, balance| {
*balance -= 250;
})
.modify("bob".to_string(), |_name, balance| {
*balance += 250;
})
.into_transaction();
assert!(matches!(tx.execute(), TxResult::RequirementNotMet(0, _)));
```
#### Transaction returning a value
Use one of `.get()`, `.get_copied()`, `.get_cloned()`, `get_all()`, `.get_all_copied()` or `.get_all_cloned()` before `.into_transaction()` to return a value or values at the end of the transaction.
```rust
use txmap::prelude::*;
let db: TxMap<String, u64> = TxMap::new(ShardCount::_8);
db.insert("alice".to_string(), 100);
let new_balance = db
.transaction()
.modify("alice".to_string(), |_name, balance| {
*balance += 50;
})
.get("alice".to_string(), |_name, balance| *balance)
.into_transaction()
.execute();
assert_eq!(new_balance, TxResult::Completed(Some(150)));
```
#### Transaction returning multiple values
```rust
use txmap::prelude::*;
let db: TxMap<String, u64> = TxMap::new(ShardCount::_8);
let balances = db
.transaction()
.insert_with_if_absent("alice".to_string(), |_k| 100)
.insert_with_if_absent("bob".to_string(), |_k| 200)
.get_all(
vec!["alice".to_string(), "bob".to_string()],
|_name, balance| *balance,
)
.into_transaction()
.execute();
assert_eq!(balances, TxResult::Completed(vec![Some(100), Some(200)]));
```
### Parameterized transactions
Parameterized transactions let you pass a parameter struct to all closures. This is useful for reusable transaction logic.
```rust
use txmap::prelude::*;
#[derive(Default)]
struct Transfer {
amount: u64,
}
let db: TxMap<String, u64> = TxMap::new(ShardCount::_8);
db.insert("alice".to_string(), 200);
let transfer_alice_to_bob_tx = db
.transaction()
.with_param::<Transfer>()
.require(
"Alice has sufficient funds",
["alice".to_string()],
|[balance], params| balance.is_some_and(|b| *b >= params.amount),
)
.modify("alice".to_string(), |_name, balance, params| {
*balance -= params.amount;
})
.modify("bob".to_string(), |_name, balance, params| {
*balance += params.amount;
})
.get_all(["alice".to_string(), "bob".to_string()], |_name, balance| *balance)
.into_transaction();
// Execute with different parameters
let result1 = transfer_alice_to_bob_tx.execute(&Transfer { amount: 50 });
assert_eq!(result1, TxResult::Completed(vec![Some(150), Some(50)]));
let result2 = transfer_alice_to_bob_tx.execute(&Transfer { amount: 30 });
assert_eq!(result2, TxResult::Completed(vec![Some(120), Some(80)]));
```
### Finite state machine example
Use `update` to implement state transitions that return `Some(new_state)` to update or `None` to delete.
```rust
use txmap::prelude::*;
#[derive(Debug, Clone, PartialEq)]
enum OrderState { Pending, Shipped, Delivered }
let orders: TxMap<String, OrderState> = TxMap::new(ShardCount::_8);
orders.insert("order-1".into(), OrderState::Pending);
// Transition order-1 from Pending to Shipped
let result = orders
.transaction()
.update("order-1".into(), |_id, state| {
state.and_then(|s| match s {
OrderState::Pending => Some(OrderState::Shipped),
_ => None, // reject transition, deleting the entry
})
})
.get_cloned("order-1".into())
.into_transaction()
.execute();
assert_eq!(result, TxResult::Completed(Some(OrderState::Shipped)));
```
### Swap and move operations
Atomically swap values between two keys, or move a value from one key to another.
```rust
use txmap::prelude::*;
let map: TxMap<String, u64> = TxMap::new(ShardCount::_8);
map.insert("a".to_string(), 10);
map.insert("b".to_string(), 20);
// Swap values
let result = map
.transaction()
.swap_value("a".to_string(), "b".to_string())
.get_all_copied(["a".to_string(), "b".to_string()])
.into_transaction()
.execute();
assert_eq!(result, TxResult::Completed(vec![Some(20), Some(10)]));
// Move value (from "a" to "b", leaving "a" empty)
let result = map
.transaction()
.move_value("a".to_string(), "b".to_string())
.get_all_copied(["a".to_string(), "b".to_string()])
.into_transaction()
.execute();
assert_eq!(result, TxResult::Completed(vec![None, Some(20)]));
```
### Batch operations
Conditionally remove or retain entries in bulk.
```rust
use txmap::prelude::*;
let map: TxMap<String, u64> = TxMap::new(ShardCount::_8);
map.insert("alice".into(), 1);
map.insert("bob".into(), 2);
map.insert("chuck".into(), 3);
// Remove entries with values > 1
map.transaction()
.remove_if(|_k, v| *v > 1)
.into_transaction()
.execute();
assert_eq!(map.len(), 1); // only alice remains
// Retain only specific keys
map.insert("bob".into(), 2);
map.transaction()
.retain_only(["alice".into(), "bob".into()])
.into_transaction()
.execute();
assert_eq!(map.len(), 2);
```
### Operation reference
| `insert_default` | Insert `V::default()` for the key. | `K: Clone` `V: Default` |
| `insert_default_if_absent` | Insert `V::default()` for the key, only if the key is absent. | `K: Clone` `V: Default` |
| `insert_with` | Insert a value generated from the key. | `K: Clone` |
| `insert_with_if_absent` | Insert a value generated from the key, only if the key is absent. | `K: Clone` |
| `modify` | Mutate an existing value in-place. Does nothing if key absent. | |
| `modify_peek` | Like `modify` while peeking at other values. | `K: Clone` |
| `update` | Update a single entry. Return `Some(v)` to insert/replace, `None` to delete. | `K: Clone` |
| `update_peek` | Like `update` while peeking at other values. | `K: Clone` |
| | | |
| `move_value` | Remove a value from one key and insert it with another key. | `K: Clone` |
| `swap_value` | Swap the values of two keys. | `K: Clone` |
| | | |
| `remove` | Remove the given keys. | |
| `remove_where` | Remove the given keys which also satisfy a condition. | |
| `retain_only` | Retain only the given keys. | |
| `retain_where` | Retain only the given keys which also satisfy a condition. | |
| | | |
| `clear` | Remove all entries. | |
| `remove_if` | Remove any entries which satisfy a condition. | |
| `retain` | Retain only the entries which satisfy a condition. | |
### Finisher methods
Up to one of these is called before `.into_transaction()` to define what the transaction should return.
| *(none - default)* | Execute with no return value. | `TxResult<()>` | |
| `get_copied(key)` | Copy a single value. | `TxResult<Option<V>>` | `V: Copy` |
| `get_all_copied(keys)` | Copy an array of values. | `TxResult<Vec<Option<V>>>` | `V: Copy` |
| `get_cloned(key)` | Clone a single value. | `TxResult<Option<V>>` | `V: Clone` |
| `get_all_cloned(keys)` | Clone an array of values. | `TxResult<Vec<Option<V>>>` | `V: Clone` |
| `get(key, \|k, v[, params]\| { ... })` | Read a single value and apply a transformation to it. | `TxResult<Option<R>>` | |
| `get_all(keys, \|k, v[, params]\| { ... })` | Read multiple values and apply a transformation to them. | `TxResult<Vec<Option<R>>>` | |
To create the final transaction call `into_transaction()`. This will produce a re-useable transaction that can be executed as many times as you want within the lifetime of its `TxMap`.
### `TxResult`
All transactions return `TxResult<T>`:
```rust
pub enum TxResult<T> {
Completed(T),
RequirementNotMet(usize, String),
}
```
- `Completed(result)` The transaction was executed successfully.
- `RequirementNotMet(index, name)` A guard condition failed; the transaction was aborted. The `index` indicates which guard failed, and `name` is the user-supplied description.
## Operation appendix
| `insert_default(key)` |
| `insert_default_if_absent(key)` |
| `insert_with(key, \|k[, params]\| { new_value } )` |
| `insert_with_if_absent(key, \|k[, params]\| { new_value } )` |
| `modify(key, \|k, mut v[, params]\|)` |
| `modify_peek(key, peek_keys, \|k, mut v, pks[, params]\|)` |
| `update(key, \|k, v_opt[, params]\| { new_value_opt })` |
| `update_peek(key, peek_keys, \|k, v_opt[, params]\| { new_value_opt })` |
| |
| `move_value(from, to)` |
| `swap_value(a, b)` |
| |
| `remove(keys)` |
| `remove_where(keys, \|k, v[, params]\| { remove })` |
| `retain_only(keys)` |
| `retain_where(keys, \|k, v[, params]\| { remove })` |
| |
| `clear()` |
| `remove_if(\|k, v[, params]\| { remove })` |
| `retain(\|k, v[, params]\| { remove })` |