Skip to content

Commit 4513ac6

Browse files
authored
Merge pull request #57 from runreveal/ej/claude-did-render
Render
2 parents 73d23b4 + 1065786 commit 4513ac6

File tree

7 files changed

+221
-0
lines changed

7 files changed

+221
-0
lines changed

parser/ast.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,6 +578,52 @@ func (stmt *LetStatement) Span() Span {
578578
return unionSpans(stmt.Keyword, stmt.Name.Span(), stmt.Assign, xSpan)
579579
}
580580

581+
// RenderOperator represents a `| render` operator in a [TabularExpr].
582+
// It implements [TabularOperator].
583+
type RenderOperator struct {
584+
Pipe Span
585+
Keyword Span
586+
ChartType *Ident
587+
588+
// Optional properties
589+
With Span // Span for 'with' keyword if present
590+
Lparen Span // Opening parenthesis for properties
591+
Props []*RenderProperty
592+
Rparen Span // Closing parenthesis for properties
593+
}
594+
595+
// RenderProperty represents a single property in the render operator's
596+
// with clause, like "title='My Chart'" or "kind=stacked"
597+
type RenderProperty struct {
598+
Name *Ident
599+
Assign Span
600+
Value Expr
601+
}
602+
603+
func (op *RenderProperty) Span() Span {
604+
if op == nil {
605+
return nullSpan()
606+
}
607+
return unionSpans(op.Name.Span(), op.Assign, nodeSpan(op.Value))
608+
}
609+
610+
func (op *RenderOperator) tabularOperator() {}
611+
612+
func (op *RenderOperator) Span() Span {
613+
if op == nil {
614+
return nullSpan()
615+
}
616+
return unionSpans(
617+
op.Pipe,
618+
op.Keyword,
619+
op.ChartType.Span(),
620+
op.With,
621+
op.Lparen,
622+
nodeSliceSpan(op.Props),
623+
op.Rparen,
624+
)
625+
}
626+
581627
// Walk traverses an AST in depth-first order.
582628
// If the visit function returns true for a node,
583629
// the visit function will be called for its children.
@@ -720,6 +766,17 @@ func Walk(n Node, visit func(n Node) bool) {
720766
stack = append(stack, n.X)
721767
stack = append(stack, n.Name)
722768
}
769+
// Add to Walk function's switch statement:
770+
case *RenderOperator:
771+
if visit(n) {
772+
stack = append(stack, n.ChartType)
773+
for i := len(n.Props) - 1; i >= 0; i-- {
774+
if n.Props[i].Value != nil {
775+
stack = append(stack, n.Props[i].Value)
776+
}
777+
stack = append(stack, n.Props[i].Name)
778+
}
779+
}
723780
default:
724781
panic(fmt.Errorf("unknown Node type %T", n))
725782
}

parser/parser.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,13 @@ func (p *parser) tabularExpr() (*TabularExpr, error) {
233233
expr.Operators = append(expr.Operators, op)
234234
}
235235
finalError = joinErrors(finalError, err)
236+
case "render":
237+
op, err := opParser.renderOperator(pipeToken, operatorName)
238+
if op != nil {
239+
expr.Operators = append(expr.Operators, op)
240+
}
241+
finalError = joinErrors(finalError, err)
242+
236243
default:
237244
finalError = joinErrors(finalError, &parseError{
238245
source: opParser.source,
@@ -495,6 +502,110 @@ func (p *parser) extendOperator(pipe, keyword Token) (*ExtendOperator, error) {
495502
}
496503
}
497504

505+
func (p *parser) renderOperator(pipe, keyword Token) (*RenderOperator, error) {
506+
op := &RenderOperator{
507+
Pipe: pipe.Span,
508+
Keyword: keyword.Span,
509+
With: nullSpan(),
510+
Lparen: nullSpan(),
511+
Rparen: nullSpan(),
512+
}
513+
514+
// Parse chart type (required)
515+
chartType, err := p.ident()
516+
if err != nil {
517+
return op, &parseError{
518+
source: p.source,
519+
span: keyword.Span,
520+
err: fmt.Errorf("expected chart type after render, got %v", err),
521+
}
522+
}
523+
op.ChartType = chartType
524+
525+
// Look for optional "with" clause
526+
tok, ok := p.next()
527+
if !ok {
528+
return op, nil
529+
}
530+
531+
if tok.Kind != TokenIdentifier || tok.Value != "with" {
532+
p.prev()
533+
return op, nil
534+
}
535+
op.With = tok.Span
536+
537+
// Parse opening parenthesis
538+
tok, _ = p.next()
539+
if tok.Kind != TokenLParen {
540+
return op, &parseError{
541+
source: p.source,
542+
span: tok.Span,
543+
err: fmt.Errorf("expected '(' after with, got %s", formatToken(p.source, tok)),
544+
}
545+
}
546+
op.Lparen = tok.Span
547+
548+
// Parse properties
549+
for {
550+
prop, err := p.renderProperty()
551+
if err != nil {
552+
return op, makeErrorOpaque(err)
553+
}
554+
if prop != nil {
555+
op.Props = append(op.Props, prop)
556+
}
557+
558+
// Check for comma or closing parenthesis
559+
tok, _ = p.next()
560+
if tok.Kind == TokenRParen {
561+
op.Rparen = tok.Span
562+
break
563+
}
564+
if tok.Kind != TokenComma {
565+
return op, &parseError{
566+
source: p.source,
567+
span: tok.Span,
568+
err: fmt.Errorf("expected ',' or ')', got %s", formatToken(p.source, tok)),
569+
}
570+
}
571+
}
572+
573+
return op, nil
574+
}
575+
576+
func (p *parser) renderProperty() (*RenderProperty, error) {
577+
prop := &RenderProperty{
578+
Assign: nullSpan(),
579+
}
580+
581+
// Parse property name
582+
name, err := p.ident()
583+
if err != nil {
584+
return nil, err
585+
}
586+
prop.Name = name
587+
588+
// Parse equals sign
589+
tok, _ := p.next()
590+
if tok.Kind != TokenAssign {
591+
return nil, &parseError{
592+
source: p.source,
593+
span: tok.Span,
594+
err: fmt.Errorf("expected '=' after property name, got %s", formatToken(p.source, tok)),
595+
}
596+
}
597+
prop.Assign = tok.Span
598+
599+
// Parse property value
600+
value, err := p.expr()
601+
if err != nil {
602+
return nil, err
603+
}
604+
prop.Value = value
605+
606+
return prop, nil
607+
}
608+
498609
func (p *parser) extendColumn() (*ExtendColumn, error) {
499610
restorePos := p.pos
500611

pql.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,8 @@ func canAttachSort(op parser.TabularOperator) bool {
296296
switch op.(type) {
297297
case *parser.ProjectOperator, *parser.SummarizeOperator, *parser.AsOperator:
298298
return false
299+
case *parser.RenderOperator:
300+
return false
299301
default:
300302
return true
301303
}
@@ -463,6 +465,33 @@ func (sub *subquery) write(ctx *exprContext, sb *strings.Builder) error {
463465
case *parser.CountOperator:
464466
sb.WriteString(`SELECT COUNT(*) AS "count()" FROM `)
465467
sb.WriteString(sub.sourceSQL)
468+
case *parser.RenderOperator:
469+
// First, write the source data
470+
sb.WriteString("SELECT *,\n")
471+
// Then add our render-specific metadata columns
472+
sb.WriteString(" '")
473+
sb.WriteString(op.ChartType.Name)
474+
sb.WriteString("' as \"render_type\"")
475+
476+
// Add render properties with standardized prefixes
477+
for _, prop := range op.Props {
478+
sb.WriteString(",\n ")
479+
// Quote all values as strings since they're instructions for the renderer
480+
sb.WriteString("'")
481+
if lit, ok := prop.Value.(*parser.BasicLit); ok {
482+
// Use the literal value directly
483+
sb.WriteString(lit.Value)
484+
} else if id, ok := prop.Value.(*parser.QualifiedIdent); ok {
485+
// Use the identifier name
486+
sb.WriteString(id.Parts[0].Name)
487+
}
488+
sb.WriteString("' as \"render_prop_")
489+
sb.WriteString(prop.Name.Name)
490+
sb.WriteString("\"")
491+
}
492+
493+
sb.WriteString("\nFROM ")
494+
sb.WriteString(sub.sourceSQL)
466495
default:
467496
fmt.Fprintf(sb, "SELECT NULL /* unsupported operator %T */", op)
468497
return nil

testdata/Goldens/Render/input.pql

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
StormEvents
2+
| project State, EventType, DamageProperty
3+
| summarize TotalDamage=sum(DamageProperty) by State
4+
| sort by TotalDamage desc
5+
| limit 10
6+
| render barchart with (
7+
title="Property Damage by State",
8+
xtitle="State",
9+
ytitle="Total Damage ($)"
10+
)

testdata/Goldens/Render/output.csv

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
State,TotalDamage,render_type,render_prop_title,render_prop_xtitle,render_prop_ytitle
2+
FLORIDA,6200000,barchart,Property Damage by State,State,Total Damage ($)
3+
MISSISSIPPI,20000,barchart,Property Damage by State,State,Total Damage ($)
4+
GEORGIA,2000,barchart,Property Damage by State,State,Total Damage ($)
5+
ATLANTIC SOUTH,0,barchart,Property Damage by State,State,Total Damage ($)

testdata/Goldens/Render/output.sql

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
WITH "__subquery0" AS (SELECT "State" AS "State", "EventType" AS "EventType", "DamageProperty" AS "DamageProperty" FROM "StormEvents"),
2+
"__subquery1" AS (SELECT "State" AS "State", sum("DamageProperty") AS "TotalDamage" FROM "__subquery0" GROUP BY "State"),
3+
"__subquery2" AS (SELECT * FROM "__subquery1" ORDER BY "TotalDamage" DESC NULLS LAST LIMIT 10)
4+
SELECT *,
5+
'barchart' as "render_type",
6+
'Property Damage by State' as "render_prop_title",
7+
'State' as "render_prop_xtitle",
8+
'Total Damage ($)' as "render_prop_ytitle"
9+
FROM "__subquery2";

testdata/Goldens/Render/unordered

Whitespace-only changes.

0 commit comments

Comments
 (0)