Browse Source

DB row iterator

Roman 1 year ago
parent
commit
bce59c5f92

+ 3 - 3
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;

+ 6 - 7
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.");

+ 74 - 13
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");
+    }
+  }
+}

+ 41 - 13
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);
+  }
 }

+ 15 - 1
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; }

+ 39 - 0
core/Driver/SQL/RowIterator.class.php

@@ -0,0 +1,39 @@
+<?php
+
+namespace Driver\SQL;
+
+abstract class RowIterator implements \Iterator {
+
+  protected $resultSet;
+  protected int $rowIndex;
+  protected array $fetchedRows;
+  protected int $numRows;
+  protected bool $useCache;
+
+  public function __construct($resultSet, bool $useCache = false) {
+    $this->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;
+  }
+}

+ 10 - 5
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) {
 

+ 1 - 1
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) {