Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 94 additions & 5 deletions src/QueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -797,14 +797,103 @@ public function first(string ...$columns): mixed
}

/**
* Retrieve a single column from the result
* Get a collection of values from a given column.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Outdated comment

*
* Like Laravel's pluck, this method temporarily sets the select columns
* only if no explicit select() was previously called (via onceWithColumns).
* It then strips table prefixes / aliases from the column name to correctly
* extract values from the result set.
*
* @param Expression|string $column The column to pluck values from.
* @param Expression|string|null $key Optional column to use as keys in the returned collection.
* @return array
* @throws QueryBuilderException
*/
public function pluck(string $column): array
public function pluck(Expression|string $column, Expression|string|null $key = null): array
{
$this->select($column);
$result = $this->get();
return $result->pluck($column);
$queryResult = $this->onceWithColumns(
is_null($key) || $key === $column ? [$column] : [$column, $key],
function () {
return $this->get();
}
);

if ($queryResult->isEmpty()) {
return [];
}

// Strip table prefix or alias so we can look up the column in the result rows.
$column = $this->stripTableForPluck($column);
$key = $this->stripTableForPluck($key);

$results = [];
foreach ($queryResult as $row) {
$row = (array)$row;
$value = $row[$column] ?? null;
if (is_null($key)) {
$results[] = $value;
} else {
$results[$row[$key] ?? null] = $value;
}
}

return $results;
}

/**
* Execute the given callback while selecting the given columns.
*
* If columns have already been explicitly set via select(), they are preserved.
* After running the callback, the columns are restored to their original value.
*
* @param array $columns Columns to use if none were set.
* @param callable $callback The callback that executes the query.
* @return mixed The result of the callback.
*/
private function onceWithColumns(array $columns, callable $callback): mixed
{
$original = $this->columns;

if (empty($original)) {
$this->columns = $columns;
}

$result = $callback();

$this->columns = $original;

return $result;
}

/**
* Strip off the table name or alias from a column identifier.
*
* Handles both Expression objects and plain strings.
* Examples:
* "users.id" → "id"
* "users.id AS user_id" → "user_id"
* "id AS user_id" → "user_id"
*
* @param Expression|string|null $column
* @return string|null
*/
private function stripTableForPluck(Expression|string|null $column): ?string
{
if (is_null($column)) {
return null;
}

$columnString = $column instanceof Expression
? $column->getValue()
: $column;

// If the column contains " AS " (case-insensitive), split on that.
// Otherwise, split on "." to strip the table prefix.
$separator = str_contains(strtolower($columnString), ' as ') ? ' as ' : '\.';

$parts = preg_split('~' . $separator . '~i', $columnString);

return end($parts);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Strip quotes from aliases in pluck column parsing

When pluck is called with an aliased expression that uses quoted identifiers (e.g. Expression::make('id AS "user_id"')), stripTableForPluck returns the alias with quotes still attached, so the subsequent $row[$column] lookup misses because PDO result keys are unquoted. In that scenario this method returns a collection of null values instead of the selected data, which is a functional regression for quoted Postgres/MySQL alias usage.

Useful? React with 👍 / 👎.

}

