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 26*fa7b96aaSMichael Grosse protected $user; 27*fa7b96aaSMichael Grosse 281c502704SAndreas Gohr /** 291c502704SAndreas Gohr * @var string The table name associated with the schema 301c502704SAndreas Gohr */ 311c502704SAndreas Gohr protected $table = ''; 321c502704SAndreas Gohr 331c502704SAndreas Gohr /** 341c502704SAndreas Gohr * @var Schema the previously valid schema for this table 351c502704SAndreas Gohr */ 361c502704SAndreas Gohr protected $oldschema; 371c502704SAndreas Gohr 381c502704SAndreas Gohr /** @var int the ID of the newly created schema */ 391c502704SAndreas Gohr protected $newschemaid = 0; 401c502704SAndreas Gohr 41a1603abdSAndreas Gohr /** @var \helper_plugin_struct_db */ 42a1603abdSAndreas Gohr protected $helper; 43a1603abdSAndreas Gohr 441c502704SAndreas Gohr /** @var \helper_plugin_sqlite|null */ 451c502704SAndreas Gohr protected $sqlite; 461c502704SAndreas Gohr 479d580426SAndreas Gohr /** @var int the time for which this schema should be created - default to time() can be overriden for tests */ 489d580426SAndreas Gohr protected $time = 0; 499d580426SAndreas Gohr 501c502704SAndreas Gohr /** 511c502704SAndreas Gohr * SchemaBuilder constructor. 527182938bSAndreas Gohr * 537182938bSAndreas Gohr * @param string $table The table's name 547182938bSAndreas Gohr * @param array $data The defining of the table (basically what get's posted in the schema editor form) 557182938bSAndreas Gohr * @see Schema::AdminEditor() 561c502704SAndreas Gohr */ 571c502704SAndreas Gohr public function __construct($table, $data) { 581c502704SAndreas Gohr $this->table = $table; 591c502704SAndreas Gohr $this->data = $data; 601c502704SAndreas Gohr $this->oldschema = new Schema($table); 611c502704SAndreas Gohr 62*fa7b96aaSMichael Grosse $this->helper = plugin_load('helper', 'struct_db', true); 63a1603abdSAndreas Gohr $this->sqlite = $this->helper->getDB(); 641c502704SAndreas Gohr } 651c502704SAndreas Gohr 661c502704SAndreas Gohr /** 671c502704SAndreas Gohr * Create the new schema 681c502704SAndreas Gohr * 699d580426SAndreas Gohr * @param int $time when to create this schema 0 for now 701c502704SAndreas Gohr * @return bool|int the new schema id on success 711c502704SAndreas Gohr */ 729d580426SAndreas Gohr public function build($time=0) { 739d580426SAndreas Gohr $this->time = $time; 74a1603abdSAndreas Gohr $this->fixLabelUniqueness(); 75a1603abdSAndreas Gohr 761c502704SAndreas Gohr $this->sqlite->query('BEGIN TRANSACTION'); 776390a534SMichael Große $ok = true; 786684d78dSAndreas Gohr // create the data table if new schema 796684d78dSAndreas Gohr if(!$this->oldschema->getId()) { 801c502704SAndreas Gohr $ok = $this->newDataTable(); 816684d78dSAndreas Gohr } 821c502704SAndreas Gohr 831c502704SAndreas Gohr // create a new schema 846390a534SMichael Große $ok = $ok && $this->newSchema(); 851c502704SAndreas Gohr 861c502704SAndreas Gohr // update column info 876390a534SMichael Große $ok = $ok && $this->updateColumns(); 886390a534SMichael Große $ok = $ok && $this->addColumns(); 891c502704SAndreas Gohr 906390a534SMichael Große if (!$ok) { 916390a534SMichael Große $this->sqlite->query('ROLLBACK TRANSACTION'); 926390a534SMichael Große return false; 936390a534SMichael Große } 941c502704SAndreas Gohr $this->sqlite->query('COMMIT TRANSACTION'); 951c502704SAndreas Gohr 961c502704SAndreas Gohr return $this->newschemaid; 971c502704SAndreas Gohr } 981c502704SAndreas Gohr 991c502704SAndreas Gohr /** 100a1603abdSAndreas Gohr * Makes sure all labels in the schema to save are unique 101a1603abdSAndreas Gohr */ 102a1603abdSAndreas Gohr protected function fixLabelUniqueness() { 103a1603abdSAndreas Gohr $labels = array(); 104a1603abdSAndreas Gohr 105cf25d54bSAndreas Gohr if(isset($this->data['cols'])) foreach($this->data['cols'] as $idx => $column) { 106a1603abdSAndreas Gohr $this->data['cols'][$idx]['label'] = $this->fixLabel($column['label'], $labels); 107a1603abdSAndreas Gohr } 108a1603abdSAndreas Gohr 109cf25d54bSAndreas Gohr if(isset($this->data['new'])) foreach($this->data['new'] as $idx => $column) { 110a1603abdSAndreas Gohr $this->data['new'][$idx]['label'] = $this->fixLabel($column['label'], $labels); 111a1603abdSAndreas Gohr } 112a1603abdSAndreas Gohr } 113a1603abdSAndreas Gohr 114a1603abdSAndreas Gohr /** 115a1603abdSAndreas Gohr * Creates a unique label from the given one 116a1603abdSAndreas Gohr * 117a1603abdSAndreas Gohr * @param string $wantedlabel 118a1603abdSAndreas Gohr * @param array $labels list of already assigned labels (will be filled) 119a1603abdSAndreas Gohr * @return string 120a1603abdSAndreas Gohr */ 121a1603abdSAndreas Gohr protected function fixLabel($wantedlabel, &$labels) { 122a1603abdSAndreas Gohr $wantedlabel = trim($wantedlabel); 123a1603abdSAndreas Gohr $fixedlabel = $wantedlabel; 124a1603abdSAndreas Gohr $idx = 1; 1252d8ff844SAndreas Gohr while(isset($labels[utf8_strtolower($fixedlabel)])) { 126a1603abdSAndreas Gohr $fixedlabel = $wantedlabel.$idx++; 127a1603abdSAndreas Gohr } 128a1603abdSAndreas Gohr // did we actually do a rename? apply it. 129a1603abdSAndreas Gohr if($fixedlabel != $wantedlabel) { 130a1603abdSAndreas Gohr msg(sprintf($this->helper->getLang('duplicate_label'), $wantedlabel, $fixedlabel), -1); 131a1603abdSAndreas Gohr $this->data['cols']['label'] = $fixedlabel; 132a1603abdSAndreas Gohr } 1332d8ff844SAndreas Gohr $labels[utf8_strtolower($fixedlabel)] = 1; 134a1603abdSAndreas Gohr return $fixedlabel; 135a1603abdSAndreas Gohr } 136a1603abdSAndreas Gohr 137a1603abdSAndreas Gohr /** 1381c502704SAndreas Gohr * Creates a new schema 1391c502704SAndreas Gohr * 1401c502704SAndreas Gohr * @todo use checksum or other heuristic to see if we really need a new schema OTOH we probably need one nearly always!? 1411c502704SAndreas Gohr */ 1421c502704SAndreas Gohr protected function newSchema() { 1439d580426SAndreas Gohr if(!$this->time) $this->time = time(); 1449d580426SAndreas Gohr 145*fa7b96aaSMichael Grosse $sql = "INSERT INTO schemas (tbl, ts, user) VALUES (?, ?, ?)"; 146*fa7b96aaSMichael Grosse $this->sqlite->query($sql, $this->table, $this->time, blank($this->user) ? $_SERVER['REMOTE_USER'] : $this->user); 1471c502704SAndreas Gohr $res = $this->sqlite->query('SELECT last_insert_rowid()'); 1481c502704SAndreas Gohr $this->newschemaid = $this->sqlite->res2single($res); 1491c502704SAndreas Gohr $this->sqlite->res_close($res); 1501c502704SAndreas Gohr if(!$this->newschemaid) return false; 1511c502704SAndreas Gohr return true; 1521c502704SAndreas Gohr } 1531c502704SAndreas Gohr 1541c502704SAndreas Gohr /** 1551c502704SAndreas Gohr * Updates all the existing column infos and adds them to the new schema 1561c502704SAndreas Gohr */ 1571c502704SAndreas Gohr protected function updateColumns() { 1581c502704SAndreas Gohr foreach($this->oldschema->getColumns() as $column) { 1591c502704SAndreas Gohr $oldEntry = $column->getType()->getAsEntry(); 1601c502704SAndreas Gohr $oldTid = $column->getTid(); 1611c502704SAndreas Gohr $newEntry = $oldEntry; 1621c502704SAndreas Gohr $newTid = $oldTid; 1631c502704SAndreas Gohr $sort = $column->getSort(); 1641c502704SAndreas Gohr if(isset($this->data['cols'][$column->getColref()])){ 1651c502704SAndreas Gohr // todo I'm not too happy with this hardcoded here - we should probably have a list of fields at one place 1661c502704SAndreas Gohr $newEntry['config'] = $this->data['cols'][$column->getColref()]['config']; 1671c502704SAndreas Gohr $newEntry['label'] = $this->data['cols'][$column->getColref()]['label']; 168b7a5ee93SMichael Große $newEntry['ismulti'] = $this->data['cols'][$column->getColref()]['ismulti']; 1691c502704SAndreas Gohr $newEntry['class'] = $this->data['cols'][$column->getColref()]['class']; 1701c502704SAndreas Gohr $sort = $this->data['cols'][$column->getColref()]['sort']; 17126147f8cSAndreas Gohr $enabled = (bool) $this->data['cols'][$column->getColref()]['isenabled']; 1721c502704SAndreas Gohr 1731c502704SAndreas Gohr // when the type definition has changed, we create a new one 1741c502704SAndreas Gohr if(array_diff_assoc($oldEntry, $newEntry)) { 1751c502704SAndreas Gohr $ok = $this->sqlite->storeEntry('types', $newEntry); 1761c502704SAndreas Gohr if(!$ok) return false; 1771c502704SAndreas Gohr $res = $this->sqlite->query('SELECT last_insert_rowid()'); 1781c502704SAndreas Gohr if(!$res) return false; 1791c502704SAndreas Gohr $newTid = $this->sqlite->res2single($res); 1801c502704SAndreas Gohr $this->sqlite->res_close($res); 1811c502704SAndreas Gohr } 1821c502704SAndreas Gohr } else { 18326147f8cSAndreas Gohr $enabled = false; // no longer there for some reason 1841c502704SAndreas Gohr } 1851c502704SAndreas Gohr 1861c502704SAndreas Gohr // add this type to the schema columns 1871c502704SAndreas Gohr $schemaEntry = array( 1881c502704SAndreas Gohr 'sid' => $this->newschemaid, 1891c502704SAndreas Gohr 'colref' => $column->getColref(), 1901c502704SAndreas Gohr 'enabled' => $enabled, 1911c502704SAndreas Gohr 'tid' => $newTid, 1921c502704SAndreas Gohr 'sort' => $sort 1931c502704SAndreas Gohr ); 1941c502704SAndreas Gohr $ok = $this->sqlite->storeEntry('schema_cols', $schemaEntry); 1951c502704SAndreas Gohr if(!$ok) return false; 1961c502704SAndreas Gohr } 1971c502704SAndreas Gohr return true; 1981c502704SAndreas Gohr } 1991c502704SAndreas Gohr 2001c502704SAndreas Gohr /** 2011c502704SAndreas Gohr * Adds new columns to the new schema 2021c502704SAndreas Gohr * 2031c502704SAndreas Gohr * @return bool 2041c502704SAndreas Gohr */ 2051c502704SAndreas Gohr protected function addColumns() { 2061c502704SAndreas Gohr if(!isset($this->data['new'])) return true; 2071c502704SAndreas Gohr 2081c502704SAndreas Gohr $colref = count($this->oldschema->getColumns())+1; 2091c502704SAndreas Gohr 2101c502704SAndreas Gohr foreach($this->data['new'] as $column) { 21126147f8cSAndreas Gohr if(!$column['isenabled']) continue; // we do not add a disabled column 21226147f8cSAndreas Gohr 2131c502704SAndreas Gohr // todo this duplicates the hardcoding as in the function above 2141c502704SAndreas Gohr $newEntry = array(); 2151c502704SAndreas Gohr $newEntry['config'] = $column['config']; 2161c502704SAndreas Gohr $newEntry['label'] = $column['label']; 217b7a5ee93SMichael Große $newEntry['ismulti'] = $column['ismulti']; 2181c502704SAndreas Gohr $newEntry['class'] = $column['class']; 2191c502704SAndreas Gohr $sort = $column['sort']; 22026147f8cSAndreas Gohr 2211c502704SAndreas Gohr 2226684d78dSAndreas Gohr // only save if the column got a name 2236684d78dSAndreas Gohr if(!$newEntry['label']) continue; 2246684d78dSAndreas Gohr 2256684d78dSAndreas Gohr // add new column to the data table 2266684d78dSAndreas Gohr if(!$this->addDataTableColumn($colref)) { 2276684d78dSAndreas Gohr return false; 2286684d78dSAndreas Gohr } 2296684d78dSAndreas Gohr 2301c502704SAndreas Gohr // save the type 2311c502704SAndreas Gohr $ok = $this->sqlite->storeEntry('types', $newEntry); 2321c502704SAndreas Gohr if(!$ok) return false; 2331c502704SAndreas Gohr $res = $this->sqlite->query('SELECT last_insert_rowid()'); 2341c502704SAndreas Gohr if(!$res) return false; 2351c502704SAndreas Gohr $newTid = $this->sqlite->res2single($res); 2361c502704SAndreas Gohr $this->sqlite->res_close($res); 2371c502704SAndreas Gohr 2381c502704SAndreas Gohr 2391c502704SAndreas Gohr // add this type to the schema columns 2401c502704SAndreas Gohr $schemaEntry = array( 2411c502704SAndreas Gohr 'sid' => $this->newschemaid, 2421c502704SAndreas Gohr 'colref' => $colref, 24326147f8cSAndreas Gohr 'enabled' => true, 2441c502704SAndreas Gohr 'tid' => $newTid, 2451c502704SAndreas Gohr 'sort' => $sort 2461c502704SAndreas Gohr ); 2471c502704SAndreas Gohr $ok = $this->sqlite->storeEntry('schema_cols', $schemaEntry); 2481c502704SAndreas Gohr if(!$ok) return false; 2491c502704SAndreas Gohr $colref++; 2501c502704SAndreas Gohr } 2511c502704SAndreas Gohr 2521c502704SAndreas Gohr return true; 2531c502704SAndreas Gohr } 2541c502704SAndreas Gohr 2551c502704SAndreas Gohr /** 2560fe33e72SAndreas Gohr * Create a completely new data table with no columns yet also create the appropriate 2570fe33e72SAndreas Gohr * multi value table for the schema 2581c502704SAndreas Gohr * 2591c502704SAndreas Gohr * @todo how do we want to handle indexes? 2601c502704SAndreas Gohr * @return bool 2611c502704SAndreas Gohr */ 2621c502704SAndreas Gohr protected function newDataTable() { 2630fe33e72SAndreas Gohr $ok = true; 2641c502704SAndreas Gohr 2650fe33e72SAndreas Gohr $tbl = 'data_' . $this->table; 2661c502704SAndreas Gohr $sql = "CREATE TABLE $tbl ( 2671c502704SAndreas Gohr pid NOT NULL, 2686684d78dSAndreas Gohr rev INTEGER NOT NULL, 2697059e7e1SAndreas Gohr latest BOOLEAN NOT NULL DEFAULT 0, 2706684d78dSAndreas Gohr PRIMARY KEY(pid, rev) 2716684d78dSAndreas Gohr )"; 2720fe33e72SAndreas Gohr $ok = $ok && (bool) $this->sqlite->query($sql); 2731c502704SAndreas Gohr 2740fe33e72SAndreas Gohr $tbl = 'multi_' . $this->table; 2750fe33e72SAndreas Gohr $sql = "CREATE TABLE $tbl ( 2760fe33e72SAndreas Gohr colref INTEGER NOT NULL, 2770fe33e72SAndreas Gohr pid NOT NULL, 2780fe33e72SAndreas Gohr rev INTEGER NOT NULL, 2790fe33e72SAndreas Gohr row INTEGER NOT NULL, 2800fe33e72SAndreas Gohr value, 2810fe33e72SAndreas Gohr PRIMARY KEY(colref, pid, rev, row) 2820fe33e72SAndreas Gohr );"; 2830fe33e72SAndreas Gohr $ok = $ok && (bool) $this->sqlite->query($sql); 2840fe33e72SAndreas Gohr 2850fe33e72SAndreas Gohr return $ok; 2861c502704SAndreas Gohr } 2871c502704SAndreas Gohr 2881c502704SAndreas Gohr /** 2896684d78dSAndreas Gohr * Add an additional column to the existing data table 2901c502704SAndreas Gohr * 2916684d78dSAndreas Gohr * @param int $index the new column index to add 2921c502704SAndreas Gohr * @return bool 2931c502704SAndreas Gohr */ 2946684d78dSAndreas Gohr protected function addDataTableColumn($index) { 2951c502704SAndreas Gohr $tbl = 'data_' . $this->table; 2966684d78dSAndreas Gohr $sql = " ALTER TABLE $tbl ADD COLUMN col$index DEFAULT ''"; 2971c502704SAndreas Gohr if(! $this->sqlite->query($sql)) { 2981c502704SAndreas Gohr return false; 2991c502704SAndreas Gohr } 3001c502704SAndreas Gohr return true; 3011c502704SAndreas Gohr } 3021c502704SAndreas Gohr 3031c502704SAndreas Gohr} 304