Index: src/wp-includes/class-walker-pad-term-counts.php
===================================================================
--- src/wp-includes/class-walker-pad-term-counts.php	(revision 0)
+++ src/wp-includes/class-walker-pad-term-counts.php	(working copy)
@@ -0,0 +1,160 @@
+<?php
+/**
+ * Taxonomy API: Walker_Pad_Term_Counts class
+ *
+ * @package WordPress
+ * @subpackage Template
+ * @since 5.7.0
+ */
+
+/**
+ * Core class used to pad term counts.
+ *
+ * @since 5.7.0
+ *
+ * @see Walker
+ */
+class Walker_Pad_Term_Counts {
+
+	/**
+	 * Holds all the ancestors of the current term.
+	 *
+	 * @since 5.7.0
+	 * @var array
+	 */
+	public $ancestors = array();
+
+	/**
+	 * Adds new term to the ancestors list.
+	 *
+	 * @since 5.7.0
+	 *
+	 * @param object $term The data object.
+	 */
+	public function start_lvl( &$term ) {
+		array_push( $this->ancestors, $term );
+	}
+
+	/**
+	 * Removes the last term from the ancestors list.
+	 *
+	 * @since 5.7.0
+	 *
+	 * @param object $term The data object.
+	 */
+	public function end_lvl( $term ) {
+		array_pop( $this->ancestors );
+	}
+
+	/**
+	 * Add terms count to all it's ancestors.
+	 *
+	 * @since 5.7.0
+	 *
+	 * @param object $term The data object.
+	 */
+	public function count_term( $term ) {
+		foreach ( $this->ancestors as &$ancestor ) {
+			$ancestor->count += $term->count;
+		}
+	}
+
+	/**
+	 * Traverse terms to add descendants counts to their ancestors.
+	 *
+	 * This method should not be called directly, use the walk() method instead.
+	 *
+	 * @since 5.7.0
+	 *
+	 * @param object $term           Data object.
+	 * @param array  $children_terms List of terms to continue traversing (passed by reference).
+	 */
+	public function process_term( $term, &$children_terms ) {
+		if ( ! $term ) {
+			return;
+		}
+
+		$id = $term->term_id;
+
+		$this->count_term( $term );
+
+		// Descend only when there are children for this term.
+		if ( isset( $children_terms[ $id ] ) ) {
+
+			foreach ( $children_terms[ $id ] as $child ) {
+
+				if ( ! isset( $newlevel ) ) {
+					$newlevel = true;
+					// Start the new level.
+					$this->start_lvl( $term );
+				}
+				$this->process_term( $child, $children_terms );
+			}
+			unset( $children_terms[ $id ] );
+		}
+
+		if ( isset( $newlevel ) && $newlevel ) {
+			// End the child level.
+			$this->end_lvl( $term );
+		}
+	}
+
+	/**
+	 * Process the array of terms hierarchically.
+	 *
+	 * Does not assume any existing order of terms.
+	 *
+	 * @since 5.7.0
+	 *
+	 * @param array $terms An array of terms.
+	 */
+	public function walk( $terms, $debug = false ) {
+		// Invalid parameter or nothing to walk.
+		if ( empty( $terms ) ) {
+			return;
+		}
+
+		/*
+		 * Separate terms into two buckets: top level and children terms.
+		 * Children_terms is two dimensional array, eg.
+		 * Children_terms[10][] contains all sub-terms whose parent is 10.
+		 */
+		$top_level_terms = array();
+		$children_terms  = array();
+		foreach ( $terms as $t ) {
+			if ( empty( $t->parent ) ) {
+				$top_level_terms[] = $t;
+			} else {
+				$children_terms[ $t->parent ][] = $t;
+			}
+		}
+
+		/**
+		 * In case there are no children, there is nothing to do here.
+		 */
+		if ( empty( $children_terms ) ) {
+			return;
+		}
+
+		/*
+		 * When none of the terms is top level.
+		 * Assume the terms with no parent in among terms passed to the method are top level terms.
+		 */
+		if ( empty( $top_level_terms ) ) {
+			$all_term_ids = wp_list_pluck( $terms, 'term_id' );
+			foreach ( $children_terms as $parent => $ts ) {
+				foreach ( $ts as $key => $t ) {
+					if ( ! in_array( $parent, $all_term_ids, true ) ) {
+						$top_level_terms[] = $t;
+						unset( $children_terms[ $parent ][ $key ] );
+					}
+				}
+			}
+		}
+
+		// Process the terms.
+		foreach ( $top_level_terms as $t ) {
+			$this->process_term( $t, $children_terms );
+		}
+	}
+}

Property changes on: src/wp-includes/class-walker-pad-term-counts.php
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: svn:executable
## -0,0 +1 ##
+*
\ No newline at end of property
Index: src/wp-includes/taxonomy.php
===================================================================
--- src/wp-includes/taxonomy.php	(revision 49508)
+++ src/wp-includes/taxonomy.php	(working copy)
@@ -3738,14 +3738,10 @@
  * @access private
  * @since 2.3.0
  *
- * @global wpdb $wpdb WordPress database abstraction object.
- *
  * @param array  $terms    List of term objects (passed by reference).
  * @param string $taxonomy Term context.
  */
 function _pad_term_counts( &$terms, $taxonomy ) {
-	global $wpdb;
-
 	// This function only works for hierarchical taxonomies like post categories.
 	if ( ! is_taxonomy_hierarchical( $taxonomy ) ) {
 		return;
@@ -3757,53 +3753,8 @@
 		return;
 	}
 
-	$term_items  = array();
-	$terms_by_id = array();
-	$term_ids    = array();
-
-	foreach ( (array) $terms as $key => $term ) {
-		$terms_by_id[ $term->term_id ]       = & $terms[ $key ];
-		$term_ids[ $term->term_taxonomy_id ] = $term->term_id;
-	}
-
-	// Get the object and term IDs and stick them in a lookup table.
-	$tax_obj      = get_taxonomy( $taxonomy );
-	$object_types = esc_sql( $tax_obj->object_type );
-	$results      = $wpdb->get_results( "SELECT object_id, term_taxonomy_id FROM $wpdb->term_relationships INNER JOIN $wpdb->posts ON object_id = ID WHERE term_taxonomy_id IN (" . implode( ',', array_keys( $term_ids ) ) . ") AND post_type IN ('" . implode( "', '", $object_types ) . "') AND post_status = 'publish'" );
-
-	foreach ( $results as $row ) {
-		$id = $term_ids[ $row->term_taxonomy_id ];
-
-		$term_items[ $id ][ $row->object_id ] = isset( $term_items[ $id ][ $row->object_id ] ) ? ++$term_items[ $id ][ $row->object_id ] : 1;
-	}
-
-	// Touch every ancestor's lookup row for each post in each term.
-	foreach ( $term_ids as $term_id ) {
-		$child     = $term_id;
-		$ancestors = array();
-		while ( ! empty( $terms_by_id[ $child ] ) && $parent = $terms_by_id[ $child ]->parent ) {
-			$ancestors[] = $child;
-
-			if ( ! empty( $term_items[ $term_id ] ) ) {
-				foreach ( $term_items[ $term_id ] as $item_id => $touches ) {
-					$term_items[ $parent ][ $item_id ] = isset( $term_items[ $parent ][ $item_id ] ) ? ++$term_items[ $parent ][ $item_id ] : 1;
-				}
-			}
-
-			$child = $parent;
-
-			if ( in_array( $parent, $ancestors, true ) ) {
-				break;
-			}
-		}
-	}
-
-	// Transfer the touched cells.
-	foreach ( (array) $term_items as $id => $items ) {
-		if ( isset( $terms_by_id[ $id ] ) ) {
-			$terms_by_id[ $id ]->count = count( $items );
-		}
-	}
+	$walker = new Walker_Pad_Term_Counts();
+	$walker->walk( $terms );
 }
 
 /**
Index: src/wp-settings.php
===================================================================
--- src/wp-settings.php	(revision 49508)
+++ src/wp-settings.php	(working copy)
@@ -213,6 +213,7 @@
 require ABSPATH . WPINC . '/class-wp-term.php';
 require ABSPATH . WPINC . '/class-wp-term-query.php';
 require ABSPATH . WPINC . '/class-wp-tax-query.php';
+require ABSPATH . WPINC . '/class-walker-pad-term-counts.php';
 require ABSPATH . WPINC . '/update.php';
 require ABSPATH . WPINC . '/canonical.php';
 require ABSPATH . WPINC . '/shortcodes.php';
Index: tests/phpunit/tests/taxonomy/padTermCounts.php
===================================================================
--- tests/phpunit/tests/taxonomy/padTermCounts.php	(revision 0)
+++ tests/phpunit/tests/taxonomy/padTermCounts.php	(working copy)
@@ -0,0 +1,122 @@
+<?php
+
+/**
+ * @group taxonomy
+ */
+class Tests_Taxonomy_Pad_Term_Counts extends WP_UnitTestCase {
+
+	public function test_no_child_terms() {
+		$terms = self::factory()->term->create_many( 2, array( 'taxonomy' => 'category' ) );
+
+		$terms = array_map( 'get_term', $terms );
+		_pad_term_counts( $terms, 'category' );
+
+		foreach( $terms as $term ) {
+			$this->assertEquals( 0, $term->count );
+		}
+	}
+
+	public function test_single_post_in_a_category() {
+		$terms           = array();
+		$terms['parent'] = self::create_term_with_that_many_posts( 1, array( 'taxonomy' => 'category' ) );
+
+		$terms = array_map( 'get_term', $terms );
+		_pad_term_counts( $terms, 'category' );
+
+		$this->assertEquals( 1, $terms['parent']->count );
+	}
+
+	public function test_single_child() {
+		$terms = array();
+		$terms['parent'] = self::factory()->term->create( array( 'taxonomy' => 'category' ) );
+		$terms['child']  = self::create_term_with_that_many_posts( 1, array( 'taxonomy' => 'category', 'parent' => $terms['parent'] ) );
+
+		$terms = array_map( 'get_term', $terms );
+		_pad_term_counts( $terms, 'category' );
+
+		foreach( $terms as $term ) {
+			$this->assertEquals( 1, $term->count );
+		}
+	}
+
+	public function test_single_grandchild() {
+		$terms               = array();
+		$terms['parent']     = self::factory()->term->create( array( 'taxonomy' => 'category' ) );
+		$terms['child']      = self::factory()->term->create( array( 'taxonomy' => 'category', 'parent' => $terms['parent'] ) );
+		$terms['grandchild'] = self::create_term_with_that_many_posts( 1, array( 'taxonomy' => 'category', 'parent' => $terms['child'] ) );
+
+		$terms = array_map( 'get_term', $terms );
+		_pad_term_counts( $terms, 'category' );
+
+		foreach( $terms as $term ) {
+			$this->assertEquals( 1, $term->count );
+		}
+	}
+
+	public function test_both_parent_and_grandchild() {
+		$terms = array();
+		$terms['parent']      = self::create_term_with_that_many_posts( 1, array( 'taxonomy' => 'category', 'parent' => 0 ) );
+		$terms['child']       = self::factory()->term->create( array( 'taxonomy' => 'category', 'parent' => $terms['parent'] ) );
+		$terms['grandchild']  = self::create_term_with_that_many_posts( 1, array( 'taxonomy' => 'category', 'parent' => $terms['child'] ) );
+
+		$terms = array_map( 'get_term', $terms );
+		_pad_term_counts( $terms, 'category' );
+
+		$this->assertEquals( 2, $terms['parent']->count );
+		$this->assertEquals( 1, $terms['child']->count );
+		$this->assertEquals( 1, $terms['grandchild']->count );
+	}
+
+	public function test_many_children_and_grandchildren() {
+		$terms = array();
+		$terms['parent']        = self::create_term_with_that_many_posts( 1, array( 'taxonomy' => 'category', 'parent' => 0 ) );
+
+		$terms['child_0_posts'] = self::factory()->term->create( array( 'taxonomy' => 'category', 'parent' => $terms['parent'] ) );
+		$terms['grandchild_3_posts'] = self::create_term_with_that_many_posts( 3, array( 'taxonomy' => 'category', 'parent' => $terms['child_0_posts'] ) );
+
+		$terms['child_2_posts'] = self::create_term_with_that_many_posts( 2, array( 'taxonomy' => 'category', 'parent' => $terms['parent'] ) );
+		$terms['grandchild_2_posts'] = self::create_term_with_that_many_posts( 2, array( 'taxonomy' => 'category', 'parent' => $terms['child_2_posts'] ) );
+		$terms['grandchild_1_posts'] = self::create_term_with_that_many_posts( 1, array( 'taxonomy' => 'category', 'parent' => $terms['child_2_posts'] ) );
+
+		$terms['grandgrandchild_1_posts'] = self::create_term_with_that_many_posts( 1, array( 'taxonomy' => 'category', 'parent' => $terms['grandchild_1_posts'] ) );
+
+		$terms = array_map( 'get_term', $terms );
+		_pad_term_counts( $terms, 'category' );
+
+		$this->assertEquals( 10, $terms['parent']->count );
+		$this->assertEquals( 3, $terms['child_0_posts']->count );
+		$this->assertEquals( 3, $terms['grandchild_3_posts']->count );
+		$this->assertEquals( 6, $terms['child_2_posts']->count );
+		$this->assertEquals( 2, $terms['grandchild_2_posts']->count );
+		$this->assertEquals( 2, $terms['grandchild_1_posts']->count );
+		$this->assertEquals( 1, $terms['grandgrandchild_1_posts']->count );
+	}
+
+	public function test_no_top_level_terms() {
+		$terms = array();
+		$parent = self::factory()->term->create( array( 'taxonomy' => 'category', 'parent' => 0 ) );
+		$terms['child_0_posts'] = self::factory()->term->create( array( 'taxonomy' => 'category', 'parent' => $parent ) );
+		$terms['grandchild_3_posts'] = self::create_term_with_that_many_posts( 3, array( 'taxonomy' => 'category', 'parent' => $terms['child_0_posts'] ) );
+
+		$terms['child_2_posts'] = self::create_term_with_that_many_posts( 2, array( 'taxonomy' => 'category', 'parent' => $parent ) );
+		$terms['grandchild_2_posts'] = self::create_term_with_that_many_posts( 2, array( 'taxonomy' => 'category', 'parent' => $terms['child_2_posts'] ) );
+
+		$terms = array_map( 'get_term', $terms );
+		_pad_term_counts( $terms, 'category' );
+
+		$parent = get_term( $parent );
+		$this->assertEquals( 0, $parent->count );
+		$this->assertEquals( 3, $terms['child_0_posts']->count );
+		$this->assertEquals( 3, $terms['grandchild_3_posts']->count );
+		$this->assertEquals( 4, $terms['child_2_posts']->count );
+		$this->assertEquals( 2, $terms['grandchild_2_posts']->count );
+	}
+
+	private static function create_term_with_that_many_posts( $posts_no, $args ) {
+		$term  = self::factory()->term->create( $args );
+		if ( 0 < $posts_no ) {
+			$posts = self::factory()->post->create_many( $posts_no, array( 'post_category' => (array) $term ) );
+		}
+		return $term;
+	}
+}

Property changes on: tests/phpunit/tests/taxonomy/padTermCounts.php
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: svn:executable
## -0,0 +1 ##
+*
\ No newline at end of property
