1<?php 2 3namespace plugin\struct\meta; 4 5/** 6 * Class SchemaBuilder 7 * 8 * This class builds and updates the schema definitions for our tables. This includes CREATEing and ALTERing 9 * the actual data tables as well as updating the meta information in our meta data tables. 10 * 11 * To use, simply instantiate a new object of the Builder and run the build() method on it. 12 * 13 * Note: even though data tables use a data_ prefix in the database, this prefix is internal only and should 14 * never be passed as $table anywhere! 15 * 16 * @package plugin\struct\meta 17 */ 18class SchemaBuilder { 19 20 /** 21 * @var array The posted new data for the schema 22 * @see Schema::AdminEditor() 23 */ 24 protected $data = array(); 25 26 /** 27 * @var string The table name associated with the schema 28 */ 29 protected $table = ''; 30 31 /** 32 * @var Schema the previously valid schema for this table 33 */ 34 protected $oldschema; 35 36 /** @var int the ID of the newly created schema */ 37 protected $newschemaid = 0; 38 39 /** @var \helper_plugin_struct_db */ 40 protected $helper; 41 42 /** @var \helper_plugin_sqlite|null */ 43 protected $sqlite; 44 45 /** @var int the time for which this schema should be created - default to time() can be overriden for tests */ 46 protected $time = 0; 47 48 /** 49 * SchemaBuilder constructor. 50 * 51 * @param string $table The table's name 52 * @param array $data The defining of the table (basically what get's posted in the schema editor form) 53 * @see Schema::AdminEditor() 54 */ 55 public function __construct($table, $data) { 56 $this->table = $table; 57 $this->data = $data; 58 $this->oldschema = new Schema($table); 59 60 $this->helper = plugin_load('helper', 'struct_db'); 61 $this->sqlite = $this->helper->getDB(); 62 } 63 64 /** 65 * Create the new schema 66 * 67 * @param int $time when to create this schema 0 for now 68 * @return bool|int the new schema id on success 69 */ 70 public function build($time=0) { 71 $this->time = $time; 72 $this->fixLabelUniqueness(); 73 74 $this->sqlite->query('BEGIN TRANSACTION'); 75 $ok = true; 76 // create the data table if new schema 77 if(!$this->oldschema->getId()) { 78 $ok = $this->newDataTable(); 79 } 80 81 // create a new schema 82 $ok = $ok && $this->newSchema(); 83 84 // update column info 85 $ok = $ok && $this->updateColumns(); 86 $ok = $ok && $this->addColumns(); 87 88 if (!$ok) { 89 $this->sqlite->query('ROLLBACK TRANSACTION'); 90 return false; 91 } 92 $this->sqlite->query('COMMIT TRANSACTION'); 93 94 return $this->newschemaid; 95 } 96 97 /** 98 * Makes sure all labels in the schema to save are unique 99 */ 100 protected function fixLabelUniqueness() { 101 $labels = array(); 102 103 if(isset($this->data['cols'])) foreach($this->data['cols'] as $idx => $column) { 104 $this->data['cols'][$idx]['label'] = $this->fixLabel($column['label'], $labels); 105 } 106 107 if(isset($this->data['new'])) foreach($this->data['new'] as $idx => $column) { 108 $this->data['new'][$idx]['label'] = $this->fixLabel($column['label'], $labels); 109 } 110 } 111 112 /** 113 * Creates a unique label from the given one 114 * 115 * @param string $wantedlabel 116 * @param array $labels list of already assigned labels (will be filled) 117 * @return string 118 */ 119 protected function fixLabel($wantedlabel, &$labels) { 120 $wantedlabel = trim($wantedlabel); 121 $fixedlabel = $wantedlabel; 122 $idx = 1; 123 while(isset($labels[utf8_strtolower($fixedlabel)])) { 124 $fixedlabel = $wantedlabel.$idx++; 125 } 126 // did we actually do a rename? apply it. 127 if($fixedlabel != $wantedlabel) { 128 msg(sprintf($this->helper->getLang('duplicate_label'), $wantedlabel, $fixedlabel), -1); 129 $this->data['cols']['label'] = $fixedlabel; 130 } 131 $labels[utf8_strtolower($fixedlabel)] = 1; 132 return $fixedlabel; 133 } 134 135 /** 136 * Creates a new schema 137 * 138 * @todo use checksum or other heuristic to see if we really need a new schema OTOH we probably need one nearly always!? 139 */ 140 protected function newSchema() { 141 if(!$this->time) $this->time = time(); 142 143 $sql = "INSERT INTO schemas (tbl, ts) VALUES (?, ?)"; 144 $this->sqlite->query($sql, $this->table, $this->time); 145 $res = $this->sqlite->query('SELECT last_insert_rowid()'); 146 $this->newschemaid = $this->sqlite->res2single($res); 147 $this->sqlite->res_close($res); 148 if(!$this->newschemaid) return false; 149 return true; 150 } 151 152 /** 153 * Updates all the existing column infos and adds them to the new schema 154 */ 155 protected function updateColumns() { 156 foreach($this->oldschema->getColumns() as $column) { 157 $oldEntry = $column->getType()->getAsEntry(); 158 $oldTid = $column->getTid(); 159 $newEntry = $oldEntry; 160 $newTid = $oldTid; 161 $sort = $column->getSort(); 162 if(isset($this->data['cols'][$column->getColref()])){ 163 // todo I'm not too happy with this hardcoded here - we should probably have a list of fields at one place 164 $newEntry['config'] = $this->data['cols'][$column->getColref()]['config']; 165 $newEntry['label'] = $this->data['cols'][$column->getColref()]['label']; 166 $newEntry['ismulti'] = $this->data['cols'][$column->getColref()]['ismulti']; 167 $newEntry['class'] = $this->data['cols'][$column->getColref()]['class']; 168 $sort = $this->data['cols'][$column->getColref()]['sort']; 169 $enabled = (bool) $this->data['cols'][$column->getColref()]['isenabled']; 170 171 // when the type definition has changed, we create a new one 172 if(array_diff_assoc($oldEntry, $newEntry)) { 173 $ok = $this->sqlite->storeEntry('types', $newEntry); 174 if(!$ok) return false; 175 $res = $this->sqlite->query('SELECT last_insert_rowid()'); 176 if(!$res) return false; 177 $newTid = $this->sqlite->res2single($res); 178 $this->sqlite->res_close($res); 179 } 180 } else { 181 $enabled = false; // no longer there for some reason 182 } 183 184 // add this type to the schema columns 185 $schemaEntry = array( 186 'sid' => $this->newschemaid, 187 'colref' => $column->getColref(), 188 'enabled' => $enabled, 189 'tid' => $newTid, 190 'sort' => $sort 191 ); 192 $ok = $this->sqlite->storeEntry('schema_cols', $schemaEntry); 193 if(!$ok) return false; 194 } 195 return true; 196 } 197 198 /** 199 * Adds new columns to the new schema 200 * 201 * @return bool 202 */ 203 protected function addColumns() { 204 if(!isset($this->data['new'])) return true; 205 206 $colref = count($this->oldschema->getColumns())+1; 207 208 foreach($this->data['new'] as $column) { 209 if(!$column['isenabled']) continue; // we do not add a disabled column 210 211 // todo this duplicates the hardcoding as in the function above 212 $newEntry = array(); 213 $newEntry['config'] = $column['config']; 214 $newEntry['label'] = $column['label']; 215 $newEntry['ismulti'] = $column['ismulti']; 216 $newEntry['class'] = $column['class']; 217 $sort = $column['sort']; 218 219 220 // only save if the column got a name 221 if(!$newEntry['label']) continue; 222 223 // add new column to the data table 224 if(!$this->addDataTableColumn($colref)) { 225 return false; 226 } 227 228 // save the type 229 $ok = $this->sqlite->storeEntry('types', $newEntry); 230 if(!$ok) return false; 231 $res = $this->sqlite->query('SELECT last_insert_rowid()'); 232 if(!$res) return false; 233 $newTid = $this->sqlite->res2single($res); 234 $this->sqlite->res_close($res); 235 236 237 // add this type to the schema columns 238 $schemaEntry = array( 239 'sid' => $this->newschemaid, 240 'colref' => $colref, 241 'enabled' => true, 242 'tid' => $newTid, 243 'sort' => $sort 244 ); 245 $ok = $this->sqlite->storeEntry('schema_cols', $schemaEntry); 246 if(!$ok) return false; 247 $colref++; 248 } 249 250 return true; 251 } 252 253 /** 254 * Create a completely new data table with no columns yet also create the appropriate 255 * multi value table for the schema 256 * 257 * @todo how do we want to handle indexes? 258 * @return bool 259 */ 260 protected function newDataTable() { 261 $ok = true; 262 263 $tbl = 'data_' . $this->table; 264 $sql = "CREATE TABLE $tbl ( 265 pid NOT NULL, 266 rev INTEGER NOT NULL, 267 latest BOOLEAN NOT NULL DEFAULT 0, 268 PRIMARY KEY(pid, rev) 269 )"; 270 $ok = $ok && (bool) $this->sqlite->query($sql); 271 272 $tbl = 'multi_' . $this->table; 273 $sql = "CREATE TABLE $tbl ( 274 colref INTEGER NOT NULL, 275 pid NOT NULL, 276 rev INTEGER NOT NULL, 277 row INTEGER NOT NULL, 278 value, 279 PRIMARY KEY(colref, pid, rev, row) 280 );"; 281 $ok = $ok && (bool) $this->sqlite->query($sql); 282 283 return $ok; 284 } 285 286 /** 287 * Add an additional column to the existing data table 288 * 289 * @param int $index the new column index to add 290 * @return bool 291 */ 292 protected function addDataTableColumn($index) { 293 $tbl = 'data_' . $this->table; 294 $sql = " ALTER TABLE $tbl ADD COLUMN col$index DEFAULT ''"; 295 if(! $this->sqlite->query($sql)) { 296 return false; 297 } 298 return true; 299 } 300 301} 302