Skip to content
Closed
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/sql-migration-guide-upgrade.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ displayTitle: Spark SQL Upgrading Guide

## Upgrading From Spark SQL 2.4 to 3.0

- In Spark version 2.4 and earlier, accepted format of decimals parsed from JSON is an optional sign ('+' or '-'), followed by a sequence of zero or more decimal digits, optionally followed by a fraction, optionally followed by an exponent. Any commas were removed from the input before parsing. Since Spark 3.0, format varies and depends on locale which can be set via JSON option `locale`. The default locale is `en-US`. To switch back to previous behavior, set `spark.sql.legacy.decimalParsing.enabled` to `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.

Is there any performance regression with DecimalFormat?

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.

I have the same question. Do we need the DecimalFormat when locale is en-US?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Is there any performance regression with DecimalFormat?

I haven't benchmarked the changes. Looking at JSONBenchmarks, we even don't check decimals there.

Do we need the DecimalFormat when locale is en-US?

No, we don't need it.

@cloud-fan cloud-fan Nov 28, 2018

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.

since the default value is en-US, can we skip DecimalFormat when locale is en-US? Then there is nothing changes by default, and we don't even need a legacy config.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I removed SQL config and record in the migration guid. Also I applied DecimalFormat only for not en-US locales.


- Since Spark 3.0, the Dataset and DataFrame API `unionAll` is not deprecated any more. It is an alias for `union`.

- In PySpark, when creating a `SparkSession` with `SparkSession.builder.getOrCreate()`, if there is an existing `SparkContext`, the builder was trying to update the `SparkConf` of the existing `SparkContext` with configurations specified to the builder, but the `SparkContext` is shared by all `SparkSession`s, so we should not update them. Since 3.0, the builder comes to not update the configurations. This is the same behavior as Java/Scala API in 2.3 and above. If you want to update them, you need to update them prior to creating a `SparkSession`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@

package org.apache.spark.sql.catalyst.expressions

import java.text.{DecimalFormat, DecimalFormatSymbols, ParsePosition}
import java.util.Locale

import org.apache.spark.sql.AnalysisException
import org.apache.spark.sql.catalyst.util.ArrayBasedMapData
import org.apache.spark.sql.types.{DataType, MapType, StringType, StructType}
Expand Down Expand Up @@ -83,4 +86,22 @@ object ExprUtils {
}
}
}

def getDecimalParser(useLegacyParser: Boolean, locale: Locale): String => java.math.BigDecimal = {
if (useLegacyParser) {
(s: String) => new java.math.BigDecimal(s.replaceAll(",", ""))
} else {
val decimalFormat = new DecimalFormat("", new DecimalFormatSymbols(locale))
decimalFormat.setParseBigDecimal(true)
(s: String) => {
val pos = new ParsePosition(0)
val result = decimalFormat.parse(s, pos).asInstanceOf[java.math.BigDecimal]
if (pos.getIndex() != s.length() || pos.getErrorIndex() != -1) {
throw new IllegalArgumentException("Cannot parse any decimal");
} else {
result
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,10 @@ import scala.util.parsing.combinator.RegexParsers

import com.fasterxml.jackson.core._

import org.apache.spark.sql.AnalysisException
import org.apache.spark.sql.catalyst.InternalRow
import org.apache.spark.sql.catalyst.analysis.TypeCheckResult
import org.apache.spark.sql.catalyst.expressions.codegen.CodegenFallback
import org.apache.spark.sql.catalyst.json._
import org.apache.spark.sql.catalyst.json.JsonInferSchema.inferField
import org.apache.spark.sql.catalyst.util._
import org.apache.spark.sql.internal.SQLConf
import org.apache.spark.sql.types._
Expand Down Expand Up @@ -775,6 +773,9 @@ case class SchemaOfJson(
factory
}

@transient
private lazy val jsonInferSchema = new JsonInferSchema(jsonOptions)

@transient
private lazy val json = child.eval().asInstanceOf[UTF8String]

Expand All @@ -787,7 +788,7 @@ case class SchemaOfJson(
override def eval(v: InternalRow): Any = {
val dt = Utils.tryWithResource(CreateJacksonParser.utf8String(jsonFactory, json)) { parser =>
parser.nextToken()
inferField(parser, jsonOptions)
jsonInferSchema.inferField(parser)
}

UTF8String.fromString(dt.catalogString)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import org.apache.spark.internal.Logging
import org.apache.spark.sql.catalyst.InternalRow
import org.apache.spark.sql.catalyst.expressions._
import org.apache.spark.sql.catalyst.util._
import org.apache.spark.sql.internal.SQLConf
import org.apache.spark.sql.types._
import org.apache.spark.unsafe.types.UTF8String
import org.apache.spark.util.Utils
Expand Down Expand Up @@ -135,6 +136,10 @@ class JacksonParser(
}
}

private val decimalParser = {
ExprUtils.getDecimalParser(SQLConf.get.legacyDecimalParsing, options.locale)
}

/**
* Create a converter which converts the JSON documents held by the `JsonParser`
* to a value according to a desired schema.
Expand Down Expand Up @@ -261,6 +266,9 @@ class JacksonParser(
(parser: JsonParser) => parseJsonToken[Decimal](parser, dataType) {
case (VALUE_NUMBER_INT | VALUE_NUMBER_FLOAT) =>
Decimal(parser.getDecimalValue, dt.precision, dt.scale)
case VALUE_STRING if parser.getTextLength >= 1 =>
val bigDecimal = decimalParser(parser.getText)
Decimal(bigDecimal, dt.precision, dt.scale)
}

case st: StructType =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,25 @@ package org.apache.spark.sql.catalyst.json

import java.util.Comparator

import scala.util.control.Exception.allCatch

import com.fasterxml.jackson.core._

import org.apache.spark.SparkException
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.catalyst.analysis.TypeCoercion
import org.apache.spark.sql.catalyst.expressions.ExprUtils
import org.apache.spark.sql.catalyst.json.JacksonUtils.nextUntil
import org.apache.spark.sql.catalyst.util.{DropMalformedMode, FailFastMode, ParseMode, PermissiveMode}
import org.apache.spark.sql.internal.SQLConf
import org.apache.spark.sql.types._
import org.apache.spark.util.Utils

private[sql] object JsonInferSchema {
private[sql] class JsonInferSchema(options: JSONOptions) extends Serializable {

private val decimalParser = {
ExprUtils.getDecimalParser(SQLConf.get.legacyDecimalParsing, options.locale)
}

/**
* Infer the type of a collection of json records in three stages:
Expand All @@ -40,21 +47,20 @@ private[sql] object JsonInferSchema {
*/
def infer[T](
json: RDD[T],
configOptions: JSONOptions,
createParser: (JsonFactory, T) => JsonParser): StructType = {
val parseMode = configOptions.parseMode
val columnNameOfCorruptRecord = configOptions.columnNameOfCorruptRecord
val parseMode = options.parseMode
val columnNameOfCorruptRecord = options.columnNameOfCorruptRecord

// In each RDD partition, perform schema inference on each row and merge afterwards.
val typeMerger = compatibleRootType(columnNameOfCorruptRecord, parseMode)
val typeMerger = JsonInferSchema.compatibleRootType(columnNameOfCorruptRecord, parseMode)
val mergedTypesFromPartitions = json.mapPartitions { iter =>
val factory = new JsonFactory()
configOptions.setJacksonOptions(factory)
options.setJacksonOptions(factory)
iter.flatMap { row =>
try {
Utils.tryWithResource(createParser(factory, row)) { parser =>
parser.nextToken()
Some(inferField(parser, configOptions))
Some(inferField(parser))
}
} catch {
case e @ (_: RuntimeException | _: JsonProcessingException) => parseMode match {
Expand Down Expand Up @@ -82,42 +88,25 @@ private[sql] object JsonInferSchema {
}
json.sparkContext.runJob(mergedTypesFromPartitions, foldPartition, mergeResult)

canonicalizeType(rootType, configOptions) match {
canonicalizeType(rootType, options) match {
case Some(st: StructType) => st
case _ =>
// canonicalizeType erases all empty structs, including the only one we want to keep
StructType(Nil)
}
}

private[this] val structFieldComparator = new Comparator[StructField] {
override def compare(o1: StructField, o2: StructField): Int = {
o1.name.compareTo(o2.name)
}
}

private def isSorted(arr: Array[StructField]): Boolean = {
var i: Int = 0
while (i < arr.length - 1) {
if (structFieldComparator.compare(arr(i), arr(i + 1)) > 0) {
return false
}
i += 1
}
true
}

/**
* Infer the type of a json document from the parser's token stream
*/
def inferField(parser: JsonParser, configOptions: JSONOptions): DataType = {
def inferField(parser: JsonParser): DataType = {
import com.fasterxml.jackson.core.JsonToken._
parser.getCurrentToken match {
case null | VALUE_NULL => NullType

case FIELD_NAME =>
parser.nextToken()
inferField(parser, configOptions)
inferField(parser)

case VALUE_STRING if parser.getTextLength < 1 =>
// Zero length strings and nulls have special handling to deal
Expand All @@ -128,18 +117,25 @@ private[sql] object JsonInferSchema {
// record fields' types have been combined.
NullType

case VALUE_STRING if options.prefersDecimal =>
val decimalTry = allCatch opt {
val bigDecimal = decimalParser(parser.getText)
DecimalType(bigDecimal.precision, bigDecimal.scale)
}
decimalTry.getOrElse(StringType)
case VALUE_STRING => StringType

case START_OBJECT =>
val builder = Array.newBuilder[StructField]
while (nextUntil(parser, END_OBJECT)) {
builder += StructField(
parser.getCurrentName,
inferField(parser, configOptions),
inferField(parser),
nullable = true)
}
val fields: Array[StructField] = builder.result()
// Note: other code relies on this sorting for correctness, so don't remove it!
java.util.Arrays.sort(fields, structFieldComparator)
java.util.Arrays.sort(fields, JsonInferSchema.structFieldComparator)
StructType(fields)

case START_ARRAY =>
Expand All @@ -148,15 +144,15 @@ private[sql] object JsonInferSchema {
// the type as we pass through all JSON objects.
var elementType: DataType = NullType
while (nextUntil(parser, END_ARRAY)) {
elementType = compatibleType(
elementType, inferField(parser, configOptions))
elementType = JsonInferSchema.compatibleType(
elementType, inferField(parser))
}

ArrayType(elementType)

case (VALUE_NUMBER_INT | VALUE_NUMBER_FLOAT) if configOptions.primitivesAsString => StringType
case (VALUE_NUMBER_INT | VALUE_NUMBER_FLOAT) if options.primitivesAsString => StringType

case (VALUE_TRUE | VALUE_FALSE) if configOptions.primitivesAsString => StringType
case (VALUE_TRUE | VALUE_FALSE) if options.primitivesAsString => StringType

case VALUE_NUMBER_INT | VALUE_NUMBER_FLOAT =>
import JsonParser.NumberType._
Expand All @@ -172,7 +168,7 @@ private[sql] object JsonInferSchema {
} else {
DoubleType
}
case FLOAT | DOUBLE if configOptions.prefersDecimal =>
case FLOAT | DOUBLE if options.prefersDecimal =>
val v = parser.getDecimalValue
if (Math.max(v.precision(), v.scale()) <= DecimalType.MAX_PRECISION) {
DecimalType(Math.max(v.precision(), v.scale()), v.scale())
Expand Down Expand Up @@ -217,20 +213,39 @@ private[sql] object JsonInferSchema {

case other => Some(other)
}
}

object JsonInferSchema {
val structFieldComparator = new Comparator[StructField] {
override def compare(o1: StructField, o2: StructField): Int = {
o1.name.compareTo(o2.name)
}
}

def isSorted(arr: Array[StructField]): Boolean = {
var i: Int = 0
while (i < arr.length - 1) {
if (structFieldComparator.compare(arr(i), arr(i + 1)) > 0) {
return false
}
i += 1
}
true
}

private def withCorruptField(
def withCorruptField(
struct: StructType,
other: DataType,
columnNameOfCorruptRecords: String,
parseMode: ParseMode) = parseMode match {
parseMode: ParseMode): StructType = parseMode match {
case PermissiveMode =>
// If we see any other data type at the root level, we get records that cannot be
// parsed. So, we use the struct as the data type and add the corrupt field to the schema.
if (!struct.fieldNames.contains(columnNameOfCorruptRecords)) {
// If this given struct does not have a column used for corrupt records,
// add this field.
val newFields: Array[StructField] =
StructField(columnNameOfCorruptRecords, StringType, nullable = true) +: struct.fields
StructField(columnNameOfCorruptRecords, StringType, nullable = true) +: struct.fields
// Note: other code relies on this sorting for correctness, so don't remove it!
java.util.Arrays.sort(newFields, structFieldComparator)
StructType(newFields)
Expand All @@ -253,7 +268,7 @@ private[sql] object JsonInferSchema {
/**
* Remove top-level ArrayType wrappers and merge the remaining schemas
*/
private def compatibleRootType(
def compatibleRootType(
columnNameOfCorruptRecords: String,
parseMode: ParseMode): (DataType, DataType) => DataType = {
// Since we support array of json objects at the top level,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1610,6 +1610,13 @@ object SQLConf {
""" "... N more fields" placeholder.""")
.intConf
.createWithDefault(25)

val LEGACY_DECIMAL_PARSING_ENABLED = buildConf("spark.sql.legacy.decimalParsing.enabled")
.doc("If it is set to false, it enables parsing decimals in locale specific formats. " +
"To switch back to previous behaviour when parsing was performed by java.math.BigDecimal " +
"and all commas were removed from the input, set the flag to true.")
.booleanConf
.createWithDefault(false)
Comment thread
MaxGekk marked this conversation as resolved.
Outdated
}

/**
Expand Down Expand Up @@ -2030,6 +2037,8 @@ class SQLConf extends Serializable with Logging {

def maxToStringFields: Int = getConf(SQLConf.MAX_TO_STRING_FIELDS)

def legacyDecimalParsing: Boolean = getConf(SQLConf.LEGACY_DECIMAL_PARSING_ENABLED)

/** ********************** SQLConf functionality methods ************ */

/** Set Spark SQL configuration properties. */
Expand Down
Loading