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

io.waylay.influxdb.query.InfluxQueryBuilder.scala Maven / Gradle / Ivy

The newest version!
package io.waylay.influxdb.query

import io.waylay.influxdb.Influx._
import io.waylay.influxdb.InfluxDB._
import io.waylay.influxdb.query.InfluxQueryBuilder.Order.{Ascending, Descending}
import io.waylay.influxdb.{InfluxDB, SharedProtocol}

import java.time.Instant

object InfluxQueryBuilder extends SharedProtocol {

  def simple(
    fields: Seq[String],
    tagSelector: (String, String),
    measurement: String,
    interval: Interval = Interval.beginningOfTimeUntilNow,
    order: Order = Order.defaultOrder,
    limit: Option[Long] = None
  ): String =
    simpleMultipleMeasurements(fields, tagSelector, Seq(measurement), interval, order, limit)

  def simpleMultipleMeasurements(
    fields: Seq[String],
    tagSelector: (String, String),
    measurements: Seq[String],
    interval: Interval = Interval.beginningOfTimeUntilNow,
    order: Order = Order.defaultOrder,
    limit: Option[Long] = None
  ): String = {
    val selects            = fields.map(escapeValue).mkString(", ")
    val measurementsString = measurements.map(escapeValue).mkString(", ")
    val timeWhere          = instantToWhereExpression(interval)

    s"""
       |SELECT $selects
       |FROM $measurementsString
       |WHERE ${escapeValue(tagSelector._1)}=${escapeStringLiteral(tagSelector._2)}
       |${timeWhere.map("AND " + _).getOrElse("")}
       |${toOrderClause(order)}
       |${limit.fold("")(l => s"LIMIT $l")}
       |""".stripMargin.trim.linesIterator.filterNot(_.trim.isEmpty).mkString("\n")
  }

  def grouped(
    func: IFunction,
    tagSelector: (String, String),
    measurement: String,
    grouping: Duration,
    interval: Interval = Interval.beginningOfTimeUntilNow,
    filter: Option[IFilter] = None,
    limit: Option[Long] = None
  ): String = groupedMultiple(Seq(func), tagSelector, measurement, grouping, interval, filter, limit)

  /**
   * Variation of grouped that can take multiple IFunctions
   */
  def groupedMultiple(
    funcs: Seq[IFunction],
    tagSelector: (String, String),
    measurement: String,
    grouping: Duration,
    interval: Interval = Interval.beginningOfTimeUntilNow,
    filter: Option[IFilter] = None,
    limit: Option[Long] = None
  ): String = {

    if (funcs.isEmpty) {
      throw new IllegalArgumentException("At least one function must be supplied")
    }

    val timeWhere        = instantToWhereExpression(interval)
    val fieldFilterWhere = filter.map(filterToWhereExpressions)
    // TODO validate that timeWhere is not None?

    s"""
       |SELECT ${funcs.map(functionToSelect).mkString(", ")}
       |FROM ${escapeValue(measurement)}
       |WHERE ${escapeValue(tagSelector._1)}=${escapeStringLiteral(tagSelector._2)}
       |${timeWhere.map("AND " + _).getOrElse("")}
       |${fieldFilterWhere.map("AND (" + _ + ")").getOrElse("")}
       |GROUP BY time(${durationLiteral(grouping)})
       |${limit.map(l => "\nLIMIT " + l).getOrElse("")}
       |""".stripMargin.trim.linesIterator.filterNot(_.trim.isEmpty).mkString("\n")
    // WHERE time > 1434059627s
    // WHERE time > '2013-08-12 23:32:01.232' AND time < '2013-08-13';
    // WHERE time > now() - 1h
    // limit 1000;
  }

  def dropSeries(tagSelector: (String, String)): String =
    s"""
       |DROP SERIES
       |WHERE ${escapeValue(tagSelector._1)}=${escapeStringLiteral(tagSelector._2)}
       |""".stripMargin.trim

  def deleteSeries(tagSelector: (String, String), from: Option[Instant], until: Option[Instant]): String =
    s"""
       |DELETE
       |WHERE ${escapeValue(tagSelector._1)}=${escapeStringLiteral(tagSelector._2)}
       |""".stripMargin.trim + from.fold("")(i => s" AND time >= ${i.toEpochMilli}ms") + until.fold("")(i =>
      s" AND time <= ${i.toEpochMilli}ms"
    )

  def deleteSeriesPredicate(tagSelector: (String, String)): String =
    s"""
       |${escapeValue(tagSelector._1)}=${escapeValue(tagSelector._2)}
       |""".stripMargin.trim

  /**
   * String Literals (Single-quoted)
   * String literals are values (like integers or booleans). In InfluxQL, all tag values are string literals, and any
   * field values that are not integers, floats, or booleans are also strings. String literals are also used when
   * checking equality with identifiers.
   * String Literal Quoting Requirements
   * String literals must always be single-quoted ('). String literals may contain any unicode characters except
   * for single quotes, new lines and backslashes, which must be backslash (\) escaped.
   *
   * @param tag the tag to escape
   * @return the escaped tag
   */
  private def escapeStringLiteral(tag: String) =
    "'" + tag.replace("'", "\'") + "'"

  def metricNames(database: String, tagSelector: (String, String)) =
    s"""SHOW TAG KEYS ON "$database" WHERE ${tagSelector._1} = '${tagSelector._2}'"""

  private def toOrderClause(order: Order) = {
    val orderDirective = order match {
      case Ascending  => "ASC"
      case Descending => "DESC"
    }
    s"ORDER BY time $orderDirective"
  }

  import InfluxDB._

  private def instantToExpression(instant: IInstant): String = instant match {
    case Now                          => "now()"
    case Exact(inst)                  => inst.toEpochMilli.toString + "ms" // TODO do we want to go more precise?
    case RelativeTo(to, timeToGoBack) => instantToExpression(to) + " - " + durationLiteral(timeToGoBack)
  }

  private def functionToSelect(iFunction: IFunction): String = iFunction match {
    case Count(Left(field)) => s"""COUNT(${escapeValue(field)})"""
    case Count(Right(func)) => s"""COUNT(${functionToSelect(func)})"""
    case Min(field)         => s"""MIN(${escapeValue(field)})"""
    case Max(field)         => s"""MAX(${escapeValue(field)})"""
    case Mean(field)        => s"""MEAN(${escapeValue(field)})"""
    case Median(field)      => s"""MEDIAN(${escapeValue(field)})"""
    case Distinct(field)    => s"""DISTINCT(${escapeValue(field)})"""
    case Sum(field)         => s"""SUM(${escapeValue(field)})"""
    case Stddev(field)      => s"""STDDEV(${escapeValue(field)})"""
    case Last(field)        => s"""LAST(${escapeValue(field)})"""
    case First(field)       => s"""FIRST(${escapeValue(field)})"""
    case other              => throw new RuntimeException("not implemented: " + other.toString)
  }

  private def instantToWhereExpression(interval: Interval): Option[String] =
    interval match {
      case Interval(None, None)        => None
      case Interval(Some(start), None) => Some("time >= " + instantToExpression(start))
      case Interval(None, Some(end))   => Some("time < " + instantToExpression(end))
      case Interval(Some(start), Some(end)) =>
        Some("time >= " + instantToExpression(start) + " AND time < " + instantToExpression(end))
    }

  private def operatorString(op: IFieldFilterOperation): String = op match {
    case InfluxDB.EQ  => "="
    case InfluxDB.NE  => "<>"
    case InfluxDB.LT  => "<"
    case InfluxDB.LTE => "<="
    case InfluxDB.GT  => ">"
    case InfluxDB.GTE => ">="
  }

  private def escapedFieldValue(value: IFieldValue): String = value match {
    case IString(stringValue) => escapeStringLiteral(stringValue)
    case IBoolean(bool)       => bool.toString
    case IInteger(intVal)     => intVal.toString
    case IFloat(doubleVal)    => doubleVal.toString

  }

  private def fieldFilterToWhereExpression(filter: IFieldFilter) =
    s"${escapeValue(filter.field_key)} ${operatorString(filter.operator)} ${escapedFieldValue(filter.value)}"

  private def filterToWhereExpressions(filter: IFilter): String = filter match {
    case f: IFieldFilter =>
      fieldFilterToWhereExpression(f)
    case AND(f1, f2, others @ _*) =>
      (Seq(f1, f2) ++ others).map(f => s"(${filterToWhereExpressions(f)})").mkString(" AND ")
    case OR(f1, f2, others @ _*) =>
      (Seq(f1, f2) ++ others).map(f => s"(${filterToWhereExpressions(f)})").mkString(" OR ")
    case NOT(filter) =>
      s"NOT (${filterToWhereExpressions(filter)})"
  }

  sealed trait IInstant

  sealed trait Order

  case class Exact(instant: Instant) extends IInstant

  case class RelativeTo(to: IInstant = Now, timeToGoBack: Duration) extends IInstant

  case class Interval(start: Option[IInstant], end: Option[IInstant]) {
    def toMillis: Long = this match {
      case Interval(Some(Exact(from)), Some(Exact(until))) => until.toEpochMilli - from.toEpochMilli
      case other                                           => 0
    }
  }

  object Now extends IInstant

  object Order {
    val defaultOrder: Order = Ascending

    case object Ascending extends Order

    case object Descending extends Order
  }

  /**
   * If start and end times aren’t set they will default to beginning of time until now, respectively.
   */
  object Interval {
    val beginningOfTimeUntilNow: Interval = Interval(None, None)

    def fromJava(start: Option[java.time.Instant], end: Option[java.time.Instant]): Interval =
      Interval(start.map(Exact), end.map(Exact))

    def from(start: Instant): Interval                    = Interval(Some(Exact(start)), None)
    def fromUntil(start: Instant, end: Instant): Interval = Interval(Some(Exact(start)), Some(Exact(end)))
    def relativeToNow(timeToGoBack: Duration): Interval   = Interval(Some(RelativeTo(Now, timeToGoBack)), None)
    def relativeTo(timeToGoBack: Duration, to: Instant): Interval =
      Interval(Some(RelativeTo(Exact(to), timeToGoBack)), None)
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy