From 3ae24aa915bcdce7766d329e571cf31c917f0d5d Mon Sep 17 00:00:00 2001 From: Jon Stovell Date: Sun, 16 Aug 2026 15:49:59 -0600 Subject: [PATCH 01/14] Ensures that Db::$db->update_from() returns a boolean Signed-off-by: Jon Stovell --- Sources/Db/APIs/MySQL.php | 4 +++- Sources/Db/APIs/PostgreSQL.php | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Sources/Db/APIs/MySQL.php b/Sources/Db/APIs/MySQL.php index d7a3e474977..7abd54f8a87 100644 --- a/Sources/Db/APIs/MySQL.php +++ b/Sources/Db/APIs/MySQL.php @@ -590,7 +590,7 @@ public function update_from(array $table, array $from_tables, string $set, strin return false; } - return $this->query( + $result = $this->query( 'UPDATE ' . $table['name'] . ' AS ' . $table['alias'] . ' ' . implode(' ', $joins) . ' @@ -599,6 +599,8 @@ public function update_from(array $table, array $from_tables, string $set, strin $db_values, $connection, ); + + return $result !== false; } /** diff --git a/Sources/Db/APIs/PostgreSQL.php b/Sources/Db/APIs/PostgreSQL.php index 6c4c0fa6a95..4beaeb384dd 100644 --- a/Sources/Db/APIs/PostgreSQL.php +++ b/Sources/Db/APIs/PostgreSQL.php @@ -595,7 +595,7 @@ public function update_from(array $table, array $from_tables, string $set, strin // PostgreSQL doesn't like prefixes on the columns to be set. $set = preg_replace('~\b' . $table['alias'] . '\.\b~', '', $set); - return $this->query( + $result = $this->query( 'UPDATE ' . $table['name'] . ' AS ' . $table['alias'] . ' SET ' . $set . ' FROM ' . implode(', ', $from) . (!empty($where) ? ' @@ -603,6 +603,8 @@ public function update_from(array $table, array $from_tables, string $set, strin $db_values, $connection, ); + + return $result !== false; } /** From e4ebe8a28500b65e41700b01d2d39289c6c2f784 Mon Sep 17 00:00:00 2001 From: Jon Stovell Date: Fri, 14 Aug 2026 21:34:16 -0600 Subject: [PATCH 02/14] Sets nulls to default value when changing default in PostgreSQL::change_column() Signed-off-by: Jon Stovell --- Sources/Db/APIs/PostgreSQL.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Sources/Db/APIs/PostgreSQL.php b/Sources/Db/APIs/PostgreSQL.php index 4beaeb384dd..59d46743c1c 100644 --- a/Sources/Db/APIs/PostgreSQL.php +++ b/Sources/Db/APIs/PostgreSQL.php @@ -1709,6 +1709,18 @@ public function change_column(string $table_name, string $old_column, array $col 'security_override' => true, ], ); + + // In PostgreSQL SET DEFAULT does not backfill existing rows, so do it manually. + if ($default !== 'NULL' && isset($column_info['not_null'])) { + $this->query( + 'UPDATE ' . $short_table_name . ' + SET ' . $column_info['name'] . ' = ' . $default . ' + WHERE ' . $column_info['name'] . ' IS NULL', + [ + 'security_override' => true, + ], + ); + } } // Is it null - or otherwise? From c18954673ab4975d24805a419760adea40ed7047 Mon Sep 17 00:00:00 2001 From: Jon Stovell Date: Fri, 14 Aug 2026 21:42:04 -0600 Subject: [PATCH 03/14] Respects `$column_info['not_null'] == false` in PostgreSQL::change_column() Signed-off-by: Jon Stovell --- Sources/Db/APIs/PostgreSQL.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Db/APIs/PostgreSQL.php b/Sources/Db/APIs/PostgreSQL.php index 59d46743c1c..beceb04cddb 100644 --- a/Sources/Db/APIs/PostgreSQL.php +++ b/Sources/Db/APIs/PostgreSQL.php @@ -1711,7 +1711,7 @@ public function change_column(string $table_name, string $old_column, array $col ); // In PostgreSQL SET DEFAULT does not backfill existing rows, so do it manually. - if ($default !== 'NULL' && isset($column_info['not_null'])) { + if ($default !== 'NULL' && !empty($column_info['not_null'])) { $this->query( 'UPDATE ' . $short_table_name . ' SET ' . $column_info['name'] . ' = ' . $default . ' @@ -1725,7 +1725,7 @@ public function change_column(string $table_name, string $old_column, array $col // Is it null - or otherwise? // Just go ahead & honor the setting. Type changes above introduce defaults that we might need to override here... - if (isset($column_info['not_null'])) { + if (!empty($column_info['not_null'])) { $action = 'SET NOT NULL'; } else { $action = 'DROP NOT NULL'; From b4a489986a6b15c1f63ccaa92cda3527d1480a3f Mon Sep 17 00:00:00 2001 From: Jon Stovell Date: Fri, 14 Aug 2026 22:23:13 -0600 Subject: [PATCH 04/14] Retains true name of primary key index in PostgreSQL::list_indexes() Signed-off-by: Jon Stovell --- Sources/Db/APIs/PostgreSQL.php | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Sources/Db/APIs/PostgreSQL.php b/Sources/Db/APIs/PostgreSQL.php index beceb04cddb..bd335612a00 100644 --- a/Sources/Db/APIs/PostgreSQL.php +++ b/Sources/Db/APIs/PostgreSQL.php @@ -2194,12 +2194,7 @@ public function list_indexes(string $table_name, bool $detail = false, array $pa $columns[$k] = trim($v); } - // Fix up the name to be consistent cross databases - if (str_ends_with($row['name'], '_pkey') && $row['is_primary'] == 1) { - $row['name'] = 'PRIMARY'; - } else { - $row['name'] = str_replace($real_table_name . '_', '', $row['name']); - } + $row['name'] = str_replace($real_table_name . '_', '', $row['name']); if (!$detail) { $indexes[] = $row['name']; From c034962c2227680d89c7b690015dd05bcab0d699 Mon Sep 17 00:00:00 2001 From: Jon Stovell Date: Fri, 14 Aug 2026 22:00:23 -0600 Subject: [PATCH 05/14] Leaves primary keys alone in Table::fixIndexName() Signed-off-by: Jon Stovell --- Sources/Db/Schema/Table.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Sources/Db/Schema/Table.php b/Sources/Db/Schema/Table.php index 73e2e75bfb0..446a1edba51 100644 --- a/Sources/Db/Schema/Table.php +++ b/Sources/Db/Schema/Table.php @@ -463,13 +463,22 @@ public function fixIndexName(DbIndex $index): bool continue; } + // There's no need to rename the primary key. + if ($index->type === 'primary' && $existing_index['type'] === 'primary') { + return true; + } + // If the name is already the same, there's nothing to do. if ($index->name === $existing_index['name']) { return true; } // Do the rename. - return Db::$db->rename_index('{db_prefix}' . $this->name, $existing_index['name'], $index->name); + return Db::$db->rename_index( + table_name: '{db_prefix}' . $this->name, + old_name: $existing_index['name'], + new_name: $index->name, + ); } // No matching index was found. From a9813f7b1d368dc02093b38b8a2f21a3935b2878 Mon Sep 17 00:00:00 2001 From: Jon Stovell Date: Fri, 14 Aug 2026 22:58:56 -0600 Subject: [PATCH 06/14] Only uses ON CONFLICT for columns with unique indexes in PostgreSQL::insert() Signed-off-by: Jon Stovell --- Sources/Db/APIs/PostgreSQL.php | 48 +++++++++++++++++++++++++--------- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/Sources/Db/APIs/PostgreSQL.php b/Sources/Db/APIs/PostgreSQL.php index bd335612a00..cfff5c9bb08 100644 --- a/Sources/Db/APIs/PostgreSQL.php +++ b/Sources/Db/APIs/PostgreSQL.php @@ -433,23 +433,45 @@ public function insert(string $method, string $table, array $columns, array $dat // PostgreSQL doesn't support replace: we implement a MySQL-compatible behavior instead if ($method == 'replace' || $method == 'ignore') { - $key_str = implode(',', $keys); - $col_str = ''; - $count = 0; + // The columns in an ON CONFLICT statement must exactly match the columns + // of some primary or unique index. + $possibly_conflicting_columns = []; + $column_names = array_keys($columns); - // Make a list of the non-pk fields. - foreach ($columns as $columnName => $type) { - if (!\in_array($columnName, $keys) && ($method == 'replace')) { - $col_str .= ($count > 0 ? ',' : ''); - $col_str .= $columnName . ' = EXCLUDED.' . $columnName; - $count++; + foreach ($this->list_indexes($table, true) as $index) { + if ( + // Skip if not a primary or unique index. + !\in_array($index['type'], ['primary', 'unique']) + // Skip if some of the columns in this index are not being inserted into. + || array_intersect($index['columns'], $column_names) !== $index['columns'] + // Prefer the primary index over others. + || ($index['type'] !== 'primary' && !empty($possibly_conflicting_columns)) + ) { + continue; } + + $possibly_conflicting_columns = $index['columns']; } - if ($method == 'replace') { - $replace = ' ON CONFLICT (' . $key_str . ') DO UPDATE SET ' . $col_str; - } else { - $replace = ' ON CONFLICT (' . $key_str . ') DO NOTHING'; + if (!empty($possibly_conflicting_columns)) { + $key_str = implode(',', $possibly_conflicting_columns); + $col_str = ''; + $count = 0; + + // Make a list of the non-pk fields. + foreach ($columns as $column_name => $type) { + if (!\in_array($column_name, $keys) && ($method == 'replace')) { + $col_str .= ($count > 0 ? ',' : ''); + $col_str .= $column_name . ' = EXCLUDED.' . $column_name; + $count++; + } + } + + if ($method == 'replace') { + $replace = ' ON CONFLICT (' . $key_str . ') DO UPDATE SET ' . $col_str; + } else { + $replace = ' ON CONFLICT (' . $key_str . ') DO NOTHING'; + } } } From 428d138e71287986edee03a2105263d059ef0026 Mon Sep 17 00:00:00 2001 From: Jon Stovell Date: Fri, 14 Aug 2026 23:38:12 -0600 Subject: [PATCH 07/14] Uses Db::$db->updateFrom() in AlertsObsolete migration Previously, we were using a bare query that only worked for MySQL. This code works for both MySQL and PostgreSQL. Signed-off-by: Jon Stovell --- .../Migration/v2_1/AlertsObsolete.php | 39 +++++++++++++------ 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/Sources/Maintenance/Migration/v2_1/AlertsObsolete.php b/Sources/Maintenance/Migration/v2_1/AlertsObsolete.php index bf7bae504fa..7e88d3e551e 100644 --- a/Sources/Maintenance/Migration/v2_1/AlertsObsolete.php +++ b/Sources/Maintenance/Migration/v2_1/AlertsObsolete.php @@ -15,6 +15,7 @@ namespace SMF\Maintenance\Migration\v2_1; +use SMF\Db\DatabaseApi as Db; use SMF\Maintenance\Migration\MigrationBase; class AlertsObsolete extends MigrationBase @@ -104,17 +105,33 @@ public function execute(): bool $this->handleTimeout(); - $this->query( - 'UPDATE {db_prefix}user_alerts AS a - JOIN {db_prefix}attachments AS f - ON (f.id_attach = a.content_id) - SET - a.content_type = {literal:msg}, - a.content_action = {literal:unapproved_attachment}, - a.content_id = f.id_msg - WHERE content_type = {literal:unapproved} - AND content_action = {literal:attachment}', - [], + Db::$db->update_from( + table: [ + 'name' => '{db_prefix}user_alerts', + 'alias' => 'a', + ], + from_tables: [ + [ + 'name' => '{db_prefix}attachments', + 'alias' => 'f', + 'condition' => 'f.id_attach = a.content_id', + ], + ], + set: implode(', ', [ + 'a.content_type = {string:new_type}', + 'a.content_action = {string:new_action}', + 'a.content_id = f.id_msg', + ]), + where: implode(' AND ', [ + 'content_type = {string:old_type}', + 'content_action = {string:old_action}', + ]), + db_values: [ + 'new_type' => 'msg', + 'new_action' => 'unapproved_attachment', + 'old_type' => 'unapproved', + 'old_action' => 'attachment', + ], ); $this->handleTimeout(); From 16a6fbbaf917cba1232a36066dd3786a7a278572 Mon Sep 17 00:00:00 2001 From: Jon Stovell Date: Fri, 14 Aug 2026 23:51:21 -0600 Subject: [PATCH 08/14] Performs intended changes in PostgreSqlSchemaDiff migration Signed-off-by: Jon Stovell --- Sources/Maintenance/Migration/v2_1/PostgreSqlSchemaDiff.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Maintenance/Migration/v2_1/PostgreSqlSchemaDiff.php b/Sources/Maintenance/Migration/v2_1/PostgreSqlSchemaDiff.php index b1d91af1e41..19d7a5ca1fa 100644 --- a/Sources/Maintenance/Migration/v2_1/PostgreSqlSchemaDiff.php +++ b/Sources/Maintenance/Migration/v2_1/PostgreSqlSchemaDiff.php @@ -129,7 +129,7 @@ public function isCandidate(): bool public function execute(): bool { while (Maintenance::getCurrentStart() < \count($this->schema_fixes)) { - $fix = $this->schemaFixes[Maintenance::getCurrentStart()]; + $fix = $this->schema_fixes[Maintenance::getCurrentStart()]; $this->query( 'ALTER TABLE {db_prefix}' . $fix[0] . ' From c403208fcd1b0ec5da00676545af89293098a842 Mon Sep 17 00:00:00 2001 From: Jon Stovell Date: Sun, 16 Aug 2026 15:56:32 -0600 Subject: [PATCH 09/14] Fixes defaults for two columns in the calendar table Signed-off-by: Jon Stovell --- Sources/Db/Schema/v3_0/Calendar.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Db/Schema/v3_0/Calendar.php b/Sources/Db/Schema/v3_0/Calendar.php index e46e3e7af6f..53ba0fef0a4 100644 --- a/Sources/Db/Schema/v3_0/Calendar.php +++ b/Sources/Db/Schema/v3_0/Calendar.php @@ -511,13 +511,13 @@ public function __construct() name: 'rdates', type: 'text', not_null: true, - default: null, + default: '', ), 'exdates' => new Column( name: 'exdates', type: 'text', not_null: true, - default: null, + default: '', ), 'adjustments' => new Column( name: 'adjustments', From 70f4cee146b8fa493fb3b13f6b5ebf5ea97a2085 Mon Sep 17 00:00:00 2001 From: Jon Stovell Date: Sun, 16 Aug 2026 18:04:36 -0600 Subject: [PATCH 10/14] Correctly handles primary key in PostgreSQL::remove_index() Signed-off-by: Jon Stovell --- Sources/Db/APIs/MySQL.php | 7 +++++ Sources/Db/APIs/PostgreSQL.php | 52 ++++++++++++++++++---------------- 2 files changed, 34 insertions(+), 25 deletions(-) diff --git a/Sources/Db/APIs/MySQL.php b/Sources/Db/APIs/MySQL.php index 7abd54f8a87..cbac5a1c0ad 100644 --- a/Sources/Db/APIs/MySQL.php +++ b/Sources/Db/APIs/MySQL.php @@ -2278,6 +2278,13 @@ public function remove_index(string $table_name, string $index_name, array $para { $short_table_name = str_replace('{db_prefix}', $this->prefix, $table_name); + // The list_indexes() method will report the name of the primary key as + // 'primary' on MySQL and 'pkey' on PostgreSQL. If we were handed the + // name for the wrong database engine, fix it. + if ($index_name === 'pkey') { + $index_name = 'primary'; + } + // Better exist! $indexes = $this->list_indexes($table_name, true); diff --git a/Sources/Db/APIs/PostgreSQL.php b/Sources/Db/APIs/PostgreSQL.php index cfff5c9bb08..86774103252 100644 --- a/Sources/Db/APIs/PostgreSQL.php +++ b/Sources/Db/APIs/PostgreSQL.php @@ -2279,37 +2279,39 @@ public function remove_index(string $table_name, string $index_name, array $para $parsed_table_name = str_replace('{db_prefix}', $this->prefix, $table_name); $real_table_name = preg_match('~^(`?)(.+?)\\1\\.(.*?)$~', $parsed_table_name, $match) === 1 ? $match[3] : $parsed_table_name; + // The list_indexes() method will report the name of the primary key as + // 'primary' on MySQL and 'pkey' on PostgreSQL. If we were handed the + // name for the wrong database engine, fix it. + if ($index_name === 'primary') { + $index_name = 'pkey'; + } + // Better exist! $indexes = $this->list_indexes($table_name, true); - // Do not add the table name to the index if it is already there. - if ($index_name != 'primary' && str_contains($index_name, $real_table_name)) { - $index_name = str_replace($real_table_name . '_', '', $index_name); - } + // The list_indexes() method removes the table name from the names of + // the indexes, so make sure to do the same to $index_name. + $index_name = str_replace($real_table_name . '_', '', $index_name); foreach ($indexes as $index) { - // If the name is primary we want the primary key! - if ($index['type'] == 'primary' && $index_name == 'primary') { - // Dropping primary key? - $result = $this->query( - 'ALTER TABLE ' . $real_table_name . ' - DROP CONSTRAINT ' . $index['name'], - [ - 'security_override' => true, - ], - ); - - return $result !== false; - } + if ($index['name'] === $index_name) { + if ($index['type'] == 'primary') { + $result = $this->query( + 'ALTER TABLE ' . $real_table_name . ' + DROP CONSTRAINT ' . $real_table_name . '_' . $index['name'], + [ + 'security_override' => true, + ], + ); + } else { + $result = $this->query( + 'DROP INDEX ' . $real_table_name . '_' . $index['name'], + [ + 'security_override' => true, + ], + ); - if ($index['name'] == $index_name) { - // Drop the bugger... - $result = $this->query( - 'DROP INDEX ' . $real_table_name . '_' . $index_name, - [ - 'security_override' => true, - ], - ); + } return $result !== false; } From 7f8ee8fdbebf9b3fe7744e644569501a84587c9c Mon Sep 17 00:00:00 2001 From: Jon Stovell Date: Fri, 21 Aug 2026 20:37:46 -0600 Subject: [PATCH 11/14] Uses `ON CONFLICT (...) DO NOTHING` when there's nothing to set Signed-off-by: Jon Stovell --- Sources/Db/APIs/PostgreSQL.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Sources/Db/APIs/PostgreSQL.php b/Sources/Db/APIs/PostgreSQL.php index 86774103252..9d07ec5e9a8 100644 --- a/Sources/Db/APIs/PostgreSQL.php +++ b/Sources/Db/APIs/PostgreSQL.php @@ -460,14 +460,17 @@ public function insert(string $method, string $table, array $columns, array $dat // Make a list of the non-pk fields. foreach ($columns as $column_name => $type) { - if (!\in_array($column_name, $keys) && ($method == 'replace')) { + if ( + !\in_array($column_name, $possibly_conflicting_columns) + && $method == 'replace' + ) { $col_str .= ($count > 0 ? ',' : ''); $col_str .= $column_name . ' = EXCLUDED.' . $column_name; $count++; } } - if ($method == 'replace') { + if ($method == 'replace' && !empty($col_str)) { $replace = ' ON CONFLICT (' . $key_str . ') DO UPDATE SET ' . $col_str; } else { $replace = ' ON CONFLICT (' . $key_str . ') DO NOTHING'; From f05514fde2433d368c90d5a25bf7eb3a410e3cc9 Mon Sep 17 00:00:00 2001 From: Jon Stovell Date: Fri, 21 Aug 2026 21:01:40 -0600 Subject: [PATCH 12/14] Removes opclass suffix from column names in PostgreSQL::list_indexes() Signed-off-by: Jon Stovell --- Sources/Db/APIs/PostgreSQL.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/Db/APIs/PostgreSQL.php b/Sources/Db/APIs/PostgreSQL.php index 9d07ec5e9a8..be59b5a53a0 100644 --- a/Sources/Db/APIs/PostgreSQL.php +++ b/Sources/Db/APIs/PostgreSQL.php @@ -2216,9 +2216,11 @@ public function list_indexes(string $table_name, bool $detail = false, array $pa } foreach ($columns as $k => $v) { - $columns[$k] = trim($v); + // Remove the opclass suffix, if present. + $columns[$k] = preg_replace('/\s+\w+_ops$/', '', trim($v)); } + // We only want the basic column name. $row['name'] = str_replace($real_table_name . '_', '', $row['name']); if (!$detail) { From ea2ee77878eb4f2ac1fc731486a4c5c5fcbe2b9b Mon Sep 17 00:00:00 2001 From: Jon Stovell Date: Sat, 22 Aug 2026 18:08:07 -0600 Subject: [PATCH 13/14] Caches values of PostgreSQL::list_indexes() for efficiency Signed-off-by: Jon Stovell --- Sources/Db/APIs/PostgreSQL.php | 37 ++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/Sources/Db/APIs/PostgreSQL.php b/Sources/Db/APIs/PostgreSQL.php index be59b5a53a0..103e109e043 100644 --- a/Sources/Db/APIs/PostgreSQL.php +++ b/Sources/Db/APIs/PostgreSQL.php @@ -135,6 +135,13 @@ class PostgreSQL extends DatabaseApi implements DatabaseApiInterface */ protected $connect_errno; + /** + * @var array + * + * Cache for list_indexes() method. + */ + private array $index_cache = []; + /**************** * Public methods ****************/ @@ -1387,6 +1394,8 @@ public function add_column(string $table_name, array $column_info, array $parame return $this->change_column($table_name, $column_info['name'], $column_info); } + unset($this->index_cache[$short_table_name]); + return $result !== false; } @@ -1462,6 +1471,8 @@ public function add_index(string $table_name, array $index_info, array $paramete ); } + unset($this->index_cache[$parsed_table_name]); + // Query returns a result or true if successful, false otherwise. return $result !== false; } @@ -1764,6 +1775,8 @@ public function change_column(string $table_name, string $old_column, array $col ], ); + unset($this->index_cache[$short_table_name]); + return true; } @@ -1788,6 +1801,8 @@ public function rename_index(string $table_name, string $old_name, string $new_n ); } + unset($this->index_cache[$parsed_table_name]); + return $result !== false; } @@ -1838,6 +1853,8 @@ public function create_table(string $table_name, array $columns, array $indexes } } + unset($this->index_cache[$short_table_name]); + // If we've got this far - good news - no table exists. We can build our own! if (!$db_trans) { $this->transaction('begin'); @@ -2021,6 +2038,8 @@ public function drop_table(string $table_name, array $parameters = [], string $e $tables = $this->list_tables($database); if (\in_array($full_table_name, $tables)) { + unset($this->index_cache[$short_table_name]); + // We can then drop the table. $this->transaction('begin'); @@ -2090,6 +2109,8 @@ public function rename_table(string $old_name, string $new_name, bool $allowed_r return false; } + unset($this->index_cache[$short_old_name]); + $result = $this->query( 'ALTER TABLE ' . $short_old_name . ' RENAME TO ' . $short_new_name, [ @@ -2188,6 +2209,10 @@ public function list_indexes(string $table_name, bool $detail = false, array $pa $real_table_name = preg_match('~^(`?)(.+?)\\1\\.(.*?)$~', $parsed_table_name, $match) === 1 ? $match[3] : $parsed_table_name; $database = !empty($match[2]) ? $match[2] : $this->name; + if (isset($this->index_cache[$parsed_table_name][$detail ? 'detail' : 'simple'])) { + return $this->index_cache[$parsed_table_name][$detail ? 'detail' : 'simple']; + } + $result = $this->query( 'SELECT CASE WHEN i.indisprimary THEN 1 ELSE 0 END AS is_primary, CASE WHEN i.indisunique THEN 1 ELSE 0 END AS is_unique, @@ -2233,8 +2258,16 @@ public function list_indexes(string $table_name, bool $detail = false, array $pa ]; } } + $this->free_result($result); + if ($detail) { + $this->index_cache[$parsed_table_name]['detail'] = $indexes; + $this->index_cache[$parsed_table_name]['simple'] = array_keys($indexes); + } else { + $this->index_cache[$parsed_table_name]['simple'] = $indexes; + } + return $indexes; } @@ -2245,6 +2278,8 @@ public function remove_column(string $table_name, string $column_name, array $pa { $short_table_name = str_replace('{db_prefix}', $this->prefix, $table_name); + unset($this->index_cache[$short_table_name]); + // Does it exist? $columns = $this->list_columns($table_name, true); @@ -2284,6 +2319,8 @@ public function remove_index(string $table_name, string $index_name, array $para $parsed_table_name = str_replace('{db_prefix}', $this->prefix, $table_name); $real_table_name = preg_match('~^(`?)(.+?)\\1\\.(.*?)$~', $parsed_table_name, $match) === 1 ? $match[3] : $parsed_table_name; + unset($this->index_cache[$parsed_table_name]); + // The list_indexes() method will report the name of the primary key as // 'primary' on MySQL and 'pkey' on PostgreSQL. If we were handed the // name for the wrong database engine, fix it. From a707a09437e4516b980de01c865f3598703f1e54 Mon Sep 17 00:00:00 2001 From: Jon Stovell Date: Sat, 22 Aug 2026 18:16:23 -0600 Subject: [PATCH 14/14] Checks index type, not name, in SearchResultsPrimaryKey::isCandidate() Signed-off-by: Jon Stovell --- .../v3_0/SearchResultsPrimaryKey.php | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/Sources/Maintenance/Migration/v3_0/SearchResultsPrimaryKey.php b/Sources/Maintenance/Migration/v3_0/SearchResultsPrimaryKey.php index 3f4381c0e6d..fb3ed536e9d 100644 --- a/Sources/Maintenance/Migration/v3_0/SearchResultsPrimaryKey.php +++ b/Sources/Maintenance/Migration/v3_0/SearchResultsPrimaryKey.php @@ -48,9 +48,17 @@ public function isCandidate(): bool $table = new Schema\v3_0\LogSearchResults(); $existing_structure = $table->getCurrentStructure(); - $idx = $existing_structure['indexes']['primary'] ?? null; - - return $idx == null || array_intersect($idx['columns'], self::$columns) !== []; + foreach ($existing_structure['indexes'] as $idx) { + if ($idx['type'] === 'primary') { + break; + } + } + + return ( + !isset($idx) + || $idx['type'] !== 'primary' + || $idx['columns'] !== self::$columns + ); } /** @@ -63,7 +71,10 @@ public function execute(): bool $this->handleTimeout(); - Db::$db->add_index('{db_prefix}' . $table->name, ['type' => 'primary', 'columns' => self::$columns]); + Db::$db->add_index( + '{db_prefix}' . $table->name, + ['type' => 'primary', 'columns' => self::$columns], + ); return true; }