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

renesca.QueryBuilder.scala Maven / Gradle / Ivy

package renesca

import renesca.graph._
import renesca.parameter._
import renesca.parameter.implicits._
import renesca.table.Table

import scala.collection.mutable

case class QueryConfig(item: SubGraph, query: Query, callback: (Graph, Table) => Either[String, () => Any] = (graph: Graph, table: Table) => Right(() => ()))

class QueryPatterns(resolvedItems: mutable.Map[Item,Origin]) {
  private var variableCounter = 0
  def randomVariable = {
    val variable = "V" + variableCounter
    variableCounter += 1
    variable
  }

  def selectLiteralMap(variable: String, properties: Properties, selection: Set[PropertyKey]) = {
    val remainingProperties = properties.filterKeys(!selection.contains(_))
    val selectedProperties = properties.filterKeys(selection.contains(_))
    val parameterMap = selectedProperties.toMap.map { case (k, v) => (PropertyKey(s"${ variable }_${ k }"), v) }
    val literalMap = selectedProperties.map { case (k, _) => s"$k: {${ variable }_${ k }}" }
    val literalMapMatcher = if(literalMap.isEmpty) "" else literalMap.mkString("{", ",", "}")
    (literalMapMatcher, remainingProperties, parameterMap)
  }

  //TODO should limit number of matches for merge and match to 1!
  // match/merge ... with variable limit 1 ...
  def nodePattern(node: Node, forDeletion: Boolean = false): (String, String, String, ParameterMap, String) = {
    val variable = randomVariable
    val labels = node.labels.map(label => s":`$label`").mkString
    resolvedItems.getOrElse(node, node.origin) match {
      case Id(id)                =>
        ("match", s"($variable)", s"where id($variable) = {${ variable }_nodeId}", Map(s"${ variable }_nodeId" -> id), variable)
      case Create()              =>
        ("create", s"($variable $labels {${ variable }_properties})", "", Map(s"${ variable }_properties" -> node.properties.toMap), variable)
      case Merge(merge, onMatch) =>
        val (mergeLiteralMap, remainingProperties, parameterMap) = selectLiteralMap(variable, node.properties, merge.toSet)
        val onMatchProperties = remainingProperties.filterKeys(onMatch contains _)
        ("merge", s"($variable $labels $mergeLiteralMap)", s"on create set $variable += {${ variable }_onCreateProperties} on match set $variable += {${ variable }_onMatchProperties}",
          parameterMap ++ Map(s"${ variable }_onCreateProperties" -> remainingProperties.toMap, s"${ variable }_onMatchProperties" -> onMatchProperties.toMap),
          variable)
      case Match(matches)        =>
        val (matchLiteralMap, remainingProperties, parameterMap) = selectLiteralMap(variable, node.properties, matches)
        val (propertySetter, propertyMap) = if (forDeletion || remainingProperties.isEmpty)
          ("", Map.empty)
        else
          (s"set $variable += {${ variable }_properties}", Map(s"${ variable }_properties" -> remainingProperties.toMap))

        ("match", s"($variable $labels $matchLiteralMap)", propertySetter,
          parameterMap ++ propertyMap,
          variable)
    }
  }

  def relationPattern(relation: Relation, forDeletion: Boolean = false): (String, String, String, ParameterMap, String) = {
    val variable = randomVariable
    resolvedItems.getOrElse(relation, relation.origin) match {
      case Id(id)                =>
        ("match", s"[$variable]", s"where id($variable) = {${ variable }_relationId}", Map(s"${ variable }_relationId" -> id), variable)
      case Create()              =>
        ("create", s"[$variable :`${ relation.relationType }` {${ variable }_properties}]", "", Map(s"${ variable }_properties" -> relation.properties.toMap), variable)
      case Merge(merge, onMatch) =>
        val (mergeLiteralMap, remainingProperties, parameterMap) = selectLiteralMap(variable, relation.properties, merge)
        val onMatchProperties = remainingProperties.filterKeys(onMatch)
        ("merge", s"[$variable :`${ relation.relationType }` $mergeLiteralMap]", s"on create set $variable += {${ variable }_onCreateProperties} on match set $variable += {${ variable }_onMatchProperties}",
          parameterMap ++ Map(s"${ variable }_onCreateProperties" -> remainingProperties.toMap, s"${ variable }_onMatchProperties" -> onMatchProperties.toMap),
          variable)
      case Match(matches)        =>
        val (matchLiteralMap, remainingProperties, parameterMap) = selectLiteralMap(variable, relation.properties, matches)
        val (propertySetter, propertyMap) = if (forDeletion || remainingProperties.isEmpty)
          ("", Map.empty)
        else
          (s"set $variable += {${ variable }_properties}", Map(s"${ variable }_properties" -> remainingProperties.toMap))

        ("match", s"[$variable :`${ relation.relationType }` $matchLiteralMap]", propertySetter,
          parameterMap ++ propertyMap,
          variable)
    }
  }

