All Downloads are FREE. Search and download functionalities are using the official Maven repository.

quasar.physical.mongodb.workflowbuilder.scala Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2014–2017 SlamData Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package quasar.physical.mongodb

import slamdata.Predef._
import quasar.{NonTerminal, RenderTree, Terminal}, RenderTree.ops._
import quasar.common.SortDir
import quasar.contrib.matryoshka._
import quasar.contrib.scalaz._
import quasar.fp._
import quasar._, Planner._
import quasar.javascript._
import quasar.jscore, jscore.JsFn
import quasar.physical.mongodb.accumulator._
import quasar.physical.mongodb.expression._
import quasar.physical.mongodb.workflow._
import quasar.qscript.IdStatus
import quasar.std.StdLib._

import matryoshka._
import matryoshka.data.Fix
import matryoshka.implicits._
import scalaz._, Scalaz._

sealed abstract class WorkflowBuilderF[F[_], +A] extends Product with Serializable

object WorkflowBuilderF {
  import WorkflowBuilder._

  // NB: This instance can’t be derived, because of `dummyOp`.
  implicit def equal[F[_]: Coalesce](implicit ev: WorkflowOpCoreF :<: F): Delay[Equal, WorkflowBuilderF[F, ?]] =
    new Delay[Equal, WorkflowBuilderF[F, ?]] {
      def apply[A](eq: Equal[A]) = {
        implicit val eqA: Equal[A] = eq

        Equal.equal((v1: WorkflowBuilderF[F, A], v2: WorkflowBuilderF[F, A]) => (v1, v2) match {
          case (CollectionBuilderF(g1, b1, s1), CollectionBuilderF(g2, b2, s2)) =>
            g1 == g2 && b1 == b2 && s1 ≟ s2
          case (v1 @ ShapePreservingBuilderF(s1, i1, _), v2 @ ShapePreservingBuilderF(s2, i2, _)) =>
            eq.equal(s1, s2) && i1 ≟ i2 && v1.dummyOp == v2.dummyOp
          case (ExprBuilderF(s1, e1), ExprBuilderF(s2, e2)) =>
            eq.equal(s1, s2) && e1 == e2
          case (DocBuilderF(s1, e1), DocBuilderF(s2, e2)) =>
            eq.equal(s1, s2) && e1 == e2
          case (GroupBuilderF(s1, k1, c1), GroupBuilderF(s2, k2, c2)) =>
            eq.equal(s1, s2) && k1 ≟ k2 && c1 == c2
          case (FlatteningBuilderF(s1, f1), FlatteningBuilderF(s2, f2)) =>
            eq.equal(s1, s2) && f1 == f2
          case (UnionBuilderF(ls1, rs1), UnionBuilderF(ls2, rs2)) =>
            eq.equal(ls1, ls2) && eq.equal(rs1, rs2)
          case _ => false
        })
      }
    }

  implicit def traverse[F[_]]: Traverse[WorkflowBuilderF[F, ?]] =
    new Traverse[WorkflowBuilderF[F, ?]] {
      def traverseImpl[G[_], A, B](
        fa: WorkflowBuilderF[F, A])(
        f: A => G[B])(
        implicit G: Applicative[G]):
          G[WorkflowBuilderF[F, B]] =
        fa match {
          case x @ CollectionBuilderF(_, _, _) => G.point(x)
          case ShapePreservingBuilderF(src, inputs, op) =>
            f(src).map(ShapePreservingBuilderF(_, inputs, op))
          case ExprBuilderF(src, expr) => f(src).map(ExprBuilderF(_, expr))
          case DocBuilderF(src, shape) => f(src).map(DocBuilderF(_, shape))
          case GroupBuilderF(src, keys, contents) =>
            f(src).map(GroupBuilderF(_, keys, contents))
          case FlatteningBuilderF(src, fields) =>
            f(src).map(FlatteningBuilderF(_, fields))
          case UnionBuilderF(lSrc, rSrc) =>
            (f(lSrc) ⊛ f(rSrc))(UnionBuilderF(_, _))
        }
    }

