diff --git a/src/Illuminate/Database/Query/Builder.php b/src/Illuminate/Database/Query/Builder.php index 23a7c1e8cd50..851234d06714 100755 --- a/src/Illuminate/Database/Query/Builder.php +++ b/src/Illuminate/Database/Query/Builder.php @@ -2966,10 +2966,119 @@ protected function enforceOrderBy() * Get a collection instance containing the values of a given column. * * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @param \Illuminate\Contracts\Database\Query\Expression|string|null $key + * @param string|null $key * @return \Illuminate\Support\Collection */ public function pluck($column, $key = null) + { + // First, we will need to select the results of the query accounting for the + // given columns / key. Once we have the results, we will be able to take + // the results and get the exact data that was requested for the query. + $queryResult = $this->onceWithColumns( + is_null($key) ? [$column] : [$column, $key], + function () { + return $this->processor->processSelect( + $this, $this->runSelect() + ); + } + ); + + if (empty($queryResult)) { + return collect(); + } + + // If the columns are qualified with a table or have an alias, we cannot use + // those directly in the "pluck" operations since the results from the DB + // are only keyed by the column itself. We'll strip the table out here. + $column = $this->stripTableForPluck($column); + + $key = $this->stripTableForPluck($key); + + return is_array($queryResult[0]) + ? $this->pluckFromArrayColumn($queryResult, $column, $key) + : $this->pluckFromObjectColumn($queryResult, $column, $key); + } + + /** + * Strip off the table name or alias from a column identifier. + * + * @param string $column + * @return string|null + */ + protected function stripTableForPluck($column) + { + if (is_null($column)) { + return $column; + } + + $columnString = $column instanceof ExpressionContract + ? $this->grammar->getValue($column) + : $column; + + $separator = str_contains(strtolower($columnString), ' as ') ? ' as ' : '\.'; + + return last(preg_split('~'.$separator.'~i', $columnString)); + } + + /** + * Retrieve column values from rows represented as objects. + * + * @param array $queryResult + * @param string $column + * @param string $key + * @return \Illuminate\Support\Collection + */ + protected function pluckFromObjectColumn($queryResult, $column, $key) + { + $results = []; + + if (is_null($key)) { + foreach ($queryResult as $row) { + $results[] = $row->$column; + } + } else { + foreach ($queryResult as $row) { + $results[$row->$key] = $row->$column; + } + } + + return collect($results); + } + + /** + * Retrieve column values from rows represented as arrays. + * + * @param array $queryResult + * @param string $column + * @param string $key + * @return \Illuminate\Support\Collection + */ + protected function pluckFromArrayColumn($queryResult, $column, $key) + { + $results = []; + + if (is_null($key)) { + foreach ($queryResult as $row) { + $results[] = $row[$column]; + } + } else { + foreach ($queryResult as $row) { + $results[$row[$key]] = $row[$column]; + } + } + + return collect($results); + } + + /** + * Get a collection instance containing the values of a given column + * and an optional custom key using an efficient PDO fetch mode. + * + * @param \Illuminate\Contracts\Database\Query\Expression|string $column + * @param \Illuminate\Contracts\Database\Query\Expression|string|null $key + * @return \Illuminate\Support\Collection + */ + public function pluckPDO($column, $key = null) { // A single column can be fetched efficiently using FETCH COLUMN. A combination // with a key is done using a bitwise-or with FETCH_UNIQUE. @@ -2977,7 +3086,7 @@ public function pluck($column, $key = null) return collect($this->onceWithColumns( is_null($key) ? [$column] : [$key, $column], - fn () => $this->onceWithFetchAllArgs($mode, function () { + fn () => $this->onceWithFetchAllArgs([$mode, is_null($key) ? 0 : 1], function () { return $this->processor->processSelect( $this, $this->runSelect() ); diff --git a/tests/Integration/Database/QueryBuilderTest.php b/tests/Integration/Database/QueryBuilderTest.php index a52b03d45c22..ab56a62a5010 100644 --- a/tests/Integration/Database/QueryBuilderTest.php +++ b/tests/Integration/Database/QueryBuilderTest.php @@ -9,6 +9,7 @@ use Illuminate\Support\Carbon; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; +use PHPUnit\Framework\Attributes\DataProvider; class QueryBuilderTest extends DatabaseTestCase { @@ -25,6 +26,23 @@ protected function defineDatabaseMigrationsAfterDatabaseRefreshed() ['title' => 'Foo Post', 'content' => 'Lorem Ipsum.', 'created_at' => new Carbon('2017-11-12 13:14:15')], ['title' => 'Bar Post', 'content' => 'Lorem Ipsum.', 'created_at' => new Carbon('2018-01-02 03:04:05')], ]); + + Schema::create('comments', function (Blueprint $table) { + $table->increments('id'); + $table->foreignId('post_id'); + $table->text('content'); + $table->string('tag')->nullable(); + $table->integer('votes')->nullable(); + $table->timestamp('created_at'); + }); + + DB::table('comments')->insert([ + ['post_id' => 1, 'content' => 'Lorem Ipsum a.', 'tag' => 'science', 'votes' => 1, 'created_at' => new Carbon('2023-01-01 13:14:15')], + ['post_id' => 2, 'content' => 'Lorem Ipsum b.', 'tag' => 'science', 'votes' => 0, 'created_at' => new Carbon('2023-05-14 23:59:59')], + ['post_id' => 2, 'content' => 'Lorem Ipsum c.', 'tag' => 'entertainment', 'votes' => null, 'created_at' => new Carbon('2023-02-03 17:49:14')], + ['post_id' => 1, 'content' => 'Lorem Ipsum d.', 'tag' => null, 'votes' => null, 'created_at' => new Carbon('2023-04-27 20:00:05')], + ['post_id' => 1, 'content' => 'Lorem Ipsum e.', 'tag' => '', 'votes' => null, 'created_at' => new Carbon('2022-09-22 14:30:05')], + ]); } public function testIncrement() @@ -399,27 +417,29 @@ public function testChunkMap() $this->assertCount(3, DB::getQueryLog()); } - public function testPluck() + + #[DataProvider('pluckProvider')] + public function testPluck(string $pluckFn): void { // Test SELECT override, since pluck will take the first column. $this->assertSame([ 'Foo Post', 'Bar Post', - ], DB::table('posts')->select(['content', 'id', 'title'])->pluck('title')->toArray()); + ], DB::table('posts')->select(['content', 'id', 'title'])->$pluckFn('title')->toArray()); // Test without SELECT override. $this->assertSame([ 'Foo Post', 'Bar Post', - ], DB::table('posts')->pluck('title')->toArray()); + ], DB::table('posts')->$pluckFn('title')->toArray()); // Test specific key. $this->assertSame([ 1 => 'Foo Post', 2 => 'Bar Post', - ], DB::table('posts')->pluck('title', 'id')->toArray()); + ], DB::table('posts')->$pluckFn('title', 'id')->toArray()); - $results = DB::table('posts')->pluck('title', 'created_at'); + $results = DB::table('posts')->$pluckFn('title', 'created_at'); // Test timestamps (truncates RDBMS differences). $this->assertSame([ @@ -434,15 +454,47 @@ public function testPluck() // Test duplicate keys (a match will override a previous match). $this->assertSame([ 'Lorem Ipsum.' => 'Bar Post', - ], DB::table('posts')->pluck('title', 'content')->toArray()); + ], DB::table('posts')->$pluckFn('title', 'content')->toArray()); // Test custom query calculations. $this->assertSame([ 2 => 'FOO POST', 4 => 'BAR POST', - ], DB::table('posts')->pluck( + ], DB::table('posts')->$pluckFn( DB::raw('UPPER(title)'), DB::raw('2 * id') )->toArray()); + + // Test null and empty string as key. + $this->assertSame([ + 'science' => 'Lorem Ipsum b.', + 'entertainment' => 'Lorem Ipsum c.', + null => 'Lorem Ipsum d.', + '' => 'Lorem Ipsum e.', + ], DB::table('comments')->$pluckFn('content', 'tag')->toArray()); + + // Test null and numeric as key. + $this->assertSame([ + 1 => 'Lorem Ipsum a.', + 0 => 'Lorem Ipsum b.', + null => 'Lorem Ipsum e.', + ], DB::table('comments')->$pluckFn('content', 'votes')->toArray()); + + // Test null and numeric values with string keys. + $this->assertSame([ + 'Lorem Ipsum a.' => 1, + 'Lorem Ipsum b.' => 0, + 'Lorem Ipsum c.' => null, + 'Lorem Ipsum d.' => null, + 'Lorem Ipsum e.' => null, + ], DB::table('comments')->$pluckFn('votes', 'content')->toArray()); + } + + public static function pluckProvider(): array + { + return [ + ['pluck'], + ['pluckPDO'], + ]; } }