test: cover mergeWithNoWhenClauseFailsToParse

This commit is contained in:
dualfroz committed 2026-09-09 16:39:47 +02:00
1 parent 742eeef126
commit 40197f5dc9
1 file changed
+173
+173
View File
@@ -938,4 +938,177 @@ class ParserTest {
) as MergeStatement
assertEquals(3, statement.whenClauses.size)
assertTrue(statement.whenClauses[0] is MergeMatchedDelete)
assertTrue(statement.whenClauses[1] is MergeMatchedUpdate)
assertTrue(statement.whenClauses[2] is MergeNotMatchedInsert)
}
@Test
fun mergeWithNoWhenClauseFailsToParse() {
assertFailsWith<ParseException> { parseOne("MERGE INTO t USING s ON t.id = s.id") }
}
@Test
fun mergeWhenMatchedRequiresUpdateOrDelete() {
assertFailsWith<ParseException> { parseOne("MERGE INTO t USING s ON t.id = s.id WHEN MATCHED THEN INSERT (id) VALUES (s.id)") }
}
@Test
fun mergeWhenNotMatchedOnlySupportsInsert() {
assertFailsWith<ParseException> { parseOne("MERGE INTO t USING s ON t.id = s.id WHEN NOT MATCHED THEN DELETE") }
}
@Test
fun mergeWithoutReturningLeavesItNull() {
val statement = parseOne("MERGE INTO t USING s ON t.id = s.id WHEN MATCHED THEN DELETE") as MergeStatement
assertNull(statement.returning)
}
@Test
fun mergeReturningStarParsesAsStarProjectionAfterTheLastWhenClause() {
val statement = parseOne(
"MERGE INTO t USING s ON t.id = s.id WHEN MATCHED THEN DELETE RETURNING *"
) as MergeStatement
assertEquals(StarProjection, statement.returning)
}
@Test
fun mergeReturningExpressionListParsesLikeASelectProjection() {
val statement = parseOne(
"MERGE INTO t USING s ON t.id = s.id WHEN MATCHED THEN DELETE RETURNING id, MERGE_ACTION() AS kind"
) as MergeStatement
val projection = statement.returning as ExpressionProjection
assertEquals(2, projection.items.size)
assertEquals("id", (projection.items[0].expression as ColumnExpression).name)
assertNull(projection.items[0].alias)
assertEquals(MergeActionExpression, projection.items[1].expression)
assertEquals("kind", projection.items[1].alias)
}
@Test
fun mergeActionParsesAsItsOwnExpressionNode() {
val select = parseOne("SELECT MERGE_ACTION() FROM t") as SelectStatement
val items = (select.projection as ExpressionProjection).items
assertEquals(MergeActionExpression, items.single().expression)
}
@Test
fun mergeActionTakesNoArguments() {
assertFailsWith<ParseException> { parse("SELECT MERGE_ACTION(1)") }
assertFailsWith<ParseException> { parse("SELECT MERGE_ACTION('x')") }
}
@Test
fun parsesDecimalColumnWithAndWithoutPrecisionAndScale() {
val statement = parseOne(
"CREATE TABLE t (bare DECIMAL, scaled DECIMAL(10), precise DECIMAL(10, 2))"
) as CreateTableStatement
assertEquals(ColumnType.Decimal, statement.columns[0].type)
assertNull(statement.columns[0].precision)
assertNull(statement.columns[0].scale)
assertEquals(10, statement.columns[1].precision)
assertEquals(0, statement.columns[1].scale)
assertEquals(10, statement.columns[2].precision)
assertEquals(2, statement.columns[2].scale)
}
@Test
fun parsesDateColumn() {
val statement = parseOne("CREATE TABLE t (born DATE)") as CreateTableStatement
assertEquals(ColumnType.Date, statement.columns[0].type)
}
@Test
fun parsesCastExpressionWithAndWithoutPrecisionAndScale() {
val plain = firstProjection("SELECT CAST(a AS INTEGER) FROM t") as CastExpression
assertEquals(ColumnType.Integer, plain.targetType)
assertNull(plain.precision)
val decimal = firstProjection("SELECT CAST(a AS DECIMAL(10, 2)) FROM t") as CastExpression
assertEquals(ColumnType.Decimal, decimal.targetType)
assertEquals(10, decimal.precision)
assertEquals(2, decimal.scale)
}
@Test
fun parsesCollateExpressionWithEachBuiltInSequenceCaseInsensitively() {
val nocase = firstProjection("SELECT name COLLATE NOCASE FROM t") as CollateExpression
assertEquals(Collation.NoCase, nocase.collation)
assertEquals(ColumnExpression("name"), nocase.operand)
val rtrim = firstProjection("SELECT name COLLATE rtrim FROM t") as CollateExpression
assertEquals(Collation.Rtrim, rtrim.collation)
val binary = firstProjection("SELECT name COLLATE BINARY FROM t") as CollateExpression
assertEquals(Collation.Binary, binary.collation)
}
@Test
fun collateWithAnUnknownSequenceNameFailsToParse() {
assertFailsWith<ParseException> { parse("SELECT name COLLATE BOGUS FROM t") }
}
@Test
fun collateWithoutAFollowingNameFailsToParse() {
assertFailsWith<ParseException> { parse("SELECT name COLLATE FROM t") }
}
@Test
fun collateBindsTighterThanComparisonSoItWrapsOnlyTheColumnItFollows() {
val where = whereOf("SELECT 1 FROM t WHERE name COLLATE NOCASE = 'bob'")
val comparison = where as BinaryExpression
assertEquals(BinaryOperator.Equal, comparison.operator)
val left = comparison.left as CollateExpression
assertEquals(Collation.NoCase, left.collation)
assertEquals(ColumnExpression("name"), left.operand)
assertEquals(LiteralExpression(Value.TextValue("bob")), comparison.right)
}
@Test
fun collateBindsTighterThanMultiplicationSoItWrapsOnlyItsOwnOperand() {
val projection = firstProjection("SELECT a * b COLLATE NOCASE FROM t") as BinaryExpression
assertEquals(BinaryOperator.Multiply, projection.operator)
assertEquals(ColumnExpression("a"), projection.left)
val right = projection.right as CollateExpression
assertEquals(Collation.NoCase, right.collation)
assertEquals(ColumnExpression("b"), right.operand)
}
@Test
fun collateBindsLooserThanUnaryMinusSoItWrapsTheWholeNegation() {
val projection = firstProjection("SELECT -a COLLATE NOCASE FROM t") as CollateExpression
assertEquals(Collation.NoCase, projection.collation)
val negated = projection.operand as UnaryExpression
assertEquals(UnaryOperator.Negate, negated.operator)
assertEquals(ColumnExpression("a"), negated.operand)
}
@Test
fun chainedCollateWrapsEachPreviousCollateExpression() {
val projection = firstProjection("SELECT a COLLATE BINARY COLLATE NOCASE FROM t") as CollateExpression
assertEquals(Collation.NoCase, projection.collation)
val inner = projection.operand as CollateExpression
assertEquals(Collation.Binary, inner.collation)
assertEquals(ColumnExpression("a"), inner.operand)
}
@Test
fun parsesColumnLevelCollateInCreateTable() {
val statement = parseOne("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT COLLATE NOCASE, other TEXT)") as CreateTableStatement
assertEquals(Collation.NoCase, statement.columns[1].collation)
assertEquals(Collation.Binary, statement.columns[0].collation)
assertEquals(Collation.Binary, statement.columns[2].collation)
}
/**
* COLLATE before DEFAULT is unambiguous: the column-definition loop consumes it as its own
* clause before ever calling [Parser.parseExpression] for DEFAULT's value. The other order
* (`DEFAULT 'x' COLLATE NOCASE`) is deliberately not covered here - `parseExpression` itself
* accepts a trailing `COLLATE` (see `parseCollate`), so it greedily becomes part of the DEFAULT
* expression instead (`DEFAULT ('x' COLLATE NOCASE)`), not a separate column-level clause; not a
* bug, just the same expression-grammar greediness this column-level COLLATE reuses.
*/
@Test
fun columnLevelCollateBeforeDefaultAttachesToTheColumnNotTheDefaultExpression() {
val statement = parseOne("CREATE TABLE t (name TEXT COLLATE NOCASE DEFAULT 'x')") as CreateTableStatement
assertEquals(Collation.NoCase, statement.columns[0].collation)
}