master
ブランチ master
LittleBeast / src / Std / Lib / Db / Sqlite / Pager.php
22.6 KB、583行 生 Blame 履歴
1<?php
2/*************************************************************
3# 076 License
4
5Copyright (c) テクニカル諏訪子
6
7Permission is hereby granted to any person obtaining a copy of the software
8Little Beast (the "Software") to use, modify, merge, copy, publish, distribute,
9sublicense, 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
31THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
32INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
33PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
34HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
35CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
36OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
37 */
38namespace Std\Lib\Db\Sqlite;
39
40use Std\Lib\Db\DbException;
41
42/**
43 * SQLiteのファイルのページ(読み書き、ロック、ロールバックジャーナル、フリーリスト)
44 *
45 * ジャーナルはjournal_mode = DELETEと同じ形式(「ファイル名-journal」)なので、途中で止まった書き込みは
46 * このクラスでもsqlite3でも元に戻せる。ロックはflock()なので、PHPのプロセス同士でしか効かない。
47 */
48final class Pager {
49 public const HEADER = "SQLite format 3\0";
50 private const JOURNAL_MAGIC = "\xd9\xd5\x05\xf9\x20\xa1\x63\xd7";
51 private const SECTOR = 512;
52
53 /** 1つのトランザクションでメモリーに持つ変更されたページの上限(超えたらジャーナルに書いてからファイルへ) */
54 public int $spillPages = 2000;
55 public int $pageSize = 4096;
56 public int $usable = 4096;
57 public int $nPages = 0;
58 public bool $readOnly = false;
59 /** ヘッダーの値:freeTrunk、freeCount、cookie、userVersion、counter、autoVacuum、appId */
60 public array $hdr = [];
61 /** 変更の度に増える(ページ・スキーマのキャッシュを捨てる為) */
62 public int $generation = 0;
63
64 /** @var resource|null */
65 private $fh = null;
66 private bool $memory;
67 private array $cache = [];
68 private array $dirty = [];
69 private array $journaled = [];
70 private bool $journalOpen = false;
71 /** @var resource|null */
72 private $jh = null;
73 private int $origPages = 0;
74 private int $lock = 0; // 0、LOCK_SH、LOCK_EX
75 private bool $inTxn = false;
76 private array $snaps = [];
77 /** トランザクションの外でもロックを外さない(BEGINの後) */
78 public bool $hold = false;
79 private bool $readShared = false;
80 private array $txnHdr = [];
81 private string $nonce = '';
82
83 /**
84 * @param string $path ファイル(「:memory:」はメモリー)
85 * @param bool $readOnly 読み込みだけ
86 * @param bool $create 無ければ作る
87 * @param int $busyTimeout ロックを待つミリ秒
88 */
89 public function __construct(public readonly string $path, bool $readOnly = false, bool $create = true, private int $busyTimeout = 5000) {
90 $this->memory = $path === ':memory:' || $path === '';
91 $this->readOnly = $readOnly;
92 $this->hdr = ['freeTrunk' => 0, 'freeCount' => 0, 'cookie' => 0, 'format' => 4, 'userVersion' => 0, 'counter' => 0,
93 'autoVacuum' => 0, 'appId' => 0, 'cacheSize' => 0];
94 if ($this->memory) return;
95
96 if (!is_file($path)) {
97 if ($readOnly || !$create) throw new DbException("データベースのファイルがありません:{$path}", 14, 'HY000');
98 $dir = dirname($path);
99 if (!is_dir($dir)) throw new DbException("ディレクトリがありません:{$dir}", 14, 'HY000');
100 }
101 $fh = @fopen($path, $readOnly ? 'rb' : 'c+b');
102 if ($fh === false && !$readOnly && is_file($path)) {
103 // 書けないファイル
104 $fh = @fopen($path, 'rb');
105 $this->readOnly = true;
106 }
107 if ($fh === false) throw new DbException("データベースを開けません:{$path}", 14, 'HY000');
108 $this->fh = $fh;
109 }
110
111 public function __destruct() {
112 $this->close();
113 }
114
115 public function close(): void {
116 if ($this->inTxn) $this->rollback();
117 $this->unlock();
118 if ($this->fh) @fclose($this->fh);
119 $this->fh = null;
120 }
121
122 public function isMemory(): bool {
123 return $this->memory;
124 }
125
126 public function inTransaction(): bool {
127 return $this->inTxn;
128 }
129
130 ///////////////
131 // ロック
132
133 /**
134 * 読み込みを始める(共有ロック、他の接続の変更を確認する)
135 *
136 * @throws DbException
137 */
138 public function beginRead(): void {
139 if ($this->memory || $this->lock !== 0) return;
140 $this->acquire(LOCK_SH);
141 // 途中で止まった書き込みのジャーナル(ホットジャーナル)
142 if ($this->hasHotJournal()) {
143 $this->unlock();
144 $this->acquire(LOCK_EX);
145 if ($this->hasHotJournal()) {
146 if ($this->readOnly) throw new DbException('データベースを元に戻す必要がありますが、読み込み専用です', 8, 'HY000');
147 $this->playback();
148 }
149 flock($this->fh, LOCK_SH);
150 $this->lock = LOCK_SH;
151 }
152 $this->readHeader();
153 }
154
155 /**
156 * 書き込みを始める(排他ロック)
157 *
158 * @throws DbException
159 */
160 public function beginWrite(): void {
161 if ($this->readOnly) throw new DbException('読み込み専用のデータベースです', 8, 'HY000');
162 if ($this->inTxn) return;
163 if (!$this->memory) {
164 $before = $this->lock !== 0 ? $this->hdr['counter'] : null;
165 // 共有ロックの間に読んだデータが有る(その後で他の接続が変更したら、読んだデータは古い)
166 $hadCache = $before !== null && $this->readShared;
167 if ($this->lock !== LOCK_EX) $this->acquire(LOCK_EX);
168 if ($this->hasHotJournal()) $this->playback();
169 $this->readHeader();
170 if ($hadCache && $before !== $this->hdr['counter']) {
171 // 読んだ後で他の接続が変更した(flockのロックの変更は一度外れる)
172 $this->downgrade();
173 throw new DbException('データベースはロックされています(他の接続が変更しました)', 5, 'HY000');
174 }
175 }
176 $this->inTxn = true;
177 $this->origPages = $this->nPages;
178 $this->txnHdr = $this->hdr;
179 $this->journaled = [];
180 $this->journalOpen = false;
181 }
182
183 /** 読み込みを終える(トランザクションの外なら、ロックを外す) */
184 public function endRead(): void {
185 if (!$this->inTxn && !$this->hold) $this->unlock();
186 }
187
188 private function acquire(int $op): void {
189 $deadline = microtime(true) + $this->busyTimeout / 1000;
190 while (!flock($this->fh, $op | LOCK_NB)) {
191 if (microtime(true) >= $deadline) throw new DbException('データベースはロックされています', 5, 'HY000');
192 usleep(random_int(2000, 20000));
193 }
194 $this->lock = $op;
195 $this->readShared = false;
196 }
197
198 private function downgrade(): void {
199 if ($this->memory) return;
200 flock($this->fh, LOCK_SH);
201 $this->lock = LOCK_SH;
202 }
203
204 private function unlock(): void {
205 if ($this->fh && $this->lock !== 0) flock($this->fh, LOCK_UN);
206 $this->lock = 0;
207 }
208
209 ///////////////
210 // ヘッダー
211
212 /** ファイルのヘッダーを読む(変更されていれば、キャッシュを捨てる) */
213 private function readHeader(): void {
214 fseek($this->fh, 0);
215 $h = (string)fread($this->fh, 100);
216 clearstatcache(true, $this->path);
217 $fileSize = (int)@filesize($this->path);
218 if ($h === '' || $fileSize === 0) {
219 if ($this->nPages !== 0 || $this->hdr['counter'] !== 0) $this->reset();
220 $this->nPages = 0;
221 $this->hdr['counter'] = 0;
222 return;
223 }
224 if (strlen($h) < 100 || substr($h, 0, 16) !== self::HEADER) throw new DbException('SQLiteのデータベースではありません', 26, 'HY000');
225 $ps = unpack('n', $h, 16)[1];
226 $ps = $ps === 1 ? 65536 : $ps;
227 if ($ps < 512 || ($ps & ($ps - 1))) throw new DbException("不正なページの大きさ:{$ps}", 11, 'HY000');
228 $reserved = ord($h[20]);
229 $counter = unpack('N', $h, 24)[1];
230 if ($counter !== $this->hdr['counter'] || $ps !== $this->pageSize) $this->reset();
231 $this->pageSize = $ps;
232 $this->usable = $ps - $reserved;
233 if (ord($h[18]) > 2 || ord($h[19]) > 2) throw new DbException('このSQLiteのファイル形式は未対応です', 26, 'HY000');
234 if (ord($h[19]) === 2 && is_file($this->path.'-wal') && filesize($this->path.'-wal') > 0) {
235 throw new DbException('WALモードのデータベースは未対応です(sqlite3で「PRAGMA journal_mode=DELETE」にして下さい)', 26, 'HY000');
236 }
237 $enc = unpack('N', $h, 56)[1];
238 if ($enc !== 1 && $enc !== 0) throw new DbException('UTF-8でないデータベースは未対応です', 26, 'HY000');
239 $n = unpack('N', $h, 28)[1];
240 $valid = unpack('N', $h, 92)[1];
241 // 古いバージョンが書いたファイルは、ページ数をファイルの大きさから
242 if ($n === 0 || $valid !== $counter) $n = intdiv($fileSize, $ps);
243 $this->nPages = $n;
244 $this->hdr = [
245 'counter' => $counter, 'freeTrunk' => unpack('N', $h, 32)[1], 'freeCount' => unpack('N', $h, 36)[1],
246 'cookie' => unpack('N', $h, 40)[1], 'format' => unpack('N', $h, 44)[1], 'cacheSize' => unpack('N', $h, 48)[1],
247 'autoVacuum' => unpack('N', $h, 52)[1], 'userVersion' => unpack('N', $h, 60)[1], 'appId' => unpack('N', $h, 68)[1],
248 ];
249 // 自動バキューム(ポインターマップ)・予約された領域が有るファイルには書かない
250 if ($this->hdr['autoVacuum'] !== 0 || $reserved !== 0) $this->readOnly = true;
251 }
252
253 /** ヘッダー(100バイト) */
254 public function header(): string {
255 $ps = $this->pageSize === 65536 ? 1 : $this->pageSize;
256 return self::HEADER.pack('n', $ps)."\x01\x01".chr($this->pageSize - $this->usable)."\x40\x20\x20"
257 .pack('NNNNNNNNNNNN', $this->hdr['counter'], $this->nPages, $this->hdr['freeTrunk'], $this->hdr['freeCount'],
258 $this->hdr['cookie'], $this->hdr['format'] ?: 4, $this->hdr['cacheSize'], $this->hdr['autoVacuum'], 1,
259 $this->hdr['userVersion'], 0, $this->hdr['appId'])
260 .str_repeat("\0", 20).pack('NN', $this->hdr['counter'], 3045001);
261 }
262
263 /** キャッシュを捨てる */
264 private function reset(): void {
265 $this->cache = [];
266 $this->generation++;
267 }
268
269 ///////////////
270 // ページ
271
272 /**
273 * ページを読む
274 *
275 * @param int $pgno ページ番号(1から)
276 * @return string
277 */
278 public function read(int $pgno): string {
279 if ($this->lock === LOCK_SH) $this->readShared = true;
280 if (isset($this->cache[$pgno])) return $this->cache[$pgno];
281 if ($pgno < 1 || $pgno > $this->nPages) throw new DbException("データベースが壊れています(ページ{$pgno})", 11, 'HY000');
282 if ($this->memory) return str_repeat("\0", $this->pageSize);
283 fseek($this->fh, ($pgno - 1) * $this->pageSize);
284 $data = (string)fread($this->fh, $this->pageSize);
285 if (strlen($data) < $this->pageSize) $data = str_pad($data, $this->pageSize, "\0");
286 if (count($this->cache) > 4000 && empty($this->dirty)) $this->cache = [];
287 return $this->cache[$pgno] = $data;
288 }
289
290 /**
291 * ページを書く(トランザクションの中で)
292 *
293 * @param int $pgno
294 * @param string $data ページの全部(ページ1の最初の100バイトはヘッダーで上書きする)
295 */
296 public function write(int $pgno, string $data): void {
297 if (!$this->inTxn) throw new \LogicException('SQLite:トランザクションの外で書き込み');
298 foreach ($this->snaps as $k => $snap) {
299 if (!array_key_exists($pgno, $snap['pages'])) {
300 $this->snaps[$k]['pages'][$pgno] = $pgno <= $snap['nPages'] ? $this->read($pgno) : null;
301 }
302 }
303 if (!array_key_exists($pgno, $this->dirty)) {
304 // 元のページ(ジャーナル・ロールバック用)、新しいページ・ジャーナルに書いたページはnull
305 $this->dirty[$pgno] = $pgno <= $this->origPages && !isset($this->journaled[$pgno]) ? $this->read($pgno) : null;
306 }
307 $this->cache[$pgno] = $data;
308 if (count($this->dirty) > $this->spillPages && !$this->memory) $this->spill();
309 }
310
311 /** ページ1のヘッダーを今の値にする */
312 private function writeHeader(): void {
313 $p1 = $this->nPages >= 1 ? $this->read(1) : str_repeat("\0", $this->pageSize);
314 $this->write(1, $this->header().substr($p1, 100));
315 }
316
317 /**
318 * 新しいページ(フリーリスト又はファイルの最後)
319 *
320 * @return int ページ番号(中身は呼び出し側が全部書く)
321 */
322 public function allocate(): int {
323 if ($this->hdr['freeCount'] > 0 && $this->hdr['freeTrunk'] > 0) {
324 $trunk = $this->hdr['freeTrunk'];
325 $t = $this->read($trunk);
326 $n = unpack('N', $t, 4)[1];
327 $this->hdr['freeCount']--;
328 if ($n > 0) {
329 $leaf = unpack('N', $t, 8 + ($n - 1) * 4)[1];
330 $this->write($trunk, substr($t, 0, 4).pack('N', $n - 1).substr($t, 8));
331 return $leaf;
332 }
333 $this->hdr['freeTrunk'] = unpack('N', $t)[1];
334 return $trunk;
335 }
336 $this->nPages++;
337 // ロック用のバイトの有るページは使わない
338 if ($this->nPages === intdiv(0x40000000, $this->pageSize) + 1) $this->nPages++;
339 $this->cache[$this->nPages] = str_repeat("\0", $this->pageSize);
340 return $this->nPages;
341 }
342
343 /**
344 * ページをフリーリストに入れる
345 *
346 * @param int $pgno
347 */
348 public function free(int $pgno): void {
349 $trunk = $this->hdr['freeTrunk'];
350 $max = intdiv($this->usable, 4) - 2;
351 if ($trunk > 0) {
352 $t = $this->read($trunk);
353 $n = unpack('N', $t, 4)[1];
354 if ($n < $max - 6) {
355 $t = substr($t, 0, 4).pack('N', $n + 1).substr($t, 8, $n * 4).pack('N', $pgno).substr($t, 12 + $n * 4);
356 $this->write($trunk, $t);
357 $this->hdr['freeCount']++;
358 return;
359 }
360 }
361 // 新しいトランク
362 $this->write($pgno, pack('NN', $trunk, 0).str_repeat("\0", $this->pageSize - 8));
363 $this->hdr['freeTrunk'] = $pgno;
364 $this->hdr['freeCount']++;
365 }
366
367 ///////////////
368 // トランザクション
369
370 /**
371 * スナップショット(文・セーブポイント):失敗したら、その後の変更だけを元に戻す
372 *
373 * @return int 番号(snapPop・snapRestore用)
374 */
375 public function snapPush(): int {
376 $this->snaps[] = ['pages' => [], 'nPages' => $this->nPages, 'hdr' => $this->hdr];
377 return count($this->snaps) - 1;
378 }
379
380 /** スナップショットを捨てる(変更は残す) */
381 public function snapPop(?int $n): void {
382 if ($n === null) return;
383 array_splice($this->snaps, $n);
384 }
385
386 /** スナップショットの時に戻す(スナップショットは残す) */
387 public function snapRestore(?int $n): void {
388 if ($n === null || !isset($this->snaps[$n])) return;
389 $s = $this->snaps[$n];
390 foreach ($s['pages'] as $pgno => $data) {
391 if ($data === null) {
392 unset($this->cache[$pgno]);
393 } else {
394 $this->cache[$pgno] = $data;
395 }
396 }
397 $this->nPages = $s['nPages'];
398 $this->hdr = $s['hdr'];
399 array_splice($this->snaps, $n + 1);
400 $this->snaps[$n]['pages'] = [];
401 }
402
403 /**
404 * コミット
405 *
406 * @throws DbException
407 */
408 public function commit(): void {
409 if (!$this->inTxn) return;
410 $this->snaps = [];
411 if (!empty($this->dirty) || $this->journalOpen) {
412 $this->hdr['counter'] = ($this->hdr['counter'] + 1) & 0xFFFFFFFF;
413 $this->writeHeader();
414 if (!$this->memory) {
415 $this->writeJournal();
416 $this->writePages();
417 if (function_exists('fsync')) @fsync($this->fh);
418 $this->closeJournal(true);
419 }
420 }
421 $this->dirty = [];
422 $this->journaled = [];
423 $this->inTxn = false;
424 $this->downgrade();
425 }
426
427 /** ロールバック */
428 public function rollback(): void {
429 if (!$this->inTxn) return;
430 $this->snaps = [];
431 if ($this->journalOpen) {
432 // 一部をファイルに書いた:ジャーナルから戻す
433 $this->closeJournal(false);
434 $this->playback();
435 $this->cache = [];
436 } else {
437 foreach ($this->dirty as $pgno => $orig) {
438 if ($orig === null) {
439 unset($this->cache[$pgno]);
440 } else {
441 $this->cache[$pgno] = $orig;
442 }
443 }
444 if ($this->memory) {
445 foreach (array_keys($this->cache) as $pgno) if ($pgno > $this->origPages) unset($this->cache[$pgno]);
446 }
447 }
448 $this->dirty = [];
449 $this->journaled = [];
450 $this->inTxn = false;
451 $this->generation++;
452 $this->nPages = $this->origPages;
453 $this->hdr = $this->txnHdr;
454 if (!$this->memory) {
455 $this->downgrade();
456 $this->readHeader();
457 }
458 }
459
460 /** 変更されたページが多過ぎる:ジャーナルに書いてから、ファイルに書く */
461 private function spill(): void {
462 $this->writeJournal();
463 $this->writePages();
464 }
465
466 /** 元のページをジャーナルに書く */
467 private function writeJournal(): void {
468 if (!$this->journalOpen) {
469 $jh = @fopen($this->path.'-journal', 'w+b');
470 if ($jh === false) throw new DbException('ジャーナルを作れません:'.$this->path.'-journal', 14, 'HY000');
471 $this->jh = $jh;
472 $this->nonce = random_bytes(4);
473 $head = self::JOURNAL_MAGIC.pack('N', 0xFFFFFFFF).$this->nonce.pack('NNN', $this->origPages, self::SECTOR, $this->pageSize);
474 fwrite($jh, str_pad($head, self::SECTOR, "\0"));
475 $this->journalOpen = true;
476 }
477 $init = unpack('N', $this->nonce)[1];
478 $buf = '';
479 foreach ($this->dirty as $pgno => $orig) {
480 if ($pgno > $this->origPages || isset($this->journaled[$pgno])) continue;
481 if ($orig === null) {
482 fseek($this->fh, ($pgno - 1) * $this->pageSize);
483 $orig = str_pad((string)fread($this->fh, $this->pageSize), $this->pageSize, "\0");
484 }
485 $sum = $init;
486 for ($i = $this->pageSize - 200; $i > 0; $i -= 200) $sum += ord($orig[$i]);
487 $buf .= pack('N', $pgno).$orig.pack('N', $sum & 0xFFFFFFFF);
488 $this->journaled[$pgno] = true;
489 if (strlen($buf) > 1048576) {
490 fwrite($this->jh, $buf);
491 $buf = '';
492 }
493 }
494 fwrite($this->jh, $buf);
495 fflush($this->jh);
496 if (function_exists('fsync')) @fsync($this->jh);
497 }
498
499 /** 変更されたページをファイルに書く */
500 private function writePages(): void {
501 $pages = array_keys($this->dirty);
502 sort($pages);
503 foreach ($pages as $pgno) {
504 if ($pgno > $this->nPages) continue;
505 fseek($this->fh, ($pgno - 1) * $this->pageSize);
506 fwrite($this->fh, $this->cache[$pgno]);
507 }
508 fflush($this->fh);
509 clearstatcache(true, $this->path);
510 // 書かなかった新しいページ(すぐにフリーリストに入れた物)が有っても、ファイルの大きさはページ数と同じに
511 if ((int)filesize($this->path) !== $this->nPages * $this->pageSize) ftruncate($this->fh, $this->nPages * $this->pageSize);
512 foreach ($pages as $pgno) $this->dirty[$pgno] = null;
513 // ジャーナルに書いたページは、もう元のページを持たなくても良い
514 foreach ($pages as $pgno) if ($pgno > $this->origPages || isset($this->journaled[$pgno])) unset($this->dirty[$pgno]);
515 }
516
517 private function closeJournal(bool $delete): void {
518 if ($this->jh) @fclose($this->jh);
519 $this->jh = null;
520 $this->journalOpen = false;
521 if ($delete) @unlink($this->path.'-journal');
522 }
523
524 private function hasHotJournal(): bool {
525 $j = $this->path.'-journal';
526 clearstatcache(true, $j);
527 return is_file($j) && filesize($j) > 0;
528 }
529
530 /** ジャーナルの元のページをファイルに戻す(ロールバック、途中で止まった書き込み) */
531 private function playback(): void {
532 $j = $this->path.'-journal';
533 $data = (string)@file_get_contents($j);
534 $pos = 0;
535 $origSize = null;
536 while ($pos + 28 <= strlen($data) && substr($data, $pos, 8) === self::JOURNAL_MAGIC) {
537 $nRec = unpack('N', $data, $pos + 8)[1];
538 $nonce = unpack('N', $data, $pos + 12)[1];
539 $size = unpack('N', $data, $pos + 16)[1];
540 $sector = unpack('N', $data, $pos + 20)[1] ?: self::SECTOR;
541 $ps = unpack('N', $data, $pos + 24)[1];
542 if ($ps < 512 || $ps > 65536) break;
543 $origSize ??= $size;
544 $pos += max($sector, 28);
545 if ($nRec === 0xFFFFFFFF) $nRec = intdiv(strlen($data) - $pos, $ps + 8);
546 for ($i = 0; $i < $nRec && $pos + $ps + 8 <= strlen($data); $i++) {
547 $pgno = unpack('N', $data, $pos)[1];
548 $page = substr($data, $pos + 4, $ps);
549 $sum = $nonce;
550 for ($k = $ps - 200; $k > 0; $k -= 200) $sum += ord($page[$k]);
551 if (($sum & 0xFFFFFFFF) !== unpack('N', $data, $pos + 4 + $ps)[1]) break 2;
552 if ($pgno > 0 && $pgno <= $size) {
553 fseek($this->fh, ($pgno - 1) * $ps);
554 fwrite($this->fh, $page);
555 }
556 $pos += $ps + 8;
557 }
558 // 次のヘッダーはセクターの境界
559 if ($pos % $sector) $pos += $sector - $pos % $sector;
560 }
561 if ($origSize !== null) ftruncate($this->fh, $origSize * ($ps ?? $this->pageSize));
562 fflush($this->fh);
563 if (function_exists('fsync')) @fsync($this->fh);
564 @unlink($j);
565 $this->cache = [];
566 $this->generation++;
567 }
568
569 /**
570 * 空のデータベースを作る(最初の書き込みの時)
571 *
572 * @param int $pageSize
573 */
574 public function initialize(int $pageSize = 4096): void {
575 $this->pageSize = $this->usable = $pageSize;
576 $this->nPages = 1;
577 $this->hdr = ['freeTrunk' => 0, 'freeCount' => 0, 'cookie' => 0, 'format' => 4, 'userVersion' => 0, 'counter' => 0,
578 'autoVacuum' => 0, 'appId' => 0, 'cacheSize' => 0];
579 // ページ1:sqlite_schemaの空の葉
580 $page = $this->header()."\x0D\0\0\0\0".pack('n', $pageSize === 65536 ? 0 : $pageSize)."\0";
581 $this->write(1, str_pad($page, $pageSize, "\0"));
582 }
583}