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

renesca.QueryHandler.scala Maven / Gradle / Ivy

package renesca

// QueryHandler ensures that we have the same query-interface in Transaction and in DbService.
// Both must implement queryService and handleError.
// The query-interface consists of the methods:
//  queryGraph  - submit one query [with parameters] and return a Graph
//  queryTable  - submit one query [with parameters] and return a row set
//  queryGraphs - submit multiple queries [with parameters] and return a Graph for each result
//  queryTables - submit multiple queries [with parameters] and return a row set for each result
//  query       - submit one or multiple queries [with parameters] as a side effect (returns Unit)
//  persist     - save a modified graph to the database

import renesca.graph._
import renesca.parameter._
import renesca.schema.{AbstractRelation, HyperRelation}
import renesca.table.Table

object Query {
  implicit def stringToQuery(statement: String): Query = Query(statement)
}

case class Query(statement: String, parameters: ParameterMap = Map.empty)

trait QueryInterface {
  def queryWholeGraph: Graph
  def queryGraph(query: Query): Graph
  def queryTable(query: Query): Table
  def queryGraphAndTable(query: Query): (Graph,Table)
  def queryGraphs(queries: Query*): Seq[Graph]
  def queryTables(queries: Query*): Seq[Table]
  def queryGraphsAndTables(queries: Query*): Seq[(Graph, Table)]
  def query(queries: Query*): Unit

  def queryGraph(statement: String, parameters: ParameterMap = Map.empty): Graph = queryGraph(Query(statement, parameters))
  def queryTable(statement: String, parameters: ParameterMap = Map.empty): Table = queryTable(Query(statement, parameters))
  def queryGraphAndTable(statement: String, parameters: ParameterMap = Map.empty): (Graph,Table) = queryGraphAndTable(Query(statement, parameters))
  def query(statement: String, parameters: ParameterMap = Map.empty): Unit = query(Query(statement, parameters))

  def persistChanges(graph: Graph): Option[String]

  def persistChanges(schemaGraph: schema.Graph): Option[String] = {
    validateSchemaGraph(schemaGraph) match {
      case Some(err) => Some(err)
      case None      => persistChanges(schemaGraph.graph)
    }
  }

  def persistChanges(item: Item, items: Item*): Option[String] = {
    val allItems = item :: items.toList
    val graph = Graph(allItems.collect { case n: Node => n }, allItems.collect { case r: Relation => r })
    persistChanges(graph)
  }

  def persistChanges(item: schema.Item, items: schema.Item*): Option[String] = {
    val allItems = item :: items.toList
    validateSchemaItems(allItems) match {
      case Some(err) => Some(err)
      case None      =>
        val schemaGraph = new schema.Graph {
          // the schema graph methods will never be called,
          // but we use the implementation of the add function,
          // which also adds the path of hyperrelations to the graph.
          override def nodes: Seq[_ <: schema.Node] = ???
          override def hyperRelations: Seq[_ <: HyperRelation[_, _, _, _, _]] = ???
          override def relations: Seq[_ <: schema.Relation[_, _]] = ???
          override def abstractRelations: Seq[_ <: AbstractRelation[_, _]] = ???
          val graph: Graph = Graph.empty
        }

        schemaGraph.add(allItems: _*)
        persistChanges(schemaGraph.graph)
    }
  }

  def validateSchemaGraph(schemaGraph: schema.Graph): Option[String] = {
    val changes = schemaGraph.graph.changes.collect { case c: GraphItemChange => c.item }.toSet
    val items = (schemaGraph.nodes ++ schemaGraph.relations).filter(changes contains _.rawItem)
    validateSchemaItems(items)
  }

  def validateSchemaItems(items: Iterable[schema.Item]): Option[String] = {
    val validations = items.map { item =>
      item.validate match {
        case Some(err) => Some(s"Validation for item '${ item.rawItem }' failed: $err")
        case None      => None
      }
    }.flatten

    if(validations.isEmpty)
      None
    else
      Some(validations.mkString(","))
  }
}

trait QueryHandler extends QueryInterface {
  val builder = new QueryBuilder

  override def queryWholeGraph: Graph = queryGraph("match (n) optional match (n)-[r]-() return n,r")
  override def queryGraph(query: Query): Graph = queryGraphs(query).head
  override def queryTable(query: Query): Table = queryTables(query).head
  override def queryGraphAndTable(query: Query): (Graph,Table) = queryGraphsAndTables(query).head

  override def queryGraphs(queries: Query*): Seq[Graph] = {
    val results = executeQueries(queries, List("graph"))
    extractGraphs(results)
  }

  override def queryTables(queries: Query*): Seq[Table] = {
    val results = executeQueries(queries, List("row"))
    extractTables(results)
  }

  override def queryGraphsAndTables(queries: Query*): Seq[(Graph, Table)] = {
    val results = executeQueries(queries, List("row", "graph"))
    extractGraphs(results) zip extractTables(results)
  }

  def query(queries: Query*) { executeQueries(queries, Nil) }

  //TODO: persist changes should ONLY work on transactions!
  def persistChanges(graph: Graph): Option[String] = {
    builder.generateQueries(graph.changes) match {
      case Left(msg)      => Some(msg)
      case Right(queries) =>
        val failure = builder.applyQueries(queries, queryGraphsAndTables)
        if(failure.isEmpty)
          graph.clearChanges()

        failure
    }
  }

  protected def executeQueries(queries: Seq[Query], resultDataContents: List[String]): List[json.Result] = {
    val jsonRequest = buildJsonRequest(queries, resultDataContents)
    val jsonResponse = queryService(jsonRequest)
    handleError(exceptionFromErrors(jsonResponse))
    jsonResponse.results
  }

  protected def extractGraphs(results: Seq[json.Result]): Seq[Graph] = {
    val allJsonGraphs: Seq[List[json.Graph]] = results.map(_.data.flatMap(_.graph))
    allJsonGraphs.map(_.map(Graph(_)).fold(Graph.empty)(_ merge _))
  }

  protected def extractTables(results: Seq[json.Result]): Seq[Table] = {
    results.map(r => Table(r))
  }

  protected def buildJsonRequest(queries: Seq[Query], resultDataContents: List[String]): json.Request = {
    json.Request(queries.map(query => json.Statement(query, resultDataContents)).toList)
  }

  protected def exceptionFromErrors(jsonResponse: json.Response): Option[RuntimeException] = {
    jsonResponse.errors match {
      case Nil    => None
      case errors =>
        val message = errors.map {
          case json.Error(code, msg) => s"$code\n$msg"
        }.mkString("\n", "\n\n", "\n")
        Some(new RuntimeException(message))
    }
  }

  protected def queryService(jsonRequest: json.Request): json.Response
  protected def handleError(exceptions: Option[Exception]): Unit
}

class Transaction extends QueryHandler {thisTransaction =>

  var restService: RestService = null //TODO: inject

  var id: Option[TransactionId] = None

  private var valid = true
  def isValid = valid
  def invalidate() { valid = false }
  private def throwIfNotValid() {
    if(!valid)
      throw new RuntimeException("Transaction is not valid anymore.")
  }


  override protected def queryService(jsonRequest: json.Request): json.Response = {
    throwIfNotValid()
    id match {
      case Some(transactionId) => restService.resumeTransaction(transactionId, jsonRequest)
      case None                =>
        val (transactionId, jsonResponse) = restService.openTransaction(jsonRequest)
        id = Some(transactionId)
        jsonResponse
    }
  }

  protected def handleError(exceptions: Option[Exception]) {
    for(exception <- exceptions) {
      rollback()
      throw exception
    }
  }

  def rollback() = {
    id match {
      case Some(transactionId) => restService.rollbackTransaction(transactionId)
      case None                =>
    }
    invalidate()
  }

  val commit = new CommitTransaction

  class CommitTransaction extends QueryHandler {
    // Important:
    // queryService can be called only once, because it commits and invalidates the transaction.
    // This means that methods like persistChanges which are firing multiple queries and thus calling querySerice
    // multiple times need to be modified or wrapped.

    def apply() {
      throwIfNotValid()
      for(transactionId <- id) {
        val jsonResponse = restService.commitTransaction(transactionId)
        handleError(exceptionFromErrors(jsonResponse))
      }

      invalidate()
    }

    override protected def queryService(jsonRequest: json.Request): json.Response = {
      // TODO: share code with this.Transaction.queryService
      throwIfNotValid()
      val jsonResponse = id match {
        case Some(transactionId) => restService.commitTransaction(transactionId, jsonRequest)
        case None                => restService.singleRequest(jsonRequest)
      }

      invalidate()
      jsonResponse
    }

    override def persistChanges(graph: Graph): Option[String] = {
      //TODO: this solution can send one REST request more than needed, as the commit request does not do any changes
      val errorOpt = thisTransaction.persistChanges(graph)
      errorOpt.map(error => {
        rollback()
        Some(error)
      }).getOrElse({
        apply() // commit
        None
      })
    }


    override protected def handleError(exceptions: Option[Exception]) = thisTransaction.handleError(exceptions)
  }

  override def toString = s"Transaction(id=$id, valid:$isValid, $restService)"
}

class DbService extends QueryHandler {
  var restService: RestService = null //TODO: inject

  protected def handleError(exceptions: Option[Exception]) {
    for(exception <- exceptions)
      throw exception
  }

  override protected def queryService(jsonRequest: json.Request): json.Response = {
    restService.singleRequest(jsonRequest)
  }

  def newTransaction() = {
    val tx = new Transaction
    tx.restService = restService
    tx
  }

  def transaction[T](code: Transaction => T): T = {
    val tx = newTransaction
    val result = try {
      code(tx)
    } catch {
      case e: Exception =>
        tx.rollback()
        throw e
    }

    // if there was a request and transacion is not done yet
    if(tx.id.isDefined && tx.isValid) tx.commit()

    result
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy