Skip to content

Commit

Permalink
add pluckPDO
Browse files Browse the repository at this point in the history
  • Loading branch information
bert-w committed Nov 1, 2023
1 parent 7ed1f3d commit 8257a26
Show file tree
Hide file tree
Showing 2 changed files with 170 additions and 9 deletions.
113 changes: 111 additions & 2 deletions src/Illuminate/Database/Query/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -2966,18 +2966,127 @@ 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.
$mode = \PDO::FETCH_COLUMN | (is_null($key) ? 0 : \PDO::FETCH_UNIQUE);

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()
);
Expand Down
66 changes: 59 additions & 7 deletions tests/Integration/Database/QueryBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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()
Expand Down Expand Up @@ -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([
Expand All @@ -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'],
];
}
}

0 comments on commit 8257a26

Please sign in to comment.