class Merge

Same name in this branch
  1. main core/tests/fixtures/database_drivers/module/core_fake/src/Driver/Database/CoreFakeWithAllCustomClasses/Merge.php \Drupal\core_fake\Driver\Database\CoreFakeWithAllCustomClasses\Merge
  2. main core/lib/Drupal/Core/Database/Query/Merge.php \Drupal\Core\Database\Query\Merge
Same name and namespace in other branches
  1. 8.9.x core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Merge.php \Drupal\driver_test\Driver\Database\DrivertestPgsql\Merge
  2. 8.9.x core/lib/Drupal/Core/Database/Driver/sqlite/Merge.php \Drupal\Core\Database\Driver\sqlite\Merge
  3. 8.9.x core/lib/Drupal/Core/Database/Driver/mysql/Merge.php \Drupal\Core\Database\Driver\mysql\Merge
  4. 8.9.x core/lib/Drupal/Core/Database/Driver/pgsql/Merge.php \Drupal\Core\Database\Driver\pgsql\Merge
  5. 11.x core/modules/sqlite/src/Driver/Database/sqlite/Merge.php \Drupal\sqlite\Driver\Database\sqlite\Merge
  6. 11.x core/modules/mysql/src/Driver/Database/mysql/Merge.php \Drupal\mysql\Driver\Database\mysql\Merge
  7. 11.x core/modules/pgsql/src/Driver/Database/pgsql/Merge.php \Drupal\pgsql\Driver\Database\pgsql\Merge
  8. 11.x core/tests/fixtures/database_drivers/module/core_fake/src/Driver/Database/CoreFakeWithAllCustomClasses/Merge.php \Drupal\core_fake\Driver\Database\CoreFakeWithAllCustomClasses\Merge
  9. 11.x core/lib/Drupal/Core/Database/Query/Merge.php \Drupal\Core\Database\Query\Merge
  10. 10 core/modules/sqlite/src/Driver/Database/sqlite/Merge.php \Drupal\sqlite\Driver\Database\sqlite\Merge
  11. 10 core/modules/mysql/src/Driver/Database/mysql/Merge.php \Drupal\mysql\Driver\Database\mysql\Merge
  12. 10 core/modules/pgsql/src/Driver/Database/pgsql/Merge.php \Drupal\pgsql\Driver\Database\pgsql\Merge
  13. 10 core/tests/fixtures/database_drivers/module/core_fake/src/Driver/Database/CoreFakeWithAllCustomClasses/Merge.php \Drupal\core_fake\Driver\Database\CoreFakeWithAllCustomClasses\Merge
  14. 10 core/lib/Drupal/Core/Database/Query/Merge.php \Drupal\Core\Database\Query\Merge
  15. 9 core/modules/sqlite/src/Driver/Database/sqlite/Merge.php \Drupal\sqlite\Driver\Database\sqlite\Merge
  16. 9 core/modules/mysql/src/Driver/Database/mysql/Merge.php \Drupal\mysql\Driver\Database\mysql\Merge
  17. 9 core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Merge.php \Drupal\driver_test\Driver\Database\DrivertestMysql\Merge
  18. 9 core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Merge.php \Drupal\driver_test\Driver\Database\DrivertestMysqlDeprecatedVersion\Merge
  19. 9 core/modules/pgsql/src/Driver/Database/pgsql/Merge.php \Drupal\pgsql\Driver\Database\pgsql\Merge
  20. 9 core/tests/fixtures/database_drivers/module/corefake/src/Driver/Database/corefakeWithAllCustomClasses/Merge.php \Drupal\corefake\Driver\Database\corefakeWithAllCustomClasses\Merge
  21. 9 core/lib/Drupal/Core/Database/Query/Merge.php \Drupal\Core\Database\Query\Merge
  22. 8.9.x core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Merge.php \Drupal\driver_test\Driver\Database\DrivertestMysql\Merge
  23. 8.9.x core/lib/Drupal/Core/Database/Query/Merge.php \Drupal\Core\Database\Query\Merge

PostgreSQL implementation of \Drupal\Core\Database\Query\Merge.

