Browse Source

Simplify SQL type handling

This is done in anticipation of dealing with SQL types in
places other than statements
microsub
J. King 5 years ago
parent
commit
837f3c6dd6
  1. 62
      lib/Db/AbstractStatement.php
  2. 18
      lib/Db/MySQL/Statement.php
  3. 14
      lib/Db/PDOStatement.php
  4. 16
      lib/Db/PostgreSQL/Statement.php
  5. 14
      lib/Db/SQLite3/Statement.php
  6. 58
      lib/Db/Statement.php
  7. 12
      tests/cases/Db/BaseStatement.php

62
lib/Db/AbstractStatement.php

@ -12,11 +12,25 @@ use JKingWeb\Arsse\Misc\ValueInfo;
abstract class AbstractStatement implements Statement {
use SQLState;
const TYPE_NORM_MAP = [
self::T_INTEGER => ValueInfo::M_NULL | ValueInfo::T_INT,
self::T_STRING => ValueInfo::M_NULL | ValueInfo::T_STRING,
self::T_BOOLEAN => ValueInfo::M_NULL | ValueInfo::T_BOOL,
self::T_DATETIME => ValueInfo::M_NULL | ValueInfo::T_DATE,
self::T_FLOAT => ValueInfo::M_NULL | ValueInfo::T_FLOAT,
self::T_BINARY => ValueInfo::M_NULL | ValueInfo::T_STRING,
self::T_NOT_NULL + self::T_INTEGER => ValueInfo::T_INT,
self::T_NOT_NULL + self::T_STRING => ValueInfo::T_STRING,
self::T_NOT_NULL + self::T_BOOLEAN => ValueInfo::T_BOOL,
self::T_NOT_NULL + self::T_DATETIME => ValueInfo::T_DATE,
self::T_NOT_NULL + self::T_FLOAT => ValueInfo::T_FLOAT,
self::T_NOT_NULL + self::T_BINARY => ValueInfo::T_STRING,
];
protected $types = [];
protected $isNullable = [];
abstract public function runArray(array $values = []): Result;
abstract protected function bindValue($value, string $type, int $position): bool;
abstract protected function bindValue($value, int $type, int $position): bool;
abstract protected function prepare(string $query): bool;
abstract protected static function buildEngineException($code, string $msg): array;
@ -41,18 +55,11 @@ abstract class AbstractStatement implements Statement {
// recursively flatten any arrays, which may be provided for SET or IN() clauses
$this->retypeArray($binding, true);
} else {
$binding = trim(strtolower($binding));
if (strpos($binding, "strict ")===0) {
// "strict" types' values may never be null; null values will later be cast to the type specified
$this->isNullable[] = false;
$binding = substr($binding, 7);
} else {
$this->isNullable[] = true;
}
if (!array_key_exists($binding, self::TYPES)) {
$bindId = self::TYPES[trim(strtolower($binding))] ?? 0;
if (!$bindId) {
throw new Exception("paramTypeInvalid", $binding); // @codeCoverageIgnore
}
$this->types[] = self::TYPES[$binding];
$this->types[] = $bindId;
}
}
if (!$append) {
@ -61,27 +68,16 @@ abstract class AbstractStatement implements Statement {
return true;
}
protected function cast($v, string $t, bool $nullable) {
protected function cast($v, int $t) {
switch ($t) {
case "datetime":
case self::T_DATETIME:
return Date::transform($v, "sql");
case self::T_DATETIME + self::T_NOT_NULL:
$v = Date::transform($v, "sql");
if (is_null($v) && !$nullable) {
$v = 0;
$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;
return $v ? $v : "0001-01-01 00:00:00";
default:
throw new Exception("paramTypeUnknown", $type); // @codeCoverageIgnore
$v = ValueInfo::normalize($v, self::TYPE_NORM_MAP[$t], null, "sql");
return is_bool($v) ? (int) $v : $v;
}
}
@ -92,8 +88,8 @@ abstract class AbstractStatement implements Statement {
// 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);
$value = $this->cast($value, $this->types[$a]);
$this->bindValue($value, $this->types[$a] % self::T_NOT_NULL, ++$a);
} else {
throw new Exception("paramTypeMissing", $a+1);
}
@ -102,7 +98,7 @@ abstract class AbstractStatement implements Statement {
// SQLite will happily substitute null for a missing value, but other engines (viz. PostgreSQL) produce an error
if (is_null($offset)) {
while ($a < sizeof($this->types)) {
$this->bindValue(null, $this->types[$a], ++$a);
$this->bindValue(null, $this->types[$a] % self::T_NOT_NULL, ++$a);
}
}
return $a - $offset;

18
lib/Db/MySQL/Statement.php

@ -14,12 +14,12 @@ class Statement extends \JKingWeb\Arsse\Db\AbstractStatement {
use ExceptionBuilder;
const BINDINGS = [
"integer" => "i",
"float" => "d",
"datetime" => "s",
"binary" => "b",
"string" => "s",
"boolean" => "i",
self::T_INTEGER => "i",
self::T_FLOAT => "d",
self::T_DATETIME => "s",
self::T_BINARY => "b",
self::T_STRING => "s",
self::T_BOOLEAN => "i",
];
protected $db;
@ -93,11 +93,11 @@ class Statement extends \JKingWeb\Arsse\Db\AbstractStatement {
return new Result($r, [$changes, $lastId], $this);
}
protected function bindValue($value, string $type, int $position): bool {
protected function bindValue($value, int $type, int $position): bool {
// this is a bit of a hack: we collect values (and MySQL bind types) here so that we can take
// advantage of the work done by bindValues() even though MySQL requires everything to be bound
// all at once; we also segregate large values for later packetization
if (($type === "binary" && !is_null($value)) || (is_string($value) && strlen($value) > $this->packetSize)) {
if (($type == self::T_BINARY && !is_null($value)) || (is_string($value) && strlen($value) > $this->packetSize)) {
$this->values[] = null;
$this->longs[$position - 1] = $value;
$this->binds .= "b";
@ -112,7 +112,7 @@ class Statement extends \JKingWeb\Arsse\Db\AbstractStatement {
$out = "";
for ($b = 1; $b < sizeof($query); $b++) {
$a = $b - 1;
$mark = (($types[$a] ?? "") === "datetime") ? "cast(? as datetime(0))" : "?";
$mark = (($types[$a] ?? 0) % self::T_NOT_NULL == self::T_DATETIME) ? "cast(? as datetime(0))" : "?";
$out .= $query[$a].$mark;
}
$out .= array_pop($query);

14
lib/Db/PDOStatement.php

@ -10,12 +10,12 @@ abstract class PDOStatement extends AbstractStatement {
use PDOError;
const BINDINGS = [
"integer" => \PDO::PARAM_INT,
"float" => \PDO::PARAM_STR,
"datetime" => \PDO::PARAM_STR,
"binary" => \PDO::PARAM_LOB,
"string" => \PDO::PARAM_STR,
"boolean" => \PDO::PARAM_INT, // FIXME: using \PDO::PARAM_BOOL leads to incompatibilities with versions of SQLite bundled prior to PHP 7.3
self::T_INTEGER => \PDO::PARAM_INT,
self::T_FLOAT => \PDO::PARAM_STR,
self::T_DATETIME => \PDO::PARAM_STR,
self::T_BINARY => \PDO::PARAM_LOB,
self::T_STRING => \PDO::PARAM_STR,
self::T_BOOLEAN => \PDO::PARAM_INT, // FIXME: using \PDO::PARAM_BOOL leads to incompatibilities with versions of SQLite bundled prior to PHP 7.3
];
protected $st;
@ -55,7 +55,7 @@ abstract class PDOStatement extends AbstractStatement {
return new PDOResult($this->db, $this->st);
}
protected function bindValue($value, string $type, int $position): bool {
protected function bindValue($value, int $type, int $position): bool {
return $this->st->bindValue($position, $value, is_null($value) ? \PDO::PARAM_NULL : self::BINDINGS[$type]);
}
}

16
lib/Db/PostgreSQL/Statement.php

@ -14,12 +14,12 @@ class Statement extends \JKingWeb\Arsse\Db\AbstractStatement {
use Dispatch;
const BINDINGS = [
"integer" => "bigint",
"float" => "decimal",
"datetime" => "timestamp(0) without time zone",
"binary" => "bytea",
"string" => "text",
"boolean" => "smallint", // FIXME: using boolean leads to incompatibilities with versions of SQLite bundled prior to PHP 7.3
self::T_INTEGER => "bigint",
self::T_FLOAT => "decimal",
self::T_DATETIME => "timestamp(0) without time zone",
self::T_BINARY => "bytea",
self::T_STRING => "text",
self::T_BOOLEAN => "smallint", // FIXME: using boolean leads to incompatibilities with versions of SQLite bundled prior to PHP 7.3
];
protected $db;
@ -47,7 +47,7 @@ class Statement extends \JKingWeb\Arsse\Db\AbstractStatement {
}
}
protected function bindValue($value, string $type, int $position): bool {
protected function bindValue($value, int $type, int $position): bool {
$this->in[] = $value;
return true;
}
@ -59,7 +59,7 @@ class Statement extends \JKingWeb\Arsse\Db\AbstractStatement {
for ($b = 1; $b < sizeof($q); $b++) {
$a = $b - 1;
$mark = $mungeParamMarkers ? "\$$b" : "?";
$type = isset($types[$a]) ? "::".self::BINDINGS[$types[$a]] : "";
$type = isset($types[$a]) ? "::".self::BINDINGS[$types[$a] % self::T_NOT_NULL] : "";
$out .= $q[$a].$mark.$type;
}
$out .= array_pop($q);

14
lib/Db/SQLite3/Statement.php

@ -17,12 +17,12 @@ class Statement extends \JKingWeb\Arsse\Db\AbstractStatement {
const SQLITE_CONSTRAINT = 19;
const SQLITE_MISMATCH = 20;
const BINDINGS = [
"integer" => \SQLITE3_INTEGER,
"float" => \SQLITE3_FLOAT,
"datetime" => \SQLITE3_TEXT,
"binary" => \SQLITE3_BLOB,
"string" => \SQLITE3_TEXT,
"boolean" => \SQLITE3_INTEGER,
self::T_INTEGER => \SQLITE3_INTEGER,
self::T_FLOAT => \SQLITE3_FLOAT,
self::T_DATETIME => \SQLITE3_TEXT,
self::T_BINARY => \SQLITE3_BLOB,
self::T_STRING => \SQLITE3_TEXT,
self::T_BOOLEAN => \SQLITE3_INTEGER,
];
protected $db;
@ -68,7 +68,7 @@ class Statement extends \JKingWeb\Arsse\Db\AbstractStatement {
return new Result($r, [$changes, $lastId], $this);
}
protected function bindValue($value, string $type, int $position): bool {
protected function bindValue($value, int $type, int $position): bool {
return $this->st->bindValue($position, $value, is_null($value) ? \SQLITE3_NULL : self::BINDINGS[$type]);
}
}

58
lib/Db/Statement.php

@ -8,24 +8,48 @@ namespace JKingWeb\Arsse\Db;
interface Statement {
const TYPES = [
"int" => "integer",
"integer" => "integer",
"float" => "float",
"double" => "float",
"real" => "float",
"numeric" => "float",
"datetime" => "datetime",
"timestamp" => "datetime",
"blob" => "binary",
"bin" => "binary",
"binary" => "binary",
"text" => "string",
"string" => "string",
"str" => "string",
"bool" => "boolean",
"boolean" => "boolean",
"bit" => "boolean",
'int' => self::T_INTEGER,
'integer' => self::T_INTEGER,
'float' => self::T_FLOAT,
'double' => self::T_FLOAT,
'real' => self::T_FLOAT,
'numeric' => self::T_FLOAT,
'datetime' => self::T_DATETIME,
'timestamp' => self::T_DATETIME,
'blob' => self::T_BINARY,
'bin' => self::T_BINARY,
'binary' => self::T_BINARY,
'text' => self::T_STRING,
'string' => self::T_STRING,
'str' => self::T_STRING,
'bool' => self::T_BOOLEAN,
'boolean' => self::T_BOOLEAN,
'bit' => self::T_BOOLEAN,
'strict int' => self::T_NOT_NULL + self::T_INTEGER,
'strict integer' => self::T_NOT_NULL + self::T_INTEGER,
'strict float' => self::T_NOT_NULL + self::T_FLOAT,
'strict double' => self::T_NOT_NULL + self::T_FLOAT,
'strict real' => self::T_NOT_NULL + self::T_FLOAT,
'strict numeric' => self::T_NOT_NULL + self::T_FLOAT,
'strict datetime' => self::T_NOT_NULL + self::T_DATETIME,
'strict timestamp' => self::T_NOT_NULL + self::T_DATETIME,
'strict blob' => self::T_NOT_NULL + self::T_BINARY,
'strict bin' => self::T_NOT_NULL + self::T_BINARY,
'strict binary' => self::T_NOT_NULL + self::T_BINARY,
'strict text' => self::T_NOT_NULL + self::T_STRING,
'strict string' => self::T_NOT_NULL + self::T_STRING,
'strict str' => self::T_NOT_NULL + self::T_STRING,
'strict bool' => self::T_NOT_NULL + self::T_BOOLEAN,
'strict boolean' => self::T_NOT_NULL + self::T_BOOLEAN,
'strict bit' => self::T_NOT_NULL + self::T_BOOLEAN,
];
const T_INTEGER = 1;
const T_STRING = 2;
const T_BOOLEAN = 3;
const T_DATETIME = 4;
const T_FLOAT = 5;
const T_BINARY = 6;
const T_NOT_NULL = 100;
public function run(...$values): Result;
public function runArray(array $values = []): Result;

12
tests/cases/Db/BaseStatement.php

@ -143,7 +143,7 @@ abstract class BaseStatement extends \JKingWeb\Arsse\Test\AbstractTest {
'Null as strict integer' => [null, "strict integer", "0"],
'Null as strict float' => [null, "strict float", "0.0"],
'Null as strict string' => [null, "strict string", "''"],
'Null as strict datetime' => [null, "strict datetime", "'1970-01-01 00:00:00'"],
'Null as strict datetime' => [null, "strict datetime", "'0001-01-01 00:00:00'"],
'Null as strict boolean' => [null, "strict boolean", "0"],
'True as integer' => [true, "integer", "1"],
'True as float' => [true, "float", "1.0"],
@ -153,7 +153,7 @@ abstract class BaseStatement extends \JKingWeb\Arsse\Test\AbstractTest {
'True as strict integer' => [true, "strict integer", "1"],
'True as strict float' => [true, "strict float", "1.0"],
'True as strict string' => [true, "strict string", "'1'"],
'True as strict datetime' => [true, "strict datetime", "'1970-01-01 00:00:00'"],
'True as strict datetime' => [true, "strict datetime", "'0001-01-01 00:00:00'"],
'True as strict boolean' => [true, "strict boolean", "1"],
'False as integer' => [false, "integer", "0"],
'False as float' => [false, "float", "0.0"],
@ -163,7 +163,7 @@ abstract class BaseStatement extends \JKingWeb\Arsse\Test\AbstractTest {
'False as strict integer' => [false, "strict integer", "0"],
'False as strict float' => [false, "strict float", "0.0"],
'False as strict string' => [false, "strict string", "''"],
'False as strict datetime' => [false, "strict datetime", "'1970-01-01 00:00:00'"],
'False as strict datetime' => [false, "strict datetime", "'0001-01-01 00:00:00'"],
'False as strict boolean' => [false, "strict boolean", "0"],
'Integer as integer' => [2112, "integer", "2112"],
'Integer as float' => [2112, "float", "2112.0"],
@ -213,7 +213,7 @@ abstract class BaseStatement extends \JKingWeb\Arsse\Test\AbstractTest {
'ASCII string as strict integer' => ["Random string", "strict integer", "0"],
'ASCII string as strict float' => ["Random string", "strict float", "0.0"],
'ASCII string as strict string' => ["Random string", "strict string", "'Random string'"],
'ASCII string as strict datetime' => ["Random string", "strict datetime", "'1970-01-01 00:00:00'"],
'ASCII string as strict datetime' => ["Random string", "strict datetime", "'0001-01-01 00:00:00'"],
'ASCII string as strict boolean' => ["Random string", "strict boolean", "1"],
'UTF-8 string as integer' => ["\u{e9}", "integer", "0"],
'UTF-8 string as float' => ["\u{e9}", "float", "0.0"],
@ -223,7 +223,7 @@ abstract class BaseStatement extends \JKingWeb\Arsse\Test\AbstractTest {
'UTF-8 string as strict integer' => ["\u{e9}", "strict integer", "0"],
'UTF-8 string as strict float' => ["\u{e9}", "strict float", "0.0"],
'UTF-8 string as strict string' => ["\u{e9}", "strict string", "char(233)"],
'UTF-8 string as strict datetime' => ["\u{e9}", "strict datetime", "'1970-01-01 00:00:00'"],
'UTF-8 string as strict datetime' => ["\u{e9}", "strict datetime", "'0001-01-01 00:00:00'"],
'UTF-8 string as strict boolean' => ["\u{e9}", "strict boolean", "1"],
'ISO 8601 string as integer' => ["2017-01-09T13:11:17", "integer", "0"],
'ISO 8601 string as float' => ["2017-01-09T13:11:17", "float", "0.0"],
@ -306,7 +306,7 @@ abstract class BaseStatement extends \JKingWeb\Arsse\Test\AbstractTest {
'Binary string as strict float' => [chr(233).chr(233), "strict float", "0.0"],
'Binary string as strict string' => [chr(233).chr(233), "strict string", "'".chr(233).chr(233)."'"],
'Binary string as strict binary' => [chr(233).chr(233), "strict binary", "x'e9e9'"],
'Binary string as strict datetime' => [chr(233).chr(233), "strict datetime", "'1970-01-01 00:00:00'"],
'Binary string as strict datetime' => [chr(233).chr(233), "strict datetime", "'0001-01-01 00:00:00'"],
'Binary string as strict boolean' => [chr(233).chr(233), "strict boolean", "1"],
'ISO 8601 string as binary' => ["2017-01-09T13:11:17", "binary", "x'323031372d30312d30395431333a31313a3137'"],
'ISO 8601 string as strict binary' => ["2017-01-09T13:11:17", "strict binary", "x'323031372d30312d30395431333a31313a3137'"],

Loading…
Cancel
Save