Change PDO to singleton instance

- Add SQLite throwaway DB implementation
- Add integration tests for Custom class
This commit is contained in:
Daniel J. Summers 2024-06-07 22:14:17 -04:00
parent d9ffc36fe6
commit 1ab961e35a
10 changed files with 244 additions and 11 deletions

View File

@ -17,7 +17,8 @@
"autoload-dev": {
"psr-4": {
"Test\\Unit\\": "./tests/unit",
"Test\\Integration\\": "./tests/integration"
"Test\\Integration\\": "./tests/integration",
"Test\\Integration\\SQLite\\": "./tests/integration/sqlite"
}
}
}

14
composer.lock generated
View File

@ -619,16 +619,16 @@
},
{
"name": "phpunit/phpunit",
"version": "11.1.3",
"version": "11.2.0",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "d475be032238173ca3b0a516f5cc291d174708ae"
"reference": "705eba0190afe04bc057f565ad843267717cf109"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d475be032238173ca3b0a516f5cc291d174708ae",
"reference": "d475be032238173ca3b0a516f5cc291d174708ae",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/705eba0190afe04bc057f565ad843267717cf109",
"reference": "705eba0190afe04bc057f565ad843267717cf109",
"shasum": ""
},
"require": {
@ -667,7 +667,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "11.1-dev"
"dev-main": "11.2-dev"
}
},
"autoload": {
@ -699,7 +699,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/11.1.3"
"source": "https://github.com/sebastianbergmann/phpunit/tree/11.2.0"
},
"funding": [
{
@ -715,7 +715,7 @@
"type": "tidelift"
}
],
"time": "2024-04-24T06:34:25+00:00"
"time": "2024-06-07T04:48:50+00:00"
},
{
"name": "sebastian/cli-parser",

View File

@ -58,4 +58,10 @@ class Configuration
return self::$_pdo;
}
public static function resetPDO(): void
{
self::$_pdo = null;
}
}

View File

