Skip to content
8 changes: 6 additions & 2 deletions cmd/benchmark/explain.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package main
import (
"context"
"fmt"
"maps"

"github.com/specterops/dawgs/cypher/frontend"
"github.com/specterops/dawgs/cypher/models/pgsql"
Expand Down Expand Up @@ -50,7 +51,9 @@ func newPostgresExplainer(kindMapper pgsql.KindMapper, graphID int32) ExplainFun
return nil, err
}

result := tx.Raw("EXPLAIN (ANALYZE, BUFFERS) "+sqlQuery, translation.Parameters)
maps.Copy(translation.Parameters, sqlQuery.Parameters)

result := tx.Raw("EXPLAIN (ANALYZE, BUFFERS) "+sqlQuery.Statement, translation.Parameters)
defer result.Close()

var plan []string
Expand All @@ -67,8 +70,9 @@ func newPostgresExplainer(kindMapper pgsql.KindMapper, graphID int32) ExplainFun
return nil, err
}

// TODO: should this get the parameters as well?
return &ExplainResult{
SQL: sqlQuery,
SQL: sqlQuery.Statement,
Plan: plan,
Optimization: translation.Optimization,
}, nil
Expand Down
8 changes: 6 additions & 2 deletions cmd/graphbench/postgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package main
import (
"context"
"fmt"
"maps"
"regexp"
"strconv"
"strings"
Expand Down Expand Up @@ -187,9 +188,11 @@ func (s *postgresSQLRunner) explain(ctx context.Context, cypherQuery string, par
return postgresExplain{}, err
}

maps.Copy(translation.Parameters, sqlQuery.Parameters)

var plan []string
if err := s.db.ReadTransaction(ctx, func(tx graph.Transaction) error {
result := tx.Raw("EXPLAIN (ANALYZE, BUFFERS, TIMING OFF) "+sqlQuery, translation.Parameters)
result := tx.Raw("EXPLAIN (ANALYZE, BUFFERS, TIMING OFF) "+sqlQuery.Statement, translation.Parameters)
defer result.Close()

for result.Next() {
Expand All @@ -206,8 +209,9 @@ func (s *postgresSQLRunner) explain(ctx context.Context, cypherQuery string, par
return postgresExplain{}, err
}

// TODO: should this get the parameters as well?
return postgresExplain{
SQL: sqlQuery,
SQL: sqlQuery.Statement,
Plan: plan,
Metrics: parsePostgresPlanMetrics(plan),
Optimization: translation.Optimization,
Expand Down
8 changes: 6 additions & 2 deletions cmd/plancorpus/capture.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"context"
"fmt"
"maps"
"net/url"
"os"
"path/filepath"
Expand Down Expand Up @@ -280,9 +281,11 @@ func (s *backendCapture) capturePostgres(ctx context.Context, cypherQuery string
return
}

maps.Copy(translation.Parameters, sqlQuery.Parameters)

var plan []string
if err := s.db.ReadTransaction(ctx, func(tx graph.Transaction) error {
result := tx.Raw("EXPLAIN "+sqlQuery, translation.Parameters)
result := tx.Raw("EXPLAIN "+sqlQuery.Statement, translation.Parameters)
defer result.Close()

for result.Next() {
Expand All @@ -298,7 +301,8 @@ func (s *backendCapture) capturePostgres(ctx context.Context, cypherQuery string
record.Error = err.Error()
}

record.SQL = sqlQuery
// TODO: should this get the parameters as well?
record.SQL = sqlQuery.Statement
record.PGPlan = plan
record.PGOperators = postgresOperators(plan)
record.PlannedLowerings = loweringNames(translation.Optimization.PlannedLowerings)
Expand Down
138 changes: 115 additions & 23 deletions cypher/models/pgsql/format/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,25 @@ import (
)

type OutputBuilder struct {
MaterializeParameters bool
StripLiterals bool
parameters map[string]any
params map[string]any
materializeParameters bool
materializedParams map[string]any
builder *strings.Builder
// TODO: figure out how to use a shared generator
generator pgsql.IdentifierGenerator
}

func NewOutputBuilder() *OutputBuilder {
return &OutputBuilder{
builder: &strings.Builder{},
builder: &strings.Builder{},
generator: pgsql.NewIdentifierGenerator(),
params: make(map[string]any),
}
}

func (s *OutputBuilder) WithMaterializedParameters(parameters map[string]any) *OutputBuilder {
s.MaterializeParameters = true
s.parameters = parameters
s.materializeParameters = true
s.materializedParams = parameters

return s
}
Expand All @@ -47,19 +51,32 @@ func (s *OutputBuilder) Write(values ...any) {
}
}

func (s *OutputBuilder) Build() string {
return s.builder.String()
func (s *OutputBuilder) Build() Formatted {
return Formatted{
Statement: s.builder.String(),
Parameters: s.params,
}
}

func formatSlice[T any, TS []T](builder *OutputBuilder, slice TS, dataType pgsql.DataType) error {
builder.Write("array [")

var (
tval T
fmtFunc func(builder *OutputBuilder, value any) error
)
if _, ok := any(tval).(string); ok {
fmtFunc = formatAsParameter
} else {
fmtFunc = formatValue
}

for idx, value := range slice {
if idx > 0 {
builder.Write(", ")
}

if err := formatValue(builder, value); err != nil {
if err := fmtFunc(builder, value); err != nil {
return err
}
}
Expand All @@ -68,6 +85,76 @@ func formatSlice[T any, TS []T](builder *OutputBuilder, slice TS, dataType pgsql
return nil
}

func formatParameterWithBinding(builder *OutputBuilder, value any) error {
switch value.(type) {
case int64, uint64, string, bool, float64:
default:
return fmt.Errorf("unsupported parameter type: %T", value)
}

if ident, err := builder.generator.NewIdentifier(pgsql.ParameterIdentifier); err != nil {
return fmt.Errorf("error creating bound parameter identifier: %w", err)
} else {
builder.params[ident.String()] = value
builder.Write("@", ident.String())
}

if _, ok := value.(string); ok {
builder.Write("::text")
}

return nil
}

func formatAsParameter(builder *OutputBuilder, value any) error {
switch typedValue := value.(type) {
case uint:
return formatParameterWithBinding(builder, uint64(typedValue))

case uint8:
return formatParameterWithBinding(builder, uint64(typedValue))

case uint16:
return formatParameterWithBinding(builder, uint64(typedValue))

case uint32:
return formatParameterWithBinding(builder, uint64(typedValue))

case uint64:
return formatParameterWithBinding(builder, typedValue)

case int:
return formatParameterWithBinding(builder, int64(typedValue))

case int8:
return formatParameterWithBinding(builder, int64(typedValue))

case int16:
return formatParameterWithBinding(builder, int64(typedValue))

case int32:
return formatParameterWithBinding(builder, int64(typedValue))

case int64:
return formatParameterWithBinding(builder, typedValue)

case string:
return formatParameterWithBinding(builder, typedValue)

case bool:
return formatParameterWithBinding(builder, typedValue)

case float32:
return formatParameterWithBinding(builder, float64(typedValue))

case float64:
return formatParameterWithBinding(builder, typedValue)

default:
return fmt.Errorf("unsupported parameter type: %T", value)
}
}

func formatValue(builder *OutputBuilder, value any) error {
switch typedValue := value.(type) {
case uint:
Expand Down Expand Up @@ -145,7 +232,12 @@ func formatLiteral(builder *OutputBuilder, literal pgsql.Literal) error {
builder.Write("interval ")
}

return formatValue(builder, literal.Value)
switch literal.Value.(type) {
case string:
return formatAsParameter(builder, literal.Value)
default:
return formatValue(builder, literal.Value)
}
}

func formatCase(builder *OutputBuilder, caseExpr pgsql.Case) error {
Expand Down Expand Up @@ -546,8 +638,8 @@ func formatNode(builder *OutputBuilder, rootExpr pgsql.SyntaxNode) error {
)

case pgsql.Parameter:
if builder.MaterializeParameters {
if parameterValue, hasParameter := builder.parameters[typedNextExpr.Identifier.String()]; !hasParameter {
if builder.materializeParameters {
if parameterValue, hasParameter := builder.materializedParams[typedNextExpr.Identifier.String()]; !hasParameter {
return fmt.Errorf("invalid parameter %s", typedNextExpr.Identifier.String())
} else if parameterLiteral, err := pgsql.AsLiteral(parameterValue); err != nil {
return fmt.Errorf("invalid parameter value for %s: %v", typedNextExpr.Identifier.String(), err)
Expand Down Expand Up @@ -611,9 +703,9 @@ func formatNode(builder *OutputBuilder, rootExpr pgsql.SyntaxNode) error {
return nil
}

func Expression(expression pgsql.SyntaxNode, builder *OutputBuilder) (string, error) {
func Expression(expression pgsql.SyntaxNode, builder *OutputBuilder) (Formatted, error) {
if err := formatNode(builder, expression); err != nil {
return "", err
return Formatted{}, err
}

return builder.Build(), nil
Expand Down Expand Up @@ -1159,42 +1251,42 @@ func formatDeleteStatement(builder *OutputBuilder, sqlDelete pgsql.Delete) error
return nil
}

func Statement(statement pgsql.Statement, builder *OutputBuilder) (string, error) {
func Statement(statement pgsql.Statement, builder *OutputBuilder) (Formatted, error) {
switch typedStatement := statement.(type) {
case pgsql.Merge:
if err := formatMergeStatement(builder, typedStatement); err != nil {
return "", err
return Formatted{}, err
}

case pgsql.Query:
if err := formatSetExpression(builder, typedStatement); err != nil {
return "", err
return Formatted{}, err
}

case pgsql.Insert:
if err := formatInsertStatement(builder, typedStatement); err != nil {
return "", err
return Formatted{}, err
}

case pgsql.Update:
if err := formatUpdateStatement(builder, typedStatement); err != nil {
return "", err
return Formatted{}, err
}

case pgsql.Delete:
if err := formatDeleteStatement(builder, typedStatement); err != nil {
return "", err
return Formatted{}, err
}

default:
return "", fmt.Errorf("unsupported PgSQL statement type: %T", statement)
return Formatted{}, fmt.Errorf("unsupported PgSQL statement type: %T", statement)
}

builder.Write(";")
return builder.Build(), nil
}

func SyntaxNode(node pgsql.SyntaxNode) (string, error) {
func SyntaxNode(node pgsql.SyntaxNode) (Formatted, error) {
builder := NewOutputBuilder()

switch typedNode := node.(type) {
Expand All @@ -1205,7 +1297,7 @@ func SyntaxNode(node pgsql.SyntaxNode) (string, error) {
return Expression(typedNode, builder)

default:
return "", fmt.Errorf("unknown SQL AST type: %T", node)
return Formatted{}, fmt.Errorf("unknown SQL AST type: %T", node)
}
}

Expand Down
Loading
Loading