diff --git a/lib/Db/AbstractStatement.php b/lib/Db/AbstractStatement.php index 1269a609..83901942 100644 --- a/lib/Db/AbstractStatement.php +++ b/lib/Db/AbstractStatement.php @@ -50,22 +50,11 @@ abstract class AbstractStatement implements Statement { protected function cast($v, string $t, bool $nullable) { switch ($t) { - case "date": - if (is_null($v) && !$nullable) { - $v = 0; - } - return Date::transform($v, "date"); - case "time": - if (is_null($v) && !$nullable) { - $v = 0; - } - return Date::transform($v, "time"); case "datetime": if (is_null($v) && !$nullable) { $v = 0; } return Date::transform($v, "sql"); - case "null": case "integer": case "float": case "binary": diff --git a/lib/Db/PDOStatement.php b/lib/Db/PDOStatement.php index a8d459ef..6a2534d2 100644 --- a/lib/Db/PDOStatement.php +++ b/lib/Db/PDOStatement.php @@ -10,11 +10,8 @@ class PDOStatement extends AbstractStatement { use PDOError; const BINDINGS = [ - "null" => \PDO::PARAM_NULL, "integer" => \PDO::PARAM_INT, "float" => \PDO::PARAM_STR, - "date" => \PDO::PARAM_STR, - "time" => \PDO::PARAM_STR, "datetime" => \PDO::PARAM_STR, "binary" => \PDO::PARAM_LOB, "string" => \PDO::PARAM_STR, diff --git a/lib/Db/SQLite3/Statement.php b/lib/Db/SQLite3/Statement.php index 34228ef3..55da642d 100644 --- a/lib/Db/SQLite3/Statement.php +++ b/lib/Db/SQLite3/Statement.php @@ -17,11 +17,8 @@ class Statement extends \JKingWeb\Arsse\Db\AbstractStatement { const SQLITE_CONSTRAINT = 19; const SQLITE_MISMATCH = 20; const BINDINGS = [ - "null" => \SQLITE3_NULL, "integer" => \SQLITE3_INTEGER, "float" => \SQLITE3_FLOAT, - "date" => \SQLITE3_TEXT, - "time" => \SQLITE3_TEXT, "datetime" => \SQLITE3_TEXT, "binary" => \SQLITE3_BLOB, "string" => \SQLITE3_TEXT, diff --git a/lib/Db/Statement.php b/lib/Db/Statement.php index f95bdfbb..02677c45 100644 --- a/lib/Db/Statement.php +++ b/lib/Db/Statement.php @@ -8,16 +8,12 @@ namespace JKingWeb\Arsse\Db; interface Statement { const TYPES = [ - "null" => "null", - "nil" => "null", "int" => "integer", "integer" => "integer", "float" => "float", "double" => "float", "real" => "float", "numeric" => "float", - "date" => "date", - "time" => "time", "datetime" => "datetime", "timestamp" => "datetime", "blob" => "binary", diff --git a/tests/cases/Db/SQLite3/TestStatement.php b/tests/cases/Db/SQLite3/TestStatement.php index b139f087..4bcb326e 100644 --- a/tests/cases/Db/SQLite3/TestStatement.php +++ b/tests/cases/Db/SQLite3/TestStatement.php @@ -12,8 +12,6 @@ use JKingWeb\Arsse\Db\Statement; * @covers \JKingWeb\Arsse\Db\SQLite3\Statement * @covers \JKingWeb\Arsse\Db\SQLite3\ExceptionBuilder */ class TestStatement extends \JKingWeb\Arsse\Test\AbstractTest { - use \JKingWeb\Arsse\Test\Db\BindingTests; - protected $c; protected static $imp = \JKingWeb\Arsse\Db\SQLite3\Statement::class; @@ -46,6 +44,213 @@ class TestStatement extends \JKingWeb\Arsse\Test\AbstractTest { } } + /** @dataProvider provideBindings */ + public function testBindATypedValue($value, $type, $exp) { + $typeStr = "'".str_replace("'", "''", $type)."'"; + $nativeStatement = $this->c->prepare( + "SELECT ( + (CASE WHEN substr($typeStr, 0, 7) <> 'strict ' then null else 1 end) is null + and ? is null + ) or ( + $exp = ? + ) as pass" + ); + $s = new self::$imp($this->c, $nativeStatement); + $s->rebindArray([$type, $type]); + $act = (bool) $s->run(...[$value, $value])->getRow()['pass']; + $this->assertTrue($act); + } + + public function provideBindings() { + $dateMutable = new \DateTime("Noon Today", new \DateTimezone("America/Toronto")); + $dateImmutable = new \DateTimeImmutable("Noon Today", new \DateTimezone("America/Toronto")); + $dateUTC = new \DateTime("@".$dateMutable->getTimestamp(), new \DateTimezone("UTC")); + return [ + /* input, type, expected binding as SQL fragment */ + [null, "integer", "null"], + [null, "float", "null"], + [null, "string", "null"], + [null, "binary", "null"], + [null, "datetime", "null"], + [null, "boolean", "null"], + [null, "strict integer", "0"], + [null, "strict float", "0.0"], + [null, "strict string", "''"], + [null, "strict binary", "x''"], + [null, "strict datetime", "'1970-01-01 00:00:00'"], + [null, "strict boolean", "0"], + // true + [true, "integer", "1"], + [true, "float", "1.0"], + [true, "string", "'1'"], + [true, "binary", "x'31'"], + [true, "datetime", "null"], + [true, "boolean", "1"], + [true, "strict integer", "1"], + [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 boolean", "1"], + // false + [false, "integer", "0"], + [false, "float", "0.0"], + [false, "string", "''"], + [false, "binary", "x''"], + [false, "datetime", "null"], + [false, "boolean", "0"], + [false, "strict integer", "0"], + [false, "strict float", "0.0"], + [false, "strict string", "''"], + [false, "strict binary", "x''"], + [false, "strict datetime", "'1970-01-01 00:00:00'"], + [false, "strict boolean", "0"], + // integer + [2112, "integer", "2112"], + [2112, "float", "2112.0"], + [2112, "string", "'2112'"], + [2112, "binary", "x'32313132'"], + [2112, "datetime", "'1970-01-01 00:35:12'"], + [2112, "boolean", "1"], + [2112, "strict integer", "2112"], + [2112, "strict float", "2112.0"], + [2112, "strict string", "'2112'"], + [2112, "strict binary", "x'32313132'"], + [2112, "strict datetime", "'1970-01-01 00:35:12'"], + [2112, "strict boolean", "1"], + // integer zero + [0, "integer", "0"], + [0, "float", "0.0"], + [0, "string", "'0'"], + [0, "binary", "x'30'"], + [0, "datetime", "'1970-01-01 00:00:00'"], + [0, "boolean", "0"], + [0, "strict integer", "0"], + [0, "strict float", "0.0"], + [0, "strict string", "'0'"], + [0, "strict binary", "x'30'"], + [0, "strict datetime", "'1970-01-01 00:00:00'"], + [0, "strict boolean", "0"], + // float + [2112.99, "integer", "2112"], + [2112.99, "float", "2112.99"], + [2112.99, "string", "'2112.99'"], + [2112.99, "binary", "x'323131322e3939'"], + [2112.99, "datetime", "'1970-01-01 00:35:12'"], + [2112.99, "boolean", "1"], + [2112.99, "strict integer", "2112"], + [2112.99, "strict float", "2112.99"], + [2112.99, "strict string", "'2112.99'"], + [2112.99, "strict binary", "x'323131322e3939'"], + [2112.99, "strict datetime", "'1970-01-01 00:35:12'"], + [2112.99, "strict boolean", "1"], + // float zero + [0.0, "integer", "0"], + [0.0, "float", "0.0"], + [0.0, "string", "'0'"], + [0.0, "binary", "x'30'"], + [0.0, "datetime", "'1970-01-01 00:00:00'"], + [0.0, "boolean", "0"], + [0.0, "strict integer", "0"], + [0.0, "strict float", "0.0"], + [0.0, "strict string", "'0'"], + [0.0, "strict binary", "x'30'"], + [0.0, "strict datetime", "'1970-01-01 00:00:00'"], + [0.0, "strict boolean", "0"], + // ASCII string + ["Random string", "integer", "0"], + ["Random string", "float", "0.0"], + ["Random string", "string", "'Random string'"], + ["Random string", "binary", "x'52616e646f6d20737472696e67'"], + ["Random string", "datetime", "null"], + ["Random string", "boolean", "1"], + ["Random string", "strict integer", "0"], + ["Random string", "strict float", "0.0"], + ["Random string", "strict string", "'Random string'"], + ["Random string", "strict binary", "x'52616e646f6d20737472696e67'"], + ["Random string", "strict datetime", "'1970-01-01 00:00:00'"], + ["Random string", "strict boolean", "1"], + // UTF-8 string + ["é", "integer", "0"], + ["é", "float", "0.0"], + ["é", "string", "char(233)"], + ["é", "binary", "x'c3a9'"], + ["é", "datetime", "null"], + ["é", "boolean", "1"], + ["é", "strict integer", "0"], + ["é", "strict float", "0.0"], + ["é", "strict string", "char(233)"], + ["é", "strict binary", "x'c3a9'"], + ["é", "strict datetime", "'1970-01-01 00:00:00'"], + ["é", "strict boolean", "1"], + // binary string + [chr(233).chr(233), "integer", "0"], + [chr(233).chr(233), "float", "0.0"], + [chr(233).chr(233), "string", "'".chr(233).chr(233)."'"], + [chr(233).chr(233), "binary", "x'e9e9'"], + [chr(233).chr(233), "datetime", "null"], + [chr(233).chr(233), "boolean", "1"], + [chr(233).chr(233), "strict integer", "0"], + [chr(233).chr(233), "strict float", "0.0"], + [chr(233).chr(233), "strict string", "'".chr(233).chr(233)."'"], + [chr(233).chr(233), "strict binary", "x'e9e9'"], + [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", "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 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'"], + ["2017-01-09T13:11:17", "strict boolean", "1"], + // arbitrary date string + ["Today", "integer", "0"], + ["Today", "float", "0.0"], + ["Today", "string", "'Today'"], + ["Today", "binary", "x'546f646179'"], + ["Today", "datetime", "'".date_create("Today", new \DateTimezone("UTC"))->format("Y-m-d H:i:s")."'"], + ["Today", "boolean", "1"], + ["Today", "strict integer", "0"], + ["Today", "strict float", "0.0"], + ["Today", "strict string", "'Today'"], + ["Today", "strict binary", "x'546f646179'"], + ["Today", "strict datetime", "'".date_create("Today", new \DateTimezone("UTC"))->format("Y-m-d H:i:s")."'"], + ["Today", "strict boolean", "1"], + // mutable date object + [$dateMutable, "integer", $dateUTC->getTimestamp()], + [$dateMutable, "float", $dateUTC->getTimestamp().".0"], + [$dateMutable, "string", "'".$dateUTC->format("Y-m-d H:i:s")."'"], + [$dateMutable, "binary", "x'".bin2hex($dateUTC->format("Y-m-d H:i:s"))."'"], + [$dateMutable, "datetime", "'".$dateUTC->format("Y-m-d H:i:s")."'"], + [$dateMutable, "boolean", "1"], + [$dateMutable, "strict integer", $dateUTC->getTimestamp()], + [$dateMutable, "strict float", $dateUTC->getTimestamp().".0"], + [$dateMutable, "strict string", "'".$dateUTC->format("Y-m-d H:i:s")."'"], + [$dateMutable, "strict binary", "x'".bin2hex($dateUTC->format("Y-m-d H:i:s"))."'"], + [$dateMutable, "strict datetime", "'".$dateUTC->format("Y-m-d H:i:s")."'"], + [$dateMutable, "strict boolean", "1"], + // immutable date object + [$dateImmutable, "integer", $dateUTC->getTimestamp()], + [$dateImmutable, "float", $dateUTC->getTimestamp().".0"], + [$dateImmutable, "string", "'".$dateUTC->format("Y-m-d H:i:s")."'"], + [$dateImmutable, "binary", "x'".bin2hex($dateUTC->format("Y-m-d H:i:s"))."'"], + [$dateImmutable, "datetime", "'".$dateUTC->format("Y-m-d H:i:s")."'"], + [$dateImmutable, "boolean", "1"], + [$dateImmutable, "strict integer", $dateUTC->getTimestamp()], + [$dateImmutable, "strict float", $dateUTC->getTimestamp().".0"], + [$dateImmutable, "strict string", "'".$dateUTC->format("Y-m-d H:i:s")."'"], + [$dateImmutable, "strict binary", "x'".bin2hex($dateUTC->format("Y-m-d H:i:s"))."'"], + [$dateImmutable, "strict datetime", "'".$dateUTC->format("Y-m-d H:i:s")."'"], + [$dateImmutable, "strict boolean", "1"], + ]; + } + public function testConstructStatement() { $nativeStatement = $this->c->prepare("SELECT ? as value"); $this->assertInstanceOf(Statement::class, new \JKingWeb\Arsse\Db\SQLite3\Statement($this->c, $nativeStatement)); diff --git a/tests/lib/Db/BindingTests.php b/tests/lib/Db/BindingTests.php deleted file mode 100644 index 5ba5ff3e..00000000 --- a/tests/lib/Db/BindingTests.php +++ /dev/null @@ -1,253 +0,0 @@ - null, - "integer" => null, - "float" => null, - "date" => null, - "time" => null, - "datetime" => null, - "binary" => null, - "string" => null, - "boolean" => null, - ]; - $this->checkBinding($input, $exp); - // types may also be strict (e.g. "strict integer") and never pass null to the database; this is useful for NOT NULL columns - // only null input should yield different results, so only this test has different expectations - $exp = [ - "null" => null, - "integer" => 0, - "float" => 0.0, - "date" => gmdate("Y-m-d", 0), - "time" => gmdate("H:i:s", 0), - "datetime" => gmdate("Y-m-d H:i:s", 0), - "binary" => "", - "string" => "", - "boolean" => 0, - ]; - $this->checkBinding($input, $exp, true); - } - - public function testBindTrue() { - $input = true; - $exp = [ - "null" => null, - "integer" => 1, - "float" => 1.0, - "date" => null, - "time" => null, - "datetime" => null, - "binary" => "1", - "string" => "1", - "boolean" => 1, - ]; - $this->checkBinding($input, $exp); - $this->checkBinding($input, $exp, true); - } - - public function testBindFalse() { - $input = false; - $exp = [ - "null" => null, - "integer" => 0, - "float" => 0.0, - "date" => null, - "time" => null, - "datetime" => null, - "binary" => "", - "string" => "", - "boolean" => 0, - ]; - $this->checkBinding($input, $exp); - $this->checkBinding($input, $exp, true); - } - - public function testBindInteger() { - $input = 2112; - $exp = [ - "null" => null, - "integer" => 2112, - "float" => 2112.0, - "date" => gmdate("Y-m-d", 2112), - "time" => gmdate("H:i:s", 2112), - "datetime" => gmdate("Y-m-d H:i:s", 2112), - "binary" => "2112", - "string" => "2112", - "boolean" => 1, - ]; - $this->checkBinding($input, $exp); - $this->checkBinding($input, $exp, true); - } - - public function testBindIntegerZero() { - $input = 0; - $exp = [ - "null" => null, - "integer" => 0, - "float" => 0.0, - "date" => gmdate("Y-m-d", 0), - "time" => gmdate("H:i:s", 0), - "datetime" => gmdate("Y-m-d H:i:s", 0), - "binary" => "0", - "string" => "0", - "boolean" => 0, - ]; - $this->checkBinding($input, $exp); - $this->checkBinding($input, $exp, true); - } - - public function testBindFloat() { - $input = 2112.0; - $exp = [ - "null" => null, - "integer" => 2112, - "float" => 2112.0, - "date" => gmdate("Y-m-d", 2112), - "time" => gmdate("H:i:s", 2112), - "datetime" => gmdate("Y-m-d H:i:s", 2112), - "binary" => "2112", - "string" => "2112", - "boolean" => 1, - ]; - $this->checkBinding($input, $exp); - $this->checkBinding($input, $exp, true); - } - - public function testBindFloatZero() { - $input = 0.0; - $exp = [ - "null" => null, - "integer" => 0, - "float" => 0.0, - "date" => gmdate("Y-m-d", 0), - "time" => gmdate("H:i:s", 0), - "datetime" => gmdate("Y-m-d H:i:s", 0), - "binary" => "0", - "string" => "0", - "boolean" => 0, - ]; - $this->checkBinding($input, $exp); - $this->checkBinding($input, $exp, true); - } - - public function testBindAsciiString() { - $input = "Random string"; - $exp = [ - "null" => null, - "integer" => 0, - "float" => 0.0, - "date" => null, - "time" => null, - "datetime" => null, - "binary" => $input, - "string" => $input, - "boolean" => 1, - ]; - $this->checkBinding($input, $exp); - $this->checkBinding($input, $exp, true); - } - - public function testBindUtf8String() { - $input = "é"; - $exp = [ - "null" => null, - "integer" => 0, - "float" => 0.0, - "date" => null, - "time" => null, - "datetime" => null, - "binary" => $input, - "string" => $input, - "boolean" => 1, - ]; - $this->checkBinding($input, $exp); - $this->checkBinding($input, $exp, true); - } - - public function testBindBinaryString() { - // FIXME: This test may be unreliable; SQLite happily stores invalid UTF-8 text as bytes untouched, but other engines probably don't do this - $input = chr(233); - $exp = [ - "null" => null, - "integer" => 0, - "float" => 0.0, - "date" => null, - "time" => null, - "datetime" => null, - "binary" => $input, - "string" => $input, - "boolean" => 1, - ]; - $this->checkBinding($input, $exp); - $this->checkBinding($input, $exp, true); - } - - public function testBindIso8601DateString() { - $input = "2017-01-09T13:11:17"; - $time = strtotime($input." UTC"); - $exp = [ - "null" => null, - "integer" => 2017, - "float" => 2017.0, - "date" => gmdate("Y-m-d", $time), - "time" => gmdate("H:i:s", $time), - "datetime" => gmdate("Y-m-d H:i:s", $time), - "binary" => $input, - "string" => $input, - "boolean" => 1, - ]; - $this->checkBinding($input, $exp); - $this->checkBinding($input, $exp, true); - } - - public function testBindArbitraryDateString() { - $input = "Today"; - $time = date_create($input, new \DateTimezone("UTC"))->getTimestamp(); - $exp = [ - "null" => null, - "integer" => 0, - "float" => 0.0, - "date" => gmdate("Y-m-d", $time), - "time" => gmdate("H:i:s", $time), - "datetime" => gmdate("Y-m-d H:i:s", $time), - "binary" => $input, - "string" => $input, - "boolean" => 1, - ]; - $this->checkBinding($input, $exp); - $this->checkBinding($input, $exp, true); - } - - public function testBindMutableDateObject($class = '\DateTime') { - $input = new $class("Noon Today"); - $time = $input->getTimestamp(); - $exp = [ - "null" => null, - "integer" => $time, - "float" => (float) $time, - "date" => gmdate("Y-m-d", $time), - "time" => gmdate("H:i:s", $time), - "datetime" => gmdate("Y-m-d H:i:s", $time), - "binary" => gmdate("Y-m-d H:i:s", $time), - "string" => gmdate("Y-m-d H:i:s", $time), - "boolean" => 1, - ]; - $this->checkBinding($input, $exp); - $this->checkBinding($input, $exp, true); - } - - public function testBindImmutableDateObject() { - $this->testBindMutableDateObject('\DateTimeImmutable'); - } -}