package clickhouse import ( "context" "crypto/tls" "encoding/json" "fmt" "io" "net/http" "net/url" "strings" "time" ) // Client 通过 HTTP 接口执行只读查询(SELECT),返回 JSONEachRow 解析为 map 或结构体 type Client struct { cfg *Config httpCli *http.Client } // NewClient 使用共享配置创建客户端 func NewClient() *Client { cfg := SharedConfig() transport := &http.Transport{} if cfg != nil && strings.EqualFold(cfg.Scheme, "https") { transport.TLSClientConfig = &tls.Config{ InsecureSkipVerify: cfg.TLSSkipVerify, ServerName: cfg.TLSServerName, } } return &Client{ cfg: cfg, httpCli: &http.Client{ Timeout: 30 * time.Second, Transport: transport, }, } } // IsConfigured 是否已配置 func (c *Client) IsConfigured() bool { return c.cfg != nil && c.cfg.IsConfigured() } // Query 执行 SELECT,将每行 JSON 解析到 dest 切片;dest 元素类型需为 *struct 或 map func (c *Client) Query(ctx context.Context, query string, dest interface{}) error { if !c.IsConfigured() { return fmt.Errorf("clickhouse: not configured") } // 强制 JSONEachRow 便于解析 q := strings.TrimSpace(query) if !strings.HasSuffix(strings.ToUpper(q), "FORMAT JSONEACHROW") { query = q + " FORMAT JSONEachRow" } u := c.buildURL(query) req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil) if err != nil { return err } if c.cfg.User != "" || c.cfg.Password != "" { req.SetBasicAuth(c.cfg.User, c.cfg.Password) } resp, err := c.httpCli.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return fmt.Errorf("clickhouse HTTP %d: %s", resp.StatusCode, string(body)) } dec := json.NewDecoder(resp.Body) return decodeRows(dec, dest) } // QueryRow 执行仅返回一行的查询,将结果解析到 dest(*struct 或 *map) func (c *Client) QueryRow(ctx context.Context, query string, dest interface{}) error { if !c.IsConfigured() { return fmt.Errorf("clickhouse: not configured") } q := strings.TrimSpace(query) if !strings.HasSuffix(strings.ToUpper(q), "FORMAT JSONEACHROW") { query = q + " FORMAT JSONEachRow" } u := c.buildURL(query) req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil) if err != nil { return err } if c.cfg.User != "" || c.cfg.Password != "" { req.SetBasicAuth(c.cfg.User, c.cfg.Password) } resp, err := c.httpCli.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return fmt.Errorf("clickhouse HTTP %d: %s", resp.StatusCode, string(body)) } dec := json.NewDecoder(resp.Body) return decodeOneRow(dec, dest) } func (c *Client) buildURL(query string) string { scheme := "http" if c.cfg != nil && strings.EqualFold(c.cfg.Scheme, "https") { scheme = "https" } rawURL := fmt.Sprintf("%s://%s:%d/?query=%s&database=%s", scheme, c.cfg.Host, c.cfg.Port, url.QueryEscape(query), url.QueryEscape(c.cfg.Database)) return rawURL } // decodeRows 将 JSONEachRow 流解析到 slice;元素类型须为 *struct 或 *[]map[string]interface{} func decodeRows(dec *json.Decoder, dest interface{}) error { // dest 应为 *[]*SomeStruct 或 *[]map[string]interface{} switch d := dest.(type) { case *[]map[string]interface{}: *d = (*d)[:0] for { var row map[string]interface{} if err := dec.Decode(&row); err != nil { if err == io.EOF { return nil } return err } *d = append(*d, row) } default: return fmt.Errorf("clickhouse: unsupported dest type for Query (use *[]map[string]interface{} or implement decoder)") } } func decodeOneRow(dec *json.Decoder, dest interface{}) error { switch d := dest.(type) { case *map[string]interface{}: if err := dec.Decode(d); err != nil { return err } return nil default: return fmt.Errorf("clickhouse: unsupported dest type for QueryRow (use *map[string]interface{})") } }