<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[56508] trunk: Options, Meta APIs: Introduce `wp_set_option_autoload_values()`.</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { white-space: pre-line; overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="https://core.trac.wordpress.org/changeset/56508">56508</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"https://core.trac.wordpress.org/changeset/56508","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>flixos90</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2023-09-01 19:55:11 +0000 (Fri, 01 Sep 2023)</dd>
</dl>

<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>Options, Meta APIs: Introduce `wp_set_option_autoload_values()`.

This function accepts an associative array of option names and their autoload values to set, and it will update those values in the database in bulk, only for those options where the autoload field is not already set to the given value.

Two wrapper functions for ease of use accompany the new main function:
* `wp_set_options_autoload( $options, $autoload )` can be used to set multiple options to the same autoload value.
* `wp_set_option_autoload( $option, $autoload )` can be used to set the autoload value for a single option.

All of these functions allow changing the autoload value of an option, which previously has only been possible in combination with updating the value. This limitation prevented some relevant use-cases: For example, a plugin deactivation hook could set all of its options to not autoload, as a cleanup routine, while not actually deleting any data. The plugin's activation hook could then implement the reverse, resetting those options' autoload values to the originally intended ones for when using the plugin.

Props boonebgorges, joemcgill, costdev, mukesh27, SergeyBiryukov, tabrisrp, flixos90.
Fixes <a href="https://core.trac.wordpress.org/ticket/58964">#58964</a>.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcwpincludesoptionphp">trunk/src/wp-includes/option.php</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li><a href="#trunktestsphpunittestsoptionwpSetOptionAutoloadphp">trunk/tests/phpunit/tests/option/wpSetOptionAutoload.php</a></li>
<li><a href="#trunktestsphpunittestsoptionwpSetOptionAutoloadValuesphp">trunk/tests/phpunit/tests/option/wpSetOptionAutoloadValues.php</a></li>
<li><a href="#trunktestsphpunittestsoptionwpSetOptionsAutoloadphp">trunk/tests/phpunit/tests/option/wpSetOptionsAutoload.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcwpincludesoptionphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/option.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/option.php  2023-09-01 17:30:02 UTC (rev 56507)
+++ trunk/src/wp-includes/option.php    2023-09-01 19:55:11 UTC (rev 56508)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -365,6 +365,172 @@
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * Sets the autoload values for multiple options in the database.
+ *
+ * Autoloading too many options can lead to performance problems, especially if the options are not frequently used.
+ * This function allows modifying the autoload value for multiple options without changing the actual option value.
+ * This is for example recommended for plugin activation and deactivation hooks, to ensure any options exclusively used
+ * by the plugin which are generally autoloaded can be set to not autoload when the plugin is inactive.
+ *
+ * @since 6.4.0
+ *
+ * @global wpdb $wpdb WordPress database abstraction object.
+ *
+ * @param array $options Associative array of option names and their autoload values to set. The option names are
+ *                       expected to not be SQL-escaped. The autoload values accept 'yes'|true to enable or 'no'|false
+ *                       to disable.
+ * @return array Associative array of all provided $options as keys and boolean values for whether their autoload value
+ *               was updated.
+ */
+function wp_set_option_autoload_values( array $options ) {
+       global $wpdb;
+
+       if ( ! $options ) {
+               return array();
+       }
+
+       $grouped_options = array(
+               'yes' => array(),
+               'no'  => array(),
+       );
+       $results         = array();
+       foreach ( $options as $option => $autoload ) {
+               wp_protect_special_option( $option ); // Ensure only valid options can be passed.
+               if ( 'no' === $autoload || false === $autoload ) { // Sanitize autoload value and categorize accordingly.
+                       $grouped_options['no'][] = $option;
+               } else {
+                       $grouped_options['yes'][] = $option;
+               }
+               $results[ $option ] = false; // Initialize result value.
+       }
+
+       $where      = array();
+       $where_args = array();
+       foreach ( $grouped_options as $autoload => $options ) {
+               if ( ! $options ) {
+                       continue;
+               }
+               $placeholders = implode( ',', array_fill( 0, count( $options ), '%s' ) );
+               $where[]      = "autoload != '%s' AND option_name IN ($placeholders)";
+               $where_args[] = $autoload;
+               foreach ( $options as $option ) {
+                       $where_args[] = $option;
+               }
+       }
+       $where = 'WHERE ' . implode( ' OR ', $where );
+
+       /*
+        * Determine the relevant options that do not already use the given autoload value.
+        * If no options are returned, no need to update.
+        */
+       // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
+       $options_to_update = $wpdb->get_col( $wpdb->prepare( "SELECT option_name FROM $wpdb->options $where", $where_args ) );
+       if ( ! $options_to_update ) {
+               return $results;
+       }
+
+       // Run UPDATE queries as needed (maximum 2) to update the relevant options' autoload values to 'yes' or 'no'.
+       foreach ( $grouped_options as $autoload => $options ) {
+               if ( ! $options ) {
+                       continue;
+               }
+               $options                      = array_intersect( $options, $options_to_update );
+               $grouped_options[ $autoload ] = $options;
+               if ( ! $grouped_options[ $autoload ] ) {
+                       continue;
+               }
+
+               // Run query to update autoload value for all the options where it is needed.
+               $success = $wpdb->query(
+                       $wpdb->prepare(
+                               "UPDATE $wpdb->options SET autoload = %s WHERE option_name IN (" . implode( ',', array_fill( 0, count( $grouped_options[ $autoload ] ), '%s' ) ) . ')',
+                               array_merge(
+                                       array( $autoload ),
+                                       $grouped_options[ $autoload ]
+                               )
+                       )
+               );
+               if ( ! $success ) {
+                       // Set option list to an empty array to indicate no options were updated.
+                       $grouped_options[ $autoload ] = array();
+                       continue;
+               }
+
+               // Assume that on success all options were updated, which should be the case given only new values are sent.
+               foreach ( $grouped_options[ $autoload ] as $option ) {
+                       $results[ $option ] = true;
+               }
+       }
+
+       /*
+        * If any options were changed to 'yes', delete their individual caches, and delete 'alloptions' cache so that it
+        * is refreshed as needed.
+        * If no options were changed to 'yes' but any options were changed to 'no', delete them from the 'alloptions'
+        * cache. This is not necessary when options were changed to 'yes', since in that situation the entire cache is
+        * deleted anyway.
+        */
+       if ( $grouped_options['yes'] ) {
+               wp_cache_delete_multiple( $grouped_options['yes'], 'options' );
+               wp_cache_delete( 'alloptions', 'options' );
+       } elseif ( $grouped_options['no'] ) {
+               $alloptions = wp_load_alloptions( true );
+               foreach ( $grouped_options['no'] as $option ) {
+                       if ( isset( $alloptions[ $option ] ) ) {
+                               unset( $alloptions[ $option ] );
+                       }
+               }
+               wp_cache_set( 'alloptions', $alloptions, 'options' );
+       }
+
+       return $results;
+}
+
+/**
+ * Sets the autoload value for multiple options in the database.
+ *
+ * This is a wrapper for {@see wp_set_option_autoload_values()}, which can be used to set different autoload values for
+ * each option at once.
+ *
+ * @since 6.4.0
+ *
+ * @see wp_set_option_autoload_values()
+ *
+ * @param array       $options  List of option names. Expected to not be SQL-escaped.
+ * @param string|bool $autoload Autoload value to control whether to load the options when WordPress starts up.
+ *                              Accepts 'yes'|true to enable or 'no'|false to disable.
+ * @return array Associative array of all provided $options as keys and boolean values for whether their autoload value
+ *               was updated.
+ */
+function wp_set_options_autoload( array $options, $autoload ) {
+       return wp_set_option_autoload_values(
+               array_fill_keys( $options, $autoload )
+       );
+}
+
+/**
+ * Sets the autoload value for an option in the database.
+ *
+ * This is a wrapper for {@see wp_set_option_autoload_values()}, which can be used to set the autoload value for
+ * multiple options at once.
+ *
+ * @since 6.4.0
+ *
+ * @see wp_set_option_autoload_values()
+ *
+ * @param string      $option   Name of the option. Expected to not be SQL-escaped.
+ * @param string|bool $autoload Autoload value to control whether to load the option when WordPress starts up.
+ *                              Accepts 'yes'|true to enable or 'no'|false to disable.
+ * @return bool True if the autoload value was modified, false otherwise.
+ */
+function wp_set_option_autoload( $option, $autoload ) {
+       $result = wp_set_option_autoload_values( array( $option => $autoload ) );
+       if ( isset( $result[ $option ] ) ) {
+               return $result[ $option ];
+       }
+       return false;
+}
+
+/**
</ins><span class="cx" style="display: block; padding: 0 10px">  * Protects WordPress special option from being modified.
</span><span class="cx" style="display: block; padding: 0 10px">  *
</span><span class="cx" style="display: block; padding: 0 10px">  * Will die if $option is in protected list. Protected options are 'alloptions'
</span></span></pre></div>
<a id="trunktestsphpunittestsoptionwpSetOptionAutoloadphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/option/wpSetOptionAutoload.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/option/wpSetOptionAutoload.php                          (rev 0)
+++ trunk/tests/phpunit/tests/option/wpSetOptionAutoload.php    2023-09-01 19:55:11 UTC (rev 56508)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,114 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Test wp_set_option_autoload().
+ *
+ * @group option
+ *
+ * @covers ::wp_set_option_autoload
+ */
+class Tests_Option_WpSetOptionAutoload extends WP_UnitTestCase {
+
+       /**
+        * Tests that setting an option's autoload value to 'yes' works as expected.
+        *
+        * @ticket 58964
+        */
+       public function test_wp_set_option_autoload_yes() {
+               global $wpdb;
+
+               $option = 'test_option';
+               $value  = 'value';
+
+               add_option( $option, $value, '', 'no' );
+
+               $this->assertTrue( wp_set_option_autoload( $option, 'yes' ), 'Function did not succeed' );
+               $this->assertSame( 'yes', $wpdb->get_var( $wpdb->prepare( "SELECT autoload FROM $wpdb->options WHERE option_name = %s", $option ) ), 'Option autoload value not updated in database' );
+               $this->assertFalse( wp_cache_get( $option, 'options' ), 'Option not deleted from individual cache' );
+               $this->assertFalse( wp_cache_get( 'alloptions', 'options' ), 'Alloptions cache not cleared' );
+       }
+
+       /**
+        * Tests that setting an option's autoload value to 'no' works as expected.
+        *
+        * @ticket 58964
+        */
+       public function test_wp_set_option_autoload_no() {
+               global $wpdb;
+
+               $option = 'test_option';
+               $value  = 'value';
+
+               add_option( $option, $value, '', 'yes' );
+
+               $this->assertTrue( wp_set_option_autoload( $option, 'no' ), 'Function did not succeed' );
+               $this->assertSame( 'no', $wpdb->get_var( $wpdb->prepare( "SELECT autoload FROM $wpdb->options WHERE option_name = %s", $option ) ), 'Option autoload value not updated in database' );
+               $this->assertArrayNotHasKey( $option, wp_cache_get( 'alloptions', 'options' ), 'Option not deleted from alloptions cache' );
+       }
+
+       /**
+        * Tests that setting an option's autoload value to the same value as prior works as expected.
+        *
+        * @ticket 58964
+        */
+       public function test_wp_set_option_autoload_same() {
+               global $wpdb;
+
+               $option = 'test_option';
+               $value  = 'value';
+
+               add_option( $option, $value, '', 'yes' );
+
+               $this->assertFalse( wp_set_option_autoload( $option, 'yes' ), 'Function did unexpectedly succeed' );
+               $this->assertSame( 'yes', $wpdb->get_var( $wpdb->prepare( "SELECT autoload FROM $wpdb->options WHERE option_name = %s", $option ) ), 'Option autoload value unexpectedly updated in database' );
+       }
+
+       /**
+        * Tests that setting a missing option's autoload value does not do anything.
+        *
+        * @ticket 58964
+        */
+       public function test_wp_set_option_autoload_missing() {
+               global $wpdb;
+
+               $option = 'test_option';
+
+               $this->assertFalse( wp_set_option_autoload( $option, 'yes' ), 'Function did unexpectedly succeed' );
+               $this->assertNull( $wpdb->get_var( $wpdb->prepare( "SELECT autoload FROM $wpdb->options WHERE option_name = %s", $option ) ), 'Missing option autoload value was set in database' );
+               $this->assertArrayNotHasKey( $option, wp_cache_get( 'alloptions', 'options' ), 'Missing option found in alloptions cache' );
+               $this->assertFalse( wp_cache_get( $option, 'options' ), 'Missing option found in individual cache' );
+       }
+
+       /**
+        * Tests setting an option's autoload value to boolean true.
+        *
+        * @ticket 58964
+        */
+       public function test_wp_set_option_autoload_true() {
+               global $wpdb;
+
+               $option = 'test_option';
+               $value  = 'value';
+
+               add_option( $option, $value, '', false );
+
+               $this->assertTrue( wp_set_option_autoload( $option, true ), 'Function did not succeed' );
+               $this->assertSame( 'yes', $wpdb->get_var( $wpdb->prepare( "SELECT autoload FROM $wpdb->options WHERE option_name = %s", $option ) ), 'Option autoload value not updated in database' );
+       }
+
+       /**
+        * Tests setting an option's autoload value to boolean false.
+        *
+        * @ticket 58964
+        */
+       public function test_wp_set_option_autoload_false() {
+               global $wpdb;
+
+               $option = 'test_option';
+               $value  = 'value';
+
+               add_option( $option, $value, '', true );
+
+               $this->assertTrue( wp_set_option_autoload( $option, false ), 'Function did not succeed' );
+               $this->assertSame( 'no', $wpdb->get_var( $wpdb->prepare( "SELECT autoload FROM $wpdb->options WHERE option_name = %s", $option ) ), 'Option autoload value not updated in database' );
+       }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/tests/phpunit/tests/option/wpSetOptionAutoload.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunktestsphpunittestsoptionwpSetOptionAutoloadValuesphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/option/wpSetOptionAutoloadValues.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/option/wpSetOptionAutoloadValues.php                            (rev 0)
+++ trunk/tests/phpunit/tests/option/wpSetOptionAutoloadValues.php      2023-09-01 19:55:11 UTC (rev 56508)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,240 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Test wp_set_option_autoload_values().
+ *
+ * @group option
+ *
+ * @covers ::wp_set_option_autoload_values
+ */
+class Tests_Option_WpSetOptionAutoloadValues extends WP_UnitTestCase {
+
+       /**
+        * Tests setting options' autoload to 'yes' where for some options this is already the case.
+        *
+        * @ticket 58964
+        */
+       public function test_wp_set_option_autoload_values_all_yes_partial_update() {
+               global $wpdb;
+
+               $options = array(
+                       'test_option1' => 'yes',
+                       'test_option2' => 'yes',
+               );
+               add_option( 'test_option1', 'value1', '', 'yes' );
+               add_option( 'test_option2', 'value2', '', 'no' );
+               $expected = array(
+                       'test_option1' => false,
+                       'test_option2' => true,
+               );
+
+               $num_queries = get_num_queries();
+               $this->assertSame( $expected, wp_set_option_autoload_values( $options ), 'Function produced unexpected result' );
+               $this->assertSame( $num_queries + 2, get_num_queries(), 'Function made unexpected amount of database queries' );
+               $this->assertSame( array( 'yes', 'yes' ), $wpdb->get_col( $wpdb->prepare( "SELECT autoload FROM $wpdb->options WHERE option_name IN (" . implode( ',', array_fill( 0, count( $options ), '%s' ) ) . ')', ...array_keys( $options ) ) ), 'Option autoload values not updated in database' );
+               foreach ( $options as $option => $autoload ) {
+                       $this->assertFalse( wp_cache_get( $option, 'options' ), sprintf( 'Option %s not deleted from individual cache', $option ) );
+               }
+               $this->assertFalse( wp_cache_get( 'alloptions', 'options' ), 'Alloptions cache not cleared' );
+       }
+
+       /**
+        * Tests setting options' autoload to 'no' where for some options this is already the case.
+        *
+        * In this case, the 'alloptions' cache should not be cleared, but only its options set to 'no' should be deleted.
+        *
+        * @ticket 58964
+        */
+       public function test_wp_set_option_autoload_values_all_no_partial_update() {
+               global $wpdb;
+
+               $options = array(
+                       'test_option1' => 'no',
+                       'test_option2' => 'no',
+               );
+               add_option( 'test_option1', 'value1', '', 'yes' );
+               add_option( 'test_option2', 'value2', '', 'no' );
+               $expected = array(
+                       'test_option1' => true,
+                       'test_option2' => false,
+               );
+
+               $num_queries = get_num_queries();
+               $this->assertSame( $expected, wp_set_option_autoload_values( $options ), 'Function produced unexpected result' );
+               $this->assertSame( $num_queries + 2, get_num_queries(), 'Function made unexpected amount of database queries' );
+               $this->assertSame( array( 'no', 'no' ), $wpdb->get_col( $wpdb->prepare( "SELECT autoload FROM $wpdb->options WHERE option_name IN (" . implode( ',', array_fill( 0, count( $options ), '%s' ) ) . ')', ...array_keys( $options ) ) ), 'Option autoload values not updated in database' );
+               foreach ( $options as $option => $autoload ) {
+                       $this->assertArrayNotHasKey( $option, wp_cache_get( 'alloptions', 'options' ), sprintf( 'Option %s not deleted from alloptions cache', $option ) );
+               }
+       }
+
+       /**
+        * Tests setting options' autoload to 'yes' where for all of them this is already the case.
+        *
+        * @ticket 58964
+        */
+       public function test_wp_set_option_autoload_values_all_yes_no_update() {
+               global $wpdb;
+
+               $options = array(
+                       'test_option1' => 'yes',
+                       'test_option2' => 'yes',
+               );
+               add_option( 'test_option1', 'value1', '', 'yes' );
+               add_option( 'test_option2', 'value2', '', 'yes' );
+               $expected = array(
+                       'test_option1' => false,
+                       'test_option2' => false,
+               );
+
+               $num_queries = get_num_queries();
+               $this->assertSame( $expected, wp_set_option_autoload_values( $options ), 'Function produced unexpected result' );
+               $this->assertSame( $num_queries + 1, get_num_queries(), 'Function made unexpected amount of database queries' );
+               $this->assertSame( array( 'yes', 'yes' ), $wpdb->get_col( $wpdb->prepare( "SELECT autoload FROM $wpdb->options WHERE option_name IN (" . implode( ',', array_fill( 0, count( $options ), '%s' ) ) . ')', ...array_keys( $options ) ) ), 'Option autoload values not updated in database' );
+               foreach ( $options as $option => $autoload ) {
+                       $this->assertArrayHasKey( $option, wp_cache_get( 'alloptions', 'options' ), sprintf( 'Option %s unexpectedly deleted from alloptions cache', $option ) );
+               }
+       }
+
+       /**
+        * Tests setting options' autoload to either 'yes' or 'no' where for some options this is already the case.
+        *
+        * The test also covers one option that is entirely missing.
+        *
+        * @ticket 58964
+        */
+       public function test_wp_set_option_autoload_values_mixed_partial_update() {
+               global $wpdb;
+
+               $options = array(
+                       'test_option1' => 'yes',
+                       'test_option2' => 'no',
+                       'test_option3' => 'yes',
+                       'missing_opt'  => 'yes',
+               );
+               add_option( 'test_option1', 'value1', '', 'no' );
+               add_option( 'test_option2', 'value2', '', 'yes' );
+               add_option( 'test_option3', 'value3', '', 'yes' );
+               $expected = array(
+                       'test_option1' => true,
+                       'test_option2' => true,
+                       'test_option3' => false,
+                       'missing_opt'  => false,
+               );
+
+               $num_queries = get_num_queries();
+               $this->assertSame( $expected, wp_set_option_autoload_values( $options ), 'Function produced unexpected result' );
+               $this->assertSame( $num_queries + 3, get_num_queries(), 'Function made unexpected amount of database queries' );
+               $this->assertSameSets( array( 'yes', 'no', 'yes' ), $wpdb->get_col( $wpdb->prepare( "SELECT autoload FROM $wpdb->options WHERE option_name IN (" . implode( ',', array_fill( 0, count( $options ), '%s' ) ) . ')', ...array_keys( $options ) ) ), 'Option autoload values not updated in database' );
+               foreach ( $options as $option => $autoload ) {
+                       $this->assertFalse( wp_cache_get( $option, 'options' ), sprintf( 'Option %s not deleted from individual cache', $option ) );
+               }
+               $this->assertFalse( wp_cache_get( 'alloptions', 'options' ), 'Alloptions cache not cleared' );
+       }
+
+       /**
+        * Tests setting options' autoload to either 'yes' or 'no' while only the 'no' options actually need to be updated.
+        *
+        * In this case, the 'alloptions' cache should not be cleared, but only its options set to 'no' should be deleted.
+        *
+        * @ticket 58964
+        */
+       public function test_wp_set_option_autoload_values_mixed_only_update_no() {
+               global $wpdb;
+
+               $options = array(
+                       'test_option1' => 'yes',
+                       'test_option2' => 'no',
+                       'test_option3' => 'yes',
+               );
+               add_option( 'test_option1', 'value1', '', 'yes' );
+               add_option( 'test_option2', 'value2', '', 'yes' );
+               add_option( 'test_option3', 'value3', '', 'yes' );
+               $expected = array(
+                       'test_option1' => false,
+                       'test_option2' => true,
+                       'test_option3' => false,
+               );
+
+               $num_queries = get_num_queries();
+               $this->assertSame( $expected, wp_set_option_autoload_values( $options ), 'Function produced unexpected result' );
+               $this->assertSame( $num_queries + 2, get_num_queries(), 'Function made unexpected amount of database queries' );
+               $this->assertSameSets( array( 'yes', 'no', 'yes' ), $wpdb->get_col( $wpdb->prepare( "SELECT autoload FROM $wpdb->options WHERE option_name IN (" . implode( ',', array_fill( 0, count( $options ), '%s' ) ) . ')', ...array_keys( $options ) ) ), 'Option autoload values not updated in database' );
+               foreach ( $options as $option => $autoload ) {
+                       if ( 'no' === $autoload ) {
+                               $this->assertArrayNotHasKey( $option, wp_cache_get( 'alloptions', 'options' ), sprintf( 'Option %s not deleted from alloptions cache', $option ) );
+                       } else {
+                               $this->assertArrayHasKey( $option, wp_cache_get( 'alloptions', 'options' ), sprintf( 'Option %s unexpectedly deleted from alloptions cache', $option ) );
+                       }
+               }
+       }
+
+       /**
+        * Tests setting options' autoload with a simulated SQL query failure.
+        *
+        * @ticket 58964
+        */
+       public function test_wp_set_option_autoload_values_with_sql_query_failure() {
+               global $wpdb;
+
+               $options = array(
+                       'test_option1' => 'yes',
+                       'test_option2' => 'yes',
+               );
+               add_option( 'test_option1', 'value1', '', 'no' );
+               add_option( 'test_option2', 'value2', '', 'no' );
+
+               // Force UPDATE queries to fail, leading to no autoload values being updated.
+               add_filter(
+                       'query',
+                       static function( $query ) {
+                               if ( str_starts_with( $query, 'UPDATE ' ) ) {
+                                       return '';
+                               }
+                               return $query;
+                       }
+               );
+               $expected = array(
+                       'test_option1' => false,
+                       'test_option2' => false,
+               );
+
+               $this->assertSame( $expected, wp_set_option_autoload_values( $options ), 'Function produced unexpected result' );
+               $this->assertSame( array( 'no', 'no' ), $wpdb->get_col( $wpdb->prepare( "SELECT autoload FROM $wpdb->options WHERE option_name IN (" . implode( ',', array_fill( 0, count( $options ), '%s' ) ) . ')', ...array_keys( $options ) ) ), 'Option autoload values not updated in database' );
+       }
+
+       /**
+        * Tests setting options' autoload with boolean values.
+        *
+        * @ticket 58964
+        */
+       public function test_wp_set_option_autoload_values_with_bool() {
+               global $wpdb;
+
+               $options = array(
+                       'test_option1' => true,
+                       'test_option2' => false,
+               );
+               add_option( 'test_option1', 'value1', '', false );
+               add_option( 'test_option2', 'value2', '', true );
+               $expected = array(
+                       'test_option1' => true,
+                       'test_option2' => true,
+               );
+
+               $num_queries = get_num_queries();
+               $this->assertSame( $expected, wp_set_option_autoload_values( $options ), 'Function produced unexpected result' );
+               $this->assertSame( $num_queries + 3, get_num_queries(), 'Function made unexpected amount of database queries' );
+               $this->assertSameSets( array( 'yes', 'no' ), $wpdb->get_col( $wpdb->prepare( "SELECT autoload FROM $wpdb->options WHERE option_name IN (" . implode( ',', array_fill( 0, count( $options ), '%s' ) ) . ')', ...array_keys( $options ) ) ), 'Option autoload values not updated in database' );
+       }
+
+       /**
+        * Tests calling the function with an empty array (i.e. do nothing).
+        *
+        * @ticket 58964
+        */
+       public function test_wp_set_option_autoload_values_with_empty_array() {
+               $num_queries = get_num_queries();
+               $this->assertSame( array(), wp_set_option_autoload_values( array() ), 'Function produced unexpected result' );
+               $this->assertSame( $num_queries, get_num_queries(), 'Function made unexpected amount of database queries' );
+       }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/tests/phpunit/tests/option/wpSetOptionAutoloadValues.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunktestsphpunittestsoptionwpSetOptionsAutoloadphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/option/wpSetOptionsAutoload.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/option/wpSetOptionsAutoload.php                         (rev 0)
+++ trunk/tests/phpunit/tests/option/wpSetOptionsAutoload.php   2023-09-01 19:55:11 UTC (rev 56508)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,190 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Test wp_set_options_autoload().
+ *
+ * @group option
+ *
+ * @covers ::wp_set_options_autoload
+ */
+class Tests_Option_WpSetOptionsAutoload extends WP_UnitTestCase {
+
+       /**
+        * Tests that setting options' autoload value to 'yes' works as expected.
+        *
+        * @ticket 58964
+        */
+       public function test_wp_set_options_autoload_yes() {
+               global $wpdb;
+
+               $options = array(
+                       'test_option1' => 'value1',
+                       'test_option2' => 'value2',
+               );
+
+               $expected = array();
+               foreach ( $options as $option => $value ) {
+                       add_option( $option, $value, '', 'no' );
+                       $expected[ $option ] = true;
+               }
+
+               $num_queries = get_num_queries();
+               $this->assertSame( $expected, wp_set_options_autoload( array_keys( $options ), 'yes' ), 'Function did not succeed' );
+               $this->assertSame( $num_queries + 2, get_num_queries(), 'Updating options autoload value ran too many queries' );
+               $this->assertSame( array( 'yes', 'yes' ), $wpdb->get_col( $wpdb->prepare( "SELECT autoload FROM $wpdb->options WHERE option_name IN (" . implode( ',', array_fill( 0, count( $options ), '%s' ) ) . ')', ...array_keys( $options ) ) ), 'Option autoload values not updated in database' );
+               foreach ( $options as $option => $value ) {
+                       $this->assertFalse( wp_cache_get( $option, 'options' ), sprintf( 'Option %s not deleted from individual cache', $option ) );
+               }
+               $this->assertFalse( wp_cache_get( 'alloptions', 'options' ), 'Alloptions cache not cleared' );
+       }
+
+       /**
+        * Tests that setting options' autoload value to 'no' works as expected.
+        *
+        * @ticket 58964
+        */
+       public function test_wp_set_options_autoload_no() {
+               global $wpdb;
+
+               $options = array(
+                       'test_option1' => 'value1',
+                       'test_option2' => 'value2',
+               );
+
+               $expected = array();
+               foreach ( $options as $option => $value ) {
+                       add_option( $option, $value, '', 'yes' );
+                       $expected[ $option ] = true;
+               }
+
+               $num_queries = get_num_queries();
+               $this->assertSame( $expected, wp_set_options_autoload( array_keys( $options ), 'no' ), 'Function did not succeed' );
+               $this->assertSame( $num_queries + 2, get_num_queries(), 'Updating options autoload value ran too many queries' );
+               $this->assertSame( array( 'no', 'no' ), $wpdb->get_col( $wpdb->prepare( "SELECT autoload FROM $wpdb->options WHERE option_name IN (" . implode( ',', array_fill( 0, count( $options ), '%s' ) ) . ')', ...array_keys( $options ) ) ), 'Option autoload values not updated in database' );
+               foreach ( $options as $option => $value ) {
+                       $this->assertArrayNotHasKey( $option, wp_cache_get( 'alloptions', 'options' ), sprintf( 'Option %s not deleted from alloptions cache', $option ) );
+               }
+       }
+
+       /**
+        * Tests that setting options' autoload value to the same value as prior works as expected.
+        *
+        * @ticket 58964
+        */
+       public function test_wp_set_options_autoload_same() {
+               global $wpdb;
+
+               $options = array(
+                       'test_option1' => 'value1',
+                       'test_option2' => 'value2',
+               );
+
+               $expected = array();
+               foreach ( $options as $option => $value ) {
+                       add_option( $option, $value, '', 'yes' );
+                       $expected[ $option ] = false;
+               }
+
+               $num_queries = get_num_queries();
+               $this->assertSame( $expected, wp_set_options_autoload( array_keys( $options ), 'yes' ), 'Function did unexpectedly succeed' );
+               $this->assertSame( $num_queries + 1, get_num_queries(), 'Function attempted to update options autoload value in database' );
+               $this->assertSame( array( 'yes', 'yes' ), $wpdb->get_col( $wpdb->prepare( "SELECT autoload FROM $wpdb->options WHERE option_name IN (" . implode( ',', array_fill( 0, count( $options ), '%s' ) ) . ')', ...array_keys( $options ) ) ), 'Options autoload value unexpectedly updated in database' );
+       }
+
+       /**
+        * Tests that setting missing option's autoload value does not do anything.
+        *
+        * @ticket 58964
+        */
+       public function test_wp_set_options_autoload_missing() {
+               global $wpdb;
+
+               $options = array(
+                       'test_option1',
+                       'test_option2',
+               );
+
+               $expected = array();
+               foreach ( $options as $option ) {
+                       $expected[ $option ] = false;
+               }
+
+               $this->assertSame( $expected, wp_set_options_autoload( $options, 'yes' ), 'Function did unexpectedly succeed' );
+               $this->assertSame( array(), $wpdb->get_col( $wpdb->prepare( "SELECT autoload FROM $wpdb->options WHERE option_name IN (" . implode( ',', array_fill( 0, count( $options ), '%s' ) ) . ')', ...array_keys( $options ) ) ), 'Missing options autoload value was set in database' );
+       }
+
+       /**
+        * Tests that setting option's autoload value only updates those that need to be updated.
+        *
+        * @ticket 58964
+        */
+       public function test_wp_set_options_autoload_mixed() {
+               global $wpdb;
+
+               $options = array(
+                       'test_option1' => 'value1',
+                       'test_option2' => 'value2',
+               );
+
+               add_option( 'test_option1', $options['test_option1'], '', 'yes' );
+               add_option( 'test_option2', $options['test_option2'], '', 'no' );
+               $expected = array(
+                       'test_option1' => false,
+                       'test_option2' => true,
+               );
+
+               $this->assertSame( $expected, wp_set_options_autoload( array_keys( $options ), 'yes' ), 'Function produced unexpected result' );
+               $this->assertSame( array( 'yes', 'yes' ), $wpdb->get_col( $wpdb->prepare( "SELECT autoload FROM $wpdb->options WHERE option_name IN (" . implode( ',', array_fill( 0, count( $options ), '%s' ) ) . ')', ...array_keys( $options ) ) ), 'Option autoload values not updated in database' );
+               foreach ( $options as $option => $value ) {
+                       $this->assertFalse( wp_cache_get( $option, 'options' ), sprintf( 'Option %s not deleted from individual cache', $option ) );
+               }
+               $this->assertFalse( wp_cache_get( 'alloptions', 'options' ), 'Alloptions cache not cleared' );
+       }
+
+       /**
+        * Tests setting option's autoload value with boolean true.
+        *
+        * @ticket 58964
+        */
+       public function test_wp_set_options_autoload_true() {
+               global $wpdb;
+
+               $options = array(
+                       'test_option1' => 'value1',
+                       'test_option2' => 'value2',
+               );
+
+               add_option( 'test_option1', $options['test_option1'], '', false );
+               add_option( 'test_option2', $options['test_option2'], '', false );
+               $expected = array(
+                       'test_option1' => true,
+                       'test_option2' => true,
+               );
+
+               $this->assertSame( $expected, wp_set_options_autoload( array_keys( $options ), true ), 'Function produced unexpected result' );
+               $this->assertSame( array( 'yes', 'yes' ), $wpdb->get_col( $wpdb->prepare( "SELECT autoload FROM $wpdb->options WHERE option_name IN (" . implode( ',', array_fill( 0, count( $options ), '%s' ) ) . ')', ...array_keys( $options ) ) ), 'Option autoload values not updated in database' );
+       }
+
+       /**
+        * Tests setting option's autoload value with boolean false.
+        *
+        * @ticket 58964
+        */
+       public function test_wp_set_options_autoload_false() {
+               global $wpdb;
+
+               $options = array(
+                       'test_option1' => 'value1',
+                       'test_option2' => 'value2',
+               );
+
+               add_option( 'test_option1', $options['test_option1'], '', true );
+               add_option( 'test_option2', $options['test_option2'], '', true );
+               $expected = array(
+                       'test_option1' => true,
+                       'test_option2' => true,
+               );
+
+               $this->assertSame( $expected, wp_set_options_autoload( array_keys( $options ), false ), 'Function produced unexpected result' );
+               $this->assertSame( array( 'no', 'no' ), $wpdb->get_col( $wpdb->prepare( "SELECT autoload FROM $wpdb->options WHERE option_name IN (" . implode( ',', array_fill( 0, count( $options ), '%s' ) ) . ')', ...array_keys( $options ) ) ), 'Option autoload values not updated in database' );
+       }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/tests/phpunit/tests/option/wpSetOptionsAutoload.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span></div>

</body>
</html>