Skip to content

Commit

Permalink
Finished UpsertBuilder
Browse files Browse the repository at this point in the history
  • Loading branch information
darkfrog26 committed Sep 29, 2023
1 parent 204f678 commit 44b9ca5
Show file tree
Hide file tree
Showing 2 changed files with 92 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ package com.outr.arango.collection
import cats.effect.IO
import com.outr.arango.query._
import com.outr.arango.query.dsl._
import com.outr.arango.{Document, DocumentModel, DocumentRef, FieldAndValue}
import fabric.{Json, Str}
import com.outr.arango.{Document, DocumentModel, DocumentRef, Field, FieldAndValue, Ref}
import fabric._
import fabric.io.JsonFormatter
import fabric.rw._

case class UpsertBuilder[D <: Document[D], M <: DocumentModel[D]](collection: DocumentCollection[D, M],
search: List[(String, QueryPart)] = Nil,
list: Option[() => (Ref, List[Json], List[QueryPart])] = None,
search: List[QueryPart] = Nil,
insert: Option[Json] = None,
upsert: Option[Upsert[D]] = None,
ignoreErrors: Boolean = false,
Expand All @@ -24,20 +25,33 @@ case class UpsertBuilder[D <: Document[D], M <: DocumentModel[D]](collection: Do

def withSearch(f: FieldAndValue[_]): UpsertBuilder[D, M] =
withSearch(f.field.fieldName, QueryPart.Variable(f.value))
def withSearch(entry: (String, QueryPart)): UpsertBuilder[D, M] = copy(
search = entry :: search
)
def withListSearch[T <: Document[T], TM <: DocumentModel[T]](list: List[T])
(f: DocumentRef[T, TM] => List[SearchEntry]): UpsertBuilder[D, M] = {
???
def withSearch(entry: (String, QueryPart)): UpsertBuilder[D, M] = {
val part = QueryPart.Container(List(QueryPart.Static(entry._1), QueryPart.Static(": "), entry._2))
copy(
search = part :: search
)
}
def withListSearch[T <: Document[T], TM <: DocumentModel[T]](collection: DocumentCollection[T, TM], list: List[T])
(f: DocumentRef[T, TM] => List[Searchable]): UpsertBuilder[D, M] = {
copy(
list = Some(() => {
val ref = collection.ref
addRef(ref)
val entries = f(ref).map(_.toSearch)
(ref, list.map(_.json(collection.model.rw)), entries)
})
)
}
def withListSearch(list: List[D])(f: DocumentRef[D, M] => List[Searchable]): UpsertBuilder[D, M] =
withListSearch[D, M](collection, list)(f)
def withInsert(doc: D): UpsertBuilder[D, M] = withInsert(doc.json(collection.model.rw))
def withInsert(json: Json): UpsertBuilder[D, M] = withInsert(JsonFormatter.Compact(json))
def withInsert(insert: String): UpsertBuilder[D, M] = copy(insert = Some(insert))

def withUpdate(doc: D): UpsertBuilder[D, M] = withUpdate(doc.json(collection.model.rw))
def withUpdate(json: Json): UpsertBuilder[D, M] = withUpdate(JsonFormatter.Compact(json))
def withUpdate(update: String): UpsertBuilder[D, M] = copy(upsert = Some(Upsert.Update(update)))
def withNoUpdate: UpsertBuilder[D, M] = withUpdate(obj())

def withReplace(doc: D): UpsertBuilder[D, M] = copy(upsert = Some(Upsert.Replace(doc)))

Expand All @@ -59,27 +73,46 @@ case class UpsertBuilder[D <: Document[D], M <: DocumentModel[D]](collection: Do
forceIndexHint = forceIndexHint
)

def toQuery(includeReturn: Boolean): Query = {
assert(search.nonEmpty, "At least one search criteria must be defined")
assert(insert.nonEmpty, "Insert must be defined")
assert(upsert.nonEmpty, "Update or Replace must be defined")
def toQuery(includeReturn: Boolean): Query = noConsumingRefs {
assert(search.nonEmpty || list.nonEmpty, "At least one search criteria must be defined")
assert(insert.nonEmpty || list.nonEmpty, "Insert must be defined")
assert(upsert.nonEmpty || upsert.nonEmpty || list.nonEmpty, "Update or Replace must be defined")

var searchEntries = search

val listValue = list.map(f => f())
val forQuery = listValue.map {
case (ref, list, entries) =>
searchEntries = searchEntries ::: entries
aql"""FOR ${QueryPart.Ref(ref)} IN $list"""
}

val commaPart = QueryPart.Static(", ")
val searchQuery = Query(search.flatMap {
case (key, value) => List(
val searchQuery = Query(searchEntries.flatMap { part =>
List(
commaPart,
QueryPart.Static(s"$key: "),
value
part
)
}.tail)

val upsertQuery = aql"""UPSERT { $searchQuery }"""
val insertQuery = aql"""INSERT ${insert.get}"""
val updateReplaceQuery = upsert.get match {
val insertQuery = insert match {
case Some(i) => aql"""INSERT $i"""
case None =>
val ref = listValue.get._1
aql"""INSERT ${QueryPart.Ref(ref)}"""
}
val updateReplaceQuery = upsert.map {
case Upsert.Update(value) => aql"""UPDATE ${QueryPart.Static(value)} IN $collection"""
case Upsert.Replace(replacement) => aql"""REPLACE $replacement IN $collection"""
}.getOrElse {
val ref = listValue.get._1
aql"""REPLACE ${QueryPart.Ref(ref)} IN $collection"""
}
var queries = List(upsertQuery, insertQuery, updateReplaceQuery)
forQuery.foreach { q =>
queries = q :: queries
}
if (includeReturn) {
val returnQuery = aql"""RETURN { original: OLD, newValue: NEW }"""
queries = queries ::: List(returnQuery)
Expand All @@ -93,6 +126,11 @@ case class UpsertBuilder[D <: Document[D], M <: DocumentModel[D]](collection: Do
}

def toList: IO[List[UpsertResult[D]]] = toStream.compile.toList

def execute(): IO[Unit] = {
val query = toQuery(includeReturn = false)
collection.graph.execute(query)
}
}

sealed trait Upsert[D <: Document[D]]
Expand All @@ -108,4 +146,19 @@ object UpsertResult {
implicit def rw[D: RW]: RW[UpsertResult[D]] = RW.gen
}

sealed trait SearchEntry
sealed trait Searchable {
def toSearch: QueryPart
}

object Searchable {
case class Filter[F](field1: Field[F], condition: String, field2: Field[F]) extends Searchable {
override val toSearch: QueryPart = Query.merge(
List(
Query(field1.fieldName),
Query(":"),
Query(List(field2.fqfPart))
),
separator = " "
)
}
}
27 changes: 19 additions & 8 deletions driver/src/test/scala/spec/AdvancedSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import cats.effect.IO
import cats.effect.testing.scalatest.AsyncIOSpec
import com.outr.arango._
import com.outr.arango.backup.{DatabaseBackup, DatabaseRestore}
import com.outr.arango.collection.DocumentCollection
import com.outr.arango.collection.{DocumentCollection, Searchable}
import com.outr.arango.core.{ComputeOn, ComputedValue, DeleteOptions, StreamTransaction, TransactionLock, TransactionStatus}
import com.outr.arango.query._
import com.outr.arango.query.dsl._
import com.outr.arango.queue.OperationQueueSupport
import fabric.rw._
import fabric._
import fabric.search.SearchEntry
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AsyncWordSpec
import profig.Profig
Expand Down Expand Up @@ -335,16 +336,26 @@ class AdvancedSpec extends AsyncWordSpec with AsyncIOSpec with Matchers with Ope
result.newValue.bio should be("Replaced!")
}
}
// "upsert multiple bios" in {
// database.people.upsert
// .withListSearch()
// }
// TODO: Upsert from a list
"upsert multiple bios" in {
database.people.upsert
.withListSearch(List(Person("Bethany", 30), Person("Adam", 30), Person("Tom", 30))) { p =>
List(Searchable.Filter(Person.name, "==", p.name))
}
.withNoUpdate
.toList
.map { list =>
list.length should be(3)
val names = list.map(_.newValue.name).toSet
names should be(Set("Bethany", "Adam", "Tom"))
val ages = list.map(_.newValue.age).toSet
ages should be(Set(19, 23, 30))
}
}
"batch delete" in {
database.people.query.byFilter(_.age > 10).toList.flatMap { list =>
list.map(_.name).toSet should be(Set("Bethany", "Donna", "Adam"))
list.map(_.name).toSet should be(Set("Bethany", "Donna", "Adam", "Tom"))
database.people.batch.delete(list.map(_._id), DeleteOptions(waitForSync = true, silent = false)).map { results =>
results.documents.length should be(3)
results.documents.length should be(4)
}
}
}
Expand Down

0 comments on commit 44b9ca5

Please sign in to comment.