/**
Expand Down
63 changes: 63 additions & 0 deletions tests/MysqlDialectTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
use Abdulelahragih\QueryBuilder\Builders\JoinClauseBuilder;
use Abdulelahragih\QueryBuilder\Builders\WhereQueryBuilder;
use Abdulelahragih\QueryBuilder\Data\QueryBuilderException;
use Abdulelahragih\QueryBuilder\Data\Collection;
use Abdulelahragih\QueryBuilder\Grammar\Expression;
use Abdulelahragih\QueryBuilder\QueryBuilder;
use Abdulelahragih\QueryBuilder\Tests\Traits\TestTrait;
use Error;
Expand Down Expand Up @@ -417,9 +419,70 @@ public function testPluck()
->table('users')
->limit(3)
->pluck('id');
$this->assertIsArray($result);
$this->assertEquals([1, 2, 3], $result);
}

public function testPluckWithTablePrefix()
{
$builder = new QueryBuilder($this->pdo);
$result = $builder
->table('users')
->pluck('users.id');
$this->assertEquals([1, 2, 3], $result);
}

public function testPluckPreservesExistingSelect()
{
$builder = new QueryBuilder($this->pdo);
$result = $builder
->table('users')
->select($builder->raw('id AS user_id'))
->pluck('user_id');
$this->assertEquals([1, 2, 3], $result);
}

public function testPluckWithKey()
{
$builder = new QueryBuilder($this->pdo);
$result = $builder
->table('users')
->pluck('name', 'id');
$this->assertEquals([1 => 'Sam', 2 => 'John', 3 => 'Jane'], $result);
}

public function testPluckEmptyResult()
{
$builder = new QueryBuilder($this->pdo);
$result = $builder
->table('users')
->where('id', '=', 999)
->pluck('id');
$this->assertIsArray($result);
$this->assertEmpty($result);
}

public function testPluckWithExpression()
{
$builder = new QueryBuilder($this->pdo);
$result = $builder
->table('users')
->limit(3)
->pluck(Expression::make('id'));
$this->assertEquals([1, 2, 3], $result);
}

public function testPluckWithJoinAndAlias()
{
$builder = new QueryBuilder($this->pdo);
$result = $builder
->table('posts')
->join('users', 'users.id', '=', 'posts.user_id')
->select($builder->raw('posts.user_id AS uid'))
->pluck('uid');
$this->assertEquals([1, 1, 2], $result);
}

public function testSingleInsert()
{
$builder = new QueryBuilder($this->pdo);
Expand Down
62 changes: 62 additions & 0 deletions tests/PostgresQueryBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Abdulelahragih\QueryBuilder\Builders\JoinClauseBuilder;
use Abdulelahragih\QueryBuilder\Builders\WhereQueryBuilder;
use Abdulelahragih\QueryBuilder\Data\QueryBuilderException;
use Abdulelahragih\QueryBuilder\Data\Collection;
use Abdulelahragih\QueryBuilder\DB;
use Abdulelahragih\QueryBuilder\Grammar\Dialects\PostgresDialect;
use Abdulelahragih\QueryBuilder\Grammar\Expression;
Expand Down Expand Up @@ -439,9 +440,70 @@ public function testPluck()
->table('users')
->limit(3)
->pluck('id');
$this->assertIsArray($result);
$this->assertEquals([1, 2, 3], $result);
}

public function testPluckWithTablePrefix()
{
$builder = new QueryBuilder($this->pdo, new PostgresDialect());
$result = $builder
->table('users')
->pluck('users.id');
$this->assertEquals([1, 2, 3], $result);
}

public function testPluckPreservesExistingSelect()
{
$builder = new QueryBuilder($this->pdo, new PostgresDialect());
$result = $builder
->table('users')
->select($builder->raw('id AS user_id'))
->pluck('user_id');
$this->assertEquals([1, 2, 3], $result);
}

public function testPluckWithKey()
{
$builder = new QueryBuilder($this->pdo, new PostgresDialect());
$result = $builder
->table('users')
->pluck('name', 'id');
$this->assertEquals([1 => 'Sam', 2 => 'John', 3 => 'Jane'], $result);
}

public function testPluckEmptyResult()
{
$builder = new QueryBuilder($this->pdo, new PostgresDialect());
$result = $builder
->table('users')
->where('id', '=', 999)
->pluck('id');
$this->assertIsArray($result);
$this->assertEmpty($result);
}

public function testPluckWithExpression()
{
$builder = new QueryBuilder($this->pdo, new PostgresDialect());
$result = $builder
->table('users')
->limit(3)
->pluck(Expression::make('id'));
$this->assertEquals([1, 2, 3], $result);
}

public function testPluckWithJoinAndAlias()
{
$builder = new QueryBuilder($this->pdo, new PostgresDialect());
$result = $builder
->table('posts')
->join('users', 'users.id', '=', 'posts.user_id')
->select($builder->raw('posts.user_id AS uid'))
->pluck('uid');
$this->assertEquals([1, 1, 2], $result);
}

public function testSingleInsert()
{
$builder = new QueryBuilder($this->pdo, new PostgresDialect());
Expand Down
13 changes: 13 additions & 0 deletions tests/Traits/TestTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,18 @@ private function seedFakeData()
$this->pdo->exec("INSERT INTO users (id, name) VALUES (1, 'Sam');");
$this->pdo->exec("INSERT INTO users (id, name) VALUES (2, 'John');");
$this->pdo->exec("INSERT INTO users (id, name) VALUES (3, 'Jane');");

// create table posts for join tests
$this->pdo->exec('
CREATE TABLE IF NOT EXISTS posts (
id INT PRIMARY KEY,
user_id INT,
title VARCHAR(255)
);
');
$this->pdo->exec('DELETE FROM posts');
$this->pdo->exec("INSERT INTO posts (id, user_id, title) VALUES (1, 1, 'Post 1');");
$this->pdo->exec("INSERT INTO posts (id, user_id, title) VALUES (2, 1, 'Post 2');");
$this->pdo->exec("INSERT INTO posts (id, user_id, title) VALUES (3, 2, 'Post 3');");
}
}