From 1766a1b589b23b6cd7a57bcfe022f4ce812b332e Mon Sep 17 00:00:00 2001 From: LancelotProgrammer Date: Thu, 26 Feb 2026 15:36:51 +0300 Subject: [PATCH 1/2] feat: enhance pluck method to support key-value pairs and handle table prefixes and add tests for various pluck scenarios --- src/QueryBuilder.php | 99 ++++++++++++++++++++++++++++-- tests/MysqlDialectTest.php | 65 +++++++++++++++++++- tests/PostgresQueryBuilderTest.php | 64 ++++++++++++++++++- tests/Traits/TestTrait.php | 13 ++++ 4 files changed, 234 insertions(+), 7 deletions(-) diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index d4f89e6..1cd7db5 100644 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -788,14 +788,103 @@ public function first(string ...$columns): mixed } /** - * Retrieve a single column from the result + * Get a collection of values from a given column. + * + * 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 Collection * @throws QueryBuilderException */ - public function pluck(string $column): array + public function pluck(Expression|string $column, Expression|string|null $key = null): Collection { - $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 new Collection(); + } + + // 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 new Collection($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); } /** diff --git a/tests/MysqlDialectTest.php b/tests/MysqlDialectTest.php index 2ef026f..aae5fa8 100644 --- a/tests/MysqlDialectTest.php +++ b/tests/MysqlDialectTest.php @@ -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; @@ -356,7 +358,68 @@ public function testPluck() ->table('users') ->limit(3) ->pluck('id'); - $this->assertEquals([1, 2, 3], $result); + $this->assertInstanceOf(Collection::class, $result); + $this->assertEquals([1, 2, 3], $result->toArray()); + } + + public function testPluckWithTablePrefix() + { + $builder = new QueryBuilder($this->pdo); + $result = $builder + ->table('users') + ->pluck('users.id'); + $this->assertEquals([1, 2, 3], $result->toArray()); + } + + 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->toArray()); + } + + public function testPluckWithKey() + { + $builder = new QueryBuilder($this->pdo); + $result = $builder + ->table('users') + ->pluck('name', 'id'); + $this->assertEquals([1 => 'Sam', 2 => 'John', 3 => 'Jane'], $result->jsonSerialize()); + } + + public function testPluckEmptyResult() + { + $builder = new QueryBuilder($this->pdo); + $result = $builder + ->table('users') + ->where('id', '=', 999) + ->pluck('id'); + $this->assertInstanceOf(Collection::class, $result); + $this->assertTrue($result->isEmpty()); + } + + public function testPluckWithExpression() + { + $builder = new QueryBuilder($this->pdo); + $result = $builder + ->table('users') + ->limit(3) + ->pluck(Expression::make('id')); + $this->assertEquals([1, 2, 3], $result->toArray()); + } + + 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->toArray()); } public function testSingleInsert() diff --git a/tests/PostgresQueryBuilderTest.php b/tests/PostgresQueryBuilderTest.php index 618b166..00cd079 100644 --- a/tests/PostgresQueryBuilderTest.php +++ b/tests/PostgresQueryBuilderTest.php @@ -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; @@ -378,7 +379,68 @@ public function testPluck() ->table('users') ->limit(3) ->pluck('id'); - $this->assertEquals([1, 2, 3], $result); + $this->assertInstanceOf(Collection::class, $result); + $this->assertEquals([1, 2, 3], $result->toArray()); + } + + public function testPluckWithTablePrefix() + { + $builder = new QueryBuilder($this->pdo, new PostgresDialect()); + $result = $builder + ->table('users') + ->pluck('users.id'); + $this->assertEquals([1, 2, 3], $result->toArray()); + } + + 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->toArray()); + } + + 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->jsonSerialize()); + } + + public function testPluckEmptyResult() + { + $builder = new QueryBuilder($this->pdo, new PostgresDialect()); + $result = $builder + ->table('users') + ->where('id', '=', 999) + ->pluck('id'); + $this->assertInstanceOf(Collection::class, $result); + $this->assertTrue($result->isEmpty()); + } + + 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->toArray()); + } + + 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->toArray()); } public function testSingleInsert() diff --git a/tests/Traits/TestTrait.php b/tests/Traits/TestTrait.php index bf655a5..fc8fff9 100644 --- a/tests/Traits/TestTrait.php +++ b/tests/Traits/TestTrait.php @@ -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');"); } } From a450ae78c30e347976d03c6e2cc173c407cfa888 Mon Sep 17 00:00:00 2001 From: LancelotProgrammer Date: Thu, 26 Feb 2026 16:05:03 +0300 Subject: [PATCH 2/2] refactor: change the return type of the pluck method to an array --- src/QueryBuilder.php | 8 ++++---- tests/MysqlDialectTest.php | 18 +++++++++--------- tests/PostgresQueryBuilderTest.php | 18 +++++++++--------- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index 1cd7db5..093b820 100644 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -797,10 +797,10 @@ public function first(string ...$columns): mixed * * @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 Collection + * @return array * @throws QueryBuilderException */ - public function pluck(Expression|string $column, Expression|string|null $key = null): Collection + public function pluck(Expression|string $column, Expression|string|null $key = null): array { $queryResult = $this->onceWithColumns( is_null($key) || $key === $column ? [$column] : [$column, $key], @@ -810,7 +810,7 @@ function () { ); if ($queryResult->isEmpty()) { - return new Collection(); + return []; } // Strip table prefix or alias so we can look up the column in the result rows. @@ -828,7 +828,7 @@ function () { } } - return new Collection($results); + return $results; } /** diff --git a/tests/MysqlDialectTest.php b/tests/MysqlDialectTest.php index aae5fa8..585e6e3 100644 --- a/tests/MysqlDialectTest.php +++ b/tests/MysqlDialectTest.php @@ -358,8 +358,8 @@ public function testPluck() ->table('users') ->limit(3) ->pluck('id'); - $this->assertInstanceOf(Collection::class, $result); - $this->assertEquals([1, 2, 3], $result->toArray()); + $this->assertIsArray($result); + $this->assertEquals([1, 2, 3], $result); } public function testPluckWithTablePrefix() @@ -368,7 +368,7 @@ public function testPluckWithTablePrefix() $result = $builder ->table('users') ->pluck('users.id'); - $this->assertEquals([1, 2, 3], $result->toArray()); + $this->assertEquals([1, 2, 3], $result); } public function testPluckPreservesExistingSelect() @@ -378,7 +378,7 @@ public function testPluckPreservesExistingSelect() ->table('users') ->select($builder->raw('id AS user_id')) ->pluck('user_id'); - $this->assertEquals([1, 2, 3], $result->toArray()); + $this->assertEquals([1, 2, 3], $result); } public function testPluckWithKey() @@ -387,7 +387,7 @@ public function testPluckWithKey() $result = $builder ->table('users') ->pluck('name', 'id'); - $this->assertEquals([1 => 'Sam', 2 => 'John', 3 => 'Jane'], $result->jsonSerialize()); + $this->assertEquals([1 => 'Sam', 2 => 'John', 3 => 'Jane'], $result); } public function testPluckEmptyResult() @@ -397,8 +397,8 @@ public function testPluckEmptyResult() ->table('users') ->where('id', '=', 999) ->pluck('id'); - $this->assertInstanceOf(Collection::class, $result); - $this->assertTrue($result->isEmpty()); + $this->assertIsArray($result); + $this->assertEmpty($result); } public function testPluckWithExpression() @@ -408,7 +408,7 @@ public function testPluckWithExpression() ->table('users') ->limit(3) ->pluck(Expression::make('id')); - $this->assertEquals([1, 2, 3], $result->toArray()); + $this->assertEquals([1, 2, 3], $result); } public function testPluckWithJoinAndAlias() @@ -419,7 +419,7 @@ public function testPluckWithJoinAndAlias() ->join('users', 'users.id', '=', 'posts.user_id') ->select($builder->raw('posts.user_id AS uid')) ->pluck('uid'); - $this->assertEquals([1, 1, 2], $result->toArray()); + $this->assertEquals([1, 1, 2], $result); } public function testSingleInsert() diff --git a/tests/PostgresQueryBuilderTest.php b/tests/PostgresQueryBuilderTest.php index 00cd079..8b88d73 100644 --- a/tests/PostgresQueryBuilderTest.php +++ b/tests/PostgresQueryBuilderTest.php @@ -379,8 +379,8 @@ public function testPluck() ->table('users') ->limit(3) ->pluck('id'); - $this->assertInstanceOf(Collection::class, $result); - $this->assertEquals([1, 2, 3], $result->toArray()); + $this->assertIsArray($result); + $this->assertEquals([1, 2, 3], $result); } public function testPluckWithTablePrefix() @@ -389,7 +389,7 @@ public function testPluckWithTablePrefix() $result = $builder ->table('users') ->pluck('users.id'); - $this->assertEquals([1, 2, 3], $result->toArray()); + $this->assertEquals([1, 2, 3], $result); } public function testPluckPreservesExistingSelect() @@ -399,7 +399,7 @@ public function testPluckPreservesExistingSelect() ->table('users') ->select($builder->raw('id AS user_id')) ->pluck('user_id'); - $this->assertEquals([1, 2, 3], $result->toArray()); + $this->assertEquals([1, 2, 3], $result); } public function testPluckWithKey() @@ -408,7 +408,7 @@ public function testPluckWithKey() $result = $builder ->table('users') ->pluck('name', 'id'); - $this->assertEquals([1 => 'Sam', 2 => 'John', 3 => 'Jane'], $result->jsonSerialize()); + $this->assertEquals([1 => 'Sam', 2 => 'John', 3 => 'Jane'], $result); } public function testPluckEmptyResult() @@ -418,8 +418,8 @@ public function testPluckEmptyResult() ->table('users') ->where('id', '=', 999) ->pluck('id'); - $this->assertInstanceOf(Collection::class, $result); - $this->assertTrue($result->isEmpty()); + $this->assertIsArray($result); + $this->assertEmpty($result); } public function testPluckWithExpression() @@ -429,7 +429,7 @@ public function testPluckWithExpression() ->table('users') ->limit(3) ->pluck(Expression::make('id')); - $this->assertEquals([1, 2, 3], $result->toArray()); + $this->assertEquals([1, 2, 3], $result); } public function testPluckWithJoinAndAlias() @@ -440,7 +440,7 @@ public function testPluckWithJoinAndAlias() ->join('users', 'users.id', '=', 'posts.user_id') ->select($builder->raw('posts.user_id AS uid')) ->pluck('uid'); - $this->assertEquals([1, 1, 2], $result->toArray()); + $this->assertEquals([1, 1, 2], $result); } public function testSingleInsert()