Browse Source

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).
microsub
J. King 7 years ago
parent
commit
1529fc367a
  1. 2
      lib/AbstractException.php
  2. 13
      lib/Db/Common.php
  3. 10
      lib/Db/Driver.php
  4. 2
      lib/Db/DriverSQLite3.php
  5. 2
      lib/Db/ResultSQLite3.php
  6. 26
      lib/Db/Statement.php
  7. 102
      lib/Db/StatementSQLite3.php
  8. 2
      locale/en.php
  9. 41
      tests/Db/SQLite3/TestDbStatementSQLite3.php

2
lib/AbstractException.php

@ -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,

13
lib/Db/Common.php

@ -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;
}

10
lib/Db/Driver.php

@ -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;

2
lib/Db/DriverSQLite3.php

@ -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;

2
lib/Db/ResultSQLite3.php

@ -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;

26
lib/Db/Statement.php

@ -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;

102
lib/Db/StatementSQLite3.php

@ -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,56 +27,81 @@ 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;
}
$this->st->bindParam($a+1, $values[$a], $type);
}
return new ResultSQLite3($this->st->execute(), $this->db->changes(), $this);
}
public function rebind(...$bindings): bool {
return $this->rebindArray($bindings);
}
public function rebindArray(array $bindings): bool {
$this->types = [];
foreach($bindings as $binding) {
switch(trim(strtolower($binding))) {
// cast values if necessary
switch($this->types[$a]) {
case "null":
case "nil":
$this->types[] = \SQLITE3_NULL; break;
case "int":
$value = null; break;
case "integer":
$this->types[] = \SQLITE3_INTEGER; break;
$value = (int) $values[$a]; break;
case "float":
case "double":
case "real":
case "numeric":
$this->types[] = \SQLITE3_FLOAT; break;
$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":
case "timestamp":
$this->types[] = \SQLITE3_TEXT; break;
case "blob":
case "bin":
$value = Driver::formatDate($values[$a], Driver::TS_BOTH); break;
case "binary":
$this->types[] = \SQLITE3_BLOB; break;
$value = (string) $values[$a]; break;
case "text":
case "string":
case "str":
$this->types[] = \SQLITE3_TEXT; break;
case "bool":
$value = $values[$a]; break;
case "boolean":
case "bit":
$this->types[] = \SQLITE3_INTEGER; break;
$value = (bool) $values[$a]; break;
default:
$this->types[] = \SQLITE3_TEXT; break;
throw new Exception("paramTypeUnknown", $type);
}
if($type===null) {
$this->st->bindParam($a+1, $value);
} else {
$this->st->bindParam($a+1, $value, $type);
}
}
return true;
return new ResultSQLite3($this->st->execute(), $this->db->changes(), $this);
}
public function rebind(...$bindings): bool {
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) {
$binding = trim(strtolower($binding));
if(!array_key_exists($binding, self::TYPES)) throw new Db\Exception("paramTypeInvalid", $binding);
$this->types[] = self::TYPES[$binding];
}
return true;
}
}

2
locale/en.php

@ -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,

41
tests/Db/SQLite3/TestDbStatementSQLite3.php

@ -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…
Cancel
Save