diff --git a/src/Db/Adapter/AbstractAdapter.php b/src/Db/Adapter/AbstractAdapter.php index f9f652ccc..c03166f82 100644 --- a/src/Db/Adapter/AbstractAdapter.php +++ b/src/Db/Adapter/AbstractAdapter.php @@ -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: @@ -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: @@ -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)); + } + + 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 $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; + } } diff --git a/src/Db/Adapter/MysqlAdapter.php b/src/Db/Adapter/MysqlAdapter.php index 8e2ec4a47..563c13a24 100644 --- a/src/Db/Adapter/MysqlAdapter.php +++ b/src/Db/Adapter/MysqlAdapter.php @@ -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 $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+) diff --git a/src/Db/Plan/Plan.php b/src/Db/Plan/Plan.php index f2c36fe7d..ef1b87b81 100644 --- a/src/Db/Plan/Plan.php +++ b/src/Db/Plan/Plan.php @@ -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; @@ -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 * @@ -100,6 +109,7 @@ protected function createPlan(array $actions): void $this->gatherTableMoves($actions); $this->gatherIndexes($actions); $this->gatherConstraints($actions); + $this->gatherPartitions($actions); $this->resolveConflicts(); } @@ -114,6 +124,7 @@ protected function updatesSequence(): array $this->tableUpdates, $this->constraints, $this->indexes, + $this->partitions, $this->columnRemoves, $this->tableMoves, ]; @@ -129,6 +140,7 @@ protected function inverseUpdatesSequence(): array return [ $this->constraints, $this->tableMoves, + $this->partitions, $this->indexes, $this->columnRemoves, $this->tableUpdates, @@ -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); } } @@ -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); + } + } } diff --git a/tests/TestCase/Db/Adapter/MysqlAdapterTest.php b/tests/TestCase/Db/Adapter/MysqlAdapterTest.php index 1dce2e491..ddaad69df 100644 --- a/tests/TestCase/Db/Adapter/MysqlAdapterTest.php +++ b/tests/TestCase/Db/Adapter/MysqlAdapterTest.php @@ -3142,4 +3142,245 @@ public function testCreateTableWithExpressionPartitioning() $this->assertTrue($this->adapter->hasTable('partitioned_events')); } + + public function testAddSinglePartitionToExistingTable() + { + // 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 single 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 testAddMultiplePartitionsToExistingTable() + { + // Create a partitioned table with room to add more partitions + $table = new Table('partitioned_sales', ['id' => false, 'primary_key' => ['id', 'sale_date']], $this->adapter); + $table->addColumn('id', 'integer') + ->addColumn('sale_date', 'date') + ->addColumn('amount', 'decimal', ['precision' => 10, 'scale' => 2]) + ->partitionBy(Partition::TYPE_RANGE_COLUMNS, 'sale_date') + ->addPartition('p2022', '2023-01-01') + ->create(); + + $this->assertTrue($this->adapter->hasTable('partitioned_sales')); + + // Add multiple partitions at once - this is the main test for the fix + // MySQL requires: ADD PARTITION (PARTITION p1 ..., PARTITION p2 ...) + // NOT: ADD PARTITION (...), ADD PARTITION (...) + $table = new Table('partitioned_sales', [], $this->adapter); + $table->addPartitionToExisting('p2023', '2024-01-01') + ->addPartitionToExisting('p2024', '2025-01-01') + ->addPartitionToExisting('p2025', '2026-01-01') + ->save(); + + // Verify all partitions were added by inserting data into each + $this->adapter->execute( + "INSERT INTO partitioned_sales (id, sale_date, amount) VALUES (1, '2023-06-15', 100.00)", + ); + $this->adapter->execute( + "INSERT INTO partitioned_sales (id, sale_date, amount) VALUES (2, '2024-06-15', 200.00)", + ); + $this->adapter->execute( + "INSERT INTO partitioned_sales (id, sale_date, amount) VALUES (3, '2025-06-15', 300.00)", + ); + + $rows = $this->adapter->fetchAll('SELECT * FROM partitioned_sales'); + $this->assertCount(3, $rows); + } + + public function testDropSinglePartitionFromExistingTable() + { + // 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); + } + + public function testDropMultiplePartitionsFromExistingTable() + { + // Create a partitioned table with multiple partitions + $table = new Table('partitioned_archive', ['id' => false, 'primary_key' => ['id']], $this->adapter); + $table->addColumn('id', 'biginteger') + ->addColumn('data', 'text') + ->partitionBy(Partition::TYPE_RANGE, 'id') + ->addPartition('p0', 1000000) + ->addPartition('p1', 2000000) + ->addPartition('p2', 3000000) + ->addPartition('p3', 4000000) + ->addPartition('pmax', 'MAXVALUE') + ->create(); + + $this->assertTrue($this->adapter->hasTable('partitioned_archive')); + + // Insert data into partitions p0 and p1 + $this->adapter->execute( + "INSERT INTO partitioned_archive (id, data) VALUES (500, 'data in p0')", + ); + $this->adapter->execute( + "INSERT INTO partitioned_archive (id, data) VALUES (1500000, 'data in p1')", + ); + $this->adapter->execute( + "INSERT INTO partitioned_archive (id, data) VALUES (2500000, 'data in p2')", + ); + + // Drop multiple partitions at once + // MySQL allows: DROP PARTITION p0, p1 + $table = new Table('partitioned_archive', [], $this->adapter); + $table->dropPartition('p0') + ->dropPartition('p1') + ->save(); + + // Verify the data was removed with the partitions + $rows = $this->adapter->fetchAll('SELECT * FROM partitioned_archive WHERE id < 2000000'); + $this->assertCount(0, $rows); + + // Verify data in p2 still exists + $rows = $this->adapter->fetchAll('SELECT * FROM partitioned_archive WHERE id = 2500000'); + $this->assertCount(1, $rows); + } + + public function testAddMultipleListPartitionsToExistingTable() + { + // Create a LIST partitioned table + $table = new Table('partitioned_regions', ['id' => false, 'primary_key' => ['id', 'region_id']], $this->adapter); + $table->addColumn('id', 'integer') + ->addColumn('region_id', 'integer') + ->addColumn('name', 'string', ['limit' => 100]) + ->partitionBy(Partition::TYPE_LIST, 'region_id') + ->addPartition('p_north', [1, 2, 3]) + ->addPartition('p_south', [4, 5, 6]) + ->create(); + + $this->assertTrue($this->adapter->hasTable('partitioned_regions')); + + // Add multiple LIST partitions at once + $table = new Table('partitioned_regions', [], $this->adapter); + $table->addPartitionToExisting('p_east', [7, 8, 9]) + ->addPartitionToExisting('p_west', [10, 11, 12]) + ->save(); + + // Verify all partitions work by inserting data + $this->adapter->execute( + "INSERT INTO partitioned_regions (id, region_id, name) VALUES (1, 7, 'East Region')", + ); + $this->adapter->execute( + "INSERT INTO partitioned_regions (id, region_id, name) VALUES (2, 10, 'West Region')", + ); + + $rows = $this->adapter->fetchAll('SELECT * FROM partitioned_regions WHERE region_id IN (7, 10)'); + $this->assertCount(2, $rows); + } + + public function testAddPartitionsWithMaxvalue() + { + // Create a partitioned table without MAXVALUE partition + $table = new Table('partitioned_data', ['id' => false, 'primary_key' => ['id']], $this->adapter); + $table->addColumn('id', 'biginteger') + ->addColumn('value', 'integer') + ->partitionBy(Partition::TYPE_RANGE, 'id') + ->addPartition('p0', 100) + ->addPartition('p1', 200) + ->create(); + + $this->assertTrue($this->adapter->hasTable('partitioned_data')); + + // Add multiple partitions including one with MAXVALUE + $table = new Table('partitioned_data', [], $this->adapter); + $table->addPartitionToExisting('p2', 300) + ->addPartitionToExisting('pmax', 'MAXVALUE') + ->save(); + + // Verify MAXVALUE partition catches all higher values + $this->adapter->execute( + 'INSERT INTO partitioned_data (id, value) VALUES (250, 1)', + ); + $this->adapter->execute( + 'INSERT INTO partitioned_data (id, value) VALUES (999999, 2)', + ); + + $rows = $this->adapter->fetchAll('SELECT * FROM partitioned_data WHERE id >= 200'); + $this->assertCount(2, $rows); + } + + public function testCombinedPartitionAndColumnOperations(): void + { + // Create a partitioned table + $table = new Table('combined_test', ['id' => false, 'primary_key' => ['id', 'created_year']], $this->adapter); + $table->addColumn('id', 'integer') + ->addColumn('created_year', 'integer') + ->addColumn('name', 'string', ['limit' => 100]) + ->partitionBy(Partition::TYPE_RANGE, 'created_year') + ->addPartition('p2022', 2023) + ->addPartition('p2023', 2024) + ->create(); + + $this->assertTrue($this->adapter->hasTable('combined_test')); + + // Combine adding a column AND adding a partition in one save() + $table = new Table('combined_test', [], $this->adapter); + $table->addColumn('description', 'text', ['null' => true]) + ->addPartitionToExisting('p2024', 2025) + ->save(); + + // Verify the column was added + $this->assertTrue($this->adapter->hasColumn('combined_test', 'description')); + + // Verify the partition was added by inserting data + $this->adapter->execute( + "INSERT INTO combined_test (id, created_year, name, description) VALUES (1, 2024, 'Test', 'A description')", + ); + + $rows = $this->adapter->fetchAll('SELECT * FROM combined_test WHERE created_year = 2024'); + $this->assertCount(1, $rows); + $this->assertEquals('A description', $rows[0]['description']); + } } diff --git a/tests/TestCase/Db/Adapter/PostgresAdapterTest.php b/tests/TestCase/Db/Adapter/PostgresAdapterTest.php index c832b84e1..2bc5988e4 100644 --- a/tests/TestCase/Db/Adapter/PostgresAdapterTest.php +++ b/tests/TestCase/Db/Adapter/PostgresAdapterTest.php @@ -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; @@ -2983,4 +2984,159 @@ public function testInsertOrUpdateModeResetsAfterSave() ['code' => 'ITEM1', 'name' => 'Different Name'], ])->save(); } + + public function testAddSinglePartitionToExistingTable() + { + // 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 testAddMultiplePartitionsToExistingTable() + { + // Create a partitioned table + $table = new Table('partitioned_sales', ['id' => false, 'primary_key' => ['id', 'sale_date']], $this->adapter); + $table->addColumn('id', 'integer') + ->addColumn('sale_date', 'date') + ->addColumn('amount', 'decimal', ['precision' => 10, 'scale' => 2]) + ->partitionBy(Partition::TYPE_RANGE, 'sale_date') + ->addPartition('p2022', ['from' => '2022-01-01', 'to' => '2023-01-01']) + ->create(); + + $this->assertTrue($this->adapter->hasTable('partitioned_sales')); + + // Add multiple partitions at once + $table = new Table('partitioned_sales', [], $this->adapter); + $table->addPartitionToExisting('p2023', ['from' => '2023-01-01', 'to' => '2024-01-01']) + ->addPartitionToExisting('p2024', ['from' => '2024-01-01', 'to' => '2025-01-01']) + ->addPartitionToExisting('p2025', ['from' => '2025-01-01', 'to' => '2026-01-01']) + ->save(); + + // Verify all partitions were added by inserting data into each + $this->adapter->execute( + "INSERT INTO partitioned_sales (id, sale_date, amount) VALUES (1, '2023-06-15', 100.00)", + ); + $this->adapter->execute( + "INSERT INTO partitioned_sales (id, sale_date, amount) VALUES (2, '2024-06-15', 200.00)", + ); + $this->adapter->execute( + "INSERT INTO partitioned_sales (id, sale_date, amount) VALUES (3, '2025-06-15', 300.00)", + ); + + $rows = $this->adapter->fetchAll('SELECT * FROM partitioned_sales'); + $this->assertCount(3, $rows); + + // Cleanup + $this->adapter->dropTable('partitioned_sales'); + } + + public function testDropSinglePartitionFromExistingTable() + { + // 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'); + } + + public function testDropMultiplePartitionsFromExistingTable() + { + // Create a partitioned table with multiple partitions + $table = new Table('partitioned_archive', ['id' => false, 'primary_key' => ['id']], $this->adapter); + $table->addColumn('id', 'biginteger') + ->addColumn('data', 'text') + ->partitionBy(Partition::TYPE_RANGE, 'id') + ->addPartition('p0', ['from' => 0, 'to' => 1000000]) + ->addPartition('p1', ['from' => 1000000, 'to' => 2000000]) + ->addPartition('p2', ['from' => 2000000, 'to' => 3000000]) + ->addPartition('p3', ['from' => 3000000, 'to' => 4000000]) + ->create(); + + $this->assertTrue($this->adapter->hasTable('partitioned_archive')); + + // Insert data into partitions + $this->adapter->execute( + "INSERT INTO partitioned_archive (id, data) VALUES (500, 'data in p0')", + ); + $this->adapter->execute( + "INSERT INTO partitioned_archive (id, data) VALUES (1500000, 'data in p1')", + ); + $this->adapter->execute( + "INSERT INTO partitioned_archive (id, data) VALUES (2500000, 'data in p2')", + ); + + // Drop multiple partitions at once + $table = new Table('partitioned_archive', [], $this->adapter); + $table->dropPartition('p0') + ->dropPartition('p1') + ->save(); + + // Verify the partition tables were dropped + $this->assertFalse($this->adapter->hasTable('partitioned_archive_p0')); + $this->assertFalse($this->adapter->hasTable('partitioned_archive_p1')); + + // Verify data in p2 still exists + $rows = $this->adapter->fetchAll('SELECT * FROM partitioned_archive WHERE id = 2500000'); + $this->assertCount(1, $rows); + + // Cleanup + $this->adapter->dropTable('partitioned_archive'); + } }