diff --git a/README.md b/README.md index 3010851..b395eb8 100644 --- a/README.md +++ b/README.md @@ -340,13 +340,13 @@ II Array update operators case class Student(id: Int, courses: List[Int]) val q = update[Student](_.addToSet(_.courses, 55)) -// q us {"$addToSet": {"courses": 55}} +// q is {"$addToSet": {"courses": 55}} ``` In order to append multiple values to array `addToSetAll` should be used: ```scala val q = update[Student](_.addToSetAll(_.courses, List(42, 44, 53))) -// q us {"$addToSet": {"courses": {$each: [42, 44, 53] }}} +// q is {"$addToSet": {"courses": {$each: [42, 44, 53] }}} ``` 2. $pop @@ -355,10 +355,26 @@ val q = update[Student](_.addToSetAll(_.courses, List(42, 44, 53))) case class Student(id: Int, courses: List[Int]) val q = update[Student](_.popHead(_.courses)) // removes the first element -// q us {"$pop": {"courses": -1}} +// q is {"$pop": {"courses": -1}} val q1 = update[Student](_.popLast(_.courses)) // removes the last element -// q1 us {"$pop": {"courses": 1}} +// q1 is {"$pop": {"courses": 1}} +``` + +3. $pull +```scala +case class Student(id: Int, courses: List[Int]) + +val q = update[Student](_.pull(_.courses, _ >= 42)) +// q is {"$pull": {"courses": {"$gte": 42}}} +``` +4. $pullAll + +```scala +case class Student(id: Int, courses: List[Int]) + +val q = update[Student](_.pullAll(_.courses, List(5, 10, 42))) +// q is {"$pullAll": {"courses": [5, 10, 42]}} ``` #### Projection diff --git a/oolong-core/src/main/scala/oolong/AstParser.scala b/oolong-core/src/main/scala/oolong/AstParser.scala index b2ea22f..fe3dab2 100644 --- a/oolong-core/src/main/scala/oolong/AstParser.scala +++ b/oolong-core/src/main/scala/oolong/AstParser.scala @@ -325,7 +325,13 @@ private[oolong] class DefaultAstParser(using quotes: Quotes) extends AstParser { case '{type t; ($updater: Updater[Doc]).pull[`t`,`t`]($selectProp, $input)} => val prop = parsePropSelector(selectProp) parseUpdater(updater, FieldUpdateExpr.Pull(UExpr.Prop(prop), parseQExpr[`t`](input)) :: acc) - + + case '{type t; ($updater: Updater[Doc]).pullAll[`t`,`t`]($selectProp, $input: Iterable[`t`])} => + val prop = parsePropSelector(selectProp) + val value = getValueOrIterable(input) + parseUpdater(updater, FieldUpdateExpr.PullAll(UExpr.Prop(prop), value) :: acc) + + case '{ $updater: Updater[Doc] } => updater match { case AsTerm(Ident(name)) if name == paramName => diff --git a/oolong-core/src/main/scala/oolong/UExpr.scala b/oolong-core/src/main/scala/oolong/UExpr.scala index 01373b7..1cdb9ec 100644 --- a/oolong-core/src/main/scala/oolong/UExpr.scala +++ b/oolong-core/src/main/scala/oolong/UExpr.scala @@ -53,6 +53,8 @@ private[oolong] object UExpr { case class Pull(prop: Prop, cond: QExpr) extends FieldUpdateExpr(prop) + case class PullAll(prop: Prop, expr: UExpr) extends FieldUpdateExpr(prop) + } } diff --git a/oolong-core/src/main/scala/oolong/dsl/Dsl.scala b/oolong-core/src/main/scala/oolong/dsl/Dsl.scala index b100b43..cd9ac72 100644 --- a/oolong-core/src/main/scala/oolong/dsl/Dsl.scala +++ b/oolong-core/src/main/scala/oolong/dsl/Dsl.scala @@ -62,4 +62,8 @@ sealed trait Updater[DocT] { def pull[PropT, ValueT](selectProp: DocT => Iterable[PropT], input: PropT => Boolean)(using PropT =:= ValueT ): Updater[DocT] = useWithinMacro("pull") + + def pullAll[PropT, ValueT](selectProp: DocT => Iterable[PropT], input: Iterable[ValueT])(using + PropT =:= ValueT + ): Updater[DocT] = useWithinMacro("pullAll") } diff --git a/oolong-mongo-it/src/test/scala/oolong/mongo/OolongMongoUpdateSpec.scala b/oolong-mongo-it/src/test/scala/oolong/mongo/OolongMongoUpdateSpec.scala index c18ddc2..d287aec 100644 --- a/oolong-mongo-it/src/test/scala/oolong/mongo/OolongMongoUpdateSpec.scala +++ b/oolong-mongo-it/src/test/scala/oolong/mongo/OolongMongoUpdateSpec.scala @@ -8,7 +8,8 @@ import com.dimafeng.testcontainers.MongoDBContainer import com.mongodb.client.model.FindOneAndUpdateOptions import com.mongodb.client.model.ReturnDocument import concurrent.duration.DurationInt -import oolong.bson.BsonDecoder +import oolong.bson.* +import oolong.bson.given import oolong.dsl.* import org.mongodb.scala.MongoClient import org.mongodb.scala.bson.BsonDocument @@ -33,7 +34,8 @@ class OolongMongoUpdateSpec extends AsyncFlatSpec with ForAllTestContainer with TestClass("4", 13, InnerClass("sdf", 1), List(1), None, List.empty), TestClass("12345", 12, InnerClass("sdf", 11), Nil, None, List.empty), TestClass("popHead", 1, InnerClass("popHead", 1), List(1, 2, 3), None, List.empty), - TestClass("popTail", 1, InnerClass("popTail", 1), List(1, 2, 3), None, List.empty) + TestClass("popTail", 1, InnerClass("popTail", 1), List(1, 2, 3), None, List.empty), + TestClass("pullAll", 1, InnerClass("pullAll", 1), List(1, 2, 3), None, List(InnerClass("1", 1), InnerClass("2", 2))), ) implicit val ec = ExecutionContext.global @@ -204,4 +206,41 @@ class OolongMongoUpdateSpec extends AsyncFlatSpec with ForAllTestContainer with ) } + it should "$pulAll primitive elements" in { + for { + upd <- collection + .findOneAndUpdate( + query[TestClass](_.field1 == "pullAll"), + update[TestClass]( + _.pullAll(_.field4, List(1, 100)) + ), + new FindOneAndUpdateOptions().returnDocument(ReturnDocument.AFTER) + ) + .head() + .map(BsonDecoder[TestClass].fromBson(_).get) + _ = pprint.log(upd) + + } yield assert( + upd.field4 == List(2, 3) + ) + } + + it should "$pulAll documents" in { + for { + upd <- collection + .findOneAndUpdate( + query[TestClass](_.field1 == "pullAll"), + update[TestClass]( + _.pullAll(_.field6, lift(List(InnerClass("1", 1)))) // TODO: fix lift for update + ), + new FindOneAndUpdateOptions().returnDocument(ReturnDocument.AFTER) + ) + .head() + .map(BsonDecoder[TestClass].fromBson(_).get) + _ = pprint.log(upd) + } yield assert( + upd.field6 == List(InnerClass("2", 2)) + ) + } + } diff --git a/oolong-mongo/src/main/scala/oolong/mongo/MongoUpdateCompiler.scala b/oolong-mongo/src/main/scala/oolong/mongo/MongoUpdateCompiler.scala index f1eea5c..59843c8 100644 --- a/oolong-mongo/src/main/scala/oolong/mongo/MongoUpdateCompiler.scala +++ b/oolong-mongo/src/main/scala/oolong/mongo/MongoUpdateCompiler.scala @@ -60,6 +60,8 @@ object MongoUpdateCompiler extends Backend[UExpr, MU, BsonDocument] { MU.Prop(renames.getOrElse(prop.path, prop.path)), MU.QueryWrapper(optimized) ) + case FieldUpdateExpr.PullAll(prop, expr) => + MU.MongoUpdateOp.PullAll(MU.Prop(renames.getOrElse(prop.path, prop.path)), rec(expr)) }) case UExpr.ScalaCode(code) => MU.ScalaCode(code) @@ -110,7 +112,10 @@ object MongoUpdateCompiler extends Backend[UExpr, MU, BsonDocument] { )("$pop"), renderOps( ops.collect { case s: MU.MongoUpdateOp.Pull => s }.map(op => render(op.prop) + ": " + render(op.value)) - )("$pull") + )("$pull"), + renderOps( + ops.collect { case s: MU.MongoUpdateOp.PullAll => s }.map(op => render(op.prop) + ": " + render(op.value)) + )("$pullAll"), ).flatten .mkString("{\n", ",\n", "\n}") @@ -179,6 +184,7 @@ object MongoUpdateCompiler extends Backend[UExpr, MU, BsonDocument] { val tAddToSets = targetOps(ops.collect { case s: MU.MongoUpdateOp.AddToSet => s }) val tPops = targetOps(ops.collect { case s: MU.MongoUpdateOp.Pop => s }) val tPulls = targetOps(ops.collect { case s: MU.MongoUpdateOp.Pull => s }) + val tPullAlls = targetOps(ops.collect { case s: MU.MongoUpdateOp.PullAll => s }) // format: off def updaterGroup(groupName: String, updaters: List[Expr[(String, BsonValue)]]): Option[Expr[(String, BsonDocument)]] = @@ -201,6 +207,7 @@ object MongoUpdateCompiler extends Backend[UExpr, MU, BsonDocument] { updaterGroup("$addToSet", tAddToSets), updaterGroup("$pop", tPops), updaterGroup("$pull", tPulls), + updaterGroup("$pullAll", tPullAlls), ).flatten '{ diff --git a/oolong-mongo/src/main/scala/oolong/mongo/MongoUpdateNode.scala b/oolong-mongo/src/main/scala/oolong/mongo/MongoUpdateNode.scala index d503d5b..1620cbe 100644 --- a/oolong-mongo/src/main/scala/oolong/mongo/MongoUpdateNode.scala +++ b/oolong-mongo/src/main/scala/oolong/mongo/MongoUpdateNode.scala @@ -47,5 +47,7 @@ case object MongoUpdateNode { } case class Pull(override val prop: Prop, fieldQuery: QueryWrapper) extends MongoUpdateOp(prop, fieldQuery) + + case class PullAll(override val prop: Prop, override val value: MU) extends MongoUpdateOp(prop, value) } } diff --git a/oolong-mongo/src/test/scala/oolong/mongo/UpdateSpec.scala b/oolong-mongo/src/test/scala/oolong/mongo/UpdateSpec.scala index bc3c185..d241563 100644 --- a/oolong-mongo/src/test/scala/oolong/mongo/UpdateSpec.scala +++ b/oolong-mongo/src/test/scala/oolong/mongo/UpdateSpec.scala @@ -274,6 +274,20 @@ class UpdateSpec extends AnyFunSuite { ) } + test("$pullAll #1") { + val q = update[TestClass](_.pullAll(_.listField, Vector(1, 2, 3))) + val repr = renderUpdate[TestClass](_.pullAll(_.listField, Vector(1, 2, 3))) + test( + q, + repr, + BsonDocument( + "$pullAll" -> BsonDocument( + "listField" -> BsonArray(1, 2, 3) + ) + ), + ) + } + test("several update operators combined") { val q = update[TestClass]( _.unset(_.dateField)