| 1 | <?php |
| 2 | /************************************************************* |
| 3 | # 076 License |
| 4 | |
| 5 | Copyright (c) テクニカル諏訪子 |
| 6 | |
| 7 | Permission is hereby granted to any person obtaining a copy of the software |
| 8 | Little Beast (the "Software") to use, modify, merge, copy, publish, distribute, |
| 9 | sublicense, and/or sell copies of the Software, subject to the following conditions: |
| 10 | |
| 11 | 1. **Origin Attribution**: |
| 12 | - You must not misrepresent the origin of the Software; you must not claim |
| 13 | you created the original Software. |
| 14 | - If the Software is used in a product, you must either: |
| 15 | a. Provide clear attribution in the product's documentation, user interface, |
| 16 | or other visible areas, **OR** |
| 17 | b. Pay the original developers a fee they specify in writing. |
| 18 | 2. **Usage Restriction**: |
| 19 | - The Software, or any derivative works, dependencies, or libraries |
| 20 | incorporating it, must not be used for censorship or to suppress freedom of |
| 21 | speech, expression, or creativity. Prohibited uses include, but are not |
| 22 | limited to: |
| 23 | - Censorship of so-called "hate speech", visuals, non-mainstream opinions, |
| 24 | ideas, or objective reality. |
| 25 | - Tools or systems designed to restrict access to information or |
| 26 | artistic works. |
| 27 | 3. **Notice Preservation**: |
| 28 | - This license and the above copyright notice must remain intact in all copies |
| 29 | of the source code. |
| 30 | |
| 31 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, |
| 32 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A |
| 33 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT |
| 34 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF |
| 35 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE |
| 36 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
| 37 | */ |
| 38 | namespace Std\Test; |
| 39 | |
| 40 | require_once __DIR__.'/../../../autoload.php'; |
| 41 | |
| 42 | use Std\Lib\Tester; |
| 43 | use Std\Lib\Db; |
| 44 | use Std\Lib\Db\DbException; |
| 45 | use Std\Lib\Db\Sqlite; |
| 46 | use Std\Lib\Db\Sqlite\Blob; |
| 47 | use Std\Lib\Db\Sqlite\Btree; |
| 48 | use Std\Lib\Db\Sqlite\Pager; |
| 49 | use Std\Lib\Db\Sqlite\Parser; |
| 50 | use Std\Lib\Db\Sqlite\Record; |
| 51 | use Std\Lib\Db\Sqlite\Value; |
| 52 | |
| 53 | $test = new Tester([ |
| 54 | 'colorOutput' => true, |
| 55 | 'verboseOutput' => true |
| 56 | ]); |
| 57 | |
| 58 | enum LibSqliteColor: string { |
| 59 | case Red = 'あか'; |
| 60 | } |
| 61 | |
| 62 | $tmp = sys_get_temp_dir().'/lbsqlite_'.bin2hex(random_bytes(6)); |
| 63 | mkdir($tmp, 0700); |
| 64 | register_shutdown_function(function() use ($tmp): void { |
| 65 | foreach (glob($tmp.'/*') ?: [] as $f) @unlink($f); |
| 66 | @rmdir($tmp); |
| 67 | }); |
| 68 | $fixtures = json_decode((string)file_get_contents(__DIR__.'/Helper/sqlite.json'), true); |
| 69 | |
| 70 | $catch = function(callable $fn): ?\Throwable { |
| 71 | try { |
| 72 | $fn(); |
| 73 | } catch (\Throwable $e) { |
| 74 | return $e; |
| 75 | } |
| 76 | return null; |
| 77 | }; |
| 78 | $open = function(string $name, array $o = []) use ($tmp): Sqlite { |
| 79 | $db = new Sqlite($o + ['path' => $tmp.'/'.$name]); |
| 80 | $db->connect(); |
| 81 | return $db; |
| 82 | }; |
| 83 | $rows = fn(Sqlite $db, string $sql) => $db->query($sql)['rows']; |
| 84 | $one = fn(Sqlite $db, string $sql) => array_values($db->query($sql)['rows'][0] ?? [])[0] ?? null; |
| 85 | $run = function(Sqlite $db, string $sql, array $params = []): array { |
| 86 | $id = $db->prepare($sql); |
| 87 | try { |
| 88 | return $db->execute($id, $params); |
| 89 | } finally { |
| 90 | $db->demolish($id); |
| 91 | } |
| 92 | }; |
| 93 | // pdo_sqlite(本物のSQLite)が有れば、互換性も確認する |
| 94 | $pdo = fn(string $file): ?\PDO => class_exists('PDO') && in_array('sqlite', \PDO::getAvailableDrivers(), true) ? new \PDO('sqlite:'.$file) : null; |
| 95 | |
| 96 | |
| 97 | $test->describe('SQLite(レコードと値)', function($test) use ($catch): void { |
| 98 | $test->it('可変長整数を読み書き出来るはず', function($test): void { |
| 99 | foreach ([0, 1, 127, 128, 240, 2287, 16383, 16384, 0x00FFFFFFFFFFFFFF, 0x0100000000000000, PHP_INT_MAX, -1, PHP_INT_MIN] as $v) { |
| 100 | $b = Record::putVarint($v); |
| 101 | $pos = 0; |
| 102 | $test->assertSame($v, Record::getVarint($b, $pos), (string)$v); |
| 103 | $test->assertSame(strlen($b), $pos); |
| 104 | $test->assertSame(strlen($b), Record::varintLen($v), "長さ:{$v}"); |
| 105 | } |
| 106 | $test->assertSame("\x81\x00", Record::putVarint(128)); |
| 107 | $test->assertSame(9, strlen(Record::putVarint(-1))); |
| 108 | }); |
| 109 | |
| 110 | $test->it('レコードの全部の型を読み書き出来るはず', function($test): void { |
| 111 | $vals = [null, 0, 1, -1, 127, -128, 128, 32767, -32769, 8388607, -8388609, 2147483647, -2147483649, 140737488355327, -140737488355329, |
| 112 | PHP_INT_MAX, PHP_INT_MIN, 1.5, -0.0, 1e300, '', '日本語', new Blob(''), new Blob("\x00\xff")]; |
| 113 | $rec = Record::encode($vals); |
| 114 | $back = Record::decode($rec); |
| 115 | $test->assertCount(count($vals), $back); |
| 116 | foreach ($vals as $i => $v) { |
| 117 | if ($v instanceof Blob) { |
| 118 | $test->assertTrue($back[$i] instanceof Blob && $back[$i]->data === $v->data, "BLOB {$i}"); |
| 119 | } else { |
| 120 | $test->assertSame($v, $back[$i], "値{$i}"); |
| 121 | } |
| 122 | } |
| 123 | // 0と1は値の無いシリアル型(8、9) |
| 124 | $test->assertSame("\x03\x08\x09", Record::encode([0, 1])); |
| 125 | // 長いヘッダー |
| 126 | $many = array_fill(0, 200, 'x'); |
| 127 | $test->assertSame($many, Record::decode(Record::encode($many))); |
| 128 | }); |
| 129 | |
| 130 | $test->it('SQLiteの順番で比べるはず', function($test): void { |
| 131 | $sorted = [null, -5, 1, 1.5, 2, 'A', 'B', 'a', new Blob('A')]; |
| 132 | for ($i = 0; $i < count($sorted) - 1; $i++) { |
| 133 | $test->assertSame(-1, Record::compare($sorted[$i], $sorted[$i + 1]), "{$i}"); |
| 134 | } |
| 135 | $test->assertSame(0, Record::compare(1, 1.0)); |
| 136 | $test->assertSame(0, Record::compare('abc', 'ABC', 'NOCASE')); |
| 137 | $test->assertSame(0, Record::compare('a ', 'a', 'RTRIM')); |
| 138 | $test->assertSame(1, Record::compare(PHP_INT_MAX, 9.2e18)); |
| 139 | }); |
| 140 | |
| 141 | $test->it('型の親和性と数値の文字列', function($test): void { |
| 142 | $test->assertSame(Value::INTEGER, Value::affinity('BIGINT UNSIGNED')); |
| 143 | $test->assertSame(Value::TEXT, Value::affinity('VARCHAR(255)')); |
| 144 | $test->assertSame(Value::BLOB, Value::affinity('')); |
| 145 | $test->assertSame(Value::REAL, Value::affinity('DOUBLE PRECISION')); |
| 146 | $test->assertSame(Value::NUMERIC, Value::affinity('DECIMAL(10,2)')); |
| 147 | $test->assertSame(Value::INTEGER, Value::affinity('POINT'), 'POINTは「INT」を含む'); |
| 148 | $test->assertSame(12, Value::apply('12', Value::INTEGER)); |
| 149 | $test->assertSame(3, Value::apply('3.0', Value::NUMERIC)); |
| 150 | $test->assertSame(2.5, Value::apply(' 2.5 ', Value::NUMERIC)); |
| 151 | $test->assertSame('12abc', Value::apply('12abc', Value::INTEGER)); |
| 152 | $test->assertSame('1.0', Value::apply(1.0, Value::TEXT)); |
| 153 | $test->assertSame('1.0e+100', Value::realToText(1e100)); |
| 154 | $test->assertSame('1.0e-05', Value::realToText(1e-5)); |
| 155 | $test->assertSame('0.1', Value::realToText(0.1)); |
| 156 | $test->assertSame('123456789012345.0', Value::realToText(123456789012345.0)); |
| 157 | }); |
| 158 | |
| 159 | $test->it('SQLを構文解析出来るはず', function($test) use ($catch): void { |
| 160 | $p = new Parser(); |
| 161 | $st = $p->parseAll("SELECT a, 'x;y' AS b FROM t WHERE c = ? AND d = :d -- コメント\n; /* 2つ目 */ INSERT INTO t VALUES (1)"); |
| 162 | $test->assertCount(2, $st); |
| 163 | $test->assertSame('select', $st[0]['t']); |
| 164 | $test->assertSame(2, $st[0]['nParam']); |
| 165 | $test->assertSame([':d' => 2], $st[0]['names']); |
| 166 | $test->assertSame('insert', $st[1]['t']); |
| 167 | $e = $catch(fn() => $p->parseAll('SELEC 1')); |
| 168 | $test->assertTrue($e instanceof DbException); |
| 169 | $test->assertStringContains('構文エラー', $e->getMessage()); |
| 170 | $test->assertStringContains('未対応', $catch(fn() => $p->parseAll('CREATE TRIGGER x AFTER INSERT ON t BEGIN SELECT 1; END'))->getMessage()); |
| 171 | $test->assertStringContains('未対応', $catch(fn() => $p->parseAll('SELECT rank() OVER (ORDER BY a) FROM t'))->getMessage()); |
| 172 | }); |
| 173 | }); |
| 174 | |
| 175 | $test->describe('SQLite(テーブルとクエリ)', function($test) use ($open, $rows, $one, $run, $catch, $tmp, $pdo): void { |
| 176 | $test->it('テーブルを作って、データを入れて読めるはず', function($test) use ($open, $rows, $one, $run): void { |
| 177 | $db = $open('a.sqlite'); |
| 178 | $test->assertTrue($db->isConnected()); |
| 179 | $test->assertTrue($db->ping()); |
| 180 | $db->query('CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, age INT DEFAULT 17, score REAL, data BLOB, note TEXT)'); |
| 181 | $res = $db->query("INSERT INTO users (name, age, score) VALUES ('諏訪子', 1000, 9.5), ('神奈子', 999, 8.25)"); |
| 182 | $test->assertSame(2, $res['affectedRows']); |
| 183 | $test->assertSame(2, $res['insertId']); |
| 184 | $res = $run($db, 'INSERT INTO users (name, data, note) VALUES (?, ?, ?)', ['早苗', "\x00\xffバイナリ", str_repeat('長い', 5000)]); |
| 185 | $test->assertSame(3, $res['insertId']); |
| 186 | $test->assertSame(3, $db->lastInsertId()); |
| 187 | |
| 188 | $res = $db->query('SELECT * FROM users ORDER BY id'); |
| 189 | $test->assertSame(['id', 'name', 'age', 'score', 'data', 'note'], array_column($res['fields'], 'name')); |
| 190 | $test->assertSame('INTEGER', $res['fields'][0]['type']); |
| 191 | $test->assertSame('users', $res['fields'][0]['table']); |
| 192 | $test->assertSame(['id' => 1, 'name' => '諏訪子', 'age' => 1000, 'score' => 9.5, 'data' => null, 'note' => null], $res['rows'][0]); |
| 193 | $test->assertSame(17, $res['rows'][2]['age']); |
| 194 | $test->assertSame("\x00\xffバイナリ", $res['rows'][2]['data']); |
| 195 | $test->assertSame(30000, strlen($res['rows'][2]['note'])); |
| 196 | $test->assertSame('blob', $one($db, 'SELECT typeof(data) FROM users WHERE id = 3')); |
| 197 | $test->assertSame(['神奈子'], array_column($run($db, 'SELECT name FROM users WHERE age = ?', [999])['rows'], 'name')); |
| 198 | $test->assertSame([['name' => '諏訪子']], $run($db, 'SELECT name FROM users WHERE id = :id', ['id' => 1])['rows']); |
| 199 | $test->assertSame([['name' => '諏訪子']], $run($db, 'SELECT name FROM users WHERE id = ?1 OR id = ?1', [1])['rows']); |
| 200 | |
| 201 | $res = $db->query('UPDATE users SET age = age + 1 WHERE age > 100'); |
| 202 | $test->assertSame(2, $res['affectedRows']); |
| 203 | $test->assertSame(0, $res['insertId']); |
| 204 | $test->assertSame(1, $db->query('DELETE FROM users WHERE id = 2')['affectedRows']); |
| 205 | $test->assertSame(2, $one($db, 'SELECT count(*) FROM users')); |
| 206 | // AUTOINCREMENTは消した番号を使わない |
| 207 | $db->query('DELETE FROM users WHERE id = 3'); |
| 208 | $test->assertSame(4, $db->query("INSERT INTO users (name) VALUES ('霊夢')")['insertId']); |
| 209 | $test->assertSame(4, $one($db, "SELECT seq FROM sqlite_sequence WHERE name = 'users'")); |
| 210 | }); |
| 211 | |
| 212 | $test->it('PHPの色々な型をパラメータとして送れるはず', function($test) use ($open, $run, $catch): void { |
| 213 | $db = $open('a.sqlite'); |
| 214 | $cases = [ |
| 215 | [42, 42, 'integer'], [-1.5, -1.5, 'real'], [true, 1, 'integer'], [false, 0, 'integer'], [null, null, 'null'], ['', '', 'text'], |
| 216 | ['諏訪子', '諏訪子', 'text'], ["\xff\xfe", "\xff\xfe", 'blob'], [new Blob('abc'), 'abc', 'blob'], |
| 217 | [new \DateTimeImmutable('2026-11-12 10:20:30'), '2026-11-12 10:20:30', 'text'], |
| 218 | [new \DateTimeImmutable('2026-11-12 10:20:30.5'), '2026-11-12 10:20:30.500', 'text'], [LibSqliteColor::Red, 'あか', 'text'], |
| 219 | ]; |
| 220 | foreach ($cases as [$in, $out, $type]) { |
| 221 | $r = $run($db, 'SELECT ? AS v, typeof(?) AS t', [$in, $in])['rows'][0]; |
| 222 | $test->assertSame($out, $r['v'], var_export($in, true)); |
| 223 | $test->assertSame($type, $r['t'], var_export($in, true)); |
| 224 | } |
| 225 | $e = $catch(fn() => $run($db, 'SELECT ?', [[1]])); |
| 226 | $test->assertStringContains('パラメータ0の型は未対応です', $e->getMessage()); |
| 227 | $e = $catch(fn() => $run($db, 'SELECT ?, ?', [1])); |
| 228 | $test->assertStringContains('パラメータ数が一致しません', $e->getMessage()); |
| 229 | $e = $catch(fn() => $run($db, 'SELECT :a', ['b' => 1])); |
| 230 | $test->assertStringContains('パラメータがありません:b', $e->getMessage()); |
| 231 | // query()のパラメータはNULL |
| 232 | $test->assertSame([['?' => null]], $db->query('SELECT ?')['rows']); |
| 233 | }); |
| 234 | |
| 235 | $test->it('型の親和性で値を変換するはず', function($test) use ($open, $rows): void { |
| 236 | $db = $open('b.sqlite'); |
| 237 | $db->query('CREATE TABLE t (i INTEGER, r REAL, t TEXT, n NUMERIC, b BLOB, x)'); |
| 238 | $db->query("INSERT INTO t VALUES ('12', '1', 3, '4.0', '5', '6')"); |
| 239 | $db->query("INSERT INTO t VALUES ('a', 'b', 1.5, '1e2', x'00', 7.0)"); |
| 240 | $test->assertSame([ |
| 241 | ['i' => 'integer', 'r' => 'real', 't' => 'text', 'n' => 'integer', 'b' => 'text', 'x' => 'text'], |
| 242 | ['i' => 'text', 'r' => 'text', 't' => 'text', 'n' => 'integer', 'b' => 'blob', 'x' => 'real'], |
| 243 | ], $rows($db, 'SELECT typeof(i) AS i, typeof(r) AS r, typeof(t) AS t, typeof(n) AS n, typeof(b) AS b, typeof(x) AS x FROM t')); |
| 244 | $test->assertSame([['t' => '3', 'n' => 4], ['t' => '1.5', 'n' => 100]], $rows($db, 'SELECT t, n FROM t')); |
| 245 | // 比較の親和性 |
| 246 | $test->assertSame(1, count($rows($db, "SELECT * FROM t WHERE i = '12'"))); |
| 247 | $test->assertSame(1, count($rows($db, 'SELECT * FROM t WHERE t = 3'))); |
| 248 | $test->assertSame([['a' => 1, 'b' => 0, 'c' => 1]], $rows($db, "SELECT 10 = 10.0 AS a, '10' = 10 AS b, 'b' > 5 AS c")); |
| 249 | }); |
| 250 | |
| 251 | $test->it('式と関数', function($test) use ($open, $rows): void { |
| 252 | $db = $open('b.sqlite'); |
| 253 | $r = $rows($db, "SELECT 7 / 2 AS a, 7.0 / 2 AS b, 7 % 3 AS c, 1 / 0 AS d, 'a' || 1 || 2.5 AS e, -(-9223372036854775807 - 1) AS f, |
| 254 | abs(-3) AS g, round(2.567, 2) AS h, length('日本語') AS i, substr('日本語テキスト', 2, 3) AS j, upper('abc') AS k, |
| 255 | coalesce(NULL, 'x') AS l, nullif(1, 1) AS m, instr('hello', 'll') AS n, replace('aaa', 'a', 'b') AS o, trim(' x ') AS p, |
| 256 | hex('AB') AS q, quote('it''s') AS r, typeof(1.0) AS s, CAST('12abc' AS INTEGER) AS t, CAST(12 AS TEXT) AS u, |
| 257 | printf('%05.1f|%s', 3.14159, 'x') AS v, iif(1, 'y', 'n') AS w, 'abc' LIKE 'A_C' AS x, 'abc' GLOB 'A*' AS y, max(1, 5, 3) AS z")[0]; |
| 258 | $test->assertSame(['a' => 3, 'b' => 3.5, 'c' => 1, 'd' => null, 'e' => 'a12.5', 'f' => 9.2233720368547758e18, 'g' => 3, 'h' => 2.57, |
| 259 | 'i' => 3, 'j' => '本語テ', 'k' => 'ABC', 'l' => 'x', 'm' => null, 'n' => 3, 'o' => 'bbb', 'p' => 'x', 'q' => '4142', 'r' => "'it''s'", |
| 260 | 's' => 'real', 't' => 12, 'u' => '12', 'v' => '003.1|x', 'w' => 'y', 'x' => 1, 'y' => 0, 'z' => 5], $r); |
| 261 | $r = $rows($db, "SELECT 1 IN (1, 2) AS a, NULL IN (1) AS b, 3 IN (NULL, 1) AS c, 2 BETWEEN 1 AND 3 AS d, |
| 262 | CASE 2 WHEN 1 THEN 'one' WHEN 2 THEN 'two' END AS e, NULL IS NULL AS f, 1 IS NOT 2 AS g")[0]; |
| 263 | $test->assertSame(['a' => 1, 'b' => null, 'c' => null, 'd' => 1, 'e' => 'two', 'f' => 1, 'g' => 1], $r); |
| 264 | $r = $rows($db, "SELECT datetime('2026-01-31 03:04:05', '+1 month') AS a, date('2026-11-12', 'start of month', '-1 day') AS b, |
| 265 | strftime('%Y/%m/%d %H:%M', '2026-11-12 10:20:30') AS c, unixepoch('2026-01-01') AS d, date(1767225600, 'unixepoch') AS e, |
| 266 | julianday('2000-01-01 12:00:00') AS f")[0]; |
| 267 | $test->assertSame(['a' => '2026-03-03 03:04:05', 'b' => '2026-10-31', 'c' => '2026/11/12 10:20', 'd' => 1767225600, 'e' => '2026-01-01', 'f' => 2451545.0], $r); |
| 268 | $r = $rows($db, "SELECT json_extract('{\"a\": [1, {\"b\": \"x\"}]}', '$.a[1].b') AS a, '{\"a\": {\"b\": 2}}' ->> '$.a.b' AS b, |
| 269 | json_object('k', 1) AS c, json_array(1, 'a', NULL) AS d")[0]; |
| 270 | $test->assertSame(['a' => 'x', 'b' => 2, 'c' => '{"k":1}', 'd' => '[1,"a",null]'], $r); |
| 271 | }); |
| 272 | |
| 273 | $test->it('結合・集約・サブクエリ・複合', function($test) use ($open, $rows): void { |
| 274 | $db = $open('c.sqlite'); |
| 275 | $db->query('CREATE TABLE emp (id INTEGER PRIMARY KEY, name TEXT, dept TEXT, salary INT, boss INT)'); |
| 276 | $db->query("INSERT INTO emp (name, dept, salary, boss) VALUES ('A', 'eng', 100, NULL), ('B', 'eng', 80, 1), ('C', 'ops', 70, 1), ('D', 'ops', NULL, 3), ('E', NULL, 50, 3)"); |
| 277 | $test->assertSame([['name' => 'A', 'boss' => null], ['name' => 'B', 'boss' => 'A'], ['name' => 'C', 'boss' => 'A'], ['name' => 'D', 'boss' => 'C'], ['name' => 'E', 'boss' => 'C']], |
| 278 | $rows($db, 'SELECT e.name, b.name AS boss FROM emp e LEFT JOIN emp b ON b.id = e.boss ORDER BY e.id')); |
| 279 | $test->assertSame([['dept' => null, 'n' => 1, 's' => 50, 'a' => 50.0], ['dept' => 'eng', 'n' => 2, 's' => 180, 'a' => 90.0], ['dept' => 'ops', 'n' => 2, 's' => 70, 'a' => 70.0]], |
| 280 | $rows($db, 'SELECT dept, count(*) AS n, sum(salary) AS s, avg(salary) AS a FROM emp GROUP BY dept')); |
| 281 | $test->assertSame([['dept' => 'eng', 's' => 180]], $rows($db, 'SELECT dept, sum(salary) AS s FROM emp GROUP BY dept HAVING s > 100')); |
| 282 | $test->assertSame([['name' => 'A', 'm' => 100]], $rows($db, 'SELECT name, max(salary) AS m FROM emp'), 'max()の行の他の列'); |
| 283 | $test->assertSame(['A', 'B'], array_column($rows($db, 'SELECT name FROM emp WHERE salary > (SELECT avg(salary) FROM emp) ORDER BY name'), 'name')); |
| 284 | $test->assertSame(['A', 'C'], array_column($rows($db, 'SELECT name FROM emp e WHERE EXISTS (SELECT 1 FROM emp x WHERE x.boss = e.id) ORDER BY 1'), 'name')); |
| 285 | $test->assertSame([['name' => 'A', 'r' => 2], ['name' => 'C', 'r' => 2]], |
| 286 | $rows($db, 'SELECT name, (SELECT count(*) FROM emp x WHERE x.boss = emp.id) AS r FROM emp WHERE r > 0 ORDER BY name')); |
| 287 | $test->assertSame(['E', 'D', 'C', 'B', 'A'], array_column($rows($db, "SELECT name FROM emp WHERE dept = 'eng' UNION SELECT name FROM emp ORDER BY 1 DESC"), 'name')); |
| 288 | $test->assertSame([['dept' => 'eng'], ['dept' => 'ops']], $rows($db, 'SELECT DISTINCT dept FROM emp WHERE dept IS NOT NULL ORDER BY dept')); |
| 289 | $test->assertSame(['B', 'C'], array_column($rows($db, 'SELECT name FROM emp ORDER BY salary DESC NULLS LAST LIMIT 2 OFFSET 1'), 'name')); |
| 290 | $test->assertSame([['s' => 300]], $rows($db, 'WITH t AS (SELECT salary FROM emp WHERE salary IS NOT NULL) SELECT sum(salary) AS s FROM t')); |
| 291 | $test->assertSame([['column1' => 1, 'column2' => 'a'], ['column1' => 2, 'column2' => 'b']], $rows($db, "VALUES (1, 'a'), (2, 'b')")); |
| 292 | $res = $db->query('SELECT 1 AS a; SELECT 2 AS b'); |
| 293 | $test->assertSame([['a' => 1]], $res['rows']); |
| 294 | $test->assertSame([['b' => 2]], $res['more'][0]['rows']); |
| 295 | $db->query('CREATE VIEW bosses AS SELECT DISTINCT b.name FROM emp e JOIN emp b ON b.id = e.boss'); |
| 296 | $test->assertSame(['A', 'C'], array_column($rows($db, 'SELECT * FROM bosses ORDER BY name'), 'name')); |
| 297 | }); |
| 298 | |
| 299 | $test->it('インデックスを使って検索するはず(結果は同じ)', function($test) use ($open, $rows, $run): void { |
| 300 | $db = $open('d.sqlite'); |
| 301 | $db->query('CREATE TABLE t (id INTEGER PRIMARY KEY, a INT, b TEXT COLLATE NOCASE, c REAL)'); |
| 302 | $db->beginTransaction(); |
| 303 | $ins = $db->prepare('INSERT INTO t (a, b, c) VALUES (?, ?, ?)'); |
| 304 | for ($i = 0; $i < 3000; $i++) $db->execute($ins, [$i % 97, 'name'.($i % 50), $i / 7]); |
| 305 | $db->commit(); |
| 306 | $before = [ |
| 307 | $rows($db, 'SELECT id FROM t WHERE a = 5 ORDER BY id'), $rows($db, "SELECT id FROM t WHERE b = 'NAME7' ORDER BY id"), |
| 308 | $rows($db, 'SELECT id FROM t WHERE a BETWEEN 3 AND 4 AND c > 100 ORDER BY id'), $rows($db, 'SELECT id FROM t WHERE a IN (1, 2) ORDER BY id'), |
| 309 | $rows($db, 'SELECT count(*) AS n FROM t WHERE a > 90'), $rows($db, 'SELECT id FROM t WHERE id > 2990 OR id = 5 ORDER BY id'), |
| 310 | ]; |
| 311 | $db->query('CREATE INDEX t_a ON t(a)'); |
| 312 | $db->query('CREATE INDEX t_b ON t(b)'); |
| 313 | $after = [ |
| 314 | $rows($db, 'SELECT id FROM t WHERE a = 5 ORDER BY id'), $rows($db, "SELECT id FROM t WHERE b = 'NAME7' ORDER BY id"), |
| 315 | $rows($db, 'SELECT id FROM t WHERE a BETWEEN 3 AND 4 AND c > 100 ORDER BY id'), $rows($db, 'SELECT id FROM t WHERE a IN (1, 2) ORDER BY id'), |
| 316 | $rows($db, 'SELECT count(*) AS n FROM t WHERE a > 90'), $rows($db, 'SELECT id FROM t WHERE id > 2990 OR id = 5 ORDER BY id'), |
| 317 | ]; |
| 318 | $test->assertSame($before, $after); |
| 319 | $test->assertCount(31, $after[0]); |
| 320 | $test->assertCount(60, $after[1]); |
| 321 | $test->assertSame([['ok' => 'ok']], $rows($db, 'SELECT integrity_check AS ok FROM (SELECT 1) JOIN (SELECT \'ok\' AS integrity_check)')); |
| 322 | // 速さ:インデックスで1000回 |
| 323 | $q = $db->prepare('SELECT count(*) AS n FROM t WHERE a = ?'); |
| 324 | $start = microtime(true); |
| 325 | for ($i = 0; $i < 1000; $i++) $n = $db->execute($q, [$i % 97])['rows'][0]['n']; |
| 326 | $test->assertTrue(microtime(true) - $start < 10, 'インデックスが遅い'); |
| 327 | $test->assertSame([['integrity_check' => 'ok']], $rows($db, 'PRAGMA integrity_check')); |
| 328 | }); |
| 329 | |
| 330 | $test->it('制約と衝突の解決', function($test) use ($open, $rows, $one, $catch): void { |
| 331 | $db = $open('e.sqlite'); |
| 332 | $db->query('CREATE TABLE u (id INTEGER PRIMARY KEY, n TEXT UNIQUE NOT NULL, c INT CHECK (c > 0) DEFAULT 1)'); |
| 333 | $db->query("INSERT INTO u (n) VALUES ('a')"); |
| 334 | foreach (["INSERT INTO u (n) VALUES ('a')" => 'UNIQUE制約に違反しました:u.n', 'INSERT INTO u (n) VALUES (NULL)' => 'NOT NULL制約に違反しました:u.n', |
| 335 | "INSERT INTO u (n, c) VALUES ('b', 0)" => 'CHECK制約に違反しました', "INSERT INTO u (id, n) VALUES (1, 'z')" => 'UNIQUE制約に違反しました:u.id', |
| 336 | "INSERT INTO u (id, n) VALUES ('x', 'z')" => 'データ型が違います'] as $sql => $msg) { |
| 337 | $e = $catch(fn() => $db->query($sql)); |
| 338 | $test->assertTrue($e instanceof DbException, $sql); |
| 339 | $test->assertStringContains($msg, $e->getMessage()); |
| 340 | } |
| 341 | $e = $catch(fn() => $db->query("INSERT INTO u (n) VALUES ('a')")); |
| 342 | $test->assertSame(19, $e->getCode()); |
| 343 | $test->assertSame('23000', $e->sqlState); |
| 344 | // 文の途中で失敗したら、その文は全部取り消す |
| 345 | $catch(fn() => $db->query("INSERT INTO u (n) VALUES ('b'), ('c'), ('a')")); |
| 346 | $test->assertSame(1, $one($db, 'SELECT count(*) FROM u')); |
| 347 | $db->query("INSERT OR IGNORE INTO u (n) VALUES ('a'), ('b')"); |
| 348 | $test->assertSame(['a', 'b'], array_column($rows($db, 'SELECT n FROM u ORDER BY id'), 'n')); |
| 349 | $db->query("INSERT OR REPLACE INTO u (n, c) VALUES ('a', 5)"); |
| 350 | $test->assertSame([['id' => 2, 'n' => 'b', 'c' => 1], ['id' => 3, 'n' => 'a', 'c' => 5]], $rows($db, 'SELECT * FROM u ORDER BY id')); |
| 351 | $catch(fn() => $db->query("INSERT OR FAIL INTO u (n) VALUES ('x'), ('a'), ('y')")); |
| 352 | $test->assertSame(['a', 'b', 'x'], array_column($rows($db, 'SELECT n FROM u ORDER BY n'), 'n'), 'FAILは前の行を残す'); |
| 353 | // ON CONFLICT、RETURNING |
| 354 | $res = $db->query("INSERT INTO u (n, c) VALUES ('a', 9) ON CONFLICT(n) DO UPDATE SET c = c + excluded.c RETURNING *"); |
| 355 | $test->assertSame([], $res['rows']); |
| 356 | $test->assertSame(14, $one($db, "SELECT c FROM u WHERE n = 'a'")); |
| 357 | $db->query("INSERT INTO u (n) VALUES ('a') ON CONFLICT DO NOTHING"); |
| 358 | $test->assertSame([['id' => 5, 'n' => 'z']], $db->query("INSERT INTO u (n) VALUES ('z') RETURNING id, n")['rows']); |
| 359 | $test->assertSame([['n' => 'z']], $db->query("DELETE FROM u WHERE id = 5 RETURNING n")['rows']); |
| 360 | $test->assertSame([['n' => 'b', 'c' => 2]], $db->query("UPDATE u SET c = 2 WHERE n = 'b' RETURNING n, c")['rows']); |
| 361 | $e = $catch(fn() => $db->query("UPDATE u SET n = 'a' WHERE n = 'b'")); |
| 362 | $test->assertStringContains('UNIQUE制約', $e->getMessage()); |
| 363 | $test->assertSame([['integrity_check' => 'ok']], $rows($db, 'PRAGMA integrity_check')); |
| 364 | }); |
| 365 | |
| 366 | $test->it('トランザクションとセーブポイント', function($test) use ($open, $rows, $one, $catch, $tmp): void { |
| 367 | $db = $open('f.sqlite'); |
| 368 | $db->query('CREATE TABLE t (v TEXT UNIQUE)'); |
| 369 | $test->assertFalse($db->inTransaction()); |
| 370 | $db->beginTransaction(); |
| 371 | $test->assertTrue($db->inTransaction()); |
| 372 | $db->query("INSERT INTO t VALUES ('a')"); |
| 373 | $catch(fn() => $db->query("INSERT INTO t VALUES ('a')")); |
| 374 | $test->assertTrue($db->inTransaction(), 'エラーの後もトランザクションの中'); |
| 375 | $db->query("INSERT INTO t VALUES ('b')"); |
| 376 | $db->rollback(); |
| 377 | $test->assertFalse($db->inTransaction()); |
| 378 | $test->assertSame(0, $one($db, 'SELECT count(*) FROM t')); |
| 379 | |
| 380 | $db->beginTransaction(); |
| 381 | $db->query("INSERT INTO t VALUES ('a')"); |
| 382 | $db->query('SAVEPOINT s1'); |
| 383 | $db->query("INSERT INTO t VALUES ('b')"); |
| 384 | $db->query('SAVEPOINT s2'); |
| 385 | $db->query("INSERT INTO t VALUES ('c')"); |
| 386 | $db->query('ROLLBACK TO s2'); |
| 387 | $db->query("INSERT INTO t VALUES ('d')"); |
| 388 | $db->query('RELEASE s1'); |
| 389 | $db->commit(); |
| 390 | $test->assertSame(['a', 'b', 'd'], array_column($rows($db, 'SELECT v FROM t ORDER BY v'), 'v')); |
| 391 | $e = $catch(fn() => $db->query('COMMIT')); |
| 392 | $test->assertStringContains('トランザクションが開始されていません', $e->getMessage()); |
| 393 | // 他の接続から見える |
| 394 | $db2 = $open('f.sqlite'); |
| 395 | $test->assertSame(3, $one($db2, 'SELECT count(*) FROM t')); |
| 396 | |
| 397 | $tx = new Db(['driver' => 'sqlite', 'path' => $tmp.'/f.sqlite']); |
| 398 | $tx->transaction(fn(Db $d) => $d->query('INSERT INTO t VALUES (?)', ['e'])); |
| 399 | $e = $catch(fn() => $tx->transaction(function(Db $d): void { |
| 400 | $d->query('INSERT INTO t VALUES (?)', ['f']); |
| 401 | throw new \RuntimeException('やめる'); |
| 402 | })); |
| 403 | $test->assertSame('やめる', $e->getMessage()); |
| 404 | $test->assertSame(['a', 'b', 'd', 'e'], array_column($rows($db, 'SELECT v FROM t ORDER BY v'), 'v')); |
| 405 | }); |
| 406 | |
| 407 | $test->it('同じファイルの2つの接続(ロック)', function($test) use ($open, $one, $catch): void { |
| 408 | $a = $open('g.sqlite'); |
| 409 | $a->query('CREATE TABLE c (n INT)'); |
| 410 | $a->query('INSERT INTO c VALUES (0)'); |
| 411 | $b = $open('g.sqlite', ['busy_timeout' => 100]); |
| 412 | $test->assertSame(0, $one($b, 'SELECT n FROM c')); |
| 413 | $a->query('BEGIN IMMEDIATE'); |
| 414 | $a->query('UPDATE c SET n = 1'); |
| 415 | $e = $catch(fn() => $b->query('UPDATE c SET n = 2')); |
| 416 | $test->assertTrue($e instanceof DbException); |
| 417 | $test->assertStringContains('ロックされています', $e->getMessage()); |
| 418 | $test->assertSame(5, $e->getCode()); |
| 419 | $a->query('COMMIT'); |
| 420 | $test->assertSame(1, $one($b, 'SELECT n FROM c'), '他の接続の変更が見える'); |
| 421 | $b->query('UPDATE c SET n = n + 1'); |
| 422 | $test->assertSame(2, $one($a, 'SELECT n FROM c')); |
| 423 | }); |
| 424 | |
| 425 | $test->it('スキーマを変更出来るはず', function($test) use ($open, $rows, $one, $catch): void { |
| 426 | $db = $open('h.sqlite'); |
| 427 | $db->query('CREATE TABLE p (id INTEGER PRIMARY KEY, name TEXT, n INT)'); |
| 428 | $db->query("INSERT INTO p (name, n) VALUES ('a', 1), ('b', 2)"); |
| 429 | $db->query('CREATE INDEX p_n ON p(n)'); |
| 430 | $db->query("ALTER TABLE p ADD COLUMN extra TEXT DEFAULT 'def'"); |
| 431 | $test->assertSame(['def', 'def'], array_column($rows($db, 'SELECT extra FROM p'), 'extra')); |
| 432 | $db->query('ALTER TABLE p RENAME COLUMN name TO title'); |
| 433 | $test->assertSame(['a', 'b'], array_column($rows($db, 'SELECT title FROM p ORDER BY id'), 'title')); |
| 434 | $db->query('ALTER TABLE p RENAME TO q'); |
| 435 | $test->assertSame('CREATE INDEX p_n ON "q"(n)', $one($db, "SELECT sql FROM sqlite_master WHERE name = 'p_n'")); |
| 436 | $db->query('ALTER TABLE q DROP COLUMN extra'); |
| 437 | $test->assertSame([['id' => 1, 'title' => 'a', 'n' => 1], ['id' => 2, 'title' => 'b', 'n' => 2]], $rows($db, 'SELECT * FROM q ORDER BY id')); |
| 438 | $test->assertStringContains('インデックスの有る列は消せません', $catch(fn() => $db->query('ALTER TABLE q DROP COLUMN n'))->getMessage()); |
| 439 | $db->query('CREATE TABLE r AS SELECT id, title || \'!\' AS t FROM q'); |
| 440 | $test->assertSame('CREATE TABLE r(id INT,t)', $one($db, "SELECT sql FROM sqlite_master WHERE name = 'r'")); |
| 441 | $test->assertSame([['cid' => 0, 'name' => 'id', 'type' => 'INTEGER', 'notnull' => 0, 'dflt_value' => null, 'pk' => 1], |
| 442 | ['cid' => 1, 'name' => 'title', 'type' => 'TEXT', 'notnull' => 0, 'dflt_value' => null, 'pk' => 0], |
| 443 | ['cid' => 2, 'name' => 'n', 'type' => 'INT', 'notnull' => 0, 'dflt_value' => null, 'pk' => 0]], $rows($db, 'PRAGMA table_info(q)')); |
| 444 | $test->assertSame([['seq' => 0, 'name' => 'p_n', 'unique' => 0, 'origin' => 'c', 'partial' => 0]], $rows($db, 'PRAGMA index_list(q)')); |
| 445 | $db->query('DROP INDEX p_n'); |
| 446 | $db->query('DROP TABLE r'); |
| 447 | $db->query('DROP TABLE IF EXISTS nope'); |
| 448 | $test->assertStringContains('テーブルがありません', $catch(fn() => $db->query('DROP TABLE nope'))->getMessage()); |
| 449 | $test->assertStringContains('既に有ります', $catch(fn() => $db->query('CREATE TABLE q (a)'))->getMessage()); |
| 450 | $db->query('CREATE TABLE IF NOT EXISTS q (a)'); |
| 451 | $test->assertSame([['name' => 'q']], $rows($db, "SELECT name FROM sqlite_master WHERE type = 'table'")); |
| 452 | $db->query('PRAGMA user_version = 7'); |
| 453 | $test->assertSame(7, $one($db, 'PRAGMA user_version')); |
| 454 | $test->assertSame([['integrity_check' => 'ok']], $rows($db, 'PRAGMA integrity_check')); |
| 455 | }); |
| 456 | |
| 457 | $test->it('エラーと未対応の機能', function($test) use ($open, $catch, $tmp): void { |
| 458 | $db = $open('i.sqlite'); |
| 459 | $db->query('CREATE TABLE t (a INT)'); |
| 460 | foreach (['SELECT * FROM nope' => 'テーブルがありません:nope', 'SELECT b FROM t' => '列がありません:b', 'SELECT count(*) FROM t WHERE count(*) > 1' => 'WHEREに集約関数', |
| 461 | 'SELECT nofunc(1)' => '関数がありません', 'SELECT 1 UNION SELECT 1, 2' => '列の数が違います', 'CREATE TABLE w (a) WITHOUT ROWID' => 'WITHOUT ROWID', |
| 462 | 'SELECT * FROM t a JOIN t b' => '', 'ATTACH DATABASE \'x\' AS y' => '未対応', 'SELECT a FROM t a, t b' => '曖昧'] as $sql => $msg) { |
| 463 | $e = $catch(fn() => $db->query($sql)); |
| 464 | if ($msg === '') { |
| 465 | $test->assertNull($e, $sql); |
| 466 | continue; |
| 467 | } |
| 468 | $test->assertTrue($e instanceof DbException, $sql); |
| 469 | $test->assertStringContains($msg, $e->getMessage()); |
| 470 | } |
| 471 | $file = $tmp.'/notdb.sqlite'; |
| 472 | file_put_contents($file, str_repeat('x', 4096)); |
| 473 | $e = $catch(fn() => (new Sqlite(['path' => $file]))->connect()); |
| 474 | $test->assertStringContains('SQLiteのデータベースではありません', $e->getMessage()); |
| 475 | $e = $catch(fn() => (new Sqlite(['path' => $tmp.'/none.sqlite', 'create' => false]))->connect()); |
| 476 | $test->assertStringContains('ファイルがありません', $e->getMessage()); |
| 477 | $e = $catch(fn() => (new Sqlite(['path' => $tmp.'/nodir/x.sqlite']))->connect()); |
| 478 | $test->assertStringContains('ディレクトリがありません', $e->getMessage()); |
| 479 | $ro = $open('i.sqlite', ['readonly' => true]); |
| 480 | $test->assertStringContains('読み込み専用', $catch(fn() => $ro->query('INSERT INTO t VALUES (1)'))->getMessage()); |
| 481 | $mem = new Sqlite(['path' => ':memory:']); |
| 482 | $mem->connect(); |
| 483 | $mem->query('CREATE TABLE m (a)'); |
| 484 | $mem->query('BEGIN'); |
| 485 | $mem->query('INSERT INTO m VALUES (1)'); |
| 486 | $mem->query('ROLLBACK'); |
| 487 | $test->assertSame([['c' => 0]], $mem->query('SELECT count(*) AS c FROM m')['rows']); |
| 488 | $test->assertFalse(file_exists(':memory:')); |
| 489 | $closed = new Sqlite(['path' => ':memory:']); |
| 490 | $test->assertStringContains('接続していません', $catch(fn() => $closed->query('SELECT 1'))->getMessage()); |
| 491 | $test->assertFalse($closed->ping()); |
| 492 | }); |
| 493 | }); |
| 494 | |
| 495 | $test->describe('SQLite(ファイル形式の互換性)', function($test) use ($fixtures, $tmp, $rows, $one, $pdo): void { |
| 496 | $test->it('本物のSQLiteが書いたファイルを読めるはず', function($test) use ($fixtures, $tmp, $rows, $one): void { |
| 497 | $file = $tmp.'/real.sqlite'; |
| 498 | file_put_contents($file, base64_decode($fixtures['real'])); |
| 499 | $db = new Sqlite(['path' => $file, 'readonly' => true]); |
| 500 | $db->connect(); |
| 501 | $exp = $fixtures['expected']; |
| 502 | $test->assertSame($exp['items'], $rows($db, 'SELECT id, name, price, qty, hex(data) AS data, length(note) AS note, extra FROM items ORDER BY id')); |
| 503 | $test->assertSame($exp['tags'], $rows($db, 'SELECT count(*) AS c, max(tag) AS m FROM tags')); |
| 504 | $test->assertSame($exp['cheap'], $rows($db, 'SELECT * FROM cheap ORDER BY name')); |
| 505 | $test->assertSame($exp['byPrice'], array_column($rows($db, 'SELECT id FROM items WHERE price > 300 ORDER BY price DESC'), 'id')); |
| 506 | $test->assertSame($exp['seq'], $one($db, 'SELECT seq FROM sqlite_sequence')); |
| 507 | $test->assertSame(1024, $db->getServerInfo()['pageSize']); |
| 508 | $test->assertSame(42, $one($db, 'PRAGMA user_version')); |
| 509 | $test->assertSame([['integrity_check' => 'ok']], $rows($db, 'PRAGMA integrity_check')); |
| 510 | // NOCASEのUNIQUE |
| 511 | $test->assertSame(1, count($rows($db, "SELECT * FROM items WHERE name = '品物1'"))); |
| 512 | }); |
| 513 | |
| 514 | $test->it('本物のSQLiteのファイルに書けるはず', function($test) use ($fixtures, $tmp, $rows, $one, $pdo): void { |
| 515 | $file = $tmp.'/real2.sqlite'; |
| 516 | file_put_contents($file, base64_decode($fixtures['real'])); |
| 517 | $db = new Sqlite(['path' => $file]); |
| 518 | $db->connect(); |
| 519 | $db->beginTransaction(); |
| 520 | for ($i = 0; $i < 200; $i++) $db->query("INSERT INTO items (name, price, note) VALUES ('新しい$i', $i, '".str_repeat('x', $i * 10)."')"); |
| 521 | $db->query('DELETE FROM items WHERE id < 20'); |
| 522 | $db->query("UPDATE items SET qty = 99 WHERE price < 50"); |
| 523 | $db->commit(); |
| 524 | $test->assertSame([['integrity_check' => 'ok']], $rows($db, 'PRAGMA integrity_check')); |
| 525 | $test->assertSame($fixtures['expected']['seq'] + 200, $one($db, 'SELECT seq FROM sqlite_sequence')); |
| 526 | $p = $pdo($file); |
| 527 | if ($p === null) { |
| 528 | $test->skip('本物のSQLiteで確認', 'pdo_sqliteが無い'); |
| 529 | return; |
| 530 | } |
| 531 | $test->assertSame('ok', $p->query('PRAGMA integrity_check')->fetchColumn()); |
| 532 | $test->assertSame($one($db, 'SELECT count(*) FROM items'), (int)$p->query('SELECT count(*) FROM items')->fetchColumn()); |
| 533 | $test->assertSame(md5(json_encode($rows($db, 'SELECT * FROM items ORDER BY id'))), md5(json_encode($p->query('SELECT * FROM items ORDER BY id')->fetchAll(\PDO::FETCH_ASSOC)))); |
| 534 | }); |
| 535 | |
| 536 | $test->it('途中で止まった書き込み(本物のSQLiteのジャーナル)を元に戻すはず', function($test) use ($fixtures, $tmp, $rows): void { |
| 537 | $file = $tmp.'/hot.sqlite'; |
| 538 | file_put_contents($file, base64_decode($fixtures['hot'])); |
| 539 | file_put_contents($file.'-journal', base64_decode($fixtures['hotJournal'])); |
| 540 | $db = new Sqlite(['path' => $file]); |
| 541 | $db->connect(); |
| 542 | $test->assertSame([$fixtures['expected']['hot']], $rows($db, 'SELECT count(*) AS c, group_concat(v) AS g FROM t')); |
| 543 | $test->assertFalse(file_exists($file.'-journal')); |
| 544 | $test->assertSame([['integrity_check' => 'ok']], $rows($db, 'PRAGMA integrity_check')); |
| 545 | }); |
| 546 | |
| 547 | $test->it('自分のジャーナルも元に戻せるはず(途中でファイルに書いた場合)', function($test) use ($tmp, $rows, $one, $pdo): void { |
| 548 | $file = $tmp.'/spill.sqlite'; |
| 549 | $db = new Sqlite(['path' => $file]); |
| 550 | $db->connect(); |
| 551 | $db->query('CREATE TABLE t (id INTEGER PRIMARY KEY, v TEXT)'); |
| 552 | $db->query("INSERT INTO t (v) VALUES ('元')"); |
| 553 | $pg = new Pager($file); |
| 554 | $pg->spillPages = 5; |
| 555 | $bt = new Btree($pg); |
| 556 | $pg->beginRead(); |
| 557 | $pg->beginWrite(); |
| 558 | $root = Record::decode($bt->get(1, 1))[3]; |
| 559 | for ($i = 2; $i < 300; $i++) $bt->insert($root, $i, Record::encode([null, str_repeat('壊', 100)])); |
| 560 | $bt->insert($root, 1, Record::encode([null, '壊れた'])); |
| 561 | $bt->flush(); |
| 562 | $test->assertTrue(file_exists($file.'-journal'), 'ジャーナルに書いた'); |
| 563 | // ここで止まった事にする |
| 564 | copy($file, $tmp.'/spill2.sqlite'); |
| 565 | copy($file.'-journal', $tmp.'/spill2.sqlite-journal'); |
| 566 | $pg->rollback(); |
| 567 | $test->assertSame([['v' => '元']], $rows($db, 'SELECT v FROM t'), 'ロールバック'); |
| 568 | $test->assertFalse(file_exists($file.'-journal')); |
| 569 | $db2 = new Sqlite(['path' => $tmp.'/spill2.sqlite']); |
| 570 | $db2->connect(); |
| 571 | $test->assertSame([['v' => '元']], $rows($db2, 'SELECT v FROM t'), '途中で止まった'); |
| 572 | $test->assertSame([['integrity_check' => 'ok']], $rows($db2, 'PRAGMA integrity_check')); |
| 573 | $pg->close(); |
| 574 | }); |
| 575 | |
| 576 | $test->it('このクラスが書いたファイルを本物のSQLiteが読めるはず', function($test) use ($tmp, $rows, $pdo): void { |
| 577 | $file = $tmp.'/mine.sqlite'; |
| 578 | $db = new Sqlite(['path' => $file]); |
| 579 | $db->connect(); |
| 580 | $db->query('CREATE TABLE a (id INTEGER PRIMARY KEY, n TEXT UNIQUE, x REAL, b BLOB)'); |
| 581 | $db->query('CREATE INDEX a_x ON a(x DESC)'); |
| 582 | $db->beginTransaction(); |
| 583 | $ins = $db->prepare('INSERT INTO a (n, x, b) VALUES (?, ?, ?)'); |
| 584 | for ($i = 0; $i < 1500; $i++) $db->execute($ins, ["名前$i", $i / 3, new Blob(str_repeat(chr($i % 256), $i % 700))]); |
| 585 | $db->commit(); |
| 586 | $db->query('DELETE FROM a WHERE id % 3 = 0'); |
| 587 | $db->query('UPDATE a SET n = n || \'!\' WHERE id % 5 = 0'); |
| 588 | $test->assertSame([['integrity_check' => 'ok']], $rows($db, 'PRAGMA integrity_check')); |
| 589 | $p = $pdo($file); |
| 590 | if ($p === null) { |
| 591 | $test->skip('本物のSQLiteで確認', 'pdo_sqliteが無い'); |
| 592 | return; |
| 593 | } |
| 594 | $test->assertSame('ok', $p->query('PRAGMA integrity_check')->fetchColumn()); |
| 595 | $test->assertSame(1000, (int)$p->query('SELECT count(*) FROM a')->fetchColumn()); |
| 596 | $test->assertSame(md5(json_encode($rows($db, 'SELECT * FROM a ORDER BY id'))), md5(json_encode($p->query('SELECT * FROM a ORDER BY id')->fetchAll(\PDO::FETCH_ASSOC)))); |
| 597 | $test->assertSame($rows($db, 'SELECT count(*) AS c FROM a WHERE x > 200')[0]['c'], (int)$p->query('SELECT count(*) FROM a INDEXED BY a_x WHERE x > 200')->fetchColumn()); |
| 598 | }); |
| 599 | |
| 600 | $test->it('Db経由で使えるはず', function($test) use ($tmp): void { |
| 601 | $test->assertTrue(Db::isSupported('sqlite')); |
| 602 | $test->assertTrue(Db::isSupported('SQLite3')); |
| 603 | $db = new Db(['driver' => 'sqlite', 'path' => $tmp.'/db.sqlite']); |
| 604 | $test->assertTrue($db->driver() instanceof Sqlite); |
| 605 | $db->query('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)'); |
| 606 | $test->assertSame(1, $db->query('INSERT INTO users (name) VALUES (?)', ['諏訪子'])['insertId']); |
| 607 | $test->assertSame([['name' => '諏訪子']], $db->query('SELECT name FROM users WHERE id = ?', [1])['rows']); |
| 608 | $test->assertSame('"a""b"', $db->quoteIdentifier('a"b')); |
| 609 | $db->close(); |
| 610 | $test->assertFalse($db->isConnected()); |
| 611 | }); |
| 612 | }); |
| 613 | |
| 614 | $test->printSummary(); |