Skip to content

Commit

Permalink
Refactor RequestCondition, introduce AttributeNamesAndValues
Browse files Browse the repository at this point in the history
* **`RequestCondition` dynamo values become non-optional**: instead they can just be `DynamoObject.empty`
* **New class `AttributeNamesAndValues`**: we often combine the different sources of attribute names/values in a request (eg, 'update' and 'condition') and there's no point in repeatedly writing code where names are combined, and then _values_ are combined - we always want to aggregate both at the same time, now supported with `AttributeNamesAndValues`
* **New class `UpdateAndCondition`**: combines `UpdateExpression` & `Option[Condition]` - models the payload of both normal `UpdateItem` & transactional `Update` requests.
* Using the [lambda syntax for Single Abstract Method (SAM) types](https://www.scala-lang.org/news/2.12.0/#lambda-syntax-for-sam-types) in the implementation of many typeclass instances of `ConditionExpression` and `QueryableKeyCondition` (eg `QueryableKeyCondition[KeyEquals[V]]`) makes the implementations much shorter.

The 'values' on `RequestCondition` have been optional since `RequestCondition` was
introduced with scanamo#31 in May 2016 - back then they
were `attributeValues: Option[Map[String, AttributeValue]]`), and became
`dynamoValues: Option[DynamoObject]` with scanamo#400
in May 2019.

The values are probably 'optional' because there are certain DynamoDB condition expression
functions (like `attribute_exists()`) that _don't_ take a value, so it was _reasonable_ for
implementations like `ConditionExpression[AttributeExists]` to create instances of
`RequestCondition` where `dynamoValues` was `None`.

However, if we make `dynamoValues` non-optional, we can just supply `DynamoObject.empty`
rather than `None`- and this allows us to simplify so much logic around combining the
attribute values that come from different sources (eg as conditions are combined).

* Introduce AttributeNamesAndValues and use UpdateExpression as a useful grouping
  • Loading branch information
rtyley authored Aug 16, 2024
1 parent 93ab1e7 commit 5ed3280
Show file tree
Hide file tree
Showing 11 changed files with 263 additions and 269 deletions.
11 changes: 7 additions & 4 deletions scanamo/src/main/scala/org/scanamo/DynamoObject.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,8 @@

package org.scanamo

import cats.Parallel
import cats.kernel.Monoid
import cats.syntax.apply.*
import cats.syntax.semigroup.*
import cats.implicits.*
import cats.{ Monoid, Parallel }
import software.amazon.awssdk.services.dynamodb.model.AttributeValue

import java.util.{ HashMap, Map as JMap }
Expand Down Expand Up @@ -375,4 +373,9 @@ object DynamoObject {
m.putAll(ys)
m
}

/** Technically, this isn't a Monoid - it's not strictly associative (combining `Map`s can arbitrarily annihilate
* different keys), making it just a 'unital magma'... practically though, the difference shouldn't be a problem.
*/
implicit val m: Monoid[DynamoObject] = Monoid.instance(empty, _ <> _)
}
20 changes: 6 additions & 14 deletions scanamo/src/main/scala/org/scanamo/ScanamoFree.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import org.scanamo.ops.ScanamoOps.Results.*
import org.scanamo.ops.{ ScanamoOps, ScanamoOpsT }
import org.scanamo.query.*
import org.scanamo.request.*
import org.scanamo.update.UpdateExpression
import org.scanamo.update.{ UpdateAndCondition, UpdateExpression }
import software.amazon.awssdk.services.dynamodb.model.*

