Commit a22e8db0 authored by Carsten  Rose's avatar Carsten Rose
Browse files

Database: Moved implementation from PDO to mysqli. Reason: mysqli_num needed...

Database: Moved implementation from PDO to mysqli. Reason: mysqli_num needed for `report`, but PDO can't fetch column names than.
parent 7b18a4b4
......@@ -50,6 +50,7 @@ const ROW_EXPECT_0 = "expect_0";
const ROW_EXPECT_1 = "expect_1";
const ROW_EXPECT_0_1 = "expect_0_1";
const ROW_EXPECT_GE_1 = "expect_ge_1";
const ROW_KEYS = "keys";
// KeyValueParser
const IF_VALUE_EMPTY_COPY_KEY = 'if_value_empty_copy_key';
......@@ -97,7 +98,7 @@ const ERROR_UNKNOWN_MODE = 1032;
const ERROR_NOT_IMPLEMENTED = 1033;
const ERROR_RESERVED_KEY_NAME = 1034;
const ERROR_VALUE_HAS_NO_KEY = 1035;
const ERROR_DB_EXECUTE = 1036;
const ERROR_COLUMN_NOT_FOUND_IN_TABLE = 1037;
const ERROR_MISSING_HIDDEN_FIELD_IN_SIP = 1038;
const ERROR_KEY_EXIST_IN_STORE = 1039;
......@@ -112,6 +113,13 @@ const ERROR_GET_STORE_ZERO = 1047;
const ERROR_SET_STORE_ZERO = 1048;
const ERROR_MISSING_FORMELEMENT = 1049;
const ERROR_DB_EXECUTE = 1050;
const ERROR_DB_PREPARE = 1051;
const ERROR_DB_BIND = 1052;
const ERROR_DB_QUERY = 1053;
const ERROR_CLOSE_MYSQLI_RESULT = 1054;
const ERROR_CLOSE_MYSQLI_STMT = 1055;
// DB Errors
//const ERROR_DB_QUERY_SIMPLE = 2000;
......@@ -182,7 +190,6 @@ const SYSTEM_DBPW = 'DBPW';
const SYSTEM_DB = 'DB';
const SYSTEM_TESTDB = 'TESTDB';
const SYSTEM_SESSIONNAME = 'SESSIONNAME';
const SYSTEM_DBH = 'dataBaseHandle';
// Information for: Log / Debug / Exception
const SYSTEM_SQL_RAW = 'sqlRaw'; // Type: SANATIZE_ALL / String. SQL Query (before substitute). Useful for error reporting.
const SYSTEM_SQL_FINAL = 'sqlFinal'; // Type: SANATIZE_ALL / String. SQL Query (after substitute). Useful for error reporting.
......
......@@ -9,19 +9,20 @@
namespace qfq;
use qfq;
use qfq\DbException;
use qfq\Store;
use qfq\CodeException;
use qfq\UserException;
use qfq\DbException;
use qfq\Support;
require_once(__DIR__ . '/../qfq/exceptions/UserException.php');
require_once(__DIR__ . '/../qfq/exceptions/CodeException.php');
require_once(__DIR__ . '/../qfq/exceptions/DbException.php');
require_once(__DIR__ . '/exceptions/UserException.php');
require_once(__DIR__ . '/exceptions/CodeException.php');
require_once(__DIR__ . '/exceptions/DbException.php');
require_once(__DIR__ . '/../qfq/store/Store.php');
require_once(__DIR__ . '/../qfq/helper/Support.php');
require_once(__DIR__ . '/store/Store.php');
require_once(__DIR__ . '/helper/Support.php');
require_once(__DIR__ . '/helper/BindParam.php');
/**
* Class Database
......@@ -29,15 +30,26 @@ require_once(__DIR__ . '/../qfq/helper/Support.php');
*/
class Database {
/**
* @var \PDO
*/
public $pdo = null;
/**
* @var Store
*/
private $store = null;
private $stmt = null;
/**
* @var \mysqli
*/
private $mysqli = null;
/**
* @var \mysqli_stmt
*/
private $mysqli_stmt = null;
/**
* @var \mysqli_result
*/
private $mysqli_result = null;
/**
* Returns current data base handle from Store[System][SYSTEM_DBH].
......@@ -49,32 +61,33 @@ class Database {
public function __construct() {
$this->store = Store::getInstance();
$this->pdo = $this->store->getVar(SYSTEM_DBH, STORE_SYSTEM);
if ($this->pdo === false) {
$this->pdo = $this->dbConnect();
$this->store->setVar(SYSTEM_DBH, $this->pdo, STORE_SYSTEM);
if ($this->mysqli === null) {
$this->mysqli = $this->dbConnect();
}
}
/**
* If not
* @return \PDO
* Open mysqli database connection if not already done.
*
* @return \mysqli
* @throws UserException
*/
private function dbConnect() {
$mysqli = null;
$dbuser = $this->store->getVar(SYSTEM_DBUSER, STORE_SYSTEM);
$dbserver = $this->store->getVar(SYSTEM_DBSERVER, STORE_SYSTEM);
$dbpw = $this->store->getVar(SYSTEM_DBPW, STORE_SYSTEM);
$db = $this->store->getVar(SYSTEM_DB, STORE_SYSTEM);
try {
$pdo = new \PDO("mysql:host=" . $dbserver . ";dbname=" . $db, $dbuser, $dbpw, array(\PDO::ATTR_PERSISTENT => true));
} catch (\Exception $e) {
throw new UserException ("Error open Database 'mysql:host=" . $dbserver . ";dbname=" . $db . ";dbuser=" . $dbuser . "'': " . $e->getMessage(), ERROR_OPEN_DATABASE);
$mysqli = new \mysqli($dbserver, $dbuser, $dbpw, $db);
if ($mysqli->connect_error) {
throw new UserException ("Error open Database 'mysql:host=" . $dbserver . ";dbname=" . $db . ";dbuser=" . $dbuser . "'': " . $mysqli->connect_errno . PHP_EOL . $mysqli->connect_error, ERROR_OPEN_DATABASE);
}
return $pdo;
return $mysqli;
}
/**
......@@ -86,11 +99,11 @@ class Database {
* has never been called prior a call to this method, false is returned.
*/
public function getRowCount() {
if ($this->stmt == null) {
if ($this->mysqli_result == null) {
return false;
}
return $this->stmt->rowCount();
return $this->mysqli_result->num_rows;
}
/**
......@@ -134,11 +147,9 @@ class Database {
* @param string $table name of the table
*
* @param string $columnName name of the column
*
* @return array the definition of the column as retrieved by Database::getTableDefinition().
*
* @throws UserException
*
* @throws \qfq\DbException
*/
private function getFieldDefinitionFromTable($table, $columnName) {
$tableDefinition = $this->getTableDefinition($table);
......@@ -185,7 +196,9 @@ class Database {
* @throws \qfq\CodeException
* @throws \qfq\DbException
*/
public function sql($sql, $mode = ROW_REGULAR, array $parameterArray = array(), $specificMessage = '') {
public function sql($sql, $mode = ROW_REGULAR, array $parameterArray = array(), $specificMessage = '', array &$keys = array()) {
$result = array();
$this->closeMysqliStmt();
// CR often forgets to specify the $mode and use prepared statement with parameter instead.
if (is_array($mode))
......@@ -200,52 +213,68 @@ class Database {
throw new DbException($specificMessage . "No idea why this error happens - please take some time and check this: $sql", ERROR_DB_GENERIC_CHECK);
}
if ($mode === ROW_IMPLODE_ALL) {
return $this->fetchAll(ROW_IMPLODE_ALL);
}
if ($mode === ROW_REGULAR) {
return $this->fetchAll();
}
switch ($mode) {
case ROW_IMPLODE_ALL:
return $this->fetchAll(ROW_IMPLODE_ALL);
$result = $this->fetchAll($mode);
break;
case ROW_KEYS:
case ROW_REGULAR:
return $this->fetchAll();
$result = $this->fetchAll($mode, $keys);
break;
case ROW_EXPECT_0:
if ($count === 0)
return array();
$result = array();
else
throw new DbException($specificMessage . "Expected no row, got $count rows: $sql", ERROR_DB_TOO_MANY_ROWS);
break;
case ROW_EXPECT_1:
if ($count === 1)
return $this->fetchOne();
$result = $this->fetchAll($mode)[0];
else
throw new DbException($specificMessage . "Expected one row, got 0 or more than 1: $sql", ERROR_DB_COUNT_DO_NOT_MATCH);
break;
case ROW_EXPECT_0_1:
if ($count === 1)
return $this->fetchOne();
if ($count === 0)
return array();
$result = $this->fetchAll($mode)[0];
elseif ($count === 0)
$result = array();
else
throw new DbException($specificMessage . "Expected no row, got $count rows: $sql", ERROR_DB_TOO_MANY_ROWS);
break;
// case ROW_EXPECT_GE_0:
// if ($count === 0)
// return array();
// return $this->fetchAll();
// break;
case ROW_EXPECT_GE_1:
if ($count > 0)
return $this->fetchAll();
$result = $this->fetchAll($mode);
else
throw new DbException($specificMessage . "Expected at least one row, got nothing: $sql", ERROR_DB_TOO_FEW_ROWS);
break;
default:
throw new DbException($specificMessage . "Unknown mode: $mode", ERROR_UNKNOWN_MODE);
}
$this->closeMysqliStmt();
return $result;
}
/**
* Close an optional open MySQLi Statement.
*
* @throws \qfq\DbException
*/
private function closeMysqliStmt() {
if ($this->mysqli_result !== null && $this->mysqli_result !== false) {
$this->mysqli_result->free_result();
}
if ($this->mysqli_stmt !== null && $this->mysqli_stmt !== false) {
$this->mysqli_stmt->free_result();
if (!$this->mysqli_stmt->close())
throw new DbException('Error closing mysqli_stmt' . ERROR_CLOSE_MYSQLI_STMT);
}
$this->mysqli_stmt = null;
$this->mysqli_result = null;
}
/**
......@@ -259,24 +288,69 @@ class Database {
* @throws \qfq\DbException
* @throws \qfq\UserException
*/
public function prepareExecute($sql, array $parameterArray = array()) {
private function prepareExecute($sql, array $parameterArray = array()) {
$result = null;
$this->store->setVar(SYSTEM_SQL_FINAL, $sql, STORE_SYSTEM);
$this->store->setVar(SYSTEM_SQL_PARAM_ARRAY, $parameterArray, STORE_SYSTEM);
// Logfile
$this->dbLog($sql, $parameterArray);
$this->stmt = $this->pdo->prepare($sql);
if (false === $this->stmt->execute($parameterArray)) {
throw new DbException($this->stmt->errorInfo()[2], ERROR_DB_EXECUTE);
// Prepared Statement?
// if (count($parameterArray) === 0) {
// if (false === ($result = $this->mysqli->query($sql))) {
// throw new DbException('[ mysqli: ' . $this->mysqli->errno . ' ] ' . $this->mysqli->error, ERROR_DB_QUERY);
// }
//
// } else {
if (false === ($this->mysqli_stmt = $this->mysqli->prepare($sql))) {
throw new DbException('[ mysqli: ' . $this->mysqli->errno . ' ] ' . $this->mysqli->error, ERROR_DB_PREPARE);
}
if (count($parameterArray) > 0) {
if (false === $this->fakeCallUserFunc($parameterArray)) {
throw new DbException('[ mysqli: ' . $this->mysqli_stmt->errno . ' ] ' . $this->mysqli_stmt->error, ERROR_DB_BIND);
}
}
if (false === $this->mysqli_stmt->execute()) {
throw new DbException('[ mysqli: ' . $this->mysqli_stmt->errno . ' ] ' . $this->mysqli_stmt->error, ERROR_DB_EXECUTE);
}
// }
$msg = '';
$count = 0;
$command = strtoupper(explode(' ', $sql, 2)[0]);
switch ($command) {
case 'SELECT':
case 'SHOW':
case 'DESCRIBE':
case 'EXPLAIN':
if (false === ($result = $this->mysqli_stmt->get_result())) {
throw new DbException('[ mysqli: ' . $this->mysqli_stmt->errno . ' ] ' . $this->mysqli_stmt->error, ERROR_DB_EXECUTE);
}
$this->mysqli_result = $result;
$count = $this->mysqli_result->num_rows;
$msg = 'Get rows: ' . $count;
break;
case 'INSERT':
$count = $this->mysqli->insert_id;
$msg = 'ID: ' . $count;
break;
case 'UPDATE':
case 'REPLACE':
case 'DELETE':
$count = $this->mysqli->affected_rows;
$msg = 'Affected rows: ' . $count;
break;
default:
break;
}
$count = $this->stmt->rowCount();
$this->store->setVar(SYSTEM_SQL_COUNT, $count, STORE_SYSTEM);
// Logfile
$msg = (substr($sql, 0, 7) === 'INSERT ') ? 'ID: ' . $this->getLastInsertId() : 'Affected rows: ' . $count;
$this->dbLog($msg);
return $count;
......@@ -313,12 +387,48 @@ class Database {
}
/**
* Returns lastInsertId
*
* @return string
* @param $arr
*/
public function getLastInsertId() {
return $this->pdo->lastInsertId();
private function fakeCallUserFunc($arr) {
$type = '';
foreach ($arr as $value) {
if (is_int($value)) {
$type .= 'i';
} elseif (is_double($value)) {
$type .= 'd';
} else {
$type .= 's';
}
}
switch (count($arr)) {
case 0:
break;
case 1:
$this->mysqli_stmt->bind_param($type, $arr[0]);
break;
case 2:
$this->mysqli_stmt->bind_param($type, $arr[0], $arr[1]);
break;
case 3:
$this->mysqli_stmt->bind_param($type, $arr[0], $arr[1], $arr[2]);
break;
case 4:
$this->mysqli_stmt->bind_param($type, $arr[0], $arr[1], $arr[2], $arr[3]);
break;
case 5:
$this->mysqli_stmt->bind_param($type, $arr[0], $arr[1], $arr[2], $arr[3], $arr[4]);
break;
case 6:
$this->mysqli_stmt->bind_param($type, $arr[0], $arr[1], $arr[2], $arr[3], $arr[4], $arr[5]);
break;
case 7:
$this->mysqli_stmt->bind_param($type, $arr[0], $arr[1], $arr[2], $arr[3], $arr[4], $arr[5], $arr[6]);
break;
default:
throw new DbException('Oops, too many parameter in prepared statement.', 0);
}
}
/**
......@@ -330,43 +440,56 @@ class Database {
* All rows and all columns imploded to one string if $mode=IMPLODE_ALL
*
*/
public function fetchAll($mode = '') {
if ($this->stmt == null) {
private function fetchAll($mode = '', &$keys = array()) {
if ($this->mysqli_result == null || $this->mysqli_result == false) {
return false;
}
if ($this->stmt->rowCount() === 0) {
if ($this->mysqli_result->num_rows === 0) {
return ($mode === ROW_IMPLODE_ALL) ? "" : array();
}
switch ($mode) {
case ROW_IMPLODE_ALL:
$str = "";
if ($mode === ROW_IMPLODE_ALL) {
foreach ($this->stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) {
foreach ($this->mysqli_result->fetch_all(MYSQLI_ASSOC) as $row) {
$str .= implode($row);
}
return $str;
} else {
return $this->stmt->fetchAll(\PDO::FETCH_ASSOC);
break;
case ROW_KEYS:
$keys = array();
for ($ii = 0; $ii < $this->mysqli_result->field_count; $ii++) {
$keys[$ii] = $this->mysqli_result->fetch_field_direct($ii)->name;
}
return $this->mysqli_result->fetch_all(MYSQLI_NUM);
break;
default:
return $this->mysqli_result->fetch_all(MYSQLI_ASSOC);
}
}
/**
* Fetch one row of the result as associative array.
*
* @return null|mixed the first row of the result of Database::execute() as associative array.
* If Database::execute() has never been called prior a call to this method, false is returned.
* Empty string is returned if the query didn't yield any rows.
* @param $sql
* @param array $keys
* @return array|bool
* @throws \qfq\DbException
*/
public function fetchOne() {
if ($this->stmt == null) {
return false;
}
public function sqlKeys($sql, array &$keys) {
if ($this->stmt->rowCount() === 0) {
return array();
return $this->sql($sql, ROW_KEYS, array(), '', $keys);
}
return $this->stmt->fetch(\PDO::FETCH_ASSOC);
/**
* Returns lastInsertId
*
* @return string
*/
public function getLastInsertId() {
return $this->mysqli->insert_id;
}
}
\ No newline at end of file
......@@ -31,7 +31,7 @@ require_once(__DIR__ . '/../../qfq/Database.php');
/**
* Class Store
* @package qfq\store
* @package qfq
*/
class Store {
......
......@@ -20,16 +20,12 @@ abstract class AbstractDatabaseTest extends PHPUnit_Framework_TestCase {
* @var null
*/
static protected $mysqli = null;
/**
* @var \PDO
*/
protected $pdo = null;
/**
* @var qfq\Database
*/
protected $db = null;
/*
* @var qfq\store\Store
* @var qfq\Store
*/
protected $store = null;
......@@ -65,6 +61,10 @@ abstract class AbstractDatabaseTest extends PHPUnit_Framework_TestCase {
// SWITCH to TestDB
$this->store->setVar(SYSTEM_DB, $this->store->getVar(SYSTEM_TESTDB, STORE_SYSTEM), STORE_SYSTEM);
if ($this->db === null) {
$this->db = new \qfq\Database();
}
/// Establish additional mysqli access
$dbserver = $this->store->getVar(SYSTEM_DBSERVER, STORE_SYSTEM);
$dbuser = $this->store->getVar(SYSTEM_DBUSER, STORE_SYSTEM);
......@@ -80,9 +80,5 @@ abstract class AbstractDatabaseTest extends PHPUnit_Framework_TestCase {
}
}
if ($this->db === null) {
$this->db = new \qfq\Database();
$this->pdo = $this->db->pdo;
}
}
}
......@@ -3,58 +3,17 @@
* @author Rafael Ostertag <rafael.ostertag@math.uzh.ch>
*/
//namespace qfq;
//use qfq\DbException;
require_once(__DIR__ . '/AbstractDatabaseTest.php');
require_once(__DIR__ . '/../../qfq/Database.php');
require_once(__DIR__ . '/../../qfq/exceptions/DbException.php');
class DatabaseTest extends AbstractDatabaseTest {
/**
* @throws \qfq\DbException
*/
public function testExecute() {
$this->db->prepareExecute('SELECT * FROM Person');
$this->assertEquals(2, $this->db->getRowCount());
}
/**
* @throws \qfq\DbException
*/
public function testFetchOneRowAssocEmptyResult() {
$this->db->prepareExecute('SELECT * FROM Person WHERE id=0', []);
$this->assertEquals(array(), $this->db->fetchOne());
}
/**
* @throws \qfq\DbException
*/
public function testFetchOne() {
$this->db->prepareExecute('SELECT * FROM Person');
$oneRow = $this->db->fetchOne();
$this->assertArrayHasKey('name', $oneRow);
$this->assertArrayHasKey('firstname', $oneRow);
$this->assertArrayHasKey('gender', $oneRow);
$this->assertEquals('Doe', $oneRow['name']);
$this->assertEquals('John', $oneRow['firstname']);
}
/**
* @throws \qfq\DbException
*/
public function testFetchAll() {
$this->db->prepareExecute('SELECT * FROM Person ORDER BY id LIMIT 2');
$allRows = $this->db->fetchAll();
$allRows = $this->db->sql('SELECT * FROM Person ORDER BY id LIMIT 2');
$this->assertCount(2, $allRows);
$this->assertEquals(1, $allRows[0]['id']);
......@@ -65,9 +24,9 @@ class DatabaseTest extends AbstractDatabaseTest {
* @throws \qfq\DbException
*/
public function testFetchAllEmpty() {
$this->db->prepareExecute('SELECT * FROM Person WHERE id=0 ORDER BY id');
$allRows = $this->db->sql('SELECT * FROM Person WHERE id=0 ORDER BY id');
$this->assertEquals(array(), $this->db->fetchAll());
$this->assertEquals(array(), $allRows);
}
/**
......@@ -92,56 +51,62 @@ class DatabaseTest extends AbstractDatabaseTest {
],
];
// Check read rows
$dataArray = $this->db->sql('SELECT * FROM Person ORDER BY id LIMIT 1');
// Check count
$this->assertEquals(1, $this->store->getVar(SYSTEM_SQL_COUNT, STORE_SYSTEM));
// Check read rows
$dataArray = $this->db->sql('SELECT * FROM Person ORDER BY id LIMIT 2');
// Check count
$this->assertEquals(2, $this->db->getRowCount());
$this->assertEquals(2, $this->store->getVar(SYSTEM_SQL_COUNT, STORE_SYSTEM));