diff --git a/src/Driver/Test/TestTable.php b/src/Driver/Test/TestTable.php index 9825aa6..8f1c753 100644 --- a/src/Driver/Test/TestTable.php +++ b/src/Driver/Test/TestTable.php @@ -57,7 +57,7 @@ public function addEntry(TestTableEntry ...$entry): static */ public function query(Query $query): QueryResult { - $entries = $this->findEntries($query->getWhere(), $query->getLimit()?->start, $query->getLimit()?->length, $query->getOrder()); + $entries = $this->findEntries($query->getWhere()); if ($query instanceof SelectQuery) { $clonedEntries = []; @@ -65,8 +65,37 @@ public function query(Query $query): QueryResult $clonedEntries[] = clone $entry; } $entries = $this->groupAndAggregateEntries($clonedEntries, $query->getGroup(), $query->getFields()); + + if ($query->isDistinct() && $query->getFields()) { + $distinctEntries = []; + foreach ($entries as $entry) { + foreach ($distinctEntries as $distinctEntry) { + $same = true; + foreach ($query->getFields() as $field) { + $key = $field->alias ?? $field->key; + if ($distinctEntry->getField($key) !== $entry->getField($key)) { + $same = false; + break; + } + } + + if ($same) { + continue 2; + } + } + + $distinctEntries[] = clone $entry; + } + $entries = $distinctEntries; + } } + if ($query->getOrder() !== null) { + $entries = $this->orderEntries($entries, $query->getOrder()); + } + + $entries = array_slice($entries, $query->getLimit()?->start ?? 0, $query->getLimit()?->length); + $queryResult = new QueryResult(); foreach ($entries as $entry) { if ($query instanceof SelectQuery) { @@ -125,29 +154,21 @@ public function groupAndAggregateEntries(array $entries, ?array $group, ?array $ /** * @param WhereGroup|null $where - * @param int|null $offset - * @param int|null $limit - * @param array|null $order * @return TestTableEntry[] */ - protected function findEntries(?WhereGroup $where, ?int $offset = null, ?int $limit = null, ?array $order = null): array + protected function findEntries(?WhereGroup $where): array { + /** @var TestTableEntry[] $entries */ $entries = []; - if ($offset === null) { - $offset = 0; - } foreach ($this->entries as $entry) { if (!$entry->matchesWhereGroup($where)) { continue; } - $entries[] = $entry; - } - if ($order !== null) { - $entries = $this->orderEntries($entries, $order); + $entries[] = $entry; } - return array_slice($entries, $offset, $limit); + return $entries; } /** diff --git a/src/Query/Generator/SQL.php b/src/Query/Generator/SQL.php index c37ca9a..dd6072c 100644 --- a/src/Query/Generator/SQL.php +++ b/src/Query/Generator/SQL.php @@ -73,6 +73,10 @@ public function generate(Query $query): string if ($query instanceof SelectQuery) { $queryString .= "SELECT"; + if ($query->isDistinct()) { + $queryString .= " DISTINCT"; + } + if ($query->getFields()) { $queryString .= " " . $this->generateFields($query); } else { diff --git a/src/Query/SelectQuery.php b/src/Query/SelectQuery.php index 16e649d..e8b192d 100644 --- a/src/Query/SelectQuery.php +++ b/src/Query/SelectQuery.php @@ -24,13 +24,16 @@ class SelectQuery extends Query * @param array|int|Limit|null $limit * @param array|GroupField[]|string[]|null $group * @param bool $saveResultsToRegistry Whether results of this query should be saved in the model registry. + * @param bool $distinct only select unique rows */ public function __construct(null|WhereCondition|array|WhereGroup $where = null, null|array $order = null, null|array $fields = null, null|Limit|array|int $limit = null, null|array $group = null, - protected bool $saveResultsToRegistry = true) + protected bool $saveResultsToRegistry = true, + protected bool $distinct = false, + ) { if ($where) { $this->where($where); @@ -100,6 +103,26 @@ public function getGroup(): ?array return $this->group; } + /** + * @param bool $distinct + * @return $this + */ + public function distinct(bool $distinct = true): static + { + $this->distinct = $distinct; + return $this; + } + + /** + * Get whether this query should be distinct + * + * @return bool + */ + public function isDistinct(): bool + { + return $this->distinct; + } + /** * Set whether results of this query should be saved in the model registry * diff --git a/test/tests/SQLTest.php b/test/tests/SQLTest.php index 10c0444..ed1a2c6 100644 --- a/test/tests/SQLTest.php +++ b/test/tests/SQLTest.php @@ -310,6 +310,14 @@ public function testSelectGroup() $this->assertEquals("SELECT * FROM `test` GROUP BY `number`, `text`", $this->sql->generate($query)); } + public function testSelectDistinct() + { + $query = new SelectQuery()->distinct(); + $query->modelClassName = TestModel::class; + + $this->assertEquals("SELECT DISTINCT * FROM `test`", $this->sql->generate($query)); + } + public function testDelete() { $query = new DeleteQuery(); diff --git a/test/tests/TestDriverTest.php b/test/tests/TestDriverTest.php index 4f2a0bb..bb4180e 100644 --- a/test/tests/TestDriverTest.php +++ b/test/tests/TestDriverTest.php @@ -12,6 +12,7 @@ use Aternos\Model\Query\MaxField; use Aternos\Model\Query\MinField; use Aternos\Model\Query\SelectField; +use Aternos\Model\Query\SelectQuery; use Aternos\Model\Query\SumField; use Aternos\Model\Query\WhereCondition; use Aternos\Model\Query\WhereGroup; @@ -564,6 +565,168 @@ public function testOrderBeforeLimit(): void $this->assertEquals(7, $result[2]->number); } + public function testSelectDistinct(): void + { + TestModel::clearTestEntries(); + + $testData = "AABCCDDDEEFG"; + foreach (str_split($testData) as $i => $char) { + TestModel::addTestEntry([ + "id" => $i . $char, + "text" => $char, + "number" => $i + ]); + } + + $result = TestModel::query(new SelectQuery()->distinct()); + + $this->assertEquals(12, $result->count()); + for ($i = 0; $i < 12; $i++) { + $this->assertEquals($i, $result[$i]->number); + $this->assertEquals($testData[$i], $result[$i]->text); + } + } + + public function testSelectDistinctField(): void + { + TestModel::clearTestEntries(); + + $testData = "AABCCDDDEEFG"; + foreach (str_split($testData) as $i => $char) { + TestModel::addTestEntry([ + "id" => $i . $char, + "text" => $char, + "number" => $i + ]); + } + + $result = TestModel::query(new SelectQuery()->distinct()->fields(["text"])); + + $this->assertEquals(7, $result->count()); + $this->assertEquals("A", $result[0]->text); + $this->assertEquals("B", $result[1]->text); + $this->assertEquals("C", $result[2]->text); + $this->assertEquals("D", $result[3]->text); + $this->assertEquals("E", $result[4]->text); + $this->assertEquals("F", $result[5]->text); + $this->assertEquals("G", $result[6]->text); + } + + public function testSelectDistinctBeforeLimit(): void + { + TestModel::clearTestEntries(); + + $testData = "AABCCDDDEEFG"; + foreach (str_split($testData) as $i => $char) { + TestModel::addTestEntry([ + "id" => $i . $char, + "text" => $char, + "number" => $i + ]); + } + + $result = TestModel::query(new SelectQuery()->distinct()->fields(["text"])->limit(5)); + + $this->assertEquals(5, $result->count()); + $this->assertEquals("A", $result[0]->text); + $this->assertEquals("B", $result[1]->text); + $this->assertEquals("C", $result[2]->text); + $this->assertEquals("D", $result[3]->text); + $this->assertEquals("E", $result[4]->text); + } + + public function testSelectDistinctWithNull(): void + { + TestModel::clearTestEntries(); + + $testData = "AABCCDDDEEFG"; + foreach (str_split($testData) as $i => $char) { + TestModel::addTestEntry([ + "id" => $i . $char, + "text" => $char, + "number" => $i + ]); + } + TestModel::addTestEntry([ + "id" => "101", + "text" => null, + "number" => 101 + ]); + + $result = TestModel::query(new SelectQuery()->distinct()->fields(["text"])); + + $this->assertEquals(8, $result->count()); + $this->assertEquals("A", $result[0]->text); + $this->assertEquals("B", $result[1]->text); + $this->assertEquals("C", $result[2]->text); + $this->assertEquals("D", $result[3]->text); + $this->assertEquals("E", $result[4]->text); + $this->assertEquals("F", $result[5]->text); + $this->assertEquals("G", $result[6]->text); + $this->assertEquals(null, $result[7]->text); + } + + public function testSelectDistinctWithNullAlias(): void + { + TestModel::clearTestEntries(); + + $testData = "AABCCDDDEEFG"; + foreach (str_split($testData) as $i => $char) { + TestModel::addTestEntry([ + "id" => $i . $char, + "text" => $char, + "number" => $i + ]); + } + TestModel::addTestEntry([ + "id" => "101", + "text" => null, + "number" => 101 + ]); + + $result = TestModel::query(new SelectQuery()->distinct()->fields([ + new SelectField("text")->setAlias("text_alias") + ])); + + $this->assertEquals(8, $result->count()); + $this->assertEquals("A", $result[0]->getField("text_alias")); + $this->assertEquals("B", $result[1]->getField("text_alias")); + $this->assertEquals("C", $result[2]->getField("text_alias")); + $this->assertEquals("D", $result[3]->getField("text_alias")); + $this->assertEquals("E", $result[4]->getField("text_alias")); + $this->assertEquals("F", $result[5]->getField("text_alias")); + $this->assertEquals("G", $result[6]->getField("text_alias")); + $this->assertEquals(null, $result[7]->getField("text_alias")); + } + + public function testSelectDistinctWithAggregateAndGroup(): void + { + TestModel::clearTestEntries(); + // Insert multiple rows with duplicate 'text' values + $testData = "AABCCDDDEEFG"; + foreach (str_split($testData) as $i => $char) { + TestModel::addTestEntry([ + "id" => $i . $char, + "text" => $char, + "number" => $i + ]); + } + + $query = new SelectQuery() + ->distinct() + ->fields([ + new CountField(), + ]) + ->groupBy(["text"]) + ->orderBy([CountField::COUNT_FIELD => Direction::ASCENDING]); + $result = TestModel::query($query); + + $this->assertEquals(3, $result->count()); + $this->assertEquals(1, $result[0]->getField(CountField::COUNT_FIELD)); + $this->assertEquals(2, $result[1]->getField(CountField::COUNT_FIELD)); + $this->assertEquals(3, $result[2]->getField(CountField::COUNT_FIELD)); + } + protected function tearDown(): void { TestModel::clearTestEntries();