PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 2.2.0
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v2.2.0
2.2.0 2.1.0 2.0.15 2.0.12 2.0.10 2.0.4 2.0.1 2.0.0 1.95.3 1.95.2 1.95 1.91.6 trunk 1.11 1.12 1.13 1.20 1.21 1.22 1.23 1.30 1.31 1.32 1.35 1.40 All 43 releases
fluent-boards / app / Services / IndexUpgradeService.php

IndexUpgradeService.php in FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration 2.2.0, at app/Services/IndexUpgradeService.php

144 lines 6.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentBoards\App\Services;
4
5 use FluentBoards\App\App;
6 use FluentBoards\App\Models\BoardTerm;
7 use FluentBoards\App\Models\Relation;
8 use FluentBoards\App\Models\Task;
9 use RuntimeException;
10 use Throwable;
11 use wpdb;
12
13 /** Add lookup indexes without replaying legacy column/data migrations. */
14 class IndexUpgradeService
15 {
16 const VERSION = 1;
17 const VERSION_OPTION = '_fluent_boards_index_schema_version';
18
19 /** Retry incomplete upgrades on an administrator visit; never commit caller transactions. */
20 public function maybeUpgrade()
21 {
22 if (!current_user_can('manage_options') || App::getInstance('db')->inTransaction()) {
23 return false;
24 }
25 if ((int) get_option(self::VERSION_OPTION, 0) >= self::VERSION) {
26 return true;
27 }
28
29 global $wpdb;
30 // Do not bypass a database drop-in's routing with an unmanaged connection.
31 if (get_class($wpdb) !== 'wpdb') {
32 error_log('FluentBoards index upgrade: custom database driver requires a managed schema upgrade.');
33 return false;
34 }
35 $database = null;
36 $lock = 'fbs_indexes_' . md5($wpdb->dbname . ':' . $wpdb->prefix);
37 $locked = false;
38 $lockWait = null;
39 try {
40 // DDL on the request handle would commit raw/other-ORM transactions.
41 $database = new class($wpdb->dbuser, $wpdb->dbpassword, $wpdb->dbname, $wpdb->dbhost) extends wpdb {
42 public function db_connect($allow_bail = true)
43 {
44 return parent::db_connect(false);
45 }
46
47 /** Stop this attempt: reconnecting loses its lock and session timeout. */
48 public function check_connection($allow_bail = true)
49 {
50 $this->ready = false;
51 $this->last_error = 'Schema connection lost; retry on a later request.';
52 return false;
53 }
54
55 public function bail($message, $error_code = '500')
56 {
57 // Connection/select failures must not terminate the admin request.
58 $this->ready = false;
59 return false;
60 }
61 };
62 $database->prefix = $wpdb->prefix;
63 $database->suppress_errors(true);
64 if (!$database->ready) {
65 throw new RuntimeException('Could not open an isolated schema connection.');
66 }
67 // Connection-owned locks survive long ALTERs and release on disconnect.
68 $locked = (int) $database->get_var($database->prepare('SELECT GET_LOCK(%s, 0)', $lock)) === 1;
69 if (!$locked) {
70 return false;
71 }
72 if ((int) get_option(self::VERSION_OPTION, 0) >= self::VERSION) {
73 return true;
74 }
75
76 $lockWait = $database->get_var('SELECT @@SESSION.lock_wait_timeout');
77 if (!is_numeric($lockWait) || $database->query('SET SESSION lock_wait_timeout = 1') === false) {
78 throw new RuntimeException('Could not limit index metadata lock waits.');
79 }
80
81 $indexes = [
82 [(new BoardTerm())->getTable(), 'fbs_board_type', ['board_id', 'type']],
83 [(new Task())->getTable(), 'fbs_stage_id', ['stage_id']],
84 [(new Task())->getTable(), 'fbs_parent_id', ['parent_id']],
85 [(new Relation())->getTable(), 'fbs_object_type_id', ['object_type', 'object_id']],
86 ];
87 foreach ($indexes as [$table, $name, $columns]) {
88 $table = $database->prefix . $table;
89 if ($this->hasIndex($database, $table, $columns)) {
90 continue;
91 }
92 $quotedTable = '`' . str_replace('`', '``', $table) . '`';
93 $quotedColumns = '`' . implode('`, `', $columns) . '`';
94 // Unsupported engines remain pending; never fall back to blocking COPY.
95 $sql = "ALTER TABLE $quotedTable ADD INDEX `$name` ($quotedColumns), ALGORITHM=INPLACE, LOCK=NONE";
96 if ($database->query($sql) === false || !$this->hasIndex($database, $table, $columns)) {
97 throw new RuntimeException('Index ' . $name . ' on ' . $table . ' remains pending: ' . $database->last_error);
98 }
99 }
100
101 update_option(self::VERSION_OPTION, self::VERSION, false);
102 return (int) get_option(self::VERSION_OPTION, 0) >= self::VERSION;
103 } catch (Throwable $e) {
104 error_log('FluentBoards index upgrade: ' . sanitize_text_field($e->getMessage()));
105 return false;
106 } finally {
107 if ($database) {
108 if ($database->ready && is_numeric($lockWait)) {
109 $database->query($database->prepare('SET SESSION lock_wait_timeout = %d', (int) $lockWait));
110 }
111 if ($database->ready && $locked) {
112 $database->get_var($database->prepare('SELECT RELEASE_LOCK(%s)', $lock));
113 }
114 $database->close();
115 }
116 }
117 }
118
119 /** An existing full-column BTREE left prefix also satisfies the lookup. */
120 private function hasIndex($database, $table, array $columns)
121 {
122 $quotedTable = '`' . str_replace('`', '``', $table) . '`';
123 $rows = $database->get_results("SHOW INDEX FROM $quotedTable", ARRAY_A);
124 if ($database->last_error) {
125 throw new RuntimeException('Could not inspect ' . $table . ': ' . $database->last_error);
126 }
127
128 $indexes = [];
129 foreach ($rows as $row) {
130 if (strtoupper($row['Index_type']) !== 'BTREE' || ($row['Visible'] ?? 'YES') === 'NO' || ($row['Ignored'] ?? 'NO') === 'YES') {
131 continue;
132 }
133 $indexes[$row['Key_name']][(int) $row['Seq_in_index']] = $row['Sub_part'] === null ? $row['Column_name'] : null;
134 }
135 foreach ($indexes as $index) {
136 ksort($index);
137 if (array_slice(array_values($index), 0, count($columns)) === $columns) {
138 return true;
139 }
140 }
141 return false;
142 }
143 }
144