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_sqlite|null */ 40 protected $sqlite; 41 42 /** 43 * SchemaBuilder constructor. 44 * 45 * @param string $table The table's name 46 * @param array $data The defining of the table (basically what get's posted in the schema editor form) 47 * @see Schema::AdminEditor() 48 */ 49 public function __construct($table, $data) { 50 $this->table = $table; 51 $this->data = $data; 52 $this->oldschema = new Schema($table); 53 54 /** @var \helper_plugin_struct_db $helper */ 55 $helper = plugin_load('helper', 'struct_db'); 56 $this->sqlite = $helper->getDB(); 57 } 58 59 /** 60 * Create the new schema 61 * 62 * @return bool|int the new schema id on success 63 */ 64 public function build() { 65 $this->sqlite->query('BEGIN TRANSACTION'); 66 67 // create or update the data table 68 if($this->oldschema->getId()) { 69 $ok = $this->updateDataTable(); 70 } else { 71 $ok = $this->newDataTable(); 72 } 73 if(!$ok) return false; 74 75 // create a new schema 76 if(!$this->newSchema()) return false; 77 78 // update column info 79 if(!$this->updateColumns()) return false; 80 if(!$this->addColumns()) return false; 81 82 $this->sqlite->query('COMMIT TRANSACTION'); 83 84 return $this->newschemaid; 85 } 86 87 /** 88 * Creates a new schema 89 * 90 * @todo use checksum or other heuristic to see if we really need a new schema OTOH we probably need one nearly always!? 91 */ 92 protected function newSchema() { 93 $sql = "INSERT INTO schemas (tbl, ts) VALUES (?, ?)"; 94 $this->sqlite->query($sql, $this->table, time()); 95 $res = $this->sqlite->query('SELECT last_insert_rowid()'); 96 $this->newschemaid = $this->sqlite->res2single($res); 97 $this->sqlite->res_close($res); 98 if(!$this->newschemaid) return false; 99 return true; 100 } 101 102 /** 103 * Updates all the existing column infos and adds them to the new schema 104 */ 105 protected function updateColumns() { 106 foreach($this->oldschema->getColumns() as $column) { 107 $oldEntry = $column->getType()->getAsEntry(); 108 $oldTid = $column->getTid(); 109 $newEntry = $oldEntry; 110 $newTid = $oldTid; 111 $enabled = true; 112 $sort = $column->getSort(); 113 if(isset($this->data['cols'][$column->getColref()])){ 114 // todo I'm not too happy with this hardcoded here - we should probably have a list of fields at one place 115 $newEntry['config'] = $this->data['cols'][$column->getColref()]['config']; 116 $newEntry['label'] = $this->data['cols'][$column->getColref()]['label']; 117 $newEntry['ismulti'] = $this->data['cols'][$column->getColref()]['multi']; 118 $newEntry['class'] = $this->data['cols'][$column->getColref()]['class']; 119 $sort = $this->data['cols'][$column->getColref()]['sort']; 120 121 // when the type definition has changed, we create a new one 122 if(array_diff_assoc($oldEntry, $newEntry)) { 123 $ok = $this->sqlite->storeEntry('types', $newEntry); 124 if(!$ok) return false; 125 $res = $this->sqlite->query('SELECT last_insert_rowid()'); 126 if(!$res) return false; 127 $newTid = $this->sqlite->res2single($res); 128 $this->sqlite->res_close($res); 129 } 130 } else { 131 $enabled = false; // no longer there FIXME this assumes we remove the entry from the form completely. We might not want to do that 132 } 133 134 // add this type to the schema columns 135 $schemaEntry = array( 136 'sid' => $this->newschemaid, 137 'colref' => $column->getColref(), 138 'enabled' => $enabled, 139 'tid' => $newTid, 140 'sort' => $sort 141 ); 142 $ok = $this->sqlite->storeEntry('schema_cols', $schemaEntry); 143 if(!$ok) return false; 144 } 145 return true; 146 } 147 148 /** 149 * Adds new columns to the new schema 150 * 151 * @return bool 152 */ 153 protected function addColumns() { 154 if(!isset($this->data['new'])) return true; 155 156 $colref = count($this->oldschema->getColumns())+1; 157 158 foreach($this->data['new'] as $column) { 159 // todo this duplicates the hardcoding as in the function above 160 $newEntry = array(); 161 $newEntry['config'] = $column['config']; 162 $newEntry['label'] = $column['label']; 163 $newEntry['ismulti'] = $column['multi']; 164 $newEntry['class'] = $column['class']; 165 $sort = $column['sort']; 166 $enabled = true; 167 168 // save the type 169 $ok = $this->sqlite->storeEntry('types', $newEntry); 170 if(!$ok) return false; 171 $res = $this->sqlite->query('SELECT last_insert_rowid()'); 172 if(!$res) return false; 173 $newTid = $this->sqlite->res2single($res); 174 $this->sqlite->res_close($res); 175 176 177 // add this type to the schema columns 178 $schemaEntry = array( 179 'sid' => $this->newschemaid, 180 'colref' => $colref, 181 'enabled' => $enabled, 182 'tid' => $newTid, 183 'sort' => $sort 184 ); 185 $ok = $this->sqlite->storeEntry('schema_cols', $schemaEntry); 186 if(!$ok) return false; 187 $colref++; 188 } 189 190 return true; 191 } 192 193 /** 194 * Create a completely new data table 195 * 196 * @todo how do we want to handle indexes? 197 * @return bool 198 */ 199 protected function newDataTable() { 200 $tbl = 'data_' . $this->table; 201 $cols = count($this->data['new']); // number of columns in the schema 202 203 $sql = "CREATE TABLE $tbl ( 204 pid NOT NULL, 205 rev INTEGER NOT NULL,\n"; 206 for($i = 1; $i <= $cols; $i++) { 207 $sql .= "col$i DEFAULT '',\n"; 208 } 209 $sql .= "PRIMARY KEY(pid, rev) )"; 210 211 return (bool) $this->sqlite->query($sql); 212 } 213 214 /** 215 * Add additional columns to an existing data table 216 * 217 * @return bool 218 */ 219 protected function updateDataTable() { 220 $tbl = 'data_' . $this->table; 221 $oldcols = count($this->oldschema->getColumns()); // number of columns in the old schema 222 $newcols = count($this->data['new']); // number of *new* columns in the schema 223 224 for($i = $oldcols+1; $i <= $oldcols + $newcols; $i++) { 225 $sql = " ALTER TABLE $tbl ADD COLUMN col$i DEFAULT ''"; 226 if(! $this->sqlite->query($sql)) { 227 return false; 228 } 229 } 230 231 return true; 232 } 233 234} 235