Skip to content

Commit d3d28aa

Browse files
committed
Addressing Hyukjin Kwon's review comments
1 parent e0cebf4 commit d3d28aa

8 files changed

Lines changed: 57 additions & 60 deletions

File tree

python/pyspark/sql/readwriter.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -238,8 +238,9 @@ def json(self, path, schema=None, primitivesAsString=None, prefersDecimal=None,
238238
:param allowUnquotedControlChars: allows JSON Strings to contain unquoted control
239239
characters (ASCII characters with value less than 32,
240240
including tab and line feed characters) or not.
241-
:param encoding: standard encoding (charset) name, for example UTF-8, UTF-16LE and UTF-32BE.
242-
If None is set, the encoding of input JSON will be detected automatically
241+
:param encoding: allows to forcibly set one of standard basic or extended encoding for
242+
the JSON files. For example UTF-16BE, UTF-32LE. If None is set,
243+
the encoding of input JSON will be detected automatically
243244
when the multiLine option is set to ``true``.
244245
:param lineSep: defines the line separator that should be used for parsing. If None is
245246
set, it covers all ``\\r``, ``\\r\\n`` and ``\\n``.

sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/json/CreateJacksonParser.scala

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,17 @@ private[sql] object CreateJacksonParser extends Serializable {
4747
jsonFactory.createParser(record.getBytes, 0, record.getLength)
4848
}
4949

50-
def getStreamDecoder(enc: String, in: Array[Byte], length: Int): StreamDecoder = {
50+
// Jackson parsers can be ranked according to their performance:
51+
// 1. Array based with actual encoding UTF-8 in the array. This is the fastest parser
52+
// but it doesn't allow to set encoding explicitly. Actual encoding is detected automatically
53+
// by checking leading bytes of the array.
54+
// 2. InputStream based with actual encoding UTF-8 in the stream. Encoding is detected
55+
// automatically by analyzing first bytes of the input stream.
56+
// 3. Reader based parser. This is the slowest parser used here but it allows to create
57+
// a reader with specific encoding.
58+
// The method creates a reader for an array with given encoding and sets size of internal
59+
// decoding buffer according to size of input array.
60+
private def getStreamDecoder(enc: String, in: Array[Byte], length: Int): StreamDecoder = {
5161
val bais = new ByteArrayInputStream(in, 0, length)
5262
val byteChannel = Channels.newChannel(bais)
5363
val decodingBufferSize = Math.min(length, 8192)

sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/json/JSONOptions.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,9 @@ private[sql] class JSONOptions(
113113
s"""The ${enc} encoding must not be included in the blacklist when multiLine is disabled:
114114
| ${blacklist.mkString(", ")}""".stripMargin)
115115

116-
val forcingLineSep = !(multiLine == false &&
116+
val isLineSepRequired = !(multiLine == false &&
117117
Charset.forName(enc) != StandardCharsets.UTF_8 && lineSeparator.isEmpty)
118-
require(forcingLineSep, s"The lineSep option must be specified for the $enc encoding")
118+
require(isLineSepRequired, s"The lineSep option must be specified for the $enc encoding")
119119

120120
enc
121121
}
File renamed without changes.

sql/core/src/test/resources/json-tests/utf16WithBOM.json renamed to sql/core/src/test/resources/test-data/utf16WithBOM.json

File renamed without changes.

sql/core/src/test/resources/json-tests/utf32BEWithBOM.json renamed to sql/core/src/test/resources/test-data/utf32BEWithBOM.json

File renamed without changes.

sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/json/JsonBenchmarks.scala

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import org.apache.spark.sql.types.{LongType, StringType, StructType}
2424
import org.apache.spark.util.{Benchmark, Utils}
2525

2626
/**
27-
* Benchmark to measure JSON read/write performance.
27+
* The benchmarks aims to measure performance of JSON parsing when encoding is set and isn't.
2828
* To run this:
2929
* spark-submit --class <this class> --jars <spark sql test jar>
3030
*/
@@ -49,9 +49,9 @@ object JSONBenchmarks {
4949
val benchmark = new Benchmark("JSON schema inferring", rowsNum)
5050

5151
withTempPath { path =>
52-
// scalastyle:off
52+
// scalastyle:off println
5353
benchmark.out.println("Preparing data for benchmarking ...")
54-
// scalastyle:on
54+
// scalastyle:on println
5555

5656
spark.sparkContext.range(0, rowsNum, 1)
5757
.map(_ => "a")
@@ -86,9 +86,9 @@ object JSONBenchmarks {
8686
val benchmark = new Benchmark("JSON per-line parsing", rowsNum)
8787

8888
withTempPath { path =>
89-
// scalastyle:off
89+
// scalastyle:off println
9090
benchmark.out.println("Preparing data for benchmarking ...")
91-
// scalastyle:on
91+
// scalastyle:on println
9292

9393
spark.sparkContext.range(0, rowsNum, 1)
9494
.map(_ => "a")
@@ -127,9 +127,9 @@ object JSONBenchmarks {
127127
val benchmark = new Benchmark("JSON parsing of wide lines", rowsNum)
128128

129129
withTempPath { path =>
130-
// scalastyle:off
130+
// scalastyle:off println
131131
benchmark.out.println("Preparing data for benchmarking ...")
132-
// scalastyle:on
132+
// scalastyle:on println
133133

134134
spark.sparkContext.range(0, rowsNum, 1)
135135
.map { i =>

sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/json/JsonSuite.scala

Lines changed: 34 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ class TestFileFilter extends PathFilter {
4848
class JsonSuite extends QueryTest with SharedSQLContext with TestJsonData {
4949
import testImplicits._
5050

51+
def testFile(fileName: String): String = {
52+
Thread.currentThread().getContextClassLoader.getResource(fileName).toString
53+
}
54+
5155
test("Type promotion") {
5256
def checkTypePromotion(expected: Any, actual: Any) {
5357
assert(expected.getClass == actual.getClass,
@@ -2168,12 +2172,8 @@ class JsonSuite extends QueryTest with SharedSQLContext with TestJsonData {
21682172
assert(sampled.count() == ds.count())
21692173
}
21702174

2171-
def testFile(fileName: String): String = {
2172-
Thread.currentThread().getContextClassLoader.getResource(fileName).toString
2173-
}
2174-
21752175
test("SPARK-23723: json in UTF-16 with BOM") {
2176-
val fileName = "json-tests/utf16WithBOM.json"
2176+
val fileName = "test-data/utf16WithBOM.json"
21772177
val schema = new StructType().add("firstName", StringType).add("lastName", StringType)
21782178
val jsonDF = spark.read.schema(schema)
21792179
.option("multiline", "true")
@@ -2184,7 +2184,7 @@ class JsonSuite extends QueryTest with SharedSQLContext with TestJsonData {
21842184
}
21852185

21862186
test("SPARK-23723: multi-line json in UTF-32BE with BOM") {
2187-
val fileName = "json-tests/utf32BEWithBOM.json"
2187+
val fileName = "test-data/utf32BEWithBOM.json"
21882188
val schema = new StructType().add("firstName", StringType).add("lastName", StringType)
21892189
val jsonDF = spark.read.schema(schema)
21902190
.option("multiline", "true")
@@ -2194,7 +2194,7 @@ class JsonSuite extends QueryTest with SharedSQLContext with TestJsonData {
21942194
}
21952195

21962196
test("SPARK-23723: Use user's encoding in reading of multi-line json in UTF-16LE") {
2197-
val fileName = "json-tests/utf16LE.json"
2197+
val fileName = "test-data/utf16LE.json"
21982198
val schema = new StructType().add("firstName", StringType).add("lastName", StringType)
21992199
val jsonDF = spark.read.schema(schema)
22002200
.option("multiline", "true")
@@ -2209,15 +2209,15 @@ class JsonSuite extends QueryTest with SharedSQLContext with TestJsonData {
22092209
val exception = intercept[UnsupportedCharsetException] {
22102210
spark.read
22112211
.options(Map("encoding" -> invalidCharset, "lineSep" -> "\n"))
2212-
.json(testFile("json-tests/utf16LE.json"))
2212+
.json(testFile("test-data/utf16LE.json"))
22132213
.count()
22142214
}
22152215

22162216
assert(exception.getMessage.contains(invalidCharset))
22172217
}
22182218

22192219
test("SPARK-23723: checking that the encoding option is case agnostic") {
2220-
val fileName = "json-tests/utf16LE.json"
2220+
val fileName = "test-data/utf16LE.json"
22212221
val schema = new StructType().add("firstName", StringType).add("lastName", StringType)
22222222
val jsonDF = spark.read.schema(schema)
22232223
.option("multiline", "true")
@@ -2229,7 +2229,7 @@ class JsonSuite extends QueryTest with SharedSQLContext with TestJsonData {
22292229

22302230

22312231
test("SPARK-23723: specified encoding is not matched to actual encoding") {
2232-
val fileName = "json-tests/utf16LE.json"
2232+
val fileName = "test-data/utf16LE.json"
22332233
val schema = new StructType().add("firstName", StringType).add("lastName", StringType)
22342234
val exception = intercept[SparkException] {
22352235
spark.read.schema(schema)
@@ -2244,17 +2244,15 @@ class JsonSuite extends QueryTest with SharedSQLContext with TestJsonData {
22442244
assert(errMsg.contains("Malformed records are detected in record parsing"))
22452245
}
22462246

2247-
def checkEncoding(
2248-
expectedEncoding: String,
2249-
pathToJsonFiles: String,
2247+
def checkEncoding(expectedEncoding: String, pathToJsonFiles: String,
22502248
expectedContent: String): Unit = {
22512249
val jsonFiles = new File(pathToJsonFiles)
22522250
.listFiles()
22532251
.filter(_.isFile)
22542252
.filter(_.getName.endsWith("json"))
22552253
val actualContent = jsonFiles.map { file =>
22562254
new String(Files.readAllBytes(file.toPath), expectedEncoding)
2257-
}.mkString.trim.replaceAll(" ", "")
2255+
}.mkString.trim
22582256

22592257
assert(actualContent == expectedContent)
22602258
}
@@ -2265,8 +2263,7 @@ class JsonSuite extends QueryTest with SharedSQLContext with TestJsonData {
22652263
val df = spark.createDataset(Seq(("Dog", 42)))
22662264
df.write
22672265
.options(Map("encoding" -> encoding, "lineSep" -> "\n"))
2268-
.format("json").mode("overwrite")
2269-
.save(path.getCanonicalPath)
2266+
.json(path.getCanonicalPath)
22702267

22712268
checkEncoding(
22722269
expectedEncoding = encoding,
@@ -2278,9 +2275,7 @@ class JsonSuite extends QueryTest with SharedSQLContext with TestJsonData {
22782275
test("SPARK-23723: save json in default encoding - UTF-8") {
22792276
withTempPath { path =>
22802277
val df = spark.createDataset(Seq(("Dog", 42)))
2281-
df.write
2282-
.format("json").mode("overwrite")
2283-
.save(path.getCanonicalPath)
2278+
df.write.json(path.getCanonicalPath)
22842279

22852280
checkEncoding(
22862281
expectedEncoding = "UTF-8",
@@ -2296,24 +2291,19 @@ class JsonSuite extends QueryTest with SharedSQLContext with TestJsonData {
22962291
val df = spark.createDataset(Seq((0)))
22972292
df.write
22982293
.options(Map("encoding" -> encoding, "lineSep" -> "\n"))
2299-
.format("json").mode("overwrite")
2300-
.save(path.getCanonicalPath)
2294+
.json(path.getCanonicalPath)
23012295
}
23022296
}
23032297

23042298
assert(exception.getMessage == encoding)
23052299
}
23062300

2307-
test("SPARK-23723: read written json in UTF-16LE") {
2301+
test("SPARK-23723: read back json in UTF-16LE") {
23082302
val options = Map("encoding" -> "UTF-16LE", "lineSep" -> "\n")
23092303
withTempPath { path =>
2310-
val ds = spark.createDataset(Seq(
2311-
("a", 1), ("b", 2), ("c", 3))
2312-
).repartition(2)
2313-
ds.write
2314-
.options(options)
2315-
.format("json").mode("overwrite")
2316-
.save(path.getCanonicalPath)
2304+
val ds = spark.createDataset(Seq(("a", 1), ("b", 2), ("c", 3))).repartition(2)
2305+
ds.write.options(options).json(path.getCanonicalPath)
2306+
23172307
val readBack = spark
23182308
.read
23192309
.options(options)
@@ -2325,20 +2315,12 @@ class JsonSuite extends QueryTest with SharedSQLContext with TestJsonData {
23252315

23262316
def checkReadJson(lineSep: String, encoding: String, inferSchema: Boolean, id: Int): Unit = {
23272317
test(s"SPARK-23724: checks reading json in ${encoding} #${id}") {
2328-
val lineSepInBytes = {
2329-
if (lineSep.startsWith("x")) {
2330-
lineSep.replaceAll("[^0-9A-Fa-f]", "")
2331-
.sliding(2, 2).toArray.map(Integer.parseInt(_, 16).toByte)
2332-
} else {
2333-
lineSep.getBytes(encoding)
2334-
}
2335-
}
23362318
val schema = new StructType().add("f1", StringType).add("f2", IntegerType)
23372319
withTempPath { path =>
23382320
val records = List(("a", 1), ("b", 2))
23392321
val data = records
23402322
.map(rec => s"""{"f1":"${rec._1}", "f2":${rec._2}}""".getBytes(encoding))
2341-
.reduce((a1, a2) => a1 ++ lineSepInBytes ++ a2)
2323+
.reduce((a1, a2) => a1 ++ lineSep.getBytes(encoding) ++ a2)
23422324
val os = new FileOutputStream(path)
23432325
os.write(data)
23442326
os.close()
@@ -2372,15 +2354,17 @@ class JsonSuite extends QueryTest with SharedSQLContext with TestJsonData {
23722354
(11, "\u000a\u000d", "UTF-8", true),
23732355
(12, "===", "US-ASCII", false),
23742356
(13, "$^+", "utf-32le", true)
2375-
).foreach{case (i, d, c, s) => checkReadJson(d, c, s, i)}
2357+
).foreach {
2358+
case (testNum, sep, encoding, inferSchema) => checkReadJson(sep, encoding, inferSchema, testNum)
2359+
}
23762360
// scalastyle:on nonascii
23772361

23782362
test("SPARK-23724: lineSep should be set if encoding if different from UTF-8") {
23792363
val encoding = "UTF-16LE"
23802364
val exception = intercept[IllegalArgumentException] {
23812365
spark.read
23822366
.options(Map("encoding" -> encoding))
2383-
.json(testFile("json-tests/utf16LE.json"))
2367+
.json(testFile("test-data/utf16LE.json"))
23842368
.count()
23852369
}
23862370

@@ -2390,36 +2374,38 @@ class JsonSuite extends QueryTest with SharedSQLContext with TestJsonData {
23902374

23912375
private val badJson = "\u0000\u0000\u0000A\u0001AAA"
23922376

2393-
test("SPARK-23094: invalid json with leading nulls - from file (multiLine=true)") {
2394-
withTempDir { tempDir =>
2377+
test("SPARK-23094: permissively read JSON file with leading nulls when multiLine is enabled") {
2378+
withTempPath { tempDir =>
23952379
val path = tempDir.getAbsolutePath
2396-
Seq(badJson + """{"a":1}""").toDS().write.mode("overwrite").text(path)
2380+
Seq(badJson + """{"a":1}""").toDS().write.text(path)
23972381
val expected = s"""${badJson}{"a":1}\n"""
23982382
val schema = new StructType().add("a", IntegerType).add("_corrupt_record", StringType)
23992383
val df = spark.read.format("json")
2384+
.option("mode", "PERMISSIVE")
24002385
.option("multiLine", true)
24012386
.option("encoding", "UTF-8")
24022387
.schema(schema).load(path)
24032388
checkAnswer(df, Row(null, expected))
24042389
}
24052390
}
24062391

2407-
test("SPARK-23094: invalid json with leading nulls - from file (multiLine=false)") {
2408-
withTempDir { tempDir =>
2392+
test("SPARK-23094: permissively read JSON file with leading nulls when multiLine is disabled") {
2393+
withTempPath { tempDir =>
24092394
val path = tempDir.getAbsolutePath
2410-
Seq(badJson, """{"a":1}""").toDS().write.mode("overwrite").text(path)
2395+
Seq(badJson, """{"a":1}""").toDS().write.text(path)
24112396
val schema = new StructType().add("a", IntegerType).add("_corrupt_record", StringType)
24122397
val df = spark.read.format("json")
2398+
.option("mode", "PERMISSIVE")
24132399
.option("multiLine", false)
24142400
.option("encoding", "UTF-8")
24152401
.schema(schema).load(path)
24162402
checkAnswer(df, Seq(Row(1, null), Row(null, badJson)))
24172403
}
24182404
}
24192405

2420-
test("SPARK-23094: invalid json with leading nulls - from dataset") {
2406+
test("SPARK-23094: permissively parse a dataset contains JSON with leading nulls") {
24212407
checkAnswer(
2422-
spark.read.option("encoding", "UTF-8").json(Seq(badJson).toDS()),
2408+
spark.read.option("mode", "PERMISSIVE").option("encoding", "UTF-8").json(Seq(badJson).toDS()),
24232409
Row(badJson))
24242410
}
24252411
}

0 commit comments

Comments
 (0)