* * * * DB_Partition * * * * * * * * * * @author gERD Schaufelberger * @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.3.2 * @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 = ''; const MODE_EXECUTE = 1; const MODE_ECHO = 2; /** * Current Working Mode * @var int */ private $workingMode = self::MODE_EXECUTE; /** * Constructor * * @param array */ public function __construct($parameter = array()) { $this->flushTable(); $this->knownTables[] = 'linktracking'; $this->knownTables[] = 'forwardtracking'; $this->knownTables[] = 'cookiepreferences'; $this->knownTables[] = 'sessionview'; $this->knownTables[] = 'pageview'; $this->knownTables[] = 'genericview'; $this->knownTables[] = 'vfsfileview'; } /** * Initialize objects */ private function init() { if (!empty($this->db)) { return; } $this->sqlc = WBClass::create('WBDatasource_SQLCommon'); $this->sqlc->loadTables(); $this->db = WBClass::create('WBDatasource_SQL'); } /** * Set Working Mode */ public function setWorkingMode($mode = self::MODE_ECHO) { $this->workingMode = $mode; } /** * Empty List of Known Tables * */ public function flushTable() { $this->knownTables = array(); } /** * Get List Of Tables * * @return array */ public function addTable($name) { $this->knownTables[] = $name; } /** * Convert Existing Table to Partitioned Table * * @CAUTION Use this method only to create partitioned table. * This 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) { $q = sprintf($s, $info['table'], $range); $this->query($q); } } /** * 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->query($sql); } } public function addPartiton4TableYear($table, $year) { $this->init(); $info = $this->sqlc->getTableInfo($table); $sql = 'ALTER TABLE %s ADD PARTITION (PARTITION p%d VALUES LESS THAN (%d))'; $sql = sprintf($sql, $info['table'], $year, $year + 1); $this->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; } $this->addPartiton4TableYear($item['table'], $this->year); $item['partition_added'] = $this->year; } private function query($q) { switch ($this->workingMode) { case self::MODE_EXECUTE: $this->db->query($q); break; case self::MODE_ECHO: echo $q . ";\n"; break; default: break; } } }