diff --git a/CHANGELOG.md b/CHANGELOG.md index 40679b1500..f878b93146 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ Thank you to all who have contributed! ### Changed - Change `StaticType.AnyOfType`'s `.toString` to not perform `.flatten()` +- Change modeling of `COALESCE` and `NULLIF` to dedicated nodes in logical plan ### Deprecated @@ -43,7 +44,7 @@ Thank you to all who have contributed! ### Contributors Thank you to all who have contributed! -- @ +- @alancai98 ## [0.14.4] diff --git a/partiql-plan/src/main/resources/partiql_plan.ion b/partiql-plan/src/main/resources/partiql_plan.ion index 1e970807f3..541da45af3 100644 --- a/partiql-plan/src/main/resources/partiql_plan.ion +++ b/partiql-plan/src/main/resources/partiql_plan.ion @@ -135,6 +135,15 @@ rex::{ ], }, + nullif::{ + v1: rex, + v2: rex + }, + + coalesce::{ + values: list::[rex] + }, + collection::{ values: list::[rex], }, diff --git a/partiql-planner/src/main/kotlin/org/partiql/planner/internal/ir/Nodes.kt b/partiql-planner/src/main/kotlin/org/partiql/planner/internal/ir/Nodes.kt index 0d444cb340..bd89080267 100644 --- a/partiql-planner/src/main/kotlin/org/partiql/planner/internal/ir/Nodes.kt +++ b/partiql-planner/src/main/kotlin/org/partiql/planner/internal/ir/Nodes.kt @@ -51,10 +51,12 @@ import org.partiql.planner.internal.ir.builder.RexOpCallDynamicCandidateBuilder import org.partiql.planner.internal.ir.builder.RexOpCallStaticBuilder import org.partiql.planner.internal.ir.builder.RexOpCaseBranchBuilder import org.partiql.planner.internal.ir.builder.RexOpCaseBuilder +import org.partiql.planner.internal.ir.builder.RexOpCoalesceBuilder import org.partiql.planner.internal.ir.builder.RexOpCollectionBuilder import org.partiql.planner.internal.ir.builder.RexOpErrBuilder import org.partiql.planner.internal.ir.builder.RexOpGlobalBuilder import org.partiql.planner.internal.ir.builder.RexOpLitBuilder +import org.partiql.planner.internal.ir.builder.RexOpNullifBuilder import org.partiql.planner.internal.ir.builder.RexOpPathIndexBuilder import org.partiql.planner.internal.ir.builder.RexOpPathKeyBuilder import org.partiql.planner.internal.ir.builder.RexOpPathSymbolBuilder @@ -312,6 +314,8 @@ internal data class Rex( is Path -> visitor.visitRexOpPath(this, ctx) is Call -> visitor.visitRexOpCall(this, ctx) is Case -> visitor.visitRexOpCase(this, ctx) + is Nullif -> visitor.visitRexOpNullif(this, ctx) + is Coalesce -> visitor.visitRexOpCoalesce(this, ctx) is Collection -> visitor.visitRexOpCollection(this, ctx) is Struct -> visitor.visitRexOpStruct(this, ctx) is Pivot -> visitor.visitRexOpPivot(this, ctx) @@ -567,6 +571,47 @@ internal data class Rex( } } + internal data class Nullif( + @JvmField + internal val v1: Rex, + @JvmField + internal val v2: Rex, + ) : Op() { + internal override val children: List by lazy { + val kids = mutableListOf() + kids.add(v1) + kids.add(v2) + kids.filterNotNull() + } + + internal override fun accept(visitor: PlanVisitor, ctx: C): R = + visitor.visitRexOpNullif(this, ctx) + + internal companion object { + @JvmStatic + internal fun builder(): RexOpNullifBuilder = RexOpNullifBuilder() + } + } + + internal data class Coalesce( + @JvmField + internal val values: List, + ) : Op() { + override val children: List by lazy { + val kids = mutableListOf() + kids.addAll(values) + kids.filterNotNull() + } + + override fun accept(visitor: PlanVisitor, ctx: C): R = + visitor.visitRexOpCoalesce(this, ctx) + + internal companion object { + @JvmStatic + internal fun builder(): RexOpCoalesceBuilder = RexOpCoalesceBuilder() + } + } + internal data class Collection( @JvmField internal val values: List, ) : Op() { diff --git a/partiql-planner/src/main/kotlin/org/partiql/planner/internal/transforms/PlanTransform.kt b/partiql-planner/src/main/kotlin/org/partiql/planner/internal/transforms/PlanTransform.kt index 89741aadb2..576d29f4c7 100644 --- a/partiql-planner/src/main/kotlin/org/partiql/planner/internal/transforms/PlanTransform.kt +++ b/partiql-planner/src/main/kotlin/org/partiql/planner/internal/transforms/PlanTransform.kt @@ -180,6 +180,17 @@ internal object PlanTransform : PlanBaseVisitor() { branches = node.branches.map { visitRexOpCaseBranch(it, ctx) }, default = visitRex(node.default, ctx) ) + override fun visitRexOpNullif(node: Rex.Op.Nullif, ctx: ProblemCallback) = + org.partiql.plan.Rex.Op.Nullif( + v1 = visitRex(node.v1, ctx), + v2 = visitRex(node.v2, ctx), + ) + + override fun visitRexOpCoalesce(node: Rex.Op.Coalesce, ctx: ProblemCallback) = + org.partiql.plan.Rex.Op.Coalesce( + values = node.values.map { visitRex(it, ctx) } + ) + override fun visitRexOpCaseBranch(node: Rex.Op.Case.Branch, ctx: ProblemCallback) = org.partiql.plan.Rex.Op.Case.Branch( condition = visitRex(node.condition, ctx), rex = visitRex(node.rex, ctx) diff --git a/partiql-planner/src/main/kotlin/org/partiql/planner/internal/transforms/RexConverter.kt b/partiql-planner/src/main/kotlin/org/partiql/planner/internal/transforms/RexConverter.kt index 498dea8a2c..d022346be3 100644 --- a/partiql-planner/src/main/kotlin/org/partiql/planner/internal/transforms/RexConverter.kt +++ b/partiql-planner/src/main/kotlin/org/partiql/planner/internal/transforms/RexConverter.kt @@ -30,8 +30,10 @@ import org.partiql.planner.internal.ir.identifierQualified import org.partiql.planner.internal.ir.identifierSymbol import org.partiql.planner.internal.ir.rex import org.partiql.planner.internal.ir.rexOpCallStatic +import org.partiql.planner.internal.ir.rexOpCoalesce import org.partiql.planner.internal.ir.rexOpCollection import org.partiql.planner.internal.ir.rexOpLit +import org.partiql.planner.internal.ir.rexOpNullif import org.partiql.planner.internal.ir.rexOpPathIndex import org.partiql.planner.internal.ir.rexOpPathKey import org.partiql.planner.internal.ir.rexOpPathSymbol @@ -107,7 +109,7 @@ internal object RexConverter { private fun visitExprCoerce(node: Expr, ctx: Env, coercion: Rex.Op.Subquery.Coercion = Rex.Op.Subquery.Coercion.SCALAR): Rex { val rex = super.visitExpr(node, ctx) return when (rex.op is Rex.Op.Select) { - true -> rex(StaticType.ANY, rexOpSubquery(rex.op as Rex.Op.Select, coercion)) + true -> rex(StaticType.ANY, rexOpSubquery(rex.op, coercion)) else -> rex } } @@ -439,44 +441,21 @@ internal object RexConverter { return rex(type, call) } - // coalesce(expr1, expr2, ... exprN) -> - // CASE - // WHEN expr1 IS NOT NULL THEN EXPR1 - // ... - // WHEN exprn is NOT NULL THEN exprn - // ELSE NULL END - override fun visitExprCoalesce(node: Expr.Coalesce, ctx: Env): Rex = plan { + override fun visitExprCoalesce(node: Expr.Coalesce, ctx: Env): Rex { val type = StaticType.ANY - val createBranch: (Rex) -> Rex.Op.Case.Branch = { expr: Rex -> - val updatedCondition = rex(type, negate(call("is_null", expr))) - rexOpCaseBranch(updatedCondition, expr) + val values = node.args.map { arg -> + visitExprCoerce(arg, ctx) } - - val branches = node.args.map { - createBranch(visitExpr(it, ctx)) - }.toMutableList() - - val defaultRex = rex(type = StaticType.NULL, op = rexOpLit(value = nullValue())) - val op = rexOpCase(branches, defaultRex) - rex(type, op) + val op = rexOpCoalesce(values) + return rex(type, op) } - // nullIf(expr1, expr2) -> - // CASE - // WHEN expr1 = expr2 THEN NULL - // ELSE expr1 END - override fun visitExprNullIf(node: Expr.NullIf, ctx: Env): Rex = plan { + override fun visitExprNullIf(node: Expr.NullIf, ctx: Env): Rex { val type = StaticType.ANY - val expr1 = visitExpr(node.value, ctx) - val expr2 = visitExpr(node.nullifier, ctx) - val id = identifierSymbol(Expr.Binary.Op.EQ.name.lowercase(), Identifier.CaseSensitivity.SENSITIVE) - val fn = fnUnresolved(id, true) - val call = rexOpCallStatic(fn, listOf(expr1, expr2)) - val branches = listOf( - rexOpCaseBranch(rex(type, call), rex(type = StaticType.NULL, op = rexOpLit(value = nullValue()))), - ) - val op = rexOpCase(branches.toMutableList(), expr1) - rex(type, op) + val expr1 = visitExprCoerce(node.value, ctx) + val expr2 = visitExprCoerce(node.nullifier, ctx) + val op = rexOpNullif(expr1, expr2) + return rex(type, op) } /** diff --git a/partiql-planner/src/main/kotlin/org/partiql/planner/internal/typer/PlanTyper.kt b/partiql-planner/src/main/kotlin/org/partiql/planner/internal/typer/PlanTyper.kt index 9ee9bfbbb6..a4cbf51369 100644 --- a/partiql-planner/src/main/kotlin/org/partiql/planner/internal/typer/PlanTyper.kt +++ b/partiql-planner/src/main/kotlin/org/partiql/planner/internal/typer/PlanTyper.kt @@ -55,9 +55,11 @@ import org.partiql.planner.internal.ir.rexOpCallDynamic import org.partiql.planner.internal.ir.rexOpCallDynamicCandidate import org.partiql.planner.internal.ir.rexOpCallStatic import org.partiql.planner.internal.ir.rexOpCaseBranch +import org.partiql.planner.internal.ir.rexOpCoalesce import org.partiql.planner.internal.ir.rexOpCollection import org.partiql.planner.internal.ir.rexOpErr import org.partiql.planner.internal.ir.rexOpLit +import org.partiql.planner.internal.ir.rexOpNullif import org.partiql.planner.internal.ir.rexOpPathIndex import org.partiql.planner.internal.ir.rexOpPathKey import org.partiql.planner.internal.ir.rexOpPathSymbol @@ -760,6 +762,46 @@ internal class PlanTyper( return rex(type, op) } + override fun visitRexOpCoalesce(node: Rex.Op.Coalesce, ctx: StaticType?): Rex { + val values = node.values.map { visitRex(it, it.type) }.toMutableList() + val typer = DynamicTyper() + values.forEach { v -> + typer.accumulate(v.type) + } + val (type, mapping) = typer.mapping() + if (mapping != null) { + assert(mapping.size == values.size) + for (i in values.indices) { + val (operand, target) = mapping[i] + if (operand == target) continue // skip + val cast = env.fnResolver.cast(operand, target) + val rex = rex(type, rexOpCallStatic(fnResolved(cast), listOf(values[i]))) + values[i] = rex + } + } + val op = rexOpCoalesce(values) + return rex(type, op) + } + + // NULLIF(v1, v2) + // == + // CASE + // WHEN V1 = V2 THEN NULL -- WHEN branch always a boolean + // ELSE V1 + // END + // --> union(null, ) + override fun visitRexOpNullif(node: Rex.Op.Nullif, ctx: StaticType?): Rex { + val v1 = visitRex(node.v1, node.v1.type) + val v2 = visitRex(node.v2, node.v2.type) + // reuse dynamic typing logic + val typer = DynamicTyper() + typer.accumulate(NULL) + typer.accumulate(v1.type) + val (type, _) = typer.mapping() + val op = rexOpNullif(v1, v2) + return rex(type, op) + } + /** * In this context, Boolean means PartiQLValueType Bool, which can be nullable. * Hence, we permit Static Type BOOL, Static Type NULL, Static Type Missing here. diff --git a/partiql-planner/src/main/resources/partiql_plan_internal.ion b/partiql-planner/src/main/resources/partiql_plan_internal.ion index d2cf418e67..c562e34be5 100644 --- a/partiql-planner/src/main/resources/partiql_plan_internal.ion +++ b/partiql-planner/src/main/resources/partiql_plan_internal.ion @@ -158,6 +158,15 @@ rex::{ ], }, + nullif::{ + v1: rex, + v2: rex + }, + + coalesce::{ + values: list::[rex] + }, + collection::{ values: list::[rex], }, diff --git a/partiql-planner/src/test/kotlin/org/partiql/planner/internal/typer/PlanTyperTestsPorted.kt b/partiql-planner/src/test/kotlin/org/partiql/planner/internal/typer/PlanTyperTestsPorted.kt index de6fa6cbc0..f02f0922d1 100644 --- a/partiql-planner/src/test/kotlin/org/partiql/planner/internal/typer/PlanTyperTestsPorted.kt +++ b/partiql-planner/src/test/kotlin/org/partiql/planner/internal/typer/PlanTyperTestsPorted.kt @@ -2461,7 +2461,7 @@ class PlanTyperTestsPorted { SuccessTestCase( key = PartiQLTest.Key("basics", "case-when-11"), catalog = "pql", - expected = unionOf(StaticType.INT, StaticType.NULL, StaticType.MISSING), + expected = unionOf(StaticType.INT, StaticType.MISSING), ), SuccessTestCase( key = PartiQLTest.Key("basics", "case-when-12"), @@ -2574,6 +2574,194 @@ class PlanTyperTestsPorted { ), ) + @JvmStatic + fun nullIf() = listOf( + SuccessTestCase( + key = PartiQLTest.Key("basics", "nullif-00"), + catalog = "pql", + expected = StaticType.INT4.asNullable() + ), + SuccessTestCase( + key = PartiQLTest.Key("basics", "nullif-01"), + catalog = "pql", + expected = StaticType.INT4.asNullable() + ), + SuccessTestCase( + key = PartiQLTest.Key("basics", "nullif-02"), + catalog = "pql", + expected = StaticType.INT4.asNullable() + ), + SuccessTestCase( + key = PartiQLTest.Key("basics", "nullif-03"), + catalog = "pql", + expected = StaticType.INT4.asNullable() + ), + SuccessTestCase( + key = PartiQLTest.Key("basics", "nullif-04"), + catalog = "pql", + expected = StaticType.INT8.asNullable() + ), + SuccessTestCase( + key = PartiQLTest.Key("basics", "nullif-05"), + catalog = "pql", + expected = StaticType.INT4.asNullable() + ), + SuccessTestCase( + key = PartiQLTest.Key("basics", "nullif-06"), + catalog = "pql", + expected = StaticType.NULL + ), + SuccessTestCase( + key = PartiQLTest.Key("basics", "nullif-07"), + catalog = "pql", + expected = StaticType.INT4.asNullable() + ), + SuccessTestCase( + key = PartiQLTest.Key("basics", "nullif-08"), + catalog = "pql", + expected = StaticType.NULL_OR_MISSING + ), + SuccessTestCase( + key = PartiQLTest.Key("basics", "nullif-09"), + catalog = "pql", + expected = StaticType.INT4.asNullable() + ), + SuccessTestCase( + key = PartiQLTest.Key("basics", "nullif-10"), + catalog = "pql", + expected = StaticType.INT4.asNullable() + ), + SuccessTestCase( + key = PartiQLTest.Key("basics", "nullif-11"), + catalog = "pql", + expected = StaticType.INT4.asNullable() + ), + SuccessTestCase( + key = PartiQLTest.Key("basics", "nullif-12"), + catalog = "pql", + expected = StaticType.INT8.asNullable() + ), + SuccessTestCase( + key = PartiQLTest.Key("basics", "nullif-13"), + catalog = "pql", + expected = StaticType.INT4.asNullable() + ), + SuccessTestCase( + key = PartiQLTest.Key("basics", "nullif-14"), + catalog = "pql", + expected = StaticType.STRING.asNullable() + ), + SuccessTestCase( + key = PartiQLTest.Key("basics", "nullif-15"), + catalog = "pql", + expected = StaticType.INT4.asNullable() + ), + SuccessTestCase( + key = PartiQLTest.Key("basics", "nullif-16"), + catalog = "pql", + expected = unionOf(StaticType.INT2, StaticType.INT4, StaticType.INT8, StaticType.INT, StaticType.DECIMAL, StaticType.NULL) + ), + SuccessTestCase( + key = PartiQLTest.Key("basics", "nullif-17"), + catalog = "pql", + expected = StaticType.INT4.asNullable() + ), + SuccessTestCase( + key = PartiQLTest.Key("basics", "nullif-18"), + catalog = "pql", + expected = unionOf(StaticType.ALL_TYPES.toSet()) + ), + ) + + @JvmStatic + fun coalesce() = listOf( + SuccessTestCase( + key = PartiQLTest.Key("basics", "coalesce-00"), + catalog = "pql", + expected = StaticType.INT4 + ), + SuccessTestCase( + key = PartiQLTest.Key("basics", "coalesce-01"), + catalog = "pql", + expected = StaticType.INT4 + ), + SuccessTestCase( + key = PartiQLTest.Key("basics", "coalesce-02"), + catalog = "pql", + expected = StaticType.DECIMAL + ), + SuccessTestCase( + key = PartiQLTest.Key("basics", "coalesce-03"), + catalog = "pql", + expected = unionOf(StaticType.NULL, StaticType.DECIMAL) + ), + SuccessTestCase( + key = PartiQLTest.Key("basics", "coalesce-04"), + catalog = "pql", + expected = unionOf(StaticType.NULL, StaticType.MISSING, StaticType.DECIMAL) + ), + SuccessTestCase( + key = PartiQLTest.Key("basics", "coalesce-05"), + catalog = "pql", + expected = unionOf(StaticType.NULL, StaticType.MISSING, StaticType.DECIMAL) + ), + SuccessTestCase( + key = PartiQLTest.Key("basics", "coalesce-06"), + catalog = "pql", + expected = StaticType.INT4 + ), + SuccessTestCase( + key = PartiQLTest.Key("basics", "coalesce-07"), + catalog = "pql", + expected = StaticType.INT4 + ), + SuccessTestCase( + key = PartiQLTest.Key("basics", "coalesce-08"), + catalog = "pql", + expected = StaticType.INT8 + ), + SuccessTestCase( + key = PartiQLTest.Key("basics", "coalesce-09"), + catalog = "pql", + expected = StaticType.INT8.asNullable() + ), + SuccessTestCase( + key = PartiQLTest.Key("basics", "coalesce-10"), + catalog = "pql", + expected = unionOf(StaticType.INT8, StaticType.NULL, StaticType.MISSING) + ), + SuccessTestCase( + key = PartiQLTest.Key("basics", "coalesce-11"), + catalog = "pql", + expected = unionOf(StaticType.INT8, StaticType.STRING) + ), + SuccessTestCase( + key = PartiQLTest.Key("basics", "coalesce-12"), + catalog = "pql", + expected = unionOf(StaticType.INT8, StaticType.NULL, StaticType.STRING) + ), + SuccessTestCase( + key = PartiQLTest.Key("basics", "coalesce-13"), + catalog = "pql", + expected = unionOf(StaticType.INT2, StaticType.INT4, StaticType.INT8, StaticType.INT, StaticType.DECIMAL) + ), + SuccessTestCase( + key = PartiQLTest.Key("basics", "coalesce-14"), + catalog = "pql", + expected = unionOf(StaticType.INT2, StaticType.INT4, StaticType.INT8, StaticType.INT, StaticType.DECIMAL, StaticType.STRING) + ), + SuccessTestCase( + key = PartiQLTest.Key("basics", "coalesce-15"), + catalog = "pql", + expected = unionOf(StaticType.INT2, StaticType.INT4, StaticType.INT8, StaticType.INT, StaticType.DECIMAL, StaticType.STRING, StaticType.NULL) + ), + SuccessTestCase( + key = PartiQLTest.Key("basics", "coalesce-16"), + catalog = "pql", + expected = unionOf(StaticType.ALL_TYPES.toSet()) + ), + ) + @JvmStatic fun pathExpressions() = listOf( SuccessTestCase( @@ -3227,6 +3415,16 @@ class PlanTyperTestsPorted { @Execution(ExecutionMode.CONCURRENT) fun testCaseWhens(tc: TestCase) = runTest(tc) + @ParameterizedTest + @MethodSource("nullIf") + @Execution(ExecutionMode.CONCURRENT) + fun testNullIf(tc: TestCase) = runTest(tc) + + @ParameterizedTest + @MethodSource("coalesce") + @Execution(ExecutionMode.CONCURRENT) + fun testCoalesce(tc: TestCase) = runTest(tc) + @ParameterizedTest @MethodSource("subqueryCases") @Execution(ExecutionMode.CONCURRENT) diff --git a/partiql-planner/src/testFixtures/resources/inputs/basics/case.sql b/partiql-planner/src/testFixtures/resources/inputs/basics/case.sql index ba1589e803..5c5d274706 100644 --- a/partiql-planner/src/testFixtures/resources/inputs/basics/case.sql +++ b/partiql-planner/src/testFixtures/resources/inputs/basics/case.sql @@ -85,8 +85,7 @@ CASE t_item.t_string END; --#[case-when-11] --- type: (int|null|missing) --- TODO should really be (int|missing) but our translation of coalesce doesn't consider types. +-- type: (int|missing) COALESCE(CAST(t_item.t_string AS INT), 1); -- ----------------------------- diff --git a/partiql-planner/src/testFixtures/resources/inputs/basics/coalesce.sql b/partiql-planner/src/testFixtures/resources/inputs/basics/coalesce.sql new file mode 100644 index 0000000000..7d96b941c0 --- /dev/null +++ b/partiql-planner/src/testFixtures/resources/inputs/basics/coalesce.sql @@ -0,0 +1,67 @@ +--#[coalesce-00] +-- type: (int32) +COALESCE(1); + +--#[coalesce-01] +-- type: (int32) +COALESCE(1, 2); + +--#[coalesce-02] +-- type: (decimal) +COALESCE(1, 1.23); + +--#[coalesce-03] +-- type: (null | decimal) +COALESCE(NULL, 1, 1.23); + +--#[coalesce-04] +-- type: (null | missing | int32 | decimal) +COALESCE(NULL, MISSING, 1, 1.23); + +--#[coalesce-05] +-- type: (null | missing | int32 | decimal); same as above +COALESCE(1, 1.23, NULL, MISSING); + +--#[coalesce-06] +-- type: (int32) +COALESCE(t_item.t_int32); + +--#[coalesce-07] +-- type: (int32) +COALESCE(t_item.t_int32, t_item.t_int32); + +--#[coalesce-08] +-- type: (int64) +COALESCE(t_item.t_int64, t_item.t_int32); + +--#[coalesce-09] +-- type: (int64 | null) +COALESCE(t_item.t_int64_null, t_item.t_int32, t_item.t_int32_null); + +--#[coalesce-10] +-- type: (int64 | null) +COALESCE(t_item.t_int64_null, t_item.t_int32, t_item.t_int32_null, MISSING); + +--#[coalesce-11] +-- type: (int64 | string) +COALESCE(t_item.t_int64, t_item.t_string); + +--#[coalesce-12] +-- type: (int64 | null | string) +COALESCE(t_item.t_int64_null, t_item.t_string); + +--#[coalesce-13] +-- type: (int16 | int32 | int64 | int | decimal) +COALESCE(t_item.t_num_exact, t_item.t_int32); + +--#[coalesce-14] +-- type: (int16 | int32 | int64 | int | decimal, string) +COALESCE(t_item.t_num_exact, t_item.t_string); + +--#[coalesce-15] +-- type: (int16 | int32 | int64 | int | decimal, string, null) +COALESCE(t_item.t_num_exact, t_item.t_string, NULL); + +--#[coalesce-16] +-- type: (any) +COALESCE(t_item.t_any, t_item.t_int32); diff --git a/partiql-planner/src/testFixtures/resources/inputs/basics/nullif.sql b/partiql-planner/src/testFixtures/resources/inputs/basics/nullif.sql new file mode 100644 index 0000000000..d03c3d863a --- /dev/null +++ b/partiql-planner/src/testFixtures/resources/inputs/basics/nullif.sql @@ -0,0 +1,77 @@ +--#[nullif-00] +-- Currently, no constant-folding. If there was, return type could be int32. +-- type: (int32 | null) +NULLIF(1, 2); + +--#[nullif-01] +-- Currently, no constant-folding. If there was, return type could be null. +-- type: (int32 | null) +NULLIF(1, 1); + +--#[nullif-02] +-- type: (int32 | null) +NULLIF(t_item.t_int32, t_item.t_int32); + +--#[nullif-03] +-- type: (int32 | null) +NULLIF(t_item.t_int32, t_item.t_int64); + +--#[nullif-04] +-- type: (int64 | null) +NULLIF(t_item.t_int64, t_item.t_int32); + +--#[nullif-05] +-- type: (int32 | null) +NULLIF(t_item.t_int32, t_item.t_null); + +--#[nullif-06] +-- type: (null) +NULLIF(t_item.t_null, t_item.t_int32); + +--#[nullif-07] +-- type: (int32 | null) +NULLIF(t_item.t_int32, MISSING); + +--#[nullif-08] +-- type: (missing | null) +NULLIF(MISSING, t_item.t_int32); + +--#[nullif-09] +-- type: (int32 | null) +NULLIF(t_item.t_int32, t_item.t_int32_null); + +--#[nullif-10] +-- type: (int32 | null) +NULLIF(t_item.t_int32_null, t_item.t_int32); + +--#[nullif-11] +-- type: (int32 | null) +NULLIF(t_item.t_int32, t_item.t_int64_null); + +--#[nullif-12] +-- type: (int64 | null) +NULLIF(t_item.t_int64_null, t_item.t_int32); + +--#[nullif-13] +-- type: (int32 | null) +NULLIF(t_item.t_int32, t_item.t_string); + +--#[nullif-14] +-- type: (string | null) +NULLIF(t_item.t_string, t_item.t_int32); + +--#[nullif-15] +-- type: (int32 | null) +NULLIF(t_item.t_int32, t_item.t_num_exact); + +--#[nullif-16] +-- type: (int16 | int32 | int64 | int | decimal | null) +NULLIF(t_item.t_num_exact, t_item.t_int32); + +--#[nullif-17] +-- type: (int32 | null) +NULLIF(t_item.t_int32, t_item.t_any); + +--#[nullif-18] +-- type: (any) +NULLIF(t_item.t_any, t_item.t_int32);