*
*
*
* 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;
}
}
}