com.twitter.finagle.mysql.Value.scala Maven / Gradle / Ivy
package com.twitter.finagle.mysql
import com.twitter.finagle.mysql.transport.MysqlBuf
import com.twitter.util.TwitterDateFormat
import java.sql.{Date, Timestamp}
import java.text.ParsePosition
import java.util.logging.Logger
import java.util.{Calendar, TimeZone}
/**
* Defines a Value ADT that represents the domain of values
* received from a mysql server.
*/
sealed trait Value
case class ByteValue(b: Byte) extends Value
case class ShortValue(s: Short) extends Value
case class IntValue(i: Int) extends Value
case class LongValue(l: Long) extends Value
case class FloatValue(f: Float) extends Value
case class DoubleValue(d: Double) extends Value
case class StringValue(s: String) extends Value
case object EmptyValue extends Value
case object NullValue extends Value
/**
* A RawValue contains the raw bytes that represent
* a value and enough meta data to decode the
* bytes.
*
* @param typ The mysql type code for this value.
* @param charset The charset encoding of the bytes.
* @param isBinary Disambiguates between the text and binary protocol.
* @param bytes The raw bytes for this value.
*/
case class RawValue(
typ: Short,
charset: Short,
isBinary: Boolean,
bytes: Array[Byte]
) extends Value
/**
* A type class used for injecting values of a domain type `A` into
* [[com.twitter.finagle.mysql.Value Values]] for insertion into a MySQL
* database.
*/
private[mysql] trait Injectable[A] {
def apply(a: A): Value
}
/**
* A type class used for extracting [[com.twitter.finagle.mysql.Value Values]]
* into a domain type `A`.
*/
private[mysql] trait Extractable[A] {
def unapply(v: Value): Option[A]
}
/**
* An injector/extractor of [[java.sql.Timestamp]] values.
*
* @param injectionTimeZone The timezone in which
* [[java.sql.Timestamp Timestamps]] are injected into MySQL TIMESTAMP values.
* @param extractionTimeZone The timezone in which TIMESTAMP and DATETIME
* rows are extracted from database rows into [[java.sql.Timestamp Timestamps]].
*/
class TimestampValue(
val injectionTimeZone: TimeZone,
val extractionTimeZone: TimeZone)
extends Injectable[Timestamp] with Extractable[Timestamp]
{
/**
* Injects a [[java.sql.Timestamp]] into a
* [[com.twitter.finagle.mysql.RawValue]] in a given `injectionTimeZone`
*/
def apply(ts: Timestamp): Value = {
val bytes = new Array[Byte](11)
val bw = MysqlBuf.writer(bytes)
val cal = Calendar.getInstance(injectionTimeZone)
cal.setTimeInMillis(ts.getTime)
bw.writeShortLE(cal.get(Calendar.YEAR))
bw.writeByte(cal.get(Calendar.MONTH) + 1) // increment 0 indexed month
bw.writeByte(cal.get(Calendar.DATE))
bw.writeByte(cal.get(Calendar.HOUR_OF_DAY))
bw.writeByte(cal.get(Calendar.MINUTE))
bw.writeByte(cal.get(Calendar.SECOND))
bw.writeIntLE(ts.getNanos / 1000) // sub-second part is written as microseconds
RawValue(Type.Timestamp, Charset.Binary, isBinary = true, bytes)
}
/**
* Value extractor for [[java.sql.Timestamp]].
*
* Extracts timestamps in `extractionTimeZone` for values encoded in either
* the binary or text MySQL protocols.
*/
def unapply(v: Value): Option[Timestamp] = v match {
case RawValue(t, charset, false, bytes) if t == Type.Timestamp || t == Type.DateTime =>
val str = new String(bytes, Charset(charset))
val ts = fromString(str, extractionTimeZone)
Some(ts)
case RawValue(t, _, true, bytes) if t == Type.Timestamp || t == Type.DateTime =>
val ts = fromBytes(bytes, extractionTimeZone)
Some(ts)
case _ => None
}
/**
* Timestamp object that can appropriately
* represent MySQL zero Timestamp.
*/
private[this] object Zero extends Timestamp(0) {
override val getTime = 0L
override val toString = "0000-00-00 00:00:00"
}
/**
* Convert a string-encoded timestamp into a [[java.sql.Timestamp]] in a given
* timezone.
*
* Invalid DATETIME or TIMESTAMP values are converted to the “zero” value
* ('0000-00-00 00:00:00').
*
* @param str A string representing a TIMESTAMP written in the
* MySQL text protocol.
* @param timeZone The timezone in which to interpret the timestamp.
*/
private[this] def fromString(str: String, timeZone: TimeZone): Timestamp = {
if (str == Zero.toString) {
return Zero
}
val parsePosition = new ParsePosition(0)
val format = TwitterDateFormat("yyyy-MM-dd HH:mm:ss")
format.setTimeZone(extractionTimeZone)
val timeInMillis = format.parse(str, parsePosition).getTime
/**
* Extracts the fractional part of a timestamp, up to the
* nanoseconds. It takes care of padding properly so that .1
* is interpreted as 100 millis and not 1 nanoseconds (like
* SimpleDateFormat wrongly does.)
*/
object Nanos {
def unapply(str: String) : Option[Int] = {
str match {
case "" => Some(0)
case s : String if !s.startsWith(".") => None
case s : String if s.length() > 10 => None
case s : String => Some(s.stripPrefix(".").padTo(9,'0').toInt)
case _ => None
}
}
}
// Parse fractional part
str.substring(parsePosition.getIndex) match {
case Nanos(nanos) =>
val ts = new Timestamp(timeInMillis)
ts.setNanos(nanos)
ts
case _ => Zero
}
}
/**
* Convert a binary-encoded timestamp into a [[java.sql.Timestamp]] in a given
* timezone.
*
* Invalid DATETIME or TIMESTAMP values are converted to the “zero” value
* ('0000-00-00 00:00:00').
*
* @param bytes A byte-array representing a TIMESTAMP written in the
* MySQL binary protocol.
* @param timeZone The timezone in which to interpret the timestamp.
*/
private[this] def fromBytes(bytes: Array[Byte], timeZone: TimeZone): Timestamp = {
if (bytes.isEmpty) {
return Zero
}
var year, month, day, hour, min, sec, micro = 0
val br = MysqlBuf.reader(bytes)
// If the len was not zero, we can strictly
// expect year, month, and day to be included.
if (br.remaining >= 4) {
year = br.readUnsignedShortLE()
month = br.readUnsignedByte()
day = br.readUnsignedByte()
} else {
return Zero
}
// if the time-part is 00:00:00, it isn't included.
if (br.remaining >= 3) {
hour = br.readUnsignedByte()
min = br.readUnsignedByte()
sec = br.readUnsignedByte()
}
// if the sub-seconds are 0, they aren't included.
if (br.remaining >= 4) {
micro = br.readIntLE()
}
val cal = Calendar.getInstance(timeZone)
cal.set(year, month-1, day, hour, min, sec)
val ts = new Timestamp(0)
ts.setTime(cal.getTimeInMillis)
ts.setNanos(micro * 1000)
ts
}
}
/**
* Extracts a value in UTC. To use a different time zone, create an instance of
* [[com.twitter.finagle.mysql.TimestampValue]].
*/
@deprecated("Injects `java.sql.Timestamp`s in local time and extracts them in UTC." +
"To use a different time zone, create an instance of " +
"TimestampValue(InjectionTimeZone, ExtractionTimeZone)",
"6.20.2")
object TimestampValue extends TimestampValue(
TimeZone.getDefault(),
TimeZone.getTimeZone("UTC")
) {
private[this] val log = Logger.getLogger("finagle-mysql")
override def apply(ts: Timestamp): Value = {
log.warning(
"Injecting timezone-less `java.sql.Timestamp` with a hardcoded local timezone (%s)"
.format(injectionTimeZone.getID)
)
super.apply(ts)
}
override def unapply(v: Value): Option[Timestamp] = {
log.warning(
"Extracting TIMESTAMP or DATETIME row as a `java.sql.Timestamp` with a hardcoded timezone (%s)"
.format(extractionTimeZone.getID)
)
super.unapply(v)
}
}
object DateValue extends Injectable[Date] with Extractable[Date] {
/**
* Creates a RawValue from a java.sql.Date
*/
def apply(date: Date): Value = {
val bytes = new Array[Byte](4)
val bw = MysqlBuf.writer(bytes)
val cal = Calendar.getInstance
cal.setTimeInMillis(date.getTime)
bw.writeShortLE(cal.get(Calendar.YEAR))
bw.writeByte(cal.get(Calendar.MONTH) + 1) // increment 0 indexed month
bw.writeByte(cal.get(Calendar.DATE))
RawValue(Type.Date, Charset.Binary, true, bytes)
}
/**
* Value extractor for java.sql.Date
*/
def unapply(v: Value): Option[Date] = v match {
case RawValue(Type.Date, Charset.Binary, false, bytes) =>
val str = new String(bytes, Charset(Charset.Binary))
if (str == Zero.toString) Some(Zero)
else Some(Date.valueOf(str))
case RawValue(Type.Date, Charset.Binary, true, bytes) =>
Some(fromBytes(bytes))
case _ => None
}
/**
* Date object that can appropriately
* represent MySQL zero Date.
*/
private[this] object Zero extends Date(0) {
override val getTime = 0L
override val toString = "0000-00-00"
}
/**
* Creates a DateValue from its MySQL binary representation.
* Invalid DATE values are converted to
* the “zero” value of the appropriate type
* ('0000-00-00' or '0000-00-00 00:00:00').
* @param bytes An array of bytes representing a DATE written in the
* MySQL binary protocol.
*/
private[this] def fromBytes(bytes: Array[Byte]): Date = {
if (bytes.isEmpty) {
return Zero
}
var year, month, day = 0
val br = MysqlBuf.reader(bytes)
if (br.remaining >= 4) {
year = br.readUnsignedShortLE()
month = br.readUnsignedByte()
day = br.readUnsignedByte()
} else {
return Zero
}
val cal = Calendar.getInstance
cal.set(year, month-1, day)
new Date(cal.getTimeInMillis)
}
}
object BigDecimalValue extends Injectable[BigDecimal] with Extractable[BigDecimal] {
def apply(b: BigDecimal): Value = {
val str = b.toString.getBytes(Charset(Charset.Binary))
RawValue(Type.NewDecimal, Charset.Binary, true, str)
}
def unapply(v: Value): Option[BigDecimal] = v match {
case RawValue(Type.NewDecimal, Charset.Binary, _, bytes) =>
Some(BigDecimal(new String(bytes, Charset(Charset.Binary))))
case _ => None
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy