diff --git a/src/Database/Database.php b/src/Database/Database.php index 1ccea9ec8..0c33cd96b 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -5624,6 +5624,66 @@ public function find(string $collection, array $queries = [], string $forPermiss return $results; } + /** + * Call callback for each document of the given collection + * that matches the given queries + * + * @param string $collection + * @param callable $callback + * @param array $queries + * @param string $forPermission + * @throws \Utopia\Database\Exception + * @return void + */ + public function foreach(string $collection, callable $callback, array $queries = [], string $forPermission = Database::PERMISSION_READ): void + { + $grouped = Query::groupByType($queries); + $limitExists = $grouped['limit'] !== null; + $limit = $grouped['limit'] ?? 25; + $offset = $grouped['offset']; + + $cursor = $grouped['cursor']; + $cursorDirection = $grouped['cursorDirection']; + + // Cursor before is not supported + if ($cursor !== null && $cursorDirection === Database::CURSOR_BEFORE) { + throw new DatabaseException('Cursor ' . Database::CURSOR_BEFORE . ' not supported in this method.'); + } + + $results = []; + $sum = $limit; + $latestDocument = null; + + while ($sum === $limit) { + $newQueries = $queries; + if ($latestDocument !== null) { + //reset offset and cursor as groupByType ignores same type query after first one is encountered + if ($offset !== null) { + array_unshift($newQueries, Query::offset(0)); + } + + array_unshift($newQueries, Query::cursorAfter($latestDocument)); + } + if (!$limitExists) { + $newQueries[] = Query::limit($limit); + } + $results = $this->find($collection, $newQueries, $forPermission); + + if (empty($results)) { + return; + } + + $sum = count($results); + + foreach ($results as $document) { + if (is_callable($callback)) { + $callback($document); + } + } + + $latestDocument = $results[array_key_last($results)]; + } + } /** * @param string $collection * @param array $queries diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 0191ea853..945376ecb 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -5017,6 +5017,54 @@ public function testFindSelect(): void } } + /** @depends testFind */ + public function testForeach(): void + { + /** + * Test, foreach goes through all the documents + */ + $documents = []; + static::getDatabase()->foreach('movies', queries: [Query::limit(2)], callback: function ($document) use (&$documents) { + $documents[] = $document; + }); + $this->assertEquals(6, count($documents)); + + /** + * Test, foreach with initial cursor + */ + + $first = $documents[0]; + $documents = []; + static::getDatabase()->foreach('movies', queries: [Query::limit(2), Query::cursorAfter($first)], callback: function ($document) use (&$documents) { + $documents[] = $document; + }); + $this->assertEquals(5, count($documents)); + + /** + * Test, foreach with initial offset + */ + + $documents = []; + static::getDatabase()->foreach('movies', queries: [Query::limit(2), Query::offset(2)], callback: function ($document) use (&$documents) { + $documents[] = $document; + }); + $this->assertEquals(4, count($documents)); + + /** + * Test, cursor before throws error + */ + try { + static::getDatabase()->foreach('movies', queries: [Query::cursorBefore($documents[0]), Query::offset(2)], callback: function ($document) use (&$documents) { + $documents[] = $document; + }); + + } catch (Throwable $e) { + $this->assertInstanceOf(DatabaseException::class, $e); + $this->assertEquals('Cursor ' . Database::CURSOR_BEFORE . ' not supported in this method.', $e->getMessage()); + } + + } + /** * @depends testFind */