Files
go/datastore.go
zeekay 0c90ebe02e debrand: finish the rename across source, docs and dev infra
Source: the unexported clickhouse struct becomes client, the ch receiver and
chproto import alias follow, error message prefixes read datastore, and the
JSON serializer interface drops the vendor infix. resources' metadata key and
its accessor are renamed together, with a test proving the embedded YAML still
unmarshals — a silent drift there would have made every connection take the
unsupported-server-version branch.

Codegen: all three templates still emitted the upstream module paths, so
regenerating would have quietly undone the rename. Note the templates have
drifted from the files they produce (regenerating today drops QBit and
scanTypeDuration), so the generated files are left as tracked and only the
templates are corrected.

Dead links: an earlier mechanical pass invented URLs that do not resolve —
github.com/ClickHouse/datastore-go, hanzo-ds/driver issue numbers that belong to
upstream, and mangled names of independent projects (mailru, uptrace, nikepan,
EpicStep). Third-party names are not ours to rewrite, so those directories are
removed rather than repaired, and provenance comments now point at real files.

Dev infra: compose runs our own server image at the tag production runs, since
the vendor's alpine tag does not exist in our registry. Cluster hostnames,
nginx upstreams and server configs are renamed as one set.

CHANGELOG.md is upstream's release history for versions this module never
released; rewriting it would fabricate a lineage, so it is dropped and the
upstream base recorded in README instead.

The two long-standing vet findings are fixed by keying the struct literals
rather than suppressed. LICENSE and contributors/list keep the vendor name on
purpose: one is the copyright notice Apache-2.0 requires, the other is real
people's names. CI now enforces build, vet, test, gofmt and a grep gate.
2026-07-26 11:46:39 -07:00

418 lines
10 KiB
Go

package datastore
import (
"context"
"errors"
"fmt"
"log/slog"
"math/rand"
"sync"
"sync/atomic"
"time"
_ "time/tzdata"
"github.com/hanzo-ds/go/contributors"
"github.com/hanzo-ds/go/lib/column"
"github.com/hanzo-ds/go/lib/driver"
"github.com/hanzo-ds/go/lib/proto"
)
type Conn = driver.Conn
type (
Progress = proto.Progress
Exception = proto.Exception
ProfileInfo = proto.ProfileInfo
ServerVersion = proto.ServerHandshake
)
var (
ErrBatchInvalid = errors.New("datastore: batch is invalid. check appended data is correct")
ErrBatchAlreadySent = errors.New("datastore: batch has already been sent")
ErrBatchNotSent = errors.New("datastore: invalid retry, batch not sent yet")
ErrAcquireConnTimeout = errors.New("datastore: acquire conn timeout. you can increase the number of max open conn or the dial timeout")
ErrUnsupportedServerRevision = errors.New("datastore: unsupported server revision")
ErrBindMixedParamsFormats = errors.New("datastore [bind]: mixed named, numeric or positional parameters")
ErrAcquireConnNoAddress = errors.New("datastore: no valid address supplied")
ErrServerUnexpectedData = errors.New("code: 101, message: Unexpected packet Data received from client")
ErrConnectionClosed = errors.New("datastore: connection is closed")
)
type OpError struct {
Op string
ColumnName string
Err error
}
func (e *OpError) Error() string {
switch err := e.Err.(type) {
case *column.Error:
return fmt.Sprintf("datastore [%s]: (%s %s) %s", e.Op, e.ColumnName, err.ColumnType, err.Err)
case *column.ColumnConverterError:
var hint string
if len(err.Hint) != 0 {
hint += ". " + err.Hint
}
return fmt.Sprintf("datastore [%s]: (%s) converting %s to %s is unsupported%s",
err.Op, e.ColumnName,
err.From, err.To,
hint,
)
}
return fmt.Sprintf("datastore [%s]: %s", e.Op, e.Err)
}
func Open(opt *Options) (driver.Conn, error) {
if opt == nil {
opt = &Options{}
}
o := opt.setDefaults()
conn := &client{
opt: o,
idle: newConnPool(o.ConnMaxLifetime, o.MaxIdleConns),
open: make(chan struct{}, o.MaxOpenConns),
closeOnce: &sync.Once{},
closed: &atomic.Bool{},
}
return conn, nil
}
// nativeTransport represents an implementation (TCP or HTTP) that can be pooled by the main client struct.
// Implementations are not expected to be thread safe, which is why we provide acquire/release functions.
type nativeTransport interface {
serverVersion() (*ServerVersion, error)
query(ctx context.Context, release nativeTransportRelease, query string, args ...any) (*rows, error)
queryRow(ctx context.Context, release nativeTransportRelease, query string, args ...any) *row
prepareBatch(ctx context.Context, release nativeTransportRelease, acquire nativeTransportAcquire, query string, opts driver.PrepareBatchOptions) (driver.Batch, error)
exec(ctx context.Context, query string, args ...any) error
asyncInsert(ctx context.Context, query string, wait bool, args ...any) error
ping(context.Context) error
isBad() bool
connID() int
connectedAtTime() time.Time
isReleased() bool
setReleased(released bool)
getLogger() *slog.Logger
// freeBuffer is called if Options.FreeBufOnConnRelease is set
freeBuffer()
close() error
}
type nativeTransportAcquire func(context.Context) (nativeTransport, error)
type nativeTransportRelease func(nativeTransport, error)
// connectionPooler is an connection pool maintain
// idle connections.
type connectionPooler interface {
Get(ctx context.Context) (nativeTransport, error)
Put(conn nativeTransport)
Len() int
Cap() int
Close() error
}
type client struct {
opt *Options
connID int64
idle connectionPooler
open chan struct{}
closeOnce *sync.Once
closed *atomic.Bool
}
func (client) Contributors() []string {
list := contributors.List
if len(list[len(list)-1]) == 0 {
return list[:len(list)-1]
}
return list
}
func (c *client) ServerVersion() (*driver.ServerVersion, error) {
var (
ctx, cancel = context.WithTimeout(context.Background(), c.opt.DialTimeout)
conn, err = c.acquire(ctx)
)
defer cancel()
if err != nil {
return nil, err
}
defer c.release(conn, nil)
return conn.serverVersion()
}
func (c *client) Query(ctx context.Context, query string, args ...any) (rows driver.Rows, err error) {
conn, err := c.acquire(ctx)
if err != nil {
return nil, err
}
conn.getLogger().Debug("executing query", slog.String("sql", query))
return conn.query(ctx, c.release, query, args...)
}
func (c *client) QueryRow(ctx context.Context, query string, args ...any) driver.Row {
conn, err := c.acquire(ctx)
if err != nil {
return &row{
err: err,
}
}
conn.getLogger().Debug("executing query row", slog.String("sql", query))
return conn.queryRow(ctx, c.release, query, args...)
}
func (c *client) Exec(ctx context.Context, query string, args ...any) error {
conn, err := c.acquire(ctx)
if err != nil {
return err
}
conn.getLogger().Debug("executing statement", slog.String("sql", query))
if asyncOpt := queryOptionsAsync(ctx); asyncOpt.ok {
err = conn.asyncInsert(ctx, query, asyncOpt.wait, args...)
} else {
err = conn.exec(ctx, query, args...)
}
if err != nil {
c.release(conn, err)
return err
}
c.release(conn, nil)
return nil
}
func (c *client) PrepareBatch(ctx context.Context, query string, opts ...driver.PrepareBatchOption) (driver.Batch, error) {
conn, err := c.acquire(ctx)
if err != nil {
return nil, err
}
conn.getLogger().Debug("preparing batch", slog.String("sql", query))
batch, err := conn.prepareBatch(ctx, c.release, c.acquire, query, getPrepareBatchOptions(opts...))
if err != nil {
return nil, err
}
return batch, nil
}
func getPrepareBatchOptions(opts ...driver.PrepareBatchOption) driver.PrepareBatchOptions {
var options driver.PrepareBatchOptions
for _, opt := range opts {
opt(&options)
}
return options
}
// Deprecated: use context aware `WithAsync()` for any async operations
func (c *client) AsyncInsert(ctx context.Context, query string, wait bool, args ...any) error {
conn, err := c.acquire(ctx)
if err != nil {
return err
}
conn.getLogger().Debug("async insert", slog.String("sql", query), slog.Bool("wait", wait))
if err := conn.asyncInsert(ctx, query, wait, args...); err != nil {
c.release(conn, err)
return err
}
c.release(conn, nil)
return nil
}
func (c *client) Ping(ctx context.Context) (err error) {
conn, err := c.acquire(ctx)
if err != nil {
return err
}
conn.getLogger().Debug("ping")
if err := conn.ping(ctx); err != nil {
c.release(conn, err)
return err
}
c.release(conn, nil)
return nil
}
func (c *client) Stats() driver.Stats {
return driver.Stats{
Open: len(c.open),
MaxOpenConns: cap(c.open),
Idle: c.idle.Len(),
MaxIdleConns: c.idle.Cap(),
}
}
func (c *client) dial(ctx context.Context) (conn nativeTransport, err error) {
if err := ctx.Err(); err != nil {
return nil, err
}
connID := int(atomic.AddInt64(&c.connID, 1))
dialFunc := func(ctx context.Context, addr string, opt *Options) (DialResult, error) {
var conn nativeTransport
var err error
switch opt.Protocol {
case HTTP:
conn, err = dialHttp(ctx, addr, connID, opt)
default:
conn, err = dial(ctx, addr, connID, opt)
}
return DialResult{conn}, err
}
dialStrategy := DefaultDialStrategy
if c.opt.DialStrategy != nil {
dialStrategy = c.opt.DialStrategy
}
result, err := dialStrategy(ctx, connID, c.opt, dialFunc)
if err != nil {
return nil, err
}
return result.conn, nil
}
func DefaultDialStrategy(ctx context.Context, connID int, opt *Options, dial Dial) (r DialResult, err error) {
for i := range opt.Addr {
var num int
switch opt.ConnOpenStrategy {
case ConnOpenInOrder:
num = i
case ConnOpenRoundRobin:
num = (connID + i) % len(opt.Addr)
case ConnOpenRandom:
random := rand.Int()
num = (random + i) % len(opt.Addr)
}
if r, err = dial(ctx, opt.Addr[num], opt); err == nil {
return r, nil
}
}
if err == nil {
err = ErrAcquireConnNoAddress
}
return r, err
}
func (c *client) acquire(ctx context.Context) (conn nativeTransport, err error) {
if c.closed.Load() {
return nil, ErrConnectionClosed
}
ctx, cancel := context.WithTimeoutCause(ctx, c.opt.DialTimeout, ErrAcquireConnTimeout)
defer cancel()
// If context is already cancelled, just return without any work
// done this way with single case with default. Otherwise if both ctx is cancelled and c.open is ready,
// Go would choose one of those at random, thus missing to return deterministically when context is cancelled
// at this point in time.
// Known pattern: https://go.dev/ref/spec#Select_statements
select {
case <-ctx.Done():
return nil, context.Cause(ctx)
default:
}
select {
case c.open <- struct{}{}:
case <-ctx.Done():
return nil, context.Cause(ctx)
}
conn, err = c.idle.Get(ctx)
if err != nil && !errors.Is(err, errQueueEmpty) {
select {
case <-c.open:
default:
}
return nil, err
}
if err == nil && conn != nil {
if !conn.isBad() {
conn.setReleased(false)
conn.getLogger().Debug("connection acquired from pool")
return conn, nil
}
conn.close()
}
if conn, err = c.dial(ctx); err != nil {
select {
case <-c.open:
default:
}
return nil, err
}
conn.getLogger().Debug("new connection established")
return conn, nil
}
func (c *client) release(conn nativeTransport, err error) {
if conn.isReleased() {
return
}
conn.setReleased(true)
if err != nil {
conn.getLogger().Debug("connection released with error", slog.Any("error", err))
} else {
conn.getLogger().Debug("connection released to pool")
}
select {
case <-c.open:
default:
}
if err != nil {
conn.getLogger().Debug("connection closed due to error", slog.Any("error", err))
conn.close()
return
} else if time.Since(conn.connectedAtTime()) >= c.opt.ConnMaxLifetime {
conn.getLogger().Debug("connection closed: lifetime expired",
slog.Duration("age", time.Since(conn.connectedAtTime())),
slog.Duration("max_lifetime", c.opt.ConnMaxLifetime))
conn.close()
return
}
if c.opt.FreeBufOnConnRelease {
conn.getLogger().Debug("freeing connection buffer")
conn.freeBuffer()
}
if c.closed.Load() {
conn.close()
return
}
c.idle.Put(conn)
}
func (c *client) Close() (err error) {
c.closeOnce.Do(func() {
err = c.idle.Close()
c.closed.Store(true)
})
return
}