11c502704SAndreas Gohr<?php 21c502704SAndreas Gohr 3ba766201SAndreas Gohrnamespace dokuwiki\plugin\struct\meta; 41c502704SAndreas Gohr 57182938bSAndreas Gohr/** 67182938bSAndreas Gohr * Class SchemaBuilder 77182938bSAndreas Gohr * 87182938bSAndreas Gohr * This class builds and updates the schema definitions for our tables. This includes CREATEing and ALTERing 97182938bSAndreas Gohr * the actual data tables as well as updating the meta information in our meta data tables. 107182938bSAndreas Gohr * 117182938bSAndreas Gohr * To use, simply instantiate a new object of the Builder and run the build() method on it. 127182938bSAndreas Gohr * 137182938bSAndreas Gohr * Note: even though data tables use a data_ prefix in the database, this prefix is internal only and should 147182938bSAndreas Gohr * never be passed as $table anywhere! 157182938bSAndreas Gohr * 16ba766201SAndreas Gohr * @package dokuwiki\plugin\struct\meta 177182938bSAndreas Gohr */ 181c502704SAndreas Gohrclass SchemaBuilder { 191c502704SAndreas Gohr 201c502704SAndreas Gohr /** 211c502704SAndreas Gohr * @var array The posted new data for the schema 227182938bSAndreas Gohr * @see Schema::AdminEditor() 231c502704SAndreas Gohr */ 241c502704SAndreas Gohr protected $data = array(); 251c502704SAndreas Gohr 261c502704SAndreas Gohr /** 271c502704SAndreas Gohr * @var string The table name associated with the schema 281c502704SAndreas Gohr */ 291c502704SAndreas Gohr protected $table = ''; 301c502704SAndreas Gohr 311c502704SAndreas Gohr /** 321c502704SAndreas Gohr * @var Schema the previously valid schema for this table 331c502704SAndreas Gohr */ 341c502704SAndreas Gohr protected $oldschema; 351c502704SAndreas Gohr 361c502704SAndreas Gohr /** @var int the ID of the newly created schema */ 371c502704SAndreas Gohr protected $newschemaid = 0; 381c502704SAndreas Gohr 39a1603abdSAndreas Gohr /** @var \helper_plugin_struct_db */ 40a1603abdSAndreas Gohr protected $helper; 41a1603abdSAndreas Gohr 421c502704SAndreas Gohr /** @var \helper_plugin_sqlite|null */ 431c502704SAndreas Gohr protected $sqlite; 441c502704SAndreas Gohr 459d580426SAndreas Gohr /** @var int the time for which this schema should be created - default to time() can be overriden for tests */ 469d580426SAndreas Gohr protected $time = 0; 479d580426SAndreas Gohr 481c502704SAndreas Gohr /** 491c502704SAndreas Gohr * SchemaBuilder constructor. 507182938bSAndreas Gohr * 517182938bSAndreas Gohr * @param string $table The table's name 527182938bSAndreas Gohr * @param array $data The defining of the table (basically what get's posted in the schema editor form) 537182938bSAndreas Gohr * @see Schema::AdminEditor() 541c502704SAndreas Gohr */ 551c502704SAndreas Gohr public function __construct($table, $data) { 561c502704SAndreas Gohr $this->table = $table; 571c502704SAndreas Gohr $this->data = $data; 587c080d69SAndreas Gohr $this->oldschema = new Schema($table, 0, $data['islookup']); 591c502704SAndreas Gohr 60a1603abdSAndreas Gohr $this->helper = plugin_load('helper', 'struct_db'); 61a1603abdSAndreas Gohr $this->sqlite = $this->helper->getDB(); 621c502704SAndreas Gohr } 631c502704SAndreas Gohr 641c502704SAndreas Gohr /** 651c502704SAndreas Gohr * Create the new schema 661c502704SAndreas Gohr * 679d580426SAndreas Gohr * @param int $time when to create this schema 0 for now 681c502704SAndreas Gohr * @return bool|int the new schema id on success 691c502704SAndreas Gohr */ 709d580426SAndreas Gohr public function build($time=0) { 719d580426SAndreas Gohr $this->time = $time; 72a1603abdSAndreas Gohr $this->fixLabelUniqueness(); 73a1603abdSAndreas Gohr 741c502704SAndreas Gohr $this->sqlite->query('BEGIN TRANSACTION'); 756390a534SMichael Große $ok = true; 766684d78dSAndreas Gohr // create the data table if new schema 776684d78dSAndreas Gohr if(!$this->oldschema->getId()) { 787c080d69SAndreas Gohr if($this->oldschema->isLookup()) { 797c080d69SAndreas Gohr $ok = $this->newLookupTable(); 807c080d69SAndreas Gohr } else { 811c502704SAndreas Gohr $ok = $this->newDataTable(); 826684d78dSAndreas Gohr } 837c080d69SAndreas Gohr } 841c502704SAndreas Gohr 851c502704SAndreas Gohr // create a new schema 866390a534SMichael Große $ok = $ok && $this->newSchema(); 871c502704SAndreas Gohr 881c502704SAndreas Gohr // update column info 896390a534SMichael Große $ok = $ok && $this->updateColumns(); 906390a534SMichael Große $ok = $ok && $this->addColumns(); 911c502704SAndreas Gohr 926390a534SMichael Große if (!$ok) { 936390a534SMichael Große $this->sqlite->query('ROLLBACK TRANSACTION'); 946390a534SMichael Große return false; 956390a534SMichael Große } 961c502704SAndreas Gohr $this->sqlite->query('COMMIT TRANSACTION'); 971c502704SAndreas Gohr 981c502704SAndreas Gohr return $this->newschemaid; 991c502704SAndreas Gohr } 1001c502704SAndreas Gohr 1011c502704SAndreas Gohr /** 102a1603abdSAndreas Gohr * Makes sure all labels in the schema to save are unique 103a1603abdSAndreas Gohr */ 104a1603abdSAndreas Gohr protected function fixLabelUniqueness() { 105a1603abdSAndreas Gohr $labels = array(); 106a1603abdSAndreas Gohr 107cf25d54bSAndreas Gohr if(isset($this->data['cols'])) foreach($this->data['cols'] as $idx => $column) { 108a1603abdSAndreas Gohr $this->data['cols'][$idx]['label'] = $this->fixLabel($column['label'], $labels); 109a1603abdSAndreas Gohr } 110a1603abdSAndreas Gohr 111cf25d54bSAndreas Gohr if(isset($this->data['new'])) foreach($this->data['new'] as $idx => $column) { 112a1603abdSAndreas Gohr $this->data['new'][$idx]['label'] = $this->fixLabel($column['label'], $labels); 113a1603abdSAndreas Gohr } 114a1603abdSAndreas Gohr } 115a1603abdSAndreas Gohr 116a1603abdSAndreas Gohr /** 117a1603abdSAndreas Gohr * Creates a unique label from the given one 118a1603abdSAndreas Gohr * 119a1603abdSAndreas Gohr * @param string $wantedlabel 120a1603abdSAndreas Gohr * @param array $labels list of already assigned labels (will be filled) 121a1603abdSAndreas Gohr * @return string 122a1603abdSAndreas Gohr */ 123a1603abdSAndreas Gohr protected function fixLabel($wantedlabel, &$labels) { 124a1603abdSAndreas Gohr $wantedlabel = trim($wantedlabel); 125a1603abdSAndreas Gohr $fixedlabel = $wantedlabel; 126a1603abdSAndreas Gohr $idx = 1; 1272d8ff844SAndreas Gohr while(isset($labels[utf8_strtolower($fixedlabel)])) { 128a1603abdSAndreas Gohr $fixedlabel = $wantedlabel.$idx++; 129a1603abdSAndreas Gohr } 130a1603abdSAndreas Gohr // did we actually do a rename? apply it. 131a1603abdSAndreas Gohr if($fixedlabel != $wantedlabel) { 132a1603abdSAndreas Gohr msg(sprintf($this->helper->getLang('duplicate_label'), $wantedlabel, $fixedlabel), -1); 133a1603abdSAndreas Gohr $this->data['cols']['label'] = $fixedlabel; 134a1603abdSAndreas Gohr } 1352d8ff844SAndreas Gohr $labels[utf8_strtolower($fixedlabel)] = 1; 136a1603abdSAndreas Gohr return $fixedlabel; 137a1603abdSAndreas Gohr } 138a1603abdSAndreas Gohr 139a1603abdSAndreas Gohr /** 1401c502704SAndreas Gohr * Creates a new schema 1411c502704SAndreas Gohr * 1421c502704SAndreas Gohr * @todo use checksum or other heuristic to see if we really need a new schema OTOH we probably need one nearly always!? 1431c502704SAndreas Gohr */ 1441c502704SAndreas Gohr protected function newSchema() { 1459d580426SAndreas Gohr if(!$this->time) $this->time = time(); 1469d580426SAndreas Gohr 1477c080d69SAndreas Gohr $sql = "INSERT INTO schemas (tbl, ts, islookup) VALUES (?, ?, ?)"; 1487c080d69SAndreas Gohr $this->sqlite->query($sql, $this->table, $this->time, (int) $this->oldschema->isLookup()); 1491c502704SAndreas Gohr $res = $this->sqlite->query('SELECT last_insert_rowid()'); 1501c502704SAndreas Gohr $this->newschemaid = $this->sqlite->res2single($res); 1511c502704SAndreas Gohr $this->sqlite->res_close($res); 1521c502704SAndreas Gohr if(!$this->newschemaid) return false; 1531c502704SAndreas Gohr return true; 1541c502704SAndreas Gohr } 1551c502704SAndreas Gohr 1561c502704SAndreas Gohr /** 1571c502704SAndreas Gohr * Updates all the existing column infos and adds them to the new schema 1581c502704SAndreas Gohr */ 1591c502704SAndreas Gohr protected function updateColumns() { 1601c502704SAndreas Gohr foreach($this->oldschema->getColumns() as $column) { 1611c502704SAndreas Gohr $oldEntry = $column->getType()->getAsEntry(); 1621c502704SAndreas Gohr $oldTid = $column->getTid(); 1631c502704SAndreas Gohr $newEntry = $oldEntry; 1641c502704SAndreas Gohr $newTid = $oldTid; 1651c502704SAndreas Gohr $sort = $column->getSort(); 1661c502704SAndreas Gohr if(isset($this->data['cols'][$column->getColref()])){ 1671c502704SAndreas Gohr // todo I'm not too happy with this hardcoded here - we should probably have a list of fields at one place 1681c502704SAndreas Gohr $newEntry['config'] = $this->data['cols'][$column->getColref()]['config']; 1691c502704SAndreas Gohr $newEntry['label'] = $this->data['cols'][$column->getColref()]['label']; 170b7a5ee93SMichael Große $newEntry['ismulti'] = $this->data['cols'][$column->getColref()]['ismulti']; 1711c502704SAndreas Gohr $newEntry['class'] = $this->data['cols'][$column->getColref()]['class']; 1721c502704SAndreas Gohr $sort = $this->data['cols'][$column->getColref()]['sort']; 17326147f8cSAndreas Gohr $enabled = (bool) $this->data['cols'][$column->getColref()]['isenabled']; 1741c502704SAndreas Gohr 1751c502704SAndreas Gohr // when the type definition has changed, we create a new one 1761c502704SAndreas Gohr if(array_diff_assoc($oldEntry, $newEntry)) { 1771c502704SAndreas Gohr $ok = $this->sqlite->storeEntry('types', $newEntry); 1781c502704SAndreas Gohr if(!$ok) return false; 1791c502704SAndreas Gohr $res = $this->sqlite->query('SELECT last_insert_rowid()'); 1801c502704SAndreas Gohr if(!$res) return false; 1811c502704SAndreas Gohr $newTid = $this->sqlite->res2single($res); 1821c502704SAndreas Gohr $this->sqlite->res_close($res); 183*b3e0ad6dSMichael Grosse if ($oldEntry['ismulti'] == false && $newEntry['ismulti'] == '1') { 184*b3e0ad6dSMichael Grosse $this->migrateSingleToMulti($this->oldschema->getTable(), $column->getColref()); 185*b3e0ad6dSMichael Grosse } 1861c502704SAndreas Gohr } 1871c502704SAndreas Gohr } else { 18826147f8cSAndreas Gohr $enabled = false; // no longer there for some reason 1891c502704SAndreas Gohr } 1901c502704SAndreas Gohr 1911c502704SAndreas Gohr // add this type to the schema columns 1921c502704SAndreas Gohr $schemaEntry = array( 1931c502704SAndreas Gohr 'sid' => $this->newschemaid, 1941c502704SAndreas Gohr 'colref' => $column->getColref(), 1951c502704SAndreas Gohr 'enabled' => $enabled, 1961c502704SAndreas Gohr 'tid' => $newTid, 1971c502704SAndreas Gohr 'sort' => $sort 1981c502704SAndreas Gohr ); 1991c502704SAndreas Gohr $ok = $this->sqlite->storeEntry('schema_cols', $schemaEntry); 2001c502704SAndreas Gohr if(!$ok) return false; 2011c502704SAndreas Gohr } 2021c502704SAndreas Gohr return true; 2031c502704SAndreas Gohr } 2041c502704SAndreas Gohr 205*b3e0ad6dSMichael Grosse private function migrateSingleToMulti($table, $colref) { 206*b3e0ad6dSMichael Grosse $sqlSelect = "SELECT pid, rev, col$colref AS value FROM data_$table WHERE latest = 1"; 207*b3e0ad6dSMichael Grosse $res = $this->sqlite->query($sqlSelect); 208*b3e0ad6dSMichael Grosse $valueSet = $this->sqlite->res2arr($res); 209*b3e0ad6dSMichael Grosse $this->sqlite->res_close($res); 210*b3e0ad6dSMichael Grosse $valueString = array(); 211*b3e0ad6dSMichael Grosse foreach ($valueSet as $values) { 212*b3e0ad6dSMichael Grosse if (blank($values['value']) || trim($values['value']) == '') { 213*b3e0ad6dSMichael Grosse continue; 214*b3e0ad6dSMichael Grosse } 215*b3e0ad6dSMichael Grosse $valueString[] = "($colref, '$values[pid]', $values[rev], 1, '$values[value]')"; 216*b3e0ad6dSMichael Grosse } 217*b3e0ad6dSMichael Grosse if (empty($valueString)) { 218*b3e0ad6dSMichael Grosse return; 219*b3e0ad6dSMichael Grosse } 220*b3e0ad6dSMichael Grosse $valueString = join(',', $valueString); 221*b3e0ad6dSMichael Grosse $sqlInsert = "INSERT OR REPLACE INTO multi_$table (colref, pid, rev, row, value) VALUES $valueString"; 222*b3e0ad6dSMichael Grosse $this->sqlite->query($sqlInsert); 223*b3e0ad6dSMichael Grosse } 224*b3e0ad6dSMichael Grosse 2251c502704SAndreas Gohr /** 2261c502704SAndreas Gohr * Adds new columns to the new schema 2271c502704SAndreas Gohr * 2281c502704SAndreas Gohr * @return bool 2291c502704SAndreas Gohr */ 2301c502704SAndreas Gohr protected function addColumns() { 2311c502704SAndreas Gohr if(!isset($this->data['new'])) return true; 2321c502704SAndreas Gohr 2331c502704SAndreas Gohr $colref = count($this->oldschema->getColumns())+1; 2341c502704SAndreas Gohr 2351c502704SAndreas Gohr foreach($this->data['new'] as $column) { 23626147f8cSAndreas Gohr if(!$column['isenabled']) continue; // we do not add a disabled column 23726147f8cSAndreas Gohr 2381c502704SAndreas Gohr // todo this duplicates the hardcoding as in the function above 2391c502704SAndreas Gohr $newEntry = array(); 2401c502704SAndreas Gohr $newEntry['config'] = $column['config']; 2411c502704SAndreas Gohr $newEntry['label'] = $column['label']; 242b7a5ee93SMichael Große $newEntry['ismulti'] = $column['ismulti']; 2431c502704SAndreas Gohr $newEntry['class'] = $column['class']; 2441c502704SAndreas Gohr $sort = $column['sort']; 24526147f8cSAndreas Gohr 2461c502704SAndreas Gohr 2476684d78dSAndreas Gohr // only save if the column got a name 2486684d78dSAndreas Gohr if(!$newEntry['label']) continue; 2496684d78dSAndreas Gohr 2506684d78dSAndreas Gohr // add new column to the data table 2516684d78dSAndreas Gohr if(!$this->addDataTableColumn($colref)) { 2526684d78dSAndreas Gohr return false; 2536684d78dSAndreas Gohr } 2546684d78dSAndreas Gohr 2551c502704SAndreas Gohr // save the type 2561c502704SAndreas Gohr $ok = $this->sqlite->storeEntry('types', $newEntry); 2571c502704SAndreas Gohr if(!$ok) return false; 2581c502704SAndreas Gohr $res = $this->sqlite->query('SELECT last_insert_rowid()'); 2591c502704SAndreas Gohr if(!$res) return false; 2601c502704SAndreas Gohr $newTid = $this->sqlite->res2single($res); 2611c502704SAndreas Gohr $this->sqlite->res_close($res); 2621c502704SAndreas Gohr 2631c502704SAndreas Gohr 2641c502704SAndreas Gohr // add this type to the schema columns 2651c502704SAndreas Gohr $schemaEntry = array( 2661c502704SAndreas Gohr 'sid' => $this->newschemaid, 2671c502704SAndreas Gohr 'colref' => $colref, 26826147f8cSAndreas Gohr 'enabled' => true, 2691c502704SAndreas Gohr 'tid' => $newTid, 2701c502704SAndreas Gohr 'sort' => $sort 2711c502704SAndreas Gohr ); 2721c502704SAndreas Gohr $ok = $this->sqlite->storeEntry('schema_cols', $schemaEntry); 2731c502704SAndreas Gohr if(!$ok) return false; 2741c502704SAndreas Gohr $colref++; 2751c502704SAndreas Gohr } 2761c502704SAndreas Gohr 2771c502704SAndreas Gohr return true; 2781c502704SAndreas Gohr } 2791c502704SAndreas Gohr 2801c502704SAndreas Gohr /** 2810fe33e72SAndreas Gohr * Create a completely new data table with no columns yet also create the appropriate 2820fe33e72SAndreas Gohr * multi value table for the schema 2831c502704SAndreas Gohr * 2841c502704SAndreas Gohr * @todo how do we want to handle indexes? 2851c502704SAndreas Gohr * @return bool 2861c502704SAndreas Gohr */ 2871c502704SAndreas Gohr protected function newDataTable() { 2880fe33e72SAndreas Gohr $ok = true; 2891c502704SAndreas Gohr 2900fe33e72SAndreas Gohr $tbl = 'data_' . $this->table; 2911c502704SAndreas Gohr $sql = "CREATE TABLE $tbl ( 2921c502704SAndreas Gohr pid NOT NULL, 2936684d78dSAndreas Gohr rev INTEGER NOT NULL, 2947059e7e1SAndreas Gohr latest BOOLEAN NOT NULL DEFAULT 0, 2956684d78dSAndreas Gohr PRIMARY KEY(pid, rev) 2966684d78dSAndreas Gohr )"; 2970fe33e72SAndreas Gohr $ok = $ok && (bool) $this->sqlite->query($sql); 2981c502704SAndreas Gohr 2990fe33e72SAndreas Gohr $tbl = 'multi_' . $this->table; 3000fe33e72SAndreas Gohr $sql = "CREATE TABLE $tbl ( 3010fe33e72SAndreas Gohr colref INTEGER NOT NULL, 3020fe33e72SAndreas Gohr pid NOT NULL, 3030fe33e72SAndreas Gohr rev INTEGER NOT NULL, 3040fe33e72SAndreas Gohr row INTEGER NOT NULL, 3050fe33e72SAndreas Gohr value, 3060fe33e72SAndreas Gohr PRIMARY KEY(colref, pid, rev, row) 3070fe33e72SAndreas Gohr );"; 3080fe33e72SAndreas Gohr $ok = $ok && (bool) $this->sqlite->query($sql); 3090fe33e72SAndreas Gohr 3100fe33e72SAndreas Gohr return $ok; 3111c502704SAndreas Gohr } 3121c502704SAndreas Gohr 3131c502704SAndreas Gohr /** 3147c080d69SAndreas Gohr * Creates a new lookup table with no columns 3157c080d69SAndreas Gohr * 3167c080d69SAndreas Gohr * This is basically the same as @see newDataTable() but sets 3177c080d69SAndreas Gohr * different primary keys and types 3187c080d69SAndreas Gohr * 3197c080d69SAndreas Gohr * @return bool 3207c080d69SAndreas Gohr */ 3217c080d69SAndreas Gohr protected function newLookupTable() { 3227c080d69SAndreas Gohr $ok = true; 3237c080d69SAndreas Gohr 3247c080d69SAndreas Gohr $tbl = 'data_' . $this->table; 3257c080d69SAndreas Gohr $sql = "CREATE TABLE $tbl ( 3267c080d69SAndreas Gohr pid INTEGER PRIMARY KEY, 3277c080d69SAndreas Gohr rev INTEGER NOT NULL DEFAULT 0, 3287c080d69SAndreas Gohr latest BOOLEAN NOT NULL DEFAULT 1 3297c080d69SAndreas Gohr )"; 3307c080d69SAndreas Gohr $ok = $ok && (bool) $this->sqlite->query($sql); 3317c080d69SAndreas Gohr 3327c080d69SAndreas Gohr $tbl = 'multi_' . $this->table; 3337c080d69SAndreas Gohr $sql = "CREATE TABLE $tbl ( 3347c080d69SAndreas Gohr colref INTEGER NOT NULL, 3357c080d69SAndreas Gohr pid INTEGER NOT NULL, 3367c080d69SAndreas Gohr rev INTEGER NOT NULL DEFAULT 0, 3377c080d69SAndreas Gohr row INTEGER NOT NULL, 3387c080d69SAndreas Gohr value, 3397c080d69SAndreas Gohr PRIMARY KEY(colref, pid, row) 3407c080d69SAndreas Gohr );"; 3417c080d69SAndreas Gohr $ok = $ok && (bool) $this->sqlite->query($sql); 3427c080d69SAndreas Gohr 3437c080d69SAndreas Gohr return $ok; 3447c080d69SAndreas Gohr } 3457c080d69SAndreas Gohr 3467c080d69SAndreas Gohr /** 3476684d78dSAndreas Gohr * Add an additional column to the existing data table 3481c502704SAndreas Gohr * 3496684d78dSAndreas Gohr * @param int $index the new column index to add 3501c502704SAndreas Gohr * @return bool 3511c502704SAndreas Gohr */ 3526684d78dSAndreas Gohr protected function addDataTableColumn($index) { 3531c502704SAndreas Gohr $tbl = 'data_' . $this->table; 3546684d78dSAndreas Gohr $sql = " ALTER TABLE $tbl ADD COLUMN col$index DEFAULT ''"; 3551c502704SAndreas Gohr if(! $this->sqlite->query($sql)) { 3561c502704SAndreas Gohr return false; 3571c502704SAndreas Gohr } 3581c502704SAndreas Gohr return true; 3591c502704SAndreas Gohr } 3601c502704SAndreas Gohr 3611c502704SAndreas Gohr} 362