mirror of
https://code.mensbeam.com/MensBeam/Arsse.git
synced 2024-12-22 13:12:41 +00:00
Simplify SQL type handling
This is done in anticipation of dealing with SQL types in places other than statements
This commit is contained in:
parent
b55d0b374f
commit
837f3c6dd6
7 changed files with 107 additions and 87 deletions
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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…
Reference in a new issue