diff --git a/parser/altinity_format_test.go b/parser/altinity_format_test.go new file mode 100644 index 0000000..f9c1886 --- /dev/null +++ b/parser/altinity_format_test.go @@ -0,0 +1,27 @@ +package parser + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestSelectFormatFunctionAndOutputClause(t *testing.T) { + for _, sql := range []string{ + "SELECT format('{}', 1)", + "SELECT 1, format('{}', 1)", + "SELECT 1, format('{}', 1) FORMAT JSON", + "SELECT 1 FORMAT JSON", + } { + t.Run(sql, func(t *testing.T) { + stmts, err := NewParser(sql).ParseStmts() + require.NoError(t, err) + require.Len(t, stmts, 1) + formatted := Format(stmts[0]) + again, err := NewParser(formatted).ParseStmts() + require.NoError(t, err) + require.Len(t, again, 1) + require.Equal(t, formatted, Format(again[0])) + }) + } +} diff --git a/parser/parser_column.go b/parser/parser_column.go index b630193..c850694 100644 --- a/parser/parser_column.go +++ b/parser/parser_column.go @@ -517,6 +517,11 @@ func (p *Parser) keywordIsSelectItemIdentifier() bool { // that begins a clause following the SELECT item list. When true, we should // not treat the keyword itself as a bare alias. func (p *Parser) isSelectItemTerminatorKeyword() bool { + // FORMAT followed by '(' is a scalar function in the next SELECT item, + // not the output FORMAT clause. The latter takes a format name. + if p.matchKeyword(KeywordFormat) && p.peekTokenKind(TokenKindLParen) { + return false + } if p.keywordIsSelectItemIdentifier() { return false }