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

org.neo4j.cypher.internal.runtime.slotted.SlottedRow.scala Maven / Gradle / Ivy

/*
 * 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.runtime.slotted

import org.neo4j.cypher.internal.expressions.ASTCachedProperty
import org.neo4j.cypher.internal.macros.AssertMacros.checkOnlyWhenAssertionsAreEnabled
import org.neo4j.cypher.internal.physicalplanning.LongSlot
import org.neo4j.cypher.internal.physicalplanning.RefSlot
import org.neo4j.cypher.internal.physicalplanning.SlotAllocation.LOAD_CSV_METADATA_KEY
import org.neo4j.cypher.internal.physicalplanning.SlotConfiguration
import org.neo4j.cypher.internal.physicalplanning.SlotConfiguration.ApplyPlanSlotKey
import org.neo4j.cypher.internal.physicalplanning.SlotConfiguration.CachedPropertySlotKey
import org.neo4j.cypher.internal.physicalplanning.SlotConfiguration.DuplicatedSlotKey
import org.neo4j.cypher.internal.physicalplanning.SlotConfiguration.MetaDataSlotKey
import org.neo4j.cypher.internal.physicalplanning.SlotConfiguration.OuterNestedApplyPlanSlotKey
import org.neo4j.cypher.internal.physicalplanning.SlotConfiguration.SlotWithKeyAndAliases
import org.neo4j.cypher.internal.physicalplanning.SlotConfiguration.VariableSlotKey
import org.neo4j.cypher.internal.runtime.CypherRow
import org.neo4j.cypher.internal.runtime.EntityById
import org.neo4j.cypher.internal.runtime.ReadableRow
import org.neo4j.cypher.internal.runtime.ResourceLinenumber
import org.neo4j.cypher.internal.runtime.RuntimeMetadataValue
import org.neo4j.cypher.internal.runtime.slotted.helpers.NullChecker.entityIsNull
import org.neo4j.cypher.internal.util.symbols.CTNode
import org.neo4j.cypher.internal.util.symbols.CTRelationship
import org.neo4j.exceptions.CypherTypeException
import org.neo4j.exceptions.InternalException
import org.neo4j.graphdb.NotFoundException
import org.neo4j.kernel.api.StatementConstants
import org.neo4j.memory.HeapEstimator
import org.neo4j.memory.HeapEstimator.shallowSizeOfInstance
import org.neo4j.values.AnyValue
import org.neo4j.values.storable.Value
import org.neo4j.values.storable.Values
import org.neo4j.values.virtual.VirtualNodeValue
import org.neo4j.values.virtual.VirtualRelationshipValue

object SlottedRow {
  final val INSTANCE_SIZE = shallowSizeOfInstance(classOf[SlottedRow])

  def empty = new SlottedRow(SlotConfiguration.empty)
  final val DEBUG = false

  def getNodeId(row: ReadableRow, offset: Int, isReference: Boolean): Long =
    if (isReference) {
      row.getRefAt(offset) match {
        case node: VirtualNodeValue => node.id()
        case Values.NO_VALUE        => StatementConstants.NO_SUCH_NODE
        case null                   => throw new CypherTypeException(s"Expected a node, but got null ")
        case other                  => throw new CypherTypeException(s"Expected a node, but got ${other.getTypeName} ")
      }
    } else {
      row.getLongAt(offset)
    }

  def getLinenumber(row: ReadableRow, slots: SlotConfiguration): Option[ResourceLinenumber] = {
    slots.getMetaDataSlot(LOAD_CSV_METADATA_KEY) match {
      case Some(RefSlot(offset, _, _)) =>
        row.getRefAt(offset) match {
          case RuntimeMetadataValue(l: ResourceLinenumber) =>
            Some(l)
          case Values.NO_VALUE =>
            None
          case null =>
            None
          case _ =>
            throw new InternalException("Wrong type of linenumber")
        }

      case _ =>
        None
    }
  }
}

trait SlottedCompatible {
  def copyAllToSlottedRow(target: SlottedRow): Unit
  def copyLongsToSlottedRow(target: SlottedRow, fromOffset: Int, toOffset: Int, length: Int): Unit
  def copyRefsToSlottedRow(target: SlottedRow, fromOffset: Int, toOffset: Int, length: Int): Unit
}

/**
 * Execution context which uses a slot configuration to store values in two arrays.
 *
 * @param slots the slot configuration to use.
 */
