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
68 changes: 60 additions & 8 deletions src/Db/Adapter/AbstractAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -1656,6 +1656,12 @@ public function executeActions(TableMetadata $table, array $actions): void
{
$instructions = new AlterInstructions();

// Collect partition actions separately as they need special batching
/** @var \Migrations\Db\Table\PartitionDefinition[] $addPartitions */
$addPartitions = [];
/** @var string[] $dropPartitions */
$dropPartitions = [];

foreach ($actions as $action) {
switch (true) {
case $action instanceof AddColumn:
Expand Down Expand Up @@ -1764,18 +1770,12 @@ public function executeActions(TableMetadata $table, array $actions): void

case $action instanceof AddPartition:
/** @var \Migrations\Db\Action\AddPartition $action */
$instructions->merge($this->getAddPartitionInstructions(
$table,
$action->getPartition(),
));
$addPartitions[] = $action->getPartition();
break;

case $action instanceof DropPartition:
/** @var \Migrations\Db\Action\DropPartition $action */
$instructions->merge($this->getDropPartitionInstructions(
$table->getName(),
$action->getPartitionName(),
));
$dropPartitions[] = $action->getPartitionName();
break;

default:
Expand All @@ -1785,6 +1785,58 @@ public function executeActions(TableMetadata $table, array $actions): void
}
}

// Handle batched partition operations
if ($addPartitions) {
$instructions->merge($this->getAddPartitionsInstructions($table, $addPartitions));
}
if ($dropPartitions) {
$instructions->merge($this->getDropPartitionsInstructions($table->getName(), $dropPartitions));
}

$this->executeAlterSteps($table->getName(), $instructions);
}

/**
* Get instructions for adding multiple partitions to an existing table.
*
* This method handles batching multiple partition additions into a single
* ALTER TABLE statement where supported by the database.
*
* @param \Migrations\Db\Table\TableMetadata $table The table
* @param array<\Migrations\Db\Table\PartitionDefinition> $partitions The partitions to add
* @return \Migrations\Db\AlterInstructions
*/
protected function getAddPartitionsInstructions(TableMetadata $table, array $partitions): AlterInstructions
{
// Default implementation calls single partition method for each
// Subclasses can override for database-specific batching
$instructions = new AlterInstructions();
foreach ($partitions as $partition) {
$instructions->merge($this->getAddPartitionInstructions($table, $partition));
}
Comment on lines +1814 to +1816
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need an abstraction for both the collection and the single partition expression? It seems like the collection + single methods are always implemented together and are only used in tandem. Could we have only a single abstract method then?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right. Looking at it now:

  • The collection method is the only entry point (called from executeActions)
  • MySQL's collection method doesn't even use the single method - it uses a private getAddPartitionSql() helper
  • The single method only exists for AbstractAdapter's default loop implementation

I can simplify to just the collection methods (getAddPartitionsInstructions / getDropPartitionsInstructions). Each adapter can implement the batching however it needs internally - MySQL with its combined syntax, PostgreSQL with a simple loop. The single-partition logic becomes a private implementation detail rather than part of the abstract contract.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes made in #989


return $instructions;
}

/**
* Get instructions for dropping multiple partitions from an existing table.
*
* This method handles batching multiple partition drops into a single
* ALTER TABLE statement where supported by the database.
*
* @param string $tableName The table name
* @param array<string> $partitionNames The partition names to drop
* @return \Migrations\Db\AlterInstructions
*/
protected function getDropPartitionsInstructions(string $tableName, array $partitionNames): AlterInstructions
{
// Default implementation calls single partition method for each
// Subclasses can override for database-specific batching
$instructions = new AlterInstructions();
foreach ($partitionNames as $partitionName) {
$instructions->merge($this->getDropPartitionInstructions($tableName, $partitionName));
}

return $instructions;
}
}
84 changes: 84 additions & 0 deletions src/Db/Adapter/MysqlAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -1389,6 +1389,90 @@ protected function getDropPartitionInstructions(string $tableName, string $parti
return new AlterInstructions([$sql]);
}

/**
* Get instructions for adding multiple partitions to an existing table.
*
* MySQL requires all partitions in a single ADD PARTITION clause:
* ADD PARTITION (PARTITION p1 ..., PARTITION p2 ...)
*
* @param \Migrations\Db\Table\TableMetadata $table The table
* @param array<\Migrations\Db\Table\PartitionDefinition> $partitions The partitions to add
* @return \Migrations\Db\AlterInstructions
*/
protected function getAddPartitionsInstructions(TableMetadata $table, array $partitions): AlterInstructions
{
if (empty($partitions)) {
return new AlterInstructions();
}

$partitionDefs = [];
foreach ($partitions as $partition) {
$partitionDefs[] = $this->getAddPartitionSql($partition);
}

$sql = 'ADD PARTITION (' . implode(', ', $partitionDefs) . ')';

return new AlterInstructions([$sql]);
}

