test: cover lexer and parser

This commit is contained in:
dualfroz committed 2026-08-27 14:12:19 +02:00
1 parent f4f6f76716
commit 7ffbc94191
2 files changed
+278

No files matched your search

+108
View File
@@ -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 @") }
}
}
+170
View File
@@ -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") }
}
}