diff --git a/src/main/kotlin/kestrel/sql/parser/Parser.kt b/src/main/kotlin/kestrel/sql/parser/Parser.kt index df871c6..5e19228 100644 --- a/src/main/kotlin/kestrel/sql/parser/Parser.kt +++ b/src/main/kotlin/kestrel/sql/parser/Parser.kt @@ -1348,19 +1348,61 @@ class Parser(private val tokens: List) { private fun parseComparison(): Expression { var left = parseAdditive() while (true) { - val operator = when { - check(TokenType.Equal) -> BinaryOperator.Equal - check(TokenType.NotEqual) -> BinaryOperator.NotEqual - check(TokenType.Less) -> BinaryOperator.Less - check(TokenType.LessEqual) -> BinaryOperator.LessEqual - check(TokenType.Greater) -> BinaryOperator.Greater - check(TokenType.GreaterEqual) -> BinaryOperator.GreaterEqual - else -> break + val negated = match(TokenType.Not) + left = when { + check(TokenType.In) -> { advance(); parseInSuffix(left, negated) } + check(TokenType.Between) -> { advance(); parseBetweenSuffix(left, negated) } + check(TokenType.Like) -> { advance(); parseLikeSuffix(left, negated) } + negated -> throw error("expected IN, BETWEEN, or LIKE after NOT", peek()) + check(TokenType.Is) -> { advance(); parseIsSuffix(left) } + else -> { + val operator = when { + check(TokenType.Equal) -> BinaryOperator.Equal + check(TokenType.NotEqual) -> BinaryOperator.NotEqual + check(TokenType.Less) -> BinaryOperator.Less + check(TokenType.LessEqual) -> BinaryOperator.LessEqual + check(TokenType.Greater) -> BinaryOperator.Greater + check(TokenType.GreaterEqual) -> BinaryOperator.GreaterEqual + else -> return left + } + advance() + BinaryExpression(operator, left, parseAdditive()) + } } - advance() - left = BinaryExpression(operator, left, parseAdditive()) } - return left + } + + private fun parseIsSuffix(left: Expression): Expression { + val negated = match(TokenType.Not) + expect(TokenType.Null, "expected NULL after IS" + if (negated) " NOT" else "") + return IsNullExpression(left, negated) + } + + private fun parseInSuffix(left: Expression, negated: Boolean): Expression { + expect(TokenType.LeftParen, "expected '(' after IN") + if (check(TokenType.Select)) { + val select = parseSelect() + expect(TokenType.RightParen, "expected ')' after IN subquery") + return InSubqueryExpression(left, select, negated) + } + val values = mutableListOf() + do { + values.add(parseExpression()) + } while (match(TokenType.Comma)) + expect(TokenType.RightParen, "expected ')' after IN list") + return InExpression(left, values, negated) + } + + private fun parseBetweenSuffix(left: Expression, negated: Boolean): Expression { + val low = parseAdditive() + expect(TokenType.And, "expected AND in BETWEEN") + val high = parseAdditive() + return BetweenExpression(left, low, high, negated) + } + + private fun parseLikeSuffix(left: Expression, negated: Boolean): Expression { + val pattern = parseAdditive() + return LikeExpression(left, pattern, negated) } private fun parseAdditive(): Expression { @@ -1378,7 +1420,7 @@ class Parser(private val tokens: List) { } private fun parseMultiplicative(): Expression { - var left = parseUnary() + var left = parseCollate() while (true) { val operator = when { check(TokenType.Star) -> BinaryOperator.Multiply @@ -1386,11 +1428,30 @@ class Parser(private val tokens: List) { else -> break } advance() - left = BinaryExpression(operator, left, parseUnary()) + left = BinaryExpression(operator, left, parseCollate()) } return left } + /** + * `expr COLLATE name` - sits between unary and multiplicative in precedence, matching + * SQLite's own grammar (binds tighter than every arithmetic/comparison operator, but looser + * than unary `-`/`NOT`, so `-x COLLATE NOCASE` is `(-x) COLLATE NOCASE` and + * `a || b COLLATE NOCASE` would apply to `b` alone). Left-associative chaining + * (`x COLLATE A COLLATE B`) is accepted like SQLite accepts it - only the last one written + * has any effect, since each wraps the previous [CollateExpression] as its own operand. + */ + private fun parseCollate(): Expression { + var expression = parseUnary() + while (match(TokenType.Collate)) { + val nameToken = expect(TokenType.Identifier, "expected a collation name after COLLATE") + val collation = Collation.fromName(nameToken.lexeme) + ?: throw error("no such collating sequence: ${nameToken.lexeme}", nameToken) + expression = CollateExpression(expression, collation) + } + return expression + } + private fun parseUnary(): Expression { if (match(TokenType.Minus)) { return UnaryExpression(UnaryOperator.Negate, parseUnary()) @@ -1419,6 +1480,27 @@ class Parser(private val tokens: List) { TokenType.Null -> { advance(); LiteralExpression(Value.NullValue) } + TokenType.Parameter -> { + advance() + val index = token.intValue!! + if (index < 1) throw error("parameter index must be >= 1 but was $index", token) + ParameterExpression(index.toInt()) + } + TokenType.Case -> parseCase() + TokenType.Exists -> parseExists() + TokenType.Cast -> parseCast() + // LEFT/RIGHT are otherwise reserved (LEFT/RIGHT JOIN), but LEFT(str, n)/RIGHT(str, n) are + // real, commonly expected scalar functions - only valid here as a call, never as a bare + // column reference, so a missing '(' is unconditionally an error rather than falling back + // to ColumnExpression like the TokenType.Identifier branch below does. + TokenType.Left -> { + advance() + if (check(TokenType.LeftParen)) parseFunctionCall(token.lexeme) else throw error("expected '(' after LEFT", peek()) + } + TokenType.Right -> { + advance() + if (check(TokenType.LeftParen)) parseFunctionCall(token.lexeme) else throw error("expected '(' after RIGHT", peek()) + } TokenType.Identifier -> { advance() when { @@ -1429,19 +1511,71 @@ class Parser(private val tokens: List) { } TokenType.LeftParen -> { advance() - val expression = parseExpression() - expect(TokenType.RightParen, "expected ')' after expression") - expression + if (check(TokenType.Select)) { + val select = parseSelect() + expect(TokenType.RightParen, "expected ')' after subquery") + ScalarSubqueryExpression(select) + } else { + val expression = parseExpression() + expect(TokenType.RightParen, "expected ')' after expression") + expression + } } else -> throw error("expected an expression but found '${describe(token)}'", token) } } + private fun parseCase(): Expression { + expect(TokenType.Case, "expected CASE") + val operand = if (!check(TokenType.When)) parseExpression() else null + val branches = mutableListOf() + while (match(TokenType.When)) { + val condition = parseExpression() + expect(TokenType.Then, "expected THEN after CASE condition") + val result = parseExpression() + branches.add(CaseBranch(condition, result)) + } + if (branches.isEmpty()) throw error("expected at least one WHEN clause in CASE expression", peek()) + val elseExpression = if (match(TokenType.Else)) parseExpression() else null + expect(TokenType.End, "expected END to close CASE expression") + return CaseExpression(operand, branches, elseExpression) + } + + private fun parseCast(): Expression { + expect(TokenType.Cast, "expected CAST") + expect(TokenType.LeftParen, "expected '(' after CAST") + val operand = parseExpression() + expect(TokenType.As, "expected AS in CAST") + val spec = parseTypeSpec() + expect(TokenType.RightParen, "expected ')' after CAST target type") + return CastExpression(operand, spec.type, spec.precision, spec.scale) + } + + private fun parseExists(): Expression { + expect(TokenType.Exists, "expected EXISTS") + expect(TokenType.LeftParen, "expected '(' after EXISTS") + val select = parseSelect() + expect(TokenType.RightParen, "expected ')' after EXISTS subquery") + return ExistsExpression(select) + } + private fun parseFunctionCall(name: String): Expression { expect(TokenType.LeftParen, "expected '(' after function name") - if (match(TokenType.Star)) { - expect(TokenType.RightParen, "expected ')' after '*'") - return FunctionExpression(name, argument = null, star = true) + val upperName = name.uppercase() + if (upperName == "STRING_AGG") return parseStringAggCall(name) + val isAggregate = upperName in AGGREGATE_FUNCTIONS + var star = false + var distinct = false + val arguments = mutableListOf() + if (isAggregate && match(TokenType.Distinct)) { + distinct = true + arguments.add(parseExpression()) + } else if (isAggregate && match(TokenType.Star)) { + star = true + } else if (!check(TokenType.RightParen)) { + do { + arguments.add(parseExpression()) + } while (match(TokenType.Comma)) } val argument = parseExpression() expect(TokenType.RightParen, "expected ')' after function argument")