1
1
Fork 0
mirror of https://code.mensbeam.com/MensBeam/Arsse.git synced 2024-12-23 06:55:03 +00:00

More binding tests and related changes

- Introduced abstract Statement class to hold common methods
- Common methods currently consist of a date formatter and type caster
- Moved binding tests to a trait for reuse with future drivers
This commit is contained in:
J. King 2017-03-02 18:42:19 -05:00
parent 1529fc367a
commit 0c410fcf50
8 changed files with 369 additions and 159 deletions

View file

@ -291,16 +291,14 @@ class Database {
throw new Feed\Exception($url, $e); throw new Feed\Exception($url, $e);
} }
$this->db->prepare("INSERT INTO newssync_feeds(url,title,favicon,source,updated,modified,etag,username,password) values(?,?,?)", "str", "str", "str", "str", "str", "str", "str", "str", "str")->run( $this->db->prepare("INSERT INTO newssync_feeds(url,title,favicon,source,updated,modified,etag,username,password) values(?,?,?,?,?,?,?,?,?)", "str", "str", "str", "str", "datetime", "datetime", "str", "str", "str")->run(
$url, $url,
$feed->title, $feed->title,
// Grab the favicon for the Goodfeed; returns an empty string if it cannot find one. // Grab the favicon for the Goodfeed; returns an empty string if it cannot find one.
(new PicoFeed\Reader\Favicon)->find($url), (new PicoFeed\Reader\Favicon)->find($url),
$feed->siteUrl, $feed->siteUrl,
// Convert the date formats to SQL date format before inserting. $feed->date,
// FIXME: Dates should be formatted transparently by the driver's Statement wrapper, not here $resource->getLastModified(),
$this->driver::formatDate($feed->date),
$this->driver::formatDate($resource->getLastModified()),
$resource->getEtag(), $resource->getEtag(),
$fetchUser, $fetchUser,
$fetchPassword $fetchPassword

View file

@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace JKingWeb\NewsSync\Db;
abstract class AbstractStatement implements Statement {
abstract function runArray(array $values): Result;
abstract static function dateFormat(int $part = self::TS_BOTH): string;
public function run(...$values): Result {
return $this->runArray($values);
}
public function rebind(...$bindings): bool {
return $this->rebindArray($bindings);
}
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 Exception("paramTypeInvalid", $binding);
$this->types[] = self::TYPES[$binding];
}
return true;
}
protected function cast($v, string $t) {
switch($t) {
case "date":
return $this->formatDate($v, self::TS_DATE);
case "time":
return $this->formatDate($v, self::TS_TIME);
case "datetime":
return $this->formatDate($v, self::TS_BOTH);
case "null":
case "integer":
case "float":
case "binary":
case "string":
case "boolean":
if($t=="binary") $t = "string";
$value = $v;
try{
settype($value, $t);
} catch(\Throwable $e) {
// handle objects
$value = $v;
if($value instanceof \DateTimeInterface) {
$value = $value->getTimestamp();
if($t=="string") $value = $this->formatDate($value, self::TS_BOTH);
settype($value, $t);
} else {
$value = null;
settype($value, $t);
}
}
return $value;
default:
throw new Exception("paramTypeUnknown", $type);
}
}
protected function formatDate($date, int $part = self::TS_BOTH) {
// 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($date instanceof \DateTimeInterface) {
$time = $date->getTimestamp();
} else if(is_numeric($date)) {
$time = (int) $date;
} else if($date===null) {
return null;
} else if(is_string($date)) {
$time = strtotime($date);
if($time===false) return null;
} else if (is_bool($date)) {
return null;
} else {
$time = (int) $date;
}
// ISO 8601 with space in the middle instead of T.
$date = date($this->dateFormat($part), $time);
date_default_timezone_set($timezone);
return $date;
}
}

View file

@ -69,23 +69,4 @@ Trait Common {
public function prepare(string $query, string ...$paramType): Statement { public function prepare(string $query, string ...$paramType): Statement {
return $this->prepareArray($query, $paramType); return $this->prepareArray($query, $paramType);
} }
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(self::TS_FORMAT[$precision], $time);
date_default_timezone_set($timezone);
return $date;
}
} }

View file

@ -3,16 +3,6 @@ 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;

View file

