Skip to content

Commit

Permalink
Increase IT coverage for queries (#40)
Browse files Browse the repository at this point in the history
* Increase IT coverage for queries

* Add IT for updates

* Add test for $elemMatch

* Fix $elemMatch documentation

---------

Co-authored-by: denis_savitsky <[email protected]>
  • Loading branch information
desavitsky and denis_savitsky authored Jan 27, 2024
1 parent 53ce389 commit 07a46f9
Show file tree
Hide file tree
Showing 5 changed files with 226 additions and 29 deletions.
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,13 +230,15 @@ val q = query[Course](_.studentNames.length == 20)
```scala
import oolong.dsl.*

case class Course(studentNames: List[String], tutor: String)
case class Student(name: String, age: Int)

val q = query[Course](_.studentNames.exists(_ == 20)) // $elemMatch ommited when querying single field
// q is {"studentNames": 20}
case class Course(students: List[Student], tutor: String)

val q = query[Course](course => course.studentNames.exists(_ > 20) && course.tutor == "Pavlov")
// q is {"studentNames": {"$elemMatch": {"studentNames": {"$gt": 20}, "tutor": "Pavlov"}}}
val q = query[Course](_.students.exists(_.age == 20)) // $elemMatch ommited when querying single field
// q is {"students.age": 20}

val q = query[Course](course => course.students.exists(st => st.age > 20 && st.name == "Pavel"))
// q is {"students": {"$elemMatch": {"age": {"$gt": 20}, "name": "Pavel"}}}

```

Expand Down
3 changes: 3 additions & 0 deletions oolong-core/src/main/scala/oolong/AstParser.scala
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,9 @@ private[oolong] class DefaultAstParser(using quotes: Quotes) extends AstParser {
case '{ ($x: Option[_]).isDefined } =>
QExpr.Exists(parse(x), QExpr.Constant(true))

case '{ ($x: Option[_]).nonEmpty } =>
QExpr.Exists(parse(x), QExpr.Constant(true))

case PropSelector(name, path) if name == paramName =>
QExpr.Prop(path.mkString("."))

Expand Down
103 changes: 80 additions & 23 deletions oolong-mongo-it/src/test/scala/oolong/mongo/OolongMongoSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,15 @@ class OolongMongoSpec extends AsyncFlatSpec with ForAllTestContainer with Before
container.start()

val client = MongoClient(container.replicaSetUrl)
val collection = client.getDatabase("test").getCollection[BsonDocument]("testColection")
val collection = client.getDatabase("test").getCollection[BsonDocument]("testCollection")

override def beforeAll(): Unit = {
val documents = List(
TestClass("0", 0, InnerClass("sdf"), Nil),
TestClass("1", 1, InnerClass("qwe"), Nil),
TestClass("2", 2, InnerClass("asd"), Nil),
TestClass("3", 12, InnerClass("sdf"), Nil)
TestClass("0", 0, InnerClass("sdf", 1), List(1, 2), None, List(InnerClass("abc", 1))),
TestClass("1", 1, InnerClass("qwe", 2), Nil, Some(2L), List(InnerClass("cde", 10))),
TestClass("2", 2, InnerClass("asd", 3), Nil, None, List.empty),
TestClass("3", 12, InnerClass("sdf", 4), List(10, 25), None, List.empty),
TestClass("12345", 12, InnerClass("sdf", 5), Nil, None, List.empty),
)

implicit val ec = ExecutionContext.global
Expand All @@ -55,14 +56,14 @@ class OolongMongoSpec extends AsyncFlatSpec with ForAllTestContainer with Before
case Failure(exception) => Future.failed(exception)
case Success(value) => Future.successful(value)
}
} yield assert(v == TestClass("1", 1, InnerClass("qwe"), Nil))
} yield assert(v == TestClass("1", 1, InnerClass("qwe", 2), Nil, Some(2L), List(InnerClass("cde", 10))))
}

it should "find documents in a collection with query with runtime constant" in {
val q = query[TestClass](_.field2 <= lift(Random.between(13, 100)))
for {
res <- collection.find(q).toFuture()
} yield assert(res.size == 4)
} yield assert(res.size == 5)
}

