|null $configuration */ public function __construct(?ClientInterface $http = null, private readonly ?array $configuration = null) { $config = $this->config(); $this->http = $http ?? new Client([ 'base_uri' => rtrim((string) $config['url'], '/') . '/', 'connect_timeout' => (float) $config['connect_timeout_seconds'], 'timeout' => (float) $config['query_timeout_seconds'], ]); } public function enabled(): bool { return (bool) $this->config()['enabled']; } public function ping(): bool { if (! $this->enabled()) { return false; } try { $response = $this->http->request('GET', 'ping', $this->authOptions()); return $response->getStatusCode() === 200 && trim((string) $response->getBody()) === 'Ok.'; } catch (GuzzleException) { return false; } } /** @param array $parameters */ public function command(string $sql, array $parameters = []): void { $this->request($sql, $parameters); } /** @param array $parameters @return list> */ public function rows(string $sql, array $parameters = []): array { $body = (string) $this->request(rtrim($sql, "; \t\n\r") . ' FORMAT JSONEachRow', $parameters); if (trim($body) === '') { return []; } $rows = []; foreach (preg_split('/\r?\n/', trim($body)) ?: [] as $line) { $decoded = json_decode($line, true, 64, JSON_THROW_ON_ERROR); if (! is_array($decoded)) { throw new RuntimeException('clickhouse_invalid_json_row'); } $rows[] = $decoded; } return $rows; } /** @param array $parameters */ public function scalar(string $sql, array $parameters = []): mixed { $body = (string) $this->request(rtrim($sql, "; \t\n\r") . ' FORMAT TabSeparatedRaw', $parameters); return trim($body); } /** * @param list $columns * @param list> $rows */ public function insertJsonEachRow(string $table, array $columns, array $rows): void { if ($rows === []) { return; } $table = $this->qualifiedTable($table); $columns = array_map([$this, 'identifier'], $columns); $payload = ''; foreach ($rows as $row) { $payload .= json_encode($row, JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION | JSON_THROW_ON_ERROR) . "\n"; } $sql = sprintf('INSERT INTO %s (%s) FORMAT JSONEachRow', $table, implode(', ', $columns)); $this->request($sql, [], $payload, 'application/x-ndjson'); } /** @param array $parameters */ public function stream(string $sql, string $format, array $parameters = []): StreamInterface { if (preg_match('/^[A-Za-z][A-Za-z0-9]*$/', $format) !== 1) { throw new RuntimeException('clickhouse_invalid_format'); } return $this->request( rtrim($sql, "; \t\n\r") . ' FORMAT ' . $format, $parameters, null, null, true, ); } public function qualifiedTable(string $table): string { return $this->identifier((string) $this->config()['database']) . '.' . $this->identifier($table); } public function identifier(string $value): string { if (preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/', $value) !== 1) { throw new RuntimeException('clickhouse_invalid_identifier'); } return '`' . $value . '`'; } /** * @param array $parameters * @return StreamInterface|string */ private function request( string $sql, array $parameters = [], ?string $body = null, ?string $contentType = null, bool $stream = false, ): StreamInterface|string { if (! $this->enabled()) { throw new RuntimeException('clickhouse_disabled'); } $query = [ 'database' => (string) $this->config()['database'], 'wait_end_of_query' => 1, 'query' => $sql, ]; foreach ($parameters as $name => $value) { if (preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/', $name) !== 1 || (! is_scalar($value) && $value !== null)) { throw new RuntimeException('clickhouse_invalid_parameter'); } $query['param_' . $name] = $value; } $options = $this->authOptions() + ['query' => $query, 'stream' => $stream]; if ($stream) { $options['timeout'] = (float) $this->config()['stream_timeout_seconds']; } if ($body !== null) { $options['body'] = $body; } if ($contentType !== null) { $options['headers'] = ['Content-Type' => $contentType]; } try { $response = $this->http->request('POST', '', $options); } catch (GuzzleException $exception) { throw new RuntimeException('clickhouse_request_failed', 0, $exception); } if ($response->getStatusCode() < 200 || $response->getStatusCode() >= 300) { throw new RuntimeException('clickhouse_unexpected_status'); } return $stream ? $response->getBody() : (string) $response->getBody(); } /** @return array */ private function authOptions(): array { $username = (string) $this->config()['username']; return $username === '' ? [] : ['auth' => [$username, (string) $this->config()['password']]]; } /** @return array */ private function config(): array { return ($this->configuration ?? (array) config('business_data.clickhouse', [])) + [ 'enabled' => false, 'url' => 'http://clickhouse:8123', 'database' => 'caiwu_business_analytics', 'username' => 'caiwu_app', 'password' => '', 'connect_timeout_seconds' => 3, 'query_timeout_seconds' => 30, 'stream_timeout_seconds' => 1800, ]; } }