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

org.neo4j.cypher.internal.ir.QueryHorizon.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.AliasedReturnItem
import org.neo4j.cypher.internal.ast.CommandClause
import org.neo4j.cypher.internal.ast.GraphReference
import org.neo4j.cypher.internal.ast.Hint
import org.neo4j.cypher.internal.ast.SubqueryCall.InTransactionsParameters
import org.neo4j.cypher.internal.expressions.Expression
import org.neo4j.cypher.internal.expressions.LogicalVariable
import org.neo4j.cypher.internal.expressions.Parameter
import org.neo4j.cypher.internal.expressions.StringLiteral
import org.neo4j.cypher.internal.expressions.Variable
import org.neo4j.cypher.internal.frontend.phases.ResolvedCall
import org.neo4j.cypher.internal.ir.ast.IRExpression
import org.neo4j.cypher.internal.ir.helpers.ExpressionConverters.PredicateConverter
import org.neo4j.cypher.internal.util.Foldable
import org.neo4j.cypher.internal.util.Foldable.FoldableAny
import org.neo4j.cypher.internal.util.InputPosition

sealed trait QueryHorizon extends Foldable {

  def exposedSymbols(coveredIds: Set[LogicalVariable]): Set[LogicalVariable]

  def dependingExpressions: Iterable[Expression]

  def dependencies: Set[LogicalVariable] = dependingExpressions.folder.findAllByClass[LogicalVariable].toSet

  def readOnly = true

  def allHints: Set[Hint]
  def withoutHints(hintsToIgnore: Set[Hint]): QueryHorizon

  /**
   * @return whether this horizon is the final projection of a single top-level planner query.
   */
  def isProjectionInFinalPosition: Boolean =
    this match {
      case qp: QueryProjection => qp.position.isFinal
      case _                   => false
    }

  /**
   * If dependingExpressions is empty, or only contains variables, we can assume that it doesn't contain any reads
   * @return 'true' if this horizon might do database reads. 'false' otherwise.
   */
  def couldContainRead: Boolean = dependingExpressions.exists(!_.isInstanceOf[Variable]) || returnsNodesOrRelationships

  private def returnsNodesOrRelationships: Boolean = {
    this match {
      case qp: QueryProjection => qp.position.isFinal && qp.projections.values.exists(_.isInstanceOf[Variable])
      case _                   => false
    }
  }

  /**
   * @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.
   */
  protected def getAllQGsWithLeafInfo: Seq[QgWithLeafInfo] = {
    val filtered = dependingExpressions.filter(!_.isInstanceOf[Variable]).toSeq
    val iRExpressions: Seq[QgWithLeafInfo] = filtered.folder.findAllByClass[IRExpression].flatMap((e: IRExpression) =>
      e.query.allQGsWithLeafInfo
    )
    QgWithLeafInfo.qgWithNoStableIdentifierAndOnlyLeaves(
      getQueryGraphFromDependingExpressions,
      isProjectionInFinalPosition
    ) +: iRExpressions
  }

  protected def getQueryGraphFromDependingExpressions: QueryGraph = {
    val dependencies = dependingExpressions
      .flatMap(_.dependencies)
      .toSet

    QueryGraph(
      argumentIds = dependencies,
      selections = Selections.from(dependingExpressions)
    )
  }

  lazy val allQueryGraphs: Seq[QgWithLeafInfo] = getAllQGsWithLeafInfo
}

final case class PassthroughAllHorizon() extends QueryHorizon {
  override def exposedSymbols(coveredIds: Set[LogicalVariable]): Set[LogicalVariable] = coveredIds

  override def dependingExpressions: Seq[Expression] = Seq.empty

  override lazy val allQueryGraphs: Seq[QgWithLeafInfo] = Seq.empty

  override def allHints: Set[Hint] = Set.empty

  override def withoutHints(hintsToIgnore: Set[Hint]): QueryHorizon = this
}

case class UnwindProjection(variable: LogicalVariable, exp: Expression) extends QueryHorizon {
  override def exposedSymbols(coveredIds: Set[LogicalVariable]): Set[LogicalVariable] = coveredIds + variable

  override def dependingExpressions: Seq[Expression] = Seq(exp)

  override def allHints: Set[Hint] = Set.empty

  override def withoutHints(hintsToIgnore: Set[Hint]): QueryHorizon = this
}

case class LoadCSVProjection(
  variable: LogicalVariable,
  url: Expression,
  format: CSVFormat,
  fieldTerminator: Option[StringLiteral]
) extends QueryHorizon {
  override def exposedSymbols(coveredIds: Set[LogicalVariable]): Set[LogicalVariable] = coveredIds + variable

  override def dependingExpressions: Seq[Expression] = Seq(url)

  override def allHints: Set[Hint] = Set.empty

  override def withoutHints(hintsToIgnore: Set[Hint]): QueryHorizon = this
}

case class CallSubqueryHorizon(
  callSubquery: PlannerQuery,
  correlated: Boolean,
  yielding: Boolean,
  inTransactionsParameters: Option[InTransactionsParameters],
  optional: Boolean
) extends QueryHorizon {

  override def exposedSymbols(coveredIds: Set[LogicalVariable]): Set[LogicalVariable] = {
    val maybeReportAs = inTransactionsParameters.flatMap(_.reportParams.map(_.reportAs))
    coveredIds ++ callSubquery.returns ++ maybeReportAs.toSeq
  }

  override def dependingExpressions: Seq[Expression] = Seq.empty

  override def readOnly: Boolean = callSubquery.readOnly

  override def allHints: Set[Hint] = callSubquery.allHints

  override def withoutHints(hintsToIgnore: Set[Hint]): QueryHorizon =
    copy(callSubquery = callSubquery.withoutHints(hintsToIgnore))

  /**
   * We don't analyze the subquery but just assume that it's doing reads.
   */
  override def couldContainRead: Boolean = true

  override lazy val allQueryGraphs: Seq[QgWithLeafInfo] = super.getAllQGsWithLeafInfo ++ callSubquery.allQGsWithLeafInfo
}

sealed abstract class QueryProjection extends QueryHorizon {
  def selections: Selections
  def projections: Map[LogicalVariable, Expression]
  def queryPagination: QueryPagination
  def keySet: Set[LogicalVariable]
  def position: QueryProjection.Position
  def withSelection(selections: Selections): QueryProjection
  def withAddedProjections(projections: Map[LogicalVariable, Expression]): QueryProjection
  def withPagination(queryPagination: QueryPagination): QueryProjection
  def markAsFinal: QueryProjection

  override def dependingExpressions: Iterable[Expression] = projections.view.values ++ selections.predicates.map(_.expr)

  def updatePagination(f: QueryPagination => QueryPagination): QueryProjection = withPagination(f(queryPagination))

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

  override def allHints: Set[Hint] = Set.empty

  override def withoutHints(hintsToIgnore: Set[Hint]): QueryHorizon = this
}

object QueryProjection {

  /**
   * Position relative to the top-level (single) planner query.
   * Final if at the end of a single top-level query.
   * Intermediate if not at the end of a query, part of a union, or inside a sub-query.
   * Used for eagerness analysis.
   */
  sealed trait Position {

    /**
     * @return whether it is at the end of the top-level single planner query.
     */
    def isFinal: Boolean

    /**
     * @param other the other position to combine with.
     * @return Final if both positions are final.
     */
    def combine(other: Position): Position
  }

  object Position {

    case object Intermediate extends Position {
      override def isFinal: Boolean = false
      override def combine(other: Position): Position = Intermediate
    }

    /**
     * Signifies the last projection that will conclude the query.
     */
    case object Final extends Position {
      override def isFinal: Boolean = true
      override def combine(other: Position): Position = other
    }
  }

  def empty: RegularQueryProjection = RegularQueryProjection()

  def forVariables(variables: Set[LogicalVariable]): Seq[AliasedReturnItem] =
    variables.toIndexedSeq.map(variable =>
      AliasedReturnItem(variable, variable)(InputPosition.NONE)
    )
}

final case class RegularQueryProjection(
  projections: Map[LogicalVariable, Expression] = Map.empty,
  queryPagination: QueryPagination = QueryPagination.empty,
  selections: Selections = Selections(),
  position: QueryProjection.Position = QueryProjection.Position.Intermediate
) extends QueryProjection {
  def keySet: Set[LogicalVariable] = projections.keySet

  def ++(other: RegularQueryProjection): RegularQueryProjection =
    RegularQueryProjection(
      projections = projections ++ other.projections,
      queryPagination = queryPagination ++ other.queryPagination,
      selections = selections ++ other.selections,
      position = position.combine(other.position)
    )

  override def withAddedProjections(projections: Map[LogicalVariable, Expression]): RegularQueryProjection =
    copy(projections = this.projections ++ projections)

  def withPagination(queryPagination: QueryPagination): RegularQueryProjection =
    copy(queryPagination = queryPagination)

  override def exposedSymbols(coveredIds: Set[LogicalVariable]): Set[LogicalVariable] = projections.keySet

  override def withSelection(selections: Selections): QueryProjection = copy(selections = selections)

  /**
   * @return a copy of the projection marked as being at the end of a top-level single planner query.
   */
  override def markAsFinal: QueryProjection = copy(position = QueryProjection.Position.Final)
}

final case class AggregatingQueryProjection(
  groupingExpressions: Map[LogicalVariable, Expression] = Map.empty,
  aggregationExpressions: Map[LogicalVariable, Expression] = Map.empty,
  queryPagination: QueryPagination = QueryPagination.empty,
  selections: Selections = Selections(),
  position: QueryProjection.Position = QueryProjection.Position.Intermediate
) extends QueryProjection {

  assert(
    !(groupingExpressions.isEmpty && aggregationExpressions.isEmpty),
    "Everything can't be empty"
  )

  override def projections: Map[LogicalVariable, Expression] = groupingExpressions ++ aggregationExpressions

  override def keySet: Set[LogicalVariable] = groupingExpressions.keySet ++ aggregationExpressions.keySet

  override def dependingExpressions: Iterable[Expression] = super.dependingExpressions ++ aggregationExpressions.values

  override def withAddedProjections(groupingKeys: Map[LogicalVariable, Expression]): AggregatingQueryProjection =
    copy(groupingExpressions = this.groupingExpressions ++ groupingKeys)

  override def withPagination(queryPagination: QueryPagination): AggregatingQueryProjection =
    copy(queryPagination = queryPagination)

  override def exposedSymbols(coveredIds: Set[LogicalVariable]): Set[LogicalVariable] =
    groupingExpressions.keySet ++ aggregationExpressions.keySet

  override def withSelection(selections: Selections): QueryProjection = copy(selections = selections)

  override def markAsFinal: QueryProjection = copy(position = QueryProjection.Position.Final)
}

final case class DistinctQueryProjection(
  groupingExpressions: Map[LogicalVariable, Expression] = Map.empty,
  queryPagination: QueryPagination = QueryPagination.empty,
  selections: Selections = Selections(),
  position: QueryProjection.Position = QueryProjection.Position.Intermediate
) extends QueryProjection {

  def projections: Map[LogicalVariable, Expression] = groupingExpressions

  def keySet: Set[LogicalVariable] = groupingExpressions.keySet

  override def withAddedProjections(groupingKeys: Map[LogicalVariable, Expression]): DistinctQueryProjection =
    copy(groupingExpressions = this.groupingExpressions ++ groupingKeys)

  override def withPagination(queryPagination: QueryPagination): DistinctQueryProjection =
    copy(queryPagination = queryPagination)

  override def exposedSymbols(coveredIds: Set[LogicalVariable]): Set[LogicalVariable] = groupingExpressions.keySet

  override def withSelection(selections: Selections): QueryProjection = copy(selections = selections)

  override def markAsFinal: QueryProjection = copy(position = QueryProjection.Position.Final)
}

/**
 * Query fragment, part of a composite query.
 *
 * @param graphReference the graph on which to execute the query fragment
 * @param queryString the query to execute, serialised as a standalone Cypher query string
 * @param parameters query parameters used inside of the query fragment
 * @param importsAsParameters variables imported from the outer query inside of the query fragment are passed via additional parameters; mapping from the parameters to the original variables
 * @param columns the variables returned by the query fragment
 */
case class RunQueryAtProjection(
  graphReference: GraphReference,
  queryString: String,
  parameters: Set[Parameter],
  importsAsParameters: Map[Parameter, LogicalVariable],
  columns: Set[LogicalVariable]
) extends QueryProjection {
  override def exposedSymbols(coveredIds: Set[LogicalVariable]): Set[LogicalVariable] = coveredIds ++ columns
  override def selections: Selections = Selections.empty
  override def projections: Map[LogicalVariable, Expression] = columns.view.map(column => column -> column).toMap
  override def queryPagination: QueryPagination = QueryPagination.empty
  override def keySet: Set[LogicalVariable] = columns

  override def position: QueryProjection.Position =
    QueryProjection.Position.Intermediate // No eagerness analysis for composite queries as it stands

  override def withSelection(selections: Selections): QueryProjection =
    throw new UnsupportedOperationException("Cannot modify the selections of a RunQueryAt projection")

  override def withAddedProjections(projections: Map[LogicalVariable, Expression]): QueryProjection =
    throw new UnsupportedOperationException("Cannot add projections to a RunQueryAt projection")

  override def withPagination(queryPagination: QueryPagination): QueryProjection =
    throw new UnsupportedOperationException("Cannot modify the pagination of a RunQueryAt projection")
  override def markAsFinal: QueryProjection = this // No eagerness analysis for composite queries as it stands
}

case class CommandProjection(clause: CommandClause) extends QueryHorizon {

  override def exposedSymbols(coveredIds: Set[LogicalVariable]): Set[LogicalVariable] = {
    val columns = clause match {
      case t: CommandClause if t.yieldItems.nonEmpty =>
        t.yieldItems.map(_.aliasedVariable)
      case _ => clause.unfilteredColumns.columns.map(_.variable)
    }
    coveredIds ++ columns
  }

  override def dependingExpressions: Seq[Expression] = Seq()

  override def allHints: Set[Hint] = Set.empty

  override def withoutHints(hintsToIgnore: Set[Hint]): QueryHorizon = this
}

abstract class AbstractProcedureCallProjection extends QueryHorizon {
  val call: ResolvedCall
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy