Page Menu
Home
DevCentral
Search
Configure Global Search
Log In
Files
F11726343
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
17 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/DatabaseEngine.php b/src/DatabaseEngine.php
index e8802bf..ee2bffa 100644
--- a/src/DatabaseEngine.php
+++ b/src/DatabaseEngine.php
@@ -1,138 +1,145 @@
<?php
namespace Keruald\Database;
use Keruald\Database\Exceptions\SqlException;
use BadMethodCallException;
use Keruald\Database\Result\DatabaseResult;
use LogicException;
abstract class DatabaseEngine {
///
/// Traits
///
use WithLegacyMethods;
///
/// Methods the specific engine need to implement to access database
///
public abstract function escape (string $expression) : string;
public abstract function query (string $query);
public abstract function nextId () : int|string;
public abstract function countAffectedRows () : int;
protected abstract function getExceptionContext (): array;
+ /**
+ * Determines if the specified table or view exists.
+ */
+ public abstract function isExistingTable (
+ string $database, string $table
+ ) : bool;
+
///
/// Engine mechanics
///
public abstract static function load (array $config): DatabaseEngine;
public abstract function getUnderlyingDriver () : mixed;
///
/// Helpers we can use across all engines
///
/**
* Runs a query, then returns the first scalar element of result,
* ie the element in the first column of the first row.
*
* This is intended for queries with only one scalar result like
* 'SELECT count(*) FROM …' or 'SELECT value WHERE unique_key = …'.
*
* @param string $query The query to execute
* @return string the scalar result
* @throws SqlException
*/
public function queryScalar (string $query = '') : string {
if ($query === '') {
//No query, no value
return '';
}
$result = $this->query($query);
// If legacy mode is enabled, we have a MySQL error here.
if ($result === false) {
throw SqlException::fromQuery($query, $this->getExceptionContext());
}
// Ensure the query is SELECT / SHOW / DESCRIBE / EXPLAIN,
// so we have a scalar result to actually return.
//
// That allows to detect bugs where queryScalar() is used
// with the intent to fetch metadata information,
// e.g. the amount of rows updated or deleted.
if (is_bool($result)) {
throw new LogicException("The queryScalar method is intended
to be used with SELECT queries and assimilated");
}
// Fetches first row of the query, and return the first element
// If there isn't any result row, returns an empty string.
return $result->fetchRow()[0] ?? "";
}
///
/// Compatibility with legacy code
///
/**
* @var bool Don't throw exceptions if a query doesn't succeed
* @deprecated Replace `if (!$result = $db->query(…))` by an error handler
*/
public bool $dontThrowExceptions = false;
/**
* Gets the number of rows affected or returned by a query.
*
* @return int the number of rows affected (delete/insert/update)
* or the number of rows in query result
* @deprecated Use $result->numRows or $db->countAffectedRows();
*/
public function numRows (DatabaseResult|bool $result = false) : int {
if ($result instanceof DatabaseResult) {
return $result->numRows();
}
return $this->countAffectedRows();
}
/**
* Fetches a row from the query result.
*
* @param DatabaseResult $result The query result
* @return array|null An associative array with the database result,
* or null if no more result is available.
*/
public function fetchRow (DatabaseResult $result) : ?array {
return $result->fetchRow();
}
/**
* Allows the legacy use of sql_query, sql_fetchrow, sql_escape, etc.
*
* @throws BadMethodCallException when the method name doesn't exist.
* @deprecated
*/
public function __call (string $name, array $arguments) {
if (str_starts_with($name, 'sql_')) {
return $this->callByLegacyMethodName($name, $arguments);
}
$className = get_class($this);
throw new BadMethodCallException(
"Method doesn't exist: $className::$name"
);
}
}
diff --git a/src/Engines/MySQLiEngine.php b/src/Engines/MySQLiEngine.php
index 2d53424..cac9d1c 100644
--- a/src/Engines/MySQLiEngine.php
+++ b/src/Engines/MySQLiEngine.php
@@ -1,204 +1,206 @@
<?php
namespace Keruald\Database\Engines;
use Keruald\Database\DatabaseEngine;
use Keruald\Database\Exceptions\EngineSetupException;
use Keruald\Database\Exceptions\SqlException;
use Keruald\Database\Result\MySQLiDatabaseResult;
use RuntimeException;
use mysqli;
use mysqli_driver;
use mysqli_result;
use mysqli_sql_exception;
class MySQLiEngine extends DatabaseEngine {
+ use WithMySQL;
+
/**
* The connection identifier
*/
private mysqli $db;
/**
* The MySQL driver
*/
private mysqli_driver $driver;
/**
* Initializes a new instance of the database abstraction class,
* for MySQLi engine.
*
* @param string $host The host of the MySQL server [optional, default: localhost]
* @param string $username The username used to connect [optional, default: root]
* @param string $password The password used to connect [optional, default: empty]
* @param string $database The database to select [optional]
*/
function __construct(
string $host = 'localhost',
string $username = 'root',
string $password = '',
string $database = ''
) {
// Checks extension requirement
if (!class_exists("mysqli")) {
throw new RuntimeException("You've chosen to use a MySQLi database engine, but the MySQLi extension is missing.");
}
// Connects to MySQL server
$this->driver = new mysqli_driver();
$this->db = new mysqli($host, $username, $password);
$this->setCharset('utf8mb4');
// Selects database
if ($database !== '') {
$this->db->select_db($database);
}
}
/**
* Sends a unique query to the database.
*
* @return mysqli_result|bool For successful SELECT, SHOW, DESCRIBE or
* EXPLAIN queries, a <b>mysqli_result</b> object; otherwise, true, or false
* on failure in legacy mode.
* @throws SqlException if legacy mode is disabled, and the query fails.
*/
function query (string $query) : MySQLiDatabaseResult|bool {
// Run query
try {
$result = $this->db->query($query);
} catch (mysqli_sql_exception $ex) {
if ($this->dontThrowExceptions) {
return false;
}
$this->throwException($ex, $query);
}
if (is_bool($result)) {
return $result;
}
return new MySQLiDatabaseResult($result);
}
/**
* Gets more information about the last SQL error.
*
* @return array an array with two keys, code and message, containing error information
* @deprecated The PHP drivers and our abstraction now throw exceptions when an error occur.
*/
function error () : array {
return [
'code' => $this->db->errno,
'message' => $this->db->error,
];
}
/**
* Gets the primary key value of the last query
* (works only in INSERT context)
*
* @return int|string the primary key value
*/
public function nextId () : int|string {
return $this->db->insert_id;
}
/**
* Escapes a SQL expression.
*
* @param string $expression The expression to escape
* @return string The escaped expression
*/
public function escape (string $expression) : string {
return $this->db->real_escape_string($expression);
}
public function countAffectedRows () : int {
return $this->db->affected_rows;
}
///
/// MySQL specific
///
/**
* Sets charset
*/
public function setCharset ($encoding) {
$this->db->set_charset($encoding);
}
///
/// Engine mechanics methods
///
private function throwException (mysqli_sql_exception $ex, string $query) {
$context = $this->getExceptionContext();
throw SqlException::fromException($ex, $query, $context);
}
protected function getExceptionContext () : array {
return [
'error' => $this->db->error,
'errno' => $this->db->errno,
'errors' => $this->db->error_list,
];
}
private static function getConfig (array $config) : array {
return $config + [
'host' => 'localhost',
'username' => '',
'password' => '',
'database' => '',
];
}
/**
* Loads a database instance, connected and ready to process queries.
*
* @throws EngineSetupException
*/
static function load (array $config): MySQLiEngine {
$config = self::getConfig($config);
// We need to return an exception if it fails.
// Switch report mode to the exception throwing one.
$driver = new mysqli_driver();
$configuredReportMode = $driver->report_mode;
$driver->report_mode = MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT;
try {
$instance = new self(
$config['host'],
$config['username'],
$config['password'],
$config['database'],
);
} catch (mysqli_sql_exception $ex) {
throw new EngineSetupException(
$ex->getMessage(),
$ex->getCode(),
$ex
);
}
// Restore report mode as previously configured
$driver->report_mode = $configuredReportMode;
return $instance;
}
/**
* @return mysqli Represents a connection between PHP and a MySQL database.
*/
public function getUnderlyingDriver (): mysqli {
return $this->db;
}
}
diff --git a/src/Engines/WithMySQL.php b/src/Engines/WithMySQL.php
new file mode 100644
index 0000000..9346ab8
--- /dev/null
+++ b/src/Engines/WithMySQL.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace Keruald\Database\Engines;
+
+trait WithMySQL {
+
+ public function isExistingTable (string $database, string $table) : bool {
+ $escapedTable = $this->escape($table);
+ $sql = "SHOW TABLE STATUS
+ FROM `$database`
+ WHERE Name = '$escapedTable'";
+
+ return $this->queryScalar($sql) === $table;
+ }
+
+ public function isView (string $database, string $view) : bool {
+ $escapedView = $this->escape($view);
+ $escapedDatabase = $this->escape($database);
+
+ $sql = <<<EOF
+SELECT TABLE_TYPE
+FROM information_schema.tables
+WHERE TABLE_SCHEMA = "$escapedDatabase" AND TABLE_NAME = "$escapedView"
+EOF;
+
+ return $this->queryScalar($sql) === "VIEW";
+ }
+
+}
diff --git a/tests/Engines/MySQLiEngineTest.php b/tests/Engines/MySQLiEngineTest.php
index 372af26..e618d56 100644
--- a/tests/Engines/MySQLiEngineTest.php
+++ b/tests/Engines/MySQLiEngineTest.php
@@ -1,178 +1,208 @@
<?php
namespace Keruald\Database\Tests\Engines;
use Keruald\Database\Engines\MySQLiEngine;
use Keruald\Database\Exceptions\EngineSetupException;
use Keruald\Database\Exceptions\SqlException;
use LogicException;
use PHPUnit\Framework\TestCase;
class MySQLiEngineTest extends TestCase {
private MySQLiEngine $db;
+ const DB_NAME = "test_keruald_db";
+
protected function setUp (): void {
- $this->db = new MySQLiEngine('localhost', '', '', 'test_keruald_db');
+ $this->db = new MySQLiEngine('localhost', '', '', self::DB_NAME);
}
public function testLoad () {
$instance = MySQLiEngine::load([
'host' => 'localhost',
'username' => '',
'password' => '',
- 'database' => 'test_keruald_db',
+ 'database' => self::DB_NAME,
]);
$this->assertInstanceOf("mysqli", $instance->getUnderlyingDriver());
}
public function testLoadWithWrongPassword () {
$this->expectException(EngineSetupException::class);
$instance = MySQLiEngine::load([
'host' => 'localhost',
'username' => 'notexisting',
'password' => 'notexistingeither',
- 'database' => 'test_keruald_db',
+ 'database' => self::DB_NAME,
]);
}
public function testQueryScalar () {
$sql = "SELECT 1+1";
$this->assertEquals(2, $this->db->queryScalar($sql));
}
public function testQueryScalarWithoutQuery () {
$this->assertEquals("", $this->db->queryScalar(""));
}
public function testQueryScalarWithWrongQuery () {
$this->expectException(SqlException::class);
$sql = "DELETE FROM nonexisting";
$this->db->queryScalar($sql);
}
public function testQueryScalarWithNonSelectQuery () {
$this->expectException(LogicException::class);
$sql = "UPDATE numbers SET number = number * 2";
$this->db->queryScalar($sql);
}
public function testSetCharset () {
$expected = "binary";
$this->db->setCharset($expected);
$sql = "SELECT @@SESSION.character_set_connection";
$actual = $this->db->queryScalar($sql);
$this->assertEquals($expected, $actual);
}
public function testFetchRow () {
$sql = "SELECT 10 UNION SELECT 20 UNION SELECT 30";
$result = $this->db->query($sql);
// First, we get associative arrays like [0 => 10, 10u => 10]
// ^ position ^ column name
$this->assertEquals(10, $this->db->fetchRow($result)[0]);
$this->assertEquals(20, $this->db->fetchRow($result)[0]);
$this->assertEquals(30, $this->db->fetchRow($result)[0]);
// Then, we get a null value
$this->assertEquals(null, $this->db->fetchRow($result));
}
public function testArrayShapeForFetchRow () {
$sql = "SELECT 10 as score, 50 as `limit`";
$result = $this->db->query($sql);
$expected = [
// By position
0 => 10,
1 => 50,
// By column name
"score" => 10,
"limit" => 50
];
$this->assertEquals($expected, $this->db->fetchRow($result));
}
public function testQueryWhenItSucceeds () {
$result = $this->db->query("DELETE FROM numbers");
$this->assertTrue($result);
}
public function testQueryWhenItFailsWithoutException () {
mysqli_report(MYSQLI_REPORT_OFF);
$result = $this->db->query("TRUNCATE not_existing");
$this->assertFalse($result);
}
public function testQueryWhenItFailsWithException () {
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$this->expectException(SqlException::class);
$this->db->query("TRUNCATE not_existing_table");
}
public function testQueryWithWrongQueryInLegacyMode () {
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$this->db->dontThrowExceptions = true;
$result = $this->db->query("TRUNCATE not_existing");
$this->assertFalse($result);
}
public function testNextId () {
$this->db->query("TRUNCATE numbers");
$this->db->query("INSERT INTO numbers VALUES (1700, 42742)");
$this->db->query("INSERT INTO numbers (number) VALUES (666)");
$this->assertSame(1701, $this->db->nextId());
}
public function testEscape () {
$this->assertEquals("foo\')", $this->db->escape("foo')"));
}
public function testGetUnderlyingDriver () {
$this->assertInstanceOf("mysqli", $this->db->getUnderlyingDriver());
}
public function testNumRowsForSelect () {
$sql = "SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4";
$result = $this->db->query($sql);
$this->assertSame(4, $this->db->numRows($result));
}
public function testNumRowsForInsert () {
$sql = "INSERT INTO numbers (number) VALUES (1), (2), (3), (4), (5)";
$result = $this->db->query($sql);
$this->assertSame(5, $this->db->numRows($result));
}
public function testError () {
$expected = [
"code" => 1146,
"message" => "Table 'test_keruald_db.not_existing' doesn't exist",
];
mysqli_report(MYSQLI_REPORT_OFF);
$this->db->query("TRUNCATE not_existing");
$this->assertEquals($expected, $this->db->error());
}
+ public function testIsView () {
+ $this->assertTrue($this->db->isView(self::DB_NAME, "ships_count"));
+ }
+
+ public function testIsViewWhenItIsTable () {
+ $this->assertFalse($this->db->isView(self::DB_NAME, "ships"));
+ }
+
+ public function testIsViewWhenNotExisting () {
+ $this->assertFalse($this->db->isView(self::DB_NAME, "notexisting"));
+ }
+
+ public function testIsExisting () {
+ $this->assertTrue($this->db->isExistingTable(self::DB_NAME, "ships"));
+ }
+
+ public function testIsExistingWithView () {
+ $this->assertTrue($this->db->isExistingTable(
+ self::DB_NAME, "ships_count")
+ );
+ }
+
+ public function testIsExistingWhenNotExisting () {
+ $this->assertFalse($this->db->isExistingTable(
+ self::DB_NAME, "notexisting")
+ );
+ }
+
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Fri, Sep 19, 02:59 (14 h, 7 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2992105
Default Alt Text
(17 KB)
Attached To
Mode
rKERUALD Keruald libraries development repository
Attached
Detach File
Event Timeline
Log In to Comment