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