Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamic updates #77

Merged
merged 2 commits into from Jan 10, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/main/scala/com/gu/scanamo/Scanamo.scala
Expand Up @@ -229,8 +229,8 @@ object Scanamo {
* List(Right(Forecast(London,Sun)))
* }}}
*/
def update[V: DynamoFormat, U: UpdateExpression](client: AmazonDynamoDB)(tableName: String)(key: UniqueKey[_], expression: U): Either[DynamoReadError, V] =
exec(client)(ScanamoFree.update[V, U](tableName)(key)(expression))
def update[V: DynamoFormat](client: AmazonDynamoDB)(tableName: String)(key: UniqueKey[_], expression: UpdateExpression): Either[DynamoReadError, V] =
exec(client)(ScanamoFree.update[V](tableName)(key)(expression))

/**
* Scans all elements of a table
Expand Down
6 changes: 3 additions & 3 deletions src/main/scala/com/gu/scanamo/ScanamoAsync.scala
Expand Up @@ -49,10 +49,10 @@ object ScanamoAsync {
(implicit ec: ExecutionContext): Future[List[BatchWriteItemResult]] =
exec(client)(ScanamoFree.deleteAll(tableName)(items))

def update[V: DynamoFormat, U: UpdateExpression](client: AmazonDynamoDBAsync)(tableName: String)(
key: UniqueKey[_], expression: U)(implicit ec: ExecutionContext
def update[V: DynamoFormat](client: AmazonDynamoDBAsync)(tableName: String)(
key: UniqueKey[_], expression: UpdateExpression)(implicit ec: ExecutionContext
): Future[Either[DynamoReadError,V]] =
exec(client)(ScanamoFree.update[V, U](tableName)(key)(expression))
exec(client)(ScanamoFree.update[V](tableName)(key)(expression))

def scan[T: DynamoFormat](client: AmazonDynamoDBAsync)(tableName: String)
(implicit ec: ExecutionContext): Future[List[Either[DynamoReadError, T]]] =
Expand Down
6 changes: 3 additions & 3 deletions src/main/scala/com/gu/scanamo/ScanamoFree.scala
Expand Up @@ -100,11 +100,11 @@ object ScanamoFree {
def queryIndexWithLimit[T: DynamoFormat](tableName: String, indexName: String)(query: Query[_], limit: Int): ScanamoOps[List[Either[DynamoReadError, T]]] =
QueryResultStream.stream[T](query(new QueryRequest().withTableName(tableName)).withIndexName(indexName).withLimit(limit))

def update[T, U](tableName: String)(key: UniqueKey[_])(expression: U)(
implicit update: UpdateExpression[U], format: DynamoFormat[T]
def update[T](tableName: String)(key: UniqueKey[_])(update: UpdateExpression)(
implicit format: DynamoFormat[T]
): ScanamoOps[Either[DynamoReadError, T]] =
ScanamoOps.update(ScanamoUpdateRequest(
tableName, key.asAVMap, update.expression(expression), update.attributeNames(expression), update.attributeValues(expression), None)
tableName, key.asAVMap, update.expression, update.attributeNames, update.attributeValues, None)
).map(
r => format.read(new AttributeValue().withM(r.getAttributes))
)
Expand Down
4 changes: 2 additions & 2 deletions src/main/scala/com/gu/scanamo/Table.scala
Expand Up @@ -183,8 +183,8 @@ case class Table[V: DynamoFormat](name: String) {
* Right(Bar(x,11,Set(Second)))
* }}}
*/
def update[U: UpdateExpression](key: UniqueKey[_], expression: U): ScanamoOps[Either[DynamoReadError, V]] =
ScanamoFree.update[V, U](name)(key)(expression)
def update(key: UniqueKey[_], expression: UpdateExpression): ScanamoOps[Either[DynamoReadError, V]] =
ScanamoFree.update[V](name)(key)(expression)