  implicit def renderTree[F[_]: Coalesce: Functor](implicit
      RG: RenderTree[ListMap[BsonField.Name, AccumOp[Fix[ExprOp]]]],
      RF: RenderTree[Fix[F]],
      ev: WorkflowOpCoreF :<: F
    ): Delay[RenderTree, WorkflowBuilderF[F, ?]] =
    Delay.fromNT(λ[RenderTree ~> (RenderTree ∘ WorkflowBuilderF[F, ?])#λ](rt => {
      val nodeType = "WorkflowBuilder" :: Nil
      RenderTree.make {
        case CollectionBuilderF(graph, base, struct) =>
          NonTerminal("CollectionBuilder" :: nodeType, Some(base.shows),
            graph.render ::
              Terminal("Schema" :: "CollectionBuilder" :: nodeType, struct ∘ (_.shows)) ::
              Nil)
        case spb @ ShapePreservingBuilderF(src, inputs, op) =>
          val nt = "ShapePreservingBuilder" :: nodeType
          NonTerminal(nt, None,
            rt.render(src) :: (inputs.map(_.render) :+ spb.dummyOp.render))
        case ExprBuilderF(src, expr) =>
          NonTerminal("ExprBuilder" :: nodeType, None,
            rt.render(src) :: expr.render :: Nil)
        case DocBuilderF(src, shape) =>
          val nt = "DocBuilder" :: nodeType
          NonTerminal(nt, None,
            rt.render(src) ::
              NonTerminal("Shape" :: nt, None,
                shape.toList.map {
                  case (name, expr) =>
                    NonTerminal("Name" :: nodeType, Some(name.value), List(expr.render))
                }) ::
              Nil)
        case GroupBuilderF(src, keys, content) =>
          val nt = "GroupBuilder" :: nodeType
          NonTerminal(nt, None,
            rt.render(src) ::
              NonTerminal("By" :: nt, None, keys.map(_.render)) ::
              RG.render(content).copy(nodeType = "Content" :: nt) ::
              Nil)
        case FlatteningBuilderF(src, fields) =>
          val nt = "FlatteningBuilder" :: nodeType
          NonTerminal(nt, None,
            rt.render(src) ::
              fields.toList.map {
                case StructureType.Array(field, id) =>
                  NonTerminal("Array" :: nt, field.shows.some, List(id.render))
                case StructureType.Object(field, id) =>
                  NonTerminal("Object" :: nt, field.shows.some, List(id.render))
              })
        case UnionBuilderF(lSrc, rSrc) =>
          val nt = "UnionBuilder" :: nodeType
          NonTerminal(nt, None, List(rt.render(lSrc), rt.render(rSrc)))
      }
    }))
}

object WorkflowBuilder {
  import fixExprOp._

  /** A partial description of a query that can be run on an instance of MongoDB */
  type WorkflowBuilder[F[_]] = Fix[WorkflowBuilderF[F, ?]]
  /** If we know what the shape is, represents the list of Fields. */
  type Schema = Option[NonEmptyList[BsonField.Name]]

  /** Either arbitrary javascript expression or Pipeline expression
    * An arbitrary javascript is more powerful but less performant because it
    * gets materialized into a Map/Reduce operation.
    */
  // TODO: We should be able to handle _every_ MapFunc as JS, so this should
  //       eventually be `AndMaybe[JsFn, Fix[ExprOp]]`
  type Expr = JsFn \&/ Fix[ExprOp]

  // FIXME: We should never need to convert to JS anymore, so get rid of this.
  private def exprToJs(expr: Expr): PlannerError \/ JsFn = expr match {
    case HasThis(js)        => js.right
    case \&/.That($var(dv)) => dv.toJs.right
    case _                  => UnsupportedJS(expr.render.shows).left
  }

  def docVarToExpr(dv: DocVar): Expr = \&/(dv.toJs, $var(dv))

  def semiAlignExpr[F[_]: Traverse](fa: F[Expr])
      : Option[F[Fix[ExprOp]]] =
    fa.traverse(HasThat.unapply)

  def alignExpr[F[_]: Traverse](fa: F[Expr])
      : Option[F[JsFn] \/ F[Fix[ExprOp]]] =
    Bitraverse[\/].leftTraverse.sequence(
      fa.traverse(HasThat.unapply) \/> fa.traverse(HasThis.unapply))

