Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Render #57

Merged
merged 5 commits into from
Jan 2, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
render initial mvp
  • Loading branch information
ejcx committed Oct 22, 2024
commit f582548bd92b3c552c1592a7f3f735cdfbd47707
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