import java.util.{ List as JList, Map as JMap }
Expand Down Expand Up @@ -88,7 +88,9 @@ object ScanamoFree {
case r @ TransactionalWriteAction.Put(table, _) =>
acc.copy(putItems = acc.putItems :+ TransactPutItem(table, r.asDynamoValue, None))
case TransactionalWriteAction.Update(table, key, updateExpr) =>
acc.copy(updateItems = acc.updateItems :+ TransactUpdateItem(table, key.toDynamoObject, updateExpr, None))
acc.copy(updateItems =
acc.updateItems :+ TransactUpdateItem(table, key.toDynamoObject, UpdateAndCondition(updateExpr))
)
case TransactionalWriteAction.Delete(table, key) =>
acc.copy(deleteItems = acc.deleteItems :+ TransactDeleteItem(table, key.toDynamoObject, None))
case r @ TransactionalWriteAction.ConditionCheck(table, key, _) =>
Expand Down Expand Up @@ -125,7 +127,7 @@ object ScanamoFree {
tableAndItems: List[(String, (UniqueKey[_], UpdateExpression))]
): ScanamoOps[Transact[TransactWriteItemsResponse]] = {
val items = tableAndItems.map { case (tableName, (key, updateExpression)) =>
TransactUpdateItem(tableName, key.toDynamoObject, updateExpression, None)
TransactUpdateItem(tableName, key.toDynamoObject, UpdateAndCondition(updateExpression))
}
ScanamoOps
.transactWriteAll(ScanamoTransactWriteRequest(Seq.empty, items, Seq.empty, Seq.empty))
Expand Down Expand Up @@ -250,17 +252,7 @@ object ScanamoFree {
tableName: String
)(key: UniqueKey[_])(update: UpdateExpression): ScanamoOps[Either[DynamoReadError, T]] =
ScanamoOps
.update(
ScanamoUpdateRequest(
tableName,
key.toDynamoObject,
update.expression,
update.attributeNames,
DynamoObject(update.dynamoValues),
update.addEmptyList,
None
)
)
.update(ScanamoUpdateRequest(tableName, key.toDynamoObject, UpdateAndCondition(update)))
.map(r => read[T](DynamoObject(r.attributes)))

def read[T](m: DynamoObject)(implicit f: DynamoFormat[T]): Either[DynamoReadError, T] =
Expand Down
68 changes: 26 additions & 42 deletions scanamo/src/main/scala/org/scanamo/ops/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ package org.scanamo

import cats.data.NonEmptyList
import cats.free.{ Free, FreeT }
import cats.syntax.apply.*
import cats.implicits.*
import org.scanamo.request.*
import software.amazon.awssdk.services.dynamodb.model.*

Expand All @@ -43,13 +43,10 @@ package object ops {
queryRefinement(_.options.exclusiveStartKey)((r, k) => r.exclusiveStartKey(k.toJavaMap)),
queryRefinement(_.options.filter) { (r, f) =>
val requestCondition = f.apply.runEmptyA.value
requestCondition.dynamoValues
.filter(_.nonEmpty)
.flatMap(_.toExpressionAttributeValues)
.foldLeft(
r.filterExpression(requestCondition.expression)
.expressionAttributeNames(requestCondition.attributeNames.asJava)
)(_ expressionAttributeValues _)
val attributes = requestCondition.attributes
val builder =
r.filterExpression(requestCondition.expression).expressionAttributeNames(attributes.names.asJava)
attributes.values.toExpressionAttributeValues.foldLeft(builder)(_ expressionAttributeValues _)
}
)
.reduceLeft(_.compose(_))(
Expand Down Expand Up @@ -83,24 +80,16 @@ package object ops {
)

requestCondition.fold {
val requestWithCondition = requestBuilder.expressionAttributeNames(queryCondition.attributeNames.asJava)
queryCondition.dynamoValues
.filter(_.nonEmpty)
.flatMap(_.toExpressionAttributeValues)
val requestWithCondition = requestBuilder.expressionAttributeNames(queryCondition.attributes.names.asJava)
queryCondition.attributes.values.toExpressionAttributeValues
.foldLeft(requestWithCondition)(_ expressionAttributeValues _)
} { condition =>
val attributes = queryCondition.attributes |+| condition.attributes

val requestWithCondition = requestBuilder
.filterExpression(condition.expression)
.expressionAttributeNames((queryCondition.attributeNames ++ condition.attributeNames).asJava)
val attributeValues =
(
queryCondition.dynamoValues orElse Some(DynamoObject.empty),
condition.dynamoValues orElse Some(DynamoObject.empty)
).mapN(_ <> _)

attributeValues
.flatMap(_.toExpressionAttributeValues)
.foldLeft(requestWithCondition)(_ expressionAttributeValues _)
.expressionAttributeNames(attributes.names.asJava)
attributes.values.toExpressionAttributeValues.foldLeft(requestWithCondition)(_ expressionAttributeValues _)
}.build
}

Expand All @@ -114,10 +103,9 @@ package object ops {
.fold(request) { condition =>
val requestWithCondition = request
.conditionExpression(condition.expression)
.expressionAttributeNames(condition.attributeNames.asJava)
.expressionAttributeNames(condition.attributes.names.asJava)

condition.dynamoValues
.flatMap(_.toExpressionAttributeValues)
condition.attributes.values.toExpressionAttributeValues
.foldLeft(requestWithCondition)(_ expressionAttributeValues _)
}
.build
Expand All @@ -133,33 +121,30 @@ package object ops {
.fold(request) { condition =>
val requestWithCondition = request
.conditionExpression(condition.expression)
.expressionAttributeNames(condition.attributeNames.asJava)
.expressionAttributeNames(condition.attributes.names.asJava)

condition.dynamoValues
.flatMap(_.toExpressionAttributeValues)
condition.attributes.values.toExpressionAttributeValues
.foldLeft(requestWithCondition)(_ expressionAttributeValues _)
}
.build
}

def update(req: ScanamoUpdateRequest): UpdateItemRequest = {
val attributeNames: Map[String, String] = req.condition.map(_.attributeNames).foldLeft(req.attributeNames)(_ ++ _)
val attributeValues: DynamoObject = req.condition.flatMap(_.dynamoValues).foldLeft(req.dynamoValues)(_ <> _)
val request = UpdateItemRequest.builder
.tableName(req.tableName)
.key(req.key.toJavaMap)
.updateExpression(req.updateExpression)
.updateExpression(req.updateAndCondition.update.expression)
.returnValues(ReturnValue.ALL_NEW)
.expressionAttributeNames(attributeNames.asJava)
.expressionAttributeNames(req.updateAndCondition.attributes.names.asJava)

val requestWithCondition =
req.condition.fold(request)(condition => request.conditionExpression(condition.expression))
req.updateAndCondition.condition.fold(request)(condition => request.conditionExpression(condition.expression))

attributeValues.toExpressionAttributeValues
req.updateAndCondition.attributes.values.toExpressionAttributeValues
.fold(requestWithCondition) { avs =>
if (req.addEmptyList)
if (req.updateAndCondition.update.addEmptyList)
avs.put(":emptyList", DynamoValue.EmptyList)
requestWithCondition expressionAttributeValues avs
requestWithCondition.expressionAttributeValues(avs)
}
.build
}
Expand All @@ -179,11 +164,11 @@ package object ops {
val updateItems = req.updateItems.map { item =>
val update = software.amazon.awssdk.services.dynamodb.model.Update.builder
.tableName(item.tableName)
.updateExpression(item.updateExpression.expression)
.expressionAttributeNames(item.updateExpression.attributeNames.asJava)
.updateExpression(item.updateAndCondition.update.expression)
.expressionAttributeNames(item.updateAndCondition.update.attributes.names.asJava)
.key(item.key.toJavaMap)

val updateWithAvs = DynamoObject(item.updateExpression.dynamoValues).toExpressionAttributeValues
val updateWithAvs = item.updateAndCondition.update.attributes.values.toExpressionAttributeValues
.fold(update)(avs => update.expressionAttributeValues(avs))
.build

Expand All @@ -205,10 +190,9 @@ package object ops {
.key(item.key.toJavaMap)
.tableName(item.tableName)
.conditionExpression(item.condition.expression)
.expressionAttributeNames(item.condition.attributeNames.asJava)
.expressionAttributeNames(item.condition.attributes.names.asJava)

val checkWithAvs = item.condition.dynamoValues
.flatMap(_.toExpressionAttributeValues)
val checkWithAvs = item.condition.attributes.values.toExpressionAttributeValues
.foldLeft(check)(_ expressionAttributeValues _)
.build

Expand Down
Loading

0 comments on commit 5ed3280

Please sign in to comment.