mirror of
https://code.mensbeam.com/MensBeam/Arsse.git
synced 2024-12-22 13:12:41 +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.fileUncreatable" => 10206,
|
||||
"Db/Exception.fileCorrupt" => 10207,
|
||||
"Db/Exception.paramTypeInvalid" => 10401,
|
||||
"Db/Exception.paramTypeUnknown" => 10402,
|
||||
"Db/Update/Exception.tooNew" => 10211,
|
||||
"Db/Update/Exception.fileMissing" => 10212,
|
||||
"Db/Update/Exception.fileUnusable" => 10213,
|
||||
|
|
|
@ -70,12 +70,21 @@ Trait Common {
|
|||
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.
|
||||
$timezone = date_default_timezone_get();
|
||||
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.
|
||||
$date = date('Y-m-d h:i:sP', strtotime($date));
|
||||
$date = date(self::TS_FORMAT[$precision], $time);
|
||||
date_default_timezone_set($timezone);
|
||||
return $date;
|
||||
}
|
||||
|
|
|
@ -3,6 +3,16 @@ declare(strict_types=1);
|
|||
namespace JKingWeb\NewsSync\Db;
|
||||
|
||||
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);
|
||||
// returns a human-friendly name for the driver (for display in installer, for example)
|
||||
static function driverName(): string;
|
||||
|
|
|
@ -8,7 +8,7 @@ class DriverSQLite3 implements Driver {
|
|||
protected $db;
|
||||
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
|
||||
if(!class_exists("SQLite3")) throw new Exception("extMissing", self::driverName());
|
||||
$this->data = $data;
|
||||
|
|
|
@ -31,7 +31,7 @@ class ResultSQLite3 implements Result {
|
|||
|
||||
// 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->set = $result;
|
||||
$this->rows = $changes;
|
||||
|
|
|
@ -3,7 +3,31 @@ declare(strict_types=1);
|
|||
namespace JKingWeb\NewsSync\Db;
|
||||
|
||||
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 runArray(array $values): Result;
|
||||
function rebind(...$bindings): bool;
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\NewsSync\Db;
|
||||
use JKingWeb\NewsSync\Db\DriverSQLite3 as Driver;
|
||||
|
||||
class StatementSQLite3 implements Statement {
|
||||
protected $db;
|
||||
protected $st;
|
||||
protected $types;
|
||||
|
||||
public function __construct($db, $st, array $bindings = []) {
|
||||
public function __construct(\SQLite3 $db, \SQLite3Stmt $st, array $bindings = []) {
|
||||
$this->db = $db;
|
||||
$this->st = $st;
|
||||
$this->rebindArray($bindings);
|
||||
|
@ -18,10 +19,6 @@ class StatementSQLite3 implements Statement {
|
|||
unset($this->st);
|
||||
}
|
||||
|
||||
public function __invoke(...$values) {
|
||||
return $this->runArray($values);
|
||||
}
|
||||
|
||||
public function run(...$values): Result {
|
||||
return $this->runArray($values);
|
||||
}
|
||||
|
@ -30,12 +27,43 @@ class StatementSQLite3 implements Statement {
|
|||
$this->st->clear();
|
||||
$l = sizeof($values);
|
||||
for($a = 0; $a < $l; $a++) {
|
||||
// find the right SQLite binding type for the value/specified type
|
||||
$type = null;
|
||||
if($values[$a]===null) {
|
||||
$type = \SQLITE3_NULL;
|
||||
} else if(array_key_exists($a,$this->types)) {
|
||||
$type = $this->translateType($this->types[$a]);
|
||||
} 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);
|
||||
}
|
||||
|
@ -44,42 +72,36 @@ class StatementSQLite3 implements Statement {
|
|||
return $this->rebindArray($bindings);
|
||||
}
|
||||
|
||||
protected function translateType(string $type) {
|
||||
switch($type) {
|
||||
case "null":
|
||||
return \SQLITE3_NULL;
|
||||
case "integer":
|
||||
return \SQLITE3_INTEGER;
|
||||
case "float":
|
||||
return \SQLITE3_FLOAT;
|
||||
case "date":
|
||||
case "time":
|
||||
case "datetime":
|
||||
return \SQLITE3_TEXT;
|
||||
case "binary":
|
||||
return \SQLITE3_BLOB;
|
||||
case "text":
|
||||
return \SQLITE3_TEXT;
|
||||
case "boolean":
|
||||
return \SQLITE3_INTEGER;
|
||||
default:
|
||||
throw new Db\Exception("paramTypeUnknown", $binding);
|
||||
}
|
||||
}
|
||||
|
||||
public function rebindArray(array $bindings): bool {
|
||||
$this->types = [];
|
||||
foreach($bindings as $binding) {
|
||||
switch(trim(strtolower($binding))) {
|
||||
case "null":
|
||||
case "nil":
|
||||
$this->types[] = \SQLITE3_NULL; break;
|
||||
case "int":
|
||||
case "integer":
|
||||
$this->types[] = \SQLITE3_INTEGER; break;
|
||||
case "float":
|
||||
case "double":
|
||||
case "real":
|
||||
case "numeric":
|
||||
$this->types[] = \SQLITE3_FLOAT; break;
|
||||
case "date":
|
||||
case "time":
|
||||
case "datetime":
|
||||
case "timestamp":
|
||||
$this->types[] = \SQLITE3_TEXT; break;
|
||||
case "blob":
|
||||
case "bin":
|
||||
case "binary":
|
||||
$this->types[] = \SQLITE3_BLOB; break;
|
||||
case "text":
|
||||
case "string":
|
||||
case "str":
|
||||
$this->types[] = \SQLITE3_TEXT; break;
|
||||
case "bool":
|
||||
case "boolean":
|
||||
case "bit":
|
||||
$this->types[] = \SQLITE3_INTEGER; break;
|
||||
default:
|
||||
$this->types[] = \SQLITE3_TEXT; break;
|
||||
}
|
||||
$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.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.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' =>
|
||||
'{from_version, select,
|
||||
|
|
|
@ -36,14 +36,45 @@ class TestDbStatementSQLite3 extends \PHPUnit\Framework\TestCase {
|
|||
}
|
||||
|
||||
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);
|
||||
$val = $s->runArray([null])->get()['value'];
|
||||
$this->assertSame(null, $val);
|
||||
$types = array_unique(Db\Statement::TYPES);
|
||||
foreach($types as $type) {
|
||||
$s->rebindArray([$type]);
|
||||
$val = $s->runArray([null])->get()['value'];
|
||||
$this->assertSame($exp[$type], $val);
|
||||
}
|
||||
}
|
||||
|
||||
function testBindInteger() {
|
||||
$s = new Db\StatementSQLite3($this->c, $this->s, ["int"]);
|
||||
$val = $s->runArray([2112])->get()['value'];
|
||||
$this->assertSame(2112, $val);
|
||||
$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'];
|
||||
$this->assertSame($exp[$type], $val, "Type $type failed comparison.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in a new issue