99
1010namespace Piwik \DataAccess ;
1111
12+ use Piwik \Common ;
1213use Piwik \Config ;
1314use Piwik \Container \StaticContainer ;
1415use Piwik \Db ;
2728 */
2829final class ArchiveBlobColumnType
2930{
31+ /**
32+ * The [database] config key used to signal that archive_blob tables may still contain
33+ * MEDIUMBLOB `value` columns (set by the 5.10.0-b1 migration on existing installs).
34+ */
35+ public const CONFIG_KEY = 'archive_blob_tables_may_contain_mediumblob ' ;
36+
3037 /**
3138 * Per-request cache: table name → isMediumBlob result.
3239 *
@@ -54,9 +61,21 @@ public static function isMediumBlob(string $tableName): bool
5461 [$ tableName , 'value ' ]
5562 );
5663
57- // MySQL returns the type in lowercase ('mediumblob', 'longblob'); use
58- // case-insensitive comparison so we are not surprised by unusual drivers.
59- $ result = (strtolower ((string ) $ columnType ) === 'mediumblob ' );
64+ $ normalized = strtolower ((string ) $ columnType );
65+ if ($ normalized === '' ) {
66+ // INFORMATION_SCHEMA returned no row: the table or column is missing.
67+ // This is unexpected (callers create the table before calling us) and
68+ // indicates a race condition or schema corruption. Apply the cap conservatively.
69+ StaticContainer::get (LoggerInterface::class)->warning (
70+ 'ArchiveBlobColumnType: INFORMATION_SCHEMA returned no row for table {table}; applying cap conservatively. ' ,
71+ ['table ' => $ tableName ]
72+ );
73+ $ result = true ;
74+ } else {
75+ // MySQL returns the type in lowercase ('mediumblob', 'longblob'); use
76+ // case-insensitive comparison so we are not surprised by unusual drivers.
77+ $ result = ($ normalized === 'mediumblob ' );
78+ }
6079 } catch (\Exception $ e ) {
6180 StaticContainer::get (LoggerInterface::class)->warning (
6281 'ArchiveBlobColumnType: could not determine column type for table {table}: {exception} ' ,
@@ -81,7 +100,8 @@ public static function clearCache(): void
81100 /**
82101 * Checks whether any archive_blob_* tables in the current schema still use MEDIUMBLOB for
83102 * their `value` column. If none remain, the
84- * `[database] archive_blob_tables_may_contain_mediumblob` config flag is removed.
103+ * `[database] archive_blob_tables_may_contain_mediumblob` ({@see CONFIG_KEY}) config flag is
104+ * removed.
85105 *
86106 * If the flag is not set (0 / unset) this method returns immediately without any I/O so that
87107 * fresh installs pay zero runtime cost during updates.
@@ -90,47 +110,57 @@ public static function clearCache(): void
90110 * - `core/Updater.php` after each component update finishes and at the end of a full batch.
91111 * - `plugins/CoreUpdater/Commands/RecheckArchiveBlobTypes.php` as an on-demand CLI tool.
92112 *
93- * @return bool `true` when MEDIUMBLOB tables were found (flag left as-is or not set),
94- * `false` when flag was unset because no MEDIUMBLOB tables remain.
113+ * @return string[] Names of archive_blob tables that still use MEDIUMBLOB.
114+ * An empty array means no MEDIUMBLOB tables remain and the flag was cleared.
115+ * A non-empty array means the flag was left as-is.
116+ * Returns an empty array (without I/O) when the flag was not set.
95117 */
96- public static function recheckAndUpdateFlag (): bool
118+ public static function recheckAndUpdateFlag (): array
97119 {
98- $ flag = (int ) (Config::getInstance ()->database [' archive_blob_tables_may_contain_mediumblob ' ] ?? 0 );
120+ $ flag = (int ) (Config::getInstance ()->database [self :: CONFIG_KEY ] ?? 0 );
99121 if ($ flag === 0 ) {
100122 // Nothing to do on fresh installs or after the flag was already cleared.
101- return false ;
123+ return [] ;
102124 }
103125
104126 $ mediumBlobTables = self ::getMediumBlobArchiveTables ();
105127 if (empty ($ mediumBlobTables )) {
106128 // All tables have been migrated (or none existed). Remove the flag.
107129 $ config = Config::getInstance ();
108130 $ database = $ config ->database ;
109- unset($ database [' archive_blob_tables_may_contain_mediumblob ' ]);
131+ unset($ database [self :: CONFIG_KEY ]);
110132 $ config ->database = $ database ;
111133 $ config ->forceSave ();
112- return false ;
113134 }
114135
115- return true ;
136+ return $ mediumBlobTables ;
116137 }
117138
118139 /**
119140 * Returns the names of all archive_blob_* tables in the current schema whose `value` column
120- * is MEDIUMBLOB.
141+ * is MEDIUMBLOB. Only tables whose name begins with the configured Matomo table prefix are
142+ * inspected, so tables belonging to other Matomo instances (or unrelated tables that happen to
143+ * contain "archive_blob_" in their name) are never returned.
121144 *
122145 * @return string[]
123146 */
124147 public static function getMediumBlobArchiveTables (): array
125148 {
149+ // Build a prefix-anchored LIKE pattern, e.g. "matomo_archive\_blob\_%".
150+ // LIKE-escape any '%' or '_' in the prefix itself so a weird table-prefix cannot
151+ // accidentally broaden the match.
152+ $ rawPrefix = Common::prefixTable ('archive_blob_ ' );
153+ $ likePrefix = str_replace (['\\' , '% ' , '_ ' ], ['\\\\' , '\\% ' , '\\_ ' ], $ rawPrefix );
154+ $ likePattern = $ likePrefix . '% ' ;
155+
126156 try {
127157 $ rows = Db::fetchAll (
128158 "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.COLUMNS
129159 WHERE TABLE_SCHEMA = DATABASE()
130160 AND TABLE_NAME LIKE ?
131161 AND COLUMN_NAME = ?
132162 AND LOWER(COLUMN_TYPE) = ? " ,
133- [' %archive\_blob\_% ' , 'value ' , 'mediumblob ' ]
163+ [$ likePattern , 'value ' , 'mediumblob ' ]
134164 );
135165 } catch (\Exception $ e ) {
136166 StaticContainer::get (LoggerInterface::class)->warning (
0 commit comments