-
Notifications
You must be signed in to change notification settings - Fork 29.3k
[SPARK-46380][SQL]Replace current time/date prior to evaluating inline table expressions. #44316
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
Changes from 8 commits
6757409
ed33d5e
0127596
5e24fe1
f019b9c
0f5fd12
2220ee8
ba321fc
9f46333
971e3cc
15a8bab
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 | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -17,28 +17,29 @@ | |||||||||||
|
|
||||||||||||
| package org.apache.spark.sql.catalyst.analysis | ||||||||||||
|
|
||||||||||||
| import scala.util.control.NonFatal | ||||||||||||
|
|
||||||||||||
| import org.apache.spark.sql.catalyst.InternalRow | ||||||||||||
| import org.apache.spark.sql.catalyst.expressions.{AliasHelper, EvalHelper} | ||||||||||||
| import org.apache.spark.sql.catalyst.plans.logical.{LocalRelation, LogicalPlan} | ||||||||||||
| import org.apache.spark.sql.catalyst.expressions.{AliasHelper, EvalHelper, Expression} | ||||||||||||
| import org.apache.spark.sql.catalyst.optimizer.EvalInlineTables | ||||||||||||
| import org.apache.spark.sql.catalyst.plans.logical.LogicalPlan | ||||||||||||
| import org.apache.spark.sql.catalyst.rules.Rule | ||||||||||||
| import org.apache.spark.sql.catalyst.trees.AlwaysProcess | ||||||||||||
| import org.apache.spark.sql.catalyst.trees.TreePattern.CURRENT_LIKE | ||||||||||||
| import org.apache.spark.sql.catalyst.types.DataTypeUtils | ||||||||||||
| import org.apache.spark.sql.catalyst.util.TypeUtils.{toSQLExpr, toSQLId} | ||||||||||||
| import org.apache.spark.sql.types.{StructField, StructType} | ||||||||||||
|
|
||||||||||||
| /** | ||||||||||||
| * An analyzer rule that replaces [[UnresolvedInlineTable]] with [[LocalRelation]]. | ||||||||||||
| * An analyzer rule that replaces [[UnresolvedInlineTable]] with [[ResolvedInlineTable]]. | ||||||||||||
| */ | ||||||||||||
| object ResolveInlineTables extends Rule[LogicalPlan] | ||||||||||||
| with CastSupport with AliasHelper with EvalHelper { | ||||||||||||
| override def apply(plan: LogicalPlan): LogicalPlan = plan.resolveOperatorsWithPruning( | ||||||||||||
| AlwaysProcess.fn, ruleId) { | ||||||||||||
| case table: UnresolvedInlineTable if table.expressionsResolved => | ||||||||||||
| validateInputDimension(table) | ||||||||||||
| validateInputEvaluable(table) | ||||||||||||
| convert(table) | ||||||||||||
| override def apply(plan: LogicalPlan): LogicalPlan = { | ||||||||||||
| plan.resolveOperatorsWithPruning(AlwaysProcess.fn, ruleId) { | ||||||||||||
| case table: UnresolvedInlineTable if table.expressionsResolved => | ||||||||||||
| validateInputDimension(table) | ||||||||||||
| validateInputEvaluable(table) | ||||||||||||
| val resolvedTable = findCommonTypesAndCast(table) | ||||||||||||
| earlyEvalIfPossible(resolvedTable) | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| /** | ||||||||||||
|
|
@@ -74,7 +75,10 @@ object ResolveInlineTables extends Rule[LogicalPlan] | |||||||||||
| table.rows.foreach { row => | ||||||||||||
| row.foreach { e => | ||||||||||||
| // Note that nondeterministic expressions are not supported since they are not foldable. | ||||||||||||
| if (!e.resolved || !trimAliases(prepareForEval(e)).foldable) { | ||||||||||||
| // Only exception are CURRENT_LIKE expressions, which are replaced by a literal | ||||||||||||
| // In later stages. | ||||||||||||
| if ((!e.resolved && !e.containsPattern(CURRENT_LIKE)) | ||||||||||||
| || !trimAliases(prepareForEval(e)).foldable) { | ||||||||||||
| e.failAnalysis( | ||||||||||||
| errorClass = "INVALID_INLINE_TABLE.CANNOT_EVALUATE_EXPRESSION_IN_INLINE_TABLE", | ||||||||||||
| messageParameters = Map("expr" -> toSQLExpr(e))) | ||||||||||||
|
|
@@ -84,14 +88,12 @@ object ResolveInlineTables extends Rule[LogicalPlan] | |||||||||||
| } | ||||||||||||
|
|
||||||||||||
| /** | ||||||||||||
| * Convert a valid (with right shape and foldable inputs) [[UnresolvedInlineTable]] | ||||||||||||
| * into a [[LocalRelation]]. | ||||||||||||
| * | ||||||||||||
| * This function attempts to coerce inputs into consistent types. | ||||||||||||
| * | ||||||||||||
| * This is package visible for unit testing. | ||||||||||||
| */ | ||||||||||||
| private[analysis] def convert(table: UnresolvedInlineTable): LocalRelation = { | ||||||||||||
| private[analysis] def findCommonTypesAndCast(table: UnresolvedInlineTable): | ||||||||||||
| ResolvedInlineTable = { | ||||||||||||
| // For each column, traverse all the values and find a common data type and nullability. | ||||||||||||
| val fields = table.rows.transpose.zip(table.names).map { case (column, name) => | ||||||||||||
| val inputTypes = column.map(_.dataType) | ||||||||||||
|
|
@@ -105,26 +107,31 @@ object ResolveInlineTables extends Rule[LogicalPlan] | |||||||||||
| val attributes = DataTypeUtils.toAttributes(StructType(fields)) | ||||||||||||
| assert(fields.size == table.names.size) | ||||||||||||
|
|
||||||||||||
| val newRows: Seq[InternalRow] = table.rows.map { row => | ||||||||||||
| InternalRow.fromSeq(row.zipWithIndex.map { case (e, ci) => | ||||||||||||
| val targetType = fields(ci).dataType | ||||||||||||
| try { | ||||||||||||
| val castedRows: Seq[Seq[Expression]] = table.rows.map { row => | ||||||||||||
| row.zipWithIndex.map { | ||||||||||||
| case (e, ci) => | ||||||||||||
| val targetType = fields(ci).dataType | ||||||||||||
| val castedExpr = if (DataTypeUtils.sameType(e.dataType, targetType)) { | ||||||||||||
| e | ||||||||||||
| } else { | ||||||||||||
| cast(e, targetType) | ||||||||||||
| } | ||||||||||||
| prepareForEval(castedExpr).eval() | ||||||||||||
| } catch { | ||||||||||||
| case NonFatal(ex) => | ||||||||||||
| table.failAnalysis( | ||||||||||||
| errorClass = "INVALID_INLINE_TABLE.FAILED_SQL_EXPRESSION_EVALUATION", | ||||||||||||
| messageParameters = Map("sqlExpr" -> toSQLExpr(e)), | ||||||||||||
| cause = ex) | ||||||||||||
| } | ||||||||||||
| }) | ||||||||||||
| castedExpr | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| LocalRelation(attributes, newRows) | ||||||||||||
| ResolvedInlineTable(castedRows, attributes) | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| /** | ||||||||||||
| * This function attempts to early evaluate rows in inline table. | ||||||||||||
| * If evaluation doesn't rely on non-deterministic expressions (e.g. current_like) | ||||||||||||
| * expressions will be evaluated and inlined as [[LocalRelation]] | ||||||||||||
| * This is package visible for unit testing. | ||||||||||||
| */ | ||||||||||||
| private[analysis] def earlyEvalIfPossible(table: ResolvedInlineTable): LogicalPlan = { | ||||||||||||
| def earlyEvalPossible = | ||||||||||||
| table.rows.flatten.forall(!_.containsPattern(CURRENT_LIKE)) | ||||||||||||
| if (earlyEvalPossible) EvalInlineTables(table) else table | ||||||||||||
|
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.
Suggested change
|
||||||||||||
| } | ||||||||||||
| } | ||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,14 +19,19 @@ package org.apache.spark.sql.catalyst.optimizer | |
|
|
||
| import java.time.{Instant, LocalDateTime, ZoneId} | ||
|
|
||
| import org.apache.spark.sql.catalyst.CurrentUserContext | ||
| import scala.util.control.NonFatal | ||
|
|
||
| import org.apache.spark.sql.catalyst.{CurrentUserContext, InternalRow} | ||
| import org.apache.spark.sql.catalyst.analysis.{CastSupport, ResolvedInlineTable} | ||
| import org.apache.spark.sql.catalyst.analysis.ResolveInlineTables.prepareForEval | ||
| import org.apache.spark.sql.catalyst.expressions._ | ||
| import org.apache.spark.sql.catalyst.plans.logical._ | ||
| import org.apache.spark.sql.catalyst.rules._ | ||
| import org.apache.spark.sql.catalyst.trees.{AlwaysProcess, TreePatternBits} | ||
| import org.apache.spark.sql.catalyst.trees.TreePattern._ | ||
| import org.apache.spark.sql.catalyst.trees.TreePatternBits | ||
| import org.apache.spark.sql.catalyst.util.DateTimeUtils | ||
| import org.apache.spark.sql.catalyst.util.DateTimeUtils.{convertSpecialDate, convertSpecialTimestamp, convertSpecialTimestampNTZ, instantToMicros, localDateTimeToMicros} | ||
| import org.apache.spark.sql.catalyst.util.TypeUtils.toSQLExpr | ||
| import org.apache.spark.sql.connector.catalog.CatalogManager | ||
| import org.apache.spark.sql.types._ | ||
|
|
||
|
|
@@ -70,6 +75,33 @@ object RewriteNonCorrelatedExists extends Rule[LogicalPlan] { | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * Computes expressions in inline tables. This rule is supposed to be called at the very end | ||
| * of the analysis phase, given that all the expressions need to be fully resolved/replaced | ||
| * at this point. | ||
| */ | ||
| object EvalInlineTables extends Rule[LogicalPlan] with CastSupport { | ||
| override def apply(plan: LogicalPlan): LogicalPlan = plan.transformDownWithSubqueriesAndPruning( | ||
| AlwaysProcess.fn, ruleId) { | ||
|
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. let's add pruning for |
||
| case table: ResolvedInlineTable => | ||
| val newRows: Seq[InternalRow] = | ||
| table.rows.map { row => InternalRow.fromSeq( | ||
| row.map { e => | ||
|
dbatomic marked this conversation as resolved.
Outdated
|
||
| try { | ||
| prepareForEval(e).eval() | ||
| } catch { | ||
| case NonFatal(ex) => | ||
| table.failAnalysis( | ||
| errorClass = "INVALID_INLINE_TABLE.FAILED_SQL_EXPRESSION_EVALUATION", | ||
| messageParameters = Map("sqlExpr" -> toSQLExpr(e)), | ||
| cause = ex) | ||
| }}) | ||
| } | ||
|
|
||
| LocalRelation(table.output, newRows) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Computes the current date and time to make sure we return the same result in a single query. | ||
| */ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -271,3 +271,11 @@ select * from values (10 + try_divide(5, 0)) | |
| struct<col1:double> | ||
| -- !query output | ||
| NULL | ||
|
|
||
|
|
||
| -- !query | ||
| select count(distinct ct) from values now(), now(), now() as data(ct) | ||
|
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. We can add a test for |
||
| -- !query schema | ||
| struct<count(DISTINCT ct):bigint> | ||
| -- !query output | ||
| 1 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -78,6 +78,11 @@ class DateFunctionsSuite extends QueryTest with SharedSparkSession { | |
|
|
||
| // Execution in one query should return the same value | ||
| checkAnswer(sql("""SELECT CURRENT_TIMESTAMP() = CURRENT_TIMESTAMP()"""), Row(true)) | ||
| checkAnswer(sql( | ||
| """SELECT COUNT(DISTINCT ct) FROM VALUES | ||
| | CURRENT_TIMESTAMP(), | ||
| | CURRENT_TIMESTAMP(), | ||
| | CURRENT_TIMESTAMP() as data(ct)""".stripMargin), Row(1)) | ||
|
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. This seems not needed as we have the same test in the golden file |
||
|
|
||
| // Current timestamp should return the current timestamp ... | ||
| val before = System.currentTimeMillis | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
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.
It seems we only need theSeq[Expression]here.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.
it's a table (rows X columns)
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.
I know that. You means the X columns for each row is different?
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.
I got it now. Thank you!