test: cover lexer and parser
This commit is contained in:
1 parent
f4f6f76716
commit
7ffbc94191
2 files changed
+278
No files matched your search
@@ -0,0 +1,108 @@
|
|||||||
|
package kestrel
|
||||||
|
|
||||||
|
import kestrel.sql.lexer.Lexer
|
||||||
|
import kestrel.sql.lexer.TokenType
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertFailsWith
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
class LexerTest {
|
||||||
|
private fun lex(sql: String) = Lexer(sql).tokenize()
|
||||||
|
|
||||||
|
private fun types(sql: String) = lex(sql).map { it.type }
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun keywordsAreCaseInsensitive() {
|
||||||
|
assertEquals(
|
||||||
|
listOf(TokenType.Select, TokenType.Select, TokenType.Select, TokenType.EndOfInput),
|
||||||
|
types("SELECT select SeLeCt")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun identifiersAreDistinctFromKeywords() {
|
||||||
|
val tokens = lex("users user_name")
|
||||||
|
assertEquals(TokenType.Identifier, tokens[0].type)
|
||||||
|
assertEquals("users", tokens[0].lexeme)
|
||||||
|
assertEquals(TokenType.Identifier, tokens[1].type)
|
||||||
|
assertEquals("user_name", tokens[1].lexeme)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun integerLiteralCarriesValue() {
|
||||||
|
val token = lex("42").first()
|
||||||
|
assertEquals(TokenType.IntLiteral, token.type)
|
||||||
|
assertEquals(42L, token.intValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun realLiteralCarriesValue() {
|
||||||
|
val token = lex("3.14").first()
|
||||||
|
assertEquals(TokenType.RealLiteral, token.type)
|
||||||
|
assertEquals(3.14, token.realValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun trailingDotIsNotPartOfNumber() {
|
||||||
|
assertEquals(
|
||||||
|
listOf(TokenType.IntLiteral, TokenType.EndOfInput),
|
||||||
|
types("7")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun stringLiteralHandlesDoubledQuoteEscape() {
|
||||||
|
val token = lex("'it''s ada'").first()
|
||||||
|
assertEquals(TokenType.TextLiteral, token.type)
|
||||||
|
assertEquals("it's ada", token.textValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun comparisonOperatorsAreTwoCharacterAware() {
|
||||||
|
assertEquals(
|
||||||
|
listOf(
|
||||||
|
TokenType.LessEqual, TokenType.NotEqual, TokenType.GreaterEqual,
|
||||||
|
TokenType.NotEqual, TokenType.Less, TokenType.Greater, TokenType.Equal,
|
||||||
|
TokenType.EndOfInput
|
||||||
|
),
|
||||||
|
types("<= <> >= != < > =")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun lineCommentsAreIgnored() {
|
||||||
|
assertEquals(
|
||||||
|
listOf(TokenType.Select, TokenType.Star, TokenType.EndOfInput),
|
||||||
|
types("SELECT -- pick everything\n *")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun blockCommentsAreIgnored() {
|
||||||
|
assertEquals(
|
||||||
|
listOf(TokenType.Select, TokenType.Star, TokenType.EndOfInput),
|
||||||
|
types("SELECT /* inline\n comment */ *")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun positionsTrackLineAndColumn() {
|
||||||
|
val tokens = lex("SELECT\n age")
|
||||||
|
val ageToken = tokens[1]
|
||||||
|
assertEquals("age", ageToken.lexeme)
|
||||||
|
assertEquals(2, ageToken.line)
|
||||||
|
assertEquals(3, ageToken.column)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun unterminatedStringReportsIncomplete() {
|
||||||
|
val exception = assertFailsWith<LexException> { lex("'open") }
|
||||||
|
assertTrue(exception.incomplete)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun unexpectedCharacterFails() {
|
||||||
|
assertFailsWith<LexException> { lex("SELECT @") }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
package kestrel
|
||||||
|
|
||||||
|
import kestrel.sql.ast.Assignment
|
||||||
|
import kestrel.sql.ast.BinaryExpression
|
||||||
|
import kestrel.sql.ast.BinaryOperator
|
||||||
|
import kestrel.sql.ast.ColumnExpression
|
||||||
|
import kestrel.sql.ast.CreateTableStatement
|
||||||
|
import kestrel.sql.ast.DeleteStatement
|
||||||
|
import kestrel.sql.ast.DropTableStatement
|
||||||
|
import kestrel.sql.ast.Expression
|
||||||
|
import kestrel.sql.ast.ExpressionProjection
|
||||||
|
import kestrel.sql.ast.InsertStatement
|
||||||
|
import kestrel.sql.ast.LiteralExpression
|
||||||
|
import kestrel.sql.ast.SelectStatement
|
||||||
|
import kestrel.sql.ast.StarProjection
|
||||||
|
import kestrel.sql.ast.Statement
|
||||||
|
import kestrel.sql.ast.UnaryExpression
|
||||||
|
import kestrel.sql.ast.UnaryOperator
|
||||||
|
import kestrel.sql.ast.UpdateStatement
|
||||||
|
import kestrel.sql.lexer.Lexer
|
||||||
|
import kestrel.sql.parser.Parser
|
||||||
|
import kestrel.storage.ColumnType
|
||||||
|
import kestrel.storage.Value
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertFailsWith
|
||||||
|
import kotlin.test.assertNull
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
class ParserTest {
|
||||||
|
private fun parse(sql: String): List<Statement> = Parser(Lexer(sql).tokenize()).parseProgram()
|
||||||
|
|
||||||
|
private fun parseOne(sql: String): Statement = parse(sql).single()
|
||||||
|
|
||||||
|
private fun intLit(value: Long) = LiteralExpression(Value.IntegerValue(value))
|
||||||
|
|
||||||
|
private fun whereOf(sql: String): Expression = (parseOne(sql) as SelectStatement).where!!
|
||||||
|
|
||||||
|
private fun firstProjection(sql: String): Expression =
|
||||||
|
((parseOne(sql) as SelectStatement).projection as ExpressionProjection).items.first().expression
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun parsesCreateTableWithConstraints() {
|
||||||
|
val statement = parseOne("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL, age INTEGER)")
|
||||||
|
assertTrue(statement is CreateTableStatement)
|
||||||
|
assertEquals("users", statement.tableName)
|
||||||
|
assertEquals(3, statement.columns.size)
|
||||||
|
val id = statement.columns[0]
|
||||||
|
assertEquals(ColumnType.Integer, id.type)
|
||||||
|
assertTrue(id.primaryKey)
|
||||||
|
assertTrue(id.notNull)
|
||||||
|
val name = statement.columns[1]
|
||||||
|
assertTrue(name.notNull)
|
||||||
|
assertTrue(!name.primaryKey)
|
||||||
|
assertTrue(!statement.columns[2].notNull)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun parsesInsertWithoutColumnList() {
|
||||||
|
val statement = parseOne("INSERT INTO users VALUES (1, 'ada', 36)")
|
||||||
|
assertTrue(statement is InsertStatement)
|
||||||
|
assertNull(statement.columns)
|
||||||
|
assertEquals(1, statement.rows.size)
|
||||||
|
assertEquals(3, statement.rows[0].size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun parsesInsertWithColumnListAndMultipleRows() {
|
||||||
|
val statement = parseOne("INSERT INTO t (a, b) VALUES (1, 2), (3, 4), (5, 6)") as InsertStatement
|
||||||
|
assertEquals(listOf("a", "b"), statement.columns)
|
||||||
|
assertEquals(3, statement.rows.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun parsesSelectStar() {
|
||||||
|
val statement = parseOne("SELECT * FROM users") as SelectStatement
|
||||||
|
assertEquals(StarProjection, statement.projection)
|
||||||
|
assertEquals("users", statement.tableName)
|
||||||
|
assertNull(statement.where)
|
||||||
|
assertNull(statement.orderBy)
|
||||||
|
assertNull(statement.limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun parsesSelectWithAliasWhereOrderLimit() {
|
||||||
|
val statement = parseOne(
|
||||||
|
"SELECT name AS who, age FROM users WHERE age > 40 ORDER BY age DESC LIMIT 5"
|
||||||
|
) as SelectStatement
|
||||||
|
val projection = statement.projection as ExpressionProjection
|
||||||
|
assertEquals("who", projection.items[0].alias)
|
||||||
|
assertEquals(true, statement.orderBy!!.descending)
|
||||||
|
assertEquals(5, statement.limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun parsesDeleteAndDropAndUpdate() {
|
||||||
|
assertTrue(parseOne("DELETE FROM t WHERE id = 1") is DeleteStatement)
|
||||||
|
assertTrue(parseOne("DROP TABLE t") is DropTableStatement)
|
||||||
|
val update = parseOne("UPDATE t SET a = 1, b = a + 2 WHERE id = 3") as UpdateStatement
|
||||||
|
assertEquals(2, update.assignments.size)
|
||||||
|
assertEquals(Assignment("a", intLit(1)), update.assignments[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun multipleStatementsAreSeparatedBySemicolons() {
|
||||||
|
val statements = parse("CREATE TABLE t (a INTEGER); DROP TABLE t;")
|
||||||
|
assertEquals(2, statements.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun arithmeticHonoursPrecedence() {
|
||||||
|
val expression = firstProjection("SELECT 1 + 2 * 3 FROM t")
|
||||||
|
val expected = BinaryExpression(
|
||||||
|
BinaryOperator.Add,
|
||||||
|
intLit(1),
|
||||||
|
BinaryExpression(BinaryOperator.Multiply, intLit(2), intLit(3))
|
||||||
|
)
|
||||||
|
assertEquals(expected, expression)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun notBindsTighterThanAnd() {
|
||||||
|
val expression = whereOf("SELECT 1 FROM t WHERE NOT a AND b")
|
||||||
|
val expected = BinaryExpression(
|
||||||
|
BinaryOperator.And,
|
||||||
|
UnaryExpression(UnaryOperator.Not, ColumnExpression("a")),
|
||||||
|
ColumnExpression("b")
|
||||||
|
)
|
||||||
|
assertEquals(expected, expression)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun andBindsTighterThanOr() {
|
||||||
|
val expression = whereOf("SELECT 1 FROM t WHERE a OR b AND c")
|
||||||
|
val expected = BinaryExpression(
|
||||||
|
BinaryOperator.Or,
|
||||||
|
ColumnExpression("a"),
|
||||||
|
BinaryExpression(BinaryOperator.And, ColumnExpression("b"), ColumnExpression("c"))
|
||||||
|
)
|
||||||
|
assertEquals(expected, expression)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun parenthesesOverridePrecedence() {
|
||||||
|
val expression = firstProjection("SELECT (1 + 2) * 3 FROM t")
|
||||||
|
val expected = BinaryExpression(
|
||||||
|
BinaryOperator.Multiply,
|
||||||
|
BinaryExpression(BinaryOperator.Add, intLit(1), intLit(2)),
|
||||||
|
intLit(3)
|
||||||
|
)
|
||||||
|
assertEquals(expected, expression)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun reportsMissingProjectionWithPosition() {
|
||||||
|
val exception = assertFailsWith<ParseException> { parse("SELECT FROM t") }
|
||||||
|
assertEquals(1, exception.line)
|
||||||
|
assertEquals(8, exception.column)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun reportsMissingClosingParen() {
|
||||||
|
assertFailsWith<ParseException> { parse("CREATE TABLE t (a INTEGER") }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun reportsUnknownStatement() {
|
||||||
|
assertFailsWith<ParseException> { parse("EXPLAIN users") }
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user