Skip to content

Commit 22990fb

Browse files
jamisonbryantdereuromarkJamison Bryant
authored
Add support for partitionBy() + update() on existing tables (#989)
* Fix multiple partition SQL syntax for MySQL When adding multiple partitions to an existing table, MySQL requires: ALTER TABLE foo ADD PARTITION (PARTITION p1 ..., PARTITION p2 ...) Previously, each AddPartition action generated its own ADD PARTITION clause, which when joined with commas resulted in invalid SQL: ALTER TABLE foo ADD PARTITION (...), ADD PARTITION (...) This fix: - Batches AddPartition actions together in executeActions() - Adds new getAddPartitionsInstructions() method to AbstractAdapter with a default implementation that calls the single partition method - Overrides getAddPartitionsInstructions() in MysqlAdapter to generate correct batched SQL: ADD PARTITION (PARTITION p1, PARTITION p2) - Similarly batches DropPartition actions for efficiency - Adds gatherPartitions() to Plan.php to properly gather partition actions - Includes extensive tests for single/multiple partition add/drop scenarios Refs #986 * Fix coding standard - use single quotes * Trigger CI re-run * Add test for combined partition and column operations * Add SetPartitioning action for partitionBy() + update() Enables adding partitioning to existing non-partitioned tables using: $table->partitionBy(Partition::TYPE_RANGE_COLUMNS, 'created') ->addPartition('p2023', '2024-01-01') ->update(); Previously this generated no SQL. Now it properly generates: ALTER TABLE `table` PARTITION BY RANGE COLUMNS (created) (...) Changes: - Add SetPartitioning action class - Update Plan to handle SetPartitioning in gatherPartitions() - Add getSetPartitioningInstructions() to AbstractAdapter/MysqlAdapter - Create SetPartitioning action in Table::executeActions() when updating * Add test for composite partition keys * Each adapter now only needs to implement the collection methods * Remove unused import --------- Co-authored-by: mscherer <dereuromark@web.de> Co-authored-by: Jamison Bryant <jbryant@ticketsauce.com>
1 parent 8f5ca19 commit 22990fb

8 files changed

Lines changed: 743 additions & 60 deletions

File tree

src/Db/Action/SetPartitioning.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
/**
5+
* MIT License
6+
* For full license information, please view the LICENSE file that was distributed with this source code.
7+
*/
8+
9+
namespace Migrations\Db\Action;
10+
11+
use Migrations\Db\Table\Partition;
12+
use Migrations\Db\Table\TableMetadata;
13+
14+
/**
15+
* Add partitioning to an existing non-partitioned table
16+
*/
17+
class SetPartitioning extends Action
18+
{
19+
/**
20+
* @var \Migrations\Db\Table\Partition
21+
*/
22+
protected Partition $partition;
23+
24+
/**
25+
* Constructor
26+
*
27+
* @param \Migrations\Db\Table\TableMetadata $table The table to add partitioning to
28+
* @param \Migrations\Db\Table\Partition $partition The partition configuration
29+
*/
30+
public function __construct(TableMetadata $table, Partition $partition)
31+
{
32+
parent::__construct($table);
33+
$this->partition = $partition;
34+
}
35+
36+
/**
37+
* Returns the partition configuration
38+
*
39+
* @return \Migrations\Db\Table\Partition
40+
*/
41+
public function getPartition(): Partition
42+
{
43+
return $this->partition;
44+
}
45+
}

src/Db/Adapter/AbstractAdapter.php

Lines changed: 68 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
use Migrations\Db\Action\RemoveColumn;
3838
use Migrations\Db\Action\RenameColumn;
3939
use Migrations\Db\Action\RenameTable;
40+
use Migrations\Db\Action\SetPartitioning;
4041
use Migrations\Db\AlterInstructions;
4142
use Migrations\Db\InsertMode;
4243
use Migrations\Db\Literal;
@@ -45,7 +46,7 @@
4546
use Migrations\Db\Table\Column;
4647
use Migrations\Db\Table\ForeignKey;
4748
use Migrations\Db\Table\Index;
48-
use Migrations\Db\Table\PartitionDefinition;
49+
use Migrations\Db\Table\Partition;
4950
use Migrations\Db\Table\TableMetadata;
5051
use Migrations\MigrationInterface;
5152
use Migrations\SeedInterface;
@@ -1549,32 +1550,6 @@ public function dropCheckConstraint(string $tableName, string $constraintName):
15491550
*/
15501551
abstract protected function getDropCheckConstraintInstructions(string $tableName, string $constraintName): AlterInstructions;
15511552

1552-
/**
1553-
* Returns the instructions to add a partition to an existing partitioned table.
1554-
*
1555-
* @param \Migrations\Db\Table\TableMetadata $table The table
1556-
* @param \Migrations\Db\Table\PartitionDefinition $partition The partition definition to add
1557-
* @throws \RuntimeException If partitioning is not supported
1558-
* @return \Migrations\Db\AlterInstructions
1559-
*/
1560-
protected function getAddPartitionInstructions(TableMetadata $table, PartitionDefinition $partition): AlterInstructions
1561-
{
1562-
throw new RuntimeException('Table partitioning is not supported by this adapter');
1563-
}
1564-
1565-
/**
1566-
* Returns the instructions to drop a partition from an existing partitioned table.
1567-
*
1568-
* @param string $tableName The table name
1569-
* @param string $partitionName The partition name to drop
1570-
* @throws \RuntimeException If partitioning is not supported
1571-
* @return \Migrations\Db\AlterInstructions
1572-
*/
1573-
protected function getDropPartitionInstructions(string $tableName, string $partitionName): AlterInstructions
1574-
{
1575-
throw new RuntimeException('Table partitioning is not supported by this adapter');
1576-
}
1577-
15781553
/**
15791554
* @inheritdoc
15801555
*/
@@ -1656,6 +1631,12 @@ public function executeActions(TableMetadata $table, array $actions): void
16561631
{
16571632
$instructions = new AlterInstructions();
16581633

1634+
// Collect partition actions separately as they need special batching
1635+
/** @var \Migrations\Db\Table\PartitionDefinition[] $addPartitions */
1636+
$addPartitions = [];
1637+
/** @var string[] $dropPartitions */
1638+
$dropPartitions = [];
1639+
16591640
foreach ($actions as $action) {
16601641
switch (true) {
16611642
case $action instanceof AddColumn:
@@ -1764,17 +1745,19 @@ public function executeActions(TableMetadata $table, array $actions): void
17641745

17651746
case $action instanceof AddPartition:
17661747
/** @var \Migrations\Db\Action\AddPartition $action */
1767-
$instructions->merge($this->getAddPartitionInstructions(
1768-
$table,
1769-
$action->getPartition(),
1770-
));
1748+
$addPartitions[] = $action->getPartition();
17711749
break;
17721750

17731751
case $action instanceof DropPartition:
17741752
/** @var \Migrations\Db\Action\DropPartition $action */
1775-
$instructions->merge($this->getDropPartitionInstructions(
1776-
$table->getName(),
1777-
$action->getPartitionName(),
1753+
$dropPartitions[] = $action->getPartitionName();
1754+
break;
1755+
1756+
case $action instanceof SetPartitioning:
1757+
/** @var \Migrations\Db\Action\SetPartitioning $action */
1758+
$instructions->merge($this->getSetPartitioningInstructions(
1759+
$table,
1760+
$action->getPartition(),
17781761
));
17791762
break;
17801763

@@ -1785,6 +1768,57 @@ public function executeActions(TableMetadata $table, array $actions): void
17851768
}
17861769
}
17871770

1771+
// Handle batched partition operations
1772+
if ($addPartitions) {
1773+
$instructions->merge($this->getAddPartitionsInstructions($table, $addPartitions));
1774+
}
1775+
if ($dropPartitions) {
1776+
$instructions->merge($this->getDropPartitionsInstructions($table->getName(), $dropPartitions));
1777+
}
1778+
17881779
$this->executeAlterSteps($table->getName(), $instructions);
17891780
}
1781+
1782+
/**
1783+
* Get instructions for adding multiple partitions to an existing table.
1784+
*
1785+
* This method handles batching multiple partition additions into a single
1786+
* ALTER TABLE statement where supported by the database.
1787+
*
1788+
* @param \Migrations\Db\Table\TableMetadata $table The table
1789+
* @param array<\Migrations\Db\Table\PartitionDefinition> $partitions The partitions to add
1790+
* @return \Migrations\Db\AlterInstructions
1791+
*/
1792+
protected function getAddPartitionsInstructions(TableMetadata $table, array $partitions): AlterInstructions
1793+
{
1794+
throw new RuntimeException('Table partitioning is not supported by this adapter');
1795+
}
1796+
1797+
/**
1798+
* Get instructions for dropping multiple partitions from an existing table.
1799+
*
1800+
* This method handles batching multiple partition drops into a single
1801+
* ALTER TABLE statement where supported by the database.
1802+
*
1803+
* @param string $tableName The table name
1804+
* @param array<string> $partitionNames The partition names to drop
1805+
* @return \Migrations\Db\AlterInstructions
1806+
*/
1807+
protected function getDropPartitionsInstructions(string $tableName, array $partitionNames): AlterInstructions
1808+
{
1809+
throw new RuntimeException('Table partitioning is not supported by this adapter');
1810+
}
1811+
1812+
/**
1813+
* Get instructions for adding partitioning to an existing table.
1814+
*
1815+
* @param \Migrations\Db\Table\TableMetadata $table The table
1816+
* @param \Migrations\Db\Table\Partition $partition The partition configuration
1817+
* @throws \RuntimeException If partitioning is not supported
1818+
* @return \Migrations\Db\AlterInstructions
1819+
*/
1820+
protected function getSetPartitioningInstructions(TableMetadata $table, Partition $partition): AlterInstructions
1821+
{
1822+
throw new RuntimeException('Adding partitioning to existing tables is not supported by this adapter');
1823+
}
17901824
}

src/Db/Adapter/MysqlAdapter.php

Lines changed: 69 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1338,18 +1338,80 @@ protected function quotePartitionValue(mixed $value): string
13381338
}
13391339

13401340
/**
1341-
* Get instructions for adding a partition to an existing table.
1341+
* Get instructions for adding partitioning to an existing table.
13421342
*
13431343
* @param \Migrations\Db\Table\TableMetadata $table The table
1344-
* @param \Migrations\Db\Table\PartitionDefinition $partition The partition to add
1344+
* @param \Migrations\Db\Table\Partition $partition The partition configuration
13451345
* @return \Migrations\Db\AlterInstructions
13461346
*/
1347-
protected function getAddPartitionInstructions(TableMetadata $table, PartitionDefinition $partition): AlterInstructions
1347+
protected function getSetPartitioningInstructions(TableMetadata $table, Partition $partition): AlterInstructions
1348+
{
1349+
$sql = $this->getPartitionSqlDefinition($partition);
1350+
1351+
return new AlterInstructions([$sql]);
1352+
}
1353+
1354+
/**
1355+
* Get instructions for adding multiple partitions to an existing table.
1356+
*
1357+
* MySQL requires all partitions in a single ADD PARTITION clause:
1358+
* ADD PARTITION (PARTITION p1 ..., PARTITION p2 ...)
1359+
*
1360+
* @param \Migrations\Db\Table\TableMetadata $table The table
1361+
* @param array<\Migrations\Db\Table\PartitionDefinition> $partitions The partitions to add
1362+
* @return \Migrations\Db\AlterInstructions
1363+
*/
1364+
protected function getAddPartitionsInstructions(TableMetadata $table, array $partitions): AlterInstructions
1365+
{
1366+
if (empty($partitions)) {
1367+
return new AlterInstructions();
1368+
}
1369+
1370+
$partitionDefs = [];
1371+
foreach ($partitions as $partition) {
1372+
$partitionDefs[] = $this->getAddPartitionSql($partition);
1373+
}
1374+
1375+
$sql = 'ADD PARTITION (' . implode(', ', $partitionDefs) . ')';
1376+
1377+
return new AlterInstructions([$sql]);
1378+
}
1379+
1380+
/**
1381+
* Get instructions for dropping multiple partitions from an existing table.
1382+
*
1383+
* MySQL allows dropping multiple partitions in a single statement:
1384+
* DROP PARTITION p1, p2, p3
1385+
*
1386+
* @param string $tableName The table name
1387+
* @param array<string> $partitionNames The partition names to drop
1388+
* @return \Migrations\Db\AlterInstructions
1389+
*/
1390+
protected function getDropPartitionsInstructions(string $tableName, array $partitionNames): AlterInstructions
1391+
{
1392+
if (empty($partitionNames)) {
1393+
return new AlterInstructions();
1394+
}
1395+
1396+
$quotedNames = array_map(fn($name) => $this->quoteColumnName($name), $partitionNames);
1397+
$sql = 'DROP PARTITION ' . implode(', ', $quotedNames);
1398+
1399+
return new AlterInstructions([$sql]);
1400+
}
1401+
1402+
/**
1403+
* Generate the SQL definition for a single partition when adding to existing table.
1404+
*
1405+
* This method is used when adding partitions to an existing table and must
1406+
* infer the partition type from the value format since we don't have table metadata.
1407+
*
1408+
* @param \Migrations\Db\Table\PartitionDefinition $partition The partition definition
1409+
* @return string
1410+
*/
1411+
protected function getAddPartitionSql(PartitionDefinition $partition): string
13481412
{
1349-
// For MySQL, we need to know the partition type to generate correct SQL
1350-
// This is a simplified version - in practice you'd need to query the table's partition type
13511413
$value = $partition->getValue();
1352-
$sql = 'ADD PARTITION (PARTITION ' . $this->quoteColumnName($partition->getName());
1414+
$sql = 'PARTITION ' . $this->quoteColumnName($partition->getName());
13531415

13541416
// Detect RANGE vs LIST based on value type (simplified heuristic)
13551417
if ($value === 'MAXVALUE' || is_scalar($value)) {
@@ -1369,23 +1431,8 @@ protected function getAddPartitionInstructions(TableMetadata $table, PartitionDe
13691431
if ($partition->getComment()) {
13701432
$sql .= ' COMMENT = ' . $this->quoteString($partition->getComment());
13711433
}
1372-
$sql .= ')';
13731434

1374-
return new AlterInstructions([$sql]);
1375-
}
1376-
1377-
/**
1378-
* Get instructions for dropping a partition from an existing table.
1379-
*
1380-
* @param string $tableName The table name
1381-
* @param string $partitionName The partition name to drop
1382-
* @return \Migrations\Db\AlterInstructions
1383-
*/
1384-
protected function getDropPartitionInstructions(string $tableName, string $partitionName): AlterInstructions
1385-
{
1386-
$sql = 'DROP PARTITION ' . $this->quoteColumnName($partitionName);
1387-
1388-
return new AlterInstructions([$sql]);
1435+
return $sql;
13891436
}
13901437

13911438
/**

src/Db/Adapter/PostgresAdapter.php

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1439,13 +1439,30 @@ protected function quotePartitionValue(mixed $value): string
14391439
}
14401440

14411441
/**
1442-
* Get instructions for adding a partition to an existing table.
1442+
* Get instructions for adding multiple partitions to an existing table.
1443+
*
1444+
* @param \Migrations\Db\Table\TableMetadata $table The table
1445+
* @param array<\Migrations\Db\Table\PartitionDefinition> $partitions The partitions to add
1446+
* @return \Migrations\Db\AlterInstructions
1447+
*/
1448+
protected function getAddPartitionsInstructions(TableMetadata $table, array $partitions): AlterInstructions
1449+
{
1450+
$instructions = new AlterInstructions();
1451+
foreach ($partitions as $partition) {
1452+
$instructions->merge($this->getAddPartitionSql($table, $partition));
1453+
}
1454+
1455+
return $instructions;
1456+
}
1457+
1458+
/**
1459+
* Get instructions for adding a single partition to an existing table.
14431460
*
14441461
* @param \Migrations\Db\Table\TableMetadata $table The table
14451462
* @param \Migrations\Db\Table\PartitionDefinition $partition The partition to add
14461463
* @return \Migrations\Db\AlterInstructions
14471464
*/
1448-
protected function getAddPartitionInstructions(TableMetadata $table, PartitionDefinition $partition): AlterInstructions
1465+
private function getAddPartitionSql(TableMetadata $table, PartitionDefinition $partition): AlterInstructions
14491466
{
14501467
// PostgreSQL requires creating partition tables using CREATE TABLE ... PARTITION OF
14511468
// This is more complex as we need the partition type info
@@ -1483,13 +1500,30 @@ protected function getAddPartitionInstructions(TableMetadata $table, PartitionDe
14831500
}
14841501

14851502
/**
1486-
* Get instructions for dropping a partition from an existing table.
1503+
* Get instructions for dropping multiple partitions from an existing table.
1504+
*
1505+
* @param string $tableName The table name
1506+
* @param array<string> $partitionNames The partition names to drop
1507+
* @return \Migrations\Db\AlterInstructions
1508+
*/
1509+
protected function getDropPartitionsInstructions(string $tableName, array $partitionNames): AlterInstructions
1510+
{
1511+
$instructions = new AlterInstructions();
1512+
foreach ($partitionNames as $partitionName) {
1513+
$instructions->merge($this->getDropPartitionSql($tableName, $partitionName));
1514+
}
1515+
1516+
return $instructions;
1517+
}
1518+
1519+
/**
1520+
* Get instructions for dropping a single partition from an existing table.
14871521
*
14881522
* @param string $tableName The table name
14891523
* @param string $partitionName The partition name to drop
14901524
* @return \Migrations\Db\AlterInstructions
14911525
*/
1492-
protected function getDropPartitionInstructions(string $tableName, string $partitionName): AlterInstructions
1526+
private function getDropPartitionSql(string $tableName, string $partitionName): AlterInstructions
14931527
{
14941528
// In PostgreSQL, partitions are tables, so we drop the partition table
14951529
// The partition name is typically the table_partitionname

0 commit comments

Comments
 (0)