diff --git a/parser/error.go b/parser/error.go index ac73a2b..5e6ee18 100644 --- a/parser/error.go +++ b/parser/error.go @@ -5,6 +5,16 @@ import ( "strings" ) +// lexerError keeps a lexical failure attached to its original byte position. +type lexerError struct { + pos Pos + err error +} + +func (e *lexerError) Error() string { return e.err.Error() } + +func (e *lexerError) Unwrap() error { return e.err } + // ParseError is a structured parse error. It carries the byte offset and the // 1-based line/column where parsing stopped, the offending token, and (when // known) the tokens the grammar expected at that point. Callers such as diff --git a/parser/error_test.go b/parser/error_test.go index 9d185cc..b926f7d 100644 --- a/parser/error_test.go +++ b/parser/error_test.go @@ -47,3 +47,92 @@ func TestParseError_ExpectedTokenKind(t *testing.T) { require.Equal(t, []TokenKind{TokenKindRParen}, pe.Expected) require.True(t, strings.HasPrefix(pe.Error(), "line ")) } + +func TestParseError_LexicalFailure(t *testing.T) { + for _, prefix := range []string{"SELECT 1 ", "SELECT case ", "SELECT interval + ", "SELECT 1;\n", "SELECT 1 /* closed */\n"} { + for _, suffix := range []struct { + sql string + msg string + }{ + {"/*", "unclosed multi-line comment"}, + {"'unclosed", "invalid string"}, + {"`unclosed", "unclosed quoted identifier"}, + {"1e+", "exponent part should contain at least one digit"}, + {"中文", "unexpected character"}, + } { + sql := prefix + suffix.sql + t.Run(sql, func(t *testing.T) { + stmts, err := NewParser(sql).ParseStmts() + require.Error(t, err) + require.Nil(t, stmts) + + var pe *ParseError + require.ErrorAs(t, err, &pe) + require.Contains(t, pe.Msg, suffix.msg) + require.Equal(t, Pos(len(prefix)), pe.Pos) + if strings.HasSuffix(prefix, "\n") { + require.Equal(t, 2, pe.Line) + require.Equal(t, 1, pe.Column) + } + }) + } + } +} + +func TestParser_TokenConsumptionError(t *testing.T) { + for _, tc := range []struct { + name string + sql string + parse func(*Parser) error + }{ + {"required token", ") /*", func(p *Parser) error { return p.expectTokenKind(TokenKindRParen) }}, + {"optional token", ". /*", func(p *Parser) error { + _, err := p.tryParseDotIdent(p.Pos()) + return err + }}, + {"list separator", "a, /*", func(p *Parser) error { + _, err := p.parseUserNames() + return err + }}, + } { + t.Run(tc.name, func(t *testing.T) { + p := NewParser(tc.sql) + require.NoError(t, p.lexer.consumeToken()) + err := tc.parse(p) + var lexicalErr *lexerError + require.ErrorAs(t, err, &lexicalErr) + require.Equal(t, Pos(strings.Index(tc.sql, "/*")), lexicalErr.pos) + require.EqualError(t, lexicalErr, "unclosed multi-line comment") + }) + } +} + +func TestParser_TryConsumeTokenKind(t *testing.T) { + for _, tc := range []struct { + name string + sql string + kind TokenKind + matched bool + next string + }{ + {"mismatch", "a /*", TokenKindComma, false, "a"}, + {"advance", "a b", TokenKindIdent, true, "b"}, + {"last token", "a", TokenKindIdent, true, ""}, + {"empty input", "", TokenKindIdent, false, ""}, + } { + t.Run(tc.name, func(t *testing.T) { + p := NewParser(tc.sql) + require.NoError(t, p.lexer.consumeToken()) + current := p.current() + token, err := p.tryConsumeTokenKind(tc.kind) + require.NoError(t, err) + if tc.matched { + require.Same(t, current, token) + } else { + require.Nil(t, token) + require.Equal(t, current, p.current()) + } + require.Equal(t, tc.next, p.currentTokenString()) + }) + } +} diff --git a/parser/lexer.go b/parser/lexer.go index 96df452..c8a861e 100644 --- a/parser/lexer.go +++ b/parser/lexer.go @@ -83,6 +83,11 @@ type Lexer struct { lexerState input string + + // Lexical failures are fatal for this input, even when discovered during + // lookahead. Keep them outside lexerState so restoring a cursor cannot + // discard the error or its original position. + err *lexerError } func NewLexer(buf string) *Lexer { @@ -252,6 +257,7 @@ func (l *Lexer) consumeSingleLineComment() { } func (l *Lexer) consumeMultiLineComment() error { + pos := Pos(l.offset) l.skipN(2) i := 0 for l.peekOk(i) { @@ -262,7 +268,7 @@ func (l *Lexer) consumeMultiLineComment() error { i++ } l.skipN(i) - return errors.New("unclosed multi-line comment") + return &lexerError{pos: pos, err: errors.New("unclosed multi-line comment")} } func (l *Lexer) consumeString() error { @@ -335,13 +341,11 @@ func (l *Lexer) skipComments() error { func (l *Lexer) peekToken() (*Token, error) { savedState := l.saveState() + defer l.restoreState(savedState) if err := l.consumeToken(); err != nil { return nil, err } - token := l.currentToken - - l.restoreState(savedState) - return token, nil + return l.currentToken, nil } func (l *Lexer) hasPrecedenceToken(last *Token) bool { @@ -356,10 +360,23 @@ func (l *Lexer) hasPrecedenceToken(last *Token) bool { last.Kind == TokenKindRBracket) } -func (l *Lexer) consumeToken() error { +func (l *Lexer) consumeToken() (err error) { // replace the current token; keep the previous one to disambiguate unary +/- prevToken := l.currentToken l.currentToken = nil + if l.err != nil { + return l.err + } + pos := Pos(l.offset) + defer func() { + if err != nil { + if !errors.As(err, &l.err) { + l.err = &lexerError{pos: pos, err: err} + } + err = l.err + } + }() + if err := l.skipComments(); err != nil { return err } @@ -367,6 +384,7 @@ func (l *Lexer) consumeToken() error { if l.isEOF() { return nil } + pos = Pos(l.offset) switch l.peekN(0) { case '>', '<', '!', '=', '|': if l.peekN(0) == '|' && l.peekOk(1) && l.peekN(1) == '|' || // || @@ -387,7 +405,7 @@ func (l *Lexer) consumeToken() error { // hasPrecedenceToken is used to distinguish between unary and binary operators if !l.hasPrecedenceToken(prevToken) && l.peekOk(1) && IsDigit(l.peekN(1)) { return l.consumeNumber() - } else if l.peekOk(1) && l.peekN(1) == '>' { + } else if l.peekN(0) == '-' && l.peekOk(1) && l.peekN(1) == '>' { l.currentToken = &Token{ String: l.slice(0, 2), Kind: TokenKindArrow, diff --git a/parser/lexer_test.go b/parser/lexer_test.go index 8cff73f..61011be 100644 --- a/parser/lexer_test.go +++ b/parser/lexer_test.go @@ -32,6 +32,32 @@ func TestConsumeComment(t *testing.T) { } +func TestPeekToken_RestoresState(t *testing.T) { + for _, suffix := range []string{"", " + 1", " /*", " 'unclosed", " `unclosed", " 1e+"} { + t.Run(suffix, func(t *testing.T) { + lexer := NewLexer("x" + suffix) + require.NoError(t, lexer.consumeToken()) + before := lexer.saveState() + + token, peekErr := lexer.peekToken() + require.Equal(t, before, lexer.saveState()) + err := lexer.consumeToken() + if peekErr != nil { + var le *lexerError + require.ErrorAs(t, peekErr, &le) + require.Equal(t, Pos(2), le.pos) + require.Equal(t, peekErr, err) + require.Nil(t, lexer.currentToken) + // Further advancement must not turn a lexical failure into EOF. + require.Equal(t, err, lexer.consumeToken()) + } else { + require.NoError(t, err) + require.Equal(t, token, lexer.currentToken) + } + }) + } +} + // TestConsumeUnterminatedComment guards against an infinite loop (a DoS hang) // when a block comment is never closed. consumeMultiLineComment previously // looped on isEOF() while only advancing a local index, so l.offset never diff --git a/parser/parse_system.go b/parser/parse_system.go index f75f091..beb500b 100644 --- a/parser/parse_system.go +++ b/parser/parse_system.go @@ -389,7 +389,9 @@ func (p *Parser) parseRoleName(_ Pos) (*RoleName, error) { return nil, err } var scope *StringLiteral - if p.tryConsumeTokenKind(TokenKindAtSign) != nil { + if token, consumeErr := p.tryConsumeTokenKind(TokenKindAtSign); consumeErr != nil { + return nil, consumeErr + } else if token != nil { scope, err = p.parseString(p.Pos()) if err != nil { return nil, err @@ -449,7 +451,9 @@ func (p *Parser) parseRoleSetting(_ Pos) (*RoleSetting, error) { p.matchTokenKind(TokenKindFloat), p.matchTokenKind(TokenKindString): var op TokenKind - if token := p.tryConsumeTokenKind(TokenKindSingleEQ); token != nil { + if token, consumeErr := p.tryConsumeTokenKind(TokenKindSingleEQ); consumeErr != nil { + return nil, consumeErr + } else if token != nil { op = token.Kind } value, err := p.parseLiteral(p.Pos()) @@ -487,7 +491,9 @@ func (p *Parser) parseRoleSettings(_ Pos) ([]*RoleSetting, error) { return nil, err } settings = append(settings, setting) - if p.tryConsumeTokenKind(TokenKindComma) == nil { + if token, consumeErr := p.tryConsumeTokenKind(TokenKindComma); consumeErr != nil { + return nil, consumeErr + } else if token == nil { break } } @@ -525,7 +531,14 @@ func (p *Parser) parseCreateRole(pos Pos) (*CreateRole, error) { return nil, err } roleNames = append(roleNames, roleName) - for p.tryConsumeTokenKind(TokenKindComma) != nil { + for { + token, consumeErr := p.tryConsumeTokenKind(TokenKindComma) + if consumeErr != nil { + return nil, consumeErr + } + if token == nil { + break + } roleName, err := p.parseRoleName(p.Pos()) if err != nil { return nil, err @@ -677,7 +690,14 @@ func (p *Parser) parseDefaultRoleClause(pos Pos) (*DefaultRoleClause, error) { } roles = append(roles, role) - for p.tryConsumeTokenKind(TokenKindComma) != nil { + for { + token, consumeErr := p.tryConsumeTokenKind(TokenKindComma) + if consumeErr != nil { + return nil, consumeErr + } + if token == nil { + break + } role, err := p.parseRoleName(p.Pos()) if err != nil { return nil, err @@ -712,7 +732,14 @@ func (p *Parser) parseGranteesClause(pos Pos) (*GranteesClause, error) { } granteeList = append(granteeList, grantee) - for p.tryConsumeTokenKind(TokenKindComma) != nil { + for { + token, consumeErr := p.tryConsumeTokenKind(TokenKindComma) + if consumeErr != nil { + return nil, consumeErr + } + if token == nil { + break + } grantee, err := p.parseRoleName(p.Pos()) if err != nil { return nil, err @@ -733,7 +760,14 @@ func (p *Parser) parseGranteesClause(pos Pos) (*GranteesClause, error) { } exceptList = append(exceptList, except) - for p.tryConsumeTokenKind(TokenKindComma) != nil { + for { + token, consumeErr := p.tryConsumeTokenKind(TokenKindComma) + if consumeErr != nil { + return nil, consumeErr + } + if token == nil { + break + } except, err := p.parseRoleName(p.Pos()) if err != nil { return nil, err @@ -777,7 +811,14 @@ func (p *Parser) parseUserNames() ([]*RoleName, error) { } userNames = append(userNames, userName) - for p.tryConsumeTokenKind(TokenKindComma) != nil { + for { + token, consumeErr := p.tryConsumeTokenKind(TokenKindComma) + if consumeErr != nil { + return nil, consumeErr + } + if token == nil { + break + } userName, err := p.parseRoleName(p.Pos()) if err != nil { return nil, err @@ -795,7 +836,14 @@ func (p *Parser) parseHostClauses() ([]*HostClause, error) { } hosts = append(hosts, host) - for p.tryConsumeTokenKind(TokenKindComma) != nil { + for { + token, consumeErr := p.tryConsumeTokenKind(TokenKindComma) + if consumeErr != nil { + return nil, consumeErr + } + if token == nil { + break + } host, err := p.parseHostClause(p.Pos()) if err != nil { return nil, err @@ -954,7 +1002,14 @@ func (p *Parser) parserDropUserOrRole(pos Pos) (*DropUserOrRole, error) { return nil, err } names = append(names, name) - for p.tryConsumeTokenKind(TokenKindComma) != nil { + for { + token, consumeErr := p.tryConsumeTokenKind(TokenKindComma) + if consumeErr != nil { + return nil, consumeErr + } + if token == nil { + break + } name, err := p.parseRoleName(p.Pos()) if err != nil { return nil, err @@ -1278,7 +1333,14 @@ func (p *Parser) parsePrivilegeRoles(_ Pos) ([]*Ident, error) { return nil, err } roles = append(roles, role) - for p.tryConsumeTokenKind(TokenKindComma) != nil { + for { + token, consumeErr := p.tryConsumeTokenKind(TokenKindComma) + if consumeErr != nil { + return nil, consumeErr + } + if token == nil { + break + } role, err := p.parseIdent() if err != nil { return nil, err @@ -1322,7 +1384,9 @@ func (p *Parser) parseGrantSource(_ Pos) (*TableIdentifier, error) { return nil, err } - if p.tryConsumeTokenKind(TokenKindDot) == nil { + if token, consumeErr := p.tryConsumeTokenKind(TokenKindDot); consumeErr != nil { + return nil, consumeErr + } else if token == nil { return &TableIdentifier{ Table: ident, }, nil @@ -1356,7 +1420,14 @@ func (p *Parser) parseGrantPrivilegeStmt(pos Pos) (*GrantPrivilegeStmt, error) { return nil, err } privileges = append(privileges, privilege) - for p.tryConsumeTokenKind(TokenKindComma) != nil { + for { + token, consumeErr := p.tryConsumeTokenKind(TokenKindComma) + if consumeErr != nil { + return nil, consumeErr + } + if token == nil { + break + } privilege, err := p.parsePrivilegeClause(p.Pos()) if err != nil { return nil, err @@ -1418,7 +1489,14 @@ func (p *Parser) parseAlterRole(pos Pos) (*AlterRole, error) { return nil, err } roleRenamePairs = append(roleRenamePairs, roleRenamePair) - for p.tryConsumeTokenKind(TokenKindComma) != nil { + for { + token, consumeErr := p.tryConsumeTokenKind(TokenKindComma) + if consumeErr != nil { + return nil, consumeErr + } + if token == nil { + break + } roleRenamePair, err := p.parseRoleRenamePair(p.Pos()) if err != nil { return nil, err diff --git a/parser/parser_alter.go b/parser/parser_alter.go index 2637dff..7c84c93 100644 --- a/parser/parser_alter.go +++ b/parser/parser_alter.go @@ -67,7 +67,9 @@ func (p *Parser) parseAlterTable(pos Pos) (*AlterTable, error) { return nil, err } alterTable.AlterExprs = append(alterTable.AlterExprs, alter) - if p.tryConsumeTokenKind(TokenKindComma) == nil { + if token, consumeErr := p.tryConsumeTokenKind(TokenKindComma); consumeErr != nil { + return nil, consumeErr + } else if token == nil { break } } @@ -868,7 +870,14 @@ func (p *Parser) parseAlterTableReset(pos Pos) (AlterTableClause, error) { } settings = append(settings, setting) - for p.tryConsumeTokenKind(TokenKindComma) != nil { + for { + token, consumeErr := p.tryConsumeTokenKind(TokenKindComma) + if consumeErr != nil { + return nil, consumeErr + } + if token == nil { + break + } setting, err = p.parseIdent() if err != nil { return nil, err @@ -922,7 +931,14 @@ func (p *Parser) parseAlterTableUpdate(pos Pos) (AlterTableClause, error) { assignments = append(assignments, assignment) // Parse additional comma-separated assignments - for p.tryConsumeTokenKind(TokenKindComma) != nil { + for { + token, consumeErr := p.tryConsumeTokenKind(TokenKindComma) + if consumeErr != nil { + return nil, consumeErr + } + if token == nil { + break + } assignment, err = p.parseUpdateAssignment(p.Pos()) if err != nil { return nil, err diff --git a/parser/parser_column.go b/parser/parser_column.go index 30de285..16257aa 100644 --- a/parser/parser_column.go +++ b/parser/parser_column.go @@ -188,7 +188,11 @@ func (p *Parser) parseInfix(expr Expr, precedence int) (Expr, error) { case p.matchTokenKind(TokenKindDot): _ = p.lexer.consumeToken() operation := TokenKindDot - hasTypeQualifier := p.tryConsumeTokenKind(TokenKindColon) != nil + qualifier, consumeErr := p.tryConsumeTokenKind(TokenKindColon) + if consumeErr != nil { + return nil, consumeErr + } + hasTypeQualifier := qualifier != nil if hasTypeQualifier { // Dynamic JSON subcolumns can pin their result type with // `.:Type`, for example `json.path.:`Array(JSON)``. @@ -381,7 +385,9 @@ func (p *Parser) parseColumnExtractExpr(pos Pos) (*ExtractExpr, error) { parameters = append(parameters, expr) } - if p.tryConsumeTokenKind(TokenKindComma) == nil { + if token, consumeErr := p.tryConsumeTokenKind(TokenKindComma); consumeErr != nil { + return nil, consumeErr + } else if token == nil { break } } @@ -488,7 +494,10 @@ func (p *Parser) peekIsClauseStarterKeyword() bool { // peekIsEndOfStatement reports whether the next token is EOF or `;`. func (p *Parser) peekIsEndOfStatement() bool { next, err := p.lexer.peekToken() - if err != nil || next == nil { + if err != nil { + return false + } + if next == nil { return true } return next.Kind == ";" @@ -593,10 +602,10 @@ func (p *Parser) parseColumnExpr(pos Pos) (Expr, error) { //nolint:funlen // (e.g. `WHERE interval > 1`), and no fixed lookahead separates the // two readings: `INTERVAL a + b DAY` only reveals the operator use at // the unit, four tokens out. Try the operator reading first and fall - // back to the identifier when it fails; the lexer state is the only - // parse state, so the restore is total and the outcome at a position - // never changes. That determinism makes memoizing failures sound, and - // retrying each position at most once is what keeps repeated interval + // back to the identifier when it fails. Restoring the lexer cursor + // retries the syntax; lexical failures remain fatal for the input. + // The outcome at a position never changes, so memoizing failures is sound. + // Retrying each position at most once is what keeps repeated interval // columns (`SELECT interval + interval + ...`) from backtracking // exponentially: a failed attempt reparses its whole suffix, retrying // every later INTERVAL inside it. @@ -763,7 +772,9 @@ func (p *Parser) parseColumnExprListWithTerm(term TokenKind, pos Pos) (*ColumnEx break } columnList = append(columnList, columnExpr) - if p.tryConsumeTokenKind(TokenKindComma) == nil { + if token, consumeErr := p.tryConsumeTokenKind(TokenKindComma); consumeErr != nil { + return nil, consumeErr + } else if token == nil { break } } @@ -785,7 +796,9 @@ func (p *Parser) parseSelectItems() ([]*SelectItem, error) { break } selectItems = append(selectItems, selectItem) - if p.tryConsumeTokenKind(TokenKindComma) == nil { + if token, consumeErr := p.tryConsumeTokenKind(TokenKindComma); consumeErr != nil { + return nil, consumeErr + } else if token == nil { break } if p.isSelectItemTerminatorKeyword() { @@ -854,7 +867,9 @@ func (p *Parser) parseColumnArgList(pos Pos) (*ColumnArgList, error) { return nil, err } items = append(items, item) - if p.tryConsumeTokenKind(TokenKindComma) == nil { + if token, consumeErr := p.tryConsumeTokenKind(TokenKindComma); consumeErr != nil { + return nil, consumeErr + } else if token == nil { break } } @@ -1011,7 +1026,9 @@ func (p *Parser) parseKeywordArgFunctionParams(pos Pos, form keywordArgForm) (*P } items = append(items, &ColumnExpr{Expr: item, Alias: alias}) - if p.tryConsumeTokenKind(TokenKindComma) == nil { + if token, consumeErr := p.tryConsumeTokenKind(TokenKindComma); consumeErr != nil { + return nil, consumeErr + } else if token == nil { break } @@ -1109,7 +1126,9 @@ func (p *Parser) parseMapLiteral(pos Pos) (*MapLiteral, error) { Key: *key, Value: value, }) - if p.tryConsumeTokenKind(TokenKindComma) == nil { + if token, consumeErr := p.tryConsumeTokenKind(TokenKindComma); consumeErr != nil { + return nil, consumeErr + } else if token == nil { break } } @@ -1311,7 +1330,9 @@ func (p *Parser) parseColumnType(_ Pos) (ColumnType, error) { } func (p *Parser) parseColumnTypeArgs(ident *Ident) (ColumnType, error) { // nolint:funlen - if lParen := p.tryConsumeTokenKind(TokenKindLParen); lParen != nil { + if lParen, consumeErr := p.tryConsumeTokenKind(TokenKindLParen); consumeErr != nil { + return nil, consumeErr + } else if lParen != nil { switch { case p.matchTokenKind(TokenKindIdent): switch { @@ -1369,7 +1390,9 @@ func (p *Parser) parseComplexType(name *Ident, pos Pos) (*ComplexType, error) { return nil, err } subTypes = append(subTypes, subExpr) - if p.tryConsumeTokenKind(TokenKindComma) == nil { + if token, consumeErr := p.tryConsumeTokenKind(TokenKindComma); consumeErr != nil { + return nil, consumeErr + } else if token == nil { break } } @@ -1400,7 +1423,9 @@ func (p *Parser) parseEnumType(name *Ident, pos Pos) (*EnumType, error) { break } enumType.Values = append(enumType.Values, *enumValue) - if p.tryConsumeTokenKind(TokenKindComma) == nil { + if token, consumeErr := p.tryConsumeTokenKind(TokenKindComma); consumeErr != nil { + return nil, consumeErr + } else if token == nil { break } } @@ -1420,7 +1445,14 @@ func (p *Parser) parseColumnTypeWithParams(name *Ident, pos Pos) (*TypeWithParam return nil, err } params = append(params, param) - for !p.lexer.isEOF() && p.tryConsumeTokenKind(TokenKindComma) != nil { + for !p.lexer.isEOF() { + token, consumeErr := p.tryConsumeTokenKind(TokenKindComma) + if consumeErr != nil { + return nil, consumeErr + } + if token == nil { + break + } size, err := p.parseLiteral(p.Pos()) if err != nil { return nil, err @@ -1448,7 +1480,14 @@ func (p *Parser) parseJSONPath() (*JSONPath, error) { } idents = append(idents, ident) - for !p.lexer.isEOF() && p.tryConsumeTokenKind(TokenKindDot) != nil { + for !p.lexer.isEOF() { + token, consumeErr := p.tryConsumeTokenKind(TokenKindDot) + if consumeErr != nil { + return nil, consumeErr + } + if token == nil { + break + } ident, err := p.parseAnyKeyword() if err != nil { return nil, err @@ -1522,7 +1561,9 @@ func (p *Parser) parseJSONOption() (*JSONOption, error) { if err != nil { return nil, err } - if p.tryConsumeTokenKind(TokenKindSingleEQ) != nil { + if token, consumeErr := p.tryConsumeTokenKind(TokenKindSingleEQ); consumeErr != nil { + return nil, consumeErr + } else if token != nil { // This is a max_dynamic_* option; only valid when path is a single ident of that name // Reconstruct handling similar to parseJSONMaxDynamicOptions but we already consumed ident and '=' // Determine which option based on the first ident name @@ -1570,7 +1611,9 @@ func (p *Parser) parseJSONType(name *Ident, pos Pos) (*JSONType, error) { return nil, err } options = append(options, option) - if p.tryConsumeTokenKind(",") == nil { + if token, consumeErr := p.tryConsumeTokenKind(","); consumeErr != nil { + return nil, consumeErr + } else if token == nil { break } } @@ -1647,7 +1690,9 @@ func (p *Parser) parseNestedTypeFieldsWithNames(columnName *Ident) ([]Expr, erro ColumnEnd: columnType.End(), }) - if p.tryConsumeTokenKind(TokenKindComma) == nil { + if token, consumeErr := p.tryConsumeTokenKind(TokenKindComma); consumeErr != nil { + return nil, consumeErr + } else if token == nil { return columns, nil } @@ -1661,7 +1706,9 @@ func (p *Parser) parseNestedTypeFieldsWithNames(columnName *Ident) ([]Expr, erro } columns = append(columns, column) - if p.tryConsumeTokenKind(TokenKindComma) == nil { + if token, consumeErr := p.tryConsumeTokenKind(TokenKindComma); consumeErr != nil { + return nil, consumeErr + } else if token == nil { break } } @@ -1679,7 +1726,9 @@ func (p *Parser) parseNestedTypeFieldsWithoutNames(columnType *Ident) ([]Expr, e columns = append(columns, column) - if p.tryConsumeTokenKind(TokenKindComma) == nil { + if token, consumeErr := p.tryConsumeTokenKind(TokenKindComma); consumeErr != nil { + return nil, consumeErr + } else if token == nil { return columns, nil } @@ -1693,7 +1742,9 @@ func (p *Parser) parseNestedTypeFieldsWithoutNames(columnType *Ident) ([]Expr, e } columns = append(columns, column) - if p.tryConsumeTokenKind(TokenKindComma) == nil { + if token, consumeErr := p.tryConsumeTokenKind(TokenKindComma); consumeErr != nil { + return nil, consumeErr + } else if token == nil { break } } @@ -1839,7 +1890,9 @@ func (p *Parser) parseColumnStar(pos Pos) (*Ident, error) { } func (p *Parser) tryParseCompressionLevel(pos Pos) (*NumberLiteral, error) { - if p.tryConsumeTokenKind(TokenKindLParen) == nil { + if token, consumeErr := p.tryConsumeTokenKind(TokenKindLParen); consumeErr != nil { + return nil, consumeErr + } else if token == nil { return nil, nil // nolint } diff --git a/parser/parser_common.go b/parser/parser_common.go index 7532601..1b72da1 100644 --- a/parser/parser_common.go +++ b/parser/parser_common.go @@ -98,7 +98,11 @@ func (p *Parser) matchTokenKind(kinds ...TokenKind) bool { // expectTokenKind consumes the current token if it is the given kind. func (p *Parser) expectTokenKind(kind TokenKind) error { - if curToken := p.tryConsumeTokenKind(kind); curToken != nil { + curToken, err := p.tryConsumeTokenKind(kind) + if err != nil { + return err + } + if curToken != nil { return nil } return &ParseError{ @@ -108,13 +112,17 @@ func (p *Parser) expectTokenKind(kind TokenKind) error { } } -func (p *Parser) tryConsumeTokenKind(kind TokenKind) *Token { +// tryConsumeTokenKind returns nil, nil on a mismatch and propagates errors +// encountered while advancing past a matching token. +func (p *Parser) tryConsumeTokenKind(kind TokenKind) (*Token, error) { if p.matchTokenKind(kind) { curToken := p.current() - _ = p.lexer.consumeToken() - return curToken + if err := p.lexer.consumeToken(); err != nil { + return nil, err + } + return curToken, nil } - return nil + return nil, nil } func (p *Parser) matchKeyword(keyword string) bool { @@ -241,7 +249,9 @@ func (p *Parser) parseIdentOrString() (*Ident, error) { } func (p *Parser) tryParseDotIdent(_ Pos) (*Ident, error) { - if p.tryConsumeTokenKind(TokenKindDot) == nil { + if token, consumeErr := p.tryConsumeTokenKind(TokenKindDot); consumeErr != nil { + return nil, consumeErr + } else if token == nil { return nil, nil // nolint } // After a dot the token can only be a member name, so even reserved @@ -250,7 +260,9 @@ func (p *Parser) tryParseDotIdent(_ Pos) (*Ident, error) { } func (p *Parser) tryParseDotIdentOrString(_ Pos) (*Ident, error) { - if p.tryConsumeTokenKind(TokenKindDot) == nil { + if token, consumeErr := p.tryConsumeTokenKind(TokenKindDot); consumeErr != nil { + return nil, consumeErr + } else if token == nil { return nil, nil // nolint } // After a dot the token can only be a member name, so even reserved @@ -459,6 +471,11 @@ func (p *Parser) parseFormat(pos Pos) (*FormatClause, error) { // captured position and expected-token information; the long tail of // fmt.Errorf sites is wrapped here with the current position. func (p *Parser) wrapError(err error) error { + // A grammar error can be a consequence of an ignored lexical failure. + // Report the original failure, including when lookahead restored the cursor. + if p.lexer.err != nil { + err = &ParseError{Pos: p.lexer.err.pos, Msg: p.lexer.err.Error()} + } if err == nil { return nil } @@ -486,7 +503,9 @@ func (p *Parser) parseRatioExpr(pos Pos) (*RatioExpr, error) { } var denominator *NumberLiteral - if p.tryConsumeTokenKind(TokenKindDiv) != nil { + if token, consumeErr := p.tryConsumeTokenKind(TokenKindDiv); consumeErr != nil { + return nil, consumeErr + } else if token != nil { // the denominator starts at its own token, not at the numerator denominator, err = p.parseNumber(p.Pos()) if err != nil { diff --git a/parser/parser_query.go b/parser/parser_query.go index 128d008..4df0c8a 100644 --- a/parser/parser_query.go +++ b/parser/parser_query.go @@ -25,7 +25,14 @@ func (p *Parser) parseWithClause(pos Pos) (*WithClause, error) { return nil, err } ctes := []*CTEStmt{cteExpr} - for p.tryConsumeTokenKind(TokenKindComma) != nil { + for { + token, consumeErr := p.tryConsumeTokenKind(TokenKindComma) + if consumeErr != nil { + return nil, consumeErr + } + if token == nil { + break + } cteExpr, err := p.parseCTEStmt(p.Pos()) if err != nil { return nil, err @@ -151,7 +158,11 @@ func (p *Parser) tryParseJoinConstraints(pos Pos) (Expr, error) { On: columnExprList, }, nil case p.tryConsumeKeywords(KeywordUsing): - hasParen := p.tryConsumeTokenKind(TokenKindLParen) != nil + lparen, consumeErr := p.tryConsumeTokenKind(TokenKindLParen) + if consumeErr != nil { + return nil, consumeErr + } + hasParen := lparen != nil columnExprList, err := p.parseColumnExprListWithLParen(p.Pos()) if err != nil { return nil, err @@ -335,8 +346,12 @@ func (p *Parser) peekJoinAfterLocality() bool { func (p *Parser) parseJoinRightExpr(pos Pos) (expr Expr, err error) { var rightExpr Expr var modifiers []string + comma, err := p.tryConsumeTokenKind(TokenKindComma) + if err != nil { + return nil, err + } switch { - case p.tryConsumeTokenKind(TokenKindComma) != nil: + case comma != nil: return p.parseJoinExpr(p.Pos()) default: // GLOBAL/LOCAL only says how the right-hand table is distributed, so @@ -641,7 +656,9 @@ func (p *Parser) parseLimitClause(pos Pos) (*LimitClause, error) { if p.tryConsumeKeywords(KeywordOffset) { offset, err = p.parseExpr(p.Pos()) - } else if p.tryConsumeTokenKind(TokenKindComma) != nil { + } else if token, consumeErr := p.tryConsumeTokenKind(TokenKindComma); consumeErr != nil { + return nil, consumeErr + } else if token != nil { offset = limit limit, err = p.parseExpr(p.Pos()) } @@ -988,7 +1005,9 @@ func (p *Parser) parseWindowClause(pos Pos) (*WindowClause, error) { Expr: condition, }) - if p.tryConsumeTokenKind(TokenKindComma) == nil { + if token, consumeErr := p.tryConsumeTokenKind(TokenKindComma); consumeErr != nil { + return nil, consumeErr + } else if token == nil { break } } @@ -1030,7 +1049,11 @@ func (p *Parser) parseHavingClause(pos Pos) (*HavingClause, error) { func (p *Parser) parseSubQuery(_ Pos) (*SubQuery, error) { - hasParen := p.tryConsumeTokenKind(TokenKindLParen) != nil + lparen, consumeErr := p.tryConsumeTokenKind(TokenKindLParen) + if consumeErr != nil { + return nil, consumeErr + } + hasParen := lparen != nil selectQuery, err := p.parseSelectQuery(p.Pos()) if err != nil { @@ -1055,7 +1078,9 @@ func (p *Parser) parseSelectQuery(_ Pos) (*SelectQuery, error) { var selectStmt *SelectQuery var err error - if lparen := p.tryConsumeTokenKind(TokenKindLParen); lparen != nil { + if lparen, consumeErr := p.tryConsumeTokenKind(TokenKindLParen); consumeErr != nil { + return nil, consumeErr + } else if lparen != nil { inner, err := p.parseSelectQuery(p.Pos()) if err != nil { return nil, err @@ -1172,11 +1197,11 @@ func (p *Parser) parseSelectStmt(pos Pos) (*SelectQuery, error) { // nolint: fun if err != nil { return nil, err } - - statementEnd := pos - if len(selectItems) > 0 { - statementEnd = selectItems[len(selectItems)-1].End() + if len(selectItems) == 0 { + return nil, fmt.Errorf("expected SELECT expression") } + + statementEnd := selectItems[len(selectItems)-1].End() from, err := p.tryParseFromClause(p.Pos()) if err != nil { return nil, err @@ -1317,7 +1342,9 @@ func (p *Parser) parseCTEStmt(pos Pos) (*CTEStmt, error) { if err := p.expectKeyword(KeywordAs); err != nil { return nil, err } - if p.tryConsumeTokenKind(TokenKindLParen) != nil { + if token, consumeErr := p.tryConsumeTokenKind(TokenKindLParen); consumeErr != nil { + return nil, consumeErr + } else if token != nil { selectQuery, err := p.parseSelectQuery(p.Pos()) if err != nil { return nil, err diff --git a/parser/parser_table.go b/parser/parser_table.go index 72156a9..6d962c5 100644 --- a/parser/parser_table.go +++ b/parser/parser_table.go @@ -245,7 +245,9 @@ func (p *Parser) parseCreateNamedCollection(pos Pos) (*CreateNamedCollection, er params = append(params, param) // Check if there's another parameter - if p.tryConsumeTokenKind(TokenKindComma) == nil { + if token, consumeErr := p.tryConsumeTokenKind(TokenKindComma); consumeErr != nil { + return nil, consumeErr + } else if token == nil { break } } @@ -663,7 +665,9 @@ func (p *Parser) parseTableColumns() ([]Expr, error) { } columns = append(columns, column) } - if p.tryConsumeTokenKind(TokenKindComma) == nil { + if token, consumeErr := p.tryConsumeTokenKind(TokenKindComma); consumeErr != nil { + return nil, consumeErr + } else if token == nil { break } } @@ -880,7 +884,9 @@ func (p *Parser) parseTableArgList(pos Pos) (*TableArgListExpr, error) { return nil, err } args = append(args, arg) - if p.tryConsumeTokenKind(TokenKindComma) == nil { + if token, consumeErr := p.tryConsumeTokenKind(TokenKindComma); consumeErr != nil { + return nil, consumeErr + } else if token == nil { break } } @@ -1011,7 +1017,12 @@ func (p *Parser) parseOrderByClause(pos Pos) (*OrderByClause, error) { } items = append(items, expr) - if p.lexer.isEOF() || p.tryConsumeTokenKind(TokenKindComma) == nil { + if p.lexer.isEOF() { + break + } + if token, consumeErr := p.tryConsumeTokenKind(TokenKindComma); consumeErr != nil { + return nil, consumeErr + } else if token == nil { break } } @@ -1142,7 +1153,9 @@ func (p *Parser) parseInterpolateClause(interpolatePos Pos) (*InterpolateClause, ListEnd: interpolatePos + Pos(len("INTERPOLATE")), } - if p.tryConsumeTokenKind(TokenKindLParen) == nil { + if token, consumeErr := p.tryConsumeTokenKind(TokenKindLParen); consumeErr != nil { + return nil, consumeErr + } else if token == nil { // INTERPOLATE without columns is valid return interpolate, nil } @@ -1166,12 +1179,17 @@ func (p *Parser) parseInterpolateClause(interpolatePos Pos) (*InterpolateClause, items = append(items, item) - if p.tryConsumeTokenKind(TokenKindComma) == nil { + if token, consumeErr := p.tryConsumeTokenKind(TokenKindComma); consumeErr != nil { + return nil, consumeErr + } else if token == nil { break } } - rparen := p.tryConsumeTokenKind(TokenKindRParen) + rparen, err := p.tryConsumeTokenKind(TokenKindRParen) + if err != nil { + return nil, err + } if rparen == nil { return nil, fmt.Errorf("expected ')' after INTERPOLATE column list") } @@ -1208,7 +1226,14 @@ func (p *Parser) parseTTLClause(pos Pos, allowMultiValues bool) ([]*TTLExpr, err return nil, err } items = append(items, expr) - for allowMultiValues && !p.lexer.isEOF() && p.tryConsumeTokenKind(TokenKindComma) != nil { + for allowMultiValues && !p.lexer.isEOF() { + token, consumeErr := p.tryConsumeTokenKind(TokenKindComma) + if consumeErr != nil { + return nil, consumeErr + } + if token == nil { + break + } expr, err = p.parseTTLExpr(pos) if err != nil { return nil, err @@ -1314,7 +1339,9 @@ func (p *Parser) parseTTLPolicyGroupBy(pos Pos) (*TTLPolicyRule, error) { } keys.Items = append(keys.Items, key) keys.ListEnd = key.End() - if p.tryConsumeTokenKind(TokenKindComma) == nil { + if token, consumeErr := p.tryConsumeTokenKind(TokenKindComma); consumeErr != nil { + return nil, consumeErr + } else if token == nil { break } } @@ -1339,7 +1366,9 @@ func (p *Parser) parseTTLPolicyGroupBy(pos Pos) (*TTLPolicyRule, error) { // none follows so parseTTLClause can treat the comma as a rule // separator. savedState := p.lexer.saveState() - if p.tryConsumeTokenKind(TokenKindComma) == nil { + if token, consumeErr := p.tryConsumeTokenKind(TokenKindComma); consumeErr != nil { + return nil, consumeErr + } else if token == nil { break } set, err := p.parseTTLPolicySet(p.Pos()) @@ -1442,7 +1471,14 @@ func (p *Parser) parseSettingsList(pos Pos) ([]*SettingExpr, error) { return nil, err } items = append(items, expr) - for p.tryConsumeTokenKind(TokenKindComma) != nil { + for { + token, consumeErr := p.tryConsumeTokenKind(TokenKindComma) + if consumeErr != nil { + return nil, consumeErr + } + if token == nil { + break + } expr, err = p.parseSettingsExpr(p.Pos()) if err != nil { return nil, err @@ -1532,7 +1568,9 @@ func (p *Parser) parseEngineExpr(pos Pos) (*EngineExpr, error) { if err := p.expectKeyword(KeywordEngine); err != nil { return nil, err } - _ = p.tryConsumeTokenKind(TokenKindSingleEQ) + if _, err := p.tryConsumeTokenKind(TokenKindSingleEQ); err != nil { + return nil, err + } engineExpr := &EngineExpr{EnginePos: pos} var engineEnd Pos @@ -1674,7 +1712,7 @@ func (p *Parser) ParseStmts() ([]Expr, error) { if err := p.lexer.consumeToken(); err != nil { return nil, p.wrapError(err) } - if p.lexer.isEOF() { + if p.current() == nil { break } if p.matchTokenKind(";") { @@ -1941,14 +1979,23 @@ func (p *Parser) parseColumnNamesExpr(pos Pos) (*ColumnNamesExpr, error) { } var columnNames []NestedIdentifier - for !p.lexer.isEOF() && p.tryConsumeTokenKind(TokenKindRParen) == nil { + for !p.lexer.isEOF() { + token, consumeErr := p.tryConsumeTokenKind(TokenKindRParen) + if consumeErr != nil { + return nil, consumeErr + } + if token != nil { + break + } name, err := p.ParseNestedIdentifier(p.Pos()) if err != nil { return nil, err } columnNames = append(columnNames, *name) - if p.tryConsumeTokenKind(TokenKindComma) == nil { + if token, consumeErr := p.tryConsumeTokenKind(TokenKindComma); consumeErr != nil { + return nil, consumeErr + } else if token == nil { break } } @@ -1999,7 +2046,14 @@ func (p *Parser) parseAssignmentValues(pos Pos) (*AssignmentValues, error) { var value Expr var err error values := make([]Expr, 0) - for !p.lexer.isEOF() && p.tryConsumeTokenKind(TokenKindRParen) == nil { + for !p.lexer.isEOF() { + token, consumeErr := p.tryConsumeTokenKind(TokenKindRParen) + if consumeErr != nil { + return nil, consumeErr + } + if token != nil { + break + } switch { case p.matchTokenKind(TokenKindLParen): value, err = p.parseAssignmentValues(p.Pos()) @@ -2013,7 +2067,9 @@ func (p *Parser) parseAssignmentValues(pos Pos) (*AssignmentValues, error) { return nil, err } values = append(values, value) - if p.tryConsumeTokenKind(TokenKindComma) == nil { + if token, consumeErr := p.tryConsumeTokenKind(TokenKindComma); consumeErr != nil { + return nil, consumeErr + } else if token == nil { break } } @@ -2073,7 +2129,9 @@ func (p *Parser) parseInsertStmt(pos Pos) (*InsertStmt, error) { return nil, err } values = append(values, value) - if p.tryConsumeTokenKind(TokenKindComma) == nil { + if token, consumeErr := p.tryConsumeTokenKind(TokenKindComma); consumeErr != nil { + return nil, consumeErr + } else if token == nil { break } } @@ -2112,7 +2170,14 @@ func (p *Parser) parseRenameStmt(pos Pos) (*RenameStmt, error) { return nil, err } tablePairList := []*TargetPair{targetPair} - for p.tryConsumeTokenKind(TokenKindComma) != nil { + for { + token, consumeErr := p.tryConsumeTokenKind(TokenKindComma) + if consumeErr != nil { + return nil, consumeErr + } + if token == nil { + break + } tablePair, err := p.parseTargetPair(p.Pos()) if err != nil { return nil, err @@ -2219,7 +2284,14 @@ func (p *Parser) parseDictionarySchemaClause(pos Pos) (*DictionarySchemaClause, schema.Attributes = append(schema.Attributes, attr) // Parse additional attributes - for p.tryConsumeTokenKind(TokenKindComma) != nil { + for { + token, consumeErr := p.tryConsumeTokenKind(TokenKindComma) + if consumeErr != nil { + return nil, consumeErr + } + if token == nil { + break + } attr, err := p.parseDictionaryAttribute(p.Pos()) if err != nil { return nil, err @@ -2712,7 +2784,14 @@ func (p *Parser) parseDictionarySettingsClause(pos Pos) (*SettingsClause, error) items = append(items, expr) // Parse additional settings - for p.tryConsumeTokenKind(TokenKindComma) != nil { + for { + token, consumeErr := p.tryConsumeTokenKind(TokenKindComma) + if consumeErr != nil { + return nil, consumeErr + } + if token == nil { + break + } expr, err := p.parseSettingsExpr(p.Pos()) if err != nil { return nil, err diff --git a/parser/parser_test.go b/parser/parser_test.go index 07f64e2..17ab8e9 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -189,6 +189,10 @@ func validFormatSQL(t *testing.T, sql string) { func TestParser_InvalidSyntax(t *testing.T) { invalidSQLs := []string{ + "SELECT", + "SELECT 1; SELECT", + "SELECT 1 /*", + "SELECT 1 +> 2", "SELECT * FROM", // WITH FILL error cases "SELECT n FROM t ORDER BY n WITH", // WITH without FILL @@ -292,6 +296,29 @@ func TestParser_InvalidSyntax(t *testing.T) { } } +func TestParser_FinalTokenConsumption(t *testing.T) { + for _, suffix := range []string{"", " ", ";", " -- trailing comment", " /* trailing comment */"} { + t.Run(suffix, func(t *testing.T) { + for _, sql := range []string{"SELECT", "SELECT 1; SELECT"} { + stmts, err := NewParser(sql + suffix).ParseStmts() + require.Error(t, err, "SQL: %s", sql+suffix) + require.Nil(t, stmts) + } + + stmts, err := NewParser("SELECT 1; SELECT 2" + suffix).ParseStmts() + require.NoError(t, err) + require.Len(t, stmts, 2) + require.Equal(t, "SELECT 2", Format(stmts[1])) + }) + } + + for _, sql := range []string{"", " ", ";;", "-- comment", "/* comment */"} { + stmts, err := NewParser(sql).ParseStmts() + require.NoError(t, err) + require.Empty(t, stmts) + } +} + func TestParser_ParenthesizedSetOperationOperands(t *testing.T) { // A parenthesized operand becomes a group node, so the operator after // ')' binds to the whole group instead of leaking into its chain. diff --git a/parser/testdata/basic/format/beautify/statement_boundaries.sql b/parser/testdata/basic/format/beautify/statement_boundaries.sql new file mode 100644 index 0000000..034b6de --- /dev/null +++ b/parser/testdata/basic/format/beautify/statement_boundaries.sql @@ -0,0 +1,9 @@ +-- Origin SQL: +SELECT arrayMap(x -> x + 1, [1, 2]); +SELECT 2 + +-- Beautify SQL: +SELECT + arrayMap(x -> x + 1, [1, 2]); +SELECT + 2; diff --git a/parser/testdata/basic/format/statement_boundaries.sql b/parser/testdata/basic/format/statement_boundaries.sql new file mode 100644 index 0000000..bae0a81 --- /dev/null +++ b/parser/testdata/basic/format/statement_boundaries.sql @@ -0,0 +1,7 @@ +-- Origin SQL: +SELECT arrayMap(x -> x + 1, [1, 2]); +SELECT 2 + +-- Format SQL: +SELECT arrayMap(x -> x + 1, [1, 2]); +SELECT 2; diff --git a/parser/testdata/basic/output/statement_boundaries.sql.golden.json b/parser/testdata/basic/output/statement_boundaries.sql.golden.json new file mode 100644 index 0000000..32112ff --- /dev/null +++ b/parser/testdata/basic/output/statement_boundaries.sql.golden.json @@ -0,0 +1,151 @@ +[ + { + "SelectPos": 0, + "StatementEnd": 34, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "Name": { + "Name": "arrayMap", + "QuoteType": 1, + "NamePos": 7, + "NameEnd": 15 + }, + "Params": { + "LeftParenPos": 15, + "RightParenPos": 34, + "Items": { + "ListPos": 16, + "ListEnd": 33, + "HasDistinct": false, + "Items": [ + { + "Expr": { + "LeftExpr": { + "Name": "x", + "QuoteType": 1, + "NamePos": 16, + "NameEnd": 17 + }, + "Operation": "-\u003e", + "RightExpr": { + "LeftExpr": { + "Name": "x", + "QuoteType": 1, + "NamePos": 21, + "NameEnd": 22 + }, + "Operation": "+", + "RightExpr": { + "NumPos": 25, + "NumEnd": 26, + "Literal": "1", + "Base": 10 + }, + "HasGlobal": false, + "HasNot": false + }, + "HasGlobal": false, + "HasNot": false + }, + "Alias": null + }, + { + "Expr": { + "LeftBracketPos": 28, + "RightBracketPos": 33, + "Items": { + "ListPos": 29, + "ListEnd": 33, + "HasDistinct": false, + "Items": [ + { + "Expr": { + "NumPos": 29, + "NumEnd": 30, + "Literal": "1", + "Base": 10 + }, + "Alias": null + }, + { + "Expr": { + "NumPos": 32, + "NumEnd": 33, + "Literal": "2", + "Base": 10 + }, + "Alias": null + } + ] + } + }, + "Alias": null + } + ] + }, + "ColumnArgList": null + } + }, + "Modifiers": [], + "Alias": null + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "UnionAll": null, + "UnionDistinct": null, + "Except": null, + "Intersect": null + }, + { + "SelectPos": 37, + "StatementEnd": 45, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 44, + "NumEnd": 45, + "Literal": "2", + "Base": 10 + }, + "Modifiers": [], + "Alias": null + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "UnionAll": null, + "UnionDistinct": null, + "Except": null, + "Intersect": null + } +] \ No newline at end of file diff --git a/parser/testdata/basic/statement_boundaries.sql b/parser/testdata/basic/statement_boundaries.sql new file mode 100644 index 0000000..c0d0ccc --- /dev/null +++ b/parser/testdata/basic/statement_boundaries.sql @@ -0,0 +1,2 @@ +SELECT arrayMap(x -> x + 1, [1, 2]); +SELECT 2 \ No newline at end of file