Skip to content
Closed
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
87 changes: 60 additions & 27 deletions src/Database/Schema/Grammars/Concerns/MySqlBasedGrammar.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

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
Expand All @@ -18,42 +20,73 @@ 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();

$oldColumns = collect($schema->getColumns($table));
/** @var \Illuminate\Database\Schema\ColumnDefinition $column */
$column = clone $command->get('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)
);
$existing = collect($this->connection->getSchemaBuilder()->getColumns($blueprint->getTable()))
->firstWhere('name', $column->get('name'));

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

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);
foreach ($this->getKeptColumnAttributes($existing, $column) as $attribute => $value) {
if (!array_key_exists($attribute, $attributes)) {
$column[$attribute] = $value;
}
}
$columns[] = $sql;
}

return 'alter table '.$this->wrapTable($blueprint).' '.implode(', ', $columns);
return parent::compileChange($blueprint, new Fluent(['column' => $column] + $command->getAttributes()));
}

/**
* 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.
*
* @param array|\ArrayAccess $existing
* @param \Illuminate\Support\Fluent $column
* @return array
*/
protected function getKeptColumnAttributes($existing, Fluent $column): array
{
$attributes = [
'nullable' => (bool) $existing['nullable'],
'default' => $this->getKeptColumnDefault($existing['default']),
'comment' => $existing['comment'],
'unsigned' => str_contains(strtolower((string) $existing['type']), 'unsigned') ?: null,
];

// Like Laravel 9, only keep the collation while the column stays a text column
if (in_array($column->get('type'), ['char', 'string', 'tinyText', 'text', 'mediumText', 'longText', 'enum', 'set'])) {
$attributes['collation'] = $existing['collation'];
}

return array_filter($attributes, fn ($value) => !is_null($value));
}

/**
* 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;
}

if ($this instanceof MariaDbGrammar || ($this->connection instanceof MySqlConnection && $this->connection->isMaria())) {
return $default === 'NULL' ? null : new Expression($default);
}

return str_starts_with(strtolower($default), 'current_timestamp') ? new Expression($default) : $default;
}

/**
Expand Down
64 changes: 36 additions & 28 deletions src/Database/Schema/Grammars/PostgresGrammar.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

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;

Expand All @@ -16,48 +16,56 @@ class PostgresGrammar extends BasePostgresGrammar
* 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();
/** @var \Illuminate\Database\Schema\ColumnDefinition $column */
$column = $command->get('column');

$oldColumns = collect($schema->getColumns($table));
$existing = collect($this->connection->getSchemaBuilder()->getColumns($blueprint->getTable()))
->firstWhere('name', $column->get('name'));

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

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

foreach ($this->modifiers as $modifier) {
if ($modifier === 'Collate') {
continue;
foreach ($this->getKeptColumnAttributes($existing, $column) as $attribute => $value) {
if (!array_key_exists($attribute, $attributes)) {
$column[$attribute] = $value;
}
}
}

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;
}
}
}
/**
* 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.
*
* @param array|\ArrayAccess $existing
* @param \Illuminate\Support\Fluent $column
* @return array
*/
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 (in_array($column->get('type'), ['char', 'string', 'tinyText', 'text', 'mediumText', 'longText', 'enum', 'set'])) {
$attributes['collation'] = $existing['collation'];
}

return 'alter table '.$this->wrapTable($blueprint).' '.implode(', ', $columns);
return array_filter($attributes, fn ($value) => !is_null($value));
}

/**
Expand Down
62 changes: 36 additions & 26 deletions src/Database/Schema/Grammars/SqlServerGrammar.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

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;

Expand All @@ -16,45 +16,55 @@ class SqlServerGrammar extends BaseSqlServerGrammar
* 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();

$oldColumns = collect($schema->getColumns($table));
/** @var \Illuminate\Database\Schema\ColumnDefinition $column */
$column = $command->get('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)
);
$existing = collect($this->connection->getSchemaBuilder()->getColumns($blueprint->getTable()))
->firstWhere('name', $column->get('name'));

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

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);
foreach ($this->getKeptColumnAttributes($existing, $column) as $attribute => $value) {
if (!array_key_exists($attribute, $attributes)) {
$column[$attribute] = $value;
}
}
}

return parent::compileChange($blueprint, $command);
}

/**
* 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.
*
* @param array|\ArrayAccess $existing
* @param \Illuminate\Support\Fluent $column
* @return array
*/
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 (in_array($column->get('type'), ['char', 'string', 'tinyText', 'text', 'mediumText', 'longText', 'enum', 'set'])) {
$attributes['collation'] = $existing['collation'];
}

return $changes;
return array_filter($attributes, fn ($value) => !is_null($value));
}

/**
Expand Down
Loading
Loading