Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/infection.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,19 @@ jobs:
- name: Build the docker compose stack
run: docker compose -f tests/docker-compose.yaml up -d

- name: Wait for ClickHouse
run: |
for attempt in {1..30}; do
if curl --fail --silent http://127.0.0.1:28123/ping; then
exit 0
fi

sleep 1
done

docker compose -f tests/docker-compose.yaml logs
exit 1

- name: Install PHP
uses: shivammathur/setup-php@v2
with:
Expand Down
34 changes: 26 additions & 8 deletions src/Param/ParamValueConverterRegistry.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,16 @@ public function __construct(array $registry = [])

'UUID' => self::stringConverter(),

'Nullable' => fn (mixed $v, Type $type) => $this->get($type->params)($v, null, false),
'LowCardinality' => fn (mixed $v, Type $type) => $this->get($type->params)($v, null, false),
'Nullable' => fn (
mixed $v,
Type $type,
bool $nested = false,
) => $this->convertWrappedValue($v, $type, $nested),
'LowCardinality' => fn (
mixed $v,
Type $type,
bool $nested = false,
) => $this->convertWrappedValue($v, $type, $nested),

'decimal' => self::decimalConverter(),
'decimal32' => self::decimalConverter(),
Expand All @@ -102,13 +110,15 @@ public function __construct(array $registry = [])
'date32' => self::dateConverter(),
'datetime' => self::dateTimeConverter(),
'datetime32' => self::dateTimeConverter(),
'datetime64' => static function (mixed $value) {
'datetime64' => static function (mixed $value, Type|string|null $type = null, bool $nested = false) {
if ($value instanceof DateTimeInterface) {
return $value->format('U.u');
$value = $value->format('U.u');
}

if (is_string($value) || is_float($value) || is_int($value)) {
return $value;
return $nested
? "'" . Escaper::escape((string) $value) . "'"
: $value;
}
Comment thread
simPod marked this conversation as resolved.

throw UnsupportedParamValue::type($value);
Expand All @@ -123,8 +133,8 @@ public function __construct(array $registry = [])
'Dynamic' => self::noopConverter(),
'Variant' => self::noopConverter(),

'IPv4' => self::noopConverter(),
'IPv6' => self::noopConverter(),
'IPv4' => self::stringConverter(),
'IPv6' => self::stringConverter(),

'enum' => self::noopConverter(),
'Enum8' => self::noopConverter(),
Expand Down Expand Up @@ -236,7 +246,7 @@ public function __construct(array $registry = [])
return '(' . $innerExpression . ')';
},
];
$this->registry = array_merge($defaultRegistry, $registry);
$this->registry = array_merge($defaultRegistry, $registry);
}

/**
Expand Down Expand Up @@ -359,4 +369,12 @@ private function splitTypes(string $types): array

return $result;
}

/** @throws UnsupportedParamType */
private function convertWrappedValue(mixed $value, Type $type, bool $nested): mixed
{
$innerType = Type::fromString($type->params);

return $this->get($innerType)($value, $innerType, $nested);
}
}
118 changes: 118 additions & 0 deletions tests/Client/Http/RequestFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
use SimPod\ClickHouseClient\Settings\EmptySettingsProvider;
use SimPod\ClickHouseClient\Tests\TestCaseBase;

use function sprintf;

#[CoversClass(RequestFactory::class)]
final class RequestFactoryTest extends TestCaseBase
{
Expand Down Expand Up @@ -129,4 +131,120 @@ public function testMultipleNestedParamsParsed(): void
self::assertStringContainsString('param_serverIds', $body);
self::assertStringContainsString('param_sensorIds', $body);
}

public function testNestedDateTime64ParamIsQuoted(): void
{
$dateTime = new DateTimeImmutable('2026-07-30 12:00:00.123456+02:00');

$requestFactory = new RequestFactory(
new ParamValueConverterRegistry(),
new Psr17Factory(),
new Psr17Factory(),
);

$request = $requestFactory->prepareSqlRequest(
'SELECT {inputs:Array(Tuple(DateTime64(6), UUID))}',
new RequestSettings(
new EmptySettingsProvider(),
new EmptySettingsProvider(),
),
new RequestOptions(
[
'inputs' => [
[
$dateTime,
'c8965e35-e785-4b05-a675-000000000000',
],
],
],
),
);

self::assertStringContainsString(
"('" . $dateTime->format('U.u') . "','c8965e35-e785-4b05-a675-000000000000')",
$request->getBody()->__toString(),
);
}

public function testTopLevelDateTime64ParamRemainsNumeric(): void
{
$requestFactory = new RequestFactory(
new ParamValueConverterRegistry(),
new Psr17Factory(),
new Psr17Factory(),
);

$request = $requestFactory->prepareSqlRequest(
'SELECT {value:DateTime64(6)}',
new RequestSettings(
new EmptySettingsProvider(),
new EmptySettingsProvider(),
),
new RequestOptions(
[
'value' => new DateTimeImmutable('2026-07-30 12:00:00.123456'),
],
),
);

self::assertStringContainsString('1785412800.123456', $request->getBody()->__toString());
}

public function testNullableNestedDateTime64ParamIsQuoted(): void
{
$requestFactory = new RequestFactory(
new ParamValueConverterRegistry(),
new Psr17Factory(),
new Psr17Factory(),
);

$request = $requestFactory->prepareSqlRequest(
'SELECT {inputs:Array(Nullable(DateTime64(6)))}',
new RequestSettings(
new EmptySettingsProvider(),
new EmptySettingsProvider(),
),
new RequestOptions(
[
'inputs' => [
new DateTimeImmutable('2026-07-30 12:00:00.123456'),
],
],
),
);

self::assertStringContainsString(
"['1785412800.123456']",
$request->getBody()->__toString(),
);
}

/** @param list<string> $values */
#[DataProvider('provideNestedIpParameters')]
public function testNestedIpParametersAreQuoted(string $type, array $values, string $expected): void
{
$requestFactory = new RequestFactory(
new ParamValueConverterRegistry(),
new Psr17Factory(),
new Psr17Factory(),
);

$request = $requestFactory->prepareSqlRequest(
sprintf('SELECT {inputs:Array(%s)}', $type),
new RequestSettings(
new EmptySettingsProvider(),
new EmptySettingsProvider(),
),
new RequestOptions(['inputs' => $values]),
);

self::assertStringContainsString($expected, $request->getBody()->__toString());
}

/** @return Generator<string, array{string, list<string>, string}> */
public static function provideNestedIpParameters(): Generator
{
yield 'IPv4' => ['IPv4', ['192.0.2.1', '198.51.100.1'], "['192.0.2.1','198.51.100.1']"];
yield 'IPv6' => ['IPv6', ['::ffff:192.0.2.1', '2001:db8::1'], "['::ffff:192.0.2.1','2001:db8::1']"];
}
}
32 changes: 32 additions & 0 deletions tests/Param/ParamValueConverterRegistryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ public static function providerConvert(): Generator
{
yield 'Array' => ['Array(String)', "['foo','bar']", "['foo','bar']"];
yield 'Array LC' => ['Array(LowCardinality(String))', "['foo','bar']", "['foo','bar']"];
yield 'Array LC (array)' => ['Array(LowCardinality(String))', ['foo', 'bar'], "['foo','bar']"];
yield 'Array (array)' => ['Array(String)', ['foo', 'bar', "baz'"], "['foo','bar','baz\\'']"];
yield 'Array Tuple' => ['Array(Tuple(String, String))', [['foo', 'bar']], "[('foo','bar')]"];
yield 'Array Tuple Complex' => [
Expand Down Expand Up @@ -156,6 +157,13 @@ public static function providerConvert(): Generator

yield 'UUID' => ['UUID', 'de90cd12-7100-436e-bfb8-f77e4c7a224f', 'de90cd12-7100-436e-bfb8-f77e4c7a224f'];

yield 'Array IPv4' => ['Array(IPv4)', ['192.0.2.1', '198.51.100.1'], "['192.0.2.1','198.51.100.1']"];
yield 'Array IPv6' => [
'Array(IPv6)',
['::ffff:192.0.2.1', '2001:db8::1'],
"['::ffff:192.0.2.1','2001:db8::1']",
];

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'];
Expand Down Expand Up @@ -194,6 +202,30 @@ public static function providerConvert(): Generator
'2023-02-01 01:02:03.123456000',
];

yield 'Array DateTime64(9) (integer)' => [
"Array(DateTime64(9, 'UTC'))",
[1675213323123456789],
"['2023-02-01 01:02:03.123456789']",
];

yield 'Array DateTime64(9) (float)' => [
"Array(DateTime64(9, 'UTC'))",
[1675213323.1235],
"['2023-02-01 01:02:03.123500000']",
];

yield 'Array DateTime64(9) (string)' => [
"Array(DateTime64(9, 'UTC'))",
['1675213323.123456789'],
"['2023-02-01 01:02:03.123456789']",
];

yield 'Array DateTime64(6) (DateTime with timezone)' => [
"Array(DateTime64(6, 'UTC'))",
[new DateTimeImmutable('2023-02-01 03:02:03.123456+02:00')],
"['2023-02-01 01:02:03.123456']",
];

yield 'Bool' => ['Bool', true, 'true'];

yield 'Dynamic' => ['Bool', true, 'true'];
Expand Down
Loading