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