* @license PHP License * @package WB * @subpackage db */ WBClass::load('WBDatasource_Decorator_DB'); /** * Datasource decorator: DB_Partition * * Maintain partitioned tables. Supports tables with range partion based on year, e.g. created date * Tables like this are used for statistics data like vfsfileview and pageview * * * This decorator adds a partition for next year. * * @version 0.1.0 * @package WB * @subpackage db */ class WBDatasource_Decorator_DB_Partition extends WBDatasource_Decorator_DB { /** * Decorator's parameter list * * Parameters: * - year: * * @var array */ protected $parameter = array( 'year' => '+1year', ); /** * Year for Partitions * @var int */ private $year = 2021; /** * @var WBDatasource_SQL */ private $db; /** * @var WBDatasource_SQLCommon */ private $sqlc; /** * List of Known Tables * @var array */ private $knownTables = array(); /** * SQL to Create Partition * @var string */ private $sqlPart = ''; /** * Initialize objects */ private function init() { if (!empty($this->db)) { return; } $this->knownTables[] = 'forwardtracking'; $this->knownTables[] = 'cookiepreferences'; $this->knownTables[] = 'sessionview'; $this->knownTables[] = 'pageview'; $this->knownTables[] = 'genericview'; $this->sqlc = WBClass::create('WBDatasource_SQLCommon'); $this->sqlc->loadTables(); $this->db = WBClass::create('WBDatasource_SQL'); } /** * Get List Of Tables * * @return array */ public function addTable($name) { $this->knownTables[] = $name; } /** * Set Year * * @param int start year * @param int end year */ public function setYear($min, $max = null) { $this->yearMin = $min; if (empty($max)) { return; } $this->year = $max; } /** * Convert Existing Table to Partitioned Table * * @CATION This method only to create partitioned table only suits for logging-table with lots of INSERTs * and basically no SELECTs during normal operation * * Actually this is going to be a range partition, based on column "created", * one partition per year. This suits for logging tables like pageview and such. * * This will run a bunch of SQL queries for each knonw table. * In avoid interference with running websites that actually inserts data into those table frequently, * a set of temporary tables are created. During the migration to partitioned table, * the website is going to insert new data to a temporary table. Hence the actual migration * may take several hours but still does not block anything. * * When cleaning up, all inserts to the temporay table will be copied and all temporary tables will be droped. */ public function makePartition($yearMin = null) { $this->init(); $this->year = intval(date('Y')); if (empty($yearMin)) { $yearMin = $this->year; } $this->sqlPart = array(); $this->sqlPart[] = 'CREATE TABLE `w%1$s` LIKE %1$s'; $this->sqlPart[] = 'ALTER TABLE w%1$s ENGINE=InnoDB PARTITION BY RANGE (year(created)) (%2$s);'; $this->sqlPart[] = 'ALTER TABLE %1$s RENAME TO x%1$s'; $this->sqlPart[] = 'CREATE TABLE %1$s LIKE x%1$s'; $this->sqlPart[] = 'ALTER TABLE w%1$s DISABLE KEYS'; $this->sqlPart[] = 'INSERT INTO w%1$s SELECT * FROM x%1$s'; $this->sqlPart[] = 'ALTER TABLE w%1$s ENABLE KEYS;'; $this->sqlPart[] = 'ALTER TABLE %1$s RENAME TO y%1$s;'; $this->sqlPart[] = 'ALTER TABLE w%1$s RENAME TO %1$s;'; $this->sqlPart[] = 'INSERT INTO %1$s SELECT * FROM y%1$s;'; $this->sqlPart[] = 'DROP TABLE x%1$s'; $this->sqlPart[] = 'DROP TABLE y%1$s'; foreach ($this->knownTables as $t) { $this->makePartition4Table($t, $yearMin); } } /** * Convert Table to Partioned Table * * Create partions for each year from min year until now * * @param string table name * @param int min year */ private function makePartition4Table($table, $yearMin) { $info = $this->sqlc->getTableInfo($table); if (empty($info)) { return; } $range = array(); for ($i = $yearMin; $i <= $this->year; ++$i) { $range[] = sprintf('PARTITION p%d VALUES LESS THAN (%d)', $i, $i + 1); } $range = implode(",\n", $range); foreach ($this->sqlPart as $s) { $this->db->query(sprintf($s, $info['table'], $range)); } } /** * Drop Partition 4 Year * * @param int year */ public function removePartition($year) { $this->init(); foreach ($this->knownTables as $table) { $info = $this->sqlc->getTableInfo($table); if (empty($info)) { continue; } $sql = sprintf('ALTER TABLE %s DROP PARTITION IF EXISTS p%s', $info['table'], $year); $this->db->query($sql); } } /** * Setup * * Initialize Objects */ public function onStart(&$item) { $this->init(); $year = intval(date('Y', strtotime($this->parameter['year']))); $this->year = $year; } /** * Actually Decorate List Item * * Create partition for next year * * @param array $item * @return void */ public function decorate(&$item) { if (empty($item['table'])) { return; } $info = $this->sqlc->getTableInfo($item['table']); $sql = 'ALTER TABLE %s ADD PARTITION (PARTITION p%d VALUES LESS THAN (%d))'; $sql = sprintf($sql, $info['table'], $this->year, $this->year + 1); $this->db->query($sql); $item['partition_added'] = $this->year; } }