From 72a23fdffbac9fb96c40d89027b618322b7b6c84 Mon Sep 17 00:00:00 2001 From: Simon Podlipsky Date: Fri, 12 Jan 2024 09:58:47 +0100 Subject: [PATCH] feat: pass parameters natively via http interface along the query --- composer.json | 1 + phpstan-baseline.neon | 5 + src/Client/Http/RequestFactory.php | 38 +++- src/Client/Http/RequestOptions.php | 9 +- src/Client/PsrClickHouseAsyncClient.php | 28 +-- src/Client/PsrClickHouseClient.php | 39 ++-- src/Exception/UnsupportedType.php | 16 ++ src/Param/ParamValueConverterRegistry.php | 176 +++++++++++++++++ src/Sql/Escaper.php | 7 +- src/Sql/SqlFactory.php | 5 +- src/Sql/Type.php | 21 +++ src/Sql/ValueFormatter.php | 5 +- tests/Client/Http/RequestFactoryTest.php | 6 +- tests/Client/Http/RequestOptionsTest.php | 1 + tests/Client/SelectTest.php | 9 + tests/Param/ParamValueConverterTest.php | 218 ++++++++++++++++++++++ tests/WithClient.php | 38 ++-- 17 files changed, 564 insertions(+), 58 deletions(-) create mode 100644 src/Exception/UnsupportedType.php create mode 100644 src/Param/ParamValueConverterRegistry.php create mode 100644 src/Sql/Type.php create mode 100644 tests/Param/ParamValueConverterTest.php diff --git a/composer.json b/composer.json index 3878704..44a55a4 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 76f6c98..2e8aecb 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -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 diff --git a/src/Client/Http/RequestFactory.php b/src/Client/Http/RequestFactory.php index 499565a..5e3b000 100644 --- a/src/Client/Http/RequestFactory.php +++ b/src/Client/Http/RequestFactory.php @@ -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; @@ -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 = '', ) { @@ -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 { @@ -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(); } diff --git a/src/Client/Http/RequestOptions.php b/src/Client/Http/RequestOptions.php index f01de80..1f5e12b 100644 --- a/src/Client/Http/RequestOptions.php +++ b/src/Client/Http/RequestOptions.php @@ -10,11 +10,16 @@ final class RequestOptions public array $settings; /** + * @param array $params * @param array $defaultSettings * @param array $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; } } diff --git a/src/Client/PsrClickHouseAsyncClient.php b/src/Client/PsrClickHouseAsyncClient.php index 166db38..38a056d 100644 --- a/src/Client/PsrClickHouseAsyncClient.php +++ b/src/Client/PsrClickHouseAsyncClient.php @@ -39,16 +39,7 @@ public function __construct( */ public function select(string $query, Format $outputFormat, array $settings = []): PromiseInterface { - $formatClause = $outputFormat::toSql(); - - return $this->executeRequest( - << $outputFormat::output($response->getBody()->__toString()) - ); + return $this->selectWithParams($query, [], $outputFormat, $settings); } /** @@ -62,14 +53,23 @@ 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( + << $outputFormat::output($response->getBody()->__toString()) ); } /** + * @param array $params * @param array $settings * @param (callable(ResponseInterface):mixed)|null $processResponse * @@ -77,12 +77,14 @@ public function selectWithParams( */ 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, ), diff --git a/src/Client/PsrClickHouseClient.php b/src/Client/PsrClickHouseClient.php index 6c5f114..6c7bad5 100644 --- a/src/Client/PsrClickHouseClient.php +++ b/src/Client/PsrClickHouseClient.php @@ -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( <<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 === []) { @@ -111,7 +114,8 @@ public function insert(string $table, array $values, array|null $columns = null, $columnsSql VALUES $valuesSql CLICKHOUSE, - $settings, + params: [], + settings: $settings, ); } @@ -125,21 +129,24 @@ public function insertWithFormat(string $table, Format $inputFormat, string $dat << $params * @param array $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, ), diff --git a/src/Exception/UnsupportedType.php b/src/Exception/UnsupportedType.php new file mode 100644 index 0000000..fdcf295 --- /dev/null +++ b/src/Exception/UnsupportedType.php @@ -0,0 +1,16 @@ +name); + } +} diff --git a/src/Param/ParamValueConverterRegistry.php b/src/Param/ParamValueConverterRegistry.php new file mode 100644 index 0000000..cf16866 --- /dev/null +++ b/src/Param/ParamValueConverterRegistry.php @@ -0,0 +1,176 @@ + */ + private array $registry; + + public function __construct() + { + /** @phpstan-var array $registry */ + $registry = [ + 'String' => self::stringConverter(), + 'FixedString' => self::stringConverter(), + + 'UUID' => self::stringConverter(), + + 'Nullable' => fn (mixed $v, Type $type) => $this->get($type->params)($v, null, false), + + 'Decimal' => self::decimalConverter(), + 'Decimal32' => self::decimalConverter(), + 'Decimal64' => self::decimalConverter(), + 'Decimal128' => self::decimalConverter(), + 'Decimal256' => self::decimalConverter(), + + 'Bool' => static fn (bool $value) => $value, + + 'Date' => $this->dateConverter(), + 'Date32' => $this->dateConverter(), + 'DateTime' => $this->dateTimeConverter(), + 'DateTime32' => $this->dateTimeConverter(), + 'DateTime64' => static fn (DateTimeInterface|string|int|float $value) => $value instanceof DateTimeInterface + ? $value->format('Y-m-d H:i:s.u') + : $value, + + 'IPv4' => self::noopConverter(), + 'IPv6' => self::noopConverter(), + + 'Enum' => self::noopConverter(), + 'Enum8' => self::noopConverter(), + 'Enum16' => self::noopConverter(), + 'Enum32' => self::noopConverter(), + 'Enum64' => self::noopConverter(), + + 'JSON' => static fn (array|string $value) => is_string($value) ? $value : json_encode($value), + 'Object' => fn (mixed $v, Type $type) => $this->get(trim($type->params, "'"))($v, $type, true), + 'Map' => self::noopConverter(), + 'Nested' => function (array|string $v, Type $type) { + if (is_string($v)) { + return $v; + } + + $types = array_map(static fn ($type) => explode(' ', trim($type))[1], explode(',', $type->params)); + + return sprintf('[%s]', implode(',', array_map( + fn (array $row) => sprintf('(%s)', implode(',', array_map( + fn (int|string $i) => $this->get($types[$i])($row[$i], $types[$i], true), + array_keys($row), + ))), + $v, + ))); + }, + + 'Float32' => self::floatConverter(), + 'Float64' => self::floatConverter(), + + 'Int8' => self::intConverter(), + 'Int16' => self::intConverter(), + 'Int32' => self::intConverter(), + 'Int64' => self::intConverter(), + 'Int128' => self::intConverter(), + 'Int256' => self::intConverter(), + + 'UInt8' => self::intConverter(), + 'UInt16' => self::intConverter(), + 'UInt32' => self::intConverter(), + 'UInt64' => self::intConverter(), + 'UInt128' => self::intConverter(), + 'UInt256' => self::intConverter(), + + 'Array' => fn (array|string $v, Type $type) => is_string($v) + ? $v + : sprintf('[%s]', implode( + ',', + array_map(fn (mixed $v) => $this->get($type->params)($v, $type, true), $v), + )), + 'Tuple' => function (array|string $v, Type $type) { + if (is_string($v)) { + return $v; + } + + $types = array_map(static fn ($p) => trim($p), explode(',', $type->params)); + + return '(' . implode( + ',', + array_map(fn (mixed $i) => $this->get($types[$i])($v[$i], null, true), array_keys($v)), + ) . ')'; + }, + ]; + $this->registry = $registry; + } + + /** @phpstan-return Converter */ + public function get(Type|string $type): Closure + { + return $this->registry[is_string($type) ? $type : $type->name] ?? self::unsupportedFormatConverter(); + } + + public static function stringConverter(): Closure + { + return static fn ( + string $value, + Type|string|null $type = null, + bool $nested = false, + ) => $nested ? '\'' . str_replace("'", "\'", $value) . '\'' : $value; + } + + public static function noopConverter(): Closure + { + return static fn (mixed $value) => $value; + } + + public static function floatConverter(): Closure + { + return static fn (float|string $value) => $value; + } + + public static function intConverter(): Closure + { + return static fn (int|string $value) => $value; + } + + public static function decimalConverter(): Closure + { + return static fn (float|int|string $value) => $value; + } + + public static function unsupportedFormatConverter(): Closure + { + return static fn (mixed $value, Type $type) => throw UnsupportedType::fromType($type); + } + + public function dateConverter(): Closure + { + return static fn (DateTimeInterface|string|float $value) => $value instanceof DateTimeInterface + ? $value->format('Y-m-d') + : $value; + } + + public function dateTimeConverter(): Closure + { + return static fn (DateTimeInterface|string|int|float $value) => $value instanceof DateTimeInterface + ? $value->format('Y-m-d H:i:s') + : $value; + } +} diff --git a/src/Sql/Escaper.php b/src/Sql/Escaper.php index 02807b7..e00f6ed 100644 --- a/src/Sql/Escaper.php +++ b/src/Sql/Escaper.php @@ -6,8 +6,11 @@ use function str_replace; -// phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong -/** @link https://github.com/ClickHouse/clickhouse-jdbc/blob/8481c1323f5de09bb9dbbf67085e5e1b2585756a/src/main/java/ru/yandex/clickhouse/ClickHouseUtil.java */ +/** + * @deprecated + * + * @link https://github.com/ClickHouse/clickhouse-jdbc/blob/8481c1323f5de09bb9dbbf67085e5e1b2585756a/src/main/java/ru/yandex/clickhouse/ClickHouseUtil.java + */ final class Escaper { public static function escape(string $s): string diff --git a/src/Sql/SqlFactory.php b/src/Sql/SqlFactory.php index 03302a4..09cd867 100644 --- a/src/Sql/SqlFactory.php +++ b/src/Sql/SqlFactory.php @@ -13,7 +13,10 @@ use function sprintf; use function str_replace; -/** @internal */ +/** + * @internal + * @deprecated + */ final class SqlFactory { public function __construct(private ValueFormatter $valueFormatter) diff --git a/src/Sql/Type.php b/src/Sql/Type.php new file mode 100644 index 0000000..b998f7a --- /dev/null +++ b/src/Sql/Type.php @@ -0,0 +1,21 @@ +prepareRequest(new RequestOptions( 'SELECT 1', + [], ['max_block_size' => 1], ['database' => 'database'], )); @@ -37,7 +39,7 @@ public function testPrepareRequest(string $uri, string $expectedUri): void $expectedUri, $request->getUri()->__toString(), ); - self::assertSame('SELECT 1', $request->getBody()->__toString()); + self::assertStringContainsString('SELECT 1', $request->getBody()->__toString()); } /** @return Generator */ diff --git a/tests/Client/Http/RequestOptionsTest.php b/tests/Client/Http/RequestOptionsTest.php index 28e2cdf..3eb7018 100644 --- a/tests/Client/Http/RequestOptionsTest.php +++ b/tests/Client/Http/RequestOptionsTest.php @@ -15,6 +15,7 @@ public function testMergeSettings(): void { $requestOptions = new RequestOptions( '', + [], ['database' => 'foo', 'a' => 1], ['database' => 'bar', 'b' => 2], ); diff --git a/tests/Client/SelectTest.php b/tests/Client/SelectTest.php index 4258944..e0bea1e 100644 --- a/tests/Client/SelectTest.php +++ b/tests/Client/SelectTest.php @@ -13,6 +13,7 @@ use SimPod\ClickHouseClient\Format\JsonCompact; use SimPod\ClickHouseClient\Format\JsonEachRow; use SimPod\ClickHouseClient\Format\Null_; +use SimPod\ClickHouseClient\Format\TabSeparated; use SimPod\ClickHouseClient\Tests\TestCaseBase; use SimPod\ClickHouseClient\Tests\WithClient; @@ -31,6 +32,14 @@ final class SelectTest extends TestCaseBase { use WithClient; + public function testSelectWithParams(): void + { + $client = $this->client; + $output = $client->selectWithParams('SELECT {p1:UInt8} AS data', ['p1' => 3], new TabSeparated()); + + self::assertSame("3\n", $output->contents); + } + #[DataProvider('providerJson')] public function testJson(mixed $expectedData, string $sql): void { diff --git a/tests/Param/ParamValueConverterTest.php b/tests/Param/ParamValueConverterTest.php new file mode 100644 index 0000000..c2ce2cd --- /dev/null +++ b/tests/Param/ParamValueConverterTest.php @@ -0,0 +1,218 @@ + */ + private static array $types = []; + + /** + * @throws ClientExceptionInterface + * @throws ServerError + */ + #[BeforeClass] + public static function fetchAllTypes(): void + { + /** @var JsonEachRow $format */ + $format = new JsonEachRow(); + $rows = self::$client->select( + <<<'CLICKHOUSE' + SELECT * FROM system.data_type_families + CLICKHOUSE, + $format, + )->data; + + foreach ($rows as $row) { + self::$types[] = $row['alias_to'] === '' ? $row['name'] : $row['alias_to']; + } + + self::$types = array_unique(self::$types); + } + + public function testAllTypesAreCovered(): void + { + self::assertNotEmpty(self::$types); + + $coveredTypeNames = array_map(static fn (array $v) => Type::fromString($v[0])->name, iterator_to_array( + (static function () { + yield from self::providerConvert(); + yield from self::providerUnsupported(); + })(), + )); + + foreach (self::$types as $type) { + self::assertTrue(in_array($type, $coveredTypeNames, true), $type); + } + } + + #[DataProvider('providerConvert')] + public function testConvert(string $type, mixed $value, mixed $expected): void + { + $registry = new ParamValueConverterRegistry(); + + $converter = $registry->get(Type::fromString($type)); + $reflection = new ReflectionFunction($converter); +// $argTypeName = $reflection->getParameters()[0]->getType()->getName(); + + self::assertSame( + $expected, + trim( + self::$client->selectWithParams( + sprintf('SELECT {p1:%s}', $type), + ['p1' => $value], + new TabSeparated(), + )->contents, + ), + ); + } + + /** @return Generator */ + public static function providerConvert(): Generator + { + yield 'Array' => ['Array(String)', "['foo','bar']", "['foo','bar']"]; + yield 'Array (array)' => ['Array(String)', ['foo', 'bar'], "['foo','bar']"]; + yield 'Tuple' => ['Tuple(String, Int8)', "('k',1)", "('k',1)"]; + yield 'Tuple (array)' => ['Tuple(String, Int8)', ['k', 1], "('k',1)"]; + yield 'JSON' => ['JSON', '{"k":"v"}', '{"k":"v"}']; + yield 'JSON (array)' => ['JSON', ['k' => 'v'], '{"k":"v"}']; + yield 'Object' => ["Object('JSON')", '{"k":"v"}', '{"k":"v"}']; + yield 'Object (array)' => ["Object('JSON')", ['k' => 'v'], '{"k":"v"}']; + yield 'Map' => ['Map(String, UInt64)', "{'k1':1}", "{'k1':1}"]; + yield 'Nested' => [ + 'Nested(id UUID, a String)', + "[('084caa96-915b-449d-8fc6-0292c73d6399','1')]", + "[('084caa96-915b-449d-8fc6-0292c73d6399','1')]", + ]; + + yield 'Nested (array)' => [ + 'Nested(id UUID, a String)', + [['084caa96-915b-449d-8fc6-0292c73d6399','1']], + "[('084caa96-915b-449d-8fc6-0292c73d6399','1')]", + ]; + + yield 'String' => ['String', 'foo', 'foo']; + yield 'FixedString' => ['FixedString(3)', 'foo', 'foo']; + + yield 'UUID' => ['UUID', 'de90cd12-7100-436e-bfb8-f77e4c7a224f', 'de90cd12-7100-436e-bfb8-f77e4c7a224f']; + + yield 'Date' => ['Date', '2023-02-01', '2023-02-01']; + yield 'Date (datetime)' => ['Date', new DateTimeImmutable('2023-02-01'), '2023-02-01']; + yield 'Date32' => ['Date32', new DateTimeImmutable('2023-02-01'), '2023-02-01']; + yield 'DateTime' => ['DateTime', new DateTimeImmutable('2023-02-01 01:02:03'), '2023-02-01 01:02:03']; + yield 'DateTime32' => ['DateTime32', new DateTimeImmutable('2023-02-01 01:02:03'), '2023-02-01 01:02:03']; + yield 'DateTime64(3)' => [ + 'DateTime64(3)', + new DateTimeImmutable('2023-02-01 01:02:03.123456'), + '2023-02-01 01:02:03.123', + ]; + + yield 'DateTime64(4)' => [ + 'DateTime64(4)', + new DateTimeImmutable('2023-02-01 01:02:03.123456'), + '2023-02-01 01:02:03.1234', + ]; + + yield 'DateTime64(6)' => [ + 'DateTime64(6)', + new DateTimeImmutable('2023-02-01 01:02:03.123456'), + '2023-02-01 01:02:03.123456', + ]; + + yield 'DateTime64(9)' => ['DateTime64(9)', 1675213323123456789, '2023-02-01 01:02:03.123456789']; + yield 'DateTime64(9) (float)' => ['DateTime64(9)', 1675213323.1235, '2023-02-01 01:02:03.123500000']; + yield 'DateTime64(9) (string)' => ['DateTime64(9)', '1675213323.123456789', '2023-02-01 01:02:03.123456789']; + + yield 'Bool' => ['Bool', true, 'true']; + + yield 'Nullable' => ['Nullable(String)', 'foo', 'foo']; + + yield 'Enum' => ["Enum('a' = 1, 'b' = 2)", 'a', 'a']; + yield 'Enum8' => ["Enum8('a' = 1, 'b' = 2)", 'a', 'a']; + yield 'Enum16' => ["Enum16('a' = 1, 'b' = 2)", 'a', 'a']; + + yield 'Int8' => ['Int8', 1, '1']; + yield 'Int8 (string)' => ['Int8', '1', '1']; + yield 'Int16' => ['Int16', 1, '1']; + yield 'Int32' => ['Int32', 1, '1']; + yield 'Int64' => ['Int64', 1, '1']; + yield 'Int128' => ['Int128', 1, '1']; + yield 'Int256' => ['Int256', 1, '1']; + + yield 'Float32' => ['Float32', 1.1, '1.1']; + yield 'Float32 (string)' => ['Float32', '1.1', '1.1']; + yield 'Float64' => ['Float64', 1.1, '1.1']; + + yield 'UInt8' => ['UInt8', 1, '1']; + yield 'UInt8 (string)' => ['UInt8', '1', '1']; + yield 'UInt16' => ['UInt16', 1, '1']; + yield 'UInt32' => ['UInt32', 1, '1']; + yield 'UInt64' => ['UInt64', 1, '1']; + yield 'UInt128' => ['UInt128', 1, '1']; + yield 'UInt256' => ['UInt256', 1, '1']; + + yield 'Decimal' => ['Decimal', 3.33, '3']; + yield 'Decimal (string)' => ['Decimal', '3.33', '3']; + yield 'Decimal32' => ['Decimal32(2)', 3.33, '3.33']; + yield 'Decimal64' => ['Decimal64(2)', 3.33, '3.33']; + yield 'Decimal128' => ['Decimal128(2)', 3.33, '3.33']; + yield 'Decimal256' => ['Decimal256(2)', 3.33, '3.33']; + + yield 'IPv4' => ['IPv4', '1.2.3.4', '1.2.3.4']; + yield 'IPv6' => ['IPv6', '2001:0000:130F:0000:0000:09C0:876A:130B', '2001:0:130f::9c0:876a:130b']; + } + + /** @return Generator */ + public static function providerUnsupported(): Generator + { + yield ['AggregateFunction']; + yield ['SimpleAggregateFunction']; + + yield ['Polygon']; + yield ['MultiPolygon']; + yield ['Ring']; + yield ['Point']; + + yield ['IntervalNanosecond']; + yield ['IntervalMicrosecond']; + yield ['IntervalMillisecond']; + yield ['IntervalSecond']; + yield ['IntervalMinute']; + yield ['IntervalHour']; + yield ['IntervalDay']; + yield ['IntervalWeek']; + yield ['IntervalMonth']; + yield ['IntervalQuarter']; + yield ['IntervalYear']; + + yield ['LowCardinality']; + yield ['Nothing']; + } +} diff --git a/tests/WithClient.php b/tests/WithClient.php index 78ad6bd..eb24e33 100644 --- a/tests/WithClient.php +++ b/tests/WithClient.php @@ -8,6 +8,7 @@ use Nyholm\Psr7\Factory\Psr17Factory; use PHPUnit\Framework\Attributes\After; use PHPUnit\Framework\Attributes\Before; +use PHPUnit\Framework\Attributes\BeforeClass; use Psr\Http\Client\ClientExceptionInterface; use Safe\Exceptions\PcreException; use SimPod\ClickHouseClient\Client\ClickHouseAsyncClient; @@ -16,6 +17,7 @@ use SimPod\ClickHouseClient\Client\PsrClickHouseAsyncClient; use SimPod\ClickHouseClient\Client\PsrClickHouseClient; use SimPod\ClickHouseClient\Exception\ServerError; +use SimPod\ClickHouseClient\Param\ParamValueConverterRegistry; use Symfony\Component\HttpClient\CurlHttpClient; use Symfony\Component\HttpClient\HttplugClient; use Symfony\Component\HttpClient\Psr18Client; @@ -28,25 +30,26 @@ trait WithClient { - private ClickHouseClient $client; + private static ClickHouseClient $client; - private ClickHouseAsyncClient $asyncClient; + private static ClickHouseAsyncClient $asyncClient; /** @internal */ - private ClickHouseClient $controllerClient; + private static ClickHouseClient $controllerClient; - private string|null $currentDbName = null; + private static string|null $currentDbName = null; + #[BeforeClass] #[Before] - public function setupClickHouseClient(): void + public static function setupClickHouseClient(): void { - $this->restartClickHouseClient(); + static::restartClickHouseClient(); } #[After] public function tearDownDataBase(): void { - $this->controllerClient->executeQuery(sprintf('DROP DATABASE IF EXISTS "%s"', $this->currentDbName)); + static::$controllerClient->executeQuery(sprintf('DROP DATABASE IF EXISTS "%s"', static::$currentDbName)); } /** @@ -55,7 +58,7 @@ public function tearDownDataBase(): void * @throws PcreException * @throws ServerError */ - private function restartClickHouseClient(): void + private static function restartClickHouseClient(): void { $databaseName = getenv('CLICKHOUSE_DATABASE'); $username = getenv('CLICKHOUSE_USER'); @@ -67,14 +70,14 @@ private function restartClickHouseClient(): void assert(is_string($endpoint)); assert(is_string($password)); - $this->currentDbName = 'clickhouse_client_test__' . time(); + static::$currentDbName = 'clickhouse_client_test__' . time(); $headers = [ 'X-ClickHouse-User' => $username, 'X-ClickHouse-Key' => $password, ]; - $this->controllerClient = new PsrClickHouseClient( + static::$controllerClient = new PsrClickHouseClient( new Psr18Client( new CurlHttpClient([ 'base_uri' => $endpoint, @@ -83,40 +86,43 @@ private function restartClickHouseClient(): void ]), ), new RequestFactory( + new ParamValueConverterRegistry(), new Psr17Factory(), new Psr17Factory(), ), ); - $this->client = new PsrClickHouseClient( + static::$client = new PsrClickHouseClient( new Psr18Client( new CurlHttpClient([ 'base_uri' => $endpoint, 'headers' => $headers, - 'query' => ['database' => $this->currentDbName], + 'query' => ['database' => static::$currentDbName], ]), ), new RequestFactory( + new ParamValueConverterRegistry(), new Psr17Factory(), new Psr17Factory(), ), ); - $this->asyncClient = new PsrClickHouseAsyncClient( + static::$asyncClient = new PsrClickHouseAsyncClient( new HttplugClient( new CurlHttpClient([ 'base_uri' => $endpoint, 'headers' => $headers, - 'query' => ['database' => $this->currentDbName], + 'query' => ['database' => static::$currentDbName], ]), ), new RequestFactory( + new ParamValueConverterRegistry(), new Psr17Factory(), new Psr17Factory(), ), ); - $this->controllerClient->executeQuery(sprintf('DROP DATABASE IF EXISTS "%s"', $this->currentDbName)); - $this->controllerClient->executeQuery(sprintf('CREATE DATABASE "%s"', $this->currentDbName)); + static::$controllerClient->executeQuery(sprintf('DROP DATABASE IF EXISTS "%s"', static::$currentDbName)); + static::$controllerClient->executeQuery(sprintf('CREATE DATABASE "%s"', static::$currentDbName)); } }