Skip to content

Commit 3f2c466

Browse files
authored
feat: pass parameters natively via http interface along the query (#224)
1 parent 2cbdc98 commit 3f2c466

25 files changed

+773
-60
lines changed

README.md

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Naming used here is the same as in ClickHouse docs.
1515
- Works with any HTTP Client implementation ([PSR-18 compliant](https://www.php-fig.org/psr/psr-18/))
1616
- All [ClickHouse Formats](https://clickhouse.yandex/docs/en/interfaces/formats/) support
1717
- Logging ([PSR-3 compliant](https://www.php-fig.org/psr/psr-3/))
18-
- SQL Factory for [parameters "binding"](#parameters-binding)
18+
- [Native query parameters](#native-query-parameters) support
1919

2020
## Contents
2121

@@ -29,7 +29,7 @@ Naming used here is the same as in ClickHouse docs.
2929
- [Insert](#insert)
3030
- [Async API](#async-api)
3131
- [Select](#select-1)
32-
- [Parameters "binding"](#parameters-binding)
32+
- [Native Query Parameters](#native-query-parameters)
3333
- [Snippets](#snippets)
3434

3535
## Setup
@@ -227,7 +227,10 @@ If not provided they're not passed either:
227227

228228
### Select
229229

230-
## Parameters "binding"
230+
## Native Query Parameters
231+
232+
> [!TIP]
233+
> [Official docs](https://clickhouse.com/docs/en/interfaces/http#cli-queries-with-parameters)
231234

232235
```php
233236
<?php
@@ -238,17 +241,14 @@ use SimPod\ClickHouseClient\Sql\ValueFormatter;
238241
$sqlFactory = new SqlFactory(new ValueFormatter());
239242
240243
$sql = $sqlFactory->createWithParameters(
241-
'SELECT :param',
244+
'SELECT {p1:String}',
242245
['param' => 'value']
243246
);
244247
```
245-
This produces `SELECT 'value'` and it can be passed to `ClickHouseClient::select()`.
248+
This produces `SELECT 'value'` in ClickHouse and it can be passed to `ClickHouseClient::select()`.
246249

247-
Supported types are:
248-
- scalars
249-
- DateTimeImmutable (`\DateTime` is not supported because `ValueFormatter` might modify its timezone so it's not considered safe)
250-
- [Expression](#expression)
251-
- objects implementing `__toString()`
250+
All types are supported (except `AggregateFunction`, `SimpleAggregateFunction` and `Nothing` by design).
251+
You can also pass `DateTimeInterface` into `Date*` types or native array into `Array`, `Tuple`, `Native` and `Geo` types
252252

253253
### Expression
254254

src/Client/ClickHouseClient.php

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
use Psr\Http\Client\ClientExceptionInterface;
88
use SimPod\ClickHouseClient\Exception\CannotInsert;
99
use SimPod\ClickHouseClient\Exception\ServerError;
10-
use SimPod\ClickHouseClient\Exception\UnsupportedValue;
10+
use SimPod\ClickHouseClient\Exception\UnsupportedParamType;
11+
use SimPod\ClickHouseClient\Exception\UnsupportedParamValue;
1112
use SimPod\ClickHouseClient\Format\Format;
1213
use SimPod\ClickHouseClient\Output\Output;
1314

@@ -27,7 +28,8 @@ public function executeQuery(string $query, array $settings = []): void;
2728
*
2829
* @throws ClientExceptionInterface
2930
* @throws ServerError
30-
* @throws UnsupportedValue
31+
* @throws UnsupportedParamType
32+
* @throws UnsupportedParamValue
3133
*/
3234
public function executeQueryWithParams(string $query, array $params, array $settings = []): void;
3335

@@ -53,7 +55,8 @@ public function select(string $query, Format $outputFormat, array $settings = []
5355
*
5456
* @throws ClientExceptionInterface
5557
* @throws ServerError
56-
* @throws UnsupportedValue
58+
* @throws UnsupportedParamType
59+
* @throws UnsupportedParamValue
5760
*
5861
* @template O of Output
5962
*/

src/Client/Http/RequestFactory.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,15 @@
1010
use Psr\Http\Message\RequestInterface;
1111
use Psr\Http\Message\UriFactoryInterface;
1212
use Psr\Http\Message\UriInterface;
13+
use SimPod\ClickHouseClient\Exception\UnsupportedParamType;
14+
use SimPod\ClickHouseClient\Param\ParamValueConverterRegistry;
15+
use SimPod\ClickHouseClient\Sql\Type;
1316

17+
use function array_keys;
18+
use function array_reduce;
1419
use function http_build_query;
1520
use function is_string;
21+
use function preg_match_all;
1622
use function SimPod\ClickHouseClient\absurd;
1723

1824
use const PHP_QUERY_RFC3986;
@@ -23,6 +29,7 @@ final class RequestFactory
2329

2430
/** @throws InvalidArgumentException */
2531
public function __construct(
32+
private ParamValueConverterRegistry $paramValueConverterRegistry,
2633
private RequestFactoryInterface $requestFactory,
2734
UriFactoryInterface|null $uriFactory = null,
2835
UriInterface|string $uri = '',
@@ -40,6 +47,7 @@ public function __construct(
4047
$this->uri = $uri;
4148
}
4249

50+
/** @throws UnsupportedParamType */
4351
public function prepareRequest(RequestOptions $requestOptions): RequestInterface
4452
{
4553
$query = http_build_query(
@@ -62,7 +70,30 @@ public function prepareRequest(RequestOptions $requestOptions): RequestInterface
6270

6371
$request = $this->requestFactory->createRequest('POST', $uri);
6472

73+
preg_match_all('~\{([a-zA-Z\d]+):([a-zA-Z\d ]+(\(.+\))?)}~', $requestOptions->sql, $matches);
74+
75+
$typeToParam = array_reduce(
76+
array_keys($matches[1]),
77+
static function (array $acc, string|int $k) use ($matches) {
78+
$acc[$matches[1][$k]] = Type::fromString($matches[2][$k]);
79+
80+
return $acc;
81+
},
82+
[],
83+
);
84+
6585
$streamElements = [['name' => 'query', 'contents' => $requestOptions->sql]];
86+
foreach ($requestOptions->params as $name => $value) {
87+
$type = $typeToParam[$name] ?? null;
88+
if ($type === null) {
89+
continue;
90+
}
91+
92+
$streamElements[] = [
93+
'name' => 'param_' . $name,
94+
'contents' => $this->paramValueConverterRegistry->get($type)($value, $type, false),
95+
];
96+
}
6697

6798
try {
6899
$body = new MultipartStream($streamElements);

src/Client/Http/RequestOptions.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,16 @@ final class RequestOptions
1010
public array $settings;
1111

1212
/**
13+
* @param array<string, mixed> $params
1314
* @param array<string, float|int|string> $defaultSettings
1415
* @param array<string, float|int|string> $querySettings
1516
*/
16-
public function __construct(public string $sql, array $defaultSettings, array $querySettings)
17-
{
17+
public function __construct(
18+
public string $sql,
19+
public array $params,
20+
array $defaultSettings,
21+
array $querySettings,
22+
) {
1823
$this->settings = $querySettings + $defaultSettings;
1924
}
2025
}

src/Client/PsrClickHouseAsyncClient.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,25 +62,31 @@ public function selectWithParams(
6262
$sql
6363
$formatClause
6464
CLICKHOUSE,
65-
$settings,
66-
static fn (ResponseInterface $response): Output => $outputFormat::output($response->getBody()->__toString())
65+
params: $params,
66+
settings: $settings,
67+
processResponse: static fn (ResponseInterface $response): Output => $outputFormat::output(
68+
$response->getBody()->__toString(),
69+
)
6770
);
6871
}
6972

7073
/**
74+
* @param array<string, mixed> $params
7175
* @param array<string, float|int|string> $settings
7276
* @param (callable(ResponseInterface):mixed)|null $processResponse
7377
*
7478
* @throws Exception
7579
*/
7680
private function executeRequest(
7781
string $sql,
82+
array $params,
7883
array $settings = [],
7984
callable|null $processResponse = null,
8085
): PromiseInterface {
8186
$request = $this->requestFactory->prepareRequest(
8287
new RequestOptions(
8388
$sql,
89+
$params,
8490
$this->defaultSettings,
8591
$settings,
8692
),

src/Client/PsrClickHouseClient.php

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
use SimPod\ClickHouseClient\Client\Http\RequestOptions;
1313
use SimPod\ClickHouseClient\Exception\CannotInsert;
1414
use SimPod\ClickHouseClient\Exception\ServerError;
15-
use SimPod\ClickHouseClient\Exception\UnsupportedValue;
15+
use SimPod\ClickHouseClient\Exception\UnsupportedParamType;
16+
use SimPod\ClickHouseClient\Exception\UnsupportedParamValue;
1617
use SimPod\ClickHouseClient\Format\Format;
1718
use SimPod\ClickHouseClient\Output\Output;
1819
use SimPod\ClickHouseClient\Sql\Escaper;
@@ -46,13 +47,18 @@ public function __construct(
4647

4748
public function executeQuery(string $query, array $settings = []): void
4849
{
49-
$this->executeRequest($query, settings: $settings);
50+
try {
51+
$this->executeRequest($query, params: [], settings: $settings);
52+
} catch (UnsupportedParamType) {
53+
absurd();
54+
}
5055
}
5156

5257
public function executeQueryWithParams(string $query, array $params, array $settings = []): void
5358
{
5459
$this->executeRequest(
5560
$this->sqlFactory->createWithParameters($query, $params),
61+
params: $params,
5662
settings: $settings,
5763
);
5864
}
@@ -61,7 +67,7 @@ public function select(string $query, Format $outputFormat, array $settings = []
6167
{
6268
try {
6369
return $this->selectWithParams($query, params: [], outputFormat: $outputFormat, settings: $settings);
64-
} catch (UnsupportedValue) {
70+
} catch (UnsupportedParamValue | UnsupportedParamType) {
6571
absurd();
6672
}
6773
}
@@ -77,6 +83,7 @@ public function selectWithParams(string $query, array $params, Format $outputFor
7783
$sql
7884
$formatClause
7985
CLICKHOUSE,
86+
params: $params,
8087
settings: $settings,
8188
);
8289

@@ -112,14 +119,19 @@ public function insert(string $table, array $values, array|null $columns = null,
112119

113120
$table = Escaper::quoteIdentifier($table);
114121

115-
$this->executeRequest(
116-
<<<CLICKHOUSE
117-
INSERT INTO $table
118-
$columnsSql
119-
VALUES $valuesSql
120-
CLICKHOUSE,
121-
settings: $settings,
122-
);
122+
try {
123+
$this->executeRequest(
124+
<<<CLICKHOUSE
125+
INSERT INTO $table
126+
$columnsSql
127+
VALUES $valuesSql
128+
CLICKHOUSE,
129+
params: [],
130+
settings: $settings,
131+
);
132+
} catch (UnsupportedParamType) {
133+
absurd();
134+
}
123135
}
124136

125137
public function insertWithFormat(string $table, Format $inputFormat, string $data, array $settings = []): void
@@ -128,25 +140,33 @@ public function insertWithFormat(string $table, Format $inputFormat, string $dat
128140

129141
$table = Escaper::quoteIdentifier($table);
130142

131-
$this->executeRequest(
132-
<<<CLICKHOUSE
133-
INSERT INTO $table $formatSql $data
134-
CLICKHOUSE,
135-
settings: $settings,
136-
);
143+
try {
144+
$this->executeRequest(
145+
<<<CLICKHOUSE
146+
INSERT INTO $table $formatSql $data
147+
CLICKHOUSE,
148+
params: [],
149+
settings: $settings,
150+
);
151+
} catch (UnsupportedParamType) {
152+
absurd();
153+
}
137154
}
138155

139156
/**
157+
* @param array<string, mixed> $params
140158
* @param array<string, float|int|string> $settings
141159
*
142160
* @throws ServerError
143161
* @throws ClientExceptionInterface
162+
* @throws UnsupportedParamType
144163
*/
145-
private function executeRequest(string $sql, array $settings): ResponseInterface
164+
private function executeRequest(string $sql, array $params, array $settings): ResponseInterface
146165
{
147166
$request = $this->requestFactory->prepareRequest(
148167
new RequestOptions(
149168
$sql,
169+
$params,
150170
$this->defaultSettings,
151171
$settings,
152172
),
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SimPod\ClickHouseClient\Exception;
6+
7+
use InvalidArgumentException;
8+
use SimPod\ClickHouseClient\Sql\Type;
9+
10+
final class UnsupportedParamType extends InvalidArgumentException implements ClickHouseClientException
11+
{
12+
public static function fromType(Type $type): self
13+
{
14+
return new self($type->name);
15+
}
16+
17+
public static function fromString(string $type): self
18+
{
19+
return new self($type);
20+
}
21+
}

src/Exception/UnsupportedValue.php renamed to src/Exception/UnsupportedParamValue.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
use function sprintf;
1212
use function var_export;
1313

14-
final class UnsupportedValue extends InvalidArgumentException implements ClickHouseClientException
14+
final class UnsupportedParamValue extends InvalidArgumentException implements ClickHouseClientException
1515
{
1616
public static function type(mixed $value): self
1717
{

0 commit comments

Comments
 (0)