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
36 changes: 0 additions & 36 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -930,42 +930,6 @@ parameters:
count: 1
path: src/Database/Schema/Grammars/PostgresGrammar.php

-
message: '#^Access to an undefined property Illuminate\\Support\\Fluent\:\:\$autoIncrement\.$#'
identifier: property.notFound
count: 1
path: src/Database/Schema/Grammars/SQLiteGrammar.php

-
message: '#^Access to an undefined property Illuminate\\Support\\Fluent\:\:\$name\.$#'
identifier: property.notFound
count: 2
path: src/Database/Schema/Grammars/SQLiteGrammar.php

-
message: '#^Access to an undefined property Illuminate\\Support\\Fluent\:\:\$storedAs\.$#'
identifier: property.notFound
count: 1
path: src/Database/Schema/Grammars/SQLiteGrammar.php

-
message: '#^Access to an undefined property Illuminate\\Support\\Fluent\:\:\$storedAsJson\.$#'
identifier: property.notFound
count: 1
path: src/Database/Schema/Grammars/SQLiteGrammar.php

-
message: '#^Access to an undefined property Illuminate\\Support\\Fluent\:\:\$virtualAs\.$#'
identifier: property.notFound
count: 1
path: src/Database/Schema/Grammars/SQLiteGrammar.php

-
message: '#^Access to an undefined property Illuminate\\Support\\Fluent\:\:\$virtualAsJson\.$#'
identifier: property.notFound
count: 1
path: src/Database/Schema/Grammars/SQLiteGrammar.php

-
message: '#^Access to an undefined property Illuminate\\Database\\Schema\\ColumnDefinition\:\:\$name\.$#'
identifier: property.notFound
Expand Down
19 changes: 19 additions & 0 deletions src/Database/Schema/Blueprint.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,23 @@ public function dropColumnIfExists($columns)

return !empty($columns) ? $this->dropColumn($columns) : $this;
}

/**
* Add the commands that are implied by the blueprint's state.
*
* Swaps in Winter's BlueprintState so that a `->change()` preserves the existing column's
* attributes (see {@see \Winter\Storm\Database\Schema\BlueprintState}). Laravel hard-codes its
* own state class when an alter command is present, so we re-seed with ours once the base
* implied commands - and therefore the base state - have been created.
*
* @return void
*/
protected function addImpliedCommands()
{
parent::addImpliedCommands();

if (!is_null($this->state)) {
$this->state = new BlueprintState($this, $this->connection);
}
}
}
77 changes: 77 additions & 0 deletions src/Database/Schema/BlueprintState.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php namespace Winter\Storm\Database\Schema;

use Illuminate\Database\Schema\BlueprintState as BaseBlueprintState;
use Illuminate\Support\Fluent;

/**
* Restores the pre-Laravel 11 behaviour where a column's existing attributes are preserved across a
* `->change()` unless the new column definition explicitly overrides them.
*
* Laravel 11 replaces the column definition wholesale when a column is changed, dropping any
* attribute (nullable, default, collation, generated expression, ...) the migration did not
* re-specify. On engines that rebuild the table to apply a change (SQLite) this state drives the
* rebuild, so merging the previous attributes here restores the expected behaviour through Laravel's
* own single rebuild - without Storm maintaining a second, hand-rolled rebuild in the grammar.
*/
class BlueprintState extends BaseBlueprintState
{
/**
* Attributes carried over from the existing column definition when the changed definition does
* not set them explicitly.
*
* This mirrors the standard column modifiers Laravel's grammars emit. Should a future Laravel
* version introduce a new preservable modifier, add it here - omitting one is never a
* regression (Laravel already drops it on change), only a missed preservation opportunity.
*
* @var string[]
*/
protected array $preservedAttributes = [
'nullable',
'default',
'collation',
'comment',
'virtualAs',
'virtualAsJson',
'storedAs',
'storedAsJson',
];

/**
* Update the blueprint's state, preserving existing column attributes when a column is changed.
*
* @param \Illuminate\Support\Fluent $command
* @return void
*/
public function update(Fluent $command)
{
if ($command['name'] === 'change' && $command['column'] instanceof Fluent) {
$this->preserveExistingColumnAttributes($command['column']);
}

parent::update($command);
}

/**
* Copy any preserved attribute from the current (pre-change) column definition onto the changed
* column definition when the migration did not set it explicitly.
*
* @param \Illuminate\Support\Fluent $column
* @return void
*/
protected function preserveExistingColumnAttributes(Fluent $column): void
{
foreach ($this->getColumns() as $existing) {
if ($existing['name'] !== $column['name']) {
continue;
}

foreach ($this->preservedAttributes as $attribute) {
if (!isset($column[$attribute]) && isset($existing[$attribute])) {
$column[$attribute] = $existing[$attribute];
}
}

return;
}
}
}
168 changes: 3 additions & 165 deletions src/Database/Schema/Grammars/SQLiteGrammar.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,152 +2,16 @@

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\ForeignKeyDefinition;
use Illuminate\Database\Schema\IndexDefinition;
use Illuminate\Database\Schema\Grammars\SQLiteGrammar as BaseSQLiteGrammar;
use Illuminate\Support\Fluent;

class SQLiteGrammar extends BaseSQLiteGrammar
{
/**
* Compile a change column command into a series of SQL statements.
* Format a value so that it can be used in "default" clauses.
*
* 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.
*
* @param \Illuminate\Database\Schema\Blueprint $blueprint
* @param \Illuminate\Support\Fluent $command
* @return array|string
*
* @throws \RuntimeException
* @param mixed $value
* @return string
*/
public function compileChange(Blueprint $blueprint, Fluent $command)
{
$autoIncrementColumn = null;
$columnNames = [];
$schema = $this->connection->getSchemaBuilder();
$table = $blueprint->getTable();

$changedColumns = collect($blueprint->getChangedColumns());
$oldColumns = collect($schema->getColumns($table));

$columns = $oldColumns
->map(function ($column) use ($blueprint, $changedColumns, &$columnNames, &$autoIncrementColumn, $oldColumns) {
$column = $changedColumns->first(fn ($col) => $col->name === $column['name'], $column);

if (! $column instanceof Fluent) {
$isGenerated = ! is_null($column['generation']);
$column = new ColumnDefinition([
'change' => true,
'name' => $column['name'],
'type' => $column['type_name'],
'nullable' => $column['nullable'],
'default' => $column['default'] ? new Expression($column['default']) : null,
'autoIncrement' => $column['auto_increment'],
'collation' => $column['collation'],
'comment' => $column['comment'],
'virtualAs' => $isGenerated && $column['generation']['type'] === 'virtual'
? $column['generation']['expression'] : null,
'storedAs' => $isGenerated && $column['generation']['type'] === 'stored'
? $column['generation']['expression'] : null,
]);
}

$name = $this->wrap($column);
$autoIncrementColumn = $column->autoIncrement ? $column->name : $autoIncrementColumn;

if (is_null($column->virtualAs) && is_null($column->virtualAsJson) &&
is_null($column->storedAs) && is_null($column->storedAsJson)
) {
$columnNames[] = $name;
}

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

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);
}
}
return $sql;
})->all();

$foreignKeys = collect($schema->getForeignKeys($table))->map(fn ($foreignKey) => new ForeignKeyDefinition([
'columns' => $foreignKey['columns'],
'on' => $foreignKey['foreign_table'],
'references' => $foreignKey['foreign_columns'],
'onUpdate' => $foreignKey['on_update'],
'onDelete' => $foreignKey['on_delete'],
]))->all();

[$primary, $indexes] = collect($schema->getIndexes($table))->map(fn ($index) => new IndexDefinition([
'name' => match (true) {
$index['primary'] => 'primary',
$index['unique'] => 'unique',
default => 'index',
},
'index' => $index['name'],
'columns' => $index['columns'],
]))->partition(fn ($index) => $index->name === 'primary');

$indexes = collect($indexes)->reject(fn ($index) => str_starts_with('sqlite_', $index->index))->map(
fn ($index) => $this->{'compile'.ucfirst($index->name)}($blueprint, $index)
)->all();

$tempTable = $this->wrapTable($blueprint, '__temp__'.$this->connection->getTablePrefix());
$table = $this->wrapTable($blueprint);
$columnNames = implode(', ', $columnNames);

$foreignKeyConstraintsEnabled = $this->connection->scalar($this->pragma('foreign_keys'));

$sqlQuery = array_filter(
array_merge(
[
$foreignKeyConstraintsEnabled ? $this->compileDisableForeignKeyConstraints() : null,
sprintf(
'create table %s (%s%s%s)',
$tempTable,
implode(', ', $columns),
$this->addForeignKeys($foreignKeys),
$autoIncrementColumn ? '' : $this->addPrimaryKeys($primary->first())
),
sprintf(
'insert into %s (%s) select %s from %s',
$tempTable,
$columnNames,
$columnNames,
$table
),
sprintf(
'drop table %s',
$table
),
sprintf(
'alter table %s rename to %s',
$tempTable,
$table
),
],
$indexes,
[
$foreignKeyConstraintsEnabled ? $this->compileEnableForeignKeyConstraints() : null
]
)
);

return $sqlQuery;
}

public function getDefaultValue($value)
{
if (is_string($value)) {
Expand All @@ -156,30 +20,4 @@ public function getDefaultValue($value)

return parent::getDefaultValue($value);
}

/**
* Create the column definition for a tinyint type.
*
* SQLite's column introspection returns 'tinyint' as the type_name for boolean and tinyInteger
* columns. The base grammar only has typeTinyInteger, so we need this alias to avoid
* BadMethodCallException when compileChange rebuilds a table that contains boolean columns.
*
* @param \Illuminate\Support\Fluent $column
* @return string
*/
protected function typeTinyint(Fluent $column)
{
return 'integer';
}

/**
* Create the column definition for a varchar type.
*
* @param \Illuminate\Support\Fluent $column
* @return string
*/
protected function typeVarChar(Fluent $column)
{
return 'varchar';
}
}
Loading
Loading