diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index cead857ca..c468a9666 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -22,6 +22,8 @@ abstract class Adapter */ protected array $debug = []; + protected static ?int $timeout = null; + /** * @param string $key * @param mixed $value @@ -385,7 +387,7 @@ abstract public function find(string $collection, array $queries = [], ?int $lim * * @return int|float */ - abstract public function sum(string $collection, string $attribute, array $queries = [], ?int $max = null): float|int; + abstract public function sum(string $collection, string $attribute, array $queries = [], ?int $max = null, ?int $timeout = null): float|int; /** * Count Documents @@ -396,7 +398,7 @@ abstract public function sum(string $collection, string $attribute, array $queri * * @return int */ - abstract public function count(string $collection, array $queries = [], ?int $max = null): int; + abstract public function count(string $collection, array $queries = [], ?int $max = null, ?int $timeout = null): int; /** * Get max STRING limit @@ -641,4 +643,38 @@ abstract public function increaseDocumentAttribute(string $collection, string $i * @return int */ abstract public function getMaxIndexLength(): int; + + + /** + * Set a global timeout for database queries in milliseconds. + * + * This function allows you to set a maximum execution time for all database + * queries executed using the library. Once this timeout is set, any database + * query that takes longer than the specified time will be automatically + * terminated by the library, and an appropriate error or exception will be + * raised to handle the timeout condition. + * + * @param int $milliseconds The timeout value in milliseconds for database queries. + * @return void + * + * @throws \Exception The provided timeout value must be greater than or equal to 0. + */ + public static function setTimeoutForQueries(int $milliseconds): void + { + if ($milliseconds <= 0) { + throw new Exception('Timeout must be greater than 0'); + } + self::$timeout = $milliseconds; + } + + /** + * Clears a global timeout for database queries. + * + * @return void + * + */ + public static function clearTimeoutForQueries(): void + { + self::$timeout = null; + } } diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 3ffc52da1..8c1d90c9b 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -1005,8 +1005,8 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, {$sqlLimit}; "; - if ($timeout) { - $sql = $this->setTimeout($sql, $timeout); + if ($timeout || static::$timeout) { + $sql = $this->setTimeout($sql, $timeout ? $timeout : static::$timeout); } $stmt = $this->getPDO()->prepare($sql); @@ -1078,7 +1078,7 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, * @throws Exception * @throws PDOException */ - public function count(string $collection, array $queries = [], ?int $max = null): int + public function count(string $collection, array $queries = [], ?int $max = null, ?int $timeout = null): int { $name = $this->filter($collection); $roles = Authorization::getRoles(); @@ -1103,6 +1103,10 @@ public function count(string $collection, array $queries = [], ?int $max = null) {$limit} ) table_count "; + if ($timeout || self::$timeout) { + $sql = $this->setTimeout($sql, $timeout ? $timeout : self::$timeout); + } + $stmt = $this->getPDO()->prepare($sql); foreach ($queries as $query) { $this->bindConditionValue($stmt, $query); @@ -1130,7 +1134,7 @@ public function count(string $collection, array $queries = [], ?int $max = null) * @throws Exception * @throws PDOException */ - public function sum(string $collection, string $attribute, array $queries = [], ?int $max = null): int|float + public function sum(string $collection, string $attribute, array $queries = [], ?int $max = null, ?int $timeout = null): int|float { $name = $this->filter($collection); $roles = Authorization::getRoles(); @@ -1146,16 +1150,20 @@ public function sum(string $collection, string $attribute, array $queries = [], } $sqlWhere = !empty($where) ? 'where ' . implode(' AND ', $where) : ''; + $sql = "SELECT SUM({$attribute}) as sum + FROM + ( + SELECT {$attribute} + FROM {$this->getSQLTable($name)} table_main + " . $sqlWhere . " + {$limit} + ) table_count + "; + if ($timeout || self::$timeout) { + $sql = $this->setTimeout($sql, $timeout ? $timeout : self::$timeout); + } - $stmt = $this->getPDO()->prepare(" - SELECT SUM({$attribute}) as sum - FROM ( - SELECT {$attribute} - FROM {$this->getSQLTable($name)} table_main - " . $sqlWhere . " - {$limit} - ) table_count - "); + $stmt = $this->getPDO()->prepare($sql); foreach ($queries as $query) { $this->bindConditionValue($stmt, $query); @@ -1224,7 +1232,7 @@ protected function getSQLCondition(Query $query): string default => $query->getAttribute() }); - $attribute = "`{$query->getAttribute()}`" ; + $attribute = "`{$query->getAttribute()}`"; $placeholder = $this->getSQLPlaceholder($query); switch ($query->getMethod()) { @@ -1241,7 +1249,7 @@ protected function getSQLCondition(Query $query): string default: $conditions = []; foreach ($query->getValues() as $key => $value) { - $conditions[] = $attribute.' '.$this->getSQLOperator($query->getMethod()).' :'.$placeholder.'_'.$key; + $conditions[] = $attribute . ' ' . $this->getSQLOperator($query->getMethod()) . ' :' . $placeholder . '_' . $key; } $condition = implode(' OR ', $conditions); return empty($condition) ? '' : '(' . $condition . ')'; diff --git a/src/Database/Adapter/Mongo.php b/src/Database/Adapter/Mongo.php index a68440e16..2595e03d8 100644 --- a/src/Database/Adapter/Mongo.php +++ b/src/Database/Adapter/Mongo.php @@ -803,8 +803,8 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, $options['skip'] = $offset; } - if ($timeout) { - $options['maxTimeMS'] = $timeout; + if ($timeout || self::$timeout) { + $options['maxTimeMS'] = $timeout ? $timeout : self::$timeout; } $selections = $this->getAttributeSelections($queries); @@ -1040,7 +1040,7 @@ private function recursiveReplace(array $array, string $from, string $to, array * @return int * @throws Exception */ - public function count(string $collection, array $queries = [], ?int $max = null): int + public function count(string $collection, array $queries = [], ?int $max = null, ?int $timeout = null): int { $name = $this->getNamespace() . '_' . $this->filter($collection); @@ -1052,6 +1052,10 @@ public function count(string $collection, array $queries = [], ?int $max = null) $options['limit'] = $max; } + if ($timeout || self::$timeout) { + $options['maxTimeMS'] = $timeout ? $timeout : self::$timeout; + } + // queries $filters = $this->buildFilters($queries); @@ -1075,11 +1079,14 @@ public function count(string $collection, array $queries = [], ?int $max = null) * @return int|float * @throws Exception */ - public function sum(string $collection, string $attribute, array $queries = [], ?int $max = null): float|int + public function sum(string $collection, string $attribute, array $queries = [], ?int $max = null, ?int $timeout = null): float|int { $name = $this->getNamespace() . '_' . $this->filter($collection); $collection = $this->getDatabase()->selectCollection($name); // todo $collection is not used? + + // todo add $timeout for aggregate in Mongo utopia client + $filters = []; // queries diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index 348a2bd51..15b578ec3 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -1015,8 +1015,8 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, {$sqlLimit}; "; - if ($timeout) { - $sql = $this->setTimeout($sql, $timeout); + if ($timeout || self::$timeout) { + $sql = $this->setTimeout($sql, $timeout ? $timeout : self::$timeout); } $stmt = $this->getPDO()->prepare($sql); @@ -1089,7 +1089,7 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, * * @return int */ - public function count(string $collection, array $queries = [], ?int $max = null): int + public function count(string $collection, array $queries = [], ?int $max = null, ?int $timeout = null): int { $name = $this->filter($collection); $roles = Authorization::getRoles(); @@ -1114,6 +1114,11 @@ public function count(string $collection, array $queries = [], ?int $max = null) {$limit} ) table_count "; + + if ($timeout || self::$timeout) { + $sql = $this->setTimeout($sql, $timeout ? $timeout : self::$timeout); + } + $stmt = $this->getPDO()->prepare($sql); foreach ($queries as $query) { $this->bindConditionValue($stmt, $query); @@ -1142,7 +1147,7 @@ public function count(string $collection, array $queries = [], ?int $max = null) * * @return int|float */ - public function sum(string $collection, string $attribute, array $queries = [], ?int $max = null): int|float + public function sum(string $collection, string $attribute, array $queries = [], ?int $max = null, ?int $timeout = null): int|float { $name = $this->filter($collection); $roles = Authorization::getRoles(); @@ -1159,13 +1164,21 @@ public function sum(string $collection, string $attribute, array $queries = [], $where[] = $this->getSQLPermissionsCondition($name, $roles); } - $stmt = $this->getPDO()->prepare("SELECT SUM({$attribute}) as sum - FROM ( - SELECT {$attribute} - FROM {$this->getSQLTable($name)} table_main - WHERE {$permissions} AND " . implode(' AND ', $where) . " - {$limit} - ) table_count"); + $sql = "SELECT SUM({$attribute}) as sum + FROM + ( + SELECT {$attribute} + FROM {$this->getSQLTable($name)} table_main + WHERE {$permissions} AND " . implode(' AND ', $where) . " + {$limit} + ) table_count + "; + + if ($timeout || self::$timeout) { + $sql = $this->setTimeout($sql, $timeout ? $timeout : self::$timeout); + } + + $stmt = $this->getPDO()->prepare($sql); foreach ($queries as $query) { $this->bindConditionValue($stmt, $query); diff --git a/src/Database/Database.php b/src/Database/Database.php index 4b39ef231..c5a9de712 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -4168,6 +4168,15 @@ public function sum(string $collection, string $attribute, array $queries = [], return $sum; } + public function setTimeoutForQueries(int $milliseconds): void + { + $this->adapter->setTimeoutForQueries($milliseconds); + } + + public function clearTimeoutForQueries(): void + { + $this->adapter->clearTimeoutForQueries(); + } /** * Add Attribute Filter * diff --git a/tests/Database/Base.php b/tests/Database/Base.php index 403ca424b..524167a52 100644 --- a/tests/Database/Base.php +++ b/tests/Database/Base.php @@ -222,13 +222,46 @@ public function testCreatedAtUpdatedAt(): void $this->assertNotNull($document->getInternalId()); } + public function testQueryTimeoutUsingStaticTimeout(): void + { + if ($this->getDatabase()->getAdapter()->getSupportForTimeouts()) { + static::getDatabase()->createCollection('global-timeouts'); + $this->assertEquals(true, static::getDatabase()->createAttribute('global-timeouts', 'longtext', Database::VAR_STRING, 100000000, true)); + + for ($i = 0 ; $i <= 5 ; $i++) { + static::getDatabase()->createDocument('global-timeouts', new Document([ + 'longtext' => file_get_contents(__DIR__ . '/../resources/longtext.txt'), + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()) + ] + ])); + } + + $this->expectException(Timeout::class); + static::getDatabase()->setTimeoutForQueries(1); + + try { + static::getDatabase()->find('global-timeouts', [ + Query::notEqual('longtext', 'appwrite'), + ]); + } catch(Timeout $ex) { + static::getDatabase()->clearTimeoutForQueries(); + static::getDatabase()->deleteCollection('global-timeouts'); + throw $ex; + } + } + $this->expectNotToPerformAssertions(); + } + + /** * @depends testCreateExistsDelete */ public function testCreateListExistsDeleteCollection(): void { $this->assertInstanceOf('Utopia\Database\Document', static::getDatabase()->createCollection('actors')); - $this->assertCount(2, static::getDatabase()->listCollections()); $this->assertEquals(true, static::getDatabase()->exists($this->testDatabase, 'actors'));