package repo import ( "context" "crypto/tls" "fmt" "github.com/ClickHouse/clickhouse-go/v2" "github.com/ClickHouse/clickhouse-go/v2/lib/driver" ) // NewClickHouse opens a ClickHouse connection. `secure` enables TLS. The // wire protocol is auto-selected from the port: 8123/8443 (HTTP interface) // use HTTP, the native default otherwise. func NewClickHouse(ctx context.Context, addr, db, user, password string, secure bool) (driver.Conn, error) { opts := &clickhouse.Options{ Addr: []string{addr}, Protocol: protocolFromAddr(addr), Auth: clickhouse.Auth{ Database: db, Username: user, Password: password, }, Settings: clickhouse.Settings{ "readonly": 0, // analytics queries; per-user read-only enforced for /query/sql separately }, } if secure { opts.TLS = &tls.Config{MinVersion: tls.VersionTLS12} } conn, err := clickhouse.Open(opts) if err != nil { return nil, fmt.Errorf("open clickhouse: %w", err) } if err := conn.Ping(ctx); err != nil { _ = conn.Close() return nil, fmt.Errorf("ping clickhouse: %w", err) } return conn, nil } // NewClickHouseReadOnly opens a ClickHouse connection using a SELECT-only // account. Used to back the /query/sql sandbox: DDL/DML are rejected at the DB // level even if the app-level keyword guard is bypassed. func NewClickHouseReadOnly(ctx context.Context, addr, db, user, password string, secure bool) (driver.Conn, error) { opts := &clickhouse.Options{ Addr: []string{addr}, Protocol: protocolFromAddr(addr), Auth: clickhouse.Auth{ Database: db, Username: user, Password: password, }, Settings: clickhouse.Settings{ "readonly": 2, // belt-and-braces: server-side enforce read-only }, } if secure { opts.TLS = &tls.Config{MinVersion: tls.VersionTLS12} } conn, err := clickhouse.Open(opts) if err != nil { return nil, fmt.Errorf("open clickhouse (ro): %w", err) } if err := conn.Ping(ctx); err != nil { _ = conn.Close() return nil, fmt.Errorf("ping clickhouse (ro): %w", err) } return conn, nil } // protocolFromAddr selects HTTP for the well-known ClickHouse HTTP-interface // ports (8123/8443) and Native otherwise. Lets CLICKHOUSE_ADDR target either // kind of endpoint without an extra env var. func protocolFromAddr(addr string) clickhouse.Protocol { switch port := portOf(addr); port { case "8123", "8443": return clickhouse.HTTP default: return clickhouse.Native } } func portOf(addr string) string { for i := len(addr) - 1; i >= 0; i-- { if addr[i] == ':' { return addr[i+1:] } } return "" }