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

dev.chopsticks.kvdb.fdb.FdbReadApi.scala Maven / Gradle / Ivy

The newest version!
package dev.chopsticks.kvdb.fdb

import cats.syntax.either._
import com.apple.foundationdb.tuple.ByteArrayUtil
import com.apple.foundationdb.{KeySelector, ReadTransaction}
import dev.chopsticks.kvdb.ColumnFamily
import dev.chopsticks.kvdb.KvdbDatabase.keySatisfies
import dev.chopsticks.kvdb.codec.KeySerdes
import dev.chopsticks.kvdb.fdb.FdbDatabase.FdbContext
import dev.chopsticks.kvdb.proto.KvdbKeyConstraint.Operator
import dev.chopsticks.kvdb.proto.{KvdbKeyConstraint, KvdbKeyRange}
import dev.chopsticks.kvdb.util.KvdbAliases.KvdbPair
import dev.chopsticks.kvdb.util.KvdbException.InvalidKvdbArgumentException
import eu.timepit.refined.types.numeric.PosInt

import java.util.concurrent.CompletableFuture
import scala.jdk.CollectionConverters._

class FdbReadApi[BCF[A, B] <: ColumnFamily[A, B]](
  val tx: ReadTransaction,
  dbContext: FdbContext[BCF]
) {
  type CF = BCF[_, _]

  def snapshot(): FdbReadApi[BCF] = {
    new FdbReadApi[BCF](tx.snapshot(), dbContext)
  }

  def nonEqualFromConstraintToKeySelector(operator: Operator, operand: Array[Byte]): KeySelector = {
    operator match {
      case Operator.PREFIX =>
        KeySelector.firstGreaterOrEqual(operand)
      case Operator.GREATER =>
        KeySelector.firstGreaterThan(operand)
      case Operator.LESS =>
        KeySelector.lastLessThan(operand)
      case Operator.GREATER_EQUAL =>
        KeySelector.firstGreaterOrEqual(operand)
      case Operator.LESS_EQUAL =>
        KeySelector.lastLessOrEqual(operand)
      case Operator.FIRST =>
        KeySelector.firstGreaterOrEqual(operand)
      case Operator.LAST =>
        KeySelector.lastLessOrEqual(ByteArrayUtil.strinc(operand))
      case Operator.EQUAL =>
        throw new IllegalArgumentException("Constraint must not have EQUAL operator")
      case o =>
        throw new IllegalArgumentException(s"Unrecognized operator: $o")
    }
  }

  def toConstraintToKeySelector(
    operator: Operator,
    operand: Array[Byte],
    firstOutOfRangeOperand: Array[Byte]
  ): KeySelector = {
    operator match {
      case Operator.EQUAL =>
        KeySelector.firstGreaterOrEqual(operand)
      case Operator.LESS =>
        KeySelector.firstGreaterOrEqual(operand)
      case Operator.LESS_EQUAL =>
        KeySelector.firstGreaterThan(operand)
      case Operator.PREFIX =>
        KeySelector.firstGreaterOrEqual(ByteArrayUtil.strinc(operand))
      case _ =>
        KeySelector.firstGreaterOrEqual(firstOutOfRangeOperand)
    }
  }

  private[chopsticks] def getEitherByColumnId(
    columnId: String,
    constraints: List[KvdbKeyConstraint]
  ): CompletableFuture[Either[Array[Byte], KvdbPair]] = {
    if (constraints.isEmpty) {
      CompletableFuture.completedFuture(Left(Array.emptyByteArray))
    }
    else {
      val prefixedConstraints = dbContext.prefixKeyConstraints(columnId, constraints)
      val headConstraint = prefixedConstraints.head
      val headOperand = headConstraint.operand.toByteArray
      val operator = headConstraint.operator

      operator match {
        case Operator.EQUAL =>
          tx.get(headOperand).thenApply { value =>
            if (value != null && keySatisfies(headOperand, prefixedConstraints.tail)) {
              Right((headOperand, value))
            }
            else Left(headOperand)
          }

        case op =>
          val keySelector = nonEqualFromConstraintToKeySelector(operator, headOperand)
          val keyFuture = tx.getKey(keySelector)
          val tailConstraints = if (op == Operator.PREFIX) prefixedConstraints else prefixedConstraints.tail

          val ret: CompletableFuture[Either[Array[Byte], KvdbPair]] = keyFuture.thenCompose { key: Array[Byte] =>
            if (
              key != null && key.length > 0 && KeySerdes.isPrefix(
                dbContext.columnPrefix(columnId),
                key
              ) && keySatisfies(
                key,
                tailConstraints
              )
            ) {
              tx.get(key).thenApply(value => Either.right[Array[Byte], KvdbPair]((key, value)))
            }
            else {
              CompletableFuture.completedFuture(Either.left[Array[Byte], KvdbPair](Array.emptyByteArray))
            }
          }

          ret
      }
    }
  }

  def addReadConflictKeyIfNotSnapshot[Col <: CF](column: Col, key: Array[Byte]): Boolean = {
    tx.addReadConflictKeyIfNotSnapshot(dbContext.prefixKey(column, key))
  }

  def addReadConflictRangeIfNotSnapshot[Col <: CF](column: Col, begin: Array[Byte], end: Array[Byte]): Boolean = {
    tx.addReadConflictRangeIfNotSnapshot(dbContext.prefixKey(column, begin), dbContext.prefixKey(column, end))
  }

  def getEither[Col <: CF](
    column: Col,
    constraints: List[KvdbKeyConstraint]
  ): CompletableFuture[Either[Array[Byte], KvdbPair]] = {
    getEitherByColumnId(column.id, constraints)
  }

  private def doGetRangeFuture[Col <: CF](
    column: Col,
    from: List[KvdbKeyConstraint],
    to: List[KvdbKeyConstraint],
    limit: PosInt
  ): CompletableFuture[List[KvdbPair]] = {
    val prefixedFrom = dbContext.prefixKeyConstraints(column, from)
    val prefixedTo = dbContext.prefixKeyConstraints(column, to)
    val fromHead = prefixedFrom.head
    val operator = fromHead.operator
    val headValidation =
      if (operator == Operator.EQUAL || operator == Operator.PREFIX) prefixedFrom else prefixedFrom.tail
    val headOperand = fromHead.operand.toByteArray

    val startKeySelector = operator match {
      case Operator.EQUAL => KeySelector.firstGreaterOrEqual(headOperand)
      case _ => nonEqualFromConstraintToKeySelector(operator, headOperand)
    }

    val toHead = prefixedTo.head
    val endKeySelector = toConstraintToKeySelector(
      toHead.operator,
      toHead.operand.toByteArray,
      dbContext.strinc(column)
    )
    val columnPrefix = dbContext.columnPrefix(column)

    tx.getRange(startKeySelector, endKeySelector, limit.value)
      .asList()
      .thenApply { javaList =>
        val pairs = javaList.asScala.toList match {
          case head :: tail =>
            val headKey = head.getKey
            if (
              headKey != null && headKey.nonEmpty && KeySerdes.isPrefix(columnPrefix, headKey) && keySatisfies(
                headKey,
                headValidation
              )
            ) {
              head :: tail.takeWhile { p =>
                val key = p.getKey
                key != null && key.nonEmpty && KeySerdes.isPrefix(columnPrefix, headKey) && keySatisfies(
                  key,
                  prefixedTo
                )
              }
            }
            else Nil
          case Nil => Nil
        }

        pairs.map(p => dbContext.unprefixKey(column, p.getKey) -> p.getValue)
      }
  }

  def getByColumnId(
    columnId: String,
    constraints: List[KvdbKeyConstraint]
  ): CompletableFuture[Option[KvdbPair]] = {
    getEitherByColumnId(columnId, constraints).thenApply { //noinspection MatchToPartialFunction
      result =>
        result match {
          case Right((key, value)) =>
            Some(dbContext.unprefixKey(columnId, key) -> value)
          case _ => None
        }
    }
  }

  def get[Col <: CF](
    column: Col,
    constraints: List[KvdbKeyConstraint]
  ): CompletableFuture[Option[KvdbPair]] = {
    getEither(column, constraints).thenApply { //noinspection MatchToPartialFunction
      result =>
        result match {
          case Right((key, value)) =>
            Some(dbContext.unprefixKey(column, key) -> value)
          case _ => None
        }
    }
  }

  def getRange[Col <: CF](
    column: Col,
    range: KvdbKeyRange
  ): CompletableFuture[List[KvdbPair]] = {
    PosInt.from(range.limit) match {
      case Left(_) =>
        CompletableFuture.failedFuture(
          InvalidKvdbArgumentException(s"range.limit of '${range.limit}' is invalid, must be a positive integer")
        )
      case Right(limit) =>
        doGetRangeFuture(column, range.from, range.to, limit)
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy