diff --git a/src/test/kotlin/kestrel/ParserTest.kt b/src/test/kotlin/kestrel/ParserTest.kt index 390a9bd..f4c55de 100644 --- a/src/test/kotlin/kestrel/ParserTest.kt +++ b/src/test/kotlin/kestrel/ParserTest.kt @@ -592,4 +592,177 @@ class ParserTest { @Test fun filterColumnNamedFilterIsStillUsableAsAPlainIdentifier() { + val expression = firstProjection("SELECT filter FROM t") as ColumnExpression + assertEquals("filter", expression.name) + } + + // LEFT/RIGHT are otherwise reserved (LEFT/RIGHT JOIN), so LEFT(str, n)/RIGHT(str, n) as scalar + // functions each need their own disambiguation in the parser. + @Test + fun leftParsesAsAScalarFunctionCallDespiteAlsoBeingTheLeftJoinKeyword() { + val expression = firstProjection("SELECT LEFT(name, 3) FROM t") + assertEquals(CallExpression("LEFT", listOf(ColumnExpression("name"), intLit(3))), expression) + } + + @Test + fun rightParsesAsAScalarFunctionCallDespiteAlsoBeingTheRightJoinKeyword() { + val expression = firstProjection("SELECT RIGHT(name, 3) FROM t") + assertEquals(CallExpression("RIGHT", listOf(ColumnExpression("name"), intLit(3))), expression) + } + + @Test + fun leftJoinStillParsesAsAJoinNotAFunctionCall() { + val statement = parseOne("SELECT * FROM t LEFT JOIN u ON t.id = u.id") as SelectStatement + assertEquals(1, statement.joins.size) + assertEquals(JoinType.Left, statement.joins[0].type) + } + + @Test + fun bareLeftKeywordWithoutAFollowingParenIsAParseError() { + assertFailsWith { parse("SELECT LEFT FROM t") } + } + + @Test + fun bareRightKeywordWithoutAFollowingParenIsAParseError() { + assertFailsWith { parse("SELECT RIGHT FROM t") } + } + + @Test + fun rightJoinAndFullJoinParseWithOptionalOuter() { + val right = parseOne("SELECT * FROM t RIGHT JOIN u ON t.id = u.id") as SelectStatement + assertEquals(JoinType.Right, right.joins[0].type) + val rightOuter = parseOne("SELECT * FROM t RIGHT OUTER JOIN u ON t.id = u.id") as SelectStatement + assertEquals(JoinType.Right, rightOuter.joins[0].type) + val full = parseOne("SELECT * FROM t FULL JOIN u ON t.id = u.id") as SelectStatement + assertEquals(JoinType.Full, full.joins[0].type) + val fullOuter = parseOne("SELECT * FROM t FULL OUTER JOIN u ON t.id = u.id") as SelectStatement + assertEquals(JoinType.Full, fullOuter.joins[0].type) + } + + @Test + fun rightAndFullJoinLateralAreRejected() { + assertFailsWith { + parse("SELECT * FROM t RIGHT JOIN LATERAL (SELECT * FROM u WHERE u.id = t.id) x ON true") + } + assertFailsWith { + parse("SELECT * FROM t FULL JOIN LATERAL (SELECT * FROM u WHERE u.id = t.id) x ON true") + } + } + + @Test + fun crossJoinParsesWithoutAnOnClauseAndOnIsNull() { + val statement = parseOne("SELECT * FROM t CROSS JOIN u") as SelectStatement + assertEquals(JoinType.Cross, statement.joins[0].type) + assertEquals(null, statement.joins[0].on) + } + + @Test + fun crossJoinWithAnExplicitOnClauseIsAParseError() { + assertFailsWith { parse("SELECT * FROM t CROSS JOIN u ON t.id = u.id") } + } + + @Test + fun crossJoinLateralIsAccepted() { + val statement = parseOne( + "SELECT * FROM t CROSS JOIN LATERAL (SELECT * FROM u WHERE u.id = t.id) x" + ) as SelectStatement + assertEquals(JoinType.Cross, statement.joins[0].type) + assertEquals(null, statement.joins[0].on) + } + + @Test + fun parsesIsNullAndIsNotNull() { + assertEquals(IsNullExpression(ColumnExpression("age"), negated = false), whereOf("SELECT 1 FROM t WHERE age IS NULL")) + assertEquals( + IsNullExpression(ColumnExpression("age"), negated = true), + whereOf("SELECT 1 FROM t WHERE age IS NOT NULL") + ) + } + + @Test + fun parsesInAndNotIn() { + assertEquals( + InExpression(ColumnExpression("id"), listOf(intLit(1), intLit(2), intLit(3)), negated = false), + whereOf("SELECT 1 FROM t WHERE id IN (1, 2, 3)") + ) + assertEquals( + InExpression(ColumnExpression("id"), listOf(intLit(1)), negated = true), + whereOf("SELECT 1 FROM t WHERE id NOT IN (1)") + ) + } + + @Test + fun parsesBetweenAndNotBetween() { + assertEquals( + BetweenExpression(ColumnExpression("age"), intLit(18), intLit(65), negated = false), + whereOf("SELECT 1 FROM t WHERE age BETWEEN 18 AND 65") + ) + assertEquals( + BetweenExpression(ColumnExpression("age"), intLit(18), intLit(65), negated = true), + whereOf("SELECT 1 FROM t WHERE age NOT BETWEEN 18 AND 65") + ) + } + + @Test + fun parsesLikeAndNotLike() { + assertEquals( + LikeExpression(ColumnExpression("name"), LiteralExpression(Value.TextValue("a%")), negated = false), + whereOf("SELECT 1 FROM t WHERE name LIKE 'a%'") + ) + assertEquals( + LikeExpression(ColumnExpression("name"), LiteralExpression(Value.TextValue("a%")), negated = true), + whereOf("SELECT 1 FROM t WHERE name NOT LIKE 'a%'") + ) + } + + @Test + fun notInWithoutInOrBetweenOrLikeFails() { + assertFailsWith { parse("SELECT 1 FROM t WHERE id NOT = 1") } + } + + @Test + fun parsesSearchedCaseExpression() { + val expression = firstProjection( + "SELECT CASE WHEN age < 18 THEN 'minor' WHEN age < 65 THEN 'adult' ELSE 'senior' END FROM t" + ) as CaseExpression + assertNull(expression.operand) + assertEquals(2, expression.branches.size) + assertEquals(LiteralExpression(Value.TextValue("senior")), expression.elseExpression) + } + + @Test + fun parsesSimpleCaseExpressionWithoutElse() { + val expression = firstProjection("SELECT CASE grade WHEN 1 THEN 'a' END FROM t") as CaseExpression + assertEquals(ColumnExpression("grade"), expression.operand) + assertEquals(1, expression.branches.size) + assertNull(expression.elseExpression) + } + + @Test + fun caseWithoutWhenClauseFails() { + assertFailsWith { parse("SELECT CASE WHEN THEN 1 END FROM t") } + assertFailsWith { parse("SELECT CASE ELSE 1 END FROM t") } + } + + @Test + fun caseMissingEndFails() { + assertFailsWith { parse("SELECT CASE WHEN 1 THEN 2 FROM t") } + } + + @Test + fun parsesScalarSubqueryInWhereClause() { + val expression = whereOf("SELECT 1 FROM t WHERE age > (SELECT AVG(age) FROM t)") as BinaryExpression + val subquery = expression.right as ScalarSubqueryExpression + assertEquals("t", tableName(subquery.select)) + assertEquals(BinaryOperator.Greater, expression.operator) + } + + @Test + fun parsesInSubquery() { + val expression = whereOf("SELECT 1 FROM t WHERE id IN (SELECT id FROM other)") as InSubqueryExpression + assertEquals(ColumnExpression("id"), expression.operand) + assertEquals("other", tableName(expression.select)) + assertEquals(false, expression.negated) + } + }