Skip to content
Closed
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
74a76c2
New and legacy time parser
MaxGekk Nov 24, 2018
63cf611
Add config spark.sql.legacy.timeParser.enabled
MaxGekk Nov 24, 2018
2a2ab83
Fallback legacy parser
MaxGekk Nov 24, 2018
667bf9f
something
MaxGekk Nov 24, 2018
227a7bd
Using instances
MaxGekk Nov 25, 2018
73ee560
Added generator
MaxGekk Nov 25, 2018
f35f6e1
Refactoring of TimeFormatter
MaxGekk Nov 25, 2018
1c09b58
Renaming to DateTimeFormatter
MaxGekk Nov 25, 2018
7b213d5
Added DateFormatter
MaxGekk Nov 25, 2018
242ba47
Default values in parsing
MaxGekk Nov 25, 2018
db48ee6
Parse as date type because format for timestamp is not not matched to…
MaxGekk Nov 25, 2018
e18841b
Fix tests
MaxGekk Nov 25, 2018
8db0238
CSVSuite passed
MaxGekk Nov 25, 2018
0b9ed92
Fix imports
MaxGekk Nov 26, 2018
799ebb3
Revert test back
MaxGekk Nov 26, 2018
5a22391
Set timeZone
MaxGekk Nov 26, 2018
f287b77
Removing default for micros because it causes conflicts in parsing
MaxGekk Nov 26, 2018
52074f7
Set timezone otherwise default is using
MaxGekk Nov 27, 2018
647b09c
Removing CSVOptions param from CsvInferSchema methods
MaxGekk Nov 29, 2018
4d6c86b
Use constants
MaxGekk Nov 29, 2018
6552dcf
Merge remote-tracking branch 'origin/master' into time-parser
MaxGekk Nov 30, 2018
f3f46c7
Merging followup
MaxGekk Nov 30, 2018
3f3ca70
Updating the migration guide
MaxGekk Dec 1, 2018
1dd9ed1
Inlining method's arguments
MaxGekk Dec 1, 2018
83bf58b
Additional fallback
MaxGekk Dec 1, 2018
00509d3
Removing unrelated changes
MaxGekk Dec 1, 2018
9b0570e
Merge remote-tracking branch 'origin/master' into time-parser
MaxGekk Dec 2, 2018
e9d6bb0
Using floorDiv to take days from seconds
MaxGekk Dec 2, 2018
1ad1184
A test for roundtrip timestamp parsing
MaxGekk Dec 2, 2018
f8097b4
Tests for DateTimeFormatter
MaxGekk Dec 2, 2018
3848795
Fix typo
MaxGekk Dec 3, 2018
60c0974
Merge remote-tracking branch 'fork/time-parser' into time-parser
MaxGekk Dec 3, 2018
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
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,16 @@ import scala.util.control.Exception.allCatch

import org.apache.spark.rdd.RDD
import org.apache.spark.sql.catalyst.analysis.TypeCoercion
import org.apache.spark.sql.catalyst.util.DateTimeUtils
import org.apache.spark.sql.catalyst.util.DateTimeFormatter
import org.apache.spark.sql.types._

