Skip to content

[SPARK-35912][SQL] Fix cast struct contains null value to string/struct - #33146

Closed
cfmcgrady wants to merge 3 commits into
apache:masterfrom
cfmcgrady:SPARK-35912
Closed

[SPARK-35912][SQL] Fix cast struct contains null value to string/struct#33146
cfmcgrady wants to merge 3 commits into
apache:masterfrom
cfmcgrady:SPARK-35912

Conversation

@cfmcgrady

@cfmcgradycfmcgrady commented Jun 30, 2021

Copy link
Copy Markdown
Contributor

What changes were proposed in this pull request?

This PR fixes an issue that cast the struct which contains null value to other type has a difference result when we enable/disable codegen.

Here is an example:

valschema=StructType(Seq(StructField("value",
StructType(Seq(
StructField("x", IntegerType, nullable =false),
StructField("y", IntegerType, nullable =false)
))
)))
schema.printTreeString()
// root// |-- value: struct (nullable = true)// | |-- x: integer (nullable = false)// | |-- y: integer (nullable = false)valtestDS=Seq("""{"value":{"x":1}}""").toDS
valjsonDS= spark.read.schema(schema).json(testDS)
jsonDS.show()
// incorrect result// +---------+// | value|// +---------+// |{1, null}|// +---------+
spark.sql("set spark.sql.codegen.wholeStage=false")
jsonDS.show()
// the result we expected due to y.nullable = false.// +------+// | value|// +------+// |{1, 0}|// +------+

Actually, the result should be depending on the field nullable setting. this bug also happens when we cast struct to struct.

Does this PR introduce any user-facing change?

No, only bug fix.

How was this patch tested?

New test.

@AmplabJenkins

Copy link
Copy Markdown

Can one of the admins verify this patch?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

When the actual value is null, for primitive type field, row.isNullAt(i) return ture, but row.getXXX return a default value.

For exmaple:

valr=new org.apache.spark.sql.catalyst.expressions.GenericInternalRow(Array(1, null))
println(r.getInt(0)) // 1
println(r.getInt(1)) // 0
println(r.isNullAt(1)) // true

so we cann't only check row.isNullAt(i) here, we need to do the same logical like BoundReference.doGenCode(), add nullable check.

@HyukjinKwon

Copy link
Copy Markdown
Member

how is the cache issue related to the cast?

@cfmcgrady

Copy link
Copy Markdown
ContributorAuthor

how is the cache issue related to the cast?

HI, @HyukjinKwon
The root cause is cast struct to other types, updated the pr description.

@cfmcgradycfmcgrady changed the title [WIP][SPARK-35912][SQL] Fix cast struct contains null value to string[SPARK-35912][SQL] Fix cast struct contains null value to string/structJun 30, 2021
@cfmcgrady

Copy link
Copy Markdown
ContributorAuthor

cc @cloud-fan@viirya@maropu

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.

if fields(0).nullable is false, how can row.isNullAt(0) be true?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

(I have the same question)

@cfmcgradycfmcgradyJul 1, 2021

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

If user create dataframe from spark.internalCreateDataFrame(), the row.isNullAt() may be true even though the schema nullable is false.
For instance:

valschema=StructType(Seq(
StructField("x",
StructType(Seq(
StructField("y", IntegerType, true),
StructField("z", IntegerType, false)
)))))
valrdd= spark.sparkContext.parallelize(Seq(InternalRow(InternalRow(1, null))))
valdf= spark.internalCreateDataFrame(rdd, schema)
df.show
// current master branch output// +---------+// | x|// +---------+// |{1, null}|// +---------+

Although the spark.internalCreateDataFrame() is sql package private API, but spark.read.json() and spark.read.csv() call it without null value handled.(the example show in pr description)

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.

Then we need to fix the nullability. There are so many places in the Spark codebase that relies on nullability to do optimizations. It's not possible to change all of them to not trust the nullability anymore.

Can we fix spark.read.json() to set the nullability correctly?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Ok, let me try.

@HyukjinKwon

Copy link
Copy Markdown
Member

Hey mind explaining why cast path issue is related to being cached?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit:

 Seq(true, false).foreach { nullable =>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: Seq(true, false).foreach { nullable =>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: please remove this blank.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

(I have the same question)

@cfmcgrady

Copy link
Copy Markdown
ContributorAuthor

Hey mind explaining why cast path issue is related to being cached?

Actually, the cached result is what we want. The issue is that spark.read.json is not an expected result when the input schema has a nullable setting to false. From the log, we found it's a cast operation.

=== Result of Batch Preparations ===
CollectLimit 21 CollectLimit 21
!+- Project [cast(value#5 as string) AS value#18] +- *(1) Project [cast(value#5 as string) AS value#18]
! +- Scan ExistingRDD[value#5] +- *(1) Scan ExistingRDD[value#5]

@HyukjinKwon

HyukjinKwon commented Jul 1, 2021

Copy link
Copy Markdown
Member

Shouldn't it fail instead of setting it as 0 or null? I feel like the handling should be done somewhere in JacksonParser.

@cfmcgrady

Copy link
Copy Markdown
ContributorAuthor

Shouldn't it fail instead of setting it as 0 or null? I feel like the handling should be done somewhere in JacksonParser.

Thanks for your suggestion, I'll try.

@cfmcgrady

Copy link
Copy Markdown
ContributorAuthor

Create a new PR

BTW, Shall we merge this PR? The cast issue may occur when the user create dataframe from API spark.internalCreateDataFrame(). cc @cloud-fan@maropu

@cloud-fan

Copy link
Copy Markdown
Contributor

spark.internalCreateDataFrame() is a private API and users should be responsible if they do something wrong. We can't merge this PR, because we can't sacrifice the performance in the happy path for invalid usage of private APIs from users.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@cfmcgrady@AmplabJenkins@HyukjinKwon@cloud-fan@maropu