mirror of
https://code.mensbeam.com/MensBeam/Arsse.git
synced 2024-12-23 07:04:53 +00:00
Partial proper tests for parameter bindings
Null and (>0) integer tested so far Many related changes to accomodate the necessary type juggling (required for databases other than SQLite).
This commit is contained in:
parent
337b2cf90c
commit
1529fc367a
9 changed files with 150 additions and 50 deletions
|
@ -21,6 +21,8 @@ abstract class AbstractException extends \Exception {
|
||||||
"Db/Exception.fileUnwritable" => 10205,
|
"Db/Exception.fileUnwritable" => 10205,
|
||||||
"Db/Exception.fileUncreatable" => 10206,
|
"Db/Exception.fileUncreatable" => 10206,
|
||||||
"Db/Exception.fileCorrupt" => 10207,
|
"Db/Exception.fileCorrupt" => 10207,
|
||||||
|
"Db/Exception.paramTypeInvalid" => 10401,
|
||||||
|
"Db/Exception.paramTypeUnknown" => 10402,
|
||||||
"Db/Update/Exception.tooNew" => 10211,
|
"Db/Update/Exception.tooNew" => 10211,
|
||||||
"Db/Update/Exception.fileMissing" => 10212,
|
"Db/Update/Exception.fileMissing" => 10212,
|
||||||
"Db/Update/Exception.fileUnusable" => 10213,
|
"Db/Update/Exception.fileUnusable" => 10213,
|
||||||
|
|
|
@ -70,12 +70,21 @@ Trait Common {
|
||||||
return $this->prepareArray($query, $paramType);
|
return $this->prepareArray($query, $paramType);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function formatDate(string $date): string {
|
public static function formatDate($date, int $precision = self::TS_BOTH): string {
|
||||||
// Force UTC.
|
// Force UTC.
|
||||||
$timezone = date_default_timezone_get();
|
$timezone = date_default_timezone_get();
|
||||||
date_default_timezone_set('UTC');
|
date_default_timezone_set('UTC');
|
||||||
|
// convert input to a Unix timestamp
|
||||||
|
// FIXME: there are more kinds of date representations
|
||||||
|
if(is_int($date)) {
|
||||||
|
$time = $date;
|
||||||
|
} else if($date===null) {
|
||||||
|
$time = 0;
|
||||||
|
} else {
|
||||||
|
$time = strtotime($date);
|
||||||
|
}
|
||||||
// ISO 8601 with space in the middle instead of T.
|
// ISO 8601 with space in the middle instead of T.
|
||||||
$date = date('Y-m-d h:i:sP', strtotime($date));
|
$date = date(self::TS_FORMAT[$precision], $time);
|
||||||
date_default_timezone_set($timezone);
|
date_default_timezone_set($timezone);
|
||||||
return $date;
|
return $date;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,16 @@ declare(strict_types=1);
|
||||||
namespace JKingWeb\NewsSync\Db;
|
namespace JKingWeb\NewsSync\Db;
|
||||||
|
|
||||||
interface Driver {
|
interface Driver {
|
||||||
|
const TS_TIME = -1;
|
||||||
|
const TS_DATE = 0;
|
||||||
|
const TS_BOTH = 1;
|
||||||
|
|
||||||
|
const TS_FORMAT = [
|
||||||
|
self::TS_TIME => 'h:i:sP',
|
||||||
|
self::TS_DATE => 'Y-m-d',
|
||||||
|
self::TS_BOTH => 'Y-m-d h:i:sP',
|
||||||
|
];
|
||||||
|
|
||||||
function __construct(\JKingWeb\NewsSync\RuntimeData $data, bool $install = false);
|
function __construct(\JKingWeb\NewsSync\RuntimeData $data, bool $install = false);
|
||||||
// returns a human-friendly name for the driver (for display in installer, for example)
|
// returns a human-friendly name for the driver (for display in installer, for example)
|
||||||
static function driverName(): string;
|
static function driverName(): string;
|
||||||
|
|
|
@ -8,7 +8,7 @@ class DriverSQLite3 implements Driver {
|
||||||
protected $db;
|
protected $db;
|
||||||
protected $data;
|
protected $data;
|
||||||
|
|
||||||
private function __construct(\JKingWeb\NewsSync\RuntimeData $data, bool $install = false) {
|
public function __construct(\JKingWeb\NewsSync\RuntimeData $data, bool $install = false) {
|
||||||
// check to make sure required extension is loaded
|
// check to make sure required extension is loaded
|
||||||
if(!class_exists("SQLite3")) throw new Exception("extMissing", self::driverName());
|
if(!class_exists("SQLite3")) throw new Exception("extMissing", self::driverName());
|
||||||
$this->data = $data;
|
$this->data = $data;
|
||||||
|
|
|
@ -31,7 +31,7 @@ class ResultSQLite3 implements Result {
|
||||||
|
|
||||||
// constructor/destructor
|
// constructor/destructor
|
||||||
|
|
||||||
public function __construct($result, $changes = 0, $statement = null) {
|
public function __construct(\SQLite3Result $result, int $changes = 0, StatementSQLite3 $statement = null) {
|
||||||
$this->st = $statement; //keeps the statement from being destroyed, invalidating the result set
|
$this->st = $statement; //keeps the statement from being destroyed, invalidating the result set
|
||||||
$this->set = $result;
|
$this->set = $result;
|
||||||
$this->rows = $changes;
|
$this->rows = $changes;
|
||||||
|
|
|
@ -3,7 +3,31 @@ declare(strict_types=1);
|
||||||
namespace JKingWeb\NewsSync\Db;
|
namespace JKingWeb\NewsSync\Db;
|
||||||
|
|
||||||
interface Statement {
|
interface Statement {
|
||||||
function __invoke(...$values); // alias of run()
|
|
||||||
|
const TYPES = [
|
||||||
|
"null" => "null",
|
||||||
|
"nil" => "null",
|
||||||
|
"int" => "integer",
|
||||||
|
"integer" => "integer",
|
||||||
|
"float" => "float",
|
||||||
|
"double" => "float",
|
||||||
|
"real" => "float",
|
||||||
|
"numeric" => "float",
|
||||||
|
"date" => "date",
|
||||||
|
"time" => "time",
|
||||||
|
"datetime" => "datetime",
|
||||||
|
"timestamp" => "datetime",
|
||||||
|
"blob" => "binary",
|
||||||
|
"bin" => "binary",
|
||||||
|
"binary" => "binary",
|
||||||
|
"text" => "text",
|
||||||
|
"string" => "text",
|
||||||
|
"str" => "text",
|
||||||
|
"bool" => "boolean",
|
||||||
|
"boolean" => "boolean",
|
||||||
|
"bit" => "boolean",
|
||||||
|
];
|
||||||
|
|
||||||
function run(...$values): Result;
|
function run(...$values): Result;
|
||||||
function runArray(array $values): Result;
|
function runArray(array $values): Result;
|
||||||
function rebind(...$bindings): bool;
|
function rebind(...$bindings): bool;
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
namespace JKingWeb\NewsSync\Db;
|
namespace JKingWeb\NewsSync\Db;
|
||||||
|
use JKingWeb\NewsSync\Db\DriverSQLite3 as Driver;
|
||||||
|
|
||||||
class StatementSQLite3 implements Statement {
|
class StatementSQLite3 implements Statement {
|
||||||
protected $db;
|
protected $db;
|
||||||
protected $st;
|
protected $st;
|
||||||
protected $types;
|
protected $types;
|
||||||
|
|
||||||
public function __construct($db, $st, array $bindings = []) {
|
public function __construct(\SQLite3 $db, \SQLite3Stmt $st, array $bindings = []) {
|
||||||
$this->db = $db;
|
$this->db = $db;
|
||||||
$this->st = $st;
|
$this->st = $st;
|
||||||
$this->rebindArray($bindings);
|
$this->rebindArray($bindings);
|
||||||
|
@ -18,10 +19,6 @@ class StatementSQLite3 implements Statement {
|
||||||
unset($this->st);
|
unset($this->st);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function __invoke(...$values) {
|
|
||||||
return $this->runArray($values);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function run(...$values): Result {
|
public function run(...$values): Result {
|
||||||
return $this->runArray($values);
|
return $this->runArray($values);
|
||||||
}
|
}
|
||||||
|
@ -30,12 +27,43 @@ class StatementSQLite3 implements Statement {
|
||||||
$this->st->clear();
|
$this->st->clear();
|
||||||
$l = sizeof($values);
|
$l = sizeof($values);
|
||||||
for($a = 0; $a < $l; $a++) {
|
for($a = 0; $a < $l; $a++) {
|
||||||
|
// find the right SQLite binding type for the value/specified type
|
||||||
|
$type = null;
|
||||||
if($values[$a]===null) {
|
if($values[$a]===null) {
|
||||||
$type = \SQLITE3_NULL;
|
$type = \SQLITE3_NULL;
|
||||||
|
} else if(array_key_exists($a,$this->types)) {
|
||||||
|
$type = $this->translateType($this->types[$a]);
|
||||||
} else {
|
} else {
|
||||||
$type = (array_key_exists($a,$this->types)) ? $this->types[$a] : \SQLITE3_TEXT;
|
$type = \SQLITE3_TEXT;
|
||||||
|
}
|
||||||
|
// cast values if necessary
|
||||||
|
switch($this->types[$a]) {
|
||||||
|
case "null":
|
||||||
|
$value = null; break;
|
||||||
|
case "integer":
|
||||||
|
$value = (int) $values[$a]; break;
|
||||||
|
case "float":
|
||||||
|
$value = (float) $values[$a]; break;
|
||||||
|
case "date":
|
||||||
|
$value = Driver::formatDate($values[$a], Driver::TS_DATE); break;
|
||||||
|
case "time":
|
||||||
|
$value = Driver::formatDate($values[$a], Driver::TS_TIME); break;
|
||||||
|
case "datetime":
|
||||||
|
$value = Driver::formatDate($values[$a], Driver::TS_BOTH); break;
|
||||||
|
case "binary":
|
||||||
|
$value = (string) $values[$a]; break;
|
||||||
|
case "text":
|
||||||
|
$value = $values[$a]; break;
|
||||||
|
case "boolean":
|
||||||
|
$value = (bool) $values[$a]; break;
|
||||||
|
default:
|
||||||
|
throw new Exception("paramTypeUnknown", $type);
|
||||||
|
}
|
||||||
|
if($type===null) {
|
||||||
|
$this->st->bindParam($a+1, $value);
|
||||||
|
} else {
|
||||||
|
$this->st->bindParam($a+1, $value, $type);
|
||||||
}
|
}
|
||||||
$this->st->bindParam($a+1, $values[$a], $type);
|
|
||||||
}
|
}
|
||||||
return new ResultSQLite3($this->st->execute(), $this->db->changes(), $this);
|
return new ResultSQLite3($this->st->execute(), $this->db->changes(), $this);
|
||||||
}
|
}
|
||||||
|
@ -44,42 +72,36 @@ class StatementSQLite3 implements Statement {
|
||||||
return $this->rebindArray($bindings);
|
return $this->rebindArray($bindings);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function rebindArray(array $bindings): bool {
|
protected function translateType(string $type) {
|
||||||
$this->types = [];
|
switch($type) {
|
||||||
foreach($bindings as $binding) {
|
|
||||||
switch(trim(strtolower($binding))) {
|
|
||||||
case "null":
|
case "null":
|
||||||
case "nil":
|
return \SQLITE3_NULL;
|
||||||
$this->types[] = \SQLITE3_NULL; break;
|
|
||||||
case "int":
|
|
||||||
case "integer":
|
case "integer":
|
||||||
$this->types[] = \SQLITE3_INTEGER; break;
|
return \SQLITE3_INTEGER;
|
||||||
case "float":
|
case "float":
|
||||||
case "double":
|
return \SQLITE3_FLOAT;
|
||||||
case "real":
|
|
||||||
case "numeric":
|
|
||||||
$this->types[] = \SQLITE3_FLOAT; break;
|
|
||||||
case "date":
|
case "date":
|
||||||
case "time":
|
case "time":
|
||||||
case "datetime":
|
case "datetime":
|
||||||
case "timestamp":
|
return \SQLITE3_TEXT;
|
||||||
$this->types[] = \SQLITE3_TEXT; break;
|
|
||||||
case "blob":
|
|
||||||
case "bin":
|
|
||||||
case "binary":
|
case "binary":
|
||||||
$this->types[] = \SQLITE3_BLOB; break;
|
return \SQLITE3_BLOB;
|
||||||
case "text":
|
case "text":
|
||||||
case "string":
|
return \SQLITE3_TEXT;
|
||||||
case "str":
|
|
||||||
$this->types[] = \SQLITE3_TEXT; break;
|
|
||||||
case "bool":
|
|
||||||
case "boolean":
|
case "boolean":
|
||||||
case "bit":
|
return \SQLITE3_INTEGER;
|
||||||
$this->types[] = \SQLITE3_INTEGER; break;
|
|
||||||
default:
|
default:
|
||||||
$this->types[] = \SQLITE3_TEXT; break;
|
throw new Db\Exception("paramTypeUnknown", $binding);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function rebindArray(array $bindings): bool {
|
||||||
|
$this->types = [];
|
||||||
|
foreach($bindings as $binding) {
|
||||||
|
$binding = trim(strtolower($binding));
|
||||||
|
if(!array_key_exists($binding, self::TYPES)) throw new Db\Exception("paramTypeInvalid", $binding);
|
||||||
|
$this->types[] = self::TYPES[$binding];
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -27,6 +27,8 @@ return [
|
||||||
'Exception.JKingWeb/NewsSync/Db/Exception.fileUnusable' => 'Insufficient permissions to open database file "{0}" for reading or writing',
|
'Exception.JKingWeb/NewsSync/Db/Exception.fileUnusable' => 'Insufficient permissions to open database file "{0}" for reading or writing',
|
||||||
'Exception.JKingWeb/NewsSync/Db/Exception.fileUncreatable' => 'Insufficient permissions to create new database file "{0}"',
|
'Exception.JKingWeb/NewsSync/Db/Exception.fileUncreatable' => 'Insufficient permissions to create new database file "{0}"',
|
||||||
'Exception.JKingWeb/NewsSync/Db/Exception.fileCorrupt' => 'Database file "{0}" is corrupt or not a valid database',
|
'Exception.JKingWeb/NewsSync/Db/Exception.fileCorrupt' => 'Database file "{0}" is corrupt or not a valid database',
|
||||||
|
'Exception.JKingWeb/NewsSync/Db/Exception.paramTypeInvalid' => 'Prepared statement parameter type "{0}" is invalid',
|
||||||
|
'Exception.JKingWeb/NewsSync/Db/Exception.paramTypeUnknown' => 'Prepared statement parameter type "{0}" is valid, but not implemented',
|
||||||
|
|
||||||
'Exception.JKingWeb/NewsSync/Db/Update/Exception.manual' =>
|
'Exception.JKingWeb/NewsSync/Db/Update/Exception.manual' =>
|
||||||
'{from_version, select,
|
'{from_version, select,
|
||||||
|
|
|
@ -36,14 +36,45 @@ class TestDbStatementSQLite3 extends \PHPUnit\Framework\TestCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
function testBindNull() {
|
function testBindNull() {
|
||||||
|
$exp = [
|
||||||
|
"null" => null,
|
||||||
|
"integer" => null,
|
||||||
|
"float" => null,
|
||||||
|
"date" => null,
|
||||||
|
"time" => null,
|
||||||
|
"datetime" => null,
|
||||||
|
"binary" => null,
|
||||||
|
"text" => null,
|
||||||
|
"boolean" => null,
|
||||||
|
];
|
||||||
$s = new Db\StatementSQLite3($this->c, $this->s);
|
$s = new Db\StatementSQLite3($this->c, $this->s);
|
||||||
|
$types = array_unique(Db\Statement::TYPES);
|
||||||
|
foreach($types as $type) {
|
||||||
|
$s->rebindArray([$type]);
|
||||||
$val = $s->runArray([null])->get()['value'];
|
$val = $s->runArray([null])->get()['value'];
|
||||||
$this->assertSame(null, $val);
|
$this->assertSame($exp[$type], $val);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function testBindInteger() {
|
function testBindInteger() {
|
||||||
$s = new Db\StatementSQLite3($this->c, $this->s, ["int"]);
|
$exp = [
|
||||||
|
"null" => null,
|
||||||
|
"integer" => 2112,
|
||||||
|
"float" => 2112.0,
|
||||||
|
"date" => date('Y-m-d', 2112),
|
||||||
|
"time" => date('h:i:sP', 2112),
|
||||||
|
"datetime" => date('Y-m-d h:i:sP', 2112),
|
||||||
|
"binary" => "2112",
|
||||||
|
"text" => "2112",
|
||||||
|
"boolean" => 1,
|
||||||
|
];
|
||||||
|
$s = new Db\StatementSQLite3($this->c, $this->s);
|
||||||
|
$types = array_unique(Db\Statement::TYPES);
|
||||||
|
foreach($types as $type) {
|
||||||
|
$s->rebindArray([$type]);
|
||||||
$val = $s->runArray([2112])->get()['value'];
|
$val = $s->runArray([2112])->get()['value'];
|
||||||
$this->assertSame(2112, $val);
|
$this->assertSame($exp[$type], $val, "Type $type failed comparison.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in a new issue