Browse Source

Make SQL statement type conversion use ValueInfo normalizer

This sees the addition of a dateOutFormat parameter to ValueInfo::normalize(), as well as a general simplification of how parameter binding works.

Some value type-casting results are slightly different, but this simply makes SQL statement objects consistent with the rest of the system.
microsub
J. King 6 years ago
parent
commit
ba0aeab7ec
  1. 42
      lib/Db/AbstractStatement.php
  2. 31
      lib/Db/PDOStatement.php
  3. 31
      lib/Db/SQLite3/Statement.php
  4. 20
      lib/Misc/ValueInfo.php
  5. 10
      tests/cases/Db/SQLite3/TestStatement.php
  6. 10
      tests/cases/Db/SQLite3PDO/TestStatement.php

42
lib/Db/AbstractStatement.php

@ -7,12 +7,14 @@ declare(strict_types=1);
namespace JKingWeb\Arsse\Db;
use JKingWeb\Arsse\Misc\Date;
use JKingWeb\Arsse\Misc\ValueInfo;
abstract class AbstractStatement implements Statement {
protected $types = [];
protected $isNullable = [];
abstract public function runArray(array $values = []): Result;
abstract protected function bindValue($value, string $type, int $position): bool;
public function run(...$values): Result {
return $this->runArray($values);
@ -51,31 +53,41 @@ abstract class AbstractStatement implements Statement {
protected function cast($v, string $t, bool $nullable) {
switch ($t) {
case "datetime":
$v = Date::transform($v, "sql");
if (is_null($v) && !$nullable) {
$v = 0;
$v = Date::transform($v, "sql");
}
return Date::transform($v, "sql");
return $v;
case "integer":
return ValueInfo::normalize($v, ValueInfo::T_INT | ($nullable ? ValueInfo::M_NULL : 0), null, "sql");
case "float":
return ValueInfo::normalize($v, ValueInfo::T_FLOAT | ($nullable ? ValueInfo::M_NULL : 0), null, "sql");
case "binary":
case "string":
return ValueInfo::normalize($v, ValueInfo::T_STRING | ($nullable ? ValueInfo::M_NULL : 0), null, "sql");
case "boolean":
if ($t=="binary") {
$t = "string";
}
if ($v instanceof \DateTimeInterface) {
if ($t=="string") {
return Date::transform($v, "sql");
} else {
$v = $v->getTimestamp();
settype($v, $t);
}
} else {
settype($v, $t);
}
return $v;
$v = ValueInfo::normalize($v, ValueInfo::T_BOOL | ($nullable ? ValueInfo::M_NULL : 0), null, "sql");
return is_null($v) ? $v : (int) $v;
default:
throw new Exception("paramTypeUnknown", $type); // @codeCoverageIgnore
}
}
protected function bindValues(array $values, int $offset = 0): int {
$a = $offset;
foreach ($values as $value) {
if (is_array($value)) {
// recursively flatten any arrays, which may be provided for SET or IN() clauses
$a += $this->bindValues($value, $a);
} elseif (array_key_exists($a, $this->types)) {
$value = $this->cast($value, $this->types[$a], $this->isNullable[$a]);
$this->bindValue($value, $this->types[$a], $a+1);
$a++;
} else {
throw new Exception("paramTypeMissing", $a+1);
}
}
return $a - $offset;
}
}

31
lib/Db/PDOStatement.php

@ -49,34 +49,7 @@ class PDOStatement extends AbstractStatement {
return new PDOResult($this->st, [$changes, $lastId]);
}
protected function bindValues(array $values, int $offset = 0): int {
$a = $offset;
foreach ($values as $value) {
if (is_array($value)) {
// recursively flatten any arrays, which may be provided for SET or IN() clauses
$a += $this->bindValues($value, $a);
} elseif (array_key_exists($a, $this->types)) {
// if the parameter type is something other than the known values, this is an error
assert(array_key_exists($this->types[$a], self::BINDINGS), new Exception("paramTypeUnknown", $this->types[$a]));
// if the parameter type is null or the value is null (and the type is nullable), just bind null
if ($this->types[$a]=="null" || ($this->isNullable[$a] && is_null($value))) {
$this->st->bindValue($a+1, null, \PDO::PARAM_NULL);
} else {
// otherwise cast the value to the right type and bind the result
$type = self::BINDINGS[$this->types[$a]];
$value = $this->cast($value, $this->types[$a], $this->isNullable[$a]);
// re-adjust for null casts
if ($value===null) {
$type = \PDO::PARAM_NULL;
}
// perform binding
$this->st->bindValue($a+1, $value, $type);
}
$a++;
} else {
throw new Exception("paramTypeMissing", $a+1);
}
}
return $a - $offset;
protected function bindValue($value, string $type, int $position): bool {
return $this->st->bindValue($position, $value, is_null($value) ? \PDO::PARAM_NULL : self::BINDINGS[$type]);
}
}

31
lib/Db/SQLite3/Statement.php

@ -56,34 +56,7 @@ class Statement extends \JKingWeb\Arsse\Db\AbstractStatement {
return new Result($r, [$changes, $lastId], $this);
}
protected function bindValues(array $values, int $offset = 0): int {
$a = $offset;
foreach ($values as $value) {
if (is_array($value)) {
// recursively flatten any arrays, which may be provided for SET or IN() clauses
$a += $this->bindValues($value, $a);
} elseif (array_key_exists($a, $this->types)) {
// if the parameter type is something other than the known values, this is an error
assert(array_key_exists($this->types[$a], self::BINDINGS), new Exception("paramTypeUnknown", $this->types[$a]));
// if the parameter type is null or the value is null (and the type is nullable), just bind null
if ($this->types[$a]=="null" || ($this->isNullable[$a] && is_null($value))) {
$this->st->bindValue($a+1, null, \SQLITE3_NULL);
} else {
// otherwise cast the value to the right type and bind the result
$type = self::BINDINGS[$this->types[$a]];
$value = $this->cast($value, $this->types[$a], $this->isNullable[$a]);
// re-adjust for null casts
if ($value===null) {
$type = \SQLITE3_NULL;
}
// perform binding
$this->st->bindValue($a+1, $value, $type);
}
$a++;
} else {
throw new Exception("paramTypeMissing", $a+1);
}
}
return $a - $offset;
protected function bindValue($value, string $type, int $position): bool {
return $this->st->bindValue($position, $value, is_null($value) ? \SQLITE3_NULL : self::BINDINGS[$type]);
}
}

20
lib/Misc/ValueInfo.php

@ -34,7 +34,7 @@ class ValueInfo {
const M_STRICT = 1 << 30; // throw an exception if the type doesn't match
const M_ARRAY = 1 << 31; // the value should be a flat array of values of the specified type; indexed and associative are both acceptable
public static function normalize($value, int $type, string $dateFormat = null) {
public static function normalize($value, int $type, string $dateInFormat = null, $dateOutFormat = null) {
$allowNull = ($type & self::M_NULL);
$strict = ($type & (self::M_STRICT | self::M_DROP));
$drop = ($type & self::M_DROP);
@ -48,7 +48,7 @@ class ValueInfo {
if ($arrayVal) {
$value = self::normalize($value, self::T_ARRAY);
foreach ($value as $key => $v) {
$value[$key] = self::normalize($v, $type | ($allowNull ? self::M_NULL : 0) | ($strict ? self::M_STRICT : 0) | ($drop ? self::M_DROP : 0), $dateFormat);
$value[$key] = self::normalize($v, $type | ($allowNull ? self::M_NULL : 0) | ($strict ? self::M_STRICT : 0) | ($drop ? self::M_DROP : 0), $dateInFormat, $dateOutFormat);
}
return $value;
}
@ -131,12 +131,14 @@ class ValueInfo {
if (is_string($value)) {
return $value;
}
$dateOutFormat = $dateOutFormat ?? "iso8601";
$dateOutFormat = isset(Date::FORMAT[$dateOutFormat]) ? Date::FORMAT[$dateOutFormat][1] : $dateOutFormat;
if ($value instanceof \DateTimeImmutable) {
return $value->setTimezone(new \DateTimeZone("UTC"))->format(Date::FORMAT['iso8601'][1]);
return $value->setTimezone(new \DateTimeZone("UTC"))->format($dateOutFormat);
} elseif ($value instanceof \DateTime) {
$out = clone $value;
$out->setTimezone(new \DateTimeZone("UTC"));
return $out->format(Date::FORMAT['iso8601'][1]);
return $out->format($dateOutFormat);
} elseif (is_float($value) && is_finite($value)) {
$out = (string) $value;
if (!strpos($out, "E")) {
@ -175,9 +177,9 @@ class ValueInfo {
return \DateTime::createFromFormat("U.u", sprintf("%F", $value), new \DateTimeZone("UTC"));
} elseif (is_string($value)) {
try {
if (!is_null($dateFormat)) {
if (!is_null($dateInFormat)) {
$out = false;
if ($dateFormat=="microtime") {
if ($dateInFormat=="microtime") {
// PHP is not able to correctly handle the output of microtime() as the input of DateTime::createFromFormat(), so we fudge it to look like a float
if (preg_match("<^0\.\d{6}00 \d+$>", $value)) {
$value = substr($value, 11).".".substr($value, 2, 6);
@ -185,10 +187,10 @@ class ValueInfo {
throw new \Exception;
}
}
$f = isset(Date::FORMAT[$dateFormat]) ? Date::FORMAT[$dateFormat][0] : $dateFormat;
if ($dateFormat=="iso8601" || $dateFormat=="iso8601m") {
$f = isset(Date::FORMAT[$dateInFormat]) ? Date::FORMAT[$dateInFormat][0] : $dateInFormat;
if ($dateInFormat=="iso8601" || $dateInFormat=="iso8601m") {
// DateTime::createFromFormat() doesn't provide one catch-all for ISO 8601 timezone specifiers, so we try all of them till one works
if ($dateFormat=="iso8601m") {
if ($dateInFormat=="iso8601m") {
$f2 = Date::FORMAT["iso8601"][0];
$zones = [$f."", $f."\Z", $f."P", $f."O", $f2."", $f2."\Z", $f2."P", $f2."O"];
} else {

10
tests/cases/Db/SQLite3/TestStatement.php

@ -90,7 +90,7 @@ class TestStatement extends \JKingWeb\Arsse\Test\AbstractTest {
[true, "strict float", "1.0"],
[true, "strict string", "'1'"],
[true, "strict binary", "x'31'"],
[true, "strict datetime", "'1970-01-01 00:00:01'"],
[true, "strict datetime", "'1970-01-01 00:00:00'"],
[true, "strict boolean", "1"],
// false
[false, "integer", "0"],
@ -197,14 +197,14 @@ class TestStatement extends \JKingWeb\Arsse\Test\AbstractTest {
[chr(233).chr(233), "strict datetime", "'1970-01-01 00:00:00'"],
[chr(233).chr(233), "strict boolean", "1"],
// ISO 8601 date string
["2017-01-09T13:11:17", "integer", "2017"],
["2017-01-09T13:11:17", "float", "2017.0"],
["2017-01-09T13:11:17", "integer", "0"],
["2017-01-09T13:11:17", "float", "0.0"],
["2017-01-09T13:11:17", "string", "'2017-01-09T13:11:17'"],
["2017-01-09T13:11:17", "binary", "x'323031372d30312d30395431333a31313a3137'"],
["2017-01-09T13:11:17", "datetime", "'2017-01-09 13:11:17'"],
["2017-01-09T13:11:17", "boolean", "1"],
["2017-01-09T13:11:17", "strict integer", "2017"],
["2017-01-09T13:11:17", "strict float", "2017.0"],
["2017-01-09T13:11:17", "strict integer", "0"],
["2017-01-09T13:11:17", "strict float", "0.0"],
["2017-01-09T13:11:17", "strict string", "'2017-01-09T13:11:17'"],
["2017-01-09T13:11:17", "strict binary", "x'323031372d30312d30395431333a31313a3137'"],
["2017-01-09T13:11:17", "strict datetime", "'2017-01-09 13:11:17'"],

10
tests/cases/Db/SQLite3PDO/TestStatement.php

@ -91,7 +91,7 @@ class TestStatement extends \JKingWeb\Arsse\Test\AbstractTest {
[true, "strict float", "'1'"],
[true, "strict string", "'1'"],
[true, "strict binary", "x'31'"],
[true, "strict datetime", "'1970-01-01 00:00:01'"],
[true, "strict datetime", "'1970-01-01 00:00:00'"],
[true, "strict boolean", "1"],
// false
[false, "integer", "0"],
@ -198,14 +198,14 @@ class TestStatement extends \JKingWeb\Arsse\Test\AbstractTest {
[chr(233).chr(233), "strict datetime", "'1970-01-01 00:00:00'"],
[chr(233).chr(233), "strict boolean", "1"],
// ISO 8601 date string
["2017-01-09T13:11:17", "integer", "2017"],
["2017-01-09T13:11:17", "float", "'2017'"],
["2017-01-09T13:11:17", "integer", "0"],
["2017-01-09T13:11:17", "float", "'0'"],
["2017-01-09T13:11:17", "string", "'2017-01-09T13:11:17'"],
["2017-01-09T13:11:17", "binary", "x'323031372d30312d30395431333a31313a3137'"],
["2017-01-09T13:11:17", "datetime", "'2017-01-09 13:11:17'"],
["2017-01-09T13:11:17", "boolean", "1"],
["2017-01-09T13:11:17", "strict integer", "2017"],
["2017-01-09T13:11:17", "strict float", "'2017'"],
["2017-01-09T13:11:17", "strict integer", "0"],
["2017-01-09T13:11:17", "strict float", "'0'"],
["2017-01-09T13:11:17", "strict string", "'2017-01-09T13:11:17'"],
["2017-01-09T13:11:17", "strict binary", "x'323031372d30312d30395431333a31313a3137'"],
["2017-01-09T13:11:17", "strict datetime", "'2017-01-09 13:11:17'"],

Loading…
Cancel
Save