Verified Commit 7bdc1c89 authored by Andrei Mateescu's avatar Andrei Mateescu
Browse files

task: #3604286 Upsert - Allow to customize the behavior of the update when the...

task: #3604286 Upsert - Allow to customize the behavior of the update when the insert fails and use more Upsert queries in core

By: mondrake
By: daffie
By: amateescu
(cherry picked from commit 047e314b)
parent 24c62565
Loading
Loading
Loading
Loading
Loading
+11 −4
Original line number Diff line number Diff line
@@ -4,6 +4,7 @@

use Drupal\Core\Database\Connection;
use Drupal\Core\Database\DatabaseException;
use Drupal\Core\Database\Query\UpsertUpdateExpression;

/**
 * Cache tags invalidations checksum implementation that uses the database.
@@ -35,10 +36,16 @@ public function __construct(Connection $connection) {
  protected function doInvalidateTags(array $tags) {
    try {
      foreach ($tags as $tag) {
        $this->connection->merge('cachetags')
          ->insertFields(['invalidations' => 1])
          ->expression('invalidations', '[invalidations] + 1')
          ->key('tag', $tag)
        $this->connection->upsert('cachetags')
          ->key(['tag'])
          ->fields([
            'tag',
            'invalidations' => new UpsertUpdateExpression('{cachetags}.[invalidations] + 1'),
          ])
          ->values([
            'tag' => $tag,
            'invalidations' => 1,
          ])
          ->execute();
      }
    }
+25 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\Core\Database;

/**
 * A value object representing an expression with its arguments.
 */
class Expression {

  /**
   * Constructor.
   *
   * @param string $expression
   *   The expression text. Never include user input in the expression text;
   *   pass it through $arguments placeholders instead.
   * @param array<string,string|int|float|bool> $arguments
   *   (optional) The expression arguments, keyed by placeholder name.
   */
  public function __construct(
    public readonly string $expression,
    public readonly array $arguments = [],
  ) {
  }

}
+83 −0
Original line number Diff line number Diff line
@@ -22,6 +22,19 @@ abstract class Upsert extends Query implements \Countable {
   */
  protected $key;

  /**
   * Explicit updates to be done in case of insert failure.
   *
   * If the insert part of the upsert fails, the default behavior is to update
   * the existing record with the values passed in. However, in some cases you
   * may want to see what is the value of a column of the existing record, and
   * manipulate that - typically, counters or the like. This array contains
   * such overrides.
   *
   * @var array<string,\Drupal\Core\Database\Query\UpsertUpdateExpression>
   */
  protected array $updateExpressions = [];

  /**
   * Constructs an Upsert object.
   *
@@ -51,6 +64,71 @@ public function key(string|array $field) {
    return $this;
  }

  /**
   * Adds a set of column->value pairs to be inserted.
   *
   * This method may only be called once. Calling it a second time will be
   * ignored. To queue up multiple sets of values to be inserted at once,
   * use the values() method.
   *
   * @param array<string|int,string|\Drupal\Core\Database\Query\UpsertUpdateExpression>|array<string,string|int|float|bool|array> $fields
   *   An array of columns on which to insert. This array may be indexed or
   *   associative. If indexed, the array is taken to be the list of columns.
   *   If associative, the keys of the array are taken to be the columns and
   *   the values are taken to be corresponding values to insert. If a
   *   $values argument is provided, $fields must be indexed.
   *   In the indexed form, an entry may map a column name to an
   *   UpsertUpdateExpression. When the row already exists, that expression is
   *   run for the column in place of overwriting it with the insert value.
   * @param list<string|int|float|bool|array> $values
   *   (optional) An array of values to insert into the database. The values
   *   must be specified in the same order as the $fields array.
   *
   * @return $this
   *   The called object.
   *
   * @throws \InvalidArgumentException
   *   If an update expression is not keyed by its column name, or if a column
   *   name maps to an insert value while the $fields array is not fully
   *   associative.
   */
  public function fields(array $fields, array $values = []): static {
    if (empty($this->insertFields)) {
      if (empty($values)) {
        $isFieldsAssociative = TRUE;
        array_walk($fields, function (mixed $value, int|string $key) use (&$isFieldsAssociative): void {
          if (is_numeric($key) || (is_string($key) && $value instanceof UpsertUpdateExpression)) {
            $isFieldsAssociative = FALSE;
          }
        });
        if ($isFieldsAssociative) {
          $values = array_values($fields);
          $fields = array_keys($fields);
        }
      }
      foreach ($fields as $key => $def) {
        if ($def instanceof UpsertUpdateExpression) {
          if (!is_string($key)) {
            throw new \InvalidArgumentException('An update expression must be keyed by its column name.');
          }
          $this->insertFields[] = $key;
          $this->updateExpressions[$key] = $def;
        }
        elseif (is_string($key)) {
          throw new \InvalidArgumentException(sprintf('The entry for "%s" must be a column name or an update expression keyed by column name. Pass insert values via the $values argument or the values() method.', $key));
        }
        else {
          $this->insertFields[] = $def;
        }
      }
      if (!empty($values)) {
        $this->values($values);
      }
    }

    return $this;
  }

  /**
   * Preprocesses and validates the query.
   *
@@ -104,6 +182,11 @@ public function execute() {
        $values[':db_insert_placeholder_' . $max_placeholder++] = $value;
      }
    }
    foreach ($this->updateExpressions as $update_expression) {
      foreach ($update_expression->arguments as $argument => $value) {
        $values[$argument] = $value;
      }
    }

    $stmt = $this->connection->prepareStatement((string) $this, $this->queryOptions, TRUE);
    try {
+11 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\Core\Database\Query;

use Drupal\Core\Database\Expression;

/**
 * A value object to handle an Upsert update expression.
 */
class UpsertUpdateExpression extends Expression {
}
+14 −6
Original line number Diff line number Diff line
@@ -1426,9 +1426,13 @@ function hook_entity_predelete(\Drupal\Core\Entity\EntityInterface $entity): voi
    ->fetchField();

  // Log the count in a table that records this statistic for deleted entities.
  $connection->merge('example_deleted_entity_statistics')
    ->keys(['type' => $type, 'id' => $id])
    ->fields(['count' => $count])
  $connection->upsert('example_deleted_entity_statistics')
    ->key(['type', 'id'])
    ->fields([
      'type' => $type,
      'id' => $id,
      'count' => $count,
    ])
    ->execute();
}

@@ -1455,9 +1459,13 @@ function hook_ENTITY_TYPE_predelete(\Drupal\Core\Entity\EntityInterface $entity)
    ->fetchField();

  // Log the count in a table that records this statistic for deleted entities.
  $connection->merge('example_deleted_entity_statistics')
    ->keys(['type' => $type, 'id' => $id])
    ->fields(['count' => $count])
  $connection->upsert('example_deleted_entity_statistics')
    ->key(['type', 'id'])
    ->fields([
      'type' => $type,
      'id' => $id,
      'count' => $count,
    ])
    ->execute();
}

Loading