diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml deleted file mode 100644 index 38cee8905..000000000 --- a/.github/workflows/frontend.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Frontend CI -on: - workflow_dispatch: - pull_request: - push: - branches: - - main -jobs: - build: - runs-on: ubuntu-latest - strategy: - matrix: - node-version: [18.x] - steps: - - name: Check out code - uses: actions/checkout@v3 - with: - fetch-depth: 1 - - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 - with: - node-version: ${{ matrix.node-version }} - - - name: Build - run: | - make build-frontend diff --git a/app/broker/api/exec/execute.go b/app/broker/api/exec/execute.go index faa0f09f2..82c049066 100644 --- a/app/broker/api/exec/execute.go +++ b/app/broker/api/exec/execute.go @@ -21,6 +21,7 @@ import ( "context" "errors" "fmt" + "net/http" "reflect" "github.com/gin-gonic/gin" @@ -85,9 +86,20 @@ func (e *ExecuteAPI) Register(route gin.IRoutes) { // @Router /exec [post] func (e *ExecuteAPI) Execute(c *gin.Context) { if err := e.deps.QueryLimiter.Do(func() error { + // FIXME: move to common pkg + defer func() { + if err := recover(); err != nil { + msg := fmt.Sprintf("%v", err) + _ = c.Error(errors.New(msg)) + c.Header("Content-Type", "text/plain") + c.String(http.StatusInternalServerError, msg) + } + }() return e.execute(c) }); err != nil { - httppkg.Error(c, err) + _ = c.Error(err) + c.Header("Content-Type", "text/plain") + c.String(http.StatusInternalServerError, err.Error()) } } @@ -129,7 +141,6 @@ func (e *ExecuteAPI) execute(c *gin.Context) error { if stmt == nil { return errors.New("can't parse lin query language") } - // set query session context preparedStmt := &tree.PreparedStatement{ Statement: stmt, diff --git a/constants/render.go b/constants/render.go new file mode 100644 index 000000000..31c6cfdf9 --- /dev/null +++ b/constants/render.go @@ -0,0 +1,7 @@ +package constants + +const ( + VerticalLine = "│" + LastNode = "└─" + IntermediateNode = "├─" +) diff --git a/pkg/strutil/indent.go b/pkg/strutil/indent.go new file mode 100644 index 000000000..8cb9b1f76 --- /dev/null +++ b/pkg/strutil/indent.go @@ -0,0 +1,61 @@ +package strutil + +import ( + "strings" + + "github.com/lindb/lindb/constants" +) + +type Indent struct { + firstLinePrefix string + nextLinesPrefix string + hasChildren bool +} + +func NewIndent(level int, hasChildren bool) *Indent { + indent := indentString(level) + return &Indent{ + firstLinePrefix: indent, + nextLinesPrefix: indent, + hasChildren: hasChildren, + } +} + +func (i *Indent) NodeIndent() string { + return i.firstLinePrefix +} + +func (i *Indent) DetailIndent() string { + indent := "" + if i.hasChildren { + indent = constants.VerticalLine + } + return i.nextLinesPrefix + pad(indent, 2) +} + +func (i *Indent) ForChild(last, hasChildren bool) *Indent { + var ( + first string + next string + ) + if last { + first = pad(constants.LastNode, 3) + next = pad("", 3) + } else { + first = pad(constants.IntermediateNode, 3) + next = pad(constants.VerticalLine, 3) + } + return &Indent{ + firstLinePrefix: i.nextLinesPrefix + first, + nextLinesPrefix: i.nextLinesPrefix + next, + hasChildren: hasChildren, + } +} + +func indentString(indent int) string { + return strings.Repeat(" ", indent) +} + +func pad(text string, length int) string { + return text + strings.Repeat(" ", length-len([]rune(text))) +} diff --git a/sql/execution/buffer/result_build.go b/sql/execution/buffer/result_build.go index b8f8dbb9b..58114d3d0 100644 --- a/sql/execution/buffer/result_build.go +++ b/sql/execution/buffer/result_build.go @@ -107,11 +107,12 @@ func (rsb *ResultSetBuild) Process() { columns[i] = timeSeries.GetValue() } case types.DTTimestamp: + // FIXME: maybe timestamp is nil columns[i] = row.GetTimestamp(i).UnixMilli() case types.DTDuration: columns[i] = row.GetDuration(i) default: - panic(fmt.Sprintf("build result set error, column:%v, unknown data type:%v", meta.Name, meta.DataType)) + panic(fmt.Sprintf("build resultset error, column:%v, unknown data type:%v", meta.Name, meta.DataType)) } } rsb.resultSet.Rows = append(rsb.resultSet.Rows, columns) @@ -130,6 +131,5 @@ func (rsb *ResultSetBuild) ResultSet() *model.ResultSet { // waiting process result page completed <-rsb.completed fmt.Println("result.....") - fmt.Println(string(encoding.JSONMarshal(rsb.resultSet))) return rsb.resultSet } diff --git a/sql/execution/pipeline/operator/exchange/local.go b/sql/execution/pipeline/operator/exchange/local.go index be2c6f84a..69132c3a6 100644 --- a/sql/execution/pipeline/operator/exchange/local.go +++ b/sql/execution/pipeline/operator/exchange/local.go @@ -52,3 +52,7 @@ func (l *LocalExchangeOperator) Run(output chan<- *types.Page) { func (l *LocalExchangeOperator) GetLayout() []*plan.Symbol { return l.node.GetOutputSymbols() } + +func (l *LocalExchangeOperator) Children() []operator.Operator { + return []operator.Operator{l.child} +} diff --git a/sql/execution/pipeline/operator/exchange/remote.go b/sql/execution/pipeline/operator/exchange/remote.go index bdfdd7d3f..d2690d970 100644 --- a/sql/execution/pipeline/operator/exchange/remote.go +++ b/sql/execution/pipeline/operator/exchange/remote.go @@ -77,3 +77,7 @@ func (op *RemoteExchangeOperator) GetLayout() []*plan.Symbol { func (op *RemoteExchangeOperator) Complete() { close(op.inbound) } + +func (op *RemoteExchangeOperator) Children() []operator.Operator { + return nil +} diff --git a/sql/execution/pipeline/operator/hash_aggregation_operator.go b/sql/execution/pipeline/operator/hash_aggregation_operator.go index c095a2df2..6be401ace 100644 --- a/sql/execution/pipeline/operator/hash_aggregation_operator.go +++ b/sql/execution/pipeline/operator/hash_aggregation_operator.go @@ -51,3 +51,7 @@ func (h *HashAggregationOperator) Run(output chan<- *types.Page) { func (h *HashAggregationOperator) GetLayout() []*plan.Symbol { return h.node.GetOutputSymbols() } + +func (h *HashAggregationOperator) Children() []Operator { + return []Operator{h.child} +} diff --git a/sql/execution/pipeline/operator/operator.go b/sql/execution/pipeline/operator/operator.go index e2a3f0ad1..42cc77327 100644 --- a/sql/execution/pipeline/operator/operator.go +++ b/sql/execution/pipeline/operator/operator.go @@ -26,6 +26,7 @@ type Operator interface { Run(output chan<- *types.Page) // GetLayout returns the output layout. GetLayout() []*plan.Symbol + Children() []Operator } type SourceOperator interface { diff --git a/sql/execution/pipeline/operator/output/result_set_output.go b/sql/execution/pipeline/operator/output/result_set_output.go index e5baaf60e..f2c712871 100644 --- a/sql/execution/pipeline/operator/output/result_set_output.go +++ b/sql/execution/pipeline/operator/output/result_set_output.go @@ -96,3 +96,7 @@ func (op *ResultSetOutputOperator) Run(output chan<- *types.Page) { func (op *ResultSetOutputOperator) GetLayout() []*plan.Symbol { panic("result set output operator should not get layout") } + +func (op *ResultSetOutputOperator) Children() []operator.Operator { + return []operator.Operator{op.child} +} diff --git a/sql/execution/pipeline/operator/projection.go b/sql/execution/pipeline/operator/projection.go index 7f1e78c93..cf55350d6 100644 --- a/sql/execution/pipeline/operator/projection.go +++ b/sql/execution/pipeline/operator/projection.go @@ -104,6 +104,10 @@ func (h *ProjectionOperator) GetLayout() []*plan.Symbol { return h.project.GetOutputSymbols() } +func (h *ProjectionOperator) Children() []Operator { + return []Operator{h.child} +} + func (h *ProjectionOperator) prepare() { h.exprCtx = expression.NewEvalContext(h.ctx) h.exprs = make([]expression.Expression, len(h.project.Assignments)) diff --git a/sql/execution/pipeline/operator/scan/table_scan.go b/sql/execution/pipeline/operator/scan/table_scan.go index 6f4333565..be1c5a2b0 100644 --- a/sql/execution/pipeline/operator/scan/table_scan.go +++ b/sql/execution/pipeline/operator/scan/table_scan.go @@ -43,3 +43,7 @@ func (op *TableScanOperator) Run(output chan<- *types.Page) { func (op *TableScanOperator) GetLayout() []*plan.Symbol { return op.node.GetOutputSymbols() } + +func (op *TableScanOperator) Children() []operator.Operator { + return nil +} diff --git a/sql/execution/pipeline/operator/values.go b/sql/execution/pipeline/operator/values.go index 5bea9fef2..2d2672c88 100644 --- a/sql/execution/pipeline/operator/values.go +++ b/sql/execution/pipeline/operator/values.go @@ -50,3 +50,7 @@ func (op *ValuesOperator) Run(output chan<- *types.Page) { func (op *ValuesOperator) GetLayout() []*plan.Symbol { return op.node.GetOutputSymbols() } + +func (op *ValuesOperator) Children() []Operator { + return nil +} diff --git a/sql/execution/pipeline/pipeline.go b/sql/execution/pipeline/pipeline.go index e38bcf72f..1cf587a5b 100644 --- a/sql/execution/pipeline/pipeline.go +++ b/sql/execution/pipeline/pipeline.go @@ -38,6 +38,6 @@ func NewPipeline(taskCtx *context.TaskContext, root operator.Operator) *Pipeline } func (p *Pipeline) Run(output chan<- *types.Page) { - fmt.Printf("run pipeline, root=%T\n", p.root) + fmt.Printf("run pipeline, root=>\n%s\n", renderText(p.root)) p.root.Run(output) } diff --git a/sql/execution/pipeline/render.go b/sql/execution/pipeline/render.go new file mode 100644 index 000000000..de027a2fd --- /dev/null +++ b/sql/execution/pipeline/render.go @@ -0,0 +1,24 @@ +package pipeline + +import ( + "fmt" + "strings" + + "github.com/lindb/lindb/pkg/strutil" + "github.com/lindb/lindb/sql/execution/pipeline/operator" +) + +func renderText(root operator.Operator) string { + sb := &strings.Builder{} + return strings.TrimSuffix(writeTextOutput(sb, strutil.NewIndent(0, len(root.Children()) > 0), root), "\n") +} + +func writeTextOutput(sb *strings.Builder, indent *strutil.Indent, node operator.Operator) string { + sb.WriteString(indent.NodeIndent()) + sb.WriteString(strings.TrimPrefix(fmt.Sprintf("%T\n", node), "*")) + children := node.Children() + for i, child := range children { + writeTextOutput(sb, indent.ForChild(i == len(children)-1, len(child.Children()) > 0), child) + } + return sb.String() +} diff --git a/sql/execution/planner.go b/sql/execution/planner.go index dcef67ecf..e4cfed137 100644 --- a/sql/execution/planner.go +++ b/sql/execution/planner.go @@ -22,6 +22,7 @@ import ( sqlContext "github.com/lindb/lindb/sql/context" "github.com/lindb/lindb/sql/planner" "github.com/lindb/lindb/sql/planner/plan" + "github.com/lindb/lindb/sql/planner/validate" "github.com/lindb/lindb/sql/tree" ) @@ -51,7 +52,12 @@ func (p *Planner) Plan(session *Session, // plan query logicalPlanner := planner.NewLogicalPlanner(plannerContext, planOptimizers()) - return logicalPlanner.Plan() + plan := logicalPlanner.Plan() + v := validate.NewValidators() + if err := v.Validate(plannerContext, plan.Root); err != nil { + panic(err) + } + return plan } func (p *Planner) PlanDistribution(plan *plan.Plan) *plan.SubPlan { diff --git a/sql/planner/iterative/rule/push_timestamp.go b/sql/planner/iterative/rule/push_timestamp.go index 6387531e0..b8f2ad1f4 100644 --- a/sql/planner/iterative/rule/push_timestamp.go +++ b/sql/planner/iterative/rule/push_timestamp.go @@ -25,21 +25,27 @@ import ( "github.com/lindb/lindb/sql/planner/plan" ) +// PushTimestampIntoTableScan pushes timestamp column into table scan. type PushTimestampIntoTableScan struct { Base[*plan.OutputNode] } +// NewPushTimestampIntoTableScan creates a PushTimestampIntoTableScan instance. func NewPushTimestampIntoTableScan() iterative.Rule { rule := &PushTimestampIntoTableScan{} rule.apply = rule.pushTimestampIntoTableScan return rule } -func (rule *PushTimestampIntoTableScan) pushTimestampIntoTableScan(context *iterative.Context, node *plan.OutputNode) plan.PlanNode { +// pushTimestampIntoTableScan pushes timestamp column into table scan. +func (rule *PushTimestampIntoTableScan) pushTimestampIntoTableScan( + context *iterative.Context, node *plan.OutputNode, +) plan.PlanNode { timestamp, ok := lo.Find(node.GetOutputSymbols(), func(item *plan.Symbol) bool { return item.DataType == types.DTTimestamp }) if !ok { + // output node has no timestamp column return nil } tableScan := iterative.ExtractTableScan(context, node) @@ -49,6 +55,7 @@ func (rule *PushTimestampIntoTableScan) pushTimestampIntoTableScan(context *iter if _, ok = lo.Find(tableScan.GetOutputSymbols(), func(item *plan.Symbol) bool { return item.DataType == types.DTTimestamp }); ok { + // table scan already has timestamp column return nil } tableScan.OutputSymbols = append(tableScan.OutputSymbols, timestamp) diff --git a/sql/planner/printer/render.go b/sql/planner/printer/render.go index f17de4eee..2e3359c6c 100644 --- a/sql/planner/printer/render.go +++ b/sql/planner/printer/render.go @@ -20,12 +20,8 @@ package printer import ( "regexp" "strings" -) -const ( - VerticalLine = "│" - LastNode = "└─" - IntermediateNode = "├─" + "github.com/lindb/lindb/pkg/strutil" ) type Render interface { @@ -47,11 +43,14 @@ func (r *TextRender) Render(plan *PlanRepresentation) string { sb := &strings.Builder{} hasChildren := hasChildren(plan, root) - return strings.TrimSuffix(r.writeTextOutput(sb, plan, NewIndent(r.level, hasChildren), root), "\n") + return strings.TrimSuffix(r.writeTextOutput(sb, plan, strutil.NewIndent(r.level, hasChildren), root), "\n") } -func (r *TextRender) writeTextOutput(output *strings.Builder, plan *PlanRepresentation, indent *Indent, node *NodeRepresentation) string { - output.WriteString(indent.nodeIndent()) +func (r *TextRender) writeTextOutput( + output *strings.Builder, plan *PlanRepresentation, + indent *strutil.Indent, node *NodeRepresentation, +) string { + output.WriteString(indent.NodeIndent()) output.WriteString(node.getName()) var kvs []string for key, value := range node.descriptor { @@ -60,12 +59,12 @@ func (r *TextRender) writeTextOutput(output *strings.Builder, plan *PlanRepresen output.WriteString("[" + strings.Join(kvs, ", ") + "]") output.WriteString("\n") - output.WriteString(indentMultilineString("Layout: "+formatSymbols(node.outputs), indent.detailIndent())) + output.WriteString(indentMultilineString("Layout: "+formatSymbols(node.outputs), indent.DetailIndent())) output.WriteString("\n") if len(node.details) > 0 { details := strings.Join(node.details, "\n") - details = indentMultilineString(details, indent.detailIndent()) + details = indentMultilineString(details, indent.DetailIndent()) output.WriteString(details) if !strings.HasSuffix(details, "\n") { output.WriteString("\n") @@ -78,7 +77,7 @@ func (r *TextRender) writeTextOutput(output *strings.Builder, plan *PlanRepresen child := plan.getNode(childID) if child != nil { r.writeTextOutput(output, plan, - indent.forChild(i == len(childrenIDs)-1, hasChildren(plan, child)), child) + indent.ForChild(i == len(childrenIDs)-1, hasChildren(plan, child)), child) } } @@ -90,60 +89,6 @@ func indentMultilineString(str, indent string) string { return m1.ReplaceAllString(str, indent) } -type Indent struct { - firstLinePrefix string - nextLinesPrefix string - hasChildren bool -} - -func NewIndent(level int, hasChildren bool) *Indent { - indent := indentString(level) - return &Indent{ - firstLinePrefix: indent, - nextLinesPrefix: indent, - hasChildren: hasChildren, - } -} - -func (i *Indent) nodeIndent() string { - return i.firstLinePrefix -} - -func (i *Indent) detailIndent() string { - indent := "" - if i.hasChildren { - indent = VerticalLine - } - return i.nextLinesPrefix + pad(indent, 2) -} - -func (i *Indent) forChild(last, hasChildren bool) *Indent { - var ( - first string - next string - ) - if last { - first = pad(LastNode, 3) - next = pad("", 3) - } else { - first = pad(IntermediateNode, 3) - next = pad(VerticalLine, 3) - } - return &Indent{ - firstLinePrefix: i.nextLinesPrefix + first, - nextLinesPrefix: i.nextLinesPrefix + next, - hasChildren: hasChildren, - } -} - -func indentString(indent int) string { - return strings.Repeat(" ", indent) -} - -func pad(text string, length int) string { - return text + strings.Repeat(" ", length-len([]rune(text))) -} - func hasChildren(plan *PlanRepresentation, node *NodeRepresentation) bool { for _, childID := range node.children { child := plan.getNode(childID) diff --git a/sql/planner/validate/output_validator.go b/sql/planner/validate/output_validator.go new file mode 100644 index 000000000..a93f4b6db --- /dev/null +++ b/sql/planner/validate/output_validator.go @@ -0,0 +1,52 @@ +// Licensed to LinDB under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. LinDB licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package validate + +import ( + "errors" + + "github.com/samber/lo" + + "github.com/lindb/lindb/spi/types" + "github.com/lindb/lindb/sql/context" + "github.com/lindb/lindb/sql/planner/plan" +) + +type OutputValidator struct { + Base[*plan.OutputNode] +} + +func NewOutputValidator() Validator { + v := &OutputValidator{} + v.validate = func(ctx *context.PlannerContext, node *plan.OutputNode) error { + _, ok := lo.Find(node.GetOutputSymbols(), func(item *plan.Symbol) bool { + return item.DataType == types.DTTimestamp + }) + if !ok { + // output node has no timestamp column + return nil + } + if _, ok = lo.Find(node.GetOutputSymbols(), func(item *plan.Symbol) bool { + return item.DataType == types.DTTimeSeries + }); !ok { + return errors.New("push timestamp column failed, output node has no time series column") + } + return nil + } + return v +} diff --git a/models/result_set.go b/sql/planner/validate/validator.go similarity index 61% rename from models/result_set.go rename to sql/planner/validate/validator.go index 5993c51ec..9337d2cfa 100644 --- a/models/result_set.go +++ b/sql/planner/validate/validator.go @@ -15,9 +15,24 @@ // specific language governing permissions and limitations // under the License. -package models +package validate -// SuggestResult represents the suggest result set -type SuggestResult struct { - Values []string `json:"values"` +import ( + "github.com/lindb/lindb/sql/context" + "github.com/lindb/lindb/sql/planner/plan" +) + +type Validator interface { + Validate(ctx *context.PlannerContext, node plan.PlanNode) error +} + +type Base[N plan.PlanNode] struct { + validate func(ctx *context.PlannerContext, node N) error +} + +func (v *Base[N]) Validate(ctx *context.PlannerContext, node plan.PlanNode) error { + if targetNode, ok := node.(N); ok { + return v.validate(ctx, targetNode) + } + return nil } diff --git a/sql/planner/validate/validators.go b/sql/planner/validate/validators.go new file mode 100644 index 000000000..862b607ce --- /dev/null +++ b/sql/planner/validate/validators.go @@ -0,0 +1,57 @@ +// Licensed to LinDB under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. LinDB licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package validate + +import ( + "github.com/lindb/lindb/sql/context" + "github.com/lindb/lindb/sql/planner/plan" +) + +type Validators struct { + validators []Validator +} + +func NewValidators() Validator { + return &Validators{ + validators: []Validator{ + NewOutputValidator(), + }, + } +} + +func (v *Validators) Validate(ctx *context.PlannerContext, node plan.PlanNode) error { + if err := v.validate(ctx, node); err != nil { + return err + } + + for _, child := range node.GetSources() { + if err := v.Validate(ctx, child); err != nil { + return err + } + } + return nil +} + +func (v *Validators) validate(ctx *context.PlannerContext, node plan.PlanNode) error { + for _, validator := range v.validators { + if err := validator.Validate(ctx, node); err != nil { + return err + } + } + return nil +} diff --git a/sql/tree/encoding.go b/sql/tree/encoding.go index 506a67602..1cbdbe2f3 100644 --- a/sql/tree/encoding.go +++ b/sql/tree/encoding.go @@ -41,4 +41,5 @@ func init() { encoding.RegisterNodeType(Cast{}) encoding.RegisterNodeType(FunctionCall{}) encoding.RegisterNodeType(SymbolReference{}) + encoding.RegisterNodeType(Constant{}) }