diff --git a/src/Param/ParamValueConverterRegistry.php b/src/Param/ParamValueConverterRegistry.php index 0358c2d2..dd28e991 100644 --- a/src/Param/ParamValueConverterRegistry.php +++ b/src/Param/ParamValueConverterRegistry.php @@ -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(), @@ -102,13 +110,17 @@ 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 = $nested + ? $value->format('Y-m-d H:i:s.u') + : $value->format('U.u'); } if (is_string($value) || is_float($value) || is_int($value)) { - return $value; + return $nested + ? "'" . Escaper::escape((string) $value) . "'" + : $value; } throw UnsupportedParamValue::type($value); @@ -236,7 +248,7 @@ public function __construct(array $registry = []) return '(' . $innerExpression . ')'; }, ]; - $this->registry = array_merge($defaultRegistry, $registry); + $this->registry = array_merge($defaultRegistry, $registry); } /** @@ -359,4 +371,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); + } } diff --git a/tests/Client/Http/RequestFactoryTest.php b/tests/Client/Http/RequestFactoryTest.php index 35be86ee..37746830 100644 --- a/tests/Client/Http/RequestFactoryTest.php +++ b/tests/Client/Http/RequestFactoryTest.php @@ -129,4 +129,92 @@ public function testMultipleNestedParamsParsed(): void self::assertStringContainsString('param_serverIds', $body); self::assertStringContainsString('param_sensorIds', $body); } + + public function testNestedDateTime64ParamIsQuoted(): void + { + $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' => [ + [ + new DateTimeImmutable('2026-07-30 12:00:00.123456'), + 'c8965e35-e785-4b05-a675-000000000000', + ], + ], + ], + ), + ); + + self::assertStringContainsString( + "('2026-07-30 12:00:00.123456','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 testWrappedNestedDateTime64ParamIsQuoted(): void + { + $requestFactory = new RequestFactory( + new ParamValueConverterRegistry(), + new Psr17Factory(), + new Psr17Factory(), + ); + + $request = $requestFactory->prepareSqlRequest( + 'SELECT {inputs:Array(Tuple(Nullable(DateTime64(6)), LowCardinality(DateTime64(6))))}', + new RequestSettings( + new EmptySettingsProvider(), + new EmptySettingsProvider(), + ), + new RequestOptions( + [ + 'inputs' => [ + [ + new DateTimeImmutable('2026-07-30 12:00:00.123456'), + new DateTimeImmutable('2026-07-30 12:01:00.123456'), + ], + ], + ], + ), + ); + + self::assertStringContainsString( + "('2026-07-30 12:00:00.123456','2026-07-30 12:01:00.123456')", + $request->getBody()->__toString(), + ); + } }