Skip to content

Commit 0fe5025

Browse files
laylatichygitbutler-client
authored andcommitted
fix(database): convert backtick identifiers to double quotes for PostgreSQL
Query::compile() stripped backtick quoting entirely for PostgreSQL, leaving identifiers unquoted. This breaks for reserved words like order, user, group, and type. Now converts backticks to double quotes, which is the correct PostgreSQL identifier quoting.
1 parent ba92595 commit 0fe5025

2 files changed

Lines changed: 178 additions & 1 deletion

File tree

packages/database/src/Query.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ public function compile(): ImmutableString
7676
}
7777

7878
if ($dialect === DatabaseDialect::POSTGRESQL) {
79-
$sql = str_replace('`', '', $sql);
79+
$sql = str_replace('`', '"', $sql);
8080
}
8181

8282
return new ImmutableString($sql);
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace Tempest\Database\Tests;
6+
7+
use PHPUnit\Framework\Attributes\Test;
8+
use PHPUnit\Framework\TestCase;
9+
use Tempest\Container\GenericContainer;
10+
use Tempest\Database\Config\MysqlConfig;
11+
use Tempest\Database\Config\PostgresConfig;
12+
use Tempest\Database\Config\SQLiteConfig;
13+
use Tempest\Database\Connection\PDOConnection;
14+
use Tempest\Database\Database;
15+
use Tempest\Database\GenericDatabase;
16+
use Tempest\Database\Query;
17+
use Tempest\Database\Transactions\GenericTransactionManager;
18+
use Tempest\EventBus\EventBusConfig;
19+
use Tempest\EventBus\GenericEventBus;
20+
use Tempest\Mapper\SerializerFactory;
21+
22+
final class QueryCompileTest extends TestCase
23+
{
24+
protected function tearDown(): void
25+
{
26+
GenericContainer::setInstance(instance: null);
27+
28+
parent::tearDown();
29+
}
30+
31+
private function createDatabaseWithMysqlDialect(): void
32+
{
33+
$config = new MysqlConfig();
34+
$connection = new PDOConnection(config: $config);
35+
36+
$database = new GenericDatabase(
37+
connection: $connection,
38+
transactionManager: new GenericTransactionManager(connection: $connection),
39+
serializerFactory: new SerializerFactory(container: new GenericContainer()),
40+
eventBus: new GenericEventBus(
41+
container: new GenericContainer(),
42+
eventBusConfig: new EventBusConfig(),
43+
),
44+
);
45+
46+
$container = new GenericContainer();
47+
$container->singleton(
48+
className: Database::class,
49+
definition: $database
50+
);
51+
GenericContainer::setInstance(instance: $container);
52+
}
53+
54+
private function createDatabaseWithPostgresDialect(): void
55+
{
56+
$config = new PostgresConfig();
57+
$connection = new PDOConnection(config: $config);
58+
59+
$database = new GenericDatabase(
60+
connection: $connection,
61+
transactionManager: new GenericTransactionManager(connection: $connection),
62+
serializerFactory: new SerializerFactory(container: new GenericContainer()),
63+
eventBus: new GenericEventBus(
64+
container: new GenericContainer(),
65+
eventBusConfig: new EventBusConfig(),
66+
),
67+
);
68+
69+
$container = new GenericContainer();
70+
$container->singleton(
71+
className: Database::class,
72+
definition: $database
73+
);
74+
GenericContainer::setInstance(instance: $container);
75+
}
76+
77+
private function createDatabaseWithSqliteDialect(): void
78+
{
79+
$config = new SQLiteConfig(path: ':memory:');
80+
$connection = new PDOConnection(config: $config);
81+
82+
$database = new GenericDatabase(
83+
connection: $connection,
84+
transactionManager: new GenericTransactionManager(connection: $connection),
85+
serializerFactory: new SerializerFactory(container: new GenericContainer()),
86+
eventBus: new GenericEventBus(
87+
container: new GenericContainer(),
88+
eventBusConfig: new EventBusConfig(),
89+
),
90+
);
91+
92+
$container = new GenericContainer();
93+
$container->singleton(
94+
className: Database::class,
95+
definition: $database
96+
);
97+
GenericContainer::setInstance(instance: $container);
98+
}
99+
100+
#[Test]
101+
public function postgresql_converts_backticks_to_double_quotes(): void
102+
{
103+
$this->createDatabaseWithPostgresDialect();
104+
105+
$query = new Query(sql: 'SELECT `name`, `email` FROM `users` WHERE `id` = ?');
106+
107+
$this->assertSame(
108+
'SELECT "name", "email" FROM "users" WHERE "id" = ?',
109+
$query->compile()->toString(),
110+
);
111+
}
112+
113+
#[Test]
114+
public function postgresql_handles_qualified_identifiers(): void
115+
{
116+
$this->createDatabaseWithPostgresDialect();
117+
118+
$query = new Query(sql: 'SELECT `users`.`name` FROM `users`');
119+
120+
$this->assertSame(
121+
'SELECT "users"."name" FROM "users"',
122+
$query->compile()->toString(),
123+
);
124+
}
125+
126+
#[Test]
127+
public function postgresql_handles_reserved_words(): void
128+
{
129+
$this->createDatabaseWithPostgresDialect();
130+
131+
$query = new Query(sql: 'SELECT `order`, `user`, `type` FROM `group`');
132+
133+
$this->assertSame(
134+
'SELECT "order", "user", "type" FROM "group"',
135+
$query->compile()->toString(),
136+
);
137+
}
138+
139+
#[Test]
140+
public function postgresql_passes_through_sql_without_backticks(): void
141+
{
142+
$this->createDatabaseWithPostgresDialect();
143+
144+
$query = new Query(sql: 'SELECT 1');
145+
146+
$this->assertSame(
147+
'SELECT 1',
148+
$query->compile()->toString()
149+
);
150+
}
151+
152+
#[Test]
153+
public function mysql_retains_backticks(): void
154+
{
155+
$this->createDatabaseWithMysqlDialect();
156+
157+
$query = new Query(sql: 'SELECT `name` FROM `users` WHERE `id` = ?');
158+
159+
$this->assertSame(
160+
'SELECT `name` FROM `users` WHERE `id` = ?',
161+
$query->compile()->toString(),
162+
);
163+
}
164+
165+
#[Test]
166+
public function sqlite_retains_backticks(): void
167+
{
168+
$this->createDatabaseWithSqliteDialect();
169+
170+
$query = new Query(sql: 'SELECT `name` FROM `users` WHERE `id` = ?');
171+
172+
$this->assertSame(
173+
'SELECT `name` FROM `users` WHERE `id` = ?',
174+
$query->compile()->toString(),
175+
);
176+
}
177+
}

0 commit comments

Comments
 (0)