From 4d5175f395ec404bd0dbc99978705b9a5b9170a1 Mon Sep 17 00:00:00 2001
From: Jan Thiel <jan@hive-it.de>
Date: Fri, 18 Jun 2021 17:48:04 +0200
Subject: [PATCH] Adds the new LIKE based compare operators STARTSWITH,
 ENDSWITH, NOT STARTSWITH, NOT ENDSWITH to the meta query value compares. They
 allow more performant regular query cases than REGEXP which would be the
 alternative.

---
 src/wp-includes/class-wp-meta-query.php |  20 +++++
 tests/phpunit/tests/query/metaQuery.php | 104 ++++++++++++++++++++++++
 2 files changed, 124 insertions(+)

diff --git a/src/wp-includes/class-wp-meta-query.php b/src/wp-includes/class-wp-meta-query.php
index 5a1b0c41479..a2339b79248 100644
--- a/src/wp-includes/class-wp-meta-query.php
+++ b/src/wp-includes/class-wp-meta-query.php
@@ -102,6 +102,7 @@ class WP_Meta_Query {
 	 * @since 5.1.0 Introduced $compare_key clause parameter, which enables LIKE key matches.
 	 * @since 5.3.0 Increased the number of operators available to $compare_key. Introduced $type_key,
 	 *              which enables the $key to be cast to a new data type for comparisons.
+	 * @since 5.9.0 Adds LIKE based STARTSWITH and ENDSWITH operators for value compares
 	 *
 	 * @param array $meta_query {
 	 *     Array of meta query clauses. When first-order clauses or sub-clauses use strings as
@@ -123,6 +124,7 @@ class WP_Meta_Query {
 	 *         @type string $value       Meta value to filter by.
 	 *         @type string $compare     MySQL operator used for comparing the $value. Accepts '=',
 	 *                                   '!=', '>', '>=', '<', '<=', 'LIKE', 'NOT LIKE',
+	 *                                   'STARTSWITH', 'NOT STARTSWITH', 'ENDSWITH', 'NOT ENDSWITH',
 	 *                                   'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN', 'REGEXP',
 	 *                                   'NOT REGEXP', 'RLIKE', 'EXISTS' or 'NOT EXISTS'.
 	 *                                   Default is 'IN' when `$value` is an array, '=' otherwise.
@@ -509,7 +511,11 @@ public function get_sql_for_clause( &$clause, $parent_query, $clause_key = '' )
 			'=',
 			'!=',
 			'LIKE',
+			'STARTSWITH',
+			'ENDSWITH',
 			'NOT LIKE',
+			'NOT STARTSWITH',
+			'NOT ENDSWITH',
 			'IN',
 			'NOT IN',
 			'EXISTS',
@@ -713,6 +719,20 @@ public function get_sql_for_clause( &$clause, $parent_query, $clause_key = '' )
 					$where      = $wpdb->prepare( '%s', $meta_value );
 					break;
 
+				case 'STARTSWITH':
+				case 'NOT STARTSWITH':
+					$meta_compare = $meta_compare === 'STARTSWITH' ? 'LIKE' : 'NOT LIKE';
+					$meta_value   = $wpdb->esc_like( $meta_value ) . '%';
+					$where        = $wpdb->prepare( '%s', $meta_value );
+					break;
+
+				case 'ENDSSWITH':
+				case 'NOT ENDSWITH':
+					$meta_compare = $meta_compare === 'ENDSSWITH' ? 'LIKE' : 'NOT LIKE';
+					$meta_value   = '%' . $wpdb->esc_like( $meta_value );
+					$where        = $wpdb->prepare( '%s', $meta_value );
+					break;
+
 				// EXISTS with a value is interpreted as '='.
 				case 'EXISTS':
 					$meta_compare = '=';
diff --git a/tests/phpunit/tests/query/metaQuery.php b/tests/phpunit/tests/query/metaQuery.php
index 5acec55d3f9..239173893e1 100644
--- a/tests/phpunit/tests/query/metaQuery.php
+++ b/tests/phpunit/tests/query/metaQuery.php
@@ -271,6 +271,110 @@ public function test_meta_query_single_query_compare_not_like() {
 		$this->assertSameSets( $expected, $query->posts );
 	}
 
+	public function test_meta_query_single_query_compare_startswith() {
+		$p1 = self::factory()->post->create();
+		$p2 = self::factory()->post->create();
+
+		add_post_meta( $p1, 'foo', 'bar' );
+
+		$query = new WP_Query(
+			array(
+				'update_post_meta_cache' => false,
+				'update_post_term_cache' => false,
+				'fields'                 => 'ids',
+				'meta_query'             => array(
+					array(
+						'key'     => 'foo',
+						'value'   => 'ba',
+						'compare' => 'STARTSWITH',
+					),
+				),
+			)
+		);
+
+		$expected = array( $p1 );
+		$this->assertSameSets( $expected, $query->posts );
+	}
+
+	public function test_meta_query_single_query_compare_not_startswith() {
+		$p1 = self::factory()->post->create();
+		$p2 = self::factory()->post->create();
+		$p3 = self::factory()->post->create();
+
+		add_post_meta( $p1, 'foo', 'bar' );
+		add_post_meta( $p2, 'foo', 'rab' );
+
+		$query = new WP_Query(
+			array(
+				'update_post_meta_cache' => false,
+				'update_post_term_cache' => false,
+				'fields'                 => 'ids',
+				'meta_query'             => array(
+					array(
+						'key'     => 'foo',
+						'value'   => 'ba',
+						'compare' => 'NOT STARTSWITH',
+					),
+				),
+			)
+		);
+
+		$expected = array( $p2 );
+		$this->assertSameSets( $expected, $query->posts );
+	}
+
+	public function test_meta_query_single_query_compare_endswith() {
+		$p1 = self::factory()->post->create();
+		$p2 = self::factory()->post->create();
+
+		add_post_meta( $p1, 'foo', 'bar' );
+
+		$query = new WP_Query(
+			array(
+				'update_post_meta_cache' => false,
+				'update_post_term_cache' => false,
+				'fields'                 => 'ids',
+				'meta_query'             => array(
+					array(
+						'key'     => 'foo',
+						'value'   => 'ar',
+						'compare' => 'ENDSWITH',
+					),
+				),
+			)
+		);
+
+		$expected = array( $p1 );
+		$this->assertSameSets( $expected, $query->posts );
+	}
+
+	public function test_meta_query_single_query_compare_not_endswith() {
+		$p1 = self::factory()->post->create();
+		$p2 = self::factory()->post->create();
+		$p3 = self::factory()->post->create();
+
+		add_post_meta( $p1, 'foo', 'bar' );
+		add_post_meta( $p2, 'foo', 'rab' );
+
+		$query = new WP_Query(
+			array(
+				'update_post_meta_cache' => false,
+				'update_post_term_cache' => false,
+				'fields'                 => 'ids',
+				'meta_query'             => array(
+					array(
+						'key'     => 'foo',
+						'value'   => 'ar',
+						'compare' => 'NOT ENDSWITH',
+					),
+				),
+			)
+		);
+
+		$expected = array( $p2 );
+		$this->assertSameSets( $expected, $query->posts );
+	}
+
 	public function test_meta_query_single_query_compare_between_not_between() {
 		$p1 = self::factory()->post->create();
 		$p2 = self::factory()->post->create();
