├── .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 |
--------------------------------------------------------------------------------