Skip to content

fix: nvl2 result type should not depend on its first argument - #26498

Open
CuteChuanChuan wants to merge 1 commit into
apache:mainfrom
CuteChuanChuan:issue-26024/nvl2-result-type-fix
Open

CuteChuanChuan wants to merge 1 commit into
apache:mainfrom
CuteChuanChuan:issue-26024/nvl2-result-type-fix

Conversation

@CuteChuanChuan

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Rationale for this change

The result type of nvl2(x, a, b) depended on the type of x, even though x is only checked for NULL. This caused decimal results to be widened (e.g. Decimal128(22, 2)) or turned into strings, and made timestamp results fail during planning or optimization. The equivalent CASE WHEN x IS NOT NULL THEN a ELSE b END worked in all of these cases.

What changes are included in this PR?

  • NVL2Func::coerce_types now finds the common type of a and b only, using type_union_coercion (the same rule CASE uses for its THEN/ELSE branches). x keeps its original type.
  • If a and b have no common type, it now returns a plan error instead of an internal error.

What is the testing strategy for this PR?

Added sqllogictest cases to nvl2.slt that cover the reproducers from the issue (decimal with int/float/string tests, timestamp with int/string-literal tests). Each case checks both the value and arrow_typeof.

Are there any user-facing changes?

Yes. nvl2 now returns the same type as the equivalent CASE expression. Queries that used to get a widened decimal or Utf8 result now get the type of a/b, and the timestamp cases that used to error now work. No public API changes.

@github-actions github-actions Bot added sqllogictest SQL Logic Tests (.slt) functions Changes to functions implementation labels Oct 19, 2026

@neilconway neilconway left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm overall, two minor suggestions! Thanks @CuteChuanChuan

})?;
Ok(vec![new_type; arg_types.len()])
let Some(new_type) = type_union_coercion(if_non_null, if_null) else {
return plan_err!("Coercion from {if_non_null} to {if_null} failed.");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically the user didn't ask for a coercion; and this results in a doubled period. Howabout:

nvl2 cannot coerce {if_non_null} and {if_null} to a common type

}
})?;
Ok(vec![new_type; arg_types.len()])
let Some(new_type) = type_union_coercion(if_non_null, if_null) else {

@neilconway neilconway Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

type_union_coercion has the property that it depends on argument order (it takes the TZ of the leftmost argument if the timezones of the inputs disagree):

CREATE TABLE t AS
SELECT i, a, arrow_cast(a, 'Timestamp(Nanosecond, Some("+05:00"))') AS b
FROM (SELECT 1 AS i,
             arrow_cast(TIMESTAMP '2024-01-01 22:00:00', 'Timestamp(Nanosecond, Some("+00:00"))') AS a);

-- a and b are the same instant, typed with different zones
SELECT a, b, a = b AS same_instant FROM t;
-- +----------------------+---------------------------+--------------+
-- | a                    | b                         | same_instant |
-- +----------------------+---------------------------+--------------+
-- | 2024-01-01T22:00:00Z | 2024-01-02T03:00:00+05:00 | true         |
-- +----------------------+---------------------------+--------------+

SELECT nvl2(i, a, b) AS v,
       arrow_typeof(nvl2(i, a, b)) AS v_type,
       CAST(nvl2(i, a, b) AS DATE) AS v_date
FROM t;
-- +----------------------+-------------------------+------------+
-- | v                    | v_type                  | v_date     |
-- +----------------------+-------------------------+------------+
-- | 2024-01-01T22:00:00Z | Timestamp(ns, "+00:00") | 2024-01-01 |
-- +----------------------+-------------------------+------------+

SELECT CASE WHEN i IS NOT NULL THEN a ELSE b END AS v,
       arrow_typeof(CASE WHEN i IS NOT NULL THEN a ELSE b END) AS v_type,
       CAST(CASE WHEN i IS NOT NULL THEN a ELSE b END AS DATE) AS v_date
FROM t;
-- +---------------------------+-------------------------+------------+
-- | v                         | v_type                  | v_date     |
-- +---------------------------+-------------------------+------------+
-- | 2024-01-02T03:00:00+05:00 | Timestamp(ns, "+05:00") | 2024-01-02 |
-- +---------------------------+-------------------------+------------+

To match CASE's behavior, can we call type_union_coercion(if_null, if_non_null) instead? And add a test case for this.

(type_union_coercion's behavior here is probably not optimal but that's a separate issue.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

functions Changes to functions implementation sqllogictest SQL Logic Tests (.slt)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

nvl2 result type depends on the type of its first argument

2 participants