  def queryNode(node: Node) = {
    val (keyword, query, postfix, parameters, variable) = nodePattern(node)
    Query(s"$keyword $query $postfix return $variable", parameters)
  }

  def queryRelationPattern(relation: Relation, forDeletion: Boolean = false) = {
    if(resolvedItems.getOrElse(relation.startNode, relation.startNode.origin).isLocal)
      throw new Exception("Start node in relation is still local: " + relation)
    if(resolvedItems.getOrElse(relation.endNode, relation.endNode.origin).isLocal)
      throw new Exception("End node in relation is still local: " + relation)

    val (startKeyword, startQuery, startPostfix, startParameters, startVariable) = nodePattern(relation.startNode, forDeletion)
    val (endKeyword, endQuery, endPostfix, endParameters, endVariable) = nodePattern(relation.endNode, forDeletion)
    val (keyword, query, postfix, parameters, variable) = relationPattern(relation, forDeletion)

    (s"$startKeyword $startQuery $startPostfix $endKeyword $endQuery $endPostfix $keyword ($startVariable)-$query->($endVariable) $postfix", variable, startParameters ++ parameters ++ endParameters)
  }

  def queryRelation(relation: Relation) = {
    val (queryPattern, variable, params) = queryRelationPattern(relation)
    Query(s"$queryPattern return $variable", params)
  }

  def queryPathPattern(path: Path, forDeletion: Boolean = false) = {
    val variableMap = mutable.LinkedHashMap.empty[Item, String]

    // first match all non-path nodes
    val boundNodes = path.relations.flatMap(r => Seq(r.startNode, r.endNode)).distinct diff path.nodes
    if(boundNodes.exists(n => resolvedItems.getOrElse(n, n.origin).isLocal))
      throw new Exception("Bound node on path is still local: " + path)

    val (nodeQueries, nodeParameters) = boundNodes.map(node => {
      val (keyword, query, postfix, parameters, variable) = nodePattern(node, forDeletion)
      variableMap += node -> variable
      (s"$keyword $query $postfix", parameters.toMap)
    }).unzip

    val nodeQuery = nodeQueries.mkString(" ")

    // now the actual path can matched/merged/created
    val (pathQuery, pathParameters) = {
      val (pathQueries, pathSetters, pathParameters) = path.relations.zipWithIndex.map { case (relation, i) =>
        // a path assures that the start node was already matched by the previous relation (if there is one)
        val (_, startQuery, startSetter, startParameters, startVariable) = if(i == 0)
                                                                             variableMap.get(relation.startNode).map(variable => ("", s"($variable)", "", Map.empty, variable)).getOrElse(nodePattern(relation.startNode, forDeletion))
                                                                           else
                                                                             ("", "", "", Map.empty, variableMap(relation.startNode))
        val (_, endQuery, endSetter, endParameters, endVariable) = variableMap.get(relation.endNode).map(variable => ("", s"($variable)", "", Map.empty, variable)).getOrElse(nodePattern(relation.endNode, forDeletion))
        val (_, relQuery, relSetter, relParameters, relVariable) = relationPattern(relation, forDeletion)
        variableMap ++= Seq(relation.startNode -> startVariable, relation.endNode -> endVariable, relation -> relVariable)

        (s"$startQuery-$relQuery->$endQuery",
          s"$startSetter $endSetter $relSetter",
          startParameters ++ endParameters ++ relParameters)
      }.unzip3

      val parameters = pathParameters.flatten.toMap
      val pathQuery = s"""${ pathQueries.mkString(" ") } ${ pathSetters.mkString(" ") }"""
      (path.origin match {
        case Create()    => s"create $pathQuery"
        case Match(_)    => s"match $pathQuery"
        case Merge(_, _) => s"merge $pathQuery"
      }, parameters)
    }

    val parameters = (nodeParameters.flatten ++ pathParameters).toMap

    (s"$nodeQuery $pathQuery", parameters, variableMap)
  }

  def queryPath(path: Path) = {
    val (queryPattern, parameters, variableMap) = queryPathPattern(path)
    val reverseVariableMap = variableMap.map { case (k, v) => (v, k) }.toMap

    val returnClause = "return " + variableMap.map {
      case (k, variable) => k match {
        case _: Node     => s"{id: id($variable), properties: $variable, labels: labels($variable)} as $variable"
        case _: Relation => s"{id: id($variable), properties: $variable} as $variable"
      }
    }.mkString(",")

    (Query(s"$queryPattern $returnClause", parameters), reverseVariableMap)
  }
}

class QueryGenerator {
  val resolvedItems: mutable.Map[Item,Origin] = mutable.Map.empty

  def contentChangesToQueries(contentChanges: Seq[GraphContentChange])() = {
    contentChanges.groupBy(_.item).map {
      case (item, changes) =>
        if(item.origin.isLocal)
          throw new Exception("Trying to edit local item: " + item)

        val propertyAdditions: mutable.Map[PropertyKey, ParameterValue] = mutable.Map.empty
        val propertyRemovals: mutable.Set[PropertyKey] = mutable.Set.empty
        val labelAdditions: mutable.Set[Label] = mutable.Set.empty
        val labelRemovals: mutable.Set[Label] = mutable.Set.empty
        changes.foreach {
          case SetProperty(_, key, value) =>
            propertyRemovals -= key
            propertyAdditions += key -> value
          case RemoveProperty(_, key)     =>
            propertyRemovals += key
            propertyAdditions -= key
          case SetLabel(_, label)         =>
            labelRemovals -= label
            labelAdditions += label
          case RemoveLabel(_, label)      =>
            labelRemovals += label
            labelAdditions -= label
        }

        val isRelation = item match {
          case _: Node     => false
          case _: Relation => true
        }

        val qPatterns = new QueryPatterns(resolvedItems)
        val variable = qPatterns.randomVariable
        val matcher = if(isRelation) s"match ()-[$variable]->()" else s"match ($variable)"
        val propertyRemove = propertyRemovals.map(r => s"remove $variable.`$r`").mkString(" ")
        val labelAdd = labelAdditions.map(a => s"set $variable:`$a`").mkString(" ")
        val labelRemove = labelRemovals.map(r => s"remove $variable:`$r`").mkString(" ")
        val setters = s"set $variable += {${ variable }_propertyAdditions} $propertyRemove $labelAdd $labelRemove"
        val setterMap = Map(s"${ variable }_propertyAdditions" -> propertyAdditions.toMap)

        val query = Query(
          s"$matcher where id($variable) = {${ variable }_itemId} $setters",
          Map(s"${ variable }_itemId" -> item.origin.asInstanceOf[Id].id) ++ setterMap
        )

        QueryConfig(item, query)
    }.toSeq
  }

  def deletionToQueries(deleteItems: Seq[Item])() = {
    deleteItems.map { item =>
      val qPatterns = new QueryPatterns(resolvedItems)
      item match {
        case n: Node =>
          val (keyword, query, postfix, parameters, variable) = qPatterns.nodePattern(n, forDeletion = true)
          val optionalVariable = qPatterns.randomVariable
          QueryConfig(n, Query(s"$keyword $query $postfix optional match ($variable)-[$optionalVariable]-() delete $optionalVariable, $variable", parameters))
        case r: Relation =>
          //TODO: invalidate Id origin of deleted item?
          if (!r.origin.isLocal) { // if not local we can match by id and have a simpler query
            val (keyword, query, postfix, parameters, variable) = qPatterns.relationPattern(r,  forDeletion =true)
            QueryConfig(r, Query(s"$keyword ()-$query-() $postfix delete $variable", parameters))
          } else {
            val (queryPattern, variable, params) = qPatterns.queryRelationPattern(r, forDeletion = true)
            QueryConfig(r, Query(s"$queryPattern delete $variable", params))
          }
      }
    }
  }

  def deletionPathsToQueries(deletePaths: Seq[Path])() = {
    deletePaths.map { path =>
      val qPatterns = new QueryPatterns(resolvedItems)
      val (queryPattern, parameters, variableMap) = qPatterns.queryPathPattern(path, forDeletion = true)

      val (optionalMatchers, matcherVariables) = path.nodes.map { n =>
        val optionalVariable = qPatterns.randomVariable
        val variable = variableMap(n)
        (s"optional match ($variable)-[$optionalVariable]-()", optionalVariable)
      }.unzip

      val variables = (path.relations ++ path.nodes).map(variableMap)
      val returnClause = "delete " + (matcherVariables ++ variables).mkString(",")

      QueryConfig(path, Query(s"$queryPattern ${optionalMatchers.mkString(" ")} $returnClause", parameters))
    }
  }

