Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding safe divide function #11904

Merged
merged 7 commits into from
Nov 17, 2021
Merged

Conversation

somu-imply
Copy link
Contributor

@somu-imply somu-imply commented Nov 11, 2021

Adds a Safe_Divide function

Description

Added a new safe divide function that is guarded against division by 0. Safe_divide(x,y) returns x/y if y is not equal to 0. It returns 0 otherwise.

Fixed the bug ...

Renamed the class ...

Added a forbidden-apis entry ...


Key changed/added classes in this PR

Safe_Divide function
Calcite layer direct operator to connect with newly created safe_divide native function


This PR has:

  • been self-reviewed.
  • added documentation for new or modified features or behaviors.
  • added Javadocs for most classes and all non-trivial methods. Linked related entities via Javadoc links.
  • added or updated version, license, or notice information in licenses.yaml
  • added comments explaining the "why" and the intent of the code wherever would not be obvious for an unfamiliar reader.
  • added unit tests or modified existing tests to cover new code paths, ensuring the threshold for code coverage is met.
  • added integration tests.
  • been tested in a test Druid cluster.

@suneet-s suneet-s changed the title IMPLY-4344: Adding safe divide function along with testcases and docu… Adding safe divide function Nov 11, 2021
@suneet-s suneet-s closed this Nov 11, 2021
@suneet-s suneet-s reopened this Nov 11, 2021
@suneet-s
Copy link
Contributor

Added label Release Notes because this is a new function that users may want to take advantage of.

@@ -154,6 +154,7 @@ See javadoc of java.lang.Math for detailed explanation for each function.
|remainder|remainder(x, y) returns the remainder operation on two arguments as prescribed by the IEEE 754 standard|
|rint|rint(x) returns value that is closest in value to x and is equal to a mathematical integer|
|round|round(x, y) returns the value of the x rounded to the y decimal places. While x can be an integer or floating-point number, y must be an integer. The type of the return value is specified by that of x. y defaults to 0 if omitted. When y is negative, x is rounded on the left side of the y decimal points. If x is `NaN`, x returns 0. If x is infinity, x will be converted to the nearest finite double. |
|safe_divide|safe_divide(x,y) returns the division of x by y if y is not equal to 0, returns 0 otherwise|
Copy link
Member

Choose a reason for hiding this comment

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

it's better to put this new function along with the 'div' function above.

Copy link
Member

Choose a reason for hiding this comment

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

What if either x or y is NULL?

Copy link
Member

Choose a reason for hiding this comment

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

they appear to be alphabetically sorted right now, maybe we should consider leaving it where it is (though I don't have a strong opinion)

It is worth documenting the null handling behavior (especially if we change it to return null in SQL compatible null handling mode)

|`HUMAN_READABLE_BINARY_BYTE_FORMAT(value[, precision])`| Format a number in human-readable [IEC](https://en.wikipedia.org/wiki/Binary_prefix) format. For example, HUMAN_READABLE_BINARY_BYTE_FORMAT(1048576) returns `1.00 MiB`. `precision` must be in the range of [0,3] (default: 2). |
|`HUMAN_READABLE_DECIMAL_BYTE_FORMAT(value[, precision])`| Format a number in human-readable [SI](https://en.wikipedia.org/wiki/Binary_prefix) format. HUMAN_READABLE_DECIMAL_BYTE_FORMAT(1048576) returns `1.04 MB`. `precision` must be in the range of [0,3] (default: 2). `precision` must be in the range of [0,3] (default: 2). |
|`HUMAN_READABLE_DECIMAL_FORMAT(value[, precision])`| Format a number in human-readable SI format. For example, HUMAN_READABLE_DECIMAL_FORMAT(1048576) returns `1.04 M`. `precision` must be in the range of [0,3] (default: 2). |
|`SAFE_DIVIDE(x,y)`|Returns the division of x by y guarded on division by 0 |
Copy link
Member

Choose a reason for hiding this comment

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

It's better to put it along with the 'DIV' function above.

Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
|`SAFE_DIVIDE(x,y)`|Returns the division of x by y guarded on division by 0 |
|`SAFE_DIVIDE(x,y)`|Returns the division of x by y guarded on division by 0. In case y is 0 it returns 0, or `null` if `druid.generic.useDefaultValueForNull=false` |

@@ -154,6 +154,7 @@ See javadoc of java.lang.Math for detailed explanation for each function.
|remainder|remainder(x, y) returns the remainder operation on two arguments as prescribed by the IEEE 754 standard|
|rint|rint(x) returns value that is closest in value to x and is equal to a mathematical integer|
|round|round(x, y) returns the value of the x rounded to the y decimal places. While x can be an integer or floating-point number, y must be an integer. The type of the return value is specified by that of x. y defaults to 0 if omitted. When y is negative, x is rounded on the left side of the y decimal points. If x is `NaN`, x returns 0. If x is infinity, x will be converted to the nearest finite double. |
|safe_divide|safe_divide(x,y) returns the division of x by y if y is not equal to 0, returns 0 otherwise|
Copy link
Member

Choose a reason for hiding this comment

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

What if either x or y is NULL?

Copy link
Member

@clintropolis clintropolis left a comment

Choose a reason for hiding this comment

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

nice, overall lgtm, just a few minor comments (and noticed a few minor formatting issues that checkstyle will probably flag) 👍

Comment on lines 1191 to 1193
if(y==0) {
return ExprEval.of(0);
}
Copy link
Member

Choose a reason for hiding this comment

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

hmm, looking at the behavior of another SAFE_DIVIDE I could find in other databases (https://cloud.google.com/bigquery/docs/reference/standard-sql/functions-and-operators#safe_divide), I think we should consider returning null here if NullHandling.replaceWithDefault() is true

Suggested change
if(y==0) {
return ExprEval.of(0);
}
if (y == 0 && x != 0) {
return ExprEval.ofLong(NullHandling.defaultLongValue());
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Change made. Added a case and testcases where x or y is NaN or +- Infinity

Comment on lines 1200 to 1202
if(y==0) {
return ExprEval.of(0);
}
Copy link
Member

Choose a reason for hiding this comment

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

same suggestion (and also should preserve double typing)

Suggested change
if(y==0) {
return ExprEval.of(0);
}
if (y == 0 && x != 0) {
return ExprEval.ofDouble(NullHandling.defaultDoubleValue());
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Change made. Added a case and testcases where x or y is NaN or +- Infinity

Comment on lines 741 to 742
assertExpr("safe_divide(3, 0)", 0L);
assertExpr("safe_divide(3.7, 0.0)", 0L);
Copy link
Member

Choose a reason for hiding this comment

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

if other suggestion is applied:

Suggested change
assertExpr("safe_divide(3, 0)", 0L);
assertExpr("safe_divide(3.7, 0.0)", 0L);
assertExpr("safe_divide(3, 0)", NullHandling.defaultLongValue());
assertExpr("safe_divide(3.7, 0.0)", NullHandling.defaultDoubleValue());

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

@@ -154,6 +154,7 @@ See javadoc of java.lang.Math for detailed explanation for each function.
|remainder|remainder(x, y) returns the remainder operation on two arguments as prescribed by the IEEE 754 standard|
|rint|rint(x) returns value that is closest in value to x and is equal to a mathematical integer|
|round|round(x, y) returns the value of the x rounded to the y decimal places. While x can be an integer or floating-point number, y must be an integer. The type of the return value is specified by that of x. y defaults to 0 if omitted. When y is negative, x is rounded on the left side of the y decimal points. If x is `NaN`, x returns 0. If x is infinity, x will be converted to the nearest finite double. |
|safe_divide|safe_divide(x,y) returns the division of x by y if y is not equal to 0, returns 0 otherwise|
Copy link
Member

Choose a reason for hiding this comment

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

they appear to be alphabetically sorted right now, maybe we should consider leaving it where it is (though I don't have a strong opinion)

It is worth documenting the null handling behavior (especially if we change it to return null in SQL compatible null handling mode)

@@ -0,0 +1,34 @@
package org.apache.druid.sql.calcite.expression.builtin;
Copy link
Member

Choose a reason for hiding this comment

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

missing apache license header, can copy from any other file

Copy link
Contributor Author

Choose a reason for hiding this comment

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

License headed added, alphabetical order maintained, null handling info added

Copy link
Member

@clintropolis clintropolis left a comment

Choose a reason for hiding this comment

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

looks like a couple of legit ci failures still

@@ -154,6 +154,7 @@ See javadoc of java.lang.Math for detailed explanation for each function.
|remainder|remainder(x, y) returns the remainder operation on two arguments as prescribed by the IEEE 754 standard|
|rint|rint(x) returns value that is closest in value to x and is equal to a mathematical integer|
|round|round(x, y) returns the value of the x rounded to the y decimal places. While x can be an integer or floating-point number, y must be an integer. The type of the return value is specified by that of x. y defaults to 0 if omitted. When y is negative, x is rounded on the left side of the y decimal points. If x is `NaN`, x returns 0. If x is infinity, x will be converted to the nearest finite double. |
|safe_divide|safe_divide(x,y) returns the division of x by y if y is not equal to 0. In case y is 0 it returns null or default values (if returning default values are enabled). |
Copy link
Member

Choose a reason for hiding this comment

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

nit: to match other docs that describe difference between default and sql compatible null handling mode

Suggested change
|safe_divide|safe_divide(x,y) returns the division of x by y if y is not equal to 0. In case y is 0 it returns null or default values (if returning default values are enabled). |
|safe_divide|safe_divide(x,y) returns the division of x by y if y is not equal to 0. In case y is 0 it returns 0, or `null` if `druid.generic.useDefaultValueForNull=false`. |

Also, since this file doesn't backtick the expression names, safe_divide needs added to the spelling exceptions file https://github.com/apache/druid/blob/master/website/.spelling#L1202

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

|`HUMAN_READABLE_BINARY_BYTE_FORMAT(value[, precision])`| Format a number in human-readable [IEC](https://en.wikipedia.org/wiki/Binary_prefix) format. For example, HUMAN_READABLE_BINARY_BYTE_FORMAT(1048576) returns `1.00 MiB`. `precision` must be in the range of [0,3] (default: 2). |
|`HUMAN_READABLE_DECIMAL_BYTE_FORMAT(value[, precision])`| Format a number in human-readable [SI](https://en.wikipedia.org/wiki/Binary_prefix) format. HUMAN_READABLE_DECIMAL_BYTE_FORMAT(1048576) returns `1.04 MB`. `precision` must be in the range of [0,3] (default: 2). `precision` must be in the range of [0,3] (default: 2). |
|`HUMAN_READABLE_DECIMAL_FORMAT(value[, precision])`| Format a number in human-readable SI format. For example, HUMAN_READABLE_DECIMAL_FORMAT(1048576) returns `1.04 M`. `precision` must be in the range of [0,3] (default: 2). |
|`SAFE_DIVIDE(x,y)`|Returns the division of x by y guarded on division by 0 |
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
|`SAFE_DIVIDE(x,y)`|Returns the division of x by y guarded on division by 0 |
|`SAFE_DIVIDE(x,y)`|Returns the division of x by y guarded on division by 0. In case y is 0 it returns 0, or `null` if `druid.generic.useDefaultValueForNull=false` |

Comment on lines 1182 to 1186
ExpressionType type = ExpressionType.DOUBLE;
for (Expr arg : args) {
type = ExpressionTypeConversion.function(type, arg.getOutputType(inspector));
}
return ExpressionType.asArrayType(type);
Copy link
Member

Choose a reason for hiding this comment

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

this shouldn't be array typed

Suggested change
ExpressionType type = ExpressionType.DOUBLE;
for (Expr arg : args) {
type = ExpressionTypeConversion.function(type, arg.getOutputType(inspector));
}
return ExpressionType.asArrayType(type);
return ExpressionTypeConversion.function(
args.get(0).getOutputType(inspector),
args.get(1).getOutputType(inspector)
);

Could you also add some tests with a mix of longs and doubles to https://github.com/apache/druid/blob/master/core/src/test/java/org/apache/druid/math/expr/OutputTypeTest.java#L163?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

.operatorBuilder(StringUtils.toUpperCase(Function.SafeDivide.NAME))
.operandTypeChecker(OperandTypes.ANY_NUMERIC)
.returnTypeInference(ReturnTypes.QUOTIENT_NULLABLE)
.functionCategory(SqlFunctionCategory.USER_DEFINED_FUNCTION)
Copy link
Member

Choose a reason for hiding this comment

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

nit: this could also probably be SqlFunctionCategory.NUMERIC, though tbh i'm not sure if it matters that much?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Left as is

Copy link
Member

@clintropolis clintropolis left a comment

Choose a reason for hiding this comment

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

👍 (though try not to auto format entire files when making changes if it makes a lot of noise as it is much harder to review)

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

Successfully merging this pull request may close these issues.

5 participants