/**
* Query or scan a table, limiting the number of items evaluated by Dynamo
Expand Down
16 changes: 8 additions & 8 deletions src/main/scala/com/gu/scanamo/package.scala
Expand Up @@ -49,21 +49,21 @@ package object scanamo {
def or[Y: ConditionExpression](y: Y) = OrCondition(x, y)
}

def set[V: DynamoFormat](fieldValue: (Symbol, V)) =
def set[V: DynamoFormat](fieldValue: (Symbol, V)): UpdateExpression =
SetExpression(fieldValue._1, fieldValue._2)
def append[V: DynamoFormat](fieldValue: (Symbol, V)) =
def append[V: DynamoFormat](fieldValue: (Symbol, V)): UpdateExpression =
AppendExpression(fieldValue._1, fieldValue._2)
def prepend[V: DynamoFormat](fieldValue: (Symbol, V)) =
def prepend[V: DynamoFormat](fieldValue: (Symbol, V)): UpdateExpression =
PrependExpression(fieldValue._1, fieldValue._2)
def add[V: DynamoFormat](fieldValue: (Symbol, V)) =
def add[V: DynamoFormat](fieldValue: (Symbol, V)): UpdateExpression =
AddExpression(fieldValue._1, fieldValue._2)
def delete[V: DynamoFormat](fieldValue: (Symbol, V)) =
def delete[V: DynamoFormat](fieldValue: (Symbol, V)): UpdateExpression =
DeleteExpression(fieldValue._1, fieldValue._2)
def remove(field: Symbol) =
def remove(field: Symbol): UpdateExpression =
RemoveExpression(field)

implicit class AndUpdateExpression[X: UpdateExpression](x: X) {
def and[Y: UpdateExpression](y: Y) = AndUpdate(x, y)
implicit class AndUpdateExpression(x: UpdateExpression) {
def and(y: UpdateExpression): UpdateExpression = AndUpdate(x, y)
}
}
}
4 changes: 2 additions & 2 deletions src/main/scala/com/gu/scanamo/query/ConditionExpression.scala
Expand Up @@ -23,11 +23,11 @@ case class ConditionalOperation[V, T](tableName: String, t: T)(
condition = Some(state.apply(t)(unconditionalRequest.condition))))
}

def update[U](key: UniqueKey[_], expression: U)(implicit update: UpdateExpression[U]):
def update(key: UniqueKey[_], update: UpdateExpression):
ScanamoOps[Either[ScanamoError, V]] = {

val unconditionalRequest = ScanamoUpdateRequest(
tableName, key.asAVMap, update.expression(expression), update.attributeNames(expression), update.attributeValues(expression), None)
tableName, key.asAVMap, update.expression, update.attributeNames, update.attributeValues, None)
ScanamoOps.conditionalUpdate(unconditionalRequest.copy(
condition = Some(state.apply(t)(unconditionalRequest.condition)))
).map(either => either.leftMap[ScanamoError](ConditionNotMet(_)).flatMap(
Expand Down
199 changes: 83 additions & 116 deletions src/main/scala/com/gu/scanamo/update/UpdateExpression.scala
Expand Up @@ -6,11 +6,17 @@ import com.amazonaws.services.dynamodbv2.model.AttributeValue
import com.gu.scanamo.DynamoFormat
import simulacrum.typeclass

@typeclass trait UpdateExpression[T] {
def expression(t: T): String = typeExpressions(t).map{ case (t, e) => s"${t.op} $e" }.mkString(" ")
def typeExpressions(t: T): Map[UpdateType, String]
def attributeNames(t: T): Map[String, String]
def attributeValues(t: T): Map[String, AttributeValue]
sealed trait UpdateExpression extends Product with Serializable {
def expression: String = typeExpressions.map{ case (t, e) => s"${t.op} $e" }.mkString(" ")
def typeExpressions: Map[UpdateType, String]
def attributeNames: Map[String, String]
def attributeValues: Map[String, AttributeValue]
}

object UpdateExpression {
implicit object Semigroup extends Semigroup[UpdateExpression] {
override def combine(x: UpdateExpression, y: UpdateExpression): UpdateExpression = AndUpdate(x, y)
}
}

sealed trait UpdateType { val op: String }
Expand All @@ -19,66 +25,44 @@ case object ADD extends UpdateType { override val op = "ADD" }
case object DELETE extends UpdateType { override val op = "DELETE" }
case object REMOVE extends UpdateType { override val op = "REMOVE" }

case class SetExpression[V: DynamoFormat](field: Symbol, value: V)

object SetExpression {
implicit def setUpdateExpression[V](implicit format: DynamoFormat[V]) =
new UpdateExpression[SetExpression[V]] {
override def typeExpressions(t: SetExpression[V]): Map[UpdateType, String] =
Map(SET -> "#update = :update")
override def attributeNames(t: SetExpression[V]): Map[String, String] =
Map("#update" -> t.field.name)
override def attributeValues(t: SetExpression[V]): Map[String, AttributeValue] =
Map(":update" -> format.write(t.value))
}
case class SetExpression[V: DynamoFormat](field: Symbol, value: V) extends UpdateExpression {
val format = DynamoFormat[V]

override def typeExpressions: Map[UpdateType, String] =
Map(SET -> "#update = :update")
override def attributeNames: Map[String, String] =
Map("#update" -> field.name)
override def attributeValues: Map[String, AttributeValue] =
Map(":update" -> format.write(value))
}

case class AppendExpression[V: DynamoFormat](field: Symbol, value: V)

object AppendExpression {
implicit def appendUpdateExpression[V](implicit format: DynamoFormat[V]) =
new UpdateExpression[AppendExpression[V]] {
override def typeExpressions(t: AppendExpression[V]): Map[UpdateType, String] =
Map(SET -> "#update = list_append(if_not_exists(#update, :emptyList), :update)")
override def attributeNames(t: AppendExpression[V]): Map[String, String] =
Map("#update" -> t.field.name)
override def attributeValues(t: AppendExpression[V]): Map[String, AttributeValue] =
Map(
":update" -> DynamoFormat.listFormat[V].write(List(t.value)),
":emptyList" -> new AttributeValue().withL()
)
}
case class AppendExpression[V: DynamoFormat](field: Symbol, value: V) extends UpdateExpression {
override def typeExpressions: Map[UpdateType, String] =
Map(SET -> "#update = list_append(if_not_exists(#update, :emptyList), :update)")
override def attributeNames: Map[String, String] =
Map("#update" -> field.name)
override def attributeValues: Map[String, AttributeValue] =
Map(":update" -> DynamoFormat.listFormat[V].write(List(value)),
":emptyList" -> new AttributeValue().withL())
}

case class PrependExpression[V: DynamoFormat](field: Symbol, value: V)

object PrependExpression {
implicit def appendUpdateExpression[V](implicit format: DynamoFormat[V]) =
new UpdateExpression[PrependExpression[V]] {
override def typeExpressions(t: PrependExpression[V]): Map[UpdateType, String] =
Map(SET -> "#update = list_append(:update, if_not_exists(#update, :emptyList))")
override def attributeNames(t: PrependExpression[V]): Map[String, String] =
Map("#update" -> t.field.name)
override def attributeValues(t: PrependExpression[V]): Map[String, AttributeValue] =
Map(
":update" -> DynamoFormat.listFormat[V].write(List(t.value)),
":emptyList" -> new AttributeValue().withL()
)
}
case class PrependExpression[V: DynamoFormat](field: Symbol, value: V) extends UpdateExpression {
override def typeExpressions: Map[UpdateType, String] =
Map(SET -> "#update = list_append(:update, if_not_exists(#update, :emptyList))")
override def attributeNames: Map[String, String] =
Map("#update" -> field.name)
override def attributeValues: Map[String, AttributeValue] =
Map(":update" -> DynamoFormat.listFormat[V].write(List(value)),
":emptyList" -> new AttributeValue().withL())
}

case class AddExpression[V: DynamoFormat](field: Symbol, value: V)

object AddExpression {
implicit def addUpdateExpression[V](implicit format: DynamoFormat[V]) =
new UpdateExpression[AddExpression[V]] {
override def typeExpressions(t: AddExpression[V]): Map[UpdateType, String] =
Map(ADD -> "#update :update")
override def attributeNames(t: AddExpression[V]): Map[String, String] =
Map("#update" -> t.field.name)
override def attributeValues(t: AddExpression[V]): Map[String, AttributeValue] =
Map(":update" -> format.write(t.value))
}
case class AddExpression[V: DynamoFormat](field: Symbol, value: V) extends UpdateExpression {
override def typeExpressions: Map[UpdateType, String] =
Map(ADD -> "#update :update")
override def attributeNames: Map[String, String] =
Map("#update" -> field.name)
override def attributeValues: Map[String, AttributeValue] =
Map(":update" -> DynamoFormat[V].write(value))
}

/*
Expand All @@ -87,74 +71,57 @@ Note the difference between DELETE and REMOVE:
- REMOVE is used to remove an attribute from an item
*/

