Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 0 additions & 30 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -754,36 +754,6 @@ parameters:
identifier: method.childParameterType
path: src/Database/Relations/MorphToMany.php

-
message: '#^Access to an undefined property Illuminate\\Database\\Schema\\ColumnDefinition\:\:\$name\.$#'
identifier: property.notFound
path: src/Database/Schema/Grammars/MariaDbGrammar.php

-
message: '#^Access to an undefined property Illuminate\\Database\\Schema\\ColumnDefinition\:\:\$renameTo\.$#'
identifier: property.notFound
path: src/Database/Schema/Grammars/MariaDbGrammar.php

-
message: '#^Access to an undefined property Illuminate\\Database\\Schema\\ColumnDefinition\:\:\$name\.$#'
identifier: property.notFound
path: src/Database/Schema/Grammars/MySqlGrammar.php

-
message: '#^Access to an undefined property Illuminate\\Database\\Schema\\ColumnDefinition\:\:\$renameTo\.$#'
identifier: property.notFound
path: src/Database/Schema/Grammars/MySqlGrammar.php

-
message: '#^Access to an undefined property Illuminate\\Database\\Schema\\ColumnDefinition\:\:\$name\.$#'
identifier: property.notFound
path: src/Database/Schema/Grammars/PostgresGrammar.php

-
message: '#^Access to an undefined property Illuminate\\Database\\Schema\\ColumnDefinition\:\:\$name\.$#'
identifier: property.notFound
path: src/Database/Schema/Grammars/SqlServerGrammar.php

-
message: '#^Call to an undefined method Illuminate\\Database\\Eloquent\\Model\:\:getSortOrderColumn\(\)\.$#'
identifier: method.notFound
Expand Down
4 changes: 3 additions & 1 deletion src/Database/Schema/BlueprintState.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,10 @@ protected function preserveExistingColumnAttributes(Fluent $column): void
continue;
}

$attributes = $column->getAttributes();

foreach ($this->preservedAttributes as $attribute) {
if (!isset($column[$attribute]) && isset($existing[$attribute])) {
if (!array_key_exists($attribute, $attributes) && isset($existing[$attribute])) {
$column[$attribute] = $existing[$attribute];
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

namespace Winter\Storm\Database\Schema\Grammars\Concerns;

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Fluent;

/**
* Shared by the grammars that restore Laravel's pre-11 behaviour of keeping the attributes of a
* changed column that the new Blueprint doesn't set. Each grammar decides which attributes those
* are, and in what form the server reports them, in getKeptColumnAttributes().
*/
trait KeepsExistingColumnAttributes
{
/**
* Get the attributes of an existing column, as reported by the schema builder, that a change keeps
* when the new definition doesn't set them. A null value is left to the new definition.
*
* @param array|\ArrayAccess $existing
* @param \Illuminate\Support\Fluent $column
* @return array
*/
abstract protected function getKeptColumnAttributes($existing, Fluent $column): array;

/**
* Copy the attributes of the existing column that the new definition doesn't set onto the column.
*
* The attributes are set on the column itself, because on PostgreSQL and SQL Server its comment and
* default are compiled by separate commands that run after the change and read the same column.
*/
protected function keepExistingColumnAttributes(Blueprint $blueprint, Fluent $column): void
{
$existing = collect($this->connection->getSchemaBuilder()->getColumns($blueprint->getTable()))
->firstWhere('name', $column->get('name'));

if (!$existing) {
return;
}

$attributes = $column->getAttributes();

foreach ($this->getKeptColumnAttributes($existing, $column) as $attribute => $value) {
if (!is_null($value) && !array_key_exists($attribute, $attributes)) {
$column[$attribute] = $value;
}
}
}

/**
* Whether the column is being changed to a type that accepts a collation.
*
* Like Laravel 9, the collation is only kept while the column stays a text column.
*/
protected function changesToATextColumn(Fluent $column): bool
{
return in_array($column->get('type'), [
'char', 'string', 'tinyText', 'text', 'mediumText', 'longText', 'enum', 'set',
]);
}
}
76 changes: 48 additions & 28 deletions src/Database/Schema/Grammars/Concerns/MySqlBasedGrammar.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@

namespace Winter\Storm\Database\Schema\Grammars\Concerns;

use Illuminate\Database\MySqlConnection;
use Illuminate\Database\Query\Expression;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Schema\ColumnDefinition;
use Illuminate\Database\Schema\Grammars\MariaDbGrammar;
use Illuminate\Support\Fluent;

trait MySqlBasedGrammar
{
use KeepsExistingColumnAttributes;

/**
* Compile a change column command into a series of SQL statements.
*
Expand All @@ -18,42 +22,58 @@ trait MySqlBasedGrammar
* @param \Illuminate\Database\Schema\Blueprint $blueprint
* @param \Illuminate\Support\Fluent $command
* @return array|string
*
* @throws \RuntimeException
*/
public function compileChange(Blueprint $blueprint, Fluent $command)
{
$columns = [];
$schema = $this->connection->getSchemaBuilder();
$table = $blueprint->getTable();
/** @var \Illuminate\Database\Schema\ColumnDefinition $column */
$column = $command->get('column');

$oldColumns = collect($schema->getColumns($table));
$this->keepExistingColumnAttributes($blueprint, $column);

foreach ($blueprint->getChangedColumns() as $column) {
$sql = sprintf(
'%s %s%s %s',
is_null($column->renameTo) ? 'modify' : 'change',
$this->wrap($column->name),
is_null($column->renameTo) ? '' : ' '.$this->wrap($column->renameTo),
$this->getType($column)
);
return parent::compileChange($blueprint, $command);
}

/** @inheritDoc */
protected function getKeptColumnAttributes($existing, Fluent $column): array
{
$attributes = [
'nullable' => (bool) $existing['nullable'],
'default' => $this->getKeptColumnDefault($existing['default']),
'comment' => $existing['comment'],
];

// Only keep the unsigned attribute while the column stays a numeric column, as no other type accepts it
if (in_array($column->get('type'), ['boolean', 'tinyInteger', 'smallInteger', 'mediumInteger', 'integer', 'bigInteger', 'decimal', 'float', 'double'])) {
$attributes['unsigned'] = str_contains(strtolower((string) $existing['type']), 'unsigned') ?: null;
}

if ($this->changesToATextColumn($column)) {
$attributes['collation'] = $existing['collation'];
}

$oldColumn = $oldColumns->where('name', $column->name)->first();
if (!$oldColumn instanceof ColumnDefinition) {
$oldColumn = new ColumnDefinition($oldColumn);
}
return $attributes;
}

/**
* Turn the default of an existing column, as reported by the schema builder, back into a definition.
*
* MariaDB reports every default as an SQL expression ('text', NULL, current_timestamp()). MySQL reports
* a literal default as its raw value, and an expression such as CURRENT_TIMESTAMP as is.
*
* @param mixed $default
* @return mixed
*/
protected function getKeptColumnDefault($default)
{
if (!is_string($default)) {
return $default;
}

foreach ($this->modifiers as $modifier) {
if (method_exists($this, $method = "modify{$modifier}")) {
$mod = strtolower($modifier);
$col = isset($oldColumn->{$mod}) && !isset($column->{$mod}) ? $oldColumn : $column;
$sql .= $this->{$method}($blueprint, $col);
}
}
$columns[] = $sql;
if ($this instanceof MariaDbGrammar || ($this->connection instanceof MySqlConnection && $this->connection->isMaria())) {
return $default === 'NULL' ? null : new Expression($default);
}

return 'alter table '.$this->wrapTable($blueprint).' '.implode(', ', $columns);
return str_starts_with(strtolower($default), 'current_timestamp') ? new Expression($default) : $default;
}

/**
Expand Down
54 changes: 23 additions & 31 deletions src/Database/Schema/Grammars/PostgresGrammar.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,62 +2,54 @@

namespace Winter\Storm\Database\Schema\Grammars;

use Illuminate\Database\Query\Expression;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Schema\ColumnDefinition;
use Illuminate\Database\Schema\Grammars\PostgresGrammar as BasePostgresGrammar;
use Illuminate\Support\Fluent;

class PostgresGrammar extends BasePostgresGrammar
{
use Concerns\KeepsExistingColumnAttributes;

/**
* Compile a change column command into a series of SQL statements.
*
* Starting with Laravel 11, previous column attributes do not persist when changing a column.
* This restores Laravel previous behavior where existing column attributes are kept
* unless they get changed by the new Blueprint.
*
* The kept attributes are set on the column itself, so that the comment command that runs
* after the change keeps the existing comment as well.
*
* @param \Illuminate\Database\Schema\Blueprint $blueprint
* @param \Illuminate\Support\Fluent $command
* @return array|string
*
* @throws \RuntimeException
*/
public function compileChange(Blueprint $blueprint, Fluent $command)
{
$columns = [];
$schema = $this->connection->getSchemaBuilder();
$table = $blueprint->getTable();

$oldColumns = collect($schema->getColumns($table));
/** @var \Illuminate\Database\Schema\ColumnDefinition $column */
$column = $command->get('column');

foreach ($blueprint->getChangedColumns() as $column) {
$changes = ['type '.$this->getType($column).$this->modifyCollate($blueprint, $column)];
$this->keepExistingColumnAttributes($blueprint, $column);

$oldColumn = $oldColumns->where('name', $column->name)->first();
if (!$oldColumn instanceof ColumnDefinition) {
$oldColumn = new ColumnDefinition($oldColumn);
}

foreach ($this->modifiers as $modifier) {
if ($modifier === 'Collate') {
continue;
}

if (method_exists($this, $method = "modify{$modifier}")) {
$mod = strtolower($modifier);
$col = isset($oldColumn->{$mod}) && ! isset($column->{$mod}) ? $oldColumn : $column;
$constraints = (array) $this->{$method}($blueprint, $col);
return parent::compileChange($blueprint, $command);
}

foreach ($constraints as $constraint) {
$changes[] = $constraint;
}
}
}
/** @inheritDoc */
protected function getKeptColumnAttributes($existing, Fluent $column): array
{
$attributes = [
'nullable' => (bool) $existing['nullable'],
// PostgreSQL reports every default as an SQL expression, such as 'text'::character varying
'default' => is_null($existing['default']) ? null : new Expression($existing['default']),
'comment' => $existing['comment'],
];

$columns[] = implode(', ', $this->prefixArray('alter column '.$this->wrap($column->name), $changes));
if ($this->changesToATextColumn($column)) {
$attributes['collation'] = $existing['collation'];
}

return 'alter table '.$this->wrapTable($blueprint).' '.implode(', ', $columns);
return $attributes;
}

/**
Expand Down
50 changes: 22 additions & 28 deletions src/Database/Schema/Grammars/SqlServerGrammar.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,59 +2,53 @@

namespace Winter\Storm\Database\Schema\Grammars;

use Illuminate\Database\Query\Expression;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Schema\ColumnDefinition;
use Illuminate\Database\Schema\Grammars\SqlServerGrammar as BaseSqlServerGrammar;
use Illuminate\Support\Fluent;

class SqlServerGrammar extends BaseSqlServerGrammar
{
use Concerns\KeepsExistingColumnAttributes;

/**
* Compile a change column command into a series of SQL statements.
*
* Starting with Laravel 11, previous column attributes do not persist when changing a column.
* This restores Laravel previous behavior where existing column attributes are kept
* unless they get changed by the new Blueprint.
*
* The kept attributes are set on the column itself, so that the default command that runs
* after the change adds the existing default back once the change has dropped its constraint.
*
* @param \Illuminate\Database\Schema\Blueprint $blueprint
* @param \Illuminate\Support\Fluent $command
* @return array|string
*
* @throws \RuntimeException
*/
public function compileChange(Blueprint $blueprint, Fluent $command)
{
$changes = (array) $this->compileDropDefaultConstraint($blueprint, $command);
$schema = $this->connection->getSchemaBuilder();
$table = $blueprint->getTable();
/** @var \Illuminate\Database\Schema\ColumnDefinition $column */
$column = $command->get('column');

$oldColumns = collect($schema->getColumns($table));
$this->keepExistingColumnAttributes($blueprint, $column);

foreach ($blueprint->getChangedColumns() as $column) {
$sql = sprintf(
'alter table %s alter column %s %s',
$this->wrapTable($blueprint),
$this->wrap($column->name),
$this->getType($column)
);

$oldColumn = $oldColumns->where('name', $column->name)->first();
if (!$oldColumn instanceof ColumnDefinition) {
$oldColumn = new ColumnDefinition($oldColumn);
}
return parent::compileChange($blueprint, $command);
}

foreach ($this->modifiers as $modifier) {
if (method_exists($this, $method = "modify{$modifier}")) {
$mod = strtolower($modifier);
$col = isset($oldColumn->{$mod}) && !isset($column->{$mod}) ? $oldColumn : $column;
$sql .= $this->{$method}($blueprint, $col);
}
}
/** @inheritDoc */
protected function getKeptColumnAttributes($existing, Fluent $column): array
{
$attributes = [
'nullable' => (bool) $existing['nullable'],
// SQL Server reports a default as the expression of its constraint, such as ('text')
'default' => is_null($existing['default']) ? null : new Expression($existing['default']),
];

$changes[] = $sql;
if ($this->changesToATextColumn($column)) {
$attributes['collation'] = $existing['collation'];
}

return $changes;
return $attributes;
}

/**
Expand Down
Loading
Loading