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
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 @@ -63,6 +65,13 @@ class Plan
*/
protected array $indexes = [];

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

/**
* List of constraint additions or removals
*
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 @@ -468,6 +481,32 @@ protected function gatherIndexes(array $actions): void
}
}

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

/**
* Collects all foreign key creation and drops from the given intent
*
Expand Down
65 changes: 65 additions & 0 deletions tests/TestCase/Db/Adapter/MysqlAdapterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3142,4 +3142,69 @@ public function testCreateTableWithExpressionPartitioning()

$this->assertTrue($this->adapter->hasTable('partitioned_events'));
}

public function testAddPartitionToExistingTable()
{
// Create a partitioned table with room to add more partitions
$table = new Table('partitioned_orders', ['id' => false, 'primary_key' => ['id', 'order_date']], $this->adapter);
$table->addColumn('id', 'integer')
->addColumn('order_date', 'date')
->addColumn('amount', 'decimal', ['precision' => 10, 'scale' => 2])
->partitionBy(Partition::TYPE_RANGE_COLUMNS, 'order_date')
->addPartition('p2022', '2023-01-01')
->addPartition('p2023', '2024-01-01')
->create();

$this->assertTrue($this->adapter->hasTable('partitioned_orders'));

// Add a new partition to the existing table
$table = new Table('partitioned_orders', [], $this->adapter);
$table->addPartitionToExisting('p2024', '2025-01-01')
->save();

// Verify the partition was added by inserting data that belongs in the new partition
$this->adapter->execute(
"INSERT INTO partitioned_orders (id, order_date, amount) VALUES (1, '2024-06-15', 100.00)",
);

$rows = $this->adapter->fetchAll('SELECT * FROM partitioned_orders WHERE order_date = "2024-06-15"');
$this->assertCount(1, $rows);
}

public function testDropPartitionFromExistingTable()
{
// Create a partitioned table with multiple partitions
$table = new Table('partitioned_logs', ['id' => false, 'primary_key' => ['id']], $this->adapter);
$table->addColumn('id', 'biginteger')
->addColumn('message', 'text')
->partitionBy(Partition::TYPE_RANGE, 'id')
->addPartition('p0', 1000000)
->addPartition('p1', 2000000)
->addPartition('pmax', 'MAXVALUE')
->create();

$this->assertTrue($this->adapter->hasTable('partitioned_logs'));

// Insert data into partition p0
$this->adapter->execute(
"INSERT INTO partitioned_logs (id, message) VALUES (500, 'test message')",
);

// Drop the partition (this also removes the data)
$table = new Table('partitioned_logs', [], $this->adapter);
$table->dropPartition('p0')
->save();

// Verify the data was removed with the partition
$rows = $this->adapter->fetchAll('SELECT * FROM partitioned_logs WHERE id = 500');
$this->assertCount(0, $rows);

// Verify the table still works by inserting into the next partition
$this->adapter->execute(
"INSERT INTO partitioned_logs (id, message) VALUES (1500000, 'another message')",
);

$rows = $this->adapter->fetchAll('SELECT * FROM partitioned_logs WHERE id = 1500000');
$this->assertCount(1, $rows);
}
}
74 changes: 74 additions & 0 deletions tests/TestCase/Db/Adapter/PostgresAdapterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use Migrations\Db\Table\Column;
use Migrations\Db\Table\ForeignKey;
use Migrations\Db\Table\Index;
use Migrations\Db\Table\Partition;
use PDO;
use PDOException;
use PHPUnit\Framework\Attributes\DataProvider;
Expand Down Expand Up @@ -2983,4 +2984,77 @@ public function testInsertOrUpdateModeResetsAfterSave()
['code' => 'ITEM1', 'name' => 'Different Name'],
])->save();
}

public function testAddPartitionToExistingTable()
{
// Create a partitioned table with room to add more partitions
$table = new Table('partitioned_orders', ['id' => false, 'primary_key' => ['id', 'order_date']], $this->adapter);
$table->addColumn('id', 'integer')
->addColumn('order_date', 'date')
->addColumn('amount', 'decimal', ['precision' => 10, 'scale' => 2])
->partitionBy(Partition::TYPE_RANGE, 'order_date')
->addPartition('p2022', ['from' => '2022-01-01', 'to' => '2023-01-01'])
->addPartition('p2023', ['from' => '2023-01-01', 'to' => '2024-01-01'])
->create();

$this->assertTrue($this->adapter->hasTable('partitioned_orders'));

// Add a new partition to the existing table
$table = new Table('partitioned_orders', [], $this->adapter);
$table->addPartitionToExisting('p2024', ['from' => '2024-01-01', 'to' => '2025-01-01'])
->save();

// Verify the partition was added by inserting data that belongs in the new partition
$this->adapter->execute(
"INSERT INTO partitioned_orders (id, order_date, amount) VALUES (1, '2024-06-15', 100.00)",
);

$rows = $this->adapter->fetchAll("SELECT * FROM partitioned_orders WHERE order_date = '2024-06-15'");
$this->assertCount(1, $rows);

// Cleanup - drop partitioned table (CASCADE drops partitions)
$this->adapter->dropTable('partitioned_orders');
}

public function testDropPartitionFromExistingTable()
{
// Create a partitioned table with multiple partitions
$table = new Table('partitioned_logs', ['id' => false, 'primary_key' => ['id']], $this->adapter);
$table->addColumn('id', 'biginteger')
->addColumn('message', 'text')
->partitionBy(Partition::TYPE_RANGE, 'id')
->addPartition('p0', ['from' => 0, 'to' => 1000000])
->addPartition('p1', ['from' => 1000000, 'to' => 2000000])
->addPartition('p2', ['from' => 2000000, 'to' => 3000000])
->create();

$this->assertTrue($this->adapter->hasTable('partitioned_logs'));

// Insert data into partition p0
$this->adapter->execute(
"INSERT INTO partitioned_logs (id, message) VALUES (500, 'test message')",
);

// Drop the partition (this also removes the data in PostgreSQL)
$table = new Table('partitioned_logs', [], $this->adapter);
$table->dropPartition('p0')
->save();

// Verify the partition table was dropped
$this->assertFalse($this->adapter->hasTable('partitioned_logs_p0'));

// Verify the main partitioned table still exists
$this->assertTrue($this->adapter->hasTable('partitioned_logs'));

// Verify the table still works by inserting into the next partition
$this->adapter->execute(
"INSERT INTO partitioned_logs (id, message) VALUES (1500000, 'another message')",
);

$rows = $this->adapter->fetchAll('SELECT * FROM partitioned_logs WHERE id = 1500000');
$this->assertCount(1, $rows);

// Cleanup - drop partitioned table (CASCADE drops remaining partitions)
$this->adapter->dropTable('partitioned_logs');
}
}