Skip to content

Commit 45c490a

Browse files
committed
Progress on Postgre specific query builder
1 parent 0c581a5 commit 45c490a

File tree

5 files changed

+320
-18
lines changed

5 files changed

+320
-18
lines changed

config/pgsql/config.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
return [
4+
'connection' => [
5+
'host' => $_ENV['DB_HOST_PGSQL'] ?? $_ENV['DB_HOST'] ?? '127.0.0.1',
6+
'port' => (int) ($_ENV['DB_PORT_PGSQL'] ?? $_ENV['DB_PORT'] ?? 5432),
7+
'database' => $_ENV['DB_DATABASE_PGSQL'] ?? $_ENV['DB_DATABASE'] ?? 'postgres',
8+
'username' => $_ENV['DB_USERNAME_PGSQL'] ?? $_ENV['DB_USERNAME'] ?? 'postgres',
9+
'password' => $_ENV['DB_PASSWORD_PGSQL'] ?? $_ENV['DB_PASSWORD'] ?? '',
10+
'options' => [
11+
'connect_timeout' => 10,
12+
'application_name' => 'fiber-async-app',
13+
],
14+
],
15+
16+
'pool_size' => (int) ($_ENV['DB_POOL_SIZE'] ?? 20),
17+
];

src/Api/AsyncPostgreSQL.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,6 @@ private static function executeAsyncQuery(string $sql, array $params, string $re
227227

228228
try {
229229
if (! empty($params)) {
230-
// Use async version for parameterized queries
231230
if (! pg_send_query_params($connection, $sql, $params)) {
232231
throw new \RuntimeException('Failed to send parameterized query: '.pg_last_error($connection));
233232
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
<?php
2+
3+
namespace Rcalicdan\FiberAsync\Config;
4+
5+
use Dotenv\Dotenv;
6+
use Exception;
7+
8+
/**
9+
* A singleton configuration loader that automatically finds the project root.
10+
*
11+
* It searches upwards from its directory to locate the project root (identified
12+
* by a 'vendor' folder), loads the .env and config files from config/postgre,
13+
* and then caches the results to ensure the expensive search happens only once.
14+
*/
15+
final class PostgresConfigLoader
16+
{
17+
private static ?self $instance = null;
18+
private ?string $rootPath = null;
19+
20+
/** @var array<string, mixed> */
21+
private array $config = [];
22+
23+
/**
24+
* The constructor is private to enforce the singleton pattern.
25+
* It performs the entire one-time loading process.
26+
*/
27+
private function __construct()
28+
{
29+
$this->rootPath = $this->findProjectRoot();
30+
31+
if ($this->rootPath !== null) {
32+
$this->loadDotEnv();
33+
$this->loadConfigFiles();
34+
}
35+
}
36+
37+
/**
38+
* Gets the singleton instance of the loader.
39+
*/
40+
public static function getInstance(): self
41+
{
42+
if (self::$instance === null) {
43+
self::$instance = new self;
44+
}
45+
46+
return self::$instance;
47+
}
48+
49+
/**
50+
* Resets the singleton instance, primarily for testing.
51+
*/
52+
public static function reset(): void
53+
{
54+
self::$instance = null;
55+
}
56+
57+
/**
58+
* Retrieves a configuration array by its key (the filename).
59+
* e.g., get('database') loads and returns config/pgsql/database.php
60+
*
61+
* @param mixed $default
62+
* @return mixed
63+
*/
64+
public function get(string $key, $default = null)
65+
{
66+
return $this->config[$key] ?? $default;
67+
}
68+
69+
/**
70+
* Searches upwards from the current directory to find the project root.
71+
* The root is identified by the presence of a `vendor` directory.
72+
* This operation is memoized (cached) for performance.
73+
*/
74+
private function findProjectRoot(): ?string
75+
{
76+
$dir = __DIR__;
77+
for ($i = 0; $i < 10; $i++) {
78+
if (is_dir($dir . '/vendor')) {
79+
return $dir;
80+
}
81+
82+
$parentDir = dirname($dir);
83+
if ($parentDir === $dir) {
84+
return null;
85+
}
86+
$dir = $parentDir;
87+
}
88+
89+
return null;
90+
}
91+
92+
private function loadDotEnv(): void
93+
{
94+
if ($this->rootPath === null) {
95+
throw new Exception('Root path not found, cannot load .env file');
96+
}
97+
98+
$envFile = $this->rootPath . '/.env';
99+
100+
if (file_exists($envFile)) {
101+
file_get_contents($envFile);
102+
103+
try {
104+
$dotenv = Dotenv::createImmutable($this->rootPath);
105+
$dotenv->load();
106+
} catch (\Throwable $e) {
107+
throw new Exception("Error loading .env file: {$e->getMessage()}");
108+
}
109+
} else {
110+
throw new Exception("Env file not found at: $envFile");
111+
}
112+
}
113+
114+
/**
115+
* Loads all .php files from the project root's /config/postgre directory.
116+
*/
117+
private function loadConfigFiles(): void
118+
{
119+
if ($this->rootPath === null) {
120+
throw new Exception('Root path not found, cannot load config files');
121+
}
122+
123+
$configDir = $this->rootPath . '/config/pgsql';
124+
if (is_dir($configDir)) {
125+
$files = glob($configDir . '/*.php');
126+
if ($files !== false) {
127+
foreach ($files as $file) {
128+
$key = basename($file, '.php');
129+
$this->config[$key] = require $file;
130+
}
131+
}
132+
}
133+
}
134+
}

src/PostgreSQL/DB.php

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
<?php
2+
3+
namespace Rcalicdan\FiberAsync\PostgreSQL;
4+
5+
use Rcalicdan\FiberAsync\Api\AsyncPostgreSQL;
6+
use Rcalicdan\FiberAsync\Config\PostgresConfigLoader;
7+
use Rcalicdan\FiberAsync\Promise\Interfaces\PromiseInterface;
8+
use Rcalicdan\FiberAsync\QueryBuilder\PostgresQueryBuilder;
9+
10+
/**
11+
* DB API - Main entry point for auto-configured async database operations using AsyncPostgreSQL under the hood
12+
* with asynchronous query builder support.
13+
*
14+
* This API automatically loads configuration from .env and config/pgsql/config.php
15+
* the first time it is used, providing a zero-setup experience for the developer.
16+
*/
17+
class DB
18+
{
19+
private static bool $isInitialized = false;
20+
private static bool $hasValidationError = false;
21+
22+
/**
23+
* The core of the new design: A private, self-configuring initializer.
24+
* This method is called by every public method to ensure the system is ready.
25+
* Validates only once if successful, but re-validates if there were previous errors.
26+
*/
27+
private static function initializeIfNeeded(): void
28+
{
29+
if (self::$isInitialized && !self::$hasValidationError) {
30+
return;
31+
}
32+
33+
self::$hasValidationError = false;
34+
35+
try {
36+
$configLoader = PostgresConfigLoader::getInstance();
37+
$dbConfig = $configLoader->get('config');
38+
39+
if (!is_array($dbConfig)) {
40+
throw new \RuntimeException("Postgres configuration not found. Ensure 'config/pgsql/config.php' exists in your project root.");
41+
}
42+
43+
$connectionConfig = $dbConfig['connection'] ?? null;
44+
if (!is_array($connectionConfig)) {
45+
throw new \RuntimeException('Postgres connection configuration must be an array.');
46+
}
47+
48+
$required = ['host', 'database', 'username'];
49+
foreach ($required as $key) {
50+
if (!isset($connectionConfig[$key])) {
51+
throw new \RuntimeException("Missing required Postgres connection parameter: {$key}");
52+
}
53+
}
54+
55+
/** @var array<string, mixed> $validatedConfig */
56+
$validatedConfig = [];
57+
foreach ($connectionConfig as $key => $value) {
58+
if (!is_string($key)) {
59+
throw new \RuntimeException('Postgres connection configuration must have string keys only.');
60+
}
61+
$validatedConfig[$key] = $value;
62+
}
63+
64+
$poolSize = $dbConfig['pool_size'] ?? 10;
65+
if (!is_int($poolSize) || $poolSize < 1) {
66+
throw new \RuntimeException('Postgres pool size must be a positive integer.');
67+
}
68+
69+
AsyncPostgreSQL::init($validatedConfig, $poolSize);
70+
self::$isInitialized = true;
71+
72+
} catch (\Exception $e) {
73+
self::$hasValidationError = true;
74+
self::$isInitialized = false;
75+
76+
throw $e;
77+
}
78+
}
79+
80+
/**
81+
* Resets the entire database system. Crucial for isolated testing.
82+
*/
83+
public static function reset(): void
84+
{
85+
AsyncPostgreSQL::reset();
86+
PostgresConfigLoader::reset();
87+
self::$isInitialized = false;
88+
self::$hasValidationError = false;
89+
}
90+
91+
/**
92+
* Start a new query builder instance for the given table.
93+
*/
94+
public static function table(string $table): PostgresQueryBuilder
95+
{
96+
self::initializeIfNeeded();
97+
98+
return new PostgresQueryBuilder($table);
99+
}
100+
101+
/**
102+
* Execute a raw query.
103+
*
104+
* @param array<string, mixed> $bindings
105+
* @return PromiseInterface<array<int, array<string, mixed>>>
106+
*/
107+
public static function raw(string $sql, array $bindings = []): PromiseInterface
108+
{
109+
self::initializeIfNeeded();
110+
111+
return AsyncPostgreSQL::query($sql, $bindings);
112+
}
113+
114+
/**
115+
* Execute a raw query and return the first result.
116+
*
117+
* @param array<string, mixed> $bindings
118+
* @return PromiseInterface<array<string, mixed>|false>
119+
*/
120+
public static function rawFirst(string $sql, array $bindings = []): PromiseInterface
121+
{
122+
self::initializeIfNeeded();
123+
124+
return AsyncPostgreSQL::fetchOne($sql, $bindings);
125+
}
126+
127+
/**
128+
* Execute a raw query and return a single scalar value.
129+
*
130+
* @param array<string, mixed> $bindings
131+
* @return PromiseInterface<mixed>
132+
*/
133+
public static function rawValue(string $sql, array $bindings = []): PromiseInterface
134+
{
135+
self::initializeIfNeeded();
136+
137+
return AsyncPostgreSQL::fetchValue($sql, $bindings);
138+
}
139+
140+
/**
141+
* Execute a raw statement (INSERT, UPDATE, DELETE).
142+
*
143+
* @param array<string, mixed> $bindings
144+
* @return PromiseInterface<int>
145+
*/
146+
public static function rawExecute(string $sql, array $bindings = []): PromiseInterface
147+
{
148+
self::initializeIfNeeded();
149+
150+
return AsyncPostgreSQL::execute($sql, $bindings);
151+
}
152+
153+
/**
154+
* Run a database transaction.
155+
*
156+
* @return PromiseInterface<mixed>
157+
*/
158+
public static function transaction(callable $callback): PromiseInterface
159+
{
160+
self::initializeIfNeeded();
161+
162+
return AsyncPostgreSQL::transaction($callback);
163+
}
164+
}

test.php

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,12 @@
11
<?php
22

33
use Rcalicdan\FiberAsync\Benchmark\BenchmarkRunner;
4-
use Rcalicdan\FiberAsync\MySQLi\DB;
4+
use Rcalicdan\FiberAsync\PostgreSQL\DB;
55
use Rcalicdan\FiberAsync\Promise\Promise;
66

77
require 'vendor/autoload.php';
88

9-
for ($i = 1; $i <= 5; $i++) {
10-
echo "Round $i\n";
11-
$start = microtime(true);
12-
$task = run(function () {
13-
$queries = Promise::all([
14-
DB::rawExecute("SELECT SLEEP(1)"),
15-
DB::rawExecute("SELECT SLEEP(2)"),
16-
DB::rawExecute("SELECT SLEEP(3)"),
17-
]);
18-
19-
await($queries);
20-
});
21-
$endTime = microtime(true);
22-
$executionTime = $endTime - $start;
23-
echo "Execution time: " . $executionTime . " seconds\n";
24-
}
9+
run(function (){
10+
$results = await(DB::table('users')->first());
11+
print_r($results);
12+
});

0 commit comments

Comments
 (0)