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 26fa7b96aaSMichael Grosse protected $user; 27fa7b96aaSMichael 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; 607c080d69SAndreas Gohr $this->oldschema = new Schema($table, 0, $data['islookup']); 611c502704SAndreas Gohr 62a1603abdSAndreas Gohr $this->helper = plugin_load('helper', 'struct_db'); 63a1603abdSAndreas Gohr $this->sqlite = $this->helper->getDB(); 6412fe9b2cSMichael Grosse $this->user = $_SERVER['REMOTE_USER']; 651c502704SAndreas Gohr } 661c502704SAndreas Gohr 671c502704SAndreas Gohr /** 681c502704SAndreas Gohr * Create the new schema 691c502704SAndreas Gohr * 709d580426SAndreas Gohr * @param int $time when to create this schema 0 for now 711c502704SAndreas Gohr * @return bool|int the new schema id on success 721c502704SAndreas Gohr */ 739d580426SAndreas Gohr public function build($time=0) { 749d580426SAndreas Gohr $this->time = $time; 75a1603abdSAndreas Gohr $this->fixLabelUniqueness(); 76a1603abdSAndreas Gohr 771c502704SAndreas Gohr $this->sqlite->query('BEGIN TRANSACTION'); 786390a534SMichael Große $ok = true; 796684d78dSAndreas Gohr // create the data table if new schema 806684d78dSAndreas Gohr if(!$this->oldschema->getId()) { 817c080d69SAndreas Gohr if($this->oldschema->isLookup()) { 827c080d69SAndreas Gohr $ok = $this->newLookupTable(); 837c080d69SAndreas Gohr } else { 841c502704SAndreas Gohr $ok = $this->newDataTable(); 856684d78dSAndreas Gohr } 867c080d69SAndreas Gohr } 871c502704SAndreas Gohr 881c502704SAndreas Gohr // create a new schema 896390a534SMichael Große $ok = $ok && $this->newSchema(); 901c502704SAndreas Gohr 911c502704SAndreas Gohr // update column info 926390a534SMichael Große $ok = $ok && $this->updateColumns(); 936390a534SMichael Große $ok = $ok && $this->addColumns(); 941c502704SAndreas Gohr 956390a534SMichael Große if (!$ok) { 966390a534SMichael Große $this->sqlite->query('ROLLBACK TRANSACTION'); 976390a534SMichael Große return false; 986390a534SMichael Große } 991c502704SAndreas Gohr $this->sqlite->query('COMMIT TRANSACTION'); 1001c502704SAndreas Gohr 1011c502704SAndreas Gohr return $this->newschemaid; 1021c502704SAndreas Gohr } 1031c502704SAndreas Gohr 1041c502704SAndreas Gohr /** 105a1603abdSAndreas Gohr * Makes sure all labels in the schema to save are unique 106a1603abdSAndreas Gohr */ 107a1603abdSAndreas Gohr protected function fixLabelUniqueness() { 108a1603abdSAndreas Gohr $labels = array(); 109a1603abdSAndreas Gohr 110cf25d54bSAndreas Gohr if(isset($this->data['cols'])) foreach($this->data['cols'] as $idx => $column) { 111a1603abdSAndreas Gohr $this->data['cols'][$idx]['label'] = $this->fixLabel($column['label'], $labels); 112a1603abdSAndreas Gohr } 113a1603abdSAndreas Gohr 114cf25d54bSAndreas Gohr if(isset($this->data['new'])) foreach($this->data['new'] as $idx => $column) { 115a1603abdSAndreas Gohr $this->data['new'][$idx]['label'] = $this->fixLabel($column['label'], $labels); 116a1603abdSAndreas Gohr } 117a1603abdSAndreas Gohr } 118a1603abdSAndreas Gohr 119a1603abdSAndreas Gohr /** 120a1603abdSAndreas Gohr * Creates a unique label from the given one 121a1603abdSAndreas Gohr * 122a1603abdSAndreas Gohr * @param string $wantedlabel 123a1603abdSAndreas Gohr * @param array $labels list of already assigned labels (will be filled) 124a1603abdSAndreas Gohr * @return string 125a1603abdSAndreas Gohr */ 126a1603abdSAndreas Gohr protected function fixLabel($wantedlabel, &$labels) { 127a1603abdSAndreas Gohr $wantedlabel = trim($wantedlabel); 128a1603abdSAndreas Gohr $fixedlabel = $wantedlabel; 129a1603abdSAndreas Gohr $idx = 1; 1302d8ff844SAndreas Gohr while(isset($labels[utf8_strtolower($fixedlabel)])) { 131a1603abdSAndreas Gohr $fixedlabel = $wantedlabel.$idx++; 132a1603abdSAndreas Gohr } 133a1603abdSAndreas Gohr // did we actually do a rename? apply it. 134a1603abdSAndreas Gohr if($fixedlabel != $wantedlabel) { 135a1603abdSAndreas Gohr msg(sprintf($this->helper->getLang('duplicate_label'), $wantedlabel, $fixedlabel), -1); 136a1603abdSAndreas Gohr $this->data['cols']['label'] = $fixedlabel; 137a1603abdSAndreas Gohr } 1382d8ff844SAndreas Gohr $labels[utf8_strtolower($fixedlabel)] = 1; 139a1603abdSAndreas Gohr return $fixedlabel; 140a1603abdSAndreas Gohr } 141a1603abdSAndreas Gohr 142a1603abdSAndreas Gohr /** 1431c502704SAndreas Gohr * Creates a new schema 1441c502704SAndreas Gohr */ 1451c502704SAndreas Gohr protected function newSchema() { 1469d580426SAndreas Gohr if(!$this->time) $this->time = time(); 1479d580426SAndreas Gohr 148*76f0676dSAndreas Gohr /** @noinspection SqlResolve */ 149fe23a86eSAndreas Gohr $sql = "INSERT INTO schemas (tbl, ts, islookup, user) VALUES (?, ?, ?, ?)"; 150fe23a86eSAndreas Gohr $this->sqlite->query($sql, $this->table, $this->time, (int) $this->oldschema->isLookup(), $this->user); 1511c502704SAndreas Gohr $res = $this->sqlite->query('SELECT last_insert_rowid()'); 1521c502704SAndreas Gohr $this->newschemaid = $this->sqlite->res2single($res); 1531c502704SAndreas Gohr $this->sqlite->res_close($res); 1541c502704SAndreas Gohr if(!$this->newschemaid) return false; 1551c502704SAndreas Gohr return true; 1561c502704SAndreas Gohr } 1571c502704SAndreas Gohr 1581c502704SAndreas Gohr /** 1591c502704SAndreas Gohr * Updates all the existing column infos and adds them to the new schema 1601c502704SAndreas Gohr */ 1611c502704SAndreas Gohr protected function updateColumns() { 1621c502704SAndreas Gohr foreach($this->oldschema->getColumns() as $column) { 1631c502704SAndreas Gohr $oldEntry = $column->getType()->getAsEntry(); 1641c502704SAndreas Gohr $oldTid = $column->getTid(); 1651c502704SAndreas Gohr $newEntry = $oldEntry; 1661c502704SAndreas Gohr $newTid = $oldTid; 1671c502704SAndreas Gohr $sort = $column->getSort(); 1681c502704SAndreas Gohr if(isset($this->data['cols'][$column->getColref()])){ 1691c502704SAndreas Gohr // todo I'm not too happy with this hardcoded here - we should probably have a list of fields at one place 1701c502704SAndreas Gohr $newEntry['config'] = $this->data['cols'][$column->getColref()]['config']; 1711c502704SAndreas Gohr $newEntry['label'] = $this->data['cols'][$column->getColref()]['label']; 172b7a5ee93SMichael Große $newEntry['ismulti'] = $this->data['cols'][$column->getColref()]['ismulti']; 1731c502704SAndreas Gohr $newEntry['class'] = $this->data['cols'][$column->getColref()]['class']; 1741c502704SAndreas Gohr $sort = $this->data['cols'][$column->getColref()]['sort']; 17526147f8cSAndreas Gohr $enabled = (bool) $this->data['cols'][$column->getColref()]['isenabled']; 1761c502704SAndreas Gohr 1771c502704SAndreas Gohr // when the type definition has changed, we create a new one 1781c502704SAndreas Gohr if(array_diff_assoc($oldEntry, $newEntry)) { 1791c502704SAndreas Gohr $ok = $this->sqlite->storeEntry('types', $newEntry); 1801c502704SAndreas Gohr if(!$ok) return false; 1811c502704SAndreas Gohr $res = $this->sqlite->query('SELECT last_insert_rowid()'); 1821c502704SAndreas Gohr if(!$res) return false; 1831c502704SAndreas Gohr $newTid = $this->sqlite->res2single($res); 1841c502704SAndreas Gohr $this->sqlite->res_close($res); 185b3e0ad6dSMichael Grosse if ($oldEntry['ismulti'] == false && $newEntry['ismulti'] == '1') { 186b3e0ad6dSMichael Grosse $this->migrateSingleToMulti($this->oldschema->getTable(), $column->getColref()); 187b3e0ad6dSMichael Grosse } 1881c502704SAndreas Gohr } 1891c502704SAndreas Gohr } else { 19026147f8cSAndreas Gohr $enabled = false; // no longer there for some reason 1911c502704SAndreas Gohr } 1921c502704SAndreas Gohr 1931c502704SAndreas Gohr // add this type to the schema columns 1941c502704SAndreas Gohr $schemaEntry = array( 1951c502704SAndreas Gohr 'sid' => $this->newschemaid, 1961c502704SAndreas Gohr 'colref' => $column->getColref(), 1971c502704SAndreas Gohr 'enabled' => $enabled, 1981c502704SAndreas Gohr 'tid' => $newTid, 1991c502704SAndreas Gohr 'sort' => $sort 2001c502704SAndreas Gohr ); 2011c502704SAndreas Gohr $ok = $this->sqlite->storeEntry('schema_cols', $schemaEntry); 2021c502704SAndreas Gohr if(!$ok) return false; 2031c502704SAndreas Gohr } 2041c502704SAndreas Gohr return true; 2051c502704SAndreas Gohr } 2061c502704SAndreas Gohr 2074050be94SMichael Grosse /** 2084050be94SMichael Grosse * Write the latest value from an entry in a data_ table to the corresponding multi_table 2094050be94SMichael Grosse * 2104050be94SMichael Grosse * @param string $table 2114050be94SMichael Grosse * @param int $colref 2124050be94SMichael Grosse */ 2134050be94SMichael Grosse protected function migrateSingleToMulti($table, $colref) { 214*76f0676dSAndreas Gohr /** @noinspection SqlResolve */ 215b3e0ad6dSMichael Grosse $sqlSelect = "SELECT pid, rev, col$colref AS value FROM data_$table WHERE latest = 1"; 216b3e0ad6dSMichael Grosse $res = $this->sqlite->query($sqlSelect); 217b3e0ad6dSMichael Grosse $valueSet = $this->sqlite->res2arr($res); 218b3e0ad6dSMichael Grosse $this->sqlite->res_close($res); 219b3e0ad6dSMichael Grosse $valueString = array(); 2204050be94SMichael Grosse $arguments = array(); 221b3e0ad6dSMichael Grosse foreach ($valueSet as $values) { 222b3e0ad6dSMichael Grosse if (blank($values['value']) || trim($values['value']) == '') { 223b3e0ad6dSMichael Grosse continue; 224b3e0ad6dSMichael Grosse } 2254050be94SMichael Grosse $valueString[] = "(?, ?, ?, ?, ?)"; 2264050be94SMichael Grosse $arguments = array_merge($arguments, array($colref, $values['pid'], $values['rev'], 1, $values['value'])); 227b3e0ad6dSMichael Grosse } 228b3e0ad6dSMichael Grosse if (empty($valueString)) { 229b3e0ad6dSMichael Grosse return; 230b3e0ad6dSMichael Grosse } 231b3e0ad6dSMichael Grosse $valueString = join(',', $valueString); 232*76f0676dSAndreas Gohr /** @noinspection SqlResolve */ 233b3e0ad6dSMichael Grosse $sqlInsert = "INSERT OR REPLACE INTO multi_$table (colref, pid, rev, row, value) VALUES $valueString"; 2344050be94SMichael Grosse $this->sqlite->query($sqlInsert, $arguments); 235b3e0ad6dSMichael Grosse } 236b3e0ad6dSMichael Grosse 2371c502704SAndreas Gohr /** 2381c502704SAndreas Gohr * Adds new columns to the new schema 2391c502704SAndreas Gohr * 2401c502704SAndreas Gohr * @return bool 2411c502704SAndreas Gohr */ 2421c502704SAndreas Gohr protected function addColumns() { 2431c502704SAndreas Gohr if(!isset($this->data['new'])) return true; 2441c502704SAndreas Gohr 2451c502704SAndreas Gohr $colref = count($this->oldschema->getColumns())+1; 2461c502704SAndreas Gohr 2471c502704SAndreas Gohr foreach($this->data['new'] as $column) { 24826147f8cSAndreas Gohr if(!$column['isenabled']) continue; // we do not add a disabled column 24926147f8cSAndreas Gohr 2501c502704SAndreas Gohr // todo this duplicates the hardcoding as in the function above 2511c502704SAndreas Gohr $newEntry = array(); 2521c502704SAndreas Gohr $newEntry['config'] = $column['config']; 2531c502704SAndreas Gohr $newEntry['label'] = $column['label']; 254b7a5ee93SMichael Große $newEntry['ismulti'] = $column['ismulti']; 2551c502704SAndreas Gohr $newEntry['class'] = $column['class']; 2561c502704SAndreas Gohr $sort = $column['sort']; 25726147f8cSAndreas Gohr 2581c502704SAndreas Gohr 2596684d78dSAndreas Gohr // only save if the column got a name 2606684d78dSAndreas Gohr if(!$newEntry['label']) continue; 2616684d78dSAndreas Gohr 2626684d78dSAndreas Gohr // add new column to the data table 2636684d78dSAndreas Gohr if(!$this->addDataTableColumn($colref)) { 2646684d78dSAndreas Gohr return false; 2656684d78dSAndreas Gohr } 2666684d78dSAndreas Gohr 2671c502704SAndreas Gohr // save the type 2681c502704SAndreas Gohr $ok = $this->sqlite->storeEntry('types', $newEntry); 2691c502704SAndreas Gohr if(!$ok) return false; 2701c502704SAndreas Gohr $res = $this->sqlite->query('SELECT last_insert_rowid()'); 2711c502704SAndreas Gohr if(!$res) return false; 2721c502704SAndreas Gohr $newTid = $this->sqlite->res2single($res); 2731c502704SAndreas Gohr $this->sqlite->res_close($res); 2741c502704SAndreas Gohr 2751c502704SAndreas Gohr 2761c502704SAndreas Gohr // add this type to the schema columns 2771c502704SAndreas Gohr $schemaEntry = array( 2781c502704SAndreas Gohr 'sid' => $this->newschemaid, 2791c502704SAndreas Gohr 'colref' => $colref, 28026147f8cSAndreas Gohr 'enabled' => true, 2811c502704SAndreas Gohr 'tid' => $newTid, 2821c502704SAndreas Gohr 'sort' => $sort 2831c502704SAndreas Gohr ); 2841c502704SAndreas Gohr $ok = $this->sqlite->storeEntry('schema_cols', $schemaEntry); 2851c502704SAndreas Gohr if(!$ok) return false; 2861c502704SAndreas Gohr $colref++; 2871c502704SAndreas Gohr } 2881c502704SAndreas Gohr 2891c502704SAndreas Gohr return true; 2901c502704SAndreas Gohr } 2911c502704SAndreas Gohr 2921c502704SAndreas Gohr /** 2930fe33e72SAndreas Gohr * Create a completely new data table with no columns yet also create the appropriate 2940fe33e72SAndreas Gohr * multi value table for the schema 2951c502704SAndreas Gohr * 2961c502704SAndreas Gohr * @todo how do we want to handle indexes? 2971c502704SAndreas Gohr * @return bool 2981c502704SAndreas Gohr */ 2991c502704SAndreas Gohr protected function newDataTable() { 3000fe33e72SAndreas Gohr $ok = true; 3011c502704SAndreas Gohr 3020fe33e72SAndreas Gohr $tbl = 'data_' . $this->table; 3031c502704SAndreas Gohr $sql = "CREATE TABLE $tbl ( 3041c502704SAndreas Gohr pid NOT NULL, 3056684d78dSAndreas Gohr rev INTEGER NOT NULL, 3067059e7e1SAndreas Gohr latest BOOLEAN NOT NULL DEFAULT 0, 3076684d78dSAndreas Gohr PRIMARY KEY(pid, rev) 3086684d78dSAndreas Gohr )"; 3090fe33e72SAndreas Gohr $ok = $ok && (bool) $this->sqlite->query($sql); 3101c502704SAndreas Gohr 3110fe33e72SAndreas Gohr $tbl = 'multi_' . $this->table; 3120fe33e72SAndreas Gohr $sql = "CREATE TABLE $tbl ( 3130fe33e72SAndreas Gohr colref INTEGER NOT NULL, 3140fe33e72SAndreas Gohr pid NOT NULL, 3150fe33e72SAndreas Gohr rev INTEGER NOT NULL, 3160fe33e72SAndreas Gohr row INTEGER NOT NULL, 3170fe33e72SAndreas Gohr value, 3180fe33e72SAndreas Gohr PRIMARY KEY(colref, pid, rev, row) 3190fe33e72SAndreas Gohr );"; 3200fe33e72SAndreas Gohr $ok = $ok && (bool) $this->sqlite->query($sql); 3210fe33e72SAndreas Gohr 3220fe33e72SAndreas Gohr return $ok; 3231c502704SAndreas Gohr } 3241c502704SAndreas Gohr 3251c502704SAndreas Gohr /** 3267c080d69SAndreas Gohr * Creates a new lookup table with no columns 3277c080d69SAndreas Gohr * 3287c080d69SAndreas Gohr * This is basically the same as @see newDataTable() but sets 3297c080d69SAndreas Gohr * different primary keys and types 3307c080d69SAndreas Gohr * 3317c080d69SAndreas Gohr * @return bool 3327c080d69SAndreas Gohr */ 3337c080d69SAndreas Gohr protected function newLookupTable() { 3347c080d69SAndreas Gohr $ok = true; 3357c080d69SAndreas Gohr 3367c080d69SAndreas Gohr $tbl = 'data_' . $this->table; 3377c080d69SAndreas Gohr $sql = "CREATE TABLE $tbl ( 3387c080d69SAndreas Gohr pid INTEGER PRIMARY KEY, 3397c080d69SAndreas Gohr rev INTEGER NOT NULL DEFAULT 0, 3407c080d69SAndreas Gohr latest BOOLEAN NOT NULL DEFAULT 1 3417c080d69SAndreas Gohr )"; 3427c080d69SAndreas Gohr $ok = $ok && (bool) $this->sqlite->query($sql); 3437c080d69SAndreas Gohr 3447c080d69SAndreas Gohr $tbl = 'multi_' . $this->table; 3457c080d69SAndreas Gohr $sql = "CREATE TABLE $tbl ( 3467c080d69SAndreas Gohr colref INTEGER NOT NULL, 3477c080d69SAndreas Gohr pid INTEGER NOT NULL, 3487c080d69SAndreas Gohr rev INTEGER NOT NULL DEFAULT 0, 3497c080d69SAndreas Gohr row INTEGER NOT NULL, 3507c080d69SAndreas Gohr value, 3517c080d69SAndreas Gohr PRIMARY KEY(colref, pid, row) 3527c080d69SAndreas Gohr );"; 3537c080d69SAndreas Gohr $ok = $ok && (bool) $this->sqlite->query($sql); 3547c080d69SAndreas Gohr 3557c080d69SAndreas Gohr return $ok; 3567c080d69SAndreas Gohr } 3577c080d69SAndreas Gohr 3587c080d69SAndreas Gohr /** 3596684d78dSAndreas Gohr * Add an additional column to the existing data table 3601c502704SAndreas Gohr * 3616684d78dSAndreas Gohr * @param int $index the new column index to add 3621c502704SAndreas Gohr * @return bool 3631c502704SAndreas Gohr */ 3646684d78dSAndreas Gohr protected function addDataTableColumn($index) { 3651c502704SAndreas Gohr $tbl = 'data_' . $this->table; 3666684d78dSAndreas Gohr $sql = " ALTER TABLE $tbl ADD COLUMN col$index DEFAULT ''"; 3671c502704SAndreas Gohr if(! $this->sqlite->query($sql)) { 3681c502704SAndreas Gohr return false; 3691c502704SAndreas Gohr } 3701c502704SAndreas Gohr return true; 3711c502704SAndreas Gohr } 3721c502704SAndreas Gohr 37378bff02fSMichael Grosse /** 37478bff02fSMichael Grosse * @param string $user 37578bff02fSMichael Grosse * @return SchemaBuilder 37678bff02fSMichael Grosse */ 37778bff02fSMichael Grosse public function setUser($user) { 37878bff02fSMichael Grosse $this->user = $user; 37978bff02fSMichael Grosse return $this; 38078bff02fSMichael Grosse } 38178bff02fSMichael Grosse 38278bff02fSMichael Grosse 3831c502704SAndreas Gohr} 384