Skip to content

Commit f88bc32

Browse files
committed
[sync] Update embedded LibDB from standalone
1 parent ec2e927 commit f88bc32

8 files changed

Lines changed: 1143 additions & 0 deletions

File tree

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace imperazim\db\async;
6+
7+
use pocketmine\scheduler\AsyncTask;
8+
9+
/**
10+
* Internal AsyncTask for running database queries off the main thread.
11+
* Uses SQL + params (serializable data) instead of closures for the async thread.
12+
* Callbacks are stored via storeLocal() for main thread completion.
13+
*/
14+
final class AsyncDatabaseTask extends AsyncTask {
15+
16+
private const TLS_CALLBACKS = 'callbacks';
17+
18+
private string $driverType;
19+
private string $serializedConfig;
20+
private string $sql;
21+
private string $serializedParams;
22+
23+
/**
24+
* @param string $driverType 'sqlite' or 'mysql'
25+
* @param array $config Connection config
26+
* @param string $sql SQL query with ? placeholders
27+
* @param array $params Bound parameters
28+
* @param \Closure|null $onComplete fn(array $rows): void
29+
* @param \Closure|null $onError fn(\Throwable $error): void
30+
*/
31+
public function __construct(
32+
string $driverType,
33+
array $config,
34+
string $sql,
35+
array $params = [],
36+
?\Closure $onComplete = null,
37+
?\Closure $onError = null
38+
) {
39+
$this->driverType = $driverType;
40+
$this->serializedConfig = serialize($config);
41+
$this->sql = $sql;
42+
$this->serializedParams = serialize($params);
43+
44+
$this->storeLocal(self::TLS_CALLBACKS, [
45+
'onComplete' => $onComplete,
46+
'onError' => $onError,
47+
]);
48+
}
49+
50+
public function onRun(): void {
51+
try {
52+
$config = unserialize($this->serializedConfig);
53+
$params = unserialize($this->serializedParams);
54+
55+
$rows = match ($this->driverType) {
56+
'sqlite' => $this->runSqlite($config, $this->sql, $params),
57+
'mysql' => $this->runMysql($config, $this->sql, $params),
58+
default => throw new \RuntimeException("Unsupported async driver: {$this->driverType}"),
59+
};
60+
61+
$this->setResult(['success' => true, 'data' => serialize($rows)]);
62+
} catch (\Throwable $e) {
63+
$this->setResult(['success' => false, 'error' => $e->getMessage()]);
64+
}
65+
}
66+
67+
public function onCompletion(): void {
68+
/** @var array{onComplete: ?\Closure, onError: ?\Closure} $callbacks */
69+
$callbacks = $this->fetchLocal(self::TLS_CALLBACKS);
70+
$result = $this->getResult();
71+
72+
if ($result['success']) {
73+
$callbacks['onComplete']?->__invoke(unserialize($result['data']));
74+
} else {
75+
$callbacks['onError']?->__invoke(new \RuntimeException($result['error']));
76+
}
77+
}
78+
79+
private function runSqlite(array $config, string $sql, array $params): array {
80+
$pdo = new \PDO("sqlite:{$config['database']}", null, null, [
81+
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
82+
]);
83+
$stmt = $pdo->prepare($sql);
84+
$stmt->execute($params);
85+
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
86+
}
87+
88+
private function runMysql(array $config, string $sql, array $params): array {
89+
$mysqli = new \mysqli($config['host'], $config['username'], $config['password'], $config['database']);
90+
if ($mysqli->connect_error) {
91+
throw new \RuntimeException("MySQL connection failed: " . $mysqli->connect_error);
92+
}
93+
94+
$stmt = $mysqli->prepare($sql);
95+
if ($stmt === false) {
96+
throw new \RuntimeException("MySQL prepare failed: " . $mysqli->error);
97+
}
98+
99+
if (!empty($params)) {
100+
$types = '';
101+
foreach ($params as $p) {
102+
$types .= match (true) {
103+
is_int($p) => 'i',
104+
is_float($p) => 'd',
105+
default => 's',
106+
};
107+
}
108+
$stmt->bind_param($types, ...$params);
109+
}
110+
111+
$stmt->execute();
112+
$result = $stmt->get_result();
113+
$rows = $result ? $result->fetch_all(MYSQLI_ASSOC) : [];
114+
$stmt->close();
115+
$mysqli->close();
116+
return $rows;
117+
}
118+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace imperazim\db\async;
6+
7+
use pocketmine\Server;
8+
9+
/**
10+
* Non-blocking database queries using PocketMine's AsyncTask system.
11+
*
12+
* Usage:
13+
* AsyncQuery::sqlite('path/to/db.db', "SELECT * FROM users WHERE level > ?", [10],
14+
* onComplete: fn(array $rows) => var_dump($rows),
15+
* onError: fn(\Throwable $e) => echo $e->getMessage()
16+
* );
17+
*
18+
* AsyncQuery::mysql(
19+
* ['host' => 'localhost', 'username' => 'root', 'password' => '', 'database' => 'mydb'],
20+
* "SELECT * FROM users",
21+
* onComplete: fn(array $rows) => handleResults($rows)
22+
* );
23+
*/
24+
final class AsyncQuery {
25+
26+
/**
27+
* Runs an async SQLite query.
28+
*
29+
* @param string $dbPath Full path to SQLite database file
30+
* @param string $sql SQL query with ? placeholders
31+
* @param array $params Bound parameters
32+
* @param \Closure|null $onComplete fn(array $rows): void — called on main thread
33+
* @param \Closure|null $onError fn(\Throwable $error): void — called on main thread
34+
*/
35+
public static function sqlite(string $dbPath, string $sql, array $params = [], ?\Closure $onComplete = null, ?\Closure $onError = null): void {
36+
$task = new AsyncDatabaseTask('sqlite', ['database' => $dbPath], $sql, $params, $onComplete, $onError);
37+
Server::getInstance()->getAsyncPool()->submitTask($task);
38+
}
39+
40+
/**
41+
* Runs an async MySQL query.
42+
*
43+
* @param array $config Connection config: host, username, password, database
44+
* @param string $sql SQL query with ? placeholders
45+
* @param array $params Bound parameters
46+
* @param \Closure|null $onComplete fn(array $rows): void — called on main thread
47+
* @param \Closure|null $onError fn(\Throwable $error): void — called on main thread
48+
*/
49+
public static function mysql(array $config, string $sql, array $params = [], ?\Closure $onComplete = null, ?\Closure $onError = null): void {
50+
$task = new AsyncDatabaseTask('mysql', $config, $sql, $params, $onComplete, $onError);
51+
Server::getInstance()->getAsyncPool()->submitTask($task);
52+
}
53+
}
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace imperazim\db\cache;
6+
7+
use imperazim\db\Database;
8+
9+
/**
10+
* Transparent query cache layer for Database.
11+
*
12+
* Usage:
13+
* $cached = new CacheLayer($db, defaultTtl: 60);
14+
* $users = $cached->remember('users:all', 30, fn() => $db->select('users', '*'));
15+
* $cached->forget('users:all');
16+
* $cached->flush();
17+
*/
18+
final class CacheLayer {
19+
20+
/** @var array<string, array{data: mixed, expires: float}> */
21+
private array $cache = [];
22+
23+
/** @var array<string, string[]> table => [cache keys] for auto-invalidation */
24+
private array $tableKeys = [];
25+
26+
/**
27+
* @param Database $db Database instance
28+
* @param int $defaultTtl Default TTL in seconds
29+
*/
30+
public function __construct(
31+
private Database $db,
32+
private int $defaultTtl = 60
33+
) {}
34+
35+
/**
36+
* Gets a cached value or computes and caches it.
37+
*
38+
* @param string $key Cache key
39+
* @param int|null $ttl TTL in seconds (null = default)
40+
* @param \Closure $loader Callback that returns the data to cache
41+
* @return mixed Cached or freshly loaded data
42+
*/
43+
public function remember(string $key, ?int $ttl, \Closure $loader): mixed {
44+
if ($this->has($key)) {
45+
return $this->cache[$key]['data'];
46+
}
47+
48+
$data = $loader();
49+
$this->put($key, $data, $ttl);
50+
return $data;
51+
}
52+
53+
/**
54+
* Stores a value in the cache.
55+
*
56+
* @param string $key Cache key
57+
* @param mixed $data Data to cache
58+
* @param int|null $ttl TTL in seconds
59+
*/
60+
public function put(string $key, mixed $data, ?int $ttl = null): void {
61+
$this->cache[$key] = [
62+
'data' => $data,
63+
'expires' => microtime(true) + ($ttl ?? $this->defaultTtl),
64+
];
65+
}
66+
67+
/**
68+
* Checks if a non-expired cache entry exists.
69+
*
70+
* @param string $key Cache key
71+
* @return bool
72+
*/
73+
public function has(string $key): bool {
74+
if (!isset($this->cache[$key])) {
75+
return false;
76+
}
77+
if (microtime(true) > $this->cache[$key]['expires']) {
78+
unset($this->cache[$key]);
79+
return false;
80+
}
81+
return true;
82+
}
83+
84+
/**
85+
* Gets a cached value.
86+
*
87+
* @param string $key Cache key
88+
* @param mixed $default Default if not found
89+
* @return mixed
90+
*/
91+
public function get(string $key, mixed $default = null): mixed {
92+
return $this->has($key) ? $this->cache[$key]['data'] : $default;
93+
}
94+
95+
/**
96+
* Removes a cache entry.
97+
*
98+
* @param string $key Cache key
99+
*/
100+
public function forget(string $key): void {
101+
unset($this->cache[$key]);
102+
}
103+
104+
/**
105+
* Tags a cache key as belonging to a table (for auto-invalidation).
106+
*
107+
* @param string $table Table name
108+
* @param string $key Cache key
109+
*/
110+
public function tag(string $table, string $key): void {
111+
$this->tableKeys[$table][] = $key;
112+
}
113+
114+
/**
115+
* Cached select with auto table tagging.
116+
*
117+
* @param string $table Table name
118+
* @param string $columns Columns to select
119+
* @param array $filters Where conditions
120+
* @param int|null $ttl Cache TTL
121+
* @return array Result rows
122+
*/
123+
public function select(string $table, string $columns = '*', array $filters = [], ?int $ttl = null): array {
124+
$key = "select:{$table}:" . md5($columns . serialize($filters));
125+
$this->tag($table, $key);
126+
127+
return $this->remember($key, $ttl, fn() => $this->db->select($table, $columns, $filters));
128+
}
129+
130+
/**
131+
* Inserts a row and invalidates cache for the table.
132+
*
133+
* @param string $table Table name
134+
* @param array $data Row data
135+
*/
136+
public function insert(string $table, array $data): void {
137+
$this->db->insert($table, $data);
138+
$this->invalidateTable($table);
139+
}
140+
141+
/**
142+
* Updates rows and invalidates cache for the table.
143+
*
144+
* @param string $table Table name
145+
* @param string $column Column to update
146+
* @param mixed $value New value
147+
* @param array $filters Where conditions
148+
* @return bool
149+
*/
150+
public function update(string $table, string $column, mixed $value, array $filters = []): bool {
151+
$result = $this->db->update($table, $column, $value, $filters);
152+
$this->invalidateTable($table);
153+
return $result;
154+
}
155+
156+
/**
157+
* Deletes rows and invalidates cache for the table.
158+
*
159+
* @param string $table Table name
160+
* @param array $filters Where conditions
161+
* @return int
162+
*/
163+
public function delete(string $table, array $filters): int {
164+
$result = $this->db->delete($table, $filters);
165+
$this->invalidateTable($table);
166+
return $result;
167+
}
168+
169+
/**
170+
* Invalidates all cache entries tagged to a table.
171+
*
172+
* @param string $table Table name
173+
*/
174+
public function invalidateTable(string $table): void {
175+
foreach ($this->tableKeys[$table] ?? [] as $key) {
176+
unset($this->cache[$key]);
177+
}
178+
unset($this->tableKeys[$table]);
179+
}
180+
181+
/**
182+
* Clears all cached data.
183+
*/
184+
public function flush(): void {
185+
$this->cache = [];
186+
$this->tableKeys = [];
187+
}
188+
189+
/**
190+
* Returns cache statistics.
191+
*
192+
* @return array{entries: int, tables: int}
193+
*/
194+
public function stats(): array {
195+
$this->cleanup();
196+
return [
197+
'entries' => count($this->cache),
198+
'tables' => count($this->tableKeys),
199+
];
200+
}
201+
202+
/**
203+
* Removes expired entries.
204+
*/
205+
private function cleanup(): void {
206+
$now = microtime(true);
207+
foreach ($this->cache as $key => $entry) {
208+
if ($now > $entry['expires']) {
209+
unset($this->cache[$key]);
210+
}
211+
}
212+
}
213+
214+
/**
215+
* Returns the underlying database instance.
216+
*
217+
* @return Database
218+
*/
219+
public function getDatabase(): Database {
220+
return $this->db;
221+
}
222+
}

0 commit comments

Comments
 (0)