Skip to content

Commit

Permalink
wip: clickhouse sql dsl
Browse files Browse the repository at this point in the history
  • Loading branch information
stergiotis committed Jan 4, 2025
1 parent 318d0d9 commit b2db684
Show file tree
Hide file tree
Showing 7 changed files with 381 additions and 0 deletions.
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.22.0
toolchain go1.23.1

require (
github.com/AfterShip/clickhouse-sql-parser v0.4.1
github.com/brianvoe/gofakeit/v7 v7.1.2
github.com/cenkalti/backoff/v4 v4.3.0
github.com/davecgh/go-spew v1.1.1
Expand Down Expand Up @@ -34,6 +35,7 @@ require (
github.com/klauspost/compress v1.17.11 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/matoous/go-nanoid/v2 v2.1.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
Expand Down
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
fortio.org/assert v1.2.1 h1:48I39urpeDj65RP1KguF7akCjILNeu6vICiYMEysR7Q=
fortio.org/assert v1.2.1/go.mod h1:039mG+/iYDPO8Ibx8TrNuJCm2T2SuhwRI3uL9nHTTls=
github.com/AfterShip/clickhouse-sql-parser v0.4.1 h1:H6i9GqXnFw8OccVtlbZWR83hLDyYka41Tg+QIi67JJ4=
github.com/AfterShip/clickhouse-sql-parser v0.4.1/go.mod h1:W0Z82wJWkJxz2RVun/RMwxue3g7ut47Xxl+SFqdJGus=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/brianvoe/gofakeit/v7 v7.1.2 h1:vSKaVScNhWVpf1rlyEKSvO8zKZfuDtGqoIHT//iNNb8=
github.com/brianvoe/gofakeit/v7 v7.1.2/go.mod h1:QXuPeBw164PJCzCUZVmgpgHJ3Llj49jSLVkKPMtxtxA=
Expand Down Expand Up @@ -71,6 +73,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/matoous/go-nanoid/v2 v2.1.0 h1:P64+dmq21hhWdtvZfEAofnvJULaRR1Yib0+PnU669bE=
github.com/matoous/go-nanoid/v2 v2.1.0/go.mod h1:KlbGNQ+FhrUNIHUxZdL63t7tl4LaPkZNpUULS8H4uVM=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
Expand Down Expand Up @@ -101,6 +105,10 @@ github.com/scalalang2/golang-fifo v1.0.2 h1:sfOJBB86iXuqB5WoLtVI7+wxn8UOEOr9SnJa
github.com/scalalang2/golang-fifo v1.0.2/go.mod h1:TsyVkLbka5m8tmfqsWBXwJ7Om1jV/uuOuvoPulZbMmA=
github.com/schollz/progressbar/v3 v3.17.1 h1:bI1MTaoQO+v5kzklBjYNRQLoVpe0zbyRZNK6DFkVC5U=
github.com/schollz/progressbar/v3 v3.17.1/go.mod h1:RzqpnsPQNjUyIgdglUjRLgD7sVnxN1wpmBMV+UiEbL4=
github.com/sebdah/goldie/v2 v2.5.3 h1:9ES/mNN+HNUbNWpVAlrzuZ7jE+Nrczbj8uFRjM7624Y=
github.com/sebdah/goldie/v2 v2.5.3/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
Expand Down
95 changes: 95 additions & 0 deletions public/db/clickhouse/dsl/dsl.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package dsl

import (
"fmt"
chparser "github.com/AfterShip/clickhouse-sql-parser/parser"
"github.com/stergiotis/boxer/public/observability/eh"
"github.com/stergiotis/boxer/public/observability/eh/eb"
"strings"
)

type Dsl struct {
tableIdTransformer TransfomerI
Exprs []chparser.Expr
}

func NewDsl(tableIdTransformer TransfomerI) (inst *Dsl, err error) {
inst = &Dsl{
tableIdTransformer: tableIdTransformer,
Exprs: nil,
}
return
}
func (inst *Dsl) Parse(sql string) (err error) {
p := chparser.NewParser(sql)
inst.Exprs, err = p.ParseStmts()
if err != nil {
err = eh.Errorf("unable to parse sql: %w", err)
return
}
return
}
func (inst *Dsl) Transform() (err error) {
err = inst.checkParsed()
if err != nil {
return
}
if inst.tableIdTransformer != nil {
tr := inst.tableIdTransformer
for i, expr := range inst.Exprs {
err = tr.Apply(expr)
if err != nil {
err = eb.Build().Int("exprIndex", i).Errorf("unable to apply ast visitor: %w", err)
return
}
}
}

return
}

var ErrNoParsedAsAvailable = eh.Errorf("no parsed AST available")

func (inst *Dsl) checkParsed() (err error) {
if len(inst.Exprs) == 0 {
return ErrNoParsedAsAvailable
}
return
}

/*
func (inst *Dsl) FromExprs() (err error) {
err = inst.checkParsed()
if err != nil {
return
}
return
}
*/
func (inst *Dsl) Apply(visitor chparser.ASTVisitor) (err error) {
err = inst.checkParsed()
if err != nil {
return
}
for i, expr := range inst.Exprs {
err = expr.Accept(visitor)
if err != nil {
err = eb.Build().Int("exprIndex", i).Errorf("unable to apply ast visitor: %w", err)
return
}
}
return
}
func (inst *Dsl) String() string {
if len(inst.Exprs) == 0 {
return ""
}
b := strings.Builder{}
for _, s := range inst.Exprs {
b.WriteString(s.String())
b.WriteString(";\n")
}
return b.String()
}

var _ fmt.Stringer = (*Dsl)(nil)
47 changes: 47 additions & 0 deletions public/db/clickhouse/dsl/preparedsql.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package dsl

import (
"fmt"
chparser "github.com/AfterShip/clickhouse-sql-parser/parser"
"github.com/stergiotis/boxer/public/observability/eh"
"github.com/stergiotis/boxer/public/observability/eh/eb"
)

type PreparedSql struct {
inputSql string
ast chparser.Expr
}

func (inst *PreparedSql) String() string {
return inst.ast.String()
}

func NewPreparedSql(sql string) (inst *PreparedSql, err error) {
inst = &PreparedSql{
inputSql: sql,
ast: nil,
}
err = inst.prepare()
if err != nil {
err = eh.Errorf("unable to prepare sql: %w", err)
return
}
return
}
func (inst *PreparedSql) prepare() (err error) {
p := chparser.NewParser(inst.inputSql)
var exprs []chparser.Expr
exprs, err = p.ParseStmts()
if err != nil {
err = eh.Errorf("unable to parse sql: %w", err)
return
}
if len(exprs) != 1 {
err = eb.Build().Int("nExprs", len(exprs)).Errorf("sql must contain exactly on expression")
return
}
inst.ast = exprs[0]
return
}

var _ fmt.Stringer = (*PreparedSql)(nil)
180 changes: 180 additions & 0 deletions public/db/clickhouse/dsl/tableidtransformer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
package dsl

import (
chparser "github.com/AfterShip/clickhouse-sql-parser/parser"
"github.com/matoous/go-nanoid/v2"
"github.com/rs/zerolog/log"
"github.com/stergiotis/boxer/public/observability/eh"
"github.com/stergiotis/boxer/public/observability/eh/eb"
"slices"
"strconv"
)

type TableIdTransformer struct {
chparser.DefaultASTVisitor
plugins []TableIdTransformerPluginI

tableIdentifiers []*chparser.TableIdentifier
replacements []chparser.Expr
ctes []*chparser.CTEStmt
}

func NewTableIdTransformer() *TableIdTransformer {
return &TableIdTransformer{
DefaultASTVisitor: chparser.DefaultASTVisitor{},
plugins: make([]TableIdTransformerPluginI, 0, 64),

tableIdentifiers: make([]*chparser.TableIdentifier, 0, 128),
replacements: make([]chparser.Expr, 0, 128),
ctes: make([]*chparser.CTEStmt, 0, 128),
}
}
func (inst *TableIdTransformer) AddPlugin(plugin TableIdTransformerPluginI) {
if plugin != nil {
inst.plugins = append(inst.plugins, plugin)
}
}

func (inst *TableIdTransformer) Reset() (err error) {
clear(inst.tableIdentifiers)
inst.tableIdentifiers = inst.tableIdentifiers[:0]
clear(inst.replacements)
inst.replacements = inst.replacements[:0]
clear(inst.ctes)
inst.ctes = inst.ctes[:0]
return
}

var ErrUnhandledAstType = eh.Errorf("unhandled ast type")

func (inst *TableIdTransformer) Apply(ast chparser.Expr) (err error) {
err = inst.Reset()
if err != nil {
err = eh.Errorf("unable to reset transformer: %w", err)
return
}
err = ast.Accept(inst)
if err != nil {
err = eh.Errorf("unable to apply ast visitor: %w", err)
return
}
if len(inst.tableIdentifiers) > 0 {
inst.replacements = slices.Grow(inst.replacements, len(inst.tableIdentifiers))
for _, t := range inst.tableIdentifiers {
var repl chparser.Expr
repl, err = inst.transformTableIdentifier(t)
if err != nil {
err = eh.Errorf("unable to transform table identifier: %w", err)
return
}
inst.replacements = append(inst.replacements, repl)
}
}
err = inst.populateCtes()
if err != nil {
err = eh.Errorf("unable to populate CTEs: %w", err)
return
}
switch astt := ast.(type) {
case *chparser.SelectQuery:
if astt.With == nil {
astt.With = &chparser.WithClause{
WithPos: 0,
EndPos: 0,
CTEs: inst.ctes,
}
} else {
astt.With.CTEs = append(inst.ctes, astt.With.CTEs...)
}
break
default:
err = eb.Build().Type("ast", ast).Errorf("unhandled type: %w", ErrUnhandledAstType)
}
return
}
func (inst *TableIdTransformer) populateCtes() (err error) {
ctes := slices.Grow(inst.ctes, len(inst.tableIdentifiers))
var key string
key, err = gonanoid.Generate("abcdefghijklmnopqrstuvwxyz_", 24)
if err != nil {
err = eh.Errorf("unable to generate key for table identifiers: %w", err)
return
}
var u uint64
for i, t := range inst.tableIdentifiers {
r := inst.replacements[i]
if r != nil {
n := key + "_" + strconv.FormatUint(u, 10)
t.Database = nil
t.Table.Name = n
t.Table.NamePos = 0
t.Table.NameEnd = 0
t.Table.QuoteType = chparser.BackTicks

ctes = append(ctes, &chparser.CTEStmt{
CTEPos: 0,
Expr: &chparser.Ident{
Name: n,
QuoteType: chparser.BackTicks,
NamePos: 0,
NameEnd: 0,
},
Alias: r,
})
u++
}
}
inst.ctes = ctes
return
}
func (inst *TableIdTransformer) transformTableIdentifier(t *chparser.TableIdentifier) (replacement chparser.Expr, err error) {
var db string
if t.Database != nil {
db = t.Database.Name
}
tbl := t.Table.Name
for _, p := range inst.plugins {
var repl *PreparedSql
var isStaticReplacement bool
var appl bool
repl, isStaticReplacement, appl, err = p.Transform(db, tbl)
if appl {
name := p.Name()
if repl == nil {
log.Warn().Str("plugin", name).Msg("plugin returned nil for transformed expression, skipping")
} else {
if !isStaticReplacement {
replacement, err = deepCopyAst(repl.ast)
if err != nil {
err = eh.Errorf("unable to deep copy non-static replacement: %w", err)
return
}
}
log.Debug().Str("db", db).Str("tbl", tbl).Str("transformer", p.Name()).Bool("isStaticReplacement", isStaticReplacement).Msg("transforming table identifier")
// first applicable transformer wins
return
}
}
}
log.Debug().Str("db", db).Str("tbl", tbl).Msg("no transformer is applicable for table identifier")
return
}

func (inst *TableIdTransformer) VisitTableIdentifier(expr *chparser.TableIdentifier) error {
inst.tableIdentifiers = append(inst.tableIdentifiers, expr)
if expr.Database == nil || expr.Table == nil {
return nil
}

input := expr.Table
output := &chparser.Ident{
Name: "asdadasd",
QuoteType: chparser.BackTicks,
}
expr.Table = output
log.Debug().Stringer("db", expr.Database).Stringer("input", input).Stringer("output", output).Msg("transforming table identifier")
return nil
}

var _ chparser.ASTVisitor = (*TableIdTransformer)(nil)
var _ TransfomerI = (*TableIdTransformer)(nil)
11 changes: 11 additions & 0 deletions public/db/clickhouse/dsl/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package dsl

import chparser "github.com/AfterShip/clickhouse-sql-parser/parser"

type TransfomerI interface {
Apply(ast chparser.Expr) (err error)
}
type TableIdTransformerPluginI interface {
Name() string
Transform(db string, table string) (replacement *PreparedSql, isStaticReplacement bool, applicable bool, err error)
}
Loading

0 comments on commit b2db684

Please sign in to comment.