Skip to content

Commit

Permalink
render initial mvp
Browse files Browse the repository at this point in the history
  • Loading branch information
ejcx committed Oct 22, 2024
1 parent 73d23b4 commit f582548
Show file tree
Hide file tree
Showing 3 changed files with 192 additions and 0 deletions.
57 changes: 57 additions & 0 deletions parser/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,52 @@ func (stmt *LetStatement) Span() Span {
return unionSpans(stmt.Keyword, stmt.Name.Span(), stmt.Assign, xSpan)
}

// RenderOperator represents a `| render` operator in a [TabularExpr].
// It implements [TabularOperator].
type RenderOperator struct {
Pipe Span
Keyword Span
ChartType *Ident

// Optional properties
With Span // Span for 'with' keyword if present
Lparen Span // Opening parenthesis for properties
Props []*RenderProperty
Rparen Span // Closing parenthesis for properties
}

// RenderProperty represents a single property in the render operator's
// with clause, like "title='My Chart'" or "kind=stacked"
type RenderProperty struct {
Name *Ident
Assign Span
Value Expr
}

func (op *RenderProperty) Span() Span {
if op == nil {
return nullSpan()
}
return unionSpans(op.Name.Span(), op.Assign, nodeSpan(op.Value))
}

func (op *RenderOperator) tabularOperator() {}

func (op *RenderOperator) Span() Span {
if op == nil {
return nullSpan()
}
return unionSpans(
op.Pipe,
op.Keyword,
op.ChartType.Span(),
op.With,
op.Lparen,
nodeSliceSpan(op.Props),
op.Rparen,
)
}

// Walk traverses an AST in depth-first order.
// If the visit function returns true for a node,
// the visit function will be called for its children.
Expand Down Expand Up @@ -720,6 +766,17 @@ func Walk(n Node, visit func(n Node) bool) {
stack = append(stack, n.X)
stack = append(stack, n.Name)
}
// Add to Walk function's switch statement:
case *RenderOperator:
if visit(n) {
stack = append(stack, n.ChartType)
for i := len(n.Props) - 1; i >= 0; i-- {
if n.Props[i].Value != nil {
stack = append(stack, n.Props[i].Value)
}
stack = append(stack, n.Props[i].Name)
}
}
default:
panic(fmt.Errorf("unknown Node type %T", n))
}
Expand Down
111 changes: 111 additions & 0 deletions parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,13 @@ func (p *parser) tabularExpr() (*TabularExpr, error) {
expr.Operators = append(expr.Operators, op)
}
finalError = joinErrors(finalError, err)
case "render":
op, err := opParser.renderOperator(pipeToken, operatorName)
if op != nil {
expr.Operators = append(expr.Operators, op)
}
finalError = joinErrors(finalError, err)

default:
finalError = joinErrors(finalError, &parseError{
source: opParser.source,
Expand Down Expand Up @@ -495,6 +502,110 @@ func (p *parser) extendOperator(pipe, keyword Token) (*ExtendOperator, error) {
}
}

func (p *parser) renderOperator(pipe, keyword Token) (*RenderOperator, error) {
op := &RenderOperator{
Pipe: pipe.Span,
Keyword: keyword.Span,
With: nullSpan(),
Lparen: nullSpan(),
Rparen: nullSpan(),
}

// Parse chart type (required)
chartType, err := p.ident()
if err != nil {
return op, &parseError{
source: p.source,
span: keyword.Span,
err: fmt.Errorf("expected chart type after render, got %v", err),
}
}
op.ChartType = chartType

// Look for optional "with" clause
tok, ok := p.next()
if !ok {
return op, nil
}

if tok.Kind != TokenIdentifier || tok.Value != "with" {
p.prev()
return op, nil
}
op.With = tok.Span

// Parse opening parenthesis
tok, _ = p.next()
if tok.Kind != TokenLParen {
return op, &parseError{
source: p.source,
span: tok.Span,
err: fmt.Errorf("expected '(' after with, got %s", formatToken(p.source, tok)),
}
}
op.Lparen = tok.Span

// Parse properties
for {
prop, err := p.renderProperty()
if err != nil {
return op, makeErrorOpaque(err)
}
if prop != nil {
op.Props = append(op.Props, prop)
}

// Check for comma or closing parenthesis
tok, _ = p.next()
if tok.Kind == TokenRParen {
op.Rparen = tok.Span
break
}
if tok.Kind != TokenComma {
return op, &parseError{
source: p.source,
span: tok.Span,
err: fmt.Errorf("expected ',' or ')', got %s", formatToken(p.source, tok)),
}
}
}

return op, nil
}

func (p *parser) renderProperty() (*RenderProperty, error) {
prop := &RenderProperty{
Assign: nullSpan(),
}

// Parse property name
name, err := p.ident()
if err != nil {
return nil, err
}
prop.Name = name

// Parse equals sign
tok, _ := p.next()
if tok.Kind != TokenAssign {
return nil, &parseError{
source: p.source,
span: tok.Span,
err: fmt.Errorf("expected '=' after property name, got %s", formatToken(p.source, tok)),
}
}
prop.Assign = tok.Span

// Parse property value
value, err := p.expr()
if err != nil {
return nil, err
}
prop.Value = value

return prop, nil
}

func (p *parser) extendColumn() (*ExtendColumn, error) {
restorePos := p.pos

Expand Down
24 changes: 24 additions & 0 deletions pql.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,8 @@ func canAttachSort(op parser.TabularOperator) bool {
switch op.(type) {
case *parser.ProjectOperator, *parser.SummarizeOperator, *parser.AsOperator:
return false
case *parser.RenderOperator:
return false
default:
return true
}
Expand Down Expand Up @@ -463,6 +465,28 @@ func (sub *subquery) write(ctx *exprContext, sb *strings.Builder) error {
case *parser.CountOperator:
sb.WriteString(`SELECT COUNT(*) AS "count()" FROM `)
sb.WriteString(sub.sourceSQL)
case *parser.RenderOperator:
sb.WriteString("SELECT\n")
sb.WriteString(" 'render' as __viz_type,\n")
sb.WriteString(" ")
quoteIdentifier(sb, op.ChartType.Name)
sb.WriteString(" as __chart_type")

// Add properties if present
if len(op.Props) > 0 {
for _, prop := range op.Props {
sb.WriteString(",\n ")
if err := writeExpression(ctx, sb, prop.Value); err != nil {
return err
}
sb.WriteString(" as ")
quoteIdentifier(sb, prop.Name.Name)
}
}

sb.WriteString("\nFROM ")
sb.WriteString(sub.sourceSQL)

default:
fmt.Fprintf(sb, "SELECT NULL /* unsupported operator %T */", op)
return nil
Expand Down

0 comments on commit f582548

Please sign in to comment.