@ -3,7 +3,9 @@ declare(strict_types=1);
namespace JKingWeb\NewsSync\Db; namespace JKingWeb\NewsSync\Db;
interface Statement { interface Statement {
const TS_TIME = -1;
const TS_DATE = 0;
const TS_BOTH = 1;
const TYPES = [ const TYPES = [
"null" => "null", "null" => "null",
"nil" => "null", "nil" => "null",
@ -20,14 +22,16 @@ interface Statement {
"blob" => "binary", "blob" => "binary",
"bin" => "binary", "bin" => "binary",
"binary" => "binary", "binary" => "binary",
"text" => "text", "text" => "string",
"string" => "text", "string" => "string",
"str" => "text", "str" => "string",
"bool" => "boolean", "bool" => "boolean",
"boolean" => "boolean", "boolean" => "boolean",
"bit" => "boolean", "bit" => "boolean",
]; ];
static function dateFormat(int $part = self::TS_BOTH): string;
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;

View file

@ -1,9 +1,20 @@
<?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 extends AbstractStatement {
const BINDINGS = [
"null" => \SQLITE3_NULL,
"integer" => \SQLITE3_INTEGER,
"float" => \SQLITE3_FLOAT,
"date" => \SQLITE3_TEXT,
"time" => \SQLITE3_TEXT,
"datetime" => \SQLITE3_TEXT,
"binary" => \SQLITE3_BLOB,
"string" => \SQLITE3_TEXT,
"boolean" => \SQLITE3_INTEGER,
];
protected $db; protected $db;
protected $st; protected $st;
protected $types; protected $types;
@ -19,8 +30,12 @@ class StatementSQLite3 implements Statement {
unset($this->st); unset($this->st);
} }
public function run(...$values): Result { public static function dateFormat(int $part = self::TS_BOTH): string {
return $this->runArray($values); return ([
self::TS_TIME => 'h:i:sP',
self::TS_DATE => 'Y-m-d',
self::TS_BOTH => 'Y-m-d h:i:sP',
])[$part];
} }
public function runArray(array $values = null): Result { public function runArray(array $values = null): Result {
@ -28,80 +43,21 @@ class StatementSQLite3 implements Statement {
$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 // 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)) { } else if(array_key_exists($a,$this->types)) {
$type = $this->translateType($this->types[$a]); if(!array_key_exists($this->types[$a], self::BINDINGS)) throw new Exception("paramTypeUnknown", $this->types[$a]);
$type = self::BINDINGS[$this->types[$a]];
} else { } else {
$type = \SQLITE3_TEXT; $type = \SQLITE3_TEXT;
} }
// cast values if necessary // cast value if necessary
switch($this->types[$a]) { $value = $this->cast($values[$a], $this->types[$a]);
case "null": // re-adjust for null casts
$value = null; break; if($value===null) $type = \SQLITE3_NULL;
case "integer": // perform binding
$value = (int) $values[$a]; break; $this->st->bindParam($a+1, $value, $type);
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);
}
} }
return new ResultSQLite3($this->st->execute(), $this->db->changes(), $this); 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;
}
} }

View file

@ -4,10 +4,11 @@ namespace JKingWeb\NewsSync;
class TestDbStatementSQLite3 extends \PHPUnit\Framework\TestCase { class TestDbStatementSQLite3 extends \PHPUnit\Framework\TestCase {
use Test\Tools; use Test\Tools, Test\Db\BindingTests;
protected $c; protected $c;
protected $s; protected $s;
static protected $imp = Db\StatementSQLite3::class;
function setUp() { function setUp() {
date_default_timezone_set("UTC"); date_default_timezone_set("UTC");
@ -28,53 +29,4 @@ class TestDbStatementSQLite3 extends \PHPUnit\Framework\TestCase {
function testConstructStatement() { function testConstructStatement() {
$this->assertInstanceOf(Db\StatementSQLite3::class, new Db\StatementSQLite3($this->c, $this->s)); $this->assertInstanceOf(Db\StatementSQLite3::class, new Db\StatementSQLite3($this->c, $this->s));
} }
function testBindMissingValue() {
$s = new Db\StatementSQLite3($this->c, $this->s);
$val = $s->runArray()->get()['value'];
$this->assertSame(null, $val);
}
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);
$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() {
$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.");
}
}
} }

View file

@ -0,0 +1,240 @@
<?php
declare(strict_types=1);
namespace JKingWeb\NewsSync\Test\Db;
use JKingWeb\NewsSync\Db\Statement;
use JKingWeb\NewsSync\Db\Driver;
trait BindingTests {
function testBindMissingValue() {
$s = new self::$imp($this->c, $this->s);
$val = $s->runArray()->get()['value'];
$this->assertSame(null, $val);
}
function testBindNull() {
$input = null;
$exp = [
"null" => null,
"integer" => null,
"float" => null,
"date" => null,
"time" => null,
"datetime" => null,
"binary" => null,
"string" => null,
"boolean" => null,
];
$this->checkBinding($input, $exp);
}
function testBindTrue() {
$input = true;
$exp = [
"null" => null,
"integer" => 1,
"float" => 1.0,
"date" => null,
"time" => null,
"datetime" => null,
"binary" => "1",
"string" => "1",
"boolean" => 1,
];
$this->checkBinding($input, $exp);
}
function testBindFalse() {
$input = false;
$exp = [
"null" => null,
"integer" => 0,
"float" => 0.0,
"date" => null,
"time" => null,
"datetime" => null,
"binary" => "",
"string" => "",
"boolean" => 0,
];
$this->checkBinding($input, $exp);
}
function testBindInteger() {
$input = 2112;
$exp = [
"null" => null,
"integer" => 2112,
"float" => 2112.0,
"date" => date(self::$imp::dateFormat(Statement::TS_DATE), 2112),
"time" => date(self::$imp::dateFormat(Statement::TS_TIME), 2112),
"datetime" => date(self::$imp::dateFormat(Statement::TS_BOTH), 2112),
"binary" => "2112",
"string" => "2112",
"boolean" => 1,
];
$this->checkBinding($input, $exp);
}
function testBindIntegerZero() {
$input = 0;
$exp = [
"null" => null,
"integer" => 0,
"float" => 0.0,
"date" => date(self::$imp::dateFormat(Statement::TS_DATE), 0),
"time" => date(self::$imp::dateFormat(Statement::TS_TIME), 0),
"datetime" => date(self::$imp::dateFormat(Statement::TS_BOTH), 0),
"binary" => "0",
"string" => "0",
"boolean" => 0,
];
$this->checkBinding($input, $exp);
}
function testBindFloat() {
$input = 2112.0;
$exp = [
"null" => null,
"integer" => 2112,
"float" => 2112.0,
"date" => date(self::$imp::dateFormat(Statement::TS_DATE), 2112),
"time" => date(self::$imp::dateFormat(Statement::TS_TIME), 2112),
"datetime" => date(self::$imp::dateFormat(Statement::TS_BOTH), 2112),
"binary" => "2112",
"string" => "2112",
"boolean" => 1,
];
$this->checkBinding($input, $exp);
}
function testBindFloatZero() {
$input = 0.0;
$exp = [
"null" => null,
"integer" => 0,
"float" => 0.0,
"date" => date(self::$imp::dateFormat(Statement::TS_DATE), 0),
"time" => date(self::$imp::dateFormat(Statement::TS_TIME), 0),
"datetime" => date(self::$imp::dateFormat(Statement::TS_BOTH), 0),
"binary" => "0",
"string" => "0",
"boolean" => 0,
];
$this->checkBinding($input, $exp);
}
function testBindAsciiString() {
$input = "Random string";
$exp = [
"null" => null,
"integer" => 0,
"float" => 0.0,
"date" => null,
"time" => null,
"datetime" => null,
"binary" => $input,
"string" => $input,
"boolean" => 1,
];
$this->checkBinding($input, $exp);
}
function testBindUtf8String() {
$input = "é";
$exp = [
"null" => null,
"integer" => 0,
"float" => 0.0,
"date" => null,
"time" => null,
"datetime" => null,
"binary" => $input,
"string" => $input,
"boolean" => 1,
];
$this->checkBinding($input, $exp);
}
function testBindBinaryString() {
// FIXME: This test may be unreliable; SQLite happily stores invalid UTF-8 text as bytes untouched, but other engines probably don't do this
$input = chr(233);
$exp = [
"null" => null,
"integer" => 0,
"float" => 0.0,
"date" => null,
"time" => null,
"datetime" => null,
"binary" => $input,
"string" => $input,
"boolean" => 1,
];
$this->checkBinding($input, $exp);
}
function testBindIso8601DateString() {
$input = "2017-01-09T13:11:17";
$time = strtotime($input);
$exp = [
"null" => null,
"integer" => 2017,
"float" => 2017.0,
"date" => date(self::$imp::dateFormat(Statement::TS_DATE), $time),
"time" => date(self::$imp::dateFormat(Statement::TS_TIME), $time),
"datetime" => date(self::$imp::dateFormat(Statement::TS_BOTH), $time),
"binary" => $input,
"string" => $input,
"boolean" => 1,
];
$this->checkBinding($input, $exp);
}
function testBindArbitraryDateString() {
$input = "Today";
$time = strtotime($input);
$exp = [
"null" => null,
"integer" => 0,
"float" => 0.0,
"date" => date(self::$imp::dateFormat(Statement::TS_DATE), $time),
"time" => date(self::$imp::dateFormat(Statement::TS_TIME), $time),
"datetime" => date(self::$imp::dateFormat(Statement::TS_BOTH), $time),
"binary" => $input,
"string" => $input,
"boolean" => 1,
];
$this->checkBinding($input, $exp);
}
function testBindMutableDateObject($class = '\DateTime') {
$input = new $class("Noon Today");
$time = $input->getTimestamp();
$exp = [
"null" => null,
"integer" => $time,
"float" => (float) $time,
"date" => date(self::$imp::dateFormat(Statement::TS_DATE), $time),
"time" => date(self::$imp::dateFormat(Statement::TS_TIME), $time),
"datetime" => date(self::$imp::dateFormat(Statement::TS_BOTH), $time),
"binary" => date(self::$imp::dateFormat(Statement::TS_BOTH), $time),
"string" => date(self::$imp::dateFormat(Statement::TS_BOTH), $time),
"boolean" => 1,
];
$this->checkBinding($input, $exp);
}
function testBindImmutableDateObject() {
$this->testBindMutableDateObject('\DateTimeImmutable');
}
protected function checkBinding($input, array $expectations) {
$s = new self::$imp($this->c, $this->s);
$types = array_unique(Statement::TYPES);
foreach($types as $type) {
$s->rebindArray([$type]);
$val = $s->runArray([$input])->get()['value'];
$this->assertSame($expectations[$type], $val, "Type $type failed comparison.");
}
}
}