diff --git a/core/Api/SettingsAPI.class.php b/core/Api/SettingsAPI.class.php index 0338f64..f7299fb 100644 --- a/core/Api/SettingsAPI.class.php +++ b/core/Api/SettingsAPI.class.php @@ -134,14 +134,14 @@ namespace Api\Settings { ->from("Settings") ->where(new CondBool("readonly")) ->where(new CondIn(new Column("name"), $keys)) - ->limit(1) + ->first() ->execute(); $this->success = ($res !== FALSE); $this->lastError = $sql->getLastError(); - if ($this->success && !empty($res)) { - return $res[0]["name"]; + if ($this->success && $res !== null) { + return $res["name"]; } return null; diff --git a/core/Api/UserAPI.class.php b/core/Api/UserAPI.class.php index e09ce62..8713ee6 100644 --- a/core/Api/UserAPI.class.php +++ b/core/Api/UserAPI.class.php @@ -690,21 +690,20 @@ namespace Api\User { ->from("User") ->where(new Compare("User.name", $username), new Compare("User.email", $username)) ->leftJoin("2FA", "2FA.uid", "User.2fa_id") - ->limit(1) + ->first() ->execute(); $this->success = ($res !== FALSE); $this->lastError = $sql->getLastError(); if ($this->success) { - if (!is_array($res) || count($res) === 0) { + if ($res === null) { return $this->wrongCredentials(); } else { - $row = $res[0]; - $uid = $row['uid']; - $confirmed = $sql->parseBool($row["confirmed"]); - $token = $row["2fa_id"] ? TwoFactorToken::newInstance($row["2fa_type"], $row["2fa_data"], $row["2fa_id"], $sql->parseBool($row["2fa_confirmed"])) : null; - if (password_verify($password, $row['password'])) { + $uid = $res['uid']; + $confirmed = $sql->parseBool($res["confirmed"]); + $token = $res["2fa_id"] ? TwoFactorToken::newInstance($res["2fa_type"], $res["2fa_data"], $res["2fa_id"], $sql->parseBool($res["2fa_confirmed"])) : null; + if (password_verify($password, $res['password'])) { if (!$confirmed) { $this->result["emailConfirmed"] = false; return $this->createError("Your email address has not been confirmed yet."); diff --git a/core/Driver/SQL/MySQL.class.php b/core/Driver/SQL/MySQL.class.php index ec8966c..e49f1b4 100644 --- a/core/Driver/SQL/MySQL.class.php +++ b/core/Driver/SQL/MySQL.class.php @@ -134,9 +134,9 @@ class MySQL extends SQL { return $sqlParams; } - protected function execute($query, $values = NULL, $returnValues = false) { + protected function execute($query, $values = NULL, int $fetchType = self::FETCH_NONE) { - $resultRows = array(); + $result = null; $this->lastError = ""; $stmt = null; $res = null; @@ -145,10 +145,21 @@ class MySQL extends SQL { try { if (empty($values)) { $res = mysqli_query($this->connection, $query); - $success = $res !== FALSE; - if ($success && $returnValues) { - while ($row = $res->fetch_assoc()) { - $resultRows[] = $row; + $success = ($res !== FALSE); + if ($success) { + switch ($fetchType) { + case self::FETCH_NONE: + $result = true; + break; + case self::FETCH_ONE: + $result = $res->fetch_assoc(); + break; + case self::FETCH_ALL: + $result = $res->fetch_all(MYSQLI_ASSOC); + break; + case self::FETCH_ITERATIVE: + $result = new RowIteratorMySQL($res); + break; } } } else if ($stmt = $this->connection->prepare($query)) { @@ -156,18 +167,27 @@ class MySQL extends SQL { $sqlParams = $this->getPreparedParams($values); if ($stmt->bind_param(...$sqlParams)) { if ($stmt->execute()) { - if ($returnValues) { + if ($fetchType === self::FETCH_NONE) { + $result = true; + $success = true; + } else { $res = $stmt->get_result(); if ($res) { - while ($row = $res->fetch_assoc()) { - $resultRows[] = $row; + switch ($fetchType) { + case self::FETCH_ONE: + $result = $res->fetch_assoc(); + break; + case self::FETCH_ALL: + $result = $res->fetch_all(MYSQLI_ASSOC); + break; + case self::FETCH_ITERATIVE: + $result = new RowIteratorMySQL($res); + break; } $success = true; } else { $this->lastError = $this->logger->error("PreparedStatement::get_result failed: $stmt->error ($stmt->errno)"); } - } else { - $success = true; } } else { $this->lastError = $this->logger->error("PreparedStatement::execute failed: $stmt->error ($stmt->errno)"); @@ -179,16 +199,18 @@ class MySQL extends SQL { } catch (\mysqli_sql_exception $exception) { $this->lastError = $this->logger->error("MySQL::execute failed: $stmt->error ($stmt->errno)"); } finally { - if ($res !== null && !is_bool($res)) { + + if ($res !== null && !is_bool($res) && $fetchType !== self::FETCH_ITERATIVE) { $res->close(); } if ($stmt !== null && !is_bool($stmt)) { $stmt->close(); } + } - return ($success && $returnValues) ? $resultRows : $success; + return $success ? $result : false; } public function getOnDuplicateStrategy(?Strategy $strategy, &$params): ?string { @@ -440,3 +462,42 @@ class MySQL extends SQL { } } } + +class RowIteratorMySQL extends RowIterator { + + public function __construct($resultSet, bool $useCache = false) { + parent::__construct($resultSet, $useCache); + } + + protected function getNumRows(): int { + return $this->resultSet->num_rows; + } + + protected function fetchRow(int $index): array { + // check if we already fetched that row + if (!$this->useCache || $index >= count($this->fetchedRows)) { + // if not, fetch it from the result set + $row = $this->resultSet->fetch_assoc(); + if ($this->useCache) { + $this->fetchedRows[] = $row; + } + + // close result set, after everything's fetched + if ($index >= $this->numRows - 1) { + $this->resultSet->close(); + } + } else { + $row = $this->fetchedRows[$index]; + } + + return $row; + } + + public function rewind() { + if ($this->useCache) { + $this->rowIndex = 0; + } else if ($this->rowIndex !== 0) { + throw new \Exception("RowIterator::rewind() not supported, when caching is disabled"); + } + } +} \ No newline at end of file diff --git a/core/Driver/SQL/PostgreSQL.class.php b/core/Driver/SQL/PostgreSQL.class.php index 2ada3f3..e2351a2 100644 --- a/core/Driver/SQL/PostgreSQL.class.php +++ b/core/Driver/SQL/PostgreSQL.class.php @@ -92,7 +92,7 @@ class PostgreSQL extends SQL { return $lastError; } - protected function execute($query, $values = NULL, $returnValues = false) { + protected function execute($query, $values = NULL, int $fetchType = self::FETCH_NONE) { $this->lastError = ""; $stmt_name = uniqid(); @@ -132,17 +132,21 @@ class PostgreSQL extends SQL { return false; } - if ($returnValues) { - $rows = pg_fetch_all($result); - if ($rows === FALSE) { - if (empty(trim($this->getLastError()))) { - $rows = array(); + switch ($fetchType) { + case self::FETCH_NONE: + return true; + case self::FETCH_ONE: + return pg_fetch_assoc($result); + case self::FETCH_ALL: + $rows = pg_fetch_all($result); + if ($rows === FALSE) { + if (empty(trim($this->getLastError()))) { + $rows = array(); + } } - } - - return $rows; - } else { - return true; + return $rows; + case self::FETCH_ITERATIVE: + return new RowIteratorPostgreSQL($result); } } @@ -182,8 +186,13 @@ class PostgreSQL extends SQL { return $columns ? (" RETURNING " . $this->columnName($columns)) : ""; } - public function executeQuery(Query $query, bool $fetchResult = false) { - return parent::executeQuery($query, $fetchResult || ($query instanceof Insert && !empty($query->getReturning()))); + public function executeQuery(Query $query, int $fetchType = self::FETCH_NONE) { + + if ($query instanceof Insert && !empty($query->getReturning())) { + $fetchType = self::FETCH_ONE; + } + + return parent::executeQuery($query, $fetchType); } protected function fetchReturning($res, string $returningCol) { @@ -449,4 +458,23 @@ class PostgreSQL extends SQL { return parent::createExpression($exp, $params); } } +} + +class RowIteratorPostgreSQL extends RowIterator { + + public function __construct($resultSet, bool $useCache = false) { + parent::__construct($resultSet, false); // caching not needed + } + + protected function getNumRows(): int { + return pg_num_rows($this->resultSet); + } + + public function rewind() { + $this->rowIndex = 0; + } + + protected function fetchRow(int $index): array { + return pg_fetch_assoc($this->resultSet, $index); + } } \ No newline at end of file diff --git a/core/Driver/SQL/Query/Select.class.php b/core/Driver/SQL/Query/Select.class.php index 8b642ac..b0d254c 100644 --- a/core/Driver/SQL/Query/Select.class.php +++ b/core/Driver/SQL/Query/Select.class.php @@ -15,10 +15,12 @@ class Select extends Query { private array $joins; private array $orderColumns; private array $groupColumns; + private array $havings; private bool $sortAscending; private int $limit; private int $offset; private bool $forUpdate; + private int $fetchType; public function __construct($sql, ...$selectValues) { parent::__construct($sql); @@ -33,6 +35,7 @@ class Select extends Query { $this->offset = 0; $this->sortAscending = true; $this->forUpdate = false; + $this->fetchType = SQL::FETCH_ALL; } public function from(...$tables): Select { @@ -95,8 +98,19 @@ class Select extends Query { return $this; } + public function iterator(): Select { + $this->fetchType = SQL::FETCH_ITERATIVE; + return $this; + } + + public function first(): Select { + $this->fetchType = SQL::FETCH_ONE; + $this->limit = 1; + return $this; + } + public function execute() { - return $this->sql->executeQuery($this, true); + return $this->sql->executeQuery($this, $this->fetchType); } public function getSelectValues(): array { return $this->selectValues; } diff --git a/core/Driver/SQL/RowIterator.class.php b/core/Driver/SQL/RowIterator.class.php new file mode 100644 index 0000000..3f3d031 --- /dev/null +++ b/core/Driver/SQL/RowIterator.class.php @@ -0,0 +1,39 @@ +resultSet = $resultSet; + $this->fetchedRows = []; + $this->rowIndex = 0; + $this->numRows = $this->getNumRows(); + $this->useCache = $useCache; + } + + protected abstract function getNumRows(): int; + protected abstract function fetchRow(int $index): array; + + public function current() { + return $this->fetchRow($this->rowIndex); + } + + public function next() { + $this->rowIndex++; + } + + public function key() { + return $this->rowIndex; + } + + public function valid(): bool { + return $this->rowIndex < $this->numRows; + } +} diff --git a/core/Driver/SQL/SQL.class.php b/core/Driver/SQL/SQL.class.php index 515a7c6..0339b5b 100644 --- a/core/Driver/SQL/SQL.class.php +++ b/core/Driver/SQL/SQL.class.php @@ -41,6 +41,11 @@ use Objects\ConnectionData; abstract class SQL { + const FETCH_NONE = 0; + const FETCH_ONE = 1; + const FETCH_ALL = 2; + const FETCH_ITERATIVE = 3; + protected Logger $logger; protected string $lastError; protected $connection; @@ -116,7 +121,7 @@ abstract class SQL { public abstract function connect(); public abstract function disconnect(); - public function executeQuery(Query $query, bool $fetchResult = false) { + public function executeQuery(Query $query, int $fetchType = self::FETCH_NONE) { $parameters = []; $queryStr = $query->build($parameters); @@ -130,16 +135,16 @@ abstract class SQL { return false; } - $res = $this->execute($queryStr, $parameters, $fetchResult); + $res = $this->execute($queryStr, $parameters, $fetchType); $success = ($res !== FALSE); // fetch generated serial ids for Insert statements $generatedColumn = ($query instanceof Insert ? $query->getReturning() : null); - if($success && $generatedColumn) { + if ($success && $generatedColumn) { $this->fetchReturning($res, $generatedColumn); } - return $fetchResult ? $res : $success; + return $fetchType === self::FETCH_NONE ? $success : $res; } public function getWhereClause($conditions, &$params): string { @@ -237,7 +242,7 @@ abstract class SQL { } // Statements - protected abstract function execute($query, $values=NULL, $returnValues=false); + protected abstract function execute($query, $values = NULL, int $fetchType = self::FETCH_NONE); public function buildCondition($condition, &$params) { diff --git a/core/core.php b/core/core.php index 8adbda4..8f94107 100644 --- a/core/core.php +++ b/core/core.php @@ -5,7 +5,7 @@ if (is_file($autoLoad)) { require_once $autoLoad; } -define("WEBBASE_VERSION", "1.5.0"); +define("WEBBASE_VERSION", "1.5.1"); spl_autoload_extensions(".php"); spl_autoload_register(function($class) {