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 { abstract class AbstractStatement implements Statement {
use SQLState; 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 $types = [];
protected $isNullable = [];
abstract public function runArray(array $values = []): Result; 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 function prepare(string $query): bool;
abstract protected static function buildEngineException($code, string $msg): array; 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 // recursively flatten any arrays, which may be provided for SET or IN() clauses
$this->retypeArray($binding, true); $this->retypeArray($binding, true);
} else { } else {
$binding = trim(strtolower($binding)); $bindId = self::TYPES[trim(strtolower($binding))] ?? 0;
if (strpos($binding, "strict ")===0) { if (!$bindId) {
// "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)) {
throw new Exception("paramTypeInvalid", $binding); // @codeCoverageIgnore throw new Exception("paramTypeInvalid", $binding); // @codeCoverageIgnore
} }
$this->types[] = self::TYPES[$binding]; $this->types[] = $bindId;
} }
} }
if (!$append) { if (!$append) {
@ -61,27 +68,16 @@ abstract class AbstractStatement implements Statement {
return true; return true;
} }
protected function cast($v, string $t, bool $nullable) { protected function cast($v, int $t) {
switch ($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"); $v = Date::transform($v, "sql");
if (is_null($v) && !$nullable) { return $v ? $v : "0001-01-01 00:00:00";
$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;
default: 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 // recursively flatten any arrays, which may be provided for SET or IN() clauses
$a += $this->bindValues($value, $a); $a += $this->bindValues($value, $a);
} elseif (array_key_exists($a, $this->types)) { } elseif (array_key_exists($a, $this->types)) {
$value = $this->cast($value, $this->types[$a], $this->isNullable[$a]); $value = $this->cast($value, $this->types[$a]);
$this->bindValue($value, $this->types[$a], ++$a); $this->bindValue($value, $this->types[$a] % self::T_NOT_NULL, ++$a);
} else { } else {
throw new Exception("paramTypeMissing", $a+1); 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 // SQLite will happily substitute null for a missing value, but other engines (viz. PostgreSQL) produce an error
if (is_null($offset)) { if (is_null($offset)) {
while ($a < sizeof($this->types)) { 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; return $a - $offset;

18
lib/Db/MySQL/Statement.php

@ -14,12 +14,12 @@ class Statement extends \JKingWeb\Arsse\Db\AbstractStatement {
use ExceptionBuilder; use ExceptionBuilder;
const BINDINGS = [ const BINDINGS = [
"integer" => "i", self::T_INTEGER => "i",
"float" => "d", self::T_FLOAT => "d",
"datetime" => "s", self::T_DATETIME => "s",
"binary" => "b", self::T_BINARY => "b",
"string" => "s", self::T_STRING => "s",
"boolean" => "i", self::T_BOOLEAN => "i",
]; ];
protected $db; protected $db;
@ -93,11 +93,11 @@ class Statement extends \JKingWeb\Arsse\Db\AbstractStatement {
return new Result($r, [$changes, $lastId], $this); 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 // 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 // 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 // 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->values[] = null;
$this->longs[$position - 1] = $value; $this->longs[$position - 1] = $value;
$this->binds .= "b"; $this->binds .= "b";
@ -112,7 +112,7 @@ class Statement extends \JKingWeb\Arsse\Db\AbstractStatement {
$out = ""; $out = "";
for ($b = 1; $b < sizeof($query); $b++) { for ($b = 1; $b < sizeof($query); $b++) {
$a = $b - 1; $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 .= $query[$a].$mark;
} }
$out .= array_pop($query); $out .= array_pop($query);

14
lib/Db/PDOStatement.php

@ -10,12 +10,12 @@ abstract class PDOStatement extends AbstractStatement {
use PDOError; use PDOError;
const BINDINGS = [ const BINDINGS = [
"integer" => \PDO::PARAM_INT, self::T_INTEGER => \PDO::PARAM_INT,
"float" => \PDO::PARAM_STR, self::T_FLOAT => \PDO::PARAM_STR,
"datetime" => \PDO::PARAM_STR, self::T_DATETIME => \PDO::PARAM_STR,
"binary" => \PDO::PARAM_LOB, self::T_BINARY => \PDO::PARAM_LOB,
"string" => \PDO::PARAM_STR, self::T_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_BOOLEAN => \PDO::PARAM_INT, // FIXME: using \PDO::PARAM_BOOL leads to incompatibilities with versions of SQLite bundled prior to PHP 7.3
]; ];
protected $st; protected $st;
@ -55,7 +55,7 @@ abstract class PDOStatement extends AbstractStatement {
return new PDOResult($this->db, $this->st); 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]); 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; use Dispatch;
const BINDINGS = [ const BINDINGS = [
"integer" => "bigint", self::T_INTEGER => "bigint",
"float" => "decimal", self::T_FLOAT => "decimal",
"datetime" => "timestamp(0) without time zone", self::T_DATETIME => "timestamp(0) without time zone",
"binary" => "bytea", self::T_BINARY => "bytea",
"string" => "text", self::T_STRING => "text",
"boolean" => "smallint", // FIXME: using boolean leads to incompatibilities with versions of SQLite bundled prior to PHP 7.3 self::T_BOOLEAN => "smallint", // FIXME: using boolean leads to incompatibilities with versions of SQLite bundled prior to PHP 7.3
]; ];
protected $db; 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; $this->in[] = $value;
return true; return true;
} }
@ -59,7 +59,7 @@ class Statement extends \JKingWeb\Arsse\Db\AbstractStatement {
for ($b = 1; $b < sizeof($q); $b++) { for ($b = 1; $b < sizeof($q); $b++) {
$a = $b - 1; $a = $b - 1;
$mark = $mungeParamMarkers ? "\$$b" : "?"; $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 .= $q[$a].$mark.$type;
} }
$out .= array_pop($q); $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_CONSTRAINT = 19;
const SQLITE_MISMATCH = 20; const SQLITE_MISMATCH = 20;
const BINDINGS = [ const BINDINGS = [
"integer" => \SQLITE3_INTEGER, self::T_INTEGER => \SQLITE3_INTEGER,
"float" => \SQLITE3_FLOAT, self::T_FLOAT => \SQLITE3_FLOAT,
"datetime" => \SQLITE3_TEXT, self::T_DATETIME => \SQLITE3_TEXT,
"binary" => \SQLITE3_BLOB, self::T_BINARY => \SQLITE3_BLOB,
"string" => \SQLITE3_TEXT, self::T_STRING => \SQLITE3_TEXT,
"boolean" => \SQLITE3_INTEGER, self::T_BOOLEAN => \SQLITE3_INTEGER,
]; ];
protected $db; protected $db;
@ -68,7 +68,7 @@ class Statement extends \JKingWeb\Arsse\Db\AbstractStatement {
return new Result($r, [$changes, $lastId], $this); 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]); 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 { interface Statement {
const TYPES = [ const TYPES = [
"int" => "integer", 'int' => self::T_INTEGER,
"integer" => "integer", 'integer' => self::T_INTEGER,
"float" => "float", 'float' => self::T_FLOAT,
"double" => "float", 'double' => self::T_FLOAT,
"real" => "float", 'real' => self::T_FLOAT,
"numeric" => "float", 'numeric' => self::T_FLOAT,
"datetime" => "datetime", 'datetime' => self::T_DATETIME,
"timestamp" => "datetime", 'timestamp' => self::T_DATETIME,
"blob" => "binary", 'blob' => self::T_BINARY,
"bin" => "binary", 'bin' => self::T_BINARY,
"binary" => "binary", 'binary' => self::T_BINARY,
"text" => "string", 'text' => self::T_STRING,
"string" => "string", 'string' => self::T_STRING,
"str" => "string", 'str' => self::T_STRING,
"bool" => "boolean", 'bool' => self::T_BOOLEAN,
"boolean" => "boolean", 'boolean' => self::T_BOOLEAN,
"bit" => "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 run(...$values): Result;
public function runArray(array $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 integer' => [null, "strict integer", "0"],
'Null as strict float' => [null, "strict float", "0.0"], 'Null as strict float' => [null, "strict float", "0.0"],
'Null as strict string' => [null, "strict string", "''"], '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"], 'Null as strict boolean' => [null, "strict boolean", "0"],
'True as integer' => [true, "integer", "1"], 'True as integer' => [true, "integer", "1"],
'True as float' => [true, "float", "1.0"], '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 integer' => [true, "strict integer", "1"],
'True as strict float' => [true, "strict float", "1.0"], 'True as strict float' => [true, "strict float", "1.0"],
'True as strict string' => [true, "strict string", "'1'"], '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"], 'True as strict boolean' => [true, "strict boolean", "1"],
'False as integer' => [false, "integer", "0"], 'False as integer' => [false, "integer", "0"],
'False as float' => [false, "float", "0.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 integer' => [false, "strict integer", "0"],
'False as strict float' => [false, "strict float", "0.0"], 'False as strict float' => [false, "strict float", "0.0"],
'False as strict string' => [false, "strict string", "''"], '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"], 'False as strict boolean' => [false, "strict boolean", "0"],
'Integer as integer' => [2112, "integer", "2112"], 'Integer as integer' => [2112, "integer", "2112"],
'Integer as float' => [2112, "float", "2112.0"], '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 integer' => ["Random string", "strict integer", "0"],
'ASCII string as strict float' => ["Random string", "strict float", "0.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 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"], 'ASCII string as strict boolean' => ["Random string", "strict boolean", "1"],
'UTF-8 string as integer' => ["\u{e9}", "integer", "0"], 'UTF-8 string as integer' => ["\u{e9}", "integer", "0"],
'UTF-8 string as float' => ["\u{e9}", "float", "0.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 integer' => ["\u{e9}", "strict integer", "0"],
'UTF-8 string as strict float' => ["\u{e9}", "strict float", "0.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 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"], '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 integer' => ["2017-01-09T13:11:17", "integer", "0"],
'ISO 8601 string as float' => ["2017-01-09T13:11:17", "float", "0.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 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 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 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"], '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 binary' => ["2017-01-09T13:11:17", "binary", "x'323031372d30312d30395431333a31313a3137'"],
'ISO 8601 string as strict binary' => ["2017-01-09T13:11:17", "strict binary", "x'323031372d30312d30395431333a31313a3137'"], 'ISO 8601 string as strict binary' => ["2017-01-09T13:11:17", "strict binary", "x'323031372d30312d30395431333a31313a3137'"],

Loading…
Cancel
Save