-
Notifications
You must be signed in to change notification settings - Fork 7
initial implementation for clickhouse #54
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
68e3f96
fb0b93b
eca0654
2743269
bd1faa8
7860460
cd8f8f7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,183 @@ | ||
| package clickhouse | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
|
|
||
| "github.com/ClickHouse/clickhouse-go/v2" | ||
| "github.com/ClickHouse/clickhouse-go/v2/lib/driver" | ||
| "github.com/hladush/go-telemetry/pkg/telemetry" | ||
| hdctx "github.com/patterninc/heimdall/pkg/context" | ||
| "github.com/patterninc/heimdall/pkg/object/cluster" | ||
| "github.com/patterninc/heimdall/pkg/object/job" | ||
| "github.com/patterninc/heimdall/pkg/object/job/status" | ||
| "github.com/patterninc/heimdall/pkg/plugin" | ||
| "github.com/patterninc/heimdall/pkg/result" | ||
| "github.com/patterninc/heimdall/pkg/result/column" | ||
| ) | ||
|
|
||
| type commandContext struct { | ||
| Username string `yaml:"username,omitempty" json:"username,omitempty"` | ||
| Password string `yaml:"password,omitempty" json:"password,omitempty"` | ||
| } | ||
|
|
||
| type clusterContext struct { | ||
| Endpoints []string `yaml:"endpoints" json:"endpoints"` | ||
| Database string `yaml:"database,omitempty" json:"database,omitempty"` | ||
| } | ||
|
|
||
| type jobContext struct { | ||
| Query string `yaml:"query" json:"query"` | ||
| Params map[string]string `yaml:"params,omitempty" json:"params,omitempty"` | ||
| ReturnResult bool `yaml:"return_result,omitempty" json:"return_result,omitempty"` | ||
| conn driver.Conn | ||
| } | ||
|
|
||
| const ( | ||
| serviceName = "clickhouse" | ||
| ) | ||
|
|
||
| var ( | ||
| dummyRowsInstance = dummyRows() | ||
| handleMethod = telemetry.NewMethod("handle", serviceName) | ||
| createExcMethod = telemetry.NewMethod("createExc", serviceName) | ||
| collectResultsMethod = telemetry.NewMethod("collectResults", serviceName) | ||
| ) | ||
|
|
||
| // New creates a new clickhouse plugin handler | ||
| func New(ctx *hdctx.Context) (plugin.Handler, error) { | ||
| t := &commandContext{} | ||
|
|
||
| if ctx != nil { | ||
| if err := ctx.Unmarshal(t); err != nil { | ||
| return nil, err | ||
| } | ||
| } | ||
|
|
||
| return t.handler, nil | ||
| } | ||
|
|
||
| func (cmd *commandContext) handler(r *plugin.Runtime, j *job.Job, c *cluster.Cluster) error { | ||
| ctx := context.Background() | ||
|
|
||
| jobContext, err := cmd.createJobContext(j, c) | ||
| if err != nil { | ||
| handleMethod.LogAndCountError(err, "create_job_context") | ||
| return err | ||
| } | ||
|
|
||
| rows, err := jobContext.execute(ctx) | ||
| if err != nil { | ||
| handleMethod.LogAndCountError(err, "execute") | ||
| return err | ||
| } | ||
| res, err := collectResults(rows) | ||
| if err != nil { | ||
| handleMethod.LogAndCountError(err, "collect_results") | ||
| return err | ||
| } | ||
| j.Result = res | ||
| j.Status = status.Succeeded | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| func (cmd *commandContext) createJobContext(j *job.Job, c *cluster.Cluster) (*jobContext, error) { | ||
| // get cluster context | ||
| clusterCtx := &clusterContext{} | ||
| if c.Context != nil { | ||
| if err := c.Context.Unmarshal(clusterCtx); err != nil { | ||
| createExcMethod.CountError("unmarshal_cluster_context") | ||
| return nil, fmt.Errorf("failed to unmarshal cluster context: %v", err) | ||
| } | ||
| } | ||
|
|
||
| // get job context | ||
| jobCtx := &jobContext{} | ||
| if j.Context != nil { | ||
| if err := j.Context.Unmarshal(jobCtx); err != nil { | ||
| createExcMethod.CountError("unmarshal_job_context") | ||
| return nil, fmt.Errorf("failed to unmarshal job context: %v", err) | ||
| } | ||
| } | ||
|
|
||
hladush marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| conn, err := clickhouse.Open(&clickhouse.Options{ | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. apart from the basic username and password the user should be able to override or control the connection settings from either command or cluster level as it supports only support single query execution at a time. Reference for enabling settings in connection open() method: https://github.com/ClickHouse/clickhouse-go/blob/main/examples/clickhouse_api/connect_settings.go |
||
| Addr: clusterCtx.Endpoints, | ||
| Auth: clickhouse.Auth{ | ||
| Database: clusterCtx.Database, | ||
| Username: cmd.Username, | ||
| Password: cmd.Password, | ||
| }, | ||
| }) | ||
| if err != nil { | ||
| createExcMethod.CountError("open_connection") | ||
| return nil, fmt.Errorf("failed to open ClickHouse connection: %v", err) | ||
| } | ||
| jobCtx.conn = conn | ||
| return jobCtx, nil | ||
| } | ||
|
|
||
| func (j *jobContext) execute(ctx context.Context) (driver.Rows, error) { | ||
| var args []any | ||
| for k, v := range j.Params { | ||
| args = append(args, clickhouse.Named(k, v)) | ||
| } | ||
| if j.ReturnResult { | ||
| return j.conn.Query(ctx, j.Query, args...) | ||
| } | ||
| return dummyRowsInstance, j.conn.Exec(ctx, j.Query, args...) | ||
|
|
||
| } | ||
|
|
||
| func collectResults(rows driver.Rows) (*result.Result, error) { | ||
| defer rows.Close() | ||
|
|
||
| cols := rows.Columns() | ||
| colTypes := rows.ColumnTypes() | ||
|
|
||
| out := &result.Result{ | ||
| Columns: make([]*column.Column, len(cols)), | ||
| Data: make([][]any, 0, 128), | ||
| } | ||
| for i, c := range cols { | ||
| base, _ := unwrapCHType(colTypes[i].DatabaseTypeName()) | ||
| columnTypeName := colTypes[i].DatabaseTypeName() | ||
| if val, ok := chTypeToResultTypeName[base]; ok { | ||
| columnTypeName = val | ||
| } | ||
| out.Columns[i] = &column.Column{ | ||
| Name: c, | ||
| Type: column.Type(columnTypeName), | ||
| } | ||
| } | ||
|
|
||
| // For each column we keep: scan target and a reader that returns a normalized interface{} | ||
|
|
||
| for rows.Next() { | ||
| scanTargets := make([]any, len(cols)) | ||
| readers := make([]func() any, len(cols)) | ||
|
|
||
| for i, ct := range colTypes { | ||
| base, nullable := unwrapCHType(ct.DatabaseTypeName()) | ||
|
|
||
| if handler, ok := chTypeHandlers[base]; ok { | ||
| scanTargets[i], readers[i] = handler(nullable) | ||
| } else { | ||
| // Fallback (covers unknown + legacy decimal detection) | ||
| scanTargets[i], readers[i] = handleDefault(nullable) | ||
| } | ||
| } | ||
|
|
||
| if err := rows.Scan(scanTargets...); err != nil { | ||
| collectResultsMethod.CountError("row_scan") | ||
| return nil, fmt.Errorf("row scan error: %w", err) | ||
| } | ||
|
|
||
| row := make([]any, len(cols)) | ||
| for i := range readers { | ||
| row[i] = readers[i]() | ||
| } | ||
| out.Data = append(out.Data, row) | ||
| } | ||
| return out, nil | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Username and password is stored on the command level because different teams eventually might have different access permitions and it should be managed on command level not on the cluster level.
Please share your thoughts
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
my thought is ideally it should be a connection string in cluster level something like this =>
clickhouse://username:password@host:port/database?ssl=trueand then at command level we control different access to different teams using the RBAC roles.WDYT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My thoughts that have a second line of defense is always nice, specially when companies don't have RBAC.