cloud-fan commented on code in PR #56919:
URL: https://github.com/apache/spark/pull/56919#discussion_r3510583454


##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/xml/XmlInferSchema.scala:
##########
@@ -312,41 +338,72 @@ class XmlInferSchema(private val options: XmlOptions, 
private val caseSensitive:
   }
 
   private def inferFrom(datum: String): DataType = {
+    // Start type inference from scratch, without any prior knowledge of the 
field's type.
+    inferFrom(datum, NullType)
+  }
+
+  /**
+   * Infer the type of a primitive value, refining a type already inferred for 
the same field
+   * from previous values.
+   *
+   * Rather than probing every candidate type from scratch on every value (the 
historical
+   * approach), this dispatches on `typeSoFar` -- the type accumulated for 
this field so far --
+   * and only attempts to widen it. Given a value already known to be 
`Double`, for example,
+   * there is no point checking whether it is a `Long`, as the merged type can 
only be `Double`
+   * or wider. This mirrors the schema inference used by the CSV datasource
+   * (`CSVInferSchema.inferField`) and keeps the type lattice monotonic: each 
value can only
+   * hold the type where it is or widen it, never narrow it. `compatibleType` 
reconciles the
+   * newly inferred type with `typeSoFar`, falling back to `StringType` (the 
top type) when they
+   * are incompatible.
+   */
+  private[xml] def inferFrom(datum: String, typeSoFar: DataType): DataType = {
     val value = if (datum != null && options.ignoreSurroundingSpaces) {
       datum.trim()
     } else {
       datum
     }
 
-    if (options.inferSchema) {
-      lazy val decimalTry = tryParseDecimal(value)
-      lazy val timestampNTZTry = tryParseTimestampNTZ(value)
-      value match {
-        case null => NullType
-        case v if v.isEmpty => NullType
-        case v if isLong(v) => LongType
-        case v if options.prefersDecimal && decimalTry.isDefined => 
decimalTry.get
-        case v if isDouble(v) => DoubleType
-        case v if isBoolean(v) => BooleanType
-        case v if isTimeTypeEnabled && isTime(v) => 
TimeType(TimeType.DEFAULT_PRECISION)
-        case v if isDate(v) => DateType
-        case v if timestampNTZTry.isDefined => timestampNTZTry.get
-        case v if isTimestamp(v) => TimestampType
-        case _ => StringType
-      }
-    } else {
-      StringType
-    }
+    if (!options.inferSchema) {
+      return StringType
+    }
+    if (value == null || value.isEmpty) {
+      return typeSoFar
+    }
+
+    val inferredType = typeSoFar match {
+      // For a numeric type inferred so far, re-enter the cascade at its top 
(`tryParseLong`)
+      // rather than at the narrower numeric parser. Entering at 
`tryParseDecimal` would infer an
+      // integer value directly as a narrow `Decimal` (e.g. "5" -> 
Decimal(1,0)) instead of the
+      // `Long` that from-scratch inference produces; `compatibleType` then 
merges those to
+      // different Decimal precisions (Decimal(5,2) vs Decimal(22,2)), which 
would make the
+      // inferred type differ from the legacy path and depend on row order. 
Re-entering at the top
+      // yields the same representative as from-scratch inference, so the 
merged type matches.
+      case NullType | LongType | DoubleType | _: DecimalType => 
tryParseLong(value)
+      case BooleanType => tryParseBoolean(value)
+      case _: TimeType => tryParseTime(value)
+      case DateType => tryParseDate(value)
+      case TimestampNTZType | _: TimestampNTZNanosType => 
tryParseTimestampNTZ(value)
+      case TimestampType => tryParseTimestamp(value)

Review Comment:
   Good catch, and confirmed empirically -- you're right. Fixed symmetrically 
with the numeric case: all temporal `typeSoFar` 
(`Time`/`Date`/`NTZ`/`Timestamp`) now re-enter at the top of the temporal 
sub-cascade (`tryParseTime`, flowing Time -> Date -> TimestampNTZ -> 
Timestamp), which is exactly the tail from-scratch inference reaches via 
`tryParseDouble`. So a date-only value infers as `Date` rather than falling 
through to `String`, and `findWiderDateTimeType` widens `Date` + 
`Timestamp`/`NTZ` identically on both paths. Skipping the numeric parsers on 
this branch stays safe (numeric<->temporal merges to `String` either way).
   
   Also added:
   - a direct unit assertion `inferFrom("2024-01-15", TimestampType) == 
TimestampType` (under `timestampFormat = "yyyy-MM-dd'T'HH:mm:ss"`) in 
`XmlInferSchemaTypeCastingSuite` -- `String` before the fix, `Timestamp` after 
-- plus the NTZ case (`inferFrom("2024-01-15", TimestampNTZType) == 
TimestampNTZType`, since `DATE` adopts the other side's family);
   - the reverse-order temporal case (timestamp-first and date-first, both with 
an explicit time-requiring `timestampFormat`) to the single-partition 
equivalence test.
   
   Pushed in 27a071c951e.



-- 
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]

Reply via email to