From 3d81f3e55fbb56521e8406c99e55c27cfb56712a Mon Sep 17 00:00:00 2001 From: tijmen Date: Fri, 8 May 2026 20:23:34 +0200 Subject: [PATCH 01/21] rename meta key to private meta key --- src/config/class-tiny-config.php | 2 +- test/unit/TinyImageTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/config/class-tiny-config.php b/src/config/class-tiny-config.php index dfab8f81..6378578c 100644 --- a/src/config/class-tiny-config.php +++ b/src/config/class-tiny-config.php @@ -9,5 +9,5 @@ class Tiny_Config { const SHRINK_URL = 'https://api.tinify.com/shrink'; const KEYS_URL = 'https://api.tinify.com/keys'; const MONTHLY_FREE_COMPRESSIONS = 500; - const META_KEY = 'tiny_compress_images'; + const META_KEY = '_tiny_compress_images'; } diff --git a/test/unit/TinyImageTest.php b/test/unit/TinyImageTest.php index f70f3b4a..114e17c8 100644 --- a/test/unit/TinyImageTest.php +++ b/test/unit/TinyImageTest.php @@ -17,7 +17,7 @@ public function set_up() { } public function test_tiny_post_meta_key_may_never_change() { - $this->assertEquals( '61b16225f107e6f0a836bf19d47aa0fd912f8925', sha1( Tiny_Config::META_KEY ) ); + $this->assertEquals( '438fc52ce17b9aedf0cf70dea52d5551affba59a', sha1( Tiny_Config::META_KEY ) ); } public function test_update_wp_metadata_should_not_update_with_no_resized_original() { From 5d99e3a2cff029ff3a39f974671aa16c757ca87a Mon Sep 17 00:00:00 2001 From: tijmen Date: Fri, 8 May 2026 20:23:57 +0200 Subject: [PATCH 02/21] add migration 370 --- src/class-tiny-migrate.php | 95 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 src/class-tiny-migrate.php diff --git a/src/class-tiny-migrate.php b/src/class-tiny-migrate.php new file mode 100644 index 00000000..3c3940f5 --- /dev/null +++ b/src/class-tiny-migrate.php @@ -0,0 +1,95 @@ +=' ) ) { + return; + } + + if ( version_compare( $stored_version, '3.7.0', '<' ) ) { + self::migrate_370(); + } + + update_option( self::DB_VERSION_OPTION, self::DB_VERSION ); + } + + /** + * Migrates the tiny meta key from public to private. + * + * Renames all `tiny_compress_images` post meta entries to + * `_tiny_compress_images`. + * + * @since 3.7.0 + * + * @return void + */ + private static function migrate_370() { + global $wpdb; + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching + $wpdb->update( + $wpdb->postmeta, + array( 'meta_key' => '_tiny_compress_images' ), + array( 'meta_key' => 'tiny_compress_images' ), + array( '%s' ), + array( '%s' ) + ); + } +} From 159d5e7730d276658749ab098525aa5688957880 Mon Sep 17 00:00:00 2001 From: tijmen Date: Fri, 8 May 2026 20:24:29 +0200 Subject: [PATCH 03/21] run migration on plugin load --- tiny-compress-images.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tiny-compress-images.php b/tiny-compress-images.php index e6aa9cad..a1412f47 100644 --- a/tiny-compress-images.php +++ b/tiny-compress-images.php @@ -10,6 +10,7 @@ */ require dirname( __FILE__ ) . '/src/config/class-tiny-config.php'; +require dirname( __FILE__ ) . '/src/class-tiny-migrate.php'; require dirname( __FILE__ ) . '/src/class-tiny-helpers.php'; require dirname( __FILE__ ) . '/src/class-tiny-php.php'; require dirname( __FILE__ ) . '/src/class-tiny-wp-base.php'; @@ -37,6 +38,8 @@ require dirname( __FILE__ ) . '/src/class-tiny-compress-fopen.php'; } +Tiny_Migrate::run(); + $tiny_plugin = new Tiny_Plugin(); register_uninstall_hook( From 7cc6dde6f3dc8e9e2f41e5273033c36bc9f6f4f5 Mon Sep 17 00:00:00 2001 From: tijmen Date: Fri, 15 May 2026 08:53:12 +0200 Subject: [PATCH 04/21] add migration tests --- test/helpers/wordpress.php | 7 +++- test/unit/TinyMigrateTest.php | 66 +++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 test/unit/TinyMigrateTest.php diff --git a/test/helpers/wordpress.php b/test/helpers/wordpress.php index 45e2406d..a441056b 100644 --- a/test/helpers/wordpress.php +++ b/test/helpers/wordpress.php @@ -56,6 +56,7 @@ class WordPressStubs private $calls; private $stubs; private $filters; + public $postmeta = 'wp_postmeta'; public function __construct($vfs) { @@ -98,6 +99,7 @@ public function __construct($vfs) $this->addMethod('get_locale'); $this->addMethod('wp_timezone_string'); $this->addMethod('update_option'); + $this->addMethod('update'); $this->addMethod('check_ajax_referer'); $this->addMethod('wp_json_encode'); $this->addMethod('wp_send_json_error'); @@ -144,7 +146,10 @@ public function call($method, $args) } // Allow explicit stubs to override defaults/behaviors if (isset($this->stubs[$method]) && $this->stubs[$method]) { - return call_user_func_array($this->stubs[$method], $args); + if (is_callable($this->stubs[$method])) { + return call_user_func_array($this->stubs[$method], $args); + } + return $this->stubs[$method]; } if ('add_filter' === $method) { $tag = isset($args[0]) ? $args[0] : ''; diff --git a/test/unit/TinyMigrateTest.php b/test/unit/TinyMigrateTest.php new file mode 100644 index 00000000..20f91983 --- /dev/null +++ b/test/unit/TinyMigrateTest.php @@ -0,0 +1,66 @@ +wp->postmeta = 'wp_postmeta'; + $this->wp->stub('update', 1); + } + + /** + * Helper to check if a specific option update occurred. + */ + private function assertOptionWasUpdated($option, $value) + { + $calls = $this->wp->getCalls('update_option'); + foreach ($calls as $call) { + if (($call[0] ?? null) === $option && ($call[1] ?? null) === $value) { + return $this->assertTrue(true); + } + } + $this->fail("Failed asserting that option '$option' was updated to '$value'."); + } + + public function test_run_skips_migration_when_db_version_is_current() + { + $this->wp->addOption(Tiny_Migrate::DB_VERSION_OPTION, Tiny_Migrate::DB_VERSION); + + Tiny_Migrate::run(); + + $this->assertCount(0, $this->wp->getCalls('update'), 'Should not touch DB if version matches.'); + } + + public function test_run_performs_migration_and_updates_version() + { + Tiny_Migrate::run(); + + $update_calls = $this->wp->getCalls('update'); + $this->assertCount(1, $update_calls); + + list($table, $data, $where) = $update_calls[0]; + + $this->assertEquals('wp_postmeta', $table); + $this->assertEquals(['meta_key' => '_tiny_compress_images'], $data); + $this->assertEquals(['meta_key' => 'tiny_compress_images'], $where); + + $this->assertOptionWasUpdated(Tiny_Migrate::DB_VERSION_OPTION, Tiny_Migrate::DB_VERSION); + } + + public function test_run_does_not_update_option_if_unnecessary() + { + $this->wp->addOption(Tiny_Migrate::DB_VERSION_OPTION, Tiny_Migrate::DB_VERSION); + + Tiny_Migrate::run(); + + $option_calls = $this->wp->getCalls('update_option'); + $version_updates = array_filter($option_calls, fn($call) => $call[0] === Tiny_Migrate::DB_VERSION_OPTION); + + $this->assertEmpty($version_updates, 'Should not re-save the version if already current.'); + } +} From 662a5260e2e1c7a66d0fb93c758a5f9894b709a0 Mon Sep 17 00:00:00 2001 From: tijmen Date: Fri, 15 May 2026 09:28:20 +0200 Subject: [PATCH 05/21] Change version to plain counter --- src/class-tiny-migrate.php | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/class-tiny-migrate.php b/src/class-tiny-migrate.php index 3c3940f5..7a2e8143 100644 --- a/src/class-tiny-migrate.php +++ b/src/class-tiny-migrate.php @@ -29,13 +29,15 @@ class Tiny_Migrate { /** - * The target version for the database. Not the same as plug-in version. - * This will be incremented on every new migration. + * The current database schema version. + * + * Increment this integer by 1 each time a new migration is added. + * This is independent of the plugin version. * * @since 3.7.0 - * @var string + * @var int */ - const DB_VERSION = '3.7.0'; + const DB_VERSION = 1; /** * WordPress option key used to track the applied database version. @@ -57,13 +59,13 @@ class Tiny_Migrate { * @return void */ public static function run() { - $stored_version = get_option( self::DB_VERSION_OPTION, '0' ); + $stored_version = (int) get_option( self::DB_VERSION_OPTION, 0 ); - if ( version_compare( $stored_version, self::DB_VERSION, '>=' ) ) { + if ( $stored_version >= self::DB_VERSION ) { return; } - if ( version_compare( $stored_version, '3.7.0', '<' ) ) { + if ( $stored_version < 1 ) { self::migrate_370(); } From ab4f772d23c989057f01fecda090a10a66fe40d4 Mon Sep 17 00:00:00 2001 From: tijmen Date: Fri, 15 May 2026 09:29:11 +0200 Subject: [PATCH 06/21] Rename migration --- src/class-tiny-migrate.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/class-tiny-migrate.php b/src/class-tiny-migrate.php index 7a2e8143..1e313f2a 100644 --- a/src/class-tiny-migrate.php +++ b/src/class-tiny-migrate.php @@ -32,7 +32,6 @@ class Tiny_Migrate { * The current database schema version. * * Increment this integer by 1 each time a new migration is added. - * This is independent of the plugin version. * * @since 3.7.0 * @var int @@ -66,7 +65,7 @@ public static function run() { } if ( $stored_version < 1 ) { - self::migrate_370(); + self::migrate_meta_key_to_private(); } update_option( self::DB_VERSION_OPTION, self::DB_VERSION ); @@ -82,7 +81,7 @@ public static function run() { * * @return void */ - private static function migrate_370() { + private static function migrate_meta_key_to_private() { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching From ace767f42665baf3d99c1dc7e559f86a836d8d45 Mon Sep 17 00:00:00 2001 From: tijmen Date: Fri, 15 May 2026 09:29:50 +0200 Subject: [PATCH 07/21] exit if migration fails --- src/class-tiny-migrate.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/class-tiny-migrate.php b/src/class-tiny-migrate.php index 1e313f2a..43a59264 100644 --- a/src/class-tiny-migrate.php +++ b/src/class-tiny-migrate.php @@ -85,12 +85,16 @@ private static function migrate_meta_key_to_private() { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching - $wpdb->update( + $result = $wpdb->update( $wpdb->postmeta, array( 'meta_key' => '_tiny_compress_images' ), array( 'meta_key' => 'tiny_compress_images' ), array( '%s' ), array( '%s' ) ); + + if ( false === $result ) { + return false; + } } } From d70b46c478f2457ae441217ccc4458263c25d2d1 Mon Sep 17 00:00:00 2001 From: tijmen Date: Fri, 15 May 2026 09:57:49 +0200 Subject: [PATCH 08/21] consistent return value --- src/class-tiny-migrate.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/class-tiny-migrate.php b/src/class-tiny-migrate.php index 43a59264..bcf6016e 100644 --- a/src/class-tiny-migrate.php +++ b/src/class-tiny-migrate.php @@ -79,7 +79,7 @@ public static function run() { * * @since 3.7.0 * - * @return void + * @return boolean */ private static function migrate_meta_key_to_private() { global $wpdb; @@ -96,5 +96,7 @@ private static function migrate_meta_key_to_private() { if ( false === $result ) { return false; } + + return true; } } From 27bfcc7cea51097ad27eebdc63f4f4fee113013c Mon Sep 17 00:00:00 2001 From: tijmen Date: Fri, 15 May 2026 10:10:19 +0200 Subject: [PATCH 09/21] add changelog entry --- readme.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/readme.txt b/readme.txt index c2b7dc6f..486c9306 100644 --- a/readme.txt +++ b/readme.txt @@ -174,6 +174,9 @@ A: You can upgrade to a paid account by adding your *Payment details* on your [a A: When the conversion feature is enabled (to convert images to AVIF or WebP), each image will use double the number of credits: one for compression and one for format conversion. == Changelog == += 3.7.0 = +* chore: migrated meta key from `tiny_compress_images` to `_tiny_compress_images` + = 3.6.14 = * fix: added check for valid path before deleting converted image * fix: use hook uninstall_plugin instead of uninstall.php to prevent dependency deletion From 5a5faaca8fc883071d5c2fdef76b5dfa5c72c742 Mon Sep 17 00:00:00 2001 From: tijmen Date: Fri, 15 May 2026 10:19:15 +0200 Subject: [PATCH 10/21] cancel migration if it errored --- src/class-tiny-migrate.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/class-tiny-migrate.php b/src/class-tiny-migrate.php index bcf6016e..d3142559 100644 --- a/src/class-tiny-migrate.php +++ b/src/class-tiny-migrate.php @@ -64,8 +64,8 @@ public static function run() { return; } - if ( $stored_version < 1 ) { - self::migrate_meta_key_to_private(); + if ( $stored_version < 1 && ! self::migrate_meta_key_to_private() ) { + return; } update_option( self::DB_VERSION_OPTION, self::DB_VERSION ); From bde636885a4f05db93dd71c9a35179feb68c7431 Mon Sep 17 00:00:00 2001 From: tijmen Date: Fri, 15 May 2026 10:19:23 +0200 Subject: [PATCH 11/21] remove redundent set --- test/unit/TinyMigrateTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/test/unit/TinyMigrateTest.php b/test/unit/TinyMigrateTest.php index 20f91983..8488bbab 100644 --- a/test/unit/TinyMigrateTest.php +++ b/test/unit/TinyMigrateTest.php @@ -9,7 +9,6 @@ class Tiny_Migrate_Test extends Tiny_TestCase public function set_up() { parent::set_up(); - $this->wp->postmeta = 'wp_postmeta'; $this->wp->stub('update', 1); } From 48f2c24f1c1b5d6ff66082b4219bc8c3cab2b255 Mon Sep 17 00:00:00 2001 From: tijmen Date: Fri, 15 May 2026 11:14:32 +0200 Subject: [PATCH 12/21] move migration to plugins_loaded hook --- tiny-compress-images.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tiny-compress-images.php b/tiny-compress-images.php index a1412f47..8846f882 100644 --- a/tiny-compress-images.php +++ b/tiny-compress-images.php @@ -38,7 +38,7 @@ require dirname( __FILE__ ) . '/src/class-tiny-compress-fopen.php'; } -Tiny_Migrate::run(); +add_action( 'plugins_loaded', array( 'Tiny_Migrate', 'run' ) ); $tiny_plugin = new Tiny_Plugin(); From 669853ef6df550ee06d66b6b81bdc836b896f2e5 Mon Sep 17 00:00:00 2001 From: tijmen Date: Fri, 15 May 2026 13:14:28 +0200 Subject: [PATCH 13/21] refer to the const instead of option --- src/class-tiny-migrate.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/class-tiny-migrate.php b/src/class-tiny-migrate.php index d3142559..b4fb4d44 100644 --- a/src/class-tiny-migrate.php +++ b/src/class-tiny-migrate.php @@ -22,7 +22,7 @@ * Handles sequential database migrations for the TinyPNG plugin. * * Each migration method targets a specific version and is only executed - * once per site, tracked via the `tinypng_db_version` option. + * once per site, tracked via the `DB_VERSION_OPTION` constant. * * @since 3.7.0 */ From da54abb30ec261910c319f5b38274192baa34123 Mon Sep 17 00:00:00 2001 From: tijmen Date: Fri, 15 May 2026 13:17:45 +0200 Subject: [PATCH 14/21] comment for clarity where nothing to migration is positive --- src/class-tiny-migrate.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/class-tiny-migrate.php b/src/class-tiny-migrate.php index b4fb4d44..e42d287a 100644 --- a/src/class-tiny-migrate.php +++ b/src/class-tiny-migrate.php @@ -97,6 +97,8 @@ private static function migrate_meta_key_to_private() { return false; } - return true; + // A return value of 0 means there was nothing to migrate, which is valid + // for fresh installs or databases that were already migrated. + return false !== $result;; } } From c53595891c1d7128a07436edfa9ce71eb8932deb Mon Sep 17 00:00:00 2001 From: tijmen Date: Fri, 15 May 2026 13:17:52 +0200 Subject: [PATCH 15/21] add test to validate migration failure --- test/unit/TinyMigrateTest.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/unit/TinyMigrateTest.php b/test/unit/TinyMigrateTest.php index 8488bbab..5b902d9e 100644 --- a/test/unit/TinyMigrateTest.php +++ b/test/unit/TinyMigrateTest.php @@ -51,6 +51,18 @@ public function test_run_performs_migration_and_updates_version() $this->assertOptionWasUpdated(Tiny_Migrate::DB_VERSION_OPTION, Tiny_Migrate::DB_VERSION); } + public function test_run_does_not_update_db_version_when_migration_fails() + { + $this->wp->stub('update', function() { return false; }); + + Tiny_Migrate::run(); + + $option_calls = $this->wp->getCalls('update_option'); + $version_updates = array_filter($option_calls, fn($call) => $call[0] === Tiny_Migrate::DB_VERSION_OPTION); + + $this->assertEmpty($version_updates, 'Should not update DB version when migration fails.'); + } + public function test_run_does_not_update_option_if_unnecessary() { $this->wp->addOption(Tiny_Migrate::DB_VERSION_OPTION, Tiny_Migrate::DB_VERSION); From 044ea9a15d0fe0f8929432261ea18bd7f852181c Mon Sep 17 00:00:00 2001 From: tijmen Date: Fri, 15 May 2026 13:29:55 +0200 Subject: [PATCH 16/21] create an order list of migrations --- src/class-tiny-migrate.php | 23 +++++++++++++++++++++-- test/unit/TinyMigrateTest.php | 10 +++++----- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/class-tiny-migrate.php b/src/class-tiny-migrate.php index e42d287a..f850ab30 100644 --- a/src/class-tiny-migrate.php +++ b/src/class-tiny-migrate.php @@ -46,6 +46,23 @@ class Tiny_Migrate { */ const DB_VERSION_OPTION = 'tinypng_db_version'; + /** + * Returns an ordered map of migrations keyed by version number. + * + * Each entry maps a version integer to a callable that performs the + * corresponding migration. Add new entries in ascending version order. + * Increment `DB_VERSION` when adding a new migration. + * + * @since 3.7.0 + * + * @return array Ordered map of version to migration callable. + */ + private static function migrations() { + return array( + 1 => array( self::class, 'migrate_meta_key_to_private' ), + ); + } + /** * Runs all pending migrations in version order. * @@ -64,8 +81,10 @@ public static function run() { return; } - if ( $stored_version < 1 && ! self::migrate_meta_key_to_private() ) { - return; + foreach ( self::migrations() as $version => $migration ) { + if ( $stored_version < $version && ! call_user_func( $migration ) ) { + return; + } } update_option( self::DB_VERSION_OPTION, self::DB_VERSION ); diff --git a/test/unit/TinyMigrateTest.php b/test/unit/TinyMigrateTest.php index 5b902d9e..1f2c80d0 100644 --- a/test/unit/TinyMigrateTest.php +++ b/test/unit/TinyMigrateTest.php @@ -19,7 +19,7 @@ private function assertOptionWasUpdated($option, $value) { $calls = $this->wp->getCalls('update_option'); foreach ($calls as $call) { - if (($call[0] ?? null) === $option && ($call[1] ?? null) === $value) { + if (isset($call[0], $call[1]) && $call[0] === $option && $call[1] === $value) { return $this->assertTrue(true); } } @@ -45,8 +45,8 @@ public function test_run_performs_migration_and_updates_version() list($table, $data, $where) = $update_calls[0]; $this->assertEquals('wp_postmeta', $table); - $this->assertEquals(['meta_key' => '_tiny_compress_images'], $data); - $this->assertEquals(['meta_key' => 'tiny_compress_images'], $where); + $this->assertEquals(array('meta_key' => '_tiny_compress_images'), $data); + $this->assertEquals(array('meta_key' => 'tiny_compress_images'), $where); $this->assertOptionWasUpdated(Tiny_Migrate::DB_VERSION_OPTION, Tiny_Migrate::DB_VERSION); } @@ -58,7 +58,7 @@ public function test_run_does_not_update_db_version_when_migration_fails() Tiny_Migrate::run(); $option_calls = $this->wp->getCalls('update_option'); - $version_updates = array_filter($option_calls, fn($call) => $call[0] === Tiny_Migrate::DB_VERSION_OPTION); + $version_updates = array_filter($option_calls, function($call) { return $call[0] === Tiny_Migrate::DB_VERSION_OPTION; }); $this->assertEmpty($version_updates, 'Should not update DB version when migration fails.'); } @@ -70,7 +70,7 @@ public function test_run_does_not_update_option_if_unnecessary() Tiny_Migrate::run(); $option_calls = $this->wp->getCalls('update_option'); - $version_updates = array_filter($option_calls, fn($call) => $call[0] === Tiny_Migrate::DB_VERSION_OPTION); + $version_updates = array_filter($option_calls, function($call) { return $call[0] === Tiny_Migrate::DB_VERSION_OPTION; }); $this->assertEmpty($version_updates, 'Should not re-save the version if already current.'); } From 9a75b4bd74b3866516c6d0bdf2e6270c4ee51542 Mon Sep 17 00:00:00 2001 From: tijmen Date: Fri, 15 May 2026 13:44:09 +0200 Subject: [PATCH 17/21] log error when migration failed --- src/class-tiny-migrate.php | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/class-tiny-migrate.php b/src/class-tiny-migrate.php index f850ab30..e2becdc9 100644 --- a/src/class-tiny-migrate.php +++ b/src/class-tiny-migrate.php @@ -82,7 +82,11 @@ public static function run() { } foreach ( self::migrations() as $version => $migration ) { - if ( $stored_version < $version && ! call_user_func( $migration ) ) { + if ( $stored_version >= $version ) { + continue; + } + + if ( ! call_user_func( $migration ) ) { return; } } @@ -98,7 +102,7 @@ public static function run() { * * @since 3.7.0 * - * @return boolean + * @return bool True on success or when there is nothing to migrate, false on DB error. */ private static function migrate_meta_key_to_private() { global $wpdb; @@ -113,11 +117,13 @@ private static function migrate_meta_key_to_private() { ); if ( false === $result ) { + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log + error_log( 'Tinify: failed to migrate meta key. DB error: ' . $wpdb->last_error ); return false; } // A return value of 0 means there was nothing to migrate, which is valid - // for fresh installs or databases that were already migrated. - return false !== $result;; + // for fresh installs or databases that were already migrated. + return false !== $result; } } From 454ad776c60ca8340f5132c4295faeaf6faf1674 Mon Sep 17 00:00:00 2001 From: tijmen Date: Fri, 15 May 2026 13:50:10 +0200 Subject: [PATCH 18/21] add backoff mechanism to retry migration each hour --- src/class-tiny-migrate.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/class-tiny-migrate.php b/src/class-tiny-migrate.php index e2becdc9..c9304b6d 100644 --- a/src/class-tiny-migrate.php +++ b/src/class-tiny-migrate.php @@ -46,6 +46,15 @@ class Tiny_Migrate { */ const DB_VERSION_OPTION = 'tinypng_db_version'; + /** + * When migration fails, will pause migration for an hour + * when the key exists in memory + * + * @since 3.7.0 + * @var string + */ + const MIGRATION_BACKOFF_KEY = 'tinypng_migration_backoff'; + /** * Returns an ordered map of migrations keyed by version number. * @@ -86,7 +95,14 @@ public static function run() { continue; } + if ( get_transient( self::MIGRATION_BACKOFF_KEY ) ) { + // transient key to hold migrations exists so exit early + return; + } + + if ( ! call_user_func( $migration ) ) { + set_transient( self::MIGRATION_BACKOFF_KEY, 1, HOUR_IN_SECONDS ); return; } } From 54027e05fd8c21d894b150a6daf5c3ef6af578f2 Mon Sep 17 00:00:00 2001 From: tijmen Date: Fri, 15 May 2026 13:50:26 +0200 Subject: [PATCH 19/21] format --- src/class-tiny-migrate.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/class-tiny-migrate.php b/src/class-tiny-migrate.php index c9304b6d..093d68d7 100644 --- a/src/class-tiny-migrate.php +++ b/src/class-tiny-migrate.php @@ -49,7 +49,7 @@ class Tiny_Migrate { /** * When migration fails, will pause migration for an hour * when the key exists in memory - * + * * @since 3.7.0 * @var string */ @@ -100,7 +100,6 @@ public static function run() { return; } - if ( ! call_user_func( $migration ) ) { set_transient( self::MIGRATION_BACKOFF_KEY, 1, HOUR_IN_SECONDS ); return; From 1029b73c79ba75e268905443f41912a6ee62be11 Mon Sep 17 00:00:00 2001 From: tijmen Date: Fri, 15 May 2026 14:07:04 +0200 Subject: [PATCH 20/21] add tests for backoff --- test/helpers/wordpress.php | 24 ++++++++++++++++++++++++ test/unit/TinyMigrateTest.php | 23 +++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/test/helpers/wordpress.php b/test/helpers/wordpress.php index a441056b..02f546ba 100644 --- a/test/helpers/wordpress.php +++ b/test/helpers/wordpress.php @@ -2,6 +2,7 @@ define('ABSPATH', dirname(__FILE__) . '/../'); define('WPINC', 'wp-includes-for-tests'); +define('HOUR_IN_SECONDS', 3600); require_once dirname(__FILE__) . '/../' . WPINC . '/file.php'; use org\bovigo\vfs\vfsStream; @@ -53,10 +54,12 @@ class WordPressStubs private $admin_initFunctions; private $options; private $metadata; + private $transients; private $calls; private $stubs; private $filters; public $postmeta = 'wp_postmeta'; + public $last_error = ''; public function __construct($vfs) { @@ -100,6 +103,9 @@ public function __construct($vfs) $this->addMethod('wp_timezone_string'); $this->addMethod('update_option'); $this->addMethod('update'); + $this->addMethod('get_transient'); + $this->addMethod('set_transient'); + $this->addMethod('delete_transient'); $this->addMethod('check_ajax_referer'); $this->addMethod('wp_json_encode'); $this->addMethod('wp_send_json_error'); @@ -124,6 +130,7 @@ public function defaults() $this->admin_initFunctions = array(); $this->options = new WordPressOptions(); $this->metadata = array(); + $this->transients = array(); $this->filters = array(); $GLOBALS['_wp_additional_image_sizes'] = array(); } @@ -190,6 +197,18 @@ public function call($method, $args) } if ('translate' === $method) { return $args[0]; + } elseif ('get_transient' === $method) { + $key = isset($args[0]) ? $args[0] : ''; + return isset($this->transients[$key]) ? $this->transients[$key] : false; + } elseif ('set_transient' === $method) { + $key = isset($args[0]) ? $args[0] : ''; + $value = isset($args[1]) ? $args[1] : ''; + $this->transients[$key] = $value; + return true; + } elseif ('delete_transient' === $method) { + $key = isset($args[0]) ? $args[0] : ''; + unset($this->transients[$key]); + return true; } elseif ('get_option' === $method) { return call_user_func_array(array($this->options, 'get'), $args); } elseif ('get_post_meta' === $method) { @@ -229,6 +248,11 @@ public function addOption($key, $value) $this->options->set($key, $value); } + public function addTransient($key, $value) + { + $this->transients[$key] = $value; + } + public function addImageSize($size, $values) { $GLOBALS['_wp_additional_image_sizes'][$size] = $values; diff --git a/test/unit/TinyMigrateTest.php b/test/unit/TinyMigrateTest.php index 1f2c80d0..937df2b4 100644 --- a/test/unit/TinyMigrateTest.php +++ b/test/unit/TinyMigrateTest.php @@ -74,4 +74,27 @@ public function test_run_does_not_update_option_if_unnecessary() $this->assertEmpty($version_updates, 'Should not re-save the version if already current.'); } + + public function test_run_sets_backoff_transient_when_migration_fails() + { + $this->wp->stub('update', function() { return false; }); + + Tiny_Migrate::run(); + + $set_transient_calls = $this->wp->getCalls('set_transient'); + $this->assertCount(1, $set_transient_calls, 'A backoff transient should be set after a failed migration.'); + $this->assertEquals(Tiny_Migrate::MIGRATION_BACKOFF_KEY, $set_transient_calls[0][0]); + $this->assertEquals(HOUR_IN_SECONDS, $set_transient_calls[0][2]); + } + + public function test_run_skips_migration_when_backoff_transient_is_set() + { + $this->wp->stub('get_transient', function($key) { + return Tiny_Migrate::MIGRATION_BACKOFF_KEY === $key ? 1 : false; + }); + + Tiny_Migrate::run(); + + $this->assertCount(0, $this->wp->getCalls('update'), 'DB update should not be attempted during the backoff period.'); + } } From 4c0f7fa80a52464b5cc34dd209a1bc4ed91111a6 Mon Sep 17 00:00:00 2001 From: tijmen Date: Fri, 15 May 2026 14:37:43 +0200 Subject: [PATCH 21/21] only add callable methods to stubs, swallow error logs in test --- test/helpers/wordpress.php | 5 +---- test/unit/TinyMigrateTest.php | 11 ++++++----- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/test/helpers/wordpress.php b/test/helpers/wordpress.php index 02f546ba..f18543a6 100644 --- a/test/helpers/wordpress.php +++ b/test/helpers/wordpress.php @@ -153,10 +153,7 @@ public function call($method, $args) } // Allow explicit stubs to override defaults/behaviors if (isset($this->stubs[$method]) && $this->stubs[$method]) { - if (is_callable($this->stubs[$method])) { - return call_user_func_array($this->stubs[$method], $args); - } - return $this->stubs[$method]; + return call_user_func_array($this->stubs[$method], $args); } if ('add_filter' === $method) { $tag = isset($args[0]) ? $args[0] : ''; diff --git a/test/unit/TinyMigrateTest.php b/test/unit/TinyMigrateTest.php index 937df2b4..491a8543 100644 --- a/test/unit/TinyMigrateTest.php +++ b/test/unit/TinyMigrateTest.php @@ -9,7 +9,11 @@ class Tiny_Migrate_Test extends Tiny_TestCase public function set_up() { parent::set_up(); - $this->wp->stub('update', 1); + // migration test logs error in stdout so swallow error logs + ini_set('error_log', '/dev/null'); + $this->wp->stub('update', function() { + return 1; + }); } /** @@ -69,10 +73,7 @@ public function test_run_does_not_update_option_if_unnecessary() Tiny_Migrate::run(); - $option_calls = $this->wp->getCalls('update_option'); - $version_updates = array_filter($option_calls, function($call) { return $call[0] === Tiny_Migrate::DB_VERSION_OPTION; }); - - $this->assertEmpty($version_updates, 'Should not re-save the version if already current.'); + $this->assertEmpty($this->wp->getCalls('update_option'), 'Should not call update_option at all when version is already current.'); } public function test_run_sets_backoff_transient_when_migration_fails()