xref: /plugin/struct/meta/SchemaBuilder.php (revision 1dfe5343b719d50bfe1c5e0b85e6cdaa31180e35)
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