Skip to content

Commit 4fad77c

Browse files
authored
Add table partitioning support for MySQL and PostgreSQL (#966)
This adds support for PARTITION BY (RANGE, LIST, HASH, KEY) clauses when creating tables and managing partitions on existing tables. Features: - RANGE, RANGE COLUMNS, LIST, LIST COLUMNS, HASH, KEY partitioning - Composite partition keys - Add/drop partitions on existing tables - PostgreSQL declarative partitioning with auto-generated table names - Expression-based partitioning via Literal class New classes: - Partition: Value object for partition configuration - PartitionDefinition: Value object for individual partition definitions - AddPartition: Action for adding partitions to existing tables - DropPartition: Action for dropping partitions New Table methods: - partitionBy(): Define partitioning on new tables - addPartition(): Add partition definitions when creating tables - addPartitionToExisting(): Add partition to existing partitioned tables - dropPartition(): Remove partition from existing tables * Fix PHPStan and PHPCS errors in partition implementation - Remove redundant is_array() check in MysqlAdapter (is_scalar already excludes arrays) - Remove redundant ?? operator in PostgresAdapter (from key always exists) - Remove : static return type from Partition::addDefinition() per CakePHP coding standards
1 parent 037354b commit 4fad77c

13 files changed

Lines changed: 1288 additions & 0 deletions

docs/en/writing-migrations.rst

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,186 @@ configuration key for the time being.
599599

600600
To view available column types and options, see :ref:`adding-columns` for details.
601601

602+
Table Partitioning
603+
------------------
604+
605+
Migrations supports table partitioning for MySQL and PostgreSQL. Partitioning helps
606+
manage large tables by splitting them into smaller, more manageable pieces.
607+
608+
.. note::
609+
610+
Partition columns must be included in the primary key for MySQL. SQLite does
611+
not support partitioning. MySQL's ``RANGE`` and ``LIST`` types only work with
612+
integer columns - use ``RANGE COLUMNS`` and ``LIST COLUMNS`` for DATE/STRING columns.
613+
614+
RANGE Partitioning
615+
~~~~~~~~~~~~~~~~~~
616+
617+
RANGE partitioning is useful when you want to partition by numeric ranges. For MySQL,
618+
use ``TYPE_RANGE`` with integer columns or expressions, and ``TYPE_RANGE_COLUMNS`` for
619+
DATE/DATETIME/STRING columns::
620+
621+
<?php
622+
623+
use Migrations\BaseMigration;
624+
use Migrations\Db\Table\Partition;
625+
626+
class CreatePartitionedOrders extends BaseMigration
627+
{
628+
public function change(): void
629+
{
630+
// Use RANGE COLUMNS for DATE columns in MySQL
631+
$table = $this->table('orders', [
632+
'id' => false,
633+
'primary_key' => ['id', 'order_date'],
634+
]);
635+
$table->addColumn('id', 'integer', ['identity' => true])
636+
->addColumn('order_date', 'date')
637+
->addColumn('amount', 'decimal', ['precision' => 10, 'scale' => 2])
638+
->partitionBy(Partition::TYPE_RANGE_COLUMNS, 'order_date')
639+
->addPartition('p2022', '2023-01-01')
640+
->addPartition('p2023', '2024-01-01')
641+
->addPartition('p2024', '2025-01-01')
642+
->addPartition('pmax', 'MAXVALUE')
643+
->create();
644+
}
645+
}
646+
647+
LIST Partitioning
648+
~~~~~~~~~~~~~~~~~
649+
650+
LIST partitioning is useful when you want to partition by discrete values. For MySQL,
651+
use ``TYPE_LIST`` with integer columns and ``TYPE_LIST_COLUMNS`` for STRING columns::
652+
653+
<?php
654+
655+
use Migrations\BaseMigration;
656+
use Migrations\Db\Table\Partition;
657+
658+
class CreatePartitionedCustomers extends BaseMigration
659+
{
660+
public function change(): void
661+
{
662+
// Use LIST COLUMNS for STRING columns in MySQL
663+
$table = $this->table('customers', [
664+
'id' => false,
665+
'primary_key' => ['id', 'region'],
666+
]);
667+
$table->addColumn('id', 'integer', ['identity' => true])
668+
->addColumn('region', 'string', ['limit' => 20])
669+
->addColumn('name', 'string')
670+
->partitionBy(Partition::TYPE_LIST_COLUMNS, 'region')
671+
->addPartition('p_americas', ['US', 'CA', 'MX', 'BR'])
672+
->addPartition('p_europe', ['UK', 'DE', 'FR', 'IT'])
673+
->addPartition('p_asia', ['JP', 'CN', 'IN', 'KR'])
674+
->create();
675+
}
676+
}
677+
678+
HASH Partitioning
679+
~~~~~~~~~~~~~~~~~
680+
681+
HASH partitioning distributes data evenly across a specified number of partitions::
682+
683+
<?php
684+
685+
use Migrations\BaseMigration;
686+
use Migrations\Db\Table\Partition;
687+
688+
class CreatePartitionedSessions extends BaseMigration
689+
{
690+
public function change(): void
691+
{
692+
$table = $this->table('sessions');
693+
$table->addColumn('user_id', 'integer')
694+
->addColumn('data', 'text')
695+
->partitionBy(Partition::TYPE_HASH, 'user_id', ['count' => 8])
696+
->create();
697+
}
698+
}
699+
700+
KEY Partitioning (MySQL only)
701+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
702+
703+
KEY partitioning is similar to HASH but uses MySQL's internal hashing function::
704+
705+
<?php
706+
707+
use Migrations\BaseMigration;
708+
use Migrations\Db\Table\Partition;
709+
710+
class CreatePartitionedCache extends BaseMigration
711+
{
712+
public function change(): void
713+
{
714+
$table = $this->table('cache', [
715+
'id' => false,
716+
'primary_key' => ['cache_key'],
717+
]);
718+
$table->addColumn('cache_key', 'string', ['limit' => 255])
719+
->addColumn('value', 'binary')
720+
->partitionBy(Partition::TYPE_KEY, 'cache_key', ['count' => 16])
721+
->create();
722+
}
723+
}
724+
725+
Partitioning with Expressions
726+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
727+
728+
You can partition by expressions using the ``Literal`` class::
729+
730+
<?php
731+
732+
use Migrations\BaseMigration;
733+
use Migrations\Db\Literal;
734+
use Migrations\Db\Table\Partition;
735+
736+
class CreatePartitionedEvents extends BaseMigration
737+
{
738+
public function change(): void
739+
{
740+
$table = $this->table('events', [
741+
'id' => false,
742+
'primary_key' => ['id', 'created_at'],
743+
]);
744+
$table->addColumn('id', 'integer', ['identity' => true])
745+
->addColumn('created_at', 'datetime')
746+
->partitionBy(Partition::TYPE_RANGE, Literal::from('YEAR(created_at)'))
747+
->addPartition('p2022', 2023)
748+
->addPartition('p2023', 2024)
749+
->addPartition('pmax', 'MAXVALUE')
750+
->create();
751+
}
752+
}
753+
754+
Modifying Partitions on Existing Tables
755+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
756+
757+
You can add or drop partitions on existing partitioned tables::
758+
759+
<?php
760+
761+
use Migrations\BaseMigration;
762+
763+
class ModifyOrdersPartitions extends BaseMigration
764+
{
765+
public function up(): void
766+
{
767+
// Add a new partition
768+
$this->table('orders')
769+
->addPartitionToExisting('p2025', '2026-01-01')
770+
->update();
771+
}
772+
773+
public function down(): void
774+
{
775+
// Drop the partition
776+
$this->table('orders')
777+
->dropPartition('p2025')
778+
->update();
779+
}
780+
}
781+
602782
Saving Changes
603783
--------------
604784

src/Db/Action/AddPartition.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\PartitionDefinition;
12+
use Migrations\Db\Table\TableMetadata;
13+
14+
/**
15+
* Add a partition to an existing partitioned table
16+
*/
17+
class AddPartition extends Action
18+
{
19+
/**
20+
* @var \Migrations\Db\Table\PartitionDefinition
21+
*/
22+
protected PartitionDefinition $partition;
23+
24+
/**
25+
* Constructor
26+
*
27+
* @param \Migrations\Db\Table\TableMetadata $table The table to add the partition to
28+
* @param \Migrations\Db\Table\PartitionDefinition $partition The partition definition
29+
*/
30+
public function __construct(TableMetadata $table, PartitionDefinition $partition)
31+
{
32+
parent::__construct($table);
33+
$this->partition = $partition;
34+
}
35+
36+
/**
37+
* Returns the partition definition to add
38+
*
39+
* @return \Migrations\Db\Table\PartitionDefinition
40+
*/
41+
public function getPartition(): PartitionDefinition
42+
{
43+
return $this->partition;
44+
}
45+
}

src/Db/Action/DropPartition.php

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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\TableMetadata;
12+
13+
/**
14+
* Drop a partition from an existing partitioned table
15+
*/
16+
class DropPartition extends Action
17+
{
18+
/**
19+
* @var string
20+
*/
21+
protected string $partitionName;
22+
23+
/**
24+
* Constructor
25+
*
26+
* @param \Migrations\Db\Table\TableMetadata $table The table to drop the partition from
27+
* @param string $partitionName The name of the partition to drop
28+
*/
29+
public function __construct(TableMetadata $table, string $partitionName)
30+
{
31+
parent::__construct($table);
32+
$this->partitionName = $partitionName;
33+
}
34+
35+
/**
36+
* Returns the partition name to drop
37+
*
38+
* @return string
39+
*/
40+
public function getPartitionName(): string
41+
{
42+
return $this->partitionName;
43+
}
44+
}

src/Db/Adapter/AbstractAdapter.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,13 @@
2626
use Migrations\Db\Action\AddColumn;
2727
use Migrations\Db\Action\AddForeignKey;
2828
use Migrations\Db\Action\AddIndex;
29+
use Migrations\Db\Action\AddPartition;
2930
use Migrations\Db\Action\ChangeColumn;
3031
use Migrations\Db\Action\ChangeComment;
3132
use Migrations\Db\Action\ChangePrimaryKey;
3233
use Migrations\Db\Action\DropForeignKey;
3334
use Migrations\Db\Action\DropIndex;
35+
use Migrations\Db\Action\DropPartition;
3436
use Migrations\Db\Action\DropTable;
3537
use Migrations\Db\Action\RemoveColumn;
3638
use Migrations\Db\Action\RenameColumn;
@@ -43,6 +45,7 @@
4345
use Migrations\Db\Table\Column;
4446
use Migrations\Db\Table\ForeignKey;
4547
use Migrations\Db\Table\Index;
48+
use Migrations\Db\Table\PartitionDefinition;
4649
use Migrations\Db\Table\TableMetadata;
4750
use Migrations\MigrationInterface;
4851
use Migrations\SeedInterface;
@@ -1495,6 +1498,32 @@ public function dropCheckConstraint(string $tableName, string $constraintName):
14951498
*/
14961499
abstract protected function getDropCheckConstraintInstructions(string $tableName, string $constraintName): AlterInstructions;
14971500

1501+
/**
1502+
* Returns the instructions to add a partition to an existing partitioned table.
1503+
*
1504+
* @param \Migrations\Db\Table\TableMetadata $table The table
1505+
* @param \Migrations\Db\Table\PartitionDefinition $partition The partition definition to add
1506+
* @throws \RuntimeException If partitioning is not supported
1507+
* @return \Migrations\Db\AlterInstructions
1508+
*/
1509+
protected function getAddPartitionInstructions(TableMetadata $table, PartitionDefinition $partition): AlterInstructions
1510+
{
1511+
throw new RuntimeException('Table partitioning is not supported by this adapter');
1512+
}
1513+
1514+
/**
1515+
* Returns the instructions to drop a partition from an existing partitioned table.
1516+
*
1517+
* @param string $tableName The table name
1518+
* @param string $partitionName The partition name to drop
1519+
* @throws \RuntimeException If partitioning is not supported
1520+
* @return \Migrations\Db\AlterInstructions
1521+
*/
1522+
protected function getDropPartitionInstructions(string $tableName, string $partitionName): AlterInstructions
1523+
{
1524+
throw new RuntimeException('Table partitioning is not supported by this adapter');
1525+
}
1526+
14981527
/**
14991528
* @inheritdoc
15001529
*/
@@ -1682,6 +1711,22 @@ public function executeActions(TableMetadata $table, array $actions): void
16821711
));
16831712
break;
16841713

1714+
case $action instanceof AddPartition:
1715+
/** @var \Migrations\Db\Action\AddPartition $action */
1716+
$instructions->merge($this->getAddPartitionInstructions(
1717+
$table,
1718+
$action->getPartition(),
1719+
));
1720+
break;
1721+
1722+
case $action instanceof DropPartition:
1723+
/** @var \Migrations\Db\Action\DropPartition $action */
1724+
$instructions->merge($this->getDropPartitionInstructions(
1725+
$table->getName(),
1726+
$action->getPartitionName(),
1727+
));
1728+
break;
1729+
16851730
default:
16861731
throw new InvalidArgumentException(
16871732
sprintf("Don't know how to execute action `%s`", get_class($action)),

0 commit comments

Comments
 (0)