Commit fa4fd791 authored by catch's avatar catch
Browse files

feat: #3586688 Add support for generated columns to the DB Schema Definition API

By: amateescu
By: mondrake
By: daffie
parent 35bbc4e3
Loading
Loading
Loading
Loading
+57 −0
Changes for core/lib/Drupal/Core/Database/SchemaDefinition/Column.php: 57 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -59,6 +59,10 @@ final class Column implements SchemaDefinitionInterface {
   *   has no effect on other database types for which case sensitivity is
   *   already the default behavior. If NULL, it is not specified. Defaults to
   *   NULL.
   * @param ?GeneratedColumnStorage $generatedStorage
   *   The storage strategy for a generated column (VIRTUAL or STORED).
   * @param ?GeneratedColumnExpression $generatedExpression
   *   The expression for a generated column.
   * @param array<string,array<string,mixed>>|null $dbSpecificExtra
   *   (Optional) If you need to use a column type not included in the
   *   officially supported list of types above, you can specify a type for
@@ -69,6 +73,8 @@ final class Column implements SchemaDefinitionInterface {
   *
   * @see \Drupal\Core\Database\SchemaDefinition\ColumnType
   * @see \Drupal\Core\Database\SchemaDefinition\ColumnSize
   * @see \Drupal\Core\Database\SchemaDefinition\GeneratedColumnExpression
   * @see \Drupal\Core\Database\SchemaDefinition\GeneratedColumnStorage
   */
  private function __construct(
    public readonly string $name,
@@ -83,6 +89,8 @@ private function __construct(
    public readonly ?int $precision = NULL,
    public readonly ?int $scale = NULL,
    public readonly ?bool $binary = NULL,
    public readonly ?GeneratedColumnStorage $generatedStorage = NULL,
    public readonly ?GeneratedColumnExpression $generatedExpression = NULL,
    public readonly ?array $dbSpecificExtra = NULL,
  ) {
  }
@@ -141,6 +149,12 @@ private function validate(): void {
      throw new SchemaDefinitionException("Cannot set 'notNull' for {$this->type->value} column '{$this->name}'");
    }

    // A serial column takes its value from a sequence, so it cannot also be
    // computed from an expression.
    if ($this->generatedExpression !== NULL && $this->type === ColumnType::Serial) {
      throw new SchemaDefinitionException("Cannot set 'generatedExpression' for {$this->type->value} column '{$this->name}'");
    }

    // Can only set unsigned for some column types.
    if ($this->unsigned !== NULL && $this->type !== NULL && !in_array($this->type, [
      ColumnType::Int,
@@ -215,6 +229,9 @@ public function toArray(): array {
    if ($this->description !== NULL) {
      $spec['description'] = $this->description;
    }
    if ($this->generatedStorage) {
      $spec['generated'] = $this;
    }
    if ($this->serialize !== NULL) {
      $spec['serialize'] = $this->serialize;
    }
@@ -458,4 +475,44 @@ public static function blob(
    );
  }

  /**
   * Returns a Column object for a generated column.
   *
   * @see https://dev.mysql.com/doc/refman/9.7/en/create-table-generated-columns.html
   * @see https://mariadb.com/docs/server/reference/sql-statements/data-definition/create/generated-columns
   * @see https://www.postgresql.org/docs/current/ddl-generated-columns.html
   * @see https://sqlite.org/gencol.html
   */
  public static function generated(
    string $name,
    ColumnType $type,
    GeneratedColumnExpression $expression,
    GeneratedColumnStorage $storage,
    ?string $description = NULL,
    ?ColumnSize $size = NULL,
    ?int $length = NULL,
    ?bool $unsigned = NULL,
    ?int $precision = NULL,
    ?int $scale = NULL,
    ?bool $binary = NULL,
    ?array $dbSpecificExtra = NULL,
  ): self {
    $column = new self(
      type: $type,
      name: $name,
      description: $description,
      size: $size,
      length: $length,
      unsigned: $unsigned,
      precision: $precision,
      scale: $scale,
      binary: $binary,
      generatedStorage: $storage,
      generatedExpression: $expression,
      dbSpecificExtra: $dbSpecificExtra,
    );
    $column->validate();
    return $column;
  }

}
+26 −0
Changes for core/lib/Drupal/Core/Database/SchemaDefinition/GeneratedColumnExpression.php: 26 added lines, 0 removed lines.
Original line number Diff line number Diff line
<?php

declare(strict_types=1);

namespace Drupal\Core\Database\SchemaDefinition;

use Drupal\Core\Database\Expression;

/**
 * A value object representing an expression for a generated column.
 */
final class GeneratedColumnExpression extends Expression {

  /**
   * Constructor.
   *
   * @param string $expression
   *   The expression text.
   */
  public function __construct(
    string $expression,
  ) {
    parent::__construct($expression);
  }

}
+15 −0
Changes for core/lib/Drupal/Core/Database/SchemaDefinition/GeneratedColumnStorage.php: 15 added lines, 0 removed lines.
Original line number Diff line number Diff line
<?php

declare(strict_types=1);

namespace Drupal\Core\Database\SchemaDefinition;

/**
 * Enumeration of cases for generated column.
 */
enum GeneratedColumnStorage {

  case Virtual;
  case Stored;

}
+14 −1
Changes for core/lib/Drupal/Core/Database/SchemaDefinition/Table.php: 14 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -62,7 +62,6 @@ public function __construct(
   * @throws \Drupal\Core\Database\Exception\SchemaDefinitionException
   */
  private function validate(): void {

    if ($this->columns && !Inspector::assertAllObjects($this->columns, Column::class)) {
      throw new SchemaDefinitionException("All members of the 'columns' argument of the '{$this->name}' Table must be Column objects");
    }
@@ -76,6 +75,20 @@ private function validate(): void {
      throw new SchemaDefinitionException("All members of the 'foreignKeys' argument of the '{$this->name}' Table must be ForeignKey objects");
    }

    // MariaDB and SQLite do not allow generated columns in the primary key.
    if ($this->columns && $this->primaryKey) {
      $generatedColumnNames = array_map(
        static fn (Column $column): string => $column->name,
        array_filter($this->columns, static fn (Column $column): bool => $column->generatedExpression !== NULL),
      );
      $primaryKeyColumnNames = array_map(
        static fn (KeyColumn $keyColumn): string => $keyColumn->name,
        $this->primaryKey->columns,
      );
      if (array_intersect($primaryKeyColumnNames, $generatedColumnNames)) {
        throw new SchemaDefinitionException("The primary key of the '{$this->name}' Table cannot include generated columns");
      }
    }
  }

  /**
+7 −0
Changes for core/lib/Drupal/Core/Database/database.api.php: 7 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -377,6 +377,13 @@
 *       'varchar' or 'text' fields to use case-sensitive binary collation.
 *       This has no effect on other database types for which case sensitivity
 *       is already the default behavior.
 *     - 'generated': A \Drupal\Core\Database\SchemaDefinition\Column object
 *       returned by Column::generated(). It holds the expression the database
 *       computes the column value from, and whether that value is stored on
 *       write or computed on read. A generated column cannot have a 'default'
 *       or a 'not null' constraint, and cannot be part of the primary key.
 *       Prefer stored over virtual for portability: PostgreSQL supports virtual
 *       columns only from version 18 and cannot index them.
 *     All parameters apart from 'type' are optional except that type
 *     'numeric' columns must specify 'precision' and 'scale', and type
 *     'varchar' must specify the 'length' parameter.
Loading