diff --git a/README.md b/README.md
index 71a8adc..6a114e7 100644
--- a/README.md
+++ b/README.md
@@ -269,25 +269,26 @@ let doesntExistCondition = compile <@ fun t -> NOT_EXISTS t.Value @>
let existsCondition = compile <@ fun t -> EXISTS t.Value @>
let key = TableKey.Combined(hashKey, rangeKey)
-let transaction = Transaction()
+let transaction = table.CreateTransaction()
transaction.Check(table, key, doesntExistCondition)
transaction.Put(table, item2, None)
transaction.Put(table, item3, Some existsCondition)
-transaction.Delete (table ,table.Template.ExtractKey item5, None)
+transaction.Delete(table, table.Template.ExtractKey item5, None)
+
do! transaction.TransactWriteItems()
```
-Failed preconditions (or `TransactWrite.Check`s) are signalled as per the underlying API: via a `TransactionCanceledException`.
-Use `TransactWriteItemsRequest.TransactionCanceledConditionalCheckFailed` to trap such conditions:
+Failed preconditions (or `Check`s) are signalled as per the underlying API: via a `TransactionCanceledException`.
+Use `Transaction.TransactionCanceledConditionalCheckFailed` to trap such conditions:
```fsharp
try do! transaction.TransactWriteItems()
return Some result
-with TransactWriteItemsRequest.TransactionCanceledConditionalCheckFailed -> return None
+with Transaction.TransactionCanceledConditionalCheckFailed -> return None
```
-See [`TransactWriteItems tests`](./tests/FSharp.AWS.DynamoDB.Tests/SimpleTableOperationTests.fs#130) for more details and examples.
+See [`TransactWriteItems tests`](./tests/FSharp.AWS.DynamoDB.Tests/SimpleTableOperationTests.fs#156) for more details and examples.
It generally costs [double or more the Write Capacity Units charges compared to using precondition expressions](https://zaccharles.medium.com/calculating-a-dynamodb-items-size-and-consumed-capacity-d1728942eb7c)
on individual operations.
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 829a09a..38d26c7 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,3 +1,6 @@
+### 0.12.2-beta
+* (breaking) Revised multi-table transaction API (thanks @bartelink)
+
### 0.12.1-beta
* Added support for `defaultArg` in update expressions on the same attribute, allowing SET if_not_exists semantics (eg { record with OptionalValue = Some (defaultArg record.OptionalValue "Default") })
* Allow empty strings in non-key attributes (thanks @purkhusid)
diff --git a/src/FSharp.AWS.DynamoDB/TableContext.fs b/src/FSharp.AWS.DynamoDB/TableContext.fs
index c35a315..c442607 100644
--- a/src/FSharp.AWS.DynamoDB/TableContext.fs
+++ b/src/FSharp.AWS.DynamoDB/TableContext.fs
@@ -1318,22 +1318,17 @@ type TableContext<'TRecord>
else
t.VerifyTableAsync()
- member t.Transaction() =
- match metricsCollector with
- | Some metricsCollector -> Transaction(metricsCollector = metricsCollector)
- | None -> Transaction()
+ /// Creates a new `Transaction`, using the DynamoDB client and metricsCollector configured for this `TableContext`
+ member _.CreateTransaction() =
+ Transaction(client, ?metricsCollector = metricsCollector)
///
/// Represents a transactional set of operations to be applied atomically to a arbitrary number of DynamoDB tables.
///
+/// DynamoDB client instance
/// Function to receive request metrics.
-and Transaction(?metricsCollector: (RequestMetrics -> unit)) =
+and Transaction(client: IAmazonDynamoDB, ?metricsCollector: (RequestMetrics -> unit)) =
let transactionItems = ResizeArray()
- let mutable (dynamoDbClient: IAmazonDynamoDB) = null
-
- let setClient client =
- if dynamoDbClient = null then
- dynamoDbClient <- client
let reportMetrics collector (tableName: string) (operation: Operation) (consumedCapacity: ConsumedCapacity list) (itemCount: int) =
collector
@@ -1353,20 +1348,18 @@ and Transaction(?metricsCollector: (RequestMetrics -> unit)) =
/// Table context to operate on.
/// Item to be put.
/// Optional precondition expression.
- member this.Put<'TRecord>
+ member _.Put<'TRecord>
(
tableContext: TableContext<'TRecord>,
item: 'TRecord,
?precondition: ConditionExpression<'TRecord>
- ) : Transaction =
- setClient tableContext.Client
+ ) =
let req = Put(TableName = tableContext.TableName, Item = tableContext.Template.ToAttributeValues item)
precondition
|> Option.iter (fun cond ->
let writer = AttributeWriter(req.ExpressionAttributeNames, req.ExpressionAttributeValues)
req.ConditionExpression <- cond.Conditional.Write writer)
transactionItems.Add(TransactWriteItem(Put = req))
- this
///
/// Adds a ConditionCheck operation to the transaction.
@@ -1374,14 +1367,11 @@ and Transaction(?metricsCollector: (RequestMetrics -> unit)) =
/// Table context to operate on.
/// Key of item to check.
/// Condition to check.
- member this.Check(tableContext: TableContext<'TRecord>, key: TableKey, condition: ConditionExpression<'TRecord>) : Transaction =
- setClient tableContext.Client
-
+ member _.Check(tableContext: TableContext<'TRecord>, key: TableKey, condition: ConditionExpression<'TRecord>) =
let req = ConditionCheck(TableName = tableContext.TableName, Key = tableContext.Template.ToAttributeValues key)
let writer = AttributeWriter(req.ExpressionAttributeNames, req.ExpressionAttributeValues)
req.ConditionExpression <- condition.Conditional.Write writer
transactionItems.Add(TransactWriteItem(ConditionCheck = req))
- this
///
/// Adds an Update operation to the transaction.
@@ -1390,22 +1380,19 @@ and Transaction(?metricsCollector: (RequestMetrics -> unit)) =
/// Key of item to update.
/// Update expression.
/// Optional precondition expression.
- member this.Update
+ member _.Update
(
tableContext: TableContext<'TRecord>,
key: TableKey,
updater: UpdateExpression<'TRecord>,
?precondition: ConditionExpression<'TRecord>
- ) : Transaction =
- setClient tableContext.Client
-
+ ) =
let req = Update(TableName = tableContext.TableName, Key = tableContext.Template.ToAttributeValues key)
let writer = AttributeWriter(req.ExpressionAttributeNames, req.ExpressionAttributeValues)
req.UpdateExpression <- updater.UpdateOps.Write(writer)
precondition |> Option.iter (fun cond -> req.ConditionExpression <- cond.Conditional.Write writer)
transactionItems.Add(TransactWriteItem(Update = req))
- this
///
/// Adds a Delete operation to the transaction.
@@ -1413,21 +1400,18 @@ and Transaction(?metricsCollector: (RequestMetrics -> unit)) =
/// Table context to operate on.
/// Key of item to delete.
/// Optional precondition expression.
- member this.Delete
+ member _.Delete
(
tableContext: TableContext<'TRecord>,
key: TableKey,
- precondition: option>
- ) : Transaction =
- setClient tableContext.Client
-
+ ?precondition: ConditionExpression<'TRecord>
+ ) =
let req = Delete(TableName = tableContext.TableName, Key = tableContext.Template.ToAttributeValues key)
precondition
|> Option.iter (fun cond ->
let writer = AttributeWriter(req.ExpressionAttributeNames, req.ExpressionAttributeValues)
req.ConditionExpression <- cond.Conditional.Write writer)
transactionItems.Add(TransactWriteItem(Delete = req))
- this
///
/// Atomically applies a set of 1-100 operations to the table.
@@ -1436,13 +1420,13 @@ and Transaction(?metricsCollector: (RequestMetrics -> unit)) =
///
/// The ClientRequestToken to supply as an idempotency key (10 minute window).
member _.TransactWriteItems(?clientRequestToken) : Async = async {
- if (Seq.length transactionItems) = 0 || (Seq.length transactionItems) > 100 then
+ if transactionItems.Count = 0 || transactionItems.Count > 100 then
raise
<| System.ArgumentOutOfRangeException(nameof transactionItems, "must be between 1 and 100 items.")
- let req = TransactWriteItemsRequest(ReturnConsumedCapacity = returnConsumedCapacity, TransactItems = (ResizeArray transactionItems))
+ let req = TransactWriteItemsRequest(ReturnConsumedCapacity = returnConsumedCapacity, TransactItems = transactionItems)
clientRequestToken |> Option.iter (fun x -> req.ClientRequestToken <- x)
let! ct = Async.CancellationToken
- let! response = dynamoDbClient.TransactWriteItemsAsync(req, ct) |> Async.AwaitTaskCorrect
+ let! response = client.TransactWriteItemsAsync(req, ct) |> Async.AwaitTaskCorrect
maybeReport
|> Option.iter (fun r ->
response.ConsumedCapacity
@@ -2150,8 +2134,8 @@ module Scripting =
let spec = Throughput.Provisioned provisionedThroughput
t.UpdateTableIfRequiredAsync(spec) |> Async.Ignore |> Async.RunSynchronously
-/// Helpers for working with TransactWriteItemsRequest
-module TransactWriteItemsRequest =
+/// Helpers for working with Transaction
+module Transaction =
/// Exception filter to identify whether a TransactWriteItems call has failed due to
/// one or more of the supplied precondition checks failing.
let (|TransactionCanceledConditionalCheckFailed|_|): exn -> unit option =
diff --git a/tests/FSharp.AWS.DynamoDB.Tests/MetricsCollectorTests.fs b/tests/FSharp.AWS.DynamoDB.Tests/MetricsCollectorTests.fs
index dddcb83..a204cac 100644
--- a/tests/FSharp.AWS.DynamoDB.Tests/MetricsCollectorTests.fs
+++ b/tests/FSharp.AWS.DynamoDB.Tests/MetricsCollectorTests.fs
@@ -106,10 +106,9 @@ type Tests(fixture: TableFixture) =
collector.Clear()
let item = mkItem (guid ()) (guid ()) 0
- do!
- Transaction(collector.Collect)
- .Put(sut, item, compile <@ fun t -> NOT_EXISTS t.RangeKey @>)
- .TransactWriteItems()
+ let transaction = Transaction(sut.Client, collector.Collect)
+ transaction.Put(sut, item, compile <@ fun t -> NOT_EXISTS t.RangeKey @>)
+ do! transaction.TransactWriteItems()
test
<@
@@ -131,14 +130,14 @@ type Tests(fixture: TableFixture) =
let sut = rawTable.WithMetricsCollector(collector.Collect)
let item = mkItem (guid ()) (guid ()) 0
+ let transaction = rawTable.CreateTransaction()
+ transaction.Put(sut, item, compile <@ fun t -> EXISTS t.RangeKey @>)
let mutable failed = false
try
do!
// The check will fail, which triggers a throw from the underlying AWS SDK; there's no way to extract the consumption info in that case
- Transaction()
- .Put(sut, item, compile <@ fun t -> EXISTS t.RangeKey @>)
- .TransactWriteItems()
- with TransactWriteItemsRequest.TransactionCanceledConditionalCheckFailed ->
+ transaction.TransactWriteItems()
+ with Transaction.TransactionCanceledConditionalCheckFailed ->
failed <- true
true =! failed
[] =! collector.Metrics
diff --git a/tests/FSharp.AWS.DynamoDB.Tests/SimpleTableOperationTests.fs b/tests/FSharp.AWS.DynamoDB.Tests/SimpleTableOperationTests.fs
index a1db252..561acb4 100644
--- a/tests/FSharp.AWS.DynamoDB.Tests/SimpleTableOperationTests.fs
+++ b/tests/FSharp.AWS.DynamoDB.Tests/SimpleTableOperationTests.fs
@@ -182,10 +182,9 @@ type ``TransactWriteItems tests``(table1: TableFixture, table2: TableFixture) =
[]
let ``Minimal happy path`` () = async {
let item = mkItem ()
- do!
- Transaction()
- .Put(table1, item, doesntExistConditionTable1)
- .TransactWriteItems()
+ let transaction = table1.CreateTransaction()
+ transaction.Put(table1, item, doesntExistConditionTable1)
+ do! transaction.TransactWriteItems()
let! itemFound = table1.ContainsKeyAsync(table1.Template.ExtractKey item)
true =! itemFound
@@ -196,11 +195,10 @@ type ``TransactWriteItems tests``(table1: TableFixture, table2: TableFixture) =
let item = mkItem ()
let compatibleItem = mkCompatibleItem ()
- do!
- Transaction()
- .Put(table1, item, doesntExistConditionTable1)
- .Put(table2, compatibleItem, doesntExistConditionTable2)
- .TransactWriteItems()
+ let transaction = table1.CreateTransaction()
+ transaction.Put(table1, item, doesntExistConditionTable1)
+ transaction.Put(table2, compatibleItem, doesntExistConditionTable2)
+ do! transaction.TransactWriteItems()
let! itemFound = table1.ContainsKeyAsync(table1.Template.ExtractKey item)
true =! itemFound
@@ -213,13 +211,12 @@ type ``TransactWriteItems tests``(table1: TableFixture, table2: TableFixture) =
let ``Minimal Canceled path`` () = async {
let item = mkItem ()
+ let transaction = table1.CreateTransaction()
+ transaction.Put(table1, item, existsConditionTable1)
let mutable failed = false
try
- do!
- Transaction()
- .Put(table1, item, existsConditionTable1)
- .TransactWriteItems()
- with TransactWriteItemsRequest.TransactionCanceledConditionalCheckFailed ->
+ do! transaction.TransactWriteItems()
+ with Transaction.TransactionCanceledConditionalCheckFailed ->
failed <- true
true =! failed
@@ -233,18 +230,17 @@ type ``TransactWriteItems tests``(table1: TableFixture, table2: TableFixture) =
let item, item2 = mkItem (), mkItem ()
let! key = table1.PutItemAsync item
- let transaction =
- if shouldFail then
- Transaction().Check(table1, key, doesntExistConditionTable1)
- else
- Transaction()
- .Check(table1, key, existsConditionTable1)
- .Put(table1, item2)
+ let transaction = table1.CreateTransaction()
+ if shouldFail then
+ transaction.Check(table1, key, doesntExistConditionTable1)
+ else
+ transaction.Check(table1, key, existsConditionTable1)
+ transaction.Put(table1, item2)
let mutable failed = false
try
do! transaction.TransactWriteItems()
- with TransactWriteItemsRequest.TransactionCanceledConditionalCheckFailed ->
+ with Transaction.TransactionCanceledConditionalCheckFailed ->
failed <- true
failed =! shouldFail
@@ -257,14 +253,14 @@ type ``TransactWriteItems tests``(table1: TableFixture, table2: TableFixture) =
let ``All paths`` shouldFail = async {
let item, item2, item3, item4, item5, item6, item7 = mkItem (), mkItem (), mkItem (), mkItem (), mkItem (), mkItem (), mkItem ()
let! key = table1.PutItemAsync item
- let transaction = Transaction()
+ let transaction = table1.CreateTransaction()
let requests =
[ transaction.Update(table1, key, compileUpdateTable1 <@ fun t -> { t with Value = 42 } @>, existsConditionTable1)
transaction.Put(table1, item2)
transaction.Put(table1, item3, doesntExistConditionTable1)
- transaction.Delete(table1, table1.Template.ExtractKey item4, Some doesntExistConditionTable1)
- transaction.Delete(table1, table1.Template.ExtractKey item5, None)
+ transaction.Delete(table1, table1.Template.ExtractKey item4, doesntExistConditionTable1)
+ transaction.Delete(table1, table1.Template.ExtractKey item5)
transaction.Check(
table1,
table1.Template.ExtractKey item6,
@@ -281,7 +277,7 @@ type ``TransactWriteItems tests``(table1: TableFixture, table2: TableFixture) =
let mutable failed = false
try
do! transaction.TransactWriteItems()
- with TransactWriteItemsRequest.TransactionCanceledConditionalCheckFailed ->
+ with Transaction.TransactionCanceledConditionalCheckFailed ->
failed <- true
failed =! shouldFail
@@ -310,13 +306,12 @@ type ``TransactWriteItems tests``(table1: TableFixture, table2: TableFixture) =
[]
let ``Empty request list is rejected with AORE`` () =
- shouldBeRejectedWithArgumentOutOfRangeException (Transaction())
+ shouldBeRejectedWithArgumentOutOfRangeException (Transaction(table1.Client))
|> Async.RunSynchronously
- |> ignore
[]
let ``Over 100 writes are rejected with AORE`` () =
- let Transaction = Transaction()
+ let Transaction = Transaction(table1.Client)
for _x in 1..101 do
Transaction.Put(table1, mkItem ()) |> ignore