Skip to content

Commit 23c36a2

Browse files
authored
Optimize pluck, simplify stuff (#78)
1 parent 28811e3 commit 23c36a2

16 files changed

+128
-84
lines changed

lib/Connection.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -391,15 +391,15 @@ public function query_and_fetch_one(string $sql, array &$values = []): int
391391
/**
392392
* Execute a raw SQL query and fetch the results.
393393
*
394-
* @param string $sql raw SQL string to execute
395-
* @param \Closure $handler closure that will be passed the fetched results
394+
* @param string $sql raw SQL string to execute
395+
* @param array<mixed> $values
396396
*/
397-
public function query_and_fetch(string $sql, \Closure $handler): void
397+
public function query_and_fetch(string $sql, array $values = [], int $method = \PDO::FETCH_ASSOC): \Generator
398398
{
399-
$sth = $this->query($sql);
399+
$sth = $this->query($sql, $values);
400400

401-
while ($row = $sth->fetch(\PDO::FETCH_ASSOC)) {
402-
$handler($row);
401+
while ($row = $sth->fetch($method)) {
402+
yield $row;
403403
}
404404
}
405405

lib/Model.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1599,6 +1599,16 @@ public static function select(): Relation
15991599
return static::Relation()->select(...func_get_args());
16001600
}
16011601

1602+
/**
1603+
* @return Relation<static>
1604+
*
1605+
*@see Relation::distinct()
1606+
*/
1607+
public static function distinct(bool $distinct = true): Relation
1608+
{
1609+
return static::Relation()->distinct($distinct);
1610+
}
1611+
16021612
/**
16031613
* @return Relation<static>
16041614
*

lib/Relation.php

Lines changed: 54 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -107,46 +107,52 @@ public function none(): Relation
107107
}
108108

109109
/**
110-
* Plucks the columns from the table, returning an column_value[] rather than a Model[]
110+
* Use pluck as a shortcut to select one or more attributes without
111+
* loading an entire record object per row.
111112
*
112-
* $this->where(['name' => 'Bill'])->pluck('id') // returns [1]
113-
* $this->where(['age' => 42])->pluck('id', 'name') // returns [[1, "David"], [2, "Fran"], [3, "Jose"]]
114-
* $this->where(['age' => 42])->pluck('id, name') // returns [[1, "David"], [2, "Fran"], [3, "Jose"]]
115-
* $this->where(['age' => 42])->pluck(['id', 'name']) // returns [[1, "David"], [2, "Fran"], [3, "Jose"]]
113+
* $names = Person::pluck('name');
114+
*
115+
* instead of
116+
*
117+
* $names = array_map(fn($person) =>$person->name, Person::all()->to_a());
118+
*
119+
* Pluck returns an Array of attribute values type-casted to match
120+
* the plucked column names, if they can be deduced.
121+
*
122+
* Person::pluck('name') // SELECT people.name FROM people
123+
* => ['David', 'Jeremy', 'Jose']
124+
*
125+
* Person::pluck('id', 'name'); // SELECT people.id, people.name FROM people
126+
* => [[1, 'David'], [2, 'Jeremy'], [3, 'Jose']]
127+
*
128+
* Person::distinct()->pluck('role'); // SELECT DISTINCT role FROM people
129+
* => ['admin', 'member', 'guest']
130+
*
131+
* Person::where(['age' => 21]) // SELECT people.id FROM people WHERE people.age = 21 LIMIT 5
132+
* ->limit(5).pluck('id')
133+
* => [2, 3]
134+
*
135+
* @see Relation::ids()
116136
*
117137
* @return array<mixed>
118138
*/
119139
public function pluck(): array
120140
{
121-
$columns = $this->possibleListToArray(...func_get_args());
122-
if (0 === count($columns)) {
141+
$args = func_get_args();
142+
if (0 === count($args)) {
123143
throw new ValidationsArgumentError('pluck requires at least one argument');
124144
}
125145

126-
$oldSelect = array_key_exists('select', $this->options) ? $this->options['select'] : null;
127-
$this->reselect($columns);
128-
129-
$models = $this->to_a();
130-
131-
if (null === $oldSelect) {
132-
unset($this->options['select']);
133-
} else {
134-
$this->options['select'] = $oldSelect;
135-
}
136-
137-
$retValue = [];
138-
foreach ($models as $model) {
139-
$row = [];
140-
foreach ($columns as $column) {
141-
array_push($row, $model->$column);
142-
}
143-
if (1 === count($row)) {
144-
$row = $row[0];
145-
}
146-
array_push($retValue, $row);
147-
}
146+
$options = array_merge($this->options, ['select' => [static::toSingleArg(...$args)]]);
147+
$table = $this->table();
148+
$sql = $table->options_to_sql($options);
149+
$retValue = iterator_to_array(
150+
$table->conn->query_and_fetch($sql->to_s(), $sql->get_where_values(), \PDO::FETCH_NUM)
151+
);
148152

149-
return $retValue;
153+
return array_map(static function ($row) {
154+
return 1 == count($row) ? $row[0] : $row;
155+
}, $retValue);
150156
}
151157

152158
/**
@@ -192,21 +198,6 @@ public function reselect(): Relation
192198
return $this;
193199
}
194200

195-
/**
196-
* Converts "name,id" to ["name", "id"] if necessary
197-
*
198-
* @return array<string>
199-
*/
200-
private function possibleListToArray(): array
201-
{
202-
$args = static::toSingleArg(...func_get_args());
203-
if (!is_array($args)) {
204-
$args = array_map('trim', explode(',', $args));
205-
}
206-
207-
return $args;
208-
}
209-
210201
/**
211202
* Performs JOINs on +args+. The given symbol(s) should match the name of
212203
* the association(s).
@@ -550,6 +541,24 @@ public function readonly(bool $readonly): Relation
550541
return $this;
551542
}
552543

544+
/**
545+
* Specifies whether the records should be unique or not. For example:
546+
*
547+
* User::select('name') // Might return two records with the same name
548+
*
549+
* User::select('name')->distinct() // Returns 1 record per distinct name
550+
*
551+
* User::select('name')->distinct()->distinct(false) // You can also remove the uniqueness
552+
*
553+
* @return Relation<TModel>
554+
*/
555+
public function distinct(bool $distinct=true): Relation
556+
{
557+
$this->options['distinct'] = $distinct;
558+
559+
return $this;
560+
}
561+
553562
/**
554563
* Find by id - This can either be a specific id (1),
555564
* a list of ids (1, 5, 6), or an array of ids ([5, 6, 10]).

lib/SQLBuilder.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ class SQLBuilder
1818
{
1919
private Connection $connection;
2020
private string $operation = 'SELECT';
21+
22+
private bool $distinct = false;
2123
private string $table;
2224
private string $select = '*';
2325

@@ -175,8 +177,9 @@ public function offset(int $offset): static
175177
return $this;
176178
}
177179

178-
public function select(string $select): static
180+
public function select(string $select, bool $distinct = false): static
179181
{
182+
$this->distinct = $distinct;
180183
$this->operation = 'SELECT';
181184
$this->select = $select;
182185

@@ -344,7 +347,7 @@ private function build_insert(): string
344347

345348
private function build_select(): string
346349
{
347-
$sql = "SELECT $this->select FROM $this->table";
350+
$sql = 'SELECT ' . ($this->distinct ? 'DISTINCT ' : '') . "$this->select FROM $this->table";
348351

349352
if (!empty($this->joins)) {
350353
$sql .= ' ' . $this->joins;

lib/Table.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,10 @@ public function options_to_sql(array $options): SQLBuilder
223223
$tokens = array_merge($tokens, array_map('trim', explode(',', $select)));
224224
}
225225

226-
$sql->select(array_search('*', $tokens) ? '*' : implode(', ', array_unique($tokens)));
226+
$sql->select(
227+
array_search('*', $tokens) ? '*' : implode(', ', array_unique($tokens)),
228+
!empty($options['distinct'])
229+
);
227230
}
228231

229232
$sql->where($options['conditions'] ?? [], $options['mapped_names'] ?? []);

lib/Types.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
* offset?: int,
4343
* order?: string,
4444
* readonly?: bool,
45+
* distinct?: bool,
4546
* select?: string|array<string>,
4647
* }
4748
*/

test/ActiveRecordCountTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ class ActiveRecordCountTest extends \DatabaseTestCase
99
{
1010
public function testNoArguments()
1111
{
12-
$this->assertEquals(4, Author::count());
12+
$this->assertEquals(5, Author::count());
1313
}
1414

1515
public function testColumnNameAsArgument()

test/ActiveRecordFindTest.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -234,8 +234,8 @@ public function testFindBySqltakesValuesArray()
234234
public function testFindLast()
235235
{
236236
$author = Author::last();
237-
$this->assertEquals(4, $author->author_id);
238-
$this->assertEquals('Uncle Bob', $author->name);
237+
$this->assertEquals(5, $author->author_id);
238+
$this->assertEquals('Tito', $author->name);
239239
}
240240

241241
public function testFindLastUsingStringCondition()
@@ -316,7 +316,7 @@ public function testFindWithHash()
316316
{
317317
$this->assertNotNull(Author::where(['name' => 'Tito'])->first());
318318
$this->assertNull(Author::where(['name' => 'Mortimer'])->first());
319-
$this->assertEquals(1, count(Author::where(['name' => 'Tito'])->to_a()));
319+
$this->assertEquals(2, count(Author::where(['name' => 'Tito'])->to_a()));
320320
}
321321

322322
public function testFindOrCreateByOnExistingRecord()

test/ActiveRecordPluckTest.php

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,39 @@ class ActiveRecordPluckTest extends \DatabaseTestCase
1111
public function testNoArguments()
1212
{
1313
$this->expectException(ValidationsArgumentError::class);
14-
$author = Author::pluck();
14+
Author::pluck();
1515
}
1616

1717
public function testSingleArgument()
1818
{
19-
$authors = Author::where(['mixedCaseField' => 'Bill'])->pluck('name');
20-
$this->assertEquals(2, count($authors));
21-
$this->assertEquals('Bill Clinton', $authors[0]);
22-
$this->assertEquals('Uncle Bob', $authors[1]);
19+
$authors = Author::pluck('name');
20+
$this->assertEquals(5, count($authors));
21+
$this->assertEquals('Tito', $authors[0]);
22+
$this->assertEquals('George W. Bush', $authors[1]);
23+
}
24+
25+
public function testSingleArgumentWithDistinct()
26+
{
27+
$authors = Author::distinct()->pluck('name');
28+
$this->assertEquals(4, count($authors));
29+
$this->assertEquals('Tito', $authors[0]);
30+
$this->assertEquals('George W. Bush', $authors[1]);
31+
}
32+
33+
public function testSingleArgumentWithRemoveDistinct()
34+
{
35+
$authors = Author::distinct()->distinct(false)->pluck('name');
36+
$this->assertEquals(5, count($authors));
2337
}
2438

2539
public function testMultipleArguments()
2640
{
27-
$authors = Author::where(['mixedCaseField' => 'Bill'])->pluck('name', 'author_id');
28-
$this->assertMultipleArgumentsResult($authors);
41+
$authors = Author::pluck('name', 'author_id');
42+
$this->assertEquals(5, count($authors));
43+
$this->assertEquals('Tito', $authors[0][0]);
44+
$this->assertEquals(1, $authors[0][1]);
45+
$this->assertEquals('George W. Bush', $authors[1][0]);
46+
$this->assertEquals(2, $authors[1][1]);
2947
}
3048

3149
public function testCommaDelimitedString()
@@ -69,6 +87,6 @@ public function testIds()
6987
public function testIdsAll()
7088
{
7189
$authors = Author::ids();
72-
$this->assertEquals(4, count($authors));
90+
$this->assertEquals(5, count($authors));
7391
}
7492
}

test/ActiveRecordTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,6 @@ public function testQuery()
478478
$this->assertTrue($row['n'] > 1);
479479

480480
$row = Author::query('SELECT COUNT(*) AS n FROM authors WHERE name=?', ['Tito'])->fetch();
481-
$this->assertEquals(['n' => 1], $row);
481+
$this->assertEquals(['n' => 2], $row);
482482
}
483483
}

0 commit comments

Comments
 (0)