parseFromClause parses the FROM clause including comma-separated table references and any subsequent JOIN clauses. Returns the primary table name (for compatibility), the full table-reference slice, and the join slice.
()
| 32 | // and any subsequent JOIN clauses. Returns the primary table name (for compatibility), |
| 33 | // the full table-reference slice, and the join slice. |
| 34 | func (p *Parser) parseFromClause() (tableName string, tables []ast.TableReference, joins []ast.JoinClause, err error) { |
| 35 | // FROM is optional (e.g. SELECT 1). Validate that the next token makes sense. |
| 36 | if !p.isType(models.TokenTypeFrom) { |
| 37 | // advance() sets a zero-value sentinel (TokenType == TokenTypeEOF == 0) when |
| 38 | // currentPos goes past the end of the token slice. A real EOF token produced by |
| 39 | // the tokenizer is also TokenTypeEOF but is still within the slice. |
| 40 | // To replicate the correct behaviour we need to know what the last consumed token |
| 41 | // was: e.g. SELECT (1,2,3) ends with ')' (FROM is optional) while SELECT * |
| 42 | // ends with '*' (FROM is required). When currentPos is past the slice end, |
| 43 | // inspect the last real token directly instead of relying on currentToken. |
| 44 | if p.currentPos >= len(p.tokens) { |
| 45 | // Past end of token stream - check whether the last real token implies |
| 46 | // that FROM can legitimately be omitted. |
| 47 | if len(p.tokens) > 0 { |
| 48 | lastTokType := p.tokens[len(p.tokens)-1].Token.Type |
| 49 | if lastTokType == models.TokenTypeSemicolon || |
| 50 | lastTokType == models.TokenTypeRParen || |
| 51 | lastTokType == models.TokenTypeEOF || |
| 52 | lastTokType == models.TokenTypeUnion || |
| 53 | lastTokType == models.TokenTypeExcept || |
| 54 | lastTokType == models.TokenTypeIntersect { |
| 55 | return "", nil, nil, nil |
| 56 | } |
| 57 | } |
| 58 | return "", nil, nil, p.expectedError("FROM, semicolon, or end of statement") |
| 59 | } |
| 60 | // Current token is still within the slice - use the normal token-type check. |
| 61 | if !p.isType(models.TokenTypeEOF) && |
| 62 | !p.isType(models.TokenTypeSemicolon) && |
| 63 | !p.isType(models.TokenTypeRParen) && |
| 64 | !p.isAnyType(models.TokenTypeUnion, models.TokenTypeExcept, models.TokenTypeIntersect) && |
| 65 | !p.isMinusSetOp() { |
| 66 | return "", nil, nil, p.expectedError("FROM, semicolon, or end of statement") |
| 67 | } |
| 68 | return "", nil, nil, nil |
| 69 | } |
| 70 | |
| 71 | p.advance() // Consume FROM |
| 72 | |
| 73 | if p.isType(models.TokenTypeEOF) || p.isType(models.TokenTypeSemicolon) { |
| 74 | return "", nil, nil, goerrors.ExpectedTokenError( |
| 75 | "table name", |
| 76 | p.currentToken.Token.Type.String(), |
| 77 | p.currentLocation(), |
| 78 | "FROM clause requires at least one table reference", |
| 79 | ) |
| 80 | } |
| 81 | |
| 82 | // First table reference |
| 83 | firstRef, e := p.parseFromTableReference() |
| 84 | if e != nil { |
| 85 | return "", nil, nil, e |
| 86 | } |
| 87 | tableName = firstRef.Name |
| 88 | |
| 89 | // ClickHouse FINAL modifier — consumed after table reference, before JOINs. |
| 90 | // NOTE: FINAL only applies to the first (primary) table reference. In ClickHouse, |
| 91 | // FINAL is a per-table modifier; multi-table FROM with FINAL on a non-first |
no test coverage detected