Skip to content

Commit

Permalink
Add list-appending support to transactional Updates
Browse files Browse the repository at this point in the history
This change makes list-appending support for `Update`s embedded in
*transactions* consistent with the support in normal `UpdateItemRequest`s.

The injection of the necessary `:emptyList` attribute has moved from
`update(req: ScanamoUpdateRequest): UpdateItemRequest` into the code of
`UpdateExpression`, where it can be used for both transactional and
non-transactional code.

# Context

DynamoDB update operations let you append to existing list attributes with the
`list_append()` function:

https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.UpdateExpressions.html#Expressions.UpdateExpressions.SET.UpdatingListElements

However, if you're attempting to execute an update-append on an attribute that
is not yet populated (eg the item does not even _exist_ yet), DynamoDB will
return this error:

```
The provided expression refers to an attribute that does not exist in the item
```

To get friendlier behaviour, in January 2017 Scanamo PR scanamo#80
(commit 75814f9) provided an `if_not_exists()`
check on the attribute, which effectively provides an empty list to append to
if the attribute does not already exist. This code now lives inside the
`LeafUpdateExpression` trait.

Initially the test for this functionality was a doctest, but was moved to a regular test
(named "Appending or prepending creates the list if it does not yet exist:") in TableTest.scala
with scanamo#770.
  • Loading branch information
rtyley committed Aug 19, 2024
1 parent 5ed3280 commit dcc4b4b
Show file tree
Hide file tree
Showing 3 changed files with 12 additions and 12 deletions.
6 changes: 1 addition & 5 deletions scanamo/src/main/scala/org/scanamo/ops/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -141,11 +141,7 @@ package object ops {
req.updateAndCondition.condition.fold(request)(condition => request.conditionExpression(condition.expression))

req.updateAndCondition.attributes.values.toExpressionAttributeValues
.fold(requestWithCondition) { avs =>
if (req.updateAndCondition.update.addEmptyList)
avs.put(":emptyList", DynamoValue.EmptyList)
requestWithCondition.expressionAttributeValues(avs)
}
.fold(requestWithCondition)(requestWithCondition.expressionAttributeValues)
.build
}

Expand Down
17 changes: 11 additions & 6 deletions scanamo/src/main/scala/org/scanamo/update/UpdateExpression.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ package org.scanamo.update
import cats.data.NonEmptyVector
import org.scanamo.query.*
import org.scanamo.request.AttributeNamesAndValues
import org.scanamo.update.UpdateExpression.prefixKeys
import org.scanamo.{ DynamoFormat, DynamoObject, DynamoValue }
import org.scanamo.update.LeafUpdateExpression.EmptyListName
import org.scanamo.update.UpdateExpression.{ prefixKeys, someEmptyList }
import org.scanamo.{ DynamoArray, DynamoFormat, DynamoObject, DynamoValue }
import software.amazon.awssdk.services.dynamodb.model.AttributeValue

import scala.collection.immutable.HashMap
Expand Down Expand Up @@ -56,7 +57,7 @@ sealed trait UpdateExpression extends Product with Serializable { self =>

final def attributes: AttributeNamesAndValues = AttributeNamesAndValues(
names = unprefixedAttributeNames.map { case (k, v) => (s"#$k", v) },
values = DynamoObject(unprefixedDynamoValues)
values = DynamoObject(unprefixedDynamoValues ++ (if (addEmptyList) someEmptyList else None))
)

final val addEmptyList: Boolean = self match {
Expand All @@ -83,6 +84,8 @@ final private[scanamo] case class SimpleUpdate(leaf: LeafUpdateExpression) exten
final private[scanamo] case class AndUpdate(l: UpdateExpression, r: UpdateExpression) extends UpdateExpression

object UpdateExpression {
private val someEmptyList: Some[(String, DynamoValue)] = Some(EmptyListName -> DynamoArray.empty.toDynamoValue)

def prefixKeys[T](map: Map[String, T], prefix: String) =
map.map { case (k, v) => (s"$prefix$k", v) }

Expand Down Expand Up @@ -165,9 +168,9 @@ sealed private[update] trait LeafUpdateExpression { self =>
case LeafDeleteExpression(namePlaceholder, _, valuePlaceholder, _) => s"#$namePlaceholder :$valuePlaceholder"
case LeafSetExpression(namePlaceholder, _, valuePlaceholder, _) => s"#$namePlaceholder = :$valuePlaceholder"
case LeafAppendExpression(namePlaceholder, _, valuePlaceholder, _) =>
s"#$namePlaceholder = list_append(if_not_exists(#$namePlaceholder, :emptyList), :$valuePlaceholder)"
s"#$namePlaceholder = list_append(if_not_exists(#$namePlaceholder, :$EmptyListName), :$valuePlaceholder)"
case LeafPrependExpression(namePlaceholder, _, valuePlaceholder, _) =>
s"#$namePlaceholder = list_append(:$valuePlaceholder, if_not_exists(#$namePlaceholder, :emptyList))"
s"#$namePlaceholder = list_append(:$valuePlaceholder, if_not_exists(#$namePlaceholder, :$EmptyListName))"
case LeafRemoveExpression(namePlaceholder, _) => s"#$namePlaceholder"
case LeafAttributeExpression(prefix, from, to) => s"#${to.placeholder(prefix)} = #${from.placeholder(prefix)}"
case LeafSetIfNotExistsExpression(namePlaceholder, _, valuePlaceholder, _) =>
Expand Down Expand Up @@ -198,7 +201,9 @@ sealed private[update] trait LeafUpdateExpression { self =>
def attributeNames: Map[String, String]
}

private[update] object LeafUpdateExpression
private[update] object LeafUpdateExpression {
val EmptyListName = "emptyList"
}

final private[update] case class LeafSetExpression(
namePlaceholder: String,
Expand Down
1 change: 0 additions & 1 deletion scanamo/src/test/scala/org/scanamo/TableTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,6 @@ class TableTest extends AnyFunSpec with Matchers with NonImplicitAssertions {
}

it("Appending or prepending creates the list if it does not yet exist:") {

LocalDynamoDB.withRandomTable(client)("name" -> S) { t =>
val characters = Table[Character](t)
val operations = for {
Expand Down

0 comments on commit dcc4b4b

Please sign in to comment.