xref: /plugin/struct/meta/SchemaBuilder.php (revision 12fe9b2c1634e3688e99987f2b3f231c971a3c04)
11c502704SAndreas Gohr<?php
21c502704SAndreas Gohr
3ba766201SAndreas Gohrnamespace dokuwiki\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 *
16ba766201SAndreas Gohr * @package dokuwiki\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
26fa7b96aaSMichael Grosse    protected $user;
27fa7b96aaSMichael Grosse
281c502704SAndreas Gohr    /**
291c502704SAndreas Gohr     * @var string The table name associated with the schema
301c502704SAndreas Gohr     */
311c502704SAndreas Gohr    protected $table = '';
321c502704SAndreas Gohr
331c502704SAndreas Gohr    /**
341c502704SAndreas Gohr     * @var Schema the previously valid schema for this table
351c502704SAndreas Gohr     */
361c502704SAndreas Gohr    protected $oldschema;
371c502704SAndreas Gohr
381c502704SAndreas Gohr    /** @var int the ID of the newly created schema */
391c502704SAndreas Gohr    protected $newschemaid = 0;
401c502704SAndreas Gohr
41a1603abdSAndreas Gohr    /** @var \helper_plugin_struct_db */
42a1603abdSAndreas Gohr    protected $helper;
43a1603abdSAndreas Gohr
441c502704SAndreas Gohr    /** @var \helper_plugin_sqlite|null  */
451c502704SAndreas Gohr    protected $sqlite;
461c502704SAndreas Gohr
479d580426SAndreas Gohr    /** @var int the time for which this schema should be created - default to time() can be overriden for tests */
489d580426SAndreas Gohr    protected $time = 0;
499d580426SAndreas Gohr
501c502704SAndreas Gohr    /**
511c502704SAndreas Gohr     * SchemaBuilder constructor.
527182938bSAndreas Gohr     *
537182938bSAndreas Gohr     * @param string $table The table's name
547182938bSAndreas Gohr     * @param array $data The defining of the table (basically what get's posted in the schema editor form)
557182938bSAndreas Gohr     * @see Schema::AdminEditor()
561c502704SAndreas Gohr     */
571c502704SAndreas Gohr    public function __construct($table, $data) {
581c502704SAndreas Gohr        $this->table = $table;
591c502704SAndreas Gohr        $this->data = $data;
601c502704SAndreas Gohr        $this->oldschema = new Schema($table);
611c502704SAndreas Gohr
62aeca15adSMichael Grosse        $this->helper = plugin_load('helper', 'struct_db');
63a1603abdSAndreas Gohr        $this->sqlite = $this->helper->getDB();
64*12fe9b2cSMichael Grosse        $this->user = $_SERVER['REMOTE_USER'];
651c502704SAndreas Gohr    }
661c502704SAndreas Gohr
671c502704SAndreas Gohr    /**
681c502704SAndreas Gohr     * Create the new schema
691c502704SAndreas Gohr     *
709d580426SAndreas Gohr     * @param int $time when to create this schema 0 for now
711c502704SAndreas Gohr     * @return bool|int the new schema id on success
721c502704SAndreas Gohr     */
739d580426SAndreas Gohr    public function build($time=0) {
749d580426SAndreas Gohr        $this->time = $time;
75a1603abdSAndreas Gohr        $this->fixLabelUniqueness();
76a1603abdSAndreas Gohr
771c502704SAndreas Gohr        $this->sqlite->query('BEGIN TRANSACTION');
786390a534SMichael Große        $ok = true;
796684d78dSAndreas Gohr        // create the data table if new schema
806684d78dSAndreas Gohr        if(!$this->oldschema->getId()) {
811c502704SAndreas Gohr            $ok = $this->newDataTable();
826684d78dSAndreas Gohr        }
831c502704SAndreas Gohr
841c502704SAndreas Gohr        // create a new schema
856390a534SMichael Große        $ok = $ok && $this->newSchema();
861c502704SAndreas Gohr
871c502704SAndreas Gohr        // update column info
886390a534SMichael Große        $ok = $ok && $this->updateColumns();
896390a534SMichael Große        $ok = $ok && $this->addColumns();
901c502704SAndreas Gohr
916390a534SMichael Große        if (!$ok) {
926390a534SMichael Große            $this->sqlite->query('ROLLBACK TRANSACTION');
936390a534SMichael Große            return false;
946390a534SMichael Große        }
951c502704SAndreas Gohr        $this->sqlite->query('COMMIT TRANSACTION');
961c502704SAndreas Gohr
971c502704SAndreas Gohr        return $this->newschemaid;
981c502704SAndreas Gohr    }
991c502704SAndreas Gohr
1001c502704SAndreas Gohr    /**
101a1603abdSAndreas Gohr     * Makes sure all labels in the schema to save are unique
102a1603abdSAndreas Gohr     */
103a1603abdSAndreas Gohr    protected function fixLabelUniqueness() {
104a1603abdSAndreas Gohr        $labels = array();
105a1603abdSAndreas Gohr
106cf25d54bSAndreas Gohr        if(isset($this->data['cols'])) foreach($this->data['cols'] as $idx => $column) {
107a1603abdSAndreas Gohr            $this->data['cols'][$idx]['label'] = $this->fixLabel($column['label'], $labels);
108a1603abdSAndreas Gohr        }
109a1603abdSAndreas Gohr
110cf25d54bSAndreas Gohr        if(isset($this->data['new'])) foreach($this->data['new'] as $idx => $column) {
111a1603abdSAndreas Gohr            $this->data['new'][$idx]['label'] = $this->fixLabel($column['label'], $labels);
112a1603abdSAndreas Gohr        }
113a1603abdSAndreas Gohr    }
114a1603abdSAndreas Gohr
115a1603abdSAndreas Gohr    /**
116a1603abdSAndreas Gohr     * Creates a unique label from the given one
117a1603abdSAndreas Gohr     *
118a1603abdSAndreas Gohr     * @param string $wantedlabel
119a1603abdSAndreas Gohr     * @param array $labels list of already assigned labels (will be filled)
120a1603abdSAndreas Gohr     * @return string
121a1603abdSAndreas Gohr     */
122a1603abdSAndreas Gohr    protected function fixLabel($wantedlabel, &$labels) {
123a1603abdSAndreas Gohr        $wantedlabel = trim($wantedlabel);
124a1603abdSAndreas Gohr        $fixedlabel = $wantedlabel;
125a1603abdSAndreas Gohr        $idx = 1;
1262d8ff844SAndreas Gohr        while(isset($labels[utf8_strtolower($fixedlabel)])) {
127a1603abdSAndreas Gohr            $fixedlabel = $wantedlabel.$idx++;
128a1603abdSAndreas Gohr        }
129a1603abdSAndreas Gohr        // did we actually do a rename? apply it.
130a1603abdSAndreas Gohr        if($fixedlabel != $wantedlabel) {
131a1603abdSAndreas Gohr            msg(sprintf($this->helper->getLang('duplicate_label'), $wantedlabel, $fixedlabel), -1);
132a1603abdSAndreas Gohr            $this->data['cols']['label'] = $fixedlabel;
133a1603abdSAndreas Gohr        }
1342d8ff844SAndreas Gohr        $labels[utf8_strtolower($fixedlabel)] = 1;
135a1603abdSAndreas Gohr        return $fixedlabel;
136a1603abdSAndreas Gohr    }
137a1603abdSAndreas Gohr
138a1603abdSAndreas Gohr    /**
1391c502704SAndreas Gohr     * Creates a new schema
1401c502704SAndreas Gohr     *
1411c502704SAndreas Gohr     * @todo use checksum or other heuristic to see if we really need a new schema OTOH we probably need one nearly always!?
1421c502704SAndreas Gohr     */
1431c502704SAndreas Gohr    protected function newSchema() {
1449d580426SAndreas Gohr        if(!$this->time) $this->time = time();
1459d580426SAndreas Gohr
146fa7b96aaSMichael Grosse        $sql = "INSERT INTO schemas (tbl, ts, user) VALUES (?, ?, ?)";
147*12fe9b2cSMichael Grosse        $this->sqlite->query($sql, $this->table, $this->time, $this->user);
1481c502704SAndreas Gohr        $res = $this->sqlite->query('SELECT last_insert_rowid()');
1491c502704SAndreas Gohr        $this->newschemaid = $this->sqlite->res2single($res);
1501c502704SAndreas Gohr        $this->sqlite->res_close($res);
1511c502704SAndreas Gohr        if(!$this->newschemaid) return false;
1521c502704SAndreas Gohr        return true;
1531c502704SAndreas Gohr    }
1541c502704SAndreas Gohr
1551c502704SAndreas Gohr    /**
1561c502704SAndreas Gohr     * Updates all the existing column infos and adds them to the new schema
1571c502704SAndreas Gohr     */
1581c502704SAndreas Gohr    protected function updateColumns() {
1591c502704SAndreas Gohr        foreach($this->oldschema->getColumns() as $column) {
1601c502704SAndreas Gohr            $oldEntry = $column->getType()->getAsEntry();
1611c502704SAndreas Gohr            $oldTid   = $column->getTid();
1621c502704SAndreas Gohr            $newEntry = $oldEntry;
1631c502704SAndreas Gohr            $newTid   = $oldTid;
1641c502704SAndreas Gohr            $sort = $column->getSort();
1651c502704SAndreas Gohr            if(isset($this->data['cols'][$column->getColref()])){
1661c502704SAndreas Gohr                // todo I'm not too happy with this hardcoded here - we should probably have a list of fields at one place
1671c502704SAndreas Gohr                $newEntry['config'] = $this->data['cols'][$column->getColref()]['config'];
1681c502704SAndreas Gohr                $newEntry['label'] = $this->data['cols'][$column->getColref()]['label'];
169b7a5ee93SMichael Große                $newEntry['ismulti'] = $this->data['cols'][$column->getColref()]['ismulti'];
1701c502704SAndreas Gohr                $newEntry['class'] = $this->data['cols'][$column->getColref()]['class'];
1711c502704SAndreas Gohr                $sort = $this->data['cols'][$column->getColref()]['sort'];
17226147f8cSAndreas Gohr                $enabled = (bool) $this->data['cols'][$column->getColref()]['isenabled'];
1731c502704SAndreas Gohr
1741c502704SAndreas Gohr                // when the type definition has changed, we create a new one
1751c502704SAndreas Gohr                if(array_diff_assoc($oldEntry, $newEntry)) {
1761c502704SAndreas Gohr                    $ok = $this->sqlite->storeEntry('types', $newEntry);
1771c502704SAndreas Gohr                    if(!$ok) return false;
1781c502704SAndreas Gohr                    $res = $this->sqlite->query('SELECT last_insert_rowid()');
1791c502704SAndreas Gohr                    if(!$res) return false;
1801c502704SAndreas Gohr                    $newTid = $this->sqlite->res2single($res);
1811c502704SAndreas Gohr                    $this->sqlite->res_close($res);
1821c502704SAndreas Gohr                }
1831c502704SAndreas Gohr            } else {
18426147f8cSAndreas Gohr                $enabled = false; // no longer there for some reason
1851c502704SAndreas Gohr            }
1861c502704SAndreas Gohr
1871c502704SAndreas Gohr            // add this type to the schema columns
1881c502704SAndreas Gohr            $schemaEntry = array(
1891c502704SAndreas Gohr                'sid' => $this->newschemaid,
1901c502704SAndreas Gohr                'colref' => $column->getColref(),
1911c502704SAndreas Gohr                'enabled' => $enabled,
1921c502704SAndreas Gohr                'tid' => $newTid,
1931c502704SAndreas Gohr                'sort' => $sort
1941c502704SAndreas Gohr            );
1951c502704SAndreas Gohr            $ok = $this->sqlite->storeEntry('schema_cols', $schemaEntry);
1961c502704SAndreas Gohr            if(!$ok) return false;
1971c502704SAndreas Gohr        }
1981c502704SAndreas Gohr        return true;
1991c502704SAndreas Gohr    }
2001c502704SAndreas Gohr
2011c502704SAndreas Gohr    /**
2021c502704SAndreas Gohr     * Adds new columns to the new schema
2031c502704SAndreas Gohr     *
2041c502704SAndreas Gohr     * @return bool
2051c502704SAndreas Gohr     */
2061c502704SAndreas Gohr    protected function addColumns() {
2071c502704SAndreas Gohr        if(!isset($this->data['new'])) return true;
2081c502704SAndreas Gohr
2091c502704SAndreas Gohr        $colref = count($this->oldschema->getColumns())+1;
2101c502704SAndreas Gohr
2111c502704SAndreas Gohr        foreach($this->data['new'] as $column) {
21226147f8cSAndreas Gohr            if(!$column['isenabled']) continue; // we do not add a disabled column
21326147f8cSAndreas Gohr
2141c502704SAndreas Gohr            // todo this duplicates the hardcoding as in  the function above
2151c502704SAndreas Gohr            $newEntry = array();
2161c502704SAndreas Gohr            $newEntry['config'] = $column['config'];
2171c502704SAndreas Gohr            $newEntry['label'] = $column['label'];
218b7a5ee93SMichael Große            $newEntry['ismulti'] = $column['ismulti'];
2191c502704SAndreas Gohr            $newEntry['class'] = $column['class'];
2201c502704SAndreas Gohr            $sort = $column['sort'];
22126147f8cSAndreas Gohr
2221c502704SAndreas Gohr
2236684d78dSAndreas Gohr            // only save if the column got a name
2246684d78dSAndreas Gohr            if(!$newEntry['label']) continue;
2256684d78dSAndreas Gohr
2266684d78dSAndreas Gohr            // add new column to the data table
2276684d78dSAndreas Gohr            if(!$this->addDataTableColumn($colref)) {
2286684d78dSAndreas Gohr                return false;
2296684d78dSAndreas Gohr            }
2306684d78dSAndreas Gohr
2311c502704SAndreas Gohr            // save the type
2321c502704SAndreas Gohr            $ok = $this->sqlite->storeEntry('types', $newEntry);
2331c502704SAndreas Gohr            if(!$ok) return false;
2341c502704SAndreas Gohr            $res = $this->sqlite->query('SELECT last_insert_rowid()');
2351c502704SAndreas Gohr            if(!$res) return false;
2361c502704SAndreas Gohr            $newTid = $this->sqlite->res2single($res);
2371c502704SAndreas Gohr            $this->sqlite->res_close($res);
2381c502704SAndreas Gohr
2391c502704SAndreas Gohr
2401c502704SAndreas Gohr            // add this type to the schema columns
2411c502704SAndreas Gohr            $schemaEntry = array(
2421c502704SAndreas Gohr                'sid' => $this->newschemaid,
2431c502704SAndreas Gohr                'colref' => $colref,
24426147f8cSAndreas Gohr                'enabled' => true,
2451c502704SAndreas Gohr                'tid' => $newTid,
2461c502704SAndreas Gohr                'sort' => $sort
2471c502704SAndreas Gohr            );
2481c502704SAndreas Gohr            $ok = $this->sqlite->storeEntry('schema_cols', $schemaEntry);
2491c502704SAndreas Gohr            if(!$ok) return false;
2501c502704SAndreas Gohr            $colref++;
2511c502704SAndreas Gohr        }
2521c502704SAndreas Gohr
2531c502704SAndreas Gohr        return true;
2541c502704SAndreas Gohr    }
2551c502704SAndreas Gohr
2561c502704SAndreas Gohr    /**
2570fe33e72SAndreas Gohr     * Create a completely new data table with no columns yet also create the appropriate
2580fe33e72SAndreas Gohr     * multi value table for the schema
2591c502704SAndreas Gohr     *
2601c502704SAndreas Gohr     * @todo how do we want to handle indexes?
2611c502704SAndreas Gohr     * @return bool
2621c502704SAndreas Gohr     */
2631c502704SAndreas Gohr    protected function newDataTable() {
2640fe33e72SAndreas Gohr        $ok = true;
2651c502704SAndreas Gohr
2660fe33e72SAndreas Gohr        $tbl = 'data_' . $this->table;
2671c502704SAndreas Gohr        $sql = "CREATE TABLE $tbl (
2681c502704SAndreas Gohr                    pid NOT NULL,
2696684d78dSAndreas Gohr                    rev INTEGER NOT NULL,
2707059e7e1SAndreas Gohr                    latest BOOLEAN NOT NULL DEFAULT 0,
2716684d78dSAndreas Gohr                    PRIMARY KEY(pid, rev)
2726684d78dSAndreas Gohr                )";
2730fe33e72SAndreas Gohr        $ok = $ok && (bool) $this->sqlite->query($sql);
2741c502704SAndreas Gohr
2750fe33e72SAndreas Gohr        $tbl = 'multi_' . $this->table;
2760fe33e72SAndreas Gohr        $sql = "CREATE TABLE $tbl (
2770fe33e72SAndreas Gohr                    colref INTEGER NOT NULL,
2780fe33e72SAndreas Gohr                    pid NOT NULL,
2790fe33e72SAndreas Gohr                    rev INTEGER NOT NULL,
2800fe33e72SAndreas Gohr                    row INTEGER NOT NULL,
2810fe33e72SAndreas Gohr                    value,
2820fe33e72SAndreas Gohr                    PRIMARY KEY(colref, pid, rev, row)
2830fe33e72SAndreas Gohr                );";
2840fe33e72SAndreas Gohr        $ok = $ok && (bool) $this->sqlite->query($sql);
2850fe33e72SAndreas Gohr
2860fe33e72SAndreas Gohr        return $ok;
2871c502704SAndreas Gohr    }
2881c502704SAndreas Gohr
2891c502704SAndreas Gohr    /**
2906684d78dSAndreas Gohr     * Add an additional column to the existing data table
2911c502704SAndreas Gohr     *
2926684d78dSAndreas Gohr     * @param int $index the new column index to add
2931c502704SAndreas Gohr     * @return bool
2941c502704SAndreas Gohr     */
2956684d78dSAndreas Gohr    protected function addDataTableColumn($index) {
2961c502704SAndreas Gohr        $tbl = 'data_' . $this->table;
2976684d78dSAndreas Gohr        $sql = " ALTER TABLE $tbl ADD COLUMN col$index DEFAULT ''";
2981c502704SAndreas Gohr        if(! $this->sqlite->query($sql)) {
2991c502704SAndreas Gohr            return false;
3001c502704SAndreas Gohr        }
3011c502704SAndreas Gohr        return true;
3021c502704SAndreas Gohr    }
3031c502704SAndreas Gohr
30478bff02fSMichael Grosse    /**
30578bff02fSMichael Grosse     * @param string $user
30678bff02fSMichael Grosse     * @return SchemaBuilder
30778bff02fSMichael Grosse     */
30878bff02fSMichael Grosse    public function setUser($user) {
30978bff02fSMichael Grosse        $this->user = $user;
31078bff02fSMichael Grosse        return $this;
31178bff02fSMichael Grosse    }
31278bff02fSMichael Grosse
31378bff02fSMichael Grosse
3141c502704SAndreas Gohr}
315