├── LICENSE ├── README.md ├── composer.json ├── phpstan-baseline.neon ├── renovate.json └── src ├── Client ├── ClickHouseAsyncClient.php ├── ClickHouseClient.php ├── Http │ ├── RequestFactory.php │ ├── RequestOptions.php │ └── RequestSettings.php ├── PsrClickHouseAsyncClient.php └── PsrClickHouseClient.php ├── Exception ├── CannotInsert.php ├── ClickHouseClientException.php ├── ServerError.php ├── UnsupportedParamType.php └── UnsupportedParamValue.php ├── Format ├── Format.php ├── Json.php ├── JsonCompact.php ├── JsonEachRow.php ├── Null_.php ├── Pretty.php ├── PrettySpace.php ├── RowBinary.php └── TabSeparated.php ├── Logger ├── LoggerChain.php ├── PsrLogger.php └── SqlLogger.php ├── Output ├── Basic.php ├── Json.php ├── JsonCompact.php ├── JsonEachRow.php ├── Null_.php └── Output.php ├── Param └── ParamValueConverterRegistry.php ├── Schema └── Table.php ├── Snippet ├── CurrentDatabase.php ├── DatabaseSize.php ├── Parts.php ├── ShowCreateTable.php ├── ShowDatabases.php ├── TableSizes.php └── Version.php ├── Sql ├── Escaper.php ├── Expression.php ├── ExpressionFactory.php ├── SqlFactory.php ├── Type.php └── ValueFormatter.php └── functions.php /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Šimon Podlipský 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHP ClickHouse Client 2 | 3 | [![Build Status](https://github.com/simPod/PhpClickHouseClient/workflows/CI/badge.svg?branch=master)](https://github.com/simPod/PhpClickHouseClient/actions) 4 | [![Code Coverage][Coverage image]][CodeCov Master] 5 | [![Downloads](https://poser.pugx.org/simpod/clickhouse-client/d/total.svg)](https://packagist.org/packages/simpod/clickhouse-client) 6 | [![Infection MSI](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2FsimPod%2FPhpClickHouseClient%2Fmaster)](https://dashboard.stryker-mutator.io/reports/github.com/simPod/PhpClickHouseClient/master) 7 | 8 | ## Motivation 9 | 10 | The library is trying not to hide any ClickHouse HTTP interface specific details. 11 | That said everything is as much transparent as possible and so object-oriented API is provided without inventing own abstractions. 12 | Naming used here is the same as in ClickHouse docs. 13 | 14 | - Works with any HTTP Client implementation ([PSR-18 compliant](https://www.php-fig.org/psr/psr-18/)) 15 | - All [ClickHouse Formats](https://clickhouse.yandex/docs/en/interfaces/formats/) support 16 | - Logging ([PSR-3 compliant](https://www.php-fig.org/psr/psr-3/)) 17 | - SQL Factory for [parameters "binding"](#parameters-binding) 18 | - [Native query parameters](#native-query-parameters) support 19 | 20 | ## Contents 21 | 22 | - [Setup](#setup) 23 | - [Logging](#logging) 24 | - [PSR Factories who?](#psr-factories-who) 25 | - [Sync API](#sync-api) 26 | - [Select](#select) 27 | - [Select With Params](#select-with-params) 28 | - [Insert](#insert) 29 | - [Async API](#async-api) 30 | - [Select](#select-1) 31 | - [Native Query Parameters](#native-query-parameters) 32 | - [Snippets](#snippets) 33 | 34 | ## Setup 35 | 36 | ```sh 37 | composer require simpod/clickhouse-client 38 | ``` 39 | 40 | 1. Read about ClickHouse [Http Interface](https://clickhouse.com/docs/en/interfaces/http/). _It's short and useful for concept understanding._ 41 | 2. Create a new instance of ClickHouse client and pass PSR factories. 42 | 1. Symfony HttpClient is recommended (performance, less bugs, maintenance) 43 | 2. The plot twist is there's no endpoint/credentials etc. config in this library, provide it via client 44 | 3. See tests 45 | 46 | ```php 47 | select( 115 | 'SELECT * FROM table', 116 | new JsonEachRow(), 117 | ['force_primary_key' => 1] 118 | ); 119 | ``` 120 | 121 | ### Select With Params 122 | 123 | `ClickHouseClient::selectWithParams()` 124 | 125 | Same as `ClickHouseClient::select()` except it also allows [parameter binding](#parameters-binding). 126 | 127 | ```php 128 | selectWithParams( 137 | 'SELECT * FROM :table', 138 | ['table' => 'table_name'], 139 | new JsonEachRow(), 140 | ['force_primary_key' => 1] 141 | ); 142 | ``` 143 | 144 | ### Insert 145 | 146 | `ClickHouseClient::insert()` 147 | 148 | ```php 149 | insert('table', $data, $columnNames); 155 | ``` 156 | 157 | If `$columnNames` is provided and is key->value array column names are generated based on it and values are passed as parameters: 158 | 159 | `$client->insert( 'table', [[1,2]], ['a' => 'Int8, 'b' => 'String'] );` generates `INSERT INTO table (a,b) VALUES ({p1:Int8},{p2:String})` and values are passed along the query. 160 | 161 | If `$columnNames` is provided column names are generated based on it: 162 | 163 | `$client->insert( 'table', [[1,2]], ['a', 'b'] );` generates `INSERT INTO table (a,b) VALUES (1,2)`. 164 | 165 | If `$columnNames` is omitted column names are read from `$data`: 166 | 167 | `$client->insert( 'table', [['a' => 1,'b' => 2]]);` generates `INSERT INTO table (a,b) VALUES (1,2)`. 168 | 169 | Column names are read only from the first item: 170 | 171 | `$client->insert( 'table', [['a' => 1,'b' => 2], ['c' => 3,'d' => 4]]);` generates `INSERT INTO table (a,b) VALUES (1,2),(3,4)`. 172 | 173 | If not provided they're not passed either: 174 | 175 | `$client->insert( 'table', [[1,2]]);` generates `INSERT INTO table VALUES (1,2)`. 176 | 177 | ## Async API 178 | 179 | ### Select 180 | 181 | ## Parameters "binding" 182 | 183 | ```php 184 | createWithParameters( 192 | 'SELECT :param', 193 | ['param' => 'value'] 194 | ); 195 | ``` 196 | This produces `SELECT 'value'` and it can be passed to `ClickHouseClient::select()`. 197 | 198 | Supported types are: 199 | - scalars 200 | - DateTimeInterface 201 | - [Expression](#expression) 202 | - objects implementing `__toString()` 203 | 204 | ## Native Query Parameters 205 | 206 | > [!TIP] 207 | > [Official docs](https://clickhouse.com/docs/en/interfaces/http#cli-queries-with-parameters) 208 | 209 | ```php 210 | selectWithParams( 217 | 'SELECT {p1:String}', 218 | ['param' => 'value'] 219 | ); 220 | ``` 221 | 222 | All types are supported (except `AggregateFunction`, `SimpleAggregateFunction` and `Nothing` by design). 223 | You can also pass `DateTimeInterface` into `Date*` types or native array into `Array`, `Tuple`, `Native` and `Geo` types 224 | 225 | ### Custom Query Parameter Value Conversion 226 | 227 | Query parameters passed to `selectWithParams()` are converted into an HTTP-API-compatible format. To overwrite an existing value converter or 228 | provide a converter for a type that the library does not (yet) support, pass these to the 229 | `SimPod\ClickHouseClient\Param\ParamValueConverterRegistry` constructor: 230 | 231 | ```php 232 | static fn (mixed $v) => $v instanceof DateTimeInterface ? $v->format('c') : throw UnsupportedParamValue::type($value) 241 | ]); 242 | 243 | $client = new PsrClickHouseClient(..., new RequestFactory($paramValueConverterRegistry, ...)); 244 | ``` 245 | 246 | Be aware that the library can not ensure that passed values have a certain type. They are passed as-is and closures must accept `mixed` values. 247 | 248 | Throw an exception of type `UnsupportedParamValue` if your converter does not support the passed value type. 249 | 250 | ### Expression 251 | 252 | To represent complex expressions there's `SimPod\ClickHouseClient\Sql\Expression` class. When passed to `SqlFactory` its value gets evaluated. 253 | 254 | To pass eg. `UUIDStringToNum('6d38d288-5b13-4714-b6e4-faa59ffd49d8')` to SQL: 255 | 256 | ```php 257 | templateAndValues( 273 | 'UUIDStringToNum(%s)', 274 | '6d38d288-5b13-4714-b6e4-faa59ffd49d8' 275 | ); 276 | ``` 277 | 278 | ## Snippets 279 | 280 | There are handy queries like getting database size, table list, current database etc. 281 | 282 | To prevent Client API pollution, those are extracted into Snippets. 283 | 284 | Example to obtain current database name: 285 | ```php 286 | simPod/renovatebot-presets:php-lib" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/Client/ClickHouseAsyncClient.php: -------------------------------------------------------------------------------- 1 | $outputFormat 16 | * @param array $settings 17 | * 18 | * @template O of Output 19 | */ 20 | public function select(string $query, Format $outputFormat, array $settings = []): PromiseInterface; 21 | 22 | /** 23 | * @param array $params 24 | * @param Format $outputFormat 25 | * @param array $settings 26 | * 27 | * @template O of Output 28 | */ 29 | public function selectWithParams( 30 | string $query, 31 | array $params, 32 | Format $outputFormat, 33 | array $settings = [], 34 | ): PromiseInterface; 35 | } 36 | -------------------------------------------------------------------------------- /src/Client/ClickHouseClient.php: -------------------------------------------------------------------------------- 1 | $settings 21 | * 22 | * @throws ClientExceptionInterface 23 | * @throws ServerError 24 | */ 25 | public function executeQuery(string $query, array $settings = []): void; 26 | 27 | /** 28 | * @param array $params 29 | * @param array $settings 30 | * 31 | * @throws ClientExceptionInterface 32 | * @throws ServerError 33 | * @throws UnsupportedParamType 34 | * @throws UnsupportedParamValue 35 | */ 36 | public function executeQueryWithParams(string $query, array $params, array $settings = []): void; 37 | 38 | /** 39 | * @param array $settings 40 | * @param Format $outputFormat 41 | * 42 | * @return O 43 | * 44 | * @throws ClientExceptionInterface 45 | * @throws ServerError 46 | * 47 | * @template O of Output 48 | */ 49 | public function select(string $query, Format $outputFormat, array $settings = []): Output; 50 | 51 | /** 52 | * @param array $settings 53 | * @param array $params 54 | * @param Format $outputFormat 55 | * 56 | * @return O 57 | * 58 | * @throws ClientExceptionInterface 59 | * @throws ServerError 60 | * @throws UnsupportedParamType 61 | * @throws UnsupportedParamValue 62 | * 63 | * @template O of Output 64 | */ 65 | public function selectWithParams(string $query, array $params, Format $outputFormat, array $settings = []): Output; 66 | 67 | /** 68 | * @param array> $values 69 | * @param list|array|null $columns 70 | * @param array $settings 71 | * 72 | * @throws CannotInsert 73 | * @throws ClientExceptionInterface 74 | * @throws ServerError 75 | * @throws UnsupportedParamType 76 | * @throws UnsupportedParamValue 77 | */ 78 | public function insert(Table|string $table, array $values, array|null $columns = null, array $settings = []): void; 79 | 80 | /** 81 | * @param array $settings 82 | * @param Format $inputFormat 83 | * 84 | * @throws ClientExceptionInterface 85 | * @throws ServerError 86 | * 87 | * @template O of Output 88 | */ 89 | public function insertWithFormat( 90 | Table|string $table, 91 | Format $inputFormat, 92 | string $data, 93 | array $settings = [], 94 | ): void; 95 | 96 | /** 97 | * @param array $settings 98 | * @param list $columns 99 | * @param Format> $inputFormat 100 | * 101 | * @throws ClientExceptionInterface 102 | * @throws CannotInsert 103 | * @throws ServerError 104 | */ 105 | public function insertPayload( 106 | Table|string $table, 107 | Format $inputFormat, 108 | StreamInterface $payload, 109 | array $columns = [], 110 | array $settings = [], 111 | ): void; 112 | } 113 | -------------------------------------------------------------------------------- /src/Client/Http/RequestFactory.php: -------------------------------------------------------------------------------- 1 | createUri($uri); 47 | } 48 | 49 | $this->uri = $uri; 50 | } 51 | 52 | /** @param array $additionalOptions */ 53 | public function initRequest( 54 | RequestSettings $requestSettings, 55 | array $additionalOptions = [], 56 | ): RequestInterface { 57 | $query = http_build_query( 58 | $requestSettings->settings + $additionalOptions, 59 | '', 60 | '&', 61 | PHP_QUERY_RFC3986, 62 | ); 63 | 64 | if ($this->uri === null) { 65 | $uri = $query === '' ? '' : '?' . $query; 66 | } else { 67 | $uriQuery = $this->uri->getQuery(); 68 | try { 69 | $uri = $this->uri->withQuery($uriQuery . ($uriQuery !== '' && $query !== '' ? '&' : '') . $query); 70 | } catch (InvalidArgumentException) { 71 | absurd(); 72 | } 73 | } 74 | 75 | return $this->requestFactory->createRequest('POST', $uri); 76 | } 77 | 78 | /** @throws UnsupportedParamType */ 79 | public function prepareSqlRequest( 80 | string $sql, 81 | RequestSettings $requestSettings, 82 | RequestOptions $requestOptions, 83 | ): RequestInterface { 84 | $request = $this->initRequest($requestSettings); 85 | 86 | preg_match_all('~\{([a-zA-Z\d_]+):([a-zA-Z\d ]+(\(.+\))?)}~', $sql, $matches); 87 | if ($matches[0] === []) { 88 | $body = $this->streamFactory->createStream($sql); 89 | try { 90 | return $request->withBody($body); 91 | } catch (InvalidArgumentException) { 92 | absurd(); 93 | } 94 | } 95 | 96 | /** @var array $paramToType */ 97 | $paramToType = array_reduce( 98 | array_keys($matches[1]), 99 | static function (array $acc, string|int $k) use ($matches) { 100 | $acc[$matches[1][$k]] = Type::fromString($matches[2][$k]); 101 | 102 | return $acc; 103 | }, 104 | [], 105 | ); 106 | 107 | $streamElements = [['name' => 'query', 'contents' => $sql]]; 108 | foreach ($requestOptions->params as $name => $value) { 109 | $type = $paramToType[$name] ?? null; 110 | if ($type === null) { 111 | continue; 112 | } 113 | 114 | $streamElements[] = [ 115 | 'name' => 'param_' . $name, 116 | 'contents' => $this->paramValueConverterRegistry->get($type)($value, $type, false), 117 | ]; 118 | } 119 | 120 | try { 121 | $body = new MultipartStream($streamElements); 122 | $request = $request->withBody($body) 123 | ->withHeader('Content-Type', 'multipart/form-data; boundary=' . $body->getBoundary()); 124 | } catch (InvalidArgumentException) { 125 | absurd(); 126 | } 127 | 128 | return $request; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/Client/Http/RequestOptions.php: -------------------------------------------------------------------------------- 1 | $params */ 10 | public function __construct( 11 | public array $params, 12 | ) { 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Client/Http/RequestSettings.php: -------------------------------------------------------------------------------- 1 | */ 10 | public array $settings; 11 | 12 | /** 13 | * @param array $defaultSettings 14 | * @param array $querySettings 15 | */ 16 | public function __construct( 17 | array $defaultSettings, 18 | array $querySettings, 19 | ) { 20 | $this->settings = $querySettings + $defaultSettings; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Client/PsrClickHouseAsyncClient.php: -------------------------------------------------------------------------------- 1 | $defaultSettings */ 29 | public function __construct( 30 | private HttpAsyncClient $asyncClient, 31 | private RequestFactory $requestFactory, 32 | private SqlLogger|null $sqlLogger = null, 33 | private array $defaultSettings = [], 34 | ) { 35 | $this->sqlFactory = new SqlFactory(new ValueFormatter()); 36 | } 37 | 38 | /** 39 | * {@inheritDoc} 40 | * 41 | * @throws Exception 42 | */ 43 | public function select(string $query, Format $outputFormat, array $settings = []): PromiseInterface 44 | { 45 | return $this->selectWithParams($query, [], $outputFormat, $settings); 46 | } 47 | 48 | /** 49 | * {@inheritDoc} 50 | * 51 | * @throws Exception 52 | */ 53 | public function selectWithParams( 54 | string $query, 55 | array $params, 56 | Format $outputFormat, 57 | array $settings = [], 58 | ): PromiseInterface { 59 | $formatClause = $outputFormat::toSql(); 60 | 61 | $sql = $this->sqlFactory->createWithParameters($query, $params); 62 | 63 | return $this->executeRequest( 64 | << $outputFormat::output( 71 | $response->getBody()->__toString(), 72 | ), 73 | ); 74 | } 75 | 76 | /** 77 | * @param array $params 78 | * @param array $settings 79 | * @param (callable(ResponseInterface):mixed)|null $processResponse 80 | * 81 | * @throws Exception 82 | */ 83 | private function executeRequest( 84 | string $sql, 85 | array $params, 86 | array $settings = [], 87 | callable|null $processResponse = null, 88 | ): PromiseInterface { 89 | $request = $this->requestFactory->prepareSqlRequest( 90 | $sql, 91 | new RequestSettings( 92 | $this->defaultSettings, 93 | $settings, 94 | ), 95 | new RequestOptions( 96 | $params, 97 | ), 98 | ); 99 | 100 | $id = uniqid('', true); 101 | $this->sqlLogger?->startQuery($id, $sql); 102 | 103 | return Create::promiseFor( 104 | $this->asyncClient->sendAsyncRequest($request), 105 | ) 106 | ->then( 107 | function (ResponseInterface $response) use ($id, $processResponse) { 108 | $this->sqlLogger?->stopQuery($id); 109 | 110 | if ($response->getStatusCode() !== 200) { 111 | throw ServerError::fromResponse($response); 112 | } 113 | 114 | if ($processResponse === null) { 115 | return $response; 116 | } 117 | 118 | return $processResponse($response); 119 | }, 120 | fn () => $this->sqlLogger?->stopQuery($id), 121 | ); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/Client/PsrClickHouseClient.php: -------------------------------------------------------------------------------- 1 | $defaultSettings */ 46 | public function __construct( 47 | private ClientInterface $client, 48 | private RequestFactory $requestFactory, 49 | private SqlLogger|null $sqlLogger = null, 50 | private array $defaultSettings = [], 51 | ) { 52 | $this->valueFormatter = new ValueFormatter(); 53 | $this->sqlFactory = new SqlFactory($this->valueFormatter); 54 | } 55 | 56 | public function executeQuery(string $query, array $settings = []): void 57 | { 58 | try { 59 | $this->executeRequest($query, params: [], settings: $settings); 60 | } catch (UnsupportedParamType) { 61 | absurd(); 62 | } 63 | } 64 | 65 | public function executeQueryWithParams(string $query, array $params, array $settings = []): void 66 | { 67 | $this->executeRequest( 68 | $this->sqlFactory->createWithParameters($query, $params), 69 | params: $params, 70 | settings: $settings, 71 | ); 72 | } 73 | 74 | public function select(string $query, Format $outputFormat, array $settings = []): Output 75 | { 76 | try { 77 | return $this->selectWithParams($query, params: [], outputFormat: $outputFormat, settings: $settings); 78 | } catch (UnsupportedParamValue | UnsupportedParamType) { 79 | absurd(); 80 | } 81 | } 82 | 83 | public function selectWithParams(string $query, array $params, Format $outputFormat, array $settings = []): Output 84 | { 85 | $formatClause = $outputFormat::toSql(); 86 | 87 | $sql = $this->sqlFactory->createWithParameters($query, $params); 88 | 89 | $response = $this->executeRequest( 90 | <<getBody()->__toString()); 99 | } 100 | 101 | public function insert(Table|string $table, array $values, array|null $columns = null, array $settings = []): void 102 | { 103 | if ($values === []) { 104 | throw CannotInsert::noValues(); 105 | } 106 | 107 | if (! $table instanceof Table) { 108 | $table = new Table($table); 109 | } 110 | 111 | $tableName = $table->fullName(); 112 | 113 | if (is_array($columns) && ! array_is_list($columns)) { 114 | $columnsSql = sprintf('(%s)', implode(',', array_keys($columns))); 115 | 116 | $types = array_values($columns); 117 | 118 | $params = []; 119 | $pN = 1; 120 | foreach ($values as $row) { 121 | foreach ($row as $value) { 122 | $params['p' . $pN++] = $value; 123 | } 124 | } 125 | 126 | $pN = 1; 127 | $valuesSql = implode( 128 | ',', 129 | array_map( 130 | static function (array $row) use (&$pN, $types): string { 131 | return sprintf( 132 | '(%s)', 133 | implode(',', array_map(static function ($i) use (&$pN, $types) { 134 | return sprintf('{p%d:%s}', $pN++, $types[$i]); 135 | }, array_keys($row))), 136 | ); 137 | }, 138 | $values, 139 | ), 140 | ); 141 | 142 | $this->executeRequest( 143 | << sprintf( 169 | '(%s)', 170 | implode(',', $this->valueFormatter->mapFormat($map)), 171 | ), 172 | $values, 173 | ), 174 | ); 175 | 176 | try { 177 | $this->executeRequest( 178 | <<fullName(); 204 | 205 | try { 206 | $this->executeRequest( 207 | <<getSize() === 0) { 226 | throw CannotInsert::noValues(); 227 | } 228 | 229 | $formatSql = $inputFormat::toSql(); 230 | 231 | if (! $table instanceof Table) { 232 | $table = new Table($table); 233 | } 234 | 235 | $tableName = $table->fullName(); 236 | 237 | $columnsSql = $columns === [] ? '' : sprintf('(%s)', implode(',', $columns)); 238 | 239 | $sql = <<requestFactory->initRequest( 244 | new RequestSettings( 245 | $this->defaultSettings, 246 | $settings, 247 | ), 248 | ['query' => $sql], 249 | ); 250 | 251 | try { 252 | $request = $request->withBody($payload); 253 | } catch (InvalidArgumentException) { 254 | absurd(); 255 | } 256 | 257 | $this->sendHttpRequest($request, $sql); 258 | } 259 | 260 | /** 261 | * @param array $params 262 | * @param array $settings 263 | * 264 | * @throws ServerError 265 | * @throws ClientExceptionInterface 266 | * @throws UnsupportedParamType 267 | */ 268 | private function executeRequest(string $sql, array $params, array $settings): ResponseInterface 269 | { 270 | $request = $this->requestFactory->prepareSqlRequest( 271 | $sql, 272 | new RequestSettings( 273 | $this->defaultSettings, 274 | $settings, 275 | ), 276 | new RequestOptions( 277 | $params, 278 | ), 279 | ); 280 | 281 | return $this->sendHttpRequest($request, $sql); 282 | } 283 | 284 | /** 285 | * @throws ClientExceptionInterface 286 | * @throws ServerError 287 | */ 288 | private function sendHttpRequest(RequestInterface $request, string $sql): ResponseInterface 289 | { 290 | $id = uniqid('', true); 291 | $this->sqlLogger?->startQuery($id, $sql); 292 | 293 | try { 294 | $response = $this->client->sendRequest($request); 295 | } finally { 296 | $this->sqlLogger?->stopQuery($id); 297 | } 298 | 299 | if ($response->getStatusCode() !== 200) { 300 | throw ServerError::fromResponse($response); 301 | } 302 | 303 | return $response; 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /src/Exception/CannotInsert.php: -------------------------------------------------------------------------------- 1 | getBody()->__toString(); 17 | 18 | return new self( 19 | $bodyContent, 20 | code: preg_match('~^Code: (\\d+). DB::Exception:~', $bodyContent, $matches) === 1 ? (int) $matches[1] : 0, 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Exception/UnsupportedParamType.php: -------------------------------------------------------------------------------- 1 | name); 15 | } 16 | 17 | public static function fromString(string $type): self 18 | { 19 | return new self($type); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Exception/UnsupportedParamValue.php: -------------------------------------------------------------------------------- 1 | > 13 | */ 14 | final class Json implements Format 15 | { 16 | /** @throws JsonException */ 17 | public static function output(string $contents): Output 18 | { 19 | /** @var \SimPod\ClickHouseClient\Output\Json $output */ 20 | $output = new \SimPod\ClickHouseClient\Output\Json($contents); 21 | 22 | return $output; 23 | } 24 | 25 | public static function toSql(): string 26 | { 27 | return 'FORMAT JSON'; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Format/JsonCompact.php: -------------------------------------------------------------------------------- 1 | > 13 | */ 14 | final class JsonCompact implements Format 15 | { 16 | /** @throws JsonException */ 17 | public static function output(string $contents): Output 18 | { 19 | /** @var \SimPod\ClickHouseClient\Output\JsonCompact $output */ 20 | $output = new \SimPod\ClickHouseClient\Output\JsonCompact($contents); 21 | 22 | return $output; 23 | } 24 | 25 | public static function toSql(): string 26 | { 27 | return 'FORMAT JSONCompact'; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Format/JsonEachRow.php: -------------------------------------------------------------------------------- 1 | > 13 | */ 14 | final class JsonEachRow implements Format 15 | { 16 | /** @throws JsonException */ 17 | public static function output(string $contents): Output 18 | { 19 | /** @var \SimPod\ClickHouseClient\Output\JsonEachRow $output */ 20 | $output = new \SimPod\ClickHouseClient\Output\JsonEachRow($contents); 21 | 22 | return $output; 23 | } 24 | 25 | public static function toSql(): string 26 | { 27 | return 'FORMAT JSONEachRow'; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Format/Null_.php: -------------------------------------------------------------------------------- 1 | > 14 | */ 15 | final class Null_ implements Format 16 | { 17 | public static function output(string $contents): Output 18 | { 19 | /** @var \SimPod\ClickHouseClient\Output\Null_ $output */ 20 | $output = new \SimPod\ClickHouseClient\Output\Null_($contents); 21 | 22 | return $output; 23 | } 24 | 25 | public static function toSql(): string 26 | { 27 | return 'FORMAT Null'; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Format/Pretty.php: -------------------------------------------------------------------------------- 1 | > 13 | */ 14 | final class Pretty implements Format 15 | { 16 | public static function output(string $contents): Output 17 | { 18 | /** @var Basic $output */ 19 | $output = new Basic($contents); 20 | 21 | return $output; 22 | } 23 | 24 | public static function toSql(): string 25 | { 26 | return 'FORMAT Pretty'; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Format/PrettySpace.php: -------------------------------------------------------------------------------- 1 | > 13 | */ 14 | final class PrettySpace implements Format 15 | { 16 | public static function output(string $contents): Output 17 | { 18 | /** @var Basic $output */ 19 | $output = new Basic($contents); 20 | 21 | return $output; 22 | } 23 | 24 | public static function toSql(): string 25 | { 26 | return 'FORMAT PrettySpace'; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Format/RowBinary.php: -------------------------------------------------------------------------------- 1 | > 13 | */ 14 | final class RowBinary implements Format 15 | { 16 | public static function output(string $contents): Output 17 | { 18 | /** @var Basic $output */ 19 | $output = new Basic($contents); 20 | 21 | return $output; 22 | } 23 | 24 | public static function toSql(): string 25 | { 26 | return 'FORMAT RowBinary'; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Format/TabSeparated.php: -------------------------------------------------------------------------------- 1 | > 13 | */ 14 | final class TabSeparated implements Format 15 | { 16 | public static function output(string $contents): Output 17 | { 18 | /** @var Basic $output */ 19 | $output = new Basic($contents); 20 | 21 | return $output; 22 | } 23 | 24 | public static function toSql(): string 25 | { 26 | return 'FORMAT TabSeparated'; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Logger/LoggerChain.php: -------------------------------------------------------------------------------- 1 | loggers as $logger) { 17 | $logger->startQuery($id, $sql); 18 | } 19 | } 20 | 21 | public function stopQuery(string $id): void 22 | { 23 | foreach ($this->loggers as $logger) { 24 | $logger->stopQuery($id); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Logger/PsrLogger.php: -------------------------------------------------------------------------------- 1 | logger->debug($sql); 18 | } 19 | 20 | public function stopQuery(string $id): void 21 | { 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Logger/SqlLogger.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class Basic implements Output 13 | { 14 | public function __construct(public string $contents) 15 | { 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Output/Json.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | final class Json implements Output 19 | { 20 | /** @var list */ 21 | public array $data; 22 | 23 | /** @var array */ 24 | public array $meta; 25 | 26 | public int $rows; 27 | 28 | public int|null $rowsBeforeLimitAtLeast; 29 | 30 | /** @var array{elapsed: float, rows_read: int, bytes_read: int} */ 31 | public array $statistics; 32 | 33 | /** @throws JsonException */ 34 | public function __construct(string $contentsJson) 35 | { 36 | /** 37 | * @var array{ 38 | * data: list, 39 | * meta: array, 40 | * rows: int, 41 | * rows_before_limit_at_least?: int, 42 | * statistics: array{elapsed: float, rows_read: int, bytes_read: int} 43 | * } $contents 44 | */ 45 | $contents = json_decode($contentsJson, true, flags: JSON_THROW_ON_ERROR); 46 | $this->data = $contents['data']; 47 | $this->meta = $contents['meta']; 48 | $this->rows = $contents['rows']; 49 | $this->rowsBeforeLimitAtLeast = $contents['rows_before_limit_at_least'] ?? null; 50 | $this->statistics = $contents['statistics']; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Output/JsonCompact.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | final class JsonCompact implements Output 19 | { 20 | /** @var list */ 21 | public array $data; 22 | 23 | /** @var array */ 24 | public array $meta; 25 | 26 | public int $rows; 27 | 28 | public int|null $rowsBeforeLimitAtLeast; 29 | 30 | /** @var array{elapsed: float, rows_read: int, bytes_read: int} */ 31 | public array $statistics; 32 | 33 | /** @throws JsonException */ 34 | public function __construct(string $contentsJson) 35 | { 36 | /** 37 | * @var array{ 38 | * data: list, 39 | * meta: array, 40 | * rows: int, 41 | * rows_before_limit_at_least?: int, 42 | * statistics: array{elapsed: float, rows_read: int, bytes_read: int} 43 | * } $contents 44 | */ 45 | $contents = json_decode($contentsJson, true, flags: JSON_THROW_ON_ERROR); 46 | $this->data = $contents['data']; 47 | $this->meta = $contents['meta']; 48 | $this->rows = $contents['rows']; 49 | $this->rowsBeforeLimitAtLeast = $contents['rows_before_limit_at_least'] ?? null; 50 | $this->statistics = $contents['statistics']; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Output/JsonEachRow.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | final class JsonEachRow implements Output 21 | { 22 | /** @var list */ 23 | public array $data; 24 | 25 | /** @throws JsonException */ 26 | public function __construct(string $contentsJson) 27 | { 28 | /** @var list $contents */ 29 | $contents = json_decode( 30 | sprintf('[%s]', str_replace("}\n{", '},{', $contentsJson)), 31 | true, 32 | flags: JSON_THROW_ON_ERROR, 33 | ); 34 | $this->data = $contents; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Output/Null_.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class Null_ implements Output 15 | { 16 | public function __construct(string $_) 17 | { 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Output/Output.php: -------------------------------------------------------------------------------- 1 | 34 | */ 35 | final class ParamValueConverterRegistry 36 | { 37 | private const CaseInsensitiveTypes = [ 38 | 'bool', 39 | 'date', 40 | 'date32', 41 | 'datetime', 42 | 'datetime32', 43 | 'datetime64', 44 | 'decimal', 45 | 'decimal32', 46 | 'decimal64', 47 | 'decimal128', 48 | 'decimal256', 49 | 'enum', 50 | 'json', 51 | ]; 52 | 53 | /** @phpstan-var ConverterRegistry */ 54 | private array $registry; 55 | 56 | /** @phpstan-param ConverterRegistry $registry */ 57 | public function __construct(array $registry = []) 58 | { 59 | $formatPoint = static fn (array $point) => sprintf('(%s)', implode(',', $point)); 60 | // phpcs:ignore SlevomatCodingStandard.Functions.RequireArrowFunction.RequiredArrowFunction 61 | $formatRingOrLineString = static function (array $v) use ($formatPoint) { 62 | /** @phpstan-var array> $v */ 63 | return sprintf('[%s]', implode( 64 | ',', 65 | array_map($formatPoint, $v), 66 | )); 67 | }; 68 | // phpcs:ignore SlevomatCodingStandard.Functions.RequireArrowFunction.RequiredArrowFunction 69 | $formatPolygonOrMultiLineString = static function (array $v) use ($formatRingOrLineString) { 70 | /** @phpstan-var array> $v */ 71 | return sprintf('[%s]', implode( 72 | ',', 73 | array_map($formatRingOrLineString, $v), 74 | )); 75 | }; 76 | 77 | /** @phpstan-var ConverterRegistry $defaultRegistry */ 78 | $defaultRegistry = [ 79 | 'String' => self::stringConverter(), 80 | 'FixedString' => self::stringConverter(), 81 | 82 | 'UUID' => self::stringConverter(), 83 | 84 | 'Nullable' => fn (mixed $v, Type $type) => $this->get($type->params)($v, null, false), 85 | 'LowCardinality' => fn (mixed $v, Type $type) => $this->get($type->params)($v, null, false), 86 | 87 | 'decimal' => self::decimalConverter(), 88 | 'decimal32' => self::decimalConverter(), 89 | 'decimal64' => self::decimalConverter(), 90 | 'decimal128' => self::decimalConverter(), 91 | 'decimal256' => self::decimalConverter(), 92 | 93 | 'bool' => static fn (bool $value) => $value, 94 | 95 | 'date' => self::dateConverter(), 96 | 'date32' => self::dateConverter(), 97 | 'datetime' => self::dateTimeConverter(), 98 | 'datetime32' => self::dateTimeConverter(), 99 | 'datetime64' => static function (mixed $value) { 100 | if ($value instanceof DateTimeInterface) { 101 | return $value->format('U.u'); 102 | } 103 | 104 | if (is_string($value) || is_float($value) || is_int($value)) { 105 | return $value; 106 | } 107 | 108 | throw UnsupportedParamValue::type($value); 109 | }, 110 | 111 | 'Dynamic' => self::noopConverter(), 112 | 'Variant' => self::noopConverter(), 113 | 114 | 'IPv4' => self::noopConverter(), 115 | 'IPv6' => self::noopConverter(), 116 | 117 | 'enum' => self::noopConverter(), 118 | 'Enum8' => self::noopConverter(), 119 | 'Enum16' => self::noopConverter(), 120 | 'Enum32' => self::noopConverter(), 121 | 'Enum64' => self::noopConverter(), 122 | 123 | 'json' => static fn (array|string $value) => is_string($value) ? $value : json_encode($value), 124 | 'Map' => self::noopConverter(), 125 | 'Nested' => function (array|string $v, Type $type) { 126 | if (is_string($v)) { 127 | return $v; 128 | } 129 | 130 | $types = array_map(static fn ($type) => explode(' ', trim($type))[1], $this->splitTypes($type->params)); 131 | 132 | /** @phpstan-var array> $v */ 133 | return sprintf('[%s]', implode(',', array_map( 134 | fn (array $row) => sprintf('(%s)', implode(',', array_map( 135 | fn (int|string $i) => $this->get($types[$i])($row[$i], $types[$i], true), 136 | array_keys($row), 137 | ))), 138 | $v, 139 | ))); 140 | }, 141 | 142 | 'Float32' => self::floatConverter(), 143 | 'Float64' => self::floatConverter(), 144 | 145 | 'Int8' => self::intConverter(), 146 | 'Int16' => self::intConverter(), 147 | 'Int32' => self::intConverter(), 148 | 'Int64' => self::intConverter(), 149 | 'Int128' => self::intConverter(), 150 | 'Int256' => self::intConverter(), 151 | 152 | 'UInt8' => self::intConverter(), 153 | 'UInt16' => self::intConverter(), 154 | 'UInt32' => self::intConverter(), 155 | 'UInt64' => self::intConverter(), 156 | 'UInt128' => self::intConverter(), 157 | 'UInt256' => self::intConverter(), 158 | 159 | 'IntervalNanosecond' => self::dateIntervalConverter(), 160 | 'IntervalMicrosecond' => self::dateIntervalConverter(), 161 | 'IntervalMillisecond' => self::dateIntervalConverter(), 162 | 'IntervalSecond' => self::dateIntervalConverter(), 163 | 'IntervalMinute' => self::dateIntervalConverter(), 164 | 'IntervalHour' => self::dateIntervalConverter(), 165 | 'IntervalDay' => self::dateIntervalConverter(), 166 | 'IntervalWeek' => self::dateIntervalConverter(), 167 | 'IntervalMonth' => self::dateIntervalConverter(), 168 | 'IntervalQuarter' => self::dateIntervalConverter(), 169 | 'IntervalYear' => self::dateIntervalConverter(), 170 | 171 | 'Point' => static fn (string|array $v) => is_string($v) 172 | ? $v 173 | : $formatPoint($v), 174 | 'Ring' => static fn (string|array $v) => is_string($v) 175 | ? $v 176 | : $formatRingOrLineString($v), 177 | 'LineString' => static fn (string|array $v) => is_string($v) 178 | ? $v 179 | : $formatRingOrLineString($v), 180 | 'MultiLineString' => static fn (string|array $v) => is_string($v) 181 | ? $v 182 | : $formatPolygonOrMultiLineString($v), 183 | 'Polygon' => static fn (string|array $v) => is_string($v) 184 | ? $v 185 | : $formatPolygonOrMultiLineString($v), 186 | 'MultiPolygon' => static fn (string|array $v) => is_string($v) 187 | ? $v 188 | // phpcs:ignore SlevomatCodingStandard.Functions.RequireArrowFunction.RequiredArrowFunction 189 | : (static function (array $vv) use ($formatPolygonOrMultiLineString) { 190 | /** @phpstan-var array> $vv */ 191 | return sprintf('[%s]', implode( 192 | ',', 193 | array_map($formatPolygonOrMultiLineString, $vv), 194 | )); 195 | })($v), 196 | 197 | 'Array' => fn (array|string $v, Type $type) => is_string($v) 198 | ? $v 199 | : sprintf('[%s]', implode( 200 | ',', 201 | array_map(function (mixed $v) use ($type) { 202 | $innerType = Type::fromString($type->params); 203 | 204 | return $this->get($innerType)($v, $innerType, true); 205 | }, $v), 206 | )), 207 | 'Tuple' => function (mixed $v, Type $type) { 208 | if (! is_array($v)) { 209 | return $v; 210 | } 211 | 212 | $innerTypes = $this->splitTypes($type->params); 213 | 214 | $innerExpression = implode( 215 | ',', 216 | array_map(function (int $i) use ($innerTypes, $v) { 217 | $innerType = Type::fromString($innerTypes[$i]); 218 | 219 | return $this->get($innerType)($v[$i], $innerType, true); 220 | }, array_keys($v)), 221 | ); 222 | 223 | return '(' . $innerExpression . ')'; 224 | }, 225 | ]; 226 | $this->registry = array_merge($defaultRegistry, $registry); 227 | } 228 | 229 | /** 230 | * @phpstan-return Converter 231 | * 232 | * @throws UnsupportedParamType 233 | */ 234 | public function get(Type|string $type): Closure 235 | { 236 | $typeName = is_string($type) ? $type : $type->name; 237 | 238 | $converter = $this->registry[$typeName] ?? null; 239 | if ($converter !== null) { 240 | return $converter; 241 | } 242 | 243 | $typeName = strtolower($typeName); 244 | $converter = $this->registry[$typeName] ?? null; 245 | if ($converter !== null && in_array($typeName, self::CaseInsensitiveTypes, true)) { 246 | return $converter; 247 | } 248 | 249 | return throw is_string($type) 250 | ? UnsupportedParamType::fromString($type) : UnsupportedParamType::fromType($type); 251 | } 252 | 253 | private static function stringConverter(): Closure 254 | { 255 | return static fn ( 256 | string $value, 257 | Type|string|null $type = null, 258 | bool $nested = false, 259 | ) => $nested ? "'" . Escaper::escape($value) . "'" : $value; 260 | } 261 | 262 | private static function noopConverter(): Closure 263 | { 264 | return static fn (mixed $value) => $value; 265 | } 266 | 267 | private static function floatConverter(): Closure 268 | { 269 | return static fn (float|string $value) => $value; 270 | } 271 | 272 | private static function intConverter(): Closure 273 | { 274 | return static fn (int|string $value) => $value; 275 | } 276 | 277 | private static function decimalConverter(): Closure 278 | { 279 | return static fn (float|int|string $value) => $value; 280 | } 281 | 282 | private static function dateConverter(): Closure 283 | { 284 | return static function (mixed $value) { 285 | if ($value instanceof DateTimeInterface) { 286 | return $value->format('Y-m-d'); 287 | } 288 | 289 | if (is_string($value) || is_float($value) || is_int($value)) { 290 | return $value; 291 | } 292 | 293 | throw UnsupportedParamValue::type($value); 294 | }; 295 | } 296 | 297 | private static function dateTimeConverter(): Closure 298 | { 299 | return static function (mixed $value) { 300 | if ($value instanceof DateTimeInterface) { 301 | return $value->getTimestamp(); 302 | } 303 | 304 | if (is_string($value) || is_float($value) || is_int($value)) { 305 | return $value; 306 | } 307 | 308 | throw UnsupportedParamValue::type($value); 309 | }; 310 | } 311 | 312 | private static function dateIntervalConverter(): Closure 313 | { 314 | return static fn (int|float $v) => $v; 315 | } 316 | 317 | /** @return list */ 318 | private function splitTypes(string $types): array 319 | { 320 | $result = []; 321 | $depth = 0; 322 | $current = ''; 323 | 324 | for ($i = 0; $i < strlen($types); $i++) { 325 | $char = $types[$i]; 326 | if ($char === '(') { 327 | $depth++; 328 | } elseif ($char === ')') { 329 | $depth--; 330 | } elseif ($char === ',' && $depth === 0) { 331 | $current = trim($current); 332 | $result[] = $current; 333 | $current = ''; 334 | 335 | continue; 336 | } 337 | 338 | $current .= $char; 339 | } 340 | 341 | $current = trim($current); 342 | 343 | if ($current !== '') { 344 | $result[] = $current; 345 | } 346 | 347 | return $result; 348 | } 349 | } 350 | -------------------------------------------------------------------------------- /src/Schema/Table.php: -------------------------------------------------------------------------------- 1 | name[0] === '`' && $this->name[-1] === '`' 20 | ? $this->name 21 | : Escaper::quoteIdentifier($this->name); 22 | 23 | if ($this->database === null) { 24 | return $escapedName; 25 | } 26 | 27 | $escapedDatabase = $this->database[0] === '`' && $this->database[-1] === '`' 28 | ? $this->database 29 | : Escaper::quoteIdentifier($this->database); 30 | 31 | return $escapedDatabase . '.' . $escapedName; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Snippet/CurrentDatabase.php: -------------------------------------------------------------------------------- 1 | $format */ 21 | $format = new JsonEachRow(); 22 | 23 | $currentDatabase = $clickHouseClient->select( 24 | <<<'CLICKHOUSE' 25 | SELECT currentDatabase() AS database 26 | CLICKHOUSE, 27 | $format, 28 | ); 29 | 30 | return $currentDatabase->data[0]['database']; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Snippet/DatabaseSize.php: -------------------------------------------------------------------------------- 1 | $format */ 26 | $format = new JsonEachRow(); 27 | 28 | $currentDatabase = $clickHouseClient->selectWithParams( 29 | <<<'CLICKHOUSE' 30 | SELECT sum(bytes) AS size 31 | FROM system.parts 32 | WHERE active AND database=:database 33 | CLICKHOUSE, 34 | ['database' => $databaseName ?? Expression::new('currentDatabase()')], 35 | $format, 36 | ); 37 | 38 | return (int) $currentDatabase->data[0]['size']; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Snippet/Parts.php: -------------------------------------------------------------------------------- 1 | > 20 | * 21 | * @throws ClientExceptionInterface 22 | * @throws ServerError 23 | * @throws UnsupportedParamType 24 | * @throws UnsupportedParamValue 25 | */ 26 | public static function run( 27 | ClickHouseClient $clickHouseClient, 28 | string $database, 29 | string $table, 30 | bool|null $active = null, 31 | ): array { 32 | $whereActiveClause = $active === null ? '' : sprintf(' AND active = %d', $active); 33 | 34 | /** @var JsonEachRow> $format */ 35 | $format = new JsonEachRow(); 36 | 37 | $output = $clickHouseClient->selectWithParams( 38 | << $database, 48 | 'table' => $table, 49 | ], 50 | $format, 51 | ); 52 | 53 | return $output->data; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Snippet/ShowCreateTable.php: -------------------------------------------------------------------------------- 1 | $format */ 21 | $format = new JsonEachRow(); 22 | 23 | $output = $clickHouseClient->select( 24 | <<data[0]['statement']; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Snippet/ShowDatabases.php: -------------------------------------------------------------------------------- 1 | 18 | * 19 | * @throws ClientExceptionInterface 20 | * @throws ServerError 21 | */ 22 | public static function run(ClickHouseClient $clickHouseClient): array 23 | { 24 | /** @var JsonEachRow $format */ 25 | $format = new JsonEachRow(); 26 | 27 | $output = $clickHouseClient->select( 28 | <<<'CLICKHOUSE' 29 | SHOW DATABASES 30 | CLICKHOUSE, 31 | $format, 32 | ); 33 | 34 | return array_map( 35 | static fn (array $database): string => $database['name'], 36 | $output->data, 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Snippet/TableSizes.php: -------------------------------------------------------------------------------- 1 | 20 | * 21 | * @throws ClientExceptionInterface 22 | * @throws ServerError 23 | * @throws UnsupportedParamType 24 | * @throws UnsupportedParamValue 25 | */ 26 | public static function run(ClickHouseClient $clickHouseClient, string|null $databaseName = null): array 27 | { 28 | /** @var JsonEachRow $format */ 29 | $format = new JsonEachRow(); 30 | 31 | return $clickHouseClient->selectWithParams( 32 | <<<'CLICKHOUSE' 33 | SELECT 34 | name AS table, 35 | database, 36 | max(size) AS size, 37 | min(min_date) AS min_date, 38 | max(max_date) AS max_date 39 | FROM system.tables 40 | ANY LEFT JOIN ( 41 | SELECT 42 | table, 43 | database, 44 | sum(bytes) AS size, 45 | min(min_date) AS min_date, 46 | max(max_date) AS max_date 47 | FROM system.parts 48 | WHERE active AND database = :database 49 | GROUP BY table,database 50 | ) parts USING ( table, database ) 51 | WHERE database = :database AND storage_policy <> '' 52 | GROUP BY table, database 53 | CLICKHOUSE, 54 | ['database' => $databaseName ?? Expression::new('currentDatabase()')], 55 | $format, 56 | )->data; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Snippet/Version.php: -------------------------------------------------------------------------------- 1 | $format */ 21 | $format = new JsonEachRow(); 22 | 23 | $output = $clickHouseClient->select( 24 | <<<'CLICKHOUSE' 25 | SELECT version() AS version 26 | CLICKHOUSE, 27 | $format, 28 | ); 29 | 30 | return $output->data[0]['version']; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Sql/Escaper.php: -------------------------------------------------------------------------------- 1 | innerExpression = $expression; 14 | } 15 | 16 | public static function new(string $expression): self 17 | { 18 | return new self($expression); 19 | } 20 | 21 | public function __toString(): string 22 | { 23 | return $this->innerExpression; 24 | } 25 | 26 | public function toString(): string 27 | { 28 | return $this->innerExpression; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Sql/ExpressionFactory.php: -------------------------------------------------------------------------------- 1 | valueFormatter, 'format'], $values)), 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Sql/SqlFactory.php: -------------------------------------------------------------------------------- 1 | $parameters 24 | * 25 | * @throws UnsupportedParamValue 26 | */ 27 | public function createWithParameters(string $query, array $parameters): string 28 | { 29 | /** @var mixed $value */ 30 | foreach ($parameters as $name => $value) { 31 | $query = preg_replace( 32 | sprintf('~:%s(?!\w)~', $name), 33 | str_replace('\\', '\\\\', $this->valueFormatter->format($value, $name, $query)), 34 | $query, 35 | ); 36 | assert(is_string($query)); 37 | } 38 | 39 | $query = preg_replace('~ ?=([\s]*?)IS NULL~', '$1IS NULL', $query); 40 | assert(is_string($query)); 41 | 42 | return $query; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Sql/Type.php: -------------------------------------------------------------------------------- 1 | value) 60 | ? "'" . Escaper::escape($value->value) . "'" 61 | : (string) $value->value; 62 | } 63 | 64 | if ($value instanceof DateTimeInterface) { 65 | return (string) $value->getTimestamp(); 66 | } 67 | 68 | if ($value instanceof Expression) { 69 | return (string) $value; 70 | } 71 | 72 | if (is_object($value) && method_exists($value, '__toString')) { 73 | return "'" . Escaper::escape((string) $value) . "'"; 74 | } 75 | 76 | if (is_array($value)) { 77 | if ( 78 | $paramName !== null && $sql !== null 79 | && preg_match(sprintf('~\s+?IN\s+?\\(:%s\\)~', $paramName), $sql) === 1 80 | ) { 81 | if ($value === []) { 82 | throw UnsupportedParamValue::value($value); 83 | } 84 | 85 | $firstValue = $value[array_key_first($value)]; 86 | $mapper = is_array($firstValue) 87 | ? function ($value): string { 88 | assert(is_array($value)); 89 | 90 | return sprintf( 91 | '(%s)', 92 | implode( 93 | ',', 94 | array_map(fn ($val) => $this->format($val), $value), 95 | ), 96 | ); 97 | } 98 | : fn ($value): string => $value === null ? 'NULL' : $this->format($value); 99 | 100 | return implode( 101 | ',', 102 | array_map($mapper, $value), 103 | ); 104 | } 105 | 106 | return $this->formatArray($value); 107 | } 108 | 109 | throw UnsupportedParamValue::type($value); 110 | } 111 | 112 | /** 113 | * @param array $values 114 | * 115 | * @return array 116 | * 117 | * @throws UnsupportedParamValue 118 | */ 119 | public function mapFormat(array $values): array 120 | { 121 | return array_map( 122 | function ($value): string { 123 | if ($value === null) { 124 | return 'NULL'; 125 | } 126 | 127 | return $this->format($value); 128 | }, 129 | $values, 130 | ); 131 | } 132 | 133 | /** 134 | * @param array $value 135 | * 136 | * @throws UnsupportedParamValue 137 | */ 138 | private function formatArray(array $value): string 139 | { 140 | return sprintf( 141 | '[%s]', 142 | implode( 143 | ',', 144 | array_map( 145 | function ($value): string { 146 | if ($value === null) { 147 | return 'NULL'; 148 | } 149 | 150 | if (is_array($value)) { 151 | return $this->formatArray($value); 152 | } 153 | 154 | return $this->format($value); 155 | }, 156 | $value, 157 | ), 158 | ), 159 | ); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/functions.php: -------------------------------------------------------------------------------- 1 |