@@ -13,20 +13,79 @@ final class IfNullReturnTypeExtension implements QueryFunctionReturnTypeExtensio
1313{
1414 public function isFunctionSupported (FunctionCall $ expression ): bool
1515 {
16- return \in_array ($ expression ->getFunction ()->getName (), [BuiltInFunction::IFNULL , BuiltInFunction:: NULLIF ], true );
16+ return \in_array ($ expression ->getFunction ()->getName (), [BuiltInFunction::IFNULL ], true );
1717 }
1818
19- public function getReturnType (FunctionCall $ expression , QueryScope $ scope ): Type
19+ public function getReturnType (FunctionCall $ expression , QueryScope $ scope ): ? Type
2020 {
2121 $ args = $ expression ->getArguments ();
2222
23- $ results = [];
24- foreach ($ args as $ arg ) {
25- $ argType = $ scope ->getType ($ arg );
23+ if (2 !== \count ($ args )) {
24+ return null ;
25+ }
26+
27+ $ argType1 = $ scope ->getType ($ args [0 ]);
28+ $ argType2 = $ scope ->getType ($ args [1 ]);
29+
30+ // If arg1 is literal null, arg2 is always returned
31+ if ($ argType1 ->isNull ()->yes ()) {
32+ return $ argType2 ;
33+ }
2634
27- $ results [] = $ argType ;
35+ $ arg1ContainsNull = TypeCombinator::containsNull ($ argType1 );
36+ $ arg2ContainsNull = TypeCombinator::containsNull ($ argType2 );
37+ $ argType1NoNull = TypeCombinator::removeNull ($ argType1 );
38+ $ argType2NoNull = TypeCombinator::removeNull ($ argType2 );
39+
40+ // If arg1 can be null, the result can be arg1 or arg2;
41+ // otherwise, the result can only be arg1.
42+ if ($ arg1ContainsNull ) {
43+ $ resultType = TypeCombinator::union ($ argType1NoNull , $ argType2NoNull );
44+ } else {
45+ $ resultType = $ argType1 ;
46+ }
47+
48+ // The result type is always the "more general" of the two args
49+ // in the order: string, float, integer.
50+ // see https://dev.mysql.com/doc/refman/5.7/en/flow-control-functions.html#function_ifnull
51+ if ($ this ->isResultString ($ argType1NoNull , $ argType2NoNull )) {
52+ $ resultType = $ resultType ->toString ();
53+ } elseif ($ this ->isResultFloat ($ argType1NoNull , $ argType2NoNull )) {
54+ $ resultType = $ resultType ->toFloat ();
2855 }
2956
30- return TypeCombinator::union (...$ results );
57+ // Re-add null if arg2 can contain null
58+ if ($ arg2ContainsNull ) {
59+ $ resultType = TypeCombinator::addNull ($ resultType );
60+ }
61+ return $ resultType ;
62+ }
63+
64+ private function isResultString (Type $ type1 , Type $ type2 ): bool
65+ {
66+ return (
67+ // If either arg is a string, the result is a string
68+ $ type1 ->isString ()->yes () ||
69+ $ type2 ->isString ()->yes () ||
70+
71+ // Special case where args are a constant float and an int
72+ // results in a numeric string
73+ (
74+ $ type1 ->isConstantScalarValue ()->yes () &&
75+ $ type1 ->isFloat ()->yes () &&
76+ $ type2 ->isInteger ()->yes ()
77+ ) ||
78+ (
79+ $ type2 ->isConstantScalarValue ()->yes () &&
80+ $ type2 ->isFloat ()->yes () &&
81+ $ type1 ->isInteger ()->yes ()
82+ )
83+ );
84+ }
85+
86+ private function isResultFloat (Type $ type1 , Type $ type2 ): bool
87+ {
88+ // If either arg is a float, the result is a float
89+ return $ type1 ->isFloat ()->yes () || $ type2 ->isFloat ()->yes ();
3190 }
3291}
0 commit comments