Skip to content

Commit 4c8d30e

Browse files
committed
Fix Postgre Query Builder Bindinngs
1 parent 45c490a commit 4c8d30e

File tree

3 files changed

+272
-30
lines changed

3 files changed

+272
-30
lines changed

src/QueryBuilder/PostgresQueryBuilder.php

Lines changed: 23 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,14 @@
66
use Rcalicdan\FiberAsync\Api\AsyncPostgreSQL;
77
use Rcalicdan\FiberAsync\Api\Promise;
88
use Rcalicdan\FiberAsync\Promise\Interfaces\PromiseInterface;
9-
use Rcalicdan\FiberAsync\QueryBuilder\Traits\QueryBuilderCoreTrait;
10-
use Rcalicdan\FiberAsync\QueryBuilder\Traits\QueryConditionsTrait;
11-
use Rcalicdan\FiberAsync\QueryBuilder\Traits\QueryJoinTrait;
12-
use Rcalicdan\FiberAsync\QueryBuilder\Traits\QueryGroupingTrait;
13-
use Rcalicdan\FiberAsync\QueryBuilder\Traits\QueryAdvancedConditionsTrait;
14-
use Rcalicdan\FiberAsync\QueryBuilder\Traits\QueryDebugTrait;
15-
use Rcalicdan\FiberAsync\QueryBuilder\Traits\SqlBuilderTrait;
169

17-
18-
class PostgresQueryBuilder
10+
/**
11+
* PostgreSQL query builder with async execution methods.
12+
*/
13+
class PostgresQueryBuilder extends PostgresQueryBuilderBase
1914
{
20-
use QueryBuilderCoreTrait,
21-
QueryConditionsTrait,
22-
QueryJoinTrait,
23-
QueryGroupingTrait,
24-
QueryAdvancedConditionsTrait,
25-
SqlBuilderTrait,
26-
QueryDebugTrait;
27-
2815
/**
29-
* Create a new AsyncQueryBuilder instance.
16+
* Create a new PostgresQueryBuilder instance.
3017
*
3118
* @param string $table The table name to query.
3219
*/
@@ -45,7 +32,6 @@ final public function __construct(string $table = '')
4532
public function get(): PromiseInterface
4633
{
4734
$sql = $this->buildSelectQuery();
48-
4935
return AsyncPostgreSQL::query($sql, $this->getCompiledBindings());
5036
}
5137

@@ -56,10 +42,8 @@ public function get(): PromiseInterface
5642
*/
5743
public function first(): PromiseInterface
5844
{
59-
// A new instance with a limit is created for this specific query execution
6045
$instanceWithLimit = $this->limit(1);
6146
$sql = $instanceWithLimit->buildSelectQuery();
62-
6347
return AsyncPostgreSQL::fetchOne($sql, $instanceWithLimit->getCompiledBindings());
6448
}
6549

@@ -91,10 +75,8 @@ public function findOrFail(mixed $id, string $column = 'id'): PromiseInterface
9175
$result = await($this->find($id, $column));
9276
if ($result === null || $result === false) {
9377
$idString = is_scalar($id) ? (string) $id : 'complex_type';
94-
9578
throw new \RuntimeException("Record not found with {$column} = {$idString}");
9679
}
97-
9880
return $result;
9981
})();
10082
}
@@ -110,7 +92,6 @@ public function value(string $column): PromiseInterface
11092
// @phpstan-ignore-next-line
11193
return Async::async(function () use ($column): mixed {
11294
$result = await($this->select($column)->first());
113-
11495
return ($result !== false && isset($result[$column])) ? $result[$column] : null;
11596
})();
11697
}
@@ -126,7 +107,6 @@ public function count(string $column = '*'): PromiseInterface
126107
$sql = $this->buildCountQuery($column);
127108
/** @var PromiseInterface<int> */
128109
$promise = AsyncPostgreSQL::fetchValue($sql, $this->getCompiledBindings());
129-
130110
return $promise;
131111
}
132112

@@ -140,7 +120,6 @@ public function exists(): PromiseInterface
140120
// @phpstan-ignore-next-line
141121
return Async::async(function (): bool {
142122
$count = await($this->count());
143-
144123
return $count > 0;
145124
})();
146125
}
@@ -157,10 +136,26 @@ public function insert(array $data): PromiseInterface
157136
return Promise::resolve(0);
158137
}
159138
$sql = $this->buildInsertQuery($data);
160-
161139
return AsyncPostgreSQL::execute($sql, array_values($data));
162140
}
163141

142+
/**
143+
* Insert a single record and return the inserted ID.
144+
*
145+
* @param array<string, mixed> $data The data to insert as column => value pairs.
146+
* @param string $idColumn The name of the ID column to return.
147+
* @return PromiseInterface<mixed> A promise that resolves to the inserted ID value.
148+
*/
149+
public function insertGetId(array $data, string $idColumn = 'id'): PromiseInterface
150+
{
151+
if ($data === []) {
152+
return Promise::resolve(null);
153+
}
154+
155+
$sql = $this->buildInsertReturningQuery($data, $idColumn);
156+
return AsyncPostgreSQL::fetchValue($sql, array_values($data));
157+
}
158+
164159
/**
165160
* Insert multiple records in a batch operation.
166161
*
@@ -221,7 +216,6 @@ public function update(array $data): PromiseInterface
221216
public function delete(): PromiseInterface
222217
{
223218
$sql = $this->buildDeleteQuery();
224-
225219
return AsyncPostgreSQL::execute($sql, $this->getCompiledBindings());
226220
}
227-
}
221+
}
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
<?php
2+
3+
namespace Rcalicdan\FiberAsync\QueryBuilder;
4+
5+
use Rcalicdan\FiberAsync\QueryBuilder\Traits\QueryBuilderCoreTrait;
6+
use Rcalicdan\FiberAsync\QueryBuilder\Traits\QueryConditionsTrait;
7+
use Rcalicdan\FiberAsync\QueryBuilder\Traits\QueryJoinTrait;
8+
use Rcalicdan\FiberAsync\QueryBuilder\Traits\QueryGroupingTrait;
9+
use Rcalicdan\FiberAsync\QueryBuilder\Traits\QueryAdvancedConditionsTrait;
10+
use Rcalicdan\FiberAsync\QueryBuilder\Traits\QueryDebugTrait;
11+
use Rcalicdan\FiberAsync\QueryBuilder\Traits\SqlBuilderTrait;
12+
13+
/**
14+
* Abstract PostgreSQL query builder base with optimizations.
15+
*/
16+
abstract class PostgresQueryBuilderBase
17+
{
18+
use QueryBuilderCoreTrait,
19+
QueryConditionsTrait,
20+
QueryJoinTrait,
21+
QueryGroupingTrait,
22+
QueryAdvancedConditionsTrait,
23+
SqlBuilderTrait,
24+
QueryDebugTrait;
25+
26+
/**
27+
* @var int Parameter counter for PostgreSQL numbered parameters
28+
*/
29+
protected int $parameterCount = 0;
30+
31+
/**
32+
* Generate PostgreSQL parameter placeholder ($1, $2, $3, etc.)
33+
*/
34+
protected function getPlaceholder(): string
35+
{
36+
return '$' . (++$this->parameterCount);
37+
}
38+
39+
/**
40+
* Reset parameter counter for new query
41+
*/
42+
protected function resetParameterCount(): void
43+
{
44+
$this->parameterCount = 0;
45+
}
46+
47+
/**
48+
* PostgreSQL ILIKE for case-insensitive matching
49+
*/
50+
public function ilike(string $column, string $value, string $side = 'both'): static
51+
{
52+
$instance = clone $this;
53+
$placeholder = $instance->getPlaceholder();
54+
$instance->where[] = "{$column} ILIKE {$placeholder}";
55+
56+
$likeValue = match ($side) {
57+
'before' => "%{$value}",
58+
'after' => "{$value}%",
59+
'both' => "%{$value}%",
60+
default => $value
61+
};
62+
63+
$instance->bindings['where'][] = $likeValue;
64+
return $instance;
65+
}
66+
67+
/**
68+
* PostgreSQL JSON field access
69+
*/
70+
public function whereJson(string $column, string $path, mixed $value): static
71+
{
72+
$instance = clone $this;
73+
$placeholder = $instance->getPlaceholder();
74+
$instance->where[] = "{$column}->'{$path}' = {$placeholder}";
75+
$instance->bindings['where'][] = $value;
76+
return $instance;
77+
}
78+
79+
/**
80+
* Override buildSelectQuery to reset parameter count and optimize for PostgreSQL
81+
*/
82+
protected function buildSelectQuery(): string
83+
{
84+
$this->resetParameterCount();
85+
86+
$sql = 'SELECT ' . implode(', ', $this->select);
87+
$sql .= ' FROM ' . $this->table;
88+
89+
foreach ($this->joins as $join) {
90+
if ($join['type'] === 'CROSS') {
91+
$sql .= " CROSS JOIN {$join['table']}";
92+
} else {
93+
$sql .= " {$join['type']} JOIN {$join['table']} ON {$join['condition']}";
94+
}
95+
}
96+
97+
$whereSql = $this->buildWhereClause();
98+
if ($whereSql !== '') {
99+
$sql .= ' WHERE ' . $whereSql;
100+
}
101+
102+
if ($this->groupBy !== []) {
103+
$sql .= ' GROUP BY ' . implode(', ', $this->groupBy);
104+
}
105+
106+
if ($this->having !== []) {
107+
$sql .= ' HAVING ' . implode(' AND ', $this->having);
108+
}
109+
110+
if ($this->orderBy !== []) {
111+
$sql .= ' ORDER BY ' . implode(', ', $this->orderBy);
112+
}
113+
114+
if ($this->limit !== null) {
115+
$sql .= ' LIMIT ' . $this->limit;
116+
if ($this->offset !== null) {
117+
$sql .= ' OFFSET ' . $this->offset;
118+
}
119+
}
120+
121+
return $sql;
122+
}
123+
124+
/**
125+
* Override buildCountQuery for PostgreSQL
126+
*/
127+
protected function buildCountQuery(string $column = '*'): string
128+
{
129+
$this->resetParameterCount();
130+
131+
$sql = "SELECT COUNT({$column}) FROM " . $this->table;
132+
133+
foreach ($this->joins as $join) {
134+
if ($join['type'] === 'CROSS') {
135+
$sql .= " CROSS JOIN {$join['table']}";
136+
} else {
137+
$sql .= " {$join['type']} JOIN {$join['table']} ON {$join['condition']}";
138+
}
139+
}
140+
141+
$whereSql = $this->buildWhereClause();
142+
if ($whereSql !== '') {
143+
$sql .= ' WHERE ' . $whereSql;
144+
}
145+
146+
if ($this->groupBy !== []) {
147+
$sql .= ' GROUP BY ' . implode(', ', $this->groupBy);
148+
}
149+
150+
if ($this->having !== []) {
151+
$sql .= ' HAVING ' . implode(' AND ', $this->having);
152+
}
153+
154+
return $sql;
155+
}
156+
157+
/**
158+
* Override buildInsertQuery for PostgreSQL
159+
*/
160+
protected function buildInsertQuery(array $data): string
161+
{
162+
$this->resetParameterCount();
163+
164+
$columns = implode(', ', array_keys($data));
165+
$placeholders = [];
166+
167+
foreach ($data as $value) {
168+
$placeholders[] = $this->getPlaceholder();
169+
}
170+
171+
$placeholderString = implode(', ', $placeholders);
172+
return "INSERT INTO {$this->table} ({$columns}) VALUES ({$placeholderString})";
173+
}
174+
175+
/**
176+
* Override buildInsertBatchQuery for PostgreSQL
177+
*/
178+
protected function buildInsertBatchQuery(array $data): string
179+
{
180+
$this->resetParameterCount();
181+
182+
if (empty($data) || !is_array($data[0])) {
183+
throw new \InvalidArgumentException('Invalid data format for batch insert');
184+
}
185+
186+
$firstRow = $data[0];
187+
$columns = implode(', ', array_keys($firstRow));
188+
189+
$valueGroups = [];
190+
foreach ($data as $row) {
191+
$rowPlaceholders = [];
192+
foreach ($row as $value) {
193+
$rowPlaceholders[] = $this->getPlaceholder();
194+
}
195+
$valueGroups[] = '(' . implode(', ', $rowPlaceholders) . ')';
196+
}
197+
198+
$allPlaceholders = implode(', ', $valueGroups);
199+
return "INSERT INTO {$this->table} ({$columns}) VALUES {$allPlaceholders}";
200+
}
201+
202+
/**
203+
* Override buildUpdateQuery for PostgreSQL
204+
*/
205+
protected function buildUpdateQuery(array $data): string
206+
{
207+
$this->resetParameterCount();
208+
209+
$setClauses = [];
210+
foreach (array_keys($data) as $column) {
211+
$setClauses[] = "{$column} = " . $this->getPlaceholder();
212+
}
213+
214+
$sql = "UPDATE {$this->table} SET " . implode(', ', $setClauses);
215+
216+
$whereSql = $this->buildWhereClause();
217+
if ($whereSql !== '') {
218+
$sql .= ' WHERE ' . $whereSql;
219+
}
220+
221+
return $sql;
222+
}
223+
224+
/**
225+
* Override buildDeleteQuery for PostgreSQL
226+
*/
227+
protected function buildDeleteQuery(): string
228+
{
229+
$this->resetParameterCount();
230+
231+
$sql = "DELETE FROM {$this->table}";
232+
233+
$whereSql = $this->buildWhereClause();
234+
if ($whereSql !== '') {
235+
$sql .= ' WHERE ' . $whereSql;
236+
}
237+
238+
return $sql;
239+
}
240+
241+
/**
242+
* Build INSERT with RETURNING clause for PostgreSQL
243+
*/
244+
protected function buildInsertReturningQuery(array $data, string $returning = 'id'): string
245+
{
246+
return $this->buildInsertQuery($data) . " RETURNING {$returning}";
247+
}
248+
}

test.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@
77
require 'vendor/autoload.php';
88

99
run(function (){
10-
$results = await(DB::table('users')->first());
10+
$results = await(DB::table('users')->where('id', 2)->first());
1111
print_r($results);
1212
});

0 commit comments

Comments
 (0)