Skip to content

Commit

Permalink
new containsSet ConditionExpression method (#620)
Browse files Browse the repository at this point in the history
  • Loading branch information
googley42 authored Feb 17, 2025
1 parent 630ef7e commit befcaeb
Show file tree
Hide file tree
Showing 6 changed files with 92 additions and 21 deletions.
3 changes: 2 additions & 1 deletion docs/reference/projection-expression.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ Once we have a `ProjectionExpression` we can use it as a springboard to create f
<br/><br/> | `beginsWith` | Only applies to string attributes
<br/><br/> | `inSet` | returns true if the attribute is in the supplied set
<br/><br/> | `in(a, b, c ...)` | returns true if the attribute matches one of the supplied values
<br/><br/> | `contains` | returns true if the attribute contains the supplied value - applies to a String or a Set
<br/><br/> | `contains` | returns true if the attribute contains the supplied value - applies to a String, Set, List
<br/><br/> | `containsSet` | returns true if the attribute contains the supplied set - applies to a String, Set, List. Creates a composite condition expression consisting of multiple `contains` expressions joined by an `and`
`UpdateExpression`| `+` | combines update actions eg `Person.name.set(42) + Person.age.set(42)`
<br/><br/> | `set` | Set an attribute `Person.name.set("John")`
<br/><br/> | `setIfNotExists` | Set attribute if it does not exists `Person.name.setIfNotExists("John")`
Expand Down
44 changes: 39 additions & 5 deletions dynamodb/src/it/scala/zio/dynamodb/TypeSafeApiCrudSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ object TypeSafeApiCrudSpec extends DynamoDBLocalSpec {
assertZIO(exit)(fails(isConditionalCheckFailedException))
}
},
test("set's a single field with an update plus a condition expression that addressSet contains an element") {
test("set's a single field with an update with a condition expression that addressSet contains an element") {
withSingleIdKeyTable { tableName =>
val person = PersonWithCollections("1", "Smith", addressSet = Set("address1"))
val expected = PersonWithCollections("1", "Brown", addressSet = Set("address1"))
Expand All @@ -273,7 +273,37 @@ object TypeSafeApiCrudSpec extends DynamoDBLocalSpec {
} yield assertTrue(p == expected)
}
},
test("set's a single field with an update plus a condition expression that addressSet has size 1") {
test("set's a single field with an with with a condition expression that addressSet contains a set") {
withSingleIdKeyTable { tableName =>
val person = PersonWithCollections("1", "Smith", addressSet = Set("address1", "address2", "address3"))
val expected = PersonWithCollections("1", "Brown", addressSet = Set("address1", "address2", "address3"))
val nonEmptySet = zio.prelude.NonEmptySet("address1", "address2", "address3")
for {
_ <- put(tableName, person).execute
_ <-
update(tableName)(PersonWithCollections.id.partitionKey === "1")(PersonWithCollections.surname.set("Brown"))
.where(PersonWithCollections.addressSet.containsSet(nonEmptySet.head, nonEmptySet.tail.toSet))
.execute
p <- get(tableName)(PersonWithCollections.id.partitionKey === "1").execute.absolve
} yield assertTrue(p == expected)
}
},
test("set's a single field with an update with a condition expression that surname contains a set") {
withSingleIdKeyTable { tableName =>
val person = PersonWithCollections("1", "Smith", addressSet = Set("address1", "address2", "address3"))
val expected = PersonWithCollections("1", "Brown", addressSet = Set("address1", "address2", "address3"))
val nonEmptySet = zio.prelude.NonEmptySet("S", "mi", "h")
for {
_ <- put(tableName, person).execute
_ <-
update(tableName)(PersonWithCollections.id.partitionKey === "1")(PersonWithCollections.surname.set("Brown"))
.where(PersonWithCollections.surname.containsSet(nonEmptySet.head, nonEmptySet.tail.toSet))
.execute
p <- get(tableName)(PersonWithCollections.id.partitionKey === "1").execute.absolve
} yield assertTrue(p == expected)
}
},
test("set's a single field with an update with a condition expression that addressSet has size 1") {
withSingleIdKeyTable { tableName =>
val person = PersonWithCollections("1", "Smith", addressSet = Set("address1"))
val expected = PersonWithCollections("1", "Brown", addressSet = Set("address1"))
Expand Down Expand Up @@ -302,7 +332,7 @@ object TypeSafeApiCrudSpec extends DynamoDBLocalSpec {
}
},
test(
"set's a single field with an update plus a condition expression that optional forename contains a substring"
"set's a single field with an update with a condition expression that optional forename contains a substring"
) {
withSingleIdKeyTable { tableName =>
val person = Person("1", "Smith", Some("John"), 21)
Expand Down Expand Up @@ -507,7 +537,7 @@ object TypeSafeApiCrudSpec extends DynamoDBLocalSpec {
}
},
test(
"append adds an Address element to addressList field"
"append adds an Address element to addressList field with a containsSet condition expression on addressList"
) {
withSingleIdKeyTable { tableName =>
val address1 = Address("1", "AAAA")
Expand All @@ -518,7 +548,11 @@ object TypeSafeApiCrudSpec extends DynamoDBLocalSpec {
_ <- put(tableName, person).execute
_ <- update(tableName)(PersonWithCollections.id.partitionKey === "1")(
PersonWithCollections.addressList.append(address2)
).execute
)
.where(
PersonWithCollections.addressList.containsSet(address1, Set.empty)
)
.execute
p <- get(tableName)(PersonWithCollections.id.partitionKey === "1").execute.absolve
} yield assertTrue(p == expected)
}
Expand Down
28 changes: 26 additions & 2 deletions dynamodb/src/main/scala/zio/dynamodb/ProjectionExpression.scala
Original file line number Diff line number Diff line change
Expand Up @@ -240,11 +240,21 @@ trait ProjectionExpressionLowPriorityImplicits0 extends ProjectionExpressionLowP
/**
* Applies to a String or Set
*/
def contains[A](av: A)(implicit ev: Containable[To, A], to: ToAttributeValue[A]): ConditionExpression[From] = {
def contains[A](a: A)(implicit ev: Containable[To, A], to: ToAttributeValue[A]): ConditionExpression[From] = {
val _ = ev
ConditionExpression.Contains(self, to.toAttributeValue(av))
ConditionExpression.Contains(self, to.toAttributeValue(a))
}

/**
* Applies fields of type Set, List, String and creates a composite of `contains` ConditionExpression's
* for each element (head plus tail) that are joined with an `&&` (and)
*/
def containsSet[A](head: A, tail: Set[A])(implicit
ev: Containable[To, A],
to: ToAttributeValue[A]
): ConditionExpression[From] =
tail.foldLeft(contains(head))((acc, a) => acc && contains(a))

/**
* adds this value as a number attribute if it does not exists, else adds the numeric value to the existing attribute
*/
Expand Down Expand Up @@ -436,6 +446,13 @@ trait ProjectionExpressionLowPriorityImplicits1 {
def contains[To2](av: To2)(implicit to: ToAttributeValue[To2]): ConditionExpression[From] =
ConditionExpression.Contains(self, to.toAttributeValue(av))

/**
* Applies fields of type Set, List, String and creates a composite of `contains` ConditionExpression's
* for each element (head plus tail) that are joined with an `&&` (and)
*/
def containsSet[To2](headAv: To2, tail: Set[To2])(implicit to: ToAttributeValue[To2]): ConditionExpression[From] =
tail.foldLeft(contains(headAv))((acc, a) => acc && contains(a))

/**
* adds a number attribute if it does not exists, else adds the numeric value to the existing attribute
*/
Expand Down Expand Up @@ -642,6 +659,13 @@ object ProjectionExpression extends ProjectionExpressionLowPriorityImplicits0 {
def contains[To](av: To)(implicit to: ToAttributeValue[To]): ConditionExpression[From] =
ConditionExpression.Contains(self, to.toAttributeValue(av))

/**
* Applies fields of type Set, List, String and creates a composite of `contains` ConditionExpression's
* for each element (head plus tail) that are joined with an `&&` (and)
*/
def containsSet[To](headAv: To, tail: Set[To])(implicit to: ToAttributeValue[To]): ConditionExpression[From] =
tail.foldLeft(contains(headAv))((acc, a) => acc && contains(a))

/**
* adds a number attribute if it does not exists, else adds the numeric value to the existing attribute
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import zio.dynamodb.ProjectionExpression
import scala.annotation.implicitNotFound

@implicitNotFound(
"DynamoDB does not support [${X}].contains([${A}]). This operator takes a String argument and only applies to Sets, String and Option[String]"
"DynamoDB does not support [${X}].contains([${A}]). This operator only applies to Sets, Lists, String and Option[String]"
)
sealed trait Containable[X, -A]
trait ContainableLowPriorityImplicits0 extends ContainableLowPriorityImplicits1 {
Expand All @@ -14,6 +14,7 @@ trait ContainableLowPriorityImplicits0 extends ContainableLowPriorityImplicits1
}
trait ContainableLowPriorityImplicits1 {
implicit def set[A]: Containable[Set[A], A] = new Containable[Set[A], A] {}
implicit def list[A]: Containable[List[A], A] = new Containable[List[A], A] {}
implicit def string: Containable[String, String] = new Containable[String, String] {}
implicit def optString: Containable[Option[String], String] = new Containable[Option[String], String] {}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,16 @@ object ProjectionExpressionSpec extends ZIOSpecDefault {
test("set contains") {
val ex = $("groups").contains("group1")
assertTrue(ex.toString == s"Contains($groups,String(group1))")
},
test("set containsSet with a single element") {
val ex = $("groups").containsSet("group1", Set.empty)
assertTrue(ex.toString == s"Contains($groups,String(group1))")
},
test("set containsSet with multiple elements") {
val ex = $("groups").containsSet("group1", Set("group2", "group3"))
assertTrue(
ex.toString == "And(And(Contains(groups,String(group1)),Contains(groups,String(group2))),Contains(groups,String(group3)))"
)
}
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,19 @@ object ConditionExpressionExamples {
val avToCond1: ConditionExpression[_] = AttributeValue("2") > $("col1")
val avToCond2: ConditionExpression[_] = AttributeValue("2") < $("col1")

val exists: ConditionExpression[_] = $("col1").exists
val notExists: ConditionExpression[_] = $("col1").notExists
val beginsWith: ConditionExpression[_] = $("col1").beginsWith("1")
val contains: ConditionExpression[_] = $("col1").contains("1")
val sizeOnLhs: ConditionExpression[_] = $("col2").size > 1
val sizeOnLhs2: ConditionExpression[_] = $("col2").size === 1
val isType: ConditionExpression[_] = $("col1").isNumber
val between: ConditionExpression[_] = $("col1").between(1, 2)
val in: ConditionExpression[_] = $("col1").in(1, 2)
val expnAnd: ConditionExpression[_] = beginsWith && sizeOnLhs
val expnOr: ConditionExpression[_] = beginsWith || sizeOnLhs
val expnNot: ConditionExpression[_] = !beginsWith
val exists: ConditionExpression[_] = $("col1").exists
val notExists: ConditionExpression[_] = $("col1").notExists
val beginsWith: ConditionExpression[_] = $("col1").beginsWith("1")
val contains: ConditionExpression[_] = $("col1").contains("1")
val containsSet: ConditionExpression[_] = $("col1").containsSet("1", Set("2", "3"))
val sizeOnLhs: ConditionExpression[_] = $("col2").size > 1
val sizeOnLhs2: ConditionExpression[_] = $("col2").size === 1
val isType: ConditionExpression[_] = $("col1").isNumber
val between: ConditionExpression[_] = $("col1").between(1, 2)
val in: ConditionExpression[_] = $("col1").in(1, 2)
val expnAnd: ConditionExpression[_] = beginsWith && sizeOnLhs
val expnOr: ConditionExpression[_] = beginsWith || sizeOnLhs
val expnNot: ConditionExpression[_] = !beginsWith

val peNeVal: ConditionExpression[_] = $("col1") <> 1
val peLtVal: ConditionExpression[_] = $("col1") < 1
Expand Down

0 comments on commit befcaeb

Please sign in to comment.