From 56b5f2037b076a436fa1347ccdbcb269e082720b Mon Sep 17 00:00:00 2001 From: denis_savitsky Date: Mon, 1 Apr 2024 18:04:42 +0200 Subject: [PATCH] Add support for $pull --- .../src/main/scala/oolong/AstParser.scala | 4 +++ oolong-core/src/main/scala/oolong/UExpr.scala | 2 ++ .../src/main/scala/oolong/dsl/Dsl.scala | 4 +++ .../oolong/mongo/MongoUpdateCompiler.scala | 21 ++++++++++++-- .../scala/oolong/mongo/MongoUpdateNode.scala | 4 +++ .../test/scala/oolong/mongo/UpdateSpec.scala | 28 +++++++++++++++++++ project/Settings.scala | 8 ++---- 7 files changed, 63 insertions(+), 8 deletions(-) diff --git a/oolong-core/src/main/scala/oolong/AstParser.scala b/oolong-core/src/main/scala/oolong/AstParser.scala index a08200d..b2ea22f 100644 --- a/oolong-core/src/main/scala/oolong/AstParser.scala +++ b/oolong-core/src/main/scala/oolong/AstParser.scala @@ -321,6 +321,10 @@ private[oolong] class DefaultAstParser(using quotes: Quotes) extends AstParser { case '{($updater: Updater[Doc]).popLast($selectProp)} => val prop = parsePropSelector(selectProp) parseUpdater(updater, FieldUpdateExpr.Pop(UExpr.Prop(prop), FieldUpdateExpr.Pop.Remove.Last) :: acc) + + 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 '{ $updater: Updater[Doc] } => updater match { diff --git a/oolong-core/src/main/scala/oolong/UExpr.scala b/oolong-core/src/main/scala/oolong/UExpr.scala index 2598a3f..01373b7 100644 --- a/oolong-core/src/main/scala/oolong/UExpr.scala +++ b/oolong-core/src/main/scala/oolong/UExpr.scala @@ -51,6 +51,8 @@ private[oolong] object UExpr { } } + case class Pull(prop: Prop, cond: QExpr) 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 7cd71aa..b100b43 100644 --- a/oolong-core/src/main/scala/oolong/dsl/Dsl.scala +++ b/oolong-core/src/main/scala/oolong/dsl/Dsl.scala @@ -58,4 +58,8 @@ sealed trait Updater[DocT] { def popHead(selectProp: DocT => Iterable[?]): Updater[DocT] = useWithinMacro("popHead") def popLast(selectProp: DocT => Iterable[?]): Updater[DocT] = useWithinMacro("popLast") + + def pull[PropT, ValueT](selectProp: DocT => Iterable[PropT], input: PropT => Boolean)(using + PropT =:= ValueT + ): Updater[DocT] = useWithinMacro("pull") } diff --git a/oolong-mongo/src/main/scala/oolong/mongo/MongoUpdateCompiler.scala b/oolong-mongo/src/main/scala/oolong/mongo/MongoUpdateCompiler.scala index fd0c323..f1eea5c 100644 --- a/oolong-mongo/src/main/scala/oolong/mongo/MongoUpdateCompiler.scala +++ b/oolong-mongo/src/main/scala/oolong/mongo/MongoUpdateCompiler.scala @@ -54,6 +54,13 @@ object MongoUpdateCompiler extends Backend[UExpr, MU, BsonDocument] { case FieldUpdateExpr.Pop.Remove.First => Remove.First case FieldUpdateExpr.Pop.Remove.Last => Remove.Last MU.MongoUpdateOp.Pop(MU.Prop(renames.getOrElse(prop.path, prop.path)), muRemove) + case FieldUpdateExpr.Pull(prop, query) => + val optimized = MongoQueryCompiler.optimize(MongoQueryCompiler.opt(LogicalOptimizer.optimize(query))) + MU.MongoUpdateOp.Pull( + MU.Prop(renames.getOrElse(prop.path, prop.path)), + MU.QueryWrapper(optimized) + ) + }) case UExpr.ScalaCode(code) => MU.ScalaCode(code) case UExpr.Constant(t) => MU.Constant(t) @@ -100,7 +107,10 @@ object MongoUpdateCompiler extends Backend[UExpr, MU, BsonDocument] { )("$addToSet"), renderOps( ops.collect { case s: MU.MongoUpdateOp.Pop => s }.map(op => render(op.prop) + ": " + render(op.value)) - )("$pop") + )("$pop"), + renderOps( + ops.collect { case s: MU.MongoUpdateOp.Pull => s }.map(op => render(op.prop) + ": " + render(op.value)) + )("$pull") ).flatten .mkString("{\n", ",\n", "\n}") @@ -121,6 +131,9 @@ object MongoUpdateCompiler extends Backend[UExpr, MU, BsonDocument] { case MU.UIterable(iterable) => iterable.map(render).mkString("[", ",", "]") case MU.ScalaCodeIterable(_) => "[ ? ]" + case MongoUpdateNode.QueryWrapper(query) => + MongoQueryCompiler.render(query) + case _ => report.errorAndAbort(s"Wrong term: $query") } @@ -142,11 +155,13 @@ object MongoUpdateCompiler extends Backend[UExpr, MU, BsonDocument] { def targetOps(setters: List[MU.MongoUpdateOp]): List[Expr[(String, BsonValue)]] = setters.map { op => val key = op.prop.path - val valueExpr = handleValues(op.value) + def valueExpr = handleValues(op.value) val finalValueExpr = op match case addToSet: MongoUpdateOp.AddToSet => if addToSet.each then '{ BsonDocument("$each" -> $valueExpr) } else valueExpr + case pull: MongoUpdateOp.Pull => + MongoQueryCompiler.target(pull.fieldQuery.query) case _ => valueExpr '{ ${ Expr(key) } -> $finalValueExpr } } @@ -163,6 +178,7 @@ object MongoUpdateCompiler extends Backend[UExpr, MU, BsonDocument] { val tSetOnInserts = targetOps(ops.collect { case s: MU.MongoUpdateOp.SetOnInsert => s }) 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 }) // format: off def updaterGroup(groupName: String, updaters: List[Expr[(String, BsonValue)]]): Option[Expr[(String, BsonDocument)]] = @@ -184,6 +200,7 @@ object MongoUpdateCompiler extends Backend[UExpr, MU, BsonDocument] { updaterGroup("$setOnInsert", tSetOnInserts), updaterGroup("$addToSet", tAddToSets), updaterGroup("$pop", tPops), + updaterGroup("$pull", tPulls), ).flatten '{ diff --git a/oolong-mongo/src/main/scala/oolong/mongo/MongoUpdateNode.scala b/oolong-mongo/src/main/scala/oolong/mongo/MongoUpdateNode.scala index 182a328..d503d5b 100644 --- a/oolong-mongo/src/main/scala/oolong/mongo/MongoUpdateNode.scala +++ b/oolong-mongo/src/main/scala/oolong/mongo/MongoUpdateNode.scala @@ -19,6 +19,8 @@ case object MongoUpdateNode { case class ScalaCodeIterable(code: Expr[Iterable[Any]]) extends MU + case class QueryWrapper(query: MongoQueryNode) extends MU + sealed abstract class MongoUpdateOp(val prop: Prop, val value: MU) extends MU object MongoUpdateOp { case class Set(override val prop: Prop, override val value: MU) extends MongoUpdateOp(prop, value) @@ -43,5 +45,7 @@ case object MongoUpdateNode { case Remove.Last => MU.Constant(1) } } + + case class Pull(override val prop: Prop, fieldQuery: QueryWrapper) extends MongoUpdateOp(prop, fieldQuery) } } diff --git a/oolong-mongo/src/test/scala/oolong/mongo/UpdateSpec.scala b/oolong-mongo/src/test/scala/oolong/mongo/UpdateSpec.scala index 8e4305b..d0c9354 100644 --- a/oolong-mongo/src/test/scala/oolong/mongo/UpdateSpec.scala +++ b/oolong-mongo/src/test/scala/oolong/mongo/UpdateSpec.scala @@ -246,6 +246,34 @@ class UpdateSpec extends AnyFunSuite { ) } + test("$pull #1") { + val q = update[TestClass](_.pull(_.listField, _ == 2)) + val repr = renderUpdate[TestClass](_.pull(_.listField, _ == 2)) + test( + q, + repr, + BsonDocument( + "$pull" -> BsonDocument( + "listField" -> BsonDocument("$eq" -> BsonInt32(2)) + ) + ), + ) + } + + test("$pull #2") { + val q = update[TestClass](_.pull(_.classInnerClassField, _.fieldOne == "1")) + val repr = renderUpdate[TestClass](_.pull(_.classInnerClassField, _.fieldOne == "1")) + test( + q, + repr, + BsonDocument( + "$pull" -> BsonDocument( + "classInnerClassField" -> BsonDocument("fieldOne" -> BsonString("1")) + ) + ), + ) + } + test("several update operators combined") { val q = update[TestClass]( _.unset(_.dateField) diff --git a/project/Settings.scala b/project/Settings.scala index fa9e169..92b20b0 100644 --- a/project/Settings.scala +++ b/project/Settings.scala @@ -1,13 +1,9 @@ -import scalafix.sbt.ScalafixPlugin.autoImport.scalafixResolvers - -import coursierapi.{MavenRepository => CoursierMvnRepo} -import sbt.Keys._ -import sbt._ +import sbt.Keys.* object Settings { val common = Seq( organization := "io.github.leviysoft", - scalaVersion := "3.3.1", + scalaVersion := "3.3.3", scalacOptions ++= Seq( // For reference: https://docs.scala-lang.org/scala3/guides/migration/options-lookup.html "-encoding",