SQLiteドライバー
守矢諏訪子 <suwako@076.moe>、2026-11-13 07:29
コミット e1e5ab1cfad63086b5fae176c5736fb9f7bf1924
・親 f9c2919
・ファイルを見る
18個のファイル、+8730 -10
変更
README-JP.md
| @@ -207,13 +207,13 @@ | ||
| 207 | 207 | |
| 208 | 208 | テンプレートやコードでは、URLは `ass_url('article/a.png')`、ディスク上のファイルは `ROOT.ass_path('article/a.png')` を使います。 |
| 209 | 209 | |
| 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 コンテキストオプションの配列)。 | |
| 213 | 213 | ```php |
| 214 | 214 | $db = new \Std\Lib\Db(); |
| 215 | 215 | $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 | |
| 217 | 217 | $id = $db->query('INSERT INTO users (name) VALUES (?) RETURNING id', ['諏訪子'])['rows'][0]['id']; // PostgreSQL |
| 218 | 218 | $db->transaction(function(\Std\Lib\Db $db): void { |
| 219 | 219 | $db->query('UPDATE users SET points = points - ? WHERE id = ?', [100, 1]); |
| @@ -224,6 +224,7 @@ | ||
| 224 | 224 | * 整数の列は `int`、`FLOAT`・`DOUBLE`(PostgreSQL は `real`・`double precision`)は `float`、`NULL` は `null`、その他(`DECIMAL`・`numeric`、日付、JSON、`BLOB` 等)は文字列で返します。PostgreSQL の `boolean` は `bool`、`bytea` は生のバイト、配列は PHP の配列で返します。 |
| 225 | 225 | * エラーは `Std\Lib\Db\DbException`(`getCode()` は MySQL のエラーコード、例:重複は1062、`sqlState` は SQLSTATE、例:PostgreSQL の重複は23505)を投げ、`log/db_log.txt` に書きます。 |
| 226 | 226 | * 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` で明示も可)。 | |
| 227 | 228 | * PostgreSQL の認証:SCRAM-SHA-256(TLS ならチャネルバインディング付きの SCRAM-SHA-256-PLUS、オプションの `channel_binding` は `prefer`・`require`・`disable`)、MD5、`password`(`allow_cleartext` を `true` にしない限り、TLS か UNIX ソケットのみ)、`trust`。GSSAPI・SSPI は未対応です。 |
| 228 | 229 | * PostgreSQL:`?` のプレースホルダは `$1`、`$2`… にします(文字列・識別子・コメントの外のみ。`?` そのもの(jsonb の演算子等)は `??` と書く)。`$1` もそのまま使えます。挿入した ID は無いので、`RETURNING` を使って下さい。トランザクションの中でエラーが起きた後の `COMMIT` は、PostgreSQL がロールバックするので `DbException`(SQLSTATE 25P02)を投げます。`COPY`、`LISTEN` の通知、実行中のクエリの取り消しは未対応です。PostgreSQL 16 で確認しました。 |
| 229 | 230 | * テストは偽物のサーバーを使います。PostgreSQL のテストを本物のサーバーでも実行するには、`LB_TEST_PGSQL=pgsql://ユーザー:パスワード@ホスト:ポート/データベース` を設定して下さい(TLS は `?cafile=/path/ca.pem` を追加)。名前が `lb_test_` で始まるテーブルを作って消します。 |
変更
README.md
| @@ -205,13 +205,13 @@ | ||
| 205 | 205 | |
| 206 | 206 | 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. |
| 207 | 207 | |
| 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']`). | |
| 211 | 211 | ```php |
| 212 | 212 | $db = new \Std\Lib\Db(); |
| 213 | 213 | $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 | |
| 215 | 215 | $id = $db->query('INSERT INTO users (name) VALUES (?) RETURNING id', ['諏訪子'])['rows'][0]['id']; // PostgreSQL |
| 216 | 216 | $db->transaction(function(\Std\Lib\Db $db): void { |
| 217 | 217 | $db->query('UPDATE users SET points = points - ? WHERE id = ?', [100, 1]); |
| @@ -222,6 +222,7 @@ | ||
| 222 | 222 | * 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. |
| 223 | 223 | * 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`. |
| 224 | 224 | * 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`). | |
| 225 | 226 | * 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. |
| 226 | 227 | * 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. |
| 227 | 228 | * 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 @@ | ||
| 76 | 76 | 'pubkey' => ROOT.'/public/static/pub.pem', |
| 77 | 77 | 'privkey' => ROOT.'/data/priv.pem', |
| 78 | 78 | ]); |
| 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 | |
| 80 | 80 | // socket:UNIXソケットのパス(指定するとhost・portの代わりに使う、例:/var/run/mysql/mysql.sock、PostgreSQLはディレクトリも可:/tmp) |
| 81 | 81 | // tls:trueで証明書を確認するTLS、又はsslのコンテキストオプションの配列(例:['cafile' => '/etc/ssl/ca.pem']) |
| 82 | 82 | // debug:送受信したパケットを出力する |
| 83 | 83 | // PostgreSQLのみ:channel_binding(prefer、require、disable)、allow_cleartext(TLSでなくてもパスワードをそのまま送る) |
| 84 | +// SQLiteのみ:path(ファイル、相対パスはROOTから、例:data/site.sqlite)、readonly、busy_timeout(ロックを待つミリ秒) | |
| 84 | 85 | define('DBINFO', [ |
| 85 | 86 | 'driver' => 'mysql', |
| 86 | 87 | 'host' => '127.0.0.1', |
| @@ -40,6 +40,7 @@ | ||
| 40 | 40 | use Std\Lib\Db\DbInterface; |
| 41 | 41 | use Std\Lib\Db\Mysql; |
| 42 | 42 | use Std\Lib\Db\Pgsql; |
| 43 | +use Std\Lib\Db\Sqlite; | |
| 43 | 44 | |
| 44 | 45 | /** |
| 45 | 46 | * データベース |
| @@ -49,7 +50,7 @@ | ||
| 49 | 50 | * 使い方: |
| 50 | 51 | * $db = new Db(); |
| 51 | 52 | * $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を使う) | |
| 53 | 54 | * $db->transaction(function(Db $db): void { |
| 54 | 55 | * $db->query('UPDATE users SET points = points - ? WHERE id = ?', [100, 1]); |
| 55 | 56 | * $db->query('UPDATE users SET points = points + ? WHERE id = ?', [100, 2]); |
| @@ -62,6 +63,8 @@ | ||
| 62 | 63 | 'pgsql' => Pgsql::class, |
| 63 | 64 | 'postgres' => Pgsql::class, |
| 64 | 65 | 'postgresql' => Pgsql::class, |
| 66 | + 'sqlite' => Sqlite::class, | |
| 67 | + 'sqlite3' => Sqlite::class, | |
| 65 | 68 | ]; |
| 66 | 69 | |
| 67 | 70 | 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+cAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFzEAAAASJQAAAA0bAAAADA8KAWYALgELAgP1A4gDewNhA1QDRwMtAyADEwPpAvkC7ALfAsUCuAKrApED3QKEAncCXQJQAkMCKQIcAg8B9QHoAdsBwQG0AacBjQPFAYABcwFZAUwBPwElARgBCwO5A60DlQEyA5UDlQOVA5UDlQOVA5UDlQOVA5UDlQOVA5UDlQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAx0B5ZOB54mpNTk7DAMdAeWTgeeJqTU4OgwDHQHlk4Hniak1NzkKAxkB5pyA5b6MPQAADAMdAeWTgeeJqTU1NwwDHQHlk4Hniak1NDYMAx0B5ZOB54mpNTM1AZoADQAAAAAAAAAAAAwDHQHlk4Hniak1MTMMAx0B5ZOB54mpNTAyDAMdAeWTgeeJqTQ5MQHOAA0AAAAAAAAAAAAMAx0B5ZOB54mpNDcvDAMdAeWTgeeJqTQ2LgwDHQHlk4Hniak0NS0CAgANAAAAAAAAAAAADAMdAeWTgeeJqTQzKwwDHQHlk4Hniak0MioMAx0B5ZOB54mpNDEpAjYADQAAAAAAAAAAAAwDHQHlk4HniakzOScMAx0B5ZOB54mpMzgmDAMdAeWTgeeJqTM3JQJqAA0AAAAAAAAAAAAMAx0B5ZOB54mpMzUjDAMdAeWTgeeJqTM0IgwDHQHlk4HniakzMyECngANAAAAAAAAAAAADAMdAeWTgeeJqTMxHwwDHQHlk4HniakzMB4MAx0B5ZOB54mpMjkdAtIADQAAAAAAAAAAAAwDHQHlk4HniakyNxsMAx0B5ZOB54mpMjYaDAMdAeWTgeeJqTI1GQMGAA0AAAAAAAAAAAAMAx0B5ZOB54mpMjMXDAMdAeWTgeeJqTIyFgwDHQHlk4HniakyMRUDOgANAAAAAAAAAAAADAMdAeWTgeeJqTE5EwwDHQHlk4HniakxOBIMAx0B5ZOB54mpMTcRA24ADQAAAAAAAAAAAAwDHQHlk4HniakxNQ8MAx0B5ZOB54mpMTQODAMdAeWTgeeJqTEzDQOhAA0AAAAAAAAAAAAMAx0B5ZOB54mpMTELDAMdAeWTgeeJqTEwCgsDGwHlk4Hniak5CQPRAAwAAAAAAAAAAAsDGwHlk4Hniak3BwsDGwHlk4Hniak2BgsDGwHlk4Hniak1BQAAAAwAAAAAAAAAAAsDGwHlk4HniakzAwsDGwHlk4HniakyAgoDGwnlk4HniakxDQAAAAED9QAD9QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJAQMXAWl0ZW1zPQoBHQAuAOIAAPAA\/wEOASYBNAFDAVsBagF4AZABnwGuAcUB1AHjAfoCCQIYAjACPgJNAmUCdAKCApoCqQK4As8C3gLtAwQDEwMiAzkDRwNWA20DfAOKA6EDsAO\/A9UD5APzAOID8wPzA\/MD8wPzA\/MD8wPzA\/MD8wPzA\/MD8wPzAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANBAcIAb\/4AAAAAAAAPQ4EBwEBQHq8AAAAAAA7Ow4EBwEBQHpIAAAAAAA6Og4EBwEBQHnUAAAAAAA5OQFSAAkAAAAAAA0EBwABQHjsAAAAAAA3DgQHAQFAeHgAAAAAADY2DgQHAQFAeAQAAAAAADU1AYcACQAAAAAADgQHAQFAdxwAAAAAADMzDQQHAAFAdqgAAAAAADIOBAcBAUB2NAAAAAAAMTEBvAAJAAAAAAAOBAcBAUB1TAAAAAAALy8OBAcBAUB02AAAAAAALi4NBAcAAUB0ZAAAAAAALQHyAAkAAAAAAA4EBwEBQHN8AAAAAAArKw4EBwEBQHMIAAAAAAAqKg4EBwEBQHKUAAAAAAApKQInAAgAAAAADgQHAQFAcawAAAAAACcnDgQHAQFAcTgAAAAAACYmDgQHAQFAcMQAAAAAACUlAlwACQAAAAAADQQHAAFAb7gAAAAAACMOBAcBAUBu0AAAAAAAIiIOBAcBAUBt6AAAAAAAISECkQAJAAAAAAAOBAcBAUBsGAAAAAAAHx8NBAcAAUBrMAAAAAAAHg4EBwEBQGpIAAAAAAAdHQLGAAkAAAAAAA4EBwEBQGh4AAAAAAAbGw4EBwEBQGeQAAAAAAAaGg0EBwABQGaoAAAAAAAZAvwACQAAAAAADgQHAQFAZNgAAAAAABcXDgQHAQFAY\/AAAAAAABYWDgQHAQFAYwgAAAAAABUVAzEACAAAAAAOBAcBAUBhOAAAAAAAExMOBAcBAUBgUAAAAAAAEhIOBAcBAUBe0AAAAAAAEREDZQAIAAAAAA0EBwABQFswAAAAAAAPDgQHAQFAWWAAAAAAAA4ODgQHAQFAV5AAAAAAAA0NA5kACAAAAAAOBAcBAUBT8AAAAAAACwsNBAcAAUBSIAAAAAAACg4EBwEBQFBQAAAAAAAJCQPNAAgAAAAADgQHAQFASWAAAAAAAAcHDgQHAQFARcAAAAAAAAYGDQQHAAFAQiAAAAAAAAUAAAAIAAAAAA4EBwEBQDXAAAAAAAADAw4EBwEBQC0AAAAAAAACAgwEBwkJQB0AAAAAAAAFAAAAAQP7AAAAACkD+wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAChmAgAAAAED8gAAAAAnA\/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+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIOmVt+OBhOODoeODojMwIA0ArwAJAD4AA7gDlgFmAUQA9wCOAD4DRQLyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATi4HAB0HAWgd5ZOB54mpNDZAdNgAAAAAAC4uLi4uLi4uLi4uLi4uLi4uLi4uLi4uLi4uLi4uLi4uLi4uLi4uLi4uLi4uLi4u44Oh44OiNDYfLQcAHQcAAB3lk4Hniak0NUB0ZAAAAAAA44Oh44OiNDUBsQBIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABLKwcAHQcBYh3lk4Hniak0M0BzfAAAAAAAKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKysrKyvjg6Hjg6I0MyAqBwAdBwEAHeWTgeeJqTQyQHMIAAAAAAAq44Oh44OiNDJJKQcAHQcBXh3lk4Hniak0MUBylAAAAAAAKSkpKSkpKSkpKSkpKSkpKSkpKSkpKSkpKSkpKSkpKSkpKSkpKSkpKSkp44Oh44OiNDEAAAFBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABRMQcAHQcBbh3lk4Hniak0OUB2NAAAAAAAMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTHjg6Hjg6I0OU8vBwAdBwFqHeWTgeeJqTQ3QHVMAAAAAAAvLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy\/jg6Hjg6I0NyAnBwAdBwEAHeWTgeeJqTM5QHGsAAAAAAAn44Oh44OiMzlGJgcAHQcBWB3lk4HniakzOEBxOAAAAAAAJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYm44Oh44OiMzgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADQAAAAkAIgACCwHpAZIBcAEYAPYAmQA7ACIAAAAAAAAAABc9CAAZBwgAAA\/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+OBhOODoeODojUwIAAAAADplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCDplbfjgYTjg6Hjg6I1MCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoAAAAvAmYAA\/oD8gPqA+ED2QPQA8gDvwO3A64DpgOdA5UDjAOEA3sDcwNqA2IDWANQA0YDPgM0AywDIgMaAxADCAL+AvYC7ALkAtoC0gLIAsACtgKuAqQCnAKSAooCgAJ4Am4CZgJcAlQCSgJCAjgCMAImAh4CFAIMAgIB+gHwAegB3gHWAcwBxAG6AbIBqAGgAZYBjgGEAXwBcgFqAWABWAFOAUYBPAE0ASoBIgEYARABBgD+APQA7ADiANoA0ADIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwQBDwEYYS8JBAETARdiMjMuBwQBDwEXYS0JBAETARZiMjIsBwQBDwEWYSsJBAETARViMjEqBwQBDwEVYSkJBAETARRiMjAoBwQBDwEUYScJBAETARNiMTkmBwQBDwETYSUJBAETARJiMTgkBwQBDwESYSMJBAETARFiMTciBwQBDwERYSEJBAETARBiMTYgBwQBDwEQYR8JBAETAQ9iMTUeBwQBDwEPYR0JBAETAQ5iMTQcBwQBDwEOYRsJBAETAQ1iMTMaBwQBDwENYRkJBAETAQxiMTIYBwQBDwEMYRcJBAETAQtiMTEWBwQBDwELYRUJBAETAQpiMTAUBwQBDwEKYRMIBAERAQliORIHBAEPAQlhEQgEAREBCGI4EAcEAQ8BCGEPCAQBEQEHYjcOBwQBDwEHYQ0IBAERAQZiNgwHBAEPAQZhCwgEAREBBWI1CgcEAQ8BBWEJCAQBEQEEYjQIBwQBDwEEYQcIBAERAQNiMwYHBAEPAQNhBQgEAREBAmIyBAcEAQ8BAmEDBwQJEQFiMQIFBAkPCWEKAAAASAF4AAP4A+4D5gPcA9QDygPCA7gDsAOmA54DlAOMA4IDegNwA2gDXgNWA0wDRAM6AzIDKAMgAxYDDgMEAvwC8gLqAuAC2ALOAsYCvAK0AqoCogKYApAChgJ+AnQCbAJiAloCUAJIAj4CNgIsAiQCGgISAggCAAH2Ae4B5AHcAdIBygHAAbgBrgGmAZwBlAGKAYIBeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACQQBEwE8YjYweAcEAQ8BPGF3CQQBEwE7YjU5dgcEAQ8BO2F1CQQBEwE6YjU4dAcEAQ8BOmFzCQQBEwE5YjU3cgcEAQ8BOWFxCQQBEwE4YjU2cAcEAQ8BOGFvCQQBEwE3YjU1bgcEAQ8BN2FtCQQBEwE2YjU0bAcEAQ8BNmFrCQQBEwE1YjUzagcEAQ8BNWFpCQQBEwE0YjUyaAcEAQ8BNGFnCQQBEwEzYjUxZgcEAQ8BM2FlCQQBEwEyYjUwZAcEAQ8BMmFjCQQBEwExYjQ5YgcEAQ8BMWFhCQQBEwEwYjQ4YAcEAQ8BMGFfCQQBEwEvYjQ3XgcEAQ8BL2FdCQQBEwEuYjQ2XAcEAQ8BLmFbCQQBEwEtYjQ1WgcEAQ8BLWFZCQQBEwEsYjQ0WAcEAQ8BLGFXCQQBEwErYjQzVgcEAQ8BK2FVCQQBEwEqYjQyVAcEAQ8BKmFTCQQBEwEpYjQxUgcEAQ8BKWFRCQQBEwEoYjQwUAcEAQ8BKGFPCQQBEwEnYjM5TgcEAQ8BJ2FNCQQBEwEmYjM4TAcEAQ8BJmFLCQQBEwElYjM3SgcEAQ8BJWFJCQQBEwEkYjM2SAcEAQ8BJGFHCQQBEwEjYjM1RgcEAQ8BI2FFCQQBEwEiYjM0RAcEAQ8BImFDCQQBEwEhYjMzQgcEAQ8BIWFBCQQBEwEgYjMyQAcEAQ8BIGE\/CQQBEwEfYjMxPgcEAQ8BH2E9CQQBEwEeYjMwPAcEAQ8BHmE7CQQBEwEdYjI5OgcEAQ8BHWE5CQQBEwEcYjI4OAcEAQ8BHGE3CQQBEwEbYjI3NgcEAQ8BG2E1CQQBEwEaYjI2NAcEAQ8BGmEzCQQBEwEZYjI1MgcEAQ8BGWExDQAAAGYA2wAD+gPzA+wD5APdA9UDzgPGA78DtwOwA6gDoQOZA5IDigODA3sDdANrA2QDWwNUA0sDRAM7AzQDKwMkAxsDFAMLAwQC+wL0AusC5ALbAtQCywLEArsCtAKrAqQCmwKUAosChAJ7AnQCawJkAlsCVAJLAkQCOwI0AisCJAIbAhQCCwIEAfsB9AHrAeQB2wHUAcsBxAG7AbQBqwGkAZsBlAGLAYQBewF0AWsBZAFbAVQBSwFEATsBNAErASQBGwEUAQsBBAD7APQA6wDkANsAAAAAAAAAB2YDARMzYjUxBWUDAQ8zYQdkAwETMmI1MAVjAwEPMmEHYgMBEzFiNDkFYQMBDzFhB2ADARMwYjQ4BV8DAQ8wYQdeAwETL2I0NwVdAwEPL2EHXAMBEy5iNDYFWwMBDy5hB1oDARMtYjQ1BVkDAQ8tYQdYAwETLGI0NAVXAwEPLGEHVgMBEytiNDMFVQMBDythB1QDARMqYjQyBVMDAQ8qYQdSAwETKWI0MQVRAwEPKWEHUAMBEyhiNDAFTwMBDyhhB04DARMnYjM5BU0DAQ8nYQdMAwETJmIzOAVLAwEPJmEHSgMBEyViMzcFSQMBDyVhB0gDARMkYjM2BUcDAQ8kYQdGAwETI2IzNQVFAwEPI2EHRAMBEyJiMzQFQwMBDyJhB0IDARMhYjMzBUEDAQ8hYQdAAwETIGIzMgU\/AwEPIGEHPgMBEx9iMzEFPQMBDx9hBzwDARMeYjMwBTsDAQ8eYQc6AwETHWIyOQU5AwEPHWEHOAMBExxiMjgFNwMBDxxhBzYDARMbYjI3BTUDAQ8bYQc0AwETGmIyNgUzAwEPGmEHMgMBExliMjUFMQMBDxlhBzADARMYYjI0BS8DAQ8YYQcuAwETF2IyMwUtAwEPF2EHLAMBExZiMjIFKwMBDxZhByoDARMVYjIxBSkDAQ8VYQcoAwETFGIyMAUnAwEPFGEHJgMBExNiMTkFJQMBDxNhByQDARMSYjE4BSMDAQ8SYQciAwETEWIxNwUhAwEPEWEHIAMBExBiMTYFHwMBDxBhBx4DARMPYjE1BR0DAQ8PYQccAwETDmIxNAUbAwEPDmEHGgMBEw1iMTMFGQMBDw1hBxgDARMMYjEyBRcDAQ8MYQcWAwETC2IxMQUVAwEPC2EHFAMBEwpiMTAFEwMBDwphBhIDAREJYjkFEQMBDwlhBhADAREIYjgFDwMBDwhhBg4DAREHYjcFDQMBDwdhBgwDAREGYjYFCwMBDwZhBgoDAREFYjUFCQMBDwVhBggDAREEYjQFBwMBDwRhBgYDAREDYjMFBQMBDwNhBgQDARECYjIFAwMBDwJhBQIDCRFiMQQBAwkPYQ0AAAASA3AAA\/kD8APpA+AD2QPQA8kDwAO5A7ADqQOgA5kDkAOJA4ADeQNwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHeAMBEzxiNjAFdwMBDzxhB3YDARM7YjU5BXUDAQ87YQd0AwETOmI1OAVzAwEPOmEHcgMBEzliNTcFcQMBDzlhB3ADARM4YjU2BW8DAQ84YQduAwETN2I1NQVtAwEPN2EHbAMBEzZiNTQFawMBDzZhB2oDARM1YjUzBWkDAQ81YQdoAwETNGI1MgVnAwEPNGE=", | |
| 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(); |