Files
go/context.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

353 lines
8.4 KiB
Go

package datastore
import (
"context"
"maps"
"slices"
"time"
"github.com/hanzo-ds/go/ext"
"go.opentelemetry.io/otel/trace"
)
var _contextOptionKey = &QueryOptions{
settings: Settings{
"_contextOption": struct{}{},
},
}
type Settings map[string]any
// CustomSetting is a helper struct to distinguish custom settings from important ones.
// For the native protocol, the is_important flag is set to 0x02 (see the server's
// src/Core/BaseSettings.h).
// Only string values are supported, until the field formatting logic the server
// implements in src/Core/Field.cpp is implemented in this driver.
type CustomSetting struct {
Value string
}
// ColumnNameAndType represents a column name and type
type ColumnNameAndType struct {
Name string
Type string
}
type Parameters map[string]string
type (
QueryOption func(*QueryOptions) error
AsyncOptions struct {
ok bool
wait bool
}
QueryOptions struct {
span trace.SpanContext
async AsyncOptions
queryID string
quotaKey string
jwt string
events struct {
logs func(*Log)
progress func(*Progress)
profileInfo func(*ProfileInfo)
profileEvents func([]ProfileEvent)
}
settings Settings
parameters Parameters
external []*ext.Table
blockBufferSize uint8
userLocation *time.Location
columnNamesAndTypes []ColumnNameAndType
clientInfo ClientInfo
}
)
func WithSpan(span trace.SpanContext) QueryOption {
return func(o *QueryOptions) error {
o.span = span
return nil
}
}
func WithQueryID(queryID string) QueryOption {
return func(o *QueryOptions) error {
o.queryID = queryID
return nil
}
}
func WithBlockBufferSize(size uint8) QueryOption {
return func(o *QueryOptions) error {
o.blockBufferSize = size
return nil
}
}
func WithQuotaKey(quotaKey string) QueryOption {
return func(o *QueryOptions) error {
o.quotaKey = quotaKey
return nil
}
}
// WithJWT overrides the existing authentication with the given JWT.
// This only applies for clients connected with HTTPS to Datastore Cloud.
func WithJWT(jwt string) QueryOption {
return func(o *QueryOptions) error {
o.jwt = jwt
return nil
}
}
// WithColumnNamesAndTypes is used to provide a predetermined list of
// column names and types for HTTP inserts.
// Without this, the HTTP implementation will parse the query and run a
// DESCRIBE TABLE request to fetch and validate column names.
func WithColumnNamesAndTypes(columnNamesAndTypes []ColumnNameAndType) QueryOption {
return func(o *QueryOptions) error {
o.columnNamesAndTypes = columnNamesAndTypes
return nil
}
}
// WithClientInfo appends client info data to the query, visible in the system.query_log table.
// This does not replace the client info provided in the connection options, it appends to it.
// Can be called multiple times to append more info.
func WithClientInfo(ci ClientInfo) QueryOption {
return func(o *QueryOptions) error {
o.clientInfo = o.clientInfo.Append(ci)
return nil
}
}
func WithSettings(settings Settings) QueryOption {
return func(o *QueryOptions) error {
o.settings = settings
return nil
}
}
func WithParameters(params Parameters) QueryOption {
return func(o *QueryOptions) error {
o.parameters = params
return nil
}
}
func WithLogs(fn func(*Log)) QueryOption {
return func(o *QueryOptions) error {
o.events.logs = fn
return nil
}
}
func WithProgress(fn func(*Progress)) QueryOption {
return func(o *QueryOptions) error {
o.events.progress = fn
return nil
}
}
func WithProfileInfo(fn func(*ProfileInfo)) QueryOption {
return func(o *QueryOptions) error {
o.events.profileInfo = fn
return nil
}
}
func WithProfileEvents(fn func([]ProfileEvent)) QueryOption {
return func(o *QueryOptions) error {
o.events.profileEvents = fn
return nil
}
}
func WithExternalTable(t ...*ext.Table) QueryOption {
return func(o *QueryOptions) error {
o.external = append(o.external, t...)
return nil
}
}
func WithAsync(wait bool) QueryOption {
return func(o *QueryOptions) error {
o.async.ok, o.async.wait = true, wait
return nil
}
}
// Deprecated: use `WithAsync` instead.
func WithStdAsync(wait bool) QueryOption {
return func(o *QueryOptions) error {
o.async.ok, o.async.wait = true, wait
return nil
}
}
func WithUserLocation(location *time.Location) QueryOption {
return func(o *QueryOptions) error {
o.userLocation = location
return nil
}
}
func ignoreExternalTables() QueryOption {
return func(o *QueryOptions) error {
o.external = nil
return nil
}
}
// Context returns a derived context with the given Datastore QueryOptions.
// Existing QueryOptions will be overwritten per option if present.
// The QueryOptions Settings map will be initialized if nil.
func Context(parent context.Context, options ...QueryOption) context.Context {
var opt QueryOptions
if ctxOpt, ok := parent.Value(_contextOptionKey).(QueryOptions); ok {
opt = ctxOpt
}
for _, f := range options {
f(&opt)
}
if opt.settings == nil {
opt.settings = make(Settings)
}
return context.WithValue(parent, _contextOptionKey, opt)
}
// queryOptions returns a mutable copy of the QueryOptions struct within the given context.
// If Datastore context was not provided, an empty struct with a valid Settings map is returned.
// If the context has a deadline greater than 1s then max_execution_time setting is appended.
func queryOptions(ctx context.Context) QueryOptions {
var opt QueryOptions
if ctxOpt, ok := ctx.Value(_contextOptionKey).(QueryOptions); ok {
opt = ctxOpt.clone()
} else {
opt = QueryOptions{
settings: make(Settings),
}
}
deadline, ok := ctx.Deadline()
if !ok {
return opt
}
if sec := time.Until(deadline).Seconds(); sec > 1 {
opt.settings["max_execution_time"] = int(sec + 5)
}
return opt
}
// queryOptionsJWT returns the JWT within the given context's QueryOptions.
// Empty string if not present.
func queryOptionsJWT(ctx context.Context) string {
if opt, ok := ctx.Value(_contextOptionKey).(QueryOptions); ok {
return opt.jwt
}
return ""
}
// queryOptionsAsync returns the AsyncOptions struct within the given context's QueryOptions.
func queryOptionsAsync(ctx context.Context) AsyncOptions {
if opt, ok := ctx.Value(_contextOptionKey).(QueryOptions); ok {
return opt.async
}
return AsyncOptions{}
}
// queryOptionsUserLocation returns the *time.Location within the given context's QueryOptions.
func queryOptionsUserLocation(ctx context.Context) *time.Location {
if opt, ok := ctx.Value(_contextOptionKey).(QueryOptions); ok {
return opt.userLocation
}
return nil
}
// WithoutProfileEvents instructs the server not to send profile events for this query.
// This is a performance optimization for servers >= 25.11 that support the send_profile_events setting.
// On older servers, the setting is unknown and the server will return an error.
func WithoutProfileEvents() QueryOption {
return func(o *QueryOptions) error {
if o.settings == nil {
o.settings = make(Settings)
}
o.settings["send_profile_events"] = 0
return nil
}
}
func (q *QueryOptions) onProcess() *onProcess {
onProcess := &onProcess{
logs: func(logs []Log) {
if q.events.logs != nil {
for _, l := range logs {
q.events.logs(&l)
}
}
},
progress: func(p *Progress) {
if q.events.progress != nil {
q.events.progress(p)
}
},
profileInfo: func(p *ProfileInfo) {
if q.events.profileInfo != nil {
q.events.profileInfo(p)
}
},
}
profileEventsHandler := q.events.profileEvents
if profileEventsHandler != nil {
onProcess.profileEvents = func(events []ProfileEvent) {
profileEventsHandler(events)
}
}
return onProcess
}
// clone returns a copy of QueryOptions where Settings and Parameters are safely mutable.
func (q *QueryOptions) clone() QueryOptions {
c := QueryOptions{
span: q.span,
async: q.async,
queryID: q.queryID,
quotaKey: q.quotaKey,
events: q.events,
settings: nil,
parameters: nil,
external: q.external,
blockBufferSize: q.blockBufferSize,
userLocation: q.userLocation,
columnNamesAndTypes: nil,
}
if q.settings != nil {
c.settings = maps.Clone(q.settings)
}
if q.parameters != nil {
c.parameters = maps.Clone(q.parameters)
}
if q.columnNamesAndTypes != nil {
c.columnNamesAndTypes = slices.Clone(q.columnNamesAndTypes)
}
if q.clientInfo.Products != nil || q.clientInfo.Comment != nil {
c.clientInfo = q.clientInfo.Append(ClientInfo{})
}
return c
}