/**
* Get instructions for dropping multiple partitions from an existing table.
*
* MySQL allows dropping multiple partitions in a single statement:
* DROP PARTITION p1, p2, p3
*
* @param string $tableName The table name
* @param array<string> $partitionNames The partition names to drop
* @return \Migrations\Db\AlterInstructions
*/
protected function getDropPartitionsInstructions(string $tableName, array $partitionNames): AlterInstructions
{
if (empty($partitionNames)) {
return new AlterInstructions();
}

$quotedNames = array_map(fn($name) => $this->quoteColumnName($name), $partitionNames);
$sql = 'DROP PARTITION ' . implode(', ', $quotedNames);

return new AlterInstructions([$sql]);
}

/**
* Generate the SQL definition for a single partition when adding to existing table.
*
* This method is used when adding partitions to an existing table and must
* infer the partition type from the value format since we don't have table metadata.
*
* @param \Migrations\Db\Table\PartitionDefinition $partition The partition definition
* @return string
*/
protected function getAddPartitionSql(PartitionDefinition $partition): string
{
$value = $partition->getValue();
$sql = 'PARTITION ' . $this->quoteColumnName($partition->getName());

// Detect RANGE vs LIST based on value type (simplified heuristic)
if ($value === 'MAXVALUE' || is_scalar($value)) {
// Likely RANGE
if ($value === 'MAXVALUE') {
$sql .= ' VALUES LESS THAN MAXVALUE';
} else {
$sql .= ' VALUES LESS THAN (' . $this->quotePartitionValue($value) . ')';
}
} elseif (is_array($value)) {
// Likely LIST
$sql .= ' VALUES IN (';
$sql .= implode(', ', array_map(fn($v) => $this->quotePartitionValue($v), $value));
$sql .= ')';
}

if ($partition->getComment()) {
$sql .= ' COMMENT = ' . $this->quoteString($partition->getComment());
}

return $sql;
}

/**
* Whether the server has a native uuid type.
* (MariaDB 10.7.0+)
Expand Down
39 changes: 39 additions & 0 deletions src/Db/Plan/Plan.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@
use Migrations\Db\Action\AddColumn;
use Migrations\Db\Action\AddForeignKey;
use Migrations\Db\Action\AddIndex;
use Migrations\Db\Action\AddPartition;
use Migrations\Db\Action\ChangeColumn;
use Migrations\Db\Action\ChangeComment;
use Migrations\Db\Action\ChangePrimaryKey;
use Migrations\Db\Action\CreateTable;
use Migrations\Db\Action\DropForeignKey;
use Migrations\Db\Action\DropIndex;
use Migrations\Db\Action\DropPartition;
use Migrations\Db\Action\DropTable;
use Migrations\Db\Action\RemoveColumn;
use Migrations\Db\Action\RenameColumn;
Expand Down Expand Up @@ -70,6 +72,13 @@ class Plan
*/
protected array $constraints = [];

/**
* List of partition additions or removals
*
* @var \Migrations\Db\Plan\AlterTable[]
*/
protected array $partitions = [];

/**
* List of dropped columns
*
Expand Down Expand Up @@ -100,6 +109,7 @@ protected function createPlan(array $actions): void
$this->gatherTableMoves($actions);
$this->gatherIndexes($actions);
$this->gatherConstraints($actions);
$this->gatherPartitions($actions);
$this->resolveConflicts();
}

Expand All @@ -114,6 +124,7 @@ protected function updatesSequence(): array
$this->tableUpdates,
$this->constraints,
$this->indexes,
$this->partitions,
$this->columnRemoves,
$this->tableMoves,
];
Expand All @@ -129,6 +140,7 @@ protected function inverseUpdatesSequence(): array
return [
$this->constraints,
$this->tableMoves,
$this->partitions,
$this->indexes,
$this->columnRemoves,
$this->tableUpdates,
Expand Down Expand Up @@ -186,6 +198,7 @@ protected function resolveConflicts(): void
$this->tableUpdates = $this->forgetTable($action->getTable(), $this->tableUpdates);
$this->constraints = $this->forgetTable($action->getTable(), $this->constraints);
$this->indexes = $this->forgetTable($action->getTable(), $this->indexes);
$this->partitions = $this->forgetTable($action->getTable(), $this->partitions);
$this->columnRemoves = $this->forgetTable($action->getTable(), $this->columnRemoves);
}
}
Expand Down Expand Up @@ -490,4 +503,30 @@ protected function gatherConstraints(array $actions): void
$this->constraints[$name]->addAction($action);
}
}

/**
* Collects all partition creation and drops from the given intent
*
* @param \Migrations\Db\Action\Action[] $actions The actions to parse
* @return void
*/
protected function gatherPartitions(array $actions): void
{
foreach ($actions as $action) {
if (!($action instanceof AddPartition) && !($action instanceof DropPartition)) {
continue;
} elseif (isset($this->tableCreates[$action->getTable()->getName()])) {
continue;
}

$table = $action->getTable();
$name = $table->getName();

if (!isset($this->partitions[$name])) {
$this->partitions[$name] = new AlterTable($table);
}

$this->partitions[$name]->addAction($action);
}
}
}
Loading