  /** This is a Leaf node which can be used to construct a more complicated
    * WorkflowBuilder. Takes a value resulting from a Workflow and wraps it in a
    * WorkflowBuilder. For example: If you want to read from MongoDB and then
    * project on a field, the read would be the CollectionBuilder.
    * @param base Name, or names under which the values produced by the src will
    *             be found. It's most often `Root`, or else it's probably a
    *             temporary `Field`.
    * @param struct In the case of read, it's None. In the case where we are
    *               converting a WorkflowBuilder into a Workflow, we have access
    *               to the shape of this Workflow and encode it in `struct`.
    */
  final case class CollectionBuilderF[F[_]](
    src: Fix[F],
    base: Base,
    struct: Schema) extends WorkflowBuilderF[F, Nothing]
  object CollectionBuilder {
    def apply[F[_]](graph: Fix[F], base: Base, struct: Schema) =
      Fix[WorkflowBuilderF[F, ?]](new CollectionBuilderF(graph, base, struct))
  }

  /** For instance, \$match, \$skip, \$limit, \$sort */
  final case class ShapePreservingBuilderF[F[_], A](
    src: A,
    inputs: List[Expr],
    op: PartialFunction[List[BsonField], FixOp[F]])
      extends WorkflowBuilderF[F, A]
  {
    def dummyOp(implicit ev0: WorkflowOpCoreF :<: F, ev1: Coalesce[F]): Fix[F] =
      op(
        inputs.zipWithIndex.map {
          case (_, index) => BsonField.Name("_" + index.toString)
        })(
        // Nb. This read is an arbitrary value that allows us to compare the partial function
        $read[F](Collection(DatabaseName(""), CollectionName(""))))
  }
  object ShapePreservingBuilder {
    def apply[F[_]: Coalesce](
      src: WorkflowBuilder[F],
      inputs: List[Expr],
      op: PartialFunction[List[BsonField], FixOp[F]])
      (implicit ev: WorkflowOpCoreF :<: F) =
      Fix[WorkflowBuilderF[F, ?]](new ShapePreservingBuilderF(src, inputs, op))
  }

  /** A query that applies an `Expr` operator to a source (which could be
    * multiple values). You can think of `Expr` as a function application in
    * MongoDB that accepts values and produces new values. It's kind of like a
    * map. The shape coming out of an `ExprBuilder` is unknown because of the
    * fact that the expression can be arbitrary.
    * @param src The values on which to apply the `Expr`
    * @param expr The expression that produces a new set of values given a set
    *             of values.
    */
  final case class ExprBuilderF[F[_], A](src: A, expr: Expr) extends WorkflowBuilderF[F, A]
  object ExprBuilder {
    def apply[F[_]](src: WorkflowBuilder[F], expr: Expr) =
      Fix[WorkflowBuilderF[F, ?]](new ExprBuilderF(src, expr))
  }

  /** Same as an `ExprBuilder` but contains the shape of the resulting query.
    * The result is a document that maps the field Name to the resulting values
    * from applying the `Expr` associated with that name.
    * NB: The shape is more restrictive than \$project because we may need to
    *     convert it to a `GroupBuilder`, and a nested `Reshape` can be realized
    *     with a chain of DocBuilders, leaving the collapsing to
    *     Workflow.coalesce.
    */
  final case class DocBuilderF[F[_], A](src: A, shape: ListMap[BsonField.Name, Expr])
      extends WorkflowBuilderF[F, A]
  object DocBuilder {
    def apply[F[_]](src: WorkflowBuilder[F], shape: ListMap[BsonField.Name, Expr]) =
      Fix[WorkflowBuilderF[F, ?]](new DocBuilderF(src, shape))
  }

  sealed abstract class DocContents[A] extends Product with Serializable
  object DocContents {
    final case class Exp[A](contents: A) extends DocContents[A]
    final case class Doc[A](contents: ListMap[BsonField.Name, A]) extends DocContents[A]

    implicit def DocContentsRenderTree[A: RenderTree]: RenderTree[DocContents[A]] =
      new RenderTree[DocContents[A]] {
        val nodeType = "Contents" :: Nil

        def render(v: DocContents[A]) =
          v match {
            case Exp(a) => NonTerminal("Exp" :: nodeType, None, a.render :: Nil)
            case Doc(b) => NonTerminal("Doc" :: nodeType, None, b.render :: Nil)
          }
      }
  }
  import DocContents._

  def contentsToBuilder[F[_]]: DocContents[Expr] => WorkflowBuilder[F] => WorkflowBuilder[F] = {
    case Exp(expr) => ExprBuilder(_, expr)
    case Doc(doc)  => DocBuilder(_, doc)
  }

  type GroupContents = ListMap[BsonField.Name, AccumOp[Fix[ExprOp]]]

  final case class GroupBuilderF[F[_], A](
    src: A, keys: List[Expr], contents: GroupContents)
      extends WorkflowBuilderF[F, A]
  object GroupBuilder {
    def apply[F[_]](
      src: WorkflowBuilder[F],
      keys: List[Expr],
      contents: GroupContents) =
      Fix[WorkflowBuilderF[F, ?]](new GroupBuilderF(src, keys, contents))
  }

  sealed abstract class StructureType[A] {
    val field: A
  }
  object StructureType {
    final case class Array[A](field: A, includeIndex: IdStatus)
        extends StructureType[A]
    final case class Object[A](field: A, includeKey: IdStatus)
        extends StructureType[A]

    implicit val StructureTypeTraverse: Traverse[StructureType] =
      new Traverse[StructureType] {
        def traverseImpl[G[_], A, B](fa: StructureType[A])(f: A => G[B])(implicit G: Applicative[G]):
            G[StructureType[B]] =
          fa match {
            case Array(field, i) => f(field).map(Array(_, i))
            case Object(field, i) => f(field).map(Object(_, i))
          }
      }
  }

  final case class FlatteningBuilderF[F[_], A](src: A, fields: Set[StructureType[DocVar]])
      extends WorkflowBuilderF[F, A]
  object FlatteningBuilder {
    def apply[F[_]](src: WorkflowBuilder[F], fields: Set[StructureType[DocVar]]) =
      Fix[WorkflowBuilderF[F, ?]](new FlatteningBuilderF(src, fields))
  }

  final case class UnionBuilderF[F[_], A](lSrc: A, rSrc: A) extends WorkflowBuilderF[F, A]
  object UnionBuilder {
    def apply[F[_]](lSrc: WorkflowBuilder[F], rSrc: WorkflowBuilder[F]) =
      Fix[WorkflowBuilderF[F, ?]](new UnionBuilderF(lSrc, rSrc))
  }

  private def rewriteDocPrefix(doc: ListMap[BsonField.Name, Expr], base: Base)
      (implicit exprOps: ExprOpOps.Uni[ExprOp]): ListMap[BsonField.Name, Expr] =
    doc ∘ (rewriteExprPrefix(_, base))

  private def rewriteExprPrefix(expr: Expr, base: Base)
      (implicit exprOps: ExprOpOps.Uni[ExprOp]): Expr =
    expr.bimap(
      base.toDocVar.toJs >>> _,
      _.cata(exprOps.rewriteRefs(prefixBase0(base))))

  private def rewriteExpr
    (expr: Expr, base: Expr)
    (implicit exprOps: ExprOpOps.Uni[ExprOp])
      : Option[Expr] =
    (expr, base) match {
      case (\&/.Both(js1, ex1), \&/.Both(js2, ex2)) =>
        ex1.transCataM(exprOps.rebase(ex2)) ∘ (\&/(js2 >>> js1, _))
      case (HasThat(ex1),       HasThat(ex2))       =>
        ex1.transCataM(exprOps.rebase(ex2)) ∘ \&/-
      case (HasThis(js1),       HasThis(js2))       => -\&/(js2 >>> js1).some
      case (_,                  _)                  => none
    }

  private def prefixBase0(base: Base): PartialFunction[DocVar, DocVar] =
    prefixBase(base.toDocVar)

  private val jsBase = jscore.Name("__val")

  // TODO: Determine if this is necessary. If so, flesh it out.
  def normalize[WF[_], T]
    (implicit T: Recursive.Aux[T, WorkflowBuilderF[WF, ?]],
      exprOps: ExprOpOps.Uni[ExprOp])
      : TransformM[Option, T, WorkflowBuilderF[WF, ?], WorkflowBuilderF[WF, ?]] = {
    case ExprBuilderF(Embed(ExprBuilderF(src, inner)), outer) =>
      rewriteExpr(outer, inner) ∘ (ExprBuilderF(src, _))
    case DocBuilderF(Embed(ExprBuilderF(src, inner)), doc) =>
      doc.traverse(rewriteExpr(_, inner)) >>=
        (d => (doc ≠ d).option(DocBuilderF(src, d)))
    case _ => none
  }

  def schema[WF[_]]: Algebra[WorkflowBuilderF[WF, ?], Schema] = {
    case CollectionBuilderF(_, _, schema)   => schema
    case ShapePreservingBuilderF(src, _, _) => src
    case ExprBuilderF(_, _)                 => none
    case DocBuilderF(_, shape)              => shape.keys.toList.toNel
    case GroupBuilderF(_, _, shape)         => shape.keys.toList.toNel
    case FlatteningBuilderF(src, _)         => src
    case UnionBuilderF(lSrc, rSrc)          => if (lSrc ≟ rSrc) lSrc else none

  }

  // TODO: See if we can extract `WB => Base` from this.
  // FIXME: There are a few recursive references to this function. We need to
  //        eliminate those.
  @SuppressWarnings(Array("org.wartremover.warts.Recursion"))
  def toWorkflow[M[_]: Monad, WF[_]: Coalesce]
    (implicit M: MonadError_[M, PlannerError], ev0: WorkflowOpCoreF :<: WF, ev1: RenderTree[WorkflowBuilder[WF]], exprOps: ExprOpOps.Uni[ExprOp])
      : AlgebraM[M, WorkflowBuilderF[WF, ?], (Fix[WF], Base)] = {
    case CollectionBuilderF(graph, base, _) => (graph, base).point[M]
    case ShapePreservingBuilderF((g, b), inputs, op) =>
      inputs match {
        case Nil => (op(Nil)(g), b).point[M]
        case _ =>
          inputs.map(rewriteExprPrefix(_, b)).traverse {
            case HasThat($var(DocField(x))) => x.some
            case _                          => none
          }.fold {
            val pairs = inputs.zipWithIndex.map(_.map(i => BsonField.Name(i.shows)).swap)
            val srcName = BsonField.Name("src")
            op.lift(pairs.map(_._1)).fold(
              M.raiseError[(Fix[WF], Base)](UnsupportedFunction(set.Filter, Some("failed to build operation"))))(
              op => toWorkflow[M, WF].apply(DocBuilderF((g, b), pairs.toListMap + (srcName -> docVarToExpr(DocVar.ROOT())))) ∘ {
                case (graph, _) => (chain(graph, op), Field(srcName))
              })
          } (
            op.lift(_).fold(
              M.raiseError[(Fix[WF], Base)](UnsupportedFunction(set.Filter, Some("failed to build operation"))))(
              op => (chain(g, op), b).point[M]))
      }
    case ExprBuilderF((g, b), HasThat($var(d))) =>
      (g, b \ fromDocVar(d)).point[M]
    case ExprBuilderF((g, b), expr) =>
      (rewriteExprPrefix(expr, b) match {
        case HasThat(op) =>
          (chain(g, $project[WF](Reshape(ListMap(BsonField.Name("0") -> \/-(op))))),
            Field(BsonField.Name("0")))
        case \&/.This(js) =>
          (chain(g, $simpleMap[WF](NonEmptyList(MapExpr(js)), ListMap())),
            Root())
      }).point[M]
    case DocBuilderF((wf, base), shape) =>
      alignExpr(rewriteDocPrefix(shape, base)).fold(
        M.raiseError[(Fix[WF], Base)](InternalError fromMsg "Could not align the expressions"))(
        s => shape.keys.toList.toNel.fold(
          M.raiseError[(Fix[WF], Base)](InternalError fromMsg "A shape with no fields does not make sense"))(
          fields => (
            chain(wf,
              s.fold(
                jsExprs => {
                  $simpleMap[WF](NonEmptyList(
                    MapExpr(JsFn(jsBase,
                      jscore.Obj(jsExprs.map {
                        case (name, expr) => jscore.Name(name.asText) -> expr(jscore.Ident(jsBase))
                      })))),
                    ListMap()) },
                exprOps => $project[WF](Reshape(exprOps ∘ \/.right)))),
            Root(): Base).point[M]))
    case GroupBuilderF((gʹ, bʹ), keysʹ, content) =>
      val keys = keysʹ ∘ (rewriteExprPrefix(_, bʹ))
      semiAlignExpr(keys).fold {
        val ks = keys.zipWithIndex.map(_.map(i => BsonField.Name(i.shows)).swap)

        toWorkflow[M, WF].apply(DocBuilderF((gʹ, Root()), ks.toListMap + (BsonField.Name("content") -> docVarToExpr(bʹ.toDocVar)))) strengthR
          ((Field(BsonField.Name("content")): Base, ks.map(key => $var(DocField(key._1)))))
      }(
        ks => ((gʹ, bʹ), (bʹ, ks)).point[M]) >>= { case ((g, b), (c, keys)) =>

          val key: Reshape.Shape[ExprOp] =
            (keys.all {
              case $literal(_) => true
              case _           => false
            }).fold(
              \/-($literal(Bson.Null)),
              -\/(Reshape(keys.zipWithIndex.map {
                case (key, index) => BsonField.Name(index.toString) -> \/-(key)
              }.toListMap)))

          (chain(g, $group[WF](Grouped(content).rewriteRefs(prefixBase0(c)), key)),
            Root(): Base).point[M]
      }
    case FlatteningBuilderF((graph, base), fields) =>
      (
        // TODO: Organize this to put all the $maps and all the $unwinds adjacent.
        fields.foldRight(graph) {
          case (StructureType.Array(field, quasar.qscript.ExcludeId), acc) =>
            $unwind[WF](base.toDocVar \\ field).apply(acc)
          case (StructureType.Array(field, includeIndex), acc) =>
            $simpleMap[WF](
              includeIndex match {
                case quasar.qscript.ExcludeId =>
                  NonEmptyList(FlatExpr((base.toDocVar \\ field).toJs))
                case quasar.qscript.IncludeId =>
                  NonEmptyList(
                    SubExpr(
                      (base.toDocVar \\ field).toJs,
                      JsFn(jsBase,
                        jscore.Let(
                          jscore.Name("m"),
                          (base.toDocVar \\ field).toJs(jscore.Ident(jsBase)),
                          jscore.Call(
                            jscore.Select(
                              jscore.Call(
                                jscore.Select(jscore.ident("Object"), "keys"),
                                List(jscore.ident("m"))),
                              "map"),
                            List(jscore.Fun(List(jscore.Name("k")), jscore.Arr(List(
                              jscore.ident("k"),
                              jscore.Access(jscore.ident("m"), jscore.ident("k")))))))))),
                    FlatExpr((base.toDocVar \\ field).toJs))
                case quasar.qscript.IdOnly =>
                  NonEmptyList(
                    SubExpr(
                      (base.toDocVar \\ field).toJs,
                      JsFn(jsBase,
                        jscore.Call(
                          jscore.Select(jscore.ident("Object"), "keys"),
                          List((base.toDocVar \\ field).toJs(jscore.Ident(jsBase)))))),
                    FlatExpr((base.toDocVar \\ field).toJs))
              },
              ListMap()).apply(acc)
          case (StructureType.Object(field, includeKey), acc) =>
            $simpleMap[WF](
              includeKey match {
                case quasar.qscript.ExcludeId =>
                  NonEmptyList(FlatExpr((base.toDocVar \\ field).toJs))
                case quasar.qscript.IncludeId =>
                  NonEmptyList(
                    SubExpr(
                      (base.toDocVar \\ field).toJs,
                      JsFn(jsBase,
                        jscore.Let(
                          jscore.Name("m"),
                          (base.toDocVar \\ field).toJs(jscore.Ident(jsBase)),
                          jscore.Call(
                            jscore.Select(
                              jscore.Call(
                                jscore.Select(jscore.ident("Object"), "keys"),
                                List(jscore.ident("m"))),
                              "map"),
                            List(jscore.Fun(List(jscore.Name("k")), jscore.Arr(List(
                              jscore.ident("k"),
                              jscore.Access(jscore.ident("m"), jscore.ident("k")))))))))),
                    FlatExpr((base.toDocVar \\ field).toJs))
                case quasar.qscript.IdOnly =>
                  NonEmptyList(
                    SubExpr(
                      (base.toDocVar \\ field).toJs,
                      JsFn(jsBase,
                        jscore.Call(
                          jscore.Select(jscore.ident("Object"), "keys"),
                          List((base.toDocVar \\ field).toJs(jscore.Ident(jsBase)))))),
                    FlatExpr((base.toDocVar \\ field).toJs))
              },
              ListMap()).apply(acc)
        },
        base).point[M]
    case UnionBuilderF((lGraph, lBase), (rGraph, rBase)) =>
      if (lBase == rBase)
        ($foldLeft(
          lGraph,
          chain(rGraph,
            $map($MapF.mapFresh, ListMap()),
            $reduce($ReduceF.reduceNOP, ListMap()))),
          lBase).point[M]
      else
        (toWorkflow[M, WF].apply(DocBuilderF((lGraph, Root()), ListMap(
          BsonField.Name("0") -> docVarToExpr(lBase.toDocVar)))) ⊛
          toWorkflow[M, WF].apply(DocBuilderF((rGraph, Root()), ListMap(
            BsonField.Name("0") -> docVarToExpr(rBase.toDocVar)))))((l, r) =>
          ($foldLeft(
            l._1,
            chain(
              r._1,
              $map($MapF.mapFresh, ListMap()),
              $reduce($ReduceF.reduceNOP, ListMap()))),
            Field(BsonField.Name("0"))))
  }