case class DeleteExpression[V: DynamoFormat](field: Symbol, value: V)

object DeleteExpression {
implicit def deleteUpdateExpression[V](implicit format: DynamoFormat[V]) =
new UpdateExpression[DeleteExpression[V]] {
override def typeExpressions(t: DeleteExpression[V]): Map[UpdateType, String] =
Map(DELETE -> "#update :update")
override def attributeNames(t: DeleteExpression[V]): Map[String, String] =
Map("#update" -> t.field.name)
override def attributeValues(t: DeleteExpression[V]): Map[String, AttributeValue] =
Map(":update" -> format.write(t.value))
}
case class DeleteExpression[V: DynamoFormat](field: Symbol, value: V) extends UpdateExpression {
override def typeExpressions: Map[UpdateType, String] =
Map(DELETE -> "#update :update")
override def attributeNames: Map[String, String] =
Map("#update" -> field.name)
override def attributeValues: Map[String, AttributeValue] =
Map(":update" -> DynamoFormat[V].write(value))
}

case class RemoveExpression(field: Symbol)

object RemoveExpression {
implicit val removeUpdateExpression =
new UpdateExpression[RemoveExpression] {
override def typeExpressions(t: RemoveExpression): Map[UpdateType, String] =
Map(REMOVE -> "#update")
override def attributeNames(t: RemoveExpression): Map[String, String] =
Map("#update" -> t.field.name)
override def attributeValues(t: RemoveExpression): Map[String, AttributeValue] =
Map()
}
case class RemoveExpression(field: Symbol) extends UpdateExpression {
override def typeExpressions: Map[UpdateType, String] =
Map(REMOVE -> "#update")
override def attributeNames: Map[String, String] =
Map("#update" -> field.name)
override def attributeValues: Map[String, AttributeValue] =
Map()
}

case class AndUpdate[L: UpdateExpression, R: UpdateExpression](l: L, r: R)

object AndUpdate {
implicit def andUpdateExpression[L, R](implicit lUpdate: UpdateExpression[L], rUpdate: UpdateExpression[R]) =
new UpdateExpression[AndUpdate[L, R]] {
case class AndUpdate(l: UpdateExpression, r: UpdateExpression) extends UpdateExpression {

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

val expressionMonoid = new MapMonoid[UpdateType, String]()(new Semigroup[String] {
override def combine(x: String, y: String): String = s"$x, $y"
})
val expressionMonoid = new MapMonoid[UpdateType, String]()(new Semigroup[String] {
override def combine(x: String, y: String): String = s"$x, $y"
})

override def typeExpressions(t: AndUpdate[L, R]): Map[UpdateType, String] = {
def prefixKeysIn(string: String, keys: Iterable[String], prefix: String, magicChar: Char) =
keys.foldLeft(string)((result, key) => result.replaceAllLiterally(key, newKey(key, prefix, magicChar)))
override def typeExpressions: Map[UpdateType, String] = {
def prefixKeysIn(string: String, keys: Iterable[String], prefix: String, magicChar: Char) =
keys.foldLeft(string)((result, key) => result.replaceAllLiterally(key, newKey(key, prefix, magicChar)))

val lPrefixedNamePlaceholders = lUpdate.typeExpressions(t.l).mapValues(exp =>
prefixKeysIn(exp, lUpdate.attributeNames(t.l).keys, "l_", '#'))
val lPrefixedNamePlaceholders = l.typeExpressions.mapValues(exp =>
prefixKeysIn(exp, l.attributeNames.keys, "l_", '#'))

val rPrefixedNamePlaceholders = rUpdate.typeExpressions(t.r).mapValues(exp =>
prefixKeysIn(exp, rUpdate.attributeNames(t.r).keys, "r_", '#'))
val rPrefixedNamePlaceholders = r.typeExpressions.mapValues(exp =>
prefixKeysIn(exp, r.attributeNames.keys, "r_", '#'))

val lPrefixedValuePlaceholders = lPrefixedNamePlaceholders.mapValues(exp =>
prefixKeysIn(exp, lUpdate.attributeValues(t.l).keys, "l_", ':'))
val rPrefixedValuePlaceholders = rPrefixedNamePlaceholders.mapValues(exp =>
prefixKeysIn(exp, rUpdate.attributeValues(t.r).keys, "r_", ':'))
val lPrefixedValuePlaceholders = lPrefixedNamePlaceholders.mapValues(exp =>
prefixKeysIn(exp, l.attributeValues.keys, "l_", ':'))
val rPrefixedValuePlaceholders = rPrefixedNamePlaceholders.mapValues(exp =>
prefixKeysIn(exp, r.attributeValues.keys, "r_", ':'))

expressionMonoid.combine(lPrefixedValuePlaceholders, rPrefixedValuePlaceholders)
}
expressionMonoid.combine(lPrefixedValuePlaceholders, rPrefixedValuePlaceholders)
}

override def attributeNames(t: AndUpdate[L, R]): Map[String, String] = {
prefixKeys(lUpdate.attributeNames(t.l), "l_", '#') ++ prefixKeys(rUpdate.attributeNames(t.r), "r_", '#')
}
override def attributeNames: Map[String, String] =
prefixKeys(l.attributeNames, "l_", '#') ++ prefixKeys(r.attributeNames, "r_", '#')

override def attributeValues(t: AndUpdate[L, R]): Map[String, AttributeValue] = {
prefixKeys(lUpdate.attributeValues(t.l), "l_", ':') ++ prefixKeys(rUpdate.attributeValues(t.r), "r_", ':')
}
}
override def attributeValues: Map[String, AttributeValue] =
prefixKeys(l.attributeValues, "l_", ':') ++ prefixKeys(r.attributeValues, "r_", ':')
}
43 changes: 42 additions & 1 deletion src/main/tut/updating.md
Expand Up @@ -15,7 +15,7 @@ import com.gu.scanamo.syntax._

val client = LocalDynamoDB.client()
import com.amazonaws.services.dynamodbv2.model.ScalarAttributeType._
val teamTableResult = LocalDynamoDB.createTable(client)("teams")('name -> S)
LocalDynamoDB.createTable(client)("teams")('name -> S)
case class Team(name: String, goals: Int, scorers: List[String], mascot: Option[String])
val teamTable = Table[Team]("teams")
val operations = for {
Expand All @@ -26,4 +26,45 @@ val operations = for {
```
```tut:book
Scanamo.exec(client)(operations)
```

You can also conditionally update different elements of the document:

```tut:silent
import cats.data.NonEmptyList
import cats.implicits._
import com.gu.scanamo.ops.ScanamoOps
import com.gu.scanamo.error.DynamoReadError

LocalDynamoDB.createTable(client)("favourites")('name -> S)
case class Favourites(name: String, colour: String, number: Long)
val favouritesTable = Table[Favourites]("favourites")

Scanamo.exec(client)(favouritesTable.put(Favourites("Alice", "Blue", 42L)))

case class FavouriteUpdate(name: String, colour: Option[String], number: Option[Long])
def updateFavourite(fu: FavouriteUpdate): Option[ScanamoOps[Either[DynamoReadError, Favourites]]] = {
val updates = List(
fu.colour.map(c => set('colour -> c)),
fu.number.map(n => set('number -> n))
)
NonEmptyList.fromList(updates.flatten).map(ups =>
favouritesTable.update('name -> fu.name, ups.reduce)
)
}
```
```tut:book
val updates = List(
FavouriteUpdate("Alice", Some("Aquamarine"), Some(93L)),
FavouriteUpdate("Alice", Some("Red"), None),
FavouriteUpdate("Alice", None, None)
)

Scanamo.exec(client)(
for {
_ <- updates.flatMap(updateFavourite).sequence
result <- favouritesTable.get('name -> "Alice")
} yield result
)

```