@ -4,6 +4,7 @@ namespace BitBadger\PDODocument;
use BitBadger\PDODocument\Mapper\Mapper;
use PDO;
use PDOException;
use PDOStatement;
/**
@ -22,7 +23,13 @@ class Custom
public static function &runQuery(string $query, array $parameters): PDOStatement
{
$debug = defined('PDO_DOC_DEBUG_SQL');
try {
$stmt = Configuration::dbConn()->prepare($query);
} catch (PDOException $ex) {
$keyword = explode(' ', $query, 2)[0];
throw new DocumentException("Error executing $keyword statement: " . Configuration::dbConn()->errorCode(),
previous: $ex);
}
foreach ($parameters as $key => $value) {
if ($debug) echo "<pre>Binding $value to $key\n</pre>";
$dataType = match (true) {
@ -115,7 +122,7 @@ class Custom
* @return mixed|false|T The scalar value if found, false if not
* @throws DocumentException If any is encountered
*/
public static function scalar(string $query, array $parameters, Mapper $mapper, ?PDO $pdo = null): mixed
public static function scalar(string $query, array $parameters, Mapper $mapper): mixed
{
try {
$stmt = &self::runQuery($query, $parameters);

View File

@ -3,7 +3,7 @@
namespace BitBadger\PDODocument\Mapper;
/**
* Map a string result from the given
* Map a string result from the
*
* @implements Mapper<string>
*/

View File

@ -0,0 +1,11 @@
<?php declare(strict_types=1);
namespace Test\Integration;
/**
* A sub-document for testing
*/
class SubDocument
{
public function __construct(public string $foo = '', public string $bar = '') { }
}

View File

@ -0,0 +1,9 @@
<?php declare(strict_types=1);
namespace Test\Integration;
class TestDocument
{
public function __construct(public string $id = '', public string $value = '', public int $num_value = 0,
public ?SubDocument $sub = null) { }
}

View File

@ -0,0 +1,128 @@
<?php declare(strict_types=1);
namespace Test\Integration\SQLite;
use BitBadger\PDODocument\Count;
use BitBadger\PDODocument\Custom;
use BitBadger\PDODocument\DocumentException;
use BitBadger\PDODocument\Mapper\CountMapper;
use BitBadger\PDODocument\Mapper\DocumentMapper;
use BitBadger\PDODocument\Mapper\StringMapper;
use BitBadger\PDODocument\Query;
use PHPUnit\Framework\TestCase;
use Test\Integration\TestDocument;
/**
* SQLite Integration tests for the Custom class
*/
class CustomTest extends TestCase
{
/** @var string Database name for throwaway database */
private string $dbName;
public function setUp(): void
{
parent::setUp();
$this->dbName = ThrowawayDb::create();
}
public function tearDown(): void
{
ThrowawayDb::destroy($this->dbName);
}
public function testRunQuerySucceedsWithAValidQuery()
{
$stmt = &Custom::runQuery('SELECT data FROM ' . ThrowawayDb::TABLE . ' LIMIT 1', []);
try {
$this->assertNotNull($stmt, 'The statement should not have been null');
} finally {
$stmt = null;
}
}
public function testRunQueryFailsWithAnInvalidQuery()
{
$this->expectException(DocumentException::class);
$stmt = &Custom::runQuery('GRAB stuff FROM over_there UNTIL done', []);
try {
$this->assertTrue(false, 'This code should not be reached');
} finally {
$stmt = null;
}
}
public function testListSucceedsWhenDataIsFound()
{
$list = Custom::list(Query::selectFromTable(ThrowawayDb::TABLE), [], new DocumentMapper(TestDocument::class));
$this->assertNotNull($list, 'The document list should not be null');
$count = 0;
foreach ($list->items() as $ignored) $count++;
$this->assertEquals(5, $count, 'There should have been 5 documents in the list');
}
public function testListSucceedsWhenNoDataIsFound()
{
$list = Custom::list(Query::selectFromTable(ThrowawayDb::TABLE) . " WHERE data->>'num_value' > :value",
[':value' => 100], new DocumentMapper(TestDocument::class));
$this->assertNotNull($list, 'The document list should not be null');
$count = 0;
foreach ($list->items() as $ignored) $count++;
$this->assertEquals(0, $count, 'There should have been no documents in the list');
}
public function testArraySucceedsWhenDataIsFound()
{
$array = Custom::array(Query::selectFromTable(ThrowawayDb::TABLE) . " WHERE data->>'sub' IS NOT NULL", [],
new DocumentMapper(TestDocument::class));
$this->assertNotNull($array, 'The document array should not be null');
$this->assertCount(2, $array, 'There should have been 2 documents in the array');
}
public function testArraySucceedsWhenNoDataIsFound()
{
$array = Custom::array(Query::selectFromTable(ThrowawayDb::TABLE) . " WHERE data->>'value' = :value",
[':value' => 'not there'], new DocumentMapper(TestDocument::class));
$this->assertNotNull($array, 'The document array should not be null');
$this->assertCount(0, $array, 'There should have been no documents in the array');
}
public function testSingleSucceedsWhenARowIsFound(): void
{
$doc = Custom::single('SELECT data FROM ' . ThrowawayDb::TABLE . " WHERE data->>'id' = :id", [':id' => 'one'],
new DocumentMapper(TestDocument::class));
$this->assertNotNull($doc, 'There should have been a document returned');
$this->assertEquals('one', $doc->id, 'The incorrect document was returned');
}
public function testSingleSucceedsWhenARowIsNotFound(): void
{
$doc = Custom::single('SELECT data FROM ' . ThrowawayDb::TABLE . " WHERE data->>'id' = :id",
[':id' => 'eighty'], new DocumentMapper(TestDocument::class));
$this->assertFalse($doc, 'There should not have been a document returned');
}
public function testNonQuerySucceedsWhenOperatingOnData()
{
Custom::nonQuery('DELETE FROM ' . ThrowawayDb::TABLE, []);
try {
$remaining = Count::all(ThrowawayDb::TABLE);
$this->assertEquals(0, $remaining, 'There should be no documents remaining in the table');
} finally {
$this->dbName = ThrowawayDb::exchange($this->dbName);
}
}
public function testNonQuerySucceedsWhenNoDataMatchesWhereClause()
{
Custom::nonQuery('DELETE FROM ' . ThrowawayDb::TABLE . " WHERE data->>'num_value' > :value", [':value' => 100]);
$remaining = Count::all(ThrowawayDb::TABLE);
$this->assertEquals(5, $remaining, 'There should be 5 documents remaining in the table');
}
public function testScalarSucceeds()
{
$value = Custom::scalar("SELECT 5 AS it", [], new CountMapper());
$this->assertEquals(5, $value, 'The scalar value was not returned correctly');
}
}

View File

@ -0,0 +1,71 @@
<?php
namespace Test\Integration\SQLite;
use BitBadger\PDODocument\Configuration;
use BitBadger\PDODocument\Definition;
use BitBadger\PDODocument\Document;
use BitBadger\PDODocument\DocumentException;
use BitBadger\PDODocument\Mode;
use Test\Integration\SubDocument;
use Test\Integration\TestDocument;
/**
* Utilities to create and destroy a throwaway SQLite database to use for testing
*/
class ThrowawayDb
{
/** @var string The table used for document manipulation */
public const string TABLE = "test_table";
/**
* Create a throwaway SQLite database
*
* @param bool $withData Whether to initialize this database with data (optional; defaults to `true`)
* @return string The name of the database (use to pass to `destroy` function at end of test)
* @throws DocumentException If any is encountered
*/
public static function create(bool $withData = true): string
{
$fileName = sprintf('throwaway-%s-%d.db', date('His'), rand(10, 99));
Configuration::$pdoDSN = "sqlite:./$fileName";
Configuration::$mode = Mode::SQLite;
Configuration::resetPDO();
if ($withData) {
Definition::ensureTable(self::TABLE);
Document::insert(self::TABLE, new TestDocument('one', 'FIRST!', 0));
Document::insert(self::TABLE, new TestDocument('two', 'another', 10, new SubDocument('green', 'blue')));
Document::insert(self::TABLE, new TestDocument('three', '', 4));
Document::insert(self::TABLE, new TestDocument('four', 'purple', 17, new SubDocument('green', 'red')));
Document::insert(self::TABLE, new TestDocument('five', 'purple', 18));
}
return $fileName;
}
/**
* Destroy a throwaway SQLite database
*
* @param string $fileName The name of the SQLite database to be deleted
*/
public static function destroy(string $fileName): void
{
Configuration::resetPDO();
unlink("./$fileName");
}
/**
* Destroy the given throwaway database and create another
*
* @param string $fileName The name of the database to be destroyed
* @param bool $withData Whether to initialize the database with data (optional; defaults to `true`)
* @return string The name of the new database
* @throws DocumentException If any is encountered
*/
public static function exchange(string $fileName, bool $withData = true): string
{
self::destroy($fileName);
return self::create($withData);
}
}