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

com.outworkers.phantom.DefaultImports.scala Maven / Gradle / Ivy

/*
 * Copyright 2013 - 2020 Outworkers Ltd.
 *
 * 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 com.outworkers.phantom

import java.util.Random

import com.datastax.driver.core.utils.UUIDs
import com.datastax.driver.core.{VersionNumber, ConsistencyLevel => CLevel}
import com.outworkers.phantom
import com.outworkers.phantom.builder.QueryBuilder
import com.outworkers.phantom.builder.batch.Batcher
import com.outworkers.phantom.builder.clauses.{UpdateClause, UsingClauseOperations, WhereClause}
import com.outworkers.phantom.builder.ops._
import com.outworkers.phantom.builder.query.engine.CQLQuery
import com.outworkers.phantom.builder.query.prepared.PrepareMark
import com.outworkers.phantom.builder.query.sasi.{DefaultSASIOps, Mode}
import com.outworkers.phantom.builder.query.{CreateImplicits, DeleteImplicits, SelectImplicits}
import com.outworkers.phantom.builder.serializers.{KeySpaceConstruction, RootSerializer}
import com.outworkers.phantom.builder.syntax.CQLSyntax
import com.outworkers.phantom.column._
import com.outworkers.phantom.connectors.DefaultVersions
import com.outworkers.phantom.keys.Indexed
import org.joda.time.DateTimeZone
import shapeless.{ HNil, :: }

import scala.concurrent.ExecutionContextExecutor
import scala.Iterable

trait DefaultImports extends ImplicitMechanism
  with CreateImplicits
  with SelectImplicits
  with Operators
  with UsingClauseOperations
  with KeySpaceConstruction
  with DeleteImplicits
  with DefaultSASIOps {

  type CassandraTable[Owner <: CassandraTable[Owner, Record], Record] = phantom.CassandraTable[Owner, Record]
  type Table[Owner <: Table[Owner, Record], Record] = phantom.Table[Owner, Record]

  type ClusteringOrder = com.outworkers.phantom.keys.ClusteringOrder
  type Ascending = com.outworkers.phantom.keys.Ascending
  type Descending = com.outworkers.phantom.keys.Descending
  type PartitionKey = com.outworkers.phantom.keys.PartitionKey
  type PrimaryKey = com.outworkers.phantom.keys.PrimaryKey
  type Index = com.outworkers.phantom.keys.Index
  type Keys = com.outworkers.phantom.keys.Keys
  type Entries = com.outworkers.phantom.keys.Entries
  type StaticColumn = com.outworkers.phantom.keys.StaticColumn

  type Database[DB <: Database[DB]] = com.outworkers.phantom.database.Database[DB]
  type DatabaseProvider[DB <: Database[DB]] = com.outworkers.phantom.database.DatabaseProvider[DB]

  type DateTime = org.joda.time.DateTime
  type LocalDate = org.joda.time.LocalDate
  type UUID = java.util.UUID
  type Row = com.outworkers.phantom.Row
  type ResultSet = com.outworkers.phantom.ResultSet
  type Session = com.datastax.driver.core.Session
  type KeySpace = com.outworkers.phantom.connectors.KeySpace
  val KeySpace = com.outworkers.phantom.connectors.KeySpace
  type CassandraConnection = com.outworkers.phantom.connectors.CassandraConnection
  type RootConnector = com.outworkers.phantom.connectors.RootConnector
  val Analyzer = com.outworkers.phantom.builder.query.sasi.Analyzer
  val Mode = com.outworkers.phantom.builder.query.sasi.Mode
  type Analyzer[M <: Mode] = com.outworkers.phantom.builder.query.sasi.Analyzer[M]
  type SASIIndex[M <: Mode] = com.outworkers.phantom.keys.SASIIndex[M]
  type CustomIndex[M <: Mode] = SASIIndex[M]

  type StandardAnalyzer[M <: Mode] = com.outworkers.phantom.builder.query.sasi.Analyzer.StandardAnalyzer[M]
  type NonTokenizingAnalyzer[M <: Mode] = com.outworkers.phantom.builder.query.sasi.Analyzer.NonTokenizingAnalyzer[M]

  val Version = DefaultVersions

  type ListResult[R] = com.outworkers.phantom.builder.query.execution.ListResult[R]
  type IteratorResult[R] = com.outworkers.phantom.builder.query.execution.IteratorResult[R]
  type RecordResult[R] = com.outworkers.phantom.builder.query.execution.RecordResult[R]

  type Primitive[RR] = com.outworkers.phantom.builder.primitives.Primitive[RR]
  val Primitive = com.outworkers.phantom.builder.primitives.Primitive

  object ? extends PrepareMark
  case object Batch extends Batcher

  object ConsistencyLevel {
    val ALL = CLevel.ALL
    val Any = CLevel.ANY
    val ONE = CLevel.ONE
    val TWO = CLevel.TWO
    val THREE = CLevel.THREE
    val QUORUM = CLevel.QUORUM
    val LOCAL_QUORUM = CLevel.LOCAL_QUORUM
    val EACH_QUORUM = CLevel.EACH_QUORUM
    val LOCAL_SERIAL = CLevel.LOCAL_SERIAL
    val LOCAL_ONE = CLevel.LOCAL_ONE
    val SERIAL = CLevel.SERIAL
  }


  type KeySpaceDef = com.outworkers.phantom.connectors.CassandraConnection
  val ContactPoint = com.outworkers.phantom.connectors.ContactPoint
  val ContactPoints = com.outworkers.phantom.connectors.ContactPoints

  /**
    * Used as a secondary option when creating a [[ContactPoint]] to allow users to provide
    * a single [[KeySpace]] derived query. When users want to provide
    * a single argument to the [[ContactPoint#keySpace]] method, they can use
    * the following syntax to generate a full keyspace initialisation query.
    * The KeySpace will implicitly convert to a [[RootSerializer]].
    *
    * {{{
    *   KeySpace("test").ifNotExists
    * }}}
    */
  implicit def keyspaceToKeyspaceQuery(k: KeySpace): RootSerializer = new RootSerializer(k)

  implicit class SelectColumnRequired[
    Owner <: CassandraTable[Owner, Record],
    Record, T
  ](col: Column[Owner, Record, T]) extends SelectColumn[T](col) {
    def apply(r: Row): T = col(r)
  }

  implicit class SelectColumnOptional[
    Owner <: CassandraTable[Owner, Record],
    Record, T
  ](col: OptionalColumn[Owner, Record, T]) extends SelectColumn[Option[T]](col) {
    def apply(r: Row): Option[T] = col(r)
  }

  implicit class RichNumber(val percent: Int) {
    def percentile: CQLQuery = CQLQuery(percent.toString)
      .pad.append(CQLSyntax.CreateOptions.percentile)
  }

  implicit class PartitionTokenHelper[T](val col: AbstractColumn[T] with PartitionKey) {

    def ltToken(value: T): WhereClause.Condition = {
      new WhereClause.Condition(
        QueryBuilder.Where.lt(
          QueryBuilder.Where.token(col.name).queryString,
          QueryBuilder.Where.fcall(CQLSyntax.token, col.asCql(value)).queryString
        )
      )
    }

    def lteToken(value: T): WhereClause.Condition = {
      new WhereClause.Condition(
        QueryBuilder.Where.lte(
          QueryBuilder.Where.token(col.name).queryString,
          QueryBuilder.Where.fcall(CQLSyntax.token, col.asCql(value)).queryString
        )
      )
    }

    def gtToken(value: T): WhereClause.Condition = {
      new WhereClause.Condition(
        QueryBuilder.Where.gt(
          QueryBuilder.Where.token(col.name).queryString,
          QueryBuilder.Where.fcall(CQLSyntax.token, col.asCql(value)).queryString
        )
      )
    }

    def gteToken(value: T): WhereClause.Condition = {
      new WhereClause.Condition(
        QueryBuilder.Where.gte(
          QueryBuilder.Where.token(col.name).queryString,
          QueryBuilder.Where.fcall(CQLSyntax.token, col.asCql(value)).queryString
        )
      )
    }

    def eqsToken(value: T): WhereClause.Condition = {
      new WhereClause.Condition(
        QueryBuilder.Where.eqs(
          QueryBuilder.Where.token(col.name).queryString,
          QueryBuilder.Where.fcall(CQLSyntax.token, col.asCql(value)).queryString
        )
      )
    }
  }

  /**
    * Used when creating a [[ContactPoint]] to allow users to provide
    * a single [[KeySpace]] derived query. When users want to provide
    * a single argument to the [[ContactPoint#keySpace]] method, they can use
    * the following syntax to generate a full keyspace initialisation query.
    *
    * {{{
    *   KeySpace("test").builder.ifNotExists
    * }}}
    */
  implicit class KeySpaceAugmenter(val k: KeySpace) {
    def builder: RootSerializer = new RootSerializer(k)
  }

  implicit class CounterOperations[
    Owner <: CassandraTable[Owner, Record],
    Record
  ](val col: CounterColumn[Owner, Record]) {
    final def +=[T : Numeric](value: T): UpdateClause.Default = {
      new UpdateClause.Condition(QueryBuilder.Update.increment(col.name, value.toString))
    }

    final def +=(value: PrepareMark): UpdateClause.Prepared[Long] = {
      increment(value)
    }

    final def increment[T : Numeric](value: T): UpdateClause.Default = +=(value)

    final def increment(value: PrepareMark): UpdateClause.Prepared[Long] = {
      new UpdateClause.Prepared[Long](
        QueryBuilder.Update.increment(col.name, value.qb.queryString)
      )
    }

    final def -=[T : Numeric](value: T): UpdateClause.Default = {
      new UpdateClause.Condition(QueryBuilder.Update.decrement(col.name, value.toString))
    }

    final def -=(value: PrepareMark): UpdateClause.Prepared[Long] = {
      decrement(value)
    }

    final def decrement[T : Numeric](value: T): UpdateClause.Default = -=(value)

    final def decrement(value: PrepareMark): UpdateClause.Prepared[Long] = {
      new UpdateClause.Prepared[Long](
        QueryBuilder.Update.decrement(col.name, value.qb.queryString)
      )
    }
  }

  /**
    * Augments Cassandra VersionNumber descriptors to support simple comparison of versions.
    * This allows for operations that can differ based on the Cassandra version used by the session.
    *
    * @param version The Cassandra version number.
    */
  implicit class VersionAugmenter(val version: VersionNumber) {
    def <(other: VersionNumber): Boolean = version.compareTo(other) == -1
    def ===(other: VersionNumber): Boolean = version.compareTo(other) == 0
    def > (other: VersionNumber): Boolean = version.compareTo(other) == 1

    def >= (other: VersionNumber): Boolean = {
      version.compareTo(other) >= 0
    }
  }

  implicit class DateTimeAugmenter(val date: DateTime) {
    def timeuuid(): UUID = {
      val random = new Random()
      new UUID(UUIDs.startOf(date.getMillis).getMostSignificantBits, random.nextLong())
    }
  }

  implicit class UUIDAugmenter(val uid: UUID) {
    def datetime: DateTime = new DateTime(UUIDs.unixTimestamp(uid), DateTimeZone.UTC)
  }

  implicit final class ListLikeModifyColumn[
    Owner <: CassandraTable[Owner, Record],
    Record,
    RR
  ](val col: AbstractColColumn[Owner, Record, List, RR]) {

    def prepend(value: RR): UpdateClause.Default = {
      new UpdateClause.Condition(QueryBuilder.Collections.prepend(col.name, col.asCql(value :: Nil)))
    }

    def prepend(values: List[RR]): UpdateClause.Default = {
      new UpdateClause.Condition(QueryBuilder.Collections.prepend(col.name, col.asCql(values)))
    }

    def prepend(mark: PrepareMark): UpdateClause.Prepared[List[RR]] = {
      new UpdateClause.Condition(QueryBuilder.Collections.prepend(col.name, mark))
    }

    def append(value: RR): UpdateClause.Default = {
      new UpdateClause.Condition(QueryBuilder.Collections.append(col.name, col.asCql(value :: Nil)))
    }

    def append(values: List[RR]): UpdateClause.Default = {
      new UpdateClause.Condition(QueryBuilder.Collections.append(col.name, col.asCql(values)))
    }

    def append(mark: PrepareMark): UpdateClause.Prepared[List[RR]] = {
      new UpdateClause.Condition(QueryBuilder.Collections.append(col.name, mark))
    }

    def discard(value: RR): UpdateClause.Default = {
      new UpdateClause.Condition(QueryBuilder.Collections.discard(col.name, col.asCql(value :: Nil)))
    }

    def discard(values: List[RR]): UpdateClause.Default = {
      new UpdateClause.Condition(QueryBuilder.Collections.discard(col.name, col.asCql(values)))
    }

    def discard(mark: PrepareMark): UpdateClause.Prepared[List[RR]] = {
      new UpdateClause.Condition(QueryBuilder.Collections.discard(col.name, mark.qb.queryString))
    }

    def setIdx(i: Int, value: RR): UpdateClause.Default = {
      new UpdateClause.Condition(QueryBuilder.Collections.setIdX(col.name, i.toString, col.valueAsCql(value)))
    }

    def setIdx(i: Int, mark: PrepareMark): UpdateClause.Prepared[RR] = {
      new UpdateClause.Condition(QueryBuilder.Collections.setIdX(col.name, i.toString, mark.qb.queryString))
    }

    def setIdx(index: PrepareMark, mark: PrepareMark): UpdateClause.Condition[RR :: Int :: HNil] = {
      new UpdateClause.Condition(
        QueryBuilder.Collections.setIdX(
          col.name,
          index.qb.queryString,
          mark.qb.queryString
        )
      )
    }
  }

  implicit final class SetLikeModifyColumn[
    Owner <: CassandraTable[Owner, Record],
    Record,
    RR
  ](val col: AbstractColColumn[Owner, Record, Set, RR]) {

    /**
      * Adds a single element to a Set collection.
      * Note, this is a Phantom API nice to have, but it is not a feature directly supported
      * by Cassandra, as Cassandra only supports adding sets to set collections.
      * Here we simply add a set of one element.
      * @param value The value to add to the set
      * @return An non-prepared update query.
      */
    def add(value: RR): UpdateClause.Default = {
      new UpdateClause.Condition(QueryBuilder.Collections.add(col.name, Set(col.valueAsCql(value))))
    }

    /**
      * Added to keep the API consistent. It is not possible to add single elements to a set
      * in Cassandra, the only CQL level support is for collections, even if they are collections
      * of a single element. This is a nicety added by the phantom API, and we do a prepared variant
      * to match with the above.
      *
      * However, there's a known and important limitation, where the [[com.outworkers.phantom.macros.BindHelper]]
      * macro is going to rely on the [[Primitive]] for the bound value to perform serialization. For this reason,
      * we need to make sure the primitive used to serialize the bound value is a collection primitive, otherwise
      * Cassandra will process an invalid update, where instead of treating the payload we send as a collection of a single
      * element, it gets confused and actually irreversibly damages the contents of the buffer where the collection is stored.
      *
      * This is because the binary payloads sent to Cassandra when a prepared statement is executed don't appear
      * to be validated against any kind of schema and are instead processed blindly.
      *
      * @param prepareMark The prepared statement mark.
      * @return A prepared update statement expecting a set of elements to be bound. !! Important !!
      */
    def add(prepareMark: PrepareMark): UpdateClause.Prepared[Set[RR]] = addAll(prepareMark)


    /**
      * Removes a single element from a Set collection.
      * Note, this is a Phantom API nice to have, but it is not a feature directly supported
      * by Cassandra, as Cassandra only supports removing entire sets from set collections.
      * Here we simply remove a set of one element.
      * @param value The value to add to the set
      * @return An non-prepared update query.
      */
    def remove(value: RR): UpdateClause.Default = {
      new UpdateClause.Condition(QueryBuilder.Collections.remove(col.name, Set(col.valueAsCql(value))))
    }

    /**
      * Added to keep the API consistent. It is not possible to remove single elements from a set
      * in Cassandra, the only CQL level support is for collections, even if they are collections
      * of a single element. This is a nicety added by the phantom API, and we do a prepared variant
      * to match with the above.
      *
      * However, there's a known and important limitation, where the [[com.outworkers.phantom.macros.BindHelper]]
      * macro is going to rely on the [[Primitive]] for the bound value to perform serialization. For this reason,
      * we need to make sure the primitive used to serialize the bound value is a collection primitive, otherwise
      * Cassandra will process an invalid update, where instead of treating the payload we send as a collection of a single
      * element, it gets confused and actually irreversibly damages the contents of the buffer where the collection is stored.
      *
      * This is because the binary payloads sent to Cassandra when a prepared statement is executed don't appear
      * to be validated against any kind of schema and are instead processed blindly.
      *
      * @param mark The prepared statement mark.
      * @return A prepared update statement expecting a set of elements to be bound. !! Important !!
      */
    def remove(mark: PrepareMark): UpdateClause.Prepared[Set[RR]] = removeAll(mark)

    def addAll(values: Set[RR]): UpdateClause.Default = {
      new UpdateClause.Condition(QueryBuilder.Collections.add(col.name, values.map(col.valueAsCql)))
    }

    def addAll(prepareMark: PrepareMark): UpdateClause.Prepared[Set[RR]] = {
      new UpdateClause.Condition(QueryBuilder.Collections.add(col.name, prepareMark))
    }

    def removeAll(values: Set[RR]): UpdateClause.Default = {
      new UpdateClause.Condition(QueryBuilder.Collections.remove(col.name, values.map(col.valueAsCql)))
    }

    def removeAll(values: PrepareMark): UpdateClause.Prepared[Set[RR]] = {
      new UpdateClause.Condition(QueryBuilder.Collections.removePrepared(col.name, values))
    }
  }

  implicit class MapLikeModifyColumn[
    Owner <: CassandraTable[Owner, Record],
    Record,
    A,
    B
  ](val col: AbstractMapColumn[Owner, Record, A, B]) {

    def set(key: A, value: B): UpdateClause.Default = {
      new UpdateClause.Default(
        QueryBuilder.Collections.mapSet(
          col.name,
          col.keyAsCql(key).toString,
          col.valueAsCql(value)
        )
      )
    }


    final def set(mark: PrepareMark): UpdateClause.Condition[B :: A :: HNil] = {
      new UpdateClause.Condition(
        QueryBuilder.Collections.mapSet(
          col.name,
          mark.qb.queryString,
          mark.qb.queryString
        )
      )
    }


    def put(value: (A, B)): UpdateClause.Default = {
      val (k, v) = value

      new UpdateClause.Default(QueryBuilder.Collections.put(
        col.name,
        col.keyAsCql(k).toString -> col.valueAsCql(v)
      )
      )
    }

    def putAll[L](values: L)(implicit ev1: L => Iterable[(A, B)]): UpdateClause.Default = {
      new UpdateClause.Default(
        QueryBuilder.Collections.put(col.name, values.map { case (key, value) =>
          col.keyAsCql(key) -> col.valueAsCql(value)
        }.toSeq : _*)
      )
    }

    final def putAll(mark: PrepareMark): UpdateClause.Prepared[Map[A, B]] = {
      new UpdateClause.Prepared[Map[A, B]](
        QueryBuilder.Collections.put(col.name, mark)
      )
    }
}

  implicit class SetConditionals[
    T <: CassandraTable[T, R],
    R,
    RR
  ](val col: AbstractColColumn[T, R, Set, RR]) {

    /**
      * Generates a Set CONTAINS clause that can be used inside a CQL Where condition.
      * @param elem The element to check for in the contains clause.
      * @return A Where clause.
      */
    final def contains(elem: RR): WhereClause.Condition = {
      new WhereClause.Condition(
        QueryBuilder.Where.contains(col.name, col.valueAsCql(elem))
      )
    }

    /**
      * Generates a Set CONTAINS clause that can be used inside a CQL Where condition.
      * @param mark The prepared statements mark.
      * @return A Where clause.
      */
    final def contains(mark: PrepareMark): WhereClause.ParametricCondition[RR] = {
      new WhereClause.ParametricCondition[RR](
        QueryBuilder.Where.contains(col.name, mark.qb.queryString)
      )
    }
  }

  /**
    * Definition used to cast an index map column with keys indexed to a query-able definition.
    * This will allow users to use "CONTAINS KEY" clauses to search for matches based on map keys.
    *
    * @param col The map column to cast to a Map column secondary index query.
    * @tparam T The Cassandra table inner type.
    * @tparam R The record type of the table.
    * @tparam K The type of the key held in the map.
    * @tparam V The type of the value held in the map.
    * @return A MapConditionals class with CONTAINS KEY support.
    */
  implicit class MapKeyConditionals[
    T <: CassandraTable[T, R],
    R,
    K,
    V
  ](val col: AbstractMapColumn[T, R, K, V] with Indexed with Keys) {

    /**
      * Generates a Map CONTAINS KEY clause that can be used inside a CQL Where condition.
      * This allows users to lookup records by a KEY inside a map column of a table.
      *
      * Key support is not yet enabled in phantom because index generation has to be done differently.
      * Otherwise, there is no support for simultaneous indexing on both KEYS and VALUES of a MAP column.
      * This limitation will be lifted in the future.
      *
      * @param elem The element to check for in the contains clause.
      * @return A Where clause.
      */
    final def containsKey(elem: K): WhereClause.Condition = {
      new WhereClause.Condition(
        QueryBuilder.Where.containsKey(col.name, col.keyAsCql(elem))
      )
    }

    /**
      * Generates a Map CONTAINS KEY clause that can be used inside a CQL Where condition.
      * This allows users to lookup records by a KEY inside a map column of a table.
      *
      * Key support is not yet enabled in phantom because index generation has to be done differently.
      * Otherwise, there is no support for simultaneous indexing on both KEYS and VALUES of a MAP column.
      * This limitation will be lifted in the future.
      *
      * @param mark The prepared query mark.
      * @return A Where clause.
      */
    final def containsKey(mark: PrepareMark): WhereClause.ParametricCondition[K] = {
      new WhereClause.ParametricCondition[K](
        QueryBuilder.Where.containsKey(col.name, mark.qb.queryString)
      )
    }
  }

  implicit val context: ExecutionContextExecutor = Manager.scalaExecutor
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy