diff --git a/src/test/kotlin/kestrel/ParserTest.kt b/src/test/kotlin/kestrel/ParserTest.kt index 7771760..d0cc5cb 100644 --- a/src/test/kotlin/kestrel/ParserTest.kt +++ b/src/test/kotlin/kestrel/ParserTest.kt @@ -1,29 +1,91 @@ package kestrel +import kestrel.catalog.ForeignKeyAction +import kestrel.sql.ast.AddColumn +import kestrel.sql.ast.AlterSequenceStatement +import kestrel.sql.ast.AlterTableStatement import kestrel.sql.ast.Assignment +import kestrel.sql.ast.BetweenExpression import kestrel.sql.ast.BinaryExpression import kestrel.sql.ast.BinaryOperator +import kestrel.sql.ast.CallExpression +import kestrel.sql.ast.CaseExpression +import kestrel.sql.ast.CastExpression +import kestrel.sql.ast.ChangeColumnType +import kestrel.sql.ast.CollateExpression import kestrel.sql.ast.ColumnExpression +import kestrel.sql.ast.CreateIndexStatement +import kestrel.sql.ast.CreateSequenceStatement import kestrel.sql.ast.CreateTableStatement +import kestrel.sql.ast.CreateTriggerStatement +import kestrel.sql.ast.CreateViewStatement +import kestrel.sql.ast.CubeSpec +import kestrel.sql.ast.CurrentValueExpression +import kestrel.sql.ast.DeallocateStatement import kestrel.sql.ast.DeleteStatement +import kestrel.sql.ast.DropColumn +import kestrel.sql.ast.DropMode +import kestrel.sql.ast.DropSequenceStatement import kestrel.sql.ast.DropTableStatement +import kestrel.sql.ast.DropTriggerStatement +import kestrel.sql.ast.DropViewStatement +import kestrel.sql.ast.ExecuteStatement +import kestrel.sql.ast.ExistsExpression +import kestrel.sql.ast.ExplainStatement import kestrel.sql.ast.Expression import kestrel.sql.ast.ExpressionProjection +import kestrel.sql.ast.FrameBoundType +import kestrel.sql.ast.FrameExclude +import kestrel.sql.ast.FrameUnit +import kestrel.sql.ast.FunctionExpression +import kestrel.sql.ast.GroupingSetsSpec +import kestrel.sql.ast.InExpression +import kestrel.sql.ast.InSubqueryExpression import kestrel.sql.ast.InsertStatement +import kestrel.sql.ast.IsNullExpression +import kestrel.sql.ast.JoinType +import kestrel.sql.ast.LikeExpression import kestrel.sql.ast.LiteralExpression +import kestrel.sql.ast.MergeActionExpression +import kestrel.sql.ast.MergeMatchedDelete +import kestrel.sql.ast.MergeMatchedUpdate +import kestrel.sql.ast.MergeNotMatchedInsert +import kestrel.sql.ast.MergeStatement +import kestrel.sql.ast.NextValueExpression +import kestrel.sql.ast.OrderKey +import kestrel.sql.ast.ParameterExpression +import kestrel.sql.ast.PrepareStatement +import kestrel.sql.ast.QueryStatement +import kestrel.sql.ast.RenameColumn +import kestrel.sql.ast.RenameTable +import kestrel.sql.ast.RollupSpec +import kestrel.sql.ast.ScalarSubqueryExpression import kestrel.sql.ast.SelectStatement +import kestrel.sql.ast.SetOperationStatement +import kestrel.sql.ast.SetOperator import kestrel.sql.ast.StarProjection import kestrel.sql.ast.Statement +import kestrel.sql.ast.SubquerySource +import kestrel.sql.ast.TableSource +import kestrel.sql.ast.TriggerEvent +import kestrel.sql.ast.TriggerTiming +import kestrel.sql.ast.TruncateTableStatement import kestrel.sql.ast.UnaryExpression import kestrel.sql.ast.UnaryOperator import kestrel.sql.ast.UpdateStatement +import kestrel.sql.ast.WindowExpression +import kestrel.sql.ast.WindowFunctionKind +import kestrel.sql.ast.WithQuery import kestrel.sql.lexer.Lexer import kestrel.sql.parser.Parser +import kestrel.storage.Collation import kestrel.storage.ColumnType import kestrel.storage.Value import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertTrue @@ -36,6 +98,8 @@ class ParserTest { private fun whereOf(sql: String): Expression = (parseOne(sql) as SelectStatement).where!! + private fun tableName(select: QueryStatement): String = ((select as SelectStatement).from as TableSource).name + private fun firstProjection(sql: String): Expression = ((parseOne(sql) as SelectStatement).projection as ExpressionProjection).items.first().expression @@ -55,6 +119,38 @@ class ParserTest { assertTrue(!statement.columns[2].notNull) } + @Test + fun parsesCreateTableWithoutTempIsNotTemporary() { + val statement = parseOne("CREATE TABLE users (id INTEGER PRIMARY KEY)") as CreateTableStatement + assertTrue(!statement.temporary) + } + + @Test + fun parsesCreateTempTable() { + val statement = parseOne("CREATE TEMP TABLE scratch (id INTEGER PRIMARY KEY, note TEXT)") as CreateTableStatement + assertTrue(statement.temporary) + assertEquals("scratch", statement.tableName) + assertEquals(2, statement.columns.size) + } + + @Test + fun parsesCreateTemporaryTable() { + val statement = parseOne("CREATE TEMPORARY TABLE scratch (id INTEGER PRIMARY KEY)") as CreateTableStatement + assertTrue(statement.temporary) + } + + @Test + fun tempKeywordIsCaseInsensitive() { + val statement = parseOne("create temp table scratch (id integer primary key)") as CreateTableStatement + assertTrue(statement.temporary) + } + + @Test + fun tempOnAnythingOtherThanTableIsAParseError() { + assertFailsWith { parse("CREATE TEMP VIEW v AS SELECT 1") } + assertFailsWith { parse("CREATE TEMP INDEX idx ON t (a)") } + } + @Test fun parsesInsertWithoutColumnList() { val statement = parseOne("INSERT INTO users VALUES (1, 'ada', 36)") @@ -71,11 +167,20 @@ class ParserTest { assertEquals(3, statement.rows.size) } + @Test + fun parsesInsertValuesWithLiteralDefaultKeyword() { + val statement = parseOne("INSERT INTO t (a, b, c) VALUES (1, DEFAULT, 3)") as InsertStatement + assertEquals(3, statement.rows[0].size) + assertNotNull(statement.rows[0][0]) + assertNull(statement.rows[0][1]) + assertNotNull(statement.rows[0][2]) + } + @Test fun parsesSelectStar() { val statement = parseOne("SELECT * FROM users") as SelectStatement assertEquals(StarProjection, statement.projection) - assertEquals("users", statement.tableName) + assertEquals("users", tableName(statement)) assertNull(statement.where) assertNull(statement.orderBy) assertNull(statement.limit) @@ -88,10 +193,90 @@ class ParserTest { ) as SelectStatement val projection = statement.projection as ExpressionProjection assertEquals("who", projection.items[0].alias) - assertEquals(true, statement.orderBy!!.descending) + assertEquals(true, statement.orderBy!!.keys.single().descending) assertEquals(5, statement.limit) } + @Test + fun parsesLimitFollowedByOffset() { + val statement = parseOne("SELECT id FROM users LIMIT 10 OFFSET 20") as SelectStatement + assertEquals(10, statement.limit) + assertEquals(20, statement.offset) + } + + @Test + fun parsesOffsetFollowedByLimitInEitherOrder() { + val statement = parseOne("SELECT id FROM users OFFSET 20 LIMIT 10") as SelectStatement + assertEquals(10, statement.limit) + assertEquals(20, statement.offset) + } + + @Test + fun parsesOffsetAloneWithoutLimit() { + val statement = parseOne("SELECT id FROM users OFFSET 5") as SelectStatement + assertNull(statement.limit) + assertEquals(5, statement.offset) + } + + @Test + fun offsetDefaultsToNullWhenAbsent() { + val statement = parseOne("SELECT id FROM users LIMIT 5") as SelectStatement + assertNull(statement.offset) + } + + @Test + fun rejectsARepeatedLimitOrOffsetClause() { + assertFailsWith { parse("SELECT id FROM users LIMIT 5 LIMIT 10") } + assertFailsWith { parse("SELECT id FROM users OFFSET 5 OFFSET 10") } + } + + @Test + fun rejectsANonIntegerAfterOffset() { + assertFailsWith { parse("SELECT id FROM users OFFSET 'five'") } + } + + @Test + fun parsesOrderByWithMultipleKeysAndIndependentDirections() { + val statement = parseOne("SELECT name FROM users ORDER BY dept ASC, age DESC, id") as SelectStatement + val keys = statement.orderBy!!.keys + assertEquals(3, keys.size) + assertEquals("dept", (keys[0].expression as ColumnExpression).name) + assertEquals(false, keys[0].descending) + assertEquals("age", (keys[1].expression as ColumnExpression).name) + assertEquals(true, keys[1].descending) + assertEquals("id", (keys[2].expression as ColumnExpression).name) + assertEquals(false, keys[2].descending) + } + + @Test + fun parsesOrderByWithExplicitNullsFirstAndNullsLastPerKey() { + val statement = parseOne( + "SELECT name FROM users ORDER BY dept NULLS FIRST, age DESC NULLS LAST, id" + ) as SelectStatement + val keys = statement.orderBy!!.keys + assertEquals(3, keys.size) + assertEquals(false, keys[0].descending) + assertEquals(true, keys[0].nullsFirst) + assertEquals(true, keys[1].descending) + assertEquals(false, keys[1].nullsFirst) + assertEquals(false, keys[2].descending) + assertNull(keys[2].nullsFirst) + } + + @Test + fun nullsAndFirstAndLastRemainUsableAsOrdinaryColumnNamesOutsideTheNullsClause() { + val statement = parseOne("SELECT id FROM users ORDER BY nulls DESC") as SelectStatement + val key = statement.orderBy!!.keys.single() + assertEquals(ColumnExpression("nulls"), key.expression) + assertEquals(true, key.descending) + assertNull(key.nullsFirst) + } + + @Test + fun nullsClauseWithoutFirstOrLastFailsToParse() { + assertFailsWith { parseOne("SELECT id FROM users ORDER BY age NULLS") } + } + @Test fun parsesDeleteAndDropAndUpdate() { assertTrue(parseOne("DELETE FROM t WHERE id = 1") is DeleteStatement)