-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement PostgreSQL support with PDO (unfinished)
- Loading branch information
1 parent
32185e7
commit 489ce65
Showing
7 changed files
with
332 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace PeachySQL; | ||
|
||
use PDO; | ||
use PeachySQL\Pgsql\Options; | ||
use PeachySQL\Pgsql\Statement; | ||
use PeachySQL\QueryBuilder\Insert; | ||
|
||
/** | ||
* Implements the standard PeachySQL features for PostgreSQL (using PDO) | ||
*/ | ||
class Pgsql extends PeachySql | ||
{ | ||
private PDO $conn; | ||
private bool $usedPrepare; | ||
|
||
public function __construct(PDO $connection, ?Options $options = null) | ||
{ | ||
$this->conn = $connection; | ||
$this->usedPrepare = true; | ||
|
||
if ($options === null) { | ||
$options = new Options(); | ||
} | ||
|
||
$this->options = $options; | ||
} | ||
|
||
/** | ||
* Begins a transaction | ||
* @throws SqlException if an error occurs | ||
*/ | ||
public function begin(): void | ||
{ | ||
if (!$this->conn->beginTransaction()) { | ||
throw $this->getError('Failed to begin transaction', $this->conn->errorInfo()); | ||
} | ||
} | ||
|
||
/** | ||
* Commits a transaction begun with begin() | ||
* @throws SqlException if an error occurs | ||
*/ | ||
public function commit(): void | ||
{ | ||
if (!$this->conn->commit()) { | ||
throw $this->getError('Failed to commit transaction', $this->conn->errorInfo()); | ||
} | ||
} | ||
|
||
/** | ||
* Rolls back a transaction begun with begin() | ||
* @throws SqlException if an error occurs | ||
*/ | ||
public function rollback(): void | ||
{ | ||
if (!$this->conn->rollback()) { | ||
throw $this->getError('Failed to roll back transaction', $this->conn->errorInfo()); | ||
} | ||
} | ||
|
||
public function makeBinaryParam(?string $binaryStr, ?int $length = null): ?string | ||
{ | ||
// binary values can be inserted directly when using MySQL | ||
return $binaryStr; | ||
} | ||
|
||
/** | ||
* @internal | ||
*/ | ||
public static function getError(string $msg, array $error, string $sql = '', array $params = []): SqlException | ||
{ | ||
/** @var array{0: string, 1: int|null, 2: string|null} $error */ | ||
$code = $error[1] ?? 0; | ||
$details = $error[2] ?? ''; | ||
$sqlState = $error[0]; | ||
|
||
return new SqlException($msg, $code, $details, $sqlState, $sql, $params); | ||
} | ||
|
||
/** | ||
* Returns a prepared statement which can be executed multiple times | ||
* @throws SqlException if an error occurs | ||
*/ | ||
public function prepare(string $sql, array $params = []): Statement | ||
{ | ||
try { | ||
if (!$stmt = $this->conn->prepare($sql)) { | ||
throw $this->getError('Failed to prepare statement', $this->conn->errorInfo(), $sql, $params); | ||
} | ||
} catch (\PDOException $e) { | ||
throw $this->getError('Failed to prepare statement', $this->conn->errorInfo(), $sql, $params); | ||
} | ||
|
||
return new Statement($stmt, $this->usedPrepare, $sql, $params); | ||
} | ||
|
||
/** | ||
* Prepares and executes a single query with bound parameters | ||
*/ | ||
public function query(string $sql, array $params = []): Statement | ||
{ | ||
$this->usedPrepare = false; | ||
$stmt = $this->prepare($sql, $params); | ||
$this->usedPrepare = true; | ||
$stmt->execute(); | ||
return $stmt; | ||
} | ||
|
||
/** | ||
* Performs a single bulk insert query | ||
*/ | ||
protected function insertBatch(string $table, array $colVals, int $identityIncrement = 1): BulkInsertResult | ||
{ | ||
$sqlParams = (new Insert($this->options))->buildQuery($table, $colVals); | ||
$result = $this->query($sqlParams->sql, $sqlParams->params); | ||
|
||
try { | ||
$lastId = (int) $this->conn->lastInsertId(); | ||
} catch (\PDOException $e) { | ||
$lastId = 0; | ||
} | ||
|
||
if ($lastId) { | ||
$firstId = $lastId - $identityIncrement * (count($colVals) -1); | ||
$ids = range($firstId, $lastId, $identityIncrement); | ||
} else { | ||
$ids = []; | ||
} | ||
|
||
return new BulkInsertResult($ids, $result->getAffected()); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace PeachySQL\Pgsql; | ||
|
||
use PeachySQL\BaseOptions; | ||
|
||
/** | ||
* Handles PostgreSQL-specific options | ||
*/ | ||
class Options extends BaseOptions | ||
{ | ||
// https://stackoverflow.com/questions/6581573/what-are-the-max-number-of-allowable-parameters-per-database-provider-type | ||
public int $maxBoundParams = 65_535; // 2 ** 16 - 1; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace PeachySQL\Pgsql; | ||
|
||
use PeachySQL\BaseStatement; | ||
use PeachySQL\Pgsql; | ||
use PDO; | ||
use PDOException; | ||
use PDOStatement; | ||
|
||
class Statement extends BaseStatement | ||
{ | ||
private ?PDOStatement $stmt; | ||
|
||
public function __construct(PDOStatement $stmt, bool $usedPrepare, string $query, array $params) | ||
{ | ||
parent::__construct($usedPrepare, $query, $params); | ||
$this->stmt = $stmt; | ||
} | ||
|
||
public function execute(): void | ||
{ | ||
if ($this->stmt === null) { | ||
throw new \Exception('Cannot execute closed statement'); | ||
} | ||
|
||
try { | ||
if (!$this->stmt->execute($this->params)) { | ||
throw Pgsql::getError('Failed to execute prepared statement', $this->stmt->errorInfo(), $this->query, $this->params); | ||
} | ||
} catch (PDOException $e) { | ||
throw Pgsql::getError('Failed to execute prepared statement', $this->stmt->errorInfo(), $this->query, $this->params); | ||
} | ||
|
||
$this->affected = $this->stmt->rowCount(); | ||
|
||
if (!$this->usedPrepare && $this->stmt->columnCount() === 0) { | ||
$this->close(); // no results, so statement can be closed | ||
} | ||
} | ||
|
||
public function getIterator(): \Generator | ||
Check failure on line 44 in lib/Pgsql/Statement.php GitHub Actions / Run tests on 8.3MixedReturnTypeCoercion
|
||
{ | ||
if ($this->stmt !== null) { | ||
/** @var array|false $row */ | ||
while ($row = $this->stmt->fetch(PDO::FETCH_ASSOC)) { | ||
yield $row; | ||
} | ||
|
||
if (!$this->usedPrepare) { | ||
$this->close(); | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Closes the prepared statement and deallocates the statement handle. | ||
* @throws \Exception if the statement has already been closed | ||
*/ | ||
public function close(): void | ||
{ | ||
if ($this->stmt === null) { | ||
throw new \Exception('Statement has already been closed'); | ||
} | ||
|
||
$this->stmt->closeCursor(); | ||
$this->stmt = null; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace PeachySQL\Test\Pgsql; | ||
|
||
use PeachySQL\Pgsql\Options; | ||
use PHPUnit\Framework\TestCase; | ||
|
||
/** | ||
* Tests base as well as PostgreSQL-specific configuration settings | ||
*/ | ||
class OptionsTest extends TestCase | ||
{ | ||
public function testEscapeIdentifier(): void | ||
{ | ||
$options = new Options(); | ||
$actual = $options->escapeIdentifier('Test"Identifier'); | ||
$this->assertSame('"Test""Identifier"', $actual); | ||
|
||
try { | ||
$options->escapeIdentifier(''); // should throw exception | ||
$this->fail('escapeIdentifier failed to throw expected exception'); | ||
} catch (\InvalidArgumentException $e) { | ||
$this->assertSame('Identifier cannot be blank', $e->getMessage()); | ||
} | ||
} | ||
|
||
public function testBuildPagination(): void | ||
{ | ||
$options = new Options(); | ||
|
||
$page1 = $options->buildPagination(25, 0); | ||
$this->assertSame('LIMIT 25 OFFSET 0', $page1); | ||
|
||
$page3 = $options->buildPagination(100, 200); | ||
$this->assertSame('LIMIT 100 OFFSET 200', $page3); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace PeachySQL\Test\Pgsql; | ||
|
||
use PeachySQL\Pgsql; | ||
use PeachySQL\Test\DbTestCase; | ||
use PeachySQL\Test\src\App; | ||
use PDO; | ||
|
||
/** | ||
* @group pgsql | ||
*/ | ||
class PgsqlDbTest extends DbTestCase | ||
{ | ||
private static ?Pgsql $db = null; | ||
|
||
protected function getExpectedBadSyntaxCode(): int | ||
{ | ||
return 7; | ||
} | ||
|
||
public static function dbProvider(): array | ||
{ | ||
if (!self::$db) { | ||
$c = App::$config; | ||
$pdo = new PDO($c->getPgsqlDsn(), $c->getPgsqlUser(), $c->getPgsqlPassword()); | ||
|
||
self::$db = new Pgsql($pdo); | ||
self::createTestTable(self::$db); | ||
} | ||
|
||
return [ | ||
[self::$db], | ||
]; | ||
} | ||
|
||
private static function createTestTable(Pgsql $db): void | ||
{ | ||
$sql = " | ||
CREATE TABLE Users ( | ||
user_id SERIAL PRIMARY KEY, | ||
name VARCHAR(50) NOT NULL, | ||
dob DATE NOT NULL, | ||
weight REAL NOT NULL, | ||
isDisabled BOOLEAN NOT NULL, | ||
uuid bytea NULL | ||
)"; | ||
|
||
$db->query("DROP TABLE IF EXISTS Users"); | ||
$db->query($sql); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters