diff --git a/migrations/version_2_0_30.php b/migrations/version_2_0_30.php index 1c75cb1..b3fa7fa 100644 --- a/migrations/version_2_0_30.php +++ b/migrations/version_2_0_30.php @@ -32,8 +32,15 @@ public function add_permission(): void $this->db->sql_freeresult($result); if (!$exists) { + // An explicit id, like the options version_1_0_0 inserted: those + // left PostgreSQL's sequence at 1, so letting it pick one fails + // with a duplicate key on a fresh PostgreSQL board + $result = $this->db->sql_query('SELECT MAX(auth_option_id) AS max_id FROM ' . $this->table_prefix . 'kb_options'); + $next_id = (int) $this->db->sql_fetchfield('max_id') + 1; + $this->db->sql_freeresult($result); + $this->db->sql_query('INSERT INTO ' . $this->table_prefix . 'kb_options ' . $this->db->sql_build_array('INSERT', [ - 'auth_option' => 'kb_m_change_author', 'is_global' => 0, 'is_local' => 1, + 'auth_option_id' => $next_id, 'auth_option' => 'kb_m_change_author', 'is_global' => 0, 'is_local' => 1, ])); } } diff --git a/migrations/version_2_0_31.php b/migrations/version_2_0_31.php index 93d615f..8fea559 100644 --- a/migrations/version_2_0_31.php +++ b/migrations/version_2_0_31.php @@ -55,6 +55,28 @@ public function rebuild_match_index(): void return; } + // On PostgreSQL, version_1_0_3 created the index as plain un_mtch, but + // phpBB 3.3's tools find it by that name and then drop + // {table}_un_mtch, which does not exist. Drop whichever is there by + // its real name instead. + if ($this->db->get_sql_layer() === 'postgres' && !($this->db_tools instanceof \phpbb\db\tools\doctrine)) + { + // Only this table's: another table may own an index called un_mtch + $result = $this->db->sql_query("SELECT indexname + FROM pg_indexes + WHERE tablename = '" . $this->db->sql_escape($table) . "' + AND " . $this->db->sql_in_set('indexname', ['un_mtch', $table . '_un_mtch'])); + $indexes = array_column($this->db->sql_fetchrowset($result), 'indexname'); + $this->db->sql_freeresult($result); + + foreach ($indexes as $index) + { + $this->db->sql_query('DROP INDEX ' . $index); + } + $this->db_tools->sql_create_unique_index($table, 'un_mtch', self::MATCH_INDEX_COLUMNS); + return; + } + // phpBB 4.0 stores the index under the shortened table name, for example // kbsrcwrdmtch_un_mtch, and only finds it by that full name $stored_name = 'un_mtch'; diff --git a/tests/kb_options_id_migration_test.php b/tests/kb_options_id_migration_test.php new file mode 100644 index 0000000..9fd67ad --- /dev/null +++ b/tests/kb_options_id_migration_test.php @@ -0,0 +1,167 @@ +tools instanceof \phpbb\db\tools\doctrine) $this->tools->get_connection()->close(); + if ($this->db) $this->db->sql_close(); + if ($this->sqlite_fixture) @unlink($this->sqlite_fixture); + } + + public function engines() + { + return [['sqlite'], ['mysql'], ['postgres']]; + } + + private function connect(string $engine): void + { + if ($engine === 'sqlite') + { + $this->db = new \phpbb\db\driver\sqlite3(); + if (class_exists(\phpbb\db\tools\doctrine::class)) + { + $this->sqlite_fixture = tempnam(sys_get_temp_dir(), 'kb-options-'); + $this->assertTrue($this->db->sql_connect($this->sqlite_fixture, '', '', '')); + $this->tools = new \phpbb\db\tools\doctrine(\phpbb\db\doctrine\connection_factory::get_connection_from_params('sqlite3', $this->sqlite_fixture)); + $this->tools->set_table_prefix('phpbb_'); + } + else + { + $this->assertTrue($this->db->sql_connect(':memory:', '', '', '')); + $this->tools = new \phpbb\db\tools\sqlite3($this->db); + } + return; + } + + $port = getenv('KB_TEST_' . strtoupper($engine) . '_PORT'); + if (!$port) $this->markTestSkipped('Set the fixture database port to run integration tests'); + $this->assertTrue(ctype_digit($port)); + $this->db = $engine === 'mysql' ? new \phpbb\db\driver\mysqli() : new \phpbb\db\driver\postgres(); + $this->assertIsObject($this->db->sql_connect('127.0.0.1', $engine === 'mysql' ? 'root' : 'postgres', '', 'kb_fixture', (int) $port)); + if (class_exists(\phpbb\db\tools\doctrine::class)) + { + $this->tools = new \phpbb\db\tools\doctrine(\phpbb\db\doctrine\connection_factory::get_connection_from_params( + $engine === 'mysql' ? 'mysqli' : 'postgres', '127.0.0.1', $engine === 'mysql' ? 'root' : 'postgres', '', 'kb_fixture', $port + )); + $this->tools->set_table_prefix('phpbb_'); + } + else + { + $this->tools = $engine === 'mysql' ? new \phpbb\db\tools\tools($this->db) : new \phpbb\db\tools\postgres($this->db); + } + } + + /** @dataProvider engines */ + public function test_the_author_permission_is_added_after_the_first_options_on_a_fresh_board($engine) + { + $this->connect($engine); + foreach (['phpbb_kb_options', 'phpbb_kb_users', 'phpbb_kb_groups'] as $table) + { + if ($this->tools->sql_table_exists($table)) $this->tools->sql_table_drop($table); + if ($this->db->get_sql_layer() === 'postgres') $this->db->sql_query('DROP SEQUENCE IF EXISTS ' . $table . '_seq'); + } + + // The options table and its first seven options, exactly as version_1_0_0 makes them + $config = new \phpbb\config\config([]); + $first = new version_1_0_0($config, $this->db, $this->tools, '', 'php', 'phpbb_', []); + $this->tools->sql_create_table('phpbb_kb_options', $first->update_schema()['add_tables']['phpbb_kb_options']); + $first->update_kb_options_table(); + + (new version_2_0_30($config, $this->db, $this->tools, '', 'php', 'phpbb_', []))->add_permission(); + + $result = $this->db->sql_query('SELECT auth_option_id, auth_option FROM phpbb_kb_options ORDER BY auth_option_id'); + $options = []; + while ($row = $this->db->sql_fetchrow($result)) + { + $options[(int) $row['auth_option_id']] = $row['auth_option']; + } + $this->db->sql_freeresult($result); + + $this->assertSame('kb_m_change_author', $options[8] ?? null); + $this->assertCount(8, $options); + + // Running it again adds nothing + (new version_2_0_30($config, $this->db, $this->tools, '', 'php', 'phpbb_', []))->add_permission(); + $result = $this->db->sql_query('SELECT COUNT(*) AS total FROM phpbb_kb_options'); + $this->assertSame(8, (int) $this->db->sql_fetchfield('total')); + $this->db->sql_freeresult($result); + + $this->tools->sql_table_drop('phpbb_kb_options'); + if ($this->db->get_sql_layer() === 'postgres') $this->db->sql_query('DROP SEQUENCE IF EXISTS phpbb_kb_options_seq'); + } + + /** @dataProvider engines */ + public function test_the_search_match_index_is_rebuilt_after_version_1_0_3_made_it($engine) + { + $this->connect($engine); + $table = 'phpbb_kb_src_wrdmtch'; + foreach ([$table, 'phpbb_kb_articles'] as $name) + { + if ($this->tools->sql_table_exists($name)) $this->tools->sql_table_drop($name); + } + // version_1_0_3 also drops a kb_articles column + $this->tools->sql_create_table('phpbb_kb_articles', ['COLUMNS' => ['article_id' => ['UINT', 0], 'bbcode_bitfield' => ['VCHAR:32', '']]]); + + // The table as version_1_0_0, 1_0_3 (schema and data), 2_0_2 and 2_0_15 leave it + $config = new \phpbb\config\config([]); + $migration = fn($class) => new $class($config, $this->db, $this->tools, '', 'php', 'phpbb_', []); + $this->tools->sql_create_table($table, $migration(version_1_0_0::class)->update_schema()['add_tables'][$table]); + $this->tools->perform_schema_changes($migration(version_1_0_3::class)->update_schema()); + $migration(version_1_0_3::class)->add_key(); + $this->tools->perform_schema_changes($migration(version_2_0_2::class)->update_schema()); + $this->tools->perform_schema_changes($migration(version_2_0_15::class)->update_schema()); + $this->db->sql_query('INSERT INTO ' . $table . ' (article_id, word_id, title_match, descr_match) VALUES (1, 1, 0, 1)'); + + $migration(version_2_0_31::class)->rebuild_match_index(); + // Run again (an enable retried): still fine + $migration(version_2_0_31::class)->rebuild_match_index(); + + // The match survives, and the four-column unique key holds + $result = $this->db->sql_query('SELECT COUNT(*) AS total FROM ' . $table); + $this->assertSame(1, (int) $this->db->sql_fetchfield('total')); + $this->db->sql_freeresult($result); + $this->db->sql_return_on_error(true); + $duplicate = $this->db->sql_query('INSERT INTO ' . $table . ' (article_id, word_id, title_match, descr_match) VALUES (1, 1, 0, 1)'); + $this->db->sql_return_on_error(false); + $this->assertFalse($duplicate, 'a duplicate match should be refused by the unique index'); + $this->assertNotFalse($this->db->sql_query('INSERT INTO ' . $table . ' (article_id, word_id, title_match, descr_match) VALUES (1, 1, 0, 0)')); + + $this->tools->sql_table_drop($table); + $this->tools->sql_table_drop('phpbb_kb_articles'); + } +}