it should "find both documents with OR operator" in {
Expand Down Expand Up @@ -93,7 +94,7 @@ class OolongMongoSpec extends AsyncFlatSpec with ForAllTestContainer with Before

for {
res <- collection.find(q).toFuture()
} yield assert(res.size == 4)
} yield assert(res.size == 5)
}

it should "compile queries with `unchecked`" in {
Expand Down Expand Up @@ -129,15 +130,23 @@ class OolongMongoSpec extends AsyncFlatSpec with ForAllTestContainer with Before
}

it should "compile queries with `.contains` #3" in {
val q = query[TestClass](x => lift(Set(InnerClass("qwe"), InnerClass("asd"))).contains(x.field3))
val q = query[TestClass](x => lift(Set(InnerClass("qwe", 2), InnerClass("asd", 3))).contains(x.field3))

for {
res <- collection.find(q).toFuture()
} yield assert(res.size == 2)
}

it should "compile queries with `.contains` #4" in {
val q = query[TestClass](x => !List(1, 2, 3).contains(x.field2))

for {
res <- collection.find(q).toFuture()
} yield assert(res.size == 3)
}

it should "compile queries with nested objects" in {
val q = query[TestClass](_.field3 == lift(InnerClass("qwe")))
val q = query[TestClass](_.field3 == lift(InnerClass("qwe", 2)))

for {
res <- collection.find(q).toFuture()
Expand All @@ -149,40 +158,88 @@ class OolongMongoSpec extends AsyncFlatSpec with ForAllTestContainer with Before

for {
res <- collection.find(q).toFuture()
} yield assert(res.size == 4)
} yield assert(res.size == 5)
}

it should "compile queries with `.isInstance` #2" in {
val q = query[TestClass](_.field3.isInstanceOf[MongoType.DOCUMENT])

for {
res <- collection.find(q).toFuture()
} yield assert(res.size == 4)
} yield assert(res.size == 5)
}

it should "compile queries with `.mod` #1" in {
it should "compile queries with `%` #1" in {
val q = query[TestClass](_.field2 % 4 == 0)

for {
res <- collection.find(q).toFuture()
} yield assert(res.size == 2)
} yield assert(res.size == 3)
}