  def generateWorkflow[M[_]: Monad, F[_]: Coalesce](wb: WorkflowBuilder[F])
    (implicit M: MonadError_[M, PlannerError], ev0: WorkflowOpCoreF :<: F, ev1: RenderTree[WorkflowBuilder[F]], ev2: ExprOpOps.Uni[ExprOp])
      : M[(Fix[F], Base)] =
    (wb: Fix[WorkflowBuilderF[F, ?]]).cataM(toWorkflow[M, F])

  def shift[F[_]: Coalesce](base: Base, struct: Schema, graph: Fix[F])
    (implicit ev: WorkflowOpCoreF :<: F)
    : (Fix[F], Base) = {
    (base, struct) match {
      case (Field(QuasarSigilName), None) =>
        (graph, Field(QuasarSigilName))

      case (_, None) =>
        (chain(graph,
          $project[F](Reshape(ListMap(QuasarSigilName -> \/-($var(base.toDocVar)))),
            ExcludeId)),
          Field(QuasarSigilName))

      case (_, Some(fields)) =>
        (chain(graph,
          $project[F](
            Reshape(fields.map(name =>
              name -> \/-($var((base \ name).toDocVar))).toList.toListMap),
            if (fields.element(IdName)) IncludeId else ExcludeId)),
          Root())
    }
  }

  def build[M[_]: Monad, F[_]: Coalesce](wb: WorkflowBuilder[F])
    (implicit M: MonadError_[M, PlannerError], ev0: WorkflowOpCoreF :<: F, ev1: RenderTree[WorkflowBuilder[F]], ev2: ExprOpOps.Uni[ExprOp])
      : M[Fix[F]] =
    (wb: Fix[WorkflowBuilderF[F, ?]]).cataM(AlgebraMZip[M, WorkflowBuilderF[F, ?]].zip(toWorkflow[M, F], schema.generalizeM[M])) ∘ {
      case ((graph, Root()), _)      => graph
      case ((graph, base),   struct) => shift(base, struct, graph)._1
    }

  def asLiteral[F[_]](wb: WorkflowBuilder[F])(implicit ev0: WorkflowOpCoreF :<: F): Option[Bson] = wb.unFix match {
    case CollectionBuilderF(Fix(ev0($PureF(value))), _, _) => value.some
    case ExprBuilderF(_, HasThat($literal(value)))         => value.some
    case _                                                 => none
  }

  /** The location of the desired content relative to the current $$ROOT.
    *
    * Various transformations (merging, conversion to Workflow, etc.) combine
    * structures that we need to be able to extract later. This tells us how to
    * extract them.
    */
  sealed abstract class Base extends Product with Serializable {
    def \ (that: Base): Base = (this, that) match {
      case (Root(),      _)            => that
      case (_,           Root())       => this
      case (Subset(_),   _)            => that // TODO: can we do better?
      case (_,           Subset(_))    => this // TODO: can we do better?
      case (Field(name), Field(name2)) => Field(name \ name2)
    }

    def \ (that: BsonField): Base = this \ Field(that)

    // NB: This is a lossy conversion.
    val toDocVar: DocVar = this match {
      case Root()      => DocVar.ROOT()
      case Field(name) => DocField(name)
      case Subset(_)   => DocVar.ROOT()
    }
  }

  object Base {
    implicit val show: Show[Base] = Show.showFromToString
  }

  /** The content is already at $$ROOT. */
  final case class Root()                              extends Base
  /** The content is nested in a field under $$ROOT. */
  final case class Field(name: BsonField)              extends Base
  /** The content is a subset of the document at $$ROOT. */
  final case class Subset(fields: Set[BsonField.Name]) extends Base

  val fromDocVar: DocVar => Base = {
    case DocVar.ROOT(None) => Root()
    case DocField(name)    => Field(name)
  }

  // TODO: Cases that match on `$$ROOT` should be generalized to look up the
  //       shape of any DocVar in the source.
  @tailrec def findKeys[F[_]](wb: WorkflowBuilder[F]): Option[Base] = {
    wb.unFix match {
      case CollectionBuilderF(_, _, s2)       => s2.map(s => Subset(s.toSet))
      case DocBuilderF(_, shape)              => Subset(shape.keySet).some
      case FlatteningBuilderF(src, _)         => findKeys(src)
      case GroupBuilderF(_, _, obj)           => Subset(obj.keySet).some
      case ShapePreservingBuilderF(src, _, _) => findKeys(src)
      case ExprBuilderF(_, _)                 => Root().some
      case _                                  => None
    }
  }

  final class Ops[F[_]: Coalesce](implicit ev0: WorkflowOpCoreF :<: F, ev1: ExprOpOps.Uni[ExprOp]) {
    def read(coll: Collection): WorkflowBuilder[F] =
      CollectionBuilder($read[F](coll), Root(), None)

    def limit(wb: WorkflowBuilder[F], count: Long): WorkflowBuilder[F] =
      ShapePreservingBuilder(wb, Nil, { case Nil => $limit[F](count) })

    def skip(wb: WorkflowBuilder[F], count: Long): WorkflowBuilder[F] =
      ShapePreservingBuilder(wb, Nil, { case Nil => $skip[F](count) })

    def filter
      (src: WorkflowBuilder[F],
        those: List[Expr],
        sel: PartialFunction[List[BsonField], Selector])
      : WorkflowBuilder[F] =
      ShapePreservingBuilder(src, those, PartialFunction(fields => $match[F](sel(fields))))

    def makeObject(wb: WorkflowBuilder[F], name: String): WorkflowBuilder[F] =
      wb.unFix match {
        case ExprBuilderF(src, expr) =>
          DocBuilder(src, ListMap(BsonField.Name(name) -> expr))
        case _ =>
          DocBuilder(wb, ListMap(BsonField.Name(name) -> \&/.Both(JsFn.identity, $$ROOT)))
      }

    private def deleteField(wb: WorkflowBuilder[F], name: String): WorkflowBuilder[F] =
      wb.unFix match {
        case DocBuilderF(wb0, doc) =>
          DocBuilder(wb0, doc - BsonField.Name(name))
        case GroupBuilderF(wb0, keys, doc) =>
          GroupBuilder(wb0, keys, doc - BsonField.Name(name))
        case ExprBuilderF(wb0, HasThis(js)) =>
          ExprBuilder(
            wb0,
            -\&/(JsFn(jsBase,
              jscore.Call(jscore.ident("remove"),
                List(js(jscore.Ident(jsBase)), jscore.Literal(Js.Str(name)))))))
        case _ =>
          ExprBuilder(
            wb,
            -\&/(JsFn(jsBase,
              jscore.Call(jscore.ident("remove"),
                List(jscore.Ident(jsBase), jscore.Literal(Js.Str(name)))))))
      }

    def groupBy(src: Fix[WorkflowBuilderF[F, ?]], keys: List[Expr], contents: GroupContents)
        : WorkflowBuilder[F] =
      keys match {
        case List(HasThat($$ROOT)) =>
          findKeys(src).fold(
            // TODO: Might not always want to delete `_id`?
            GroupBuilder(deleteField(src, "_id"), List(docVarToExpr(DocVar.ROOT())), contents)) {
            case Root()   => GroupBuilder(src, keys, contents)
            case Field(k) => GroupBuilder(src, List(docVarToExpr(DocField(k))), contents)
            case Subset(ks) =>
              GroupBuilder(
                src,
                ks.toList.map(k => docVarToExpr(DocField(k))),
                contents)
          }
        case _ => GroupBuilder(src, keys, contents)
      }

    def sortBy
      (src: WorkflowBuilder[F], keys: List[Expr], sortTypes: List[SortDir])
        : WorkflowBuilder[F] =
      ShapePreservingBuilder(
        src,
        keys,
        // FIXME: This pattern match is non total!
        // possible solution: make sortTypes and the argument to this partial function NonEmpty
        _.zip(sortTypes) match {
          case x :: xs => $sort[F](NonEmptyList.nel(x, IList.fromList(xs)))
        })
  }
  object Ops {
    implicit def apply[F[_]: Coalesce](implicit ev0: WorkflowOpCoreF :<: F, ev1: ExprOpOps.Uni[ExprOp]): Ops[F] =
      new Ops[F]
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy