-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
318d0d9
commit b2db684
Showing
7 changed files
with
381 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
Oops, something went wrong.