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

org.neo4j.cypher.internal.ir.QueryGraph.scala Maven / Gradle / Ivy

There is a newer version: 5.25.1
Show newest version
/*
 * Copyright (c) "Neo4j"
 * Neo4j Sweden AB [https://neo4j.com]
 *
 * This file is part of Neo4j.
 *
 * Neo4j is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see .
 */
package org.neo4j.cypher.internal.ir

import org.neo4j.cypher.internal.ast.Hint
import org.neo4j.cypher.internal.ast.UsingJoinHint
import org.neo4j.cypher.internal.ast.UsingStatefulShortestPathAll
import org.neo4j.cypher.internal.ast.UsingStatefulShortestPathHint
import org.neo4j.cypher.internal.ast.UsingStatefulShortestPathInto
import org.neo4j.cypher.internal.ast.prettifier.ExpressionStringifier
import org.neo4j.cypher.internal.expressions.Expression
import org.neo4j.cypher.internal.expressions.LabelName
import org.neo4j.cypher.internal.expressions.LogicalVariable
import org.neo4j.cypher.internal.expressions.PartialPredicate
import org.neo4j.cypher.internal.expressions.PropertyKeyName
import org.neo4j.cypher.internal.expressions.RelTypeName
import org.neo4j.cypher.internal.ir.ast.IRExpression
import org.neo4j.cypher.internal.ir.helpers.ExpressionConverters.PredicateConverter
import org.neo4j.cypher.internal.util.Foldable.FoldableAny

import scala.collection.mutable
import scala.collection.mutable.ArrayBuffer
import scala.runtime.ScalaRunTime

/**
 * This is one of the core classes used during query planning. It represents the declarative query,
 * it contains no more information than the AST, but it contains data in a format that is easier
 * to consume by the planner. If you want to trace this back to the original query - one QueryGraph
 * represents all the MATCH, OPTIONAL MATCHes, and update clauses between two WITHs.
 *
 * A query graph has these contracts. All methods in this class that return a QueryGraph must uphold these contracts.
 * copy is hidden from the outside to avoid the creation of QueryGraphs not adhering to these contracts.
 * apply is overridden to avoid the creation of QueryGraphs not adhering to these contracts.
 * {{{
 *   qg.nodeConnections.flatMap(_.boundaryNodesSet).subsetOf(qg.patternNodes)
 *
 *   qg.shortestRelationshipPatterns.flatMap(_.rel.boundaryNodesSet).subsetOf(qg.patternNodes)
 * }}}
 *
 * @param shortestRelationshipPatterns shortest path patterns coming from shortestPath() or allShortestPaths(), made of a single var-length relationship and its endpoints.
 * @param selectivePathPatterns        path selector such as ANY k, SHORTEST k, or SHORTEST k GROUPS, applied to a path pattern introduced as part of Graph Pattern Matching.
 * @param patternNodes                 unconditional singleton pattern nodes excluding strict interior pattern nodes of selective path patterns.
 *                                     These can be connected to each other via node connections, creating connected components, and can potentially be used for node leaf plans.
 */
final case class QueryGraph private (
  patternRelationships: Set[PatternRelationship] = Set.empty,
  quantifiedPathPatterns: Set[QuantifiedPathPattern] = Set.empty,
  patternNodes: Set[LogicalVariable] = Set.empty,
  argumentIds: Set[LogicalVariable] = Set.empty,
  selections: Selections = Selections(),
  optionalMatches: IndexedSeq[QueryGraph] = Vector.empty,
  hints: Set[Hint] = Set.empty,
  shortestRelationshipPatterns: Set[ShortestRelationshipPattern] = Set.empty,
  mutatingPatterns: IndexedSeq[MutatingPattern] = IndexedSeq.empty,
  selectivePathPatterns: Set[SelectivePathPattern] = Set.empty
  // !!! If you change anything here, make sure to update the equals, ++ and hashCode methods at the bottom of this class !!!
) extends UpdateGraph {

  // This is here to stop usage of copy from the outside
  protected def copy(
    patternRelationships: Set[PatternRelationship] = patternRelationships,
    quantifiedPathPatterns: Set[QuantifiedPathPattern] = quantifiedPathPatterns,
    patternNodes: Set[LogicalVariable] = patternNodes,
    argumentIds: Set[LogicalVariable] = argumentIds,
    selections: Selections = selections,
    optionalMatches: IndexedSeq[QueryGraph] = optionalMatches,
    hints: Set[Hint] = hints,
    shortestRelationshipPatterns: Set[ShortestRelationshipPattern] = shortestRelationshipPatterns,
    mutatingPatterns: IndexedSeq[MutatingPattern] = mutatingPatterns,
    selectivePathPatterns: Set[SelectivePathPattern] = selectivePathPatterns
  ): QueryGraph =
    new QueryGraph(
      patternRelationships,
      quantifiedPathPatterns,
      patternNodes,
      argumentIds,
      selections,
      optionalMatches,
      hints,
      shortestRelationshipPatterns,
      mutatingPatterns,
      selectivePathPatterns
    )

  val nodeConnections: Set[NodeConnection] = Set.empty[NodeConnection] ++
    patternRelationships ++
    quantifiedPathPatterns ++
    selectivePathPatterns

  /**
   * @return all recursively included query graphs, with leaf information for Eagerness analysis.
   *         Query graphs from pattern expressions and pattern comprehensions will generate variable names that might clash with existing names, so this method
   *         is not safe to use for planning pattern expressions and pattern comprehensions.
   */
  lazy val allQGsWithLeafInfo: Seq[QgWithLeafInfo] = {
    val iRExpressions: Seq[QgWithLeafInfo] = this.folder.findAllByClass[IRExpression].flatMap((e: IRExpression) =>
      e.query.allQGsWithLeafInfo
    )
    QgWithLeafInfo.qgWithNoStableIdentifierAndOnlyLeaves(this) +:
      (iRExpressions ++
        optionalMatches.flatMap(_.allQGsWithLeafInfo))
  }

  // -----------------
  // Override elements
  // -----------------

  def withPatternNodes(nodes: Set[LogicalVariable]): QueryGraph = copy(patternNodes = nodes)

  def withArgumentIds(newArgumentIds: Set[LogicalVariable]): QueryGraph = copy(argumentIds = newArgumentIds)

  def withOptionalMatches(optionalMatches: IndexedSeq[QueryGraph]): QueryGraph = copy(optionalMatches = optionalMatches)

  def withSelections(selections: Selections): QueryGraph = copy(selections = selections)

  def withHints(hints: Set[Hint]): QueryGraph = copy(hints = hints)

  /**
   * Sets both patternNodes and patternRelationships from this pattern relationship. Compare with `addPatternRelationship`.
   * Note that other patternNodes and patternRelationships get removed.
   *
   * @param pattern the relationship defining the pattern of this query graph
   */
  def withPattern(pattern: PatternRelationship): QueryGraph =
    copy(
      patternNodes = pattern.boundaryNodesSet,
      patternRelationships = Set(pattern)
    )

  /**
   * Note that pattern nodes of these patterns are added, too.
   * Other pattern nodes are not removed.
   */
  def withPatternRelationships(patterns: Set[PatternRelationship]): QueryGraph =
    copy(
      patternNodes = patternNodes ++ patterns.flatMap(_.boundaryNodesSet),
      patternRelationships = patterns
    )

  /**
   * Note that pattern nodes of these patterns are added, too.
   * Other pattern nodes are not removed.
   */
  def withQuantifiedPathPatterns(patterns: Set[QuantifiedPathPattern]): QueryGraph =
    copy(
      patternNodes = patternNodes ++ patterns.flatMap(_.boundaryNodesSet),
      quantifiedPathPatterns = patterns
    )

  def withMutatingPattern(mutatingPatterns: IndexedSeq[MutatingPattern]): QueryGraph =
    copy(mutatingPatterns = mutatingPatterns)

  // ------------
  // Add elements
  // ------------

  def addPathPatterns(pathPatterns: PathPatterns): QueryGraph =
    pathPatterns.pathPatterns.foldLeft(this) {
      case (qg, pathPattern) => qg.addPathPattern(pathPattern)
    }

  def addPathPattern(pathPattern: PathPattern): QueryGraph =
    pathPattern match {
      case exhaustivePathPattern: ExhaustivePathPattern[_]   => addExhaustivePathPattern(exhaustivePathPattern)
      case selectivePathPattern: SelectivePathPattern        => addSelectivePathPattern(selectivePathPattern)
      case shortestRelationship: ShortestRelationshipPattern => addShortestRelationship(shortestRelationship)
    }

  private def addExhaustivePathPattern(pathPattern: ExhaustivePathPattern[_]): QueryGraph =
    pathPattern match {
      case ExhaustivePathPattern.SingleNode(variable)         => addPatternNodes(variable)
      case ExhaustivePathPattern.NodeConnections(connections) => addNodeConnections(connections.toIterable)
    }

  def addPatternNodes(nodes: LogicalVariable*): QueryGraph =
    copy(patternNodes = patternNodes ++ nodes)

  def addPatternRelationship(rel: PatternRelationship): QueryGraph =
    copy(
      patternNodes = patternNodes ++ rel.boundaryNodesSet,
      patternRelationships = patternRelationships + rel
    )

  def addNodeConnection(connection: NodeConnection): QueryGraph = {
    connection match {
      case patternRelationship: PatternRelationship => addPatternRelationship(patternRelationship)
      case qpp: QuantifiedPathPattern               => addQuantifiedPathPattern(qpp)
      case spp: SelectivePathPattern                => addSelectivePathPattern(spp)
    }
  }

  def addNodeConnections(connections: Iterable[NodeConnection]): QueryGraph =
    connections.foldLeft(this) {
      case (qg, connection) => qg.addNodeConnection(connection)
    }

  def addPatternRelationships(rels: Set[PatternRelationship]): QueryGraph =
    rels.foldLeft[QueryGraph](this)((qg, rel) => qg.addPatternRelationship(rel))

  def addQuantifiedPathPattern(pattern: QuantifiedPathPattern): QueryGraph =
    copy(
      patternNodes = patternNodes ++ pattern.boundaryNodesSet,
      quantifiedPathPatterns = quantifiedPathPatterns + pattern
    )

  def addShortestRelationship(shortestRelationship: ShortestRelationshipPattern): QueryGraph = {
    val rel = shortestRelationship.rel
    copy(
      patternNodes = patternNodes ++ rel.boundaryNodesSet,
      shortestRelationshipPatterns = shortestRelationshipPatterns + shortestRelationship
    )
  }

  def addShortestRelationships(shortestRelationships: ShortestRelationshipPattern*): QueryGraph =
    shortestRelationships.foldLeft(this)((qg, p) => qg.addShortestRelationship(p))

  /**
   * Returns a copy of the query graph, with an additional selective path pattern added.
   */
  def addSelectivePathPattern(selectivePathPattern: SelectivePathPattern): QueryGraph =
    copy(
      patternNodes = patternNodes ++ selectivePathPattern.boundaryNodesSet,
      selectivePathPatterns = selectivePathPatterns + selectivePathPattern
    )

  def addArgumentId(newId: LogicalVariable): QueryGraph = copy(argumentIds = argumentIds + newId)

  def addArgumentIds(newIds: Iterable[LogicalVariable]): QueryGraph = copy(argumentIds = argumentIds ++ newIds)

  def addSelections(selections: Selections): QueryGraph =
    copy(selections = Selections(selections.predicates ++ this.selections.predicates))

  def addPredicates(predicates: Expression*): QueryGraph = {
    val newSelections = Selections(predicates.flatMap(_.asPredicates).toSet)
    copy(selections = selections ++ newSelections)
  }

  def addPredicates(predicates: Set[Predicate]): QueryGraph = {
    val newSelections = Selections(selections.predicates ++ predicates)
    copy(selections = newSelections)
  }

  def addPredicates(outerScope: Set[LogicalVariable], predicates: Expression*): QueryGraph = {
    val newSelections = Selections(predicates.flatMap(_.asPredicates(outerScope)).toSet)
    copy(selections = selections ++ newSelections)
  }

  def addHints(addedHints: IterableOnce[Hint]): QueryGraph = {
    copy(hints = hints ++ addedHints)
  }

  def addOptionalMatch(optionalMatch: QueryGraph): QueryGraph = {
    val argumentIds = allCoveredIds intersect optionalMatch.allCoveredIds
    copy(optionalMatches = optionalMatches :+ optionalMatch.addArgumentIds(argumentIds.toIndexedSeq))
  }

  def addMutatingPatterns(pattern: MutatingPattern): QueryGraph = {
    val copyPatterns = new mutable.ArrayBuffer[MutatingPattern](mutatingPatterns.size + 1)
    copyPatterns.appendAll(mutatingPatterns)
    copyPatterns += pattern

    copy(mutatingPatterns = copyPatterns.toIndexedSeq)
  }

  def addMutatingPatterns(patterns: Seq[MutatingPattern]): QueryGraph = {
    val copyPatterns = new ArrayBuffer[MutatingPattern](patterns.size)
    copyPatterns.appendAll(mutatingPatterns)
    copyPatterns.appendAll(patterns)
    copy(mutatingPatterns = copyPatterns.toIndexedSeq)
  }

  // ---------------
  // Remove elements
  // ---------------

  def removeArgumentIds(idsToRemove: Iterable[LogicalVariable]): QueryGraph =
    copy(argumentIds = argumentIds -- idsToRemove)

  def removePredicates(predicates: Set[Predicate]): QueryGraph = {
    val newSelections = Selections(selections.predicates -- predicates)
    copy(selections = newSelections)
  }

  def removeHints(hintsToIgnore: Set[Hint]): QueryGraph = copy(
    hints = hints.diff(hintsToIgnore),
    optionalMatches = optionalMatches.map(_.removeHints(hintsToIgnore))
  )

  def removeArguments(): QueryGraph = withArgumentIds(Set.empty)

  /**
   * Removes a pattern relationship.
   * Note that this method does not remove the boundary nodes from pattern nodes.
   */
  def removePatternRelationship(pattern: PatternRelationship): QueryGraph =
    copy(patternRelationships = patternRelationships - pattern)

  // ----------
  // Other defs
  // ----------

  /**
   * Dependencies from this QG to variables - from WHERE predicates and update clauses using expressions
   */
  def dependencies: Set[LogicalVariable] =
    optionalMatches.flatMap(_.dependencies).toSet ++
      selections.predicates.flatMap(_.dependencies) ++
      mutatingPatterns.flatMap(_.dependencies) ++
      quantifiedPathPatterns.flatMap(_.dependencies) ++
      selectivePathPatterns.flatMap(_.dependencies) ++
      argumentIds

  /**
   * The size of a QG is defined as the number of node connections that are introduced
   */
  def size: Int = nodeConnections.size

  def isEmpty: Boolean = this == QueryGraph.empty

  def nonEmpty: Boolean = !isEmpty

  def mapSelections(f: Selections => Selections): QueryGraph =
    copy(
      selections = f(selections),
      optionalMatches = optionalMatches.map(_.mapSelections(f)),
      quantifiedPathPatterns = quantifiedPathPatterns.map(qpp => qpp.copy(selections = f(qpp.selections))),
      selectivePathPatterns = selectivePathPatterns.map(spp => spp.copy(selections = f(spp.selections)))
    )

  /**
   * All unconditional singleton nodes of this query graph.
   * This includes not only the MATCH but also pattern nodes from CREATE and MERGE.
   */
  def allPatternNodes: Set[LogicalVariable] = {
    val nodes = mutable.Set[LogicalVariable]()
    collectAllPatternNodes(nodes.add)
    nodes.toSet
  }

  private def collectAllPatternNodes(f: LogicalVariable => Unit): Unit = {
    patternNodes.foreach(f)
    selectivePathPatterns.foreach(_.pathPattern.connections.iterator.foreach(_.boundaryNodesSet.foreach(f)))
    optionalMatches.foreach(m => m.allPatternNodes.foreach(f))
    for {
      create <- createPatterns
      createNode <- create.nodes
    } {
      f(createNode.variable)
    }
    mergeNodePatterns.foreach(p => f(p.createNode.variable))
    mergeRelationshipPatterns.foreach(p => p.createNodes.foreach(pp => f(pp.variable)))
  }

  def allPatternRelationshipsRead: Set[PatternRelationship] =
    patternRelationships ++
      optionalMatches.flatMap(_.allPatternRelationshipsRead) ++
      shortestRelationshipPatterns.map(_.rel) ++
      quantifiedPathPatterns.flatMap(_.asQueryGraph.allPatternRelationshipsRead) ++
      selectivePathPatterns.flatMap(_.asQueryGraph.allPatternRelationshipsRead)

  def allPatternNodesRead: Set[LogicalVariable] =
    patternNodes ++
      optionalMatches.flatMap(_.allPatternNodesRead) ++
      quantifiedPathPatterns.flatMap(_.asQueryGraph.allPatternNodesRead) ++
      selectivePathPatterns.flatMap(_.asQueryGraph.allPatternNodesRead)

  private def knownProperties(entity: LogicalVariable): Set[PropertyKeyName] =
    selections.allPropertyPredicatesInvolving.getOrElse(entity, Set.empty).map(_.propertyKey)

  private def possibleLabelsOnNode(node: LogicalVariable): Set[LabelName] = {
    val label = selections
      .allHasLabelsInvolving.getOrElse(node, Set.empty)
      .flatMap(_.labels)
    val labelOrType = selections
      .allHasLabelsOrTypesInvolving.getOrElse(node, Set.empty)
      .flatMap(_.labelsOrTypes).map(_.asLabelName)
    label ++ labelOrType
  }

  def inlinedRelTypes(rel: LogicalVariable): Set[RelTypeName] = {
    patternRelationships
      .find(_.variable == rel)
      .toSet[PatternRelationship]
      .flatMap(_.types.toSet)
  }

  private def possibleTypesOnRel(rel: LogicalVariable): Set[RelTypeName] = {
    val whereClauseTypes = selections
      .allHasTypesInvolving.getOrElse(rel, Set.empty)
      .flatMap(_.types)

    val whereClauseLabelOrTypes = selections
      .allHasLabelsOrTypesInvolving.getOrElse(rel, Set.empty)
      .flatMap(_.labelsOrTypes).map(lblOrType => RelTypeName(lblOrType.name)(lblOrType.position))

    inlinedRelTypes(rel) ++ whereClauseTypes ++ whereClauseLabelOrTypes
  }

  private def traverseAllQueryGraphs[A](f: QueryGraph => Set[A]): Set[A] =
    f(this) ++
      optionalMatches.flatMap(_.traverseAllQueryGraphs(f)) ++
      quantifiedPathPatterns.flatMap(_.asQueryGraph.traverseAllQueryGraphs(f)) ++
      selectivePathPatterns.flatMap(_.asQueryGraph.traverseAllQueryGraphs(f))

  def allPossibleLabelsOnNode(node: LogicalVariable): Set[LabelName] =
    traverseAllQueryGraphs(_.possibleLabelsOnNode(node))

  def allPossibleTypesOnRel(rel: LogicalVariable): Set[RelTypeName] =
    traverseAllQueryGraphs(_.possibleTypesOnRel(rel))

  def allKnownPropertiesOnIdentifier(variable: LogicalVariable): Set[PropertyKeyName] =
    traverseAllQueryGraphs(_.knownProperties(variable))

  def allSelections: Selections =
    Selections(traverseAllQueryGraphs(_.selections.predicates))

  def coveredIdsForPatterns: Set[LogicalVariable] = {
    val patternRelIds = nodeConnections.flatMap(_.coveredIds)
    patternNodes ++ patternRelIds
  }

  /**
   * Variables that are bound after matching this QG, but before optional
   * matches and updates have been applied
   */
  def idsWithoutOptionalMatchesOrUpdates: Set[LogicalVariable] =
    coveredIdsForPatterns ++
      argumentIds ++
      shortestRelationshipPatterns.flatMap(_.maybePathVar)

  /**
   * All variables that are bound after this QG has been matched
   */
  def allCoveredIds: Set[LogicalVariable] = {
    val otherSymbols = optionalMatches.flatMap(_.allCoveredIds) ++ mutatingPatterns.flatMap(_.coveredIds)
    idsWithoutOptionalMatchesOrUpdates ++ otherSymbols
  }

  def allHints: Set[Hint] =
    hints ++ optionalMatches.flatMap(_.allHints)

  def ++(other: QueryGraph): QueryGraph = {
    other match {
      case QueryGraph(
          otherPatternRelationships,
          otherQuantifiedPathPatterns,
          otherPatternNodes,
          otherArgumentIds,
          otherSelections,
          otherOptionalMatches,
          otherHints,
          otherShortestRelationshipPatterns,
          otherMutatingPatterns,
          otherSelectivePathPatterns
        ) =>
        QueryGraph(
          selections = selections ++ otherSelections,
          patternNodes = patternNodes ++ otherPatternNodes,
          quantifiedPathPatterns = quantifiedPathPatterns ++ otherQuantifiedPathPatterns,
          patternRelationships = patternRelationships ++ otherPatternRelationships,
          optionalMatches = optionalMatches ++ otherOptionalMatches,
          argumentIds = argumentIds ++ otherArgumentIds,
          hints = hints ++ otherHints,
          shortestRelationshipPatterns = shortestRelationshipPatterns ++ otherShortestRelationshipPatterns,
          mutatingPatterns = mutatingPatterns ++ otherMutatingPatterns,
          selectivePathPatterns = selectivePathPatterns ++ otherSelectivePathPatterns
        )
    }
  }

  def hasOptionalPatterns: Boolean = optionalMatches.nonEmpty

  def patternNodeLabels: Map[LogicalVariable, Set[LabelName]] = {
    // Node label predicates are extracted from the pattern nodes to predicates in LabelPredicateNormalizer.
    // Therefore, we only need to look in selections.
    val labelsOnPatternNodes = patternNodes.toVector.map(node => node -> selections.labelsOnNode(node))

    val labelsOnSelectivePathPatternNodes =
      for {
        selectivePathPattern <- selectivePathPatterns.toVector
        connection <- selectivePathPattern.pathPattern.connections.toIterable
        node <- connection.boundaryNodesSet
      } yield node -> (selections ++ selectivePathPattern.selections).labelsOnNode(node)

    (labelsOnPatternNodes ++ labelsOnSelectivePathPatternNodes).groupMapReduce(_._1)(_._2)(_.union(_))
  }

  def patternRelationshipTypes: Map[LogicalVariable, RelTypeName] = {
    // Pattern relationship type predicates are inlined in PlannerQueryBuilder::inlineRelationshipTypePredicates().
    // Therefore, we don't need to look at predicates in selections.
    val selectivePathPatternsRelationships =
      for {
        selectivePathPattern <- selectivePathPatterns.toVector
        connection <- selectivePathPattern.pathPattern.connections.toIterable
        relationship <- connection match {
          case patternRelationship: PatternRelationship => Some(patternRelationship)
          case _: QuantifiedPathPattern                 => None
        }
      } yield relationship

    val allRelationships = patternRelationships ++ selectivePathPatternsRelationships

    allRelationships.collect {
      case PatternRelationship(rel, _, _, Seq(relType), _) => rel -> relType
    }.toMap
  }

  /**
   * Returns the connected patterns of this query graph where each connected pattern is represented by a QG.
   * Connected here means can be reached through a relationship pattern.
   * Does not include optional matches, shortest paths or predicates that have dependencies across multiple of the
   * connected query graphs.
   */
  def connectedComponents: Seq[QueryGraph] = {
    val visited = mutable.Set.empty[LogicalVariable]

    val (predicatesWithLocalDependencies, strayPredicates) = selections.predicates.partition {
      p => (p.dependencies -- argumentIds).nonEmpty
    }

    def createComponentQueryGraphStartingFrom(patternNode: LogicalVariable): QueryGraph = {
      val qg = connectedComponentFor(patternNode, visited)
      val coveredIds = qg.idsWithoutOptionalMatchesOrUpdates
      val shortestRelationships = shortestRelationshipPatterns.filter {
        _.rel.boundaryNodesSet.forall(coveredIds.contains)
      }
      val shortestPathIds = shortestRelationships.flatMap(p => Set(p.rel.variable) ++ p.maybePathVar)
      val allIds = coveredIds ++ argumentIds ++ shortestPathIds

      val predicates = predicatesWithLocalDependencies.filter(_.dependencies.subsetOf(allIds))
      val filteredHints = hints.filter(_.variables.forall(coveredIds.contains))
      qg.withSelections(Selections(predicates))
        .withArgumentIds(argumentIds)
        .addHints(filteredHints)
        .addShortestRelationships(shortestRelationships.toIndexedSeq: _*)
    }

    /*
    We want the components that have patterns connected to arguments to be planned first, so we do not pull in arguments
    to other components by mistake
     */
    val argumentComponents = (patternNodes intersect argumentIds).toIndexedSeq.collect {
      case patternNode if !visited(patternNode) =>
        createComponentQueryGraphStartingFrom(patternNode)
    }

    val rest = patternNodes.toIndexedSeq.collect {
      case patternNode if !visited(patternNode) =>
        createComponentQueryGraphStartingFrom(patternNode)
    }

    (argumentComponents ++ rest) match {
      case first +: rest =>
        first.addPredicates(strayPredicates) +: rest
      case x => x
    }
  }

  def joinHints: Set[UsingJoinHint] =
    hints.collect { case hint: UsingJoinHint => hint }

  def statefulShortestPathIntoHints: Set[UsingStatefulShortestPathHint] =
    hints.collect { case hint: UsingStatefulShortestPathInto => hint }

  def statefulShortestPathAllHints: Set[UsingStatefulShortestPathHint] =
    hints.collect { case hint: UsingStatefulShortestPathAll => hint }

  private def connectedComponentFor(startNode: LogicalVariable, visited: mutable.Set[LogicalVariable]): QueryGraph = {
    val queue = mutable.Queue(startNode)
    var connectedComponent = QueryGraph.empty
    while (queue.nonEmpty) {
      val node = queue.dequeue()
      if (!visited(node)) {
        visited += node

        val (nodeConnections, nodes) = findConnectedEntities(node, connectedComponent)

        queue.enqueueAll(nodes)

        connectedComponent = connectedComponent
          .addPatternNodes(node)
          .addNodeConnections(nodeConnections)

        val alreadyHaveArguments = connectedComponent.argumentIds.nonEmpty

        if (
          !alreadyHaveArguments && (argumentsOverLapsWith(
            connectedComponent.idsWithoutOptionalMatchesOrUpdates
          ) || predicatePullsInArguments(node))
        ) {
          connectedComponent = connectedComponent.withArgumentIds(argumentIds)
          val nodesSolvedByArguments = patternNodes intersect connectedComponent.argumentIds
          queue.enqueueAll(nodesSolvedByArguments.toIndexedSeq)
        }
      }
    }
    connectedComponent
  }

  private def findConnectedEntities(
    node: LogicalVariable,
    connectedComponent: QueryGraph
  ): (Set[NodeConnection], Set[LogicalVariable]) = {

    // All node connections that either have `node` as the left or the right node.
    val nodeConnectionsOfNode = nodeConnections.filter { nc =>
      nc.boundaryNodesSet.contains(node) && !connectedComponent.nodeConnections.contains(nc)
    }
    // All nodes that get connected through `nodeConnectionsOfNode`
    val nodesConnectedThroughOneConnection = nodeConnectionsOfNode.map(_.otherSide(node))

    // `(a)-[r]->(b), (c)-[r]->(d)` are connected through both relationships being named `r`.
    val patternRelationshipsWithSameName =
      patternRelationships.filterNot(nodeConnectionsOfNode).filter { r =>
        nodeConnectionsOfNode.exists {
          case r2: PatternRelationship if r.variable == r2.variable => true
          case _                                                    => false
        }
      }
    // All nodes that get connected through `patternRelationshipsWithSameName`
    val patternRelationshipsWithSameNameNodes = patternRelationshipsWithSameName.flatMap(r => Seq(r.left, r.right))

    (
      nodeConnectionsOfNode ++ patternRelationshipsWithSameName,
      (nodesConnectedThroughOneConnection ++ patternRelationshipsWithSameNameNodes)
    )
  }

  private def argumentsOverLapsWith(coveredIds: Set[LogicalVariable]): Boolean =
    (argumentIds intersect coveredIds).nonEmpty

  private def predicatePullsInArguments(node: LogicalVariable): Boolean = selections.flatPredicates.exists { p =>
    val dependencies = p.dependencies
    dependencies(node) && (dependencies intersect argumentIds).nonEmpty
  }

  def standaloneArgumentPatternNodes: Set[LogicalVariable] = {
    patternNodes
      .intersect(argumentIds)
      .diff(nodeConnections.flatMap(_.coveredIds))
      .diff(shortestRelationshipPatterns.flatMap(_.rel.coveredIds))
  }

  override def toString: String = {
    var added = false
    val builder = new StringBuilder("QueryGraph {")
    def addSetIfNonEmpty[T](s: Iterable[T], name: String, f: T => String): Unit = {
      if (s.nonEmpty) {
        if (added)
          builder.append(", ")
        else
          added = true

        val sortedInput = if (s.isInstanceOf[Set[_]]) s.map(x => f(x)).toSeq.sorted else s.map(f)
        builder.append(s"$name: ").append(sortedInput.mkString("['", "', '", "']"))
      }
    }

    addSetIfNonEmpty(patternNodes, "Nodes", (_: LogicalVariable).name)
    addSetIfNonEmpty(patternRelationships, "Rels", (_: PatternRelationship).toString)
    addSetIfNonEmpty(quantifiedPathPatterns, "Quantified path patterns", (_: QuantifiedPathPattern).toString)
    addSetIfNonEmpty(argumentIds, "Arguments", (_: LogicalVariable).name)
    addSetIfNonEmpty(selections.flatPredicates, "Predicates", (e: Expression) => QueryGraph.stringifier.apply(e))
    addSetIfNonEmpty(shortestRelationshipPatterns, "Shortest relationships", (_: ShortestRelationshipPattern).toString)
    addSetIfNonEmpty(optionalMatches, "Optional Matches: ", (_: QueryGraph).toString)
    addSetIfNonEmpty(hints, "Hints", (_: Hint).toString)
    addSetIfNonEmpty(mutatingPatterns, "MutatingPatterns", (_: MutatingPattern).toString)
    addSetIfNonEmpty(selectivePathPatterns, "SelectivePathPatterns", (_: SelectivePathPattern).toString)

    builder.append("}")
    builder.toString()
  }

  /**
   * We have to do this special treatment of QG to avoid problems when checking that the produced plan actually
   * solves what we set out to solve. In some rare circumstances, we'll get a few optional matches that are independent of each other.
   *
   * Given the way our planner works, it can unpredictably plan these optional matches in different orders, which leads to an exception being thrown when
   * checking that the correct query has been solved.
   */
  override def equals(in: Any): Boolean = {
    in match {
      case other @ QueryGraph(
          otherPatternRelationships,
          otherQuantifiedPathPatterns,
          otherPatternNodes,
          otherArgumentIds,
          otherSelections,
          otherOptionalMatches,
          otherHints,
          otherShortestRelationshipPatterns,
          otherMutatingPatterns,
          otherSelectivePathPatterns
        ) =>
        if (this eq other) {
          true
        } else {
          patternRelationships == otherPatternRelationships &&
          quantifiedPathPatterns == otherQuantifiedPathPatterns &&
          patternNodes == otherPatternNodes &&
          argumentIds == otherArgumentIds &&
          selections == otherSelections &&
          optionalMatches.toSet == otherOptionalMatches.toSet &&
          hints == otherHints &&
          shortestRelationshipPatterns == otherShortestRelationshipPatterns &&
          mutatingPatterns == otherMutatingPatterns &&
          selectivePathPatterns == otherSelectivePathPatterns
        }
      case _ =>
        false
    }
  }

  override lazy val hashCode: Int = this match {
    // The point of this "useless" match case is to catch your attention if you modified the fields of the QueryGraph.
    // Please remember to update the hash code.
    case QueryGraph(_, _, _, _, _, _, _, _, _, _) =>
      ScalaRunTime._hashCode((
        patternRelationships,
        quantifiedPathPatterns,
        patternNodes,
        argumentIds,
        selections,
        optionalMatches.toSet,
        hints.groupBy(identity),
        shortestRelationshipPatterns,
        mutatingPatterns,
        selectivePathPatterns
      ))
  }

}

object QueryGraph {
  def empty: QueryGraph = new QueryGraph()

  // Overridden to avoid creating illegal QGs
  def apply(
    patternRelationships: Set[PatternRelationship] = Set.empty,
    quantifiedPathPatterns: Set[QuantifiedPathPattern] = Set.empty,
    patternNodes: Set[LogicalVariable] = Set.empty,
    argumentIds: Set[LogicalVariable] = Set.empty,
    selections: Selections = Selections(),
    optionalMatches: IndexedSeq[QueryGraph] = Vector.empty,
    hints: Set[Hint] = Set.empty,
    shortestRelationshipPatterns: Set[ShortestRelationshipPattern] = Set.empty,
    mutatingPatterns: IndexedSeq[MutatingPattern] = IndexedSeq.empty,
    selectivePathPatterns: Set[SelectivePathPattern] = Set.empty
  ): QueryGraph = {
    val allPatternNodes = patternNodes ++
      patternRelationships.flatMap(_.boundaryNodesSet) ++
      quantifiedPathPatterns.flatMap(_.boundaryNodesSet) ++
      selectivePathPatterns.flatMap(_.boundaryNodesSet) ++
      shortestRelationshipPatterns.flatMap(_.rel.boundaryNodesSet)

    new QueryGraph(
      patternRelationships,
      quantifiedPathPatterns,
      allPatternNodes,
      argumentIds,
      selections,
      optionalMatches,
      hints,
      shortestRelationshipPatterns,
      mutatingPatterns,
      selectivePathPatterns
    )
  }

  val stringifier: ExpressionStringifier = ExpressionStringifier(
    extensionStringifier = new ExpressionStringifier.Extension {

      override def apply(ctx: ExpressionStringifier)(expression: Expression): String = expression match {
        case pp: PartialPredicate[_] => s"partial(${ctx(pp.coveredPredicate)}, ${ctx(pp.coveringPredicate)})"
        case e                       => e.asCanonicalStringVal
      }
    },
    alwaysParens = false,
    alwaysBacktick = false,
    preferSingleQuotes = false,
    sensitiveParamsAsParams = false
  )
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy