Skip to content

Commit

Permalink
feat: pass parameters natively via http interface along the query
Browse files Browse the repository at this point in the history
  • Loading branch information
simPod committed Jan 16, 2024
1 parent 00d04f4 commit 72a23fd
Show file tree
Hide file tree
Showing 17 changed files with 564 additions and 58 deletions.
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"require": {
"php": "^8.2",
"guzzlehttp/promises": "^2.0",
"guzzlehttp/psr7": "^2.6",
"php-http/client-common": "^2.0",
"psr/http-client": "^1.0",
"psr/http-factory": "^1.0",
Expand Down
5 changes: 5 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
parameters:
ignoreErrors:
-
message: "#^Method SimPod\\\\ClickHouseClient\\\\Client\\\\PsrClickHouseClient\\:\\:select\\(\\) throws checked exception SimPod\\\\ClickHouseClient\\\\Exception\\\\UnsupportedValue but it's missing from the PHPDoc @throws tag\\.$#"
count: 1
path: src/Client/PsrClickHouseClient.php

-
message: "#^Constructor of class SimPod\\\\ClickHouseClient\\\\Output\\\\Null_ has an unused parameter \\$_\\.$#"
count: 1
Expand Down
38 changes: 33 additions & 5 deletions src/Client/Http/RequestFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,21 @@

namespace SimPod\ClickHouseClient\Client\Http;

use GuzzleHttp\Psr7\MultipartStream;
use InvalidArgumentException;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\UriFactoryInterface;
use Psr\Http\Message\UriInterface;
use RuntimeException;
use SimPod\ClickHouseClient\Param\ParamValueConverterRegistry;
use SimPod\ClickHouseClient\Sql\Type;

use function array_keys;
use function array_reduce;
use function http_build_query;
use function is_string;
use function preg_match_all;

use const PHP_QUERY_RFC3986;

Expand All @@ -23,8 +28,8 @@ final class RequestFactory

/** @throws InvalidArgumentException */
public function __construct(
private ParamValueConverterRegistry $paramValueConverterRegistry,
private RequestFactoryInterface $requestFactory,
private StreamFactoryInterface $streamFactory,
UriFactoryInterface|null $uriFactory = null,
UriInterface|string $uri = '',
) {
Expand All @@ -50,8 +55,6 @@ public function prepareRequest(RequestOptions $requestOptions): RequestInterface
PHP_QUERY_RFC3986,
);

$body = $this->streamFactory->createStream($requestOptions->sql);

if ($this->uri === null) {
$uri = $query === '' ? '' : '?' . $query;
} else {
Expand All @@ -64,8 +67,33 @@ public function prepareRequest(RequestOptions $requestOptions): RequestInterface
}

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

preg_match_all('~\{([a-zA-Z\d]+):([a-zA-Z\d ]+(\(.+\))?)}~', $requestOptions->sql, $matches);

$typeToParam = array_reduce(
array_keys($matches[1]),
static function (array $acc, string|int $k) use ($matches) {
$acc[$matches[1][$k]] = Type::fromString($matches[2][$k]);

return $acc;
},
[],
);

$streamElements = [['name' => 'query', 'contents' => $requestOptions->sql]];
foreach ($requestOptions->params as $name => $value) {
$type = $typeToParam[$name];
$streamElements[] = [
'name' => 'param_' . $name,
'contents' => $this->paramValueConverterRegistry->get($type)($value, $type, false),
];
}

try {
$request = $request->withBody($body);
$body = new MultipartStream($streamElements);
$request = $request
->withHeader('Content-Type', 'multipart/form-data; boundary=' . $body->getBoundary())
->withBody($body);
} catch (InvalidArgumentException) {
$this->absurd();
}
Expand Down
9 changes: 7 additions & 2 deletions src/Client/Http/RequestOptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,16 @@ final class RequestOptions
public array $settings;

/**
* @param array<string, mixed> $params
* @param array<string, float|int|string> $defaultSettings
* @param array<string, float|int|string> $querySettings
*/
public function __construct(public string $sql, array $defaultSettings, array $querySettings)
{
public function __construct(
public string $sql,
public array $params,
array $defaultSettings,
array $querySettings,
) {
$this->settings = $querySettings + $defaultSettings;
}
}
28 changes: 15 additions & 13 deletions src/Client/PsrClickHouseAsyncClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,7 @@ public function __construct(
*/
public function select(string $query, Format $outputFormat, array $settings = []): PromiseInterface
{
$formatClause = $outputFormat::toSql();

return $this->executeRequest(
<<<CLICKHOUSE
$query
$formatClause
CLICKHOUSE,
$settings,
static fn (ResponseInterface $response): Output => $outputFormat::output($response->getBody()->__toString())
);
return $this->selectWithParams($query, [], $outputFormat, $settings);
}

/**
Expand All @@ -62,27 +53,38 @@ public function selectWithParams(
Format $outputFormat,
array $settings = [],
): PromiseInterface {
return $this->select(
$this->sqlFactory->createWithParameters($query, $params),
$outputFormat,
$formatClause = $outputFormat::toSql();

$sql = $this->sqlFactory->createWithParameters($query, $params);

return $this->executeRequest(
<<<CLICKHOUSE
$sql
$formatClause
CLICKHOUSE,
$params,
$settings,
static fn (ResponseInterface $response): Output => $outputFormat::output($response->getBody()->__toString())
);
}

/**
* @param array<string, mixed> $params
* @param array<string, float|int|string> $settings
* @param (callable(ResponseInterface):mixed)|null $processResponse
*
* @throws Exception
*/
private function executeRequest(
string $sql,
array $params,
array $settings = [],
callable|null $processResponse = null,
): PromiseInterface {
$request = $this->requestFactory->prepareRequest(
new RequestOptions(
$sql,
$params,
$this->defaultSettings,
$settings,
),
Expand Down
39 changes: 23 additions & 16 deletions src/Client/PsrClickHouseClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,38 +44,41 @@ public function __construct(

public function executeQuery(string $query, array $settings = []): void
{
$this->executeRequest($query, $settings);
$this->executeRequest($query, params: [], settings: $settings);
}

public function executeQueryWithParams(string $query, array $params, array $settings = []): void
{
$this->executeQuery($this->sqlFactory->createWithParameters($query, $params), $settings);
$this->executeRequest(
$this->sqlFactory->createWithParameters($query, $params),
params: $params,
settings: $settings,
);
}

public function select(string $query, Format $outputFormat, array $settings = []): Output
{
return $this->selectWithParams($query, params: [], outputFormat: $outputFormat, settings: $settings);
}

public function selectWithParams(string $query, array $params, Format $outputFormat, array $settings = []): Output
{
$formatClause = $outputFormat::toSql();

$sql = $this->sqlFactory->createWithParameters($query, $params);

$response = $this->executeRequest(
<<<CLICKHOUSE
$query
$sql
$formatClause
CLICKHOUSE,
$settings,
params: $params,
settings: $settings,
);

return $outputFormat::output($response->getBody()->__toString());
}

public function selectWithParams(string $query, array $params, Format $outputFormat, array $settings = []): Output
{
return $this->select(
$this->sqlFactory->createWithParameters($query, $params),
$outputFormat,
$settings,
);
}

public function insert(string $table, array $values, array|null $columns = null, array $settings = []): void
{
if ($values === []) {
Expand Down Expand Up @@ -111,7 +114,8 @@ public function insert(string $table, array $values, array|null $columns = null,
$columnsSql
VALUES $valuesSql
CLICKHOUSE,
$settings,
params: [],
settings: $settings,
);
}

Expand All @@ -125,21 +129,24 @@ public function insertWithFormat(string $table, Format $inputFormat, string $dat
<<<CLICKHOUSE
INSERT INTO $table $formatSql $data
CLICKHOUSE,
$settings,
params: [],
settings: $settings,
);
}

/**
* @param array<string, mixed> $params
* @param array<string, float|int|string> $settings
*
* @throws ServerError
* @throws ClientExceptionInterface
*/
private function executeRequest(string $sql, array $settings = []): ResponseInterface
private function executeRequest(string $sql, array $params, array $settings): ResponseInterface
{
$request = $this->requestFactory->prepareRequest(
new RequestOptions(
$sql,
$params,
$this->defaultSettings,
$settings,
),
Expand Down
16 changes: 16 additions & 0 deletions src/Exception/UnsupportedType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace SimPod\ClickHouseClient\Exception;

use InvalidArgumentException;
use SimPod\ClickHouseClient\Sql\Type;

final class UnsupportedType extends InvalidArgumentException implements ClickHouseClientException
{
public static function fromType(Type $type): self
{
return new self($type->name);
}
}
Loading

0 comments on commit 72a23fd

Please sign in to comment.