object CSVInferSchema {
class CSVInferSchema(val options: CSVOptions) extends Serializable {

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 we get the CSVOptions in the constructor, shall we remove it as a parameter of the several methods? it is pretty confusing which one is used right now...


@transient
private lazy val timeParser = DateTimeFormatter(
options.timestampFormat,
options.timeZone,
options.locale)

/**
* Similar to the JSON schema inference
Expand Down Expand Up @@ -154,10 +160,7 @@ object CSVInferSchema {

private def tryParseTimestamp(field: String, options: CSVOptions): DataType = {
// This case infers a custom `dataFormat` is set.
if ((allCatch opt options.timestampFormat.parse(field)).isDefined) {
TimestampType
} else if ((allCatch opt DateTimeUtils.stringToTime(field)).isDefined) {
// We keep this for backwards compatibility.
if ((allCatch opt timeParser.parse(field)).isDefined) {
TimestampType
} else {
tryParseBoolean(field, options)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,13 +146,10 @@ class CSVOptions(
// A language tag in IETF BCP 47 format
val locale: Locale = parameters.get("locale").map(Locale.forLanguageTag).getOrElse(Locale.US)

// Uses `FastDateFormat` which can be direct replacement for `SimpleDateFormat` and thread-safe.
val dateFormat: FastDateFormat =
FastDateFormat.getInstance(parameters.getOrElse("dateFormat", "yyyy-MM-dd"), locale)
val dateFormat: String = parameters.getOrElse("dateFormat", "yyyy-MM-dd")

val timestampFormat: FastDateFormat =
FastDateFormat.getInstance(
parameters.getOrElse("timestampFormat", "yyyy-MM-dd'T'HH:mm:ss.SSSXXX"), timeZone, locale)
val timestampFormat: String =
parameters.getOrElse("timestampFormat", "yyyy-MM-dd'T'HH:mm:ss.SSSXXX")

val multiLine = parameters.get("multiLine").map(_.toBoolean).getOrElse(false)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import java.io.Writer
import com.univocity.parsers.csv.CsvWriter

import org.apache.spark.sql.catalyst.InternalRow
import org.apache.spark.sql.catalyst.util.DateTimeUtils
import org.apache.spark.sql.catalyst.util.{DateFormatter, DateTimeFormatter}
import org.apache.spark.sql.types._

class UnivocityGenerator(
Expand All @@ -42,14 +42,18 @@ class UnivocityGenerator(
private val valueConverters: Array[ValueConverter] =
schema.map(_.dataType).map(makeConverter).toArray

private val timeFormatter = DateTimeFormatter(
options.timestampFormat,
options.timeZone,
options.locale)
private val dateFormatter = DateFormatter(options.dateFormat, options.timeZone, options.locale)

private def makeConverter(dataType: DataType): ValueConverter = dataType match {
case DateType =>
(row: InternalRow, ordinal: Int) =>
options.dateFormat.format(DateTimeUtils.toJavaDate(row.getInt(ordinal)))
(row: InternalRow, ordinal: Int) => dateFormatter.format(row.getInt(ordinal))

case TimestampType =>
(row: InternalRow, ordinal: Int) =>
options.timestampFormat.format(DateTimeUtils.toJavaTimestamp(row.getLong(ordinal)))
(row: InternalRow, ordinal: Int) => timeFormatter.format(row.getLong(ordinal))

case udt: UserDefinedType[_] => makeConverter(udt.sqlType)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,14 @@ package org.apache.spark.sql.catalyst.csv
import java.io.InputStream
import java.math.BigDecimal

import scala.util.Try
import scala.util.control.NonFatal

import com.univocity.parsers.csv.CsvParser

import org.apache.spark.internal.Logging
import org.apache.spark.sql.catalyst.InternalRow
import org.apache.spark.sql.catalyst.expressions.GenericInternalRow
import org.apache.spark.sql.catalyst.util.{BadRecordException, DateTimeUtils, FailureSafeParser}
import org.apache.spark.sql.catalyst.util._
import org.apache.spark.sql.types._
import org.apache.spark.unsafe.types.UTF8String

Expand Down Expand Up @@ -76,6 +75,12 @@ class UnivocityParser(

private val row = new GenericInternalRow(requiredSchema.length)

private val timeFormatter = DateTimeFormatter(
options.timestampFormat,
options.timeZone,
options.locale)
private val dateFormatter = DateFormatter(options.dateFormat, options.timeZone, options.locale)

// Retrieve the raw record string.
private def getCurrentInput: UTF8String = {
UTF8String.fromString(tokenizer.getContext.currentParsedContent().stripLineEnd)
Expand All @@ -101,7 +106,7 @@ class UnivocityParser(
//
// output row - ["A", 2]
private val valueConverters: Array[ValueConverter] = {
requiredSchema.map(f => makeConverter(f.name, f.dataType, f.nullable, options)).toArray
requiredSchema.map(f => makeConverter(f.name, f.dataType, f.nullable)).toArray
}

/**
Expand All @@ -114,8 +119,7 @@ class UnivocityParser(
def makeConverter(
name: String,
dataType: DataType,
nullable: Boolean = true,
options: CSVOptions): ValueConverter = dataType match {
nullable: Boolean = true): ValueConverter = dataType match {
case _: ByteType => (d: String) =>
nullSafeDatum(d, name, nullable, options)(_.toByte)

Expand Down Expand Up @@ -154,34 +158,16 @@ class UnivocityParser(
}

case _: TimestampType => (d: String) =>
nullSafeDatum(d, name, nullable, options) { datum =>
// This one will lose microseconds parts.
// See https://issues.apache.org/jira/browse/SPARK-10681.
Try(options.timestampFormat.parse(datum).getTime * 1000L)
.getOrElse {
// If it fails to parse, then tries the way used in 2.0 and 1.x for backwards
// compatibility.
DateTimeUtils.stringToTime(datum).getTime * 1000L
}
}
nullSafeDatum(d, name, nullable, options)(timeFormatter.parse)

case _: DateType => (d: String) =>
nullSafeDatum(d, name, nullable, options) { datum =>
// This one will lose microseconds parts.
// See https://issues.apache.org/jira/browse/SPARK-10681.x
Try(DateTimeUtils.millisToDays(options.dateFormat.parse(datum).getTime))
.getOrElse {
// If it fails to parse, then tries the way used in 2.0 and 1.x for backwards
// compatibility.
DateTimeUtils.millisToDays(DateTimeUtils.stringToTime(datum).getTime)
}
}
nullSafeDatum(d, name, nullable, options)(dateFormatter.parse)

case _: StringType => (d: String) =>
nullSafeDatum(d, name, nullable, options)(UTF8String.fromString)

case udt: UserDefinedType[_] => (datum: String) =>
makeConverter(name, udt.sqlType, nullable, options)
makeConverter(name, udt.sqlType, nullable)

// We don't actually hit this exception though, we keep it for understandability
case _ => throw new RuntimeException(s"Unsupported type: ${dataType.typeName}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,9 @@ case class SchemaOfCsv(

val header = row.zipWithIndex.map { case (_, index) => s"_c$index" }
val startType: Array[DataType] = Array.fill[DataType](header.length)(NullType)
val fieldTypes = CSVInferSchema.inferRowType(parsedOptions)(startType, row)
val st = StructType(CSVInferSchema.toStructFields(fieldTypes, header, parsedOptions))
val inferSchema = new CSVInferSchema(parsedOptions)
val fieldTypes = inferSchema.inferRowType(parsedOptions)(startType, row)
val st = StructType(inferSchema.toStructFields(fieldTypes, header, parsedOptions))
UTF8String.fromString(st.catalogString)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/*
* 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

import java.time._
import java.time.format.DateTimeFormatterBuilder
import java.time.temporal.{ChronoField, TemporalQueries}
import java.util.{Locale, TimeZone}

import scala.util.Try

import org.apache.commons.lang3.time.FastDateFormat

import org.apache.spark.sql.internal.SQLConf

sealed trait DateTimeFormatter {
def parse(s: String): Long // returns microseconds since epoch
def format(us: Long): String
}

class Iso8601DateTimeFormatter(
pattern: String,
timeZone: TimeZone,
locale: Locale) extends DateTimeFormatter {
val formatter = new DateTimeFormatterBuilder()
.appendPattern(pattern)
.parseDefaulting(ChronoField.YEAR_OF_ERA, 1970)
.parseDefaulting(ChronoField.MONTH_OF_YEAR, 1)
.parseDefaulting(ChronoField.DAY_OF_MONTH, 1)
.parseDefaulting(ChronoField.HOUR_OF_DAY, 0)
.parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0)
.parseDefaulting(ChronoField.SECOND_OF_MINUTE, 0)
.toFormatter(locale)

def toInstant(s: String): Instant = {
val temporalAccessor = formatter.parse(s)
if (temporalAccessor.query(TemporalQueries.offset()) == null) {
val localDateTime = LocalDateTime.from(temporalAccessor)
val zonedDateTime = ZonedDateTime.of(localDateTime, timeZone.toZoneId)
Instant.from(zonedDateTime)
} else {
Instant.from(temporalAccessor)
}
}

def conv(instant: Instant, secMul: Long, nanoDiv: Long): Long = {
val sec = Math.multiplyExact(instant.getEpochSecond, secMul)
val result = Math.addExact(sec, instant.getNano / nanoDiv)
result
}

def parse(s: String): Long = conv(toInstant(s), 1000000, 1000)

def format(us: Long): String = {
val secs = Math.floorDiv(us, 1000000)
val mos = Math.floorMod(us, 1000000)
val instant = Instant.ofEpochSecond(secs, mos * 1000)

formatter.withZone(timeZone.toZoneId).format(instant)
}
}

class LegacyDateTimeFormatter(
pattern: String,
timeZone: TimeZone,
locale: Locale) extends DateTimeFormatter {
val format = FastDateFormat.getInstance(pattern, timeZone, locale)

protected def toMillis(s: String): Long = format.parse(s).getTime

def parse(s: String): Long = toMillis(s) * DateTimeUtils.MICROS_PER_MILLIS

def format(us: Long): String = {
format.format(DateTimeUtils.toJavaTimestamp(us))
}
}

class LegacyFallbackDateTimeFormatter(
pattern: String,
timeZone: TimeZone,
locale: Locale) extends LegacyDateTimeFormatter(pattern, timeZone, locale) {
override def toMillis(s: String): Long = {
Try {super.toMillis(s)}.getOrElse(DateTimeUtils.stringToTime(s).getTime)
}
}

object DateTimeFormatter {
def apply(format: String, timeZone: TimeZone, locale: Locale): DateTimeFormatter = {
if (SQLConf.get.legacyTimeParserEnabled) {
new LegacyFallbackDateTimeFormatter(format, timeZone, locale)
} else {
new Iso8601DateTimeFormatter(format, timeZone, locale)
}
}
}

sealed trait DateFormatter {
def parse(s: String): Int // returns days since epoch
def format(days: Int): String
}

class Iso8601DateFormatter(
pattern: String,
timeZone: TimeZone,
locale: Locale) extends DateFormatter {

val dateTimeFormatter = new Iso8601DateTimeFormatter(pattern, timeZone, locale)

override def parse(s: String): Int = {
val seconds = dateTimeFormatter.toInstant(s).getEpochSecond
(seconds / DateTimeUtils.SECONDS_PER_DAY).toInt
}

override def format(days: Int): String = {
val instant = Instant.ofEpochSecond(days * DateTimeUtils.SECONDS_PER_DAY)
dateTimeFormatter.formatter.withZone(timeZone.toZoneId).format(instant)
}
}

class LegacyDateFormatter(
pattern: String,
timeZone: TimeZone,
locale: Locale) extends DateFormatter {
val format = FastDateFormat.getInstance(pattern, timeZone, locale)

def parse(s: String): Int = {
val milliseconds = format.parse(s).getTime
DateTimeUtils.millisToDays(milliseconds)
}

def format(days: Int): String = {
val date = DateTimeUtils.toJavaDate(days)
format.format(date)
}
}

class LegacyFallbackDateFormatter(
pattern: String,
timeZone: TimeZone,
locale: Locale) extends LegacyDateFormatter(pattern, timeZone, locale) {
override def parse(s: String): Int = {
Try(super.parse(s)).getOrElse {
DateTimeUtils.millisToDays(DateTimeUtils.stringToTime(s).getTime)
}
}
}

object DateFormatter {
def apply(format: String, timeZone: TimeZone, locale: Locale): DateFormatter = {
if (SQLConf.get.legacyTimeParserEnabled) {
new LegacyFallbackDateFormatter(format, timeZone, locale)
} else {
new Iso8601DateFormatter(format, timeZone, locale)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1610,6 +1610,14 @@ object SQLConf {
""" "... N more fields" placeholder.""")
.intConf
.createWithDefault(25)

val LEGACY_TIME_PARSER_ENABLED =
buildConf("spark.sql.legacy.timeParser.enabled")
.doc("When set to true, java.text.SimpleDateFormat is using for formatting and parsing dates " +
" and timestamps in a locale-sensitive manner. When set to false, classes from " +
"java.time.* packages are using for the same purpose.")
.booleanConf
.createWithDefault(false)
}

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

def maxToStringFields: Int = getConf(SQLConf.MAX_TO_STRING_FIELDS)

def legacyTimeParserEnabled: Boolean = getConf(SQLConf.LEGACY_TIME_PARSER_ENABLED)

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

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