├── .gitignore ├── .poggit.yml ├── LICENSE ├── README.md ├── composer.json ├── src └── cooldogedev │ └── libSQL │ ├── ConnectionPool.php │ ├── exception │ └── SQLException.php │ ├── query │ ├── MySQLQuery.php │ ├── SQLQuery.php │ └── SQLiteQuery.php │ └── thread │ ├── MySQLThread.php │ ├── SQLThread.php │ └── SQLiteThread.php └── virion.yml /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE 2 | .idea/* 3 | !.idea/codeStyles/ 4 | !.idea/fileTemplates/ 5 | nbproject/* 6 | /vendor/ 7 | -------------------------------------------------------------------------------- /.poggit.yml: -------------------------------------------------------------------------------- 1 | --- # Poggit-CI Manifest. Open the CI at https://poggit.pmmp.io/ci/cooldogedev/libSQL 2 | build-by-default: true 3 | branches: 4 | - main 5 | - pm5 6 | projects: 7 | libSQL: 8 | path: "" 9 | model: virion 10 | type: library 11 | ... 12 | 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2024 cooldogedev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # libSQL 2 | 3 | A minimalistic implementation of asynchronous [SQL](https://en.wikipedia.org/wiki/SQL) for PHP. 4 | 5 | ### Usage 6 | 7 | #### Initialise the connection pool 8 | 9 | ```php 10 | $pool = new ConnectionPool(PluginBase, [ 11 | "provider" => "sqlite", 12 | "threads" => 2, 13 | "sqlite" => [ 14 | "path" => "test.db" 15 | ] 16 | ]); 17 | ``` 18 | 19 |
20 | 21 | #### Examples 22 | 23 | ###### Retrieve all customer records 24 | 25 | * Create the query class 26 | 27 | ```php 28 | final class CustomerRetrievalQuery extends SQLiteQuery { 29 | public function onRun(SQLite3 $connection): void { 30 | $this->setResult($connection->query($this->getQuery())?->fetchArray() ?: []); 31 | } 32 | 33 | public function getQuery(): string { return "SELECT * FROM customers"; } 34 | } 35 | ``` 36 | 37 | * Execute the query 38 | 39 | ```php 40 | $query = new CustomerRetrievalQuery(); 41 | $query->execute( 42 | onSuccess: function (array $customers): void { 43 | foreach ($customers as $customer) { 44 | echo $customer["name"] . " " . $customer["lastName"] . ": " . $customer["age"]; 45 | echo PHP_EOL; 46 | } 47 | }, 48 | onFailure: function (SQLException $exception): void { 49 | echo "Failed to retrieve customers due to: " . $exception->getMessage(); 50 | } 51 | ); 52 | ``` 53 | 54 |
55 | 56 | ###### Create a new customer record 57 | 58 | * Create the query class 59 | 60 | ```php 61 | final class CustomerCreationQuery extends SQLiteQuery { 62 | public function __construct( 63 | protected string $name, 64 | protected string $lastName, 65 | protected int $age 66 | ) {} 67 | 68 | public function onRun(SQLite3 $connection): bool { 69 | $statement = $connection->prepare($this->getQuery()); 70 | 71 | $statement->bindValue(":name", $this->getName()); 72 | $statement->bindValue(":lastName", $this->getLastName()); 73 | $statement->bindValue(":age", $this->getAge()); 74 | $statement->execute(); 75 | 76 | $this->setResult($connection->changes() > 0); 77 | 78 | $statement->close(); 79 | } 80 | 81 | public function getQuery(): string { 82 | return "INSERT OR IGNORE INTO customers (name, lastName, age) VALUES (:name, :lastName, :age)"; 83 | } 84 | 85 | public function getName(): string { return $this->name; } 86 | public function getLastName(): string { return $this->lastName; } 87 | public function getAge(): int { return $this->age; } 88 | } 89 | ``` 90 | 91 | * Execute the query 92 | 93 | ```php 94 | $query = new CustomerCreationQuery("Saul", "Goodman", 41); 95 | $pool->submit( 96 | query: $query, 97 | 98 | onSuccess: function (bool $created): void { 99 | echo $created ? "Customer created successfully!" : "Customer already exists!"; 100 | }, 101 | onFailure: function (SQLException $exception): void { 102 | echo "Failed to create the record due to: " . $exception->getMessage(); 103 | } 104 | ); 105 | ``` 106 | 107 | ### Projects using libSQL 108 | - [BedrockEconomy](https://github.com/cooldogepm/BedrockEconomy) 109 | - [BuildBattle](https://github.com/cooldogepm/BuildBattle) 110 | - [TheBridges](https://github.com/cooldogepm/TheBridges) 111 | - [TNTTag](https://github.com/cooldogepm/TNTTag) 112 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cooldogedev/libsql", 3 | "description": "A minimalistic implementation of asynchronous SQL", 4 | "type": "library", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "cooldogedev", 9 | "email": "cooldogedev@gmail.com" 10 | } 11 | ], 12 | "autoload": { 13 | "classmap": ["src"] 14 | }, 15 | "minimum-stability": "stable", 16 | "require": { 17 | "ext-pmmpthread": "*", 18 | "ext-sqlite3": "*", 19 | "ext-mysqli": "*", 20 | "ext-igbinary": "*" 21 | }, 22 | "extra": { 23 | "virion": { 24 | "spec": "3.1", 25 | "namespace-root": "cooldogedev\\libSQL" 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/cooldogedev/libSQL/ConnectionPool.php: -------------------------------------------------------------------------------- 1 | 53 | */ 54 | protected array $completionHandlers = []; 55 | 56 | /** 57 | * @var array 58 | */ 59 | protected array $threads = []; 60 | 61 | public function __construct(protected PluginBase $plugin, array $configuration) 62 | { 63 | self::setInstance($this); 64 | 65 | $isMySQL = $configuration["provider"] === "mysql"; 66 | 67 | for ($i = 0; $i < $configuration["threads"]; $i++) { 68 | $thread = $isMySQL ? 69 | new MySQLThread(... $configuration["mysql"]) : 70 | new SQLiteThread($plugin->getDataFolder() . $configuration["sqlite"]["path"]) 71 | ; 72 | 73 | $sleeperHandlerEntry = $this->plugin->getServer()->getTickSleeper()->addNotifier( 74 | function () use ($thread): void { 75 | /** 76 | * @var SQLQuery|null $query 77 | */ 78 | $query = $thread->getCompleteQueries()->shift(); 79 | 80 | if ($query === null) { 81 | return; 82 | } 83 | 84 | $error = $query->getError() !== null ? json_decode($query->getError(), true) : null; 85 | $exception = $error !== null ? SQLException::fromArray($error) : null; 86 | 87 | [$successHandler, $errorHandler] = $this->completionHandlers[$query->getIdentifier()] ?? [null, null]; 88 | 89 | match (true) { 90 | $exception === null && $successHandler !== null => $successHandler($query->getResult()), 91 | 92 | $exception !== null && $errorHandler !== null => $errorHandler($exception), 93 | $exception !== null => $this->plugin->getLogger()->logException($exception), 94 | 95 | default => null, 96 | }; 97 | 98 | if (isset($this->completionHandlers[$query->getIdentifier()])) { 99 | unset($this->completionHandlers[$query->getIdentifier()]); 100 | } 101 | } 102 | ); 103 | 104 | $thread->setSleeperHandlerEntry($sleeperHandlerEntry); 105 | $thread->start(); 106 | $this->threads[] = $thread; 107 | } 108 | } 109 | 110 | public function submit(SQLQuery $query, ?Closure $onSuccess = null, ?Closure $onFail = null): void 111 | { 112 | $identifier = [ 113 | spl_object_hash($query), 114 | microtime(), 115 | count($this->threads), 116 | count($this->completionHandlers), 117 | ]; 118 | 119 | $query->setIdentifier(bin2hex(implode("", $identifier))); 120 | $this->completionHandlers[$query->getIdentifier()] = [$onSuccess, $onFail]; 121 | $this->getLeastBusyThread()->addQuery($query); 122 | } 123 | 124 | protected function getLeastBusyThread(): SQLThread 125 | { 126 | $threads = $this->threads; 127 | usort($threads, static fn (SQLThread $a, SQLThread $b) => $a->getQueries()->count() <=> $b->getQueries()->count()); 128 | return $threads[0]; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/cooldogedev/libSQL/exception/SQLException.php: -------------------------------------------------------------------------------- 1 | _message, $this->_code); 44 | } 45 | 46 | public function _getMessage(): string 47 | { 48 | return $this->_message; 49 | } 50 | 51 | public function _getCode(): int 52 | { 53 | return $this->_code; 54 | } 55 | 56 | public function _getTrace(): array 57 | { 58 | return $this->_trace; 59 | } 60 | 61 | public function _getFile(): string 62 | { 63 | return $this->_file; 64 | } 65 | 66 | public function _getLine(): int 67 | { 68 | return $this->_line; 69 | } 70 | 71 | public function _getTraceAsString(): string 72 | { 73 | return $this->_traceAsString; 74 | } 75 | 76 | public static function fromArray(array $exception): SQLException 77 | { 78 | $class = $exception["class"] ?? SQLException::class; 79 | 80 | return new $class( 81 | _trace: $exception["trace"], 82 | _traceAsString: $exception["trace_string"], 83 | 84 | _message: $exception["message"], 85 | 86 | _file: $exception["file"], 87 | _code: $exception["code"], 88 | _line: $exception["line"], 89 | ); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/cooldogedev/libSQL/query/MySQLQuery.php: -------------------------------------------------------------------------------- 1 | identifier; 53 | } 54 | 55 | public function setIdentifier(string $identifier): void 56 | { 57 | $this->identifier = $identifier; 58 | } 59 | 60 | final public function run(): void 61 | { 62 | try { 63 | $this->onRun($this->getThread()->getConnection()); 64 | } catch (Throwable $throwable) { 65 | $this->error = json_encode([ 66 | "message" => $throwable->getMessage(), 67 | "code" => $throwable->getCode(), 68 | "trace" => $throwable->getTrace(), 69 | "trace_string" => $throwable->getTraceAsString(), 70 | "file" => $throwable->getFile(), 71 | "line" => $throwable->getLine(), 72 | "class" => $throwable instanceof SQLException ? $throwable::class : null 73 | ]); 74 | } 75 | } 76 | 77 | final public function getResult(): mixed 78 | { 79 | return $this->resultSerialized ? igbinary_unserialize($this->result) : $this->result; 80 | } 81 | 82 | final protected function setResult(mixed $result): void 83 | { 84 | $this->resultSerialized = !is_scalar($result) && !$result instanceof ThreadSafe; 85 | $this->result = $this->resultSerialized ? igbinary_serialize($result) : $result; 86 | } 87 | 88 | final public function getError(): ?string 89 | { 90 | return $this->error; 91 | } 92 | 93 | public function getThread(): SQLThread 94 | { 95 | $worker = NativeThread::getCurrentThread(); 96 | assert($worker instanceof SQLThread); 97 | 98 | return $worker; 99 | } 100 | 101 | final public function execute(?Closure $onSuccess = null, ?Closure $onFail = null): void 102 | { 103 | ConnectionPool::getInstance()->submit($this, $onSuccess, $onFail); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/cooldogedev/libSQL/query/SQLiteQuery.php: -------------------------------------------------------------------------------- 1 | host, $this->username, $this->password, $this->database, $this->port); 50 | 51 | if (self::$connection->connect_error) { 52 | throw new RuntimeException(self::$connection->connect_error, self::$connection->connect_errno); 53 | } 54 | } 55 | 56 | protected function onRun(): void 57 | { 58 | if (self::$connection === null || !self::$connection->ping()) { 59 | $this->reconnect(); 60 | } 61 | 62 | parent::onRun(); 63 | } 64 | 65 | public function getConnection(): mysqli 66 | { 67 | if (self::$connection === null || !self::$connection->ping()) { 68 | $this->reconnect(); 69 | } 70 | 71 | return self::$connection; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/cooldogedev/libSQL/thread/SQLThread.php: -------------------------------------------------------------------------------- 1 | queries = new ThreadSafeArray(); 46 | $this->completeQueries = new ThreadSafeArray(); 47 | } 48 | 49 | protected function onRun(): void 50 | { 51 | $this->running = true; 52 | 53 | $notifier = $this->sleeperHandlerEntry->createNotifier(); 54 | 55 | while ($this->running) { 56 | $this->synchronized( 57 | function (): void { 58 | if ($this->running && $this->queries->count() === 0 && $this->completeQueries->count() === 0) { 59 | $this->wait(); 60 | } 61 | } 62 | ); 63 | 64 | if ($this->completeQueries->count() !== 0) { 65 | $notifier->wakeupSleeper(); 66 | } 67 | 68 | /** 69 | * @var SQLQuery|null $query 70 | */ 71 | $query = $this->queries->shift(); 72 | 73 | if ($query === null) { 74 | continue; 75 | } 76 | 77 | $query->run(); 78 | 79 | $this->completeQueries[] = $query; 80 | } 81 | } 82 | 83 | public function quit(): void 84 | { 85 | $this->synchronized( 86 | function (): void { 87 | $this->running = false; 88 | $this->notify(); 89 | } 90 | ); 91 | 92 | parent::quit(); 93 | } 94 | 95 | public function setSleeperHandlerEntry(SleeperHandlerEntry $sleeperHandlerEntry): void 96 | { 97 | $this->sleeperHandlerEntry = $sleeperHandlerEntry; 98 | } 99 | 100 | public function addQuery(SQLQuery $query): void 101 | { 102 | $this->synchronized( 103 | function () use ($query): void { 104 | $this->queries[] = $query; 105 | $this->notify(); 106 | } 107 | ); 108 | } 109 | 110 | /** 111 | * @return ThreadSafeArray 112 | */ 113 | public function getQueries(): ThreadSafeArray 114 | { 115 | return $this->queries; 116 | } 117 | 118 | /** 119 | * @return ThreadSafeArray 120 | */ 121 | public function getCompleteQueries(): ThreadSafeArray 122 | { 123 | return $this->completeQueries; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/cooldogedev/libSQL/thread/SQLiteThread.php: -------------------------------------------------------------------------------- 1 | databasePath); 43 | self::$connection->busyTimeout(60000); 44 | } 45 | 46 | protected function onRun(): void 47 | { 48 | if (self::$connection === null) { 49 | $this->reconnect(); 50 | } 51 | 52 | parent::onRun(); 53 | } 54 | 55 | public function getConnection(): SQLite3 56 | { 57 | if (self::$connection === null) { 58 | $this->reconnect(); 59 | } 60 | 61 | return self::$connection; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /virion.yml: -------------------------------------------------------------------------------- 1 | name: libSQL 2 | author: cooldogedev 3 | antigen: cooldogedev\libSQL 4 | api: [ 5.0.0 ] 5 | version: 0.2.6 6 | --------------------------------------------------------------------------------