cloud-fan commented on code in PR #58317:
URL: https://github.com/apache/spark/pull/58317#discussion_r4107563513
##########
sql/core/src/test/scala/org/apache/spark/sql/connector/DataSourceV2Suite.scala:
##########
@@ -505,6 +506,51 @@ class DataSourceV2Suite extends SharedSparkSession with
AdaptiveSparkPlanHelper
}
}
+ test("SPARK-58814: catalog-less V2 writes recache both CHAR/VARCHAR scan
modes") {
+ val format = classOf[CharVarcharWritableDataSource].getName
+ val preserveConf = Seq(
+ SQLConf.PRESERVE_CHAR_VARCHAR_TYPE_INFO.key -> "true",
+ SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "false")
+ val standardConf = Seq(SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key ->
"true")
+
+ withTempPath { file =>
+ val path = file.getCanonicalPath
+ def readData: DataFrame = spark.read.format(format).option("path",
path).load()
+ def appendData(c: String, v: String): Unit = {
+ Seq((c, v)).toDF("c", "v").write
+ .format(format).option("path", path).mode("append").save()
+ }
+
+ appendData("a", "x")
+ val preserveRead = withSQLConf(preserveConf: _*) {
+ val read = readData.persist(MEMORY_ONLY)
+ checkAnswer(read, Row("a", "x"))
+ read
+ }
+ val standardRead = withSQLConf(standardConf: _*) {
+ val read = readData.persist(DISK_ONLY)
+ checkAnswer(read, Row("a", "x"))
+ read
+ }
+ try {
+ appendData("b", "y")
+ withSQLConf(preserveConf: _*) {
+ checkAnswer(readData, Seq(Row("a", "x"), Row("b", "y")))
Review Comment:
**Non-blocking (P2):** These assertions execute a newly constructed
`readData`, not the cached `preserveRead` and `standardRead` DataFrames.
Because `getTable` returns a fresh identity-based `CharVarcharTable`, a broken
`recacheByV2Relation` can match no retained entry while the new read still sees
the appended files; the storage-level lookup also does not prove that the
cached contents were refreshed. Please run `checkAnswer` on the original cached
DataFrames after the append, and do the same in the catalog-less streaming
test, so stale entries make the regression fail.
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/CacheManager.scala:
##########
@@ -422,7 +523,9 @@ class CacheManager extends Logging with
AdaptiveSparkPlanHelper {
case r @ ExtractV2CatalogAndIdentifier(catalog, ident) if
r.timeTravelSpec.isEmpty =>
val table = CatalogV2Util.getTable(catalog, ident, options =
r.options)
if (r.table.id == table.id) {
- Some(DataSourceV2Relation.create(table, Some(catalog),
Some(ident), r.options))
+ Some(DataSourceV2Relation
Review Comment:
**Non-blocking (P2):** This restores only the relation's
`charVarcharScanMode`; the refreshed plan is still analyzed under the mutating
session's SQLConf. If a PreserveNative cache is rebuilt by a SparkStandard
write or refresh, the child remains PreserveNative while `ApplyCharTypePadding`
can generate a SparkStandard read-side Project, leaving a plan that matches
neither intended cache variant. Please replay the captured policy as a unit,
for example by analyzing the full variant in an isolated session configured for
that mode, so the relation and generated Project stay coherent.
See **Shared repair plan 1** in the review body.
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/RelationResolution.scala:
##########
@@ -591,7 +594,10 @@ class RelationResolution(
cached transform {
case r: DataSourceV2Relation if matchesReference(r, ref) =>
V2TableReferenceUtils.validateLoadedTable(r.table, ref)
- r.copy(output = ref.output, options = ref.options)
+ r.copy(
+ output = ref.output,
+ options = ref.options,
+ charVarcharScanMode = None)
Review Comment:
**Blocking (P1):** Clearing only the reloaded child's mode does not rebind
the analyzer-generated read-side `Project` retained by the temporary view. A
view created under PreserveNative can keep its preserve projection while a
SparkStandard lookup rebinds the child to a physical STRING scan; because the
first projection already cleaned the child metadata, `ApplyCharTypePadding`
cannot synthesize the missing `varcharTypeReadSideCheck`, and `abcdef` can
escape from VARCHAR(4) instead of raising `EXCEED_LIMIT_LENGTH`. Please
reconstruct the relation and generated Project together under the current
policy, or preserve both together as one captured policy.
See **Shared repair plan 1** in the review body.
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/InsertIntoDataSourceCommand.scala:
##########
@@ -45,7 +45,8 @@ case class InsertIntoDataSourceCommand(
// Re-cache all cached plans(including this relation itself, if it's
cached) that refer to this
// data source relation.
- sparkSession.sharedState.cacheManager.recacheByPlan(sparkSession,
logicalRelation)
+ sparkSession.sharedState.cacheManager.recacheByV1Relation(
Review Comment:
Confirmed: the insert regression now uses a CHAR/VARCHAR relation, binds
both modes, retains both caches through overwrite, and checks their replacement
rows.
<!-- SPARK_DEV_REVIEW_REPLY
{"feedback_id":"inline:3962201895","thread_id":"inline:3962201895","verdict_sha256":"1821e60da6a15b8042b8aae53c3feec24e342c5cf8056261204a976d216fdfa6"}
-->
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/util/CharVarcharScanMode.scala:
##########
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.spark.sql.catalyst.util
+
+/**
+ * The CHAR/VARCHAR scan mode bound to a relation (and its scan) during
analysis.
+ *
+ * A relation carries `Option[CharVarcharScanMode]`: `None` means no mode was
bound, including for
+ * a relation with no CHAR/VARCHAR columns. A `Some` value pins the mode so
that `sameResult` /
+ * cache reuse keep the two variants distinct.
+ */
+private[sql] sealed trait CharVarcharScanMode
+
+/**
+ * A scan builder that accepts the CHAR/VARCHAR mode captured during relation
analysis.
+ *
+ * For example, if `SELECT c FROM t` is analyzed with standard semantics
enabled, the relation
+ * binds [[CharVarcharScanMode.SparkStandard]] before the builder creates the
physical scan.
+ */
+private[sql] trait SupportsCharVarcharScanMode {
+ def bindCharVarcharScanMode(mode: CharVarcharScanMode): Unit
+}
+
+private[sql] object CharVarcharScanMode {
+ /**
+ * Preserve the native, constrained CHAR/VARCHAR types of the source (e.g.
native ORC
+ * padding/truncation). Corresponds to preserve-only semantics.
+ */
+ case object PreserveNative extends CharVarcharScanMode
+
+ /**
+ * Request physical STRING from the source so Spark observes the original
value and applies
Review Comment:
**Nit (P3):** `SparkStandard` is also bound to `HiveTableRelation`, but with
`CONVERT_METASTORE_ORC=false` the native reader does not consume it; the new
Hive test explicitly expects native truncation to `"abcd"`. This comment
therefore overstates what binding the mode guarantees. Please qualify the
physical-STRING and Spark-side length-check behavior to mode-aware readers and
document the native Hive exception.
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/CacheManager.scala:
##########
@@ -348,6 +349,47 @@ class CacheManager extends Logging with
AdaptiveSparkPlanHelper {
recacheByCondition(spark, _.plan.exists(_.sameResult(normalized)))
}
+ /**
+ * Re-caches every entry whose plan contains a [[LogicalRelation]] for
`relation`.
+ * Unlike [[recacheByPlan]], this ignores CHAR/VARCHAR scan-mode identity so
a V1 write
+ * invalidates preserve-only, standard, and unbound cache entries for that
BaseRelation.
+ */
+ def recacheByV1Relation(spark: SparkSession, relation: BaseRelation): Unit =
{
+ recacheByCondition(spark, cd => cd.plan.exists {
+ case logical: LogicalRelation => logical.relation == relation
+ case _ => false
+ })
+ }
+
+ /**
+ * Re-caches every entry whose plan contains the given catalog-less
[[DataSourceV2Relation]].
+ * The scan mode is ignored only for this mutation-specific match so an
unbound write target
+ * invalidates preserve-native and standard cache entries without weakening
normal cache identity.
+ */
+ def recacheByV2Relation(spark: SparkSession, relation:
DataSourceV2Relation): Unit = {
+ val unboundRelation = relation.copy(charVarcharScanMode = None)
+ recacheByCondition(spark, cd => cd.plan.exists {
+ case cached: DataSourceV2Relation =>
+ cached.copy(charVarcharScanMode = None).sameResult(unboundRelation)
+ case _ => false
+ })
+ }
+
+ /**
+ * Looks up direct cache entries for a V2 table mutation while ignoring only
their analyzed
+ * CHAR/VARCHAR scan mode. Normal cache substitution remains mode-sensitive.
+ */
+ def lookupCachedDataByV2Relation(relation: DataSourceV2Relation):
Seq[CachedData] = {
+ val unboundRelation = relation.copy(charVarcharScanMode = None)
+ cachedData.filter { cd =>
+ EliminateSubqueryAliases(cd.plan) match {
Review Comment:
Confirmed: direct named V2 cache discovery now recognizes the
analyzer-generated CHAR/VARCHAR Project and matches the contained relation
without depending on the cache display name.
<!-- SPARK_DEV_REVIEW_REPLY
{"feedback_id":"inline:4009041874","thread_id":"inline:4009041874","verdict_sha256":"1821e60da6a15b8042b8aae53c3feec24e342c5cf8056261204a976d216fdfa6"}
-->
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/RenameTableExec.scala:
##########
@@ -49,9 +50,11 @@ case class RenameTableExec(
} else newIdent
catalog.renameTable(oldIdent, qualifiedNewIdent)
Review Comment:
Confirmed: rename now loads the destination table once when descriptors
exist and reuses that table for every restored cache variant.
<!-- SPARK_DEV_REVIEW_REPLY
{"feedback_id":"inline:4009041896","thread_id":"inline:4009041896","verdict_sha256":"1821e60da6a15b8042b8aae53c3feec24e342c5cf8056261204a976d216fdfa6"}
-->
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/CacheManager.scala:
##########
@@ -422,7 +464,9 @@ class CacheManager extends Logging with
AdaptiveSparkPlanHelper {
case r @ ExtractV2CatalogAndIdentifier(catalog, ident) if
r.timeTravelSpec.isEmpty =>
val table = CatalogV2Util.getTable(catalog, ident, options =
r.options)
if (r.table.id == table.id) {
- Some(DataSourceV2Relation.create(table, Some(catalog),
Some(ident), r.options))
+ Some(DataSourceV2Relation
+ .create(table, Some(catalog), Some(ident), r.options)
+ .copy(charVarcharScanMode = r.charVarcharScanMode))
Review Comment:
Thanks; SPARK-59751 records the explicit Legacy identity and three-state
lifecycle work. This review is treating that baseline behavior separately from
PR-attributable defects in the two bound modes.
<!-- SPARK_DEV_REVIEW_REPLY
{"feedback_id":"inline:4015284288","thread_id":"inline:4015284288","verdict_sha256":"1821e60da6a15b8042b8aae53c3feec24e342c5cf8056261204a976d216fdfa6"}
-->
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/DataSourceScanExec.scala:
##########
@@ -715,6 +715,9 @@ trait FileSourceScanLike extends DataSourceScanExec with
SessionStateHelper {
* @param tableIdentifier Identifier for the table in the metastore.
* @param disableBucketedScan Disable bucketed scan based on physical query
plan, see rule
* [[DisableUnnecessaryBucketedScan]] for details.
+ * @param charVarcharScanMode Analyzed CHAR/VARCHAR scan mode. Compared by
+ * sameResult so preserve-only and standard scans
are
+ * not reused. None means unbound (native ORC
types).
Review Comment:
Confirmed: the scan documentation now states the generic overload contract
first and limits native constrained versus physical STRING decoding to ORC.
<!-- SPARK_DEV_REVIEW_REPLY
{"feedback_id":"inline:4015284297","thread_id":"inline:4015284297","verdict_sha256":"1821e60da6a15b8042b8aae53c3feec24e342c5cf8056261204a976d216fdfa6"}
-->
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]