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 */ 18d6d97f60SAnna Dabrowskaclass SchemaBuilder 19d6d97f60SAnna Dabrowska{ 201c502704SAndreas Gohr 211c502704SAndreas Gohr /** 221c502704SAndreas Gohr * @var array The posted new data for the schema 237182938bSAndreas Gohr * @see Schema::AdminEditor() 241c502704SAndreas Gohr */ 251c502704SAndreas Gohr protected $data = array(); 261c502704SAndreas Gohr 27fa7b96aaSMichael Grosse protected $user; 28fa7b96aaSMichael Grosse 291c502704SAndreas Gohr /** 301c502704SAndreas Gohr * @var string The table name associated with the schema 311c502704SAndreas Gohr */ 321c502704SAndreas Gohr protected $table = ''; 331c502704SAndreas Gohr 341c502704SAndreas Gohr /** 351c502704SAndreas Gohr * @var Schema the previously valid schema for this table 361c502704SAndreas Gohr */ 371c502704SAndreas Gohr protected $oldschema; 381c502704SAndreas Gohr 391c502704SAndreas Gohr /** @var int the ID of the newly created schema */ 401c502704SAndreas Gohr protected $newschemaid = 0; 411c502704SAndreas Gohr 42a1603abdSAndreas Gohr /** @var \helper_plugin_struct_db */ 43a1603abdSAndreas Gohr protected $helper; 44a1603abdSAndreas Gohr 451c502704SAndreas Gohr /** @var \helper_plugin_sqlite|null */ 461c502704SAndreas Gohr protected $sqlite; 471c502704SAndreas Gohr 489d580426SAndreas Gohr /** @var int the time for which this schema should be created - default to time() can be overriden for tests */ 499d580426SAndreas Gohr protected $time = 0; 509d580426SAndreas Gohr 511c502704SAndreas Gohr /** 521c502704SAndreas Gohr * SchemaBuilder constructor. 537182938bSAndreas Gohr * 547182938bSAndreas Gohr * @param string $table The table's name 557182938bSAndreas Gohr * @param array $data The defining of the table (basically what get's posted in the schema editor form) 567182938bSAndreas Gohr * @see Schema::AdminEditor() 571c502704SAndreas Gohr */ 58d6d97f60SAnna Dabrowska public function __construct($table, $data) 59d6d97f60SAnna Dabrowska { 601c502704SAndreas Gohr $this->table = $table; 611c502704SAndreas Gohr $this->data = $data; 620ceefd5cSAnna Dabrowska $this->oldschema = new Schema($table, 0); 631c502704SAndreas Gohr 64a1603abdSAndreas Gohr $this->helper = plugin_load('helper', 'struct_db'); 65a1603abdSAndreas Gohr $this->sqlite = $this->helper->getDB(); 6612fe9b2cSMichael Grosse $this->user = $_SERVER['REMOTE_USER']; 671c502704SAndreas Gohr } 681c502704SAndreas Gohr 691c502704SAndreas Gohr /** 701c502704SAndreas Gohr * Create the new schema 711c502704SAndreas Gohr * 729d580426SAndreas Gohr * @param int $time when to create this schema 0 for now 731c502704SAndreas Gohr * @return bool|int the new schema id on success 741c502704SAndreas Gohr */ 75d6d97f60SAnna Dabrowska public function build($time = 0) 76d6d97f60SAnna Dabrowska { 779d580426SAndreas Gohr $this->time = $time; 78a1603abdSAndreas Gohr $this->fixLabelUniqueness(); 79a1603abdSAndreas Gohr 801c502704SAndreas Gohr $this->sqlite->query('BEGIN TRANSACTION'); 816390a534SMichael Große $ok = true; 826684d78dSAndreas Gohr // create the data table if new schema 836684d78dSAndreas Gohr if (!$this->oldschema->getId()) { 841c502704SAndreas Gohr $ok = $this->newDataTable(); 856684d78dSAndreas Gohr } 861c502704SAndreas Gohr 871c502704SAndreas Gohr // create a new schema 886390a534SMichael Große $ok = $ok && $this->newSchema(); 891c502704SAndreas Gohr 901c502704SAndreas Gohr // update column info 916390a534SMichael Große $ok = $ok && $this->updateColumns(); 926390a534SMichael Große $ok = $ok && $this->addColumns(); 931c502704SAndreas Gohr 946390a534SMichael Große if (!$ok) { 956390a534SMichael Große $this->sqlite->query('ROLLBACK TRANSACTION'); 966390a534SMichael Große return false; 976390a534SMichael Große } 981c502704SAndreas Gohr $this->sqlite->query('COMMIT TRANSACTION'); 991c502704SAndreas Gohr 1001c502704SAndreas Gohr return $this->newschemaid; 1011c502704SAndreas Gohr } 1021c502704SAndreas Gohr 1031c502704SAndreas Gohr /** 104a1603abdSAndreas Gohr * Makes sure all labels in the schema to save are unique 105a1603abdSAndreas Gohr */ 106d6d97f60SAnna Dabrowska protected function fixLabelUniqueness() 107d6d97f60SAnna Dabrowska { 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 */ 126d6d97f60SAnna Dabrowska protected function fixLabel($wantedlabel, &$labels) 127d6d97f60SAnna Dabrowska { 128a1603abdSAndreas Gohr $wantedlabel = trim($wantedlabel); 129a1603abdSAndreas Gohr $fixedlabel = $wantedlabel; 130a1603abdSAndreas Gohr $idx = 1; 1312d8ff844SAndreas Gohr while (isset($labels[utf8_strtolower($fixedlabel)])) { 132a1603abdSAndreas Gohr $fixedlabel = $wantedlabel . $idx++; 133a1603abdSAndreas Gohr } 134a1603abdSAndreas Gohr // did we actually do a rename? apply it. 135a1603abdSAndreas Gohr if ($fixedlabel != $wantedlabel) { 136a1603abdSAndreas Gohr msg(sprintf($this->helper->getLang('duplicate_label'), $wantedlabel, $fixedlabel), -1); 137a1603abdSAndreas Gohr $this->data['cols']['label'] = $fixedlabel; 138a1603abdSAndreas Gohr } 1392d8ff844SAndreas Gohr $labels[utf8_strtolower($fixedlabel)] = 1; 140a1603abdSAndreas Gohr return $fixedlabel; 141a1603abdSAndreas Gohr } 142a1603abdSAndreas Gohr 143a1603abdSAndreas Gohr /** 1441c502704SAndreas Gohr * Creates a new schema 1451c502704SAndreas Gohr */ 146d6d97f60SAnna Dabrowska protected function newSchema() 147d6d97f60SAnna Dabrowska { 1489d580426SAndreas Gohr if (!$this->time) $this->time = time(); 1499d580426SAndreas Gohr 150127d6bacSMichael Große $config = $this->data['config'] ?: '{}'; 151127d6bacSMichael Große 15276f0676dSAndreas Gohr /** @noinspection SqlResolve */ 153*5b808f9fSAnna Dabrowska $sql = "INSERT INTO schemas (tbl, ts, user, config) VALUES (?, ?, ?, ?)"; 154*5b808f9fSAnna Dabrowska $this->sqlite->query($sql, $this->table, $this->time, $this->user, $config); 1551c502704SAndreas Gohr $res = $this->sqlite->query('SELECT last_insert_rowid()'); 1561c502704SAndreas Gohr $this->newschemaid = $this->sqlite->res2single($res); 1571c502704SAndreas Gohr $this->sqlite->res_close($res); 1581c502704SAndreas Gohr if (!$this->newschemaid) return false; 1591c502704SAndreas Gohr return true; 1601c502704SAndreas Gohr } 1611c502704SAndreas Gohr 1621c502704SAndreas Gohr /** 1631c502704SAndreas Gohr * Updates all the existing column infos and adds them to the new schema 1641c502704SAndreas Gohr */ 165d6d97f60SAnna Dabrowska protected function updateColumns() 166d6d97f60SAnna Dabrowska { 1671c502704SAndreas Gohr foreach ($this->oldschema->getColumns() as $column) { 1681c502704SAndreas Gohr $oldEntry = $column->getType()->getAsEntry(); 1691c502704SAndreas Gohr $oldTid = $column->getTid(); 1701c502704SAndreas Gohr $newEntry = $oldEntry; 1711c502704SAndreas Gohr $newTid = $oldTid; 1721c502704SAndreas Gohr $sort = $column->getSort(); 1731c502704SAndreas Gohr if (isset($this->data['cols'][$column->getColref()])) { 1741c502704SAndreas Gohr // todo I'm not too happy with this hardcoded here - we should probably have a list of fields at one place 1751c502704SAndreas Gohr $newEntry['config'] = $this->data['cols'][$column->getColref()]['config']; 1761c502704SAndreas Gohr $newEntry['label'] = $this->data['cols'][$column->getColref()]['label']; 177b7a5ee93SMichael Große $newEntry['ismulti'] = $this->data['cols'][$column->getColref()]['ismulti']; 1781c502704SAndreas Gohr $newEntry['class'] = $this->data['cols'][$column->getColref()]['class']; 1791c502704SAndreas Gohr $sort = $this->data['cols'][$column->getColref()]['sort']; 18026147f8cSAndreas Gohr $enabled = (bool) $this->data['cols'][$column->getColref()]['isenabled']; 1811c502704SAndreas Gohr 1821c502704SAndreas Gohr // when the type definition has changed, we create a new one 1831c502704SAndreas Gohr if (array_diff_assoc($oldEntry, $newEntry)) { 1841c502704SAndreas Gohr $ok = $this->sqlite->storeEntry('types', $newEntry); 1851c502704SAndreas Gohr if (!$ok) return false; 1861c502704SAndreas Gohr $res = $this->sqlite->query('SELECT last_insert_rowid()'); 1871c502704SAndreas Gohr if (!$res) return false; 1881c502704SAndreas Gohr $newTid = $this->sqlite->res2single($res); 1891c502704SAndreas Gohr $this->sqlite->res_close($res); 190b3e0ad6dSMichael Grosse if ($oldEntry['ismulti'] == false && $newEntry['ismulti'] == '1') { 191b3e0ad6dSMichael Grosse $this->migrateSingleToMulti($this->oldschema->getTable(), $column->getColref()); 192b3e0ad6dSMichael Grosse } 1931c502704SAndreas Gohr } 1941c502704SAndreas Gohr } else { 19526147f8cSAndreas Gohr $enabled = false; // no longer there for some reason 1961c502704SAndreas Gohr } 1971c502704SAndreas Gohr 1981c502704SAndreas Gohr // add this type to the schema columns 1991c502704SAndreas Gohr $schemaEntry = array( 2001c502704SAndreas Gohr 'sid' => $this->newschemaid, 2011c502704SAndreas Gohr 'colref' => $column->getColref(), 2021c502704SAndreas Gohr 'enabled' => $enabled, 2031c502704SAndreas Gohr 'tid' => $newTid, 2041c502704SAndreas Gohr 'sort' => $sort 2051c502704SAndreas Gohr ); 2061c502704SAndreas Gohr $ok = $this->sqlite->storeEntry('schema_cols', $schemaEntry); 2071c502704SAndreas Gohr if (!$ok) return false; 2081c502704SAndreas Gohr } 2091c502704SAndreas Gohr return true; 2101c502704SAndreas Gohr } 2111c502704SAndreas Gohr 2124050be94SMichael Grosse /** 2134050be94SMichael Grosse * Write the latest value from an entry in a data_ table to the corresponding multi_table 2144050be94SMichael Grosse * 2154050be94SMichael Grosse * @param string $table 2164050be94SMichael Grosse * @param int $colref 2174050be94SMichael Grosse */ 218d6d97f60SAnna Dabrowska protected function migrateSingleToMulti($table, $colref) 219d6d97f60SAnna Dabrowska { 22076f0676dSAndreas Gohr /** @noinspection SqlResolve */ 221b3e0ad6dSMichael Grosse $sqlSelect = "SELECT pid, rev, col$colref AS value FROM data_$table WHERE latest = 1"; 222b3e0ad6dSMichael Grosse $res = $this->sqlite->query($sqlSelect); 223b3e0ad6dSMichael Grosse $valueSet = $this->sqlite->res2arr($res); 224b3e0ad6dSMichael Grosse $this->sqlite->res_close($res); 225b3e0ad6dSMichael Grosse $valueString = array(); 2264050be94SMichael Grosse $arguments = array(); 227b3e0ad6dSMichael Grosse foreach ($valueSet as $values) { 228b3e0ad6dSMichael Grosse if (blank($values['value']) || trim($values['value']) == '') { 229b3e0ad6dSMichael Grosse continue; 230b3e0ad6dSMichael Grosse } 2314050be94SMichael Grosse $valueString[] = "(?, ?, ?, ?, ?)"; 2324050be94SMichael Grosse $arguments = array_merge($arguments, array($colref, $values['pid'], $values['rev'], 1, $values['value'])); 233b3e0ad6dSMichael Grosse } 234b3e0ad6dSMichael Grosse if (empty($valueString)) { 235b3e0ad6dSMichael Grosse return; 236b3e0ad6dSMichael Grosse } 237b3e0ad6dSMichael Grosse $valueString = join(',', $valueString); 23876f0676dSAndreas Gohr /** @noinspection SqlResolve */ 239b3e0ad6dSMichael Grosse $sqlInsert = "INSERT OR REPLACE INTO multi_$table (colref, pid, rev, row, value) VALUES $valueString"; 2404050be94SMichael Grosse $this->sqlite->query($sqlInsert, $arguments); 241b3e0ad6dSMichael Grosse } 242b3e0ad6dSMichael Grosse 2431c502704SAndreas Gohr /** 2441c502704SAndreas Gohr * Adds new columns to the new schema 2451c502704SAndreas Gohr * 2461c502704SAndreas Gohr * @return bool 2471c502704SAndreas Gohr */ 248d6d97f60SAnna Dabrowska protected function addColumns() 249d6d97f60SAnna Dabrowska { 2501c502704SAndreas Gohr if (!isset($this->data['new'])) return true; 2511c502704SAndreas Gohr 2521c502704SAndreas Gohr $colref = count($this->oldschema->getColumns()) + 1; 2531c502704SAndreas Gohr 2541c502704SAndreas Gohr foreach ($this->data['new'] as $column) { 25526147f8cSAndreas Gohr if (!$column['isenabled']) continue; // we do not add a disabled column 25626147f8cSAndreas Gohr 2571c502704SAndreas Gohr // todo this duplicates the hardcoding as in the function above 2581c502704SAndreas Gohr $newEntry = array(); 2591c502704SAndreas Gohr $newEntry['config'] = $column['config']; 2601c502704SAndreas Gohr $newEntry['label'] = $column['label']; 261b7a5ee93SMichael Große $newEntry['ismulti'] = $column['ismulti']; 2621c502704SAndreas Gohr $newEntry['class'] = $column['class']; 2631c502704SAndreas Gohr $sort = $column['sort']; 26426147f8cSAndreas Gohr 2651c502704SAndreas Gohr 2666684d78dSAndreas Gohr // only save if the column got a name 2676684d78dSAndreas Gohr if (!$newEntry['label']) continue; 2686684d78dSAndreas Gohr 2696684d78dSAndreas Gohr // add new column to the data table 2706684d78dSAndreas Gohr if (!$this->addDataTableColumn($colref)) { 2716684d78dSAndreas Gohr return false; 2726684d78dSAndreas Gohr } 2736684d78dSAndreas Gohr 2741c502704SAndreas Gohr // save the type 2751c502704SAndreas Gohr $ok = $this->sqlite->storeEntry('types', $newEntry); 2761c502704SAndreas Gohr if (!$ok) return false; 2771c502704SAndreas Gohr $res = $this->sqlite->query('SELECT last_insert_rowid()'); 2781c502704SAndreas Gohr if (!$res) return false; 2791c502704SAndreas Gohr $newTid = $this->sqlite->res2single($res); 2801c502704SAndreas Gohr $this->sqlite->res_close($res); 2811c502704SAndreas Gohr 2821c502704SAndreas Gohr 2831c502704SAndreas Gohr // add this type to the schema columns 2841c502704SAndreas Gohr $schemaEntry = array( 2851c502704SAndreas Gohr 'sid' => $this->newschemaid, 2861c502704SAndreas Gohr 'colref' => $colref, 28726147f8cSAndreas Gohr 'enabled' => true, 2881c502704SAndreas Gohr 'tid' => $newTid, 2891c502704SAndreas Gohr 'sort' => $sort 2901c502704SAndreas Gohr ); 2911c502704SAndreas Gohr $ok = $this->sqlite->storeEntry('schema_cols', $schemaEntry); 2921c502704SAndreas Gohr if (!$ok) return false; 2931c502704SAndreas Gohr $colref++; 2941c502704SAndreas Gohr } 2951c502704SAndreas Gohr 2961c502704SAndreas Gohr return true; 2971c502704SAndreas Gohr } 2981c502704SAndreas Gohr 2991c502704SAndreas Gohr /** 3000fe33e72SAndreas Gohr * Create a completely new data table with no columns yet also create the appropriate 3010fe33e72SAndreas Gohr * multi value table for the schema 3021c502704SAndreas Gohr * 3031c502704SAndreas Gohr * @todo how do we want to handle indexes? 3041c502704SAndreas Gohr * @return bool 3051c502704SAndreas Gohr */ 306d6d97f60SAnna Dabrowska protected function newDataTable() 307d6d97f60SAnna Dabrowska { 3080fe33e72SAndreas Gohr $ok = true; 3091c502704SAndreas Gohr 3100fe33e72SAndreas Gohr $tbl = 'data_' . $this->table; 3111c502704SAndreas Gohr $sql = "CREATE TABLE $tbl ( 31286a40c1eSAnna Dabrowska pid TEXT DEFAULT '', 3130ceefd5cSAnna Dabrowska rid INTEGER, 3140ceefd5cSAnna Dabrowska rev INTEGER, 3157059e7e1SAndreas Gohr latest BOOLEAN NOT NULL DEFAULT 0, 3160ceefd5cSAnna Dabrowska PRIMARY KEY(pid, rid, rev) 3176684d78dSAndreas Gohr )"; 3180fe33e72SAndreas Gohr $ok = $ok && (bool) $this->sqlite->query($sql); 3191c502704SAndreas Gohr 3200fe33e72SAndreas Gohr $tbl = 'multi_' . $this->table; 3210fe33e72SAndreas Gohr $sql = "CREATE TABLE $tbl ( 3220fe33e72SAndreas Gohr colref INTEGER NOT NULL, 32386a40c1eSAnna Dabrowska pid TEXT DEFAULT '', 3240ceefd5cSAnna Dabrowska rid INTEGER, 3250ceefd5cSAnna Dabrowska rev INTEGER, 3268f259467SAndreas Gohr latest INTEGER NOT NULL DEFAULT 0, 3270fe33e72SAndreas Gohr row INTEGER NOT NULL, 3280fe33e72SAndreas Gohr value, 3290ceefd5cSAnna Dabrowska PRIMARY KEY(colref, pid, rid, rev, row) 3307c080d69SAndreas Gohr );"; 3317c080d69SAndreas Gohr $ok = $ok && (bool) $this->sqlite->query($sql); 3327c080d69SAndreas Gohr 3337c080d69SAndreas Gohr return $ok; 3347c080d69SAndreas Gohr } 3357c080d69SAndreas Gohr 3367c080d69SAndreas Gohr /** 3376684d78dSAndreas Gohr * Add an additional column to the existing data table 3381c502704SAndreas Gohr * 3396684d78dSAndreas Gohr * @param int $index the new column index to add 3401c502704SAndreas Gohr * @return bool 3411c502704SAndreas Gohr */ 342d6d97f60SAnna Dabrowska protected function addDataTableColumn($index) 343d6d97f60SAnna Dabrowska { 3441c502704SAndreas Gohr $tbl = 'data_' . $this->table; 3456684d78dSAndreas Gohr $sql = " ALTER TABLE $tbl ADD COLUMN col$index DEFAULT ''"; 3461c502704SAndreas Gohr if (! $this->sqlite->query($sql)) { 3471c502704SAndreas Gohr return false; 3481c502704SAndreas Gohr } 3491c502704SAndreas Gohr return true; 3501c502704SAndreas Gohr } 3511c502704SAndreas Gohr 35278bff02fSMichael Grosse /** 35378bff02fSMichael Grosse * @param string $user 35478bff02fSMichael Grosse * @return SchemaBuilder 35578bff02fSMichael Grosse */ 356d6d97f60SAnna Dabrowska public function setUser($user) 357d6d97f60SAnna Dabrowska { 35878bff02fSMichael Grosse $this->user = $user; 35978bff02fSMichael Grosse return $this; 36078bff02fSMichael Grosse } 3611c502704SAndreas Gohr} 362