xref: /plugin/struct/meta/SchemaBuilder.php (revision 26147f8c0eba4b41504e953950a7f8b8adb04f77)
11c502704SAndreas Gohr<?php
21c502704SAndreas Gohr
31c502704SAndreas Gohrnamespace plugin\struct\meta;
41c502704SAndreas Gohr
57182938bSAndreas Gohr/**
67182938bSAndreas Gohr * Class SchemaBuilder
77182938bSAndreas Gohr *
87182938bSAndreas Gohr * This class builds and updates the schema definitions for our tables. This includes CREATEing and ALTERing
97182938bSAndreas Gohr * the actual data tables as well as updating the meta information in our meta data tables.
107182938bSAndreas Gohr *
117182938bSAndreas Gohr * To use, simply instantiate a new object of the Builder and run the build() method on it.
127182938bSAndreas Gohr *
137182938bSAndreas Gohr * Note: even though data tables use a data_ prefix in the database, this prefix is internal only and should
147182938bSAndreas Gohr *       never be passed as $table anywhere!
157182938bSAndreas Gohr *
167182938bSAndreas Gohr * @package plugin\struct\meta
177182938bSAndreas Gohr */
181c502704SAndreas Gohrclass SchemaBuilder {
191c502704SAndreas Gohr
201c502704SAndreas Gohr    /**
211c502704SAndreas Gohr     * @var array The posted new data for the schema
227182938bSAndreas Gohr     * @see Schema::AdminEditor()
231c502704SAndreas Gohr     */
241c502704SAndreas Gohr    protected $data = array();
251c502704SAndreas Gohr
261c502704SAndreas Gohr    /**
271c502704SAndreas Gohr     * @var string The table name associated with the schema
281c502704SAndreas Gohr     */
291c502704SAndreas Gohr    protected $table = '';
301c502704SAndreas Gohr
311c502704SAndreas Gohr    /**
321c502704SAndreas Gohr     * @var Schema the previously valid schema for this table
331c502704SAndreas Gohr     */
341c502704SAndreas Gohr    protected $oldschema;
351c502704SAndreas Gohr
361c502704SAndreas Gohr    /** @var int the ID of the newly created schema */
371c502704SAndreas Gohr    protected $newschemaid = 0;
381c502704SAndreas Gohr
39a1603abdSAndreas Gohr    /** @var \helper_plugin_struct_db */
40a1603abdSAndreas Gohr    protected $helper;
41a1603abdSAndreas Gohr
421c502704SAndreas Gohr    /** @var \helper_plugin_sqlite|null  */
431c502704SAndreas Gohr    protected $sqlite;
441c502704SAndreas Gohr
451c502704SAndreas Gohr    /**
461c502704SAndreas Gohr     * SchemaBuilder constructor.
477182938bSAndreas Gohr     *
487182938bSAndreas Gohr     * @param string $table The table's name
497182938bSAndreas Gohr     * @param array $data The defining of the table (basically what get's posted in the schema editor form)
507182938bSAndreas Gohr     * @see Schema::AdminEditor()
511c502704SAndreas Gohr     */
521c502704SAndreas Gohr    public function __construct($table, $data) {
531c502704SAndreas Gohr        $this->table = $table;
541c502704SAndreas Gohr        $this->data = $data;
551c502704SAndreas Gohr        $this->oldschema = new Schema($table);
561c502704SAndreas Gohr
57a1603abdSAndreas Gohr        $this->helper = plugin_load('helper', 'struct_db');
58a1603abdSAndreas Gohr        $this->sqlite = $this->helper->getDB();
591c502704SAndreas Gohr    }
601c502704SAndreas Gohr
611c502704SAndreas Gohr    /**
621c502704SAndreas Gohr     * Create the new schema
631c502704SAndreas Gohr     *
641c502704SAndreas Gohr     * @return bool|int the new schema id on success
651c502704SAndreas Gohr     */
661c502704SAndreas Gohr    public function build() {
67a1603abdSAndreas Gohr        $this->fixLabelUniqueness();
68a1603abdSAndreas Gohr
691c502704SAndreas Gohr        $this->sqlite->query('BEGIN TRANSACTION');
706390a534SMichael Große        $ok = true;
716684d78dSAndreas Gohr        // create the data table if new schema
726684d78dSAndreas Gohr        if(!$this->oldschema->getId()) {
731c502704SAndreas Gohr            $ok = $this->newDataTable();
746684d78dSAndreas Gohr        }
751c502704SAndreas Gohr
761c502704SAndreas Gohr        // create a new schema
776390a534SMichael Große        $ok = $ok && $this->newSchema();
781c502704SAndreas Gohr
791c502704SAndreas Gohr        // update column info
806390a534SMichael Große        $ok = $ok && $this->updateColumns();
816390a534SMichael Große        $ok = $ok && $this->addColumns();
821c502704SAndreas Gohr
836390a534SMichael Große        if (!$ok) {
846390a534SMichael Große            $this->sqlite->query('ROLLBACK TRANSACTION');
856390a534SMichael Große            return false;
866390a534SMichael Große        }
871c502704SAndreas Gohr        $this->sqlite->query('COMMIT TRANSACTION');
881c502704SAndreas Gohr
891c502704SAndreas Gohr        return $this->newschemaid;
901c502704SAndreas Gohr    }
911c502704SAndreas Gohr
921c502704SAndreas Gohr    /**
93a1603abdSAndreas Gohr     * Makes sure all labels in the schema to save are unique
94a1603abdSAndreas Gohr     */
95a1603abdSAndreas Gohr    protected function fixLabelUniqueness() {
96a1603abdSAndreas Gohr        $labels = array();
97a1603abdSAndreas Gohr
98cf25d54bSAndreas Gohr        if(isset($this->data['cols'])) foreach($this->data['cols'] as $idx => $column) {
99a1603abdSAndreas Gohr            $this->data['cols'][$idx]['label'] = $this->fixLabel($column['label'], $labels);
100a1603abdSAndreas Gohr        }
101a1603abdSAndreas Gohr
102cf25d54bSAndreas Gohr        if(isset($this->data['new'])) foreach($this->data['new'] as $idx => $column) {
103a1603abdSAndreas Gohr            $this->data['new'][$idx]['label'] = $this->fixLabel($column['label'], $labels);
104a1603abdSAndreas Gohr        }
105a1603abdSAndreas Gohr    }
106a1603abdSAndreas Gohr
107a1603abdSAndreas Gohr    /**
108a1603abdSAndreas Gohr     * Creates a unique label from the given one
109a1603abdSAndreas Gohr     *
110a1603abdSAndreas Gohr     * @param string $wantedlabel
111a1603abdSAndreas Gohr     * @param array $labels list of already assigned labels (will be filled)
112a1603abdSAndreas Gohr     * @return string
113a1603abdSAndreas Gohr     */
114a1603abdSAndreas Gohr    protected function fixLabel($wantedlabel, &$labels) {
115a1603abdSAndreas Gohr        $wantedlabel = trim($wantedlabel);
116a1603abdSAndreas Gohr        $fixedlabel = $wantedlabel;
117a1603abdSAndreas Gohr        $idx = 1;
1182d8ff844SAndreas Gohr        while(isset($labels[utf8_strtolower($fixedlabel)])) {
119a1603abdSAndreas Gohr            $fixedlabel = $wantedlabel.$idx++;
120a1603abdSAndreas Gohr        }
121a1603abdSAndreas Gohr        // did we actually do a rename? apply it.
122a1603abdSAndreas Gohr        if($fixedlabel != $wantedlabel) {
123a1603abdSAndreas Gohr            msg(sprintf($this->helper->getLang('duplicate_label'), $wantedlabel, $fixedlabel), -1);
124a1603abdSAndreas Gohr            $this->data['cols']['label'] = $fixedlabel;
125a1603abdSAndreas Gohr        }
1262d8ff844SAndreas Gohr        $labels[utf8_strtolower($fixedlabel)] = 1;
127a1603abdSAndreas Gohr        return $fixedlabel;
128a1603abdSAndreas Gohr    }
129a1603abdSAndreas Gohr
130a1603abdSAndreas Gohr    /**
1311c502704SAndreas Gohr     * Creates a new schema
1321c502704SAndreas Gohr     *
1331c502704SAndreas Gohr     * @todo use checksum or other heuristic to see if we really need a new schema OTOH we probably need one nearly always!?
1341c502704SAndreas Gohr     */
1351c502704SAndreas Gohr    protected function newSchema() {
1361c502704SAndreas Gohr        $sql = "INSERT INTO schemas (tbl, ts) VALUES (?, ?)";
1371c502704SAndreas Gohr        $this->sqlite->query($sql, $this->table, time());
1381c502704SAndreas Gohr        $res = $this->sqlite->query('SELECT last_insert_rowid()');
1391c502704SAndreas Gohr        $this->newschemaid = $this->sqlite->res2single($res);
1401c502704SAndreas Gohr        $this->sqlite->res_close($res);
1411c502704SAndreas Gohr        if(!$this->newschemaid) return false;
1421c502704SAndreas Gohr        return true;
1431c502704SAndreas Gohr    }
1441c502704SAndreas Gohr
1451c502704SAndreas Gohr    /**
1461c502704SAndreas Gohr     * Updates all the existing column infos and adds them to the new schema
1471c502704SAndreas Gohr     */
1481c502704SAndreas Gohr    protected function updateColumns() {
1491c502704SAndreas Gohr        foreach($this->oldschema->getColumns() as $column) {
1501c502704SAndreas Gohr            $oldEntry = $column->getType()->getAsEntry();
1511c502704SAndreas Gohr            $oldTid   = $column->getTid();
1521c502704SAndreas Gohr            $newEntry = $oldEntry;
1531c502704SAndreas Gohr            $newTid   = $oldTid;
1541c502704SAndreas Gohr            $sort = $column->getSort();
1551c502704SAndreas Gohr            if(isset($this->data['cols'][$column->getColref()])){
1561c502704SAndreas Gohr                // todo I'm not too happy with this hardcoded here - we should probably have a list of fields at one place
1571c502704SAndreas Gohr                $newEntry['config'] = $this->data['cols'][$column->getColref()]['config'];
1581c502704SAndreas Gohr                $newEntry['label'] = $this->data['cols'][$column->getColref()]['label'];
159b7a5ee93SMichael Große                $newEntry['ismulti'] = $this->data['cols'][$column->getColref()]['ismulti'];
1601c502704SAndreas Gohr                $newEntry['class'] = $this->data['cols'][$column->getColref()]['class'];
1611c502704SAndreas Gohr                $sort = $this->data['cols'][$column->getColref()]['sort'];
162*26147f8cSAndreas Gohr                $enabled = (bool) $this->data['cols'][$column->getColref()]['isenabled'];
1631c502704SAndreas Gohr
1641c502704SAndreas Gohr                // when the type definition has changed, we create a new one
1651c502704SAndreas Gohr                if(array_diff_assoc($oldEntry, $newEntry)) {
1661c502704SAndreas Gohr                    $ok = $this->sqlite->storeEntry('types', $newEntry);
1671c502704SAndreas Gohr                    if(!$ok) return false;
1681c502704SAndreas Gohr                    $res = $this->sqlite->query('SELECT last_insert_rowid()');
1691c502704SAndreas Gohr                    if(!$res) return false;
1701c502704SAndreas Gohr                    $newTid = $this->sqlite->res2single($res);
1711c502704SAndreas Gohr                    $this->sqlite->res_close($res);
1721c502704SAndreas Gohr                }
1731c502704SAndreas Gohr            } else {
174*26147f8cSAndreas Gohr                $enabled = false; // no longer there for some reason
1751c502704SAndreas Gohr            }
1761c502704SAndreas Gohr
1771c502704SAndreas Gohr            // add this type to the schema columns
1781c502704SAndreas Gohr            $schemaEntry = array(
1791c502704SAndreas Gohr                'sid' => $this->newschemaid,
1801c502704SAndreas Gohr                'colref' => $column->getColref(),
1811c502704SAndreas Gohr                'enabled' => $enabled,
1821c502704SAndreas Gohr                'tid' => $newTid,
1831c502704SAndreas Gohr                'sort' => $sort
1841c502704SAndreas Gohr            );
1851c502704SAndreas Gohr            $ok = $this->sqlite->storeEntry('schema_cols', $schemaEntry);
1861c502704SAndreas Gohr            if(!$ok) return false;
1871c502704SAndreas Gohr        }
1881c502704SAndreas Gohr        return true;
1891c502704SAndreas Gohr    }
1901c502704SAndreas Gohr
1911c502704SAndreas Gohr    /**
1921c502704SAndreas Gohr     * Adds new columns to the new schema
1931c502704SAndreas Gohr     *
1941c502704SAndreas Gohr     * @return bool
1951c502704SAndreas Gohr     */
1961c502704SAndreas Gohr    protected function addColumns() {
1971c502704SAndreas Gohr        if(!isset($this->data['new'])) return true;
1981c502704SAndreas Gohr
1991c502704SAndreas Gohr        $colref = count($this->oldschema->getColumns())+1;
2001c502704SAndreas Gohr
2011c502704SAndreas Gohr        foreach($this->data['new'] as $column) {
202*26147f8cSAndreas Gohr            if(!$column['isenabled']) continue; // we do not add a disabled column
203*26147f8cSAndreas Gohr
2041c502704SAndreas Gohr            // todo this duplicates the hardcoding as in  the function above
2051c502704SAndreas Gohr            $newEntry = array();
2061c502704SAndreas Gohr            $newEntry['config'] = $column['config'];
2071c502704SAndreas Gohr            $newEntry['label'] = $column['label'];
208b7a5ee93SMichael Große            $newEntry['ismulti'] = $column['ismulti'];
2091c502704SAndreas Gohr            $newEntry['class'] = $column['class'];
2101c502704SAndreas Gohr            $sort = $column['sort'];
211*26147f8cSAndreas Gohr
2121c502704SAndreas Gohr
2136684d78dSAndreas Gohr            // only save if the column got a name
2146684d78dSAndreas Gohr            if(!$newEntry['label']) continue;
2156684d78dSAndreas Gohr
2166684d78dSAndreas Gohr            // add new column to the data table
2176684d78dSAndreas Gohr            if(!$this->addDataTableColumn($colref)) {
2186684d78dSAndreas Gohr                return false;
2196684d78dSAndreas Gohr            }
2206684d78dSAndreas Gohr
2211c502704SAndreas Gohr            // save the type
2221c502704SAndreas Gohr            $ok = $this->sqlite->storeEntry('types', $newEntry);
2231c502704SAndreas Gohr            if(!$ok) return false;
2241c502704SAndreas Gohr            $res = $this->sqlite->query('SELECT last_insert_rowid()');
2251c502704SAndreas Gohr            if(!$res) return false;
2261c502704SAndreas Gohr            $newTid = $this->sqlite->res2single($res);
2271c502704SAndreas Gohr            $this->sqlite->res_close($res);
2281c502704SAndreas Gohr
2291c502704SAndreas Gohr
2301c502704SAndreas Gohr            // add this type to the schema columns
2311c502704SAndreas Gohr            $schemaEntry = array(
2321c502704SAndreas Gohr                'sid' => $this->newschemaid,
2331c502704SAndreas Gohr                'colref' => $colref,
234*26147f8cSAndreas Gohr                'enabled' => true,
2351c502704SAndreas Gohr                'tid' => $newTid,
2361c502704SAndreas Gohr                'sort' => $sort
2371c502704SAndreas Gohr            );
2381c502704SAndreas Gohr            $ok = $this->sqlite->storeEntry('schema_cols', $schemaEntry);
2391c502704SAndreas Gohr            if(!$ok) return false;
2401c502704SAndreas Gohr            $colref++;
2411c502704SAndreas Gohr        }
2421c502704SAndreas Gohr
2431c502704SAndreas Gohr        return true;
2441c502704SAndreas Gohr    }
2451c502704SAndreas Gohr
2461c502704SAndreas Gohr    /**
2470fe33e72SAndreas Gohr     * Create a completely new data table with no columns yet also create the appropriate
2480fe33e72SAndreas Gohr     * multi value table for the schema
2491c502704SAndreas Gohr     *
2501c502704SAndreas Gohr     * @todo how do we want to handle indexes?
2511c502704SAndreas Gohr     * @return bool
2521c502704SAndreas Gohr     */
2531c502704SAndreas Gohr    protected function newDataTable() {
2540fe33e72SAndreas Gohr        $ok = true;
2551c502704SAndreas Gohr
2560fe33e72SAndreas Gohr        $tbl = 'data_' . $this->table;
2571c502704SAndreas Gohr        $sql = "CREATE TABLE $tbl (
2581c502704SAndreas Gohr                    pid NOT NULL,
2596684d78dSAndreas Gohr                    rev INTEGER NOT NULL,
2607059e7e1SAndreas Gohr                    latest BOOLEAN NOT NULL DEFAULT 0,
2616684d78dSAndreas Gohr                    PRIMARY KEY(pid, rev)
2626684d78dSAndreas Gohr                )";
2630fe33e72SAndreas Gohr        $ok = $ok && (bool) $this->sqlite->query($sql);
2641c502704SAndreas Gohr
2650fe33e72SAndreas Gohr        $tbl = 'multi_' . $this->table;
2660fe33e72SAndreas Gohr        $sql = "CREATE TABLE $tbl (
2670fe33e72SAndreas Gohr                    colref INTEGER NOT NULL,
2680fe33e72SAndreas Gohr                    pid NOT NULL,
2690fe33e72SAndreas Gohr                    rev INTEGER NOT NULL,
2700fe33e72SAndreas Gohr                    row INTEGER NOT NULL,
2710fe33e72SAndreas Gohr                    value,
2720fe33e72SAndreas Gohr                    PRIMARY KEY(colref, pid, rev, row)
2730fe33e72SAndreas Gohr                );";
2740fe33e72SAndreas Gohr        $ok = $ok && (bool) $this->sqlite->query($sql);
2750fe33e72SAndreas Gohr
2760fe33e72SAndreas Gohr        return $ok;
2771c502704SAndreas Gohr    }
2781c502704SAndreas Gohr
2791c502704SAndreas Gohr    /**
2806684d78dSAndreas Gohr     * Add an additional column to the existing data table
2811c502704SAndreas Gohr     *
2826684d78dSAndreas Gohr     * @param int $index the new column index to add
2831c502704SAndreas Gohr     * @return bool
2841c502704SAndreas Gohr     */
2856684d78dSAndreas Gohr    protected function addDataTableColumn($index) {
2861c502704SAndreas Gohr        $tbl = 'data_' . $this->table;
2876684d78dSAndreas Gohr        $sql = " ALTER TABLE $tbl ADD COLUMN col$index DEFAULT ''";
2881c502704SAndreas Gohr        if(! $this->sqlite->query($sql)) {
2891c502704SAndreas Gohr            return false;
2901c502704SAndreas Gohr        }
2911c502704SAndreas Gohr        return true;
2921c502704SAndreas Gohr    }
2931c502704SAndreas Gohr
2941c502704SAndreas Gohr}
295