This commit is contained in:
2026-05-25 13:38:20 +07:00
parent 5a1829bc0f
commit b40568dd30
7 changed files with 948 additions and 259 deletions

View File

@@ -60,9 +60,9 @@ func (r *AnalyticsRepo) Funnel(ctx context.Context, q FunnelQuery) (*model.Query
args := []any{
clickhouse.Named("workspace_id", q.WorkspaceID),
clickhouse.DateNamed("from", q.From, clickhouse.MilliSeconds),
clickhouse.DateNamed("to", q.To, clickhouse.MilliSeconds),
clickhouse.Named("window_seconds", q.WindowSeconds),
clickhouse.Named("from", chTime(q.From)),
clickhouse.Named("to", chTime(q.To)),
clickhouse.Named("window_seconds", chUint(uint64(q.WindowSeconds))),
}
for i, name := range q.Steps {
args = append(args, clickhouse.Named(fmt.Sprintf("step%d", i), name))
@@ -112,8 +112,8 @@ func (r *AnalyticsRepo) Retention(ctx context.Context, q RetentionQuery) (*model
rows, err := r.ch.Query(ctx, sql,
clickhouse.Named("workspace_id", q.WorkspaceID),
clickhouse.DateNamed("from", q.From, clickhouse.MilliSeconds),
clickhouse.DateNamed("to", q.To, clickhouse.MilliSeconds),
clickhouse.Named("from", chTime(q.From)),
clickhouse.Named("to", chTime(q.To)),
clickhouse.Named("initial_event", q.InitialEvent),
clickhouse.Named("return_event", q.ReturnEvent),
)
@@ -148,11 +148,11 @@ func (r *AnalyticsRepo) Sessions(ctx context.Context, q SessionQuery) (*model.Qu
args := []any{
clickhouse.Named("workspace_id", q.WorkspaceID),
clickhouse.DateNamed("from", q.From, clickhouse.MilliSeconds),
clickhouse.DateNamed("to", q.To, clickhouse.MilliSeconds),
clickhouse.Named("timeout_seconds", q.TimeoutSeconds),
clickhouse.Named("limit", uint32(q.Limit)),
clickhouse.Named("offset", uint32(q.Offset)),
clickhouse.Named("from", chTime(q.From)),
clickhouse.Named("to", chTime(q.To)),
clickhouse.Named("timeout_seconds", chUint(uint64(q.TimeoutSeconds))),
clickhouse.Named("limit", chUint(uint64(q.Limit))),
clickhouse.Named("offset", chUint(uint64(q.Offset))),
}
if q.UserID != "" {
args = append(args, clickhouse.Named("user_id", q.UserID))

View File

@@ -3,6 +3,8 @@ package repo
import (
"context"
"fmt"
"strconv"
"time"
"github.com/ClickHouse/clickhouse-go/v2"
"github.com/ClickHouse/clickhouse-go/v2/lib/driver"
@@ -11,6 +13,18 @@ import (
"github.com/dbiz/cdp/data-layer/api/internal/templates"
)
// chTime formats a Go time.Time for ClickHouse server-side query parameters.
// clickhouse-go v2 routes args declared via {name:Type} syntax through the
// server-side parameter protocol, which only accepts string values -- typed
// helpers like clickhouse.DateNamed fail with
// "expected string value in NamedValue for query parameter".
// We emit the format ClickHouse parses for DateTime64(3,'UTC').
func chTime(t time.Time) string {
return t.UTC().Format("2006-01-02 15:04:05.000")
}
func chUint(n uint64) string { return strconv.FormatUint(n, 10) }
type EventRepo struct {
ch driver.Conn
tpl *templates.Store
@@ -39,10 +53,10 @@ func (r *EventRepo) QueryEvents(ctx context.Context, q model.EventQuery) (*model
args := []any{
clickhouse.Named("workspace_id", q.WorkspaceID),
clickhouse.DateNamed("from", q.From, clickhouse.MilliSeconds),
clickhouse.DateNamed("to", q.To, clickhouse.MilliSeconds),
clickhouse.Named("limit", uint32(q.Limit)),
clickhouse.Named("offset", uint32(q.Offset)),
clickhouse.Named("from", chTime(q.From)),
clickhouse.Named("to", chTime(q.To)),
clickhouse.Named("limit", chUint(uint64(q.Limit))),
clickhouse.Named("offset", chUint(uint64(q.Offset))),
}
if q.UserID != "" {
args = append(args, clickhouse.Named("user_id", q.UserID))
@@ -73,8 +87,8 @@ func (r *EventRepo) QueryProfileTimeline(ctx context.Context, workspaceID, userI
rows, err := r.ch.Query(ctx, sql,
clickhouse.Named("workspace_id", workspaceID),
clickhouse.Named("user_id", userID),
clickhouse.Named("limit", uint32(limit)),
clickhouse.Named("offset", uint32(offset)),
clickhouse.Named("limit", chUint(uint64(limit))),
clickhouse.Named("offset", chUint(uint64(offset))),
)
if err != nil {
return nil, fmt.Errorf("clickhouse query: %w", err)
@@ -146,7 +160,8 @@ func newScanTarget(typeName string) any {
var v bool
return &v
case "time.Time":
return new(any) // let driver fill, deref below handles it
var v time.Time
return &v
case "map[string]string":
var v map[string]string
return &v
@@ -186,6 +201,8 @@ func derefScanTarget(p any) any {
return *v
case *[]string:
return *v
case *time.Time:
return *v
case *any:
return *v
default: