xref: /plugin/struct/meta/Assignments.php (revision d486d6d7d6c607c391a45aa431842e8fbc9d74c3)
1<?php
2
3namespace plugin\struct\meta;
4
5/**
6 * Class Assignments
7 *
8 * Manages the assignment of schemas (table names) to pages and namespaces
9 *
10 * @package plugin\struct\meta
11 */
12class Assignments {
13
14    /** @var \helper_plugin_sqlite|null */
15    protected $sqlite;
16
17    /** @var  array All the assignments patterns */
18    protected $patterns;
19
20    /**
21     * Assignments constructor.
22     */
23    public function __construct() {
24        /** @var \helper_plugin_struct_db $helper */
25        $helper = plugin_load('helper', 'struct_db');
26        $this->sqlite = $helper->getDB();
27
28        if($this->sqlite) $this->loadPatterns();
29    }
30
31    /**
32     * Load existing assignment patterns
33     */
34    protected function loadPatterns() {
35        $sql = 'SELECT * FROM schema_assignments_patterns ORDER BY pattern';
36        $res = $this->sqlite->query($sql);
37        $this->patterns = $this->sqlite->res2arr($res);
38        $this->sqlite->res_close($res);
39    }
40
41    /**
42     * Add a new assignment pattern to the pattern table
43     *
44     * @param string $pattern
45     * @param string $table
46     * @return bool
47     */
48    public function addPattern($pattern, $table) {
49        // add the pattern
50        $sql = 'REPLACE INTO schema_assignments_patterns (pattern, tbl) VALUES (?,?)';
51        $ok = (bool) $this->sqlite->query($sql, array($pattern, $table));
52
53        // reload patterns
54        $this->loadPatterns();
55
56        // fetch all pages where the schema isn't assigned, yet
57        $sql = 'SELECT pid FROM schema_assignments WHERE tbl != ? OR assigned != 1';
58        $res = $this->sqlite->query($sql, $table);
59        $pagerows = $this->sqlite->res2arr($res);
60        $this->sqlite->res_close($res);
61
62        // reevalute the pages and assign when needed
63        foreach($pagerows as $row) {
64            $tables = $this->getPageAssignments($row['pid'], true);
65            if(in_array($table, $tables)) {
66                $this->assignPageSchema($row['pid'], $table);
67            }
68        }
69
70        return $ok;
71    }
72
73    /**
74     * Remove an existing assignment pattern from the pattern table
75     *
76     * @param string $pattern
77     * @param string $table
78     * @return bool
79     */
80    public function removePattern($pattern, $table) {
81        // remove the pattern
82        $sql = 'DELETE FROM schema_assignments_patterns WHERE pattern = ? AND tbl = ?';
83        $ok = (bool) $this->sqlite->query($sql, array($pattern, $table));
84
85        // reload patterns
86        $this->loadPatterns();
87
88        // fetch possibly affected pages
89        $sql = 'SELECT pid FROM schema_assignments WHERE tbl = ?';
90        $res = $this->sqlite->query($sql, $table);
91        $pagerows = $this->sqlite->res2arr($res);
92        $this->sqlite->res_close($res);
93
94        // reevalute the pages and unassign when needed
95        foreach($pagerows as $row) {
96            $tables = $this->getPageAssignments($row['pid'], true);
97            if(!in_array($table, $tables)) {
98                $this->deassignPageSchema($row['pid'], $table);
99            }
100        }
101
102        return $ok;
103    }
104
105    /**
106     * Rechecks all assignments of a given page against the current patterns
107     *
108     * @param string $pid
109     */
110    public function reevaluatePageAssignments($pid) {
111        // reload patterns
112        $this->loadPatterns();
113        $tables = $this->getPageAssignments($pid, true);
114
115        // fetch possibly affected tables
116        $sql = 'SELECT tbl FROM schema_assignments WHERE pid = ?';
117        $res = $this->sqlite->query($sql, $pid);
118        $tablerows = $this->sqlite->res2arr($res);
119        $this->sqlite->res_close($res);
120
121        // reevalute the tables and apply assignments
122        foreach($tablerows as $row) {
123            if(in_array($row['tbl'], $tables)) {
124                $this->assignPageSchema($pid, $row['tbl']);
125            } else {
126                $this->deassignPageSchema($pid, $row['tbl']);
127            }
128        }
129    }
130
131    /**
132     * Clear all patterns - deassigns all pages
133     *
134     * This is mostly useful for testing and not used in the interface currently
135     *
136     * @param bool $full fully delete all previous assignments
137     * @return bool
138     */
139    public function clear($full=false) {
140        $sql = 'DELETE FROM schema_assignments_patterns';
141        $ok = (bool) $this->sqlite->query($sql);
142
143        if($full) {
144            $sql = 'DELETE FROM schema_assignments';
145        } else {
146            $sql = 'UPDATE schema_assignments SET assigned = 0';
147        }
148        $ok = $ok && (bool) $this->sqlite->query($sql);
149
150        // reload patterns
151        $this->loadPatterns();
152
153        return $ok;
154    }
155
156    /**
157     * Add page to assignments
158     *
159     * @param string $page
160     * @param string $table
161     * @return bool
162     */
163    public function assignPageSchema($page, $table) {
164        $sql = 'REPLACE INTO schema_assignments (pid, tbl, assigned) VALUES (?, ?, 1)';
165        return (bool) $this->sqlite->query($sql, array($page, $table));
166    }
167
168    /**
169     * Remove page from assignments
170     *
171     * @param string $page
172     * @param string $table
173     * @return bool
174     */
175    public function deassignPageSchema($page, $table) {
176        $sql = 'REPLACE INTO schema_assignments (pid, tbl, assigned) VALUES (?, ?, 0)';
177        return (bool) $this->sqlite->query($sql, array($page, $table));
178    }
179
180    /**
181     * Get the whole pattern table
182     *
183     * @return array
184     */
185    public function getAllPatterns() {
186        return $this->patterns;
187    }
188
189    /**
190     * Returns a list of table names assigned to the given page
191     *
192     * @param string $page
193     * @param bool $checkpatterns Should the current patterns be re-evaluated?
194     * @return \string[] tables assigned
195     */
196    public function getPageAssignments($page, $checkpatterns=true) {
197        $tables = array();
198        $page = cleanID($page);
199
200        if($checkpatterns) {
201            // evaluate patterns
202            $pns = ':' . getNS($page) . ':';
203            foreach($this->patterns as $row) {
204                if($this->matchPagePattern($row['pattern'], $page, $pns)) {
205                    $tables[] = $row['tbl'];
206                }
207            }
208        } else {
209            // just select
210            $sql = 'SELECT tbl FROM schema_assignments WHERE pid = ? AND assigned = 1';
211            $res = $this->sqlite->query($sql, array($page));
212            $list = $this->sqlite->res2arr($res);
213            $this->sqlite->res_close($res);
214            foreach($list as $row) {
215                $tables[] = $row['tbl'];
216            }
217        }
218
219        return array_unique($tables);
220    }
221
222    /**
223     * Get the pages known to struct and their assignment state
224     *
225     * @param null|string $schema limit results to the given schema
226     * @param bool $assignedonly limit results to currently assigned only
227     * @return array
228     */
229    public function getPages($schema = null, $assignedonly = false) {
230        $sql = 'SELECT pid, tbl, assigned FROM schema_assignments WHERE 1=1';
231
232        $opts = array();
233        if($schema) {
234            $sql .= ' AND tbl = ?';
235            $opts[] = $schema;
236        }
237        if($assignedonly) {
238            $sql .= ' AND assigned = 1';
239        }
240
241        $sql .= ' ORDER BY pid, tbl';
242
243        $res = $this->sqlite->query($sql, $opts);
244        $list = $this->sqlite->res2arr($res);
245        $this->sqlite->res_close($res);
246
247        $result = array();
248        foreach($list as $row) {
249            $pid = $row['pid'];
250            $tbl = $row['tbl'];
251            if(!isset($result[$pid])) $result[$pid] = array();
252            $result[$pid][$tbl] = (bool) $row['assigned'];
253        }
254
255        return $result;
256    }
257
258    /**
259     * Check if the given pattern matches the given page
260     *
261     * @param string $pattern the pattern to check against
262     * @param string $page the cleaned pageid to check
263     * @param string|null $pns optimization, the colon wrapped namespace of the page, set null for automatic
264     * @return bool
265     */
266    protected function matchPagePattern($pattern, $page, $pns = null) {
267        if(trim($pattern,':') == '**') return true; // match all
268
269        // regex patterns
270        if($pattern{0} == '/') {
271            return (bool) preg_match($pattern, ":$page");
272        }
273
274        if(is_null($pns)) {
275            $pns = ':' . getNS($page) . ':';
276        }
277
278        $ans = ':' . cleanID($pattern) . ':';
279        if(substr($pattern, -2) == '**') {
280            // upper namespaces match
281            if(strpos($pns, $ans) === 0) {
282                return true;
283            }
284        } else if(substr($pattern, -1) == '*') {
285            // namespaces match exact
286            if($ans == $pns) {
287                return true;
288            }
289        } else {
290            // exact match
291            if(cleanID($pattern) == $page) {
292                return true;
293            }
294        }
295
296        return false;
297    }
298
299    /**
300     * Returns all tables of schemas that existed and stored data for the page back then
301     *
302     * @deprecated because we're always only interested in the current state of affairs, even when restoring.
303     *
304     * @param string $page
305     * @param string $ts
306     * @return array
307     */
308    public function getHistoricAssignments($page, $ts) {
309        $sql = "SELECT DISTINCT tbl FROM schemas WHERE ts <= ? ORDER BY ts DESC";
310        $res = $this->sqlite->query($sql, $ts);
311        $tables = $this->sqlite->res2arr($res);
312        $this->sqlite->res_close($res);
313
314        $assigned = array();
315        foreach($tables as $row) {
316            $table = $row['tbl'];
317            /** @noinspection SqlResolve */
318            $sql = "SELECT pid FROM data_$table WHERE pid = ? AND rev <= ? LIMIT 1";
319            $res = $this->sqlite->query($sql, $page, $ts);
320            $found = $this->sqlite->res2arr($res);
321            $this->sqlite->res_close($res);
322
323            if($found) $assigned[] = $table;
324        }
325
326        return $assigned;
327    }
328}
329