it should "compile queries with `.mod` #2" in {
it should "compile queries with `%` #2" in {
val q = query[TestClass](_.field2 % 4.99 == 0)

for {
res <- collection.find(q).toFuture()
} yield assert(res.size == 3)
}

it should "compile queries with $type" in {
val q = query[TestClass](_.field2.isInstanceOf[MongoType.INT32])

for {
res <- collection.find(q).toFuture()
} yield assert(res.size == 5)
}

it should "compile queries with $exists" in {
val q = query[TestClass](_.field5.isDefined)

for {
res <- collection.find(q).toFuture()
} yield assert(res.size == 1)
}

it should "compile queries with $regex" in {
val q = query[TestClass](_.field1.matches("\\d{2,5}"))

for {
res <- collection.find(q).toFuture()
} yield assert(res.size == 1)
}

it should "compile queries with $size" in {
val q = query[TestClass](_.field4.size == 2)

for {
res <- collection.find(q).toFuture()
} yield assert(res.size == 2)
}

// TODO: test updates
// it should "compile updates" in {
// val upd = compileUpdate {
// update[CompanySuccess]
// .set(_.from, 2)
// .set(_.field4, liftU(Field("qweasd")))
// }
// }
it should "compile queries with $elemMatch" in {
val q = query[TestClass](tc => tc.field6.exists(s => s.innerField == "cde"))

for {
res <- collection.find(q).toFuture()
} yield assert(res.size == 1)
}

it should "compile queries with $elemMatch #2" in {
val q = query[TestClass](tc => tc.field6.exists(s => s.innerField != "cde" && s.otherField < 10))

for {
res <- collection.find(q).toFuture()
} yield assert(res.size == 1)
}

it should "compile queries with $all" in {
inline def variants = List(1, 2)
val q = query[TestClass](ins => variants.forall(ins.field4.contains))

for {
res <- collection.find(q).toFuture()
} yield assert(res.size == 1)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package oolong.mongo

import scala.concurrent.Await
import scala.concurrent.ExecutionContext

import com.dimafeng.testcontainers.ForAllTestContainer
import com.dimafeng.testcontainers.MongoDBContainer
import concurrent.duration.DurationInt
import oolong.dsl.*
import org.mongodb.scala.MongoClient
import org.mongodb.scala.bson.BsonDocument
import org.mongodb.scala.model.UpdateOptions
import org.scalatest.BeforeAndAfterAll
import org.scalatest.flatspec.AsyncFlatSpec

class OolongMongoUpdateSpec extends AsyncFlatSpec with ForAllTestContainer with BeforeAndAfterAll {

override val container: MongoDBContainer = MongoDBContainer()
container.start()

val client = MongoClient(container.replicaSetUrl)
val collection = client.getDatabase("test").getCollection[BsonDocument]("testCollection")

override def beforeAll(): Unit = {
val documents = List(
TestClass("0", 0, InnerClass("sdf", 2.0), List(1, 2), None, List.empty),
TestClass("1", 1, InnerClass("qwe", 3), Nil, Some(2L), List.empty),
TestClass("2", 2, InnerClass("asd", 0), Nil, None, List.empty),
TestClass("3", 12, InnerClass("sdf", 1), Nil, None, List.empty),
TestClass("12345", 12, InnerClass("sdf", 11), Nil, None, List.empty),
)

implicit val ec = ExecutionContext.global

val f = for {
_ <- client.getDatabase("test").createCollection("testCollection").head()
_ <- collection.insertMany(documents.map(_.bson.asDocument())).head()
} yield ()

Await.result(f, 30.seconds)
}

it should "update with $inc" in {
for {
res <- collection
.updateOne(
query[TestClass](_.field1 == "0"),
update[TestClass](_.inc(_.field2, 1))
)
.head()
} yield assert(res.wasAcknowledged() && res.getMatchedCount == 1 && res.getModifiedCount == 1)
}

it should "update with $min" in {
for {
res <- collection
.updateOne(
query[TestClass](_.field1 == "3"),
update[TestClass](_.min(_.field2, 1))
)
.head()
} yield assert(res.wasAcknowledged() && res.getMatchedCount == 1 && res.getModifiedCount == 1)
}

it should "update with $max" in {
for {
res <- collection
.updateOne(
query[TestClass](_.field1 == "0"),
update[TestClass](_.max(_.field2, 10))
)
.head()
} yield assert(res.wasAcknowledged() && res.getMatchedCount == 1 && res.getModifiedCount == 1)
}

it should "update with $mul" in {
for {
res <- collection
.updateOne(
query[TestClass](_.field1 == "0"),
update[TestClass](_.mul(_.field2, 10))
)
.head()
} yield assert(res.wasAcknowledged() && res.getMatchedCount == 1 && res.getModifiedCount == 1)
}

it should "update with $rename" in {
for {
res <- collection
.updateOne(
query[TestClass](_.field1 == "12345"),
update[TestClass](_.rename(_.field2, "NewField2"))
)
.head()
} yield assert(res.wasAcknowledged() && res.getMatchedCount == 1 && res.getModifiedCount == 1)
}

it should "update with $set" in {
for {
res <- collection
.updateOne(
query[TestClass](_.field1 == "0"),
update[TestClass](_.set(_.field5.!!, 2L))
)
.head()
} yield assert(res.wasAcknowledged() && res.getMatchedCount == 1 && res.getModifiedCount == 1)
}

it should "not update existing document with $senOnInsert" in {
for {
res <- collection
.updateOne(
query[TestClass](_.field1 == "0"),
update[TestClass](_.setOnInsert(_.field2, 2)),
(new UpdateOptions).upsert(true)
)
.head()
} yield assert(res.wasAcknowledged() && res.getMatchedCount == 1 && res.getModifiedCount == 0)
}

it should "update with $unset" in {
for {
res <- collection
.updateOne(
query[TestClass](_.field1 == "1"),
update[TestClass](_.unset(_.field5))
)
.head()
} yield assert(res.wasAcknowledged() && res.getMatchedCount == 1 && res.getModifiedCount == 1)
}

}
5 changes: 4 additions & 1 deletion oolong-mongo/src/test/scala/oolong/mongo/TestClass.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ case class TestClass(
field2: Int,
field3: InnerClass,
field4: List[Int],
field5: Option[Long],
field6: List[InnerClass]
) derives BsonEncoder,
BsonDecoder

case class InnerClass(
innerField: String
innerField: String,
otherField: Double
) derives BsonEncoder,
BsonDecoder

0 comments on commit 07a46f9

Please sign in to comment.