Skip to content

Commit b58edf5

Browse files
- EXPLAIN supported.
- Added robot test `Explain Select Repeatably Generates Messages`.
1 parent 3610332 commit b58edf5

File tree

15 files changed

+334
-8
lines changed

15 files changed

+334
-8
lines changed

.vscode/launch.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,9 @@
185185
"replace /*+ AWAIT */ google.compute.firewalls set data__disabled = 'true' where project = 'mutable-project' and firewall = 'replacable-firewall' returning *;",
186186
"select role_name from pgi.information_schema.applicable_roles order by role_name desc;",
187187
"SELECT * FROM datadog.organization.users;",
188+
"explain select 1 as foo;",
189+
"explain select name from google.compute.networks where project = 'stackql-demo';",
190+
"explain select * from aws.ec2.instances where region = 'ap-southeast-2';",
188191
],
189192
"default": "show providers;"
190193
},

internal/stackql/astanalysis/earlyanalysis/first_passes.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,11 @@ func (sp *standardInitialPasses) initialPasses(
219219
return err
220220
}
221221
ast := result.AST
222+
switch node := ast.(type) {
223+
case *sqlparser.Explain:
224+
ast = node.Statement
225+
}
226+
222227
annotatedAST, err := annotatedast.NewAnnotatedAst(sp.parentAnnotatedAST, ast)
223228
if err != nil {
224229
return err
@@ -235,6 +240,8 @@ func (sp *standardInitialPasses) initialPasses(
235240
switch node := subjectAST.(type) {
236241
case *sqlparser.DDL:
237242
subjectAST = node.SelectStatement
243+
// case *sqlparser.Explain:
244+
// subjectAST = node.Statement
238245
}
239246
pbi, pbiErr := planbuilderinput.NewPlanBuilderInput(
240247
annotatedAST,
@@ -326,7 +333,7 @@ func (sp *standardInitialPasses) initialPasses(
326333
pbi, err := planbuilderinput.NewPlanBuilderInput(
327334
annotatedAST,
328335
handlerCtx,
329-
ast,
336+
result.AST,
330337
threeToFiveAgg.GetTables(),
331338
threeToFiveAgg.GetAliasedColumns(),
332339
threeToFiveAgg.GetAliasMap(),

internal/stackql/mcpbackend/mcp_reverse_proxy_backend_service.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"database/sql"
66
"encoding/json"
77
"fmt"
8+
"time"
89

910
"github.com/sirupsen/logrus"
1011
"github.com/stackql/stackql/internal/stackql/handler"
@@ -79,6 +80,33 @@ func (b *stackqlMCPReverseProxyService) Greet(ctx context.Context, args dto.Gree
7980
return "Hi " + args.Name, nil
8081
}
8182

83+
func (b *stackqlMCPReverseProxyService) ExecQuery(ctx context.Context, query string) (map[string]any, error) {
84+
return b.execQuery(ctx, query)
85+
}
86+
87+
func (b *stackqlMCPReverseProxyService) ValidateQuery(ctx context.Context, query string) (map[string]any, error) {
88+
return map[string]any{}, nil
89+
}
90+
91+
func (b *stackqlMCPReverseProxyService) execQuery(ctx context.Context, query string) (map[string]any, error) {
92+
r, sqlErr := b.db.Exec(query)
93+
if sqlErr != nil {
94+
return nil, sqlErr
95+
}
96+
rowsAffected, rowsAffectedErr := r.RowsAffected()
97+
lastInsertId, lastInsertIdErr := r.LastInsertId()
98+
rv := map[string]any{}
99+
if rowsAffectedErr == nil {
100+
rv["rows_affected"] = rowsAffected
101+
}
102+
if lastInsertIdErr == nil {
103+
rv["last_insert_id"] = lastInsertId
104+
}
105+
oneLinerOutput := time.Now().Format("2006-01-02T15:04:05-07:00 MST")
106+
rv["timestamp"] = oneLinerOutput
107+
return rv, nil
108+
}
109+
82110
//nolint:gocognit,funlen // acceptable
83111
func (b *stackqlMCPReverseProxyService) query(ctx context.Context, query string, rowLimit int) ([]map[string]any, error) {
84112
r, sqlErr := b.db.Query(query)

internal/stackql/mcpbackend/mcp_service_stackql.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"io"
88
"strings"
9+
"time"
910

1011
"github.com/sirupsen/logrus"
1112
"github.com/stackql/stackql/internal/stackql/acid/tsm_physio"
@@ -319,6 +320,30 @@ func (b *stackqlMCPService) runPreprocessedQueryJSON(ctx context.Context, query
319320
return results, nil
320321
}
321322

323+
func (b *stackqlMCPService) ExecQuery(ctx context.Context, query string) (map[string]any, error) {
324+
return b.execQuery(ctx, query)
325+
}
326+
327+
func (b *stackqlMCPService) ValidateQuery(ctx context.Context, query string) (map[string]any, error) {
328+
return map[string]any{}, nil
329+
}
330+
331+
func (b *stackqlMCPService) execQuery(ctx context.Context, query string) (map[string]any, error) {
332+
rv := map[string]any{}
333+
r, ok := b.applyQuery(query)
334+
if !ok {
335+
return rv, fmt.Errorf("failed to extract query results")
336+
}
337+
messages := []string{}
338+
for _, resp := range r {
339+
messages = append(messages, resp.GetMessages()...)
340+
}
341+
rv["messages"] = messages
342+
oneLinerOutput := time.Now().Format("2006-01-02T15:04:05-07:00 MST")
343+
rv["timestamp"] = oneLinerOutput
344+
return rv, nil
345+
}
346+
322347
// func (b *stackqlMCPService) ListTableResources(ctx context.Context, hI mcp_server.HierarchyInput) ([]string, error) {
323348
// return []string{}, nil
324349
// }

internal/stackql/planbuilder/entrypoint.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,13 @@ func (pb *standardPlanBuilder) BuildPlanFromContext(handlerCtx handler.HandlerCo
163163
}
164164
}
165165

166+
switch statement.(type) {
167+
case *sqlparser.Explain:
168+
// explain plans are never cacheable and always readonly
169+
qPlan.SetCacheable(false)
170+
qPlan.SetReadOnly(true)
171+
}
172+
166173
if pGBuilder.getPlanGraphHolder().ContainsIndirect() {
167174
qPlan.SetCacheable(false)
168175
}

internal/stackql/planbuilder/plan_builder.go

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ func (pgb *standardPlanGraphBuilder) createInstructionFor(pbi planbuilderinput.P
112112
case *sqlparser.Exec:
113113
return pgb.handleExec(pbi)
114114
case *sqlparser.Explain:
115-
return iqlerror.GetStatementNotSupportedError("EXPLAIN")
115+
return pgb.handleExplain(pbi)
116116
case *sqlparser.Insert:
117117
return pgb.handleInsert(pbi)
118118
case *sqlparser.NativeQuery:
@@ -169,6 +169,44 @@ func (pgb *standardPlanGraphBuilder) nop(pbi planbuilderinput.PlanBuilderInput)
169169
return err
170170
}
171171

172+
func (pgb *standardPlanGraphBuilder) handleExplain(pbi planbuilderinput.PlanBuilderInput) error {
173+
primitiveGenerator := pgb.rootPrimitiveGenerator
174+
explain, _ok := pbi.GetExplain()
175+
if !_ok {
176+
return fmt.Errorf("could not retrieve explain AST node for statement of type '%T'", pbi.GetStatement())
177+
}
178+
childPbi, err := planbuilderinput.NewPlanBuilderInput(
179+
pbi.GetAnnotatedAST(),
180+
pbi.GetHandlerCtx(),
181+
explain.Statement,
182+
pbi.GetTableExprs(),
183+
pbi.GetAssignedAliasedColumns(),
184+
pbi.GetAliasedTables(),
185+
pbi.GetColRefs(),
186+
pbi.GetPlaceholderParams(),
187+
pbi.GetTxnCtrlCtrs(),
188+
)
189+
childPbi.SetReadOnly(true)
190+
if err != nil {
191+
return err
192+
}
193+
instructionErr := pgb.createInstructionFor(childPbi)
194+
explainMessages := []string{}
195+
if instructionErr == nil {
196+
explainMessages = append(explainMessages, "Execution plan generated successfully")
197+
}
198+
genErr := primitiveGenerator.AnalyzeExplain(pbi, explainMessages, instructionErr)
199+
if genErr != nil {
200+
return genErr
201+
}
202+
builder := primitiveGenerator.GetPrimitiveComposer().GetBuilder()
203+
if builder == nil {
204+
return fmt.Errorf("nil explain builder")
205+
}
206+
buildErr := builder.Build()
207+
return buildErr
208+
}
209+
172210
func setLogic(pbi planbuilderinput.PlanBuilderInput, setExpr *sqlparser.SetExpr) error {
173211
lhsRaw := setExpr.Name.GetRawVal()
174212
lhsTrimmed := strings.TrimPrefix(lhsRaw, "$.")

internal/stackql/planbuilderinput/plan_builder_input.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515

1616
type PlanBuilderInput interface {
1717
Clone() PlanBuilderInput
18+
GetChildren() []PlanBuilderInput
1819
Refocus(sqlparser.SQLNode) PlanBuilderInput
1920
GetAliasedTables() parserutil.TableAliasMap
2021
GetAnnotatedAST() annotatedast.AnnotatedAst
@@ -46,6 +47,7 @@ type PlanBuilderInput interface {
4647
GetUpdate() (*sqlparser.Update, bool)
4748
GetUse() (*sqlparser.Use, bool)
4849
GetSet() (*sqlparser.Set, bool)
50+
GetExplain() (*sqlparser.Explain, bool)
4951
IsTccSetAheadOfTime() bool
5052
SetIsTccSetAheadOfTime(bool)
5153
SetPrepStmtOffset(int)
@@ -61,6 +63,7 @@ type PlanBuilderInput interface {
6163
IsReadOnly() bool
6264
Next() (PlanBuilderInput, bool)
6365
WithNext(PlanBuilderInput)
66+
WithChildren([]PlanBuilderInput) PlanBuilderInput
6467
SetTxnCtrlCtrs(tcc internaldto.TxnControlCounters)
6568
}
6669

@@ -85,6 +88,7 @@ type StandardPlanBuilderInput struct {
8588
isCreateMaterializedView bool
8689
rawQuery string
8790
next PlanBuilderInput
91+
children []PlanBuilderInput
8892
}
8993

9094
func NewPlanBuilderInput(
@@ -150,6 +154,15 @@ func (pbi *StandardPlanBuilderInput) WithNext(next PlanBuilderInput) {
150154
pbi.next = next
151155
}
152156

157+
func (pbi *StandardPlanBuilderInput) GetChildren() []PlanBuilderInput {
158+
return pbi.children
159+
}
160+
161+
func (pbi *StandardPlanBuilderInput) WithChildren(children []PlanBuilderInput) PlanBuilderInput {
162+
pbi.children = children
163+
return pbi
164+
}
165+
153166
func (pbi *StandardPlanBuilderInput) Clone() PlanBuilderInput {
154167
clonedPbi := newPlanBuilderInput(
155168
pbi.annotatedAST,
@@ -390,6 +403,11 @@ func (pbi *StandardPlanBuilderInput) GetSet() (*sqlparser.Set, bool) {
390403
return rv, ok
391404
}
392405

406+
func (pbi *StandardPlanBuilderInput) GetExplain() (*sqlparser.Explain, bool) {
407+
rv, ok := pbi.stmt.(*sqlparser.Explain)
408+
return rv, ok
409+
}
410+
393411
func (pbi *StandardPlanBuilderInput) GetUpdate() (*sqlparser.Update, bool) {
394412
rv, ok := pbi.stmt.(*sqlparser.Update)
395413
return rv, ok
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package primitivebuilder
2+
3+
import (
4+
"github.com/stackql/any-sdk/public/sqlengine"
5+
"github.com/stackql/stackql/internal/stackql/handler"
6+
"github.com/stackql/stackql/internal/stackql/internal_data_transfer/internaldto"
7+
"github.com/stackql/stackql/internal/stackql/primitive"
8+
"github.com/stackql/stackql/internal/stackql/primitivegraph"
9+
"github.com/stackql/stackql/internal/stackql/util"
10+
)
11+
12+
var (
13+
defaultConcludeExplainMessages []string = []string{"OK"} //nolint:revive,gochecknoglobals // prefer declarative
14+
)
15+
16+
type ExplainBuilder struct {
17+
graph primitivegraph.PrimitiveGraphHolder
18+
handlerCtx handler.HandlerContext
19+
root primitivegraph.PrimitiveNode
20+
sqlEngine sqlengine.SQLEngine
21+
messages []string
22+
instructionErr error
23+
}
24+
25+
func NewExplainBuilder(
26+
graph primitivegraph.PrimitiveGraphHolder,
27+
txnControlCounters internaldto.TxnControlCounters, //nolint:revive // future proofing
28+
handlerCtx handler.HandlerContext,
29+
sqlEngine sqlengine.SQLEngine,
30+
messages []string,
31+
instructionErr error,
32+
) Builder {
33+
if len(messages) == 0 {
34+
messages = []string{}
35+
}
36+
messages = append(messages, defaultConcludeExplainMessages...)
37+
return &ExplainBuilder{
38+
graph: graph,
39+
handlerCtx: handlerCtx,
40+
sqlEngine: sqlEngine,
41+
messages: messages,
42+
instructionErr: instructionErr,
43+
}
44+
}
45+
46+
func (nb *ExplainBuilder) Build() error {
47+
pr := primitive.NewLocalPrimitive(
48+
//nolint:revive // no big deal
49+
func(pc primitive.IPrimitiveCtx) internaldto.ExecutorOutput {
50+
if nb.instructionErr != nil {
51+
return internaldto.NewErroneousExecutorOutput(nb.instructionErr)
52+
}
53+
return util.PrepareResultSet(
54+
internaldto.NewPrepareResultSetPlusRawDTO(
55+
nil,
56+
nil,
57+
nil,
58+
nil,
59+
nil,
60+
internaldto.NewBackendMessages(nb.messages), nil,
61+
nb.handlerCtx.GetTypingConfig()),
62+
)
63+
},
64+
)
65+
nb.root = nb.graph.CreatePrimitiveNode(pr)
66+
return nil
67+
}
68+
69+
func (nb *ExplainBuilder) GetRoot() primitivegraph.PrimitiveNode {
70+
return nb.root
71+
}
72+
73+
func (nb *ExplainBuilder) GetTail() primitivegraph.PrimitiveNode {
74+
return nb.root
75+
}

internal/stackql/primitivegenerator/primitive_generator.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ type PrimitiveGenerator interface {
3434
AddChildPrimitiveGenerator(ast sqlparser.SQLNode, leaf symtab.SymTab) PrimitiveGenerator
3535
AnalyzeInsert(pbi planbuilderinput.PlanBuilderInput) error
3636
AnalyzeNop(pbi planbuilderinput.PlanBuilderInput) error
37+
AnalyzeExplain(pbi planbuilderinput.PlanBuilderInput, messages []string, instructionErr error) error
3738
AnalyzeUpdate(pbi planbuilderinput.PlanBuilderInput) error
3839
AnalyzePGInternal(pbi planbuilderinput.PlanBuilderInput) error
3940
AnalyzeRegistry(pbi planbuilderinput.PlanBuilderInput) error

internal/stackql/primitivegenerator/statement_analyzer.go

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ func (pb *standardPrimitiveGenerator) AnalyzeStatement(
6868
case *sqlparser.Exec:
6969
return pb.analyzeExec(pbi)
7070
case *sqlparser.Explain:
71-
return iqlerror.GetStatementNotSupportedError("EXPLAIN")
71+
return fmt.Errorf("direct access to %s analysis not supported", "EXPLAIN")
7272
case *sqlparser.Insert:
7373
return pb.AnalyzeInsert(pbi)
7474
case *sqlparser.OtherRead, *sqlparser.OtherAdmin:
@@ -677,6 +677,26 @@ func (pb *standardPrimitiveGenerator) AnalyzeNop(
677677
return nil
678678
}
679679

680+
func (pb *standardPrimitiveGenerator) AnalyzeExplain(
681+
pbi planbuilderinput.PlanBuilderInput,
682+
messages []string, // future proofing free form messages
683+
instructionErr error,
684+
) error {
685+
handlerCtx := pbi.GetHandlerCtx()
686+
_ = pb.PrimitiveComposer.GetGraphHolder().Blank()
687+
pb.PrimitiveComposer.SetBuilder(
688+
primitivebuilder.NewExplainBuilder(
689+
pb.PrimitiveComposer.GetGraphHolder(),
690+
pb.PrimitiveComposer.GetTxnCtrlCtrs(),
691+
handlerCtx,
692+
handlerCtx.GetSQLEngine(),
693+
messages,
694+
instructionErr,
695+
),
696+
)
697+
return nil
698+
}
699+
680700
func (pb *standardPrimitiveGenerator) analyzeExec(pbi planbuilderinput.PlanBuilderInput) error {
681701
handlerCtx := pbi.GetHandlerCtx()
682702
annotatedAST := pbi.GetAnnotatedAST()
@@ -871,6 +891,11 @@ func (pb *standardPrimitiveGenerator) analyzeSchemaVsMap(
871891
func (pb *standardPrimitiveGenerator) AnalyzePGInternal(
872892
pbi planbuilderinput.PlanBuilderInput) error {
873893
handlerCtx := pbi.GetHandlerCtx()
894+
switch pbi.GetStatement().(type) {
895+
case *sqlparser.Explain:
896+
return pb.AnalyzeExplain(pbi, pbi.GetMessages(), nil)
897+
}
898+
// pass through
874899
if backendQueryType, ok := handlerCtx.GetDBMSInternalRouter().CanRoute(pbi.GetStatement()); ok {
875900
if backendQueryType == constants.BackendQuery {
876901
bldr := primitivebuilder.NewRawNativeSelect(

0 commit comments

Comments
 (0)