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

quasar.physical.mongodb.planner.JsFuncHandler.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.planner

import slamdata.Predef._
import quasar.Data
import quasar.javascript.Js
import quasar.jscore, jscore.{Name, JsCoreF}
import quasar.std.StdLib._
import quasar.qscript._, MapFuncsCore._
import quasar.std.TemporalPart._

import scala.Predef.implicitly

import matryoshka._
import matryoshka.data._
import matryoshka.implicits._
import matryoshka.patterns._
import scalaz.{Divide => _, _}, Scalaz._

trait JsFuncHandler[IN[_]] {
  def handle: IN ~> OptionFree[JsCoreF, ?]
}

object JsFuncHandler {

  implicit def mapFuncCore[T[_[_]]: BirecursiveT, J, E]
      : JsFuncHandler[MapFuncCore[T, ?]] =
    new JsFuncHandler[MapFuncCore[T, ?]] {
      def handle: MapFuncCore[T, ?] ~> OptionFree[JsCoreF, ?] =
        new (MapFuncCore[T, ?] ~> OptionFree[JsCoreF, ?]) {

          def apply[A](mfc: MapFuncCore[T, A]): OptionFree[JsCoreF, A] = {
            type JS = Free[JsCoreF, A]

            implicit def hole(a: A): JS = Free.pure(a)

            val mjs = quasar.physical.mongodb.javascript[JS](Free.roll)
            import mjs._
            import mjs.js._

            // NB: Math.trunc is not present in MongoDB.
            def trunc(expr: JS): JS =
              Let(Name("x"), expr,
                BinOp(jscore.Sub,
                  ident("x"),
                  BinOp(jscore.Mod, ident("x"), litNum(1))))

            def dateZ(year: JS, month: JS, day: JS, hr: JS, min: JS, sec: JS, ms: JS): JS =
              New(Name("Date"), List(
                Call(select(ident("Date"), "parse"), List(
                  binop(jscore.Add,
                    pad4(year), litStr("-"), pad2(month), litStr("-"), pad2(day), litStr("T"),
                    pad2(hr), litStr(":"), pad2(min), litStr(":"), pad2(sec), litStr("."),
                    pad3(ms), litStr("Z"))))))

            def year(date: JS): JS =
              Call(select(date, "getUTCFullYear"), Nil)

            def month(date: JS): JS =
              binop(jscore.Add, Call(select(date, "getUTCMonth"), Nil), litNum(1))

            def day(date: JS): JS =
              Call(select(date, "getUTCDate"), Nil)

            def hour(date: JS): JS =
              Call(select(date, "getUTCHours"), Nil)

            def minute(date: JS): JS =
              Call(select(date, "getUTCMinutes"), Nil)

            def second(date: JS): JS =
              Call(select(date, "getUTCSeconds"), Nil)

            def millisecond(date: JS): JS =
              Call(select(date, "getUTCMilliseconds"), Nil)

            def dayOfWeek(date: JS): JS =
              Call(select(date, "getUTCDay"), Nil)

            def quarter(date: JS): JS =
              BinOp(jscore.Add,
                Call(select(ident("Math"), "floor"), List(
                  BinOp(jscore.Div, Call(select(date, "getUTCMonth"), Nil), litNum(3)))),
                litNum(1))

            def decade(date: JS): JS =
              trunc(BinOp(jscore.Div, year(date), litNum(10)))

            def century(date: JS): JS =
              Call(select(ident("Math"), "ceil"), List(
                BinOp(jscore.Div, year(date), litNum(100))))

            def millennium(date: JS): JS =
              Call(select(ident("Math"), "ceil"), List(
                BinOp(jscore.Div, year(date), litNum(1000))))

            def litStr(s: String): JS =
              Literal(Js.Str(s))

            def litNum(i: Int): JS =
              Literal(Js.Num(i.toDouble, false))

            def pad2(x: JS) =
              Let(Name("x"), x,
                If(
                  BinOp(jscore.Lt, ident("x"), litNum(10)),
                  BinOp(jscore.Add, litStr("0"), ident("x")),
                  ident("x")))

            def pad3(x: JS) =
              Let(Name("x"), x,
                If(
                  BinOp(jscore.Lt, ident("x"), litNum(10)),
                  BinOp(jscore.Add, litStr("00"), ident("x")),
                  If(
                    BinOp(jscore.Lt, ident("x"), litNum(100)),
                    BinOp(jscore.Add, litStr("0"), ident("x")),
                    ident("x"))))

            def pad4(x: JS) =
              Let(Name("x"), x,
                If(
                  BinOp(jscore.Lt, ident("x"), litNum(10)),
                  BinOp(jscore.Add, litStr("000"), ident("x")),
                  If(
                    BinOp(jscore.Lt, ident("x"), litNum(100)),
                    BinOp(jscore.Add, litStr("00"), ident("x")),
                    If(
                      BinOp(jscore.Lt, ident("x"), litNum(1000)),
                      BinOp(jscore.Add, litStr("0"), ident("x")),
                      ident("x")))))

            def partial(mfc: MapFuncCore[T, A]): OptionFree[JsCoreF, A] = mfc.some collect {
              case Add(a1, a2)      => BinOp(jscore.Add, a1, a2)
              case Multiply(a1, a2) => BinOp(jscore.Mult, a1, a2)
              case Power(a1, a2) =>
                Call(select(ident("Math"), "pow"), List(a1, a2))
              case Subtract(a1, a2) => BinOp(jscore.Sub, a1, a2)
              case Divide(a1, a2)   => BinOp(jscore.Div, a1, a2)
              case Modulo(a1, a2)   => BinOp(jscore.Mod, a1, a2)
              case Negate(a1)       => UnOp(jscore.Neg, a1)
              case Undefined()      => ident("undefined")

              case MapFuncsCore.Eq(a1, a2)  => BinOp(jscore.Eq, a1, a2)
              case Neq(a1, a2) => BinOp(jscore.Neq, a1, a2)
              case Lt(a1, a2)  => BinOp(jscore.Lt, a1, a2)
              case Lte(a1, a2) => BinOp(jscore.Lte, a1, a2)
              case Gt(a1, a2)  => BinOp(jscore.Gt, a1, a2)
              case Gte(a1, a2) => BinOp(jscore.Gte, a1, a2)
              case Not(a1)     => UnOp(jscore.Not, a1)
              case And(a1, a2) => BinOp(jscore.And, a1, a2)
              case Or(a1, a2)  => BinOp(jscore.Or, a1, a2)
              case Between(value, min, max) =>
                  BinOp(jscore.And,
                    BinOp(jscore.Lte, min, value),
                    BinOp(jscore.Lte, value, max))

              case MakeArray(a1) => Arr(List(a1))
              case Length(str) =>
                Call(ident("NumberLong"), List(select(hole(str), "length")))
              case Substring(field, start, len) =>
                If(BinOp(jscore.Lt, start, litNum(0)),
                  litStr(""),
                  If(BinOp(jscore.Lt, len, litNum(0)),
                    Call(select(field, "substr"), List(start, select(field, "length"))),
                    Call(select(field, "substr"), List(start, len))))
              case MapFuncsCore.Split(a1, a2) =>
                Call(select(a1, "split"), List(a2))
              case Lower(a1) => Call(select(a1, "toLowerCase"), Nil)
              case Upper(a1) => Call(select(a1, "toUpperCase"), Nil)
              case Search(field, pattern, insen) =>
                  Call(
                    select(
                      New(Name("RegExp"), List(
                        pattern,
                        If(insen, litStr("im"), litStr("m")))),
                      "test"),
                    List(field))
              case Within(a1, a2) =>
                BinOp(jscore.Neq,
                  Literal(Js.Num(-1, false)),
                  Call(select(a2, "indexOf"), List(a1)))
              case Null(str) =>
                If(
                  BinOp(jscore.Eq, str, litStr("null")),
                  Literal(Js.Null),
                  ident("undefined"))
              case Bool(str) =>
                If(
                  BinOp(jscore.Eq, str, litStr("true")),
                  Literal(Js.Bool(true)),
                  If(
                    BinOp(jscore.Eq, str, litStr("false")),
                    Literal(Js.Bool(false)),
                    ident("undefined")))
              case Integer(str) =>
                If(Call(select(Call(ident("RegExp"), List(litStr("^" + string.intRegex + "$"))), "test"), List(str)),
                  Call(ident("NumberLong"), List(str)),
                  ident("undefined"))
              case Decimal(str) =>
                  If(Call(select(Call(ident("RegExp"), List(litStr("^" + string.floatRegex + "$"))), "test"), List(str)),
                    Call(ident("parseFloat"), List(str)),
                    ident("undefined"))
              case Date(str) =>
                If(Call(select(Call(ident("RegExp"), List(litStr("^" + string.dateRegex + "$"))), "test"), List(str)),
                  Call(ident("ISODate"), List(str)),
                  ident("undefined"))
              case Time(str) =>
                If(Call(select(Call(ident("RegExp"), List(litStr("^" + string.timeRegex + "$"))), "test"), List(str)),
                  str,
                  ident("undefined"))
              case Timestamp(str) =>
                If(Call(select(Call(ident("RegExp"), List(litStr("^" + string.timestampRegex + "$"))), "test"), List(str)),
                  Call(ident("ISODate"), List(str)),
                  ident("undefined"))
              // TODO: case Interval(str) =>

              case ToString(value) =>
                If(isInt(value),
                  // NB: This is a terrible way to turn an int into a string, but the
                  //     only one that doesn’t involve converting to a decimal and
                  //     losing precision.
                  Call(select(Call(ident("String"), List(value)), "replace"), List(
                    Call(ident("RegExp"), List(
                      litStr("[^-0-9]+"),
                      litStr("g"))),
                    litStr(""))),
                  If(isObjectId(value),
                    Call(select(value, "toString"), Nil),
                    (If(binop(jscore.Or, isTimestamp(value), isDate(value)),
                      Call(select(value, "toISOString"), Nil),
                      Call(ident("String"), List(value))))))

              case ToTimestamp(a1) => New(Name("Date"), List(a1))
              case TimeOfDay(date) =>
                Let(Name("t"), date,
                  binop(jscore.Add,
                    pad2(hour(ident("t"))),
                    litStr(":"),
                    pad2(minute(ident("t"))),
                    litStr(":"),
                    pad2(second(ident("t"))),
                    litStr("."),
                    pad3(millisecond(ident("t")))))

              case ExtractCentury(date) => century(date)
              case ExtractDayOfMonth(date) => day(date)
              case ExtractDecade(date) => decade(date)
              case ExtractDayOfWeek(date) => dayOfWeek(date)
              // TODO: case ExtractDayOfYear(date) =>
              case ExtractEpoch(date) =>
                BinOp(jscore.Div,
                  Call(select(date, "valueOf"), Nil),
                  litNum(1000))
              case ExtractHour(date) => hour(date)
              case ExtractIsoDayOfWeek(date) =>
                Let(Name("x"), dayOfWeek(date),
                  If(
                    BinOp(jscore.Eq, ident("x"), litNum(0)),
                    litNum(7),
                    ident("x")))
              // TODO: case ExtractIsoYear(date) =>
              case ExtractMicroseconds(date) =>
                BinOp(jscore.Mult,
                  BinOp(jscore.Add,
                    millisecond(date),
                    BinOp(jscore.Mult,
                      second(date),
                      litNum(1000))),
                  litNum(1000))
              case ExtractMillennium(date) => millennium(date)
              case ExtractMilliseconds(date) =>
                BinOp(jscore.Add,
                  millisecond(date),
                  BinOp(jscore.Mult,
                    second(date),
                    litNum(1000)))
              case ExtractMinute(date) => minute(date)
              case ExtractMonth(date) => month(date)
              case ExtractQuarter(date) => quarter(date)
              case ExtractSecond(date) =>
                BinOp(jscore.Add,
                  second(date),
                  BinOp(jscore.Div, millisecond(date), litNum(1000)))
              // TODO: case ExtractWeek(date) =>
              case ExtractYear(date) => year(date)

              case StartOfDay(date) =>
                dateZ(year(date), month(date), day(date), litNum(0), litNum(0), litNum(0), litNum(0))

              case TemporalTrunc(Century, date) =>
                val yr =
                  Call(select(ident("Math"), "floor"), List(BinOp(jscore.Div, year(date), litNum(100))))
                dateZ(
                  BinOp(jscore.Mult, yr, litNum(100)),
                  litNum(1), litNum(1), litNum(0), litNum(0), litNum(0), litNum(0))
              case TemporalTrunc(Day, date) =>
                dateZ(year(date), month(date), day(date), litNum(0), litNum(0), litNum(0), litNum(0))
              case TemporalTrunc(Decade, date) =>
                dateZ(
                  BinOp(jscore.Mult, decade(date), litNum(10)),
                  litNum(1), litNum(1), litNum(0), litNum(0), litNum(0), litNum(0))
              case TemporalTrunc(Hour, date) =>
                dateZ(year(date), month(date), day(date), hour(date), litNum(0), litNum(0), litNum(0))
              case TemporalTrunc(Millennium, date) =>
                dateZ(
                  BinOp(jscore.Mult,
                    Call(select(ident("Math"), "floor"), List(
                      BinOp(jscore.Div, year(date), litNum(1000)))),
                    litNum(1000)),
                  litNum(1), litNum(1), litNum(0), litNum(0), litNum(0), litNum(0))
              case TemporalTrunc(Microsecond | Millisecond, date) =>
                dateZ(
                  year(date), month(date), day(date),
                  hour(date), minute(date), second(date), millisecond(date))
              case TemporalTrunc(Minute, date) =>
                dateZ(year(date), month(date), day(date), hour(date), minute(date), litNum(0), litNum(0))
              case TemporalTrunc(Month, date) =>
                dateZ(year(date), month(date), litNum(1), litNum(0), litNum(0), litNum(0), litNum(0))
              case TemporalTrunc(Quarter, date) =>
                dateZ(
                  year(date),
                  BinOp(jscore.Add,
                    BinOp(jscore.Mult,
                      BinOp(jscore.Sub, quarter(date), litNum(1)),
                      litNum(3)),
                    litNum(1)),
                  litNum(1), litNum(0), litNum(0), litNum(0), litNum(0))
              case TemporalTrunc(Second, date) =>
                dateZ(year(date), month(date), day(date), hour(date), minute(date), second(date), litNum(0))
              case TemporalTrunc(Week, date) =>
                val d =
                  New(Name("Date"), List(
                    BinOp(jscore.Sub,
                      Call(select(date, "getTime"), Nil),
                      BinOp(jscore.Mult,
                        litNum(24*60*60*1000),
                        BinOp(jscore.Mod,
                          BinOp(jscore.Add, dayOfWeek(date), litNum(6)),
                          litNum(7))
                    ))))
                Let(Name("d"), d,
                  dateZ(year(d), month(d), day(d), litNum(0), litNum(0), litNum(0), litNum(0)))
              case TemporalTrunc(Year, date) =>
                dateZ(year(date), litNum(1), litNum(1), litNum(0), litNum(0), litNum(0), litNum(0))

              case ProjectKey(obj, key) => Access(obj, key)
              case ProjectIndex(arr, index) => Access(arr, index)
              case DeleteKey(a1, a2) => Call(ident("remove"), List(a1, a2))

              // TODO: This doesn't return the right values most of the time.
              case TypeOf(v) =>
                Let(Name("typ"), UnOp(jscore.TypeOf, v),
                  If(BinOp(jscore.Eq, ident("typ"), Literal(Js.Str("object"))),
                    If(BinOp(jscore.Eq, v, Literal(Js.Null)),
                      Literal(Js.Str("null")),
                      If(Call(select(ident("Array"), "isArray"), List(v)),
                        Literal(Js.Str("array")),
                        Literal(Js.Str("map")))),
                    If(BinOp(jscore.Eq, ident("typ"), Literal(Js.Str("string"))),
                      Literal(Js.Str("array")),
                      ident("typ"))))

              case IfUndefined(a1, a2) =>
                // TODO: Only evaluate `value` once.
                If(BinOp(jscore.Eq, a1, ident("undefined")), a2, a1)

              case ToId(a1) => New(Name("ObjectId"), List(a1))
              case Cond(i, t, e) => If(i, t, e)

              // FIXME: Doesn't work for Char.
              case Range(start, end) =>
                Call(
                  select(
                    Call(select(ident("Array"), "apply"), List(
                      Literal(Js.Null),
                      Call(ident("Array"), List(BinOp(jscore.Sub, end, start))))),
                    "map"),
                  List(
                    Fun(List(Name("element"), Name("index")),
                      BinOp(jscore.Add, ident("index"), start))))

            }

            partial(mfc) orElse (mfc match {
              case Constant(v1)     =>
                v1.cata(Data.fromEJson).toJs.map(_.transCata[Free[JsCoreF, A]](js => CoEnv(js.right[A])))
              case _                => None
            })
          }
        }
    }

    def mapFuncDerived[T[_[_]]]
        : JsFuncHandler[MapFuncDerived[T, ?]] =
      new JsFuncHandler[MapFuncDerived[T, ?]] {

        def handle: MapFuncDerived[T, ?] ~> OptionFree[JsCoreF, ?] =
          new (MapFuncDerived[T, ?] ~> OptionFree[JsCoreF, ?]) {

            def apply[A](mfc: MapFuncDerived[T, A]): OptionFree[JsCoreF, A] = None
          }
      }

  implicit def mapFuncDerivedUnhandled[T[_[_]]: CorecursiveT]
    (implicit core: JsFuncHandler[MapFuncCore[T, ?]])
      : JsFuncHandler[MapFuncDerived[T, ?]] =
    new JsFuncHandler[MapFuncDerived[T, ?]] {
      val derived: JsFuncHandler[MapFuncDerived[T, ?]] = mapFuncDerived

      private def handleUnhandled[F[_]]
        (derived: MapFuncDerived[T, ?] ~> OptionFree[F, ?], core: MapFuncCore[T, ?] ~> OptionFree[F, ?])
          : MapFuncDerived[T, ?] ~> OptionFree[F, ?] =
        new (MapFuncDerived[T, ?] ~> OptionFree[F, ?]) {
          def apply[A](f: MapFuncDerived[T, A]): OptionFree[F, A] = {
            val alg: AlgebraM[Option, CoEnv[A, MapFuncCore[T,?], ?], Free[F,A]] =
              _.run.fold[OptionFree[F, A]](x => Free.point(x).some, core(_).map(_.join))
            derived(f)
              .orElse(Free.roll(ExpandMapFunc.mapFuncDerived[T, MapFuncCore[T, ?]].expand(f)).cataM(alg))
          }
        }

      def handle: MapFuncDerived[T, ?] ~> OptionFree[JsCoreF, ?] =
        handleUnhandled(derived.handle, core.handle)
    }

  implicit def mapFuncCoproduct[F[_], G[_]]
      (implicit F: JsFuncHandler[F], G: JsFuncHandler[G])
      : JsFuncHandler[Coproduct[F, G, ?]] =
    new JsFuncHandler[Coproduct[F, G, ?]] {
      def handle: Coproduct[F, G, ?] ~> OptionFree[JsCoreF, ?] =
        λ[Coproduct[F, G, ?] ~> OptionFree[JsCoreF, ?]](_.run.fold(
          F.handle.apply, G.handle.apply
        ))
    }

  def handle[F[_]: JsFuncHandler]: F ~> OptionFree[JsCoreF, ?] =
    implicitly[JsFuncHandler[F]].handle
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy