All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.microsoft.sqlserver.jdbc.dtv Maven / Gradle / Ivy

There is a newer version: 12.7.0.jre11-preview
Show newest version
/*
 * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made
 * available under the terms of the MIT License. See the LICENSE file in the project root for more information.
 */

package com.microsoft.sqlserver.jdbc;

import static java.nio.charset.StandardCharsets.UTF_16LE;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.MathContext;
import java.math.RoundingMode;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.sql.Blob;
import java.sql.Clob;
import java.sql.SQLException;
import java.text.MessageFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.util.Calendar;
import java.util.EnumMap;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.Map;
import java.util.SimpleTimeZone;
import java.util.TimeZone;
import java.util.UUID;

import com.microsoft.sqlserver.jdbc.JavaType.SetterConversionAE;


/**
 * Defines an abstraction for execution of type-specific operations on DTV values.
 *
 * This abstract design keeps the logic of determining how to handle particular DTV value and jdbcType combinations in a
 * single place (DTV.executeOp()) and forces new operations to be able to handle *all* the required combinations.
 *
 * Current operations and their locations are:
 *
 * Parameter.GetTypeDefinitionOp Determines the specific parameter type definition according to the current value.
 *
 * DTV.SendByRPCOp (below) Marshals the value onto the binary (RPC) buffer for sending to the server.
 */
abstract class DTVExecuteOp {
    abstract void execute(DTV dtv, String strValue) throws SQLServerException;

    abstract void execute(DTV dtv, Clob clobValue) throws SQLServerException;

    abstract void execute(DTV dtv, Byte byteValue) throws SQLServerException;

    abstract void execute(DTV dtv, Integer intValue) throws SQLServerException;

    abstract void execute(DTV dtv, java.sql.Time timeValue) throws SQLServerException;

    abstract void execute(DTV dtv, java.sql.Date dateValue) throws SQLServerException;

    abstract void execute(DTV dtv, java.sql.Timestamp timestampValue) throws SQLServerException;

    abstract void execute(DTV dtv, java.util.Date utilDateValue) throws SQLServerException;

    abstract void execute(DTV dtv, java.util.Calendar calendarValue) throws SQLServerException;

    abstract void execute(DTV dtv, LocalDate localDateValue) throws SQLServerException;

    abstract void execute(DTV dtv, LocalTime localTimeValue) throws SQLServerException;

    abstract void execute(DTV dtv, LocalDateTime localDateTimeValue) throws SQLServerException;

    abstract void execute(DTV dtv, OffsetTime offsetTimeValue) throws SQLServerException;

    abstract void execute(DTV dtv, OffsetDateTime offsetDateTimeValue) throws SQLServerException;

    abstract void execute(DTV dtv, microsoft.sql.DateTimeOffset dtoValue) throws SQLServerException;

    abstract void execute(DTV dtv, Float floatValue) throws SQLServerException;

    abstract void execute(DTV dtv, Double doubleValue) throws SQLServerException;

    abstract void execute(DTV dtv, BigDecimal bigDecimalValue) throws SQLServerException;

    abstract void execute(DTV dtv, Long longValue) throws SQLServerException;

    abstract void execute(DTV dtv, BigInteger bigIntegerValue) throws SQLServerException;

    abstract void execute(DTV dtv, Short shortValue) throws SQLServerException;

    abstract void execute(DTV dtv, Boolean booleanValue) throws SQLServerException;

    abstract void execute(DTV dtv, byte[] byteArrayValue) throws SQLServerException;

    abstract void execute(DTV dtv, Blob blobValue) throws SQLServerException;

    abstract void execute(DTV dtv, InputStream inputStreamValue) throws SQLServerException;

    abstract void execute(DTV dtv, Reader readerValue) throws SQLServerException;

    abstract void execute(DTV dtv, SQLServerSQLXML xmlValue) throws SQLServerException;

    abstract void execute(DTV dtv, TVP tvpValue) throws SQLServerException;

    abstract void execute(DTV dtv, SqlVariant sqlVariantValue) throws SQLServerException;
}


/**
 * Outer level DTV class.
 *
 * All DTV manipulation is done through this class.
 */
final class DTV {
    static final private java.util.logging.Logger aeLogger = java.util.logging.Logger
            .getLogger("com.microsoft.sqlserver.jdbc.DTV");

    /** The source (app or server) providing the data for this value. */
    private DTVImpl impl;

    CryptoMetadata cryptoMeta = null;
    JDBCType jdbcTypeSetByUser = null;
    int valueLength = 0;
    boolean sendStringParametersAsUnicode = true;

    /**
     * Sets a DTV value from a Java object.
     *
     * The new value replaces any value (column or parameter return value) that was previously set via the TDS response.
     * Doing this may change the DTV's internal implementation to an AppDTVImpl.
     */
    void setValue(SQLCollation collation, JDBCType jdbcType, Object value, JavaType javaType,
            StreamSetterArgs streamSetterArgs, Calendar calendar, Integer scale, SQLServerConnection con,
            boolean forceEncrypt) throws SQLServerException {
        if (null == impl)
            impl = new AppDTVImpl();

        impl.setValue(this, collation, jdbcType, value, javaType, streamSetterArgs, calendar, scale, con, forceEncrypt);
    }

    final void setValue(Object value, JavaType javaType) {
        impl.setValue(value, javaType);
    }

    final void clear() {
        impl = null;
    }

    final void skipValue(TypeInfo type, TDSReader tdsReader, boolean isDiscard) throws SQLServerException {
        if (null == impl)
            impl = new ServerDTVImpl();

        impl.skipValue(type, tdsReader, isDiscard);
    }

    final void initFromCompressedNull() {
        if (null == impl)
            impl = new ServerDTVImpl();

        impl.initFromCompressedNull();
    }

    final void setStreamSetterArgs(StreamSetterArgs streamSetterArgs) {
        impl.setStreamSetterArgs(streamSetterArgs);
    }

    final void setCalendar(Calendar calendar) {
        impl.setCalendar(calendar);
    }

    final void setScale(Integer scale) {
        impl.setScale(scale);
    }

    final void setForceEncrypt(boolean forceEncrypt) {
        impl.setForceEncrypt(forceEncrypt);
    }

    StreamSetterArgs getStreamSetterArgs() {
        return impl.getStreamSetterArgs();
    }

    Calendar getCalendar() {
        return impl.getCalendar();
    }

    Integer getScale() {
        return impl.getScale();
    }

    /**
     * Returns whether the DTV's current value is null.
     */
    boolean isNull() {
        return null == impl || impl.isNull();
    }

    /**
     * @return true if impl is not null
     */
    final boolean isInitialized() {
        return (null != impl);
    }

    final void setJdbcType(JDBCType jdbcType) {
        if (null == impl)
            impl = new AppDTVImpl();

        impl.setJdbcType(jdbcType);
    }

    /**
     * Returns the DTV's current JDBC type
     */
    final JDBCType getJdbcType() {
        assert null != impl;
        return impl.getJdbcType();
    }

    /**
     * Returns the DTV's current JDBC type
     */
    final JavaType getJavaType() {
        assert null != impl;
        return impl.getJavaType();
    }

    /**
     * Returns the DTV's current value as the specified type.
     *
     * This variant of getValue() takes an extra parameter to handle the few cases where extra arguments are needed to
     * determine the value (e.g. a Calendar object for time-valued DTV values).
     */
    Object getValue(JDBCType jdbcType, int scale, InputStreamGetterArgs streamGetterArgs, Calendar cal,
            TypeInfo typeInfo, CryptoMetadata cryptoMetadata, TDSReader tdsReader, SQLServerStatement statement) throws SQLServerException {
        if (null == impl)
            impl = new ServerDTVImpl();
        return impl.getValue(this, jdbcType, scale, streamGetterArgs, cal, typeInfo, cryptoMetadata, tdsReader, statement);
    }

    Object getSetterValue() {
        return impl.getSetterValue();
    }

    SqlVariant getInternalVariant() {
        return impl.getInternalVariant();
    }

    /**
     * Called by DTV implementation instances to change to a different DTV implementation.
     */
    void setImpl(DTVImpl impl) {
        this.impl = impl;
    }

    final class SendByRPCOp extends DTVExecuteOp {
        private final String name;
        private final TypeInfo typeInfo;
        private final SQLCollation collation;
        private final int precision;
        private final int outScale;
        private final boolean isOutParam;
        private final TDSWriter tdsWriter;
        private final SQLServerConnection conn;
        private final SQLServerStatement statement;

        SendByRPCOp(String name, TypeInfo typeInfo, SQLCollation collation, int precision, int outScale,
                boolean isOutParam, TDSWriter tdsWriter, SQLServerStatement statement) {
            this.name = name;
            this.typeInfo = typeInfo;
            this.collation = collation;
            this.precision = precision;
            this.outScale = outScale;
            this.isOutParam = isOutParam;
            this.tdsWriter = tdsWriter;
            this.conn = statement.connection;
            this.statement = statement;
        }

        void execute(DTV dtv, String strValue) throws SQLServerException {
            tdsWriter.writeRPCStringUnicode(name, strValue, isOutParam, collation);
        }

        void execute(DTV dtv, Clob clobValue) throws SQLServerException {
            // executeOp should have handled null Clob as a String
            assert null != clobValue;

            long clobLength = 0;
            Reader clobReader = null;

            try {
                clobLength = DataTypes.getCheckedLength(conn, dtv.getJdbcType(), clobValue.length(), false);
                clobReader = clobValue.getCharacterStream();
            } catch (SQLException e) {
                SQLServerException.makeFromDriverError(conn, null, e.getMessage(), null, false);
            }

            // If the Clob value is to be sent as MBCS, then convert the value to an MBCS InputStream
            JDBCType jdbcType = dtv.getJdbcType();
            if (null != collation && (JDBCType.CHAR == jdbcType || JDBCType.VARCHAR == jdbcType
                    || JDBCType.LONGVARCHAR == jdbcType || JDBCType.CLOB == jdbcType)) {
                if (null == clobReader) {
                    tdsWriter.writeRPCByteArray(name, null, isOutParam, jdbcType, collation);
                } else {
                    ReaderInputStream clobStream = new ReaderInputStream(clobReader, collation.getCharset(),
                            clobLength);

                    tdsWriter.writeRPCInputStream(name, clobStream, DataTypes.UNKNOWN_STREAM_LENGTH, isOutParam,
                            jdbcType, collation);
                }
            } else // Send CLOB value as Unicode
            {
                if (null == clobReader) {
                    tdsWriter.writeRPCStringUnicode(name, null, isOutParam, collation);
                } else {
                    tdsWriter.writeRPCReaderUnicode(name, clobReader, clobLength, isOutParam, collation);
                }
            }
        }

        void execute(DTV dtv, Byte byteValue) throws SQLServerException {
            tdsWriter.writeRPCByte(name, byteValue, isOutParam);
        }

        void execute(DTV dtv, Integer intValue) throws SQLServerException {
            tdsWriter.writeRPCInt(name, intValue, isOutParam);
        }

        void execute(DTV dtv, java.sql.Time timeValue) throws SQLServerException {
            sendTemporal(dtv, JavaType.TIME, timeValue);
        }

        void execute(DTV dtv, java.sql.Date dateValue) throws SQLServerException {
            sendTemporal(dtv, JavaType.DATE, dateValue);
        }

        void execute(DTV dtv, java.sql.Timestamp timestampValue) throws SQLServerException {
            sendTemporal(dtv, JavaType.TIMESTAMP, timestampValue);
        }

        void execute(DTV dtv, java.util.Date utilDateValue) throws SQLServerException {
            sendTemporal(dtv, JavaType.UTILDATE, utilDateValue);
        }

        void execute(DTV dtv, java.util.Calendar calendarValue) throws SQLServerException {
            sendTemporal(dtv, JavaType.CALENDAR, calendarValue);
        }

        void execute(DTV dtv, LocalDate localDateValue) throws SQLServerException {
            sendTemporal(dtv, JavaType.LOCALDATE, localDateValue);
        }

        void execute(DTV dtv, LocalTime localTimeValue) throws SQLServerException {
            sendTemporal(dtv, JavaType.LOCALTIME, localTimeValue);
        }

        void execute(DTV dtv, LocalDateTime localDateTimeValue) throws SQLServerException {
            sendTemporal(dtv, JavaType.LOCALDATETIME, localDateTimeValue);
        }

        void execute(DTV dtv, OffsetTime offsetTimeValue) throws SQLServerException {
            sendTemporal(dtv, JavaType.OFFSETTIME, offsetTimeValue);
        }

        void execute(DTV dtv, OffsetDateTime offsetDateTimeValue) throws SQLServerException {
            sendTemporal(dtv, JavaType.OFFSETDATETIME, offsetDateTimeValue);
        }

        void execute(DTV dtv, microsoft.sql.DateTimeOffset dtoValue) throws SQLServerException {
            sendTemporal(dtv, JavaType.DATETIMEOFFSET, dtoValue);
        }

        void execute(DTV dtv, TVP tvpValue) throws SQLServerException {
            // shouldn't be an output parameter
            tdsWriter.writeTVP(tvpValue);
        }

        /**
         * Clears the calendar and then sets only the fields passed in. Rest of the fields will have default values.
         */
        private void clearSetCalendar(Calendar cal, boolean lenient, Integer year, Integer month, Integer day_of_month,
                Integer hour_of_day, Integer minute, Integer second) {
            cal.clear();
            cal.setLenient(lenient);
            if (null != year) {
                cal.set(Calendar.YEAR, year);
            }
            if (null != month) {
                cal.set(Calendar.MONTH, month);
            }
            if (null != day_of_month) {
                cal.set(Calendar.DAY_OF_MONTH, day_of_month);
            }
            if (null != hour_of_day) {
                cal.set(Calendar.HOUR_OF_DAY, hour_of_day);
            }
            if (null != minute) {
                cal.set(Calendar.MINUTE, minute);
            }
            if (null != second) {
                cal.set(Calendar.SECOND, second);
            }
        }

        /**
         * Sends the specified temporal type value to the server as the appropriate SQL Server type.
         *
         * To send the value to the server, this method does the following: 1) Converts its given temporal value
         * argument into a common form encapsulated by a pure, lenient GregorianCalendar instance. 2) Normalizes that
         * common form according to the target data type. 3) Sends the value to the server using the appropriate target
         * data type method.
         *
         * @param dtv
         *        DTV with type info, etc.
         * @param javaType
         *        the Java type of the Object that follows
         * @param value
         *        the temporal value to send to the server. May be null.
         */
        private void sendTemporal(DTV dtv, JavaType javaType, Object value) throws SQLServerException {
            JDBCType jdbcType = dtv.getJdbcType();
            GregorianCalendar calendar = null;
            int subSecondNanos = 0;
            int minutesOffset = 0;

            /*
             * Some precisions to consider: java.sql.Time is millisecond precision java.sql.Timestamp is nanosecond
             * precision java.util.Date is millisecond precision java.util.Calendar is millisecond precision
             * java.time.LocalTime is nanosecond precision java.time.LocalDateTime is nanosecond precision
             * java.time.OffsetTime is nanosecond precision, with a zone offset java.time.OffsetDateTime is nanosecond
             * precision, with a zone offset SQL Server Types: datetime2 is 7 digit precision, i.e 100 ns (default and
             * max) datetime is 3 digit precision, i.e ms precision (default and max) time is 7 digit precision, i.e 100
             * ns (default and max) Note: sendTimeAsDatetime is true by default and it actually sends the time value as
             * datetime, not datetime2 which looses precision values as datetime is only MS precision (1/300 of a second
             * to be precise). Null values pass right on through to the typed writers below. For non-null values, load
             * the value from its original Java object (java.sql.Time, java.sql.Date, java.sql.Timestamp,
             * java.util.Date, java.time.LocalDate, java.time.LocalTime, java.time.LocalDateTime or
             * microsoft.sql.DateTimeOffset) into a Gregorian calendar. Don't use the DTV's calendar directly, as it may
             * not be Gregorian...
             */
            if (null != value) {
                TimeZone timeZone = TimeZone.getDefault(); // Time zone to associate with the value in the Gregorian
                                                           // calendar
                long utcMillis = 0; // Value to which the calendar is to be set (in milliseconds 1/1/1970 00:00:00 GMT)

                // Figure out the value components according to the type of the Java object passed in...
                switch (javaType) {
                    case TIME: {
                        // Set the time zone from the calendar supplied by the app or use the JVM default
                        timeZone = (null != dtv.getCalendar()) ? dtv.getCalendar().getTimeZone()
                                                               : TimeZone.getDefault();

                        utcMillis = ((java.sql.Time) value).getTime();
                        subSecondNanos = Nanos.PER_MILLISECOND * (int) (utcMillis % 1000);

                        // The utcMillis value may be negative for morning times in time zones east of GMT.
                        // Since the date part of the java.sql.Time object is normalized to 1/1/1970
                        // in the local time zone, the date may still be 12/31/1969 UTC.
                        //
                        // If that is the case then adjust the sub-second nanos to the correct non-negative
                        // "wall clock" value. For example: -1 nanos (one nanosecond before midnight) becomes
                        // 999999999 nanos (999,999,999 nanoseconds after 11:59:59).
                        if (subSecondNanos < 0)
                            subSecondNanos += Nanos.PER_SECOND;

                        break;
                    }

                    case DATE: {
                        // Set the time zone from the calendar supplied by the app or use the JVM default
                        timeZone = (null != dtv.getCalendar()) ? dtv.getCalendar().getTimeZone()
                                                               : TimeZone.getDefault();

                        utcMillis = ((java.sql.Date) value).getTime();
                        break;
                    }

                    case TIMESTAMP: {
                        // Set the time zone from the calendar supplied by the app or use the JVM default
                        timeZone = (null != dtv.getCalendar()) ? dtv.getCalendar().getTimeZone()
                                                               : TimeZone.getDefault();

                        java.sql.Timestamp timestampValue = (java.sql.Timestamp) value;
                        utcMillis = timestampValue.getTime();
                        subSecondNanos = timestampValue.getNanos();
                        break;
                    }

                    case UTILDATE: {
                        // java.util.Date is mapped to JDBC type TIMESTAMP
                        // java.util.Date and java.sql.Date are both millisecond precision
                        // Set the time zone from the calendar supplied by the app or use the JVM default
                        timeZone = (null != dtv.getCalendar()) ? dtv.getCalendar().getTimeZone()
                                                               : TimeZone.getDefault();

                        utcMillis = ((java.util.Date) value).getTime();

                        // Need to use the subsecondnanoes part in UTILDATE besause it is mapped to JDBC TIMESTAMP. This
                        // is not
                        // needed in DATE because DATE is mapped to JDBC DATE (with time part normalized to midnight)
                        subSecondNanos = Nanos.PER_MILLISECOND * (int) (utcMillis % 1000);

                        // The utcMillis value may be negative for morning times in time zones east of GMT.
                        // Since the date part of the java.sql.Time object is normalized to 1/1/1970
                        // in the local time zone, the date may still be 12/31/1969 UTC.
                        //
                        // If that is the case then adjust the sub-second nanos to the correct non-negative
                        // "wall clock" value. For example: -1 nanos (one nanosecond before midnight) becomes
                        // 999999999 nanos (999,999,999 nanoseconds after 11:59:59).
                        if (subSecondNanos < 0)
                            subSecondNanos += Nanos.PER_SECOND;
                        break;
                    }

                    case CALENDAR: {
                        // java.util.Calendar is mapped to JDBC type TIMESTAMP
                        // java.util.Calendar is millisecond precision
                        // Set the time zone from the calendar supplied by the app or use the JVM default
                        timeZone = (null != dtv.getCalendar()) ? dtv.getCalendar().getTimeZone()
                                                               : TimeZone.getDefault();

                        utcMillis = ((java.util.Calendar) value).getTimeInMillis();

                        // Need to use the subsecondnanoes part in CALENDAR besause it is mapped to JDBC TIMESTAMP. This
                        // is not
                        // needed in DATE because DATE is mapped to JDBC DATE (with time part normalized to midnight)
                        subSecondNanos = Nanos.PER_MILLISECOND * (int) (utcMillis % 1000);

                        // The utcMillis value may be negative for morning times in time zones east of GMT.
                        // Since the date part of the java.sql.Time object is normalized to 1/1/1970
                        // in the local time zone, the date may still be 12/31/1969 UTC.
                        //
                        // If that is the case then adjust the sub-second nanos to the correct non-negative
                        // "wall clock" value. For example: -1 nanos (one nanosecond before midnight) becomes
                        // 999999999 nanos (999,999,999 nanoseconds after 11:59:59).
                        if (subSecondNanos < 0)
                            subSecondNanos += Nanos.PER_SECOND;
                        break;
                    }

                    case LOCALDATE:
                        // Mapped to JDBC type DATE
                        calendar = new GregorianCalendar(UTC.timeZone, Locale.US);

                        // All time fields are set to default
                        clearSetCalendar(calendar, true, ((LocalDate) value).getYear(),
                                ((LocalDate) value).getMonthValue() - 1, // Calendar 'month'
                                                                         // is 0-based, but
                                                                         // LocalDate 'month'
                                                                         // is 1-based
                                ((LocalDate) value).getDayOfMonth(), null, null, null);
                        break;

                    case LOCALTIME:
                        // Nanoseconds precision, mapped to JDBC type TIME
                        calendar = new GregorianCalendar(UTC.timeZone, Locale.US);

                        // All date fields are set to default
                        LocalTime LocalTimeValue = ((LocalTime) value);
                        clearSetCalendar(calendar, true, conn.baseYear(), 1, 1, LocalTimeValue.getHour(), // Gets
                                                                                                          // hour_of_day
                                                                                                          // field
                                LocalTimeValue.getMinute(), LocalTimeValue.getSecond());
                        subSecondNanos = LocalTimeValue.getNano();

                        // Do not need to adjust subSecondNanos as in the case for TIME
                        // because LOCALTIME does not have time zone and is not using utcMillis
                        break;

                    case LOCALDATETIME:
                        // Nanoseconds precision, mapped to JDBC type TIMESTAMP

                        calendar = new GregorianCalendar(UTC.timeZone, Locale.US);
                        // Calendar 'month' is 0-based, but LocalDateTime 'month' is 1-based
                        LocalDateTime localDateTimeValue = (LocalDateTime) value;
                        clearSetCalendar(calendar, true, localDateTimeValue.getYear(),
                                localDateTimeValue.getMonthValue() - 1, localDateTimeValue.getDayOfMonth(),
                                localDateTimeValue.getHour(), // Gets hour_of_day field
                                localDateTimeValue.getMinute(), localDateTimeValue.getSecond());
                        subSecondNanos = localDateTimeValue.getNano();

                        // Do not need to adjust subSecondNanos as in the case for TIME
                        // because LOCALDATETIME does not have time zone and is not using utcMillis
                        break;

                    case OFFSETTIME:
                        OffsetTime offsetTimeValue = (OffsetTime) value;
                        try {
                            // offsetTimeValue.getOffset() returns a ZoneOffset object which has only hours and minutes
                            // components. So the result of the division will be an integer always. SQL Server also
                            // supports
                            // offsets in minutes precision.
                            minutesOffset = offsetTimeValue.getOffset().getTotalSeconds() / 60;
                        } catch (Exception e) {
                            throw new SQLServerException(SQLServerException.getErrString("R_zoneOffsetError"), null, // SQLState
                                                                                                                     // is
                                                                                                                     // null
                                                                                                                     // as
                                                                                                                     // this
                                                                                                                     // error
                                                                                                                     // is
                                                                                                                     // generated
                                                                                                                     // in
                                                                                                                     // the
                                                                                                                     // driver
                                    0, // Use 0 instead of DriverError.NOT_SET to use the correct constructor
                                    e);
                        }
                        subSecondNanos = offsetTimeValue.getNano();

                        // If the target data type is TIME_WITH_TIMEZONE, then use UTC for the calendar that
                        // will hold the value, since writeRPCDateTimeOffset expects a UTC calendar.
                        // Otherwise, when converting from DATETIMEOFFSET to other temporal data types,
                        // use a local time zone determined by the minutes offset of the value, since
                        // the writers for those types expect local calendars.
                        timeZone = (JDBCType.TIME_WITH_TIMEZONE == jdbcType
                                && (null == typeInfo || SSType.DATETIMEOFFSET == typeInfo.getSSType())) ?

                                                                                                        UTC.timeZone
                                                                                                        : new SimpleTimeZone(
                                                                                                                minutesOffset
                                                                                                                        * 60
                                                                                                                        * 1000,
                                                                                                                "");

                        LocalDate baseDate = LocalDate.of(conn.baseYear(), 1, 1);
                        utcMillis = offsetTimeValue.atDate(baseDate).toEpochSecond() * 1000;
                        break;

                    case OFFSETDATETIME:
                        OffsetDateTime offsetDateTimeValue = (OffsetDateTime) value;
                        try {
                            // offsetTimeValue.getOffset() returns a ZoneOffset object which has only hours and minutes
                            // components. So the result of the division will be an integer always. SQL Server also
                            // supports
                            // offsets in minutes precision.
                            minutesOffset = offsetDateTimeValue.getOffset().getTotalSeconds() / 60;
                        } catch (Exception e) {
                            throw new SQLServerException(SQLServerException.getErrString("R_zoneOffsetError"), null, // SQLState
                                                                                                                     // is
                                                                                                                     // null
                                                                                                                     // as
                                                                                                                     // this
                                                                                                                     // error
                                                                                                                     // is
                                                                                                                     // generated
                                                                                                                     // in
                                                                                                                     // the
                                                                                                                     // driver
                                    0, // Use 0 instead of DriverError.NOT_SET to use the correct constructor
                                    e);
                        }

                        subSecondNanos = offsetDateTimeValue.getNano();

                        // If the target data type is TIME_WITH_TIMEZONE or TIMESTAMP_WITH_TIMEZONE, then use UTC for
                        // the calendar that
                        // will hold the value, since writeRPCDateTimeOffset expects a UTC calendar.
                        // Otherwise, when converting from DATETIMEOFFSET to other temporal data types,
                        // use a local time zone determined by the minutes offset of the value, since
                        // the writers for those types expect local calendars.
                        timeZone = ((JDBCType.TIMESTAMP_WITH_TIMEZONE == jdbcType
                                || JDBCType.TIME_WITH_TIMEZONE == jdbcType)
                                && (null == typeInfo || SSType.DATETIMEOFFSET == typeInfo.getSSType()))
                                                                                                        ? UTC.timeZone
                                                                                                        : new SimpleTimeZone(
                                                                                                                minutesOffset
                                                                                                                        * 60
                                                                                                                        * 1000,
                                                                                                                "");

                        utcMillis = offsetDateTimeValue.toEpochSecond() * 1000;
                        break;

                    case DATETIMEOFFSET: {
                        microsoft.sql.DateTimeOffset dtoValue = (microsoft.sql.DateTimeOffset) value;
                        utcMillis = dtoValue.getTimestamp().getTime();
                        subSecondNanos = dtoValue.getTimestamp().getNanos();
                        minutesOffset = dtoValue.getMinutesOffset();

                        // microsoft.sql.DateTimeOffset values have a time zone offset that is internal
                        // to the value, so there should not be any DTV calendar for DateTimeOffset values.
                        assert null == dtv.getCalendar();

                        // If the target data type is DATETIMEOFFSET, then use UTC for the calendar that
                        // will hold the value, since writeRPCDateTimeOffset expects a UTC calendar.
                        // Otherwise, when converting from DATETIMEOFFSET to other temporal data types,
                        // use a local time zone determined by the minutes offset of the value, since
                        // the writers for those types expect local calendars.
                        timeZone = (JDBCType.DATETIMEOFFSET == jdbcType
                                && (null == typeInfo || SSType.DATETIMEOFFSET == typeInfo.getSSType()
                                        || SSType.VARBINARY == typeInfo.getSSType()
                                        || SSType.VARBINARYMAX == typeInfo.getSSType())) ?

                                                                                         UTC.timeZone
                                                                                         : new SimpleTimeZone(
                                                                                                 minutesOffset * 60
                                                                                                         * 1000,
                                                                                                 "");

                        break;
                    }

                    default:
                        throw new AssertionError("Unexpected JavaType: " + javaType);
                }

                // For the LocalDate, LocalTime and LocalDateTime values, calendar should be set by now.
                if (null == calendar) {
                    // Create the calendar that will hold the value. For DateTimeOffset values, the calendar's
                    // time zone is UTC. For other values, the calendar's time zone is a local time zone.
                    calendar = new GregorianCalendar(timeZone, Locale.US);

                    // Set the calendar lenient to allow setting the DAY_OF_YEAR and MILLISECOND fields
                    // to roll other fields to their correct values.
                    calendar.setLenient(true);

                    // Clear the calendar of any existing state. The state of a new Calendar object always
                    // reflects the current date, time, DST offset, etc.
                    calendar.clear();

                    // Load the calendar with the desired value
                    calendar.setTimeInMillis(utcMillis);
                }

            }

            // With the value now stored in a Calendar object, determine the backend data type and
            // write out the value to the server, after any necessarily normalization.
            // typeInfo is null when called from PreparedStatement->Parameter->SendByRPC
            if (null != typeInfo) // updater
            {
                switch (typeInfo.getSSType()) {
                    case DATETIME:
                    case DATETIME2:
                        /*
                         * Default and max fractional precision is 7 digits (100ns) Send DateTime2 to DateTime columns
                         * to let the server handle nanosecond rounding. Also adjust scale accordingly to avoid rounding
                         * on driver's end.
                         */
                        int scale = (typeInfo.getSSType() == SSType.DATETIME) ? typeInfo.getScale() + 4
                                                                              : typeInfo.getScale();
                        tdsWriter.writeRPCDateTime2(name,
                                timestampNormalizedCalendar(calendar, javaType, conn.baseYear()), subSecondNanos, scale,
                                isOutParam);

                        break;

                    case DATE:
                        tdsWriter.writeRPCDate(name, calendar, isOutParam);

                        break;

                    case TIME:
                        // Default and max fractional precision is 7 digits (100ns)
                        tdsWriter.writeRPCTime(name, calendar, subSecondNanos, typeInfo.getScale(), isOutParam);

                        break;

                    case DATETIMEOFFSET:
                        // When converting from any other temporal Java type to DATETIMEOFFSET,
                        // deliberately interpret the "wall calendar" representation as expressing
                        // a date/time in UTC rather than the local time zone.
                        if (JavaType.DATETIMEOFFSET != javaType) {
                            calendar = timestampNormalizedCalendar(localCalendarAsUTC(calendar), javaType,
                                    conn.baseYear());

                            minutesOffset = 0; // UTC
                        }

                        tdsWriter.writeRPCDateTimeOffset(name, calendar, minutesOffset, subSecondNanos,
                                typeInfo.getScale(), isOutParam);

                        break;

                    case SMALLDATETIME:
                        tdsWriter.writeRPCDateTime(name,
                                timestampNormalizedCalendar(calendar, javaType, conn.baseYear()), subSecondNanos,
                                isOutParam);
                        break;

                    case VARBINARY:
                    case VARBINARYMAX:
                        switch (jdbcType) {
                            case DATETIME:
                            case SMALLDATETIME:
                                tdsWriter.writeEncryptedRPCDateTime(name,
                                        timestampNormalizedCalendar(calendar, javaType, conn.baseYear()),
                                        subSecondNanos, isOutParam, jdbcType, statement);
                                break;

                            case TIMESTAMP:
                                assert null != cryptoMeta;
                                tdsWriter.writeEncryptedRPCDateTime2(name,
                                        timestampNormalizedCalendar(calendar, javaType, conn.baseYear()),
                                        subSecondNanos, valueLength, isOutParam, statement);
                                break;

                            case TIME:
                                // when colum is encrypted, always send time as time, ignore sendTimeAsDatetime setting
                                assert null != cryptoMeta;
                                tdsWriter.writeEncryptedRPCTime(name, calendar, subSecondNanos, valueLength,
                                        isOutParam, statement);
                                break;

                            case DATE:
                                assert null != cryptoMeta;
                                tdsWriter.writeEncryptedRPCDate(name, calendar, isOutParam, statement);
                                break;

                            case TIMESTAMP_WITH_TIMEZONE:
                            case DATETIMEOFFSET:
                                // When converting from any other temporal Java type to
                                // DATETIMEOFFSET/TIMESTAMP_WITH_TIMEZONE,
                                // deliberately reinterpret the value as local to UTC. This is to match
                                // SQL Server behavior for such conversions.
                                if ((JavaType.DATETIMEOFFSET != javaType) && (JavaType.OFFSETDATETIME != javaType)) {
                                    calendar = timestampNormalizedCalendar(localCalendarAsUTC(calendar), javaType,
                                            conn.baseYear());

                                    minutesOffset = 0; // UTC
                                }

                                assert null != cryptoMeta;
                                tdsWriter.writeEncryptedRPCDateTimeOffset(name, calendar, minutesOffset, subSecondNanos,
                                        valueLength, isOutParam, statement);
                                break;

                            default:
                                assert false : "Unexpected JDBCType: " + jdbcType;
                        }
                        break;

                    default:
                        assert false : "Unexpected SSType: " + typeInfo.getSSType();
                }
            } else // setter
            {
                // Katmai and later
                // ----------------
                //
                // When sending as...
                // - java.sql.Types.TIMESTAMP, use DATETIME2 SQL Server data type
                // - java.sql.Types.TIME, use TIME or DATETIME SQL Server data type
                // as determined by sendTimeAsDatetime setting
                // - java.sql.Types.DATE, use DATE SQL Server data type
                // - microsoft.sql.Types.DATETIMEOFFSET, use DATETIMEOFFSET SQL Server data type
                if (conn.isKatmaiOrLater()) {
                    if (aeLogger.isLoggable(java.util.logging.Level.FINE) && (null != cryptoMeta)) {
                        aeLogger.fine("Encrypting temporal data type.");
                    }

                    switch (jdbcType) {
                        case DATETIME:
                        case SMALLDATETIME:
                        case TIMESTAMP:
                            if (null != cryptoMeta) {
                                if ((JDBCType.DATETIME == jdbcType) || (JDBCType.SMALLDATETIME == jdbcType)) {
                                    tdsWriter.writeEncryptedRPCDateTime(name,
                                            timestampNormalizedCalendar(calendar, javaType, conn.baseYear()),
                                            subSecondNanos, isOutParam, jdbcType, statement);
                                } else if (0 == valueLength) {
                                    tdsWriter.writeEncryptedRPCDateTime2(name,
                                            timestampNormalizedCalendar(calendar, javaType, conn.baseYear()),
                                            subSecondNanos, outScale, isOutParam, statement);
                                } else {
                                    tdsWriter.writeEncryptedRPCDateTime2(name,
                                            timestampNormalizedCalendar(calendar, javaType, conn.baseYear()),
                                            subSecondNanos, (valueLength), isOutParam, statement);
                                }
                            } else
                                tdsWriter.writeRPCDateTime2(name,
                                        timestampNormalizedCalendar(calendar, javaType, conn.baseYear()),
                                        subSecondNanos, TDS.MAX_FRACTIONAL_SECONDS_SCALE, isOutParam);

                            break;

                        case TIME:
                            // if column is encrypted, always send as TIME
                            if (null != cryptoMeta) {
                                if (0 == valueLength) {
                                    tdsWriter.writeEncryptedRPCTime(name, calendar, subSecondNanos, outScale,
                                            isOutParam, statement);
                                } else {
                                    tdsWriter.writeEncryptedRPCTime(name, calendar, subSecondNanos, valueLength,
                                            isOutParam, statement);
                                }
                            } else {
                                // Send the java.sql.Types.TIME value as TIME or DATETIME SQL Server
                                // data type, based on sendTimeAsDatetime setting.
                                if (conn.getSendTimeAsDatetime()) {
                                    tdsWriter.writeRPCDateTime(name,
                                            timestampNormalizedCalendar(calendar, JavaType.TIME, TDS.BASE_YEAR_1970),
                                            subSecondNanos, isOutParam);
                                } else {
                                    tdsWriter.writeRPCTime(name, calendar, subSecondNanos,
                                            TDS.MAX_FRACTIONAL_SECONDS_SCALE, isOutParam);
                                }
                            }

                            break;

                        case DATE:
                            if (null != cryptoMeta)
                                tdsWriter.writeEncryptedRPCDate(name, calendar, isOutParam, statement);
                            else
                                tdsWriter.writeRPCDate(name, calendar, isOutParam);

                            break;

                        case TIME_WITH_TIMEZONE:
                            // When converting from any other temporal Java type to TIME_WITH_TIMEZONE,
                            // deliberately reinterpret the value as local to UTC. This is to match
                            // SQL Server behavior for such conversions.
                            if ((JavaType.OFFSETDATETIME != javaType) && (JavaType.OFFSETTIME != javaType)) {
                                calendar = timestampNormalizedCalendar(localCalendarAsUTC(calendar), javaType,
                                        conn.baseYear());

                                minutesOffset = 0; // UTC
                            }

                            tdsWriter.writeRPCDateTimeOffset(name, calendar, minutesOffset, subSecondNanos,
                                    TDS.MAX_FRACTIONAL_SECONDS_SCALE, isOutParam);

                            break;

                        case TIMESTAMP_WITH_TIMEZONE:
                        case DATETIMEOFFSET:
                            // When converting from any other temporal Java type to
                            // DATETIMEOFFSET/TIMESTAMP_WITH_TIMEZONE,
                            // deliberately reinterpret the value as local to UTC. This is to match
                            // SQL Server behavior for such conversions.
                            if ((JavaType.DATETIMEOFFSET != javaType) && (JavaType.OFFSETDATETIME != javaType)) {
                                calendar = timestampNormalizedCalendar(localCalendarAsUTC(calendar), javaType,
                                        conn.baseYear());

                                minutesOffset = 0; // UTC
                            }

                            if (null != cryptoMeta) {
                                if (0 == valueLength) {
                                    tdsWriter.writeEncryptedRPCDateTimeOffset(name, calendar, minutesOffset,
                                            subSecondNanos, outScale, isOutParam, statement);
                                } else {
                                    tdsWriter.writeEncryptedRPCDateTimeOffset(name, calendar, minutesOffset,
                                            subSecondNanos,
                                            (0 == valueLength ? TDS.MAX_FRACTIONAL_SECONDS_SCALE : valueLength),
                                            isOutParam, statement);
                                }
                            } else
                                tdsWriter.writeRPCDateTimeOffset(name, calendar, minutesOffset, subSecondNanos,
                                        TDS.MAX_FRACTIONAL_SECONDS_SCALE, isOutParam);

                            break;

                        default:
                            assert false : "Unexpected JDBCType: " + jdbcType;

                    }
                }

                // Yukon and earlier
                // -----------------
                //
                // When sending as...
                // - java.sql.Types.TIMESTAMP, use DATETIME SQL Server data type (all components)
                // - java.sql.Types.TIME, use DATETIME SQL Server data type (with date = 1/1/1970)
                // - java.sql.Types.DATE, use DATETIME SQL Server data type (with time = midnight)
                // - microsoft.sql.Types.DATETIMEOFFSET (not supported - exception should have been thrown earlier)
                else {
                    assert JDBCType.TIME == jdbcType || JDBCType.DATE == jdbcType
                            || JDBCType.TIMESTAMP == jdbcType : "Unexpected JDBCType: " + jdbcType;

                    tdsWriter.writeRPCDateTime(name,
                            timestampNormalizedCalendar(calendar, javaType, TDS.BASE_YEAR_1970), subSecondNanos,
                            isOutParam);
                }
            } // setters
        }

        /**
         * Normalizes a GregorianCalendar value appropriately for a DATETIME, SMALLDATETIME, DATETIME2, or
         * DATETIMEOFFSET SQL Server data type.
         *
         * For DATE values, the time must be normalized to midnight. For TIME values, the date must be normalized to
         * January of the specified base year (1970 or 1900) For other temporal types (DATETIME, SMALLDATETIME,
         * DATETIME2, DATETIMEOFFSET), no normalization is needed - both date and time contribute to the final value.
         *
         * @param calendar
         *        the value to normalize. May be null.
         * @param javaType
         *        the Java type that the calendar value represents.
         * @param baseYear
         *        the base year (1970 or 1900) for use in normalizing TIME values.
         */
        private GregorianCalendar timestampNormalizedCalendar(GregorianCalendar calendar, JavaType javaType,
                int baseYear) {
            if (null != calendar) {
                switch (javaType) {
                    case LOCALDATE:
                    case DATE:
                        // Note: Although UTILDATE is a Date object (java.util.Date) it cannot have normalized time
                        // values.
                        // This is because java.util.Date is mapped to JDBC TIMESTAMP according to the JDBC spec.
                        // java.util.Calendar is also mapped to JDBC TIMESTAMP and hence should have both date and time
                        // parts.
                        calendar.set(Calendar.HOUR_OF_DAY, 0);
                        calendar.set(Calendar.MINUTE, 0);
                        calendar.set(Calendar.SECOND, 0);
                        calendar.set(Calendar.MILLISECOND, 0);
                        break;

                    case OFFSETTIME:
                    case LOCALTIME:
                    case TIME:
                        assert TDS.BASE_YEAR_1970 == baseYear || TDS.BASE_YEAR_1900 == baseYear;
                        calendar.set(baseYear, Calendar.JANUARY, 1);
                        break;

                    default:
                        break;
                }
            }

            return calendar;
        }

        // Conversion from Date/Time/Timestamp to DATETIMEOFFSET reinterprets (changes)
        // the "wall clock" value to be local to UTC rather than the local to the
        // local Calendar's time zone. This behavior (ignoring the local time zone
        // when converting to DATETIMEOFFSET, which is time zone-aware) may seem
        // counterintuitive, but is necessary per the data types spec to match SQL
        // Server's conversion behavior for both setters and updaters.
        private GregorianCalendar localCalendarAsUTC(GregorianCalendar cal) {
            if (null == cal)
                return null;

            // Interpret "wall clock" value of the local calendar as a date/time/timestamp in UTC
            int year = cal.get(Calendar.YEAR);
            int month = cal.get(Calendar.MONTH);
            int date = cal.get(Calendar.DATE);
            int hour = cal.get(Calendar.HOUR_OF_DAY);
            int minute = cal.get(Calendar.MINUTE);
            int second = cal.get(Calendar.SECOND);
            int millis = cal.get(Calendar.MILLISECOND);

            cal.setTimeZone(UTC.timeZone);
            cal.set(year, month, date, hour, minute, second);
            cal.set(Calendar.MILLISECOND, millis);
            return cal;
        }

        void execute(DTV dtv, Float floatValue) throws SQLServerException {
            if (JDBCType.REAL == dtv.getJdbcType()) {
                tdsWriter.writeRPCReal(name, floatValue, isOutParam);
            } else // all other jdbcTypes (not just JDBCType.FLOAT!)
            {
                /*
                 * 2.26 floating point data needs to go to the DB as 8 bytes or else rounding errors occur at the DB.
                 * The ODBC driver sends 4 byte floats as 8 bytes also. So tried assigning the float to a double with
                 * double d = float f but d now also shows rounding errors (from simply the assignment in the Java
                 * runtime). So the only way found is to convert the float to a string and init the double with that
                 * string
                 */
                Double doubleValue = (null == floatValue) ? null : (double) floatValue;
                tdsWriter.writeRPCDouble(name, doubleValue, isOutParam);
            }
        }

        void execute(DTV dtv, Double doubleValue) throws SQLServerException {
            tdsWriter.writeRPCDouble(name, doubleValue, isOutParam);
        }

        void execute(DTV dtv, BigDecimal bigDecimalValue) throws SQLServerException {
            if (DDC.exceedsMaxRPCDecimalPrecisionOrScale(bigDecimalValue)) {
                if (JDBCType.DECIMAL == dtv.getJdbcType() || JDBCType.NUMERIC == dtv.getJdbcType()) {
                    // Throw exception for DECIMAL and NUMERIC Datatypes
                    MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_valueOutOfRangeSQLType"));
                    Object[] msgArgs = {dtv.getJdbcType()};
                    throw new SQLServerException(form.format(msgArgs), SQLState.NUMERIC_DATA_OUT_OF_RANGE,
                            DriverError.NOT_SET, null);
                } else {
                    String strValue = bigDecimalValue.toString();
                    tdsWriter.writeRPCStringUnicode(name, strValue, isOutParam, collation);
                }
            } else {
                tdsWriter.writeRPCBigDecimal(name, bigDecimalValue, outScale, isOutParam);
            }
        }

        void execute(DTV dtv, Long longValue) throws SQLServerException {
            tdsWriter.writeRPCLong(name, longValue, isOutParam);
        }

        void execute(DTV dtv, BigInteger bigIntegerValue) throws SQLServerException {
            tdsWriter.writeRPCLong(name, bigIntegerValue.longValue(), isOutParam);
        }

        void execute(DTV dtv, Short shortValue) throws SQLServerException {
            tdsWriter.writeRPCShort(name, shortValue, isOutParam);
        }

        void execute(DTV dtv, Boolean booleanValue) throws SQLServerException {
            tdsWriter.writeRPCBit(name, booleanValue, isOutParam);
        }

        void execute(DTV dtv, byte[] byteArrayValue) throws SQLServerException {
            if (null != cryptoMeta) {
                tdsWriter.writeRPCNameValType(name, isOutParam, TDSType.BIGVARBINARY);
                if (null != byteArrayValue) {
                    byteArrayValue = SQLServerSecurityUtility.encryptWithKey(byteArrayValue, cryptoMeta, conn, statement);
                    tdsWriter.writeEncryptedRPCByteArray(byteArrayValue);
                    writeEncryptData(dtv, false);
                } else {
                    // long and 8000/4000 types, and output parameter without input
                    // if there is no setter is called, javaType is null
                    if ((JDBCType.LONGVARCHAR == jdbcTypeSetByUser || JDBCType.LONGNVARCHAR == jdbcTypeSetByUser
                            || JDBCType.LONGVARBINARY == jdbcTypeSetByUser
                            || (DataTypes.SHORT_VARTYPE_MAX_BYTES == precision && JDBCType.VARCHAR == jdbcTypeSetByUser)
                            || (DataTypes.SHORT_VARTYPE_MAX_CHARS == precision
                                    && JDBCType.NVARCHAR == jdbcTypeSetByUser)
                            || (DataTypes.SHORT_VARTYPE_MAX_BYTES == precision
                                    && JDBCType.VARBINARY == jdbcTypeSetByUser))
                            && null == dtv.getJavaType() && isOutParam) {
                        tdsWriter.writeEncryptedRPCPLP();
                    } else {
                        tdsWriter.writeEncryptedRPCByteArray(byteArrayValue);
                    }

                    writeEncryptData(dtv, true);
                }

            } else
                tdsWriter.writeRPCByteArray(name, byteArrayValue, isOutParam, dtv.getJdbcType(), collation);

        }

        void writeEncryptData(DTV dtv, boolean isNull) throws SQLServerException {
            JDBCType destType = (null == jdbcTypeSetByUser) ? dtv.getJdbcType() : jdbcTypeSetByUser;

            switch (destType.getIntValue()) {
                case java.sql.Types.INTEGER: // 0x38
                    tdsWriter.writeByte(TDSType.INTN.byteValue());
                    tdsWriter.writeByte((byte) 0x04);
                    break;

                case java.sql.Types.BIGINT: // 0x7f
                    tdsWriter.writeByte(TDSType.INTN.byteValue());
                    tdsWriter.writeByte((byte) 0x08);
                    break;

                case java.sql.Types.BIT: // 0x32
                    tdsWriter.writeByte(TDSType.BITN.byteValue());
                    tdsWriter.writeByte((byte) 0x01);
                    break;

                case java.sql.Types.SMALLINT: // 0x34
                    tdsWriter.writeByte(TDSType.INTN.byteValue());
                    tdsWriter.writeByte((byte) 0x02);
                    break;

                case java.sql.Types.TINYINT: // 0x30
                    tdsWriter.writeByte(TDSType.INTN.byteValue());
                    tdsWriter.writeByte((byte) 0x01);
                    break;

                case java.sql.Types.DOUBLE: // (FLT8TYPE) 0x3E
                    tdsWriter.writeByte(TDSType.FLOATN.byteValue());
                    tdsWriter.writeByte((byte) 0x08);
                    break;

                case java.sql.Types.REAL: // (FLT4TYPE) 0x3B
                    tdsWriter.writeByte(TDSType.FLOATN.byteValue());
                    tdsWriter.writeByte((byte) 0x04);
                    break;

                case microsoft.sql.Types.MONEY:
                case microsoft.sql.Types.SMALLMONEY:
                case java.sql.Types.NUMERIC:
                case java.sql.Types.DECIMAL:
                    // money/smallmoney is mapped to JDBC types java.sql.Types.Decimal
                    if ((JDBCType.MONEY == destType) || (JDBCType.SMALLMONEY == destType)) {
                        tdsWriter.writeByte(TDSType.MONEYN.byteValue()); // 0x6E
                        tdsWriter.writeByte((byte) ((JDBCType.MONEY == destType) ? 8 : 4));
                    } else {
                        tdsWriter.writeByte(TDSType.NUMERICN.byteValue()); // 0x6C
                        if (isNull) {
                            tdsWriter.writeByte((byte) 0x11); // maximum length

                            if (null != cryptoMeta && null != cryptoMeta.getBaseTypeInfo()) {
                                tdsWriter.writeByte(
                                        (byte) ((0 != valueLength) ? valueLength
                                                                   : cryptoMeta.getBaseTypeInfo().getPrecision()));
                            } else {
                                tdsWriter.writeByte((byte) ((0 != valueLength) ? valueLength : 0x12)); // default
                                                                                                       // length, 0x12
                                                                                                       // equals to 18,
                                                                                                       // which is
                                                                                                       // the default
                                                                                                       // length for
                                                                                                       // decimal value
                            }

                            tdsWriter.writeByte((byte) (outScale)); // send scale
                        } else {
                            tdsWriter.writeByte((byte) 0x11); // maximum length

                            if (null != cryptoMeta && null != cryptoMeta.getBaseTypeInfo()) {
                                tdsWriter.writeByte((byte) cryptoMeta.getBaseTypeInfo().getPrecision());
                            } else {
                                tdsWriter.writeByte((byte) ((0 != valueLength) ? valueLength : 0x12)); // default
                                                                                                       // length, 0x12
                                                                                                       // equals to 18,
                                                                                                       // which is
                                                                                                       // the default
                                                                                                       // length for
                                                                                                       // decimal value
                            }

                            if (null != cryptoMeta && null != cryptoMeta.getBaseTypeInfo()) {
                                tdsWriter.writeByte((byte) cryptoMeta.getBaseTypeInfo().getScale());
                            } else {
                                tdsWriter.writeByte((byte) ((null != dtv.getScale()) ? dtv.getScale() : 0)); // send
                                                                                                             // scale
                            }
                        }
                    }
                    break;

                case microsoft.sql.Types.GUID:
                    tdsWriter.writeByte(TDSType.GUID.byteValue());
                    if (isNull)
                        tdsWriter.writeByte((byte) ((0 != valueLength) ? valueLength : 1));
                    else
                        tdsWriter.writeByte((byte) 0x10);
                    break;

                case java.sql.Types.CHAR: // 0xAF
                    // BIGCHARTYPE
                    tdsWriter.writeByte(TDSType.BIGCHAR.byteValue());

                    if (isNull)
                        tdsWriter.writeShort((short) ((0 != valueLength) ? valueLength : 1));
                    else
                        tdsWriter.writeShort((short) (valueLength));

                    if (null != collation)
                        collation.writeCollation(tdsWriter);
                    else
                        conn.getDatabaseCollation().writeCollation(tdsWriter);
                    break;

                case java.sql.Types.NCHAR: // 0xEF
                    tdsWriter.writeByte(TDSType.NCHAR.byteValue());
                    if (isNull)
                        tdsWriter.writeShort((short) ((0 != valueLength) ? (valueLength * 2) : 1));
                    else {
                        if (isOutParam) {
                            tdsWriter.writeShort((short) (valueLength * 2));
                        } else {
                            if (valueLength > DataTypes.SHORT_VARTYPE_MAX_BYTES) {
                                tdsWriter.writeShort((short) DataTypes.MAX_VARTYPE_MAX_CHARS);
                            } else {
                                tdsWriter.writeShort((short) valueLength);
                            }
                        }
                    }
                    if (null != collation)
                        collation.writeCollation(tdsWriter);
                    else
                        conn.getDatabaseCollation().writeCollation(tdsWriter);
                    break;

                case java.sql.Types.LONGVARCHAR:
                case java.sql.Types.VARCHAR: // 0xA7
                    // BIGVARCHARTYPE
                    tdsWriter.writeByte(TDSType.BIGVARCHAR.byteValue());
                    if (isNull) {
                        if (dtv.jdbcTypeSetByUser.getIntValue() == java.sql.Types.LONGVARCHAR) {
                            tdsWriter.writeShort((short) DataTypes.MAX_VARTYPE_MAX_CHARS);
                        } else {
                            tdsWriter.writeShort((short) ((0 != valueLength) ? valueLength : 1));
                        }
                    } else {
                        if (dtv.jdbcTypeSetByUser.getIntValue() == java.sql.Types.LONGVARCHAR) {
                            tdsWriter.writeShort((short) DataTypes.MAX_VARTYPE_MAX_CHARS);
                        } else if ((dtv.getJdbcType().getIntValue() == java.sql.Types.LONGVARCHAR)
                                || (dtv.getJdbcType().getIntValue() == java.sql.Types.LONGNVARCHAR)) {
                            tdsWriter.writeShort((short) 1);
                        } else {
                            if (valueLength > DataTypes.SHORT_VARTYPE_MAX_BYTES) {
                                tdsWriter.writeShort((short) DataTypes.MAX_VARTYPE_MAX_CHARS);
                            } else {
                                tdsWriter.writeShort((short) valueLength);
                            }
                        }
                    }

                    if (null != collation)
                        collation.writeCollation(tdsWriter);
                    else
                        conn.getDatabaseCollation().writeCollation(tdsWriter);
                    break;

                case java.sql.Types.LONGNVARCHAR:
                case java.sql.Types.NVARCHAR: // 0xE7
                    tdsWriter.writeByte(TDSType.NVARCHAR.byteValue());
                    if (isNull) {
                        if (dtv.jdbcTypeSetByUser.getIntValue() == java.sql.Types.LONGNVARCHAR) {
                            tdsWriter.writeShort((short) DataTypes.MAX_VARTYPE_MAX_CHARS);
                        } else {
                            tdsWriter.writeShort((short) ((0 != valueLength) ? (valueLength * 2) : 1));
                        }
                    } else {
                        if (isOutParam) {
                            // for stored procedure output parameter, we need to
                            // double the length that is sent to SQL Server,
                            // Otherwise it gives Operand Clash exception.
                            if (dtv.jdbcTypeSetByUser.getIntValue() == java.sql.Types.LONGNVARCHAR) {
                                tdsWriter.writeShort((short) DataTypes.MAX_VARTYPE_MAX_CHARS);
                            } else {
                                tdsWriter.writeShort((short) (valueLength * 2));
                            }
                        } else {
                            if (valueLength > DataTypes.SHORT_VARTYPE_MAX_BYTES) {
                                tdsWriter.writeShort((short) DataTypes.MAX_VARTYPE_MAX_CHARS);
                            } else {
                                tdsWriter.writeShort((short) valueLength);
                            }
                        }
                    }

                    if (null != collation)
                        collation.writeCollation(tdsWriter);
                    else
                        conn.getDatabaseCollation().writeCollation(tdsWriter);
                    break;

                case java.sql.Types.BINARY: // 0xAD
                    tdsWriter.writeByte(TDSType.BIGBINARY.byteValue());
                    if (isNull)
                        tdsWriter.writeShort((short) ((0 != valueLength) ? valueLength : 1));
                    else
                        tdsWriter.writeShort((short) (valueLength));
                    break;

                case java.sql.Types.LONGVARBINARY:
                case java.sql.Types.VARBINARY: // 0xA5
                    // BIGVARBINARY
                    tdsWriter.writeByte(TDSType.BIGVARBINARY.byteValue());
                    if (isNull) {
                        if (dtv.jdbcTypeSetByUser.getIntValue() == java.sql.Types.LONGVARBINARY) {
                            tdsWriter.writeShort((short) DataTypes.MAX_VARTYPE_MAX_BYTES);
                        } else {
                            tdsWriter.writeShort((short) ((0 != valueLength) ? valueLength : 1));
                        }
                    } else {
                        if (dtv.jdbcTypeSetByUser.getIntValue() == java.sql.Types.LONGVARBINARY) {
                            tdsWriter.writeShort((short) DataTypes.MAX_VARTYPE_MAX_BYTES);
                        } else {
                            tdsWriter.writeShort((short) (valueLength));
                        }
                    }
                    break;
                default:
                    MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_UnsupportedDataTypeAE"));
                    throw new SQLServerException(form.format(new Object[] {destType}), null, 0, null);
            }

            tdsWriter.writeCryptoMetaData();
        }

        void execute(DTV dtv, Blob blobValue) throws SQLServerException {
            assert null != blobValue;

            long blobLength = 0;
            InputStream blobStream = null;

            try {
                blobLength = DataTypes.getCheckedLength(conn, dtv.getJdbcType(), blobValue.length(), false);
                blobStream = blobValue.getBinaryStream();
            } catch (SQLException e) {
                SQLServerException.makeFromDriverError(conn, null, e.getMessage(), null, false);
            }

            if (null == blobStream) {
                tdsWriter.writeRPCByteArray(name, null, isOutParam, dtv.getJdbcType(), collation);
            } else {
                tdsWriter.writeRPCInputStream(name, blobStream, blobLength, isOutParam, dtv.getJdbcType(), collation);
            }
        }

        void execute(DTV dtv, SQLServerSQLXML xmlValue) throws SQLServerException {
            InputStream o = (null == xmlValue) ? null : xmlValue.getValue();
            tdsWriter.writeRPCXML(name, o, null == o ? 0 : dtv.getStreamSetterArgs().getLength(), isOutParam);
        }

        void execute(DTV dtv, InputStream inputStreamValue) throws SQLServerException {
            tdsWriter.writeRPCInputStream(name, inputStreamValue,
                    null == inputStreamValue ? 0 : dtv.getStreamSetterArgs().getLength(), isOutParam, dtv.getJdbcType(),
                    collation);
        }

        void execute(DTV dtv, Reader readerValue) throws SQLServerException {
            JDBCType jdbcType = dtv.getJdbcType();

            // executeOp should have handled null Reader as a null String.
            assert null != readerValue;

            // Non-unicode JDBCType should have been handled before now
            assert (JDBCType.NCHAR == jdbcType || JDBCType.NVARCHAR == jdbcType || JDBCType.LONGNVARCHAR == jdbcType
                    || JDBCType.NCLOB == jdbcType) : "SendByRPCOp(Reader): Unexpected JDBC type " + jdbcType;

            // Write the reader value as a stream of Unicode characters
            tdsWriter.writeRPCReaderUnicode(name, readerValue, dtv.getStreamSetterArgs().getLength(), isOutParam,
                    collation);
        }

        /*
         * (non-Javadoc)
         * @see com.microsoft.sqlserver.jdbc.DTVExecuteOp#execute(com.microsoft.sqlserver.jdbc.DTV,
         * microsoft.sql.SqlVariant)
         */
        @Override
        void execute(DTV dtv, SqlVariant sqlVariantValue) throws SQLServerException {
            tdsWriter.writeRPCSqlVariant(name, sqlVariantValue, isOutParam);

        }
    }

    /**
     * Execute a caller-defined, object type-specific operation on a DTV.
     *
     * See DTVExecuteOp
     */
    final void executeOp(DTVExecuteOp op) throws SQLServerException {
        JDBCType jdbcType = getJdbcType();
        Object value = getSetterValue();
        JavaType javaType = getJavaType();
        boolean unsupportedConversion = false;
        byte[] byteValue = null;

        if (null != cryptoMeta && !SetterConversionAE.converts(javaType, jdbcType, sendStringParametersAsUnicode)) {
            MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_unsupportedConversionAE"));
            Object[] msgArgs = {javaType.toString().toLowerCase(Locale.ENGLISH),
                    jdbcType.toString().toLowerCase(Locale.ENGLISH)};
            throw new SQLServerException(form.format(msgArgs), null);
        }

        if (null == value) {

            switch (jdbcType) {
                case NCHAR:
                case NVARCHAR:
                case LONGNVARCHAR:
                case NCLOB:
                    if (null != cryptoMeta)
                        op.execute(this, (byte[]) null);
                    else
                        op.execute(this, (String) null);
                    break;

                case INTEGER:
                    if (null != cryptoMeta)
                        op.execute(this, (byte[]) null);
                    else
                        op.execute(this, (Integer) null);
                    break;

                case DATE:
                    op.execute(this, (java.sql.Date) null);
                    break;

                case TIME:
                    op.execute(this, (java.sql.Time) null);
                    break;

                case DATETIME:
                case SMALLDATETIME:
                case TIMESTAMP:
                    op.execute(this, (java.sql.Timestamp) null);
                    break;

                case TIME_WITH_TIMEZONE:
                case TIMESTAMP_WITH_TIMEZONE:
                case DATETIMEOFFSET:
                    op.execute(this, (microsoft.sql.DateTimeOffset) null);
                    break;

                case FLOAT:
                case REAL:
                    if (null != cryptoMeta)
                        op.execute(this, (byte[]) null);
                    else
                        op.execute(this, (Float) null);
                    break;

                case NUMERIC:
                case DECIMAL:
                case MONEY:
                case SMALLMONEY:
                    if (null != cryptoMeta)
                        op.execute(this, (byte[]) null);
                    else
                        op.execute(this, (BigDecimal) null);
                    break;

                case BINARY:
                case VARBINARY:
                case LONGVARBINARY:
                case BLOB:
                case CHAR:
                case VARCHAR:
                case LONGVARCHAR:
                case CLOB:
                case GUID:
                    op.execute(this, (byte[]) null);
                    break;

                case TINYINT:
                    if (null != cryptoMeta)
                        op.execute(this, (byte[]) null);
                    else
                        op.execute(this, (Byte) null);
                    break;

                case BIGINT:
                    if (null != cryptoMeta)
                        op.execute(this, (byte[]) null);
                    else
                        op.execute(this, (Long) null);
                    break;

                case DOUBLE:
                    if (null != cryptoMeta)
                        op.execute(this, (byte[]) null);
                    else
                        op.execute(this, (Double) null);
                    break;

                case SMALLINT:
                    if (null != cryptoMeta)
                        op.execute(this, (byte[]) null);
                    else
                        op.execute(this, (Short) null);
                    break;

                case BIT:
                case BOOLEAN:
                    if (null != cryptoMeta)
                        op.execute(this, (byte[]) null);
                    else
                        op.execute(this, (Boolean) null);
                    break;

                case SQLXML:
                    op.execute(this, (SQLServerSQLXML) null);
                    break;

                case ARRAY:
                case DATALINK:
                case DISTINCT:
                case JAVA_OBJECT:
                case NULL:
                case OTHER:
                case REF:
                case ROWID:
                case STRUCT:
                    unsupportedConversion = true;
                    break;

                case SQL_VARIANT:
                    op.execute(this, (SqlVariant) null);
                    break;

                case UNKNOWN:
                default:
                    assert false : "Unexpected JDBCType: " + jdbcType;
                    unsupportedConversion = true;
                    break;
            }
        } else // null != value
        {
            if (aeLogger.isLoggable(java.util.logging.Level.FINE) && (null != cryptoMeta)) {
                aeLogger.fine("Encrypting java data type: " + javaType);
            }

            switch (javaType) {
                case STRING:
                    if (JDBCType.GUID == jdbcType) {
                        if (null != cryptoMeta) {
                            if (value instanceof String) {
                                value = UUID.fromString((String) value);
                            }
                            byte[] bArray = Util.asGuidByteArray((UUID) value);
                            op.execute(this, bArray);
                        } else {
                            op.execute(this, String.valueOf(value));
                        }
                    } else if (JDBCType.SQL_VARIANT == jdbcType) {
                        op.execute(this, String.valueOf(value));
                    } else if (JDBCType.GEOMETRY == jdbcType) {
                        op.execute(this, ((Geometry) value).serialize());
                    } else if (JDBCType.GEOGRAPHY == jdbcType) {
                        op.execute(this, ((Geography) value).serialize());
                    } else {
                        if (null != cryptoMeta) {
                            // if streaming types check for allowed data length in AE
                            // jdbcType is set to LONGNVARCHAR if input data length is >
                            // DataTypes.SHORT_VARTYPE_MAX_CHARS for string
                            if ((jdbcType == JDBCType.LONGNVARCHAR) && (JDBCType.VARCHAR == jdbcTypeSetByUser)
                                    && (DataTypes.MAX_VARTYPE_MAX_BYTES < valueLength)) {
                                MessageFormat form = new MessageFormat(
                                        SQLServerException.getErrString("R_StreamingDataTypeAE"));
                                Object[] msgArgs = {DataTypes.MAX_VARTYPE_MAX_BYTES, JDBCType.LONGVARCHAR};
                                throw new SQLServerException(this, form.format(msgArgs), null, 0, false);
                            } else if ((JDBCType.NVARCHAR == jdbcTypeSetByUser)
                                    && (DataTypes.MAX_VARTYPE_MAX_CHARS < valueLength)) {
                                MessageFormat form = new MessageFormat(
                                        SQLServerException.getErrString("R_StreamingDataTypeAE"));
                                Object[] msgArgs = {DataTypes.MAX_VARTYPE_MAX_CHARS, JDBCType.LONGNVARCHAR};
                                throw new SQLServerException(this, form.format(msgArgs), null, 0, false);
                            }
                            // Each character is represented using 2 bytes in NVARCHAR
                            else if ((JDBCType.NVARCHAR == jdbcTypeSetByUser) || (JDBCType.NCHAR == jdbcTypeSetByUser)
                                    || (JDBCType.LONGNVARCHAR == jdbcTypeSetByUser)) {
                                byteValue = ((String) value).getBytes(UTF_16LE);
                            }
                            // Each character is represented using 1 bytes in VARCHAR
                            else if ((JDBCType.VARCHAR == jdbcTypeSetByUser) || (JDBCType.CHAR == jdbcTypeSetByUser)
                                    || (JDBCType.LONGVARCHAR == jdbcTypeSetByUser)) {
                                byteValue = ((String) value).getBytes();
                            }

                            op.execute(this, byteValue);
                        } else
                            op.execute(this, (String) value);
                    }
                    break;

                case INTEGER:
                    if (null != cryptoMeta) {
                        byteValue = ByteBuffer.allocate(Long.SIZE / Byte.SIZE).order(ByteOrder.LITTLE_ENDIAN)
                                .putLong(((Integer) value).longValue()).array();
                        op.execute(this, byteValue);
                    } else
                        op.execute(this, (Integer) value);
                    break;

                case DATE:
                    op.execute(this, (java.sql.Date) value);
                    break;

                case TIME:
                    op.execute(this, (java.sql.Time) value);
                    break;

                case TIMESTAMP:
                    op.execute(this, (java.sql.Timestamp) value);
                    break;

                case TVP:
                    op.execute(this, (TVP) value);
                    break;

                case UTILDATE:
                    op.execute(this, (java.util.Date) value);
                    break;

                case CALENDAR:
                    op.execute(this, (java.util.Calendar) value);
                    break;

                case LOCALDATE:
                    op.execute(this, (LocalDate) value);
                    break;

                case LOCALTIME:
                    op.execute(this, (LocalTime) value);
                    break;

                case LOCALDATETIME:
                    op.execute(this, (LocalDateTime) value);
                    break;

                case OFFSETTIME:
                    op.execute(this, (OffsetTime) value);
                    break;

                case OFFSETDATETIME:
                    op.execute(this, (OffsetDateTime) value);
                    break;

                case DATETIMEOFFSET:
                    op.execute(this, (microsoft.sql.DateTimeOffset) value);
                    break;

                case GEOMETRY:
                    op.execute(this, ((Geometry) value).serialize());
                    break;

                case GEOGRAPHY:
                    op.execute(this, ((Geography) value).serialize());
                    break;

                case FLOAT:
                    if (null != cryptoMeta) {
                        if (Float.isInfinite((Float) value)) {
                            MessageFormat form = new MessageFormat(
                                    SQLServerException.getErrString("R_valueOutOfRange"));
                            throw new SQLServerException(form.format(new Object[] {jdbcType}), null, 0, null);
                        }

                        byteValue = ByteBuffer.allocate((Float.SIZE / Byte.SIZE)).order(ByteOrder.LITTLE_ENDIAN)
                                .putFloat((Float) value).array();
                        op.execute(this, byteValue);
                    } else
                        op.execute(this, (Float) value);
                    break;

                case BIGDECIMAL:
                    if (null != cryptoMeta) {
                        // For AE, we need to use the setMoney/setSmallMoney methods to send encrypted data
                        // to money types. Also we need to use the TDS MONEYN rule for these.
                        // For these methods, the Java type is still BigDecimal, but the JDBCType
                        // would be JDBCType.MONEY or JDBCType.SMALLMONEY.
                        if ((JDBCType.MONEY == jdbcType) || (JDBCType.SMALLMONEY == jdbcType)) {
                            // For TDS we need to send the money value multiplied by 10^4 - this gives us the
                            // money value as integer. 4 is the default and only scale available with money.
                            // smallmoney is noralized to money.
                            BigDecimal bdValue = (BigDecimal) value;
                            // Need to validate range in the client side as we are converting BigDecimal to integers.
                            Util.validateMoneyRange(bdValue, jdbcType);

                            // Get the total number of digits after the multiplication. Scale is hardcoded to 4. This is
                            // needed to get the proper
                            // rounding.
                            // BigDecimal calculates precision a bit differently. For 0.000001, the precision is 1, but
                            // scale is 6 which makes the
                            // digit count -ve.
                            int digitCount = Math.max((bdValue.precision() - bdValue.scale()), 0) + 4;

                            long moneyVal = ((BigDecimal) value)
                                    .multiply(new BigDecimal(10000), new MathContext(digitCount, RoundingMode.HALF_UP))
                                    .longValue();
                            ByteBuffer bbuf = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN);
                            bbuf.putInt((int) (moneyVal >> 32)).array();
                            bbuf.putInt((int) moneyVal).array();
                            op.execute(this, bbuf.array());
                        } else {
                            BigDecimal bigDecimalVal = (BigDecimal) value;
                            byte[] decimalToByte = DDC.convertBigDecimalToBytes(bigDecimalVal, bigDecimalVal.scale());
                            byteValue = new byte[16];
                            // removing the precision and scale information from the decimalToByte array
                            System.arraycopy(decimalToByte, 2, byteValue, 0, decimalToByte.length - 2);
                            this.setScale(bigDecimalVal.scale());

                            if (null != cryptoMeta.getBaseTypeInfo()) {
                                // if the precision of the column is smaller than the precision of the actual value,
                                // the driver throws exception
                                if (cryptoMeta.getBaseTypeInfo().getPrecision() < Util
                                        .getValueLengthBaseOnJavaType(bigDecimalVal, javaType, null, null, jdbcType)) {
                                    MessageFormat form = new MessageFormat(
                                            SQLServerException.getErrString("R_valueOutOfRange"));
                                    Object[] msgArgs = {cryptoMeta.getBaseTypeInfo().getSSTypeName()};
                                    throw new SQLServerException(form.format(msgArgs),
                                            SQLState.NUMERIC_DATA_OUT_OF_RANGE, DriverError.NOT_SET, null);
                                }
                            } else {
                                // if the precision that user provides is smaller than the precision of the actual
                                // value,
                                // the driver assumes the precision that user provides is the correct precision, and
                                // throws
                                // exception
                                if (valueLength < Util.getValueLengthBaseOnJavaType(bigDecimalVal, javaType, null, null,
                                        jdbcType)) {
                                    MessageFormat form = new MessageFormat(
                                            SQLServerException.getErrString("R_valueOutOfRange"));
                                    Object[] msgArgs = {SSType.DECIMAL};
                                    throw new SQLServerException(form.format(msgArgs),
                                            SQLState.NUMERIC_DATA_OUT_OF_RANGE, DriverError.NOT_SET, null);
                                }
                            }

                            op.execute(this, byteValue);
                        }
                    } else
                        op.execute(this, (BigDecimal) value);
                    break;

                case BYTEARRAY:
                    if ((null != cryptoMeta) && (DataTypes.MAX_VARTYPE_MAX_BYTES < valueLength)) {
                        MessageFormat form = new MessageFormat(
                                SQLServerException.getErrString("R_StreamingDataTypeAE"));
                        Object[] msgArgs = {DataTypes.MAX_VARTYPE_MAX_BYTES, JDBCType.BINARY};
                        throw new SQLServerException(this, form.format(msgArgs), null, 0, false);
                    } else
                        op.execute(this, (byte[]) value);
                    break;

                case BYTE:
                    // for tinyint
                    if (null != cryptoMeta) {
                        byteValue = ByteBuffer.allocate(Long.SIZE / Byte.SIZE).order(ByteOrder.LITTLE_ENDIAN)
                                .putLong((byte) value & 0xFF).array();
                        op.execute(this, byteValue);
                    } else
                        op.execute(this, (Byte) value);
                    break;

                case LONG:
                    if (null != cryptoMeta) {
                        byteValue = ByteBuffer.allocate((Long.SIZE / Byte.SIZE)).order(ByteOrder.LITTLE_ENDIAN)
                                .putLong((Long) value).array();
                        op.execute(this, byteValue);
                    } else
                        op.execute(this, (Long) value);
                    break;

                case BIGINTEGER:
                    op.execute(this, (java.math.BigInteger) value);
                    break;

                case DOUBLE:
                    if (null != cryptoMeta) {
                        if (Double.isInfinite((Double) value)) {
                            MessageFormat form = new MessageFormat(
                                    SQLServerException.getErrString("R_valueOutOfRange"));
                            throw new SQLServerException(form.format(new Object[] {jdbcType}), null, 0, null);
                        }
                        byteValue = ByteBuffer.allocate((Double.SIZE / Byte.SIZE)).order(ByteOrder.LITTLE_ENDIAN)
                                .putDouble((Double) value).array();
                        op.execute(this, byteValue);
                    } else
                        op.execute(this, (Double) value);
                    break;

                case SHORT:
                    if (null != cryptoMeta) {
                        byteValue = ByteBuffer.allocate(Long.SIZE / Byte.SIZE).order(ByteOrder.LITTLE_ENDIAN)
                                .putLong((short) value).array();
                        op.execute(this, byteValue);
                    } else
                        op.execute(this, (Short) value);
                    break;

                case BOOLEAN:
                    if (null != cryptoMeta) {
                        byteValue = ByteBuffer.allocate(Long.SIZE / Byte.SIZE).order(ByteOrder.LITTLE_ENDIAN)
                                .putLong((Boolean) value ? 1 : 0).array();
                        op.execute(this, byteValue);
                    } else
                        op.execute(this, (Boolean) value);
                    break;

                case BLOB:
                    op.execute(this, (Blob) value);
                    break;

                case CLOB:
                case NCLOB:
                    op.execute(this, (Clob) value);
                    break;

                case INPUTSTREAM:
                    op.execute(this, (InputStream) value);
                    break;

                case READER:
                    op.execute(this, (Reader) value);
                    break;

                case SQLXML:
                    op.execute(this, (SQLServerSQLXML) value);
                    break;

                default:
                    assert false : "Unexpected JavaType: " + javaType;
                    unsupportedConversion = true;
                    break;
            }
        }

        if (unsupportedConversion) {
            MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_unsupportedConversionFromTo"));
            Object[] msgArgs = {javaType, jdbcType};
            throw new SQLServerException(form.format(msgArgs), SQLState.DATA_EXCEPTION_NOT_SPECIFIC,
                    DriverError.NOT_SET, null);
        }
    }

    void sendCryptoMetaData(CryptoMetadata cryptoMeta, TDSWriter tdsWriter) {
        this.cryptoMeta = cryptoMeta;
        tdsWriter.setCryptoMetaData(cryptoMeta);
    }

    void setJdbcTypeSetByUser(JDBCType jdbcTypeSetByUser, int valueLength) {
        this.jdbcTypeSetByUser = jdbcTypeSetByUser;
        this.valueLength = valueLength;
    }

    /**
     * Serializes a value as the specified type for RPC.
     */
    void sendByRPC(String name, TypeInfo typeInfo, SQLCollation collation, int precision, int outScale,
            boolean isOutParam, TDSWriter tdsWriter, SQLServerStatement statement) throws SQLServerException {
        // typeInfo is null when called from PreparedStatement->Parameter->SendByRPC
        executeOp(new SendByRPCOp(name, typeInfo, collation, precision, outScale, isOutParam, tdsWriter, statement));
    }
}


/**
 * DTV implementation class interface.
 *
 * Currently there are two implementations: one for values which originate from Java values set by the app (AppDTVImpl)
 * and one for values which originate from byte in the TDS response buffer (ServerDTVImpl).
 */
abstract class DTVImpl {
    abstract void setValue(DTV dtv, SQLCollation collation, JDBCType jdbcType, Object value, JavaType javaType,
            StreamSetterArgs streamSetterArgs, Calendar cal, Integer scale, SQLServerConnection con,
            boolean forceEncrypt) throws SQLServerException;

    abstract void setValue(Object value, JavaType javaType);

    abstract void setStreamSetterArgs(StreamSetterArgs streamSetterArgs);

    abstract void setCalendar(Calendar cal);

    abstract void setScale(Integer scale);

    abstract void setForceEncrypt(boolean forceEncrypt);

    abstract StreamSetterArgs getStreamSetterArgs();

    abstract Calendar getCalendar();

    abstract Integer getScale();

    abstract boolean isNull();

    abstract void setJdbcType(JDBCType jdbcType);

    abstract JDBCType getJdbcType();

    abstract JavaType getJavaType();

    abstract Object getValue(DTV dtv, JDBCType jdbcType, int scale, InputStreamGetterArgs streamGetterArgs,
            Calendar cal, TypeInfo type, CryptoMetadata cryptoMetadata, TDSReader tdsReader, SQLServerStatement statement) throws SQLServerException;

    abstract Object getSetterValue();

    abstract void skipValue(TypeInfo typeInfo, TDSReader tdsReader, boolean isDiscard) throws SQLServerException;

    abstract void initFromCompressedNull();

    abstract SqlVariant getInternalVariant();
}


/**
 * DTV implementation for values set by the app from Java types.
 */
final class AppDTVImpl extends DTVImpl {
    private JDBCType jdbcType = JDBCType.UNKNOWN;
    private Object value;
    private JavaType javaType;
    private StreamSetterArgs streamSetterArgs;
    private Calendar cal;
    private Integer scale;
    @SuppressWarnings("unused")
    private boolean forceEncrypt;
    private SqlVariant internalVariant;

    final void skipValue(TypeInfo typeInfo, TDSReader tdsReader, boolean isDiscard) throws SQLServerException {
        assert false;
    }

    final void initFromCompressedNull() {
        assert false;
    }

    final class SetValueOp extends DTVExecuteOp {
        private final SQLCollation collation;
        private final SQLServerConnection con;

        SetValueOp(SQLCollation collation, SQLServerConnection con) {
            this.collation = collation;
            this.con = con;
        }

        void execute(DTV dtv, String strValue) throws SQLServerException {
            JDBCType jdbcType = dtv.getJdbcType();

            // Normally we let the server convert the string to whatever backend
            // type it is going into. However, if the app says that the string
            // is a numeric/decimal value then convert the value to a BigDecimal
            // now so that GetTypeDefinitionOp can generate a type definition
            // with the correct scale.

            if ((JDBCType.DECIMAL == jdbcType) || (JDBCType.NUMERIC == jdbcType) || (JDBCType.MONEY == jdbcType)
                    || (JDBCType.SMALLMONEY == jdbcType)) {
                assert null != strValue;

                try {
                    dtv.setValue(new BigDecimal(strValue), JavaType.BIGDECIMAL);
                } catch (NumberFormatException e) {
                    DataTypes.throwConversionError("String", jdbcType.toString());
                }
            }

            // If the backend column is a binary type, or we don't know the backend
            // type, but we are being told that it is binary, then we need to convert
            // the hexized text value to binary here because the server doesn't do
            // this conversion for us.
            else if (jdbcType.isBinary()) {
                assert null != strValue;
                dtv.setValue(ParameterUtils.HexToBin(strValue), JavaType.BYTEARRAY);
            }

            // If the (Unicode) string value is to be sent to the server as MBCS,
            // then do the conversion now so that the decision to use a "short" or "long"
            // SSType (i.e. VARCHAR vs. TEXT/VARCHAR(max)) is based on the exact length of
            // the MBCS value (in bytes).
            else if (null != collation && (JDBCType.CHAR == jdbcType || JDBCType.VARCHAR == jdbcType
                    || JDBCType.LONGVARCHAR == jdbcType || JDBCType.CLOB == jdbcType)) {
                byte[] nativeEncoding = null;

                if (null != strValue) {
                    nativeEncoding = strValue.getBytes(collation.getCharset());
                }

                dtv.setValue(nativeEncoding, JavaType.BYTEARRAY);
            }
        }

        void execute(DTV dtv, Clob clobValue) throws SQLServerException {
            // executeOp should have handled null Clob as a String
            assert null != clobValue;

            // Fail fast if the Clob's advertised length is not within bounds.
            //
            // The Clob's length or contents may still change before SendByRPCOp
            // materializes the Clob at execution time if the app fails to treat
            // the parameter as immutable once set, as is recommended by the JDBC spec.
            try {
                DataTypes.getCheckedLength(con, dtv.getJdbcType(), clobValue.length(), false);
            } catch (SQLException e) {
                SQLServerException.makeFromDriverError(con, null, e.getMessage(), null, false);
            }
        }

        void execute(DTV dtv, SQLServerSQLXML xmlValue) throws SQLServerException {}

        void execute(DTV dtv, Byte byteValue) throws SQLServerException {}

        void execute(DTV dtv, Integer intValue) throws SQLServerException {}

        void execute(DTV dtv, java.sql.Time timeValue) throws SQLServerException {
            if (dtv.getJdbcType().isTextual()) {
                assert timeValue != null : "value is null";
                dtv.setValue(timeValue.toString(), JavaType.STRING);
            }
        }

        void execute(DTV dtv, java.sql.Date dateValue) throws SQLServerException {
            if (dtv.getJdbcType().isTextual()) {
                assert dateValue != null : "value is null";
                dtv.setValue(dateValue.toString(), JavaType.STRING);
            }
        }

        void execute(DTV dtv, java.sql.Timestamp timestampValue) throws SQLServerException {
            if (dtv.getJdbcType().isTextual()) {
                assert timestampValue != null : "value is null";
                dtv.setValue(timestampValue.toString(), JavaType.STRING);
            }
        }

        void execute(DTV dtv, java.util.Date utilDateValue) throws SQLServerException {
            if (dtv.getJdbcType().isTextual()) {
                assert utilDateValue != null : "value is null";
                dtv.setValue(utilDateValue.toString(), JavaType.STRING);
            }
        }

        void execute(DTV dtv, LocalDate localDateValue) throws SQLServerException {
            if (dtv.getJdbcType().isTextual()) {
                assert localDateValue != null : "value is null";
                dtv.setValue(localDateValue.toString(), JavaType.STRING);
            }
        }

        void execute(DTV dtv, LocalTime localTimeValue) throws SQLServerException {
            if (dtv.getJdbcType().isTextual()) {
                assert localTimeValue != null : "value is null";
                dtv.setValue(localTimeValue.toString(), JavaType.STRING);
            }
        }

        void execute(DTV dtv, LocalDateTime localDateTimeValue) throws SQLServerException {
            if (dtv.getJdbcType().isTextual()) {
                assert localDateTimeValue != null : "value is null";
                dtv.setValue(localDateTimeValue.toString(), JavaType.STRING);
            }
        }

        void execute(DTV dtv, OffsetTime offsetTimeValue) throws SQLServerException {
            if (dtv.getJdbcType().isTextual()) {
                assert offsetTimeValue != null : "value is null";
                dtv.setValue(offsetTimeValue.toString(), JavaType.STRING);
            }
        }

        void execute(DTV dtv, OffsetDateTime offsetDateTimeValue) throws SQLServerException {
            if (dtv.getJdbcType().isTextual()) {
                assert offsetDateTimeValue != null : "value is null";
                dtv.setValue(offsetDateTimeValue.toString(), JavaType.STRING);
            }
        }

        void execute(DTV dtv, java.util.Calendar calendarValue) throws SQLServerException {
            if (dtv.getJdbcType().isTextual()) {
                assert calendarValue != null : "value is null";
                dtv.setValue(calendarValue.toString(), JavaType.STRING);
            }
        }

        void execute(DTV dtv, microsoft.sql.DateTimeOffset dtoValue) throws SQLServerException {
            if (dtv.getJdbcType().isTextual()) {
                assert dtoValue != null : "value is null";
                dtv.setValue(dtoValue.toString(), JavaType.STRING);
            }
        }

        void execute(DTV dtv, TVP tvpValue) throws SQLServerException {}

        void execute(DTV dtv, Float floatValue) throws SQLServerException {}

        void execute(DTV dtv, Double doubleValue) throws SQLServerException {}

        void execute(DTV dtv, BigDecimal bigDecimalValue) throws SQLServerException {
            // Rescale the value if necessary
            if (null != bigDecimalValue) {
                Integer dtvScale, biScale = bigDecimalValue.scale();
                if (null == dtv.getScale() && JDBCType.DECIMAL == dtv.getJdbcType()) {
                    dtvScale = bigDecimalValue
                            .precision() > SQLServerConnection.maxDecimalPrecision ? SQLServerConnection.maxDecimalPrecision
                                    - (bigDecimalValue.precision() - biScale) : biScale;
                    if (dtvScale > SQLServerConnection.maxDecimalPrecision) {
                        dtv.setScale(SQLServerConnection.maxDecimalPrecision);
                        dtvScale = SQLServerConnection.maxDecimalPrecision;
                    } else {
                        dtv.setScale(dtvScale);
                    }
                } else
                    dtvScale = dtv.getScale();
                if (null != dtvScale && 0 != Integer.compare(dtvScale, biScale))
                    bigDecimalValue = bigDecimalValue.setScale(dtvScale, RoundingMode.DOWN);
            }
            dtv.setValue(bigDecimalValue, JavaType.BIGDECIMAL);
        }

        void execute(DTV dtv, Long longValue) throws SQLServerException {}

        void execute(DTV dtv, BigInteger bigIntegerValue) throws SQLServerException {}

        void execute(DTV dtv, Short shortValue) throws SQLServerException {}

        void execute(DTV dtv, Boolean booleanValue) throws SQLServerException {}

        void execute(DTV dtv, byte[] byteArrayValue) throws SQLServerException {}

        void execute(DTV dtv, Blob blobValue) throws SQLServerException {
            assert null != blobValue;

            // Fail fast if the Blob's advertised length is not within bounds.
            //
            // The Blob's length or contents may still change before SendByRPCOp
            // materializes the Blob at execution time if the app fails to treat
            // the parameter as immutable once set, as is recommended by the JDBC spec.
            try {
                DataTypes.getCheckedLength(con, dtv.getJdbcType(), blobValue.length(), false);
            } catch (SQLException e) {
                SQLServerException.makeFromDriverError(con, null, e.getMessage(), null, false);
            }
        }

        void execute(DTV dtv, InputStream inputStreamValue) throws SQLServerException {
            DataTypes.getCheckedLength(con, dtv.getJdbcType(), dtv.getStreamSetterArgs().getLength(), true);

            // If the stream is to be sent as Unicode, then assume it's an ASCII stream
            if (JDBCType.NCHAR == jdbcType || JDBCType.NVARCHAR == jdbcType || JDBCType.LONGNVARCHAR == jdbcType) {
                Reader readerValue = null;
                readerValue = new InputStreamReader(inputStreamValue, StandardCharsets.US_ASCII);

                dtv.setValue(readerValue, JavaType.READER);

                // No need to change SetterArgs since, for ASCII streams, the
                // Reader length, in characters, is the same as the InputStream
                // length, in bytes.

                // Re-execute with the stream value to perform any further conversions
                execute(dtv, readerValue);
            }
        }

        void execute(DTV dtv, Reader readerValue) throws SQLServerException {
            // executeOp should have handled null Reader as a null String.
            assert null != readerValue;

            JDBCType jdbcType = dtv.getJdbcType();
            long readerLength = DataTypes.getCheckedLength(con, dtv.getJdbcType(),
                    dtv.getStreamSetterArgs().getLength(), true);

            if (
            // If the backend column is a binary type, or we don't know the backend
            // type, but we are being told that it is binary, then we need to convert
            // the hexized text value to binary here because the server doesn't do
            // this conversion for us.
            jdbcType.isBinary()) {
                String stringValue = DDC.convertReaderToString(readerValue, (int) readerLength);

                // If we were given an input stream length that we had to match and
                // the actual stream length did not match then cancel the request.
                if (DataTypes.UNKNOWN_STREAM_LENGTH != readerLength && stringValue.length() != readerLength) {
                    MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_mismatchedStreamLength"));
                    Object[] msgArgs = {readerLength, stringValue.length()};
                    SQLServerException.makeFromDriverError(null, null, form.format(msgArgs), "", true);
                }

                dtv.setValue(stringValue, JavaType.STRING);
                execute(dtv, stringValue);
            }

            // If the reader value is to be sent as MBCS, then convert the value to an MBCS InputStream
            else if (null != collation && (JDBCType.CHAR == jdbcType || JDBCType.VARCHAR == jdbcType
                    || JDBCType.LONGVARCHAR == jdbcType || JDBCType.CLOB == jdbcType)) {
                ReaderInputStream streamValue = new ReaderInputStream(readerValue, collation.getCharset(),
                        readerLength);

                dtv.setValue(streamValue, JavaType.INPUTSTREAM);

                // Even if we knew the length of the Reader, we do not know the length of the converted MBCS stream up
                // front.
                dtv.setStreamSetterArgs(new StreamSetterArgs(StreamType.CHARACTER, DataTypes.UNKNOWN_STREAM_LENGTH));
                execute(dtv, streamValue);
            }
        }

        /*
         * (non-Javadoc)
         * @see com.microsoft.sqlserver.jdbc.DTVExecuteOp#execute(com.microsoft.sqlserver.jdbc.DTV,
         * microsoft.sql.SqlVariant)
         */
        @Override
        void execute(DTV dtv, SqlVariant SqlVariantValue) throws SQLServerException {}

    }

    void setValue(DTV dtv, SQLCollation collation, JDBCType jdbcType, Object value, JavaType javaType,
            StreamSetterArgs streamSetterArgs, Calendar cal, Integer scale, SQLServerConnection con,
            boolean forceEncrypt) throws SQLServerException {
        // Set the value according to its Java object type, nullness, and specified JDBC type
        dtv.setValue(value, javaType);
        dtv.setJdbcType(jdbcType);
        dtv.setStreamSetterArgs(streamSetterArgs);
        dtv.setCalendar(cal);
        dtv.setScale(scale);
        dtv.setForceEncrypt(forceEncrypt);
        dtv.executeOp(new SetValueOp(collation, con));
    }

    void setValue(Object value, JavaType javaType) {
        this.value = value;
        this.javaType = javaType;
    }

    void setStreamSetterArgs(StreamSetterArgs streamSetterArgs) {
        this.streamSetterArgs = streamSetterArgs;
    }

    void setCalendar(Calendar cal) {
        this.cal = cal;
    }

    void setScale(Integer scale) {
        this.scale = scale;
    }

    void setForceEncrypt(boolean forceEncrypt) {
        this.forceEncrypt = forceEncrypt;
    }

    StreamSetterArgs getStreamSetterArgs() {
        return streamSetterArgs;
    }

    Calendar getCalendar() {
        return cal;
    }

    Integer getScale() {
        return scale;
    }

    boolean isNull() {
        return null == value;
    }

    void setJdbcType(JDBCType jdbcType) {
        this.jdbcType = jdbcType;
    }

    JDBCType getJdbcType() {
        return jdbcType;
    }

    JavaType getJavaType() {
        return javaType;
    }

    Object getValue(DTV dtv, JDBCType jdbcType, int scale, InputStreamGetterArgs streamGetterArgs, Calendar cal,
            TypeInfo typeInfo, CryptoMetadata cryptoMetadata, TDSReader tdsReader, SQLServerStatement statement) throws SQLServerException {
        // Client side type conversion is not supported
        if (this.jdbcType != jdbcType)
            DataTypes.throwConversionError(this.jdbcType.toString(), jdbcType.toString());

        return value;
    }

    Object getSetterValue() {
        return value;
    }

    /*
     * (non-Javadoc)
     * @see com.microsoft.sqlserver.jdbc.DTVImpl#getInternalVariant()
     */
    @Override
    SqlVariant getInternalVariant() {
        return this.internalVariant;
    }

    /**
     * Sets the internal datatype of variant type
     * 
     * @param type
     *        sql_variant internal type
     */
    void setInternalVariant(SqlVariant type) {
        this.internalVariant = type;
    }
}


/**
 * Encapsulation of type information associated with values returned in the TDS response stream (TYPE_INFO).
 *
 * ResultSet rows have type info for each column returned in a COLMETADATA response stream.
 *
 * CallableStatement output parameters have their type info returned in a RETURNVALUE response stream.
 */
final class TypeInfo implements Serializable {
    /**
     * Always refresh SerialVersionUID when prompted
     */
    private static final long serialVersionUID = 6641910171379986768L;

    private int maxLength; // Max length of data
    private SSLenType ssLenType; // Length type (FIXEDLENTYPE, PARTLENTYPE, etc.)
    private int precision;
    private int displaySize;// size is in characters. display size assumes a formatted hexa decimal representation for
                            // binaries.
    private int scale;
    private short flags;
    private SSType ssType;
    private int userType;
    private String udtTypeName;

    // Collation (will be null for non-textual types).
    private SQLCollation collation;
    private Charset charset;

    SSType getSSType() {
        return ssType;
    }

    void setSSType(SSType ssType) {
        this.ssType = ssType;
    }

    SSLenType getSSLenType() {
        return ssLenType;
    }

    void setSSLenType(SSLenType ssLenType) {
        this.ssLenType = ssLenType;
    }

    String getSSTypeName() {
        return (SSType.UDT == ssType) ? udtTypeName : ssType.toString();
    }

    int getMaxLength() {
        return maxLength;
    }

    void setMaxLength(int maxLength) {
        this.maxLength = maxLength;
    }

    int getPrecision() {
        return precision;
    }

    void setPrecision(int precision) {
        this.precision = precision;
    }

    int getDisplaySize() {
        return displaySize;
    }

    void setDisplaySize(int displaySize) {
        this.displaySize = displaySize;
    }

    int getScale() {
        return scale;
    }

    SQLCollation getSQLCollation() {
        return collation;
    }

    void setSQLCollation(SQLCollation collation) {
        this.collation = collation;
    }

    Charset getCharset() {
        return charset;
    }

    void setCharset(Charset charset) {
        this.charset = charset;
    }

    boolean isNullable() {
        return 0x0001 == (flags & 0x0001);
    }

    boolean isCaseSensitive() {
        return 0x0002 == (flags & 0x0002);
    }

    boolean isSparseColumnSet() {
        return 0x0400 == (flags & 0x0400);
    }

    boolean isEncrypted() {
        return 0x0800 == (flags & 0x0800);
    }

    static int UPDATABLE_READ_ONLY = 0;
    static int UPDATABLE_READ_WRITE = 1;
    static int UPDATABLE_UNKNOWN = 2;

    int getUpdatability() {
        return (flags >> 2) & 0x0003;
    }

    boolean isIdentity() {
        return 0x0010 == (flags & 0x0010);
    }

    byte[] getFlags() {
        byte[] f = new byte[2];
        f[0] = (byte) (flags & 0xFF);
        f[1] = (byte) ((flags >> 8) & 0xFF);
        return f;
    }

    short getFlagsAsShort() {
        return flags;
    }

    void setFlags(Short flags) {
        this.flags = flags;
    }

    void setScale(int scale) {
        this.scale = scale;
    }

    // TypeInfo Builder enum defines a set of builders used to construct TypeInfo instances
    // for the various data types. Each builder builds a TypeInfo instance using a builder Strategy.
    // Some strategies are used for multiple types (for example: FixedLenStrategy)
    enum Builder {
        BIT(TDSType.BIT1, new FixedLenStrategy(SSType.BIT, 1, // TDS length (bytes)
                1, // precision (max numeric precision, in decimal digits)
                "1".length(), // column display size
                0) // scale
        ),

        BIGINT(TDSType.INT8, new FixedLenStrategy(SSType.BIGINT, 8, // TDS length (bytes)
                Long.toString(Long.MAX_VALUE).length(), // precision (max numeric precision, in decimal digits)
                ("-" + Long.toString(Long.MAX_VALUE)).length(), // column display size (includes sign)
                0) // scale
        ),

        INTEGER(TDSType.INT4, new FixedLenStrategy(SSType.INTEGER, 4, // TDS length (bytes)
                Integer.toString(Integer.MAX_VALUE).length(), // precision (max numeric precision, in decimal digits)
                ("-" + Integer.toString(Integer.MAX_VALUE)).length(), // column display size (includes sign)
                0) // scale
        ),

        SMALLINT(TDSType.INT2, new FixedLenStrategy(SSType.SMALLINT, 2, // TDS length (bytes)
                Short.toString(Short.MAX_VALUE).length(), // precision (max numeric precision, in decimal digits)
                ("-" + Short.toString(Short.MAX_VALUE)).length(), // column display size (includes sign)
                0) // scale
        ),

        TINYINT(TDSType.INT1, new FixedLenStrategy(SSType.TINYINT, 1, // TDS length (bytes)
                Byte.toString(Byte.MAX_VALUE).length(), // precision (max numeric precision, in decimal digits)
                Byte.toString(Byte.MAX_VALUE).length(), // column display size (no sign - TINYINT is unsigned)
                0) // scale
        ),

        REAL(TDSType.FLOAT4, new FixedLenStrategy(SSType.REAL, 4, // TDS length (bytes)
                7, // precision (max numeric precision, in bits)
                13, // column display size
                0) // scale
        ),

        FLOAT(TDSType.FLOAT8, new FixedLenStrategy(SSType.FLOAT, 8, // TDS length (bytes)
                15, // precision (max numeric precision, in bits)
                22, // column display size
                0) // scale
        ),

        SMALLDATETIME(TDSType.DATETIME4, new FixedLenStrategy(SSType.SMALLDATETIME, 4, // TDS length (bytes)
                "yyyy-mm-dd hh:mm".length(), // precision (formatted length, in characters, assuming max fractional
                                             // seconds precision (0))
                "yyyy-mm-dd hh:mm".length(), // column display size
                0) // scale
        ),

        DATETIME(TDSType.DATETIME8, new FixedLenStrategy(SSType.DATETIME, 8, // TDS length (bytes)
                "yyyy-mm-dd hh:mm:ss.fff".length(), // precision (formatted length, in characters, assuming max
                                                    // fractional seconds precision)
                "yyyy-mm-dd hh:mm:ss.fff".length(), // column display size
                3) // scale
        ),

        SMALLMONEY(TDSType.MONEY4, new FixedLenStrategy(SSType.SMALLMONEY, 4, // TDS length (bytes)
                Integer.toString(Integer.MAX_VALUE).length(), // precision (max unscaled numeric precision, in decimal
                                                              // digits)
                ("-" + "." + Integer.toString(Integer.MAX_VALUE)).length(), // column display size (includes sign and
                                                                            // decimal for scale)
                4) // scale
        ),

        MONEY(TDSType.MONEY8, new FixedLenStrategy(SSType.MONEY, 8, // TDS length (bytes)
                Long.toString(Long.MAX_VALUE).length(), // precision (max unscaled numeric precision, in decimal digits)
                ("-" + "." + Long.toString(Long.MAX_VALUE)).length(), // column display size (includes sign and decimal
                                                                      // for scale)
                4) // scale
        ),

        BITN(TDSType.BITN, new Strategy() {
            /**
             * Sets the fields of typeInfo to the correct values
             * 
             * @param typeInfo
             *        the TypeInfo whos values are being corrected
             * @param tdsReader
             *        the TDSReader used to set the fields of typeInfo to the correct values
             * @throws SQLServerException
             *         when an error occurs
             */
            public void apply(TypeInfo typeInfo, TDSReader tdsReader) throws SQLServerException {
                if (1 != tdsReader.readUnsignedByte())
                    tdsReader.throwInvalidTDS();

                BIT.build(typeInfo, tdsReader);
                typeInfo.ssLenType = SSLenType.BYTELENTYPE;
            }
        }),

        INTN(TDSType.INTN, new Strategy() {
            /**
             * Sets the fields of typeInfo to the correct values
             * 
             * @param typeInfo
             *        the TypeInfo whos values are being corrected
             * @param tdsReader
             *        the TDSReader used to set the fields of typeInfo to the correct values
             * @throws SQLServerException
             *         when an error occurs
             */
            public void apply(TypeInfo typeInfo, TDSReader tdsReader) throws SQLServerException {
                switch (tdsReader.readUnsignedByte()) {
                    case 8:
                        BIGINT.build(typeInfo, tdsReader);
                        break;
                    case 4:
                        INTEGER.build(typeInfo, tdsReader);
                        break;
                    case 2:
                        SMALLINT.build(typeInfo, tdsReader);
                        break;
                    case 1:
                        TINYINT.build(typeInfo, tdsReader);
                        break;
                    default:
                        tdsReader.throwInvalidTDS();
                        break;
                }

                typeInfo.ssLenType = SSLenType.BYTELENTYPE;
            }
        }),

        DECIMAL(TDSType.DECIMALN, new DecimalNumericStrategy(SSType.DECIMAL)),
        NUMERIC(TDSType.NUMERICN, new DecimalNumericStrategy(SSType.NUMERIC)),
        FLOATN(TDSType.FLOATN, new BigOrSmallByteLenStrategy(FLOAT, REAL)),
        MONEYN(TDSType.MONEYN, new BigOrSmallByteLenStrategy(MONEY, SMALLMONEY)),
        DATETIMEN(TDSType.DATETIMEN, new BigOrSmallByteLenStrategy(DATETIME, SMALLDATETIME)),

        TIME(TDSType.TIMEN, new KatmaiScaledTemporalStrategy(SSType.TIME)),
        DATETIME2(TDSType.DATETIME2N, new KatmaiScaledTemporalStrategy(SSType.DATETIME2)),
        DATETIMEOFFSET(TDSType.DATETIMEOFFSETN, new KatmaiScaledTemporalStrategy(SSType.DATETIMEOFFSET)),

        DATE(TDSType.DATEN, new Strategy() {
            /**
             * Sets the fields of typeInfo to the correct values
             * 
             * @param typeInfo
             *        the TypeInfo whos values are being corrected
             * @param tdsReader
             *        the TDSReader used to set the fields of typeInfo to the correct values
             * @throws SQLServerException
             *         when an error occurs
             */
            public void apply(TypeInfo typeInfo, TDSReader tdsReader) throws SQLServerException {
                typeInfo.ssType = SSType.DATE;
                typeInfo.ssLenType = SSLenType.BYTELENTYPE;
                typeInfo.maxLength = 3;
                typeInfo.displaySize = typeInfo.precision = "yyyy-mm-dd".length();
            }
        }),

        BIGBINARY(TDSType.BIGBINARY, new Strategy() {
            /**
             * Sets the fields of typeInfo to the correct values
             * 
             * @param typeInfo
             *        the TypeInfo whos values are being corrected
             * @param tdsReader
             *        the TDSReader used to set the fields of typeInfo to the correct values
             * @throws SQLServerException
             *         when an error occurs
             */
            public void apply(TypeInfo typeInfo, TDSReader tdsReader) throws SQLServerException {
                typeInfo.ssLenType = SSLenType.USHORTLENTYPE;
                typeInfo.maxLength = tdsReader.readUnsignedShort();
                if (typeInfo.maxLength > DataTypes.SHORT_VARTYPE_MAX_BYTES)
                    tdsReader.throwInvalidTDS();
                typeInfo.precision = typeInfo.maxLength;
                typeInfo.displaySize = 2 * typeInfo.maxLength;
                typeInfo.ssType = (UserTypes.TIMESTAMP == typeInfo.userType) ? SSType.TIMESTAMP : SSType.BINARY;
            }
        }),

        BIGVARBINARY(TDSType.BIGVARBINARY, new Strategy() {
            /**
             * Sets the fields of typeInfo to the correct values
             * 
             * @param typeInfo
             *        the TypeInfo whos values are being corrected
             * @param tdsReader
             *        the TDSReader used to set the fields of typeInfo to the correct values
             * @throws SQLServerException
             *         when an error occurs
             */
            public void apply(TypeInfo typeInfo, TDSReader tdsReader) throws SQLServerException {
                typeInfo.maxLength = tdsReader.readUnsignedShort();
                if (DataTypes.MAXTYPE_LENGTH == typeInfo.maxLength)// for PLP types
                {
                    typeInfo.ssLenType = SSLenType.PARTLENTYPE;
                    typeInfo.ssType = SSType.VARBINARYMAX;
                    typeInfo.displaySize = typeInfo.precision = DataTypes.MAX_VARTYPE_MAX_BYTES;
                } else if (typeInfo.maxLength <= DataTypes.SHORT_VARTYPE_MAX_BYTES)// for non-PLP types
                {
                    typeInfo.ssLenType = SSLenType.USHORTLENTYPE;
                    typeInfo.ssType = SSType.VARBINARY;
                    typeInfo.precision = typeInfo.maxLength;
                    typeInfo.displaySize = 2 * typeInfo.maxLength;
                } else {
                    tdsReader.throwInvalidTDS();
                }
            }
        }),

        IMAGE(TDSType.IMAGE, new Strategy() {
            /**
             * Sets the fields of typeInfo to the correct values
             * 
             * @param typeInfo
             *        the TypeInfo whos values are being corrected
             * @param tdsReader
             *        the TDSReader used to set the fields of typeInfo to the correct values
             * @throws SQLServerException
             *         when an error occurs
             */
            public void apply(TypeInfo typeInfo, TDSReader tdsReader) throws SQLServerException {
                typeInfo.ssLenType = SSLenType.LONGLENTYPE;
                typeInfo.maxLength = tdsReader.readInt();
                if (typeInfo.maxLength < 0)
                    tdsReader.throwInvalidTDS();
                typeInfo.ssType = SSType.IMAGE;
                typeInfo.displaySize = typeInfo.precision = Integer.MAX_VALUE;
            }
        }),

        BIGCHAR(TDSType.BIGCHAR, new Strategy() {
            /**
             * Sets the fields of typeInfo to the correct values
             * 
             * @param typeInfo
             *        the TypeInfo whos values are being corrected
             * @param tdsReader
             *        the TDSReader used to set the fields of typeInfo to the correct values
             * @throws SQLServerException
             *         when an error occurs
             */
            public void apply(TypeInfo typeInfo, TDSReader tdsReader) throws SQLServerException {
                typeInfo.ssLenType = SSLenType.USHORTLENTYPE;
                typeInfo.maxLength = tdsReader.readUnsignedShort();
                if (typeInfo.maxLength > DataTypes.SHORT_VARTYPE_MAX_BYTES)
                    tdsReader.throwInvalidTDS();
                typeInfo.displaySize = typeInfo.precision = typeInfo.maxLength;
                typeInfo.ssType = SSType.CHAR;
                typeInfo.collation = tdsReader.readCollation();
                typeInfo.charset = typeInfo.collation.getCharset();
            }
        }),

        BIGVARCHAR(TDSType.BIGVARCHAR, new Strategy() {
            /**
             * Sets the fields of typeInfo to the correct values
             * 
             * @param typeInfo
             *        the TypeInfo whos values are being corrected
             * @param tdsReader
             *        the TDSReader used to set the fields of typeInfo to the correct values
             * @throws SQLServerException
             *         when an error occurs
             */
            public void apply(TypeInfo typeInfo, TDSReader tdsReader) throws SQLServerException {
                typeInfo.maxLength = tdsReader.readUnsignedShort();
                if (DataTypes.MAXTYPE_LENGTH == typeInfo.maxLength)// for PLP types
                {
                    typeInfo.ssLenType = SSLenType.PARTLENTYPE;
                    typeInfo.ssType = SSType.VARCHARMAX;
                    typeInfo.displaySize = typeInfo.precision = DataTypes.MAX_VARTYPE_MAX_BYTES;
                } else if (typeInfo.maxLength <= DataTypes.SHORT_VARTYPE_MAX_BYTES)// for non-PLP types
                {
                    typeInfo.ssLenType = SSLenType.USHORTLENTYPE;
                    typeInfo.ssType = SSType.VARCHAR;
                    typeInfo.displaySize = typeInfo.precision = typeInfo.maxLength;
                } else {
                    tdsReader.throwInvalidTDS();
                }

                typeInfo.collation = tdsReader.readCollation();
                typeInfo.charset = typeInfo.collation.getCharset();
            }
        }),

        TEXT(TDSType.TEXT, new Strategy() {
            /**
             * Sets the fields of typeInfo to the correct values
             * 
             * @param typeInfo
             *        the TypeInfo whos values are being corrected
             * @param tdsReader
             *        the TDSReader used to set the fields of typeInfo to the correct values
             * @throws SQLServerException
             *         when an error occurs
             */
            public void apply(TypeInfo typeInfo, TDSReader tdsReader) throws SQLServerException {
                typeInfo.ssLenType = SSLenType.LONGLENTYPE;
                typeInfo.maxLength = tdsReader.readInt();
                if (typeInfo.maxLength < 0)
                    tdsReader.throwInvalidTDS();
                typeInfo.ssType = SSType.TEXT;
                typeInfo.displaySize = typeInfo.precision = Integer.MAX_VALUE;
                typeInfo.collation = tdsReader.readCollation();
                typeInfo.charset = typeInfo.collation.getCharset();
            }
        }),

        NCHAR(TDSType.NCHAR, new Strategy() {
            /**
             * Sets the fields of typeInfo to the correct values
             * 
             * @param typeInfo
             *        the TypeInfo whos values are being corrected
             * @param tdsReader
             *        the TDSReader used to set the fields of typeInfo to the correct values
             * @throws SQLServerException
             *         when an error occurs
             */
            public void apply(TypeInfo typeInfo, TDSReader tdsReader) throws SQLServerException {
                typeInfo.ssLenType = SSLenType.USHORTLENTYPE;
                typeInfo.maxLength = tdsReader.readUnsignedShort();
                if (typeInfo.maxLength > DataTypes.SHORT_VARTYPE_MAX_BYTES || 0 != typeInfo.maxLength % 2)
                    tdsReader.throwInvalidTDS();
                typeInfo.displaySize = typeInfo.precision = typeInfo.maxLength / 2;
                typeInfo.ssType = SSType.NCHAR;
                typeInfo.collation = tdsReader.readCollation();
                typeInfo.charset = Encoding.UNICODE.charset();
            }
        }),

        NVARCHAR(TDSType.NVARCHAR, new Strategy() {
            /**
             * Sets the fields of typeInfo to the correct values
             * 
             * @param typeInfo
             *        the TypeInfo whos values are being corrected
             * @param tdsReader
             *        the TDSReader used to set the fields of typeInfo to the correct values
             * @throws SQLServerException
             *         when an error occurs
             */
            public void apply(TypeInfo typeInfo, TDSReader tdsReader) throws SQLServerException {
                typeInfo.maxLength = tdsReader.readUnsignedShort();
                if (DataTypes.MAXTYPE_LENGTH == typeInfo.maxLength)// for PLP types
                {
                    typeInfo.ssLenType = SSLenType.PARTLENTYPE;
                    typeInfo.ssType = SSType.NVARCHARMAX;
                    typeInfo.displaySize = typeInfo.precision = DataTypes.MAX_VARTYPE_MAX_CHARS;
                } else if (typeInfo.maxLength <= DataTypes.SHORT_VARTYPE_MAX_BYTES && 0 == typeInfo.maxLength % 2)// for
                                                                                                                  // non-PLP
                                                                                                                  // types
                {
                    typeInfo.ssLenType = SSLenType.USHORTLENTYPE;
                    typeInfo.ssType = SSType.NVARCHAR;
                    typeInfo.displaySize = typeInfo.precision = typeInfo.maxLength / 2;
                } else {
                    tdsReader.throwInvalidTDS();
                }
                typeInfo.collation = tdsReader.readCollation();
                typeInfo.charset = Encoding.UNICODE.charset();
            }
        }),

        NTEXT(TDSType.NTEXT, new Strategy() {
            /**
             * Sets the fields of typeInfo to the correct values
             * 
             * @param typeInfo
             *        the TypeInfo whos values are being corrected
             * @param tdsReader
             *        the TDSReader used to set the fields of typeInfo to the correct values
             * @throws SQLServerException
             *         when an error occurs
             */
            public void apply(TypeInfo typeInfo, TDSReader tdsReader) throws SQLServerException {
                typeInfo.ssLenType = SSLenType.LONGLENTYPE;
                typeInfo.maxLength = tdsReader.readInt();
                if (typeInfo.maxLength < 0)
                    tdsReader.throwInvalidTDS();
                typeInfo.ssType = SSType.NTEXT;
                typeInfo.displaySize = typeInfo.precision = Integer.MAX_VALUE / 2;
                typeInfo.collation = tdsReader.readCollation();
                typeInfo.charset = Encoding.UNICODE.charset();
            }
        }),

        GUID(TDSType.GUID, new Strategy() {
            /**
             * Sets the fields of typeInfo to the correct values
             * 
             * @param typeInfo
             *        the TypeInfo whos values are being corrected
             * @param tdsReader
             *        the TDSReader used to set the fields of typeInfo to the correct values
             * @throws SQLServerException
             *         when an error occurs
             */
            public void apply(TypeInfo typeInfo, TDSReader tdsReader) throws SQLServerException {
                int maxLength = tdsReader.readUnsignedByte();
                if (maxLength != 16 && maxLength != 0)
                    tdsReader.throwInvalidTDS();

                typeInfo.ssLenType = SSLenType.BYTELENTYPE;
                typeInfo.ssType = SSType.GUID;
                typeInfo.maxLength = maxLength;
                typeInfo.displaySize = typeInfo.precision = "NNNNNNNN-NNNN-NNNN-NNNN-NNNNNNNNNNNN".length();
            }
        }),

        UDT(TDSType.UDT, new Strategy() {
            /**
             * Sets the fields of typeInfo to the correct values
             * 
             * @param typeInfo
             *        the TypeInfo whos values are being corrected
             * @param tdsReader
             *        the TDSReader used to set the fields of typeInfo to the correct values
             * @throws SQLServerException
             *         when an error occurs
             */
            public void apply(TypeInfo typeInfo, TDSReader tdsReader) throws SQLServerException {
                UDTTDSHeader udtTDSHeader = new UDTTDSHeader(tdsReader);
                typeInfo.maxLength = udtTDSHeader.getMaxLen();
                if (DataTypes.MAXTYPE_LENGTH == typeInfo.maxLength) {
                    typeInfo.precision = DataTypes.MAX_VARTYPE_MAX_BYTES;
                    typeInfo.displaySize = DataTypes.MAX_VARTYPE_MAX_BYTES;
                } else if (typeInfo.maxLength <= DataTypes.SHORT_VARTYPE_MAX_BYTES) {
                    typeInfo.precision = typeInfo.maxLength;
                    typeInfo.displaySize = 2 * typeInfo.maxLength;
                } else {
                    tdsReader.throwInvalidTDS();
                }

                typeInfo.ssLenType = SSLenType.PARTLENTYPE;
                typeInfo.ssType = SSType.UDT;

                // Every UDT type has an additional type name embedded in the TDS COLMETADATA
                // header. Per meta-data spec for UDT types we return this type name
                // instead of "UDT".
                typeInfo.udtTypeName = udtTDSHeader.getTypeName();
            }
        }),

        XML(TDSType.XML, new Strategy() {
            /**
             * Sets the fields of typeInfo to the correct values
             * 
             * @param typeInfo
             *        the TypeInfo whos values are being corrected
             * @param tdsReader
             *        the TDSReader used to set the fields of typeInfo to the correct values
             * @throws SQLServerException
             *         when an error occurs
             */
            public void apply(TypeInfo typeInfo, TDSReader tdsReader) throws SQLServerException {
                new XMLTDSHeader(tdsReader);
                typeInfo.ssLenType = SSLenType.PARTLENTYPE;
                typeInfo.ssType = SSType.XML;
                typeInfo.displaySize = typeInfo.precision = Integer.MAX_VALUE / 2;
                typeInfo.charset = Encoding.UNICODE.charset();
            }
        }),

        SQL_VARIANT(TDSType.SQL_VARIANT, new Strategy() {
            /**
             * Sets the fields of typeInfo to the correct values
             * 
             * @param typeInfo
             *        the TypeInfo whos values are being corrected
             * @param tdsReader
             *        the TDSReader used to set the fields of typeInfo to the correct values
             * @throws SQLServerException
             *         when an error occurs
             */
            public void apply(TypeInfo typeInfo, TDSReader tdsReader) throws SQLServerException {
                typeInfo.ssLenType = SSLenType.LONGLENTYPE; // sql_variant type should be LONGLENTYPE length.
                typeInfo.maxLength = tdsReader.readInt();
                typeInfo.ssType = SSType.SQL_VARIANT;
            }
        });

        private final TDSType tdsType;
        private final Strategy strategy;

        private interface Strategy {
            /**
             * Sets the fields of typeInfo to the correct values
             * 
             * @param typeInfo
             *        the TypeInfo whos values are being corrected
             * @param tdsReader
             *        the TDSReader used to set the fields of typeInfo to the correct values
             * @throws SQLServerException
             *         when an error occurs
             */
            public void apply(TypeInfo typeInfo, TDSReader tdsReader) throws SQLServerException;
        }

        private static final class FixedLenStrategy implements Strategy {
            private final SSType ssType;
            private final int maxLength;
            private final int precision;
            private final int displaySize;
            private final int scale;

            FixedLenStrategy(SSType ssType, int maxLength, int precision, int displaySize, int scale) {
                this.ssType = ssType;
                this.maxLength = maxLength;
                this.precision = precision;
                this.displaySize = displaySize;
                this.scale = scale;
            }

            /**
             * Sets the fields of typeInfo to the correct values
             * 
             * @param typeInfo
             *        the TypeInfo whos values are being corrected
             * @param tdsReader
             *        the TDSReader used to set the fields of typeInfo to the correct values
             */
            public void apply(TypeInfo typeInfo, TDSReader tdsReader) {
                typeInfo.ssLenType = SSLenType.FIXEDLENTYPE;
                typeInfo.ssType = ssType;
                typeInfo.maxLength = maxLength;
                typeInfo.precision = precision;
                typeInfo.displaySize = displaySize;
                typeInfo.scale = scale;
            }
        }

        private static final class DecimalNumericStrategy implements Strategy {
            private final SSType ssType;

            DecimalNumericStrategy(SSType ssType) {
                this.ssType = ssType;
            }

            /**
             * Sets the fields of typeInfo to the correct values
             * 
             * @param typeInfo
             *        the TypeInfo whos values are being corrected
             * @param tdsReader
             *        the TDSReader used to set the fields of typeInfo to the correct values
             * @throws SQLServerException
             *         when an error occurs
             */
            public void apply(TypeInfo typeInfo, TDSReader tdsReader) throws SQLServerException {
                int maxLength = tdsReader.readUnsignedByte();
                int precision = tdsReader.readUnsignedByte();
                int scale = tdsReader.readUnsignedByte();

                if (maxLength > 17)
                    tdsReader.throwInvalidTDS();

                typeInfo.ssLenType = SSLenType.BYTELENTYPE;
                typeInfo.ssType = ssType;
                typeInfo.maxLength = maxLength;
                typeInfo.precision = precision;
                typeInfo.displaySize = precision + 2;
                typeInfo.scale = scale;
            }
        }

        private static final class BigOrSmallByteLenStrategy implements Strategy {
            private final Builder bigBuilder;
            private final Builder smallBuilder;

            BigOrSmallByteLenStrategy(Builder bigBuilder, Builder smallBuilder) {
                this.bigBuilder = bigBuilder;
                this.smallBuilder = smallBuilder;
            }

            /**
             * Sets the fields of typeInfo to the correct values
             * 
             * @param typeInfo
             *        the TypeInfo whos values are being corrected
             * @param tdsReader
             *        the TDSReader used to set the fields of typeInfo to the correct values
             * @throws SQLServerException
             *         when an error occurs
             */
            public void apply(TypeInfo typeInfo, TDSReader tdsReader) throws SQLServerException {
                switch (tdsReader.readUnsignedByte()) // maxLength
                {
                    case 8:
                        bigBuilder.build(typeInfo, tdsReader);
                        break;
                    case 4:
                        smallBuilder.build(typeInfo, tdsReader);
                        break;
                    default:
                        tdsReader.throwInvalidTDS();
                        break;
                }

                typeInfo.ssLenType = SSLenType.BYTELENTYPE;
            }
        }

        private static final class KatmaiScaledTemporalStrategy implements Strategy {
            private final SSType ssType;

            KatmaiScaledTemporalStrategy(SSType ssType) {
                this.ssType = ssType;
            }

            private int getPrecision(String baseFormat, int scale) {
                // For 0-scale temporal, there is no '.' after the seconds component because there are no sub-seconds.
                // Example: 12:34:56.12134 includes a '.', but 12:34:56 doesn't
                return baseFormat.length() + ((scale > 0) ? (1 + scale) : 0);
            }

            /**
             * Sets the fields of typeInfo to the correct values
             * 
             * @param typeInfo
             *        the TypeInfo whos values are being corrected
             * @param tdsReader
             *        the TDSReader used to set the fields of typeInfo to the correct values
             * @throws SQLServerException
             *         when an error occurs
             */
            public void apply(TypeInfo typeInfo, TDSReader tdsReader) throws SQLServerException {
                typeInfo.scale = tdsReader.readUnsignedByte();
                if (typeInfo.scale > TDS.MAX_FRACTIONAL_SECONDS_SCALE)
                    tdsReader.throwInvalidTDS();

                switch (ssType) {
                    case TIME:
                        typeInfo.precision = getPrecision("hh:mm:ss", typeInfo.scale);
                        typeInfo.maxLength = TDS.timeValueLength(typeInfo.scale);
                        break;

                    case DATETIME2:
                        typeInfo.precision = getPrecision("yyyy-mm-dd hh:mm:ss", typeInfo.scale);
                        typeInfo.maxLength = TDS.datetime2ValueLength(typeInfo.scale);
                        break;

                    case DATETIMEOFFSET:
                        typeInfo.precision = getPrecision("yyyy-mm-dd hh:mm:ss +HH:MM", typeInfo.scale);
                        typeInfo.maxLength = TDS.datetimeoffsetValueLength(typeInfo.scale);
                        break;

                    default:
                        assert false : "Unexpected SSType: " + ssType;
                }

                typeInfo.ssLenType = SSLenType.BYTELENTYPE;
                typeInfo.ssType = ssType;
                typeInfo.displaySize = typeInfo.precision;
            }
        }

        private Builder(TDSType tdsType, Strategy strategy) {
            this.tdsType = tdsType;
            this.strategy = strategy;
        }

        final TDSType getTDSType() {
            return tdsType;
        }

        final TypeInfo build(TypeInfo typeInfo, TDSReader tdsReader) throws SQLServerException {
            strategy.apply(typeInfo, tdsReader);

            // Postcondition: SSType and SSLenType are initialized
            assert null != typeInfo.ssType;
            assert null != typeInfo.ssLenType;

            return typeInfo;
        }
    }

    /**
     * Returns true if this type is a textual type with a single-byte character set that is compatible with the 7-bit
     * US-ASCII character set.
     */
    boolean supportsFastAsciiConversion() {
        switch (ssType) {
            case CHAR:
            case VARCHAR:
            case VARCHARMAX:
            case TEXT:
                return collation.hasAsciiCompatibleSBCS();

            default:
                return false;
        }
    }

    private static final Map builderMap = new EnumMap<>(TDSType.class);

    static {
        for (Builder builder : Builder.values())
            builderMap.put(builder.getTDSType(), builder);
    }

    private TypeInfo() {}

    static TypeInfo getInstance(TDSReader tdsReader, boolean readFlags) throws SQLServerException {
        TypeInfo typeInfo = new TypeInfo();

        // UserType is USHORT in TDS 7.1 and earlier; ULONG in TDS 7.2 and later.
        typeInfo.userType = tdsReader.readInt();

        if (readFlags) {
            // Flags (2 bytes)
            typeInfo.flags = tdsReader.readShort();
        }

        TDSType tdsType = null;

        try {
            tdsType = TDSType.valueOf(tdsReader.readUnsignedByte());
        } catch (IllegalArgumentException e) {
            tdsReader.getConnection().terminate(SQLServerException.DRIVER_ERROR_INVALID_TDS, e.getMessage(), e);
            // not reached
        }

        assert null != builderMap.get(tdsType) : "Missing TypeInfo builder for TDSType " + tdsType;
        return builderMap.get(tdsType).build(typeInfo, tdsReader);
    }
}


/**
 * DTV implementation for values set from the TDS response stream.
 */
final class ServerDTVImpl extends DTVImpl {
    private int valueLength;
    private TDSReaderMark valueMark;
    private boolean isNull;
    private SqlVariant internalVariant;

    /**
     * Sets the value of the DTV to an app-specified Java type.
     *
     * Generally, the value cannot be stored back into the TDS byte stream (although this could be done for fixed-length
     * types). So this implementation sets the new value in a new AppDTVImpl instance.
     */
    void setValue(DTV dtv, SQLCollation collation, JDBCType jdbcType, Object value, JavaType javaType,
            StreamSetterArgs streamSetterArgs, Calendar cal, Integer scale, SQLServerConnection con,
            boolean forceEncrypt) throws SQLServerException {
        dtv.setImpl(new AppDTVImpl());
        dtv.setValue(collation, jdbcType, value, javaType, streamSetterArgs, cal, scale, con, forceEncrypt);
    }

    void setValue(Object value, JavaType javaType) {
        // This function is never called, but must be implemented; it's abstract in DTVImpl.
        assert false;
    }

    private final static int STREAMCONSUMED = -2;

    // This function is used by Adaptive stream objects to denote that the
    // whole value of the stream has been consumed.
    // Note this only to be used by the streams returned to the user.
    void setPositionAfterStreamed(TDSReader tdsReader) {
        valueMark = tdsReader.mark();
        valueLength = STREAMCONSUMED;
    }

    void setStreamSetterArgs(StreamSetterArgs streamSetterArgs) {
        // This function is never called, but must be implemented; it's abstract in DTVImpl.
        assert false;
    }

    void setCalendar(Calendar calendar) {
        // This function is never called, but must be implemented; it's abstract in DTVImpl.
        assert false;
    }

    void setScale(Integer scale) {
        // This function is never called, but must be implemented; it's abstract in DTVImpl.
        assert false;
    }

    void setForceEncrypt(boolean forceEncrypt) {
        // This function is never called, but must be implemented; it's abstract in DTVImpl.
        assert false;
    }

    StreamSetterArgs getStreamSetterArgs() {
        // This function is never called, but must be implemented; it's abstract in DTVImpl.
        assert false;
        return null;
    }

    Calendar getCalendar() {
        // This function is never called, but must be implemented; it's abstract in DTVImpl.
        assert false;
        return null;
    }

    Integer getScale() {
        // This function is never called, but must be implemented; it's abstract in DTVImpl.
        assert false;
        return null;
    }

    boolean isNull() {
        return isNull;
    }

    void setJdbcType(JDBCType jdbcType) {
        // This function is never called, but must be implemented; it's abstract in DTVImpl.
        assert false;
    }

    JDBCType getJdbcType() {
        // This function is never called, but must be implemented; it's abstract in DTVImpl.
        assert false;
        return JDBCType.UNKNOWN;
    }

    JavaType getJavaType() {
        // This function is never called, but must be implemented; it's abstract in DTVImpl.
        assert false;
        return JavaType.OBJECT;
    }

    // used to set null value
    // for the DTV when a null value is
    // received from NBCROW for a particular column
    final void initFromCompressedNull() {
        assert valueMark == null;
        isNull = true;
    }

    final void skipValue(TypeInfo type, TDSReader tdsReader, boolean isDiscard) throws SQLServerException {
        // indicates that this value was obtained from NBCROW
        // So, there is nothing else to read from the wire
        if (null == valueMark && isNull) {
            return;
        }

        if (null == valueMark)
            getValuePrep(type, tdsReader);
        tdsReader.reset(valueMark);
        // value length zero means that the stream has been already skipped to the end - adaptive case
        if (valueLength != STREAMCONSUMED) {
            if (valueLength == DataTypes.UNKNOWN_STREAM_LENGTH) {
                assert SSLenType.PARTLENTYPE == type.getSSLenType();
                // create a plp type and close it so the value can be skipped.
                // We buffer even when adaptive if the user skips this item.
                PLPInputStream tempPLP = PLPInputStream.makeTempStream(tdsReader, isDiscard, this);
                try {
                    if (null != tempPLP)
                        tempPLP.close();
                } catch (IOException e) {
                    tdsReader.getConnection().terminate(SQLServerException.DRIVER_ERROR_IO_FAILED, e.getMessage());
                }
            } else {
                assert valueLength >= 0;
                tdsReader.skip(valueLength); // jump over the value data
            }
        }

    }

    static final private java.util.logging.Logger aeLogger = java.util.logging.Logger
            .getLogger("com.microsoft.sqlserver.jdbc.DTV");

    private void getValuePrep(TypeInfo typeInfo, TDSReader tdsReader) throws SQLServerException {
        // If we've already seen this value before, then we shouldn't be here.
        assert null == valueMark;

        // Otherwise, mark the value's location, figure out its length, and determine whether it was NULL.
        switch (typeInfo.getSSLenType()) {
            case PARTLENTYPE:
                valueLength = DataTypes.UNKNOWN_STREAM_LENGTH;
                isNull = PLPInputStream.isNull(tdsReader);
                break;

            case FIXEDLENTYPE:
                valueLength = typeInfo.getMaxLength();
                isNull = (0 == valueLength);
                break;

            case BYTELENTYPE:
                valueLength = tdsReader.readUnsignedByte();
                isNull = (0 == valueLength);
                break;

            case USHORTLENTYPE:
                valueLength = tdsReader.readUnsignedShort();
                isNull = (65535 == valueLength);
                if (isNull)
                    valueLength = 0;
                break;

            case LONGLENTYPE:
                if (SSType.TEXT == typeInfo.getSSType() || SSType.IMAGE == typeInfo.getSSType()
                        || SSType.NTEXT == typeInfo.getSSType()) {
                    isNull = (0 == tdsReader.readUnsignedByte());
                    if (isNull) {
                        valueLength = 0;
                    } else {
                        // skip(24) is to skip the textptr and timestamp fields (Section 2.2.7.17 of TDS 7.3 spec)
                        tdsReader.skip(24);
                        valueLength = tdsReader.readInt();
                    }
                }

                else if (SSType.SQL_VARIANT == typeInfo.getSSType()) {
                    valueLength = tdsReader.readInt();
                    isNull = (0 == valueLength);
                    typeInfo.setSSType(SSType.SQL_VARIANT);
                }
                break;
        }

        if (valueLength > typeInfo.getMaxLength())
            tdsReader.throwInvalidTDS();

        valueMark = tdsReader.mark();
    }

    Object denormalizedValue(byte[] decryptedValue, JDBCType jdbcType, TypeInfo baseTypeInfo, SQLServerConnection con,
            InputStreamGetterArgs streamGetterArgs, byte normalizeRuleVersion, Calendar cal) throws SQLServerException {
        if (0x01 != normalizeRuleVersion) {
            MessageFormat form = new MessageFormat(
                    SQLServerException.getErrString("R_UnsupportedNormalizationVersionAE"));
            throw new SQLServerException(form.format(new Object[] {normalizeRuleVersion, 1}), null, 0, null);
        }

        if (aeLogger.isLoggable(java.util.logging.Level.FINE)) {
            aeLogger.fine("Denormalizing decrypted data based on its SQL Server type(" + baseTypeInfo.getSSType()
                    + ") and JDBC type(" + jdbcType + ").");
        }

        SSType baseSSType = baseTypeInfo.getSSType();
        switch (baseSSType) {
            case CHAR:
            case VARCHAR:
            case NCHAR:
            case NVARCHAR:
            case VARCHARMAX:
            case NVARCHARMAX: {
                try {
                    String strVal = new String(decryptedValue, 0, decryptedValue.length,
                            (null == baseTypeInfo.getCharset()) ? con.getDatabaseCollation().getCharset()
                                                                : baseTypeInfo.getCharset());
                    if ((SSType.CHAR == baseSSType) || (SSType.NCHAR == baseSSType)) {
                        // Right pad the string for CHAR types.
                        StringBuilder sb = new StringBuilder(strVal);
                        int padLength = baseTypeInfo.getPrecision() - strVal.length();
                        for (int i = 0; i < padLength; i++) {
                            sb.append(' ');
                        }
                        strVal = sb.toString();
                    }
                    return DDC.convertStringToObject(strVal, baseTypeInfo.getCharset(), jdbcType,
                            streamGetterArgs.streamType);
                } catch (IllegalArgumentException e) {
                    MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_errorConvertingValue"));
                    throw new SQLServerException(form.format(new Object[] {baseSSType, jdbcType}), null, 0, e);
                } catch (UnsupportedEncodingException e) {
                    // Important: we should not pass the exception here as it displays the data.
                    MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_unsupportedEncoding"));
                    throw new SQLServerException(form.format(new Object[] {baseTypeInfo.getCharset()}), null, 0, e);
                }
            }

            case BIT:
            case TINYINT:
            case SMALLINT:
            case INTEGER:
            case BIGINT: {
                // If data is encrypted, then these types are normalized to BIGINT. Need to denormalize here.
                if (8 != decryptedValue.length) {
                    // Integer datatypes are normalized to bigint for AE.
                    MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_NormalizationErrorAE"));
                    throw new SQLServerException(form.format(new Object[] {baseSSType}), null, 0, null);
                }
                return DDC.convertLongToObject(Util.readLong(decryptedValue, 0), jdbcType, baseSSType,
                        streamGetterArgs.streamType);

            }

            case REAL:
            case FLOAT: {
                // JDBC driver does not normalize real to float.
                if (8 == decryptedValue.length) {
                    return DDC.convertDoubleToObject(
                            ByteBuffer.wrap(decryptedValue).order(ByteOrder.LITTLE_ENDIAN).getDouble(),
                            JDBCType.VARBINARY == jdbcType ? baseSSType.getJDBCType() : jdbcType, // use jdbc type from
                                                                                                  // baseTypeInfo if
                                                                                                  // using
                                                                                                  // getObject()
                            streamGetterArgs.streamType);
                } else if (4 == decryptedValue.length) {
                    return DDC.convertFloatToObject(
                            ByteBuffer.wrap(decryptedValue).order(ByteOrder.LITTLE_ENDIAN).getFloat(),
                            JDBCType.VARBINARY == jdbcType ? baseSSType.getJDBCType() : jdbcType, // use jdbc type from
                                                                                                  // baseTypeInfo if
                                                                                                  // using
                                                                                                  // getObject()
                            streamGetterArgs.streamType);
                } else {
                    MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_NormalizationErrorAE"));
                    throw new SQLServerException(form.format(new Object[] {baseSSType}), null, 0, null);
                }
            }
            case SMALLMONEY: {
                return DDC.convertMoneyToObject(new BigDecimal(BigInteger.valueOf(Util.readInt(decryptedValue, 4)), 4),
                        JDBCType.VARBINARY == jdbcType ? baseSSType.getJDBCType() : jdbcType, // use jdbc type from
                                                                                              // baseTypeInfo if using
                                                                                              // getObject()
                        streamGetterArgs.streamType, 4);
            }

            case MONEY: {
                BigInteger bi = BigInteger.valueOf(((long) Util.readInt(decryptedValue, 0) << 32)
                        | (Util.readInt(decryptedValue, 4) & 0xFFFFFFFFL));

                return DDC.convertMoneyToObject(new BigDecimal(bi, 4),
                        JDBCType.VARBINARY == jdbcType ? baseSSType.getJDBCType() : jdbcType, // use
                                                                                              // jdbc
                                                                                              // type
                                                                                              // from
                                                                                              // baseTypeInfo
                                                                                              // if
                                                                                              // using
                                                                                              // getObject()
                        streamGetterArgs.streamType, 8);
            }

            case NUMERIC:
            case DECIMAL: {
                return DDC.convertBigDecimalToObject(
                        Util.readBigDecimal(decryptedValue, decryptedValue.length, baseTypeInfo.getScale()),
                        JDBCType.VARBINARY == jdbcType ? baseSSType.getJDBCType() : jdbcType, // use jdbc type from
                                                                                              // baseTypeInfo if using
                                                                                              // getObject()
                        streamGetterArgs.streamType);
            }

            case BINARY:
            case VARBINARY:
            case VARBINARYMAX: {
                return DDC.convertBytesToObject(decryptedValue, jdbcType, baseTypeInfo);
            }

            case DATE: {
                // get the number of days !! Size should be 3
                if (3 != decryptedValue.length) {
                    MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_NormalizationErrorAE"));
                    throw new SQLServerException(form.format(new Object[] {baseSSType}), null, 0, null);
                }

                // Getting number of days
                // Following Lines of code copied from IOBuffer.readDaysIntoCE as we
                // cannot reuse method
                int daysIntoCE = getDaysIntoCE(decryptedValue, baseSSType);

                return DDC.convertTemporalToObject(jdbcType, baseSSType, cal, daysIntoCE, 0, 0);

            }

            case TIME: {
                long localNanosSinceMidnight = readNanosSinceMidnightAE(decryptedValue, baseTypeInfo.getScale(),
                        baseSSType);

                return DDC.convertTemporalToObject(jdbcType, SSType.TIME, cal, 0, localNanosSinceMidnight,
                        baseTypeInfo.getScale());
            }

            case DATETIME2: {
                if (8 != decryptedValue.length) {
                    MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_NormalizationErrorAE"));
                    throw new SQLServerException(form.format(new Object[] {baseSSType}), null, 0, null);
                }

                // Last three bytes are for date and remaining for time
                int dateOffset = decryptedValue.length - 3;
                byte[] timePortion = new byte[dateOffset];
                byte[] datePortion = new byte[3];
                System.arraycopy(decryptedValue, 0, timePortion, 0, dateOffset);
                System.arraycopy(decryptedValue, dateOffset, datePortion, 0, 3);
                long localNanosSinceMidnight = readNanosSinceMidnightAE(timePortion, baseTypeInfo.getScale(),
                        baseSSType);

                int daysIntoCE = getDaysIntoCE(datePortion, baseSSType);

                // Convert the DATETIME2 value to the desired Java type.
                return DDC.convertTemporalToObject(jdbcType, SSType.DATETIME2, cal, daysIntoCE, localNanosSinceMidnight,
                        baseTypeInfo.getScale());

            }

            case SMALLDATETIME: {
                if (4 != decryptedValue.length) {
                    MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_NormalizationErrorAE"));
                    throw new SQLServerException(form.format(new Object[] {baseSSType}), null, 0, null);
                }

                // SQL smalldatetime has less precision. It stores 2 bytes
                // for the days since SQL Base Date and 2 bytes for minutes
                // after midnight.
                return DDC.convertTemporalToObject(jdbcType, SSType.DATETIME, cal,
                        Util.readUnsignedShort(decryptedValue, 0),
                        Util.readUnsignedShort(decryptedValue, 2) * 60L * 1000L, 0);
            }

            case DATETIME: {
                int ticksSinceMidnight = (Util.readInt(decryptedValue, 4) * 10 + 1) / 3;

                if (8 != decryptedValue.length || Integer.MAX_VALUE < ticksSinceMidnight) {
                    MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_NormalizationErrorAE"));
                    throw new SQLServerException(form.format(new Object[] {baseSSType}), null, 0, null);
                }

                // SQL datetime is 4 bytes for days since SQL Base Date
                // (January 1, 1900 00:00:00 GMT) and 4 bytes for
                // the number of three hundredths (1/300) of a second since midnight.
                return DDC.convertTemporalToObject(jdbcType, SSType.DATETIME, cal, Util.readInt(decryptedValue, 0),
                        ticksSinceMidnight, 0);
            }

            case DATETIMEOFFSET: {
                // Last 5 bytes are for date and offset
                int dateOffset = decryptedValue.length - 5;
                byte[] timePortion = new byte[dateOffset];
                byte[] datePortion = new byte[3];
                byte[] offsetPortion = new byte[2];
                System.arraycopy(decryptedValue, 0, timePortion, 0, dateOffset);
                System.arraycopy(decryptedValue, dateOffset, datePortion, 0, 3);
                System.arraycopy(decryptedValue, dateOffset + 3, offsetPortion, 0, 2);
                long localNanosSinceMidnight = readNanosSinceMidnightAE(timePortion, baseTypeInfo.getScale(),
                        baseSSType);

                int daysIntoCE = getDaysIntoCE(datePortion, baseSSType);

                int localMinutesOffset = ByteBuffer.wrap(offsetPortion).order(ByteOrder.LITTLE_ENDIAN).getShort();

                return DDC.convertTemporalToObject(jdbcType, SSType.DATETIMEOFFSET,
                        new GregorianCalendar(new SimpleTimeZone(localMinutesOffset * 60 * 1000, ""), Locale.US),
                        daysIntoCE, localNanosSinceMidnight, baseTypeInfo.getScale());

            }

            case GUID: {
                return Util.readGUID(decryptedValue);
            }

            default:
                MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_UnsupportedDataTypeAE"));
                throw new SQLServerException(form.format(new Object[] {baseSSType}), null, 0, null);
        }
    }

    Object getValue(DTV dtv, JDBCType jdbcType, int scale, InputStreamGetterArgs streamGetterArgs, Calendar cal,
            TypeInfo typeInfo, CryptoMetadata cryptoMetadata, TDSReader tdsReader, SQLServerStatement statement) throws SQLServerException {
        SQLServerConnection con = tdsReader.getConnection();
        Object convertedValue = null;
        byte[] decryptedValue;
        boolean encrypted = false;
        SSType baseSSType = typeInfo.getSSType();

        // If column encryption is not enabled on connection or on statement, cryptoMeta will be null.
        if (null != cryptoMetadata) {
            assert (SSType.VARBINARY == typeInfo.getSSType()) || (SSType.VARBINARYMAX == typeInfo.getSSType());
            baseSSType = cryptoMetadata.baseTypeInfo.getSSType();
            encrypted = true;

            if (aeLogger.isLoggable(java.util.logging.Level.FINE)) {
                aeLogger.fine("Data is encrypted, SQL Server Data Type: " + baseSSType + ", Encryption Type: "
                        + cryptoMetadata.getEncryptionType());
            }
        }

        // Note that the value should be prepped
        // only for columns whose values can be read of the wire.
        // If valueMark == null and isNull, it implies that
        // the column is null according to NBCROW and that
        // there is nothing to be read from the wire.
        if (null == valueMark && (!isNull))
            getValuePrep(typeInfo, tdsReader);

        // either there should be a valueMark
        // or valueMark should be null and isNull should be set to true(NBCROW case)
        assert ((valueMark != null) || (valueMark == null && isNull));

        if (null != streamGetterArgs) {
            if (!streamGetterArgs.streamType.convertsFrom(typeInfo))
                DataTypes.throwConversionError(typeInfo.getSSType().toString(), streamGetterArgs.streamType.toString());
        } else {
            if (!baseSSType.convertsTo(jdbcType) && !isNull) {
                // if the baseSSType is Character or NCharacter and jdbcType is Longvarbinary,
                // does not throw type conversion error, which allows getObject() on Long Character types.
                if (encrypted) {
                    if (!Util.isBinaryType(jdbcType.getIntValue())) {
                        DataTypes.throwConversionError(baseSSType.toString(), jdbcType.toString());
                    }
                } else {
                    DataTypes.throwConversionError(baseSSType.toString(), jdbcType.toString());
                }
            }

            streamGetterArgs = InputStreamGetterArgs.getDefaultArgs();
        }

        if (STREAMCONSUMED == valueLength) {
            throw new SQLServerException(null, SQLServerException.getErrString("R_dataAlreadyAccessed"), null, 0,
                    false);
        }

        if (!isNull) {
            tdsReader.reset(valueMark);

            if (encrypted) {
                if (DataTypes.UNKNOWN_STREAM_LENGTH == valueLength) {
                    convertedValue = DDC.convertStreamToObject(
                            PLPInputStream.makeStream(tdsReader, streamGetterArgs, this), typeInfo, JDBCType.VARBINARY,
                            streamGetterArgs);
                } else {
                    convertedValue = DDC.convertStreamToObject(
                            new SimpleInputStream(tdsReader, valueLength, streamGetterArgs, this), typeInfo,
                            JDBCType.VARBINARY, streamGetterArgs);
                }

                aeLogger.fine("Encrypted data is retrieved.");

                // AE does not support streaming types
                if ((convertedValue instanceof SimpleInputStream) || (convertedValue instanceof PLPInputStream)) {
                    throw new SQLServerException(SQLServerException.getErrString("R_notSupported"), null);
                }

                decryptedValue = SQLServerSecurityUtility.decryptWithKey((byte[]) convertedValue, cryptoMetadata, con, statement);
                return denormalizedValue(decryptedValue, jdbcType, cryptoMetadata.baseTypeInfo, con, streamGetterArgs,
                        cryptoMetadata.normalizationRuleVersion, cal);
            }

            switch (baseSSType) {
                // Process all PLP types here.
                case VARBINARYMAX:
                case VARCHARMAX:
                case NVARCHARMAX:
                case UDT: {
                    convertedValue = DDC.convertStreamToObject(
                            PLPInputStream.makeStream(tdsReader, streamGetterArgs, this), typeInfo, jdbcType,
                            streamGetterArgs);
                    break;
                }

                case XML: {
                    convertedValue = DDC.convertStreamToObject(
                            ((jdbcType.isBinary() || jdbcType == JDBCType.SQLXML) ? PLPXMLInputStream
                                    .makeXMLStream(tdsReader, streamGetterArgs, this)
                                                                                  : PLPInputStream.makeStream(tdsReader,
                                                                                          streamGetterArgs, this)),
                            typeInfo, jdbcType, streamGetterArgs);
                    break;
                }

                // Convert other variable length native types
                // (CHAR/VARCHAR/TEXT/NCHAR/NVARCHAR/NTEXT/BINARY/VARBINARY/IMAGE) -> ANY jdbcType.
                case CHAR:
                case VARCHAR:
                case TEXT:
                case NCHAR:
                case NVARCHAR:
                case NTEXT:
                case IMAGE:
                case BINARY:
                case VARBINARY:
                case TIMESTAMP: // A special BINARY(8)
                {
                    convertedValue = DDC.convertStreamToObject(
                            new SimpleInputStream(tdsReader, valueLength, streamGetterArgs, this), typeInfo, jdbcType,
                            streamGetterArgs);
                    break;
                }

                // Convert BIT/TINYINT/SMALLINT/INTEGER/BIGINT native type -> ANY jdbcType.
                case BIT:
                case TINYINT:
                case SMALLINT:
                case INTEGER:
                case BIGINT: {
                    switch (valueLength) {
                        case 8:
                            convertedValue = DDC.convertLongToObject(tdsReader.readLong(), jdbcType, baseSSType,
                                    streamGetterArgs.streamType);
                            break;

                        case 4:
                            convertedValue = DDC.convertIntegerToObject(tdsReader.readInt(), valueLength, jdbcType,
                                    streamGetterArgs.streamType);
                            break;

                        case 2:
                            convertedValue = DDC.convertIntegerToObject(tdsReader.readShort(), valueLength, jdbcType,
                                    streamGetterArgs.streamType);
                            break;

                        case 1:
                            convertedValue = DDC.convertIntegerToObject(tdsReader.readUnsignedByte(), valueLength,
                                    jdbcType, streamGetterArgs.streamType);
                            break;

                        default:
                            assert false : "Unexpected valueLength" + valueLength;
                            break;
                    }
                    break;
                }

                // Convert DECIMAL|NUMERIC native types -> ANY jdbcType.
                case DECIMAL:
                case NUMERIC:
                    convertedValue = tdsReader.readDecimal(valueLength, typeInfo, jdbcType,
                            streamGetterArgs.streamType);
                    break;

                // Convert MONEY|SMALLMONEY native types -> ANY jdbcType.
                case MONEY:
                case SMALLMONEY:
                    convertedValue = tdsReader.readMoney(valueLength, jdbcType, streamGetterArgs.streamType);
                    break;

                // Convert FLOAT native type -> ANY jdbcType.
                case FLOAT:
                    convertedValue = tdsReader.readFloat(valueLength, jdbcType, streamGetterArgs.streamType);
                    break;

                // Convert REAL native type -> ANY jdbcType.
                case REAL:
                    convertedValue = tdsReader.readReal(valueLength, jdbcType, streamGetterArgs.streamType);
                    break;

                // Convert DATETIME|SMALLDATETIME native types -> ANY jdbcType.
                case DATETIME:
                case SMALLDATETIME:
                    convertedValue = tdsReader.readDateTime(valueLength, cal, jdbcType, streamGetterArgs.streamType);
                    break;

                // Convert DATE native type -> ANY jdbcType.
                case DATE:
                    convertedValue = tdsReader.readDate(valueLength, cal, jdbcType);
                    break;

                // Convert TIME native type -> ANY jdbcType.
                case TIME:
                    convertedValue = tdsReader.readTime(valueLength, typeInfo, cal, jdbcType);
                    break;

                // Convert DATETIME2 native type -> ANY jdbcType.
                case DATETIME2:
                    convertedValue = tdsReader.readDateTime2(valueLength, typeInfo, cal, jdbcType);
                    break;

                // Convert DATETIMEOFFSET native type -> ANY jdbcType.
                case DATETIMEOFFSET:
                    convertedValue = tdsReader.readDateTimeOffset(valueLength, typeInfo, jdbcType);
                    break;

                // Convert GUID native type -> ANY jdbcType.
                case GUID:
                    convertedValue = tdsReader.readGUID(valueLength, jdbcType, streamGetterArgs.streamType);
                    break;

                case SQL_VARIANT:
                    /**
                     * SQL_Variant has the following structure: 1- basetype: the underlying type 2- probByte: holds
                     * count of property bytes expected for a sql_variant structure 3- properties: For example VARCHAR
                     * type has 5 byte collation and 2 byte max length 4- dataValue: the data value
                     */
                    int baseType = tdsReader.readUnsignedByte();

                    int cbPropsActual = tdsReader.readUnsignedByte();
                    // don't create new one, if we have already created an internalVariant object. For example, in
                    // bulkcopy
                    // when we are reading time column, we update the same internalvarianttype's JDBC to be timestamp
                    if (null == internalVariant) {
                        internalVariant = new SqlVariant(baseType);
                    }
                    convertedValue = readSqlVariant(baseType, cbPropsActual, valueLength, tdsReader, baseSSType,
                            typeInfo, jdbcType, streamGetterArgs, cal);
                    break;
                // Unknown SSType should have already been rejected by TypeInfo.setFromTDS()
                default:
                    assert false : "Unexpected SSType " + typeInfo.getSSType();
                    break;
            }
        } // !isNull

        // Postcondition: returned object is null only if value was null.
        assert isNull || null != convertedValue;
        return convertedValue;
    }

    SqlVariant getInternalVariant() {
        return internalVariant;
    }

    /**
     * Read the value inside sqlVariant. The reading differs based on what the internal baseType is.
     * 
     * @return sql_variant value
     * @since 6.3.0
     * @throws SQLServerException
     */
    private Object readSqlVariant(int intbaseType, int cbPropsActual, int valueLength, TDSReader tdsReader,
            SSType baseSSType, TypeInfo typeInfo, JDBCType jdbcType, InputStreamGetterArgs streamGetterArgs,
            Calendar cal) throws SQLServerException {
        Object convertedValue = null;
        int lengthConsumed = 2 + cbPropsActual; // We have already read 2bytes for baseType earlier.
        int expectedValueLength = valueLength - lengthConsumed;
        SQLCollation collation = null;
        int precision;
        int scale;
        int maxLength;
        TDSType baseType = TDSType.valueOf(intbaseType);
        switch (baseType) {
            case INT8:
                convertedValue = DDC.convertLongToObject(tdsReader.readLong(), jdbcType, baseSSType,
                        streamGetterArgs.streamType);
                break;

            case INT4:
                convertedValue = DDC.convertIntegerToObject(tdsReader.readInt(), valueLength, jdbcType,
                        streamGetterArgs.streamType);
                break;

            case INT2:
                convertedValue = DDC.convertIntegerToObject(tdsReader.readShort(), valueLength, jdbcType,
                        streamGetterArgs.streamType);
                break;

            case INT1:
                convertedValue = DDC.convertIntegerToObject(tdsReader.readUnsignedByte(), valueLength, jdbcType,
                        streamGetterArgs.streamType);
                break;

            case DECIMALN:
            case NUMERICN:
                if (cbPropsActual != sqlVariantProbBytes.DECIMALN.getIntValue()) { // Numeric and decimal have the same
                                                                                   // probbytes value
                    MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidProbbytes"));
                    throw new SQLServerException(form.format(new Object[] {baseType}), null, 0, null);
                }
                precision = tdsReader.readUnsignedByte();
                scale = tdsReader.readUnsignedByte();
                typeInfo.setScale(scale); // typeInfo needs to be updated. typeInfo is usually set when reading
                                          // columnMetaData, but for sql_variant
                // type the actual columnMetaData is is set when reading the data rows.
                internalVariant.setPrecision(precision);
                internalVariant.setScale(scale);
                convertedValue = tdsReader.readDecimal(expectedValueLength, typeInfo, jdbcType,
                        streamGetterArgs.streamType);
                break;

            case FLOAT4:
                convertedValue = tdsReader.readReal(expectedValueLength, jdbcType, streamGetterArgs.streamType);
                break;

            case FLOAT8:
                convertedValue = tdsReader.readFloat(expectedValueLength, jdbcType, streamGetterArgs.streamType);
                break;

            case MONEY4:
                precision = Long.toString(Long.MAX_VALUE).length();
                typeInfo.setPrecision(precision);
                scale = 4;
                typeInfo.setDisplaySize(("-" + "." + Integer.toString(Integer.MAX_VALUE)).length());
                typeInfo.setScale(scale);
                internalVariant.setPrecision(precision);
                internalVariant.setScale(scale);
                convertedValue = tdsReader.readMoney(expectedValueLength, jdbcType, streamGetterArgs.streamType);
                break;

            case MONEY8:
                precision = Long.toString(Long.MAX_VALUE).length();
                scale = 4;
                typeInfo.setPrecision(precision);
                typeInfo.setDisplaySize(("-" + "." + Integer.toString(Integer.MAX_VALUE)).length());
                typeInfo.setScale(scale);
                internalVariant.setPrecision(precision);
                internalVariant.setScale(scale);
                convertedValue = tdsReader.readMoney(expectedValueLength, jdbcType, streamGetterArgs.streamType);
                break;

            case BIT1:
            case BITN:
                    convertedValue = DDC.convertIntegerToObject(tdsReader.readUnsignedByte(), expectedValueLength,
                            jdbcType, streamGetterArgs.streamType);
                break;

            case BIGVARCHAR:
            case BIGCHAR:
                if (cbPropsActual != sqlVariantProbBytes.BIGCHAR.getIntValue()) {
                    MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidProbbytes"));
                    throw new SQLServerException(form.format(new Object[] {baseType}), null, 0, null);
                }
                collation = tdsReader.readCollation();
                typeInfo.setSQLCollation(collation);
                maxLength = tdsReader.readUnsignedShort();
                if (maxLength > DataTypes.SHORT_VARTYPE_MAX_BYTES)
                    tdsReader.throwInvalidTDS();
                typeInfo.setDisplaySize(maxLength);
                typeInfo.setPrecision(maxLength);
                internalVariant.setPrecision(maxLength);
                internalVariant.setCollation(collation);
                typeInfo.setCharset(collation.getCharset());
                convertedValue = DDC.convertStreamToObject(
                        new SimpleInputStream(tdsReader, expectedValueLength, streamGetterArgs, this), typeInfo,
                        jdbcType, streamGetterArgs);
                break;

            case NCHAR:
            case NVARCHAR:
                if (cbPropsActual != sqlVariantProbBytes.NCHAR.getIntValue()) {
                    MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidProbbytes"));
                    throw new SQLServerException(form.format(new Object[] {baseType}), null, 0, null);
                }
                collation = tdsReader.readCollation();
                typeInfo.setSQLCollation(collation);
                maxLength = tdsReader.readUnsignedShort();
                if (maxLength > DataTypes.SHORT_VARTYPE_MAX_BYTES || 0 != maxLength % 2)
                    tdsReader.throwInvalidTDS();
                typeInfo.setDisplaySize(maxLength / 2);
                typeInfo.setPrecision(maxLength / 2);
                internalVariant.setPrecision(maxLength / 2);
                internalVariant.setCollation(collation);
                typeInfo.setCharset(Encoding.UNICODE.charset());
                convertedValue = DDC.convertStreamToObject(
                        new SimpleInputStream(tdsReader, expectedValueLength, streamGetterArgs, this), typeInfo,
                        jdbcType, streamGetterArgs);
                break;

            case DATETIME8:
                jdbcType = JDBCType.DATETIME;
                convertedValue = tdsReader.readDateTime(expectedValueLength, cal, jdbcType,
                        streamGetterArgs.streamType);
                break;

            case DATETIME4:
                jdbcType = JDBCType.SMALLDATETIME;
                convertedValue = tdsReader.readDateTime(expectedValueLength, cal, jdbcType,
                        streamGetterArgs.streamType);
                break;

            case DATEN:
                jdbcType = JDBCType.DATE;
                convertedValue = tdsReader.readDate(expectedValueLength, cal, jdbcType);
                break;

            case TIMEN:
                if (cbPropsActual != sqlVariantProbBytes.TIMEN.getIntValue()) {
                    MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidProbbytes"));
                    throw new SQLServerException(form.format(new Object[] {baseType}), null, 0, null);
                }
                if (internalVariant.isBaseTypeTimeValue()) {
                    jdbcType = JDBCType.TIMESTAMP;
                }
                scale = tdsReader.readUnsignedByte();
                typeInfo.setScale(scale);
                internalVariant.setScale(scale);
                convertedValue = tdsReader.readTime(expectedValueLength, typeInfo, cal, jdbcType);
                break;

            case DATETIME2N:
                if (cbPropsActual != sqlVariantProbBytes.DATETIME2N.getIntValue()) {
                    MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidProbbytes"));
                    throw new SQLServerException(form.format(new Object[] {baseType}), null, 0, null);
                }
                jdbcType = JDBCType.TIMESTAMP;
                scale = tdsReader.readUnsignedByte();
                typeInfo.setScale(scale);
                internalVariant.setScale(scale);
                convertedValue = tdsReader.readDateTime2(expectedValueLength, typeInfo, cal, jdbcType);
                break;

            case BIGBINARY: // e.g binary20, binary 512, binary 8000 -> reads as bigbinary
            case BIGVARBINARY:
                if (cbPropsActual != sqlVariantProbBytes.BIGBINARY.getIntValue()) {
                    MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidProbbytes"));
                    throw new SQLServerException(form.format(new Object[] {baseType}), null, 0, null);
                }
                if (TDSType.BIGBINARY == baseType)
                    jdbcType = JDBCType.BINARY;// LONGVARCHAR;
                else if (TDSType.BIGVARBINARY == baseType)
                    jdbcType = JDBCType.VARBINARY;
                maxLength = tdsReader.readUnsignedShort();
                internalVariant.setMaxLength(maxLength);
                if (maxLength > DataTypes.SHORT_VARTYPE_MAX_BYTES)
                    tdsReader.throwInvalidTDS();
                typeInfo.setDisplaySize(2 * maxLength);
                typeInfo.setPrecision(maxLength);
                convertedValue = DDC.convertStreamToObject(
                        new SimpleInputStream(tdsReader, expectedValueLength, streamGetterArgs, this), typeInfo,
                        jdbcType, streamGetterArgs);
                break;

            case GUID:
                jdbcType = JDBCType.GUID;
                internalVariant.setBaseType(intbaseType);
                internalVariant.setBaseJDBCType(jdbcType);
                typeInfo.setDisplaySize("NNNNNNNN-NNNN-NNNN-NNNN-NNNNNNNNNNNN".length());
                lengthConsumed = 2 + cbPropsActual;
                convertedValue = tdsReader.readGUID(expectedValueLength, jdbcType, streamGetterArgs.streamType);
                break;

            // Unsupported TdsType should throw error message
            default: {
                MessageFormat form = new MessageFormat(
                        SQLServerException.getErrString("R_invalidDataTypeSupportForSQLVariant"));
                throw new SQLServerException(form.format(new Object[] {baseType}), null, 0, null);
            }

        }
        return convertedValue;
    }

    Object getSetterValue() {
        // This function is never called, but must be implemented; it's abstract in DTVImpl.
        assert false;
        return null;
    }

    private long readNanosSinceMidnightAE(byte[] value, int scale, SSType baseSSType) throws SQLServerException {
        long hundredNanosSinceMidnight = 0;
        for (int i = 0; i < value.length; i++)
            hundredNanosSinceMidnight |= (value[i] & 0xFFL) << (8 * i);

        if (!(0 <= hundredNanosSinceMidnight && hundredNanosSinceMidnight < Nanos.PER_DAY / 100)) {
            MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_NormalizationErrorAE"));
            throw new SQLServerException(form.format(new Object[] {baseSSType}), null, 0, null);
        }

        return 100 * hundredNanosSinceMidnight;
    }

    private int getDaysIntoCE(byte[] datePortion, SSType baseSSType) throws SQLServerException {
        int daysIntoCE = 0;
        for (int i = 0; i < datePortion.length; i++) {
            daysIntoCE |= ((datePortion[i] & 0xFF) << (8 * i));
        }

        if (daysIntoCE < 0) {
            MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_NormalizationErrorAE"));
            throw new SQLServerException(form.format(new Object[] {baseSSType}), null, 0, null);
        }

        return daysIntoCE;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy