diff --git a/README.md b/README.md index b5b13aa..18a633c 100644 --- a/README.md +++ b/README.md @@ -298,6 +298,9 @@ $output = $client->selectWithParams( All types are supported (except `AggregateFunction`, `SimpleAggregateFunction` and `Nothing` by design). You can also pass `DateTimeInterface` into `Date*` types or native array into `Array`, `Tuple`, `Nested` and Geo types (`Point`, `Polygon`, `Geometry` etc.). +Pass raw strings to native `String` and `FixedString` parameters. The client escapes backslashes and control characters +for ClickHouse; do not escape these values yourself. This also applies to `Nullable` and `LowCardinality` string wrappers. + ### Custom Query Parameter Value Conversion Query parameters passed to `selectWithParams()` are converted into an HTTP-API-compatible format. To overwrite an existing value converter or diff --git a/src/Param/ParamValueConverterRegistry.php b/src/Param/ParamValueConverterRegistry.php index 9028b61..9496c74 100644 --- a/src/Param/ParamValueConverterRegistry.php +++ b/src/Param/ParamValueConverterRegistry.php @@ -26,6 +26,7 @@ use function sprintf; use function strlen; use function strtolower; +use function strtr; use function trim; /** @@ -279,7 +280,16 @@ private static function stringConverter(): Closure string $value, Type|string|null $type = null, bool $nested = false, - ) => $nested ? "'" . Escaper::escape($value) . "'" : $value; + ) => $nested ? "'" . Escaper::escape($value) . "'" : strtr($value, [ + // Native scalar strings use escaped text; nested strings use quoted literals. + '\\' => '\\\\', + "\n" => '\\n', + "\t" => '\\t', + "\r" => '\\r', + "\0" => '\\0', + "\x08" => '\\b', + "\x0C" => '\\f', + ]); } private static function noopConverter(): Closure diff --git a/tests/Client/SelectTest.php b/tests/Client/SelectTest.php index 0711b65..a16dff1 100644 --- a/tests/Client/SelectTest.php +++ b/tests/Client/SelectTest.php @@ -14,15 +14,22 @@ use SimPod\ClickHouseClient\Format\JsonEachRow; use SimPod\ClickHouseClient\Format\Null_; use SimPod\ClickHouseClient\Format\TabSeparated; +use SimPod\ClickHouseClient\Param\ParamValueConverterRegistry; use SimPod\ClickHouseClient\Settings\ArraySettingsProvider; use SimPod\ClickHouseClient\Tests\ClickHouseVersion; use SimPod\ClickHouseClient\Tests\TestCaseBase; use SimPod\ClickHouseClient\Tests\WithClient; +use function bin2hex; +use function implode; use function iterator_to_array; +use function sprintf; +use function strlen; +use function strtoupper; #[CoversClass(RequestFactory::class)] #[CoversClass(PsrClickHouseClient::class)] +#[CoversClass(ParamValueConverterRegistry::class)] #[CoversClass(ServerError::class)] #[CoversClass(Json::class)] #[CoversClass(\SimPod\ClickHouseClient\Output\Json::class)] @@ -38,10 +45,41 @@ final class SelectTest extends TestCaseBase public function testSelectWithParams(): void { - $client = self::$client; - $output = $client->selectWithParams('SELECT {p1:UInt8} AS data', ['p1' => 3], new TabSeparated()); + $value = "actual\n\t\r\0\x08\x0C literal\\n \\N \\x41 'quoted' `identifier` trailing\\"; + $hex = strtoupper(bin2hex($value)); + $sql = sprintf( + <<<'CLICKHOUSE' +SELECT + {number:UInt8}, + hex({string:String}), + hex({fixed:FixedString(%d)}), + hex({nullable:Nullable(String)}), + hex({low_cardinality:LowCardinality(String)}), + hex({nested:Array(Tuple(String, UInt8))}[1].1), + hex({literal_null:Nullable(String)}), + hex(:legacy) +CLICKHOUSE, + strlen($value), + ); + $output = self::$client->selectWithParams( + $sql, + [ + 'number' => 3, + 'string' => $value, + 'fixed' => $value, + 'nullable' => $value, + 'low_cardinality' => $value, + 'nested' => [[$value, 1]], + 'literal_null' => '\\N', + 'legacy' => $value, + ], + new TabSeparated(), + ); - self::assertSame("3\n", $output->contents); + self::assertSame( + implode("\t", ['3', $hex, $hex, $hex, $hex, $hex, '5C4E', $hex]) . "\n", + $output->contents, + ); } #[DataProvider('providerJson')]