ch-go

repository·main·Indexed 19 days ago

https://github.com/clickhouse/ch-go

A low-level Go implementation of the ClickHouse TCP protocol optimized for high-performance, columnar data streaming. It provides minimal CPU and memory overhead compared to row-based SQL clients or the HTTP API. Features include support for Native format dumps, automatic result inference, and configurable compression algorithms (LZ4, ZSTD, etc.). Note that the client is not goroutine-safe by default; for connection pooling, the chpool package is recommended.

Tokens
7.1K
Snippets
24
Records
31
Agent score
15%

What's inside ch-go

  1. Stream query results with OnResult

    main

    To stream query results, set the Result and OnResult fields of a ch.Query. The OnResult callback is triggered after the Result fields are populated with a received data block.

    Note: If the query returns more than one block, you must provide an OnResult handler, otherwise the query will fail. If you only expect a single block (e.g., one row), you can solely set the Result field.

  2. Write ClickHouse dumps in Native format

    main

    You can manually encode data into the ClickHouse Native format using proto.Block.EncodeRawBlock. This is useful for creating highly efficient binary dumps.

    var (
        colK proto.ColInt64
        colV proto.ColInt64
    )
    for i := 0; i < 100; i++ {
        colK.Append(int64(i))
        colV.Append(int64(i) + 1000)
    }
    
    var buf proto.Buffer
    input := proto.Input{
        {"k", colK},
        {"v", colV},
    }
    b := proto.Block{
        Rows:    colK.Rows(),
        Columns: len(input),
    }
    
    // Encode the block to the buffer
    if err := b.EncodeRawBlock(&buf, 54451, input); err != nil {
        panic(err)
    }
    
    // buf.Buf can now be written to an io.Writer
  3. Write data to ClickHouse

    main

    To write data, prepare a proto.Input containing columns that implement the necessary proto interfaces. You can then execute an INSERT query using c.Do.

    var (
    	body      proto.ColStr
    	name      proto.ColStr
    	sevText   proto.ColEnum
    	sevNumber proto.ColUInt8
    
        serviceName = proto.NewLowCardinality(new(proto.ColStr))
        ts          = new(proto.ColDateTime64).WithPrecision(proto.PrecisionNano)
        arr         = new(proto.ColStr).Array()
        now         = time.Date(2010, 1, 1, 10, 22, 33, 345678, time.UTC)
    )
    
    // Append data to columns
    for i := 0; i < 10; i++ {
    	body.AppendBytes([]byte("Hello"))
    	ts.Append(now)
    	name.Append("name")
    	sevText.Append("INFO")
    	sevNumber.Append(10)
    	arr.Append([]string{"foo", "bar", "baz"})
        serviceName.Append("service")
    }
    
    input := proto.Input{
    	{Name: "ts", Data: ts},
    	{Name: "severity_text", Data: &sevText},
    	{Name: "severity_number", Data: sevNumber},
    	{Name: "body", Data: body},
    	{Name: "name", Data: name},
    	{Name: "arr", Data: arr},
    }
    
    // Execute insertion
    if err := conn.Do(ctx, ch.Query{
    	Body: "INSERT INTO test_table_insert VALUES",
    	Input: input,
    }); err != nil {
    	panic(err)
    }
  4. Stream data in multiple blocks using OnInput

    main

    To stream large amounts of data in multiple blocks, use the OnInput field in ch.Query. The OnInput function is called before each block is encoded and sent.

    Crucial: You must call input.Reset() inside OnInput to clear the columns before appending new data; otherwise, data will be duplicated in subsequent blocks. To stop streaming, return io.EOF from OnInput.

    var blocks int
    if err := conn.Do(ctx, ch.Query{
    	Body:  input.Into("test_table_insert"),
    	Input: input,
    	OnInput: func(ctx context.Context) error {
    		input.Reset() // Important: reset columns to avoid duplication
    
    		if blocks >= 10 {
    			return io.EOF
    		}
    
    		// Append new values for this block
    		for i := 0; i < 10; i++ {
    			body.AppendBytes([]byte("Hello"))
    			ts.Append(now)
    			name.Append("name")
    			sevText.Append("DEBUG")
    			sevNumber.Append(10)
    			arr.Append([]string{"foo", "bar", "baz"})
                serviceName.Append("service")
    		}
    
    		blocks++
    		return nil
    	},
    }); err != nil {
    	panic(err)
    }
  5. Read Native format dumps

    main

    To read data from a ClickHouse Native format dump, use the proto.Block.DecodeRawBlock method. This method requires a proto.NewReader wrapping your data source (e.g., a file or byte buffer) and a proto.Results slice that maps column names to their corresponding proto column types.

    // Example: Decoding a Native format dump
    var (
    	dec    proto.Block
    	ids    proto.ColInt8
    	values proto.ColStr
    )
    
    err := dec.DecodeRawBlock(
    	proto.NewReader(bytes.NewReader(data)),
    	proto.Results{
    		{Name: "id", Data: &ids},
    		{Name: "v", Data: &values},
    	},
    )
  6. How to use ch-go (Basic Example)

    main

    The ch-go package is a low-level TCP client designed for high-performance data streaming. It is not goroutine-safe by default and does not provide connection pooling or automatic reconnects. For pooling, use the chpool package.

    To execute a query and stream results, use ch.Dial to establish a connection and c.Do to execute a ch.Query.

    package main
    
    import (
      "context"
      "fmt"
    
      "github.com/ClickHouse/ch-go"
      "github.com/ClickHouse/ch-go/proto"
    )
    
    func main() {
      ctx := context.Background()
      c, err := ch.Dial(ctx, ch.Options{Address: "localhost:9000"})
      if err != nil {
        panic(err)
      }
      var (
        numbers int
        data    proto.ColUInt64
      )
      if err := c.Do(ctx, ch.Query{
        Body: "SELECT number FROM system.numbers LIMIT 500000000",
        Result: proto.Results{
          {Name: "number", Data: &data},
        },
        // OnResult will be called on next received data block.
        OnResult: func(ctx context.Context, b proto.Block) error {
          numbers += len(data)
          return nil
        },
      }); err != nil {
        panic(err)
      }
      fmt.Println("numbers:", numbers)
    }
  7. Write Native format dumps

    main

    To write data in the ClickHouse Native format, use proto.Block.EncodeRawBlock.

    Requirements:

    1. Use a proto.Buffer to capture the output.
    2. Initialize a proto.Block with the correct number of Rows and Columns.
    3. Provide a version number (e.g., 54451).
    4. Pass a slice of proto.InputColumn containing the column names and their data.
    // Example: Encoding a Native format dump
    var v proto.ColStr
    // ... populate v with data ...
    
    buf := new(proto.Buffer)
    // Initialize block with expected dimensions
    b := proto.Block{Rows: 2, Columns: 2}
    
    err := b.EncodeRawBlock(buf, 54451, []proto.InputColumn{
    	{Name: "title", Data: v},
    	{Name: "data", Data: proto.ColInt64{1, 2}},
    })
  8. Understand the Variant type and its properties

    main

    The Variant struct defines the characteristics of a ClickHouse column type used during code generation. Key properties include:

    • Kind: The category of the type (e.g., KindInt, KindFloat, KindIP, KindDateTime, KindDate, KindTime32, KindTime64, KindEnum, KindDecimal, KindFixedStr).
    • Signed: Whether the type is signed or unsigned.
    • Bits: The bit width of the type.
    • GenerateUnsafe: A flag indicating if an unsafe implementation should be generated.

    Commonly used methods on Variant to determine type behavior:

    • IsFloat(), IsInt(), IsIP(): Check the fundamental kind.
    • Big(): Returns true if Bits > 64.
    • FixedStr(): Returns true if the kind is KindFixedStr.
    • Time(): Returns true if the kind is a time-related type (KindDate, KindDateTime, KindTime32, KindTime64).
    • Byte(): Returns true if the type is a single-byte integer (Bits == 8 and !Signed).
  9. Use ch-gen-col to generate column type code

    main

    The ch-gen-col tool is a CLI utility used to automatically generate Go code for ClickHouse column types. It iterates through a predefined set of data type variants (including Integers, Floats, IPs, DateTimes, Enums, Decimals, and FixedStrings) and generates several types of files for each:

    • col_<type>_gen.go: Standard column type generation.
    • col_<type>_safe_gen.go: Safe version of the column type generation.
    • col_<type>_unsafe_gen.go: Unsafe version (generated only for types that are not single-byte).
    • col_<type>_gen_test.go: Corresponding test files.
    • col_auto_gen.go: A collection of types used for automatic type inference.

    The tool uses Go templates to produce formatted, valid Go source code.

  10. Connect to ClickHouse using Dial

    main

    The easiest way to establish a connection to a ClickHouse server is using Dial. This function performs the TCP connection and the ClickHouse handshake automatically. You provide an Options struct to configure the connection parameters.

    package main
    
    import (
    	"context"
    	"time"
    
    	"github.com/ClickHouse/ch-go"
    )
    
    func main() {
    	ctx := context.Background()
    	opt := ch.Options{
    		Address: "127.0.0.1:9000",
    		User:    "default",
    		Database: "default",
    	}
    
    	client, err := ch.Dial(ctx, opt)
    	if err != nil {
    		panic(err)
    	}
    	defer client.Close()
    }
  11. Stream input data using OnInput

    main

    When performing INSERT operations, use the OnInput callback in a Query to stream data blocks to the server. This prevents high memory consumption by allowing the client to flush data periodically.

    Workflow:

    1. Set Query.Input with the initial column definitions.
    2. Implement OnInput(ctx context.Context) error.
    3. Inside OnInput, provide the next block of data via the Input field.
    4. Return io.EOF when all data has been sent. The client will then send a blank block to signal the end of the input stream.

    Note: Atomicity is only guaranteed within a single block.

    q := ch.Query{
        Body: "INSERT INTO my_table (col1, col2)",
        Input: proto.Input{
            // Initial column setup
        },
        OnInput: func(ctx context.Context) error {
            // 1. Load next block of data into q.Input
            // 2. If no more data, return io.EOF
            // 3. If error, return error
            return nil
        },
    }