-
Notifications
You must be signed in to change notification settings - Fork 29.3k
[SPARK-58372][SQL] Truncate fractional to integral casts pushed down to JDBC sources #57564
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1362,6 +1362,27 @@ private[v2] trait V2JDBCTest | |
| } | ||
| } | ||
|
|
||
| test("fractional to integral cast pushed down truncates toward zero like Spark") { | ||
| val tbl = s"$catalogName.integral_cast" | ||
| withTable(tbl) { | ||
| // label is a string so the result type is the same across dialects (a numeric column | ||
| // comes back as BigDecimal on some engines such as Oracle). | ||
| sql(s"CREATE TABLE $tbl (double_col DOUBLE, decimal_col DECIMAL(10, 2), label VARCHAR(8))") | ||
| sql(s"INSERT INTO $tbl VALUES (1.5, 1.5, 'a'), (2.5, 2.5, 'b'), (-1.5, -1.5, 'c')") | ||
|
|
||
| // Spark truncates toward zero, so 1.5, 2.5 and -1.5 become 1, 2 and -1. A database that | ||
| // rounds half away from zero would return 2, 3 and -2 instead. | ||
| Seq("double_col", "decimal_col").foreach { col => | ||
| val projected = sql(s"SELECT CAST($col AS INT) FROM $tbl ORDER BY $col") | ||
| assert(projected.collect().map(_.getInt(0)) === Array(-1, 1, 2)) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This assertion does not seem to exercise the change. JDBCScanBuilder pushes down only column pruning, filters, aggregates, limit/topN and joins, but never a projection expression. So, this test case in a SELECT list is evaluated locally by Spark and passes regardless of the fix.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Excellent comment, |
||
|
|
||
| val filtered = sql(s"SELECT label FROM $tbl WHERE CAST($col AS INT) = 2") | ||
| checkFilterPushed(filtered) | ||
| assert(filtered.collect().map(_.getString(0)) === Array("b")) | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, IIUC - no test runs in the default PR CI. The only test lives in V2JDBCTest (docker integration suites), which is not in the default check set, and there is no unit test asserting the generated SQL string per dialect (e.g. a compileExpression assertion in JDBCSuite, as done for other pushdowns). So, Snowflake's TRUNC override is unexercised. If this is the case, please add a cheap per-dialect unit test to guard against silent regression and properly cover Snowflake. |
||
| test("SPARK-52262: FAILED_JDBC.TABLE_EXISTS not thrown on connection error") { | ||
| val invalidTableName = s"$catalogName.invalid" | ||
| val originalUrl = spark.conf.get(s"spark.sql.catalog.$catalogName.url") | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -442,7 +442,17 @@ abstract class JdbcDialect extends Serializable with Logging { | |
| override def visitCast(expr: String, exprDataType: DataType, dataType: DataType): String = { | ||
| val databaseTypeDefinition = | ||
| getJDBCType(dataType).map(_.databaseTypeDefinition).getOrElse(dataType.typeName) | ||
| s"CAST($expr AS $databaseTypeDefinition)" | ||
| // Spark truncates toward zero when casting a fractional value to an integral type, but many | ||
| // databases round instead, so a pushed down cast can return a different result than Spark. | ||
| // Dialects for such databases override `truncateFractionalValue` to truncate the value first. | ||
| val castedExpr = if (exprDataType.isInstanceOf[FractionalType] && | ||
| dataType.isInstanceOf[IntegralType] && | ||
| !SQLConf.get.legacyJdbcRoundIntegralCastPushdown) { | ||
| truncateFractionalValue(expr) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: Do we need to fix this on v2 expression builder level? Other data sources may benefit of stricter API for this truncation fix. |
||
| } else { | ||
| expr | ||
| } | ||
| s"CAST($castedExpr AS $databaseTypeDefinition)" | ||
| } | ||
|
|
||
| override def visitSQLFunction(funcName: String, inputs: Array[Expression]): String = { | ||
|
|
@@ -525,6 +535,16 @@ abstract class JdbcDialect extends Serializable with Logging { | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * Truncates `expr` toward zero, so that a cast from a fractional type to an integral type that | ||
| * is pushed down matches Spark, which truncates rather than rounds. Dialects for databases that | ||
| * round such casts override this with the database's truncating function. The default returns | ||
| * `expr` unchanged, for databases whose cast already truncates like Spark. | ||
| * @param expr The SQL string of the value being cast. | ||
| * @return The SQL string to cast instead of `expr`. | ||
| */ | ||
| def truncateFractionalValue(expr: String): String = expr | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice job on having default value, it would be better to be strict here for third-party dialects, but that would break compilation on spark upgrade, so I think it is better to preserve this default implementation. What is the policy, is robustness more important than avoiding of breaking changes or vice versa @cloud-fan @uros-b ?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would advise to avoid breaking change. In any case, please add the
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. JdbcDialect is |
||
|
|
||
| /** | ||
| * Returns whether the database supports function. | ||
| * @param funcName Upper-cased function name | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we add test to verify legacy behaviour, conf offf
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1