mirror of
https://code.mensbeam.com/MensBeam/Arsse.git
synced 2024-12-22 13:12:41 +00:00
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.
This commit is contained in:
parent
bc9fcb975f
commit
ba0aeab7ec
6 changed files with 56 additions and 96 deletions
|
@ -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;
|
||||
}
|
||||
return Date::transform($v, "sql");
|
||||
case "integer":
|
||||
case "float":
|
||||
case "binary":
|
||||
case "string":
|
||||
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);
|
||||
$v = 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":
|
||||
$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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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'"],
|
||||
|
|
|
@ -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…
Reference in a new issue