  def addPathsToQueries(addPaths: Seq[Path])() = {
    addPaths.map(path => {
      val qPatterns = new QueryPatterns(resolvedItems)
      val (query, reverseVariableMap) = qPatterns.queryPath(path)

      QueryConfig(path, query, (graph: Graph, table: Table) => {
        if(table.rows.size > 1)
          Left("More than one query result for path: " + path)
        else
          table.rows.headOption.map(row => {
            table.columns.foreach(col => {
              val item = reverseVariableMap(col)
              val map = row(col).asInstanceOf[MapParameterValue].value
              resolvedItems += item -> Id(map("id").asInstanceOf[LongPropertyValue].value)
            })
            Right(() => {
              table.columns.foreach(col => {
                val item = reverseVariableMap(col)
                val map = row(col).asInstanceOf[MapParameterValue].value
                // TODO: without casts?
                item.origin = Id(map("id").asInstanceOf[LongPropertyValue].value)
                item.properties.clear()
                item.properties ++= map("properties").asInstanceOf[MapParameterValue].value.asInstanceOf[PropertyMap]
                item match {
                  case n: Node =>
                    n.labels.clear()
                    n.labels ++= map("labels").asInstanceOf[ArrayParameterValue].value.asInstanceOf[Seq[StringPropertyValue]].map(l => Label(l.value))
                  case _       =>
                }
              })
            })
          }).getOrElse(Left("Query result is missing desired path: " + path))
      })
    })
  }

  def addRelationsToQueries(addRelations: Seq[Relation])() = {
    addRelations.map(relation => {
      val qPatterns = new QueryPatterns(resolvedItems)
      val query = qPatterns.queryRelation(relation)

      QueryConfig(relation, query, (graph: Graph, table: Table) => {
        if(graph.relations.size > 1)
          Left("More than one query result for relation: " + relation)
        else
          graph.relations.headOption.map(dbRelation => {
            resolvedItems += relation -> dbRelation.origin
            Right(() => {
              relation.properties.clear()
              relation.properties ++= dbRelation.properties
              relation.origin = dbRelation.origin
            })
          }).getOrElse(Left("Query result is missing desired relation: " + relation))
      })
    })
  }

  def addNodesToQueries(addNodes: Seq[Node])() = {
    addNodes.map(node => {
      val qPatterns = new QueryPatterns(resolvedItems)
      val query = qPatterns.queryNode(node)

      QueryConfig(node, query, (graph: Graph, table: Table) => {
        if(graph.nodes.size > 1)
          Left("More than one query result for node: " + node)
        else
          graph.nodes.headOption.map(dbNode => {
            resolvedItems += node -> dbNode.origin
            Right(() => {
              node.properties.clear()
              node.labels.clear()
              node.properties ++= dbNode.properties
              node.labels ++= dbNode.labels
              node.origin = dbNode.origin
            })
          }).getOrElse(Left("Query result is missing desired node: " + node))
      })
    })
  }
}

class PathDependencyGraph(paths: Set[Path]) {
  private val resolved: mutable.ArrayBuffer[Path] = mutable.ArrayBuffer.empty
  private val seen: mutable.Set[Path] = mutable.Set.empty
  private val pathCreators: Map[Node, Path] = paths.flatMap(p => p.nodes.map(_ -> p).toMap).toMap

  case class CircularException(path: Path, dependency: Path) extends Exception

  private def resolvePath(path: Path): Unit = {
    if(resolved.contains(path))
      return

    seen += path
    val readNodes = path.allNodes.diff(path.nodes).filter(_.origin.isLocal)
    val dependencies = readNodes.flatMap(pathCreators.get(_))
    dependencies.foreach(dep => {
      if(!resolved.contains(dep)) {
        if(seen.contains(dep)) {
          throw CircularException(path, dep)
        }

        resolvePath(dep)
      }
    })

    resolved += path
  }

  def resolvePaths: Either[String, Seq[Seq[Path]]] = {
    try {
      paths.foreach(p => resolvePath(p))
    } catch {
      case CircularException(path, dep) =>
        return Left(s"Circular dependency between paths: $path depends on $dep")
    }

    Right(resolved.map(Seq(_)))
  }
}

class QueryBuilder {

  protected def newQueryGenerator = new QueryGenerator

  protected def newPathDependencyGraph(paths: Set[Path]) = new PathDependencyGraph(paths)

  protected def filterGraphChanges(graphChanges: Seq[GraphChange]) = {
    // First get rid of duplicate changes, keep only the last change
    val reversedDistinctChanges = graphChanges.reverse.distinct

    // handle deletions
    val deletedItems = mutable.Set.empty[Item]
    val addedItems = mutable.Set.empty[Item]
    reversedDistinctChanges.filter {
      case DeleteItem(item)   =>
        if(addedItems.contains(item))
          false
        else {
          deletedItems += item
          true
        }
      case AddItem(item)      =>
        if(deletedItems.contains(item))
          false
        else {
          addedItems += item
          true
        }
      case c: GraphItemChange =>
        !deletedItems.contains(c.item)
      case _                  =>
        true
    }.reverse
  }

  protected def checkChanges(allChanges: Seq[GraphChange], deleteItems: Seq[Item], deletePaths: Seq[Path], addPaths: Seq[Path], addLocalPaths: Seq[Path], addRelations: Seq[Relation]): Option[String] = {
    val illegalChange = allChanges.find(!_.isValid)
    if(illegalChange.isDefined)
      return Some("Found invalid graph change: " + illegalChange.get)

    // check whether paths try to resolve the same items
    val pathNodes = addPaths.flatMap(_.nodes)
    if(pathNodes.distinct.size != pathNodes.size) {
      return Some("Paths cannot resolve the same nodes")
    }

    val pathRelations = addPaths.flatMap(_.relations)
    if(pathRelations.distinct.size != pathRelations.size) {
      return Some("Paths cannot resolve the same relations")
    }

    // check whether nodes in a new relation should also be deleted
    if(deleteItems.intersect(addRelations.flatMap(r => Seq(r.startNode, r.endNode))).nonEmpty) {
      return Some("Cannot delete start- or endnode of a new relation: " + deleteItems.mkString(","))
    }

    // each path can only be deleted as a whole, there is no defined way to match partial paths.
    val incompletePath = deletePaths.find { path =>
      val pathItems = path.nodes ++ path.relations
      val intersection = deleteItems.intersect(pathItems)
      pathItems.length != intersection.length
    }

    if (incompletePath.isDefined)
      return Some("Cannot partially match paths, will not delete parts of a path: " + incompletePath.get)

    None
  }

  def generateQueries(graphChanges: Seq[GraphChange]): Either[String, Seq[() => Seq[QueryConfig]]] = {
    val gen = newQueryGenerator

    val changes = filterGraphChanges(graphChanges)

    val deleteItems = changes.collect { case DeleteItem(i) => i }

    // gather content changes
    val contentChanges = changes.collect { case c: GraphContentChange => c }

    // gather path changes, we ignore all paths that reference deleted items
    val (deletePaths, addPaths) = changes.collect { case AddPath(p) => p }.partition(p => deleteItems.intersect(p.allNodes ++ p.relations).nonEmpty)
    val (addLocalProducePaths, addLocalRelationPaths, addNonLocalPaths) = {
      val (addLocalPaths, addNonLocalPaths) = addPaths.partition(p => p.allNodes.diff(p.nodes).exists(_.origin.isLocal))
      val (addLocalProducePaths, addLocalRelationPaths) = addLocalPaths.partition(p => p.nodes.nonEmpty)

      (addLocalProducePaths, addLocalRelationPaths, addNonLocalPaths)
    }

    // all add-relation and add-node changes that are already included in paths need to be ignored,
    // as they are handled when adding the paths itself
    val addRelations = changes.collect { case AddItem(r: Relation) => r }.filterNot(addPaths.flatMap(_.relations).toSet)
    val (addLocalRelations, addNonLocalRelations) = addRelations.partition(r => r.startNode.origin.isLocal || r.endNode.origin.isLocal)
    val addNodes = changes.collect { case AddItem(n: Node) => n }.filterNot(addPaths.flatMap(_.nodes).toSet)

    // translate origin of delete items. merge nodes and relations can be
    // matched via their merge properties, so we can delete them like match
    // items.
    deleteItems.foreach( item => item.origin = item.origin match {
        case Merge(merge, _) => Match(merge)
        case other           => other
    })

    deletePaths.foreach( item => item.origin = item.origin match {
        case Merge(merge, _) => Match(merge)
        case other           => other
    })

    checkChanges(changes, deleteItems, deletePaths, addPaths, addLocalProducePaths, addRelations) match {
      case Some(err) => return Left(err)
      case None      =>
    }

    // gather delete changes
    val (independentDeleteItems, dependentDeleteRelations) = {
      val filteredDeleteItems = deleteItems.filterNot(deletePaths.flatMap(p => p.nodes ++ p.relations).toSet)
      val deleteNodes = filteredDeleteItems.collect { case n: Node => n }
      val filteredDeleteNodes = deleteNodes.filter(i => !i.origin.isLocal || i.origin.kind == Match.kind)
      val deleteRelations = filteredDeleteItems.collect { case r:Relation => r }.filter { relation =>
        // deleting a node deletes all connected relations, thus relation
        // deletion is already handled if the start- or endnode is deleted.
        // create relation can be ignored
        !deleteNodes.contains(relation.startNode) && !deleteNodes.contains(relation.endNode) && (!relation.origin.isLocal || relation.origin.kind == Match.kind)
      }

      // split delete changes into dependent and independent changes
      // independent changes that are directly handled in the first request:
      // 1. all nodes
      // 2. relations that can be referenced by their id (non-local relation)
      // 2. relations that can be referenced by the id of their non-local start- and endnode
      // all other delete changes are send in the last request, when everything is resolved
      val (deleteLocalRelations, deleteNonLocalRelations) = deleteRelations.partition(r => r.origin.isLocal && (r.startNode.origin.isLocal || r.endNode.origin.isLocal))

      (filteredDeleteNodes ++ deleteNonLocalRelations, deleteLocalRelations)
    }

    val filteredDeletePaths = deletePaths.filter(p => !p.origin.isLocal || p.origin.kind == Match.kind)

    // independent changes that do not dependent on the results of any other
    // change request and can be sent in one request
    val independentChanges = {
      val contentChangeQueries = gen.contentChangesToQueries(contentChanges) _
      val deleteQueries = gen.deletionToQueries(independentDeleteItems) _
      val addNodeQueries = gen.addNodesToQueries(addNodes) _
      val addNonLocalPathQueries = gen.addPathsToQueries(addNonLocalPaths) _
      val addNonLocalRelationQueries = gen.addRelationsToQueries(addNonLocalRelations) _

      () => contentChangeQueries() ++
        deleteQueries() ++
        addNodeQueries() ++
        addNonLocalPathQueries() ++
        addNonLocalRelationQueries()
    }

    // node producing paths that can be referenced by relations and might
    // reference nodes created in the first request the dependency graph
    // resolves dependencies between the paths, and sends a request for each
    // independent chunk
    val localProducePathChunks = newPathDependencyGraph(addLocalProducePaths.toSet).resolvePaths match {
      case Left(err)     => return Left(err)
      case Right(chunks) => chunks.map(c => gen.addPathsToQueries(c) _)
    }

    // after all possible node producing queries are resolved, the relations
    // can be handled in the last request
    val relationChanges = {
      val addLocalRelationQueries = gen.addRelationsToQueries(addLocalRelations) _
      val addLocalRelationPathQueries = gen.addPathsToQueries(addLocalRelationPaths) _
      val deleteQueries = gen.deletionToQueries(dependentDeleteRelations) _
      val deletePathQueries = gen.deletionPathsToQueries(filteredDeletePaths) _

      () => addLocalRelationQueries() ++
        addLocalRelationPathQueries() ++
        deleteQueries() ++
        deletePathQueries()
    }

    Right(
      Seq(independentChanges) ++
        localProducePathChunks ++
        Seq(relationChanges)
    )
  }

  def applyQueries(queryRequests: Seq[() => Seq[QueryConfig]], queryHandler: (Seq[Query]) => Seq[(Graph, Table)]): Option[String] = {
    val handles = queryRequests.view.flatMap(getter => {
      val configs = getter()
      if (configs.isEmpty)
        Seq.empty
      else {
        val (queries, callbacks) = configs.map(c => (c.query, c.callback)).unzip
        queryHandler(queries).zip(callbacks).view.map { case ((g, t), f) => f(g, t) }
      }
    })

    var failure: Option[String] = None
    val successHandles = handles.takeWhile(h => {
      failure = h.left.toOption
      failure.isEmpty
    }).force

    if(failure.isEmpty)
      successHandles.foreach(_.right.get())

    failure
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy