From 57d8f9ddc17169883f7dd77e51dea1443040858b Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Wed, 24 Jul 2024 20:57:23 -0400 Subject: [PATCH] Add map and iter to doc list --- src/DocumentList.php | 105 ++++++++++++------ src/Parameters.php | 3 +- .../postgresql/DocumentListTest.php | 40 ++++++- tests/integration/sqlite/DocumentListTest.php | 39 ++++++- 4 files changed, 149 insertions(+), 38 deletions(-) diff --git a/src/DocumentList.php b/src/DocumentList.php index f4ca6f9..47bdace 100644 --- a/src/DocumentList.php +++ b/src/DocumentList.php @@ -24,6 +24,9 @@ class DocumentList /** @var TDoc|null $first The first item from the results */ private mixed $first = null; + /** @var bool $isConsumed This is set to true once the generator has been exhausted */ + private bool $isConsumed = false; + /** * Constructor * @@ -39,6 +42,75 @@ class DocumentList } } + /** + * Does this list have items remaining? + * + * @return bool True if there are items still to be retrieved from the list, false if not + */ + public function hasItems(): bool + { + return !is_null($this->result); + } + + /** + * The items from the query result + * + * @return Generator The items from the document list + * @throws DocumentException If this is called once the generator has been consumed + */ + public function items(): Generator + { + if (!$this->result) { + if ($this->isConsumed) { + throw new DocumentException('Cannot call items() multiple times'); + } + $this->isConsumed = true; + return; + } + yield $this->first; + while ($row = $this->result->fetch(PDO::FETCH_ASSOC)) { + yield $this->mapper->map($row); + } + $this->isConsumed = true; + $this->result = null; + } + + /** + * Map items by consuming the generator + * + * @template U The type to which each item should be mapped + * @param callable(TDoc): U $map The mapping function + * @return Generator The result of the mapping function + * @throws DocumentException If this is called once the generator has been consumed + */ + public function map(callable $map): Generator + { + foreach ($this->items() as $item) { + yield $map($item); + } + } + + /** + * Iterate the generator, running the given function for each item + * + * @param callable(TDoc): void $f The function to run for each item + * @throws DocumentException If this is called once the generator has been consumed + */ + public function iter(callable $f): void + { + foreach ($this->items() as $item) { + $f($item); + } + } + + /** + * Ensure the statement is destroyed if the generator is not exhausted + */ + public function __destruct() + { + if (!is_null($this->result)) $this->result = null; + } + /** * Construct a new document list * @@ -53,37 +125,4 @@ class DocumentList $stmt = &Custom::runQuery($query, $parameters); return new static($stmt, $mapper); } - - /** - * The items from the query result - * - * @return Generator The items from the document list - */ - public function items(): Generator - { - if (!$this->result) return; - yield $this->first; - while ($row = $this->result->fetch(PDO::FETCH_ASSOC)) { - yield $this->mapper->map($row); - } - $this->result = null; - } - - /** - * Does this list have items remaining? - * - * @return bool True if there are items still to be retrieved from the list, false if not - */ - public function hasItems(): bool - { - return !is_null($this->result); - } - - /** - * Ensure the statement is destroyed if the generator is not exhausted - */ - public function __destruct() - { - if (!is_null($this->result)) $this->result = null; - } } diff --git a/src/Parameters.php b/src/Parameters.php index 23799e7..6dc21e9 100644 --- a/src/Parameters.php +++ b/src/Parameters.php @@ -95,7 +95,8 @@ class Parameters $mode = Configuration::mode('generate field name parameters'); return match ($mode) { Mode::PgSQL => [$paramName => "{" . implode(",", $fieldNames) . "}"], - Mode::SQLite => array_combine(array_map(fn($idx) => $paramName . $idx, range(0, sizeof($fieldNames) - 1)), + Mode::SQLite => array_combine(array_map(fn($idx) => $paramName . $idx, + empty($fieldNames) ? [] : range(0, sizeof($fieldNames) - 1)), array_map(fn($field) => "$.$field", $fieldNames)) }; } diff --git a/tests/integration/postgresql/DocumentListTest.php b/tests/integration/postgresql/DocumentListTest.php index 45511af..135e434 100644 --- a/tests/integration/postgresql/DocumentListTest.php +++ b/tests/integration/postgresql/DocumentListTest.php @@ -8,7 +8,7 @@ declare(strict_types=1); namespace Test\Integration\PostgreSQL; -use BitBadger\PDODocument\{DocumentList, Query}; +use BitBadger\PDODocument\{DocumentException, DocumentList, Query}; use BitBadger\PDODocument\Mapper\DocumentMapper; use PHPUnit\Framework\Attributes\TestDox; use PHPUnit\Framework\TestCase; @@ -43,7 +43,7 @@ class DocumentListTest extends TestCase $list = null; } - public function testItems(): void + public function testItemsSucceeds(): void { $list = DocumentList::create(Query::selectFromTable(ThrowawayDb::TABLE), [], new DocumentMapper(TestDocument::class)); @@ -57,6 +57,18 @@ class DocumentListTest extends TestCase $this->assertEquals(5, $count, 'There should have been 5 documents returned'); } + public function testItemsFailsWhenAlreadyConsumed(): void + { + $list = DocumentList::create(Query::selectFromTable(ThrowawayDb::TABLE), [], + new DocumentMapper(TestDocument::class)); + $this->assertNotNull($list, 'There should have been a document list created'); + $this->assertTrue($list->hasItems(), 'There should be items in the list'); + $ignored = iterator_to_array($list->items()); + $this->assertFalse($list->hasItems(), 'The list should no longer have items'); + $this->expectException(DocumentException::class); + iterator_to_array($list->items()); + } + public function testHasItemsSucceedsWithEmptyResults(): void { $list = DocumentList::create( @@ -77,4 +89,28 @@ class DocumentListTest extends TestCase } $this->assertFalse($list->hasItems(), 'There should be no remaining items in the list'); } + + public function testMapSucceeds(): void + { + $list = DocumentList::create(Query::selectFromTable(ThrowawayDb::TABLE), [], + new DocumentMapper(TestDocument::class)); + $this->assertNotNull($list, 'There should have been a document list created'); + $this->assertTrue($list->hasItems(), 'There should be items in the list'); + foreach ($list->map(fn($doc) => strrev($doc->id)) as $mapped) { + $this->assertContains($mapped, ['eno', 'owt', 'eerht', 'ruof', 'evif'], + 'An unexpected mapped value was returned'); + } + } + + public function testIterSucceeds(): void + { + $list = DocumentList::create(Query::selectFromTable(ThrowawayDb::TABLE), [], + new DocumentMapper(TestDocument::class)); + $this->assertNotNull($list, 'There should have been a document list created'); + $this->assertTrue($list->hasItems(), 'There should be items in the list'); + $splats = []; + $list->iter(function ($doc) use (&$splats) { $splats[] = str_repeat('*', strlen($doc->id)); }); + $this->assertEquals('*** *** ***** **** ****', implode(' ', $splats), + 'Iteration did not have the expected result'); + } } diff --git a/tests/integration/sqlite/DocumentListTest.php b/tests/integration/sqlite/DocumentListTest.php index a2b4f09..88623a0 100644 --- a/tests/integration/sqlite/DocumentListTest.php +++ b/tests/integration/sqlite/DocumentListTest.php @@ -8,7 +8,7 @@ declare(strict_types=1); namespace Test\Integration\SQLite; -use BitBadger\PDODocument\{DocumentList, Query}; +use BitBadger\PDODocument\{DocumentException, DocumentList, Query}; use BitBadger\PDODocument\Mapper\DocumentMapper; use PHPUnit\Framework\Attributes\TestDox; use PHPUnit\Framework\TestCase; @@ -43,7 +43,7 @@ class DocumentListTest extends TestCase $list = null; } - public function testItems(): void + public function testItemsSucceeds(): void { $list = DocumentList::create(Query::selectFromTable(ThrowawayDb::TABLE), [], new DocumentMapper(TestDocument::class)); @@ -57,6 +57,18 @@ class DocumentListTest extends TestCase $this->assertEquals(5, $count, 'There should have been 5 documents returned'); } + public function testItemsFailsWhenAlreadyConsumed(): void + { + $list = DocumentList::create(Query::selectFromTable(ThrowawayDb::TABLE), [], + new DocumentMapper(TestDocument::class)); + $this->assertNotNull($list, 'There should have been a document list created'); + $this->assertTrue($list->hasItems(), 'There should be items in the list'); + $ignored = iterator_to_array($list->items()); + $this->assertFalse($list->hasItems(), 'The list should no longer have items'); + $this->expectException(DocumentException::class); + iterator_to_array($list->items()); + } + public function testHasItemsSucceedsWithEmptyResults(): void { $list = DocumentList::create(Query::selectFromTable(ThrowawayDb::TABLE) . " WHERE data->>'num_value' < 0", [], @@ -76,4 +88,27 @@ class DocumentListTest extends TestCase } $this->assertFalse($list->hasItems(), 'There should be no remaining items in the list'); } + public function testMapSucceeds(): void + { + $list = DocumentList::create(Query::selectFromTable(ThrowawayDb::TABLE), [], + new DocumentMapper(TestDocument::class)); + $this->assertNotNull($list, 'There should have been a document list created'); + $this->assertTrue($list->hasItems(), 'There should be items in the list'); + foreach ($list->map(fn($doc) => strrev($doc->id)) as $mapped) { + $this->assertContains($mapped, ['eno', 'owt', 'eerht', 'ruof', 'evif'], + 'An unexpected mapped value was returned'); + } + } + + public function testIterSucceeds(): void + { + $list = DocumentList::create(Query::selectFromTable(ThrowawayDb::TABLE), [], + new DocumentMapper(TestDocument::class)); + $this->assertNotNull($list, 'There should have been a document list created'); + $this->assertTrue($list->hasItems(), 'There should be items in the list'); + $splats = []; + $list->iter(function ($doc) use (&$splats) { $splats[] = str_repeat('*', strlen($doc->id)); }); + $this->assertEquals('*** *** ***** **** ****', implode(' ', $splats), + 'Iteration did not have the expected result'); + } }