The clean & modern RSS server that doesn't give you any crap. https://thearsse.com/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

BaseStatement.php 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. <?php
  2. /** @license MIT
  3. * Copyright 2017 J. King, Dustin Wilson et al.
  4. * See LICENSE and AUTHORS files for details */
  5. declare(strict_types=1);
  6. namespace JKingWeb\Arsse\TestCase\Db;
  7. use JKingWeb\Arsse\Db\Statement;
  8. use JKingWeb\Arsse\Test\DatabaseInformation;
  9. abstract class BaseStatement extends \JKingWeb\Arsse\Test\AbstractTest {
  10. protected static $dbInfo;
  11. protected static $interface;
  12. protected $statementClass;
  13. protected $stringOutput;
  14. abstract protected function makeStatement(string $q, array $types = []): array;
  15. abstract protected function decorateTypeSyntax(string $value, string $type): string;
  16. public static function setUpBeforeClass() {
  17. // establish a clean baseline
  18. static::clearData();
  19. static::$dbInfo = new DatabaseInformation(static::$implementation);
  20. static::setConf();
  21. static::$interface = (static::$dbInfo->interfaceConstructor)();
  22. }
  23. public function setUp() {
  24. self::clearData();
  25. self::setConf();
  26. if (!static::$interface) {
  27. $this->markTestSkipped(static::$implementation." database driver not available");
  28. }
  29. // completely clear the database
  30. (static::$dbInfo->razeFunction)(static::$interface);
  31. $this->statementClass = static::$dbInfo->statementClass;
  32. $this->stringOutput = static::$dbInfo->stringOutput;
  33. }
  34. public function tearDown() {
  35. if (static::$interface) {
  36. // completely clear the database
  37. (static::$dbInfo->razeFunction)(static::$interface);
  38. }
  39. self::clearData();
  40. }
  41. public static function tearDownAfterClass() {
  42. static::$implementation = null;
  43. static::$dbInfo = null;
  44. self::clearData();
  45. }
  46. public function testConstructStatement() {
  47. $this->assertInstanceOf(Statement::class, new $this->statementClass(...$this->makeStatement("SELECT ? as value")));
  48. }
  49. /** @dataProvider provideBindings */
  50. public function testBindATypedValue($value, string $type, string $exp) {
  51. if ($exp=="null") {
  52. $query = "SELECT (? is null) as pass";
  53. } else {
  54. $query = "SELECT ($exp = ?) as pass";
  55. }
  56. $typeStr = "'".str_replace("'", "''", $type)."'";
  57. $s = new $this->statementClass(...$this->makeStatement($query));
  58. $s->retype(...[$type]);
  59. $act = $s->run(...[$value])->getValue();
  60. $this->assertTrue((bool) $act);
  61. }
  62. /** @dataProvider provideBinaryBindings */
  63. public function testHandleBinaryData($value, string $type, string $exp) {
  64. if (in_array(static::$implementation, ["PostgreSQL", "PDO PostgreSQL"])) {
  65. $this->markTestSkipped("Correct handling of binary data with PostgreSQL is currently unknown");
  66. }
  67. if ($exp=="null") {
  68. $query = "SELECT (? is null) as pass";
  69. } else {
  70. $query = "SELECT ($exp = ?) as pass";
  71. }
  72. $typeStr = "'".str_replace("'", "''", $type)."'";
  73. $s = new $this->statementClass(...$this->makeStatement($query));
  74. $s->retype(...[$type]);
  75. $act = $s->run(...[$value])->getValue();
  76. $this->assertTrue((bool) $act);
  77. }
  78. public function testBindMissingValue() {
  79. $s = new $this->statementClass(...$this->makeStatement("SELECT ? as value", ["int"]));
  80. $val = $s->runArray()->getRow()['value'];
  81. $this->assertSame(null, $val);
  82. }
  83. public function testBindMultipleValues() {
  84. $exp = [
  85. 'one' => 1,
  86. 'two' => 2,
  87. ];
  88. $exp = $this->stringOutput ? $this->stringify($exp) : $exp;
  89. $s = new $this->statementClass(...$this->makeStatement("SELECT ? as one, ? as two", ["int", "int"]));
  90. $val = $s->runArray([1,2])->getRow();
  91. $this->assertSame($exp, $val);
  92. }
  93. public function testBindRecursively() {
  94. $exp = [
  95. 'one' => 1,
  96. 'two' => 2,
  97. 'three' => 3,
  98. 'four' => 4,
  99. ];
  100. $exp = $this->stringOutput ? $this->stringify($exp) : $exp;
  101. $s = new $this->statementClass(...$this->makeStatement("SELECT ? as one, ? as two, ? as three, ? as four", ["int", ["int", "int"], "int"]));
  102. $val = $s->runArray([1, [2, 3], 4])->getRow();
  103. $this->assertSame($exp, $val);
  104. }
  105. public function testBindWithoutType() {
  106. $this->assertException("paramTypeMissing", "Db");
  107. $s = new $this->statementClass(...$this->makeStatement("SELECT ? as value", []));
  108. $s->runArray([1]);
  109. }
  110. public function testViolateConstraint() {
  111. (new $this->statementClass(...$this->makeStatement("CREATE TABLE if not exists arsse_meta(key varchar(255) primary key not null, value text)")))->run();
  112. $s = new $this->statementClass(...$this->makeStatement("INSERT INTO arsse_meta(key) values(?)", ["str"]));
  113. $this->assertException("constraintViolation", "Db", "ExceptionInput");
  114. $s->runArray([null]);
  115. }
  116. public function testMismatchTypes() {
  117. (new $this->statementClass(...$this->makeStatement("CREATE TABLE if not exists arsse_feeds(id integer primary key not null, url text not null)")))->run();
  118. $s = new $this->statementClass(...$this->makeStatement("INSERT INTO arsse_feeds(id,url) values(?,?)", ["str", "str"]));
  119. $this->assertException("typeViolation", "Db", "ExceptionInput");
  120. $s->runArray(['ook', 'eek']);
  121. }
  122. public function provideBindings() {
  123. $dateMutable = new \DateTime("Noon Today", new \DateTimezone("America/Toronto"));
  124. $dateImmutable = new \DateTimeImmutable("Noon Today", new \DateTimezone("America/Toronto"));
  125. $dateUTC = new \DateTime("@".$dateMutable->getTimestamp(), new \DateTimezone("UTC"));
  126. $tests = [
  127. 'Null as integer' => [null, "integer", "null"],
  128. 'Null as float' => [null, "float", "null"],
  129. 'Null as string' => [null, "string", "null"],
  130. 'Null as datetime' => [null, "datetime", "null"],
  131. 'Null as boolean' => [null, "boolean", "null"],
  132. 'Null as strict integer' => [null, "strict integer", "0"],
  133. 'Null as strict float' => [null, "strict float", "0.0"],
  134. 'Null as strict string' => [null, "strict string", "''"],
  135. 'Null as strict datetime' => [null, "strict datetime", "'1970-01-01 00:00:00'"],
  136. 'Null as strict boolean' => [null, "strict boolean", "0"],
  137. 'True as integer' => [true, "integer", "1"],
  138. 'True as float' => [true, "float", "1.0"],
  139. 'True as string' => [true, "string", "'1'"],
  140. 'True as datetime' => [true, "datetime", "null"],
  141. 'True as boolean' => [true, "boolean", "1"],
  142. 'True as strict integer' => [true, "strict integer", "1"],
  143. 'True as strict float' => [true, "strict float", "1.0"],
  144. 'True as strict string' => [true, "strict string", "'1'"],
  145. 'True as strict datetime' => [true, "strict datetime", "'1970-01-01 00:00:00'"],
  146. 'True as strict boolean' => [true, "strict boolean", "1"],
  147. 'False as integer' => [false, "integer", "0"],
  148. 'False as float' => [false, "float", "0.0"],
  149. 'False as string' => [false, "string", "''"],
  150. 'False as datetime' => [false, "datetime", "null"],
  151. 'False as boolean' => [false, "boolean", "0"],
  152. 'False as strict integer' => [false, "strict integer", "0"],
  153. 'False as strict float' => [false, "strict float", "0.0"],
  154. 'False as strict string' => [false, "strict string", "''"],
  155. 'False as strict datetime' => [false, "strict datetime", "'1970-01-01 00:00:00'"],
  156. 'False as strict boolean' => [false, "strict boolean", "0"],
  157. 'Integer as integer' => [2112, "integer", "2112"],
  158. 'Integer as float' => [2112, "float", "2112.0"],
  159. 'Integer as string' => [2112, "string", "'2112'"],
  160. 'Integer as datetime' => [2112, "datetime", "'1970-01-01 00:35:12'"],
  161. 'Integer as boolean' => [2112, "boolean", "1"],
  162. 'Integer as strict integer' => [2112, "strict integer", "2112"],
  163. 'Integer as strict float' => [2112, "strict float", "2112.0"],
  164. 'Integer as strict string' => [2112, "strict string", "'2112'"],
  165. 'Integer as strict datetime' => [2112, "strict datetime", "'1970-01-01 00:35:12'"],
  166. 'Integer as strict boolean' => [2112, "strict boolean", "1"],
  167. 'Integer zero as integer' => [0, "integer", "0"],
  168. 'Integer zero as float' => [0, "float", "0.0"],
  169. 'Integer zero as string' => [0, "string", "'0'"],
  170. 'Integer zero as datetime' => [0, "datetime", "'1970-01-01 00:00:00'"],
  171. 'Integer zero as boolean' => [0, "boolean", "0"],
  172. 'Integer zero as strict integer' => [0, "strict integer", "0"],
  173. 'Integer zero as strict float' => [0, "strict float", "0.0"],
  174. 'Integer zero as strict string' => [0, "strict string", "'0'"],
  175. 'Integer zero as strict datetime' => [0, "strict datetime", "'1970-01-01 00:00:00'"],
  176. 'Integer zero as strict boolean' => [0, "strict boolean", "0"],
  177. 'Float as integer' => [2112.5, "integer", "2112"],
  178. 'Float as float' => [2112.5, "float", "2112.5"],
  179. 'Float as string' => [2112.5, "string", "'2112.5'"],
  180. 'Float as datetime' => [2112.5, "datetime", "'1970-01-01 00:35:12'"],
  181. 'Float as boolean' => [2112.5, "boolean", "1"],
  182. 'Float as strict integer' => [2112.5, "strict integer", "2112"],
  183. 'Float as strict float' => [2112.5, "strict float", "2112.5"],
  184. 'Float as strict string' => [2112.5, "strict string", "'2112.5'"],
  185. 'Float as strict datetime' => [2112.5, "strict datetime", "'1970-01-01 00:35:12'"],
  186. 'Float as strict boolean' => [2112.5, "strict boolean", "1"],
  187. 'Float zero as integer' => [0.0, "integer", "0"],
  188. 'Float zero as float' => [0.0, "float", "0.0"],
  189. 'Float zero as string' => [0.0, "string", "'0'"],
  190. 'Float zero as datetime' => [0.0, "datetime", "'1970-01-01 00:00:00'"],
  191. 'Float zero as boolean' => [0.0, "boolean", "0"],
  192. 'Float zero as strict integer' => [0.0, "strict integer", "0"],
  193. 'Float zero as strict float' => [0.0, "strict float", "0.0"],
  194. 'Float zero as strict string' => [0.0, "strict string", "'0'"],
  195. 'Float zero as strict datetime' => [0.0, "strict datetime", "'1970-01-01 00:00:00'"],
  196. 'Float zero as strict boolean' => [0.0, "strict boolean", "0"],
  197. 'ASCII string as integer' => ["Random string", "integer", "0"],
  198. 'ASCII string as float' => ["Random string", "float", "0.0"],
  199. 'ASCII string as string' => ["Random string", "string", "'Random string'"],
  200. 'ASCII string as datetime' => ["Random string", "datetime", "null"],
  201. 'ASCII string as boolean' => ["Random string", "boolean", "1"],
  202. 'ASCII string as strict integer' => ["Random string", "strict integer", "0"],
  203. 'ASCII string as strict float' => ["Random string", "strict float", "0.0"],
  204. 'ASCII string as strict string' => ["Random string", "strict string", "'Random string'"],
  205. 'ASCII string as strict datetime' => ["Random string", "strict datetime", "'1970-01-01 00:00:00'"],
  206. 'ASCII string as strict boolean' => ["Random string", "strict boolean", "1"],
  207. 'UTF-8 string as integer' => ["\u{e9}", "integer", "0"],
  208. 'UTF-8 string as float' => ["\u{e9}", "float", "0.0"],
  209. 'UTF-8 string as string' => ["\u{e9}", "string", "char(233)"],
  210. 'UTF-8 string as datetime' => ["\u{e9}", "datetime", "null"],
  211. 'UTF-8 string as boolean' => ["\u{e9}", "boolean", "1"],
  212. 'UTF-8 string as strict integer' => ["\u{e9}", "strict integer", "0"],
  213. 'UTF-8 string as strict float' => ["\u{e9}", "strict float", "0.0"],
  214. 'UTF-8 string as strict string' => ["\u{e9}", "strict string", "char(233)"],
  215. 'UTF-8 string as strict datetime' => ["\u{e9}", "strict datetime", "'1970-01-01 00:00:00'"],
  216. 'UTF-8 string as strict boolean' => ["\u{e9}", "strict boolean", "1"],
  217. 'ISO 8601 string as integer' => ["2017-01-09T13:11:17", "integer", "0"],
  218. 'ISO 8601 string as float' => ["2017-01-09T13:11:17", "float", "0.0"],
  219. 'ISO 8601 string as string' => ["2017-01-09T13:11:17", "string", "'2017-01-09T13:11:17'"],
  220. 'ISO 8601 string as datetime' => ["2017-01-09T13:11:17", "datetime", "'2017-01-09 13:11:17'"],
  221. 'ISO 8601 string as boolean' => ["2017-01-09T13:11:17", "boolean", "1"],
  222. 'ISO 8601 string as strict integer' => ["2017-01-09T13:11:17", "strict integer", "0"],
  223. 'ISO 8601 string as strict float' => ["2017-01-09T13:11:17", "strict float", "0.0"],
  224. 'ISO 8601 string as strict string' => ["2017-01-09T13:11:17", "strict string", "'2017-01-09T13:11:17'"],
  225. 'ISO 8601 string as strict datetime' => ["2017-01-09T13:11:17", "strict datetime", "'2017-01-09 13:11:17'"],
  226. 'ISO 8601 string as strict boolean' => ["2017-01-09T13:11:17", "strict boolean", "1"],
  227. 'Arbitrary date string as integer' => ["Today", "integer", "0"],
  228. 'Arbitrary date string as float' => ["Today", "float", "0.0"],
  229. 'Arbitrary date string as string' => ["Today", "string", "'Today'"],
  230. 'Arbitrary date string as datetime' => ["Today", "datetime", "'".date_create("Today", new \DateTimezone("UTC"))->format("Y-m-d H:i:s")."'"],
  231. 'Arbitrary date string as boolean' => ["Today", "boolean", "1"],
  232. 'Arbitrary date string as strict integer' => ["Today", "strict integer", "0"],
  233. 'Arbitrary date string as strict float' => ["Today", "strict float", "0.0"],
  234. 'Arbitrary date string as strict string' => ["Today", "strict string", "'Today'"],
  235. 'Arbitrary date string as strict datetime' => ["Today", "strict datetime", "'".date_create("Today", new \DateTimezone("UTC"))->format("Y-m-d H:i:s")."'"],
  236. 'Arbitrary date string as strict boolean' => ["Today", "strict boolean", "1"],
  237. 'DateTime as integer' => [$dateMutable, "integer", (string) $dateUTC->getTimestamp()],
  238. 'DateTime as float' => [$dateMutable, "float", $dateUTC->getTimestamp().".0"],
  239. 'DateTime as string' => [$dateMutable, "string", "'".$dateUTC->format("Y-m-d H:i:s")."'"],
  240. 'DateTime as datetime' => [$dateMutable, "datetime", "'".$dateUTC->format("Y-m-d H:i:s")."'"],
  241. 'DateTime as boolean' => [$dateMutable, "boolean", "1"],
  242. 'DateTime as strict integer' => [$dateMutable, "strict integer", (string) $dateUTC->getTimestamp()],
  243. 'DateTime as strict float' => [$dateMutable, "strict float", $dateUTC->getTimestamp().".0"],
  244. 'DateTime as strict string' => [$dateMutable, "strict string", "'".$dateUTC->format("Y-m-d H:i:s")."'"],
  245. 'DateTime as strict datetime' => [$dateMutable, "strict datetime", "'".$dateUTC->format("Y-m-d H:i:s")."'"],
  246. 'DateTime as strict boolean' => [$dateMutable, "strict boolean", "1"],
  247. 'DateTimeImmutable as integer' => [$dateImmutable, "integer", (string) $dateUTC->getTimestamp()],
  248. 'DateTimeImmutable as float' => [$dateImmutable, "float", $dateUTC->getTimestamp().".0"],
  249. 'DateTimeImmutable as string' => [$dateImmutable, "string", "'".$dateUTC->format("Y-m-d H:i:s")."'"],
  250. 'DateTimeImmutable as datetime' => [$dateImmutable, "datetime", "'".$dateUTC->format("Y-m-d H:i:s")."'"],
  251. 'DateTimeImmutable as boolean' => [$dateImmutable, "boolean", "1"],
  252. 'DateTimeImmutable as strict integer' => [$dateImmutable, "strict integer", (string) $dateUTC->getTimestamp()],
  253. 'DateTimeImmutable as strict float' => [$dateImmutable, "strict float", $dateUTC->getTimestamp().".0"],
  254. 'DateTimeImmutable as strict string' => [$dateImmutable, "strict string", "'".$dateUTC->format("Y-m-d H:i:s")."'"],
  255. 'DateTimeImmutable as strict datetime' => [$dateImmutable, "strict datetime", "'".$dateUTC->format("Y-m-d H:i:s")."'"],
  256. 'DateTimeImmutable as strict boolean' => [$dateImmutable, "strict boolean", "1"],
  257. ];
  258. foreach ($tests as $index => list($value, $type, $exp)) {
  259. $t = preg_replace("<^strict >", "", $type);
  260. $exp = ($exp=="null") ? $exp : $this->decorateTypeSyntax($exp, $t);
  261. yield $index => [$value, $type, $exp];
  262. }
  263. }
  264. public function provideBinaryBindings() {
  265. $dateMutable = new \DateTime("Noon Today", new \DateTimezone("America/Toronto"));
  266. $dateImmutable = new \DateTimeImmutable("Noon Today", new \DateTimezone("America/Toronto"));
  267. $dateUTC = new \DateTime("@".$dateMutable->getTimestamp(), new \DateTimezone("UTC"));
  268. $tests = [
  269. 'Null as binary' => [null, "binary", "null"],
  270. 'Null as strict binary' => [null, "strict binary", "x''"],
  271. 'True as binary' => [true, "binary", "x'31'"],
  272. 'True as strict binary' => [true, "strict binary", "x'31'"],
  273. 'False as binary' => [false, "binary", "x''"],
  274. 'False as strict binary' => [false, "strict binary", "x''"],
  275. 'Integer as binary' => [2112, "binary", "x'32313132'"],
  276. 'Integer as strict binary' => [2112, "strict binary", "x'32313132'"],
  277. 'Integer zero as binary' => [0, "binary", "x'30'"],
  278. 'Integer zero as strict binary' => [0, "strict binary", "x'30'"],
  279. 'Float as binary' => [2112.5, "binary", "x'323131322e35'"],
  280. 'Float as strict binary' => [2112.5, "strict binary", "x'323131322e35'"],
  281. 'Float zero as binary' => [0.0, "binary", "x'30'"],
  282. 'Float zero as strict binary' => [0.0, "strict binary", "x'30'"],
  283. 'ASCII string as binary' => ["Random string", "binary", "x'52616e646f6d20737472696e67'"],
  284. 'ASCII string as strict binary' => ["Random string", "strict binary", "x'52616e646f6d20737472696e67'"],
  285. 'UTF-8 string as binary' => ["\u{e9}", "binary", "x'c3a9'"],
  286. 'UTF-8 string as strict binary' => ["\u{e9}", "strict binary", "x'c3a9'"],
  287. 'Binary string as integer' => [chr(233).chr(233), "integer", "0"],
  288. 'Binary string as float' => [chr(233).chr(233), "float", "0.0"],
  289. 'Binary string as string' => [chr(233).chr(233), "string", "'".chr(233).chr(233)."'"],
  290. 'Binary string as binary' => [chr(233).chr(233), "binary", "x'e9e9'"],
  291. 'Binary string as datetime' => [chr(233).chr(233), "datetime", "null"],
  292. 'Binary string as boolean' => [chr(233).chr(233), "boolean", "1"],
  293. 'Binary string as strict integer' => [chr(233).chr(233), "strict integer", "0"],
  294. 'Binary string as strict float' => [chr(233).chr(233), "strict float", "0.0"],
  295. 'Binary string as strict string' => [chr(233).chr(233), "strict string", "'".chr(233).chr(233)."'"],
  296. 'Binary string as strict binary' => [chr(233).chr(233), "strict binary", "x'e9e9'"],
  297. 'Binary string as strict datetime' => [chr(233).chr(233), "strict datetime", "'1970-01-01 00:00:00'"],
  298. 'Binary string as strict boolean' => [chr(233).chr(233), "strict boolean", "1"],
  299. 'ISO 8601 string as binary' => ["2017-01-09T13:11:17", "binary", "x'323031372d30312d30395431333a31313a3137'"],
  300. 'ISO 8601 string as strict binary' => ["2017-01-09T13:11:17", "strict binary", "x'323031372d30312d30395431333a31313a3137'"],
  301. 'Arbitrary date string as binary' => ["Today", "binary", "x'546f646179'"],
  302. 'Arbitrary date string as strict binary' => ["Today", "strict binary", "x'546f646179'"],
  303. 'DateTime as binary' => [$dateMutable, "binary", "x'".bin2hex($dateUTC->format("Y-m-d H:i:s"))."'"],
  304. 'DateTime as strict binary' => [$dateMutable, "strict binary", "x'".bin2hex($dateUTC->format("Y-m-d H:i:s"))."'"],
  305. 'DateTimeImmutable as binary' => [$dateImmutable, "binary", "x'".bin2hex($dateUTC->format("Y-m-d H:i:s"))."'"],
  306. 'DateTimeImmutable as strict binary' => [$dateImmutable, "strict binary", "x'".bin2hex($dateUTC->format("Y-m-d H:i:s"))."'"],
  307. ];
  308. foreach ($tests as $index => list($value, $type, $exp)) {
  309. $t = preg_replace("<^strict >", "", $type);
  310. $exp = ($exp=="null") ? $exp : $this->decorateTypeSyntax($exp, $t);
  311. yield $index => [$value, $type, $exp];
  312. }
  313. }
  314. }