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 14876f0676dSAndreas Gohr /** @noinspection SqlResolve */ 149e2c90eebSAndreas Gohr $sql = "INSERT INTO schemas (tbl, ts, islookup, user, editors) VALUES (?, ?, ?, ?, ?)"; 150e2c90eebSAndreas Gohr $this->sqlite->query($sql, $this->table, $this->time, (int) $this->oldschema->isLookup(), $this->user, $this->data['editors']); 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) { 21476f0676dSAndreas 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); 23276f0676dSAndreas 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, 316*8f259467SAndreas Gohr latest INTEGER NOT NULL DEFAULT 0, 3170fe33e72SAndreas Gohr row INTEGER NOT NULL, 3180fe33e72SAndreas Gohr value, 3190fe33e72SAndreas Gohr PRIMARY KEY(colref, pid, rev, row) 3200fe33e72SAndreas Gohr );"; 3210fe33e72SAndreas Gohr $ok = $ok && (bool) $this->sqlite->query($sql); 3220fe33e72SAndreas Gohr 3230fe33e72SAndreas Gohr return $ok; 3241c502704SAndreas Gohr } 3251c502704SAndreas Gohr 3261c502704SAndreas Gohr /** 3277c080d69SAndreas Gohr * Creates a new lookup table with no columns 3287c080d69SAndreas Gohr * 3297c080d69SAndreas Gohr * This is basically the same as @see newDataTable() but sets 3307c080d69SAndreas Gohr * different primary keys and types 3317c080d69SAndreas Gohr * 3327c080d69SAndreas Gohr * @return bool 3337c080d69SAndreas Gohr */ 3347c080d69SAndreas Gohr protected function newLookupTable() { 3357c080d69SAndreas Gohr $ok = true; 3367c080d69SAndreas Gohr 3377c080d69SAndreas Gohr $tbl = 'data_' . $this->table; 3387c080d69SAndreas Gohr $sql = "CREATE TABLE $tbl ( 3397c080d69SAndreas Gohr pid INTEGER PRIMARY KEY, 3407c080d69SAndreas Gohr rev INTEGER NOT NULL DEFAULT 0, 3417c080d69SAndreas Gohr latest BOOLEAN NOT NULL DEFAULT 1 3427c080d69SAndreas Gohr )"; 3437c080d69SAndreas Gohr $ok = $ok && (bool) $this->sqlite->query($sql); 3447c080d69SAndreas Gohr 3457c080d69SAndreas Gohr $tbl = 'multi_' . $this->table; 3467c080d69SAndreas Gohr $sql = "CREATE TABLE $tbl ( 3477c080d69SAndreas Gohr colref INTEGER NOT NULL, 3487c080d69SAndreas Gohr pid INTEGER NOT NULL, 3497c080d69SAndreas Gohr rev INTEGER NOT NULL DEFAULT 0, 350*8f259467SAndreas Gohr latest INTEGER NOT NULL DEFAULT 0, 3517c080d69SAndreas Gohr row INTEGER NOT NULL, 3527c080d69SAndreas Gohr value, 3537c080d69SAndreas Gohr PRIMARY KEY(colref, pid, row) 3547c080d69SAndreas Gohr );"; 3557c080d69SAndreas Gohr $ok = $ok && (bool) $this->sqlite->query($sql); 3567c080d69SAndreas Gohr 3577c080d69SAndreas Gohr return $ok; 3587c080d69SAndreas Gohr } 3597c080d69SAndreas Gohr 3607c080d69SAndreas Gohr /** 3616684d78dSAndreas Gohr * Add an additional column to the existing data table 3621c502704SAndreas Gohr * 3636684d78dSAndreas Gohr * @param int $index the new column index to add 3641c502704SAndreas Gohr * @return bool 3651c502704SAndreas Gohr */ 3666684d78dSAndreas Gohr protected function addDataTableColumn($index) { 3671c502704SAndreas Gohr $tbl = 'data_' . $this->table; 3686684d78dSAndreas Gohr $sql = " ALTER TABLE $tbl ADD COLUMN col$index DEFAULT ''"; 3691c502704SAndreas Gohr if(! $this->sqlite->query($sql)) { 3701c502704SAndreas Gohr return false; 3711c502704SAndreas Gohr } 3721c502704SAndreas Gohr return true; 3731c502704SAndreas Gohr } 3741c502704SAndreas Gohr 37578bff02fSMichael Grosse /** 37678bff02fSMichael Grosse * @param string $user 37778bff02fSMichael Grosse * @return SchemaBuilder 37878bff02fSMichael Grosse */ 37978bff02fSMichael Grosse public function setUser($user) { 38078bff02fSMichael Grosse $this->user = $user; 38178bff02fSMichael Grosse return $this; 38278bff02fSMichael Grosse } 38378bff02fSMichael Grosse 38478bff02fSMichael Grosse 3851c502704SAndreas Gohr} 386