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

org.neo4j.cypher.internal.expressions.Pattern.scala Maven / Gradle / Ivy

There is a newer version: 5.24.0
Show newest version
/*
 * Copyright (c) "Neo4j"
 * Neo4j Sweden AB [https://neo4j.com]
 *
 * 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 org.neo4j.cypher.internal.expressions

import org.neo4j.cypher.internal.expressions.PatternPart.Selector
import org.neo4j.cypher.internal.label_expressions.LabelExpression
import org.neo4j.cypher.internal.util.ASTNode
import org.neo4j.cypher.internal.util.InputPosition

import scala.annotation.tailrec

/**
 * Represents a comma-separated list of pattern parts. Therefore, this is known in the parser as PatternList.
 * As we (in contrast to PatternParts) can use this to describe arbitrary shaped graph structures, GQL refers to these as graph patterns.
 */

sealed trait Pattern extends ASTNode {

  def patternParts: Seq[PatternPart]

  lazy val length: Int = this.folder.fold(0) {
    case RelationshipChain(_, _, _) => _ + 1
    case _                          => identity
  }
}

object Pattern {

  final case class ForMatch(patternParts: Seq[PatternPartWithSelector])(val position: InputPosition) extends Pattern

  final case class ForUpdate(patternParts: Seq[NonPrefixedPatternPart])(val position: InputPosition) extends Pattern
      with HasMappableExpressions[ForUpdate] {

    override def mapExpressions(f: Expression => Expression): ForUpdate =
      copy(patternParts.map(_.mapExpressions(f)))(this.position)
  }

  sealed trait SemanticContext {
    def name: String = SemanticContext.name(this)
    def description: String = SemanticContext.description(this)
  }

  object SemanticContext {
    case object Match extends SemanticContext
    case object Merge extends SemanticContext
    case object Create extends SemanticContext
    case object Insert extends SemanticContext
    case object Expression extends SemanticContext

    def name(ctx: SemanticContext): String = ctx match {
      case Match      => "MATCH"
      case Merge      => "MERGE"
      case Create     => "CREATE"
      case Insert     => "INSERT"
      case Expression => "expression"
    }

    def description(ctx: SemanticContext): String = ctx match {
      case Match      => "a MATCH clause"
      case Merge      => "a MERGE clause"
      case Create     => "a CREATE clause"
      case Insert     => "an INSERT clause"
      case Expression => "an expression"
    }
  }
}

case class RelationshipsPattern(element: RelationshipChain)(val position: InputPosition) extends ASTNode

/**
 * Represents one part in the comma-separated list of a pattern.
 *
 * In the parser, this is just referred to as Pattern.
 * As we (in contrast to pattern lists) can only use this to describe linear graph structures, GQL refers to these as path patterns.
 */
sealed abstract class PatternPart extends ASTNode {
  def allVariables: Set[LogicalVariable]
  def element: PatternElement
  def dependencies: Set[LogicalVariable]

  def isBounded: Boolean
  def isFixedLength: Boolean
}

sealed trait NonPrefixedPatternPart extends PatternPart with HasMappableExpressions[NonPrefixedPatternPart]

case class PatternPartWithSelector(selector: Selector, part: NonPrefixedPatternPart) extends PatternPart {
  override def position: InputPosition = part.position
  override def allVariables: Set[LogicalVariable] = part.allVariables
  override def element: PatternElement = part.element
  override def isBounded: Boolean = part.isBounded || selector.isBounded
  override def isFixedLength: Boolean = part.isFixedLength
  override def dependencies: Set[LogicalVariable] = part.dependencies

  def isSelective: Boolean = selector.isBounded

  def modifyElement(f: PatternElement => PatternElement): PatternPartWithSelector = {
    def replaceInAnonymous(app: AnonymousPatternPart): AnonymousPatternPart = app match {
      case p: PathPatternPart          => p.copy(element = f(p.element))
      case s: ShortestPathsPatternPart => s.copy(element = f(s.element))(s.position)
    }

    def replaceInNonPrefixed(npp: NonPrefixedPatternPart): NonPrefixedPatternPart = npp match {
      case npp: NamedPatternPart     => npp.copy(patternPart = replaceInAnonymous(npp.patternPart))(npp.position)
      case app: AnonymousPatternPart => replaceInAnonymous(app)
    }

    copy(part = replaceInNonPrefixed(part))
  }

  def replaceElement(newElement: PatternElement): PatternPartWithSelector =
    modifyElement(_ => newElement)
}

case class NamedPatternPart(variable: Variable, patternPart: AnonymousPatternPart)(val position: InputPosition)
    extends NonPrefixedPatternPart {
  override def element: PatternElement = patternPart.element

  override def allVariables: Set[LogicalVariable] = patternPart.allVariables + variable

  override def isBounded: Boolean = patternPart.isBounded

  override def isFixedLength: Boolean = patternPart.isFixedLength

  override def mapExpressions(f: Expression => Expression): NonPrefixedPatternPart =
    copy(patternPart = patternPart.mapExpressions(f).asInstanceOf[AnonymousPatternPart])(this.position)

  override def dependencies: Set[LogicalVariable] = patternPart.dependencies
}

sealed trait AnonymousPatternPart extends NonPrefixedPatternPart {
  override def allVariables: Set[LogicalVariable] = element.allVariables
}

case class PathPatternPart(element: PatternElement) extends AnonymousPatternPart {
  override def position: InputPosition = element.position
  override def isBounded: Boolean = element.isBounded
  override def isFixedLength: Boolean = element.isFixedLength
  override def dependencies: Set[LogicalVariable] = element.dependencies

  override def dup(children: Seq[AnyRef]): this.type = {
    PathPatternPart(children.head.asInstanceOf[PatternElement]).asInstanceOf[this.type]
  }

  override def mapExpressions(f: Expression => Expression): NonPrefixedPatternPart =
    copy(element.mapExpressions(f))
}

case class ShortestPathsPatternPart(element: PatternElement, single: Boolean)(val position: InputPosition)
    extends AnonymousPatternPart {

  val name: String =
    if (single)
      "shortestPath"
    else
      "allShortestPaths"

  override def isBounded: Boolean = true
  override def isFixedLength: Boolean = false

  override def mapExpressions(f: Expression => Expression): NonPrefixedPatternPart =
    copy(element.mapExpressions(f))(this.position)

  override def dependencies: Set[LogicalVariable] = element.dependencies
}

object PatternPart {

  def apply(element: PatternElement): PathPatternPart =
    PathPatternPart(element)

  sealed trait Selector extends ASTNode {
    def prettified: String

    def isBounded: Boolean
  }

  sealed trait CountedSelector extends Selector {
    val count: UnsignedDecimalIntegerLiteral
  }

  sealed trait SelectiveSelector extends Selector

  case class AnyPath(count: UnsignedDecimalIntegerLiteral)(val position: InputPosition) extends SelectiveSelector
      with CountedSelector {
    override def prettified: String = s"ANY ${count.value} PATHS"

    override def isBounded: Boolean = true
  }

  case class AllPaths()(val position: InputPosition) extends Selector {
    override def prettified: String = "ALL PATHS"
    override def isBounded: Boolean = false
  }

  case class AnyShortestPath(count: UnsignedDecimalIntegerLiteral)(val position: InputPosition)
      extends SelectiveSelector
      with CountedSelector {
    override def prettified: String = s"SHORTEST ${count.value} PATHS"
    override def isBounded: Boolean = true
  }

  case class AllShortestPaths()(val position: InputPosition) extends SelectiveSelector {
    override def prettified: String = "ALL SHORTEST PATHS"
    override def isBounded: Boolean = true
  }

  case class ShortestGroups(count: UnsignedDecimalIntegerLiteral)(val position: InputPosition) extends SelectiveSelector
      with CountedSelector {
    override def prettified: String = s"SHORTEST ${count.value} PATH GROUPS"
    override def isBounded: Boolean = true
  }
}

/**
 * Contains a list of elements that are concatenated in the query.
 *
 * NOTE that the concatenation is recorded only in the order of the factors in the sequence.
 * That is that `factors(i)` is concatenated with `factors(i - 1)` and `factors(i + 1)` if they exist.
 */
case class PathConcatenation(factors: Seq[PathFactor])(val position: InputPosition) extends PatternElement {
  override def allVariables: Set[LogicalVariable] = factors.view.flatMap(_.allVariables).toSet

  def allTopLevelVariablesLeftToRight: Seq[LogicalVariable] = factors.flatMap(_.allTopLevelVariablesLeftToRight)

  override def variable: Option[LogicalVariable] = None

  override def isBounded: Boolean = factors.forall(_.isBounded)

  override def isFixedLength: Boolean = factors.forall(_.isFixedLength)

  override def mapExpressions(f: Expression => Expression): PatternElement =
    copy(factors.map(_.mapExpressions(f)).asInstanceOf[Seq[PathFactor]])(this.position)

  override def dependencies: Set[LogicalVariable] = factors.view.flatMap(_.dependencies).toSet
}

/**
 * Elements of this trait can be put next to each other to form a juxtaposition in a pattern.
 */
sealed trait PathFactor extends PatternElement

sealed trait PatternAtom extends ASTNode

case class QuantifiedPath(
  part: NonPrefixedPatternPart,
  quantifier: GraphPatternQuantifier,
  optionalWhereExpression: Option[Expression],
  variableGroupings: Set[VariableGrouping]
)(val position: InputPosition)
    extends PathFactor with PatternAtom {

  override def allVariables: Set[LogicalVariable] = variableGroupings.map(_.group)

  override def allTopLevelVariablesLeftToRight: Seq[LogicalVariable] = Seq.empty

  override def variable: Option[LogicalVariable] = None

  override def isBounded: Boolean = quantifier match {
    case FixedQuantifier(_)           => true
    case IntervalQuantifier(_, upper) => upper.nonEmpty
    case PlusQuantifier()             => false
    case StarQuantifier()             => false
  }

  override def isFixedLength: Boolean = quantifier match {
    case _: FixedQuantifier                                                         => true
    case IntervalQuantifier(Some(lower), Some(upper)) if lower.value == upper.value => true
    case _                                                                          => false
  }

  override def mapExpressions(f: Expression => Expression): PatternElement = copy(
    part.mapExpressions(f),
    quantifier.mapExpressions(f),
    optionalWhereExpression.map(f),
    variableGroupings.map(_.mapExpressions(f))
  )(this.position)

  override def dependencies: Set[LogicalVariable] = part.dependencies ++
    optionalWhereExpression.toSet[Expression].flatMap(_.dependencies)
}

object QuantifiedPath {

  def apply(
    part: NonPrefixedPatternPart,
    quantifier: GraphPatternQuantifier,
    optionalWhereExpression: Option[Expression]
  )(position: InputPosition): QuantifiedPath = {
    val entityBindings = part.allVariables.map(getGrouping(_, position))
    QuantifiedPath(part, quantifier, optionalWhereExpression, entityBindings)(position)
  }

  def getGrouping(innerVar: LogicalVariable, qppPosition: InputPosition): VariableGrouping = {
    VariableGrouping(innerVar.copyId, innerVar.withPosition(qppPosition))(qppPosition)
  }
}

/**
 * Describes a variable that is exposed from a [[QuantifiedPath]].
 *
 * @param singleton the singleton variable inside the QuantifiedPath.
 * @param group the group variable exposed outside of the QuantifiedPath.
 */
case class VariableGrouping(singleton: LogicalVariable, group: LogicalVariable)(val position: InputPosition)
    extends ASTNode with HasMappableExpressions[VariableGrouping] {

  override def toString: String = s"(singletonName=${singleton.name}, groupName=${group.name})"

  override def mapExpressions(f: Expression => Expression): VariableGrouping = copy(
    f(singleton).asInstanceOf[LogicalVariable],
    f(group).asInstanceOf[LogicalVariable]
  )(this.position)
}

object VariableGrouping {

  def singletonToGroup(groupings: Set[VariableGrouping], singletonName: LogicalVariable): Option[LogicalVariable] = {
    groupings.collectFirst {
      case VariableGrouping(`singletonName`, groupName) => groupName
    }
  }
}

// We can currently parse these but not plan them. Therefore, we represent them in the AST but disallow them in semantic checking when concatenated and unwrap them otherwise.
case class ParenthesizedPath(
  part: NonPrefixedPatternPart,
  optionalWhereClause: Option[Expression]
)(val position: InputPosition)
    extends PathFactor with PatternAtom {

  override def allVariables: Set[LogicalVariable] = part.element.allVariables
  override def allTopLevelVariablesLeftToRight: Seq[LogicalVariable] = part.element.allTopLevelVariablesLeftToRight

  override def variable: Option[LogicalVariable] = None

  override def isBounded: Boolean = part.isBounded

  override def isFixedLength: Boolean = part.isFixedLength

  override def mapExpressions(f: Expression => Expression): PatternElement =
    copy(part.mapExpressions(f), optionalWhereClause.map(f))(this.position)

  override def dependencies: Set[LogicalVariable] = part.dependencies ++
    optionalWhereClause.toSet[Expression].flatMap(_.dependencies)
}

object ParenthesizedPath {

  def apply(part: NonPrefixedPatternPart)(position: InputPosition): ParenthesizedPath =
    ParenthesizedPath(part, None)(position)
}

sealed abstract class PatternElement extends ASTNode with HasMappableExpressions[PatternElement] {
  def allVariables: Set[LogicalVariable]

  /**
   * In contrast to allVariables, this does not return variables that are nested inside QPPs.
   */
  def allTopLevelVariablesLeftToRight: Seq[LogicalVariable]
  def variable: Option[LogicalVariable]
  def isBounded: Boolean
  def isFixedLength: Boolean
  def dependencies: Set[LogicalVariable]

  def isSingleNode = false
}

object PatternElement {

  /**
   * Returns the boundary nodes of this pattern element. Note, this does not work on QPPs directly.
   * Therefore, qpps need to have been padded before.
   */
  @tailrec
  def boundaryNodes(element: PatternElement): Set[LogicalVariable] = {
    element match {
      // Either we have a simple pattern
      case pattern: SimplePattern =>
        val allVars = pattern.allTopLevelVariablesLeftToRight
        Set(allVars.head, allVars.last)
      // or non-simple patterns (QPPs) have been padded (see QppsHavePaddedNodes)
      case PathConcatenation(factors) =>
        val left = factors.head.asInstanceOf[SimplePattern].allTopLevelVariablesLeftToRight.head
        val right = factors.last.asInstanceOf[SimplePattern].allTopLevelVariablesLeftToRight.last
        Set(left, right)
      case ParenthesizedPath(part, _) => boundaryNodes(part.element)
      case _                          => throw new IllegalStateException()
    }
  }
}

/**
 * A part of the pattern that consists of alternating nodes and relationships, starting and ending in a node.
 */
sealed abstract class SimplePattern extends PathFactor {
  def allTopLevelVariablesLeftToRight: Seq[LogicalVariable]
}

case class RelationshipChain(
  element: SimplePattern,
  relationship: RelationshipPattern,
  rightNode: NodePattern
)(val position: InputPosition)
    extends SimplePattern {

  override def variable: Option[LogicalVariable] = relationship.variable

  override def allVariables: Set[LogicalVariable] = element.allVariables ++ relationship.variable ++ rightNode.variable

  override def allTopLevelVariablesLeftToRight: Seq[LogicalVariable] =
    element.allTopLevelVariablesLeftToRight ++ relationship.variable.toSeq ++ rightNode.allTopLevelVariablesLeftToRight

  override def isBounded: Boolean = relationship.isBounded && element.isBounded

  override def isFixedLength: Boolean = relationship.isSingleLength && element.isFixedLength && rightNode.isFixedLength

  override def dependencies: Set[LogicalVariable] =
    element.dependencies ++ relationship.dependencies ++ rightNode.dependencies

  @tailrec
  final def leftNode: NodePattern = element match {
    case node: NodePattern      => node
    case rel: RelationshipChain => rel.leftNode
  }

  override def mapExpressions(f: Expression => Expression): PatternElement = copy(
    element.mapExpressions(f).asInstanceOf[SimplePattern],
    relationship.mapExpressions(f),
    rightNode.mapExpressions(f).asInstanceOf[NodePattern]
  )(this.position)
}

/**
 * Represents one node in a pattern.
 */
case class NodePattern(
  variable: Option[LogicalVariable],
  labelExpression: Option[LabelExpression],
  properties: Option[Expression],
  predicate: Option[Expression]
)(val position: InputPosition)
    extends SimplePattern with PatternAtom {

  override def allVariables: Set[LogicalVariable] = variable.toSet

  override def allTopLevelVariablesLeftToRight: Seq[LogicalVariable] = variable.toSeq

  override def isSingleNode = true

  override def isBounded: Boolean = true

  override def isFixedLength: Boolean = true

  override def mapExpressions(f: Expression => Expression): PatternElement = {
    // The parser only allows parameters and Map literals in the properties position of a pattern.
    // The AST allows arbitrary expressions, however.
    // In order to make sure that queries can still be prettified to parseable Cypher, even after using
    // mapExpressions, we try to rewrite the items inside a MapExpression, instead of the whole MapExpression.
    val mappedProperties = properties.map {
      case me: MapExpression => me.mapExpressions(f)
      case x                 => f(x)
    }

    copy(
      variable = variable.map(f).asInstanceOf[Option[LogicalVariable]],
      properties = mappedProperties,
      predicate = predicate.map(f)
    )(this.position)
  }

  override def dependencies: Set[LogicalVariable] = (properties.toSet ++ predicate).flatMap(_.dependencies)

}

/**
 * Represents one relationship (without its neighbouring nodes) in a pattern.
 */
case class RelationshipPattern(
  variable: Option[LogicalVariable],
  labelExpression: Option[LabelExpression],
  length: Option[Option[Range]],
  properties: Option[Expression],
  predicate: Option[Expression],
  direction: SemanticDirection
)(val position: InputPosition) extends ASTNode with PatternAtom with HasMappableExpressions[RelationshipPattern] {

  def isSingleLength: Boolean = length.isEmpty

  def isDirected: Boolean = direction != SemanticDirection.BOTH

  def isBounded: Boolean = length match {
    case Some(Some(Range(_, Some(_)))) => true
    case None                          => true
    case _                             => false
  }

  def dependencies: Set[LogicalVariable] =
    (predicate.toSet ++ properties).flatMap(_.dependencies)

  override def mapExpressions(f: Expression => Expression): RelationshipPattern = {
    // The parser only allows parameters and Map literals in the properties position of a pattern.
    // The AST allows arbitrary expressions, however.
    // In order to make sure that queries can still be prettified to parseable Cypher, even after using
    // mapExpressions, we try to rewrite the items inside a MapExpression, instead of the whole MapExpression.
    val mappedProperties = properties.map {
      case me: MapExpression => me.mapExpressions(f)
      case x                 => f(x)
    }

    copy(
      variable = variable.map(f).asInstanceOf[Option[LogicalVariable]],
      length = length.map(_.map(_.mapExpressions(f))),
      properties = mappedProperties,
      predicate = predicate.map(f)
    )(this.position)
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy