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

org.yupana.externallinks.universal.SQLSourcedExternalLinkService.scala Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2019 Rusexpertiza LLC
 *
 * 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 org.yupana.externallinks.universal

import com.typesafe.scalalogging.StrictLogging
import org.yupana.api.query.Expression.Condition
import org.yupana.api.query._
import org.yupana.api.schema.{ Dimension, ExternalLink, Schema }
import org.yupana.api.types.{ BoxingTag, DataType }
import org.yupana.cache.{ Cache, CacheFactory }
import org.yupana.core.ExternalLinkService
import org.yupana.core.model.InternalRow
import org.yupana.core.utils.{ FlatAndCondition, SparseTable, Table }
import org.yupana.externallinks.ExternalLinkUtils
import org.yupana.externallinks.universal.JsonCatalogs.SQLExternalLinkDescription
import org.yupana.schema.externallinks.ExternalLinks._

import javax.sql.DataSource

class SQLSourcedExternalLinkService[DimensionValue](
    override val schema: Schema,
    override val externalLink: ExternalLink.Aux[DimensionValue],
    config: SQLExternalLinkDescription,
    dataSource: DataSource
) extends ExternalLinkService[ExternalLink]
    with StrictLogging {

  import SQLSourcedExternalLinkService._
  import config._
  import org.yupana.api.query.syntax.All._

  private val mapping = config.fieldsMapping

  private val fieldValuesForDimValuesCache: Cache[DimensionValue, Map[FieldName, FieldValue]] =
    CacheFactory.initCache[DimensionValue, Map[FieldName, FieldValue]](externalLink.linkName + "_fields")(
      externalLink.dimension.dataType.boxingTag,
      BoxingTag[Map[FieldName, FieldValue]]
    )

  override def setLinkedValues(
      exprIndex: collection.Map[Expression[_], Int],
      rows: Seq[InternalRow],
      exprs: Set[LinkExpr[_]]
  ): Unit = {
    ExternalLinkUtils.setLinkedValues(
      externalLink,
      exprIndex,
      rows,
      exprs,
      fieldValuesForDimValues
    )
  }

  override def transformCondition(condition: FlatAndCondition): Seq[ConditionTransformation] = {
    ExternalLinkUtils.transformConditionT[String](
      externalLink.linkName,
      condition,
      includeTransform,
      excludeTransform
    )
  }

  private def includeTransform(values: Seq[(SimpleCondition, String, Set[String])]): Seq[ConditionTransformation] = {
    val dimValues = dimValuesForFieldsValues(values, "AND").filter(x => x != null)
    if (externalLink.dimension.dataType == DataType[String]) {
      ConditionTransformation.replace(
        values.map(_._1).distinct,
        in(
          lower(dimension(externalLink.dimension.asInstanceOf[Dimension.Aux[String]])),
          dimValues.asInstanceOf[Set[String]]
        )
      )
    } else {
      ConditionTransformation.replace(
        values.map(_._1).distinct,
        in(dimension(externalLink.dimension.aux), dimValues)
      )
    }
  }

  private def excludeTransform(values: Seq[(SimpleCondition, String, Set[String])]): Seq[ConditionTransformation] = {
    val dimValues = dimValuesForFieldsValues(values, "OR").filter(x => x != null)
    if (externalLink.dimension.dataType == DataType[String]) {
      ConditionTransformation.replace(
        values.map(_._1).distinct,
        notIn(
          lower(dimension(externalLink.dimension.asInstanceOf[Dimension.Aux[String]])),
          dimValues.asInstanceOf[Set[String]]
        )
      )
    } else {
      ConditionTransformation.replace(
        values.map(_._1).distinct,
        notIn(dimension(externalLink.dimension.aux), dimValues)
      )
    }
  }

  private def catalogFieldToSqlField(cf: FieldName): String = mapping.flatMap(_.get(cf)).getOrElse(camelToSnake(cf))

  private def catalogFieldToSqlFieldWithAlias(cf: FieldName): String =
    s"${catalogFieldToSqlField(cf)} AS ${SQLSourcedExternalLinkService.camelToSnake(cf)}"

  def projection(fields: Set[FieldName]): String = {
    (fields + dimensionName).map(catalogFieldToSqlFieldWithAlias).mkString(", ")
  }

  def relation: String = config.relation.getOrElse(camelToSnake(linkName))

  def fieldValuesForDimValues(
      fields: Set[FieldName],
      tagValues: Set[DimensionValue]
  ): Table[DimensionValue, FieldName, FieldValue] = {

    val tableRows = fieldValuesForDimValuesCache.getAll(tagValues)

    if (tagValues.forall(tableRows.contains)) {
      SparseTable(tableRows)
    } else {
      val missedKeys = tagValues diff tableRows.keys.toSet
      val q = fieldsByTagsQuery(externalLink.fields.map(_.name), missedKeys.size)
      val params = missedKeys.map(_.asInstanceOf[Object]).toSeq
      logger.debug(s"Query for fields for catalog $linkName: $q with params: $params")
      val sqlFields = (fields + dimensionName).map(camelToSnake)
      val dataFromDb = JdbcUtils
        .runQuery(dataSource, q, sqlFields, params)
        .map(vs => vs.map { case (k, v) => snakeToCamel(k) -> v })
        .groupBy(m => m(dimensionName))
        .map {
          case (k, v) =>
            k.asInstanceOf[DimensionValue] -> (v.head - dimensionName).map { case (a, b) => a -> b.toString }
        }
      fieldValuesForDimValuesCache.putAll(dataFromDb)
      SparseTable(tableRows ++ dataFromDb)
    }
  }

  private def fieldsByTagsQuery(fields: Set[FieldName], tagValuesCount: Int): String = {
    s"""SELECT ${projection(fields)}
       |FROM $relation
       |WHERE ${catalogFieldToSqlField(dimensionName)} ${tagValueInClause(tagValuesCount)}""".stripMargin
  }

  private def tagsByFieldsQuery(
      fieldValues: Seq[(Condition, FieldName, Set[FieldValue])],
      joiningOperator: String
  ): String = {
    s"""SELECT ${catalogFieldToSqlFieldWithAlias(dimensionName)}
       |FROM $relation
       |WHERE ${fieldValuesInClauses(fieldValues).mkString(s" $joiningOperator ")}""".stripMargin
  }

  private def tagValueInClause(tagValuesCount: Int): String = {
    s"""IN (${Seq.fill(tagValuesCount)("?").mkString(", ")})"""
  }

  private def fieldValuesInClauses(fieldValues: Seq[(Condition, FieldName, Set[FieldValue])]): Seq[String] = {
    fieldValues map {
      case (_, fieldName, possibleValues) =>
        s"""lower(${catalogFieldToSqlField(fieldName)}) IN (${Seq.fill(possibleValues.size)("?").mkString(", ")})"""
    }
  }

  private def dimValuesForFieldsValues(
      fieldsValues: Seq[(Condition, FieldName, Set[FieldValue])],
      joiningOperator: String
  ): Set[DimensionValue] = {
    val q = tagsByFieldsQuery(fieldsValues, joiningOperator)
    val params = fieldsValues.flatMap(_._3).map(_.asInstanceOf[Object])
    logger.debug(s"Query for dimensions for catalog $linkName: $q with params: $params")
    JdbcUtils
      .runQuery(dataSource, q, Set(camelToSnake(dimensionName)), params)
      .flatMap(_.values.map(_.asInstanceOf[DimensionValue]))
      .toSet
  }
}

object SQLSourcedExternalLinkService {

  private val snakeParts = "_([a-z\\d])".r

  def camelToSnake(s: String): String = {
    s.drop(1).foldLeft(s.headOption.map(_.toLower.toString) getOrElse "") {
      case (acc, c) if c.isUpper => acc + "_" + c.toLower
      case (acc, c)              => acc + c
    }
  }

  def snakeToCamel(s: String): String = {
    snakeParts.replaceAllIn(s, _.group(1).toUpperCase())
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy