Index: src/wp-includes/link-template.php
===================================================================
--- src/wp-includes/link-template.php	(revision 27059)
+++ src/wp-includes/link-template.php	(working copy)
@@ -1128,79 +1128,233 @@
  * @return mixed       Post object if successful. Null if global $post is not set. Empty string if no corresponding post exists.
  */
 function get_adjacent_post( $in_same_term = false, $excluded_terms = '', $previous = true, $taxonomy = 'category' ) {
-	global $wpdb;
-
-	if ( ( ! $post = get_post() ) || ! taxonomy_exists( $taxonomy ) )
+	if ( ( ! $post = get_post() ) || ! taxonomy_exists( $taxonomy ) ) {
 		return null;
+	}
 
-	$current_post_date = $post->post_date;
+	$current_post_date = get_post_field( 'post_date', $post );
 
-	$join = '';
-	$posts_in_ex_terms_sql = '';
-	if ( $in_same_term || ! empty( $excluded_terms ) ) {
-		$join = " INNER JOIN $wpdb->term_relationships AS tr ON p.ID = tr.object_id INNER JOIN $wpdb->term_taxonomy tt ON tr.term_taxonomy_id = tt.term_taxonomy_id";
+	// Query basics
+	$args = array(
+		'posts_per_page'   => 1,
+		'post_status'      => 'publish',
+		'post_type'        => 'post',
+		'orderby'          => 'date',
+		'order'            => $previous ? 'DESC' : 'ASC',
+		'no_found_rows'    => true,
+		'cache_results'    => true,
+		'suppress_filters' => false,
+		'date_query'       => array(),
+	);
 
-		if ( $in_same_term ) {
-			if ( ! is_object_in_taxonomy( $post->post_type, $taxonomy ) )
-				return '';
-			$term_array = wp_get_object_terms( $post->ID, $taxonomy, array( 'fields' => 'ids' ) );
-			if ( ! $term_array || is_wp_error( $term_array ) )
-				return '';
-			$join .= $wpdb->prepare( " AND tt.taxonomy = %s AND tt.term_id IN (" . implode( ',', array_map( 'intval', $term_array ) ) . ")", $taxonomy );
+	// Query pieces
+	$tax_query = array();
+
+	// Set up for requests limited to posts that share terms
+	if ( $in_same_term ) {
+		$terms = get_the_terms( get_the_ID(), $taxonomy );
+
+		if ( is_array( $terms ) && ! empty( $terms ) ) {
+			$terms = wp_list_pluck( $terms, 'term_id' );
+			$terms = array_values( $terms );
+			$terms = array_map( 'intval', $terms );
+		} else {
+			unset( $terms );
 		}
+	}
 
-		$posts_in_ex_terms_sql = $wpdb->prepare( "AND tt.taxonomy = %s", $taxonomy );
-		if ( ! empty( $excluded_terms ) ) {
-			if ( ! is_array( $excluded_terms ) ) {
-				// back-compat, $excluded_terms used to be $excluded_terms with IDs separated by " and "
-				if ( false !== strpos( $excluded_terms, ' and ' ) ) {
-					_deprecated_argument( __FUNCTION__, '3.3', sprintf( __( 'Use commas instead of %s to separate excluded terms.' ), "'and'" ) );
-					$excluded_terms = explode( ' and ', $excluded_terms );
-				} else {
-					$excluded_terms = explode( ',', $excluded_terms );
-				}
+	// Handle excluded terms
+	if ( ! empty( $excluded_terms ) ) {
+		if ( ! is_array( $excluded_terms ) ) {
+			// back-compat, $excluded_terms used to be IDs separated by " and "
+			if ( false !== strpos( $excluded_terms, ' and ' ) ) {
+				_deprecated_argument( __FUNCTION__, '3.3', sprintf( __( 'Use commas instead of %s to separate excluded terms.' ), "'and'" ) );
+				$excluded_terms = explode( ' and ', $excluded_terms );
+			} else {
+				$excluded_terms = explode( ',', $excluded_terms );
 			}
+		}
 
-			$excluded_terms = array_map( 'intval', $excluded_terms );
+		$excluded_terms = array_map( 'intval', $excluded_terms );
 
-			if ( ! empty( $term_array ) ) {
-				$excluded_terms = array_diff( $excluded_terms, $term_array );
-				$posts_in_ex_terms_sql = '';
-			}
+		$tax_query[] = array(
+			'taxonomy' => $tax_query,
+			'slugs'    => $excluded_terms,
+			'compare'  => 'NOT IN',
+		);
+	}
 
-			if ( ! empty( $excluded_terms ) ) {
-				$posts_in_ex_terms_sql = $wpdb->prepare( " AND tt.taxonomy = %s AND tt.term_id NOT IN (" . implode( $excluded_terms, ',' ) . ')', $taxonomy );
-			}
+	// If requesting same term, ensure that excluded terms don't appear in term list list
+	if ( isset( $terms ) ) {
+		if ( isset( $excluded_terms ) && is_array( $excluded_terms ) ) {
+			$terms = array_diff( $terms, $excluded_terms );
 		}
+
+		if ( ! empty( $terms ) ) {
+			$tax_query[] = array(
+				'taxonomy' => $taxonomy,
+				'terms'    => $terms,
+			);
+		}
 	}
 
-	$adjacent = $previous ? 'previous' : 'next';
-	$op = $previous ? '<' : '>';
-	$order = $previous ? 'DESC' : 'ASC';
+	// If we have a tax query, add it to our query args
+	if ( ! empty( $tax_query ) ) {
+		$args['tax_query'] = $tax_query;
+	}
 
-	$join  = apply_filters( "get_{$adjacent}_post_join", $join, $in_same_term, $excluded_terms );
-	$where = apply_filters( "get_{$adjacent}_post_where", $wpdb->prepare( "WHERE p.post_date $op %s AND p.post_type = %s AND p.post_status = 'publish' $posts_in_ex_terms_sql", $current_post_date, $post->post_type), $in_same_term, $excluded_terms );
-	$sort  = apply_filters( "get_{$adjacent}_post_sort", "ORDER BY p.post_date $order LIMIT 1" );
+	// And now, the date constraint
+	if ( $previous ) {
+		$args['date_query'][] = array(
+			'before'    => $current_post_date,
+			'inclusive' => true,
+		);
+	} else {
+		$args['date_query'][] = array(
+			'after'     => $current_post_date,
+			'inclusive' => true,
+		);
+	}
 
-	$query = "SELECT p.ID FROM $wpdb->posts AS p $join $where $sort";
-	$query_key = 'adjacent_post_' . md5( $query );
-	$result = wp_cache_get( $query_key, 'counts' );
-	if ( false !== $result ) {
-		if ( $result )
-			$result = get_post( $result );
-		return $result;
+	// Ensure the current post isn't returned, since we're using an inclusive date query
+	$args['post__not_in'] = array( get_the_ID() );
+
+	/**
+	 * Let plugins modify the parameters for the adjacent post query
+	 *
+	 * @since 3.9.0
+	 *
+	 * @param array $args WP_Query arguments
+	 * @param array       Arguments when `get_adjacent_post()` was called
+	 */
+	$args = apply_filters( 'get_adjacent_post_args', $args, compact( 'in_same_term', 'excluded_terms', 'previous', 'taxonony' ) );
+
+	// Get the posts and return either the post object or null
+	WP_Legacy_Adjacent_Post_Filters::add( $previous, $in_same_term, $excluded_terms );
+	$results = get_posts( $args );
+
+	if ( is_array( $results ) && ! empty( $results ) ) {
+		return array_shift( $results );
+	} else {
+		return '';
 	}
+}
 
-	$result = $wpdb->get_var( $query );
-	if ( null === $result )
-		$result = '';
+/**
+ * BACKWARDS COMPATIBILITY FOR FILTERS PREVIOUSLY IN `get_adjacent_post()`
+ *
+ * Before `get_adjacent_post()` was updated to use `WP_Query`, the SQL it produced was passed through various filters.
+ * The following class applies those filters to the relevant clauses produced by `WP_Query` via the latter's filters.
+ * The class also makes the data available that the previous filters provided.
+ */
+class WP_Legacy_Adjacent_Post_Filters {
+	private static $adjacent;
+	private static $in_same_term;
+	private static $excluded_terms;
 
-	wp_cache_set( $query_key, $result, 'counts' );
+	/**
+	 * Set up variables and filter the clauses for WP_Query
+	 *
+	 * @param bool $previous
+	 * @param bool $in_same_term
+	 * @param array $excluded_terms
+	 * @return null
+	 */
+	public static function add( $previous, $in_same_term, $excluded_terms ) {
+		self::$adjacent       = $previous ? 'previous' : 'next';
+		self::$in_same_term   = $in_same_term;
+		self::$excluded_terms = $excluded_terms;
 
-	if ( $result )
-		$result = get_post( $result );
+		add_filter( 'posts_clauses', array( __CLASS__, 'filter' ) );
+	}
 
-	return $result;
+	/**
+	 * Clean up after ourselves
+	 *
+	 * @return null
+	 */
+	public static function remove() {
+		remove_filter( 'posts_clauses', array( __CLASS__, 'filter' ) );
+	}
+
+	/**
+	 * Apply the legacy filters to WP_Query's clauses
+	 *
+	 * @param array $clauses
+	 * @uses self::remove()
+	 * @uses self::filter_join_and_where()
+	 * @uses self::filter_sort()
+	 * @filter post_clauses
+	 * @return array
+	 */
+	public static function filter( $clauses ) {
+		// Immediately deregister these legacy filters to avoid modifying any calls to WP_Query from filter callbacks hooked to WP_Query filters
+		self::remove();
+
+		// The `join` and `where` filters are identical in their parameters, so we can use the same approach for both
+		foreach ( array( 'join', 'where', ) as $clause ) {
+			if ( has_filter( 'get_' . self::$adjacent . '_post_' . $clause ) ) {
+				$clauses[ $clause ] = self::filter_join_and_where( $clauses[ $clause ], $clause );
+			}
+		}
+
+		// The legacy `sort` filter combined the ORDER BY and LIMIT clauses, while `WP_Query` does not, which requires special handling
+		if ( has_filter( 'get_' . self::$adjacent . '_post_sort' ) ) {
+			$sort_clauses = self::filter_sort( $clauses['orderby'], $clauses['limits'] );
+			$clauses      = array_merge( $clauses, $sort_clauses );
+		}
+
+		return $clauses;
+	}
+
+	/**
+	 * Apply the legacy `join` or `where` clause filter to the clauses built by WP_Query
+	 *
+	 * @param string $value
+	 * @param string $clause
+	 * @return string
+	 */
+	protected static function filter_join_and_where( $value, $clause ) {
+		return apply_filters( 'get_' . self::$adjacent . '_post_' . $clause, $value, self::$in_same_term, self::$excluded_terms );
+	}
+
+	/**
+	 * Apply legacy `sort` filter, which applies to both the ORDER BY and LIMIT clauses
+	 *
+	 * @param string $orderby
+	 * @param string $limits
+	 * @return array
+	 */
+	protected static function filter_sort( $orderby, $limits ) {
+		$sort = apply_filters( 'get_' . self::$adjacent . '_post_sort', 'ORDER BY ' . $orderby . ' ' . $limits );
+
+		if ( ! empty( $sort ) ) {
+			// The legacy filter could allow either clause to be removed, or their order inverted, so we need to know what we have and where.
+			$has_order_by = stripos( $sort, 'order by' );
+			$has_limit    = stripos( $sort, 'limit' );
+
+			// Split the string of one or two clauses into their respective array keys
+			if ( false !== $has_order_by && false !== $has_limit ) {
+				// The LIMIT clause cannot appear before the ORDER BY clause in a valid query
+				// However, since the legacy filter would allow a user to invert the order, we maintain that handling so the same errors are triggered.
+				if ( $has_order_by < $has_limit ) {
+					$orderby = trim( str_ireplace( 'order by', '', substr( $sort, 0, $has_limit ) ) );
+					$limits  = trim( substr( $sort, $has_limit ) );
+				} else {
+					$orderby = trim( str_ireplace( 'order by', '', substr( $sort, $has_order_by ) ) );
+					$limits  = trim( substr( $sort, 0, $has_order_by ) );
+				}
+			} elseif ( false !== $has_order_by ) {
+				$orderby = trim( str_ireplace( 'order by', '', $sort ) );
+				$limits  = '';
+			} elseif ( false !== $has_limit ) {
+				$orderby = '';
+				$limits  = trim( $sort );
+			}
+		}
+
+		return compact( 'orderby', 'limits' );
+	}
 }
 
 /**
Index: tests/phpunit/tests/link.php
===================================================================
--- tests/phpunit/tests/link.php	(revision 27059)
+++ tests/phpunit/tests/link.php	(working copy)
@@ -167,4 +167,119 @@
 		$this->assertEquals( array( $post_two ), get_boundary_post( true, '', true, 'post_tag' ) );
 		$this->assertEquals( array( $post_four ), get_boundary_post( true, '', false, 'post_tag' ) );
 	}
+
+	/**
+	 * @ticket 26937
+	 */
+	function test_legacy_get_adjacent_post_filters() {
+		// Need some sample posts to test adjacency
+		$post_one = $this->factory->post->create_and_get( array(
+			'post_title' => 'First',
+			'post_date' => '2012-01-01 12:00:00'
+		) );
+
+		$post_two = $this->factory->post->create_and_get( array(
+			'post_title' => 'Second',
+			'post_date' => '2012-02-01 12:00:00'
+		) );
+
+		$post_three = $this->factory->post->create_and_get( array(
+			'post_title' => 'Third',
+			'post_date' => '2012-03-01 12:00:00'
+		) );
+
+		$post_four = $this->factory->post->create_and_get( array(
+			'post_title' => 'Fourth',
+			'post_date' => '2012-04-01 12:00:00'
+		) );
+
+		// Add some meta so we can join the postmeta table and query
+		add_post_meta( $post_three->ID, 'unit_test_meta', 'waffle' );
+
+		// Test "where" filter for a previous post
+		add_filter( 'get_previous_post_where', array( $this, 'filter_previous_post_where' ) );
+		$this->go_to( get_permalink( $post_three->ID ) );
+		$this->assertEquals( $post_one->post_title, get_adjacent_post( false, null, true )->post_title );
+		remove_filter( 'get_previous_post_where', array( $this, 'filter_previous_post_where' ) );
+
+		// Test "where" filter for a next post
+		add_filter( 'get_next_post_where', array( $this, 'filter_next_post_where' ) );
+		$this->go_to( get_permalink( $post_two->ID ) );
+		$this->assertEquals( $post_four->post_title, get_adjacent_post( false, null, false )->post_title );
+		remove_filter( 'get_next_post_where', array( $this, 'filter_next_post_where' ) );
+
+		// Test "join" filter by joining the postmeta table and restricting by meta key
+		add_filter( 'get_next_post_join', array( $this, 'filter_next_post_join' ) );
+		add_filter( 'get_next_post_where', array( $this, 'filter_next_post_where_with_join' ) );
+		$this->go_to( get_permalink( $post_one->ID ) );
+		$this->assertEquals( $post_three->post_title, get_adjacent_post( false, null, false )->post_title );
+		remove_filter( 'get_next_post_join', array( $this, 'filter_next_post_join' ) );
+		remove_filter( 'get_next_post_where', array( $this, 'filter_next_post_where_with_join' ) );
+
+		// Test "sort" filter when modifying ORDER BY clause
+		add_filter( 'get_next_post_sort', array( $this, 'filter_next_post_sort' ) );
+		$this->go_to( get_permalink( $post_one->ID ) );
+		$this->assertEquals( $post_four->post_title, get_adjacent_post( false, null, false )->post_title );
+		remove_filter( 'get_next_post_sort', array( $this, 'filter_next_post_sort' ) );
+
+		// Test "sort" filter when modifying LIMIT clause
+		add_filter( 'get_next_post_sort', array( $this, 'filter_next_post_sort_limit' ) );
+		$this->go_to( get_permalink( $post_one->ID ) );
+		$this->assertEquals( $post_three->post_title, get_adjacent_post( false, null, false )->post_title );
+		remove_filter( 'get_next_post_sort', array( $this, 'filter_next_post_sort_limit' ) );
+	}
+
+	/**
+	 * Filter callback for `test_legacy_get_adjacent_post_filters()`
+	 */
+	function filter_previous_post_where( $where ) {
+		$where .= " AND post_title !='Second'";
+		return $where;
+	}
+
+	/**
+	 * Filter callback for `test_legacy_get_adjacent_post_filters()`
+	 */
+	function filter_next_post_where( $where ) {
+		$where .= " AND post_title !='Third'";
+		return $where;
+	}
+
+	/**
+	 * Filter callback for `test_legacy_get_adjacent_post_filters()`
+	 */
+	function filter_next_post_join( $join ) {
+		global $wpdb;
+
+		$join .= " INNER JOIN {$wpdb->postmeta} ON {$wpdb->posts}.ID = {$wpdb->postmeta}.post_id";
+		return $join;
+	}
+
+	/**
+	 * Filter callback for `test_legacy_get_adjacent_post_filters()`
+	 */
+	function filter_next_post_where_with_join( $where ) {
+		global $wpdb;
+
+		$where .= " AND {$wpdb->postmeta}.meta_key = 'unit_test_meta'";
+		return $where;
+	}
+
+	/**
+	 * Filter callback for `test_legacy_get_adjacent_post_filters()`
+	 */
+	function filter_next_post_sort( $sort ) {
+		global $wpdb;
+
+		$sort = str_replace( $wpdb->posts . '.post_date', $wpdb->posts . '.post_title', $sort );
+		return $sort;
+	}
+
+	/**
+	 * Filter callback for `test_legacy_get_adjacent_post_filters()`
+	 */
+	function filter_next_post_sort_limit( $sort ) {
+		$sort = str_replace( 'LIMIT 0, 1', 'LIMIT 1, 2', $sort );
+		return $sort;
+	}
 }
\ No newline at end of file
