Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
6 changes: 6 additions & 0 deletions parser/keyword.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,10 @@ const (
KeywordOuter = "OUTER"
KeywordOutfile = "OUTFILE"
KeywordOver = "OVER"
KeywordOverlay = "OVERLAY"
KeywordOverlayUTF8 = "OVERLAYUTF8"
KeywordPartition = "PARTITION"
KeywordPlacing = "PLACING"
KeywordPipeline = "PIPELINE"
KeywordPolicy = "POLICY"
KeywordPopulate = "POPULATE"
Expand Down Expand Up @@ -488,8 +491,11 @@ var keywords = NewSet(
KeywordOverridable,
KeywordOutfile,
KeywordOver,
KeywordOverlay,
KeywordOverlayUTF8,
KeywordPartition,
KeywordPipeline,
KeywordPlacing,
Comment thread
git-hulk marked this conversation as resolved.
KeywordPolicy,
KeywordPopulate,
KeywordPreceding,
Expand Down
186 changes: 186 additions & 0 deletions parser/parser_column.go
Original file line number Diff line number Diff line change
Expand Up @@ -767,6 +767,192 @@ func (p *Parser) parseColumnArgList(pos Pos) (*ColumnArgList, error) {
}, nil
}

// keywordArgForm is a function's keyword-separated argument syntax, e.g.
// `trim(BOTH ' ' FROM s)`. These keywords are matched only inside such an
// argument list, never in getNextPrecedence: FROM also starts a clause, so
// giving it a precedence would make `SELECT x FROM t` one expression.
type keywordArgForm struct {
Modifiers []string
RequireModifier bool
Separators []string // stand in for a comma, in this order
RequiredSeparators int
ClosedKeywordForm bool // the keyword form may not be mixed with commas
MaxSlots int // argument limit once a separator keyword is used
}

var keywordArgFunctions = map[string]keywordArgForm{
KeywordTrim: {
Modifiers: []string{KeywordBoth, KeywordLeading, KeywordTrailing},
RequireModifier: true,
Separators: []string{KeywordFrom},
RequiredSeparators: 1,
},
KeywordSubstring: {
Separators: []string{KeywordFrom, KeywordFor},
RequiredSeparators: 1,
MaxSlots: 3,
},
KeywordOverlay: {
Separators: []string{KeywordPlacing, KeywordFrom, KeywordFor},
RequiredSeparators: 2,
ClosedKeywordForm: true,
},
KeywordOverlayUTF8: {
Separators: []string{KeywordPlacing, KeywordFrom, KeywordFor},
RequiredSeparators: 2,
ClosedKeywordForm: true,
},
}

func (p *Parser) parseKeywordArgItem(form keywordArgForm, slot int, allowSeparators bool) (Expr, int, error) {
expr, hasModifier, err := p.parseKeywordArgModifier(form, slot)
if err != nil {
return nil, 0, err
}

consumed := 0
for allowSeparators && slot+consumed < len(form.Separators) &&
p.matchKeyword(form.Separators[slot+consumed]) {
operation := TokenKind(p.current().ToString())
if err := p.lexer.consumeToken(); err != nil {
return nil, 0, err
}

rightExpr, err := p.parseExpr(p.Pos())
if err != nil {
return nil, 0, err
}

expr = &BinaryOperation{
LeftExpr: expr,
Operation: operation,
RightExpr: rightExpr,
}
consumed++
}

if !hasModifier && consumed == 0 {
return expr, 0, nil
}

if form.RequireModifier && !hasModifier {
return nil, 0, fmt.Errorf("expected one of %s before %s",
strings.Join(form.Modifiers, ", "), form.Separators[0])
}

if slot+consumed < form.RequiredSeparators {
return nil, 0, fmt.Errorf("expected %s", form.Separators[slot+consumed])
}

return expr, consumed, nil
}

func (p *Parser) parseKeywordArgModifier(form keywordArgForm, slot int) (Expr, bool, error) {
if slot != 0 || !p.matchOneOfKeywords(form.Modifiers...) {
expr, err := p.parseExpr(p.Pos())
return expr, false, err
}
// Only a modifier when an expression follows it; a bare `both` is an identifier.
if p.peekTokenKind(TokenKindComma) || p.peekTokenKind(TokenKindRParen) {
expr, err := p.parseExpr(p.Pos())
return expr, false, err
}

unaryPos := p.Pos()
kind := TokenKind(p.current().ToString())
if err := p.lexer.consumeToken(); err != nil {
return nil, false, err
}

expr, err := p.parseExpr(p.Pos())
if err != nil {
return nil, false, err
}

return &UnaryExpr{
UnaryPos: unaryPos,
Kind: kind,
Expr: expr,
}, true, nil
}

func (p *Parser) parseKeywordArgFunctionParams(pos Pos, form keywordArgForm) (*ParamExprList, error) {
if err := p.expectTokenKind(TokenKindLParen); err != nil {
return nil, err
}

itemsPos := p.Pos()
hasDistinct := p.tryConsumeKeywords(KeywordDistinct)

items := make([]Expr, 0)
// A comma advances the argument slot just as a separator keyword does, so the
// index runs across the list instead of resetting per item.
slot := 0
usedSeparator, usedComma := false, false
for !p.lexer.isEOF() && !p.matchTokenKind(TokenKindRParen) {
item, consumed, err := p.parseKeywordArgItem(form, slot, !(form.ClosedKeywordForm && usedComma))
if err != nil {
return nil, err
}

if consumed > 0 {
usedSeparator = true
}
slot += consumed

var alias *Ident
if p.tryConsumeKeywords(KeywordAs) {
if alias, err = p.parseAnyKeyword(); err != nil {
return nil, err
}
}

items = append(items, &ColumnExpr{Expr: item, Alias: alias})
if p.tryConsumeTokenKind(TokenKindComma) == nil {
break
Comment thread
therealpandey marked this conversation as resolved.
}

usedComma = true
slot++

if usedSeparator && (form.ClosedKeywordForm || (form.MaxSlots > 0 && slot >= form.MaxSlots)) {
return nil, fmt.Errorf("expected ')', but got ','")
}
}

itemList := &ColumnExprList{
ListPos: itemsPos,
ListEnd: itemsPos,
HasDistinct: hasDistinct,
Items: items,
}
if len(items) > 0 {
itemList.ListEnd = items[len(items)-1].End()
}

rightParenPos := p.Pos()
if err := p.expectTokenKind(TokenKindRParen); err != nil {
return nil, err
}

paramExprList := &ParamExprList{
LeftParenPos: pos,
RightParenPos: rightParenPos,
Items: itemList,
}
// Parametric argument lists, as in parseFunctionParams.
if p.matchTokenKind(TokenKindLParen) {
columnArgList, err := p.parseColumnArgList(p.Pos())
if err != nil {
return nil, err
}

paramExprList.ColumnArgList = columnArgList
}

return paramExprList, nil
}

func (p *Parser) parseFunctionParams(pos Pos) (*ParamExprList, error) {
if err := p.expectTokenKind(TokenKindLParen); err != nil {
return nil, err
Expand Down
8 changes: 7 additions & 1 deletion parser/parser_table.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package parser
import (
"errors"
"fmt"
"strings"
)

func (p *Parser) parseDDL(pos Pos) (DDL, error) {
Expand Down Expand Up @@ -423,7 +424,12 @@ func (p *Parser) parseIdentOrFunction(_ Pos) (Expr, error) {
Params: params,
}, nil
case p.matchTokenKind(TokenKindLParen):
params, err := p.parseFunctionParams(p.Pos())
var params *ParamExprList
if form, ok := keywordArgFunctions[strings.ToUpper(ident.Name)]; ok {
params, err = p.parseKeywordArgFunctionParams(p.Pos(), form)
} else {
params, err = p.parseFunctionParams(p.Pos())
}
if err != nil {
return nil, err
}
Expand Down
24 changes: 24 additions & 0 deletions parser/position_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,27 @@ func TestDictionaryAttributeEnd(t *testing.T) {
require.Equal(t, "IS_OBJECT_ID", sql[36:48])
require.Equal(t, Pos(48), attrs[0].End())
}

func TestKeywordArgFunctionPositions(t *testing.T) {
// The keyword forms reuse UnaryExpr/BinaryOperation, so their spans must match
// what the same nodes report anywhere else.
sql := "SELECT trim(BOTH ' ' FROM s)"
stmt := parseOneStmt(t, sql).(*SelectQuery)
fn := stmt.SelectItems[0].Expr.(*FunctionExpr)
require.Equal(t, Pos(7), fn.Pos())
require.Equal(t, Pos(len(sql)-1), fn.End(), "ends on the closing paren, as any call does")

from := fn.Params.Items.Items[0].(*ColumnExpr).Expr.(*BinaryOperation)
require.Equal(t, "BOTH", sql[12:16])
require.Equal(t, "s", sql[26:27])
// the operation spans its operands: BOTH ... s
require.Equal(t, Pos(12), from.Pos())
require.Equal(t, Pos(27), from.End())

// the characters literal runs from inside the opening quote to the closing one
modifier := from.LeftExpr.(*UnaryExpr)
require.Equal(t, Pos(12), modifier.Pos())
require.Equal(t, modifier.Expr.End(), modifier.End())
require.Equal(t, Pos(18), modifier.Expr.Pos())
require.Equal(t, Pos(19), modifier.Expr.End())
}
Loading
Loading