SQLiteドライバー

守矢諏訪子 <suwako@076.moe>、2026-11-13 07:29
コミット e1e5ab1cfad63086b5fae176c5736fb9f7bf1924 ・親 f9c2919 ・ファイルを見る

18個のファイル、+8730 -10

変更 README-JP.md
@@ -207,13 +207,13 @@
207207
208208 テンプレートやコードでは、URLは `ass_url('article/a.png')`、ディスク上のファイルは `ROOT.ass_path('article/a.png')` を使います。
209209
210-### データベース(MySQL・MariaDB、PostgreSQL)
211-Little Beast 自体はデータベースを使いませんが、必要な場合(既存のアプリの移植等)は `Std\Lib\Db` で繋げます。ドライバは PHP だけで書かれているので、mysqli・pdo_mysql・pgsql・pdo_pgsql・sockets は不要です。\
212-`config/config.php` で `DB_ENABLED` を `true` にして、`DBINFO` を設定して下さい(`driver` は `mysql`(MariaDB も同じ)、又は PostgreSQL の `pgsql`(ポートは普通5432)。`socket` を指定すると `host`・`port` の代わりに UNIX ソケットを使う、PostgreSQL ではソケットのファイル又は `.s.PGSQL.5432` が有るディレクトリ(例:`/tmp`)。`tls` は `true`、又は `['cafile' => '/etc/ssl/ca.pem']` 等の SSL コンテキストオプションの配列)。
210+### データベース(MySQL・MariaDB、PostgreSQL、SQLite)
211+Little Beast 自体はデータベースを使いませんが、必要な場合(既存のアプリの移植等)は `Std\Lib\Db` で繋げます。ドライバは PHP だけで書かれているので、mysqli・pdo_mysql・pgsql・pdo_pgsql・sqlite3・pdo_sqlite・sockets は不要です。\
212+`config/config.php` で `DB_ENABLED` を `true` にして、`DBINFO` を設定して下さい(`driver` は `mysql`(MariaDB も同じ)、PostgreSQL の `pgsql`(ポートは普通5432)、又は `sqlite`(`path` にデータベースのファイル、相対パスはサイトのルートから、例:`data/site.sqlite`)。`socket` を指定すると `host`・`port` の代わりに UNIX ソケットを使う、PostgreSQL ではソケットのファイル又は `.s.PGSQL.5432` が有るディレクトリ(例:`/tmp`)。`tls` は `true`、又は `['cafile' => '/etc/ssl/ca.pem']` 等の SSL コンテキストオプションの配列)。
213213 ```php
214214 $db = new \Std\Lib\Db();
215215 $rows = $db->query('SELECT * FROM users WHERE id = ?', [1])['rows'];
216-$id = $db->query('INSERT INTO users (name) VALUES (?)', ['諏訪子'])['insertId']; // MySQL
216+$id = $db->query('INSERT INTO users (name) VALUES (?)', ['諏訪子'])['insertId']; // MySQL・SQLite
217217 $id = $db->query('INSERT INTO users (name) VALUES (?) RETURNING id', ['諏訪子'])['rows'][0]['id']; // PostgreSQL
218218 $db->transaction(function(\Std\Lib\Db $db): void {
219219 $db->query('UPDATE users SET points = points - ? WHERE id = ?', [100, 1]);
@@ -224,6 +224,7 @@
224224 * 整数の列は `int`、`FLOAT`・`DOUBLE`(PostgreSQL は `real`・`double precision`)は `float`、`NULL` は `null`、その他(`DECIMAL`・`numeric`、日付、JSON、`BLOB` 等)は文字列で返します。PostgreSQL の `boolean` は `bool`、`bytea` は生のバイト、配列は PHP の配列で返します。
225225 * エラーは `Std\Lib\Db\DbException`(`getCode()` は MySQL のエラーコード、例:重複は1062、`sqlState` は SQLSTATE、例:PostgreSQL の重複は23505)を投げ、`log/db_log.txt` に書きます。
226226 * MySQL の認証:`mysql_native_password`、`caching_sha2_password`(MySQL 8 の既定)と `sha256_password`(TLS でない TCP では、パスワードをサーバーの RSA の鍵で暗号化する)、`mysql_clear_password`(TLS か UNIX ソケットのみ)。MariaDB の `client_ed25519`、圧縮、`LOAD DATA LOCAL INFILE` は未対応です。
227+* SQLite:`Std\Lib\Db\Sqlite` は SQLite 3 のファイルを自分で読み書きするので(ロールバックジャーナル、UTF-8)、`sqlite3` のコマンド等でも開けます。途中で止まった書き込みは、次に開いた時にどちらでも元に戻ります。普通の SQL に対応しています:テーブル・インデックス・ビュー、`ALTER TABLE`(名前の変更、列の追加・削除・名前の変更)、`INSERT`(`OR REPLACE`・`OR IGNORE`、`ON CONFLICT`、`RETURNING`)、`UPDATE … FROM`、結合、`GROUP BY` と集約、サブクエリ、`UNION`・`INTERSECT`・`EXCEPT`、`WITH`、トランザクションとセーブポイント、普通の関数(文字列、数値、日付、JSON)、`PRAGMA`(`table_info`、`index_list`、`user_version`、`integrity_check` 等)。`=`・`IN`・範囲の検索にはインデックスを使います。WAL モード、トリガー、`WITHOUT ROWID`、仮想テーブル(FTS)、ウィンドウ関数、再帰の `WITH`、外部キーの確認、部分・式のインデックス、`ATTACH` は未対応です。ロックは `flock()` なので、サイトの動作中にファイルに書き込むのは PHP のプロセスだけにして下さい。プレースホルダは `?`・`?1`・`:name`。UTF-8 でない文字列は BLOB として保存します(`Std\Lib\Db\Sqlite\Blob` で明示も可)。
227228 * PostgreSQL の認証:SCRAM-SHA-256(TLS ならチャネルバインディング付きの SCRAM-SHA-256-PLUS、オプションの `channel_binding` は `prefer`・`require`・`disable`)、MD5、`password`(`allow_cleartext` を `true` にしない限り、TLS か UNIX ソケットのみ)、`trust`。GSSAPI・SSPI は未対応です。
228229 * PostgreSQL:`?` のプレースホルダは `$1`、`$2`… にします(文字列・識別子・コメントの外のみ。`?` そのもの(jsonb の演算子等)は `??` と書く)。`$1` もそのまま使えます。挿入した ID は無いので、`RETURNING` を使って下さい。トランザクションの中でエラーが起きた後の `COMMIT` は、PostgreSQL がロールバックするので `DbException`(SQLSTATE 25P02)を投げます。`COPY`、`LISTEN` の通知、実行中のクエリの取り消しは未対応です。PostgreSQL 16 で確認しました。
229230 * テストは偽物のサーバーを使います。PostgreSQL のテストを本物のサーバーでも実行するには、`LB_TEST_PGSQL=pgsql://ユーザー:パスワード@ホスト:ポート/データベース` を設定して下さい(TLS は `?cafile=/path/ca.pem` を追加)。名前が `lb_test_` で始まるテーブルを作って消します。
変更 README.md
@@ -205,13 +205,13 @@
205205
206206 In templates and code, use `ass_url('article/a.png')` for the URL and `ROOT.ass_path('article/a.png')` for the file on disk.
207207
208-### Database (MySQL/MariaDB, PostgreSQL)
209-Little Beast itself needs no database, but `Std\Lib\Db` can talk to one when a site has to (for example, when porting an existing application). The drivers are written in pure PHP, so mysqli, pdo_mysql, pgsql, pdo_pgsql and sockets are not needed.\
210-Set `DB_ENABLED` to `true` and fill in `DBINFO` in `config/config.php` (`driver` is `mysql`, which also works with MariaDB, or `pgsql` for PostgreSQL, whose port is usually 5432; `socket` uses a UNIX socket instead of `host` and `port`, for PostgreSQL either the socket file or the directory that has `.s.PGSQL.5432`, such as `/tmp`; `tls` is `true` or an array of SSL context options such as `['cafile' => '/etc/ssl/ca.pem']`).
208+### Database (MySQL/MariaDB, PostgreSQL, SQLite)
209+Little Beast itself needs no database, but `Std\Lib\Db` can talk to one when a site has to (for example, when porting an existing application). The drivers are written in pure PHP, so mysqli, pdo_mysql, pgsql, pdo_pgsql, sqlite3, pdo_sqlite and sockets are not needed.\
210+Set `DB_ENABLED` to `true` and fill in `DBINFO` in `config/config.php` (`driver` is `mysql`, which also works with MariaDB, `pgsql` for PostgreSQL, whose port is usually 5432, or `sqlite` with `path` set to the database file, relative to the site root, for example `data/site.sqlite`; `socket` uses a UNIX socket instead of `host` and `port`, for PostgreSQL either the socket file or the directory that has `.s.PGSQL.5432`, such as `/tmp`; `tls` is `true` or an array of SSL context options such as `['cafile' => '/etc/ssl/ca.pem']`).
211211 ```php
212212 $db = new \Std\Lib\Db();
213213 $rows = $db->query('SELECT * FROM users WHERE id = ?', [1])['rows'];
214-$id = $db->query('INSERT INTO users (name) VALUES (?)', ['諏訪子'])['insertId']; // MySQL
214+$id = $db->query('INSERT INTO users (name) VALUES (?)', ['諏訪子'])['insertId']; // MySQL, SQLite
215215 $id = $db->query('INSERT INTO users (name) VALUES (?) RETURNING id', ['諏訪子'])['rows'][0]['id']; // PostgreSQL
216216 $db->transaction(function(\Std\Lib\Db $db): void {
217217 $db->query('UPDATE users SET points = points - ? WHERE id = ?', [100, 1]);
@@ -222,6 +222,7 @@
222222 * Integer columns come back as `int`, `FLOAT`/`DOUBLE` (PostgreSQL: `real`/`double precision`) as `float`, `NULL` as `null`, and everything else (including `DECIMAL`/`numeric`, dates, JSON and `BLOB`) as strings. PostgreSQL's `boolean` comes back as `bool`, `bytea` as the raw bytes and arrays as PHP arrays.
223223 * Errors throw `Std\Lib\Db\DbException` (`getCode()` is MySQL's error code, for example 1062 for a duplicate key, and `sqlState` the SQLSTATE, for example 23505 for a duplicate key in PostgreSQL) and are written to `log/db_log.txt`.
224224 * MySQL authentication: `mysql_native_password`, `caching_sha2_password` (the MySQL 8 default) and `sha256_password` (over plain TCP the password is encrypted with the server's RSA key), and `mysql_clear_password` (TLS or UNIX socket only). MariaDB's `client_ed25519`, compression and `LOAD DATA LOCAL INFILE` are not supported.
225+* SQLite: `Std\Lib\Db\Sqlite` reads and writes SQLite 3 files itself (rollback journal, UTF-8), so they can also be opened with the `sqlite3` tool, and a write that was interrupted is rolled back the next time the file is opened, by either. It supports the common SQL: tables, indexes and views, `ALTER TABLE` (rename, add, drop and rename columns), `INSERT` with `OR REPLACE`/`OR IGNORE`, `ON CONFLICT` and `RETURNING`, `UPDATE … FROM`, joins, `GROUP BY` and aggregates, subqueries, `UNION`/`INTERSECT`/`EXCEPT`, `WITH`, transactions and savepoints, the usual functions (text, math, date and JSON), and `PRAGMA` such as `table_info`, `index_list`, `user_version` and `integrity_check`. Indexes are used for `=`, `IN` and ranges. WAL mode, triggers, `WITHOUT ROWID`, virtual tables (FTS), window functions, recursive `WITH`, foreign key enforcement, partial and expression indexes and `ATTACH` are not supported. Locking uses `flock()`, so only let PHP processes write to the file while the site is running. `?`, `?1` and `:name` placeholders work; strings that are not valid UTF-8 are stored as BLOBs (or pass a `Std\Lib\Db\Sqlite\Blob`).
225226 * PostgreSQL authentication: SCRAM-SHA-256 (over TLS with channel binding, SCRAM-SHA-256-PLUS; the option `channel_binding` is `prefer`, `require` or `disable`), MD5, `password` (TLS or UNIX socket only, unless `allow_cleartext` is `true`) and `trust`. GSSAPI and SSPI are not supported.
226227 * PostgreSQL: `?` placeholders become `$1`, `$2`… (outside strings, identifiers and comments; write `??` for a literal `?`, such as the jsonb operator), and `$1` can be used directly too. There is no insert ID, so use `RETURNING`. A `COMMIT` after an error in the transaction throws `DbException` (SQLSTATE 25P02), because PostgreSQL rolls the transaction back. `COPY`, `LISTEN` notifications and cancelling a running query are not supported. Tested with PostgreSQL 16.
227228 * The tests use a mock server. To run the PostgreSQL tests against a real server as well, set `LB_TEST_PGSQL=pgsql://user:password@host:port/database` (add `?cafile=/path/ca.pem` for TLS); they create and drop tables whose names start with `lb_test_`.
@@ -76,11 +76,12 @@
7676 'pubkey' => ROOT.'/public/static/pub.pem',
7777 'privkey' => ROOT.'/data/priv.pem',
7878 ]);
79-// データベース(DB_ENABLEDが必要)。driver:mysql(MariaDBも)、pgsql(PostgreSQL、portは普通5432、charsetはUTF8)
79+// データベース(DB_ENABLEDが必要)。driver:mysql(MariaDBも)、pgsql(PostgreSQL、portは普通5432、charsetはUTF8)、sqlite
8080 // socket:UNIXソケットのパス(指定するとhost・portの代わりに使う、例:/var/run/mysql/mysql.sock、PostgreSQLはディレクトリも可:/tmp)
8181 // tls:trueで証明書を確認するTLS、又はsslのコンテキストオプションの配列(例:['cafile' => '/etc/ssl/ca.pem'])
8282 // debug:送受信したパケットを出力する
8383 // PostgreSQLのみ:channel_binding(prefer、require、disable)、allow_cleartext(TLSでなくてもパスワードをそのまま送る)
84+// SQLiteのみ:path(ファイル、相対パスはROOTから、例:data/site.sqlite)、readonly、busy_timeout(ロックを待つミリ秒)
8485 define('DBINFO', [
8586 'driver' => 'mysql',
8687 'host' => '127.0.0.1',
@@ -40,6 +40,7 @@
4040 use Std\Lib\Db\DbInterface;
4141 use Std\Lib\Db\Mysql;
4242 use Std\Lib\Db\Pgsql;
43+use Std\Lib\Db\Sqlite;
4344
4445 /**
4546 * データベース
@@ -49,7 +50,7 @@
4950 * 使い方:
5051 * $db = new Db();
5152 * $users = $db->query('SELECT * FROM users WHERE role > ?', [0])['rows'];
52- * $id = $db->query('INSERT INTO users (name) VALUES (?)', ['諏訪子'])['insertId']; // MySQL(PostgreSQLはRETURNINGを使う)
53+ * $id = $db->query('INSERT INTO users (name) VALUES (?)', ['諏訪子'])['insertId']; // MySQL・SQLite(PostgreSQLはRETURNINGを使う)
5354 * $db->transaction(function(Db $db): void {
5455 * $db->query('UPDATE users SET points = points - ? WHERE id = ?', [100, 1]);
5556 * $db->query('UPDATE users SET points = points + ? WHERE id = ?', [100, 2]);
@@ -62,6 +63,8 @@
6263 'pgsql' => Pgsql::class,
6364 'postgres' => Pgsql::class,
6465 'postgresql' => Pgsql::class,
66+ 'sqlite' => Sqlite::class,
67+ 'sqlite3' => Sqlite::class,
6568 ];
6669
6770 public string $driverName;
@@ -0,0 +1,327 @@
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\Lib\Db;
39+
40+use Std\Lib\Db\Sqlite\Blob;
41+use Std\Lib\Db\Sqlite\Engine;
42+use Std\Lib\Db\Sqlite\Pager;
43+
44+/**
45+ * PHP純正のSQLiteドライバ(sqlite3・pdo_sqliteのモジュールが不要)
46+ *
47+ * SQLite 3のファイル形式をそのまま読み書きするので、sqlite3のコマンド等でも開けます(ロールバックジャーナル、UTF-8)。
48+ * 対応:CREATE/DROP TABLE・INDEX・VIEW、ALTER TABLE(RENAME、ADD・DROP・RENAME COLUMN)、INSERT(OR REPLACE、
49+ * ON CONFLICT、RETURNING)、UPDATE(FROM)、DELETE、SELECT(JOIN、GROUP BY、集約、サブクエリ、UNION等、WITH)、
50+ * トランザクション、SAVEPOINT、PRAGMA(table_info、index_list、integrity_check、user_version等)
51+ * 未対応:WALモード、トリガー、WITHOUT ROWID、仮想テーブル(FTS)、ウィンドウ関数、再帰のWITH、外部キーの確認、
52+ * 部分・式のインデックス、ATTACH
53+ *
54+ * ロックはflock()なので、同時に書き込むのはPHPのプロセス同士だけにして下さい。
55+ * プレースホルダは「?」、「?1」、「:name」。パラメータの文字列は、UTF-8でなければBLOBになる(Sqlite\Blobで明示も可)。
56+ *
57+ * $db = new Sqlite(['path' => ROOT.'/data/site.sqlite']);
58+ * $db->connect();
59+ * $id = $db->query("INSERT INTO users (name) VALUES ('諏訪子')")['insertId'];
60+ */
61+class Sqlite implements DbInterface {
62+ public string $path;
63+ private bool $readOnly;
64+ private bool $create;
65+ private int $busyTimeout;
66+ private bool $debug;
67+ private ?Pager $pager = null;
68+ private ?Engine $engine = null;
69+ private array $prepared = [];
70+ private int $nextStmt = 1;
71+
72+ /**
73+ * コンストラクタ
74+ *
75+ * @param array|null $info path(又はdbname。相対パスはROOTから。「:memory:」はメモリー)、readonly、create(無ければ作る、既定true)、
76+ * busy_timeout(ロックを待つミリ秒、5000)、debug
77+ * @throws \Exception $infoを省略し、DB_ENABLEDが無効な場合
78+ */
79+ public function __construct(?array $info = null) {
80+ if ($info === null && !(defined('DB_ENABLED') && DB_ENABLED)) {
81+ throw new \Exception('データベースは無効です。');
82+ }
83+ $info ??= DBINFO;
84+ $path = (string)($info['path'] ?? $info['dbname'] ?? ':memory:');
85+ if ($path === '') $path = ':memory:';
86+ if ($path !== ':memory:' && !preg_match('#^([A-Za-z]:)?[/\\\\]#', $path) && defined('ROOT')) $path = ROOT.'/'.$path;
87+ $this->path = $path;
88+ $this->readOnly = (bool)($info['readonly'] ?? false);
89+ $this->create = (bool)($info['create'] ?? true);
90+ $this->busyTimeout = (int)($info['busy_timeout'] ?? $info['timeout'] ?? 5000);
91+ if (isset($info['timeout']) && !isset($info['busy_timeout'])) $this->busyTimeout = (int)$info['timeout'] * 1000;
92+ $this->debug = (bool)($info['debug'] ?? false);
93+ }
94+
95+ public function __destruct() {
96+ $this->close();
97+ }
98+
99+ public function setDebug(bool $debug): Sqlite {
100+ $this->debug = $debug;
101+ return $this;
102+ }
103+
104+ /**
105+ * ファイルを開く
106+ *
107+ * @return bool
108+ * @throws DbException
109+ */
110+ public function connect(): bool {
111+ if ($this->engine !== null) return true;
112+ try {
113+ $this->pager = new Pager($this->path, $this->readOnly, $this->create, $this->busyTimeout);
114+ $this->engine = new Engine($this->pager);
115+ // ファイルの確認(壊れたファイル・SQLiteでないファイル)
116+ $this->pager->beginRead();
117+ $this->engine->schema();
118+ $this->pager->endRead();
119+ } catch (DbException $e) {
120+ $this->pager?->close();
121+ $this->pager = null;
122+ $this->engine = null;
123+ $this->log($e->getMessage());
124+ throw $e;
125+ }
126+ return true;
127+ }
128+
129+ public function close(): void {
130+ $this->pager?->close();
131+ $this->pager = null;
132+ $this->engine = null;
133+ $this->prepared = [];
134+ }
135+
136+ public function isConnected(): bool {
137+ return $this->engine !== null;
138+ }
139+
140+ public function ping(): bool {
141+ return $this->engine !== null && ($this->path === ':memory:' || is_file($this->path));
142+ }
143+
144+ /**
145+ * SQLを実行する(セミコロンで区切った複数の文も可)
146+ *
147+ * @return array fields、rows、affectedRows、insertId、warningCount(、more)
148+ * @throws DbException
149+ */
150+ public function query(string $query): array {
151+ $this->requireConnection();
152+ try {
153+ $stmts = $this->engine->prepare($query);
154+ } catch (DbException $e) {
155+ $this->fail('クエリに失敗', $e);
156+ }
157+ if (empty($stmts)) return self::emptyResult();
158+ $results = [];
159+ // パラメータはNULL(SQLiteと同じ)
160+ foreach ($stmts as $st) $results[] = $this->runOne($st, [], 'クエリに失敗');
161+ $first = array_shift($results);
162+ if (!empty($results)) $first['more'] = $results;
163+ return $first;
164+ }
165+
166+ /**
167+ * プリペアドステートメント
168+ *
169+ * @return int ステートメントID
170+ * @throws DbException
171+ */
172+ public function prepare(string $query): int {
173+ $this->requireConnection();
174+ try {
175+ $stmts = $this->engine->prepare($query);
176+ } catch (DbException $e) {
177+ $this->fail('準備に失敗', $e);
178+ }
179+ if (empty($stmts)) $this->fail('準備に失敗', new DbException('空のSQL', 1, 'HY000'));
180+ $id = $this->nextStmt++;
181+ $this->prepared[$id] = $stmts;
182+ return $id;
183+ }
184+
185+ /**
186+ * プリペアドステートメントの実行
187+ *
188+ * @param int $statementId
189+ * @param array $params 順番の配列、又は名前(「:name」の「name」)の連想配列
190+ * @return array
191+ * @throws DbException
192+ */
193+ public function execute(int $statementId, array $params = []): array {
194+ $this->requireConnection();
195+ if (!isset($this->prepared[$statementId])) $this->fail('実行に失敗', new DbException("不正なステートメントID: {$statementId}", 1, 'HY000'));
196+ $results = [];
197+ foreach ($this->prepared[$statementId] as $st) {
198+ $results[] = $this->runOne($st, $this->bind($st, $params), '実行に失敗');
199+ }
200+ $first = array_shift($results);
201+ if (!empty($results)) $first['more'] = $results;
202+ return $first;
203+ }
204+
205+ public function demolish(int $statementId): bool {
206+ if (!isset($this->prepared[$statementId])) return false;
207+ unset($this->prepared[$statementId]);
208+ return true;
209+ }
210+
211+ public function beginTransaction(): bool {
212+ $this->query('BEGIN');
213+ return true;
214+ }
215+
216+ public function commit(): bool {
217+ $this->query('COMMIT');
218+ return true;
219+ }
220+
221+ public function rollback(): bool {
222+ $this->query('ROLLBACK');
223+ return true;
224+ }
225+
226+ public function inTransaction(): bool {
227+ return $this->engine !== null && $this->engine->inTransaction();
228+ }
229+
230+ public function quoteIdentifier(string $name): string {
231+ return '"'.str_replace('"', '""', $name).'"';
232+ }
233+
234+ /** 最後に入れた行のrowid */
235+ public function lastInsertId(): int {
236+ return $this->engine?->lastInsertRowid ?? 0;
237+ }
238+
239+ /** 情報:version、path、pageSize、pages、journal */
240+ public function getServerInfo(): array {
241+ return ['version' => '3.45.1', 'path' => $this->path, 'pageSize' => $this->pager?->pageSize, 'pages' => $this->pager?->nPages,
242+ 'readOnly' => $this->pager?->readOnly ?? $this->readOnly];
243+ }
244+
245+ ///////////////
246+
247+ private static function emptyResult(): array {
248+ return ['fields' => [], 'rows' => [], 'affectedRows' => 0, 'insertId' => 0, 'warningCount' => 0];
249+ }
250+
251+ /** パラメータを番号(1から)の配列にする */
252+ private function bind(array $st, array $params): array {
253+ $out = [];
254+ $isList = array_is_list($params);
255+ if ($isList) {
256+ if (count($params) !== $st['nParam']) {
257+ $this->fail('実行に失敗', new DbException("パラメータ数が一致しません: 期待 {$st['nParam']}, 取得 ".count($params), 1, 'HY000'));
258+ }
259+ foreach ($params as $i => $v) $out[$i + 1] = $this->value($v, $i);
260+ return $out;
261+ }
262+ foreach ($params as $k => $v) {
263+ if (is_int($k)) {
264+ $out[$k + 1] = $this->value($v, $k);
265+ continue;
266+ }
267+ $name = in_array($k[0] ?? '', [':', '@', '$'], true) ? $k : null;
268+ $i = null;
269+ foreach ($name !== null ? [$name] : [':'.$k, '@'.$k, '$'.$k] as $n) {
270+ if (isset($st['names'][$n])) $i = $st['names'][$n];
271+ }
272+ if ($i === null) $this->fail('実行に失敗', new DbException("パラメータがありません:{$k}", 1, 'HY000'));
273+ $out[$i] = $this->value($v, $k);
274+ }
275+ return $out;
276+ }
277+
278+ private function value(mixed $v, int|string $i): mixed {
279+ if ($v instanceof \BackedEnum) $v = $v->value;
280+ if ($v instanceof \DateTimeInterface) $v = $v->format('u') === '000000' ? $v->format('Y-m-d H:i:s') : $v->format('Y-m-d H:i:s.v');
281+ if ($v === null || is_int($v) || is_float($v) || $v instanceof Blob) return $v;
282+ if (is_bool($v)) return (int)$v;
283+ if (is_string($v)) return mb_check_encoding($v, 'UTF-8') ? $v : new Blob($v);
284+ if ($v instanceof \Stringable) return (string)$v;
285+ $this->fail('実行に失敗', new DbException("パラメータ{$i}の型は未対応です: ".get_debug_type($v), 1, 'HY000'));
286+ }
287+
288+ private function runOne(array $st, array $params, string $what): array {
289+ if ($this->debug) echo "=== SQLite === {$st['sql']}\n";
290+ $before = $this->engine->lastInsertRowid;
291+ try {
292+ $res = $this->engine->run($st, $params);
293+ } catch (DbException $e) {
294+ $this->fail($what, $e);
295+ }
296+ $fields = [];
297+ foreach ($res['cols'] as $i => $n) $fields[] = ['name' => $n, 'table' => $res['tables'][$i] ?? '', 'type' => $res['types'][$i] ?? ''];
298+ $rows = [];
299+ foreach ($res['rows'] as $vals) {
300+ $row = [];
301+ foreach ($res['cols'] as $i => $n) {
302+ $v = $vals[$i] ?? null;
303+ $row[$n] = $v instanceof Blob ? $v->data : $v;
304+ }
305+ $rows[] = $row;
306+ }
307+ $isInsert = $st['t'] === 'insert';
308+ return ['fields' => $fields, 'rows' => $rows, 'affectedRows' => in_array($st['t'], ['insert', 'update', 'delete'], true) ? $res['changes'] : count($rows),
309+ 'insertId' => $isInsert && $this->engine->lastInsertRowid !== $before ? $this->engine->lastInsertRowid : ($isInsert ? $this->engine->lastInsertRowid : 0),
310+ 'warningCount' => 0];
311+ }
312+
313+ private function requireConnection(): void {
314+ if ($this->engine === null) $this->fail('', new DbException('接続していません(先にconnect()を呼んで下さい)', 0, 'HY000'));
315+ }
316+
317+ /** @return never */
318+ private function fail(string $what, DbException $e): never {
319+ $msg = $what === '' ? $e->getMessage() : "{$what}: ".$e->getMessage();
320+ $this->log($msg);
321+ throw new DbException($msg, $e->getCode(), $e->sqlState, $e);
322+ }
323+
324+ private function log(string $msg): void {
325+ if (function_exists('logger')) logger(\LogType::Db, $msg);
326+ }
327+}
@@ -0,0 +1,51 @@
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\Lib\Db\Sqlite;
39+
40+/**
41+ * BLOBの値(TEXTと区別する為)
42+ *
43+ * パラメータとして渡すと、UTF-8の文字列でもBLOBとして保存する。
44+ */
45+final class Blob {
46+ public function __construct(public string $data) {}
47+
48+ public function __toString(): string {
49+ return $this->data;
50+ }
51+}
@@ -0,0 +1,847 @@
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\Lib\Db\Sqlite;
39+
40+use Std\Lib\Db\DbException;
41+
42+/**
43+ * SQLiteのBツリー(テーブル:rowidがキー、インデックス:レコードがキー)
44+ *
45+ * ページはPageのオブジェクトとして持ち、flush()でPagerに書く。セルは毎回ページの最後に詰めて書き直すので、
46+ * フリーブロックは使わない。ページが溢れたり少なくなったりしたら、SQLiteと同じく最大3つの兄弟のページで分け直す。
47+ */
48+final class Btree {
49+ public const TABLE_LEAF = 13;
50+ public const TABLE_INTERIOR = 5;
51+ public const INDEX_LEAF = 10;
52+ public const INDEX_INTERIOR = 2;
53+
54+ /** @var array<int, Page> */
55+ private array $pages = [];
56+ private array $dirty = [];
57+ private int $generation = -1;
58+ /** スナップショット(文・セーブポイント):ページ番号 => [変更前のページ, 変更されていたか]、又はnull(無かった) */
59+ private array $snaps = [];
60+
61+ public function __construct(private Pager $pager) {}
62+
63+ ///////////////
64+ // ページ
65+
66+ /** ページを読む(オブジェクトのキャッシュ) */
67+ public function page(int $pgno): Page {
68+ $this->sync();
69+ if ($this->snaps) $this->remember($pgno);
70+ if (isset($this->pages[$pgno])) return $this->pages[$pgno];
71+ if (count($this->pages) > 3000 && empty($this->dirty)) $this->pages = [];
72+ return $this->pages[$pgno] = $this->parse($pgno);
73+ }
74+
75+ /** Pagerのキャッシュが捨てられたら、こちらも捨てる */
76+ private function sync(): void {
77+ if ($this->generation !== $this->pager->generation) {
78+ $this->pages = [];
79+ $this->dirty = [];
80+ $this->snaps = [];
81+ $this->generation = $this->pager->generation;
82+ }
83+ }
84+
85+ /** キャッシュを捨てる(ロールバックの後) */
86+ public function reset(): void {
87+ $this->pages = [];
88+ $this->dirty = [];
89+ $this->snaps = [];
90+ }
91+
92+ /** ページの変更前の状態を覚える(今のスナップショットで初めての場合) */
93+ private function remember(int $pgno): void {
94+ $k = array_key_last($this->snaps);
95+ if (array_key_exists($pgno, $this->snaps[$k])) return;
96+ $this->snaps[$k][$pgno] = isset($this->pages[$pgno]) ? [clone $this->pages[$pgno], isset($this->dirty[$pgno])] : null;
97+ }
98+
99+ /**
100+ * スナップショット(Pagerと一緒に使う。変更されたページをPagerに書かなくても、文だけを元に戻せる)
101+ *
102+ * @return int
103+ */
104+ public function snapPush(): int {
105+ $this->sync();
106+ $this->snaps[] = [];
107+ return count($this->snaps) - 1;
108+ }
109+
110+ /** スナップショットを捨てる(変更は残す) */
111+ public function snapPop(?int $n): void {
112+ if ($n === null) return;
113+ while (count($this->snaps) > $n) {
114+ $top = array_pop($this->snaps);
115+ if ($this->snaps) {
116+ $k = array_key_last($this->snaps);
117+ $this->snaps[$k] += $top;
118+ }
119+ }
120+ }
121+
122+ /** スナップショットの時に戻す(スナップショットは残す) */
123+ public function snapRestore(?int $n): void {
124+ if ($n === null || !isset($this->snaps[$n])) return;
125+ $all = [];
126+ for ($k = $n; $k < count($this->snaps); $k++) $all += $this->snaps[$k];
127+ foreach ($all as $pgno => $e) {
128+ if ($e === null) {
129+ unset($this->pages[$pgno], $this->dirty[$pgno]);
130+ } else {
131+ $this->pages[$pgno] = $e[0];
132+ if ($e[1]) $this->dirty[$pgno] = true;
133+ else unset($this->dirty[$pgno]);
134+ }
135+ }
136+ $this->snaps = array_slice($this->snaps, 0, $n);
137+ $this->snaps[] = [];
138+ }
139+
140+ private function parse(int $pgno): Page {
141+ $raw = $this->pager->read($pgno);
142+ $h = $pgno === 1 ? 100 : 0;
143+ $p = new Page($pgno);
144+ $p->type = ord($raw[$h]);
145+ if (!in_array($p->type, [2, 5, 10, 13], true)) throw new DbException("データベースが壊れています(ページ{$pgno}の種類:{$p->type})", 11, 'HY000');
146+ $n = unpack('n', $raw, $h + 3)[1];
147+ $leaf = $p->isLeaf();
148+ $table = $p->isTable();
149+ if (!$leaf) $p->right = unpack('N', $raw, $h + 8)[1];
150+ $base = $h + ($leaf ? 8 : 12);
151+ $ptrs = $n > 0 ? array_values(unpack("n{$n}", $raw, $base)) : [];
152+ $size = 0;
153+ foreach ($ptrs as $off) {
154+ $pos = $off;
155+ if (!$leaf) {
156+ $p->left[] = unpack('N', $raw, $off)[1];
157+ $pos += 4;
158+ }
159+ $start = $pos;
160+ if ($table && !$leaf) {
161+ $p->keys[] = Record::getVarint($raw, $pos);
162+ } else {
163+ $len = Record::getVarint($raw, $pos);
164+ if ($table) $p->keys[] = Record::getVarint($raw, $pos);
165+ $local = $this->localSize($len, $table);
166+ $pos += $local + ($len > $local ? 4 : 0);
167+ }
168+ $p->cells[] = substr($raw, $start, $pos - $start);
169+ $size += $pos - $off + 2;
170+ }
171+ $p->size = $size;
172+ if (!$table) $p->dec = $n > 0 ? array_fill(0, $n, null) : [];
173+ return $p;
174+ }
175+
176+ private function serialize(Page $p): string {
177+ $h = $p->pgno === 1 ? 100 : 0;
178+ $leaf = $p->isLeaf();
179+ $off = $this->pager->usable;
180+ $ptrs = '';
181+ $parts = [];
182+ foreach ($p->cells as $i => $c) {
183+ $cell = $leaf ? $c : pack('N', $p->left[$i]).$c;
184+ $off -= strlen($cell);
185+ $ptrs .= pack('n', $off);
186+ $parts[] = $cell;
187+ }
188+ $n = count($p->cells);
189+ $head = chr($p->type)."\0\0".pack('n', $n).pack('n', $off === 65536 ? 0 : $off)."\0".($leaf ? '' : pack('N', $p->right));
190+ $gap = $off - ($h + strlen($head) + strlen($ptrs));
191+ if ($gap < 0) throw new \LogicException("SQLite:ページ{$p->pgno}が溢れました");
192+ $prefix = $h ? $this->pager->header() : '';
193+ return $prefix.$head.$ptrs.str_repeat("\0", $gap).implode('', array_reverse($parts))
194+ .str_repeat("\0", $this->pager->pageSize - $this->pager->usable);
195+ }
196+
197+ /** 変更されたページ */
198+ private function touch(Page $p): void {
199+ $this->dirty[$p->pgno] = true;
200+ }
201+
202+ /** 変更されたページが多ければPagerに書く(操作の間だけ。途中のページは溢れているかもしれない) */
203+ private function spill(): void {
204+ if (count($this->dirty) > 500) $this->flush();
205+ }
206+
207+ /** 変更されたページをPagerに書く */
208+ public function flush(): void {
209+ foreach (array_keys($this->dirty) as $pgno) {
210+ if (isset($this->pages[$pgno])) $this->pager->write($pgno, $this->serialize($this->pages[$pgno]));
211+ }
212+ $this->dirty = [];
213+ }
214+
215+ private function newPage(int $type): Page {
216+ $this->sync();
217+ $pgno = $this->pager->allocate();
218+ if ($this->snaps) $this->remember($pgno);
219+ $p = new Page($pgno);
220+ $p->type = $type;
221+ $this->pages[$pgno] = $p;
222+ $this->touch($p);
223+ return $p;
224+ }
225+
226+ private function freePage(int $pgno): void {
227+ if ($this->snaps) $this->remember($pgno);
228+ unset($this->pages[$pgno], $this->dirty[$pgno]);
229+ $this->pager->free($pgno);
230+ }
231+
232+ /** ページに入るセルの大きさ */
233+ private function capacity(Page $p): int {
234+ return $this->pager->usable - ($p->pgno === 1 ? 100 : 0) - ($p->isLeaf() ? 8 : 12);
235+ }
236+
237+ private function cellSize(Page $p, string $cell): int {
238+ return strlen($cell) + ($p->isLeaf() ? 2 : 6);
239+ }
240+
241+ ///////////////
242+ // ペイロードとオーバーフロー
243+
244+ /** ページに置くペイロードのバイト数(残りはオーバーフローのページ) */
245+ public function localSize(int $len, bool $table): int {
246+ $u = $this->pager->usable;
247+ $x = $table ? $u - 35 : intdiv(($u - 12) * 64, 255) - 23;
248+ if ($len <= $x) return $len;
249+ $m = intdiv(($u - 12) * 32, 255) - 23;
250+ $k = $m + (($len - $m) % ($u - 4));
251+ return $k <= $x ? $k : $m;
252+ }
253+
254+ /** セルのペイロードの全部 */
255+ public function payload(Page $p, int $i): string {
256+ $c = $p->cells[$i];
257+ $pos = 0;
258+ $len = Record::getVarint($c, $pos);
259+ $table = $p->isTable();
260+ if ($table) Record::getVarint($c, $pos);
261+ $local = $this->localSize($len, $table);
262+ $data = substr($c, $pos, $local);
263+ if ($len > $local) {
264+ $next = unpack('N', $c, $pos + $local)[1];
265+ $rest = $len - $local;
266+ $chunk = $this->pager->usable - 4;
267+ while ($rest > 0) {
268+ if ($next < 1) throw new DbException('データベースが壊れています(オーバーフロー)', 11, 'HY000');
269+ $o = $this->pager->read($next);
270+ $take = min($rest, $chunk);
271+ $data .= substr($o, 4, $take);
272+ $rest -= $take;
273+ $next = unpack('N', $o)[1];
274+ }
275+ }
276+ return $data;
277+ }
278+
279+ /** インデックスのセルのキー(デコードした物) */
280+ public function key(Page $p, int $i): array {
281+ return $p->dec[$i] ??= Record::decode($this->payload($p, $i));
282+ }
283+
284+ private function makeCell(string $payload, ?int $rowid): string {
285+ $len = strlen($payload);
286+ $table = $rowid !== null;
287+ $head = Record::putVarint($len).($table ? Record::putVarint($rowid) : '');
288+ $local = $this->localSize($len, $table);
289+ if ($local >= $len) return $head.$payload;
290+ $chunk = $this->pager->usable - 4;
291+ $pieces = str_split(substr($payload, $local), $chunk);
292+ $pgnos = [];
293+ foreach ($pieces as $_) $pgnos[] = $this->pager->allocate();
294+ foreach ($pieces as $k => $piece) {
295+ $next = $pgnos[$k + 1] ?? 0;
296+ $this->pager->write($pgnos[$k], str_pad(pack('N', $next).$piece, $this->pager->pageSize, "\0"));
297+ }
298+ return $head.substr($payload, 0, $local).pack('N', $pgnos[0]);
299+ }
300+
301+ /** セルのオーバーフローのページを解放する */
302+ private function freeOverflow(Page $p, string $c): void {
303+ if (!$p->isLeaf() && $p->isTable()) return;
304+ $pos = 0;
305+ $len = Record::getVarint($c, $pos);
306+ if ($p->isTable()) Record::getVarint($c, $pos);
307+ $local = $this->localSize($len, $p->isTable());
308+ if ($len <= $local) return;
309+ $next = unpack('N', $c, $pos + $local)[1];
310+ $n = intdiv($len - $local + $this->pager->usable - 5, $this->pager->usable - 4);
311+ for ($k = 0; $k < $n && $next > 0; $k++) {
312+ $o = $this->pager->read($next);
313+ $this->freePage($next);
314+ $next = unpack('N', $o)[1];
315+ }
316+ }
317+
318+ ///////////////
319+ // テーブル
320+
321+ /** 新しいテーブル(又はインデックス)のツリー */
322+ public function create(bool $table): int {
323+ return $this->newPage($table ? self::TABLE_LEAF : self::INDEX_LEAF)->pgno;
324+ }
325+
326+ private static function lowerBound(array $keys, int $k): int {
327+ $lo = 0;
328+ $hi = count($keys);
329+ while ($lo < $hi) {
330+ $mid = ($lo + $hi) >> 1;
331+ if ($keys[$mid] < $k) $lo = $mid + 1;
332+ else $hi = $mid;
333+ }
334+ return $lo;
335+ }
336+
337+ private function child(Page $p, int $i): int {
338+ return $i < count($p->cells) ? $p->left[$i] : $p->right;
339+ }
340+
341+ /** @return array [path, 葉, 位置, 有るか] */
342+ private function seek(int $root, int $rowid): array {
343+ $path = [];
344+ $p = $this->page($root);
345+ while (!$p->isLeaf()) {
346+ $i = self::lowerBound($p->keys, $rowid);
347+ $path[] = [$p, $i];
348+ $p = $this->page($this->child($p, $i));
349+ }
350+ $i = self::lowerBound($p->keys, $rowid);
351+ return [$path, $p, $i, $i < count($p->keys) && $p->keys[$i] === $rowid];
352+ }
353+
354+ /**
355+ * 行を読む
356+ *
357+ * @return string|null レコード(無ければnull)
358+ */
359+ public function get(int $root, int $rowid): ?string {
360+ [, $p, $i, $found] = $this->seek($root, $rowid);
361+ return $found ? $this->payload($p, $i) : null;
362+ }
363+
364+ /** 行を入れる(同じrowidが有れば置き換える) */
365+ public function insert(int $root, int $rowid, string $record): void {
366+ $this->spill();
367+ [$path, $p, $i, $found] = $this->seek($root, $rowid);
368+ $cell = $this->makeCell($record, $rowid);
369+ if ($found) {
370+ $this->freeOverflow($p, $p->cells[$i]);
371+ $p->size += strlen($cell) - strlen($p->cells[$i]);
372+ $p->cells[$i] = $cell;
373+ } else {
374+ array_splice($p->cells, $i, 0, [$cell]);
375+ array_splice($p->keys, $i, 0, [$rowid]);
376+ $p->size += strlen($cell) + 2;
377+ }
378+ $this->touch($p);
379+ $this->fix($path, $p);
380+ }
381+
382+ /** 行を消す */
383+ public function delete(int $root, int $rowid): bool {
384+ $this->spill();
385+ [$path, $p, $i, $found] = $this->seek($root, $rowid);
386+ if (!$found) return false;
387+ $this->freeOverflow($p, $p->cells[$i]);
388+ $p->size -= strlen($p->cells[$i]) + 2;
389+ array_splice($p->cells, $i, 1);
390+ array_splice($p->keys, $i, 1);
391+ $this->touch($p);
392+ $this->fix($path, $p);
393+ return true;
394+ }
395+
396+ /** 一番大きいrowid(空なら0) */
397+ public function maxRowid(int $root): int {
398+ $p = $this->page($root);
399+ while (!$p->isLeaf()) $p = $this->page($p->right);
400+ return empty($p->keys) ? 0 : end($p->keys);
401+ }
402+
403+ /**
404+ * テーブルの行を順番に
405+ *
406+ * @param int $root
407+ * @param int|null $from 最小のrowid
408+ * @param int|null $to 最大のrowid
409+ * @return \Generator rowid => レコード
410+ */
411+ public function scan(int $root, ?int $from = null, ?int $to = null): \Generator {
412+ $stop = false;
413+ yield from $this->walkTable($root, $from, $to, $stop);
414+ }
415+
416+ private function walkTable(int $pgno, ?int $from, ?int $to, bool &$stop): \Generator {
417+ $p = $this->page($pgno);
418+ $n = count($p->cells);
419+ $i = $from === null ? 0 : self::lowerBound($p->keys, $from);
420+ if ($p->isLeaf()) {
421+ for (; $i < $n; $i++) {
422+ if ($to !== null && $p->keys[$i] > $to) {
423+ $stop = true;
424+ return;
425+ }
426+ yield $p->keys[$i] => $this->payload($p, $i);
427+ }
428+ return;
429+ }
430+ for (; $i <= $n; $i++) {
431+ yield from $this->walkTable($this->child($p, $i), $from, $to, $stop);
432+ if ($stop || ($to !== null && $i < $n && $p->keys[$i] >= $to)) {
433+ $stop = true;
434+ return;
435+ }
436+ }
437+ }
438+
439+ /** 行の数 */
440+ public function count(int $root): int {
441+ $p = $this->page($root);
442+ if ($p->isLeaf()) return count($p->cells);
443+ $n = 0;
444+ for ($i = 0; $i <= count($p->cells); $i++) $n += $this->count($this->child($p, $i));
445+ return $n;
446+ }
447+
448+ /**
449+ * ツリーを空にする($dropならルートも解放する)
450+ */
451+ public function clear(int $root, bool $drop = false): void {
452+ $p = $this->page($root);
453+ $this->freeSubtree($p, false);
454+ if ($drop) {
455+ $this->freePage($root);
456+ return;
457+ }
458+ $p->type = $p->isTable() ? self::TABLE_LEAF : self::INDEX_LEAF;
459+ $p->cells = $p->left = $p->keys = $p->dec = [];
460+ $p->right = 0;
461+ $p->size = 0;
462+ $this->touch($p);
463+ }
464+
465+ private function freeSubtree(Page $p, bool $self): void {
466+ foreach ($p->cells as $c) $this->freeOverflow($p, $c);
467+ if (!$p->isLeaf()) {
468+ for ($i = 0; $i <= count($p->cells); $i++) $this->freeSubtree($this->page($this->child($p, $i)), true);
469+ }
470+ if ($self) $this->freePage($p->pgno);
471+ }
472+
473+ ///////////////
474+ // インデックス
475+
476+ /**
477+ * 最初の位置(キーの前方一致で比べる)
478+ *
479+ * @param bool $strict trueなら「>」、falseなら「≧」
480+ */
481+ private function bound(Page $p, array $key, array $idx, bool $strict): int {
482+ $lo = 0;
483+ $hi = count($p->cells);
484+ while ($lo < $hi) {
485+ $mid = ($lo + $hi) >> 1;
486+ $c = Record::compareKeys($this->key($p, $mid), $key, $idx['colls'], $idx['desc']);
487+ if ($c < 0 || ($strict && $c === 0)) $lo = $mid + 1;
488+ else $hi = $mid;
489+ }
490+ return $lo;
491+ }
492+
493+ /**
494+ * インデックスにキーを入れる
495+ *
496+ * @param array $idx root、colls、desc(rowidの列も含む)
497+ * @param array $key 列の値…、rowid
498+ */
499+ public function indexInsert(array $idx, array $key): void {
500+ $this->spill();
501+ $path = [];
502+ $p = $this->page($idx['root']);
503+ while (true) {
504+ $i = $this->bound($p, $key, $idx, false);
505+ if ($i < count($p->cells) && count($this->key($p, $i)) === count($key)
506+ && Record::compareKeys($this->key($p, $i), $key, $idx['colls'], $idx['desc']) === 0) return;
507+ if ($p->isLeaf()) break;
508+ $path[] = [$p, $i];
509+ $p = $this->page($this->child($p, $i));
510+ }
511+ $cell = $this->makeCell(Record::encode($key), null);
512+ array_splice($p->cells, $i, 0, [$cell]);
513+ array_splice($p->dec, $i, 0, [$key]);
514+ $p->size += strlen($cell) + 2;
515+ $this->touch($p);
516+ $this->fix($path, $p);
517+ }
518+
519+ /**
520+ * キーの前方一致の項目を探す(一意の制約の確認用)
521+ *
522+ * @param int|null $self 除くrowid
523+ * @return int|null 見つけた項目のrowid
524+ */
525+ public function indexFind(array $idx, array $prefix, ?int $self = null): ?int {
526+ $p = $this->page($idx['root']);
527+ while (true) {
528+ $i = $this->bound($p, $prefix, $idx, false);
529+ if ($i < count($p->cells)) {
530+ $k = $this->key($p, $i);
531+ if (Record::compareKeys($k, $prefix, $idx['colls'], $idx['desc']) === 0) {
532+ $id = end($k);
533+ if ($id !== $self) return $id;
534+ // 自分自身:他にも有るかもしれないので全部を見る
535+ foreach ($this->indexScan($idx, $prefix, true, $prefix, true) as $k2) {
536+ if (end($k2) !== $self) return end($k2);
537+ }
538+ return null;
539+ }
540+ }
541+ if ($p->isLeaf()) return null;
542+ $p = $this->page($this->child($p, $i));
543+ }
544+ }
545+
546+ /** インデックスからキーを消す */
547+ public function indexDelete(array $idx, array $key): bool {
548+ $this->spill();
549+ $path = [];
550+ $p = $this->page($idx['root']);
551+ while (true) {
552+ $i = $this->bound($p, $key, $idx, false);
553+ if ($i < count($p->cells) && Record::compareKeys($this->key($p, $i), $key, $idx['colls'], $idx['desc']) === 0) break;
554+ if ($p->isLeaf()) return false;
555+ $path[] = [$p, $i];
556+ $p = $this->page($this->child($p, $i));
557+ }
558+ $this->decAll($p);
559+ if ($p->isLeaf()) {
560+ $this->freeOverflow($p, $p->cells[$i]);
561+ $p->size -= strlen($p->cells[$i]) + 2;
562+ array_splice($p->cells, $i, 1);
563+ array_splice($p->dec, $i, 1);
564+ $this->touch($p);
565+ $this->fix($path, $p);
566+ return true;
567+ }
568+
569+ // 内部のページ:左のツリーの一番大きいキーと置き換える
570+ $pathL = array_merge($path, [[$p, $i]]);
571+ $q = $this->page($p->left[$i]);
572+ while (!$q->isLeaf()) {
573+ $pathL[] = [$q, count($q->cells)];
574+ $q = $this->page($q->right);
575+ }
576+ $this->decAll($q);
577+ $last = count($q->cells) - 1;
578+ $cell = $q->cells[$last];
579+ $dec = $this->key($q, $last);
580+ $q->size -= strlen($cell) + 2;
581+ array_splice($q->cells, $last, 1);
582+ array_splice($q->dec, $last, 1);
583+ $this->freeOverflow($p, $p->cells[$i]);
584+ $p->size += strlen($cell) - strlen($p->cells[$i]);
585+ $p->cells[$i] = $cell;
586+ $p->dec[$i] = $dec;
587+ $this->touch($p);
588+ $this->touch($q);
589+ if ($this->fix($pathL, $q) > count($path)) $this->fix($path, $p);
590+ return true;
591+ }
592+
593+ /** decの配列をセルと同じ長さにする(array_spliceの前に) */
594+ private function decAll(Page $p): void {
595+ for ($i = 0, $n = count($p->cells); $i < $n; $i++) $p->dec[$i] ??= null;
596+ ksort($p->dec);
597+ }
598+
599+ /**
600+ * インデックスの範囲
601+ *
602+ * @param array $idx
603+ * @param array|null $lo 最小のキー(前方一致)
604+ * @param bool $loInc 最小を含む
605+ * @param array|null $hi 最大のキー(前方一致)
606+ * @param bool $hiInc 最大を含む
607+ * @return \Generator キー(列の値…、rowid)
608+ */
609+ public function indexScan(array $idx, ?array $lo = null, bool $loInc = true, ?array $hi = null, bool $hiInc = true): \Generator {
610+ $stop = false;
611+ yield from $this->walkIndex($idx['root'], $idx, $lo, $loInc, $hi, $hiInc, $stop);
612+ }
613+
614+ private function walkIndex(int $pgno, array $idx, ?array $lo, bool $loInc, ?array $hi, bool $hiInc, bool &$stop): \Generator {
615+ $p = $this->page($pgno);
616+ $n = count($p->cells);
617+ $i = $lo === null ? 0 : $this->bound($p, $lo, $idx, !$loInc);
618+ $leaf = $p->isLeaf();
619+ for (; $i <= $n; $i++) {
620+ if (!$leaf) {
621+ yield from $this->walkIndex($this->child($p, $i), $idx, $lo, $loInc, $hi, $hiInc, $stop);
622+ if ($stop) return;
623+ }
624+ if ($i === $n) return;
625+ $k = $this->key($p, $i);
626+ if ($hi !== null) {
627+ $c = Record::compareKeys($k, $hi, $idx['colls'], $idx['desc']);
628+ if ($c > 0 || (!$hiInc && $c === 0)) {
629+ $stop = true;
630+ return;
631+ }
632+ }
633+ yield $k;
634+ }
635+ }
636+
637+ ///////////////
638+ // 釣り合い
639+
640+ /**
641+ * 変更したページを直す(溢れた・少なくなったら兄弟と分け直す)
642+ *
643+ * @param array $path 親のページと位置の配列(ルートから)
644+ * @param Page $p 変更したページ
645+ * @return int 確認を終えた深さ(パスの長さ)
646+ */
647+ private function fix(array $path, Page $p): int {
648+ while (true) {
649+ $cap = $this->capacity($p);
650+ $over = $p->size > $cap;
651+ if (empty($path)) {
652+ if ($over) {
653+ $c = $this->deeper($p);
654+ $path = [[$p, 0]];
655+ $p = $c;
656+ continue;
657+ }
658+ if (!$p->isLeaf() && empty($p->cells)) $this->shallower($p);
659+ return 0;
660+ }
661+ $under = $p->size < intdiv($cap, 3);
662+ if (!$over && !$under) return count($path);
663+ [$parent, $idx] = array_pop($path);
664+ if (!$over && empty($parent->cells)) {
665+ // 兄弟が無い
666+ $p = $parent;
667+ continue;
668+ }
669+ $this->balance($parent, $idx);
670+ $p = $parent;
671+ }
672+ }
673+
674+ /** ルートが溢れた:中身を新しいページに移して、ルートを1段上にする */
675+ private function deeper(Page $root): Page {
676+ $c = $this->newPage($root->type);
677+ $c->cells = $root->cells;
678+ $c->left = $root->left;
679+ $c->keys = $root->keys;
680+ $c->dec = $root->dec;
681+ $c->right = $root->right;
682+ $c->size = $root->size;
683+ $root->type = $root->isTable() ? self::TABLE_INTERIOR : self::INDEX_INTERIOR;
684+ $root->cells = $root->left = $root->keys = $root->dec = [];
685+ $root->size = 0;
686+ $root->right = $c->pgno;
687+ $this->touch($root);
688+ return $c;
689+ }
690+
691+ /** ルートの子が1つだけ:子の中身をルートに入れる(入れば) */
692+ private function shallower(Page $root): void {
693+ $c = $this->page($root->right);
694+ $cap = $this->pager->usable - ($root->pgno === 1 ? 100 : 0) - ($c->isLeaf() ? 8 : 12);
695+ if ($c->size > $cap) return;
696+ $this->decAll($c);
697+ $root->type = $c->type;
698+ $root->cells = $c->cells;
699+ $root->left = $c->left;
700+ $root->keys = $c->keys;
701+ $root->dec = $c->dec;
702+ $root->right = $c->right;
703+ $root->size = $c->size;
704+ $this->freePage($c->pgno);
705+ $this->touch($root);
706+ }
707+
708+ /** 親の$idx番目の子とその兄弟(最大3つ)のセルを分け直す */
709+ private function balance(Page $parent, int $idx): void {
710+ $n = count($parent->cells);
711+ $first = max(0, $idx - 1);
712+ $last = min($n, $first + 2);
713+ $first = max(0, $last - 2);
714+ $sibs = [];
715+ for ($j = $first; $j <= $last; $j++) $sibs[] = $this->page($this->child($parent, $j));
716+ $leaf = $sibs[0]->isLeaf();
717+ $table = $sibs[0]->isTable();
718+ $type = $sibs[0]->type;
719+ if (!$table) $this->decAll($parent);
720+
721+ // 全部のセル(間の区切りも)
722+ $items = [];
723+ $lastRight = 0;
724+ $ns = count($sibs);
725+ foreach ($sibs as $j => $s) {
726+ if ($s->isLeaf() !== $leaf) throw new DbException('データベースが壊れています(ツリーの深さ)', 11, 'HY000');
727+ foreach ($s->cells as $i => $c) {
728+ $items[] = [$c, $leaf ? 0 : $s->left[$i], $table ? $s->keys[$i] : 0, $table ? null : ($s->dec[$i] ?? null)];
729+ }
730+ if (!$leaf) $lastRight = $s->right;
731+ if ($j < $ns - 1) {
732+ $pi = $first + $j;
733+ if ($table && $leaf) continue;
734+ if ($table) {
735+ $items[] = [Record::putVarint($parent->keys[$pi]), $s->right, $parent->keys[$pi], null];
736+ } else {
737+ $items[] = [$parent->cells[$pi], $leaf ? 0 : $s->right, 0, $parent->dec[$pi] ?? null];
738+ }
739+ }
740+ }
741+
742+ $cap = $this->pager->usable - ($leaf ? 8 : 12);
743+ $extra = $leaf ? 2 : 6;
744+ [$groups, $divs] = $this->distribute($items, $cap, !($table && $leaf), $extra);
745+ $k = count($groups);
746+
747+ $pgs = [];
748+ for ($j = 0; $j < $k; $j++) $pgs[] = $j < $ns ? $sibs[$j] : $this->newPage($type);
749+ for ($j = $k; $j < $ns; $j++) $this->freePage($sibs[$j]->pgno);
750+
751+ foreach ($groups as $j => $g) {
752+ $pg = $pgs[$j];
753+ $pg->type = $type;
754+ $pg->cells = $pg->left = $pg->keys = $pg->dec = [];
755+ $pg->size = 0;
756+ foreach ($g as $it) {
757+ $pg->cells[] = $it[0];
758+ if (!$leaf) $pg->left[] = $it[1];
759+ if ($table) $pg->keys[] = $it[2];
760+ else $pg->dec[] = $it[3];
761+ $pg->size += strlen($it[0]) + $extra;
762+ }
763+ if (!$leaf) $pg->right = $j < $k - 1 ? $divs[$j][1] : $lastRight;
764+ $this->touch($pg);
765+ }
766+
767+ // 親の区切り
768+ $cells = $left = $keys = $dec = [];
769+ for ($j = 0; $j < $k - 1; $j++) {
770+ if ($table) {
771+ $key = $leaf ? end($groups[$j])[2] : $divs[$j][2];
772+ $cells[] = Record::putVarint($key);
773+ $keys[] = $key;
774+ } else {
775+ $cells[] = $divs[$j][0];
776+ $dec[] = $divs[$j][3];
777+ }
778+ $left[] = $pgs[$j]->pgno;
779+ }
780+ array_splice($parent->cells, $first, $ns - 1, $cells);
781+ array_splice($parent->left, $first, $ns - 1, $left);
782+ if ($table) array_splice($parent->keys, $first, $ns - 1, $keys);
783+ else array_splice($parent->dec, $first, $ns - 1, $dec);
784+ $slot = $first + $k - 1;
785+ if ($slot < count($parent->cells)) $parent->left[$slot] = $pgs[$k - 1]->pgno;
786+ else $parent->right = $pgs[$k - 1]->pgno;
787+ $parent->size = 0;
788+ foreach ($parent->cells as $c) $parent->size += strlen($c) + 6;
789+ $this->touch($parent);
790+ }
791+
792+ /**
793+ * セルをページに分ける
794+ *
795+ * @param bool $promote グループの間のセルを親の区切りにする(テーブルの葉以外)
796+ * @return array [グループの配列, 区切りの配列]
797+ */
798+ private function distribute(array $items, int $cap, bool $promote, int $extra): array {
799+ $sizes = [];
800+ $total = 0;
801+ $max = 0;
802+ foreach ($items as $it) {
803+ $s = strlen($it[0]) + $extra;
804+ $sizes[] = $s;
805+ $total += $s;
806+ $max = max($max, $s);
807+ }
808+ $pack = function(int $limit) use ($items, $sizes, $promote): array {
809+ $groups = $divs = $cur = [];
810+ $cs = 0;
811+ foreach ($items as $i => $it) {
812+ if ($cur && $cs + $sizes[$i] > $limit) {
813+ $groups[] = $cur;
814+ $cur = [];
815+ $cs = 0;
816+ if ($promote) {
817+ $divs[] = $it;
818+ continue;
819+ }
820+ }
821+ $cur[] = $it;
822+ $cs += $sizes[$i];
823+ }
824+ if ($cur || empty($groups)) {
825+ $groups[] = $cur;
826+ } else if ($promote) {
827+ $groups[] = [array_pop($divs)];
828+ }
829+ return [$groups, $divs];
830+ };
831+ $res = $pack($cap);
832+ $k = count($res[0]);
833+ if ($k > 1) {
834+ // なるべく同じ大きさに
835+ $target = max($max, intdiv($total, $k));
836+ $step = max(1, intdiv($cap - $target, 8));
837+ for ($limit = $target; $limit < $cap; $limit += $step) {
838+ $try = $pack($limit);
839+ if (count($try[0]) === $k) {
840+ $res = $try;
841+ break;
842+ }
843+ }
844+ }
845+ return $res;
846+ }
847+}
@@ -0,0 +1,2731 @@
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\Lib\Db\Sqlite;
39+
40+use Std\Lib\Db\DbException;
41+
42+/**
43+ * SQLiteのSQLを実行する
44+ *
45+ * SELECTは結合(入れ子のループ、rowid・インデックスでの検索)、GROUP BY、集約、HAVING、DISTINCT、ORDER BY、LIMIT、
46+ * 複合(UNION等)、サブクエリ(相関も)、ビュー、WITH(再帰無し)に対応。
47+ * 式は一度クロージャにしてから行毎に実行する。
48+ */
49+final class Engine {
50+ public int $changes = 0;
51+ public int $totalChanges = 0;
52+ public int $lastInsertRowid = 0;
53+
54+ private Btree $bt;
55+ private Parser $parser;
56+ private ?Schema $schema = null;
57+ private int $schemaGen = -1;
58+ private int $schemaCookie = -1;
59+ private array $params = [];
60+ /** 相関サブクエリ用:深さ => 外の行 */
61+ private array $stack = [];
62+ private array $cte = [];
63+ /** 明示的なトランザクション(BEGIN) */
64+ private bool $txn = false;
65+ private array $savepoints = [];
66+ private bool $failKeep = false;
67+ private bool $rollbackAll = false;
68+ /** 最後のaccess()で使ったインデックスの列(rowid・全部ならnull) */
69+ private ?array $lastAccess = null;
70+
71+ public function __construct(private Pager $pager) {
72+ $this->bt = new Btree($pager);
73+ $this->parser = new Parser();
74+ }
75+
76+ public function inTransaction(): bool {
77+ return $this->txn;
78+ }
79+
80+ public function btree(): Btree {
81+ return $this->bt;
82+ }
83+
84+ /** SQLを文に分ける(構文木) */
85+ public function prepare(string $sql): array {
86+ return $this->parser->parseAll($sql);
87+ }
88+
89+ /**
90+ * 1つの文を実行する
91+ *
92+ * @param array $st prepare()の結果の1つ
93+ * @param array $params 1からの番号 => 値
94+ * @return array cols(名前)、types(宣言された型)、tables、rows(値の配列の配列)、changes、lastId、isQuery
95+ * @throws DbException
96+ */
97+ public function run(array $st, array $params = []): array {
98+ $this->params = $params;
99+ switch ($st['t']) {
100+ case 'begin':
101+ return $this->begin($st['mode']);
102+ case 'commit':
103+ return $this->commit();
104+ case 'rollback':
105+ return $this->rollback();
106+ case 'savepoint':
107+ return $this->savepoint($st['name']);
108+ case 'release':
109+ return $this->release($st['name']);
110+ case 'rollbackTo':
111+ return $this->rollbackTo($st['name']);
112+ case 'noop':
113+ return self::result();
114+ }
115+
116+ $write = $this->isWrite($st);
117+ $this->pager->beginRead();
118+ $snap = $bsnap = null;
119+ $this->failKeep = $this->rollbackAll = false;
120+ try {
121+ if ($write) {
122+ $this->pager->beginWrite();
123+ if ($this->pager->nPages === 0) $this->pager->initialize();
124+ $snap = $this->pager->snapPush();
125+ $bsnap = $this->bt->snapPush();
126+ }
127+ if (in_array($st['t'], ['insert', 'update', 'delete'], true)) $this->changes = 0;
128+ $this->stack = [];
129+ $this->cte = [];
130+ $res = $this->dispatch($st);
131+ if ($write) $this->finishWrite($snap, $bsnap);
132+ return $res;
133+ } catch (\Throwable $e) {
134+ if ($write && $this->pager->inTransaction()) {
135+ $this->schema = null;
136+ if ($this->failKeep) {
137+ // FAIL:それまでの変更は残す
138+ $this->finishWrite($snap, $bsnap);
139+ } else if ($this->rollbackAll || !$this->txn) {
140+ $this->bt->reset();
141+ $this->pager->rollback();
142+ if ($this->rollbackAll) {
143+ $this->txn = false;
144+ $this->savepoints = [];
145+ }
146+ } else {
147+ $this->bt->snapRestore($bsnap);
148+ $this->bt->snapPop($bsnap);
149+ $this->pager->snapRestore($snap);
150+ $this->pager->snapPop($snap);
151+ }
152+ }
153+ if ($e instanceof DbException) throw $e;
154+ if ($e instanceof \LogicException || $e instanceof \UnexpectedValueException) throw new DbException('SQLite:'.$e->getMessage(), 11, 'HY000', $e);
155+ throw $e;
156+ } finally {
157+ $this->pager->hold = $this->txn;
158+ $this->pager->endRead();
159+ }
160+ }
161+
162+ private function finishWrite(?int $snap, ?int $bsnap): void {
163+ $this->pager->snapPop($snap);
164+ $this->bt->snapPop($bsnap);
165+ if (!$this->txn) {
166+ $this->bt->flush();
167+ $this->pager->commit();
168+ }
169+ }
170+
171+ private function isWrite(array $st): bool {
172+ if (in_array($st['t'], ['insert', 'update', 'delete', 'createTable', 'createIndex', 'createView', 'drop', 'alter'], true)) return true;
173+ if ($st['t'] === 'pragma' && $st['arg'] !== null && in_array($st['name'], ['user_version', 'application_id'], true)) return true;
174+ return false;
175+ }
176+
177+ private function dispatch(array $st): array {
178+ if (!empty($st['with']) && $st['t'] !== 'select') $this->cte[] = $st['with'];
179+ switch ($st['t']) {
180+ case 'select':
181+ $plan = $this->planSelect($st, null);
182+ $rows = ($plan->run)(-1);
183+ return self::result($plan->cols, $plan->types, $rows, 0, $plan->tables);
184+ case 'insert':
185+ return $this->insert($st);
186+ case 'update':
187+ return $this->update($st);
188+ case 'delete':
189+ return $this->delete($st);
190+ case 'createTable':
191+ return $this->createTable($st);
192+ case 'createIndex':
193+ return $this->createIndex($st);
194+ case 'createView':
195+ return $this->createView($st);
196+ case 'drop':
197+ return $this->drop($st);
198+ case 'alter':
199+ return $this->alter($st);
200+ case 'pragma':
201+ return $this->pragma($st);
202+ }
203+ throw new DbException("未対応の文:{$st['t']}", 1, 'HY000');
204+ }
205+
206+ private static function result(array $cols = [], array $types = [], array $rows = [], int $changes = 0, array $tables = []): array {
207+ return ['cols' => $cols, 'types' => $types, 'rows' => $rows, 'changes' => $changes, 'tables' => $tables];
208+ }
209+
210+ /** スキーマ(変更されていれば読み直す) */
211+ public function schema(): Schema {
212+ if ($this->schema === null || $this->schemaGen !== $this->pager->generation || $this->schemaCookie !== $this->pager->hdr['cookie']) {
213+ $this->schema = Schema::load($this->bt, $this->pager->nPages);
214+ $this->schemaGen = $this->pager->generation;
215+ $this->schemaCookie = $this->pager->hdr['cookie'];
216+ }
217+ return $this->schema;
218+ }
219+
220+ ///////////////
221+ // トランザクション
222+
223+ private function begin(string $mode): array {
224+ if ($this->txn) throw new DbException('トランザクションの中でトランザクションは開始できません', 1, 'HY000');
225+ $this->pager->beginRead();
226+ if ($mode !== 'DEFERRED') {
227+ try {
228+ $this->pager->beginWrite();
229+ if ($this->pager->nPages === 0) $this->pager->initialize();
230+ } catch (DbException $e) {
231+ $this->pager->endRead();
232+ throw $e;
233+ }
234+ }
235+ $this->txn = true;
236+ $this->pager->hold = true;
237+ return self::result();
238+ }
239+
240+ private function commit(): array {
241+ if (!$this->txn) throw new DbException('トランザクションが開始されていません', 1, 'HY000');
242+ $this->bt->flush();
243+ $this->bt->snapPop(0);
244+ $this->pager->commit();
245+ $this->txn = false;
246+ $this->savepoints = [];
247+ $this->pager->hold = false;
248+ $this->pager->endRead();
249+ return self::result();
250+ }
251+
252+ private function rollback(): array {
253+ if (!$this->txn) throw new DbException('トランザクションが開始されていません', 1, 'HY000');
254+ $this->bt->reset();
255+ $this->pager->rollback();
256+ $this->txn = false;
257+ $this->savepoints = [];
258+ $this->pager->hold = false;
259+ $this->pager->endRead();
260+ return self::result();
261+ }
262+
263+ private function savepoint(string $name): array {
264+ $started = false;
265+ if (!$this->txn) {
266+ $this->begin('IMMEDIATE');
267+ $started = true;
268+ } else {
269+ $this->pager->beginWrite();
270+ if ($this->pager->nPages === 0) $this->pager->initialize();
271+ }
272+ $this->savepoints[] = [strtolower($name), $this->pager->snapPush(), $started, $this->bt->snapPush()];
273+ return self::result();
274+ }
275+
276+ private function findSavepoint(string $name): int {
277+ for ($i = count($this->savepoints) - 1; $i >= 0; $i--) {
278+ if ($this->savepoints[$i][0] === strtolower($name)) return $i;
279+ }
280+ throw new DbException("セーブポイントがありません:{$name}", 1, 'HY000');
281+ }
282+
283+ private function release(string $name): array {
284+ $i = $this->findSavepoint($name);
285+ [, $snap, $started, $bsnap] = $this->savepoints[$i];
286+ $this->pager->snapPop($snap);
287+ $this->bt->snapPop($bsnap);
288+ array_splice($this->savepoints, $i);
289+ if ($started) return $this->commit();
290+ return self::result();
291+ }
292+
293+ private function rollbackTo(string $name): array {
294+ $i = $this->findSavepoint($name);
295+ [, $snap, , $bsnap] = $this->savepoints[$i];
296+ $this->bt->snapRestore($bsnap);
297+ $this->pager->snapRestore($snap);
298+ $this->schema = null;
299+ array_splice($this->savepoints, $i + 1);
300+ return self::result();
301+ }
302+
303+ ///////////////
304+ // スコープ(列の場所)
305+
306+ /**
307+ * スコープを作る
308+ *
309+ * @param array $srcs 各ソース:alias、name、cols(表示の名前)、types、affs、colls、table(テーブルの定義、又はnull)
310+ */
311+ private function scope(array $srcs, ?\stdClass $parent): \stdClass {
312+ $sc = new \stdClass();
313+ $sc->parent = $parent;
314+ $sc->level = $parent ? $parent->level + 1 : 0;
315+ $sc->srcs = [];
316+ $sc->srcOf = [];
317+ $off = 0;
318+ foreach ($srcs as $j => $s) {
319+ $s['base'] = $off;
320+ $s['map'] = [];
321+ foreach ($s['cols'] as $i => $n) {
322+ $s['map'][strtolower($n)] ??= $off + $i;
323+ $sc->srcOf[$off + $i] = $j;
324+ }
325+ $s['rowid'] = $off + count($s['cols']);
326+ $sc->srcOf[$s['rowid']] = $j;
327+ $s['hidden'] ??= [];
328+ $off += count($s['cols']) + 1;
329+ $sc->srcs[$j] = $s;
330+ }
331+ $sc->width = $off;
332+ $sc->usedOuter = false;
333+ return $sc;
334+ }
335+
336+ /**
337+ * 列を探す
338+ *
339+ * @return array|null level、off、aff、coll、type、src、table
340+ */
341+ private function resolve(\stdClass $sc, ?string $tbl, string $name): ?array {
342+ $ln = strtolower($name);
343+ $lt = $tbl === null ? null : strtolower($tbl);
344+ for ($s = $sc; $s !== null; $s = $s->parent) {
345+ $found = null;
346+ foreach ($s->srcs as $j => $src) {
347+ if ($lt !== null && $lt !== strtolower((string)$src['alias'])) continue;
348+ $off = $src['map'][$ln] ?? null;
349+ if ($off === null && in_array($ln, ['rowid', 'oid', '_rowid_'], true) && ($src['table'] ?? null) !== null) $off = $src['rowid'];
350+ if ($off === null) continue;
351+ if ($lt === null && isset($src['hidden'][$ln])) continue;
352+ if ($found !== null && $lt === null) throw new DbException("列の名前が曖昧です:{$name}", 1, 'HY000');
353+ $i = $off - $src['base'];
354+ $found = ['level' => $s->level, 'off' => $off, 'src' => $j, 'aff' => $src['affs'][$i] ?? Value::INTEGER,
355+ 'coll' => $src['colls'][$i] ?? 'BINARY', 'type' => $src['types'][$i] ?? 'INTEGER', 'table' => $src['name'] ?? null,
356+ 'name' => $src['cols'][$i] ?? $name, 'col' => $i];
357+ }
358+ if ($found !== null) {
359+ if ($s !== $sc) {
360+ for ($x = $sc; $x !== $s; $x = $x->parent) $x->usedOuter = true;
361+ }
362+ return $found;
363+ }
364+ }
365+ return null;
366+ }
367+
368+ ///////////////
369+ // 式
370+
371+ /**
372+ * 式をクロージャにする
373+ *
374+ * @param array $e 式の構文木
375+ * @param \stdClass $sc スコープ
376+ * @param \stdClass|null $agg 集約の文脈(集約関数を集める)
377+ * @return \Closure fn(array $row): mixed
378+ */
379+ private function compile(array $e, \stdClass $sc, ?\stdClass $agg = null): \Closure {
380+ switch ($e['t']) {
381+ case 'lit':
382+ $v = $e['v'];
383+ return fn(array $r) => $v;
384+ case 'param':
385+ $i = $e['i'];
386+ return fn(array $r) => $this->params[$i] ?? null;
387+ case 'col':
388+ $c = $this->resolve($sc, $e['tbl'], $e['name']);
389+ if ($c === null) {
390+ if (!empty($e['dq']) && $e['tbl'] === null) {
391+ $v = $e['name'];
392+ return fn(array $r) => $v;
393+ }
394+ throw new DbException('列がありません:'.($e['tbl'] !== null ? $e['tbl'].'.' : '').$e['name'], 1, 'HY000');
395+ }
396+ $o = $c['off'];
397+ if ($c['level'] === $sc->level) return fn(array $r) => $r[$o] ?? null;
398+ $L = $c['level'];
399+ return fn(array $r) => $this->stack[$L][$o] ?? null;
400+ case 'collate':
401+ return $this->compile($e['e'], $sc, $agg);
402+ case 'un':
403+ $f = $this->compile($e['e'], $sc, $agg);
404+ switch ($e['op']) {
405+ case '-':
406+ return function(array $r) use ($f) {
407+ $v = $f($r);
408+ if ($v === null) return null;
409+ $n = Value::toNumber($v);
410+ if ($n === PHP_INT_MIN) return 9.2233720368547758e18;
411+ return -$n;
412+ };
413+ case '+':
414+ return $f;
415+ case '~':
416+ return function(array $r) use ($f) {
417+ $v = $f($r);
418+ return $v === null ? null : ~(int)Value::toInt($v);
419+ };
420+ default:
421+ return function(array $r) use ($f) {
422+ $t = Value::truth($f($r));
423+ return $t === null ? null : ($t ? 0 : 1);
424+ };
425+ }
426+ case 'bin':
427+ return $this->compileBinary($e, $sc, $agg);
428+ case 'is':
429+ $l = $this->compile($e['l'], $sc, $agg);
430+ $rf = $this->compile($e['r'], $sc, $agg);
431+ $affL = $this->exprAff($e['l'], $sc);
432+ $affR = $this->exprAff($e['r'], $sc);
433+ $coll = $this->cmpColl($e['l'], $e['r'], $sc);
434+ $not = $e['not'];
435+ return function(array $r) use ($l, $rf, $affL, $affR, $coll, $not) {
436+ $a = $l($r);
437+ $b = $rf($r);
438+ if ($a === null || $b === null) $eq = $a === null && $b === null;
439+ else {
440+ [$a, $b] = Value::prepareCompare($a, $affL, $b, $affR);
441+ $eq = Record::compare($a, $b, $coll) === 0;
442+ }
443+ return ($eq xor $not) ? 1 : 0;
444+ };
445+ case 'isnull':
446+ $f = $this->compile($e['e'], $sc, $agg);
447+ $not = $e['not'];
448+ return fn(array $r) => (($f($r) === null) xor $not) ? 1 : 0;
449+ case 'between':
450+ $f = $this->compile($e['e'], $sc, $agg);
451+ $lo = $this->compile($e['lo'], $sc, $agg);
452+ $hi = $this->compile($e['hi'], $sc, $agg);
453+ $aff = $this->exprAff($e['e'], $sc);
454+ $affLo = $this->exprAff($e['lo'], $sc);
455+ $affHi = $this->exprAff($e['hi'], $sc);
456+ $coll1 = $this->cmpColl($e['e'], $e['lo'], $sc);
457+ $coll2 = $this->cmpColl($e['e'], $e['hi'], $sc);
458+ $not = $e['not'];
459+ return function(array $r) use ($f, $lo, $hi, $aff, $affLo, $affHi, $coll1, $coll2, $not) {
460+ $v = $f($r);
461+ $a = $lo($r);
462+ $b = $hi($r);
463+ $ge = $v === null || $a === null ? null : (function() use ($v, $a, $aff, $affLo, $coll1) {
464+ [$x, $y] = Value::prepareCompare($v, $aff, $a, $affLo);
465+ return Record::compare($x, $y, $coll1) >= 0;
466+ })();
467+ $le = $v === null || $b === null ? null : (function() use ($v, $b, $aff, $affHi, $coll2) {
468+ [$x, $y] = Value::prepareCompare($v, $aff, $b, $affHi);
469+ return Record::compare($x, $y, $coll2) <= 0;
470+ })();
471+ if ($ge === false || $le === false) $res = false;
472+ else if ($ge === null || $le === null) return null;
473+ else $res = true;
474+ return ($res xor $not) ? 1 : 0;
475+ };
476+ case 'like':
477+ $l = $this->compile($e['l'], $sc, $agg);
478+ $p = $this->compile($e['r'], $sc, $agg);
479+ $esc = $e['esc'] ? $this->compile($e['esc'], $sc, $agg) : null;
480+ $glob = $e['op'] === 'GLOB';
481+ $not = $e['not'];
482+ return function(array $r) use ($l, $p, $esc, $glob, $not) {
483+ $v = $l($r);
484+ $pat = $p($r);
485+ if ($v === null || $pat === null) return null;
486+ $x = $esc ? $esc($r) : null;
487+ if ($esc && ($x === null || mb_strlen((string)$x) !== 1)) throw new DbException('ESCAPEは1文字です', 1, 'HY000');
488+ $m = $glob ? Func::glob((string)Value::toText($pat), (string)Value::toText($v)) : Func::like((string)Value::toText($pat), (string)Value::toText($v), $x === null ? null : (string)$x);
489+ return ($m xor $not) ? 1 : 0;
490+ };
491+ case 'in':
492+ return $this->compileIn($e, $sc, $agg);
493+ case 'case':
494+ $base = $e['base'] ? $this->compile($e['base'], $sc, $agg) : null;
495+ $affB = $e['base'] ? $this->exprAff($e['base'], $sc) : '';
496+ $whens = [];
497+ foreach ($e['when'] as [$c, $v]) {
498+ $whens[] = [$this->compile($c, $sc, $agg), $this->compile($v, $sc, $agg), $e['base'] ? $this->exprAff($c, $sc) : '',
499+ $e['base'] ? $this->cmpColl($e['base'], $c, $sc) : 'BINARY'];
500+ }
501+ $else = $e['else'] ? $this->compile($e['else'], $sc, $agg) : null;
502+ return function(array $r) use ($base, $affB, $whens, $else) {
503+ $b = $base ? $base($r) : null;
504+ foreach ($whens as [$c, $v, $affW, $coll]) {
505+ if ($base) {
506+ $w = $c($r);
507+ if ($b === null || $w === null) continue;
508+ [$x, $y] = Value::prepareCompare($b, $affB, $w, $affW);
509+ if (Record::compare($x, $y, $coll) === 0) return $v($r);
510+ } else if (Value::truth($c($r))) {
511+ return $v($r);
512+ }
513+ }
514+ return $else ? $else($r) : null;
515+ };
516+ case 'cast':
517+ $f = $this->compile($e['e'], $sc, $agg);
518+ $aff = Value::castAffinity($e['type']);
519+ return fn(array $r) => self::cast($f($r), $aff);
520+ case 'fn':
521+ return $this->compileFunction($e, $sc, $agg);
522+ case 'exists':
523+ $plan = $this->planSelect($e['sel'], $sc);
524+ return $this->subqueryRunner($plan, $sc, 1, fn(array $rows) => empty($rows) ? 0 : 1);
525+ case 'sub':
526+ $plan = $this->planSelect($e['sel'], $sc);
527+ if (count($plan->cols) !== 1) throw new DbException('サブクエリの列は1つです('.count($plan->cols).'列)', 1, 'HY000');
528+ return $this->subqueryRunner($plan, $sc, 1, fn(array $rows) => $rows[0][0] ?? null);
529+ case 'vector':
530+ throw new DbException('行の値((a, b))は未対応です', 1, 'HY000');
531+ }
532+ throw new DbException("未対応の式:{$e['t']}", 1, 'HY000');
533+ }
534+
535+ /** サブクエリを実行するクロージャ(相関が無ければ1回だけ) */
536+ private function subqueryRunner(\stdClass $plan, \stdClass $sc, int $limit, \Closure $map): \Closure {
537+ $L = $sc->level;
538+ if (!$plan->correlated) {
539+ $done = false;
540+ $val = null;
541+ return function(array $r) use (&$done, &$val, $plan, $limit, $map, $L) {
542+ if (!$done) {
543+ $this->stack[$L] = $r;
544+ $val = $map(($plan->run)($limit));
545+ $done = true;
546+ }
547+ return $val;
548+ };
549+ }
550+ return function(array $r) use ($plan, $limit, $map, $L) {
551+ $saved = $this->stack[$L] ?? null;
552+ $this->stack[$L] = $r;
553+ try {
554+ return $map(($plan->run)($limit));
555+ } finally {
556+ $this->stack[$L] = $saved;
557+ }
558+ };
559+ }
560+
561+ private function compileBinary(array $e, \stdClass $sc, ?\stdClass $agg): \Closure {
562+ $l = $this->compile($e['l'], $sc, $agg);
563+ $rf = $this->compile($e['r'], $sc, $agg);
564+ switch ($e['op']) {
565+ case 'AND':
566+ return function(array $r) use ($l, $rf) {
567+ $a = Value::truth($l($r));
568+ if ($a === false) return 0;
569+ $b = Value::truth($rf($r));
570+ if ($b === false) return 0;
571+ return $a === null || $b === null ? null : 1;
572+ };
573+ case 'OR':
574+ return function(array $r) use ($l, $rf) {
575+ $a = Value::truth($l($r));
576+ if ($a === true) return 1;
577+ $b = Value::truth($rf($r));
578+ if ($b === true) return 1;
579+ return $a === null || $b === null ? null : 0;
580+ };
581+ case '=':
582+ case '!=':
583+ case '<':
584+ case '<=':
585+ case '>':
586+ case '>=':
587+ $affL = $this->exprAff($e['l'], $sc);
588+ $affR = $this->exprAff($e['r'], $sc);
589+ $coll = $this->cmpColl($e['l'], $e['r'], $sc);
590+ $op = $e['op'];
591+ return function(array $r) use ($l, $rf, $affL, $affR, $coll, $op) {
592+ $a = $l($r);
593+ $b = $rf($r);
594+ if ($a === null || $b === null) return null;
595+ [$a, $b] = Value::prepareCompare($a, $affL, $b, $affR);
596+ $c = Record::compare($a, $b, $coll);
597+ return match ($op) {
598+ '=' => (int)($c === 0), '!=' => (int)($c !== 0), '<' => (int)($c < 0), '<=' => (int)($c <= 0), '>' => (int)($c > 0), default => (int)($c >= 0),
599+ };
600+ };
601+ case '||':
602+ return function(array $r) use ($l, $rf) {
603+ $a = $l($r);
604+ $b = $rf($r);
605+ if ($a === null || $b === null) return null;
606+ return Value::toText($a).Value::toText($b);
607+ };
608+ case '+':
609+ case '-':
610+ case '*':
611+ $op = $e['op'];
612+ return function(array $r) use ($l, $rf, $op) {
613+ $a = $l($r);
614+ $b = $rf($r);
615+ if ($a === null || $b === null) return null;
616+ $a = Value::toNumber($a);
617+ $b = Value::toNumber($b);
618+ return match ($op) { '+' => $a + $b, '-' => $a - $b, default => $a * $b };
619+ };
620+ case '/':
621+ return function(array $r) use ($l, $rf) {
622+ $a = $l($r);
623+ $b = $rf($r);
624+ if ($a === null || $b === null) return null;
625+ $a = Value::toNumber($a);
626+ $b = Value::toNumber($b);
627+ if ($b == 0) return null;
628+ if (is_int($a) && is_int($b)) {
629+ if ($a === PHP_INT_MIN && $b === -1) return 9.2233720368547758e18;
630+ return intdiv($a, $b);
631+ }
632+ return $a / $b;
633+ };
634+ case '%':
635+ return function(array $r) use ($l, $rf) {
636+ $a = $l($r);
637+ $b = $rf($r);
638+ if ($a === null || $b === null) return null;
639+ $a = Value::toNumber($a);
640+ $b = Value::toNumber($b);
641+ if (is_int($a) && is_int($b)) {
642+ if ($b === 0) return null;
643+ if ($b === -1) return 0;
644+ return $a % $b;
645+ }
646+ // SQLite 3.45:整数にしてから計算し、REALの結果
647+ $ia = (int)Value::toInt($a);
648+ $ib = (int)Value::toInt($b);
649+ if ($ib === 0) return null;
650+ return (float)($ib === -1 ? 0 : $ia % $ib);
651+ };
652+ case '&':
653+ case '|':
654+ case '<<':
655+ case '>>':
656+ $op = $e['op'];
657+ return function(array $r) use ($l, $rf, $op) {
658+ $a = $l($r);
659+ $b = $rf($r);
660+ if ($a === null || $b === null) return null;
661+ $a = (int)Value::toInt($a);
662+ $b = (int)Value::toInt($b);
663+ switch ($op) {
664+ case '&':
665+ return $a & $b;
666+ case '|':
667+ return $a | $b;
668+ case '<<':
669+ if ($b < 0) return $b <= -64 ? ($a < 0 ? -1 : 0) : $a >> -$b;
670+ return $b >= 64 ? 0 : $a << $b;
671+ default:
672+ if ($b < 0) return $b <= -64 ? 0 : $a << -$b;
673+ return $b >= 64 ? ($a < 0 ? -1 : 0) : $a >> $b;
674+ }
675+ };
676+ }
677+ throw new DbException("未対応の演算子:{$e['op']}", 1, 'HY000');
678+ }
679+
680+ private function compileIn(array $e, \stdClass $sc, ?\stdClass $agg): \Closure {
681+ $f = $this->compile($e['e'], $sc, $agg);
682+ $aff = $this->exprAff($e['e'], $sc);
683+ $not = $e['not'];
684+ $test = function(mixed $v, iterable $list, string $affR, string $coll) use ($aff, $not) {
685+ $hasNull = false;
686+ $any = false;
687+ foreach ($list as $x) {
688+ $any = true;
689+ if ($x === null) {
690+ $hasNull = true;
691+ continue;
692+ }
693+ if ($v === null) continue;
694+ [$a, $b] = Value::prepareCompare($v, $aff, $x, $affR);
695+ if (Record::compare($a, $b, $coll) === 0) return $not ? 0 : 1;
696+ }
697+ if (!$any) return $not ? 1 : 0;
698+ if ($v === null || $hasNull) return null;
699+ return $not ? 1 : 0;
700+ };
701+ if ($e['list'] !== null) {
702+ $items = [];
703+ foreach ($e['list'] as $x) $items[] = [$this->compile($x, $sc, $agg), $this->exprAff($x, $sc), $this->cmpColl($e['e'], $x, $sc)];
704+ return function(array $r) use ($f, $items, $aff, $not) {
705+ $v = $f($r);
706+ if (empty($items)) return $not ? 1 : 0;
707+ $hasNull = false;
708+ foreach ($items as [$g, $affR, $coll]) {
709+ $x = $g($r);
710+ if ($x === null) {
711+ $hasNull = true;
712+ continue;
713+ }
714+ if ($v === null) continue;
715+ [$a, $b] = Value::prepareCompare($v, $aff, $x, $affR);
716+ if (Record::compare($a, $b, $coll) === 0) return $not ? 0 : 1;
717+ }
718+ if ($v === null || $hasNull) return null;
719+ return $not ? 1 : 0;
720+ };
721+ }
722+ $sel = $e['sel'] ?? ['t' => 'select', 'with' => [], 'cores' => [['distinct' => false, 'cols' => [['star' => null]],
723+ 'from' => [['table' => $e['table'], 'as' => null, 'join' => 'inner', 'on' => null, 'using' => null, 'natural' => false]],
724+ 'where' => null, 'group' => [], 'having' => null]], 'ops' => [], 'order' => [], 'limit' => null, 'offset' => null];
725+ $plan = $this->planSelect($sel, $sc);
726+ if (count($plan->cols) !== 1) throw new DbException('INのサブクエリの列は1つです', 1, 'HY000');
727+ $affR = $plan->affs[0] ?? '';
728+ $coll = $this->exprColl($e['e'], $sc) ?? ($plan->colls[0] ?? 'BINARY');
729+ $values = $this->subqueryRunner($plan, $sc, -1, fn(array $rows) => array_column($rows, 0));
730+ return function(array $r) use ($f, $values, $test, $affR, $coll) {
731+ return $test($f($r), $values($r), $affR, $coll);
732+ };
733+ }
734+
735+ private function compileFunction(array $e, \stdClass $sc, ?\stdClass $agg): \Closure {
736+ $name = $e['name'];
737+ if (Func::isAggregate($name, count($e['args']), $e['star'])) {
738+ if ($agg === null || $agg->inAgg) throw new DbException("集約関数の使い方が間違っています:{$name}()", 1, 'HY000');
739+ $agg->inAgg = true;
740+ $args = [];
741+ foreach ($e['args'] as $a) $args[] = $this->compile($a, $sc, $agg);
742+ $agg->inAgg = false;
743+ $k = count($agg->list);
744+ $filter = null;
745+ if (!empty($e['filter'])) {
746+ $agg->inAgg = true;
747+ $filter = $this->compile($e['filter'], $sc, $agg);
748+ $agg->inAgg = false;
749+ }
750+ $agg->list[] = ['name' => $name, 'args' => $args, 'star' => $e['star'], 'distinct' => $e['distinct'], 'filter' => $filter,
751+ 'coll' => isset($e['args'][0]) ? ($this->exprColl($e['args'][0], $sc) ?? 'BINARY') : 'BINARY'];
752+ $o = $agg->base + $k;
753+ return fn(array $r) => $r[$o] ?? null;
754+ }
755+ if ($e['star']) throw new DbException("関数{$name}(*)は使えません", 1, 'HY000');
756+ $args = [];
757+ $colls = [];
758+ foreach ($e['args'] as $a) {
759+ $args[] = $this->compile($a, $sc, $agg);
760+ $colls[] = $this->exprColl($a, $sc) ?? 'BINARY';
761+ }
762+ if (in_array($name, ['random', 'randomblob', 'changes', 'total_changes', 'last_insert_rowid', 'current_timestamp', 'current_date', 'current_time'], true)
763+ || !empty($args)) {
764+ return function(array $r) use ($name, $args, $colls) {
765+ $vals = [];
766+ foreach ($args as $g) $vals[] = $g($r);
767+ return Func::call($name, $vals, $this, $colls);
768+ };
769+ }
770+ return fn(array $r) => Func::call($name, [], $this, $colls);
771+ }
772+
773+ /** CAST */
774+ public static function cast(mixed $v, string $aff): mixed {
775+ if ($v === null) return null;
776+ switch ($aff) {
777+ case Value::INTEGER:
778+ return Value::toInt($v);
779+ case Value::REAL:
780+ return (float)Value::toNumber($v);
781+ case Value::TEXT:
782+ return Value::toText($v);
783+ case Value::BLOB:
784+ return $v instanceof Blob ? $v : new Blob((string)Value::toText($v));
785+ default:
786+ if (is_int($v) || is_float($v)) return Value::apply($v, Value::NUMERIC);
787+ $n = Value::toNumber($v);
788+ return is_float($n) && floor($n) === $n && abs($n) < 9.2e18 ? (int)$n : $n;
789+ }
790+ }
791+
792+ /** 式の親和性(列・CASTだけ) */
793+ private function exprAff(array $e, \stdClass $sc): string {
794+ if ($e['t'] === 'col') {
795+ try {
796+ $c = $this->resolve($sc, $e['tbl'], $e['name']);
797+ } catch (DbException $ex) {
798+ return '';
799+ }
800+ return $c['aff'] ?? '';
801+ }
802+ if ($e['t'] === 'cast') return Value::castAffinity($e['type']);
803+ if ($e['t'] === 'collate') return $this->exprAff($e['e'], $sc);
804+ return '';
805+ }
806+
807+ /** 式の照合順序(明示的なCOLLATE、列) */
808+ private function exprColl(array $e, \stdClass $sc, bool $explicitOnly = false): ?string {
809+ if ($e['t'] === 'collate') return $e['coll'];
810+ if ($explicitOnly) return null;
811+ if ($e['t'] === 'col') {
812+ try {
813+ $c = $this->resolve($sc, $e['tbl'], $e['name']);
814+ } catch (DbException $ex) {
815+ return null;
816+ }
817+ return $c['coll'] ?? null;
818+ }
819+ return null;
820+ }
821+
822+ /** 比較の照合順序(左の明示、右の明示、左の列、右の列) */
823+ private function cmpColl(array $l, array $r, \stdClass $sc): string {
824+ return $this->exprColl($l, $sc, true) ?? $this->exprColl($r, $sc, true) ?? $this->exprColl($l, $sc) ?? $this->exprColl($r, $sc) ?? 'BINARY';
825+ }
826+
827+ /** 式の中の集約関数が有るか */
828+ private static function hasAggregate(?array $e): bool {
829+ if ($e === null) return false;
830+ if ($e['t'] === 'fn' && Func::isAggregate($e['name'], count($e['args']), $e['star'])) return true;
831+ if (in_array($e['t'], ['sub', 'exists'], true)) return false;
832+ foreach ($e as $k => $v) {
833+ if ($k === 'sel' || !is_array($v)) continue;
834+ if (isset($v['t'])) {
835+ if (self::hasAggregate($v)) return true;
836+ } else {
837+ foreach ($v as $x) {
838+ if (is_array($x) && isset($x['t']) && self::hasAggregate($x)) return true;
839+ if (is_array($x) && !isset($x['t'])) foreach ($x as $y) if (is_array($y) && isset($y['t']) && self::hasAggregate($y)) return true;
840+ }
841+ }
842+ }
843+ return false;
844+ }
845+
846+ /** ANDで分ける */
847+ private static function conjuncts(?array $e): array {
848+ if ($e === null) return [];
849+ if ($e['t'] === 'bin' && $e['op'] === 'AND') return array_merge(self::conjuncts($e['l']), self::conjuncts($e['r']));
850+ return [$e];
851+ }
852+
853+ /**
854+ * 式が使うソース(このスコープの)
855+ *
856+ * @return array|null ソースの番号の配列、サブクエリが有ればnull(全部)
857+ */
858+ private function refs(array $e, \stdClass $sc): ?array {
859+ if (in_array($e['t'], ['sub', 'exists'], true) || ($e['t'] === 'in' && ($e['sel'] !== null || isset($e['table'])))) return null;
860+ if ($e['t'] === 'col') {
861+ $c = $this->resolve($sc, $e['tbl'], $e['name']);
862+ if ($c === null || $c['level'] !== $sc->level) return [];
863+ return [$c['src']];
864+ }
865+ $out = [];
866+ foreach ($e as $k => $v) {
867+ if (!is_array($v)) continue;
868+ $list = isset($v['t']) ? [$v] : $v;
869+ foreach ($list as $x) {
870+ if (!is_array($x)) continue;
871+ $subs = isset($x['t']) ? [$x] : $x;
872+ foreach ($subs as $y) {
873+ if (!is_array($y) || !isset($y['t'])) continue;
874+ $r = $this->refs($y, $sc);
875+ if ($r === null) return null;
876+ $out = array_merge($out, $r);
877+ }
878+ }
879+ }
880+ return array_values(array_unique($out));
881+ }
882+
883+ ///////////////
884+ // SELECT
885+
886+ /**
887+ * SELECTの計画
888+ *
889+ * @return \stdClass cols、types、affs、colls、tables、correlated、run(fn(int $limit): array 行の配列)
890+ */
891+ private function planSelect(array $sel, ?\stdClass $parent): \stdClass {
892+ $pushed = false;
893+ if (!empty($sel['with'])) {
894+ $this->cte[] = $sel['with'];
895+ $pushed = true;
896+ }
897+ try {
898+ $single = count($sel['cores']) === 1;
899+ $cores = [];
900+ $correlated = false;
901+ foreach ($sel['cores'] as $i => $core) {
902+ $cores[] = $this->planCore($core, $parent, $single ? $sel['order'] : []);
903+ $correlated = $correlated || $cores[$i]->correlated;
904+ }
905+ $first = $cores[0];
906+ foreach ($cores as $c) {
907+ if (count($c->cols) !== count($first->cols)) throw new DbException('複合のSELECTの列の数が違います', 1, 'HY000');
908+ }
909+ $limitF = $sel['limit'] ? $this->compile($sel['limit'], $this->scope([], $parent)) : null;
910+ $offsetF = $sel['offset'] ? $this->compile($sel['offset'], $this->scope([], $parent)) : null;
911+
912+ // 複合のORDER BY:出力の列
913+ $orderIdx = [];
914+ if (!$single && $sel['order']) {
915+ foreach ($sel['order'] as $o) {
916+ $k = $this->outputIndex($o['e'], $first);
917+ if ($k === null) throw new DbException('複合のSELECTのORDER BYは結果の列だけです', 1, 'HY000');
918+ $orderIdx[] = [$k, $o['desc'], $o['nulls'], $o['e']['t'] === 'collate' ? $o['e']['coll'] : ($first->colls[$k] ?? 'BINARY')];
919+ }
920+ }
921+ $ops = $sel['ops'];
922+ $order = $sel['order'];
923+ $plan = new \stdClass();
924+ $plan->cols = $first->cols;
925+ $plan->types = $first->types;
926+ $plan->affs = $first->affs;
927+ $plan->colls = $first->colls;
928+ $plan->tables = $first->tables;
929+ $plan->correlated = $correlated;
930+ $plan->run = function(int $hint) use ($cores, $single, $ops, $order, $orderIdx, $limitF, $offsetF): array {
931+ $limit = $limitF ? Value::toInt($limitF([])) : -1;
932+ $offset = $offsetF ? max(0, (int)Value::toInt($offsetF([]))) : 0;
933+ if ($limit === null) $limit = -1;
934+ if ($hint >= 0 && ($limit < 0 || $hint < $limit)) $limit = $hint;
935+ if ($single) {
936+ $rows = ($cores[0]->run)(empty($order) && $limit >= 0 ? $limit + $offset : -1);
937+ if ($order) {
938+ usort($rows, fn($a, $b) => self::compareSort($a[1], $b[1], $cores[0]->sort));
939+ }
940+ $rows = array_column($rows, 0);
941+ } else {
942+ $rows = array_column(($cores[0]->run)(-1), 0);
943+ foreach ($ops as $i => $op) {
944+ $next = array_column(($cores[$i + 1]->run)(-1), 0);
945+ $rows = self::compound($op, $rows, $next);
946+ }
947+ if ($orderIdx) {
948+ $spec = [];
949+ foreach ($orderIdx as [$k, $desc, $nulls, $coll]) $spec[] = [$desc, $nulls, $coll];
950+ usort($rows, function($a, $b) use ($orderIdx, $spec) {
951+ $ka = $kb = [];
952+ foreach ($orderIdx as [$k]) {
953+ $ka[] = $a[$k];
954+ $kb[] = $b[$k];
955+ }
956+ return self::compareSort($ka, $kb, $spec);
957+ });
958+ }
959+ }
960+ if ($offset > 0 || $limit >= 0) $rows = array_slice($rows, $offset, $limit >= 0 ? $limit : null);
961+ return $rows;
962+ };
963+ return $plan;
964+ } finally {
965+ if ($pushed) array_pop($this->cte);
966+ }
967+ }
968+
969+ private static function compound(string $op, array $a, array $b): array {
970+ $key = fn(array $row) => implode("\x1F", array_map(fn($v) => Value::key($v), $row));
971+ switch ($op) {
972+ case 'UNION ALL':
973+ return array_merge($a, $b);
974+ case 'UNION':
975+ $out = [];
976+ foreach (array_merge($a, $b) as $row) $out[$key($row)] ??= $row;
977+ return array_values($out);
978+ case 'INTERSECT':
979+ $in = [];
980+ foreach ($b as $row) $in[$key($row)] = true;
981+ $out = [];
982+ foreach ($a as $row) if (isset($in[$k = $key($row)])) $out[$k] ??= $row;
983+ return array_values($out);
984+ default:
985+ $in = [];
986+ foreach ($b as $row) $in[$key($row)] = true;
987+ $out = [];
988+ foreach ($a as $row) if (!isset($in[$k = $key($row)])) $out[$k] ??= $row;
989+ return array_values($out);
990+ }
991+ }
992+
993+ /** ORDER BYの比較 */
994+ private static function compareSort(array $a, array $b, array $spec): int {
995+ foreach ($spec as $i => [$desc, $nulls, $coll]) {
996+ $x = $a[$i];
997+ $y = $b[$i];
998+ if ($x === null || $y === null) {
999+ if ($x === $y) continue;
1000+ $nullFirst = $nulls === null ? !$desc : $nulls === 'FIRST';
1001+ return ($x === null) === $nullFirst ? -1 : 1;
1002+ }
1003+ $c = Record::compare($x, $y, $coll);
1004+ if ($c !== 0) return $desc ? -$c : $c;
1005+ }
1006+ return 0;
1007+ }
1008+
1009+ /** ORDER BYの式が結果の何番目の列か(番号、別名、同じ式) */
1010+ private function outputIndex(array $e, \stdClass $core): ?int {
1011+ if ($e['t'] === 'collate') $e = $e['e'];
1012+ if ($e['t'] === 'lit' && is_int($e['v'])) {
1013+ if ($e['v'] < 1 || $e['v'] > count($core->cols)) throw new DbException("ORDER BYの番号が範囲外です:{$e['v']}", 1, 'HY000');
1014+ return $e['v'] - 1;
1015+ }
1016+ if ($e['t'] === 'col' && $e['tbl'] === null) {
1017+ foreach ($core->aliases as $k => $a) if ($a !== null && strcasecmp($a, $e['name']) === 0) return $k;
1018+ foreach ($core->cols as $k => $n) if (strcasecmp($n, $e['name']) === 0) return $k;
1019+ }
1020+ return null;
1021+ }
1022+
1023+ /** FROMのテーブル名から、ソース(テーブル・ビュー・WITH) */
1024+ private function sourceFor(string $name, ?string $alias, ?\stdClass $parent): array {
1025+ $l = strtolower($name);
1026+ for ($i = count($this->cte) - 1; $i >= 0; $i--) {
1027+ if (isset($this->cte[$i][$l])) {
1028+ $c = $this->cte[$i][$l];
1029+ // 自分自身を参照(再帰)は未対応
1030+ $saved = $this->cte;
1031+ $this->cte = array_slice($this->cte, 0, $i);
1032+ $level = $saved[$i];
1033+ unset($level[$l]);
1034+ $this->cte[] = $level;
1035+ try {
1036+ $plan = $this->planSelect($c['sel'], null);
1037+ } catch (DbException $e) {
1038+ if ($c['recursive'] && str_contains($e->getMessage(), $name)) throw new DbException('再帰のWITHは未対応です', 1, 'HY000');
1039+ throw $e;
1040+ } finally {
1041+ $this->cte = $saved;
1042+ }
1043+ return $this->planSource($plan, $alias ?? $c['name'], $c['cols']);
1044+ }
1045+ }
1046+ $schema = $this->schema();
1047+ if (isset($schema->views[$l])) {
1048+ $v = $schema->views[$l];
1049+ $saved = $this->cte;
1050+ $this->cte = [];
1051+ try {
1052+ $plan = $this->planSelect($v['sel'], null);
1053+ } finally {
1054+ $this->cte = $saved;
1055+ }
1056+ return $this->planSource($plan, $alias ?? $v['name'], $v['cols']);
1057+ }
1058+ $t = $schema->table_($name);
1059+ if ($t === null) throw new DbException("テーブルがありません:{$name}", 1, 'HY000');
1060+ if (isset($t['unsupported'])) throw new DbException($t['unsupported'], 1, 'HY000');
1061+ $names = array_column($t['cols'], 'name');
1062+ return ['alias' => $alias ?? $t['name'], 'name' => $t['name'], 'cols' => $names, 'types' => array_column($t['cols'], 'type'),
1063+ 'affs' => array_column($t['cols'], 'aff'), 'colls' => array_column($t['cols'], 'collate'), 'table' => $t];
1064+ }
1065+
1066+ private function planSource(\stdClass $plan, string $alias, ?array $cols): array {
1067+ $names = $cols ?? $plan->cols;
1068+ if (count($names) !== count($plan->cols)) throw new DbException("{$alias}の列の数が違います", 1, 'HY000');
1069+ return ['alias' => $alias, 'name' => $alias, 'cols' => $names, 'types' => $plan->types, 'affs' => $plan->affs,
1070+ 'colls' => $plan->colls, 'table' => null, 'plan' => $plan];
1071+ }
1072+
1073+ /**
1074+ * SELECTの1つ(FROM・WHERE・GROUP BY・結果の列)
1075+ *
1076+ * @return \stdClass cols、aliases、types、affs、colls、tables、sort、correlated、run(fn(int $limit): array [値, ソートのキー]の配列)
1077+ */
1078+ private function planCore(array $core, ?\stdClass $parent, array $order): \stdClass {
1079+ if (isset($core['sub'])) {
1080+ $p = $this->planSelect($core['sub'], $parent);
1081+ $c = new \stdClass();
1082+ foreach (['cols', 'types', 'affs', 'colls', 'tables', 'correlated'] as $k) $c->$k = $p->$k;
1083+ $c->aliases = array_fill(0, count($p->cols), null);
1084+ $c->sort = [];
1085+ $c->run = fn(int $hint) => array_map(fn($r) => [$r, []], ($p->run)($hint));
1086+ return $c;
1087+ }
1088+ if (isset($core['values'])) return $this->planValues($core['values'], $parent, $order);
1089+
1090+ // FROM
1091+ $srcs = [];
1092+ foreach ($core['from'] as $item) {
1093+ if (isset($item['sel'])) {
1094+ $p = $this->planSelect($item['sel'], null);
1095+ $srcs[] = $this->planSource($p, $item['as'] ?? '(subquery-'.count($srcs).')', null);
1096+ } else {
1097+ $srcs[] = $this->sourceFor($item['table'], $item['as'], $parent);
1098+ }
1099+ }
1100+ // USING・NATURAL:右の列を隠して、条件にする
1101+ $joinOn = [];
1102+ foreach ($core['from'] as $j => $item) {
1103+ $on = $item['on'] ?? null;
1104+ $using = $item['using'] ?? null;
1105+ if ($j > 0 && ($item['natural'] || $using)) {
1106+ if ($item['natural']) {
1107+ $using = [];
1108+ $right = array_map('strtolower', $srcs[$j]['cols']);
1109+ for ($k = 0; $k < $j; $k++) {
1110+ foreach ($srcs[$k]['cols'] as $n) if (in_array(strtolower($n), $right, true) && !in_array(strtolower($n), array_map('strtolower', $using), true)) $using[] = $n;
1111+ }
1112+ }
1113+ foreach ($using as $n) {
1114+ $ln = strtolower($n);
1115+ $leftSrc = null;
1116+ for ($k = 0; $k < $j; $k++) if (in_array($ln, array_map('strtolower', $srcs[$k]['cols']), true) && !isset($srcs[$k]['hidden'][$ln])) $leftSrc = $k;
1117+ if ($leftSrc === null || !in_array($ln, array_map('strtolower', $srcs[$j]['cols']), true)) throw new DbException("USINGの列がありません:{$n}", 1, 'HY000');
1118+ $cond = ['t' => 'bin', 'op' => '=', 'l' => ['t' => 'col', 'tbl' => $srcs[$leftSrc]['alias'], 'name' => $n],
1119+ 'r' => ['t' => 'col', 'tbl' => $srcs[$j]['alias'], 'name' => $n]];
1120+ $on = $on ? ['t' => 'bin', 'op' => 'AND', 'l' => $on, 'r' => $cond] : $cond;
1121+ $srcs[$j]['hidden'][$ln] = true;
1122+ }
1123+ }
1124+ $joinOn[$j] = $on;
1125+ }
1126+ $sc = $this->scope($srcs, $parent);
1127+
1128+ // 結果の列の別名(WHERE・GROUP BY・HAVINGでも使える、SQLiteの拡張)
1129+ $aliasMap = [];
1130+ foreach ($core['cols'] as $c) {
1131+ if (isset($c['e']) && $c['as'] !== null) $aliasMap[strtolower($c['as'])] ??= $c['e'];
1132+ }
1133+ if ($aliasMap) {
1134+ if ($core['where']) $core['where'] = $this->substAliases($core['where'], $aliasMap, $sc0 = $this->scope($srcs, $parent));
1135+ if ($core['having']) $core['having'] = $this->substAliases($core['having'], $aliasMap, $sc0 ??= $this->scope($srcs, $parent));
1136+ foreach ($core['group'] as $k => $g) $core['group'][$k] = $this->substAliases($g, $aliasMap, $sc0 ??= $this->scope($srcs, $parent));
1137+ }
1138+
1139+ // 条件の場所
1140+ $n = count($srcs);
1141+ $where = self::conjuncts($core['where']);
1142+ $levelOn = array_fill(0, max(1, $n), []);
1143+ $levelPost = array_fill(0, max(1, $n), []);
1144+ $levelCand = array_fill(0, max(1, $n), []);
1145+ foreach ($core['from'] as $j => $item) {
1146+ if ($joinOn[$j] === null) continue;
1147+ if ($item['join'] === 'left') {
1148+ foreach (self::conjuncts($joinOn[$j]) as $c) {
1149+ $levelOn[$j][] = $c;
1150+ $levelCand[$j][] = $c;
1151+ }
1152+ } else {
1153+ $where = array_merge($where, self::conjuncts($joinOn[$j]));
1154+ }
1155+ }
1156+ foreach ($where as $c) {
1157+ if (self::hasAggregate($c)) throw new DbException('WHEREに集約関数は使えません', 1, 'HY000');
1158+ $r = $this->refs($c, $sc);
1159+ $lvl = $r === null ? max(0, $n - 1) : (empty($r) ? 0 : max($r));
1160+ $levelPost[$lvl][] = $c;
1161+ // LEFT JOINの右のテーブルの条件は検索に使わない
1162+ if ($r !== null && ($core['from'][$lvl]['join'] ?? 'inner') !== 'left') $levelCand[$lvl][] = $c;
1163+ }
1164+ $levels = [];
1165+ for ($j = 0; $j < $n; $j++) {
1166+ $levels[] = [
1167+ 'src' => $this->access($srcs[$j], $j, $levelCand[$j], $sc),
1168+ 'on' => array_map(fn($c) => $this->compile($c, $sc), $levelOn[$j]),
1169+ 'post' => array_map(fn($c) => $this->compile($c, $sc), $levelPost[$j]),
1170+ 'left' => ($core['from'][$j]['join'] ?? 'inner') === 'left',
1171+ 'nulls' => array_fill(0, count($srcs[$j]['cols']) + 1, null),
1172+ ];
1173+ }
1174+ $constFilters = $n === 0 ? array_map(fn($c) => $this->compile($c, $sc), $levelPost[0]) : [];
1175+
1176+ // 結果の列
1177+ $isAgg = !empty($core['group']) || self::hasAggregate($core['having']);
1178+ foreach ($core['cols'] as $c) if (isset($c['e']) && self::hasAggregate($c['e'])) $isAgg = true;
1179+ foreach ($order as $o) if (self::hasAggregate($o['e'])) $isAgg = true;
1180+ $agg = null;
1181+ if ($isAgg) {
1182+ $agg = new \stdClass();
1183+ $agg->list = [];
1184+ $agg->base = $sc->width;
1185+ $agg->inAgg = false;
1186+ }
1187+ $outFns = $cols = $aliases = $types = $affs = $colls = $tables = [];
1188+ foreach ($core['cols'] as $c) {
1189+ if (array_key_exists('star', $c)) {
1190+ $found = false;
1191+ foreach ($sc->srcs as $j => $s) {
1192+ if ($c['star'] !== null && strcasecmp($c['star'], (string)$s['alias']) !== 0) continue;
1193+ $found = true;
1194+ foreach ($s['cols'] as $i => $name) {
1195+ if ($c['star'] === null && isset($s['hidden'][strtolower($name)])) continue;
1196+ $o = $s['base'] + $i;
1197+ $outFns[] = fn(array $r) => $r[$o] ?? null;
1198+ $cols[] = $name;
1199+ $aliases[] = null;
1200+ $types[] = $s['types'][$i] ?? '';
1201+ $affs[] = $s['affs'][$i] ?? '';
1202+ $colls[] = $s['colls'][$i] ?? 'BINARY';
1203+ $tables[] = $s['table']['name'] ?? '';
1204+ }
1205+ }
1206+ if (!$found) throw new DbException($c['star'] === null ? 'テーブルが指定されていません' : "テーブルがありません:{$c['star']}", 1, 'HY000');
1207+ continue;
1208+ }
1209+ $outFns[] = $this->compile($c['e'], $sc, $agg);
1210+ $name = $c['as'];
1211+ $info = $c['e']['t'] === 'col' ? $this->resolve($sc, $c['e']['tbl'], $c['e']['name']) : null;
1212+ if ($name === null) $name = $info !== null ? $info['name'] : $c['text'];
1213+ $cols[] = $name;
1214+ $aliases[] = $c['as'];
1215+ $types[] = $info['type'] ?? ($c['e']['t'] === 'cast' ? $c['e']['type'] : '');
1216+ $affs[] = $this->exprAff($c['e'], $sc);
1217+ $colls[] = $this->exprColl($c['e'], $sc) ?? 'BINARY';
1218+ $tables[] = $info !== null && $info['level'] === $sc->level ? (string)($sc->srcs[$info['src']]['table']['name'] ?? '') : '';
1219+ }
1220+ $groupFns = array_map(fn($g) => [$this->compileGroupTerm($g, $core, $sc, $cols, $aliases, $outFns), $this->exprColl($g, $sc) ?? 'BINARY'], $core['group']);
1221+ $having = $core['having'] ? $this->compile($core['having'], $sc, $agg) : null;
1222+ if ($having && !$isAgg) throw new DbException('HAVINGはGROUP BYと一緒に使います', 1, 'HY000');
1223+
1224+ // ORDER BY(単一のSELECT)
1225+ $sortFns = [];
1226+ $sort = [];
1227+ $tmp = new \stdClass();
1228+ $tmp->cols = $cols;
1229+ $tmp->aliases = $aliases;
1230+ foreach ($order as $o) {
1231+ $k = $this->outputIndex($o['e'], $tmp);
1232+ if ($k !== null) {
1233+ $sortFns[] = ['out', $k];
1234+ $coll = $o['e']['t'] === 'collate' ? $o['e']['coll'] : $colls[$k];
1235+ } else {
1236+ $sortFns[] = ['expr', $this->compile($o['e'], $sc, $agg)];
1237+ $coll = $this->exprColl($o['e'], $sc) ?? 'BINARY';
1238+ }
1239+ $sort[] = [$o['desc'], $o['nulls'], $coll];
1240+ }
1241+
1242+ $distinct = $core['distinct'];
1243+ $aggList = $agg ? $agg->list : [];
1244+ $hasGroup = !empty($core['group']);
1245+ $width = $sc->width;
1246+ $out = new \stdClass();
1247+ $out->cols = $cols;
1248+ $out->aliases = $aliases;
1249+ $out->types = $types;
1250+ $out->affs = $affs;
1251+ $out->colls = $colls;
1252+ $out->tables = $tables;
1253+ $out->sort = $sort;
1254+ $out->correlated = $sc->usedOuter;
1255+ $out->run = function(int $hint) use ($levels, $n, $constFilters, $outFns, $sortFns, $distinct, $isAgg, $aggList, $hasGroup, $groupFns, $having, $width): array {
1256+ $emit = function(array $r) use ($outFns, $sortFns): array {
1257+ $vals = [];
1258+ foreach ($outFns as $f) $vals[] = $f($r);
1259+ $keys = [];
1260+ foreach ($sortFns as [$kind, $x]) $keys[] = $kind === 'out' ? $vals[$x] : $x($r);
1261+ return [$vals, $keys];
1262+ };
1263+ $rows = [];
1264+ $seen = [];
1265+ if ($n === 0) {
1266+ foreach ($constFilters as $f) if (!Value::truth($f([]))) return $isAgg && !$hasGroup ? $this->aggregateRows([], $aggList, $hasGroup, $groupFns, $having, $width, $emit, $distinct) : [];
1267+ $gen = (function() { yield []; })();
1268+ } else {
1269+ $gen = $this->joinLoop($levels, 0, []);
1270+ }
1271+ if ($isAgg) return $this->aggregateRows($gen, $aggList, $hasGroup, $groupFns, $having, $width, $emit, $distinct);
1272+ foreach ($gen as $r) {
1273+ $row = $emit($r);
1274+ if ($distinct) {
1275+ $k = implode("\x1F", array_map(fn($v) => Value::key($v), $row[0]));
1276+ if (isset($seen[$k])) continue;
1277+ $seen[$k] = true;
1278+ }
1279+ $rows[] = $row;
1280+ if ($hint >= 0 && count($rows) >= $hint) break;
1281+ }
1282+ return $rows;
1283+ };
1284+ return $out;
1285+ }
1286+
1287+ /** 列が見つからない名前を結果の列の別名の式にする */
1288+ private function substAliases(array $e, array $map, \stdClass $sc): array {
1289+ if ($e['t'] === 'col') {
1290+ if ($e['tbl'] === null && isset($map[strtolower($e['name'])]) && $this->resolve($sc, null, $e['name']) === null) return $map[strtolower($e['name'])];
1291+ return $e;
1292+ }
1293+ if (in_array($e['t'], ['sub', 'exists'], true)) return $e;
1294+ foreach ($e as $k => $v) {
1295+ if (!is_array($v) || $k === 'sel') continue;
1296+ if (isset($v['t'])) {
1297+ $e[$k] = $this->substAliases($v, $map, $sc);
1298+ } else {
1299+ foreach ($v as $i => $x) {
1300+ if (is_array($x) && isset($x['t'])) $e[$k][$i] = $this->substAliases($x, $map, $sc);
1301+ else if (is_array($x)) foreach ($x as $m => $y) if (is_array($y) && isset($y['t'])) $e[$k][$i][$m] = $this->substAliases($y, $map, $sc);
1302+ }
1303+ }
1304+ }
1305+ return $e;
1306+ }
1307+
1308+ /** GROUP BYの項(番号・別名は結果の列) */
1309+ private function compileGroupTerm(array $g, array $core, \stdClass $sc, array $cols, array $aliases, array $outFns): \Closure {
1310+ if ($g['t'] === 'lit' && is_int($g['v'])) {
1311+ if ($g['v'] < 1 || $g['v'] > count($cols)) throw new DbException("GROUP BYの番号が範囲外です:{$g['v']}", 1, 'HY000');
1312+ $k = $g['v'] - 1;
1313+ $e = $core['cols'][$k]['e'] ?? null;
1314+ if ($e === null) throw new DbException('GROUP BYの番号が「*」です', 1, 'HY000');
1315+ return $this->compile($e, $sc);
1316+ }
1317+ if ($g['t'] === 'col' && $g['tbl'] === null && $this->resolve($sc, null, $g['name']) === null) {
1318+ foreach ($aliases as $k => $a) {
1319+ if ($a !== null && strcasecmp($a, $g['name']) === 0) return $this->compile($core['cols'][$k]['e'], $sc);
1320+ }
1321+ }
1322+ if (self::hasAggregate($g)) throw new DbException('GROUP BYに集約関数は使えません', 1, 'HY000');
1323+ return $this->compile($g, $sc);
1324+ }
1325+
1326+ /** 集約(グループ毎) */
1327+ private function aggregateRows(iterable $gen, array $aggList, bool $hasGroup, array $groupFns, ?\Closure $having, int $width, \Closure $emit, bool $distinct): array {
1328+ $groups = [];
1329+ // min()・max()が1つだけなら、他の列はその行の値(SQLiteと同じ)
1330+ $minmax = null;
1331+ if (count($aggList) === 1 && in_array($aggList[0]['name'], ['min', 'max'], true)) $minmax = 0;
1332+ foreach ($gen as $r) {
1333+ $gk = '';
1334+ $gv = [];
1335+ foreach ($groupFns as [$f, $coll]) {
1336+ $v = $f($r);
1337+ $gv[] = $v;
1338+ $gk .= Value::key($v, $coll)."\x1F";
1339+ }
1340+ if (!isset($groups[$gk])) {
1341+ $st = [];
1342+ foreach ($aggList as $a) $st[] = Func::aggInit($a['name']);
1343+ $groups[$gk] = ['row' => $r, 'st' => $st, 'seen' => [], 'key' => $gv];
1344+ }
1345+ $g = &$groups[$gk];
1346+ if ($minmax !== null) $before = $g['st'][0]['v'];
1347+ foreach ($aggList as $k => $a) {
1348+ if ($a['filter'] && !Value::truth(($a['filter'])($r))) continue;
1349+ $args = [];
1350+ foreach ($a['args'] as $f) $args[] = $f($r);
1351+ if ($a['distinct']) {
1352+ if (($args[0] ?? null) === null) continue;
1353+ $dk = Value::key($args[0], $a['coll']);
1354+ if (isset($g['seen'][$k][$dk])) continue;
1355+ $g['seen'][$k][$dk] = true;
1356+ }
1357+ Func::aggStep($a['name'], $g['st'][$k], $args, $a['star'], $a['coll']);
1358+ }
1359+ if ($minmax !== null && $g['st'][0]['v'] !== $before) $g['row'] = $r;
1360+ unset($g);
1361+ }
1362+ // GROUP BYはキーの順番(SQLiteと同じ)
1363+ if ($hasGroup && count($groups) > 1) {
1364+ $spec = array_map(fn($x) => [false, null, $x[1]], $groupFns);
1365+ uasort($groups, fn($a, $b) => self::compareSort($a['key'], $b['key'], $spec));
1366+ }
1367+ if (!$hasGroup && empty($groups)) {
1368+ $st = [];
1369+ foreach ($aggList as $a) $st[] = Func::aggInit($a['name']);
1370+ $groups[''] = ['row' => array_fill(0, $width, null), 'st' => $st];
1371+ }
1372+ $rows = [];
1373+ $seen = [];
1374+ foreach ($groups as $g) {
1375+ $er = $g['row'];
1376+ foreach ($aggList as $k => $a) $er[$width + $k] = Func::aggFinal($a['name'], $g['st'][$k]);
1377+ if ($having && !Value::truth($having($er))) continue;
1378+ $row = $emit($er);
1379+ if ($distinct) {
1380+ $key = implode("\x1F", array_map(fn($v) => Value::key($v), $row[0]));
1381+ if (isset($seen[$key])) continue;
1382+ $seen[$key] = true;
1383+ }
1384+ $rows[] = $row;
1385+ }
1386+ return $rows;
1387+ }
1388+
1389+ /** VALUES (…), (…) */
1390+ private function planValues(array $values, ?\stdClass $parent, array $order): \stdClass {
1391+ $sc = $this->scope([], $parent);
1392+ $rows = [];
1393+ $nc = count($values[0]);
1394+ foreach ($values as $row) {
1395+ if (count($row) !== $nc) throw new DbException('VALUESの行の値の数が違います', 1, 'HY000');
1396+ $rows[] = array_map(fn($e) => $this->compile($e, $sc), $row);
1397+ }
1398+ $out = new \stdClass();
1399+ $out->cols = array_map(fn($i) => 'column'.($i + 1), range(0, $nc - 1));
1400+ $out->aliases = array_fill(0, $nc, null);
1401+ $out->types = array_fill(0, $nc, '');
1402+ $out->affs = array_fill(0, $nc, '');
1403+ $out->colls = array_fill(0, $nc, 'BINARY');
1404+ $out->tables = array_fill(0, $nc, '');
1405+ $out->correlated = $sc->usedOuter;
1406+ $sortIdx = [];
1407+ foreach ($order as $o) {
1408+ $k = $this->outputIndex($o['e'], $out);
1409+ if ($k === null) throw new DbException('VALUESのORDER BYは列の番号です', 1, 'HY000');
1410+ $sortIdx[] = $k;
1411+ $out->sort[] = [$o['desc'], $o['nulls'], 'BINARY'];
1412+ }
1413+ $out->sort ??= [];
1414+ $out->run = function(int $hint) use ($rows, $sortIdx): array {
1415+ $res = [];
1416+ foreach ($rows as $fns) {
1417+ $vals = array_map(fn($f) => $f([]), $fns);
1418+ $res[] = [$vals, array_map(fn($k) => $vals[$k], $sortIdx)];
1419+ }
1420+ return $res;
1421+ };
1422+ return $out;
1423+ }
1424+
1425+ /** 入れ子のループ(LEFT JOINはNULLの行も) */
1426+ private function joinLoop(array $levels, int $j, array $row): \Generator {
1427+ if ($j === count($levels)) {
1428+ yield $row;
1429+ return;
1430+ }
1431+ $L = $levels[$j];
1432+ $matched = false;
1433+ foreach (($L['src'])($row) as $vals) {
1434+ $r = $j === 0 ? $vals : array_merge($row, $vals);
1435+ foreach ($L['on'] as $f) if (!Value::truth($f($r))) continue 2;
1436+ $matched = true;
1437+ foreach ($L['post'] as $f) if (!Value::truth($f($r))) continue 2;
1438+ yield from $this->joinLoop($levels, $j + 1, $r);
1439+ }
1440+ if (!$matched && $L['left']) {
1441+ $r = array_merge($row, $L['nulls']);
1442+ foreach ($L['post'] as $f) if (!Value::truth($f($r))) return;
1443+ yield from $this->joinLoop($levels, $j + 1, $r);
1444+ }
1445+ }
1446+
1447+ ///////////////
1448+ // テーブルの読み方(rowid、インデックス、全部)
1449+
1450+ /** テーブルの行の値(足りない列はDEFAULT、rowidの別名、最後にrowid) */
1451+ private function rowReader(array $t): \Closure {
1452+ $nc = count($t['cols']);
1453+ $ipk = $t['ipk'] ?? null;
1454+ $defaults = [];
1455+ foreach ($t['cols'] as $i => $c) {
1456+ $defaults[$i] = $c['default'] !== null ? Value::apply($this->constValue($c['default']), $c['aff']) : null;
1457+ }
1458+ return function(int $rowid, string $rec) use ($nc, $ipk, $defaults): array {
1459+ $v = Record::decode($rec);
1460+ $k = count($v);
1461+ if ($k < $nc) {
1462+ for ($i = $k; $i < $nc; $i++) $v[$i] = $defaults[$i];
1463+ } else if ($k > $nc) {
1464+ $v = array_slice($v, 0, $nc);
1465+ }
1466+ if ($ipk !== null) $v[$ipk] = $rowid;
1467+ $v[] = $rowid;
1468+ return $v;
1469+ };
1470+ }
1471+
1472+ /** 定数の式の値(DEFAULT) */
1473+ private function constValue(array $e): mixed {
1474+ return ($this->compile($e, $this->scope([], null)))([]);
1475+ }
1476+
1477+ /**
1478+ * ソースの行を出すクロージャ(条件からrowid・インデックスを選ぶ)
1479+ *
1480+ * @return \Closure fn(array $row): iterable 値の配列
1481+ */
1482+ private function access(array $src, int $j, array $cands, \stdClass $sc): \Closure {
1483+ if (isset($src['plan'])) {
1484+ $plan = $src['plan'];
1485+ $cache = null;
1486+ return function(array $row) use ($plan, &$cache) {
1487+ if ($cache === null) {
1488+ $cache = [];
1489+ foreach (($plan->run)(-1) as $vals) {
1490+ $vals[] = null;
1491+ $cache[] = $vals;
1492+ }
1493+ }
1494+ return $cache;
1495+ };
1496+ }
1497+ $t = $src['table'];
1498+ $root = $t['root'];
1499+ $read = $this->rowReader($t);
1500+ $this->lastAccess = null;
1501+ $bt = $this->bt;
1502+ $schema = $this->schema();
1503+
1504+ // 条件を調べる:このソースの列 演算子 他のソース・定数の式
1505+ $eq = [];
1506+ $ranges = [];
1507+ $ins = [];
1508+ foreach ($cands as $c) {
1509+ if ($c['t'] === 'bin' && in_array($c['op'], ['=', '<', '<=', '>', '>='], true)) {
1510+ foreach ([[$c['l'], $c['r'], $c['op']], [$c['r'], $c['l'], self::flip($c['op'])]] as [$a, $b, $op]) {
1511+ $col = $this->columnOf($a, $j, $sc);
1512+ if ($col === null) continue;
1513+ $r = $this->refs($b, $sc);
1514+ if ($r === null || in_array($j, $r, true) || ($r && max($r) > $j)) continue;
1515+ $info = ['fn' => $this->compile($b, $sc), 'aff' => $this->exprAff($b, $sc), 'coll' => $this->cmpColl($c['l'], $c['r'], $sc), 'op' => $op];
1516+ if ($op === '=') $eq[$col] ??= $info;
1517+ else $ranges[$col][] = $info;
1518+ break;
1519+ }
1520+ } else if ($c['t'] === 'in' && !$c['not'] && $c['list'] !== null && count($c['list']) > 0) {
1521+ $col = $this->columnOf($c['e'], $j, $sc);
1522+ if ($col === null) continue;
1523+ $ok = true;
1524+ $fns = [];
1525+ foreach ($c['list'] as $x) {
1526+ $r = $this->refs($x, $sc);
1527+ if ($r === null || in_array($j, $r, true) || ($r && max($r) > $j)) {
1528+ $ok = false;
1529+ break;
1530+ }
1531+ $fns[] = [$this->compile($x, $sc), $this->exprAff($x, $sc), $this->cmpColl($c['e'], $x, $sc)];
1532+ }
1533+ if ($ok) $ins[$col] ??= $fns;
1534+ } else if ($c['t'] === 'between' && !$c['not']) {
1535+ $col = $this->columnOf($c['e'], $j, $sc);
1536+ if ($col === null) continue;
1537+ $r1 = $this->refs($c['lo'], $sc);
1538+ $r2 = $this->refs($c['hi'], $sc);
1539+ if ($r1 === null || $r2 === null || in_array($j, array_merge($r1, $r2), true) || (($r1 || $r2) && max(array_merge($r1, $r2)) > $j)) continue;
1540+ $ranges[$col][] = ['fn' => $this->compile($c['lo'], $sc), 'aff' => $this->exprAff($c['lo'], $sc), 'coll' => $this->cmpColl($c['e'], $c['lo'], $sc), 'op' => '>='];
1541+ $ranges[$col][] = ['fn' => $this->compile($c['hi'], $sc), 'aff' => $this->exprAff($c['hi'], $sc), 'coll' => $this->cmpColl($c['e'], $c['hi'], $sc), 'op' => '<='];
1542+ }
1543+ }
1544+
1545+ // rowid
1546+ if (isset($eq['rowid'])) {
1547+ $f = $eq['rowid']['fn'];
1548+ $aff = $eq['rowid']['aff'];
1549+ return function(array $row) use ($f, $aff, $bt, $root, $read) {
1550+ $id = self::rowidKey($f($row), $aff);
1551+ if ($id === null) return;
1552+ $rec = $bt->get($root, $id);
1553+ if ($rec !== null) yield $read($id, $rec);
1554+ };
1555+ }
1556+ if (isset($ins['rowid'])) {
1557+ $fns = $ins['rowid'];
1558+ return function(array $row) use ($fns, $bt, $root, $read) {
1559+ $ids = [];
1560+ foreach ($fns as [$f, $aff]) {
1561+ $id = self::rowidKey($f($row), $aff);
1562+ if ($id !== null) $ids[$id] = true;
1563+ }
1564+ $ids = array_keys($ids);
1565+ sort($ids);
1566+ foreach ($ids as $id) {
1567+ $rec = $bt->get($root, $id);
1568+ if ($rec !== null) yield $read($id, $rec);
1569+ }
1570+ };
1571+ }
1572+
1573+ // インデックス(等号の列が一番多い物)
1574+ $best = null;
1575+ $bestN = 0;
1576+ foreach ($t['indexes'] as $iname) {
1577+ $idx = $schema->indexes[$iname];
1578+ $k = 0;
1579+ foreach ($idx['cols'] as $p => $ci) {
1580+ if (!isset($eq[$ci]) || $eq[$ci]['coll'] !== $idx['colls'][$p]) break;
1581+ $k++;
1582+ }
1583+ $score = $k * 2 + ($idx['unique'] && $k === count($idx['cols']) ? 1 : 0);
1584+ if ($k > 0 && $score > $bestN) {
1585+ $best = $idx;
1586+ $bestN = $score;
1587+ }
1588+ }
1589+ if ($best !== null) {
1590+ $this->lastAccess = $best['cols'];
1591+ $parts = [];
1592+ foreach ($best['cols'] as $p => $ci) {
1593+ if (!isset($eq[$ci]) || $eq[$ci]['coll'] !== $best['colls'][$p]) break;
1594+ $parts[] = [$eq[$ci]['fn'], $eq[$ci]['aff'], $t['cols'][$ci]['aff']];
1595+ }
1596+ return function(array $row) use ($parts, $best, $bt, $root, $read) {
1597+ $key = [];
1598+ foreach ($parts as [$f, $aff, $colAff]) {
1599+ $v = $f($row);
1600+ if ($v === null) return;
1601+ $key[] = self::lookupValue($v, $colAff, $aff);
1602+ }
1603+ $ids = [];
1604+ foreach ($bt->indexScan($best, $key, true, $key, true) as $k) $ids[] = end($k);
1605+ foreach ($ids as $id) {
1606+ $rec = $bt->get($root, $id);
1607+ if ($rec !== null) yield $read($id, $rec);
1608+ }
1609+ };
1610+ }
1611+ // INのインデックス(最初の列)
1612+ foreach ($ins as $ci => $fns) {
1613+ if ($ci === 'rowid') continue;
1614+ foreach ($t['indexes'] as $iname) {
1615+ $idx = $schema->indexes[$iname];
1616+ if ($idx['cols'][0] !== $ci || $fns[0][2] !== $idx['colls'][0]) continue;
1617+ $this->lastAccess = $idx['cols'];
1618+ $colAff = $t['cols'][$ci]['aff'];
1619+ return function(array $row) use ($fns, $idx, $bt, $root, $read, $colAff) {
1620+ $done = [];
1621+ foreach ($fns as [$f, $aff]) {
1622+ $v = $f($row);
1623+ if ($v === null) continue;
1624+ $v = self::lookupValue($v, $colAff, $aff);
1625+ $dk = Value::key($v);
1626+ if (isset($done[$dk])) continue;
1627+ $done[$dk] = true;
1628+ $ids = [];
1629+ foreach ($bt->indexScan($idx, [$v], true, [$v], true) as $k) $ids[] = end($k);
1630+ foreach ($ids as $id) {
1631+ $rec = $bt->get($root, $id);
1632+ if ($rec !== null) yield $read($id, $rec);
1633+ }
1634+ }
1635+ };
1636+ }
1637+ }
1638+ // rowidの範囲
1639+ if (isset($ranges['rowid'])) {
1640+ $rs = $ranges['rowid'];
1641+ return function(array $row) use ($rs, $bt, $root, $read) {
1642+ $from = null;
1643+ $to = null;
1644+ foreach ($rs as $r) {
1645+ $v = $r['fn']($row);
1646+ if ($v === null) return;
1647+ if (is_string($v)) $v = Value::apply($v, Value::NUMERIC);
1648+ if (!is_int($v) && !is_float($v)) {
1649+ // 文字列・BLOBは全部の数値より大きい
1650+ if ($r['op'] === '>' || $r['op'] === '>=') return;
1651+ continue;
1652+ }
1653+ [$lo, $hi] = self::rowidBounds($r['op'], $v);
1654+ if ($lo !== null) $from = $from === null ? $lo : max($from, $lo);
1655+ if ($hi !== null) $to = $to === null ? $hi : min($to, $hi);
1656+ }
1657+ if ($from !== null && $to !== null && $from > $to) return;
1658+ foreach ($bt->scan($root, $from, $to) as $id => $rec) yield $read($id, $rec);
1659+ };
1660+ }
1661+ // インデックスの範囲(最初の列、昇順)
1662+ foreach ($ranges as $ci => $rs) {
1663+ if ($ci === 'rowid') continue;
1664+ foreach ($t['indexes'] as $iname) {
1665+ $idx = $schema->indexes[$iname];
1666+ if ($idx['cols'][0] !== $ci || $idx['desc'][0]) continue;
1667+ $ok = true;
1668+ foreach ($rs as $r) if ($r['coll'] !== $idx['colls'][0]) $ok = false;
1669+ if (!$ok) continue;
1670+ $this->lastAccess = $idx['cols'];
1671+ $colAff = $t['cols'][$ci]['aff'];
1672+ return function(array $row) use ($rs, $idx, $bt, $root, $read, $colAff) {
1673+ $lo = [null];
1674+ $loInc = false;
1675+ $hi = null;
1676+ $hiInc = true;
1677+ foreach ($rs as $r) {
1678+ $v = $r['fn']($row);
1679+ if ($v === null) return;
1680+ $v = self::lookupValue($v, $colAff, $r['aff']);
1681+ if ($r['op'] === '>' || $r['op'] === '>=') {
1682+ $c = $lo[0] === null ? 1 : Record::compare($v, $lo[0], $idx['colls'][0]);
1683+ if ($c > 0 || ($c === 0 && $r['op'] === '>')) {
1684+ $lo = [$v];
1685+ $loInc = $r['op'] === '>=';
1686+ }
1687+ } else {
1688+ $c = $hi === null ? -1 : Record::compare($v, $hi[0], $idx['colls'][0]);
1689+ if ($c < 0 || ($c === 0 && $r['op'] === '<')) {
1690+ $hi = [$v];
1691+ $hiInc = $r['op'] === '<=';
1692+ }
1693+ }
1694+ }
1695+ $ids = [];
1696+ foreach ($bt->indexScan($idx, $lo, $loInc, $hi, $hiInc) as $k) $ids[] = end($k);
1697+ foreach ($ids as $id) {
1698+ $rec = $bt->get($root, $id);
1699+ if ($rec !== null) yield $read($id, $rec);
1700+ }
1701+ };
1702+ }
1703+ }
1704+ // 全部
1705+ return function(array $row) use ($bt, $root, $read) {
1706+ foreach ($bt->scan($root) as $id => $rec) yield $read($id, $rec);
1707+ };
1708+ }
1709+
1710+ private static function flip(string $op): string {
1711+ return match ($op) { '<' => '>', '<=' => '>=', '>' => '<', '>=' => '<=', default => $op };
1712+ }
1713+
1714+ /** 式がソース$jの列なら列の番号(rowidは'rowid') */
1715+ private function columnOf(array $e, int $j, \stdClass $sc): int|string|null {
1716+ if ($e['t'] !== 'col') return null;
1717+ $c = $this->resolve($sc, $e['tbl'], $e['name']);
1718+ if ($c === null || $c['level'] !== $sc->level || $c['src'] !== $j) return null;
1719+ $src = $sc->srcs[$j];
1720+ if ($c['off'] === $src['rowid']) return 'rowid';
1721+ if (($src['table']['ipk'] ?? null) === $c['col']) return 'rowid';
1722+ return $c['col'];
1723+ }
1724+
1725+ /** rowidの検索の値(整数でなければnull) */
1726+ private static function rowidKey(mixed $v, string $aff): ?int {
1727+ if ($v === null) return null;
1728+ if (is_string($v)) $v = Value::apply($v, Value::NUMERIC);
1729+ if (is_float($v) && floor($v) === $v && abs($v) < 9.2e18) $v = (int)$v;
1730+ return is_int($v) ? $v : null;
1731+ }
1732+
1733+ private static function rowidBounds(string $op, int|float $v): array {
1734+ if (is_float($v)) {
1735+ if (is_nan($v)) return [PHP_INT_MAX, PHP_INT_MIN];
1736+ if ($v >= 9.2233720368547758e18) return $op === '>' || $op === '>=' ? [PHP_INT_MAX, null] : [null, PHP_INT_MAX];
1737+ if ($v <= -9.2233720368547758e18) return $op === '>' || $op === '>=' ? [PHP_INT_MIN, null] : [null, PHP_INT_MIN];
1738+ $fl = (int)floor($v);
1739+ $ce = (int)ceil($v);
1740+ return match ($op) { '>' => [$fl + 1, null], '>=' => [$ce, null], '<' => [null, $ce - 1], default => [null, $fl] };
1741+ }
1742+ return match ($op) {
1743+ '>' => [$v === PHP_INT_MAX ? PHP_INT_MAX : $v + 1, $v === PHP_INT_MAX ? PHP_INT_MIN : null],
1744+ '>=' => [$v, null],
1745+ '<' => [null, $v === PHP_INT_MIN ? PHP_INT_MIN : $v - 1],
1746+ default => [null, $v],
1747+ };
1748+ }
1749+
1750+ /** インデックスで探す値(比較と同じ親和性) */
1751+ private static function lookupValue(mixed $v, string $colAff, string $exprAff): mixed {
1752+ [$col, $x] = Value::prepareCompare(null, $colAff, $v, $exprAff);
1753+ return $x;
1754+ }
1755+
1756+ ///////////////
1757+ // INSERT・UPDATE・DELETE
1758+
1759+ private function writableTable(string $name): array {
1760+ $schema = $this->schema();
1761+ $l = strtolower($name);
1762+ if (isset($schema->views[$l])) throw new DbException("ビューは変更できません:{$name}", 1, 'HY000');
1763+ $t = $schema->table_($name);
1764+ if ($t === null) throw new DbException("テーブルがありません:{$name}", 1, 'HY000');
1765+ if (isset($t['unsupported'])) throw new DbException($t['unsupported'], 1, 'HY000');
1766+ if (!empty($t['system'])) throw new DbException("テーブル{$t['name']}は変更できません", 1, 'HY000');
1767+ if (isset($schema->triggers[strtolower($t['name'])])) throw new DbException("トリガーの有るテーブルの変更は未対応です:{$t['name']}", 1, 'HY000');
1768+ return $t;
1769+ }
1770+
1771+ /** テーブルのソース(スコープ用) */
1772+ private function tableSource(array $t, ?string $alias): array {
1773+ return ['alias' => $alias ?? $t['name'], 'name' => $t['name'], 'cols' => array_column($t['cols'], 'name'), 'types' => array_column($t['cols'], 'type'),
1774+ 'affs' => array_column($t['cols'], 'aff'), 'colls' => array_column($t['cols'], 'collate'), 'table' => $t];
1775+ }
1776+
1777+ /** RETURNING */
1778+ private function returningPlan(?array $ret, array $t, ?string $alias): ?\stdClass {
1779+ if ($ret === null) return null;
1780+ $sc = $this->scope([$this->tableSource($t, $alias)], null);
1781+ $p = new \stdClass();
1782+ $p->fns = $p->cols = $p->types = [];
1783+ foreach ($ret as $c) {
1784+ if (array_key_exists('star', $c)) {
1785+ foreach ($t['cols'] as $i => $col) {
1786+ $p->fns[] = fn(array $r) => $r[$i] ?? null;
1787+ $p->cols[] = $col['name'];
1788+ $p->types[] = $col['type'];
1789+ }
1790+ continue;
1791+ }
1792+ if (self::hasAggregate($c['e'])) throw new DbException('RETURNINGに集約関数は使えません', 1, 'HY000');
1793+ $p->fns[] = $this->compile($c['e'], $sc);
1794+ $info = $c['e']['t'] === 'col' ? $this->resolve($sc, $c['e']['tbl'], $c['e']['name']) : null;
1795+ $p->cols[] = $c['as'] ?? ($info['name'] ?? $c['text']);
1796+ $p->types[] = $info['type'] ?? '';
1797+ }
1798+ $p->rows = [];
1799+ return $p;
1800+ }
1801+
1802+ private function checks(array $t): array {
1803+ if (empty($t['checks'])) return [];
1804+ $sc = $this->scope([$this->tableSource($t, null)], null);
1805+ return array_map(fn($c) => $this->compile($c, $sc), $t['checks']);
1806+ }
1807+
1808+ private function insert(array $st): array {
1809+ $t = $this->writableTable($st['table']);
1810+ $nc = count($t['cols']);
1811+ $targets = [];
1812+ if ($st['cols'] !== null) {
1813+ foreach ($st['cols'] as $n) {
1814+ $ln = strtolower($n);
1815+ $ci = $t['map'][$ln] ?? (in_array($ln, ['rowid', 'oid', '_rowid_'], true) ? 'rowid' : null);
1816+ if ($ci === null) throw new DbException("テーブル{$t['name']}に列がありません:{$n}", 1, 'HY000');
1817+ $targets[] = $ci;
1818+ }
1819+ } else {
1820+ $targets = array_keys($t['cols']);
1821+ }
1822+ // 値の行
1823+ $rows = [];
1824+ if ($st['default']) {
1825+ $rows[] = [];
1826+ } else {
1827+ $sel = $st['sel'];
1828+ $simple = count($sel['cores']) === 1 && isset($sel['cores'][0]['values']) && !$sel['order'] && !$sel['limit'] && empty($sel['with']);
1829+ if ($simple) {
1830+ $sc = $this->scope([], null);
1831+ foreach ($sel['cores'][0]['values'] as $vals) {
1832+ if (count($vals) !== count($targets)) throw new DbException("{$t['name']}は".count($targets).'列ですが、'.count($vals).'個の値が有ります', 1, 'HY000');
1833+ $rows[] = array_map(fn($e) => ($this->compile($e, $sc))([]), $vals);
1834+ }
1835+ } else {
1836+ $plan = $this->planSelect($sel, null);
1837+ if (count($plan->cols) !== count($targets)) throw new DbException("{$t['name']}は".count($targets).'列ですが、'.count($plan->cols).'個の値が有ります', 1, 'HY000');
1838+ $rows = ($plan->run)(-1);
1839+ }
1840+ }
1841+ $defaults = [];
1842+ foreach ($t['cols'] as $i => $c) $defaults[$i] = $c['default'] !== null ? $this->compile($c['default'], $this->scope([], null)) : null;
1843+ $checks = $this->checks($t);
1844+ $ret = $this->returningPlan($st['returning'], $t, $st['alias']);
1845+ $upsert = $this->upsertPlan($st, $t);
1846+ $or = $st['or'] ?? 'ABORT';
1847+
1848+ foreach ($rows as $vals) {
1849+ $row = array_fill(0, $nc, null);
1850+ $given = array_fill(0, $nc, false);
1851+ $rowid = null;
1852+ foreach ($targets as $k => $ci) {
1853+ if ($ci === 'rowid') {
1854+ $rowid = $vals[$k];
1855+ continue;
1856+ }
1857+ $row[$ci] = $vals[$k];
1858+ $given[$ci] = true;
1859+ }
1860+ foreach ($t['cols'] as $i => $c) {
1861+ if (!$given[$i] && $defaults[$i] !== null) $row[$i] = $defaults[$i]([]);
1862+ }
1863+ if ($t['ipk'] !== null && $row[$t['ipk']] !== null) $rowid = $row[$t['ipk']];
1864+ $res = $this->insertRow($t, $row, $rowid, $or, $checks, $upsert);
1865+ if ($res !== null && $ret) {
1866+ $r = $res;
1867+ $ret->rows[] = array_map(fn($f) => $f($r), $ret->fns);
1868+ }
1869+ }
1870+ if ($ret) return self::result($ret->cols, $ret->types, $ret->rows, $this->changes);
1871+ return self::result([], [], [], $this->changes);
1872+ }
1873+
1874+ /** ON CONFLICT … DO UPDATE */
1875+ private function upsertPlan(array $st, array $t): array {
1876+ $out = [];
1877+ foreach ($st['upsert'] as $u) {
1878+ $p = ['target' => null, 'nothing' => $u['nothing']];
1879+ if ($u['target'] !== null) {
1880+ $p['target'] = array_map(fn($n) => $t['map'][strtolower($n)] ?? throw new DbException("列がありません:{$n}", 1, 'HY000'), $u['target']);
1881+ sort($p['target']);
1882+ $ok = $t['ipk'] !== null && $p['target'] === [$t['ipk']];
1883+ foreach ($t['indexes'] as $iname) {
1884+ $idx = $this->schema()->indexes[$iname];
1885+ $cols = $idx['cols'];
1886+ sort($cols);
1887+ if ($idx['unique'] && $cols === $p['target']) $ok = true;
1888+ }
1889+ if (!$ok) throw new DbException('ON CONFLICTの列がPRIMARY KEY・UNIQUEの制約と合いません', 1, 'HY000');
1890+ }
1891+ if (!$u['nothing']) {
1892+ // excludedの列は「excluded.」を付けた場合だけ
1893+ $ex = ['alias' => 'excluded', 'name' => 'excluded'] + array_slice($this->tableSource($t, null), 2);
1894+ $ex['hidden'] = array_fill_keys(array_map('strtolower', $ex['cols']), true);
1895+ $srcs = [$this->tableSource($t, $st['alias']), $ex];
1896+ $sc = $this->scope($srcs, null);
1897+ $p['set'] = [];
1898+ foreach ($u['set'] as [$n, $e]) {
1899+ $ci = $t['map'][strtolower($n)] ?? null;
1900+ if ($ci === null) throw new DbException("列がありません:{$n}", 1, 'HY000');
1901+ $p['set'][] = [$ci, $this->compile($e, $sc)];
1902+ }
1903+ $p['where'] = $u['where'] ? $this->compile($u['where'], $sc) : null;
1904+ }
1905+ $out[] = $p;
1906+ }
1907+ return $out;
1908+ }
1909+
1910+ /**
1911+ * 1行を入れる(制約の確認と衝突の解決)
1912+ *
1913+ * @return array|null 入れた行(値…、rowid)、入れなかった場合null
1914+ */
1915+ private function insertRow(array $t, array $row, mixed $rowid, string $or, array $checks, array $upsert = []): ?array {
1916+ $nc = count($t['cols']);
1917+ foreach ($t['cols'] as $i => $c) {
1918+ if ($i !== $t['ipk']) $row[$i] = Value::apply($row[$i], $c['aff']);
1919+ }
1920+ if ($rowid !== null) {
1921+ $rowid = Value::apply($rowid, Value::INTEGER);
1922+ if (!is_int($rowid)) throw new DbException('データ型が違います(rowidは整数)', 20, 'HY000');
1923+ }
1924+ // NOT NULL
1925+ foreach ($t['cols'] as $i => $c) {
1926+ if ($i === $t['ipk'] || !$c['notNull'] || $row[$i] !== null) continue;
1927+ if ($or === 'REPLACE' && $c['default'] !== null) {
1928+ $row[$i] = Value::apply($this->constValue($c['default']), $c['aff']);
1929+ if ($row[$i] !== null) continue;
1930+ }
1931+ if ($or === 'IGNORE') return null;
1932+ $this->conflictFail($or);
1933+ throw new DbException("NOT NULL制約に違反しました:{$t['name']}.{$c['name']}", 19, '23000');
1934+ }
1935+ $full = $row;
1936+ $full[] = $rowid;
1937+ if ($t['ipk'] !== null) $full[$t['ipk']] = $rowid;
1938+ foreach ($checks as $k => $f) {
1939+ if (Value::truth($f($full)) === false) {
1940+ if ($or === 'IGNORE') return null;
1941+ $this->conflictFail($or);
1942+ throw new DbException("CHECK制約に違反しました:{$t['name']}", 19, '23000');
1943+ }
1944+ }
1945+
1946+ // 一意の制約(新しいrowidは先に決める:REPLACEで一番大きいrowidの行を消しても、同じrowidにならない)
1947+ $schema = $this->schema();
1948+ $auto = $rowid === null;
1949+ if ($auto) $rowid = $this->newRowid($t);
1950+ while (true) {
1951+ $conflict = null;
1952+ if (!$auto && $this->bt->get($t['root'], $rowid) !== null) {
1953+ $conflict = [$rowid, $t['ipk'] !== null ? [$t['ipk']] : ['rowid'], 'PRIMARY KEY'];
1954+ }
1955+ if ($conflict === null) {
1956+ foreach ($t['indexes'] as $iname) {
1957+ $idx = $schema->indexes[$iname];
1958+ if (!$idx['unique']) continue;
1959+ $other = $this->uniqueConflict($idx, $row, null);
1960+ if ($other !== null) {
1961+ $conflict = [$other, $idx['cols'], 'UNIQUE'];
1962+ break;
1963+ }
1964+ }
1965+ }
1966+ if ($conflict === null) break;
1967+ [$other, $cols] = $conflict;
1968+ // ON CONFLICT
1969+ foreach ($upsert as $u) {
1970+ $sorted = $cols;
1971+ sort($sorted);
1972+ if ($u['target'] !== null && $u['target'] !== $sorted && !($sorted === ['rowid'] && $u['target'] === [$t['ipk']])) continue;
1973+ if ($u['nothing']) return null;
1974+ $this->upsertUpdate($t, $other, $full, $u);
1975+ return null;
1976+ }
1977+ if (!empty($upsert) && $or === 'ABORT') {
1978+ // 指定した制約以外の衝突
1979+ }
1980+ if ($or === 'REPLACE') {
1981+ $this->deleteRow($t, $other);
1982+ continue;
1983+ }
1984+ if ($or === 'IGNORE') return null;
1985+ $this->conflictFail($or);
1986+ $names = implode(', ', array_map(fn($ci) => $t['name'].'.'.($ci === 'rowid' ? 'rowid' : $t['cols'][$ci]['name']), $cols));
1987+ throw new DbException("UNIQUE制約に違反しました:{$names}", 19, '23000');
1988+ }
1989+
1990+ $full[$nc] = $rowid;
1991+ if ($t['ipk'] !== null) {
1992+ $full[$t['ipk']] = $rowid;
1993+ $row[$t['ipk']] = null;
1994+ }
1995+ $this->bt->insert($t['root'], $rowid, Record::encode($row));
1996+ $vals = array_slice($full, 0, $nc);
1997+ foreach ($t['indexes'] as $iname) {
1998+ $idx = $schema->indexes[$iname];
1999+ $this->bt->indexInsert($idx, $this->indexKey($idx, $vals, $rowid));
2000+ }
2001+ if ($t['autoinc']) $this->updateSequence($t, $rowid);
2002+ $this->lastInsertRowid = $rowid;
2003+ $this->changes++;
2004+ $this->totalChanges++;
2005+ return $full;
2006+ }
2007+
2008+ /** FAIL・ROLLBACKの衝突 */
2009+ private function conflictFail(string $or): void {
2010+ if ($or === 'FAIL') $this->failKeep = true;
2011+ if ($or === 'ROLLBACK') $this->rollbackAll = true;
2012+ }
2013+
2014+ private function indexKey(array $idx, array $vals, int $rowid): array {
2015+ $k = [];
2016+ foreach ($idx['cols'] as $ci) $k[] = $vals[$ci];
2017+ $k[] = $rowid;
2018+ return $k;
2019+ }
2020+
2021+ /** 一意のインデックスで同じキーの他の行(無ければnull) */
2022+ private function uniqueConflict(array $idx, array $vals, ?int $self): ?int {
2023+ $key = [];
2024+ foreach ($idx['cols'] as $ci) {
2025+ if ($vals[$ci] === null) return null;
2026+ $key[] = $vals[$ci];
2027+ }
2028+ return $this->bt->indexFind($idx, $key, $self);
2029+ }
2030+
2031+ private function newRowid(array $t): int {
2032+ $max = $this->bt->maxRowid($t['root']);
2033+ if ($t['autoinc']) {
2034+ $seq = $this->sequenceValue($t['name']);
2035+ $max = max($max, $seq);
2036+ if ($max === PHP_INT_MAX) throw new DbException('データベースが一杯です(AUTOINCREMENT)', 13, 'HY000');
2037+ return $max + 1;
2038+ }
2039+ if ($max < PHP_INT_MAX) return $max + 1;
2040+ for ($i = 0; $i < 100; $i++) {
2041+ $id = random_int(1, PHP_INT_MAX);
2042+ if ($this->bt->get($t['root'], $id) === null) return $id;
2043+ }
2044+ throw new DbException('データベースが一杯です', 13, 'HY000');
2045+ }
2046+
2047+ private function sequenceValue(string $table): int {
2048+ $seq = $this->schema()->table_('sqlite_sequence');
2049+ if ($seq === null) return 0;
2050+ foreach ($this->bt->scan($seq['root']) as $rec) {
2051+ $v = Record::decode($rec);
2052+ if (strcasecmp((string)($v[0] ?? ''), $table) === 0) return (int)($v[1] ?? 0);
2053+ }
2054+ return 0;
2055+ }
2056+
2057+ private function updateSequence(array $t, int $rowid): void {
2058+ $seq = $this->schema()->table_('sqlite_sequence');
2059+ if ($seq === null) return;
2060+ foreach ($this->bt->scan($seq['root']) as $id => $rec) {
2061+ $v = Record::decode($rec);
2062+ if (strcasecmp((string)($v[0] ?? ''), $t['name']) === 0) {
2063+ if ((int)($v[1] ?? 0) < $rowid) $this->bt->insert($seq['root'], $id, Record::encode([$v[0], $rowid]));
2064+ return;
2065+ }
2066+ }
2067+ $this->bt->insert($seq['root'], $this->bt->maxRowid($seq['root']) + 1, Record::encode([$t['name'], $rowid]));
2068+ }
2069+
2070+ private function upsertUpdate(array $t, int $rowid, array $excluded, array $u): void {
2071+ $rec = $this->bt->get($t['root'], $rowid);
2072+ if ($rec === null) return;
2073+ $old = $this->rowReader($t)($rowid, $rec);
2074+ $r = array_merge($old, $excluded);
2075+ if ($u['where'] && !Value::truth(($u['where'])($r))) return;
2076+ $new = array_slice($old, 0, count($t['cols']));
2077+ foreach ($u['set'] as [$ci, $f]) $new[$ci] = $f($r);
2078+ $this->updateRow($t, $rowid, $old, $new, 'ABORT', $this->checks($t));
2079+ }
2080+
2081+ /** 行を消す(インデックスも) */
2082+ private function deleteRow(array $t, int $rowid, ?array $vals = null): void {
2083+ if ($vals === null) {
2084+ $rec = $this->bt->get($t['root'], $rowid);
2085+ if ($rec === null) return;
2086+ $vals = $this->rowReader($t)($rowid, $rec);
2087+ }
2088+ $schema = $this->schema();
2089+ foreach ($t['indexes'] as $iname) {
2090+ $idx = $schema->indexes[$iname];
2091+ $this->bt->indexDelete($idx, $this->indexKey($idx, $vals, $rowid));
2092+ }
2093+ $this->bt->delete($t['root'], $rowid);
2094+ }
2095+
2096+ /**
2097+ * 1行を変える
2098+ *
2099+ * @param array $old 元の値(…、rowid)
2100+ * @param array $new 新しい列の値
2101+ * @return array|null 変えた行(値…、rowid)、変えなかった場合null
2102+ */
2103+ private function updateRow(array $t, int $rowid, array $old, array $new, string $or, array $checks, mixed $newRowid = null): ?array {
2104+ $nc = count($t['cols']);
2105+ foreach ($t['cols'] as $i => $c) {
2106+ if ($i !== $t['ipk']) $new[$i] = Value::apply($new[$i], $c['aff']);
2107+ }
2108+ $target = $rowid;
2109+ if ($t['ipk'] !== null && $new[$t['ipk']] !== $old[$t['ipk']]) $newRowid = $new[$t['ipk']];
2110+ if ($newRowid !== null) {
2111+ $newRowid = Value::apply($newRowid, Value::INTEGER);
2112+ if (!is_int($newRowid)) throw new DbException('データ型が違います(rowidは整数)', 20, 'HY000');
2113+ $target = $newRowid;
2114+ } else if ($t['ipk'] !== null && $new[$t['ipk']] === null) {
2115+ throw new DbException("NOT NULL制約に違反しました:{$t['name']}.{$t['cols'][$t['ipk']]['name']}", 19, '23000');
2116+ }
2117+ foreach ($t['cols'] as $i => $c) {
2118+ if ($i === $t['ipk'] || !$c['notNull'] || $new[$i] !== null) continue;
2119+ if ($or === 'REPLACE' && $c['default'] !== null) {
2120+ $new[$i] = Value::apply($this->constValue($c['default']), $c['aff']);
2121+ if ($new[$i] !== null) continue;
2122+ }
2123+ if ($or === 'IGNORE') return null;
2124+ $this->conflictFail($or);
2125+ throw new DbException("NOT NULL制約に違反しました:{$t['name']}.{$c['name']}", 19, '23000');
2126+ }
2127+ $full = $new;
2128+ $full[$nc] = $target;
2129+ if ($t['ipk'] !== null) $full[$t['ipk']] = $target;
2130+ foreach ($checks as $f) {
2131+ if (Value::truth($f($full)) === false) {
2132+ if ($or === 'IGNORE') return null;
2133+ $this->conflictFail($or);
2134+ throw new DbException("CHECK制約に違反しました:{$t['name']}", 19, '23000');
2135+ }
2136+ }
2137+ $schema = $this->schema();
2138+ while (true) {
2139+ $conflict = null;
2140+ if ($target !== $rowid && $this->bt->get($t['root'], $target) !== null) $conflict = [$target, $t['ipk'] !== null ? [$t['ipk']] : ['rowid']];
2141+ if ($conflict === null) {
2142+ foreach ($t['indexes'] as $iname) {
2143+ $idx = $schema->indexes[$iname];
2144+ if (!$idx['unique']) continue;
2145+ $other = $this->uniqueConflict($idx, array_slice($full, 0, $nc), $rowid);
2146+ if ($other !== null) {
2147+ $conflict = [$other, $idx['cols']];
2148+ break;
2149+ }
2150+ }
2151+ }
2152+ if ($conflict === null) break;
2153+ if ($or === 'REPLACE') {
2154+ $this->deleteRow($t, $conflict[0]);
2155+ continue;
2156+ }
2157+ if ($or === 'IGNORE') return null;
2158+ $this->conflictFail($or);
2159+ $names = implode(', ', array_map(fn($ci) => $t['name'].'.'.($ci === 'rowid' ? 'rowid' : $t['cols'][$ci]['name']), $conflict[1]));
2160+ throw new DbException("UNIQUE制約に違反しました:{$names}", 19, '23000');
2161+ }
2162+ $oldVals = array_slice($old, 0, $nc);
2163+ $newVals = array_slice($full, 0, $nc);
2164+ foreach ($t['indexes'] as $iname) {
2165+ $idx = $schema->indexes[$iname];
2166+ $ok = $this->indexKey($idx, $oldVals, $rowid);
2167+ $nk = $this->indexKey($idx, $newVals, $target);
2168+ if ($target === $rowid && self::sameKey($ok, $nk)) continue;
2169+ $this->bt->indexDelete($idx, $ok);
2170+ $this->bt->indexInsert($idx, $nk);
2171+ }
2172+ $rec = $new;
2173+ if ($t['ipk'] !== null) $rec[$t['ipk']] = null;
2174+ if ($target !== $rowid) $this->bt->delete($t['root'], $rowid);
2175+ $this->bt->insert($t['root'], $target, Record::encode($rec));
2176+ $this->changes++;
2177+ $this->totalChanges++;
2178+ return $full;
2179+ }
2180+
2181+ private static function sameKey(array $a, array $b): bool {
2182+ foreach ($a as $i => $v) {
2183+ if (Value::type($v) !== Value::type($b[$i]) || Record::compare($v, $b[$i]) !== 0) return false;
2184+ }
2185+ return true;
2186+ }
2187+
2188+ /** UPDATE・DELETEの対象の行 */
2189+ private function targetRows(array $t, ?string $alias, array $from, ?array $where, ?array &$setScope = null, array $setCols = []): array {
2190+ $srcs = [$this->tableSource($t, $alias)];
2191+ foreach ($from as $item) {
2192+ if (isset($item['sel'])) {
2193+ $srcs[] = $this->planSource($this->planSelect($item['sel'], null), $item['as'] ?? '(subquery)', null);
2194+ } else {
2195+ $srcs[] = $this->sourceFor($item['table'], $item['as'], null);
2196+ }
2197+ }
2198+ $core = ['distinct' => false, 'cols' => [], 'from' => [], 'where' => $where, 'group' => [], 'having' => null];
2199+ $sc = $this->scope($srcs, null);
2200+ $setScope = [$sc];
2201+ $n = count($srcs);
2202+ $conj = self::conjuncts($where);
2203+ $levelPost = array_fill(0, $n, []);
2204+ $levelCand = array_fill(0, $n, []);
2205+ foreach ($from as $j => $item) {
2206+ if (!empty($item['on'])) $conj = array_merge($conj, self::conjuncts($item['on']));
2207+ }
2208+ foreach ($conj as $c) {
2209+ if (self::hasAggregate($c)) throw new DbException('WHEREに集約関数は使えません', 1, 'HY000');
2210+ $r = $this->refs($c, $sc);
2211+ $lvl = $r === null ? $n - 1 : (empty($r) ? 0 : max($r));
2212+ $levelPost[$lvl][] = $c;
2213+ if ($r !== null) $levelCand[$lvl][] = $c;
2214+ }
2215+ $levels = [];
2216+ $byIndex = null;
2217+ for ($j = 0; $j < $n; $j++) {
2218+ $levels[] = ['src' => $this->access($srcs[$j], $j, $levelCand[$j], $sc), 'on' => [],
2219+ 'post' => array_map(fn($c) => $this->compile($c, $sc), $levelPost[$j]), 'left' => false, 'nulls' => []];
2220+ if ($j === 0) $byIndex = $this->lastAccess;
2221+ }
2222+ $out = [];
2223+ $nc = count($t['cols']);
2224+ foreach ($this->joinLoop($levels, 0, []) as $r) {
2225+ $id = $r[$nc];
2226+ if (!isset($out[$id])) $out[$id] = $r;
2227+ }
2228+ // 変える列のインデックスで読んだ場合は、rowidの順番(SQLiteと同じ)
2229+ if ($n > 1 || $byIndex === null || in_array('*', $setCols, true) || array_intersect($byIndex, $setCols)) ksort($out);
2230+ return $out;
2231+ }
2232+
2233+ private function update(array $st): array {
2234+ $t = $this->writableTable($st['table']);
2235+ $nc = count($t['cols']);
2236+ $setCols = [];
2237+ foreach ($st['set'] as [$n]) $setCols[] = $t['map'][strtolower($n)] ?? 'rowid';
2238+ // REPLACEは行を消すかもしれないので、rowidの順番(SQLiteと同じ)
2239+ if (($st['or'] ?? '') === 'REPLACE') $setCols[] = '*';
2240+ $rows = $this->targetRows($t, $st['alias'], $st['from'], $st['where'], $scopes, $setCols);
2241+ $sc = $scopes[0];
2242+ $sets = [];
2243+ foreach ($st['set'] as [$n, $e]) {
2244+ $ln = strtolower($n);
2245+ $ci = $t['map'][$ln] ?? (in_array($ln, ['rowid', 'oid', '_rowid_'], true) ? 'rowid' : null);
2246+ if ($ci === null) throw new DbException("列がありません:{$n}", 1, 'HY000');
2247+ $sets[] = [$ci, $this->compile($e, $sc)];
2248+ }
2249+ $checks = $this->checks($t);
2250+ $ret = $this->returningPlan($st['returning'], $t, $st['alias']);
2251+ $or = $st['or'] ?? 'ABORT';
2252+ foreach ($rows as $id => $r) {
2253+ // 他の行のREPLACEで消えた
2254+ if ($this->bt->get($t['root'], $id) === null) continue;
2255+ $new = array_slice($r, 0, $nc);
2256+ $newRowid = null;
2257+ foreach ($sets as [$ci, $f]) {
2258+ if ($ci === 'rowid') $newRowid = $f($r);
2259+ else $new[$ci] = $f($r);
2260+ }
2261+ $old = array_slice($r, 0, $nc + 1);
2262+ $res = $this->updateRow($t, $id, $old, $new, $or, $checks, $newRowid);
2263+ if ($res !== null && $ret) $ret->rows[] = array_map(fn($f) => $f($res), $ret->fns);
2264+ }
2265+ if ($ret) return self::result($ret->cols, $ret->types, $ret->rows, $this->changes);
2266+ return self::result([], [], [], $this->changes);
2267+ }
2268+
2269+ private function delete(array $st): array {
2270+ $t = $this->writableTable($st['table']);
2271+ $nc = count($t['cols']);
2272+ $schema = $this->schema();
2273+ if ($st['where'] === null && $st['returning'] === null) {
2274+ // 全部:ツリーを空にする
2275+ $n = $this->bt->count($t['root']);
2276+ $this->bt->clear($t['root']);
2277+ foreach ($t['indexes'] as $iname) $this->bt->clear($schema->indexes[$iname]['root']);
2278+ $this->changes = $n;
2279+ $this->totalChanges += $n;
2280+ return self::result([], [], [], $n);
2281+ }
2282+ $rows = $this->targetRows($t, $st['alias'], [], $st['where']);
2283+ $ret = $this->returningPlan($st['returning'], $t, $st['alias']);
2284+ foreach ($rows as $id => $r) {
2285+ $vals = array_slice($r, 0, $nc + 1);
2286+ $this->deleteRow($t, $id, $vals);
2287+ if ($ret) $ret->rows[] = array_map(fn($f) => $f($vals), $ret->fns);
2288+ $this->changes++;
2289+ $this->totalChanges++;
2290+ }
2291+ if ($ret) return self::result($ret->cols, $ret->types, $ret->rows, $this->changes);
2292+ return self::result([], [], [], $this->changes);
2293+ }
2294+
2295+ ///////////////
2296+ // スキーマの変更
2297+
2298+ private function schemaInsert(string $type, string $name, string $tbl, int $root, ?string $sql): void {
2299+ $id = $this->bt->maxRowid(1) + 1;
2300+ $this->bt->insert(1, $id, Record::encode([$type, $name, $tbl, $root, $sql]));
2301+ $this->schemaChanged();
2302+ }
2303+
2304+ private function schemaChanged(): void {
2305+ $this->pager->hdr['cookie'] = ($this->pager->hdr['cookie'] + 1) & 0x7FFFFFFF;
2306+ $this->schema = null;
2307+ }
2308+
2309+ private function createTable(array $st): array {
2310+ $schema = $this->schema();
2311+ if ($schema->exists($st['name'])) {
2312+ if ($st['ifNotExists'] && $schema->table_($st['name']) !== null) return self::result();
2313+ throw new DbException("テーブル{$st['name']}は既に有ります", 1, 'HY000');
2314+ }
2315+ if (stripos($st['name'], 'sqlite_') === 0 && strcasecmp($st['name'], 'sqlite_sequence') !== 0) {
2316+ throw new DbException("「sqlite_」で始まる名前は使えません:{$st['name']}", 1, 'HY000');
2317+ }
2318+ if (isset($st['as'])) {
2319+ $plan = $this->planSelect($st['as'], null);
2320+ $defs = [];
2321+ foreach ($plan->cols as $i => $n) {
2322+ $ty = match ($plan->affs[$i]) { Value::INTEGER => 'INT', Value::REAL => 'REAL', Value::TEXT => 'TEXT', Value::NUMERIC => 'NUM', default => '' };
2323+ $defs[] = self::quoteName($n).($ty !== '' ? ' '.$ty : '');
2324+ }
2325+ $sql = 'CREATE TABLE '.self::quoteName($st['name']).'('.implode(',', $defs).')';
2326+ $this->createTable($this->parser->parseAll($sql)[0]);
2327+ $rows = ($plan->run)(-1);
2328+ $t = $this->schema()->table_($st['name']);
2329+ foreach ($rows as $vals) $this->insertRow($t, $vals, null, 'ABORT', []);
2330+ return self::result();
2331+ }
2332+ $def = $st['def'];
2333+ $sql = 'CREATE TABLE '.substr($st['sql'], $st['nameStart'] - $st['start']);
2334+ if ($st['ifNotExists'] === false && !isset($st['nameStart'])) $sql = $st['sql'];
2335+ $t = Schema::table($st['name'], 0, $sql, $def);
2336+ $root = $this->bt->create(true);
2337+ $this->schemaInsert('table', $st['name'], $st['name'], $root, $sql);
2338+ foreach ($t['constraints'] as $k => [$kind, $list]) {
2339+ $iroot = $this->bt->create(false);
2340+ $this->schemaInsert('index', 'sqlite_autoindex_'.$st['name'].'_'.($k + 1), $st['name'], $iroot, null);
2341+ }
2342+ if ($t['autoinc'] && $this->schema()->table_('sqlite_sequence') === null) {
2343+ $sroot = $this->bt->create(true);
2344+ $this->schemaInsert('table', 'sqlite_sequence', 'sqlite_sequence', $sroot, 'CREATE TABLE sqlite_sequence(name,seq)');
2345+ }
2346+ return self::result();
2347+ }
2348+
2349+ private function createIndex(array $st): array {
2350+ $schema = $this->schema();
2351+ if ($schema->exists($st['name'])) {
2352+ if ($st['ifNotExists'] && isset($schema->indexes[strtolower($st['name'])])) return self::result();
2353+ throw new DbException("インデックス{$st['name']}は既に有ります", 1, 'HY000');
2354+ }
2355+ $t = $schema->table_($st['table']);
2356+ if ($t === null) throw new DbException("テーブルがありません:{$st['table']}", 1, 'HY000');
2357+ if (!empty($t['system']) || isset($t['unsupported'])) throw new DbException("テーブル{$t['name']}にインデックスは作れません", 1, 'HY000');
2358+ $idx = Schema::indexInfo(['name' => $st['name'], 'root' => 0, 'table' => strtolower($t['name']), 'unique' => $st['unique'], 'def' => $st['cols'], 'origin' => 'c'], $t);
2359+ $sql = 'CREATE '.($st['unique'] ? 'UNIQUE ' : '').'INDEX '.substr($st['sql'], $st['nameStart'] - $st['start']);
2360+ $idx['root'] = $this->bt->create(false);
2361+ $read = $this->rowReader($t);
2362+ foreach ($this->bt->scan($t['root']) as $id => $rec) {
2363+ $vals = $read($id, $rec);
2364+ if ($st['unique'] && $this->uniqueConflict($idx, $vals, $id) !== null) {
2365+ $names = implode(', ', array_map(fn($ci) => $t['name'].'.'.$t['cols'][$ci]['name'], $idx['cols']));
2366+ throw new DbException("UNIQUE制約に違反しました:{$names}", 19, '23000');
2367+ }
2368+ $this->bt->indexInsert($idx, $this->indexKey($idx, $vals, $id));
2369+ }
2370+ $this->schemaInsert('index', $st['name'], $t['name'], $idx['root'], $sql);
2371+ return self::result();
2372+ }
2373+
2374+ private function createView(array $st): array {
2375+ $schema = $this->schema();
2376+ if ($schema->exists($st['name'])) {
2377+ if ($st['ifNotExists'] && isset($schema->views[strtolower($st['name'])])) return self::result();
2378+ throw new DbException("ビュー{$st['name']}は既に有ります", 1, 'HY000');
2379+ }
2380+ // 確認
2381+ $plan = $this->planSelect($st['sel'], null);
2382+ if ($st['cols'] !== null && count($st['cols']) !== count($plan->cols)) throw new DbException('ビューの列の数が違います', 1, 'HY000');
2383+ $sql = 'CREATE VIEW '.substr($st['sql'], $st['nameStart'] - $st['start']);
2384+ $this->schemaInsert('view', $st['name'], $st['name'], 0, $sql);
2385+ return self::result();
2386+ }
2387+
2388+ private function drop(array $st): array {
2389+ $schema = $this->schema();
2390+ $l = strtolower($st['name']);
2391+ switch ($st['kind']) {
2392+ case 'table':
2393+ $t = $schema->tables[$l] ?? null;
2394+ if ($t === null) {
2395+ if ($st['ifExists']) return self::result();
2396+ throw new DbException("テーブルがありません:{$st['name']}", 1, 'HY000');
2397+ }
2398+ if (!empty($t['system']) || $l === 'sqlite_sequence') throw new DbException("テーブル{$t['name']}は消せません", 1, 'HY000');
2399+ foreach ($t['indexes'] ?? [] as $iname) {
2400+ $idx = $schema->indexes[$iname];
2401+ $this->bt->clear($idx['root'], true);
2402+ $this->bt->delete(1, $idx['schemaRowid']);
2403+ }
2404+ // 自動のインデックスの行(テーブルが読めない場合も)
2405+ foreach ($this->bt->scan(1) as $id => $rec) {
2406+ $v = Record::decode($rec);
2407+ if (($v[0] ?? '') === 'trigger' && strcasecmp((string)$v[2], $t['name']) === 0) $this->bt->delete(1, $id);
2408+ }
2409+ $this->bt->clear($t['root'], true);
2410+ $this->bt->delete(1, $t['schemaRowid']);
2411+ if (!empty($t['autoinc']) && ($seq = $schema->table_('sqlite_sequence')) !== null) {
2412+ foreach ($this->bt->scan($seq['root']) as $id => $rec) {
2413+ if (strcasecmp((string)(Record::decode($rec)[0] ?? ''), $t['name']) === 0) {
2414+ $this->bt->delete($seq['root'], $id);
2415+ break;
2416+ }
2417+ }
2418+ }
2419+ $this->schemaChanged();
2420+ return self::result();
2421+ case 'index':
2422+ $idx = $schema->indexes[$l] ?? null;
2423+ if ($idx === null) {
2424+ if ($st['ifExists']) return self::result();
2425+ throw new DbException("インデックスがありません:{$st['name']}", 1, 'HY000');
2426+ }
2427+ if ($idx['sql'] === null) throw new DbException('UNIQUE・PRIMARY KEYの制約のインデックスは消せません', 1, 'HY000');
2428+ $this->bt->clear($idx['root'], true);
2429+ $this->bt->delete(1, $idx['schemaRowid']);
2430+ $this->schemaChanged();
2431+ return self::result();
2432+ case 'view':
2433+ $v = $schema->views[$l] ?? null;
2434+ if ($v === null) {
2435+ if ($st['ifExists']) return self::result();
2436+ throw new DbException("ビューがありません:{$st['name']}", 1, 'HY000');
2437+ }
2438+ $this->bt->delete(1, $v['schemaRowid']);
2439+ $this->schemaChanged();
2440+ return self::result();
2441+ default:
2442+ $id = $schema->triggers['@'.$l] ?? null;
2443+ if ($id === null) {
2444+ if ($st['ifExists']) return self::result();
2445+ throw new DbException("トリガーがありません:{$st['name']}", 1, 'HY000');
2446+ }
2447+ $this->bt->delete(1, $id);
2448+ $this->schemaChanged();
2449+ return self::result();
2450+ }
2451+ }
2452+
2453+ public static function quoteName(string $n): string {
2454+ return preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/', $n) && !in_array(strtoupper($n), ['SELECT', 'FROM', 'WHERE', 'TABLE', 'ORDER', 'GROUP', 'INDEX', 'KEY', 'DEFAULT'], true)
2455+ ? $n : '"'.str_replace('"', '""', $n).'"';
2456+ }
2457+
2458+ /** スキーマの行を変える */
2459+ private function schemaUpdate(int $rowid, array $vals): void {
2460+ $this->bt->insert(1, $rowid, Record::encode($vals));
2461+ }
2462+
2463+ private function alter(array $st): array {
2464+ $schema = $this->schema();
2465+ $t = $this->writableTable($st['table']);
2466+ $toks = $this->parser->tokens($t['sql']);
2467+ switch ($st['op']) {
2468+ case 'rename':
2469+ if ($schema->exists($st['to'])) throw new DbException("{$st['to']}は既に有ります", 1, 'HY000');
2470+ $quoted = '"'.str_replace('"', '""', $st['to']).'"';
2471+ $sql = $this->replaceToken($t['sql'], $toks, $this->nameTokenIndex($toks), $quoted);
2472+ $this->schemaUpdate($t['schemaRowid'], ['table', $st['to'], $st['to'], $t['root'], $sql]);
2473+ foreach ($t['indexes'] as $iname) {
2474+ $idx = $schema->indexes[$iname];
2475+ $iname2 = $idx['name'];
2476+ $isql = $idx['sql'];
2477+ if ($isql === null) {
2478+ $iname2 = 'sqlite_autoindex_'.$st['to'].substr($idx['name'], strlen('sqlite_autoindex_'.$t['name']));
2479+ } else {
2480+ $itoks = $this->parser->tokens($isql);
2481+ foreach ($itoks as $k => $tk) {
2482+ if ($tk['t'] === 'id' && !$tk['q'] && $tk['u'] === 'ON') {
2483+ $isql = $this->replaceToken($isql, $itoks, $k + 1, $quoted);
2484+ break;
2485+ }
2486+ }
2487+ }
2488+ $this->schemaUpdate($idx['schemaRowid'], ['index', $iname2, $st['to'], $idx['root'], $isql]);
2489+ }
2490+ if ($t['autoinc'] && ($seq = $schema->table_('sqlite_sequence')) !== null) {
2491+ foreach ($this->bt->scan($seq['root']) as $id => $rec) {
2492+ $v = Record::decode($rec);
2493+ if (strcasecmp((string)$v[0], $t['name']) === 0) $this->bt->insert($seq['root'], $id, Record::encode([$st['to'], $v[1]]));
2494+ }
2495+ }
2496+ $this->schemaChanged();
2497+ return self::result();
2498+
2499+ case 'renameColumn':
2500+ $ci = $t['map'][strtolower($st['from'])] ?? null;
2501+ if ($ci === null) throw new DbException("列がありません:{$st['from']}", 1, 'HY000');
2502+ if (isset($t['map'][strtolower($st['to'])])) throw new DbException("列{$st['to']}は既に有ります", 1, 'HY000');
2503+ $nameIdx = $this->nameTokenIndex($toks);
2504+ $sql = $this->replaceIdentifiers($t['sql'], $toks, $st['from'], $st['to'], $nameIdx + 1);
2505+ $this->schemaUpdate($t['schemaRowid'], ['table', $t['name'], $t['name'], $t['root'], $sql]);
2506+ foreach ($t['indexes'] as $iname) {
2507+ $idx = $schema->indexes[$iname];
2508+ if ($idx['sql'] === null) continue;
2509+ $itoks = $this->parser->tokens($idx['sql']);
2510+ $start = 0;
2511+ foreach ($itoks as $k => $tk) if ($tk['t'] === 'op' && $tk['v'] === '(') {
2512+ $start = $k;
2513+ break;
2514+ }
2515+ $isql = $this->replaceIdentifiers($idx['sql'], $itoks, $st['from'], $st['to'], $start);
2516+ $this->schemaUpdate($idx['schemaRowid'], ['index', $idx['name'], $t['name'], $idx['root'], $isql]);
2517+ }
2518+ $this->schemaChanged();
2519+ return self::result();
2520+
2521+ case 'addColumn':
2522+ $c = $st['col'];
2523+ if (isset($t['map'][strtolower($c['name'])])) throw new DbException("列{$c['name']}は既に有ります", 1, 'HY000');
2524+ if ($c['pk']) throw new DbException('PRIMARY KEYの列は追加できません', 1, 'HY000');
2525+ if ($c['unique']) throw new DbException('UNIQUEの列は追加できません', 1, 'HY000');
2526+ if ($c['default'] !== null && !in_array($c['default']['t'], ['lit', 'un'], true)) throw new DbException('定数でないDEFAULTの列は追加できません', 1, 'HY000');
2527+ if ($c['notNull'] && ($c['default'] === null || $this->constValue($c['default']) === null) && $this->bt->count($t['root']) > 0) {
2528+ throw new DbException('NOT NULLの列にはNULLでないDEFAULTが必要です', 1, 'HY000');
2529+ }
2530+ // 最後の列の定義の後(テーブルの制約の前)に入れる
2531+ [$open, $close] = $this->bodyParens($toks);
2532+ $depth = 0;
2533+ $end = $open;
2534+ $inCol = true;
2535+ for ($k = $open + 1; $k < $close; $k++) {
2536+ $tk = $toks[$k];
2537+ if ($tk['t'] === 'op' && $tk['v'] === '(') $depth++;
2538+ else if ($tk['t'] === 'op' && $tk['v'] === ')') $depth--;
2539+ if ($depth === 0 && $tk['t'] === 'op' && $tk['v'] === ',') {
2540+ $nx = $toks[$k + 1];
2541+ $inCol = !($nx['t'] === 'id' && !$nx['q'] && in_array($nx['u'], ['CONSTRAINT', 'PRIMARY', 'UNIQUE', 'CHECK', 'FOREIGN'], true));
2542+ continue;
2543+ }
2544+ if ($inCol) $end = $k;
2545+ }
2546+ $at = $toks[$end]['e'];
2547+ $sql = substr($t['sql'], 0, $at).', '.$st['text'].substr($t['sql'], $at);
2548+ $this->schemaUpdate($t['schemaRowid'], ['table', $t['name'], $t['name'], $t['root'], $sql]);
2549+ $this->schemaChanged();
2550+ return self::result();
2551+
2552+ case 'dropColumn':
2553+ $ci = $t['map'][strtolower($st['col'])] ?? null;
2554+ if ($ci === null) throw new DbException("列がありません:{$st['col']}", 1, 'HY000');
2555+ if (count($t['cols']) === 1) throw new DbException('最後の列は消せません', 1, 'HY000');
2556+ if ($t['cols'][$ci]['pk']) throw new DbException('PRIMARY KEYの列は消せません', 1, 'HY000');
2557+ foreach ($t['indexes'] as $iname) {
2558+ if (in_array($ci, $schema->indexes[$iname]['cols'], true)) throw new DbException("インデックスの有る列は消せません:{$st['col']}", 1, 'HY000');
2559+ }
2560+ // 列の定義の場所
2561+ [$open, $close] = $this->bodyParens($toks);
2562+ $depth = 0;
2563+ $starts = [$open + 1];
2564+ for ($k = $open + 1; $k < $close; $k++) {
2565+ $tk = $toks[$k];
2566+ if ($tk['t'] === 'op' && $tk['v'] === '(') $depth++;
2567+ else if ($tk['t'] === 'op' && $tk['v'] === ')') $depth--;
2568+ else if ($depth === 0 && $tk['t'] === 'op' && $tk['v'] === ',') $starts[] = $k + 1;
2569+ }
2570+ $starts[] = $close + 1;
2571+ $seg = null;
2572+ foreach ($starts as $s => $k) {
2573+ if (!isset($starts[$s + 1])) break;
2574+ $tk = $toks[$k];
2575+ if ($tk['t'] === 'id' && strcasecmp($tk['v'], $t['cols'][$ci]['name']) === 0) $seg = $s;
2576+ }
2577+ if ($seg === null) throw new DbException('列の定義が見つかりません', 1, 'HY000');
2578+ $from = $seg === 0 ? $toks[$starts[0]]['s'] : $toks[$starts[$seg] - 1]['s'];
2579+ $to = $seg === 0 ? $toks[$starts[1]]['s'] : $toks[$starts[$seg + 1] - 1]['s'];
2580+ $sql = substr($t['sql'], 0, $from).substr($t['sql'], $to);
2581+ // 行を書き直す
2582+ $updates = [];
2583+ foreach ($this->bt->scan($t['root']) as $id => $rec) {
2584+ $v = Record::decode($rec);
2585+ if (count($v) > $ci) {
2586+ array_splice($v, $ci, 1);
2587+ $updates[$id] = Record::encode($v);
2588+ }
2589+ }
2590+ foreach ($updates as $id => $rec) $this->bt->insert($t['root'], $id, $rec);
2591+ $this->schemaUpdate($t['schemaRowid'], ['table', $t['name'], $t['name'], $t['root'], $sql]);
2592+ $this->schemaChanged();
2593+ // 自動のインデックスの番号は変わらない(UNIQUEの列は消せないので)
2594+ return self::result();
2595+ }
2596+ throw new DbException('未対応のALTER TABLE', 1, 'HY000');
2597+ }
2598+
2599+ /** CREATE TABLE/INDEX [IF NOT EXISTS] 名前 の名前のトークン */
2600+ private function nameTokenIndex(array $toks): int {
2601+ $k = 0;
2602+ while (isset($toks[$k]) && $toks[$k]['t'] === 'id' && !$toks[$k]['q'] && in_array($toks[$k]['u'], ['CREATE', 'TABLE', 'UNIQUE', 'INDEX', 'VIEW', 'IF', 'NOT', 'EXISTS', 'TEMP', 'TEMPORARY'], true)) $k++;
2603+ if (($toks[$k + 1]['t'] ?? '') === 'op' && $toks[$k + 1]['v'] === '.') $k += 2;
2604+ return $k;
2605+ }
2606+
2607+ private function replaceToken(string $sql, array $toks, int $k, string $new): string {
2608+ return substr($sql, 0, $toks[$k]['s']).$new.substr($sql, $toks[$k]['e']);
2609+ }
2610+
2611+ private function replaceIdentifiers(string $sql, array $toks, string $from, string $to, int $start): string {
2612+ $out = '';
2613+ $pos = 0;
2614+ foreach ($toks as $k => $tk) {
2615+ if ($k < $start || $tk['t'] !== 'id' || strcasecmp($tk['v'], $from) !== 0) continue;
2616+ $out .= substr($sql, $pos, $tk['s'] - $pos).self::quoteName($to);
2617+ $pos = $tk['e'];
2618+ }
2619+ return $out.substr($sql, $pos);
2620+ }
2621+
2622+ /** テーブルの定義の括弧(開く、閉じる)のトークン */
2623+ private function bodyParens(array $toks): array {
2624+ $open = null;
2625+ $depth = 0;
2626+ foreach ($toks as $k => $tk) {
2627+ if ($tk['t'] !== 'op') continue;
2628+ if ($tk['v'] === '(') {
2629+ if ($open === null) $open = $k;
2630+ $depth++;
2631+ } else if ($tk['v'] === ')') {
2632+ $depth--;
2633+ if ($depth === 0 && $open !== null) return [$open, $k];
2634+ }
2635+ }
2636+ throw new DbException('テーブルのSQLが不正です', 11, 'HY000');
2637+ }
2638+
2639+ ///////////////
2640+ // PRAGMA
2641+
2642+ private function pragma(array $st): array {
2643+ $schema = $this->schema();
2644+ $arg = $st['arg'];
2645+ $one = fn(string $name, mixed $v) => self::result([$name], [''], [[$v]]);
2646+ switch ($st['name']) {
2647+ case 'table_info':
2648+ case 'table_xinfo':
2649+ $t = $schema->table_((string)$arg);
2650+ if ($t === null) {
2651+ $v = $schema->views[strtolower((string)$arg)] ?? null;
2652+ if ($v === null) return self::result(['cid', 'name', 'type', 'notnull', 'dflt_value', 'pk']);
2653+ $plan = $this->planSelect($v['sel'], null);
2654+ $rows = [];
2655+ foreach ($v['cols'] ?? $plan->cols as $i => $n) $rows[] = [$i, $n, $plan->types[$i] ?? '', 0, null, 0];
2656+ return self::result(['cid', 'name', 'type', 'notnull', 'dflt_value', 'pk'], [], $rows);
2657+ }
2658+ $rows = [];
2659+ foreach ($t['cols'] as $i => $c) $rows[] = [$i, $c['name'], $c['type'], (int)$c['notNull'], $c['defaultText'], $c['pk']];
2660+ return self::result(['cid', 'name', 'type', 'notnull', 'dflt_value', 'pk'], [], $rows);
2661+ case 'index_list':
2662+ $t = $schema->table_((string)$arg);
2663+ $rows = [];
2664+ foreach (array_reverse($t['indexes'] ?? []) as $k => $iname) {
2665+ $idx = $schema->indexes[$iname];
2666+ $rows[] = [$k, $idx['name'], (int)$idx['unique'], $idx['origin'], 0];
2667+ }
2668+ return self::result(['seq', 'name', 'unique', 'origin', 'partial'], [], $rows);
2669+ case 'index_info':
2670+ case 'index_xinfo':
2671+ $idx = $schema->indexes[strtolower((string)$arg)] ?? null;
2672+ $rows = [];
2673+ if ($idx) {
2674+ $t = $schema->tables[$idx['table']];
2675+ foreach ($idx['cols'] as $k => $ci) {
2676+ $rows[] = $st['name'] === 'index_info' ? [$k, $ci, $t['cols'][$ci]['name']]
2677+ : [$k, $ci, $t['cols'][$ci]['name'], (int)$idx['desc'][$k], $idx['colls'][$k], 1];
2678+ }
2679+ if ($st['name'] === 'index_xinfo') $rows[] = [count($idx['cols']), -1, null, 0, 'BINARY', 0];
2680+ }
2681+ return self::result($st['name'] === 'index_info' ? ['seqno', 'cid', 'name'] : ['seqno', 'cid', 'name', 'desc', 'coll', 'key'], [], $rows);
2682+ case 'table_list':
2683+ $rows = [];
2684+ foreach ($schema->tables as $t) $rows[] = ['main', $t['name'] === 'sqlite_schema' ? 'sqlite_schema' : $t['name'], 'table', count($t['cols']), 0, 0];
2685+ foreach ($schema->views as $v) $rows[] = ['main', $v['name'], 'view', count($v['cols'] ?? []), 0, 0];
2686+ return self::result(['schema', 'name', 'type', 'ncol', 'wr', 'strict'], [], $rows);
2687+ case 'foreign_key_list':
2688+ return self::result(['id', 'seq', 'table', 'from', 'to', 'on_update', 'on_delete', 'match']);
2689+ case 'foreign_keys':
2690+ case 'recursive_triggers':
2691+ case 'case_sensitive_like':
2692+ return $arg === null ? $one($st['name'], 0) : self::result();
2693+ case 'user_version':
2694+ case 'application_id':
2695+ $k = $st['name'] === 'user_version' ? 'userVersion' : 'appId';
2696+ if ($arg === null) return $one($st['name'], $this->pager->hdr[$k] >= 0x80000000 ? $this->pager->hdr[$k] - 0x100000000 : $this->pager->hdr[$k]);
2697+ $this->pager->hdr[$k] = ((int)Value::toInt($arg)) & 0xFFFFFFFF;
2698+ $this->pager->write(1, $this->pager->read(1));
2699+ return self::result();
2700+ case 'schema_version':
2701+ return $one('schema_version', $this->pager->hdr['cookie']);
2702+ case 'page_size':
2703+ return $one('page_size', $this->pager->pageSize);
2704+ case 'page_count':
2705+ return $one('page_count', $this->pager->nPages);
2706+ case 'freelist_count':
2707+ return $one('freelist_count', $this->pager->hdr['freeCount']);
2708+ case 'journal_mode':
2709+ return $one('journal_mode', $this->pager->isMemory() ? 'memory' : 'delete');
2710+ case 'encoding':
2711+ return $one('encoding', 'UTF-8');
2712+ case 'database_list':
2713+ return self::result(['seq', 'name', 'file'], [], [[0, 'main', $this->pager->isMemory() ? '' : $this->pager->path]]);
2714+ case 'integrity_check':
2715+ case 'quick_check':
2716+ $msgs = (new Integrity($this->pager, $this->bt, $schema))->check();
2717+ return self::result([$st['name']], [''], array_map(fn($m) => [$m], $msgs ?: ['ok']));
2718+ case 'synchronous':
2719+ case 'cache_size':
2720+ case 'temp_store':
2721+ case 'busy_timeout':
2722+ case 'locking_mode':
2723+ case 'mmap_size':
2724+ case 'auto_vacuum':
2725+ case 'secure_delete':
2726+ return $arg === null ? $one($st['name'], 0) : self::result();
2727+ }
2728+ // 知らないPRAGMAは何もしない(SQLiteと同じ)
2729+ return self::result();
2730+ }
2731+}
@@ -0,0 +1,693 @@
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\Lib\Db\Sqlite;
39+
40+use Std\Lib\Db\DbException;
41+
42+/**
43+ * SQLの関数(スカラー、日付、JSON)と集約関数
44+ */
45+final class Func {
46+ public const AGGREGATES = ['count', 'sum', 'total', 'avg', 'min', 'max', 'group_concat', 'string_agg'];
47+
48+ private static array $likeCache = [];
49+
50+ /** 集約関数か(minとmaxは引数が1つの場合だけ) */
51+ public static function isAggregate(string $name, int $argc, bool $star): bool {
52+ if ($name === 'count') return $star || $argc <= 1;
53+ if ($name === 'min' || $name === 'max') return $argc === 1;
54+ return in_array($name, self::AGGREGATES, true);
55+ }
56+
57+ /**
58+ * スカラー関数
59+ *
60+ * @param string $name 小文字の名前
61+ * @param array $a 引数の値
62+ * @param Engine $eng last_insert_rowid()等
63+ * @param array $colls 引数の照合順序(min・max用)
64+ * @return mixed
65+ */
66+ public static function call(string $name, array $a, Engine $eng, array $colls = []): mixed {
67+ $n = count($a);
68+ $need = function(int $min, ?int $max = null) use ($n, $name): void {
69+ $max ??= $min;
70+ if ($n < $min || ($max >= 0 && $n > $max)) throw new DbException("関数{$name}()の引数の数が違います", 1, 'HY000');
71+ };
72+ switch ($name) {
73+ case 'abs':
74+ $need(1);
75+ $v = $a[0];
76+ if ($v === null) return null;
77+ $v = Value::toNumber($v);
78+ if ($v === PHP_INT_MIN) throw new DbException('整数のオーバーフロー', 1, 'HY000');
79+ return is_int($v) ? abs($v) : abs((float)$v);
80+ case 'changes':
81+ return $eng->changes;
82+ case 'total_changes':
83+ return $eng->totalChanges;
84+ case 'last_insert_rowid':
85+ return $eng->lastInsertRowid;
86+ case 'char':
87+ return implode('', array_map(fn($c) => mb_chr((int)Value::toInt($c) ?: 0xFFFD, 'UTF-8') ?: '', $a));
88+ case 'coalesce':
89+ if ($n < 2) throw new DbException('関数coalesce()の引数の数が違います', 1, 'HY000');
90+ foreach ($a as $v) if ($v !== null) return $v;
91+ return null;
92+ case 'ifnull':
93+ $need(2);
94+ return $a[0] ?? $a[1];
95+ case 'iif':
96+ $need(3);
97+ return Value::truth($a[0]) ? $a[1] : $a[2];
98+ case 'nullif':
99+ $need(2);
100+ return Record::compare($a[0], $a[1], $colls[0] ?? 'BINARY') === 0 ? null : $a[0];
101+ case 'concat':
102+ return implode('', array_map(fn($v) => (string)Value::toText($v), $a));
103+ case 'concat_ws':
104+ if ($n < 1) throw new DbException('関数concat_ws()の引数の数が違います', 1, 'HY000');
105+ if ($a[0] === null) return null;
106+ $sep = Value::toText($a[0]);
107+ return implode($sep, array_map(fn($v) => Value::toText($v), array_filter(array_slice($a, 1), fn($v) => $v !== null)));
108+ case 'hex':
109+ $need(1);
110+ if ($a[0] === null) return '';
111+ return strtoupper(bin2hex((string)Value::toText($a[0])));
112+ case 'unhex':
113+ $need(1, 2);
114+ if ($a[0] === null) return null;
115+ $s = (string)Value::toText($a[0]);
116+ if ($n === 2 && $a[1] !== null) $s = str_replace(str_split((string)Value::toText($a[1])), '', $s);
117+ if (strlen($s) % 2 || ($s !== '' && !ctype_xdigit($s))) return null;
118+ return new Blob((string)hex2bin($s));
119+ case 'instr':
120+ $need(2);
121+ if ($a[0] === null || $a[1] === null) return null;
122+ if ($a[0] instanceof Blob || $a[1] instanceof Blob) {
123+ $p = strpos((string)$a[0], (string)$a[1]);
124+ return $p === false ? 0 : $p + 1;
125+ }
126+ $p = mb_strpos((string)Value::toText($a[0]), (string)Value::toText($a[1]), 0, 'UTF-8');
127+ return $p === false ? 0 : $p + 1;
128+ case 'length':
129+ $need(1);
130+ if ($a[0] === null) return null;
131+ if ($a[0] instanceof Blob) return strlen($a[0]->data);
132+ return mb_strlen((string)Value::toText($a[0]), 'UTF-8');
133+ case 'octet_length':
134+ $need(1);
135+ return $a[0] === null ? null : strlen((string)Value::toText($a[0]));
136+ case 'likely':
137+ case 'unlikely':
138+ $need(1);
139+ return $a[0];
140+ case 'likelihood':
141+ $need(2);
142+ return $a[0];
143+ case 'lower':
144+ $need(1);
145+ return $a[0] === null ? null : strtolower((string)Value::toText($a[0]));
146+ case 'upper':
147+ $need(1);
148+ return $a[0] === null ? null : strtoupper((string)Value::toText($a[0]));
149+ case 'trim':
150+ case 'ltrim':
151+ case 'rtrim':
152+ $need(1, 2);
153+ if ($a[0] === null || ($n === 2 && $a[1] === null)) return null;
154+ $chars = $n === 2 ? (string)Value::toText($a[1]) : ' ';
155+ $s = (string)Value::toText($a[0]);
156+ if ($chars === '') return $s;
157+ $set = preg_quote($chars, '/');
158+ $u = mb_check_encoding($chars, 'UTF-8') && mb_check_encoding($s, 'UTF-8') ? 'u' : '';
159+ if ($name !== 'rtrim') $s = preg_replace("/^[{$set}]+/{$u}", '', $s);
160+ if ($name !== 'ltrim') $s = preg_replace("/[{$set}]+$/{$u}", '', $s);
161+ return $s;
162+ case 'max':
163+ case 'min':
164+ if ($n < 2) throw new DbException("関数{$name}()の引数の数が違います", 1, 'HY000');
165+ $best = null;
166+ foreach ($a as $i => $v) {
167+ if ($v === null) return null;
168+ if ($best === null || ($name === 'max' ? Record::compare($v, $best, $colls[$i] ?? 'BINARY') > 0 : Record::compare($v, $best, $colls[$i] ?? 'BINARY') < 0)) $best = $v;
169+ }
170+ return $best;
171+ case 'printf':
172+ case 'format':
173+ if ($n < 1) throw new DbException("関数{$name}()の引数の数が違います", 1, 'HY000');
174+ return $a[0] === null ? null : self::printf((string)Value::toText($a[0]), array_slice($a, 1));
175+ case 'quote':
176+ $need(1);
177+ return Value::quote($a[0]);
178+ case 'random':
179+ return random_int(PHP_INT_MIN, PHP_INT_MAX);
180+ case 'randomblob':
181+ $need(1);
182+ return new Blob(random_bytes(max(1, (int)Value::toInt($a[0]))));
183+ case 'zeroblob':
184+ $need(1);
185+ return new Blob(str_repeat("\0", max(0, (int)Value::toInt($a[0]))));
186+ case 'replace':
187+ $need(3);
188+ if ($a[0] === null || $a[1] === null || $a[2] === null) return null;
189+ $from = (string)Value::toText($a[1]);
190+ return $from === '' ? $a[0] : str_replace($from, (string)Value::toText($a[2]), (string)Value::toText($a[0]));
191+ case 'round':
192+ $need(1, 2);
193+ if ($a[0] === null) return null;
194+ $d = $n === 2 ? (int)Value::toInt($a[1]) : 0;
195+ return round((float)Value::toNumber($a[0]), max(0, min(30, $d)));
196+ case 'sign':
197+ $need(1);
198+ $v = $a[0];
199+ if ($v === null || (is_string($v) && Value::parseNumber($v, true) === null) || $v instanceof Blob) return null;
200+ $v = Value::toNumber($v);
201+ return $v > 0 ? 1 : ($v < 0 ? -1 : 0);
202+ case 'sqlite_version':
203+ return '3.45.1';
204+ case 'substr':
205+ case 'substring':
206+ $need(2, 3);
207+ if ($a[0] === null || $a[1] === null || ($n === 3 && $a[2] === null)) return null;
208+ return self::substr($a[0], (int)Value::toInt($a[1]), $n === 3 ? (int)Value::toInt($a[2]) : null);
209+ case 'typeof':
210+ $need(1);
211+ return Value::type($a[0]);
212+ case 'unicode':
213+ $need(1);
214+ if ($a[0] === null) return null;
215+ $s = (string)Value::toText($a[0]);
216+ return $s === '' ? null : mb_ord(mb_substr($s, 0, 1, 'UTF-8'), 'UTF-8');
217+ case 'glob':
218+ $need(2);
219+ return $a[0] === null || $a[1] === null ? null : (int)self::glob((string)Value::toText($a[1]), (string)Value::toText($a[0]));
220+ case 'like':
221+ $need(2, 3);
222+ if ($a[0] === null || $a[1] === null) return null;
223+ return (int)self::like((string)Value::toText($a[1]), (string)Value::toText($a[0]), $n === 3 ? Value::toText($a[2]) : null);
224+ case 'current_timestamp':
225+ return gmdate('Y-m-d H:i:s');
226+ case 'current_date':
227+ return gmdate('Y-m-d');
228+ case 'current_time':
229+ return gmdate('H:i:s');
230+ case 'date':
231+ case 'time':
232+ case 'datetime':
233+ case 'julianday':
234+ case 'unixepoch':
235+ case 'strftime':
236+ case 'timediff':
237+ return self::dateFunc($name, $a);
238+ case 'json':
239+ $need(1);
240+ $d = self::jsonDecode($a[0]);
241+ return json_encode($d, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION);
242+ case 'json_valid':
243+ $need(1);
244+ if ($a[0] === null) return null;
245+ json_decode((string)Value::toText($a[0]));
246+ return (int)(json_last_error() === JSON_ERROR_NONE);
247+ case 'json_quote':
248+ $need(1);
249+ return self::jsonText(self::toJsonValue($a[0]));
250+ case 'json_array':
251+ return self::jsonText(array_map([self::class, 'toJsonValue'], $a));
252+ case 'json_object':
253+ if ($n % 2) throw new DbException('json_object()の引数は偶数です', 1, 'HY000');
254+ $o = new \stdClass();
255+ for ($i = 0; $i < $n; $i += 2) $o->{(string)Value::toText($a[$i])} = self::toJsonValue($a[$i + 1]);
256+ return self::jsonText($o);
257+ case 'json_extract':
258+ case 'json_extract_text':
259+ case 'json_extract_json':
260+ if ($n < 2) throw new DbException('json_extract()の引数の数が違います', 1, 'HY000');
261+ if ($a[0] === null) return null;
262+ $doc = self::jsonDecode($a[0]);
263+ $out = [];
264+ foreach (array_slice($a, 1) as $path) {
265+ $found = true;
266+ $v = self::jsonPath($doc, $path, $name !== 'json_extract', $found);
267+ if ($name === 'json_extract_json') return $found ? self::jsonText($v) : null;
268+ $out[] = $v;
269+ }
270+ if ($n === 2) {
271+ $v = $out[0];
272+ if (is_array($v) || is_object($v)) return self::jsonText($v);
273+ if (is_bool($v)) return (int)$v;
274+ return $v;
275+ }
276+ return self::jsonText($out);
277+ case 'json_type':
278+ $need(1, 2);
279+ if ($a[0] === null) return null;
280+ $doc = self::jsonDecode($a[0]);
281+ $found = true;
282+ $v = $n === 2 ? self::jsonPath($doc, $a[1], false, $found) : $doc;
283+ if (!$found) return null;
284+ return match (true) {
285+ $v === null => 'null', $v === true => 'true', $v === false => 'false', is_int($v) => 'integer', is_float($v) => 'real',
286+ is_string($v) => 'text', is_array($v) => 'array', default => 'object',
287+ };
288+ case 'json_array_length':
289+ $need(1, 2);
290+ if ($a[0] === null) return null;
291+ $doc = self::jsonDecode($a[0]);
292+ $found = true;
293+ $v = $n === 2 ? self::jsonPath($doc, $a[1], false, $found) : $doc;
294+ return is_array($v) ? count($v) : 0;
295+ }
296+ throw new DbException("関数がありません:{$name}()", 1, 'HY000');
297+ }
298+
299+ private static function substr(mixed $v, int $start, ?int $len): mixed {
300+ $blob = $v instanceof Blob;
301+ $s = $blob ? $v->data : (string)Value::toText($v);
302+ $total = $blob ? strlen($s) : mb_strlen($s, 'UTF-8');
303+ // SQLiteの規則:1から、0は最初の前、負は最後から
304+ if ($start < 0) {
305+ $start = $total + $start + 1;
306+ if ($start < 1) {
307+ if ($len !== null) $len += $start - 1;
308+ $start = 1;
309+ }
310+ } else if ($start === 0) {
311+ $start = 1;
312+ if ($len !== null) $len--;
313+ }
314+ if ($len !== null && $len < 0) {
315+ $start += $len;
316+ $len = -$len;
317+ if ($start < 1) {
318+ $len += $start - 1;
319+ $start = 1;
320+ }
321+ }
322+ $len = $len === null ? null : max(0, $len);
323+ $r = $blob ? (string)substr($s, $start - 1, $len ?? PHP_INT_MAX) : mb_substr($s, $start - 1, $len, 'UTF-8');
324+ return $blob ? new Blob($r) : $r;
325+ }
326+
327+ /** LIKE(ASCIIは大文字と小文字を区別しない) */
328+ public static function like(string $pattern, string $s, ?string $escape = null): bool {
329+ $key = 'L'.$escape."\0".$pattern;
330+ if (!isset(self::$likeCache[$key])) {
331+ if (count(self::$likeCache) > 500) self::$likeCache = [];
332+ $re = '';
333+ $u = mb_check_encoding($pattern, 'UTF-8');
334+ $chars = $u ? mb_str_split($pattern, 1, 'UTF-8') : str_split($pattern);
335+ for ($i = 0; $i < count($chars); $i++) {
336+ $c = $chars[$i];
337+ if ($escape !== null && $c === $escape) {
338+ $re .= preg_quote(strtolower($chars[++$i] ?? ''), '/');
339+ } else if ($c === '%') {
340+ $re .= '.*';
341+ } else if ($c === '_') {
342+ $re .= '.';
343+ } else {
344+ $re .= preg_quote(strtolower($c), '/');
345+ }
346+ }
347+ self::$likeCache[$key] = '/^'.$re.'$/s'.($u ? 'u' : '');
348+ }
349+ return (bool)@preg_match(self::$likeCache[$key], strtolower($s));
350+ }
351+
352+ /** GLOB(大文字と小文字を区別する) */
353+ public static function glob(string $pattern, string $s): bool {
354+ $key = 'G'.$pattern;
355+ if (!isset(self::$likeCache[$key])) {
356+ $re = '';
357+ $u = mb_check_encoding($pattern, 'UTF-8');
358+ $chars = $u ? mb_str_split($pattern, 1, 'UTF-8') : str_split($pattern);
359+ for ($i = 0; $i < count($chars); $i++) {
360+ $c = $chars[$i];
361+ if ($c === '*') {
362+ $re .= '.*';
363+ } else if ($c === '?') {
364+ $re .= '.';
365+ } else if ($c === '[') {
366+ $j = $i + 1;
367+ $set = '';
368+ if (($chars[$j] ?? '') === '^') {
369+ $set .= '^';
370+ $j++;
371+ }
372+ if (($chars[$j] ?? '') === ']') {
373+ $set .= '\]';
374+ $j++;
375+ }
376+ while ($j < count($chars) && $chars[$j] !== ']') {
377+ $set .= $chars[$j] === '-' ? '-' : preg_quote($chars[$j], '/');
378+ $j++;
379+ }
380+ if ($j >= count($chars)) {
381+ $re .= '\[';
382+ continue;
383+ }
384+ $re .= '['.$set.']';
385+ $i = $j;
386+ } else {
387+ $re .= preg_quote($c, '/');
388+ }
389+ }
390+ self::$likeCache[$key] = '/^'.$re.'$/s'.($u ? 'u' : '');
391+ }
392+ return (bool)@preg_match(self::$likeCache[$key], $s);
393+ }
394+
395+ /** printf()(%d、%s、%f、%x、%q、%Q等) */
396+ private static function printf(string $fmt, array $args): string {
397+ $i = 0;
398+ return preg_replace_callback('/%([-+ 0#,!]*)(\d+|\*)?(?:\.(\d+|\*))?(ll|l)?([diouxXeEfgGcsqQwz%])/', function(array $m) use (&$i, $args): string {
399+ [, $flags, $width, $prec, , $conv] = $m + ['', '', '', '', '', ''];
400+ if ($conv === '%') return '%';
401+ if ($width === '*') $width = (string)Value::toInt($args[$i++] ?? 0);
402+ if ($prec === '*') $prec = (string)Value::toInt($args[$i++] ?? 0);
403+ $v = $args[$i++] ?? null;
404+ $flags = str_replace([',', '!'], '', $flags);
405+ $spec = '%'.$flags.$width.($prec !== '' ? '.'.$prec : '');
406+ switch ($conv) {
407+ case 'd':
408+ case 'i':
409+ return sprintf($spec.'d', (int)Value::toInt($v));
410+ case 'u':
411+ return sprintf($spec.'u', (int)Value::toInt($v));
412+ case 'o':
413+ case 'x':
414+ case 'X':
415+ return sprintf($spec.$conv, (int)Value::toInt($v));
416+ case 'e':
417+ case 'E':
418+ case 'f':
419+ case 'g':
420+ case 'G':
421+ return sprintf($spec.($conv === 'f' ? 'F' : $conv), (float)Value::toNumber($v));
422+ case 'c':
423+ return mb_substr((string)Value::toText($v), 0, 1, 'UTF-8');
424+ case 'q':
425+ return $v === null ? '(NULL)' : str_replace("'", "''", (string)Value::toText($v));
426+ case 'Q':
427+ return $v === null ? 'NULL' : "'".str_replace("'", "''", (string)Value::toText($v))."'";
428+ case 'w':
429+ return $v === null ? '(NULL)' : str_replace('"', '""', (string)Value::toText($v));
430+ default:
431+ return sprintf($spec.'s', $v === null ? '' : (string)Value::toText($v));
432+ }
433+ }, $fmt);
434+ }
435+
436+ ///////////////
437+ // 日付
438+
439+ /**
440+ * 時間の値をUNIXの秒(マイクロ秒付き)にする
441+ *
442+ * @return float|null
443+ */
444+ public static function timeValue(mixed $v): ?float {
445+ if ($v === null) return null;
446+ if (is_int($v) || is_float($v)) {
447+ // ユリウス日
448+ return ((float)$v - 2440587.5) * 86400.0;
449+ }
450+ $s = trim((string)Value::toText($v));
451+ if (strcasecmp($s, 'now') === 0) return microtime(true);
452+ if (preg_match('/^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2})(?::(\d{2})(\.\d+)?)?)?\s*(Z|[+-]\d{2}:?\d{2})?$/i', $s, $m)) {
453+ $t = gmmktime((int)($m[4] ?? 0), (int)($m[5] ?? 0), (int)($m[6] ?? 0), (int)$m[2], (int)$m[3], (int)$m[1]);
454+ $t += (float)('0'.($m[7] ?? ''));
455+ if (!empty($m[8]) && strtoupper($m[8]) !== 'Z') {
456+ $sign = $m[8][0] === '-' ? -1 : 1;
457+ $hm = str_replace(':', '', substr($m[8], 1));
458+ $t -= $sign * ((int)substr($hm, 0, 2) * 3600 + (int)substr($hm, 2, 2) * 60);
459+ }
460+ return (float)$t;
461+ }
462+ if (preg_match('/^(\d{2}):(\d{2})(?::(\d{2})(\.\d+)?)?$/', $s, $m)) {
463+ return (float)gmmktime((int)$m[1], (int)$m[2], (int)($m[3] ?? 0), 1, 1, 2000) + (float)('0'.($m[4] ?? ''));
464+ }
465+ $n = Value::parseNumber($s, true);
466+ if ($n !== null) return ((float)$n - 2440587.5) * 86400.0;
467+ return null;
468+ }
469+
470+ private static function dateFunc(string $name, array $a): mixed {
471+ $fmt = null;
472+ if ($name === 'strftime') {
473+ if (empty($a) || $a[0] === null) return null;
474+ $fmt = (string)Value::toText($a[0]);
475+ $a = array_slice($a, 1);
476+ }
477+ if ($name === 'timediff') {
478+ if (count($a) !== 2) throw new DbException('timediff()の引数の数が違います', 1, 'HY000');
479+ $x = self::timeValue($a[0]);
480+ $y = self::timeValue($a[1]);
481+ if ($x === null || $y === null) return null;
482+ $d = $x - $y;
483+ $sign = $d < 0 ? '-' : '+';
484+ $d = abs($d);
485+ $days = (int)floor($d / 86400);
486+ return sprintf('%s%04d-%02d-%02d %02d:%02d:%06.3f', $sign, 0, 0, $days, (int)($d / 3600) % 24, (int)($d / 60) % 60, fmod($d, 60));
487+ }
488+ $v = empty($a) ? 'now' : $a[0];
489+ $mods = array_slice($a, 1);
490+ // unixepochの修飾子
491+ $t = null;
492+ foreach ($mods as $m) {
493+ if (is_string($m) && strcasecmp(trim($m), 'unixepoch') === 0 && (is_int($v) || is_float($v) || (is_string($v) && is_numeric($v)))) {
494+ $t = (float)Value::toNumber($v);
495+ }
496+ }
497+ if ($t === null) $t = self::timeValue($v);
498+ if ($t === null) return null;
499+ foreach ($mods as $m) {
500+ if ($m === null) return null;
501+ $m = strtolower(trim((string)Value::toText($m)));
502+ if ($m === 'unixepoch' || $m === 'utc' || $m === 'auto' || $m === 'subsec' || $m === 'subsecond') continue;
503+ if ($m === 'localtime') {
504+ $t += (int)date('Z', (int)$t);
505+ continue;
506+ }
507+ if (preg_match('/^([+-]?\d+(?:\.\d+)?)\s*(second|minute|hour|day|month|year)s?$/', $m, $mm)) {
508+ $q = (float)$mm[1];
509+ switch ($mm[2]) {
510+ case 'second': $t += $q; break;
511+ case 'minute': $t += $q * 60; break;
512+ case 'hour': $t += $q * 3600; break;
513+ case 'day': $t += $q * 86400; break;
514+ case 'month':
515+ case 'year':
516+ $frac = $t - floor($t);
517+ $d = new \DateTimeImmutable('@'.(int)floor($t));
518+ $months = (int)$q * ($mm[2] === 'year' ? 12 : 1);
519+ $y = (int)$d->format('Y');
520+ $mo = (int)$d->format('n') + $months;
521+ $y += intdiv($mo - 1 - (($mo - 1) % 12 + 12) % 12, 12);
522+ $mo = (($mo - 1) % 12 + 12) % 12 + 1;
523+ $t = gmmktime((int)$d->format('G'), (int)$d->format('i'), (int)$d->format('s'), $mo, (int)$d->format('j'), $y) + $frac;
524+ break;
525+ }
526+ continue;
527+ }
528+ if (preg_match('/^start of (day|month|year)$/', $m, $mm)) {
529+ $d = gmdate('Y-m-d', (int)floor($t));
530+ [$y, $mo, $dd] = explode('-', $d);
531+ $t = (float)match ($mm[1]) { 'day' => gmmktime(0, 0, 0, (int)$mo, (int)$dd, (int)$y), 'month' => gmmktime(0, 0, 0, (int)$mo, 1, (int)$y), default => gmmktime(0, 0, 0, 1, 1, (int)$y) };
532+ continue;
533+ }
534+ if (preg_match('/^weekday (\d)$/', $m, $mm)) {
535+ $w = (int)gmdate('w', (int)floor($t));
536+ $t += ((((int)$mm[1] - $w) % 7 + 7) % 7) * 86400;
537+ continue;
538+ }
539+ return null;
540+ }
541+ $sec = (int)floor($t);
542+ $frac = $t - $sec;
543+ switch ($name) {
544+ case 'date':
545+ return gmdate('Y-m-d', $sec);
546+ case 'time':
547+ return gmdate('H:i:s', $sec);
548+ case 'datetime':
549+ return gmdate('Y-m-d H:i:s', $sec);
550+ case 'julianday':
551+ return $t / 86400.0 + 2440587.5;
552+ case 'unixepoch':
553+ return $sec;
554+ }
555+ // strftime
556+ return preg_replace_callback('/%(.)/', function(array $m) use ($sec, $frac, $t): string {
557+ return match ($m[1]) {
558+ 'd' => gmdate('d', $sec), 'e' => str_pad(gmdate('j', $sec), 2, ' ', STR_PAD_LEFT), 'f' => sprintf('%06.3f', (float)gmdate('s', $sec) + $frac),
559+ 'F' => gmdate('Y-m-d', $sec), 'H' => gmdate('H', $sec), 'I' => gmdate('h', $sec), 'j' => sprintf('%03d', (int)gmdate('z', $sec) + 1),
560+ 'J' => (string)($t / 86400.0 + 2440587.5), 'k' => str_pad(gmdate('G', $sec), 2, ' ', STR_PAD_LEFT), 'l' => str_pad(gmdate('g', $sec), 2, ' ', STR_PAD_LEFT),
561+ 'm' => gmdate('m', $sec), 'M' => gmdate('i', $sec), 'p' => gmdate('A', $sec), 'P' => gmdate('a', $sec), 'R' => gmdate('H:i', $sec),
562+ 's' => (string)$sec, 'S' => gmdate('s', $sec), 'T' => gmdate('H:i:s', $sec), 'u' => gmdate('N', $sec), 'w' => gmdate('w', $sec),
563+ 'W' => sprintf('%02d', intdiv((int)gmdate('z', $sec) + 7 - (((int)gmdate('w', $sec) + 6) % 7), 7)), 'Y' => gmdate('Y', $sec),
564+ 'G' => gmdate('o', $sec), 'g' => gmdate('y', $sec), 'V' => gmdate('W', $sec), 'U' => sprintf('%02d', intdiv((int)gmdate('z', $sec) + 7 - (int)gmdate('w', $sec), 7)),
565+ '%' => '%',
566+ default => '',
567+ };
568+ }, $fmt ?? '');
569+ }
570+
571+ ///////////////
572+ // JSON
573+
574+ private static function jsonDecode(mixed $v): mixed {
575+ if ($v === null) return null;
576+ $d = json_decode((string)Value::toText($v), false, 512, JSON_BIGINT_AS_STRING);
577+ if (json_last_error() !== JSON_ERROR_NONE) throw new DbException('不正なJSON', 1, 'HY000');
578+ return $d;
579+ }
580+
581+ private static function jsonText(mixed $v): string {
582+ return (string)json_encode($v, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION);
583+ }
584+
585+ public static function toJsonValue(mixed $v): mixed {
586+ if ($v instanceof Blob) throw new DbException('JSONにBLOBは使えません', 1, 'HY000');
587+ return $v;
588+ }
589+
590+ /** $.a.b[0]、->の場合は「a」・数値も */
591+ private static function jsonPath(mixed $doc, mixed $path, bool $arrow, bool &$found): mixed {
592+ if ($path === null) {
593+ $found = false;
594+ return null;
595+ }
596+ if ($arrow && is_int($path)) $path = '$['.$path.']';
597+ $p = (string)Value::toText($path);
598+ if ($arrow && !str_starts_with($p, '$')) $p = '$.'.$p;
599+ if (!str_starts_with($p, '$')) throw new DbException("不正なJSONのパス:{$p}", 1, 'HY000');
600+ preg_match_all('/\.(?:"((?:[^"\\\\]|\\\\.)*)"|([^.\[]+))|\[(#-)?(\d+)\]/', substr($p, 1), $m, PREG_SET_ORDER);
601+ $cur = $doc;
602+ foreach ($m as $part) {
603+ if (isset($part[4]) && $part[4] !== '') {
604+ if (!is_array($cur)) {
605+ $found = false;
606+ return null;
607+ }
608+ $i = (int)$part[4];
609+ if (!empty($part[3])) $i = count($cur) - $i;
610+ if (!array_key_exists($i, $cur)) {
611+ $found = false;
612+ return null;
613+ }
614+ $cur = $cur[$i];
615+ } else {
616+ $k = $part[1] !== '' ? stripcslashes($part[1]) : $part[2];
617+ if (!is_object($cur) || !property_exists($cur, $k)) {
618+ $found = false;
619+ return null;
620+ }
621+ $cur = $cur->$k;
622+ }
623+ }
624+ return $cur;
625+ }
626+
627+ ///////////////
628+ // 集約関数
629+
630+ /** 集約の初期の状態 */
631+ public static function aggInit(string $name): array {
632+ return ['n' => 0, 'v' => null, 'sum' => 0, 'float' => false, 'parts' => [], 'seen' => []];
633+ }
634+
635+ /** 1行分を足す */
636+ public static function aggStep(string $name, array &$st, array $args, bool $star, string $coll): void {
637+ if ($star) {
638+ $st['n']++;
639+ return;
640+ }
641+ $v = $args[0] ?? null;
642+ if ($v === null) return;
643+ switch ($name) {
644+ case 'count':
645+ $st['n']++;
646+ return;
647+ case 'sum':
648+ case 'total':
649+ case 'avg':
650+ $num = Value::toNumber($v);
651+ $st['n']++;
652+ // 整数以外(文字列も)が有ればREAL
653+ if (!is_int($v) || $st['float']) {
654+ $st['float'] = true;
655+ $st['sum'] = (float)$st['sum'] + (float)$num;
656+ } else {
657+ $s = $st['sum'] + $num;
658+ if (is_float($s)) {
659+ if ($name === 'sum') throw new DbException('整数のオーバーフロー', 1, 'HY000');
660+ $st['float'] = true;
661+ }
662+ $st['sum'] = $s;
663+ }
664+ return;
665+ case 'min':
666+ case 'max':
667+ if ($st['n'] === 0 || ($name === 'min' ? Record::compare($v, $st['v'], $coll) < 0 : Record::compare($v, $st['v'], $coll) > 0)) $st['v'] = $v;
668+ $st['n']++;
669+ return;
670+ case 'group_concat':
671+ case 'string_agg':
672+ $sep = array_key_exists(1, $args) ? $args[1] : ($name === 'string_agg' ? null : ',');
673+ if ($name === 'string_agg' && count($args) < 2) throw new DbException('string_agg()の引数の数が違います', 1, 'HY000');
674+ if ($st['n'] > 0) $st['parts'][] = (string)Value::toText($sep);
675+ $st['parts'][] = (string)Value::toText($v);
676+ $st['n']++;
677+ return;
678+ }
679+ }
680+
681+ /** 結果 */
682+ public static function aggFinal(string $name, array $st): mixed {
683+ return match ($name) {
684+ 'count' => $st['n'],
685+ 'sum' => $st['n'] === 0 ? null : $st['sum'],
686+ 'total' => (float)$st['sum'],
687+ 'avg' => $st['n'] === 0 ? null : (float)$st['sum'] / $st['n'],
688+ 'min', 'max' => $st['v'],
689+ 'group_concat', 'string_agg' => $st['n'] === 0 ? null : implode('', $st['parts']),
690+ default => null,
691+ };
692+ }
693+}
@@ -0,0 +1,192 @@
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\Lib\Db\Sqlite;
39+
40+/**
41+ * PRAGMA integrity_check(ツリー・フリーリスト・インデックスの確認)
42+ */
43+final class Integrity {
44+ private array $used = [];
45+ private array $msgs = [];
46+
47+ public function __construct(private Pager $pager, private Btree $bt, private Schema $schema) {}
48+
49+ /**
50+ * @return array 問題のメッセージ(無ければ空)
51+ */
52+ public function check(): array {
53+ if ($this->pager->nPages === 0) return [];
54+ $this->tree(1, null);
55+ foreach ($this->schema->tables as $t) {
56+ if (($t['root'] ?? 0) > 1) $this->tree($t['root'], null);
57+ }
58+ foreach ($this->schema->indexes as $idx) $this->tree($idx['root'], $idx);
59+ $this->freelist();
60+ $lock = intdiv(0x40000000, $this->pager->pageSize) + 1;
61+ for ($p = 1; $p <= $this->pager->nPages; $p++) {
62+ if (!isset($this->used[$p]) && $p !== $lock) $this->msgs[] = "ページ{$p}は使われていません";
63+ }
64+ if (empty($this->msgs)) $this->indexes();
65+ return array_slice($this->msgs, 0, 100);
66+ }
67+
68+ private function mark(int $p, string $what): bool {
69+ if ($p < 1 || $p > $this->pager->nPages) {
70+ $this->msgs[] = "{$what}:不正なページ番号{$p}";
71+ return false;
72+ }
73+ if (isset($this->used[$p])) {
74+ $this->msgs[] = "ページ{$p}が2回使われています({$this->used[$p]}、{$what})";
75+ return false;
76+ }
77+ $this->used[$p] = $what;
78+ return true;
79+ }
80+
81+ private function tree(int $root, ?array $idx): void {
82+ $depths = [];
83+ $this->page($root, $idx, null, null, 0, $depths, $root);
84+ if (count(array_unique($depths)) > 1) $this->msgs[] = "ツリー{$root}:葉の深さが違います";
85+ }
86+
87+ private function page(int $pgno, ?array $idx, mixed $lo, mixed $hi, int $depth, array &$depths, int $root): void {
88+ if (!$this->mark($pgno, "ツリー{$root}")) return;
89+ try {
90+ $p = $this->bt->page($pgno);
91+ } catch (\Throwable $e) {
92+ $this->msgs[] = "ページ{$pgno}:".$e->getMessage();
93+ return;
94+ }
95+ if ($p->isTable() !== ($idx === null)) {
96+ $this->msgs[] = "ページ{$pgno}:ツリーの種類が違います";
97+ return;
98+ }
99+ $n = count($p->cells);
100+ if ($n === 0 && $pgno !== $root && $p->isLeaf()) $this->msgs[] = "ページ{$pgno}:空の葉";
101+ $prev = $lo;
102+ for ($i = 0; $i <= $n; $i++) {
103+ $key = null;
104+ if ($i < $n) {
105+ try {
106+ $key = $p->isTable() ? $p->keys[$i] : $this->bt->key($p, $i);
107+ } catch (\Throwable $e) {
108+ $this->msgs[] = "ページ{$pgno}のセル{$i}:".$e->getMessage();
109+ continue;
110+ }
111+ $this->overflow($p, $i, $root);
112+ }
113+ if (!$p->isLeaf()) $this->page($i < $n ? $p->left[$i] : $p->right, $idx, $prev, $i < $n ? $key : $hi, $depth + 1, $depths, $root);
114+ if ($i === $n) break;
115+ if ($p->isTable()) {
116+ if ($prev !== null && ($p->isLeaf() ? $key <= $prev : $key < $prev)) $this->msgs[] = "ページ{$pgno}のセル{$i}:rowidの順番";
117+ if ($hi !== null && $key > $hi) $this->msgs[] = "ページ{$pgno}のセル{$i}:rowidが親より大きい";
118+ } else {
119+ if ($prev !== null && Record::compareKeys($key, $prev, $idx['colls'], $idx['desc']) <= 0) $this->msgs[] = "ページ{$pgno}のセル{$i}:キーの順番";
120+ if ($hi !== null && Record::compareKeys($key, $hi, $idx['colls'], $idx['desc']) >= 0) $this->msgs[] = "ページ{$pgno}のセル{$i}:キーが親より大きい";
121+ }
122+ $prev = $key;
123+ }
124+ if ($p->isLeaf()) $depths[] = $depth;
125+ }
126+
127+ private function overflow(Page $p, int $i, int $root): void {
128+ if (!$p->isLeaf() && $p->isTable()) return;
129+ $c = $p->cells[$i];
130+ $pos = 0;
131+ $len = Record::getVarint($c, $pos);
132+ if ($p->isTable()) Record::getVarint($c, $pos);
133+ $local = $this->bt->localSize($len, $p->isTable());
134+ if ($len <= $local) return;
135+ $next = unpack('N', $c, $pos + $local)[1];
136+ $need = intdiv($len - $local + $this->pager->usable - 5, $this->pager->usable - 4);
137+ for ($k = 0; $k < $need; $k++) {
138+ if (!$this->mark($next, "オーバーフロー(ツリー{$root})")) return;
139+ $next = unpack('N', $this->pager->read($next))[1];
140+ }
141+ if ($next !== 0) $this->msgs[] = "ツリー{$root}:オーバーフローが長過ぎます";
142+ }
143+
144+ private function freelist(): void {
145+ $t = $this->pager->hdr['freeTrunk'];
146+ $count = 0;
147+ while ($t !== 0) {
148+ if (!$this->mark($t, 'フリーリスト')) return;
149+ $count++;
150+ $raw = $this->pager->read($t);
151+ $n = unpack('N', $raw, 4)[1];
152+ if ($n > intdiv($this->pager->usable, 4) - 2) {
153+ $this->msgs[] = "フリーリストのページ{$t}:数が不正です";
154+ return;
155+ }
156+ for ($i = 0; $i < $n; $i++) {
157+ $this->mark(unpack('N', $raw, 8 + $i * 4)[1], 'フリーリスト');
158+ $count++;
159+ }
160+ $t = unpack('N', $raw)[1];
161+ }
162+ if ($count !== $this->pager->hdr['freeCount']) $this->msgs[] = "フリーリストのページ数:{$count}(ヘッダー:{$this->pager->hdr['freeCount']})";
163+ }
164+
165+ /** インデックスとテーブルの行が合っているか */
166+ private function indexes(): void {
167+ foreach ($this->schema->indexes as $idx) {
168+ $t = $this->schema->tables[$idx['table']] ?? null;
169+ if (!$t || isset($t['unsupported'])) continue;
170+ $n = 0;
171+ foreach ($this->bt->indexScan($idx) as $key) {
172+ $n++;
173+ $rowid = end($key);
174+ $rec = $this->bt->get($t['root'], $rowid);
175+ if ($rec === null) {
176+ $this->msgs[] = "インデックス{$idx['name']}:行{$rowid}がテーブルにありません";
177+ continue;
178+ }
179+ $vals = Record::decode($rec);
180+ foreach ($idx['cols'] as $k => $ci) {
181+ $v = $ci === $t['ipk'] ? $rowid : ($vals[$ci] ?? null);
182+ if (Record::compare($v, $key[$k], 'BINARY') !== 0 && $vals[$ci] !== null) {
183+ $this->msgs[] = "インデックス{$idx['name']}:行{$rowid}の値が違います";
184+ break;
185+ }
186+ }
187+ }
188+ $rows = $this->bt->count($t['root']);
189+ if ($n !== $rows) $this->msgs[] = "インデックス{$idx['name']}:{$n}行(テーブルは{$rows}行)";
190+ }
191+ }
192+}
@@ -0,0 +1,68 @@
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\Lib\Db\Sqlite;
39+
40+/**
41+ * Bツリーのページ(Btreeが使う)
42+ */
43+final class Page {
44+ /** 種類:2(インデックスの内部)、5(テーブルの内部)、10(インデックスの葉)、13(テーブルの葉) */
45+ public int $type = 13;
46+ /** セル(内部のページは左の子のページ番号を除く) */
47+ public array $cells = [];
48+ /** 内部のページ:セル毎の左の子 */
49+ public array $left = [];
50+ /** 内部のページ:一番右の子 */
51+ public int $right = 0;
52+ /** テーブル:セル毎のrowid */
53+ public array $keys = [];
54+ /** インデックス:セル毎のデコードしたキー(まだならnull) */
55+ public array $dec = [];
56+ /** セルの大きさの合計(セルのポインターも含む) */
57+ public int $size = 0;
58+
59+ public function __construct(public readonly int $pgno) {}
60+
61+ public function isLeaf(): bool {
62+ return $this->type === 10 || $this->type === 13;
63+ }
64+
65+ public function isTable(): bool {
66+ return $this->type === 5 || $this->type === 13;
67+ }
68+}
@@ -0,0 +1,583 @@
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\Lib\Db\Sqlite;
39+
40+use Std\Lib\Db\DbException;
41+
42+/**
43+ * SQLiteのファイルのページ(読み書き、ロック、ロールバックジャーナル、フリーリスト)
44+ *
45+ * ジャーナルはjournal_mode = DELETEと同じ形式(「ファイル名-journal」)なので、途中で止まった書き込みは
46+ * このクラスでもsqlite3でも元に戻せる。ロックはflock()なので、PHPのプロセス同士でしか効かない。
47+ */
48+final 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+}
@@ -0,0 +1,1351 @@
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\Lib\Db\Sqlite;
39+
40+use Std\Lib\Db\DbException;
41+
42+/**
43+ * SQLiteのSQLの構文解析(字句解析も)
44+ *
45+ * 結果は配列の構文木。式は['t' => 種類, …]。
46+ */
47+final class Parser {
48+ // 別名(AS無し)や名前に使えない言葉
49+ private const RESERVED = [
50+ 'ADD', 'ALL', 'ALTER', 'AND', 'AS', 'BETWEEN', 'BY', 'CASE', 'CHECK', 'COLLATE', 'COMMIT', 'CONSTRAINT', 'CREATE', 'CROSS',
51+ 'DEFAULT', 'DELETE', 'DISTINCT', 'DROP', 'ELSE', 'END', 'ESCAPE', 'EXCEPT', 'EXISTS', 'FOREIGN', 'FROM', 'FULL', 'GLOB',
52+ 'GROUP', 'HAVING', 'IN', 'INDEX', 'INNER', 'INSERT', 'INTERSECT', 'INTO', 'IS', 'ISNULL', 'JOIN', 'LEFT', 'LIKE', 'LIMIT',
53+ 'NATURAL', 'NOT', 'NOTNULL', 'NULL', 'OFFSET', 'ON', 'OR', 'ORDER', 'OUTER', 'PRIMARY', 'REFERENCES', 'RETURNING',
54+ 'RIGHT', 'SELECT', 'SET', 'TABLE', 'THEN', 'TO', 'UNION', 'UNIQUE', 'UPDATE', 'USING', 'VALUES', 'WHEN', 'WHERE',
55+ 'WINDOW', 'WITH', 'REGEXP', 'MATCH',
56+ ];
57+
58+ private array $tok = [];
59+ private int $p = 0;
60+ private string $sql = '';
61+ private int $nParam = 0;
62+ /** 名前付きのパラメータ(「:name」等) => 番号 */
63+ private array $names = [];
64+ private int $lastNameStart = 0;
65+
66+ /**
67+ * SQLを文に分ける
68+ *
69+ * @param string $sql
70+ * @return array 文の構文木の配列(各文の'sql'に元の文字列)
71+ * @throws DbException
72+ */
73+ public function parseAll(string $sql): array {
74+ $this->sql = $sql;
75+ $this->tok = $this->lex($sql);
76+ $this->p = 0;
77+ $out = [];
78+ while (true) {
79+ while ($this->isOp(';')) $this->p++;
80+ if ($this->cur()['t'] === 'eof') break;
81+ $this->nParam = 0;
82+ $start = $this->cur()['s'];
83+ $this->names = [];
84+ $st = $this->statement();
85+ $end = $this->tok[$this->p - 1]['e'];
86+ $st['sql'] = substr($sql, $start, $end - $start);
87+ $st['start'] = $start;
88+ $st['nParam'] = $this->nParam;
89+ $st['names'] = $this->names;
90+ $out[] = $st;
91+ if (!$this->isOp(';') && $this->cur()['t'] !== 'eof') $this->error();
92+ }
93+ return $out;
94+ }
95+
96+ /** 式だけ(DEFAULT・CHECK等の文字列から) */
97+ public function parseExpr(string $sql): array {
98+ $this->sql = $sql;
99+ $this->tok = $this->lex($sql);
100+ $this->p = 0;
101+ $e = $this->expr();
102+ if ($this->cur()['t'] !== 'eof') $this->error();
103+ return $e;
104+ }
105+
106+ ///////////////
107+ // 字句解析
108+
109+ private function lex(string $s): array {
110+ $out = [];
111+ $len = strlen($s);
112+ $i = 0;
113+ while ($i < $len) {
114+ $c = $s[$i];
115+ if (ctype_space($c)) {
116+ $i++;
117+ continue;
118+ }
119+ if ($c === '-' && ($s[$i + 1] ?? '') === '-') {
120+ $j = strpos($s, "\n", $i);
121+ $i = $j === false ? $len : $j + 1;
122+ continue;
123+ }
124+ if ($c === '/' && ($s[$i + 1] ?? '') === '*') {
125+ $j = strpos($s, '*/', $i + 2);
126+ $i = $j === false ? $len : $j + 2;
127+ continue;
128+ }
129+ $st = $i;
130+ if ($c === "'") {
131+ $v = '';
132+ $i++;
133+ while (true) {
134+ $j = strpos($s, "'", $i);
135+ if ($j === false) throw new DbException('閉じられていない文字列', 1, 'HY000');
136+ $v .= substr($s, $i, $j - $i);
137+ if (($s[$j + 1] ?? '') === "'") {
138+ $v .= "'";
139+ $i = $j + 2;
140+ continue;
141+ }
142+ $i = $j + 1;
143+ break;
144+ }
145+ $out[] = ['t' => 'str', 'v' => $v, 's' => $st, 'e' => $i];
146+ continue;
147+ }
148+ if ($c === '"' || $c === '`' || $c === '[') {
149+ $close = $c === '[' ? ']' : $c;
150+ $v = '';
151+ $i++;
152+ while (true) {
153+ $j = strpos($s, $close, $i);
154+ if ($j === false) throw new DbException('閉じられていない識別子', 1, 'HY000');
155+ $v .= substr($s, $i, $j - $i);
156+ if ($close !== ']' && ($s[$j + 1] ?? '') === $close) {
157+ $v .= $close;
158+ $i = $j + 2;
159+ continue;
160+ }
161+ $i = $j + 1;
162+ break;
163+ }
164+ $out[] = ['t' => 'id', 'v' => $v, 'u' => '', 'q' => true, 's' => $st, 'e' => $i];
165+ continue;
166+ }
167+ if (($c === 'x' || $c === 'X') && ($s[$i + 1] ?? '') === "'") {
168+ $j = strpos($s, "'", $i + 2);
169+ if ($j === false) throw new DbException('閉じられていないBLOB', 1, 'HY000');
170+ $hex = substr($s, $i + 2, $j - $i - 2);
171+ if (strlen($hex) % 2 || !ctype_xdigit($hex) && $hex !== '') throw new DbException("不正なBLOB:{$hex}", 1, 'HY000');
172+ $i = $j + 1;
173+ $out[] = ['t' => 'blob', 'v' => (string)hex2bin($hex), 's' => $st, 'e' => $i];
174+ continue;
175+ }
176+ if (ctype_digit($c) || ($c === '.' && ctype_digit($s[$i + 1] ?? ''))) {
177+ if ($c === '0' && ($s[$i + 1] ?? '') !== '' && strtolower($s[$i + 1]) === 'x' && preg_match('/\G0[xX]([0-9a-fA-F]+)/', $s, $m, 0, $i)) {
178+ $i += strlen($m[0]);
179+ $hex = str_pad($m[1], 16, '0', STR_PAD_LEFT);
180+ if (strlen($hex) > 16) throw new DbException("16進数が大き過ぎます:{$m[0]}", 1, 'HY000');
181+ $out[] = ['t' => 'num', 'v' => unpack('J', hex2bin($hex))[1], 's' => $st, 'e' => $i];
182+ continue;
183+ }
184+ preg_match('/\G(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?/', $s, $m, 0, $i);
185+ $i += strlen($m[0]);
186+ if (preg_match('/\G[A-Za-z_]/', $s, $mm, 0, $i)) throw new DbException("認識できないトークン:{$m[0]}".substr($s, $i, 1), 1, 'HY000');
187+ $txt = $m[0];
188+ if (ctype_digit($txt)) {
189+ $t2 = ltrim($txt, '0');
190+ $v = $t2 === '' ? 0 : ((string)(int)$t2 === $t2 ? (int)$t2 : (float)$txt);
191+ } else {
192+ $v = (float)$txt;
193+ }
194+ $out[] = ['t' => 'num', 'v' => $v, 's' => $st, 'e' => $i];
195+ continue;
196+ }
197+ if (ctype_alpha($c) || $c === '_' || ord($c) >= 0x80) {
198+ preg_match('/\G[A-Za-z0-9_$\x80-\xFF]+/', $s, $m, 0, $i);
199+ $i += strlen($m[0]);
200+ $out[] = ['t' => 'id', 'v' => $m[0], 'u' => strtoupper($m[0]), 'q' => false, 's' => $st, 'e' => $i];
201+ continue;
202+ }
203+ if ($c === '?') {
204+ preg_match('/\G\?(\d*)/', $s, $m, 0, $i);
205+ $i += strlen($m[0]);
206+ $out[] = ['t' => 'param', 'v' => $m[1] === '' ? null : (int)$m[1], 's' => $st, 'e' => $i];
207+ continue;
208+ }
209+ if (($c === ':' || $c === '@' || $c === '$') && preg_match('/\G[:@$][A-Za-z0-9_\x80-\xFF]+/', $s, $m, 0, $i)) {
210+ $i += strlen($m[0]);
211+ $out[] = ['t' => 'param', 'v' => $m[0], 's' => $st, 'e' => $i];
212+ continue;
213+ }
214+ foreach (['->>', '||', '<<', '>>', '<=', '>=', '==', '!=', '<>', '->'] as $op) {
215+ if (substr($s, $i, strlen($op)) === $op) {
216+ $i += strlen($op);
217+ $out[] = ['t' => 'op', 'v' => $op, 's' => $st, 'e' => $i];
218+ continue 2;
219+ }
220+ }
221+ if (str_contains('(),;.=<>+-*/%&|~', $c)) {
222+ $i++;
223+ $out[] = ['t' => 'op', 'v' => $c, 's' => $st, 'e' => $i];
224+ continue;
225+ }
226+ throw new DbException("認識できない文字:{$c}", 1, 'HY000');
227+ }
228+ $out[] = ['t' => 'eof', 'v' => '', 's' => $len, 'e' => $len];
229+ return $out;
230+ }
231+
232+ ///////////////
233+ // 補助
234+
235+ private function cur(): array {
236+ return $this->tok[$this->p];
237+ }
238+
239+ private function peek(int $n = 1): array {
240+ return $this->tok[min($this->p + $n, count($this->tok) - 1)];
241+ }
242+
243+ private function isKw(string ...$kw): bool {
244+ $t = $this->tok[$this->p];
245+ return $t['t'] === 'id' && !$t['q'] && in_array($t['u'], $kw, true);
246+ }
247+
248+ private function acceptKw(string ...$kw): ?string {
249+ if (!$this->isKw(...$kw)) return null;
250+ return $this->tok[$this->p++]['u'];
251+ }
252+
253+ private function expectKw(string $kw): void {
254+ if (!$this->acceptKw($kw)) $this->error($kw);
255+ }
256+
257+ private function isOp(string $op): bool {
258+ $t = $this->tok[$this->p];
259+ return $t['t'] === 'op' && $t['v'] === $op;
260+ }
261+
262+ private function acceptOp(string $op): bool {
263+ if (!$this->isOp($op)) return false;
264+ $this->p++;
265+ return true;
266+ }
267+
268+ private function expectOp(string $op): void {
269+ if (!$this->acceptOp($op)) $this->error($op);
270+ }
271+
272+ /** @return never */
273+ private function error(string $expected = ''): never {
274+ $t = $this->cur();
275+ $near = $t['t'] === 'eof' ? '最後' : '「'.substr($this->sql, $t['s'], max(1, min(30, $t['e'] - $t['s']))).'」';
276+ throw new DbException("SQLの構文エラー:{$near}の近く".($expected !== '' ? "({$expected}が必要)" : ''), 1, '42000');
277+ }
278+
279+ /** 名前(識別子、予約語以外の言葉、文字列も可) */
280+ private function name(bool $allowReserved = false): string {
281+ $t = $this->cur();
282+ if ($t['t'] === 'id' && ($t['q'] || $allowReserved || !in_array($t['u'], self::RESERVED, true))) {
283+ $this->p++;
284+ return $t['v'];
285+ }
286+ if ($t['t'] === 'str') {
287+ $this->p++;
288+ return $t['v'];
289+ }
290+ $this->error('名前');
291+ }
292+
293+ private function isName(): bool {
294+ $t = $this->cur();
295+ return ($t['t'] === 'id' && ($t['q'] || !in_array($t['u'], self::RESERVED, true))) || $t['t'] === 'str';
296+ }
297+
298+ /** 「schema.name」(schemaはmain・tempのみ) */
299+ private function qualifiedName(): string {
300+ $this->lastNameStart = $this->cur()['s'];
301+ $n = $this->name();
302+ if ($this->isOp('.') && $this->peek()['t'] === 'id') {
303+ if (!in_array(strtolower($n), ['main', 'temp'], true)) throw new DbException("不明なデータベース:{$n}", 1, 'HY000');
304+ $this->p++;
305+ $this->lastNameStart = $this->cur()['s'];
306+ $n = $this->name();
307+ }
308+ return $n;
309+ }
310+
311+ /** 字句解析だけ(スキーマのSQLの書き換え用) */
312+ public function tokens(string $sql): array {
313+ return $this->lex($sql);
314+ }
315+
316+ private function ifNotExists(): bool {
317+ if ($this->isKw('IF') && $this->peek()['t'] === 'id' && $this->peek()['u'] === 'NOT') {
318+ $this->p += 2;
319+ $this->expectKw('EXISTS');
320+ return true;
321+ }
322+ return false;
323+ }
324+
325+ private function ifExists(): bool {
326+ if ($this->isKw('IF') && $this->peek()['t'] === 'id' && $this->peek()['u'] === 'EXISTS') {
327+ $this->p += 2;
328+ return true;
329+ }
330+ return false;
331+ }
332+
333+ ///////////////
334+ // 文
335+
336+ private function statement(): array {
337+ if ($this->acceptKw('EXPLAIN')) throw new DbException('EXPLAINは未対応です', 1, 'HY000');
338+ if ($this->isKw('SELECT', 'VALUES', 'WITH')) {
339+ $with = $this->isKw('WITH') ? $this->with() : [];
340+ if ($this->isKw('INSERT', 'REPLACE')) return $this->insert() + ['with' => $with];
341+ if ($this->isKw('UPDATE')) return $this->update() + ['with' => $with];
342+ if ($this->isKw('DELETE')) return $this->delete() + ['with' => $with];
343+ $s = $this->select(false);
344+ $s['with'] = array_merge($with, $s['with']);
345+ return $s;
346+ }
347+ if ($this->isOp('(')) return $this->select();
348+ if ($this->isKw('INSERT', 'REPLACE')) return $this->insert();
349+ if ($this->isKw('UPDATE')) return $this->update();
350+ if ($this->isKw('DELETE')) return $this->delete();
351+ if ($this->acceptKw('CREATE')) return $this->create();
352+ if ($this->acceptKw('DROP')) {
353+ $kind = $this->acceptKw('TABLE', 'INDEX', 'VIEW', 'TRIGGER') ?? $this->error('TABLE');
354+ $if = $this->ifExists();
355+ return ['t' => 'drop', 'kind' => strtolower($kind), 'name' => $this->qualifiedName(), 'ifExists' => $if];
356+ }
357+ if ($this->acceptKw('ALTER')) return $this->alter();
358+ if ($this->acceptKw('BEGIN')) {
359+ $mode = $this->acceptKw('DEFERRED', 'IMMEDIATE', 'EXCLUSIVE') ?? 'DEFERRED';
360+ if ($this->acceptKw('TRANSACTION') && $this->isName()) $this->name();
361+ return ['t' => 'begin', 'mode' => $mode];
362+ }
363+ if ($this->acceptKw('COMMIT', 'END')) {
364+ $this->acceptKw('TRANSACTION');
365+ return ['t' => 'commit'];
366+ }
367+ if ($this->acceptKw('ROLLBACK')) {
368+ $this->acceptKw('TRANSACTION');
369+ if ($this->acceptKw('TO')) {
370+ $this->acceptKw('SAVEPOINT');
371+ return ['t' => 'rollbackTo', 'name' => $this->name()];
372+ }
373+ return ['t' => 'rollback'];
374+ }
375+ if ($this->acceptKw('SAVEPOINT')) return ['t' => 'savepoint', 'name' => $this->name()];
376+ if ($this->acceptKw('RELEASE')) {
377+ $this->acceptKw('SAVEPOINT');
378+ return ['t' => 'release', 'name' => $this->name()];
379+ }
380+ if ($this->acceptKw('PRAGMA')) return $this->pragma();
381+ if ($this->acceptKw('VACUUM', 'ANALYZE', 'REINDEX')) {
382+ while (!$this->isOp(';') && $this->cur()['t'] !== 'eof') $this->p++;
383+ return ['t' => 'noop'];
384+ }
385+ if ($this->isKw('ATTACH', 'DETACH')) throw new DbException('ATTACH・DETACHは未対応です', 1, 'HY000');
386+ $this->error();
387+ }
388+
389+ private function pragma(): array {
390+ $name = strtolower($this->qualifiedName());
391+ $arg = null;
392+ if ($this->acceptOp('=')) {
393+ $arg = $this->pragmaValue();
394+ } else if ($this->acceptOp('(')) {
395+ $arg = $this->pragmaValue();
396+ $this->expectOp(')');
397+ }
398+ return ['t' => 'pragma', 'name' => $name, 'arg' => $arg];
399+ }
400+
401+ private function pragmaValue(): mixed {
402+ $t = $this->cur();
403+ $neg = $this->acceptOp('-');
404+ $t = $this->cur();
405+ $this->p++;
406+ if ($t['t'] === 'num') return $neg ? -$t['v'] : $t['v'];
407+ if ($t['t'] === 'str' || $t['t'] === 'id') return $t['v'];
408+ $this->p--;
409+ $this->error('値');
410+ }
411+
412+ /** WITH名前(列) AS (SELECT…), … */
413+ private function with(): array {
414+ $this->expectKw('WITH');
415+ if ($this->acceptKw('RECURSIVE')) $recursive = true;
416+ $out = [];
417+ do {
418+ $name = $this->name();
419+ $cols = null;
420+ if ($this->acceptOp('(')) {
421+ $cols = [];
422+ do $cols[] = $this->name(); while ($this->acceptOp(','));
423+ $this->expectOp(')');
424+ }
425+ $this->expectKw('AS');
426+ if ($this->acceptKw('NOT')) $this->expectKw('MATERIALIZED');
427+ else $this->acceptKw('MATERIALIZED');
428+ $this->expectOp('(');
429+ $sel = $this->select();
430+ $this->expectOp(')');
431+ $out[strtolower($name)] = ['sel' => $sel, 'cols' => $cols, 'name' => $name, 'recursive' => !empty($recursive)];
432+ } while ($this->acceptOp(','));
433+ return $out;
434+ }
435+
436+ /**
437+ * SELECT(複合も)
438+ *
439+ * @return array t、with、cores、ops、order、limit、offset
440+ */
441+ private function select(bool $allowWith = true): array {
442+ $with = $allowWith && $this->isKw('WITH') ? $this->with() : [];
443+ $cores = [$this->selectCore()];
444+ $ops = [];
445+ while ($op = $this->acceptKw('UNION', 'INTERSECT', 'EXCEPT')) {
446+ if ($op === 'UNION' && $this->acceptKw('ALL')) $op = 'UNION ALL';
447+ $ops[] = $op;
448+ $cores[] = $this->selectCore();
449+ }
450+ $order = [];
451+ if ($this->acceptKw('ORDER')) {
452+ $this->expectKw('BY');
453+ $order = $this->orderList();
454+ }
455+ $limit = $offset = null;
456+ if ($this->acceptKw('LIMIT')) {
457+ $limit = $this->expr();
458+ if ($this->acceptKw('OFFSET')) {
459+ $offset = $this->expr();
460+ } else if ($this->acceptOp(',')) {
461+ $offset = $limit;
462+ $limit = $this->expr();
463+ }
464+ }
465+ return ['t' => 'select', 'with' => $with, 'cores' => $cores, 'ops' => $ops, 'order' => $order, 'limit' => $limit, 'offset' => $offset];
466+ }
467+
468+ private function orderList(): array {
469+ $out = [];
470+ do {
471+ $e = $this->expr();
472+ $desc = $this->acceptKw('DESC') !== null;
473+ if (!$desc) $this->acceptKw('ASC');
474+ $nulls = null;
475+ if ($this->acceptKw('NULLS')) $nulls = $this->acceptKw('FIRST', 'LAST') ?? $this->error('FIRST');
476+ $out[] = ['e' => $e, 'desc' => $desc, 'nulls' => $nulls];
477+ } while ($this->acceptOp(','));
478+ return $out;
479+ }
480+
481+ private function selectCore(): array {
482+ if ($this->acceptOp('(')) {
483+ // 括弧の中のSELECT(複合の一部)
484+ $s = $this->select();
485+ $this->expectOp(')');
486+ return ['sub' => $s];
487+ }
488+ if ($this->acceptKw('VALUES')) {
489+ $rows = [];
490+ do {
491+ $this->expectOp('(');
492+ $row = [];
493+ do $row[] = $this->expr(); while ($this->acceptOp(','));
494+ $this->expectOp(')');
495+ $rows[] = $row;
496+ } while ($this->acceptOp(','));
497+ return ['values' => $rows];
498+ }
499+ $this->expectKw('SELECT');
500+ $distinct = $this->acceptKw('DISTINCT') !== null;
501+ if (!$distinct) $this->acceptKw('ALL');
502+ $cols = [];
503+ do {
504+ if ($this->acceptOp('*')) {
505+ $cols[] = ['star' => null];
506+ continue;
507+ }
508+ if ($this->cur()['t'] === 'id' && $this->peek()['t'] === 'op' && $this->peek()['v'] === '.'
509+ && $this->peek(2)['t'] === 'op' && $this->peek(2)['v'] === '*') {
510+ $tbl = $this->cur()['v'];
511+ $this->p += 3;
512+ $cols[] = ['star' => $tbl];
513+ continue;
514+ }
515+ $s = $this->cur()['s'];
516+ $e = $this->expr();
517+ $text = substr($this->sql, $s, $this->tok[$this->p - 1]['e'] - $s);
518+ $as = null;
519+ if ($this->acceptKw('AS')) $as = $this->name(true);
520+ else if ($this->isName()) $as = $this->name();
521+ $cols[] = ['e' => $e, 'as' => $as, 'text' => $text];
522+ } while ($this->acceptOp(','));
523+ $from = $this->acceptKw('FROM') ? $this->from() : [];
524+ $where = $this->acceptKw('WHERE') ? $this->expr() : null;
525+ $group = [];
526+ $having = null;
527+ if ($this->acceptKw('GROUP')) {
528+ $this->expectKw('BY');
529+ do $group[] = $this->expr(); while ($this->acceptOp(','));
530+ }
531+ if ($this->acceptKw('HAVING')) $having = $this->expr();
532+ if ($this->isKw('WINDOW')) throw new DbException('ウィンドウ関数は未対応です', 1, 'HY000');
533+ return ['distinct' => $distinct, 'cols' => $cols, 'from' => $from, 'where' => $where, 'group' => $group, 'having' => $having];
534+ }
535+
536+ /** FROM:テーブル・サブクエリと結合の配列 */
537+ private function from(): array {
538+ $out = [$this->fromItem('inner')];
539+ while (true) {
540+ if ($this->acceptOp(',')) {
541+ $out[] = $this->fromItem('inner');
542+ continue;
543+ }
544+ $natural = $this->acceptKw('NATURAL') !== null;
545+ $kind = null;
546+ if ($this->acceptKw('LEFT')) {
547+ $this->acceptKw('OUTER');
548+ $kind = 'left';
549+ } else if ($this->acceptKw('INNER')) {
550+ $kind = 'inner';
551+ } else if ($this->acceptKw('CROSS')) {
552+ $kind = 'inner';
553+ } else if ($this->isKw('RIGHT', 'FULL')) {
554+ throw new DbException('RIGHT JOIN・FULL JOINは未対応です', 1, 'HY000');
555+ }
556+ if ($kind === null && !$this->isKw('JOIN')) {
557+ if ($natural) $this->error('JOIN');
558+ break;
559+ }
560+ $this->expectKw('JOIN');
561+ $item = $this->fromItem($kind ?? 'inner');
562+ $item['natural'] = $natural;
563+ if ($this->acceptKw('ON')) {
564+ $item['on'] = $this->expr();
565+ } else if ($this->acceptKw('USING')) {
566+ $this->expectOp('(');
567+ $item['using'] = [];
568+ do $item['using'][] = $this->name(); while ($this->acceptOp(','));
569+ $this->expectOp(')');
570+ }
571+ $out[] = $item;
572+ }
573+ return $out;
574+ }
575+
576+ private function fromItem(string $join): array {
577+ $item = ['join' => $join, 'on' => null, 'using' => null, 'natural' => false];
578+ if ($this->acceptOp('(')) {
579+ if ($this->isKw('SELECT', 'VALUES', 'WITH')) {
580+ $item['sel'] = $this->select();
581+ $this->expectOp(')');
582+ } else {
583+ // (a JOIN b):サブクエリにする
584+ $from = $this->from();
585+ $this->expectOp(')');
586+ $item['sel'] = ['t' => 'select', 'with' => [], 'cores' => [['distinct' => false, 'cols' => [['star' => null]], 'from' => $from,
587+ 'where' => null, 'group' => [], 'having' => null]], 'ops' => [], 'order' => [], 'limit' => null, 'offset' => null];
588+ }
589+ } else {
590+ $name = $this->qualifiedName();
591+ if ($this->acceptOp('(')) throw new DbException("テーブル値関数は未対応です:{$name}", 1, 'HY000');
592+ $item['table'] = $name;
593+ }
594+ $item['as'] = null;
595+ if ($this->acceptKw('AS')) $item['as'] = $this->name();
596+ else if ($this->isName() && !$this->isKw('INDEXED')) $item['as'] = $this->name();
597+ if ($this->acceptKw('INDEXED')) {
598+ $this->expectKw('BY');
599+ $this->name();
600+ } else if ($this->isKw('NOT') && $this->peek()['t'] === 'id' && $this->peek()['u'] === 'INDEXED') {
601+ $this->p += 2;
602+ }
603+ return $item;
604+ }
605+
606+ private function conflictClause(): ?string {
607+ if ($this->acceptKw('OR')) return $this->acceptKw('ROLLBACK', 'ABORT', 'REPLACE', 'FAIL', 'IGNORE') ?? $this->error('REPLACE');
608+ return null;
609+ }
610+
611+ private function returning(): ?array {
612+ if (!$this->acceptKw('RETURNING')) return null;
613+ $cols = [];
614+ do {
615+ if ($this->acceptOp('*')) {
616+ $cols[] = ['star' => null];
617+ continue;
618+ }
619+ $s = $this->cur()['s'];
620+ $e = $this->expr();
621+ $text = substr($this->sql, $s, $this->tok[$this->p - 1]['e'] - $s);
622+ $as = null;
623+ if ($this->acceptKw('AS')) $as = $this->name(true);
624+ else if ($this->isName()) $as = $this->name();
625+ $cols[] = ['e' => $e, 'as' => $as, 'text' => $text];
626+ } while ($this->acceptOp(','));
627+ return $cols;
628+ }
629+
630+ private function insert(): array {
631+ $or = null;
632+ if ($this->acceptKw('REPLACE')) {
633+ $or = 'REPLACE';
634+ } else {
635+ $this->expectKw('INSERT');
636+ $or = $this->conflictClause();
637+ }
638+ $this->expectKw('INTO');
639+ $table = $this->qualifiedName();
640+ $alias = null;
641+ if ($this->acceptKw('AS')) $alias = $this->name();
642+ $cols = null;
643+ if ($this->acceptOp('(')) {
644+ $cols = [];
645+ do $cols[] = $this->name(); while ($this->acceptOp(','));
646+ $this->expectOp(')');
647+ }
648+ $sel = null;
649+ $default = false;
650+ if ($this->acceptKw('DEFAULT')) {
651+ $this->expectKw('VALUES');
652+ $default = true;
653+ } else {
654+ $sel = $this->select();
655+ }
656+ $upsert = [];
657+ while ($this->isKw('ON') && $this->peek()['t'] === 'id' && $this->peek()['u'] === 'CONFLICT') {
658+ $this->p += 2;
659+ $target = null;
660+ $twhere = null;
661+ if ($this->acceptOp('(')) {
662+ $target = [];
663+ do {
664+ $target[] = $this->name();
665+ if ($this->acceptKw('COLLATE')) $this->name();
666+ $this->acceptKw('ASC', 'DESC');
667+ } while ($this->acceptOp(','));
668+ $this->expectOp(')');
669+ if ($this->acceptKw('WHERE')) $twhere = $this->expr();
670+ }
671+ $this->expectKw('DO');
672+ if ($this->acceptKw('NOTHING')) {
673+ $upsert[] = ['target' => $target, 'nothing' => true];
674+ continue;
675+ }
676+ $this->expectKw('UPDATE');
677+ $this->expectKw('SET');
678+ $set = $this->setList();
679+ $where = $this->acceptKw('WHERE') ? $this->expr() : null;
680+ $upsert[] = ['target' => $target, 'nothing' => false, 'set' => $set, 'where' => $where];
681+ }
682+ return ['t' => 'insert', 'or' => $or, 'table' => $table, 'alias' => $alias, 'cols' => $cols, 'sel' => $sel,
683+ 'default' => $default, 'upsert' => $upsert, 'returning' => $this->returning(), 'with' => []];
684+ }
685+
686+ private function setList(): array {
687+ $set = [];
688+ do {
689+ if ($this->acceptOp('(')) {
690+ $names = [];
691+ do $names[] = $this->name(); while ($this->acceptOp(','));
692+ $this->expectOp(')');
693+ $this->expectOp('=');
694+ $this->expectOp('(');
695+ $vals = [];
696+ do $vals[] = $this->expr(); while ($this->acceptOp(','));
697+ $this->expectOp(')');
698+ if (count($names) !== count($vals)) throw new DbException('SETの列と値の数が違います', 1, 'HY000');
699+ foreach ($names as $i => $n) $set[] = [$n, $vals[$i]];
700+ continue;
701+ }
702+ $n = $this->name();
703+ $this->expectOp('=');
704+ $set[] = [$n, $this->expr()];
705+ } while ($this->acceptOp(','));
706+ return $set;
707+ }
708+
709+ private function update(): array {
710+ $this->expectKw('UPDATE');
711+ $or = $this->conflictClause();
712+ $table = $this->qualifiedName();
713+ $alias = null;
714+ if ($this->acceptKw('AS')) $alias = $this->name();
715+ else if ($this->isName() && !$this->isKw('SET', 'INDEXED')) $alias = $this->name();
716+ if ($this->acceptKw('INDEXED')) {
717+ $this->expectKw('BY');
718+ $this->name();
719+ }
720+ $this->expectKw('SET');
721+ $set = $this->setList();
722+ $from = $this->acceptKw('FROM') ? $this->from() : [];
723+ $where = $this->acceptKw('WHERE') ? $this->expr() : null;
724+ return ['t' => 'update', 'or' => $or, 'table' => $table, 'alias' => $alias, 'set' => $set, 'from' => $from, 'where' => $where,
725+ 'returning' => $this->returning(), 'with' => []];
726+ }
727+
728+ private function delete(): array {
729+ $this->expectKw('DELETE');
730+ $this->expectKw('FROM');
731+ $table = $this->qualifiedName();
732+ $alias = null;
733+ if ($this->acceptKw('AS')) $alias = $this->name();
734+ else if ($this->isName() && !$this->isKw('INDEXED')) $alias = $this->name();
735+ if ($this->acceptKw('INDEXED')) {
736+ $this->expectKw('BY');
737+ $this->name();
738+ }
739+ $where = $this->acceptKw('WHERE') ? $this->expr() : null;
740+ return ['t' => 'delete', 'table' => $table, 'alias' => $alias, 'where' => $where, 'returning' => $this->returning(), 'with' => []];
741+ }
742+
743+ ///////////////
744+ // CREATE・ALTER
745+
746+ private function create(): array {
747+ $temp = $this->acceptKw('TEMP', 'TEMPORARY') !== null;
748+ $unique = $this->acceptKw('UNIQUE') !== null;
749+ if ($this->acceptKw('TABLE')) {
750+ if ($temp) throw new DbException('一時テーブルは未対応です', 1, 'HY000');
751+ return $this->createTable();
752+ }
753+ if ($this->acceptKw('INDEX')) return $this->createIndex($unique);
754+ if ($this->acceptKw('VIEW')) {
755+ $if = $this->ifNotExists();
756+ $name = $this->qualifiedName();
757+ $nameTok = ['s' => $this->lastNameStart];
758+ $cols = null;
759+ if ($this->acceptOp('(')) {
760+ $cols = [];
761+ do $cols[] = $this->name(); while ($this->acceptOp(','));
762+ $this->expectOp(')');
763+ }
764+ $this->expectKw('AS');
765+ $sel = $this->select();
766+ return ['t' => 'createView', 'name' => $name, 'ifNotExists' => $if, 'cols' => $cols, 'sel' => $sel, 'nameStart' => $nameTok['s']];
767+ }
768+ if ($this->acceptKw('TRIGGER')) throw new DbException('トリガーは未対応です', 1, 'HY000');
769+ if ($this->acceptKw('VIRTUAL')) throw new DbException('仮想テーブルは未対応です', 1, 'HY000');
770+ $this->error('TABLE');
771+ }
772+
773+ private function createTable(): array {
774+ $if = $this->ifNotExists();
775+ $name = $this->qualifiedName();
776+ $nameTok = ['s' => $this->lastNameStart];
777+ if ($this->acceptKw('AS')) {
778+ return ['t' => 'createTable', 'name' => $name, 'ifNotExists' => $if, 'as' => $this->select(), 'nameStart' => $nameTok['s']];
779+ }
780+ $def = $this->tableBody();
781+ return ['t' => 'createTable', 'name' => $name, 'ifNotExists' => $if, 'def' => $def, 'nameStart' => $nameTok['s']];
782+ }
783+
784+ /**
785+ * テーブルの定義(括弧から最後まで)
786+ *
787+ * @return array cols(name、type、notNull、default、pk、autoinc、unique、collate、check、pkDesc)、pk、uniques、checks、withoutRowid、strict
788+ */
789+ public function tableBody(): array {
790+ $this->expectOp('(');
791+ $cols = [];
792+ $pk = null;
793+ $uniques = [];
794+ $checks = [];
795+ $pkAuto = false;
796+ // 自動のインデックスの順番(sqlite_autoindex_テーブル_番号)
797+ $order = [];
798+ while (true) {
799+ if ($this->isKw('CONSTRAINT', 'PRIMARY', 'UNIQUE', 'CHECK', 'FOREIGN')) {
800+ if ($this->acceptKw('CONSTRAINT')) $this->name();
801+ if ($this->acceptKw('PRIMARY')) {
802+ $this->expectKw('KEY');
803+ $list = $this->indexedColumns();
804+ if ($this->acceptKw('AUTOINCREMENT')) $pkAuto = true;
805+ $this->onConflict();
806+ if ($pk !== null) throw new DbException("テーブルに主キーが複数有ります", 1, 'HY000');
807+ $pk = $list;
808+ $order[] = ['pk', $list];
809+ } else if ($this->acceptKw('UNIQUE')) {
810+ $list = $this->indexedColumns();
811+ $uniques[] = $list;
812+ $order[] = ['unique', $list];
813+ $this->onConflict();
814+ } else if ($this->acceptKw('CHECK')) {
815+ $s = $this->cur()['s'];
816+ $this->expectOp('(');
817+ $checks[] = $this->expr();
818+ $this->expectOp(')');
819+ } else if ($this->acceptKw('FOREIGN')) {
820+ $this->expectKw('KEY');
821+ $this->expectOp('(');
822+ do $this->name(); while ($this->acceptOp(','));
823+ $this->expectOp(')');
824+ $this->references();
825+ } else {
826+ $this->error();
827+ }
828+ } else {
829+ $c = $this->columnDef();
830+ foreach ($c['cons'] as $k) $order[] = [$k, [['name' => $c['name'], 'desc' => $k === 'pk' && $c['pkDesc'], 'collate' => null]]];
831+ $cols[] = $c;
832+ }
833+ if ($this->acceptOp(',')) continue;
834+ $this->expectOp(')');
835+ break;
836+ }
837+ $withoutRowid = false;
838+ $strict = false;
839+ while (true) {
840+ if ($this->acceptKw('WITHOUT')) {
841+ if (!$this->isKw('ROWID')) $this->error('ROWID');
842+ $this->p++;
843+ $withoutRowid = true;
844+ } else if ($this->acceptKw('STRICT')) {
845+ $strict = true;
846+ } else {
847+ break;
848+ }
849+ if (!$this->acceptOp(',')) break;
850+ }
851+ foreach ($cols as $c) {
852+ if ($c['pk']) {
853+ if ($pk !== null) throw new DbException("テーブルに主キーが複数有ります", 1, 'HY000');
854+ $pk = [['name' => $c['name'], 'desc' => $c['pkDesc'], 'collate' => null]];
855+ $pkAuto = $c['autoinc'];
856+ }
857+ }
858+ return ['cols' => $cols, 'pk' => $pk, 'pkAuto' => $pkAuto, 'uniques' => $uniques, 'checks' => $checks,
859+ 'withoutRowid' => $withoutRowid, 'strict' => $strict, 'order' => $order];
860+ }
861+
862+ private function onConflict(): void {
863+ if ($this->isKw('ON') && $this->peek()['t'] === 'id' && $this->peek()['u'] === 'CONFLICT') {
864+ $this->p += 2;
865+ $this->acceptKw('ROLLBACK', 'ABORT', 'FAIL', 'IGNORE', 'REPLACE') ?? $this->error('ABORT');
866+ }
867+ }
868+
869+ private function references(): void {
870+ $this->expectKw('REFERENCES');
871+ $this->name();
872+ if ($this->acceptOp('(')) {
873+ do $this->name(); while ($this->acceptOp(','));
874+ $this->expectOp(')');
875+ }
876+ while (true) {
877+ if ($this->acceptKw('ON')) {
878+ $this->acceptKw('DELETE', 'UPDATE') ?? $this->error('DELETE');
879+ if ($this->acceptKw('SET')) $this->acceptKw('NULL', 'DEFAULT') ?? $this->error('NULL');
880+ else if ($this->acceptKw('NO')) $this->expectKw('ACTION');
881+ else $this->acceptKw('CASCADE', 'RESTRICT') ?? $this->error('CASCADE');
882+ } else if ($this->acceptKw('MATCH')) {
883+ $this->name();
884+ } else if ($this->isKw('NOT') && $this->peek()['u'] === 'DEFERRABLE' || $this->isKw('DEFERRABLE')) {
885+ $this->acceptKw('NOT');
886+ $this->expectKw('DEFERRABLE');
887+ if ($this->acceptKw('INITIALLY')) $this->acceptKw('DEFERRED', 'IMMEDIATE');
888+ } else {
889+ break;
890+ }
891+ }
892+ }
893+
894+ /** (列 [COLLATE x] [ASC|DESC], …) */
895+ private function indexedColumns(): array {
896+ $this->expectOp('(');
897+ $out = [];
898+ do {
899+ $t = $this->cur();
900+ $e = $this->expr();
901+ $coll = null;
902+ if ($e['t'] === 'collate') {
903+ $coll = strtoupper($e['coll']);
904+ $e = $e['e'];
905+ }
906+ if ($e['t'] !== 'col' || $e['tbl'] !== null) throw new DbException('式のインデックスは未対応です', 1, 'HY000');
907+ $desc = $this->acceptKw('DESC') !== null;
908+ if (!$desc) $this->acceptKw('ASC');
909+ $out[] = ['name' => $e['name'], 'desc' => $desc, 'collate' => $coll];
910+ } while ($this->acceptOp(','));
911+ $this->expectOp(')');
912+ return $out;
913+ }
914+
915+ private function columnDef(): array {
916+ $c = ['name' => $this->name(), 'type' => '', 'notNull' => false, 'default' => null, 'pk' => false, 'pkDesc' => false,
917+ 'autoinc' => false, 'unique' => false, 'collate' => null, 'checks' => [], 'defaultText' => null, 'cons' => []];
918+ // 型の名前
919+ $words = [];
920+ while ($this->cur()['t'] === 'id' && !$this->cur()['q'] && !$this->isKw('CONSTRAINT', 'PRIMARY', 'NOT', 'NULL', 'UNIQUE',
921+ 'CHECK', 'DEFAULT', 'COLLATE', 'REFERENCES', 'GENERATED', 'AS')) {
922+ $words[] = $this->cur()['v'];
923+ $this->p++;
924+ }
925+ if (empty($words) && $this->cur()['t'] === 'id' && $this->cur()['q']) {
926+ $words[] = $this->cur()['v'];
927+ $this->p++;
928+ }
929+ $type = implode(' ', $words);
930+ if ($this->acceptOp('(')) {
931+ $args = [];
932+ do {
933+ $neg = $this->acceptOp('-') ? '-' : ($this->acceptOp('+') ? '' : '');
934+ $t = $this->cur();
935+ if ($t['t'] !== 'num') $this->error('数');
936+ $this->p++;
937+ $args[] = $neg.substr($this->sql, $t['s'], $t['e'] - $t['s']);
938+ } while ($this->acceptOp(','));
939+ $this->expectOp(')');
940+ $type .= '('.implode(',', $args).')';
941+ }
942+ $c['type'] = $type;
943+ while (true) {
944+ if ($this->acceptKw('CONSTRAINT')) $this->name();
945+ if ($this->acceptKw('PRIMARY')) {
946+ $this->expectKw('KEY');
947+ $c['pk'] = true;
948+ $c['cons'][] = 'pk';
949+ $c['pkDesc'] = $this->acceptKw('DESC') !== null;
950+ if (!$c['pkDesc']) $this->acceptKw('ASC');
951+ $this->onConflict();
952+ if ($this->acceptKw('AUTOINCREMENT')) $c['autoinc'] = true;
953+ } else if ($this->acceptKw('NOT')) {
954+ $this->expectKw('NULL');
955+ $c['notNull'] = true;
956+ $this->onConflict();
957+ } else if ($this->acceptKw('NULL')) {
958+ $this->onConflict();
959+ } else if ($this->acceptKw('UNIQUE')) {
960+ $c['unique'] = true;
961+ $c['cons'][] = 'unique';
962+ $this->onConflict();
963+ } else if ($this->acceptKw('CHECK')) {
964+ $this->expectOp('(');
965+ $c['checks'][] = $this->expr();
966+ $this->expectOp(')');
967+ } else if ($this->acceptKw('DEFAULT')) {
968+ $ds = $this->cur()['s'];
969+ if ($this->acceptOp('(')) {
970+ $c['default'] = $this->expr();
971+ $this->expectOp(')');
972+ } else {
973+ $neg = $this->acceptOp('-');
974+ if (!$neg) $this->acceptOp('+');
975+ $t = $this->cur();
976+ if ($t['t'] === 'id' && !$t['q'] && in_array($t['u'], ['CURRENT_TIMESTAMP', 'CURRENT_DATE', 'CURRENT_TIME', 'TRUE', 'FALSE', 'NULL'], true)) {
977+ $c['default'] = $this->primary();
978+ } else if ($t['t'] === 'id') {
979+ // 識別子は文字列として
980+ $this->p++;
981+ $c['default'] = ['t' => 'lit', 'v' => $t['v']];
982+ } else {
983+ $c['default'] = $this->primary();
984+ }
985+ if ($neg) $c['default'] = ['t' => 'un', 'op' => '-', 'e' => $c['default']];
986+ }
987+ $c['defaultText'] = substr($this->sql, $ds, $this->tok[$this->p - 1]['e'] - $ds);
988+ } else if ($this->acceptKw('COLLATE')) {
989+ $c['collate'] = strtoupper($this->name());
990+ } else if ($this->isKw('REFERENCES')) {
991+ $this->references();
992+ } else if ($this->isKw('GENERATED', 'AS')) {
993+ throw new DbException('生成列は未対応です', 1, 'HY000');
994+ } else {
995+ break;
996+ }
997+ }
998+ return $c;
999+ }
1000+
1001+ private function createIndex(bool $unique): array {
1002+ $if = $this->ifNotExists();
1003+ $name = $this->qualifiedName();
1004+ $nameTok = ['s' => $this->lastNameStart];
1005+ $this->expectKw('ON');
1006+ $table = $this->name();
1007+ $cols = $this->indexedColumns();
1008+ if ($this->acceptKw('WHERE')) throw new DbException('部分インデックスは未対応です', 1, 'HY000');
1009+ return ['t' => 'createIndex', 'name' => $name, 'table' => $table, 'unique' => $unique, 'cols' => $cols, 'ifNotExists' => $if,
1010+ 'nameStart' => $nameTok['s']];
1011+ }
1012+
1013+ private function alter(): array {
1014+ $this->expectKw('TABLE');
1015+ $table = $this->qualifiedName();
1016+ if ($this->acceptKw('RENAME')) {
1017+ if ($this->acceptKw('TO')) return ['t' => 'alter', 'op' => 'rename', 'table' => $table, 'to' => $this->name()];
1018+ $this->acceptKw('COLUMN');
1019+ $from = $this->name();
1020+ $this->expectKw('TO');
1021+ return ['t' => 'alter', 'op' => 'renameColumn', 'table' => $table, 'from' => $from, 'to' => $this->name()];
1022+ }
1023+ if ($this->acceptKw('ADD')) {
1024+ $this->acceptKw('COLUMN');
1025+ $s = $this->cur()['s'];
1026+ $col = $this->columnDef();
1027+ $text = substr($this->sql, $s, $this->tok[$this->p - 1]['e'] - $s);
1028+ return ['t' => 'alter', 'op' => 'addColumn', 'table' => $table, 'col' => $col, 'text' => $text];
1029+ }
1030+ if ($this->acceptKw('DROP')) {
1031+ $this->acceptKw('COLUMN');
1032+ return ['t' => 'alter', 'op' => 'dropColumn', 'table' => $table, 'col' => $this->name()];
1033+ }
1034+ $this->error('RENAME');
1035+ }
1036+
1037+ ///////////////
1038+ // 式
1039+
1040+ public function expr(): array {
1041+ $l = $this->andExpr();
1042+ while ($this->acceptKw('OR')) $l = ['t' => 'bin', 'op' => 'OR', 'l' => $l, 'r' => $this->andExpr()];
1043+ return $l;
1044+ }
1045+
1046+ private function andExpr(): array {
1047+ $l = $this->notExpr();
1048+ while ($this->acceptKw('AND')) $l = ['t' => 'bin', 'op' => 'AND', 'l' => $l, 'r' => $this->notExpr()];
1049+ return $l;
1050+ }
1051+
1052+ private function notExpr(): array {
1053+ if ($this->acceptKw('NOT')) return ['t' => 'un', 'op' => 'NOT', 'e' => $this->notExpr()];
1054+ return $this->equality();
1055+ }
1056+
1057+ private function equality(): array {
1058+ $l = $this->comparison();
1059+ while (true) {
1060+ if ($this->acceptOp('=') || $this->acceptOp('==')) {
1061+ $l = ['t' => 'bin', 'op' => '=', 'l' => $l, 'r' => $this->comparison()];
1062+ } else if ($this->acceptOp('!=') || $this->acceptOp('<>')) {
1063+ $l = ['t' => 'bin', 'op' => '!=', 'l' => $l, 'r' => $this->comparison()];
1064+ } else if ($this->acceptKw('IS')) {
1065+ $not = $this->acceptKw('NOT') !== null;
1066+ if ($this->acceptKw('DISTINCT')) {
1067+ $this->expectKw('FROM');
1068+ $not = !$not;
1069+ }
1070+ $l = ['t' => 'is', 'not' => $not, 'l' => $l, 'r' => $this->comparison()];
1071+ } else if ($this->acceptKw('ISNULL')) {
1072+ $l = ['t' => 'isnull', 'not' => false, 'e' => $l];
1073+ } else if ($this->acceptKw('NOTNULL')) {
1074+ $l = ['t' => 'isnull', 'not' => true, 'e' => $l];
1075+ } else if ($this->isKw('NOT') && $this->peek()['t'] === 'id' && in_array($this->peek()['u'], ['NULL', 'IN', 'LIKE', 'GLOB', 'BETWEEN', 'REGEXP', 'MATCH'], true)) {
1076+ $this->p++;
1077+ if ($this->acceptKw('NULL')) {
1078+ $l = ['t' => 'isnull', 'not' => true, 'e' => $l];
1079+ } else {
1080+ $l = $this->postfixOp($l, true);
1081+ }
1082+ } else if ($this->isKw('IN', 'LIKE', 'GLOB', 'BETWEEN', 'REGEXP', 'MATCH')) {
1083+ $l = $this->postfixOp($l, false);
1084+ } else {
1085+ return $l;
1086+ }
1087+ }
1088+ }
1089+
1090+ private function postfixOp(array $l, bool $not): array {
1091+ if ($this->acceptKw('IN')) {
1092+ if ($this->acceptOp('(')) {
1093+ if ($this->isKw('SELECT', 'VALUES', 'WITH')) {
1094+ $sel = $this->select();
1095+ $this->expectOp(')');
1096+ return ['t' => 'in', 'not' => $not, 'e' => $l, 'list' => null, 'sel' => $sel];
1097+ }
1098+ $list = [];
1099+ if (!$this->isOp(')')) {
1100+ do $list[] = $this->expr(); while ($this->acceptOp(','));
1101+ }
1102+ $this->expectOp(')');
1103+ return ['t' => 'in', 'not' => $not, 'e' => $l, 'list' => $list, 'sel' => null];
1104+ }
1105+ $name = $this->qualifiedName();
1106+ return ['t' => 'in', 'not' => $not, 'e' => $l, 'list' => null, 'sel' => null, 'table' => $name];
1107+ }
1108+ if ($this->acceptKw('BETWEEN')) {
1109+ $lo = $this->comparison();
1110+ $this->expectKw('AND');
1111+ $hi = $this->comparison();
1112+ return ['t' => 'between', 'not' => $not, 'e' => $l, 'lo' => $lo, 'hi' => $hi];
1113+ }
1114+ $op = $this->acceptKw('LIKE', 'GLOB', 'REGEXP', 'MATCH');
1115+ if ($op === 'REGEXP' || $op === 'MATCH') throw new DbException("{$op}は未対応です", 1, 'HY000');
1116+ $r = $this->comparison();
1117+ $esc = $this->acceptKw('ESCAPE') ? $this->comparison() : null;
1118+ return ['t' => 'like', 'op' => $op, 'not' => $not, 'l' => $l, 'r' => $r, 'esc' => $esc];
1119+ }
1120+
1121+ private function comparison(): array {
1122+ $l = $this->bitwise();
1123+ while (true) {
1124+ foreach (['<=', '>=', '<', '>'] as $op) {
1125+ if ($this->acceptOp($op)) {
1126+ $l = ['t' => 'bin', 'op' => $op, 'l' => $l, 'r' => $this->bitwise()];
1127+ continue 2;
1128+ }
1129+ }
1130+ return $l;
1131+ }
1132+ }
1133+
1134+ private function bitwise(): array {
1135+ $l = $this->additive();
1136+ while (true) {
1137+ foreach (['<<', '>>', '&', '|'] as $op) {
1138+ if ($this->acceptOp($op)) {
1139+ $l = ['t' => 'bin', 'op' => $op, 'l' => $l, 'r' => $this->additive()];
1140+ continue 2;
1141+ }
1142+ }
1143+ return $l;
1144+ }
1145+ }
1146+
1147+ private function additive(): array {
1148+ $l = $this->multiplicative();
1149+ while (true) {
1150+ if ($this->acceptOp('+')) $l = ['t' => 'bin', 'op' => '+', 'l' => $l, 'r' => $this->multiplicative()];
1151+ else if ($this->acceptOp('-')) $l = ['t' => 'bin', 'op' => '-', 'l' => $l, 'r' => $this->multiplicative()];
1152+ else return $l;
1153+ }
1154+ }
1155+
1156+ private function multiplicative(): array {
1157+ $l = $this->concat();
1158+ while (true) {
1159+ foreach (['*', '/', '%'] as $op) {
1160+ if ($this->acceptOp($op)) {
1161+ $l = ['t' => 'bin', 'op' => $op, 'l' => $l, 'r' => $this->concat()];
1162+ continue 2;
1163+ }
1164+ }
1165+ return $l;
1166+ }
1167+ }
1168+
1169+ private function concat(): array {
1170+ $l = $this->unary();
1171+ while (true) {
1172+ if ($this->acceptOp('||')) $l = ['t' => 'bin', 'op' => '||', 'l' => $l, 'r' => $this->unary()];
1173+ else if ($this->acceptOp('->>')) $l = ['t' => 'fn', 'name' => 'json_extract_text', 'args' => [$l, $this->unary()], 'distinct' => false, 'star' => false];
1174+ else if ($this->acceptOp('->')) $l = ['t' => 'fn', 'name' => 'json_extract_json', 'args' => [$l, $this->unary()], 'distinct' => false, 'star' => false];
1175+ else return $l;
1176+ }
1177+ }
1178+
1179+ private function unary(): array {
1180+ if ($this->acceptOp('-')) {
1181+ $e = $this->unary();
1182+ if ($e['t'] === 'lit' && (is_int($e['v']) || is_float($e['v']))) {
1183+ // -9223372036854775808
1184+ if (is_float($e['v']) && $e['v'] === 9.2233720368547758e18) return ['t' => 'lit', 'v' => PHP_INT_MIN];
1185+ return ['t' => 'lit', 'v' => -$e['v']];
1186+ }
1187+ return ['t' => 'un', 'op' => '-', 'e' => $e];
1188+ }
1189+ if ($this->acceptOp('+')) return ['t' => 'un', 'op' => '+', 'e' => $this->unary()];
1190+ if ($this->acceptOp('~')) return ['t' => 'un', 'op' => '~', 'e' => $this->unary()];
1191+ $e = $this->primary();
1192+ while ($this->acceptKw('COLLATE')) $e = ['t' => 'collate', 'e' => $e, 'coll' => strtoupper($this->name())];
1193+ return $e;
1194+ }
1195+
1196+ private function primary(): array {
1197+ $t = $this->cur();
1198+ switch ($t['t']) {
1199+ case 'num':
1200+ $this->p++;
1201+ return ['t' => 'lit', 'v' => $t['v']];
1202+ case 'str':
1203+ $this->p++;
1204+ return ['t' => 'lit', 'v' => $t['v']];
1205+ case 'blob':
1206+ $this->p++;
1207+ return ['t' => 'lit', 'v' => new Blob($t['v'])];
1208+ case 'param':
1209+ $this->p++;
1210+ if ($t['v'] === null) {
1211+ $i = ++$this->nParam;
1212+ } else if (is_int($t['v'])) {
1213+ $i = $t['v'];
1214+ $this->nParam = max($this->nParam, $i);
1215+ } else {
1216+ // 名前付き:最初に出た順番で番号を付ける(SQLiteと同じ)
1217+ $i = $this->names[$t['v']] ??= ++$this->nParam;
1218+ }
1219+ return ['t' => 'param', 'i' => $i];
1220+ case 'op':
1221+ if ($t['v'] === '(') {
1222+ $this->p++;
1223+ if ($this->isKw('SELECT', 'VALUES', 'WITH')) {
1224+ $sel = $this->select();
1225+ $this->expectOp(')');
1226+ return ['t' => 'sub', 'sel' => $sel];
1227+ }
1228+ $e = $this->expr();
1229+ if ($this->isOp(',')) {
1230+ $list = [$e];
1231+ while ($this->acceptOp(',')) $list[] = $this->expr();
1232+ $this->expectOp(')');
1233+ return ['t' => 'vector', 'list' => $list];
1234+ }
1235+ $this->expectOp(')');
1236+ return $e;
1237+ }
1238+ $this->error();
1239+ case 'id':
1240+ if (!$t['q']) {
1241+ switch ($t['u']) {
1242+ case 'NULL':
1243+ $this->p++;
1244+ return ['t' => 'lit', 'v' => null];
1245+ case 'TRUE':
1246+ $this->p++;
1247+ return ['t' => 'lit', 'v' => 1];
1248+ case 'FALSE':
1249+ $this->p++;
1250+ return ['t' => 'lit', 'v' => 0];
1251+ case 'CURRENT_TIMESTAMP':
1252+ case 'CURRENT_DATE':
1253+ case 'CURRENT_TIME':
1254+ $this->p++;
1255+ return ['t' => 'fn', 'name' => strtolower($t['u']), 'args' => [], 'distinct' => false, 'star' => false];
1256+ case 'CASE':
1257+ return $this->caseExpr();
1258+ case 'CAST':
1259+ $this->p++;
1260+ $this->expectOp('(');
1261+ $e = $this->expr();
1262+ $this->expectKw('AS');
1263+ $words = [];
1264+ while ($this->cur()['t'] === 'id') {
1265+ $words[] = $this->cur()['v'];
1266+ $this->p++;
1267+ }
1268+ if ($this->acceptOp('(')) {
1269+ while (!$this->acceptOp(')')) $this->p++;
1270+ }
1271+ $this->expectOp(')');
1272+ return ['t' => 'cast', 'e' => $e, 'type' => implode(' ', $words)];
1273+ case 'EXISTS':
1274+ $this->p++;
1275+ $this->expectOp('(');
1276+ $sel = $this->select();
1277+ $this->expectOp(')');
1278+ return ['t' => 'exists', 'sel' => $sel];
1279+ case 'NOT':
1280+ $this->error();
1281+ case 'RAISE':
1282+ throw new DbException('RAISEは未対応です', 1, 'HY000');
1283+ }
1284+ }
1285+ $this->p++;
1286+ // 関数
1287+ if ($this->isOp('(') && !$t['q']) {
1288+ $this->p++;
1289+ $name = strtolower($t['v']);
1290+ $distinct = false;
1291+ $star = false;
1292+ $args = [];
1293+ if ($this->acceptOp('*')) {
1294+ $star = true;
1295+ } else if (!$this->isOp(')')) {
1296+ $distinct = $this->acceptKw('DISTINCT') !== null;
1297+ do $args[] = $this->expr(); while ($this->acceptOp(','));
1298+ if ($this->acceptKw('ORDER')) throw new DbException('集約関数のORDER BYは未対応です', 1, 'HY000');
1299+ }
1300+ $this->expectOp(')');
1301+ $filter = null;
1302+ if ($this->acceptKw('FILTER')) {
1303+ $this->expectOp('(');
1304+ $this->expectKw('WHERE');
1305+ $filter = $this->expr();
1306+ $this->expectOp(')');
1307+ }
1308+ if ($this->isKw('OVER')) throw new DbException('ウィンドウ関数は未対応です', 1, 'HY000');
1309+ return ['t' => 'fn', 'name' => $name, 'args' => $args, 'distinct' => $distinct, 'star' => $star, 'filter' => $filter];
1310+ }
1311+ // 列(table.column)
1312+ if ($this->isOp('.')) {
1313+ $this->p++;
1314+ $c = $this->cur();
1315+ if ($c['t'] !== 'id') $this->error('列');
1316+ $this->p++;
1317+ if ($this->isOp('.')) {
1318+ // schema.table.column
1319+ $this->p++;
1320+ $c2 = $this->cur();
1321+ if ($c2['t'] !== 'id') $this->error('列');
1322+ $this->p++;
1323+ return ['t' => 'col', 'tbl' => $c['v'], 'name' => $c2['v']];
1324+ }
1325+ return ['t' => 'col', 'tbl' => $t['v'], 'name' => $c['v']];
1326+ }
1327+ if (!$t['q'] && in_array($t['u'], self::RESERVED, true)) {
1328+ $this->p--;
1329+ $this->error();
1330+ }
1331+ // 「"文字列"」は列が無ければ文字列(SQLiteの互換)
1332+ return ['t' => 'col', 'tbl' => null, 'name' => $t['v'], 'dq' => $t['q'] && ($this->sql[$t['s']] ?? '') === '"'];
1333+ }
1334+ $this->error();
1335+ }
1336+
1337+ private function caseExpr(): array {
1338+ $this->expectKw('CASE');
1339+ $base = $this->isKw('WHEN') ? null : $this->expr();
1340+ $when = [];
1341+ while ($this->acceptKw('WHEN')) {
1342+ $c = $this->expr();
1343+ $this->expectKw('THEN');
1344+ $when[] = [$c, $this->expr()];
1345+ }
1346+ if (empty($when)) $this->error('WHEN');
1347+ $else = $this->acceptKw('ELSE') ? $this->expr() : null;
1348+ $this->expectKw('END');
1349+ return ['t' => 'case', 'base' => $base, 'when' => $when, 'else' => $else];
1350+ }
1351+}
@@ -0,0 +1,308 @@
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\Lib\Db\Sqlite;
39+
40+/**
41+ * SQLiteのレコードの形式(可変長整数、シリアル型)と値の比較
42+ *
43+ * 値の表し方:null、int、float、string(TEXT)、Blob(BLOB)。
44+ */
45+final class Record {
46+ /**
47+ * 可変長整数を読む(ビッグエンディアン、1バイト7ビット、9バイト目は8ビット)
48+ *
49+ * @param string $b データ
50+ * @param int $pos 位置(読んだ分だけ進む)
51+ * @return int 64ビットの符号付き整数
52+ */
53+ public static function getVarint(string $b, int &$pos): int {
54+ $c = ord($b[$pos++] ?? "\0");
55+ if ($c < 0x80) return $c;
56+ $v = $c & 0x7F;
57+ for ($i = 1; $i < 8; $i++) {
58+ $c = ord($b[$pos++] ?? "\0");
59+ $v = ($v << 7) | ($c & 0x7F);
60+ if ($c < 0x80) return $v;
61+ }
62+ return ($v << 8) | ord($b[$pos++] ?? "\0");
63+ }
64+
65+ /**
66+ * 可変長整数を書く
67+ *
68+ * @param int $v 値(負の数は64ビットの符号なしとして書く)
69+ * @return string
70+ */
71+ public static function putVarint(int $v): string {
72+ if ($v >= 0 && $v < 0x80) return chr($v);
73+ if ($v < 0 || $v > 0x00FFFFFFFFFFFFFF) {
74+ // 9バイト:最後のバイトは8ビット
75+ $out = chr($v & 0xFF);
76+ $v = ($v >> 8) & 0x00FFFFFFFFFFFFFF;
77+ for ($i = 0; $i < 8; $i++) {
78+ $out = chr(($v & 0x7F) | 0x80).$out;
79+ $v >>= 7;
80+ }
81+ return $out;
82+ }
83+ $out = chr($v & 0x7F);
84+ $v >>= 7;
85+ while ($v > 0) {
86+ $out = chr(($v & 0x7F) | 0x80).$out;
87+ $v >>= 7;
88+ }
89+ return $out;
90+ }
91+
92+ /** 可変長整数のバイト数 */
93+ public static function varintLen(int $v): int {
94+ if ($v < 0) return 9;
95+ for ($n = 1; $n < 9; $n++) {
96+ if ($v < (1 << (7 * $n))) return $n;
97+ }
98+ return 9;
99+ }
100+
101+ /**
102+ * 値の配列をレコードにする
103+ *
104+ * @param array $values
105+ * @return string
106+ */
107+ public static function encode(array $values): string {
108+ $head = '';
109+ $body = '';
110+ foreach ($values as $v) {
111+ if ($v === null) {
112+ $head .= "\0";
113+ } else if (is_int($v)) {
114+ if ($v === 0) {
115+ $head .= "\x08";
116+ } else if ($v === 1) {
117+ $head .= "\x09";
118+ } else if ($v >= -128 && $v <= 127) {
119+ $head .= "\x01";
120+ $body .= chr($v & 0xFF);
121+ } else if ($v >= -32768 && $v <= 32767) {
122+ $head .= "\x02";
123+ $body .= pack('n', $v & 0xFFFF);
124+ } else if ($v >= -8388608 && $v <= 8388607) {
125+ $head .= "\x03";
126+ $body .= substr(pack('N', $v & 0xFFFFFFFF), 1);
127+ } else if ($v >= -2147483648 && $v <= 2147483647) {
128+ $head .= "\x04";
129+ $body .= pack('N', $v & 0xFFFFFFFF);
130+ } else if ($v >= -140737488355328 && $v <= 140737488355327) {
131+ $head .= "\x05";
132+ $body .= substr(pack('J', $v), 2);
133+ } else {
134+ $head .= "\x06";
135+ $body .= pack('J', $v);
136+ }
137+ } else if (is_float($v)) {
138+ $head .= "\x07";
139+ $body .= pack('E', $v);
140+ } else if ($v instanceof Blob) {
141+ $head .= self::putVarint(strlen($v->data) * 2 + 12);
142+ $body .= $v->data;
143+ } else {
144+ $v = (string)$v;
145+ $head .= self::putVarint(strlen($v) * 2 + 13);
146+ $body .= $v;
147+ }
148+ }
149+ // ヘッダーの長さ(自分自身を含む)
150+ $n = strlen($head) + 1;
151+ if ($n >= 0x80) $n = strlen($head) + self::varintLen(strlen($head) + 2);
152+ return self::putVarint($n).$head.$body;
153+ }
154+
155+ /**
156+ * レコードを値の配列にする
157+ *
158+ * @param string $rec
159+ * @return array
160+ */
161+ public static function decode(string $rec): array {
162+ $pos = 0;
163+ $hlen = self::getVarint($rec, $pos);
164+ $types = [];
165+ while ($pos < $hlen) $types[] = self::getVarint($rec, $pos);
166+ $pos = $hlen;
167+ $out = [];
168+ foreach ($types as $t) {
169+ switch ($t) {
170+ case 0:
171+ $out[] = null;
172+ break;
173+ case 1:
174+ $v = ord($rec[$pos] ?? "\0");
175+ $out[] = $v >= 0x80 ? $v - 0x100 : $v;
176+ $pos += 1;
177+ break;
178+ case 2:
179+ $v = unpack('n', $rec, $pos)[1];
180+ $out[] = $v >= 0x8000 ? $v - 0x10000 : $v;
181+ $pos += 2;
182+ break;
183+ case 3:
184+ $v = unpack('N', "\0".substr($rec, $pos, 3))[1];
185+ $out[] = $v >= 0x800000 ? $v - 0x1000000 : $v;
186+ $pos += 3;
187+ break;
188+ case 4:
189+ $v = unpack('N', $rec, $pos)[1];
190+ $out[] = $v >= 0x80000000 ? $v - 0x100000000 : $v;
191+ $pos += 4;
192+ break;
193+ case 5:
194+ $v = unpack('J', "\0\0".substr($rec, $pos, 6))[1];
195+ $out[] = $v >= 0x800000000000 ? $v - 0x1000000000000 : $v;
196+ $pos += 6;
197+ break;
198+ case 6:
199+ $out[] = unpack('J', $rec, $pos)[1];
200+ $pos += 8;
201+ break;
202+ case 7:
203+ $out[] = unpack('E', $rec, $pos)[1];
204+ $pos += 8;
205+ break;
206+ case 8:
207+ $out[] = 0;
208+ break;
209+ case 9:
210+ $out[] = 1;
211+ break;
212+ default:
213+ if ($t < 12) throw new \UnexpectedValueException("SQLite:不正なシリアル型:{$t}");
214+ $len = ($t - 12) >> 1;
215+ $s = (string)substr($rec, $pos, $len);
216+ $out[] = ($t & 1) ? $s : new Blob($s);
217+ $pos += $len;
218+ }
219+ }
220+ return $out;
221+ }
222+
223+ /**
224+ * 値の種類の順番(NULL<数値<TEXT<BLOB)
225+ */
226+ public static function typeClass(mixed $v): int {
227+ if ($v === null) return 0;
228+ if (is_int($v) || is_float($v)) return 1;
229+ if ($v instanceof Blob) return 3;
230+ return 2;
231+ }
232+
233+ /**
234+ * 2つの値を比べる(SQLiteの順番)
235+ *
236+ * @param mixed $a
237+ * @param mixed $b
238+ * @param string $coll 照合順序:BINARY、NOCASE、RTRIM
239+ * @return int -1、0、1
240+ */
241+ public static function compare(mixed $a, mixed $b, string $coll = 'BINARY'): int {
242+ $ta = self::typeClass($a);
243+ $tb = self::typeClass($b);
244+ if ($ta !== $tb) return $ta <=> $tb;
245+ switch ($ta) {
246+ case 0:
247+ return 0;
248+ case 1:
249+ if (is_int($a) && is_int($b)) return $a <=> $b;
250+ // 大きい整数と浮動小数点数
251+ if (is_int($a) && is_float($b) && is_finite($b) && abs($b) < 9.2e18) {
252+ $fb = (int)floor($b);
253+ return $a === $fb ? ($b > $fb ? -1 : 0) : ($a <=> $fb);
254+ }
255+ if (is_float($a) && is_int($b) && is_finite($a) && abs($a) < 9.2e18) {
256+ $fa = (int)floor($a);
257+ return $fa === $b ? ($a > $fa ? 1 : 0) : ($fa <=> $b);
258+ }
259+ if (is_nan($a) || is_nan($b)) return is_nan($a) <=> is_nan($b);
260+ return $a <=> $b;
261+ case 2:
262+ return self::compareText((string)$a, (string)$b, $coll);
263+ default:
264+ $c = strcmp($a->data, $b->data);
265+ return $c < 0 ? -1 : ($c > 0 ? 1 : 0);
266+ }
267+ }
268+
269+ public static function compareText(string $a, string $b, string $coll): int {
270+ if ($coll === 'NOCASE') {
271+ $a = strtolower($a);
272+ $b = strtolower($b);
273+ } else if ($coll === 'RTRIM') {
274+ $a = rtrim($a, ' ');
275+ $b = rtrim($b, ' ');
276+ }
277+ $c = strcmp($a, $b);
278+ return $c < 0 ? -1 : ($c > 0 ? 1 : 0);
279+ }
280+
281+ /**
282+ * インデックスのキーを比べる
283+ *
284+ * @param array $a キー(列の値…、rowid)
285+ * @param array $b 比べるキー(短ければ、その分だけ比べる)
286+ * @param array $colls 列毎の照合順序
287+ * @param array $desc 列毎の降順
288+ * @return int
289+ */
290+ public static function compareKeys(array $a, array $b, array $colls, array $desc): int {
291+ $n = min(count($a), count($b));
292+ for ($i = 0; $i < $n; $i++) {
293+ $x = $a[$i];
294+ $y = $b[$i];
295+ // よく有る場合を先に
296+ if (is_int($x) && is_int($y)) {
297+ $c = $x <=> $y;
298+ } else if (is_string($x) && is_string($y) && ($colls[$i] ?? 'BINARY') === 'BINARY') {
299+ $c = strcmp($x, $y);
300+ $c = $c < 0 ? -1 : ($c > 0 ? 1 : 0);
301+ } else {
302+ $c = self::compare($x, $y, $colls[$i] ?? 'BINARY');
303+ }
304+ if ($c !== 0) return !empty($desc[$i]) ? -$c : $c;
305+ }
306+ return 0;
307+ }
308+}
@@ -0,0 +1,213 @@
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\Lib\Db\Sqlite;
39+
40+use Std\Lib\Db\DbException;
41+
42+/**
43+ * データベースのスキーマ(sqlite_schemaのテーブル・インデックス・ビュー)
44+ */
45+final class Schema {
46+ /** 小文字の名前 => テーブル */
47+ public array $tables = [];
48+ /** 小文字の名前 => インデックス */
49+ public array $indexes = [];
50+ /** 小文字の名前 => ビュー(name、sel、cols) */
51+ public array $views = [];
52+ /** トリガーの有るテーブル(小文字) */
53+ public array $triggers = [];
54+ public int $maxRowid = 0;
55+
56+ /**
57+ * sqlite_schemaを読む
58+ */
59+ public static function load(Btree $bt, int $pages): self {
60+ $s = new self();
61+ $s->tables['sqlite_schema'] = self::schemaTable();
62+ if ($pages === 0) return $s;
63+ $parser = new Parser();
64+ $autos = [];
65+ foreach ($bt->scan(1) as $rowid => $rec) {
66+ $s->maxRowid = max($s->maxRowid, $rowid);
67+ [$type, $name, $tbl, $root, $sql] = Record::decode($rec) + [null, null, null, null, null];
68+ $type = (string)$type;
69+ $lname = strtolower((string)$name);
70+ try {
71+ if ($type === 'table') {
72+ $st = $parser->parseAll((string)$sql)[0] ?? null;
73+ if (($st['t'] ?? '') !== 'createTable' || !isset($st['def'])) throw new DbException("不正なテーブルのSQL:{$name}", 11, 'HY000');
74+ $s->tables[$lname] = self::table((string)$name, (int)$root, (string)$sql, $st['def']) + ['schemaRowid' => $rowid];
75+ } else if ($type === 'index') {
76+ if ($sql === null) {
77+ $autos[] = [(string)$name, strtolower((string)$tbl), (int)$root, $rowid];
78+ continue;
79+ }
80+ $st = $parser->parseAll((string)$sql)[0] ?? null;
81+ if (($st['t'] ?? '') !== 'createIndex') throw new DbException("不正なインデックスのSQL:{$name}", 11, 'HY000');
82+ $s->indexes[$lname] = ['name' => (string)$name, 'root' => (int)$root, 'table' => strtolower((string)$tbl), 'unique' => $st['unique'],
83+ 'def' => $st['cols'], 'origin' => 'c', 'sql' => (string)$sql, 'schemaRowid' => $rowid];
84+ } else if ($type === 'view') {
85+ $st = $parser->parseAll((string)$sql)[0] ?? null;
86+ if (($st['t'] ?? '') !== 'createView') throw new DbException("不正なビューのSQL:{$name}", 11, 'HY000');
87+ $s->views[$lname] = ['name' => (string)$name, 'sel' => $st['sel'], 'cols' => $st['cols'], 'sql' => (string)$sql, 'schemaRowid' => $rowid];
88+ } else if ($type === 'trigger') {
89+ $s->triggers[strtolower((string)$tbl)] = true;
90+ $s->triggers['@'.$lname] = $rowid;
91+ }
92+ } catch (DbException $e) {
93+ if ($type === 'table' && str_contains($e->getMessage(), '未対応')) {
94+ // WITHOUT ROWID等:読めないテーブルとして
95+ $s->tables[$lname] = ['name' => (string)$name, 'unsupported' => $e->getMessage(), 'root' => (int)$root, 'cols' => [], 'map' => [],
96+ 'indexes' => [], 'schemaRowid' => $rowid, 'sql' => (string)$sql];
97+ continue;
98+ }
99+ throw $e;
100+ }
101+ }
102+
103+ // 自動のインデックス(UNIQUE・PRIMARY KEYの制約)
104+ foreach ($autos as [$name, $tbl, $root, $rowid]) {
105+ $t = $s->tables[$tbl] ?? null;
106+ if (!$t || !preg_match('/_(\d+)$/', $name, $m)) continue;
107+ $c = $t['constraints'][(int)$m[1] - 1] ?? null;
108+ if ($c === null) throw new DbException("データベースが壊れています({$name})", 11, 'HY000');
109+ $s->indexes[strtolower($name)] = ['name' => $name, 'root' => $root, 'table' => $tbl, 'unique' => true, 'def' => $c[1],
110+ 'origin' => $c[0] === 'pk' ? 'pk' : 'u', 'sql' => null, 'schemaRowid' => $rowid];
111+ }
112+ foreach ($s->indexes as $k => $idx) {
113+ if (!isset($s->tables[$idx['table']])) continue;
114+ $s->indexes[$k] = self::indexInfo($idx, $s->tables[$idx['table']]);
115+ $s->tables[$idx['table']]['indexes'][] = $k;
116+ }
117+ return $s;
118+ }
119+
120+ /** sqlite_schemaのテーブル */
121+ private static function schemaTable(): array {
122+ $cols = [];
123+ foreach (['type' => 'text', 'name' => 'text', 'tbl_name' => 'text', 'rootpage' => 'int', 'sql' => 'text'] as $n => $ty) {
124+ $cols[] = ['name' => $n, 'type' => $ty, 'aff' => Value::affinity($ty), 'notNull' => false, 'default' => null, 'defaultText' => null,
125+ 'collate' => 'BINARY', 'pk' => 0];
126+ }
127+ return ['name' => 'sqlite_schema', 'root' => 1, 'sql' => '', 'cols' => $cols, 'map' => ['type' => 0, 'name' => 1, 'tbl_name' => 2, 'rootpage' => 3, 'sql' => 4],
128+ 'ipk' => null, 'autoinc' => false, 'checks' => [], 'indexes' => [], 'constraints' => [], 'system' => true];
129+ }
130+
131+ /**
132+ * テーブルの定義を作る
133+ *
134+ * @param array $def Parser::tableBody()の結果
135+ */
136+ public static function table(string $name, int $root, string $sql, array $def): array {
137+ if ($def['withoutRowid']) throw new DbException("WITHOUT ROWIDのテーブルは未対応です:{$name}", 1, 'HY000');
138+ $cols = [];
139+ $map = [];
140+ foreach ($def['cols'] as $i => $c) {
141+ $lc = strtolower($c['name']);
142+ if (isset($map[$lc])) throw new DbException("重複した列:{$c['name']}", 1, 'HY000');
143+ $map[$lc] = $i;
144+ $cols[] = ['name' => $c['name'], 'type' => $c['type'], 'aff' => Value::affinity($c['type']), 'notNull' => $c['notNull'],
145+ 'default' => $c['default'], 'defaultText' => $c['defaultText'], 'collate' => $c['collate'] ?? 'BINARY', 'pk' => 0,
146+ 'checks' => $c['checks']];
147+ }
148+ $ipk = null;
149+ $pkCols = [];
150+ if ($def['pk'] !== null) {
151+ foreach ($def['pk'] as $k => $p) {
152+ $ci = $map[strtolower($p['name'])] ?? null;
153+ if ($ci === null) throw new DbException("主キーの列がありません:{$p['name']}", 1, 'HY000');
154+ $cols[$ci]['pk'] = $k + 1;
155+ $pkCols[] = $ci;
156+ }
157+ if (count($pkCols) === 1 && strtoupper($cols[$pkCols[0]]['type']) === 'INTEGER') {
158+ // 列の「INTEGER PRIMARY KEY DESC」だけはrowidの別名にならない
159+ $colDesc = false;
160+ foreach ($def['cols'] as $c) if ($c['pk'] && $c['pkDesc']) $colDesc = true;
161+ if (!$colDesc) $ipk = $pkCols[0];
162+ }
163+ }
164+ if ($def['pkAuto'] && $ipk === null) throw new DbException('AUTOINCREMENTはINTEGER PRIMARY KEYだけに使えます', 1, 'HY000');
165+
166+ // 自動のインデックスの元になる制約(重複は1つ)
167+ $constraints = [];
168+ $seen = [];
169+ foreach ($def['order'] as [$kind, $list]) {
170+ if ($kind === 'pk' && $ipk !== null) continue;
171+ $key = implode(',', array_map(fn($p) => strtolower($p['name']), $list));
172+ if (isset($seen[$key])) continue;
173+ $seen[$key] = true;
174+ $constraints[] = [$kind, $list];
175+ }
176+ $checks = $def['checks'];
177+ foreach ($cols as $c) foreach ($c['checks'] as $ch) $checks[] = $ch;
178+ return ['name' => $name, 'root' => $root, 'sql' => $sql, 'cols' => $cols, 'map' => $map, 'ipk' => $ipk, 'autoinc' => $def['pkAuto'],
179+ 'checks' => $checks, 'indexes' => [], 'constraints' => $constraints];
180+ }
181+
182+ /** インデックスの列・照合順序・降順 */
183+ public static function indexInfo(array $idx, array $t): array {
184+ $cols = $colls = $desc = [];
185+ foreach ($idx['def'] as $d) {
186+ $ci = $t['map'][strtolower($d['name'])] ?? null;
187+ if ($ci === null) {
188+ if (in_array(strtolower($d['name']), ['rowid', 'oid', '_rowid_'], true)) throw new DbException('rowidのインデックスは未対応です', 1, 'HY000');
189+ throw new DbException("インデックスの列がありません:{$d['name']}", 1, 'HY000');
190+ }
191+ $cols[] = $ci;
192+ $colls[] = $d['collate'] ?? $t['cols'][$ci]['collate'];
193+ $desc[] = $d['desc'];
194+ }
195+ $colls[] = 'BINARY';
196+ $desc[] = false;
197+ return $idx + ['cols' => $cols, 'colls' => $colls, 'desc' => $desc];
198+ }
199+
200+ /** テーブル(無ければnull) */
201+ public function table_(string $name): ?array {
202+ $l = strtolower($name);
203+ if ($l === 'sqlite_master' || $l === 'sqlite_temp_master' || $l === 'sqlite_temp_schema') $l = 'sqlite_schema';
204+ return $this->tables[$l] ?? null;
205+ }
206+
207+ /** 名前が使われているか */
208+ public function exists(string $name): bool {
209+ $l = strtolower($name);
210+ return isset($this->tables[$l]) || isset($this->indexes[$l]) || isset($this->views[$l]) || isset($this->triggers['@'.$l])
211+ || $l === 'sqlite_master';
212+ }
213+}
@@ -0,0 +1,240 @@
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\Lib\Db\Sqlite;
39+
40+/**
41+ * SQLiteの値の変換(型の親和性、数値・文字列にする、真偽)
42+ */
43+final class Value {
44+ public const INTEGER = 'INTEGER';
45+ public const REAL = 'REAL';
46+ public const NUMERIC = 'NUMERIC';
47+ public const TEXT = 'TEXT';
48+ public const BLOB = 'BLOB';
49+
50+ /**
51+ * 宣言された型の親和性(SQLiteの規則)
52+ *
53+ * @param string $type 例:VARCHAR(255)、BIGINT
54+ * @return string
55+ */
56+ public static function affinity(string $type): string {
57+ $t = strtoupper($type);
58+ if (str_contains($t, 'INT')) return self::INTEGER;
59+ if (str_contains($t, 'CHAR') || str_contains($t, 'CLOB') || str_contains($t, 'TEXT')) return self::TEXT;
60+ if ($t === '' || str_contains($t, 'BLOB')) return self::BLOB;
61+ if (str_contains($t, 'REAL') || str_contains($t, 'FLOA') || str_contains($t, 'DOUB')) return self::REAL;
62+ return self::NUMERIC;
63+ }
64+
65+ /** CASTの型の親和性(空ならBLOB) */
66+ public static function castAffinity(string $type): string {
67+ return trim($type) === '' ? self::BLOB : self::affinity($type);
68+ }
69+
70+ /**
71+ * 列に入れる時の親和性を適用する
72+ */
73+ public static function apply(mixed $v, string $aff): mixed {
74+ if ($v === null || $v instanceof Blob) return $v;
75+ switch ($aff) {
76+ case self::TEXT:
77+ return is_string($v) ? $v : self::toText($v);
78+ case self::INTEGER:
79+ case self::NUMERIC:
80+ if (is_string($v)) {
81+ $n = self::parseNumber($v, true);
82+ if ($n === null) return $v;
83+ $v = $n;
84+ }
85+ if (is_float($v) && is_finite($v) && floor($v) === $v && abs($v) < 9.2233720368547e18) return (int)$v;
86+ return $v;
87+ case self::REAL:
88+ if (is_string($v)) {
89+ $n = self::parseNumber($v, true);
90+ return $n === null ? $v : (float)$n;
91+ }
92+ return (float)$v;
93+ }
94+ return $v;
95+ }
96+
97+ /**
98+ * 文字列全部が数値なら数値(前後の空白は可)
99+ *
100+ * @param bool $whole trueなら全部が数値の場合だけ、falseなら先頭の数値(無ければ0)
101+ * @return int|float|null
102+ */
103+ public static function parseNumber(string $s, bool $whole): int|float|null {
104+ if (preg_match('/^\s*([+-]?)(\d+)(\.\d*)?([eE][+-]?\d+)?\s*$/', $s, $m)
105+ || preg_match('/^\s*([+-]?)()(\.\d+)([eE][+-]?\d+)?\s*$/', $s, $m)) {
106+ if (($m[3] ?? '') === '' && ($m[4] ?? '') === '') {
107+ $digits = ltrim($m[2], '0');
108+ if ($digits === '') return 0;
109+ if (strlen($digits) < 19 || (strlen($digits) === 19 && strcmp($digits, $m[1] === '-' ? '9223372036854775808' : '9223372036854775807') <= 0)) {
110+ if ($m[1] === '-' && $digits === '9223372036854775808') return PHP_INT_MIN;
111+ return (int)($m[1].$digits);
112+ }
113+ }
114+ return (float)trim($s);
115+ }
116+ if ($whole) {
117+ // 16進数・無限
118+ if (preg_match('/^\s*[+-]?(inf|infinity)\s*$/i', $s)) return null;
119+ return null;
120+ }
121+ if (preg_match('/^\s*[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?/', $s, $m)) {
122+ return self::parseNumber($m[0], true) ?? 0;
123+ }
124+ return 0;
125+ }
126+
127+ /** 計算用の数値(NULLはnull、文字列は先頭の数値) */
128+ public static function toNumber(mixed $v): int|float|null {
129+ if ($v === null || is_int($v) || is_float($v)) return $v;
130+ if ($v instanceof Blob) $v = $v->data;
131+ return self::parseNumber((string)$v, false);
132+ }
133+
134+ /** 整数(CAST AS INTEGER) */
135+ public static function toInt(mixed $v): ?int {
136+ $n = self::toNumber($v);
137+ if ($n === null) return null;
138+ if (is_float($n)) {
139+ if (is_nan($n)) return 0;
140+ if ($n >= 9.2233720368547758e18) return PHP_INT_MAX;
141+ if ($n <= -9.2233720368547758e18) return PHP_INT_MIN;
142+ return (int)$n;
143+ }
144+ return $n;
145+ }
146+
147+ /** 文字列にする(数値はSQLiteと同じ形) */
148+ public static function toText(mixed $v): ?string {
149+ if ($v === null) return null;
150+ if (is_int($v)) return (string)$v;
151+ if (is_float($v)) return self::realToText($v);
152+ if ($v instanceof Blob) return $v->data;
153+ return (string)$v;
154+ }
155+
156+ /** REALの文字列(%!.15gと同じ) */
157+ public static function realToText(float $f): string {
158+ if (is_nan($f)) return 'NaN';
159+ if (is_infinite($f)) return $f > 0 ? 'Inf' : '-Inf';
160+ if ($f == 0.0) return '0.0';
161+ $e = sprintf('%.14e', $f);
162+ [$mant, $exp] = explode('e', $e);
163+ $exp = (int)$exp;
164+ if ($exp < -4 || $exp >= 15) {
165+ $mant = rtrim(rtrim($mant, '0'), '.');
166+ if (!str_contains($mant, '.')) $mant .= '.0';
167+ return $mant.'e'.($exp < 0 ? '-' : '+').str_pad((string)abs($exp), 2, '0', STR_PAD_LEFT);
168+ }
169+ $s = sprintf('%.'.max(0, 14 - $exp).'F', $f);
170+ if (str_contains($s, '.')) $s = rtrim(rtrim($s, '0'), '.');
171+ if (!str_contains($s, '.')) $s .= '.0';
172+ return $s;
173+ }
174+
175+ /**
176+ * 真偽(NULLはnull)
177+ */
178+ public static function truth(mixed $v): ?bool {
179+ if ($v === null) return null;
180+ if (is_int($v)) return $v !== 0;
181+ if (is_float($v)) return $v != 0.0;
182+ $n = self::toNumber($v);
183+ return $n != 0;
184+ }
185+
186+ /**
187+ * 比べる前の親和性(SQLiteの比較の規則)
188+ *
189+ * @param array $l [値, 親和性]
190+ * @param array $r [値, 親和性]
191+ * @return array [左の値, 右の値]
192+ */
193+ public static function prepareCompare(mixed $a, string $affA, mixed $b, string $affB): array {
194+ $numA = $affA === self::INTEGER || $affA === self::REAL || $affA === self::NUMERIC;
195+ $numB = $affB === self::INTEGER || $affB === self::REAL || $affB === self::NUMERIC;
196+ if ($numA && !$numB) {
197+ if (is_string($b)) $b = self::apply($b, self::NUMERIC);
198+ } else if ($numB && !$numA) {
199+ if (is_string($a)) $a = self::apply($a, self::NUMERIC);
200+ } else if ($affA === self::TEXT && $affB === '') {
201+ if (is_int($b) || is_float($b)) $b = self::toText($b);
202+ } else if ($affB === self::TEXT && $affA === '') {
203+ if (is_int($a) || is_float($a)) $a = self::toText($a);
204+ }
205+ return [$a, $b];
206+ }
207+
208+ /** SQLのリテラル(quote()) */
209+ public static function quote(mixed $v): string {
210+ if ($v === null) return 'NULL';
211+ if (is_int($v)) return (string)$v;
212+ if (is_float($v)) return self::realToText($v);
213+ if ($v instanceof Blob) return "X'".strtoupper(bin2hex($v->data))."'";
214+ return "'".str_replace("'", "''", (string)$v)."'";
215+ }
216+
217+ /** typeof() */
218+ public static function type(mixed $v): string {
219+ if ($v === null) return 'null';
220+ if (is_int($v)) return 'integer';
221+ if (is_float($v)) return 'real';
222+ if ($v instanceof Blob) return 'blob';
223+ return 'text';
224+ }
225+
226+ /** GROUP BY・DISTINCT用のキー */
227+ public static function key(mixed $v, string $coll = 'BINARY'): string {
228+ if ($v === null) return 'n';
229+ if (is_int($v)) return 'i'.$v;
230+ if (is_float($v)) {
231+ if (is_finite($v) && floor($v) === $v && abs($v) < 9.2e18) return 'i'.(int)$v;
232+ return 'f'.$v;
233+ }
234+ if ($v instanceof Blob) return 'b'.strlen($v->data).':'.$v->data;
235+ $s = (string)$v;
236+ if ($coll === 'NOCASE') $s = strtolower($s);
237+ else if ($coll === 'RTRIM') $s = rtrim($s, ' ');
238+ return 's'.strlen($s).':'.$s;
239+ }
240+}
@@ -0,0 +1,496 @@
1+{
2+ "real": "U1FMaXRlIGZvcm1hdCAzAAQAAQEAQCAgAAAAgAAAACkAAAAOAAAADQAAAAUAAAAEAAAAAAAAAAAAAAABAAAAKgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAC52iQ0DGwAHAMEAAMEDzQLJAnkB9AJQAZkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIFVAQcXFxcBgwl0YWJsZWl0ZW1zaXRlbXMCQ1JFQVRFIFRBQkxFIGl0ZW1zIChpZCBJTlRFR0VSIFBSSU1BUlkgS0VZIEFVVE9JTkNSRU1FTlQsIG5hbWUgVEVYVCBOT1QgTlVMTCBVTklRVUUgQ09MTEFURSBOT0NBU0UsIHByaWNlIFJFQUwsIHF0eSBJTlQgREVGQVVMVCAwLCBkYXRhIEJMT0IsIG5vdGUgVEVYVCwgZXh0cmEgVEVYVCBERUZBVUxUICfjg4fjg5Xjgqnjg6vjg4gnKVkHBxUXFwiBFXZpZXdjaGVhcGNoZWFwQ1JFQVRFIFZJRVcgY2hlYXAgQVMgU0VMRUNUIG5hbWUsIHByaWNlIEZST00gaXRlbXMgV0hFUkUgcHJpY2UgPCAxMDBaBQcXFRUBgRd0YWJsZXRhZ3N0YWdzBkNSRUFURSBUQUJMRSB0YWdzIChpdGVtX2lkIElOVCwgdGFnIFRFWFQsIFBSSU1BUlkgS0VZIChpdGVtX2lkLCB0YWcpKScGBhc7FQEAaW5kZXhzcWxpdGVfYXV0b2luZGV4X3RhZ3NfMXRhZ3MHTgQGFyMXAXFpbmRleGl0ZW1zX3ByaWNlaXRlbXMFQ1JFQVRFIElOREVYIGl0ZW1zX3ByaWNlIE9OIGl0ZW1zKHByaWNlIERFU0MsIHF0eSlQAwYXKysBWXRhYmxlc3FsaXRlX3NlcXVlbmNlc3FsaXRlX3NlcXVlbmNlBENSRUFURSBUQUJMRSBzcWxpdGVfc2VxdWVuY2UobmFtZSxzZXEpA\/gAsgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACkCBhc9FwEAaW5kZXhzcWxpdGVfYXV0b2luZGV4X2l0ZW1zXzFpdGVtcwMAAAAIAAAAAAUAAAAEA+wAAAAAHAP7A\/YD8QPsA+cAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA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\/A9UD5APzAOID8wPzA\/MD8wPzA\/MD8wPzA\/MD8wPzA\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AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAChmAgAAAAED8gAAAAAnA\/IAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAmCQQBEwEYYjI0MAAAAAnplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCAAAAAK6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAgAAAAC+mVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIOmVt+OBhOODoeODojEwIAAAAADplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCDplbfjgYTjg6Hjg6IxMCANAMAADABAAAPgA74DngNdAz0DFgLVAQgA2wCRAGEAQABAAEAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHw8HAB0HAAAd5ZOB54mpMTVAWzAAAAAAAOODoeODojE1Lg4HAB0HASgd5ZOB54mpMTRAWWAAAAAAAA4ODg4ODg4ODg4ODg4ODuODoeODojE0LQ0HAB0HASYd5ZOB54mpMTNAV5AAAAAAAA0NDQ0NDQ0NDQ0NDQ0N44Oh44OiMTMC9QAbAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAArCwcAHQcBIh3lk4HniakxMUBT8AAAAAAACwsLCwsLCwsLCwsL44Oh44OiMTGjNgoIAB0HACDGNeWTgeeJqTEwQFIgAAAAAAAKCgoKCgoKCgoK6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAg6ZW344GE44Oh44OiMTAgAAAACB4JBwAbBwEAG+WTgeeJqTlAUFAAAAAAAAnjg6Hjg6I5A4EAIQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJQcHABsHARob5ZOB54mpN0BJYAAAAAAABwcHBwcHBwfjg6Hjg6I3HgYHABsHAQAb5ZOB54mpNkBFwAAAAAAABuODoeODojYiBQcAGwcAFhvlk4Hniak1QEIgAAAAAAAFBQUFBeODoeODojUAAAAdAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4DBwAbBwEAG+WTgeeJqTNANcAAAAAAAAPjg6Hjg6IzIAIHABsHARAb5ZOB54mpMkAtAAAAAAAAAgIC44Oh44OiMh4BBwAbBwkOG+WTgeeJqTFAHQAAAAAAAAHjg6Hjg6IxDQDLAAkAMwADogOAA0sBWAEgAOcAkQBVADMAMwAzADMAAAAAAAAAAAAAAAAAAAAAAAAAIBsHAB0HAQAd5ZOB54mpMjdAaHgAAAAAABvjg6Hjg6IyNzoaBwAdBwFAHeWTgeeJqTI2QGeQAAAAAAAaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhrjg6Hjg6IyNjgZBwAdBwA+HeWTgeeJqTI1QGaoAAAAAAAZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZ44Oh44OiMjUBegAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANxcHAB0HATod5ZOB54mpMjNAZNgAAAAAABcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXF+ODoeODojIzNhYHAB0HATgd5ZOB54mpMjJAY\/AAAAAAABYWFhYWFhYWFhYWFhYWFhYWFhYWFhYW44Oh44OiMjIgFQcAHQcBAB3lk4HniakyMUBjCAAAAAAAFeODoeODojIxA9UB0QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMxMHAB0HATId5ZOB54mpMTlAYTgAAAAAABMTExMTExMTExMTExMTExMTExMT44Oh44OiMTkgEgcAHQcBAB3lk4HniakxOEBgUAAAAAAAEuODoeODojE4MREHAB0HAS4d5ZOB54mpMTdAXtAAAAAAABEREREREREREREREREREREREeODoeODojE3AAAAKwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAADwAAABAAAAARAAAAGAAAABkAAAAaAAAAGwAAACIAAAAjAAAAJAAAACUAAAAhAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANAIMABwA8AAOJAcYBhQEnAOMAnwA8ADwAPAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABFJQcAHQcBVh3lk4HniakzN0BwxAAAAAAAJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSXjg6Hjg6IzNwFJABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABCIwcAHQcAUh3lk4HniakzNUBvuAAAAAAAIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyPjg6Hjg6IzNUIiBwAdBwFQHeWTgeeJqTM0QG7QAAAAAAAiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIuODoeODojM0ICEHAB0HAQAd5ZOB54mpMzNAbegAAAAAACHjg6Hjg6IzMwPIADwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8fBwAdBwFKHeWTgeeJqTMxQGwYAAAAAAAfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fH+ODoeODojMxoyweCAAdBwAAxjXlk4HniakzMEBrMAAAAAAA6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAgAAAAEz0dBwAdBwFGHeWTgeeJqTI5QGpIAAAAAAAdHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR3jg6Hjg6IyOQAAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIAAAABXplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCDplbfjgYTjg6Hjg6IzMCAAAAAW6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAg6ZW344GE44Oh44OiMzAgAAAAAOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODo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AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADQAAAAkAIgACCwHpAZIBcAEYAPYAmQA7ACIAAAAAAAAAABc9CAAZBwgAAA\/mnIDlvoy\/+AAAAAAAAHhcOwgAHQcBgQId5ZOB54mpNTlAerwAAAAAADs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O+ODoeODojU5WzoIAB0HAYEAHeWTgeeJqTU4QHpIAAAAAAA6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6OuODoeODojU4IDkHAB0HAQAd5ZOB54mpNTdAedQAAAAAADnjg6Hjg6I1N1Y3BwAdBwB6HeWTgeeJqTU1QHjsAAAAAAA3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc344Oh44OiNTUgNgcAHQcBAB3lk4Hniak1NEB4eAAAAAAANuODoeODojU0VTUHAB0HAXYd5ZOB54mpNTNAeAQAAAAAADU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NeODoeODojUzIDMHAB0HAQAd5ZOB54mpNTFAdxwAAAAAADPjg6Hjg6I1MaNeMggAHQcAcMY15ZOB54mpNTBAdqgAAAAAADIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIy6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAgAAAAHQAAAB7plbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCAAAAAf6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAg6ZW344GE44Oh44OiNTAgAAAAIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIOmVt+OBhOODoeODojUwIAAAAADplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA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",
3+ "hot": "U1FMaXRlIGZvcm1hdCAzAAIAAQEAQCAgAAAAAgAAAAQAAAAAAAAAAAAAAAEAAAAEAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAC52iQ0AAAABAcEAAcEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA9AQYXDw8Ba3RhYmxldHQCQ1JFQVRFIFRBQkxFIHQgKGlkIElOVEVHRVIgUFJJTUFSWSBLRVksIHYgVEVYVCkFAAAAAQH7AAAAAAQB+wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADHA0AWAAUAEQAAEQAxACwAJwAiAB0AGAB7AHYAcQBsAGcAYgBdAFgAUwBOAEkARAA\/AC4AKgAmACIAHgAaABYAFgAAAAAEgEDACvjgZPjgo\/jgozjgZ\/lgKQA2AAIAAAAABIHAwAr44GT44KP44KM44Gf5YCkEgYDACvjgZPjgo\/jgozjgZ\/lgKQSBQMAK+OBk+OCj+OCjOOBn+WApBIEAwAr44GT44KP44KM44Gf5YCkEgMDACvjgZPjgo\/jgozjgZ\/lgKQSAgMAK+OBk+OCj+OCjOOBn+WApAAAACQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABIUAwAr44GT44KP44KM44Gf5YCkEhMDACvjgZPjgo\/jgozjgZ\/lgKQSEgMAK+OBk+OCj+OCjOOBn+WApBIRAwAr44GT44KP44KM44Gf5YCkEhADACvjgZPjgo\/jgozjgZ\/lgKQSDwMAK+OBk+OCj+OCjOOBn+WApBIOAwAr44GT44KP44KM44Gf5YCkEg0DACvjgZPjgo\/jgozjgZ\/lgKQSDAMAK+OBk+OCj+OCjOOBn+WApBILAwAr44GT44KP44KM44Gf5YCkEgoDACvjgZPjgo\/jgozjgZ\/lgKQSCQMAK+OBk+OCj+OCjOOBn+WApBIIAwAr44GT44KP44KM44Gf5YCkDQAAABcAOgAAwAE4ASQBEAD8AKwA6ADUAJgB7AHYAcQBsACEAZwBiAF0AWAAcAFMAF4ATAA6AAAAABArAwAn5paw44GX44GE5YCkMhAqAwAn5paw44GX44GE5YCkMRApAwAn5paw44GX44GE5YCkMBInAwAr44GT44KP44KM44Gf5YCkEiIDACvjgZPjgo\/jgozjgZ\/lgKQSHQMAK+OBk+OCj+OCjOOBn+WApBIaAwAr44GT44KP44KM44Gf5YCkEhUDACvjgZPjgo\/jgozjgZ\/lgKQSHAMAK+OBk+OCj+OCjOOBn+WApBIbAwAr44GT44KP44KM44Gf5YCkEhkDACvjgZPjgo\/jgozjgZ\/lgKQSGAMAK+OBk+OCj+OCjOOBn+WApBIXAwAr44GT44KP44KM44Gf5YCkEhYDACvjgZPjgo\/jgozjgZ\/lgKQSKAMAK+OBk+OCj+OCjOOBn+WApBImAwAr44GT44KP44KM44Gf5YCkEiUDACvjgZPjgo\/jgozjgZ\/lgKQSJAMAK+OBk+OCj+OCjOOBn+WApBIjAwAr44GT44KP44KM44Gf5YCkEiEDACvjgZPjgo\/jgozjgZ\/lgKQSIAMAK+OBk+OCj+OCjOOBn+WApBIfAwAr44GT44KP44KM44Gf5YCkEh4DACvjgZPjgo\/jgozjgZ\/lgKQNAAAAGAA\/AAHuAdwBygG4AaYBlAGCAW8BXAFJATYBIwEQAP0A6gDXAMQAsQCeAIsAeABlAFIAPwAAAAAAAAARQwMAKeaWsOOBl+OBhOWApDI2EUIDACnmlrDjgZfjgYTlgKQyNRFBAwAp5paw44GX44GE5YCkMjQRQAMAKeaWsOOBl+OBhOWApDIzET8DACnmlrDjgZfjgYTlgKQyMhE+AwAp5paw44GX44GE5YCkMjERPQMAKeaWsOOBl+OBhOWApDIwETwDACnmlrDjgZfjgYTlgKQxORE7AwAp5paw44GX44GE5YCkMTgROgMAKeaWsOOBl+OBhOWApDE3ETkDACnmlrDjgZfjgYTlgKQxNhE4AwAp5paw44GX44GE5YCkMTURNwMAKeaWsOOBl+OBhOWApDE0ETYDACnmlrDjgZfjgYTlgKQxMxE1AwAp5paw44GX44GE5YCkMTIRNAMAKeaWsOOBl+OBhOWApDExETMDACnmlrDjgZfjgYTlgKQxMBAyAwAn5paw44GX44GE5YCkORAxAwAn5paw44GX44GE5YCkOBAwAwAn5paw44GX44GE5YCkNxAvAwAn5paw44GX44GE5YCkNhAuAwAn5paw44GX44GE5YCkNRAtAwAn5paw44GX44GE5YCkNBAsAwAn5paw44GX44GE5YCkMw==",
4+ "hotJournal": "2dUF+SChY9cAAAAESqFhIAAAAAQAAAIAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADDQAAABwASQAB8QHiAdMBxAG1AaYBlwGIAXkBaQFZAUkBOQEpARkBCQD5AOkA2QDJALkAqQCZAIkAeQBpAFkASQAAAAAAAAAAAA4cAwAj5YWD44Gu5YCkMjgOGwMAI+WFg+OBruWApDI3DhoDACPlhYPjga7lgKQyNg4ZAwAj5YWD44Gu5YCkMjUOGAMAI+WFg+OBruWApDI0DhcDACPlhYPjga7lgKQyMw4WAwAj5YWD44Gu5YCkMjIOFQMAI+WFg+OBruWApDIxDhQDACPlhYPjga7lgKQyMA4TAwAj5YWD44Gu5YCkMTkOEgMAI+WFg+OBruWApDE4DhEDACPlhYPjga7lgKQxNw4QAwAj5YWD44Gu5YCkMTYODwMAI+WFg+OBruWApDE1Dg4DACPlhYPjga7lgKQxNA4NAwAj5YWD44Gu5YCkMTMODAMAI+WFg+OBruWApDEyDgsDACPlhYPjga7lgKQxMQ4KAwAj5YWD44Gu5YCkMTANCQMAIeWFg+OBruWApDkNCAMAIeWFg+OBruWApDgNBwMAIeWFg+OBruWApDcNBgMAIeWFg+OBruWApDYNBQMAIeWFg+OBruWApDUNBAMAIeWFg+OBruWApDQNAwMAIeWFg+OBruWApDMNAgMAIeWFg+OBruWApDINAQMAIeWFg+OBruWApDFKoWHXAAAAAgUAAAABAfsAAAAABAH7AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMcSqFhIAAAAAQNAAAADAFAAAHwAeAB0AHAAbABoAGQAYABcAFgAVABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4oAwAj5YWD44Gu5YCkNDAOJwMAI+WFg+OBruWApDM5DiYDACPlhYPjga7lgKQzOA4lAwAj5YWD44Gu5YCkMzcOJAMAI+WFg+OBruWApDM2DiMDACPlhYPjga7lgKQzNQ4iAwAj5YWD44Gu5YCkMzQOIQMAI+WFg+OBruWApDMzDiADACPlhYPjga7lgKQzMg4fAwAj5YWD44Gu5YCkMzEOHgMAI+WFg+OBruWApDMwDh0DACPlhYPjga7lgKQyOUqhYSAAAAABU1FMaXRlIGZvcm1hdCAzAAIAAQEAQCAgAAAAAgAAAAQAAAAAAAAAAAAAAAEAAAAEAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAC52iQ0AAAABAcEAAcEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA9AQYXDw8Ba3RhYmxldHQCQ1JFQVRFIFRBQkxFIHQgKGlkIElOVEVHRVIgUFJJTUFSWSBLRVksIHYgVEVYVClKoWEgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAXdcwPQAAAAQAAAIAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
5+ "expected": {
6+ "items": [
7+ {
8+ "id": 1,
9+ "name": "品物1",
10+ "price": 7.25,
11+ "qty": 1,
12+ "data": "01",
13+ "note": 3,
14+ "extra": "デフォルト"
15+ },
16+ {
17+ "id": 2,
18+ "name": "品物2",
19+ "price": 14.5,
20+ "qty": 2,
21+ "data": "0202",
22+ "note": 3,
23+ "extra": "デフォルト"
24+ },
25+ {
26+ "id": 3,
27+ "name": "品物3",
28+ "price": 21.75,
29+ "qty": 3,
30+ "data": "",
31+ "note": 3,
32+ "extra": "デフォルト"
33+ },
34+ {
35+ "id": 5,
36+ "name": "品物5",
37+ "price": 36.25,
38+ "qty": null,
39+ "data": "0505050505",
40+ "note": 3,
41+ "extra": "デフォルト"
42+ },
43+ {
44+ "id": 6,
45+ "name": "品物6",
46+ "price": 43.5,
47+ "qty": 6,
48+ "data": "",
49+ "note": 3,
50+ "extra": "デフォルト"
51+ },
52+ {
53+ "id": 7,
54+ "name": "品物7",
55+ "price": 50.75,
56+ "qty": 7,
57+ "data": "07070707070707",
58+ "note": 3,
59+ "extra": "デフォルト"
60+ },
61+ {
62+ "id": 9,
63+ "name": "品物9",
64+ "price": 65.25,
65+ "qty": 9,
66+ "data": "",
67+ "note": 3,
68+ "extra": "デフォルト"
69+ },
70+ {
71+ "id": 10,
72+ "name": "品物10",
73+ "price": 72.5,
74+ "qty": null,
75+ "data": "0A0A0A0A0A0A0A0A0A0A",
76+ "note": 2100,
77+ "extra": "デフォルト"
78+ },
79+ {
80+ "id": 11,
81+ "name": "品物11",
82+ "price": 79.75,
83+ "qty": 11,
84+ "data": "0B0B0B0B0B0B0B0B0B0B0B",
85+ "note": 4,
86+ "extra": "デフォルト"
87+ },
88+ {
89+ "id": 13,
90+ "name": "品物13",
91+ "price": 94.25,
92+ "qty": 13,
93+ "data": "0D0D0D0D0D0D0D0D0D0D0D0D0D",
94+ "note": 4,
95+ "extra": "デフォルト"
96+ },
97+ {
98+ "id": 14,
99+ "name": "品物14",
100+ "price": 101.5,
101+ "qty": 14,
102+ "data": "0E0E0E0E0E0E0E0E0E0E0E0E0E0E",
103+ "note": 4,
104+ "extra": "デフォルト"
105+ },
106+ {
107+ "id": 15,
108+ "name": "品物15",
109+ "price": 108.75,
110+ "qty": null,
111+ "data": "",
112+ "note": 4,
113+ "extra": "デフォルト"
114+ },
115+ {
116+ "id": 17,
117+ "name": "品物17",
118+ "price": 123.25,
119+ "qty": 17,
120+ "data": "1111111111111111111111111111111111",
121+ "note": 4,
122+ "extra": "デフォルト"
123+ },
124+ {
125+ "id": 18,
126+ "name": "品物18",
127+ "price": 130.5,
128+ "qty": 18,
129+ "data": "",
130+ "note": 4,
131+ "extra": "デフォルト"
132+ },
133+ {
134+ "id": 19,
135+ "name": "品物19",
136+ "price": 137.75,
137+ "qty": 19,
138+ "data": "13131313131313131313131313131313131313",
139+ "note": 4,
140+ "extra": "デフォルト"
141+ },
142+ {
143+ "id": 21,
144+ "name": "品物21",
145+ "price": 152.25,
146+ "qty": 21,
147+ "data": "",
148+ "note": 4,
149+ "extra": "デフォルト"
150+ },
151+ {
152+ "id": 22,
153+ "name": "品物22",
154+ "price": 159.5,
155+ "qty": 22,
156+ "data": "16161616161616161616161616161616161616161616",
157+ "note": 4,
158+ "extra": "デフォルト"
159+ },
160+ {
161+ "id": 23,
162+ "name": "品物23",
163+ "price": 166.75,
164+ "qty": 23,
165+ "data": "1717171717171717171717171717171717171717171717",
166+ "note": 4,
167+ "extra": "デフォルト"
168+ },
169+ {
170+ "id": 25,
171+ "name": "品物25",
172+ "price": 181.25,
173+ "qty": null,
174+ "data": "19191919191919191919191919191919191919191919191919",
175+ "note": 4,
176+ "extra": "デフォルト"
177+ },
178+ {
179+ "id": 26,
180+ "name": "品物26",
181+ "price": 188.5,
182+ "qty": 26,
183+ "data": "1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A",
184+ "note": 4,
185+ "extra": "デフォルト"
186+ },
187+ {
188+ "id": 27,
189+ "name": "品物27",
190+ "price": 195.75,
191+ "qty": 27,
192+ "data": "",
193+ "note": 4,
194+ "extra": "デフォルト"
195+ },
196+ {
197+ "id": 29,
198+ "name": "品物29",
199+ "price": 210.25,
200+ "qty": 29,
201+ "data": "1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D1D",
202+ "note": 4,
203+ "extra": "デフォルト"
204+ },
205+ {
206+ "id": 30,
207+ "name": "品物30",
208+ "price": 217.5,
209+ "qty": null,
210+ "data": "",
211+ "note": 2100,
212+ "extra": "デフォルト"
213+ },
214+ {
215+ "id": 31,
216+ "name": "品物31",
217+ "price": 224.75,
218+ "qty": 31,
219+ "data": "1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F",
220+ "note": 4,
221+ "extra": "デフォルト"
222+ },
223+ {
224+ "id": 33,
225+ "name": "品物33",
226+ "price": 239.25,
227+ "qty": 33,
228+ "data": "",
229+ "note": 4,
230+ "extra": "デフォルト"
231+ },
232+ {
233+ "id": 34,
234+ "name": "品物34",
235+ "price": 246.5,
236+ "qty": 34,
237+ "data": "22222222222222222222222222222222222222222222222222222222222222222222",
238+ "note": 4,
239+ "extra": "デフォルト"
240+ },
241+ {
242+ "id": 35,
243+ "name": "品物35",
244+ "price": 253.75,
245+ "qty": null,
246+ "data": "2323232323232323232323232323232323232323232323232323232323232323232323",
247+ "note": 4,
248+ "extra": "デフォルト"
249+ },
250+ {
251+ "id": 37,
252+ "name": "品物37",
253+ "price": 268.25,
254+ "qty": 37,
255+ "data": "25252525252525252525252525252525252525252525252525252525252525252525252525",
256+ "note": 4,
257+ "extra": "デフォルト"
258+ },
259+ {
260+ "id": 38,
261+ "name": "品物38",
262+ "price": 275.5,
263+ "qty": 38,
264+ "data": "2626262626262626262626262626262626262626262626262626262626262626262626262626",
265+ "note": 4,
266+ "extra": "デフォルト"
267+ },
268+ {
269+ "id": 39,
270+ "name": "品物39",
271+ "price": 282.75,
272+ "qty": 39,
273+ "data": "",
274+ "note": 4,
275+ "extra": "デフォルト"
276+ },
277+ {
278+ "id": 41,
279+ "name": "品物41",
280+ "price": 297.25,
281+ "qty": 41,
282+ "data": "2929292929292929292929292929292929292929292929292929292929292929292929292929292929",
283+ "note": 4,
284+ "extra": "デフォルト"
285+ },
286+ {
287+ "id": 42,
288+ "name": "品物42",
289+ "price": 304.5,
290+ "qty": 42,
291+ "data": "",
292+ "note": 4,
293+ "extra": "デフォルト"
294+ },
295+ {
296+ "id": 43,
297+ "name": "品物43",
298+ "price": 311.75,
299+ "qty": 43,
300+ "data": "2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B",
301+ "note": 4,
302+ "extra": "デフォルト"
303+ },
304+ {
305+ "id": 45,
306+ "name": "品物45",
307+ "price": 326.25,
308+ "qty": null,
309+ "data": "",
310+ "note": 4,
311+ "extra": "デフォルト"
312+ },
313+ {
314+ "id": 46,
315+ "name": "品物46",
316+ "price": 333.5,
317+ "qty": 46,
318+ "data": "2E2E2E2E2E2E2E2E2E2E2E2E2E2E2E2E2E2E2E2E2E2E2E2E2E2E2E2E2E2E2E2E2E2E2E2E2E2E2E2E2E2E2E2E2E2E",
319+ "note": 4,
320+ "extra": "デフォルト"
321+ },
322+ {
323+ "id": 47,
324+ "name": "品物47",
325+ "price": 340.75,
326+ "qty": 47,
327+ "data": "2F2F2F2F2F2F2F2F2F2F2F2F2F2F2F2F2F2F2F2F2F2F2F2F2F2F2F2F2F2F2F2F2F2F2F2F2F2F2F2F2F2F2F2F2F2F2F",
328+ "note": 4,
329+ "extra": "デフォルト"
330+ },
331+ {
332+ "id": 49,
333+ "name": "品物49",
334+ "price": 355.25,
335+ "qty": 49,
336+ "data": "31313131313131313131313131313131313131313131313131313131313131313131313131313131313131313131313131",
337+ "note": 4,
338+ "extra": "デフォルト"
339+ },
340+ {
341+ "id": 50,
342+ "name": "品物50",
343+ "price": 362.5,
344+ "qty": null,
345+ "data": "3232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232",
346+ "note": 2100,
347+ "extra": "デフォルト"
348+ },
349+ {
350+ "id": 51,
351+ "name": "品物51",
352+ "price": 369.75,
353+ "qty": 51,
354+ "data": "",
355+ "note": 4,
356+ "extra": "デフォルト"
357+ },
358+ {
359+ "id": 53,
360+ "name": "品物53",
361+ "price": 384.25,
362+ "qty": 53,
363+ "data": "3535353535353535353535353535353535353535353535353535353535353535353535353535353535353535353535353535353535",
364+ "note": 4,
365+ "extra": "デフォルト"
366+ },
367+ {
368+ "id": 54,
369+ "name": "品物54",
370+ "price": 391.5,
371+ "qty": 54,
372+ "data": "",
373+ "note": 4,
374+ "extra": "デフォルト"
375+ },
376+ {
377+ "id": 55,
378+ "name": "品物55",
379+ "price": 398.75,
380+ "qty": null,
381+ "data": "37373737373737373737373737373737373737373737373737373737373737373737373737373737373737373737373737373737373737",
382+ "note": 4,
383+ "extra": "デフォルト"
384+ },
385+ {
386+ "id": 57,
387+ "name": "品物57",
388+ "price": 413.25,
389+ "qty": 57,
390+ "data": "",
391+ "note": 4,
392+ "extra": "デフォルト"
393+ },
394+ {
395+ "id": 58,
396+ "name": "品物58",
397+ "price": 420.5,
398+ "qty": 58,
399+ "data": "3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A3A",
400+ "note": 4,
401+ "extra": "デフォルト"
402+ },
403+ {
404+ "id": 59,
405+ "name": "品物59",
406+ "price": 427.75,
407+ "qty": 59,
408+ "data": "3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B",
409+ "note": 4,
410+ "extra": "デフォルト"
411+ },
412+ {
413+ "id": 61,
414+ "name": "最後",
415+ "price": -1.5,
416+ "qty": 0,
417+ "data": "",
418+ "note": null,
419+ "extra": "x"
420+ }
421+ ],
422+ "tags": [
423+ {
424+ "c": 120,
425+ "m": "b9"
426+ }
427+ ],
428+ "cheap": [
429+ {
430+ "name": "品物1",
431+ "price": 7.25
432+ },
433+ {
434+ "name": "品物10",
435+ "price": 72.5
436+ },
437+ {
438+ "name": "品物11",
439+ "price": 79.75
440+ },
441+ {
442+ "name": "品物13",
443+ "price": 94.25
444+ },
445+ {
446+ "name": "品物2",
447+ "price": 14.5
448+ },
449+ {
450+ "name": "品物3",
451+ "price": 21.75
452+ },
453+ {
454+ "name": "品物5",
455+ "price": 36.25
456+ },
457+ {
458+ "name": "品物6",
459+ "price": 43.5
460+ },
461+ {
462+ "name": "品物7",
463+ "price": 50.75
464+ },
465+ {
466+ "name": "品物9",
467+ "price": 65.25
468+ },
469+ {
470+ "name": "最後",
471+ "price": -1.5
472+ }
473+ ],
474+ "byPrice": [
475+ 59,
476+ 58,
477+ 57,
478+ 55,
479+ 54,
480+ 53,
481+ 51,
482+ 50,
483+ 49,
484+ 47,
485+ 46,
486+ 45,
487+ 43,
488+ 42
489+ ],
490+ "seq": 61,
491+ "hot": {
492+ "c": 40,
493+ "g": "元の値1,元の値2,元の値3,元の値4,元の値5,元の値6,元の値7,元の値8,元の値9,元の値10,元の値11,元の値12,元の値13,元の値14,元の値15,元の値16,元の値17,元の値18,元の値19,元の値20,元の値21,元の値22,元の値23,元の値24,元の値25,元の値26,元の値27,元の値28,元の値29,元の値30,元の値31,元の値32,元の値33,元の値34,元の値35,元の値36,元の値37,元の値38,元の値39,元の値40"
494+ }
495+ }
496+}
@@ -0,0 +1,614 @@
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();