Executes the merge query as a single native MERGE statement instead of the generic emulation with a SELECT followed by an INSERT or an UPDATE. The RETURNING merge_action() clause reports whether an INSERT or an UPDATE was performed, so execute() can still return the corresponding status code.

The generic emulation of the parent class is used instead when the query cannot be expressed as a native MERGE statement or would not benefit from it:

  • the condition targets a table other than the merge table, so the ON clause cannot be built from it;
  • there are no fields to insert, in which case a WHEN NOT MATCHED THEN INSERT clause cannot be generated;
  • values are inserted into serial fields, because the generic insert path takes care of synchronizing the corresponding sequences afterwards.

A native MERGE is not immune to race conditions under the default READ COMMITTED isolation level: a concurrent transaction can insert the same row after the match phase, causing an integrity constraint violation. When that happens the statement is retried once, so the racing row is matched and updated instead.

Hierarchy

Expanded class hierarchy of Merge

1 string reference to 'Merge'
ConnectionTest::providerGetDriverClass in core/tests/Drupal/Tests/Core/Database/ConnectionTest.php
Data provider for testGetDriverClass().

File

core/modules/pgsql/src/Driver/Database/pgsql/Merge.php, line 34

Namespace

Drupal\pgsql\Driver\Database\pgsql
View source
class Merge extends QueryMerge {
  
  /**
   * {@inheritdoc}
   */
  public function execute() {
    if (!count($this->condition)) {
      throw new InvalidMergeQueryException('Invalid merge query: no conditions');
    }
    if (!$this->isNativeMerge()) {
      return parent::execute();
    }
    // Fetch the list of blobs and sequences used on that table.
    $table_information = $this->connection
      ->schema()
      ->queryTableInformation($this->table);
    // Inserting values into serial fields requires synchronizing the
    // sequence afterwards, which the generic insert path takes care of.
    if (!empty($table_information->serial_fields) && array_intersect($table_information->serial_fields, array_keys($this->insertFields))) {
      return parent::execute();
    }
    try {
      return $this->executeNativeMerge($table_information);
    } catch (IntegrityConstraintViolationException) {
      // The merge query failed. Maybe it's because a racing insert query
      // beat us in inserting the same row. Retry once: the racing row will
      // now be matched and updated instead.
      return $this->executeNativeMerge($table_information);
    }
  }
  
  /**
   * Determines whether the query executes as a single native MERGE statement.
   *
   * The native MERGE statement can only be used when the condition targets
   * the merge table itself and when there is something to insert. In all
   * other cases the generic SELECT + INSERT/UPDATE emulation is used.
   *
   * @return bool
   *   TRUE if the query executes as a native MERGE statement, FALSE if it
   *   uses the generic emulation.
   */
  protected function isNativeMerge() : bool {
    return $this->conditionTable === $this->table && ($this->insertFields || $this->defaultFields);
  }
  
  /**
   * Executes the merge as a single native MERGE statement.
   *
   * @param object $table_information
   *   The table information for the merge table, containing the blob fields.
   *
   * @return int|null
   *   One of Merge::STATUS_INSERT, Merge::STATUS_UPDATE or NULL if the row
   *   already existed and no update was requested.
   */
  protected function executeNativeMerge(object $table_information) : ?int {
    $stmt = $this->connection
      ->prepareStatement((string) $this, $this->queryOptions, TRUE);
    $client_statement = $stmt->getClientStatement();
    $blobs = [];
    $blob_count = 0;
    $bind = function ($placeholder, $value, $field = NULL) use ($client_statement, $table_information, &$blobs, &$blob_count) {
      if ($field !== NULL && isset($table_information->blob_fields[$field]) && $value !== NULL) {
        $blobs[$blob_count] = fopen('php://memory', 'a');
        fwrite($blobs[$blob_count], $value);
        rewind($blobs[$blob_count]);
        $client_statement->bindParam($placeholder, $blobs[$blob_count], \PDO::PARAM_LOB);
        ++$blob_count;
      }
      else {
        $client_statement->bindValue($placeholder, $value);
      }
    };
    // Arguments of the WHEN MATCHED THEN UPDATE clause. Expressions take
    // priority over literal fields, matching the order in __toString().
    if ($this->needsUpdate) {
      foreach ($this->expressionFields as $data) {
        if (!empty($data['arguments'])) {
          foreach ($data['arguments'] as $placeholder => $argument) {
            $bind($placeholder, $argument);
          }
        }
        if ($data['expression'] instanceof SelectInterface) {
          $data['expression']->compile($this->connection, $this);
          foreach ($data['expression']->arguments() as $placeholder => $argument) {
            $bind($placeholder, $argument);
          }
        }
      }
      $max_placeholder = 0;
      foreach (array_diff_key($this->updateFields, $this->expressionFields) as $field => $value) {
        $bind(':db_update_placeholder_' . $max_placeholder++, $value, $field);
      }
    }
    // Arguments of the WHEN NOT MATCHED THEN INSERT clause.
    $max_placeholder = 0;
    foreach ($this->insertFields as $field => $value) {
      $bind(':db_insert_placeholder_' . $max_placeholder++, $value, $field);
    }
    // Arguments of the ON condition.
    $this->condition
      ->compile($this->connection, $this);
    foreach ($this->condition
      ->arguments() as $placeholder => $value) {
      $bind($placeholder, $value);
    }
    // Create a savepoint so we can rollback a failed query. This is so we can
    // mimic MySQL and SQLite transactions which don't fail if a single query
    // fails. This is important for tables that are created on demand. For
    // example, \Drupal\Core\Cache\DatabaseBackend.
    if ($this->connection
      ->inTransaction()) {
      $savepoint = $this->connection
        ->startTransaction('mimic_implicit_commit');
    }
    try {
      $stmt->execute(NULL, $this->queryOptions);
      if (isset($savepoint)) {
        $savepoint->commitOrRelease();
      }
    } catch (\Exception $e) {
      if (isset($savepoint)) {
        $savepoint->rollback();
      }
      $this->connection
        ->exceptionHandler()
        ->handleExecutionException($e, $stmt, [], $this->queryOptions);
    }
    return match ($stmt->fetchField()) {  'INSERT' => self::STATUS_INSERT,
      'UPDATE' => self::STATUS_UPDATE,
      default => NULL,
    
    };
  }
  
  /**
   * {@inheritdoc}
   */
  public function __toString() {
    // A query using the generic emulation is potentially two queries, so it
    // has no string representation. Let the parent implementation throw.
    if (!$this->isNativeMerge()) {
      return parent::__toString();
    }
    // Create a sanitized comment string to prepend to the query.
    $comments = $this->connection
      ->makeComment($this->comments);
    $this->condition
      ->compile($this->connection, $this);
    // MERGE requires a source relation even when merging a single row of
    // literal values, so use a constant single-row subquery. The ON clause
    // then only matches against the merge table.
    $query = $comments . 'MERGE INTO {' . $this->table . '} USING (SELECT 1) AS drupal_merge_source ON (' . $this->condition . ')';
    if ($this->needsUpdate) {
      $update_fields = [];
      // Expressions take priority over literal fields, so we process those
      // first and remove any literal fields that conflict.
      foreach ($this->expressionFields as $field => $data) {
        if ($data['expression'] instanceof SelectInterface) {
          $data['expression']->compile($this->connection, $this);
          $update_fields[] = $this->connection
            ->escapeField($field) . ' = (' . $data['expression'] . ')';
        }
        else {
          $update_fields[] = $this->connection
            ->escapeField($field) . ' = ' . $data['expression'];
        }
      }
      $max_placeholder = 0;
      foreach (array_diff_key($this->updateFields, $this->expressionFields) as $field => $value) {
        $update_fields[] = $this->connection
          ->escapeField($field) . ' = :db_update_placeholder_' . $max_placeholder++;
      }
      $query .= ' WHEN MATCHED THEN UPDATE SET ' . implode(', ', $update_fields);
    }
    $insert_fields = [];
    $values = [];
    $max_placeholder = 0;
    foreach (array_keys($this->insertFields) as $field) {
      $insert_fields[] = $this->connection
        ->escapeField($field);
      $values[] = ':db_insert_placeholder_' . $max_placeholder++;
    }
    foreach ($this->defaultFields as $field) {
      $insert_fields[] = $this->connection
        ->escapeField($field);
      $values[] = 'DEFAULT';
    }
    $query .= ' WHEN NOT MATCHED THEN INSERT (' . implode(', ', $insert_fields) . ') VALUES (' . implode(', ', $values) . ')';
    // The merge_action() function reports whether the row was inserted or
    // updated, which execute() maps to the STATUS_INSERT and STATUS_UPDATE
    // return values.
    return $query . ' RETURNING merge_action()';
  }

}

Members

Title Sort descending Modifiers Object type Summary Overriden Title Overrides
Merge::$conditionTable protected property The table or subquery to be used for the condition.
Merge::$defaultFields protected property An array of fields which should be set to their database-defined defaults.
Merge::$expressionFields protected property Array of fields to update to an expression in case of a duplicate record.
Merge::$insertFields protected property An array of fields on which to insert.
Merge::$insertValues protected property An array of values to be inserted.
Merge::$needsUpdate protected property Flag indicating whether an UPDATE is necessary.
Merge::$table protected property The table to be used for INSERT and UPDATE.
Merge::$updateFields protected property An array of fields that will be updated.
Merge::conditionTable protected function Sets the table or subquery to be used for the condition.
Merge::execute public function Overrides Merge::execute
Merge::executeNativeMerge protected function Executes the merge as a single native MERGE statement.
Merge::expression public function Specifies fields to be updated as an expression.
Merge::fields public function Sets common field-value pairs in the INSERT and UPDATE query parts.
Merge::insertFields public function Adds a set of field->value pairs to be inserted.
Merge::isNativeMerge protected function Determines whether the query executes as a single native MERGE statement.
Merge::key public function Sets a single key field to be used as condition for this query.
Merge::keys public function Sets the key fields to be used as conditions for this query.
Merge::STATUS_INSERT constant Returned by execute() if an INSERT query has been executed.
Merge::STATUS_UPDATE constant Returned by execute() if an UPDATE query has been executed.
Merge::updateFields public function Adds a set of field->value pairs to be updated.
Merge::useDefaults public function Specifies fields for which the database-defaults should be used.
Merge::__construct public function Constructs a Merge object. Overrides Query::__construct
Merge::__toString public function Overrides Merge::__toString
Query::$comments protected property An array of comments that can be prepended to a query.
Query::$connection protected property The connection object on which to run this query.
Query::$connectionKey protected property The key of the connection object.
Query::$connectionTarget protected property The target of the connection object.
Query::$nextPlaceholder protected property The placeholder counter.
Query::$queryOptions protected property The query options to pass on to the connection object.
Query::$uniqueIdentifier protected property A unique identifier for this query object.
Query::comment public function Adds a comment to the query.
Query::getComments public function Returns a reference to the comments array for the query.
Query::getConnection public function Gets the database connection to be used for the query.
Query::nextPlaceholder public function Gets the next placeholder value for this query object. Overrides PlaceholderInterface::nextPlaceholder
Query::uniqueIdentifier public function Returns a unique identifier for this object. Overrides PlaceholderInterface::uniqueIdentifier
Query::__clone public function Implements the magic __clone function. 1
Query::__sleep public function Implements the magic __sleep function to disconnect from the database.
Query::__wakeup public function Implements the magic __wakeup function to reconnect to the database.
QueryConditionTrait::$condition protected property The condition object for this query.
QueryConditionTrait::alwaysFalse public function
QueryConditionTrait::andConditionGroup public function
QueryConditionTrait::arguments public function 2
QueryConditionTrait::compile public function 1
QueryConditionTrait::compiled public function 1
QueryConditionTrait::condition public function
QueryConditionTrait::conditionGroupFactory public function
QueryConditionTrait::conditions public function
QueryConditionTrait::exists public function
QueryConditionTrait::isNotNull public function
QueryConditionTrait::isNull public function
QueryConditionTrait::notExists public function
QueryConditionTrait::orConditionGroup public function
QueryConditionTrait::where public function

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.