//noinspection NameBooleanParameters
case class SlottedRow(slots: SlotConfiguration) extends CypherRow {

  val longs = new Array[Long](slots.numberOfLongs)
  // java.util.Arrays.fill(longs, -2L) // When debugging long slot issues you can uncomment this to check for uninitialized long slots (also in getLongAt below)
  val refs = new Array[AnyValue](slots.numberOfReferences)

  override def toString: String = {
    val iter = this.iterator
    val s: StringBuilder = StringBuilder.newBuilder
    s ++= s"\nSlottedExecutionContext {\n    $slots"
    while (iter.hasNext) {
      val slotValue = iter.next
      s ++= f"\n    ${slotValue._1}%-40s = ${slotValue._2}"
    }
    s ++= "\n}\n"
    s.result
  }

  override def copyAllFrom(input: ReadableRow): Unit = input match {
    case other: SlottedRow        => copyFromSlotted(other, 0, 0, 0, 0)
    case other: SlottedCompatible => other.copyAllToSlottedRow(this)
    case _                        => fail()
  }

  override def copyFrom(input: ReadableRow, nLongs: Int, nRefs: Int): Unit =
    if (nLongs > slots.numberOfLongs || nRefs > slots.numberOfReferences)
      throw new InternalException(
        "A bug has occurred in the slotted runtime: The target slotted execution context cannot hold the data to copy."
      )
    else input match {
      case other @ SlottedRow(_) =>
        System.arraycopy(other.longs, 0, longs, 0, nLongs)
        System.arraycopy(other.refs, 0, refs, 0, nRefs)

      case other: SlottedCompatible =>
        other.copyLongsToSlottedRow(this, 0, 0, nLongs)
        other.copyRefsToSlottedRow(this, 0, 0, nRefs)

      case _ => fail()
    }

  override def copyLongsFrom(input: ReadableRow, fromOffset: Int, toOffset: Int, length: Int): Unit = {
    input match {
      case from: SlottedRow        => System.arraycopy(from.longs, fromOffset, longs, toOffset, length)
      case from: SlottedCompatible => from.copyLongsToSlottedRow(this, fromOffset, toOffset, length)
      case _                       => fail()
    }
  }

  override def copyRefsFrom(input: ReadableRow, fromOffset: Int, toOffset: Int, length: Int): Unit = {
    input match {
      case from: SlottedRow        => System.arraycopy(from.refs, fromOffset, refs, toOffset, length)
      case from: SlottedCompatible => from.copyRefsToSlottedRow(this, fromOffset, toOffset, length)
      case _                       => fail()
    }
  }

  def copyMapped(func: AnyValue => AnyValue): CypherRow = {
    val clone = SlottedRow(slots)
    clone.copyAllFrom(this)
    clone.transformRefs(func)
    clone
  }

  private def transformRefs(func: AnyValue => AnyValue): Unit = {
    slots.foreachSlot {
      case (_, slot: RefSlot) =>
        val refValue = getRefAt(slot.offset)
        val newRefValue = func(refValue)
        setRefAt(slot.offset, newRefValue)
      case _ =>
        ()
    }
  }

  override def copyFromOffset(
    input: ReadableRow,
    sourceLongOffset: Int,
    sourceRefOffset: Int,
    targetLongOffset: Int,
    targetRefOffset: Int
  ): Unit =
    input match {
      case other: SlottedRow =>
        copyFromSlotted(other, sourceLongOffset, sourceRefOffset, targetLongOffset, targetRefOffset)
      case _ => fail()
    }

  private def copyFromSlotted(
    other: SlottedRow,
    sourceLongOffset: Int,
    sourceRefOffset: Int,
    targetLongOffset: Int,
    targetRefOffset: Int
  ): Unit = {
    val otherPipeline = other.slots
    if (
      otherPipeline.numberOfLongs > slots.numberOfLongs ||
      otherPipeline.numberOfReferences > slots.numberOfReferences
    ) {
      throw new InternalException(
        s"""A bug has occurred in the slotted runtime: The target slotted execution context cannot hold the data to copy
           |From : $otherPipeline
           |To :   $slots""".stripMargin
      )
    } else {
      System.arraycopy(
        other.longs,
        sourceLongOffset,
        longs,
        targetLongOffset,
        other.slots.numberOfLongs - sourceLongOffset
      )
      System.arraycopy(
        other.refs,
        sourceRefOffset,
        refs,
        targetRefOffset,
        other.slots.numberOfReferences - sourceRefOffset
      )
    }
  }

  override def setLongAt(offset: Int, value: Long): Unit =
    longs(offset) = value

  override def getLongAt(offset: Int): Long =
    longs(offset)
  // When debugging long slot issues you can uncomment and replace with this to check for uninitialized long slots
  //  {
  //    val value = longs(offset)
  //    if (value == -2L)
  //      throw new InternalException(s"Long value not initialised at offset $offset in $this")
  //    value
  //  }

  override def setRefAt(offset: Int, value: AnyValue): Unit = refs(offset) = value

  override def getRefAt(offset: Int): AnyValue = {
    val value = refs(offset)
    if (SlottedRow.DEBUG && value == null)
      throw new InternalException(s"Reference value not initialised at offset $offset in $this")
    value
  }

  override def getByName(name: String): AnyValue = {
    slots.maybeGetter(name).map(g => g(this)).getOrElse(throw new NotFoundException(s"Unknown variable `$name`."))
  }

  override def numberOfColumns: Int = refs.length + longs.length

  override def containsName(name: String): Boolean = slots.maybeGetter(name).map(g => g(this)).isDefined

  override def setCachedPropertyAt(offset: Int, value: Value): Unit = refs(offset) = value

  override def invalidateCachedProperties(): Unit = {
    slots.foreachCachedSlot {
      case (_, propertyRefSLot) => setCachedPropertyAt(propertyRefSLot.offset, null)
    }
  }

  override def invalidateCachedNodeProperties(node: Long): Unit = {
    slots.foreachCachedSlot {
      case (cnp, propertyRefSLot) =>
        slots.get(cnp.entityName) match {
          case Some(longSlot: LongSlot) =>
            if (longSlot.typ == CTNode && getLongAt(longSlot.offset) == node) {
              setCachedPropertyAt(propertyRefSLot.offset, null)
            }
          case Some(refSlot: RefSlot) =>
            if (refSlot.typ == CTNode && getRefAt(refSlot.offset).asInstanceOf[VirtualNodeValue].id == node) {
              setCachedPropertyAt(propertyRefSLot.offset, null)
            }
          case None =>
          // This case is possible to reach, when we allocate a cached property before a pipeline break and before the variable it is referencing.
          // We will never evaluate that cached property in this row, and we could improve SlotAllocation to allocate it only on the next pipeline
          // instead, but that is difficult. It is harmless if we get here, we will simply not do anything.
        }
    }
  }

  override def invalidateCachedRelationshipProperties(rel: Long): Unit = {
    slots.foreachCachedSlot {
      case (crp, propertyRefSLot) =>
        slots.get(crp.entityName) match {
          case Some(longSlot: LongSlot) =>
            if (longSlot.typ == CTRelationship && getLongAt(longSlot.offset) == rel) {
              setCachedPropertyAt(propertyRefSLot.offset, null)
            }
          case Some(refSlot: RefSlot) =>
            if (
              refSlot.typ == CTRelationship && getRefAt(refSlot.offset).asInstanceOf[VirtualRelationshipValue].id == rel
            ) {
              setCachedPropertyAt(propertyRefSLot.offset, null)
            }
          case None =>
          // This case is possible to reach, when we allocate a cached property before a pipeline break and before the variable it is referencing.
          // We will never evaluate that cached property in this row, and we could improve SlotAllocation to allocate it only on the next pipeline
          // instead, but that is difficult. It is harmless if we get here, we will simply not do anything.
        }
    }
  }

  override def setCachedProperty(key: ASTCachedProperty.RuntimeKey, value: Value): Unit =
    setCachedPropertyAt(slots.getCachedPropertyOffsetFor(key), value)

  override def getCachedPropertyAt(offset: Int): Value = refs(offset).asInstanceOf[Value]

  override def getCachedProperty(key: ASTCachedProperty.RuntimeKey): Value = fail()

  override def estimatedHeapUsage: Long = {
    var usage = SlottedRow.INSTANCE_SIZE + HeapEstimator.sizeOf(longs) + HeapEstimator.shallowSizeOf(
      refs.asInstanceOf[Array[Object]]
    )
    var i = 0
    while (i < refs.length) {
      val ref = refs(i)
      if (ref != null) {
        usage += ref.estimatedHeapUsage()
      }
      i += 1
    }
    usage
  }

  override def deduplicatedEstimatedHeapUsage(previous: CypherRow): Long = {
    if (previous eq null) {
      estimatedHeapUsage
    } else {
      var usage = SlottedRow.INSTANCE_SIZE + HeapEstimator.sizeOf(longs) + HeapEstimator.shallowSizeOf(
        refs.asInstanceOf[Array[Object]]
      )
      var i = 0
      while (i < refs.length) {
        val ref = refs(i)
        if ((ref ne null) && (ref ne previous.getRefAt(i))) {
          usage += ref.estimatedHeapUsage()
        }
        i += 1
      }
      usage
    }
  }

  private def fail(): Nothing = throw new InternalException("Tried using a slotted context as a map")

  // -----------------------------------------------------------------------------------------------------------
  // Compatibility implementations of the old ExecutionContext API used by Community interpreted runtime pipes
  // -----------------------------------------------------------------------------------------------------------

  override def setLinenumber(line: Option[ResourceLinenumber]): Unit = fail()

  override def getLinenumber: Option[ResourceLinenumber] = {
    SlottedRow.getLinenumber(this, slots)
  }

  // The newWith methods are called from Community pipes. We should already have allocated slots for the given keys,
  // so we just set the values in the existing slots instead of creating a new context like in the MapExecutionContext.
  override def set(newEntries: collection.Seq[(String, AnyValue)]): Unit =
    newEntries.foreach {
      case (k, v) =>
        setValue(k, v)
    }

  override def set(key1: String, value1: AnyValue): Unit =
    setValue(key1, value1)

  override def set(key1: String, value1: AnyValue, key2: String, value2: AnyValue): Unit = {
    setValue(key1, value1)
    setValue(key2, value2)
  }

  override def set(
    key1: String,
    value1: AnyValue,
    key2: String,
    value2: AnyValue,
    key3: String,
    value3: AnyValue
  ): Unit = {
    setValue(key1, value1)
    setValue(key2, value2)
    setValue(key3, value3)
  }

  override def copyWith(key1: String, value1: AnyValue): CypherRow = {
    // This method should throw like its siblings below as soon as reduce is changed to not use it.
    val newCopy = SlottedRow(slots)
    newCopy.copyAllFrom(this)
    newCopy.setValue(key1, value1)
    newCopy
  }

  override def copyWith(key1: String, value1: AnyValue, key2: String, value2: AnyValue): CypherRow = {
    throw new UnsupportedOperationException(
      "Use ExecutionContextFactory.copyWith instead to get the correct slot configuration"
    )
  }

  override def copyWith(
    key1: String,
    value1: AnyValue,
    key2: String,
    value2: AnyValue,
    key3: String,
    value3: AnyValue
  ): CypherRow = {
    throw new UnsupportedOperationException(
      "Use ExecutionContextFactory.copyWith instead to get the correct slot configuration"
    )
  }

  override def copyWith(newEntries: collection.Seq[(String, AnyValue)]): CypherRow = {
    throw new UnsupportedOperationException(
      "Use ExecutionContextFactory.copyWith instead to get the correct slot configuration"
    )
  }

  private def setValue(key1: String, value1: AnyValue): Unit = {
    slots.maybeSetter(key1)
      .getOrElse(throw new InternalException(s"Ouch, no suitable slot for key $key1 = $value1\nSlots: $slots"))
      .apply(this, value1)
  }

  def isRefInitialized(offset: Int): Boolean = {
    refs(offset) != null
  }

  def getRefAtWithoutCheckingInitialized(offset: Int): AnyValue =
    refs(offset)

  override def mergeWith(other: ReadableRow, entityById: EntityById, checkNullability: Boolean = true): Unit =
    other match {
      case slottedOther: SlottedRow =>
        slottedOther.slots.foreachSlot({
          case (VariableSlotKey(key), otherSlot @ LongSlot(offset, _, CTNode)) =>
            val thisSlotSetter = slots.maybePrimitiveNodeSetter(key).getOrElse(
              throw new InternalException(
                s"Tried to merge primitive node slot $otherSlot from $other but it is missing from $this." +
                  "Looks like something needs to be fixed in slot allocation."
              )
            )
            thisSlotSetter.apply(this, other.getLongAt(offset), entityById)

          case (VariableSlotKey(key), otherSlot @ LongSlot(offset, _, CTRelationship)) =>
            val thisSlotSetter = slots.maybePrimitiveRelationshipSetter(key).getOrElse(
              throw new InternalException(
                s"Tried to merge primitive relationship slot $otherSlot from $other but it is missing from $this." +
                  "Looks like something needs to be fixed in slot allocation."
              )
            )
            thisSlotSetter.apply(this, other.getLongAt(offset), entityById)

          case (VariableSlotKey(key), otherSlot @ RefSlot(offset, _, _)) if slottedOther.isRefInitialized(offset) =>
            val thisSlotSetter = slots.maybeSetter(key).getOrElse(
              throw new InternalException(s"Tried to merge slot $otherSlot from $other but it is missing from $this." +
                "Looks like something needs to be fixed in slot allocation.")
            )
            checkOnlyWhenAssertionsAreEnabled(!checkNullability || checkCompatibleNullablility(key, otherSlot))

            val otherValue = slottedOther.getRefAtWithoutCheckingInitialized(offset)
            thisSlotSetter.apply(this, otherValue)

          case (_: VariableSlotKey, _) =>
          // a slot which is not initialized(=null). This means it is allocated, but will only be used later in the pipeline.
          // Therefore, this is a no-op.

          case (CachedPropertySlotKey(property), refSlot) =>
            setCachedProperty(property, other.getCachedPropertyAt(refSlot.offset))

          case (key: MetaDataSlotKey, refSlot) =>
            val thisOffset = slots.getMetaDataOffsetFor(key)
            // Do not overwrite existing meta data here (preserves backwards compatibility with MapCypherRow)
            if (!isRefInitialized(thisOffset) || (getRefAtWithoutCheckingInitialized(thisOffset) eq Values.NO_VALUE)) {
              setRefAt(thisOffset, other.getRefAt(refSlot.offset))
            }

          case (DuplicatedSlotKey(_, _), _) =>
          // no op
        })

      case _ =>
        throw new InternalException("Well well, isn't this a delicate situation?")
    }

  private def checkCompatibleNullablility(key: String, otherSlot: RefSlot): Boolean = {
    val thisSlot = slots.get(key).get
    // This should be guaranteed by slot allocation or else we could get incorrect results
    if (!thisSlot.nullable && otherSlot.nullable)
      throw new InternalException(s"Tried to merge slot $otherSlot into $thisSlot but its nullability is incompatible")
    true
  }

  override def createClone(): CypherRow = {
    val clone = SlottedRow(slots)
    clone.copyAllFrom(this)
    clone
  }

  override def isNull(key: String): Boolean =
    slots.get(key) match {
      case Some(RefSlot(offset, true, _)) if isRefInitialized(offset) =>
        getRefAtWithoutCheckingInitialized(offset) eq Values.NO_VALUE
      case Some(LongSlot(offset, true, CTNode)) =>
        entityIsNull(getLongAt(offset))
      case Some(LongSlot(offset, true, CTRelationship)) =>
        entityIsNull(getLongAt(offset))
      case _ =>
        false
    }

  private def iterator: Iterator[(String, AnyValue)] = {
    // This method implementation is for debug usage only.
    // Please do not use in production code.
    var tuples: List[(String, AnyValue)] = Nil
    def prettyKey(key: String, aliases: collection.Set[String]): String = (key +: aliases.toSeq).mkString(",")
    slots.foreachSlotAndAliasesOrdered({
      case SlotWithKeyAndAliases(VariableSlotKey(key), RefSlot(offset, _, _), aliases) =>
        tuples ::= ((prettyKey(key, aliases), refs(offset)))
      case SlotWithKeyAndAliases(VariableSlotKey(key), LongSlot(offset, _, _), aliases) =>
        tuples ::= ((prettyKey(key, aliases), Values.longValue(longs(offset))))
      case SlotWithKeyAndAliases(CachedPropertySlotKey(cachedProperty), slot, _) =>
        tuples ::= ((cachedProperty.asCanonicalStringVal, refs(slot.offset)))
      case SlotWithKeyAndAliases(MetaDataSlotKey(key, id), slot, _) =>
        tuples ::= ((s"MetaData($key, $id)", refs(slot.offset)))
      case SlotWithKeyAndAliases(ApplyPlanSlotKey(id), slot, _) =>
        tuples ::= ((s"Apply-Plan($id)", Values.longValue(longs(slot.offset))))
      case SlotWithKeyAndAliases(OuterNestedApplyPlanSlotKey(id), slot, _) =>
        tuples ::= ((s"Nested-Apply-Plan($id)", Values.longValue(longs(slot.offset))))
      case SlotWithKeyAndAliases(DuplicatedSlotKey(key, id), slot, _) =>
        tuples ::= ((
          s"DuplicatedSlot($key, $id)",
          if (slot.isLongSlot) Values.longValue(longs(slot.offset))
          else refs(slot.offset)
        ))
    })
    tuples.iterator
  }

  /**
   * Removes slots that have been marked as "discarded" in the slot configuration.
   *
   * Caution!! Only safe to call when the current "pipeline" is done processing this row.
   * In slotted this is before a "break", where rows are copied
   * (see [[SlottedPipelineBreakingPolicy]]).
   */
  // Note, consider adding a test case in SlottedPipelineBreakingPolicyTest
  // if you add calls to this method
  override def compact(): Unit = {
    if (refs.length > 0 && slots.discardedRefSlotOffsets().nonEmpty) {
      val discard = slots.discardedRefSlotOffsets()
      var i = 0
      while (i < discard.length) {
        refs(discard(i)) = null
        i += 1
      }
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy