diff --git a/plugins/woocommerce/changelog/perf-indexes b/plugins/woocommerce/changelog/perf-indexes new file mode 100644 index 0000000000000..0aebc3734e763 --- /dev/null +++ b/plugins/woocommerce/changelog/perf-indexes @@ -0,0 +1,4 @@ +Significance: minor +Type: performance + +Add experimental support for FTS indexes in HPOS. Additionally, revert existing HPOS search queries to use post like structure. diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php b/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php index 83afdc1e88f12..feacb0e875e67 100644 --- a/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php +++ b/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php @@ -10,6 +10,8 @@ use Automattic\WooCommerce\Internal\BatchProcessing\BatchProcessingController; use Automattic\WooCommerce\Internal\Features\FeaturesController; use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods; +use Automattic\WooCommerce\Internal\Utilities\DatabaseUtil; +use Automattic\WooCommerce\Utilities\OrderUtil; use Automattic\WooCommerce\Utilities\PluginUtil; use WC_Admin_Settings; @@ -46,6 +48,12 @@ class CustomOrdersTableController { public const DEFAULT_DB_TRANSACTIONS_ISOLATION_LEVEL = 'READ UNCOMMITTED'; + public const HPOS_FTS_INDEX_OPTION = 'woocommerce_hpos_fts_index_enabled'; + + public const HPOS_FTS_ADDRESS_INDEX_CREATED_OPTION = 'woocommerce_hpos_address_fts_index_created'; + + public const HPOS_FTS_ORDER_ITEM_INDEX_CREATED_OPTION = 'woocommerce_hpos_order_item_fts_index_created'; + /** * The data store object to use. * @@ -109,6 +117,13 @@ class CustomOrdersTableController { */ private $plugin_util; + /** + * The db util object to use. + * + * @var DatabaseUtil; + */ + private $db_util; + /** * Class constructor. */ @@ -124,6 +139,7 @@ private function init_hooks() { self::add_filter( 'woocommerce_order-refund_data_store', array( $this, 'get_refunds_data_store' ), 999, 1 ); self::add_filter( 'woocommerce_debug_tools', array( $this, 'add_hpos_tools' ), 999 ); self::add_filter( 'updated_option', array( $this, 'process_updated_option' ), 999, 3 ); + self::add_filter( 'updated_option', array( $this, 'process_updated_option_fts_index' ), 999, 3 ); self::add_filter( 'pre_update_option', array( $this, 'process_pre_update_option' ), 999, 3 ); self::add_action( 'woocommerce_after_register_post_type', array( $this, 'register_post_type_for_order_placeholders' ), 10, 0 ); self::add_action( 'woocommerce_sections_advanced', array( $this, 'sync_now' ) ); @@ -144,6 +160,7 @@ private function init_hooks() { * @param OrderCache $order_cache The order cache engine to use. * @param OrderCacheController $order_cache_controller The order cache controller to use. * @param PluginUtil $plugin_util The plugin util to use. + * @param DatabaseUtil $db_util The database util to use. */ final public function init( OrdersTableDataStore $data_store, @@ -154,7 +171,8 @@ final public function init( FeaturesController $features_controller, OrderCache $order_cache, OrderCacheController $order_cache_controller, - PluginUtil $plugin_util + PluginUtil $plugin_util, + DatabaseUtil $db_util ) { $this->data_store = $data_store; $this->data_synchronizer = $data_synchronizer; @@ -165,6 +183,7 @@ final public function init( $this->order_cache = $order_cache; $this->order_cache_controller = $order_cache_controller; $this->plugin_util = $plugin_util; + $this->db_util = $db_util; } /** @@ -291,6 +310,61 @@ private function process_updated_option( $option, $old_value, $value ) { } } + /** + * Process option that enables FTS index on orders table. Tries to create an FTS index when option is enabled. + * + * @param string $option Option name. + * @param string $old_value Old value of the option. + * @param string $value New value of the option. + * + * @return void + */ + private function process_updated_option_fts_index( $option, $old_value, $value ) { + if ( self::HPOS_FTS_INDEX_OPTION !== $option ) { + return; + } + + if ( 'yes' !== $value ) { + return; + } + + if ( ! $this->custom_orders_table_usage_is_enabled() ) { + update_option( self::HPOS_FTS_INDEX_OPTION, 'no', true ); + if ( class_exists( 'WC_Admin_Settings' ) ) { + WC_Admin_Settings::add_error( __( 'Failed to create FTS index on orders table. This feature is only available when High-performance order storage is enabled.', 'woocommerce' ) ); + } + return; + } + + if ( ! $this->db_util->fts_index_on_order_address_table_exists() ) { + $this->db_util->create_fts_index_order_address_table(); + } + + // Check again to see if index was actually created. + if ( $this->db_util->fts_index_on_order_address_table_exists() ) { + update_option( self::HPOS_FTS_ADDRESS_INDEX_CREATED_OPTION, 'yes', true ); + } else { + update_option( self::HPOS_FTS_ADDRESS_INDEX_CREATED_OPTION, 'no', true ); + if ( class_exists( 'WC_Admin_Settings ' ) ) { + WC_Admin_Settings::add_error( __( 'Failed to create FTS index on address table', 'woocommerce' ) ); + } + } + + if ( ! $this->db_util->fts_index_on_order_item_table_exists() ) { + $this->db_util->create_fts_index_order_item_table(); + } + + // Check again to see if index was actually created. + if ( $this->db_util->fts_index_on_order_item_table_exists() ) { + update_option( self::HPOS_FTS_ORDER_ITEM_INDEX_CREATED_OPTION, 'yes', true ); + } else { + update_option( self::HPOS_FTS_ORDER_ITEM_INDEX_CREATED_OPTION, 'no', true ); + if ( class_exists( 'WC_Admin_Settings ' ) ) { + WC_Admin_Settings::add_error( __( 'Failed to create FTS index on order item table', 'woocommerce' ) ); + } + } + } + /** * Handler for the setting pre-update hook. * We use it to verify that authoritative orders table switch doesn't happen while sync is pending. diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableSearchQuery.php b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableSearchQuery.php index ad782ff7d497e..807f27792e9b8 100644 --- a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableSearchQuery.php +++ b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableSearchQuery.php @@ -51,7 +51,7 @@ public function __construct( OrdersTableQuery $query ) { * * @return array Array of search filters. */ - private function sanitize_search_filters( string $search_filter ) : array { + private function sanitize_search_filters( string $search_filter ): array { $core_filters = array( 'order_id', 'transaction_id', @@ -112,15 +112,7 @@ private function generate_join(): string { * * @return string JOIN clause. */ - private function generate_join_for_search_filter( $search_filter ) : string { - if ( 'products' === $search_filter ) { - $orders_table = $this->query->get_table_name( 'orders' ); - $items_table = $this->query->get_table_name( 'items' ); - return " - LEFT JOIN $items_table AS search_query_items ON search_query_items.order_id = $orders_table.id - "; - } - + private function generate_join_for_search_filter( $search_filter ): string { /** * Filter to support adding a custom order search filter. * Provide a JOIN clause for a new search filter. This should be used along with `woocommerce_hpos_admin_search_filters` @@ -181,7 +173,7 @@ private function generate_where(): string { * * @return string WHERE clause. */ - private function generate_where_for_search_filter( string $search_filter ) : string { + private function generate_where_for_search_filter( string $search_filter ): string { global $wpdb; $order_table = $this->query->get_table_name( 'orders' ); @@ -208,15 +200,11 @@ private function generate_where_for_search_filter( string $search_filter ) : str } if ( 'products' === $search_filter ) { - return $wpdb->prepare( - 'search_query_items.order_item_name LIKE %s', - '%' . $wpdb->esc_like( $this->search_term ) . '%' - ); + return $this->get_where_for_products(); } if ( 'customers' === $search_filter ) { - $meta_sub_query = $this->generate_where_for_meta_table(); - return "`$order_table`.id IN ( $meta_sub_query ) "; + return $this->get_where_for_customers(); } /** @@ -243,6 +231,74 @@ private function generate_where_for_search_filter( string $search_filter ) : str ); } + /** + * Helper function to generate the WHERE clause for products search. Uses FTS when available. + * + * @return string|null WHERE clause for products search. + */ + private function get_where_for_products() { + global $wpdb; + $items_table = $this->query->get_table_name( 'items' ); + $orders_table = $this->query->get_table_name( 'orders' ); + $fts_enabled = get_option( CustomOrdersTableController::HPOS_FTS_INDEX_OPTION ) === 'yes' && get_option( CustomOrdersTableController::HPOS_FTS_ORDER_ITEM_INDEX_CREATED_OPTION ) === 'yes'; + + if ( $fts_enabled ) { + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $orders_table and $items_table are hardcoded. + return $wpdb->prepare( + " +$orders_table.id in ( + SELECT order_id FROM $items_table search_query_items WHERE + MATCH ( search_query_items.order_item_name ) AGAINST ( %s IN BOOLEAN MODE ) +) +", + '*' . $wpdb->esc_like( $this->search_term ) . '*' + ); + // phpcs:enable + } + + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $orders_table and $items_table are hardcoded. + return $wpdb->prepare( + " +$orders_table.id in ( + SELECT order_id FROM $items_table search_query_items WHERE + search_query_items.order_item_name LIKE %s +) +", + '%' . $wpdb->esc_like( $this->search_term ) . '%' + ); + // phpcs:enable + } + + /** + * Helper function to generate the WHERE clause for customers search. Uses FTS when available. + * + * @return string|null WHERE clause for customers search. + */ + private function get_where_for_customers() { + global $wpdb; + $order_table = $this->query->get_table_name( 'orders' ); + $address_table = $this->query->get_table_name( 'addresses' ); + + $fts_enabled = get_option( CustomOrdersTableController::HPOS_FTS_INDEX_OPTION ) === 'yes' && get_option( CustomOrdersTableController::HPOS_FTS_ADDRESS_INDEX_CREATED_OPTION ) === 'yes'; + + if ( $fts_enabled ) { + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $order_table and $address_table are hardcoded. + return $wpdb->prepare( + " +$order_table.id IN ( + SELECT order_id FROM $address_table WHERE + MATCH( $address_table.first_name, $address_table.last_name, $address_table.company, $address_table.address_1, $address_table.address_2, $address_table.city, $address_table.state, $address_table.postcode, $address_table.country, $address_table.email ) AGAINST ( %s IN BOOLEAN MODE ) +) +", + '*' . $wpdb->esc_like( $this->search_term ) . '*' + ); + // phpcs:enable + } + + $meta_sub_query = $this->generate_where_for_meta_table(); + return "`$order_table`.id IN ( $meta_sub_query ) "; + } + /** * Generates where clause for meta table. * diff --git a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/OrdersDataStoreServiceProvider.php b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/OrdersDataStoreServiceProvider.php index e8bd45aebc1b6..6b37c82918b95 100644 --- a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/OrdersDataStoreServiceProvider.php +++ b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/OrdersDataStoreServiceProvider.php @@ -76,6 +76,7 @@ public function register() { OrderCache::class, OrderCacheController::class, PluginUtil::class, + DatabaseUtil::class, ) ); $this->share( OrderCache::class ); diff --git a/plugins/woocommerce/src/Internal/Features/FeaturesController.php b/plugins/woocommerce/src/Internal/Features/FeaturesController.php index fe0a9a518a1c7..be36ab7008802 100644 --- a/plugins/woocommerce/src/Internal/Features/FeaturesController.php +++ b/plugins/woocommerce/src/Internal/Features/FeaturesController.php @@ -7,6 +7,7 @@ use Automattic\WooCommerce\Internal\Admin\Analytics; use Automattic\WooCommerce\Admin\Features\Navigation\Init; +use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController; use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods; use Automattic\WooCommerce\Proxies\LegacyProxy; use Automattic\WooCommerce\Utilities\ArrayUtil; @@ -232,6 +233,17 @@ private function get_feature_definitions() { 'is_legacy' => true, 'is_experimental' => false, ), + 'hpos_fts_indexes' => array( + 'name' => __( 'HPOS Full text search indexes', 'woocommerce' ), + 'description' => __( + 'Create and use full text search indexes for orders. This feature only works with high-performance order storage.', + 'woocommerce' + ), + 'is_experimental' => true, + 'enabled_by_default' => false, + 'is_legacy' => true, + 'option_key' => CustomOrdersTableController::HPOS_FTS_INDEX_OPTION, + ), ); foreach ( $legacy_features as $slug => $definition ) { @@ -392,7 +404,7 @@ public function declare_compatibility( string $feature_id, string $plugin_name, ArrayUtil::ensure_key_is_array( $this->compatibility_info_by_plugin[ $plugin_name ], $opposite_key ); if ( in_array( $feature_id, $this->compatibility_info_by_plugin[ $plugin_name ][ $opposite_key ], true ) ) { - throw new \Exception( "Plugin $plugin_name is trying to declare itself as $key with the '$feature_id' feature, but it already declared itself as $opposite_key" ); + throw new \Exception( esc_html( "Plugin $plugin_name is trying to declare itself as $key with the '$feature_id' feature, but it already declared itself as $opposite_key" ) ); } if ( ! in_array( $feature_id, $this->compatibility_info_by_plugin[ $plugin_name ][ $key ], true ) ) { @@ -487,14 +499,15 @@ public function get_compatible_plugins_for_feature( string $feature_id, bool $ac /** * Check if the 'woocommerce_init' has run or is running, do a 'wc_doing_it_wrong' if not. * - * @param string|null $function Name of the invoking method, if not null, 'wc_doing_it_wrong' will be invoked if 'woocommerce_init' has not run and is not running. + * @param string|null $function_name Name of the invoking method, if not null, 'wc_doing_it_wrong' will be invoked if 'woocommerce_init' has not run and is not running. + * * @return bool True if 'woocommerce_init' has run or is running, false otherwise. */ - private function verify_did_woocommerce_init( string $function = null ): bool { + private function verify_did_woocommerce_init( string $function_name = null ): bool { if ( ! $this->proxy->call_function( 'did_action', 'woocommerce_init' ) && ! $this->proxy->call_function( 'doing_action', 'woocommerce_init' ) ) { - if ( ! is_null( $function ) ) { - $class_and_method = ( new \ReflectionClass( $this ) )->getShortName() . '::' . $function; + if ( ! is_null( $function_name ) ) { + $class_and_method = ( new \ReflectionClass( $this ) )->getShortName() . '::' . $function_name; /* translators: 1: class::method 2: plugins_loaded */ $this->proxy->call_function( 'wc_doing_it_wrong', $class_and_method, sprintf( __( '%1$s should not be called before the %2$s action.', 'woocommerce' ), $class_and_method, 'woocommerce_init' ), '7.0' ); } @@ -869,41 +882,41 @@ private function handle_plugin_deactivation( $plugin_name ): void { * if we are in the plugins page and the query string of the current request * looks like '?plugin_status=incompatible_with_feature&feature_id='. * - * @param array $list The original list of plugins. + * @param array $plugin_list The original list of plugins. */ - private function filter_plugins_list( $list ): array { + private function filter_plugins_list( $plugin_list ): array { if ( ! $this->verify_did_woocommerce_init() ) { - return $list; + return $plugin_list; } // phpcs:disable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput if ( ! function_exists( 'get_current_screen' ) || get_current_screen() && 'plugins' !== get_current_screen()->id || 'incompatible_with_feature' !== ArrayUtil::get_value_or_default( $_GET, 'plugin_status' ) ) { - return $list; + return $plugin_list; } $feature_id = $_GET['feature_id'] ?? 'all'; if ( 'all' !== $feature_id && ! $this->feature_exists( $feature_id ) ) { - return $list; + return $plugin_list; } - return $this->get_incompatible_plugins( $feature_id, $list ); + return $this->get_incompatible_plugins( $feature_id, $plugin_list ); } /** * Returns the list of plugins incompatible with a given feature. * * @param string $feature_id ID of the feature. Can also be `all` to denote all features. - * @param array $list List of plugins to filter. + * @param array $plugin_list List of plugins to filter. * * @return array List of plugins incompatible with the given feature. */ - private function get_incompatible_plugins( $feature_id, $list ) { + private function get_incompatible_plugins( $feature_id, $plugin_list ) { $incompatibles = array(); - $list = array_diff_key( $list, array_flip( $this->plugins_excluded_from_compatibility_ui ) ); + $plugin_list = array_diff_key( $plugin_list, array_flip( $this->plugins_excluded_from_compatibility_ui ) ); // phpcs:enable WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput - foreach ( array_keys( $list ) as $plugin_name ) { + foreach ( array_keys( $plugin_list ) as $plugin_name ) { if ( ! $this->plugin_util->is_woocommerce_aware_plugin( $plugin_name ) || ! $this->proxy->call_function( 'is_plugin_active', $plugin_name ) ) { continue; } @@ -921,7 +934,7 @@ function ( $feature_id ) { } } - return array_intersect_key( $list, array_flip( $incompatibles ) ); + return array_intersect_key( $plugin_list, array_flip( $incompatibles ) ); } /** diff --git a/plugins/woocommerce/src/Internal/Utilities/DatabaseUtil.php b/plugins/woocommerce/src/Internal/Utilities/DatabaseUtil.php index b989c2d78f317..3360db18b0497 100644 --- a/plugins/woocommerce/src/Internal/Utilities/DatabaseUtil.php +++ b/plugins/woocommerce/src/Internal/Utilities/DatabaseUtil.php @@ -170,7 +170,7 @@ public function format_object_value_for_db( $value, string $type ) { $value = $value ? ( new DateTime( "@{$value}" ) )->format( 'Y-m-d H:i:s' ) : null; break; default: - throw new \Exception( 'Invalid type received: ' . $type ); + throw new \Exception( esc_html( 'Invalid type received: ' . $type ) ); } return $value; @@ -194,7 +194,7 @@ public function get_wpdb_format_for_type( string $type ) { ); if ( ! isset( $wpdb_placeholder_for_type[ $type ] ) ) { - throw new \Exception( 'Invalid column type: ' . $type ); + throw new \Exception( esc_html( 'Invalid column type: ' . $type ) ); } return $wpdb_placeholder_for_type[ $type ]; @@ -231,7 +231,7 @@ public function generate_on_duplicate_statement_clause( array $columns ): string * * @return int Returns the value of DB's ON DUPLICATE KEY UPDATE clause. */ - public function insert_on_duplicate_key_update( $table_name, $data, $format ) : int { + public function insert_on_duplicate_key_update( $table_name, $data, $format ): int { global $wpdb; if ( empty( $data ) ) { return 0; @@ -249,7 +249,7 @@ public function insert_on_duplicate_key_update( $table_name, $data, $format ) : $values[] = $value; $value_format[] = $format[ $index ]; } - $index++; + ++$index; } $column_clause = '`' . implode( '`, `', $columns ) . '`'; $value_format_clause = implode( ', ', $value_format ); @@ -273,7 +273,7 @@ public function insert_on_duplicate_key_update( $table_name, $data, $format ) : * * @return int Max index length. */ - public function get_max_index_length() : int { + public function get_max_index_length(): int { /** * Filters the maximum index length in the database. * @@ -291,4 +291,52 @@ public function get_max_index_length() : int { // Index length cannot be more than 768, which is 3078 bytes in utf8mb4 and max allowed by InnoDB engine. return min( absint( $max_index_length ), 767 ); } + + /** + * Create a fulltext index on order address table. + * + * @return void + */ + public function create_fts_index_order_address_table(): void { + global $wpdb; + $address_table = $wpdb->prefix . 'wc_order_addresses'; + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $address_table is hardcoded. + $wpdb->query( "CREATE FULLTEXT INDEX order_addresses_fts ON $address_table (first_name, last_name, company, address_1, address_2, city, state, postcode, country, email)" ); + } + + /** + * Check if fulltext index with key `order_addresses_fts` on order address table exists. + * + * @return bool + */ + public function fts_index_on_order_address_table_exists(): bool { + global $wpdb; + $address_table = $wpdb->prefix . 'wc_order_addresses'; + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $address_table is hardcoded. + return ! empty( $wpdb->get_results( "SHOW INDEX FROM $address_table WHERE Key_name = 'order_addresses_fts'" ) ); + } + + /** + * Create a fulltext index on order item table. + * + * @return void + */ + public function create_fts_index_order_item_table(): void { + global $wpdb; + $order_item_table = $wpdb->prefix . 'woocommerce_order_items'; + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $order_item_table is hardcoded. + $wpdb->query( "CREATE FULLTEXT INDEX order_item_fts ON $order_item_table (order_item_name)" ); + } + + /** + * Check if fulltext index with key `order_item_fts` on order item table exists. + * + * @return bool + */ + public function fts_index_on_order_item_table_exists(): bool { + global $wpdb; + $order_item_table = $wpdb->prefix . 'woocommerce_order_items'; + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $order_item_table is hardcoded. + return ! empty( $wpdb->get_results( "SHOW INDEX FROM $order_item_table WHERE Key_name = 'order_item_fts'" ) ); + } } diff --git a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableQueryTests.php b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableQueryTests.php index 21ff35190f58b..acddc1c05fb21 100644 --- a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableQueryTests.php +++ b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableQueryTests.php @@ -1,5 +1,6 @@ setup_cot(); $this->cot_state = OrderUtil::custom_orders_table_usage_is_enabled(); + $this->setup_cot(); $this->toggle_cot_feature_and_usage( true ); } @@ -148,7 +149,7 @@ public function test_query_suppress_filters() { $filters_called = 0; $filter_callback = function ( $arg ) use ( &$filters_called ) { - $filters_called++; + ++$filters_called; return $arg; }; @@ -193,7 +194,7 @@ public function test_query_filters() { $this->assertCount( 2, wc_get_orders( array() ) ); // Force a query that returns nothing. - $filter_callback = function( $clauses ) { + $filter_callback = function ( $clauses ) { $clauses['where'] .= ' AND 1=0 '; return $clauses; }; @@ -203,7 +204,7 @@ public function test_query_filters() { remove_all_filters( 'woocommerce_orders_table_query_clauses' ); // Force a query that sorts orders by id ASC (as opposed to the default date DESC) if a query arg is present. - $filter_callback = function( $clauses, $query, $query_args ) { + $filter_callback = function ( $clauses, $query, $query_args ) { if ( ! empty( $query_args['my_custom_arg'] ) ) { $clauses['orderby'] = $query->get_table_name( 'orders' ) . '.id ASC'; } @@ -254,7 +255,7 @@ public function test_pre_query_escape_hook_simple() { $this->assertEquals( 2, $query->found_orders ); $this->assertEquals( 0, $query->max_num_pages ); - $callback = function( $result, $query_object, $sql ) use ( $order1 ) { + $callback = function ( $result, $query_object, $sql ) use ( $order1 ) { $this->assertNull( $result ); $this->assertInstanceOf( OrdersTableQuery::class, $query_object ); $this->assertStringContainsString( 'SELECT ', $sql ); @@ -295,7 +296,7 @@ public function test_pre_query_escape_hook_with_pagination() { $this->assertEquals( 2, $query->found_orders ); $this->assertEquals( 0, $query->max_num_pages ); - $callback = function( $result, $query_object, $sql ) use ( $order1 ) { + $callback = function ( $result, $query_object, $sql ) use ( $order1 ) { $this->assertNull( $result ); $this->assertInstanceOf( OrdersTableQuery::class, $query_object ); $this->assertStringContainsString( 'SELECT ', $sql ); @@ -330,7 +331,7 @@ public function test_pre_query_escape_hook_pass_limit() { $order1->set_date_created( time() - HOUR_IN_SECONDS ); $order1->save(); - $callback = function( $result, $query_object, $sql ) use ( $order1 ) { + $callback = function () use ( $order1 ) { // Do not return found_orders or max_num_pages so as to provoke a warning. $order_ids = array( $order1->get_id() ); return array( $order_ids, 10, null ); @@ -385,7 +386,7 @@ public function test_pre_query_escape_hook_return_null_limit() { $order1->set_date_created( time() - HOUR_IN_SECONDS ); $order1->save(); - $callback = function( $result, $query_object, $sql ) use ( $order1 ) { + $callback = function () use ( $order1 ) { // Just return null. return null; }; diff --git a/plugins/woocommerce/tests/php/src/Internal/Features/FeaturesControllerTest.php b/plugins/woocommerce/tests/php/src/Internal/Features/FeaturesControllerTest.php index 56667d49bb85f..a301705389ab7 100644 --- a/plugins/woocommerce/tests/php/src/Internal/Features/FeaturesControllerTest.php +++ b/plugins/woocommerce/tests/php/src/Internal/Features/FeaturesControllerTest.php @@ -402,7 +402,7 @@ public function test_declare_compatibility_and_incompatibility_for_the_same_plug $this->simulate_inside_before_woocommerce_init_hook(); $this->ExpectException( \Exception::class ); - $this->ExpectExceptionMessage( "Plugin the_plugin is trying to declare itself as incompatible with the 'mature1' feature, but it already declared itself as compatible" ); + $this->ExpectExceptionMessage( esc_html( "Plugin the_plugin is trying to declare itself as incompatible with the 'mature1' feature, but it already declared itself as compatible" ) ); $this->sut->declare_compatibility( 'mature1', 'the_plugin', true ); $this->sut->declare_compatibility( 'mature1', 'the_plugin', false ); diff --git a/plugins/woocommerce/tests/php/src/Internal/Utilities/DatabaseUtilTest.php b/plugins/woocommerce/tests/php/src/Internal/Utilities/DatabaseUtilTest.php index a00c57dc62773..cb0572a576083 100644 --- a/plugins/woocommerce/tests/php/src/Internal/Utilities/DatabaseUtilTest.php +++ b/plugins/woocommerce/tests/php/src/Internal/Utilities/DatabaseUtilTest.php @@ -3,6 +3,7 @@ * Tests for the DatabaseUtil utility. */ +use Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer; use Automattic\WooCommerce\Internal\Utilities\DatabaseUtil; /** @@ -57,4 +58,32 @@ public function test_get_index_columns_returns_empty_for_invalid_index() { $this->sut->get_index_columns( $wpdb->prefix . 'wc_product_meta_lookup', 'invalid_index_name' ) ); } + + /** + * @test Test that we are able to create FTS index on order address table. + */ + public function test_create_fts_index_order_address_table() { + $db = wc_get_container()->get( DataSynchronizer::class ); + // Remove the Test Suiteā€™s use of temporary tables https://wordpress.stackexchange.com/a/220308. + remove_filter( 'query', array( $this, '_create_temporary_tables' ) ); + remove_filter( 'query', array( $this, '_drop_temporary_tables' ) ); + $db->create_database_tables(); + // Add back removed filter. + add_filter( 'query', array( $this, '_create_temporary_tables' ) ); + add_filter( 'query', array( $this, '_drop_temporary_tables' ) ); + if ( ! $this->sut->fts_index_on_order_item_table_exists() ) { + $this->sut->create_fts_index_order_address_table(); + } + $this->assertTrue( $this->sut->fts_index_on_order_address_table_exists() ); + } + + /** + * @test Test that we are able to create FTS index on order item table. + */ + public function test_create_fts_index_order_item_table() { + if ( ! $this->sut->fts_index_on_order_item_table_exists() ) { + $this->sut->create_fts_index_order_item_table(); + } + $this->assertTrue( $this->sut->fts_index_on_order_item_table_exists() ); + } }