com.microsoft.sqlserver.jdbc.SQLServerBulkCopy Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of mssql-jdbc Show documentation
Show all versions of mssql-jdbc Show documentation
Microsoft JDBC Driver for SQL Server.
The 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 com.microsoft.sqlserver.jdbc.SQLServerConnection.BULK_COPY_OPERATION_CACHE;
import static com.microsoft.sqlserver.jdbc.Util.getHashedSecret;
import static java.nio.charset.StandardCharsets.UTF_16LE;
import static java.nio.charset.StandardCharsets.UTF_8;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.Serializable;
import java.io.StringReader;
import java.math.BigDecimal;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.sql.Connection;
import java.sql.Date;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.text.MessageFormat;
import java.time.DateTimeException;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoField;
import java.time.temporal.TemporalAccessor;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.SimpleTimeZone;
import java.util.TimeZone;
import java.util.UUID;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;
import javax.sql.RowSet;
import microsoft.sql.DateTimeOffset;
/**
* Provides functionality to efficiently bulk load a SQL Server table with data from another source.
*
* Microsoft SQL Server includes a popular command-prompt utility named bcp for moving data from one table to another,
* whether on a single server or between servers. The SQLServerBulkCopy class lets you write code solutions in Java that
* provide similar functionality. There are other ways to load data into a SQL Server table (INSERT statements, for
* example), but SQLServerBulkCopy offers a significant performance advantage over them.
* The SQLServerBulkCopy class can be used to write data only to SQL Server tables. However, the data source is not
* limited to SQL Server; any data source can be used, as long as the data can be read with a ResultSet or
* ISQLServerBulkRecord instance.
*/
public class SQLServerBulkCopy implements java.lang.AutoCloseable, java.io.Serializable {
/**
* Update serialVersionUID when making changes to this file
*/
private static final long serialVersionUID = 1989903904654306244L;
private static final String MAX = "(max)";
/**
* Represents the column mappings between the source and destination table
*/
private class ColumnMapping implements Serializable {
/**
* Always update serialVersionUID when prompted.
*/
private static final long serialVersionUID = 6428337550654423919L;
String sourceColumnName = null;
int sourceColumnOrdinal = -1;
String destinationColumnName = null;
int destinationColumnOrdinal = -1;
ColumnMapping(String source, String dest) {
this.sourceColumnName = source;
this.destinationColumnName = dest;
}
ColumnMapping(String source, int dest) {
this.sourceColumnName = source;
this.destinationColumnOrdinal = dest;
}
ColumnMapping(int source, String dest) {
this.sourceColumnOrdinal = source;
this.destinationColumnName = dest;
}
ColumnMapping(int source, int dest) {
this.sourceColumnOrdinal = source;
this.destinationColumnOrdinal = dest;
}
}
/**
* Class name for logging.
*/
private static final String loggerClassName = "com.microsoft.sqlserver.jdbc.SQLServerBulkCopy";
/**
* Logger
*/
private static final java.util.logging.Logger loggerExternal = java.util.logging.Logger.getLogger(loggerClassName);
/**
* Destination server connection.
*/
private SQLServerConnection connection;
/**
* Options to control how the WriteToServer methods behave.
*/
private SQLServerBulkCopyOptions copyOptions;
/**
* Mappings between columns in the data source and columns in the destination
*/
private List columnMappings;
/**
* Flag if SQLServerBulkCopy owns the connection and should close it when Close is called
*/
private boolean ownsConnection;
/**
* Name of destination table on server. If destinationTable has not been set when WriteToServer is called, an
* Exception is thrown. destinationTable is a three-part name {@code (..)}. You can
* qualify the table name with its database and owning schema if you choose. However, if the table name uses an
* underscore ("_") or any other special characters, you must escape the name using surrounding brackets. For more
* information, see "Identifiers" in SQL Server Books Online. You can bulk-copy data to a temporary table by using a
* value such as {@code tempdb..#table or tempdb..#table} for the destinationTable property.
*/
private String destinationTableName;
/**
* Source data (from a Record). Is null unless the corresponding version of writeToServer is called.
*/
private ISQLServerBulkData serverBulkData;
/**
* Source data (from ResultSet). Is null unless the corresponding version of writeToServer is called.
*/
private transient ResultSet sourceResultSet;
/**
* Metadata for the source table columns
*/
private transient ResultSetMetaData sourceResultSetMetaData;
/**
* The CekTable for the destination table.
*/
private CekTable destCekTable = null;
/**
* Statement level encryption setting needed for querying against encrypted columns.
*/
private SQLServerStatementColumnEncryptionSetting stmtColumnEncriptionSetting = SQLServerStatementColumnEncryptionSetting.USE_CONNECTION_SETTING;
/**
* Destination table metadata
*/
private transient ResultSet destinationTableMetadata;
/**
* Metadata for the destination table columns
*/
class BulkColumnMetaData {
String columnName;
SSType ssType = null;
int jdbcType;
int precision, scale;
SQLCollation collation;
byte[] flags = new byte[2];
boolean isIdentity = false;
boolean isNullable;
String collationName;
CryptoMetadata cryptoMeta = null;
DateTimeFormatter dateTimeFormatter = null;
// used when allowEncryptedValueModifications is on and encryption is turned off in connection
String encryptionType = null;
BulkColumnMetaData(Column column) {
this.cryptoMeta = column.getCryptoMetadata();
TypeInfo typeInfo = column.getTypeInfo();
this.columnName = column.getColumnName();
this.ssType = typeInfo.getSSType();
this.flags = typeInfo.getFlags();
this.isIdentity = typeInfo.isIdentity();
this.isNullable = typeInfo.isNullable();
precision = typeInfo.getPrecision();
this.scale = typeInfo.getScale();
collation = typeInfo.getSQLCollation();
this.jdbcType = ssType.getJDBCType().getIntValue();
}
// This constructor is needed for the source meta data.
BulkColumnMetaData(String colName, boolean isNullable, int precision, int scale, int jdbcType,
DateTimeFormatter dateTimeFormatter) {
this.columnName = colName;
this.isNullable = isNullable;
this.precision = precision;
this.scale = scale;
this.jdbcType = jdbcType;
this.dateTimeFormatter = dateTimeFormatter;
}
BulkColumnMetaData(Column column, String collationName, String encryptionType) {
this(column);
this.collationName = collationName;
this.encryptionType = encryptionType;
}
// update the cryptoMeta of source when reading from forward only resultset
BulkColumnMetaData(BulkColumnMetaData bulkColumnMetaData, CryptoMetadata cryptoMeta) {
this.columnName = bulkColumnMetaData.columnName;
this.isNullable = bulkColumnMetaData.isNullable;
this.precision = bulkColumnMetaData.precision;
this.scale = bulkColumnMetaData.scale;
this.jdbcType = bulkColumnMetaData.jdbcType;
this.cryptoMeta = cryptoMeta;
}
}
/**
* A map to store the metadata information for the destination table.
*/
private transient Map destColumnMetadata;
/**
* A map to store the metadata information for the source table.
*/
private transient Map srcColumnMetadata;
/**
* Variable to store destination column count.
*/
private int destColumnCount;
/**
* Variable to store source column count.
*/
private int srcColumnCount;
/**
* Shared timer
*/
private transient ScheduledFuture> timeout;
/**
* Shared timer
*/
private static final Lock DESTINATION_COL_METADATA_LOCK = new ReentrantLock();
/**
* The maximum temporal precision we can send when using varchar(precision) in bulkcommand, to send a
* smalldatetime/datetime value.
*/
private static final int SOURCE_BULK_RECORD_TEMPORAL_MAX_PRECISION = 50;
/**
* Constructs a SQLServerBulkCopy using the specified open instance of SQLServerConnection.
*
* @param connection
* Open instance of Connection to destination server. Must be from the Microsoft JDBC driver for SQL Server.
* @throws SQLServerException
* If the supplied type is not a connection from the Microsoft JDBC driver for SQL Server.
*/
public SQLServerBulkCopy(Connection connection) throws SQLServerException {
loggerExternal.entering(loggerClassName, "SQLServerBulkCopy", connection);
if (null == connection || !(connection instanceof ISQLServerConnection)) {
SQLServerException.makeFromDriverError(null, null,
SQLServerException.getErrString("R_invalidDestConnection"), null, false);
}
if (connection instanceof SQLServerConnection) {
this.connection = (SQLServerConnection) connection;
} else if (connection instanceof SQLServerConnectionPoolProxy) {
this.connection = ((SQLServerConnectionPoolProxy) connection).getWrappedConnection();
} else {
SQLServerException.makeFromDriverError(null, null,
SQLServerException.getErrString("R_invalidDestConnection"), null, false);
}
ownsConnection = false;
// useInternalTransaction will be false by default. When a connection object is passed in, bulk copy
// will use that connection object's transaction, i.e. no transaction management is done by bulk copy.
copyOptions = new SQLServerBulkCopyOptions();
initializeDefaults();
loggerExternal.exiting(loggerClassName, "SQLServerBulkCopy");
}
/**
* Constructs a SQLServerBulkCopy based on the supplied connectionString.
*
* @param connectionUrl
* Connection string for the destination server.
* @throws SQLServerException
* If a connection cannot be established.
*/
public SQLServerBulkCopy(String connectionUrl) throws SQLServerException {
loggerExternal.entering(loggerClassName, "SQLServerBulkCopy", "connectionUrl not traced.");
if ((connectionUrl == null) || "".equals(connectionUrl.trim())) {
throw new SQLServerException(null, SQLServerException.getErrString("R_nullConnection"), null, 0, false);
}
ownsConnection = true;
SQLServerDriver driver = new SQLServerDriver();
connection = (SQLServerConnection) driver.connect(connectionUrl, null);
if (null == connection) {
throw new SQLServerException(null, SQLServerException.getErrString("R_invalidConnection"), null, 0, false);
}
copyOptions = new SQLServerBulkCopyOptions();
initializeDefaults();
loggerExternal.exiting(loggerClassName, "SQLServerBulkCopy");
}
/**
* Adds a new column mapping, using ordinals to specify both the source and destination columns.
*
* @param sourceColumn
* Source column ordinal.
* @param destinationColumn
* Destination column ordinal.
* @throws SQLServerException
* If the column mapping is invalid
*/
public void addColumnMapping(int sourceColumn, int destinationColumn) throws SQLServerException {
if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) {
loggerExternal.entering(loggerClassName, "addColumnMapping",
new Object[] {sourceColumn, destinationColumn});
}
if (0 >= sourceColumn) {
throwInvalidArgument("sourceColumn");
} else if (0 >= destinationColumn) {
throwInvalidArgument("destinationColumn");
}
columnMappings.add(new ColumnMapping(sourceColumn, destinationColumn));
loggerExternal.exiting(loggerClassName, "addColumnMapping");
}
/**
* Adds a new column mapping, using an ordinal for the source column and a string for the destination column.
*
* @param sourceColumn
* Source column ordinal.
* @param destinationColumn
* Destination column name.
* @throws SQLServerException
* If the column mapping is invalid
*/
public void addColumnMapping(int sourceColumn, String destinationColumn) throws SQLServerException {
if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) {
loggerExternal.entering(loggerClassName, "addColumnMapping",
new Object[] {sourceColumn, destinationColumn});
}
if (0 >= sourceColumn) {
throwInvalidArgument("sourceColumn");
} else if (null == destinationColumn || destinationColumn.isEmpty()) {
throwInvalidArgument("destinationColumn");
}
columnMappings.add(new ColumnMapping(sourceColumn, destinationColumn));
loggerExternal.exiting(loggerClassName, "addColumnMapping");
}
/**
* Adds a new column mapping, using a column name to describe the source column and an ordinal to specify the
* destination column.
*
* @param sourceColumn
* Source column name.
* @param destinationColumn
* Destination column ordinal.
* @throws SQLServerException
* If the column mapping is invalid
*/
public void addColumnMapping(String sourceColumn, int destinationColumn) throws SQLServerException {
if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) {
loggerExternal.entering(loggerClassName, "addColumnMapping",
new Object[] {sourceColumn, destinationColumn});
}
if (0 >= destinationColumn) {
throwInvalidArgument("destinationColumn");
} else if (null == sourceColumn || sourceColumn.isEmpty()) {
throwInvalidArgument("sourceColumn");
}
columnMappings.add(new ColumnMapping(sourceColumn, destinationColumn));
loggerExternal.exiting(loggerClassName, "addColumnMapping");
}
/**
* Adds a new column mapping, using column names to specify both source and destination columns.
*
* @param sourceColumn
* Source column name.
* @param destinationColumn
* Destination column name.
* @throws SQLServerException
* If the column mapping is invalid
*/
public void addColumnMapping(String sourceColumn, String destinationColumn) throws SQLServerException {
if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) {
loggerExternal.entering(loggerClassName, "addColumnMapping",
new Object[] {sourceColumn, destinationColumn});
}
if (null == sourceColumn || sourceColumn.isEmpty()) {
throwInvalidArgument("sourceColumn");
} else if (null == destinationColumn || destinationColumn.isEmpty()) {
throwInvalidArgument("destinationColumn");
}
columnMappings.add(new ColumnMapping(sourceColumn, destinationColumn));
loggerExternal.exiting(loggerClassName, "addColumnMapping");
}
/**
* Clears the contents of the column mappings
*/
public void clearColumnMappings() {
loggerExternal.entering(loggerClassName, "clearColumnMappings");
columnMappings.clear();
loggerExternal.exiting(loggerClassName, "clearColumnMappings");
}
/**
* Closes the SQLServerBulkCopy instance
*/
public void close() {
loggerExternal.entering(loggerClassName, "close");
if (ownsConnection) {
try {
connection.close();
} catch (SQLException e) {
// Ignore this exception
}
}
loggerExternal.exiting(loggerClassName, "close");
}
/**
* Returns the name of the destination table on the server.
*
* @return Destination table name.
*/
public String getDestinationTableName() {
return destinationTableName;
}
/**
* Sets the name of the destination table on the server.
*
* @param tableName
* Destination table name.
* @throws SQLServerException
* If the table name is null
*/
public void setDestinationTableName(String tableName) throws SQLServerException {
loggerExternal.entering(loggerClassName, "setDestinationTableName", tableName);
if (null == tableName || 0 == tableName.trim().length()) {
throwInvalidArgument("tableName");
}
destinationTableName = tableName.trim();
loggerExternal.exiting(loggerClassName, "setDestinationTableName");
}
/**
* Returns the current SQLServerBulkCopyOptions.
*
* @return Current SQLServerBulkCopyOptions settings.
*/
public SQLServerBulkCopyOptions getBulkCopyOptions() {
return copyOptions;
}
/**
* Update the behavior of the SQLServerBulkCopy instance according to the options supplied, if supplied
* SQLServerBulkCopyOption is not null.
*
* @param copyOptions
* Settings to change how the WriteToServer methods behave.
* @throws SQLServerException
* If the SQLServerBulkCopyOption class was constructed using an existing Connection and the
* UseInternalTransaction option is specified.
*/
public void setBulkCopyOptions(SQLServerBulkCopyOptions copyOptions) throws SQLServerException {
loggerExternal.entering(loggerClassName, "updateBulkCopyOptions", copyOptions);
if (null != copyOptions) {
// Verify that copyOptions does not have useInternalTransaction set.
// UseInternalTrasnaction can only be used with a connection string.
// Setting it with an external connection object should throw
// exception.
if (!ownsConnection && copyOptions.isUseInternalTransaction()) {
SQLServerException.makeFromDriverError(null, null,
SQLServerException.getErrString("R_invalidTransactionOption"), null, false);
}
this.copyOptions = copyOptions;
}
loggerExternal.exiting(loggerClassName, "updateBulkCopyOptions");
}
/**
* Copies all rows in the supplied ResultSet to a destination table specified by the destinationTableName property
* of the SQLServerBulkCopy object.
*
* @param sourceData
* ResultSet to read data rows from.
* @throws SQLServerException
* If there are any issues encountered when performing the bulk copy operation
*/
public void writeToServer(ResultSet sourceData) throws SQLServerException {
writeResultSet(sourceData, false);
}
/**
* Copies all rows in the supplied RowSet to a destination table specified by the destinationTableName property of
* the SQLServerBulkCopy object.
*
* @param sourceData
* RowSet to read data rows from.
* @throws SQLServerException
* If there are any issues encountered when performing the bulk copy operation
*/
public void writeToServer(RowSet sourceData) throws SQLServerException {
writeResultSet(sourceData, true);
}
/**
* Copies all rows in the supplied ResultSet to a destination table specified by the destinationTableName property
* of the SQLServerBulkCopy object.
*
* @param sourceData
* ResultSet to read data rows from.
* @param isRowSet
* @throws SQLServerException
* If there are any issues encountered when performing the bulk copy operation
*/
private void writeResultSet(ResultSet sourceData, boolean isRowSet) throws SQLServerException {
loggerExternal.entering(loggerClassName, "writeToServer");
if (null == sourceData) {
throwInvalidArgument("sourceData");
}
try {
if (isRowSet) // Default RowSet implementation in Java doesn't have isClosed() implemented so need to do an
// alternate check instead.
{
if (!sourceData.isBeforeFirst()) {
sourceData.beforeFirst();
}
} else {
if (sourceData.isClosed()) {
SQLServerException.makeFromDriverError(null, null,
SQLServerException.getErrString("R_resultsetClosed"), null, false);
}
}
} catch (SQLException e) {
throw new SQLServerException(null, e.getMessage(), null, 0, false);
}
sourceResultSet = sourceData;
serverBulkData = null;
// Save the resultset metadata as it is used in many places.
try {
sourceResultSetMetaData = sourceResultSet.getMetaData();
} catch (SQLException e) {
throw new SQLServerException(SQLServerException.getErrString("R_unableRetrieveColMeta"), e);
}
writeToServer();
loggerExternal.exiting(loggerClassName, "writeToServer");
}
/**
* Copies all rows from the supplied ISQLServerBulkRecord to a destination table specified by the
* destinationTableName property of the SQLServerBulkCopy object.
*
* @param sourceData
* ISQLServerBulkData to read data rows from.
* @throws SQLServerException
* If there are any issues encountered when performing the bulk copy operation
*/
public void writeToServer(ISQLServerBulkData sourceData) throws SQLServerException {
loggerExternal.entering(loggerClassName, "writeToServer");
if (null == sourceData) {
throwInvalidArgument("sourceData");
}
serverBulkData = sourceData;
sourceResultSet = null;
writeToServer();
loggerExternal.exiting(loggerClassName, "writeToServer");
}
/**
* Initializes the defaults for member variables that require it.
*/
private void initializeDefaults() {
columnMappings = new ArrayList<>();
destinationTableName = null;
serverBulkData = null;
sourceResultSet = null;
sourceResultSetMetaData = null;
srcColumnCount = 0;
srcColumnMetadata = null;
destColumnMetadata = null;
destColumnCount = 0;
}
private void sendBulkLoadBCP() throws SQLServerException {
final class InsertBulk extends TDSCommand {
/**
* Always update serialVersionUID when prompted.
*/
private static final long serialVersionUID = 6714118105257791547L;
InsertBulk() {
super("InsertBulk", 0, 0);
}
final boolean doExecute() throws SQLServerException {
int timeoutSeconds = copyOptions.getBulkCopyTimeout();
if (timeoutSeconds > 0) {
connection.checkClosed();
timeout = connection.getSharedTimer().schedule(new TDSTimeoutTask(this, connection),
timeoutSeconds);
}
// doInsertBulk inserts the rows in one batch. It returns true if there are more rows in
// the resultset, false otherwise. We keep sending rows, one batch at a time until the
// resultset is done.
try {
while (doInsertBulk(this));
} catch (SQLServerException topLevelException) {
// Get to the root of this exception.
Throwable rootCause = topLevelException;
while (null != rootCause.getCause()) {
rootCause = rootCause.getCause();
}
// Check whether it is a timeout exception.
if (rootCause instanceof SQLException && timeout != null && timeout.isDone()) {
SQLException sqlEx = (SQLException) rootCause;
if (sqlEx.getSQLState() != null
&& sqlEx.getSQLState().equals(SQLState.STATEMENT_CANCELED.getSQLStateCode())) {
// If SQLServerBulkCopy is managing the transaction, a rollback is needed.
if (copyOptions.isUseInternalTransaction()) {
connection.rollback();
}
throw new SQLServerException(SQLServerException.getErrString("R_queryTimedOut"),
SQLState.STATEMENT_CANCELED, DriverError.NOT_SET, sqlEx);
}
}
// It is not a timeout exception. Re-throw.
throw topLevelException;
}
if (timeout != null) {
timeout.cancel(true);
timeout = null;
}
return true;
}
}
connection.executeCommand(new InsertBulk());
}
/**
* write ColumnData token in COLMETADATA header
*/
private void writeColumnMetaDataColumnData(TDSWriter tdsWriter, int idx) throws SQLServerException {
int srcColumnIndex, destPrecision;
int bulkJdbcType, bulkPrecision, bulkScale;
SQLCollation collation;
SSType destSSType;
boolean isStreaming, srcNullable;
// For varchar, precision is the size of the varchar type.
/*
* UserType USHORT/ULONG; (Changed to ULONG in TDS 7.2) The user type ID of the data type of the column. The
* value will be 0x0000 with the exceptions of TIMESTAMP (0x0050) and alias types (greater than 0x00FF)
*/
byte[] userType = new byte[4];
userType[0] = (byte) 0x00;
userType[1] = (byte) 0x00;
userType[2] = (byte) 0x00;
userType[3] = (byte) 0x00;
tdsWriter.writeBytes(userType);
/**
* Flags token - Bit flags in least significant bit order https://msdn.microsoft.com/en-us/library/dd357363.aspx
* flags[0] = (byte) 0x05; flags[1] = (byte) 0x00;
*/
int destColumnIndex = columnMappings.get(idx).destinationColumnOrdinal;
/**
* TYPE_INFO FIXEDLENTYPE Example INT: tdsWriter.writeByte((byte) 0x38);
*/
srcColumnIndex = columnMappings.get(idx).sourceColumnOrdinal;
byte[] flags = destColumnMetadata.get(destColumnIndex).flags;
// If AllowEncryptedValueModification is set to true (and of course AE is off),
// the driver will not sent AE information, so, we need to set Encryption bit flag to 0.
if (null == srcColumnMetadata.get(srcColumnIndex).cryptoMeta
&& null == destColumnMetadata.get(destColumnIndex).cryptoMeta
&& copyOptions.isAllowEncryptedValueModifications()) {
// flags[1]>>3 & 0x01 is the encryption bit flag.
// it is the 4th least significant bit in this byte, so minus 8 to set it to 0.
if (1 == (flags[1] >> 3 & 0x01)) {
flags[1] = (byte) (flags[1] - 8);
}
}
tdsWriter.writeBytes(flags);
bulkJdbcType = srcColumnMetadata.get(srcColumnIndex).jdbcType;
bulkPrecision = srcColumnMetadata.get(srcColumnIndex).precision;
bulkScale = srcColumnMetadata.get(srcColumnIndex).scale;
srcNullable = srcColumnMetadata.get(srcColumnIndex).isNullable;
destSSType = destColumnMetadata.get(destColumnIndex).ssType;
destPrecision = destColumnMetadata.get(destColumnIndex).precision;
bulkPrecision = validateSourcePrecision(bulkPrecision, bulkJdbcType, destPrecision);
collation = destColumnMetadata.get(destColumnIndex).collation;
if (null == collation)
collation = connection.getDatabaseCollation();
if ((java.sql.Types.NCHAR == bulkJdbcType) || (java.sql.Types.NVARCHAR == bulkJdbcType)
|| (java.sql.Types.LONGNVARCHAR == bulkJdbcType)) {
isStreaming = (DataTypes.SHORT_VARTYPE_MAX_CHARS < bulkPrecision)
|| (DataTypes.SHORT_VARTYPE_MAX_CHARS < destPrecision);
} else {
isStreaming = (DataTypes.SHORT_VARTYPE_MAX_BYTES < bulkPrecision)
|| (DataTypes.SHORT_VARTYPE_MAX_BYTES < destPrecision);
}
CryptoMetadata destCryptoMeta = destColumnMetadata.get(destColumnIndex).cryptoMeta;
/*
* if source is encrypted and destination is unencrypted, use destination's sql type to send since there is no
* way of finding if source is encrypted without accessing the resultset. Send destination type if source
* resultset set is of type SQLServer, encryption is enabled and destination column is not encrypted
*/
if ((sourceResultSet instanceof SQLServerResultSet) && (connection.isColumnEncryptionSettingEnabled())
&& (null != destCryptoMeta)) {
bulkJdbcType = destColumnMetadata.get(destColumnIndex).jdbcType;
bulkPrecision = destPrecision;
bulkScale = destColumnMetadata.get(destColumnIndex).scale;
}
// use varbinary to send if destination is encrypted
if (((null != destColumnMetadata.get(destColumnIndex).encryptionType)
&& copyOptions.isAllowEncryptedValueModifications())
|| (null != destColumnMetadata.get(destColumnIndex).cryptoMeta)) {
tdsWriter.writeByte((byte) 0xA5);
if (isStreaming) {
tdsWriter.writeShort((short) 0xFFFF);
} else {
tdsWriter.writeShort((short) (bulkPrecision));
}
}
// In this case we will explicitly send binary value.
else if (((java.sql.Types.CHAR == bulkJdbcType) || (java.sql.Types.VARCHAR == bulkJdbcType)
|| (java.sql.Types.LONGVARCHAR == bulkJdbcType))
&& (SSType.BINARY == destSSType || SSType.VARBINARY == destSSType || SSType.VARBINARYMAX == destSSType
|| SSType.IMAGE == destSSType)) {
if (isStreaming) {
// Send as VARBINARY if streaming is enabled
tdsWriter.writeByte((byte) 0xA5);
} else {
tdsWriter.writeByte((byte) ((SSType.BINARY == destSSType) ? 0xAD : 0xA5));
}
tdsWriter.writeShort((short) (bulkPrecision));
} else {
writeTypeInfo(tdsWriter, bulkJdbcType, bulkScale, bulkPrecision, destSSType, collation, isStreaming,
srcNullable, false);
}
if (null != destCryptoMeta) {
int baseDestJDBCType = destCryptoMeta.baseTypeInfo.getSSType().getJDBCType().asJavaSqlType();
int baseDestPrecision = destCryptoMeta.baseTypeInfo.getPrecision();
if ((java.sql.Types.NCHAR == baseDestJDBCType) || (java.sql.Types.NVARCHAR == baseDestJDBCType)
|| (java.sql.Types.LONGNVARCHAR == baseDestJDBCType))
isStreaming = (DataTypes.SHORT_VARTYPE_MAX_CHARS < baseDestPrecision);
else
isStreaming = (DataTypes.SHORT_VARTYPE_MAX_BYTES < baseDestPrecision);
// Send CryptoMetaData
tdsWriter.writeShort(destCryptoMeta.getOrdinal()); // Ordinal
tdsWriter.writeBytes(userType); // usertype
// BaseTypeInfo
writeTypeInfo(tdsWriter, baseDestJDBCType, destCryptoMeta.baseTypeInfo.getScale(), baseDestPrecision,
destCryptoMeta.baseTypeInfo.getSSType(), collation, isStreaming, srcNullable, true);
tdsWriter.writeByte(destCryptoMeta.cipherAlgorithmId);
tdsWriter.writeByte(destCryptoMeta.encryptionType.getValue());
tdsWriter.writeByte(destCryptoMeta.normalizationRuleVersion);
}
/*
* ColName token The column name. Contains the column name length and column name. see: SQLServerConnection.java
* toUCS16(String s)
*/
int destColNameLen = columnMappings.get(idx).destinationColumnName.length();
String destColName = columnMappings.get(idx).destinationColumnName;
byte[] colName = new byte[2 * destColNameLen];
for (int i = 0; i < destColNameLen; ++i) {
int c = destColName.charAt(i);
colName[2 * i] = (byte) (c & 0xFF);
colName[2 * i + 1] = (byte) ((c >> 8) & 0xFF);
}
tdsWriter.writeByte((byte) destColNameLen);
tdsWriter.writeBytes(colName);
}
private void writeTypeInfo(TDSWriter tdsWriter, int srcJdbcType, int srcScale, int srcPrecision, SSType destSSType,
SQLCollation collation, boolean isStreaming, boolean srcNullable,
boolean isBaseType) throws SQLServerException {
switch (srcJdbcType) {
case java.sql.Types.INTEGER: // 0x38
if (!srcNullable) {
tdsWriter.writeByte(TDSType.INT4.byteValue());
} else {
tdsWriter.writeByte(TDSType.INTN.byteValue());
tdsWriter.writeByte((byte) 0x04);
}
break;
case java.sql.Types.BIGINT: // 0x7f
if (!srcNullable) {
tdsWriter.writeByte(TDSType.INT8.byteValue());
} else {
tdsWriter.writeByte(TDSType.INTN.byteValue());
tdsWriter.writeByte((byte) 0x08);
}
break;
case java.sql.Types.BIT: // 0x32
if (!srcNullable) {
tdsWriter.writeByte(TDSType.BIT1.byteValue());
} else {
tdsWriter.writeByte(TDSType.BITN.byteValue());
tdsWriter.writeByte((byte) 0x01);
}
break;
case java.sql.Types.SMALLINT: // 0x34
if (!srcNullable) {
tdsWriter.writeByte(TDSType.INT2.byteValue());
} else {
tdsWriter.writeByte(TDSType.INTN.byteValue());
tdsWriter.writeByte((byte) 0x02);
}
break;
case java.sql.Types.TINYINT: // 0x30
if (!srcNullable) {
tdsWriter.writeByte(TDSType.INT1.byteValue());
} else {
tdsWriter.writeByte(TDSType.INTN.byteValue());
tdsWriter.writeByte((byte) 0x01);
}
break;
case java.sql.Types.FLOAT:
case java.sql.Types.DOUBLE: // (FLT8TYPE) 0x3E
if (!srcNullable) {
tdsWriter.writeByte(TDSType.FLOAT8.byteValue());
} else {
tdsWriter.writeByte(TDSType.FLOATN.byteValue());
tdsWriter.writeByte((byte) 0x08);
}
break;
case java.sql.Types.REAL: // (FLT4TYPE) 0x3B
if (!srcNullable) {
tdsWriter.writeByte(TDSType.FLOAT4.byteValue());
} else {
tdsWriter.writeByte(TDSType.FLOATN.byteValue());
tdsWriter.writeByte((byte) 0x04);
}
break;
case microsoft.sql.Types.MONEY:
case microsoft.sql.Types.SMALLMONEY:
tdsWriter.writeByte(TDSType.MONEYN.byteValue()); // 0x6E
if (SSType.MONEY == destSSType)
tdsWriter.writeByte((byte) 8);
else
tdsWriter.writeByte((byte) 4);
break;
case java.sql.Types.NUMERIC:
case java.sql.Types.DECIMAL:
/*
* SQL Server allows the insertion of decimal and numeric into a money (and smallmoney) column, but
* Azure DW only accepts money types for money column. To make the code compatible against both SQL
* Server and Azure DW, always send decimal and numeric as money/smallmoney if the destination column is
* money/smallmoney and the source is decimal/numeric.
*/
if (destSSType == SSType.MONEY) {
tdsWriter.writeByte(TDSType.MONEYN.byteValue());
tdsWriter.writeByte((byte) 8);
break;
} else if (destSSType == SSType.SMALLMONEY) {
tdsWriter.writeByte(TDSType.MONEYN.byteValue());
tdsWriter.writeByte((byte) 4);
break;
}
byte byteType = (java.sql.Types.DECIMAL == srcJdbcType) ? TDSType.DECIMALN.byteValue()
: TDSType.NUMERICN.byteValue();
tdsWriter.writeByte(byteType);
tdsWriter.writeByte((byte) TDSWriter.BIGDECIMAL_MAX_LENGTH); // maximum length
tdsWriter.writeByte((byte) srcPrecision); // unsigned byte
tdsWriter.writeByte((byte) srcScale); // unsigned byte
break;
case microsoft.sql.Types.GUID:
case java.sql.Types.CHAR: // 0xAF
if (isBaseType && (SSType.GUID == destSSType)) {
tdsWriter.writeByte(TDSType.GUID.byteValue());
tdsWriter.writeByte((byte) 0x10);
} else {
if (unicodeConversionRequired(srcJdbcType, destSSType)) {
tdsWriter.writeByte(TDSType.NCHAR.byteValue());
tdsWriter.writeShort(isBaseType ? (short) (srcPrecision) : (short) (2 * srcPrecision));
} else {
tdsWriter.writeByte(TDSType.BIGCHAR.byteValue());
tdsWriter.writeShort((short) (srcPrecision));
}
collation.writeCollation(tdsWriter);
}
break;
case java.sql.Types.NCHAR: // 0xEF
tdsWriter.writeByte(TDSType.NCHAR.byteValue());
tdsWriter.writeShort(isBaseType ? (short) (srcPrecision) : (short) (2 * srcPrecision));
collation.writeCollation(tdsWriter);
break;
case java.sql.Types.LONGVARCHAR:
case java.sql.Types.VARCHAR: // 0xA7
if (unicodeConversionRequired(srcJdbcType, destSSType)) {
tdsWriter.writeByte(TDSType.NVARCHAR.byteValue());
if (isStreaming) {
tdsWriter.writeShort((short) 0xFFFF);
} else {
tdsWriter.writeShort(isBaseType ? (short) (srcPrecision) : (short) (2 * srcPrecision));
}
} else {
tdsWriter.writeByte(TDSType.BIGVARCHAR.byteValue());
if (isStreaming) {
tdsWriter.writeShort((short) 0xFFFF);
} else {
tdsWriter.writeShort((short) (srcPrecision));
}
}
collation.writeCollation(tdsWriter);
break;
case java.sql.Types.LONGNVARCHAR:
case java.sql.Types.NVARCHAR: // 0xE7
tdsWriter.writeByte(TDSType.NVARCHAR.byteValue());
if (isStreaming) {
tdsWriter.writeShort((short) 0xFFFF);
} else {
tdsWriter.writeShort(isBaseType ? (short) (srcPrecision) : (short) (2 * srcPrecision));
}
collation.writeCollation(tdsWriter);
break;
case java.sql.Types.BINARY: // 0xAD
tdsWriter.writeByte(TDSType.BIGBINARY.byteValue());
tdsWriter.writeShort((short) (srcPrecision));
break;
case java.sql.Types.LONGVARBINARY:
case java.sql.Types.VARBINARY: // 0xA5
// BIGVARBINARY
tdsWriter.writeByte(TDSType.BIGVARBINARY.byteValue());
if (isStreaming) {
tdsWriter.writeShort((short) 0xFFFF);
} else {
tdsWriter.writeShort((short) (srcPrecision));
}
break;
case microsoft.sql.Types.DATETIME:
case microsoft.sql.Types.SMALLDATETIME:
case java.sql.Types.TIMESTAMP:
if (((!isBaseType) && (null != serverBulkData))
&& connection.getSendTemporalDataTypesAsStringForBulkCopy()) {
tdsWriter.writeByte(TDSType.BIGVARCHAR.byteValue());
tdsWriter.writeShort((short) (srcPrecision));
collation.writeCollation(tdsWriter);
} else {
switch (destSSType) {
case SMALLDATETIME:
if (!srcNullable)
tdsWriter.writeByte(TDSType.DATETIME4.byteValue());
else {
tdsWriter.writeByte(TDSType.DATETIMEN.byteValue());
tdsWriter.writeByte((byte) 4);
}
break;
case DATETIME:
if (!srcNullable)
tdsWriter.writeByte(TDSType.DATETIME8.byteValue());
else {
tdsWriter.writeByte(TDSType.DATETIMEN.byteValue());
tdsWriter.writeByte((byte) 8);
}
break;
default:
// DATETIME2 0x2A
tdsWriter.writeByte(TDSType.DATETIME2N.byteValue());
tdsWriter.writeByte((byte) srcScale);
break;
}
}
break;
case java.sql.Types.DATE: // 0x28
/*
* SQL Server supports numerous string literal formats for temporal types, hence sending them as varchar
* with approximate precision(length) needed to send supported string literals if destination is
* unencrypted. string literal formats supported by temporal types are available in MSDN page on data
* types.
*/
if (((!isBaseType) && (null != serverBulkData))
&& connection.getSendTemporalDataTypesAsStringForBulkCopy()) {
tdsWriter.writeByte(TDSType.BIGVARCHAR.byteValue());
tdsWriter.writeShort((short) (srcPrecision));
collation.writeCollation(tdsWriter);
} else {
tdsWriter.writeByte(TDSType.DATEN.byteValue());
}
break;
case java.sql.Types.TIME: // 0x29
if (((!isBaseType) && (null != serverBulkData))
&& connection.getSendTemporalDataTypesAsStringForBulkCopy()) {
tdsWriter.writeByte(TDSType.BIGVARCHAR.byteValue());
tdsWriter.writeShort((short) (srcPrecision));
collation.writeCollation(tdsWriter);
} else {
tdsWriter.writeByte(TDSType.TIMEN.byteValue());
tdsWriter.writeByte((byte) srcScale);
}
break;
// Send as DATETIMEOFFSET for TIME_WITH_TIMEZONE and TIMESTAMP_WITH_TIMEZONE
case 2013: // java.sql.Types.TIME_WITH_TIMEZONE
case 2014: // java.sql.Types.TIMESTAMP_WITH_TIMEZONE
tdsWriter.writeByte(TDSType.DATETIMEOFFSETN.byteValue());
tdsWriter.writeByte((byte) srcScale);
break;
case microsoft.sql.Types.DATETIMEOFFSET: // 0x2B
if (((!isBaseType) && (null != serverBulkData))
&& connection.getSendTemporalDataTypesAsStringForBulkCopy()) {
tdsWriter.writeByte(TDSType.BIGVARCHAR.byteValue());
tdsWriter.writeShort((short) (srcPrecision));
collation.writeCollation(tdsWriter);
} else {
tdsWriter.writeByte(TDSType.DATETIMEOFFSETN.byteValue());
tdsWriter.writeByte((byte) srcScale);
}
break;
case microsoft.sql.Types.SQL_VARIANT: // 0x62
tdsWriter.writeByte(TDSType.SQL_VARIANT.byteValue());
tdsWriter.writeInt(TDS.SQL_VARIANT_LENGTH);
break;
default:
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_BulkTypeNotSupported"));
String unsupportedDataType = JDBCType.of(srcJdbcType).toString().toLowerCase(Locale.ENGLISH);
throw new SQLServerException(form.format(new Object[] {unsupportedDataType}), null, 0, null);
}
}
/**
* Writes the CEK table needed for AE. Cek table (with 0 entries) will be present if AE was enabled and server
* supports it! OR if encryption was disabled in connection options
*/
private void writeCekTable(TDSWriter tdsWriter) throws SQLServerException {
if (connection.getServerSupportsColumnEncryption()) {
if ((null != destCekTable) && (0 < destCekTable.getSize())) {
tdsWriter.writeShort((short) destCekTable.getSize());
for (int cekIndx = 0; cekIndx < destCekTable.getSize(); cekIndx++) {
tdsWriter.writeInt(
destCekTable.getCekTableEntry(cekIndx).getColumnEncryptionKeyValues().get(0).databaseId);
tdsWriter.writeInt(
destCekTable.getCekTableEntry(cekIndx).getColumnEncryptionKeyValues().get(0).cekId);
tdsWriter.writeInt(
destCekTable.getCekTableEntry(cekIndx).getColumnEncryptionKeyValues().get(0).cekVersion);
tdsWriter.writeBytes(
destCekTable.getCekTableEntry(cekIndx).getColumnEncryptionKeyValues().get(0).cekMdVersion);
// We don't need to send the keys. So count for EncryptionKeyValue is 0 in EK_INFO TDS rule.
tdsWriter.writeByte((byte) 0x00);
}
} else {
// If no encrypted columns, but the connection setting is true write 0 as the size of the CekTable.
tdsWriter.writeShort((short) 0);
}
}
}
/**
* ...
*/
private void writeColumnMetaData(TDSWriter tdsWriter) throws SQLServerException {
/*
* TDS rules for Always Encrypted metadata COLMETADATA = TokenType Count CekTable NoMetaData / (1 *ColumnData)
* CekTable = EkValueCount EK_INFO EK_INFO = DatabaseId CekId CekVersion CekMDVersion Count EncryptionKeyValue
*/
/*
* TokenType: The token value is 0x81
*/
tdsWriter.writeByte((byte) TDS.TDS_COLMETADATA);
/*
* Count token: The count of columns. Should be USHORT. Not Supported in Java. Remedy:
* tdsWriter.writeShort((short)columnMappings.size());
*/
byte[] count = new byte[2];
count[0] = (byte) (columnMappings.size() & 0xFF);
count[1] = (byte) ((columnMappings.size() >> 8) & 0xFF);
tdsWriter.writeBytes(count);
writeCekTable(tdsWriter);
/*
* Writing ColumnData section Columndata tokens is written for each destination column in columnMappings
*/
for (int i = 0; i < columnMappings.size(); i++) {
writeColumnMetaDataColumnData(tdsWriter, i);
}
}
/**
* Validates whether the source JDBC types are compatible with the destination table data types. We need to do this
* only once for the whole bulk copy session.
*/
private void validateDataTypeConversions(int srcColOrdinal, int destColOrdinal) throws SQLServerException {
// Reducing unnecessary assignment to optimize for performance. This reduces code readability, but
// may be worth it for the bulk copy.
CryptoMetadata sourceCryptoMeta = srcColumnMetadata.get(srcColOrdinal).cryptoMeta;
CryptoMetadata destCryptoMeta = destColumnMetadata.get(destColOrdinal).cryptoMeta;
JDBCType srcJdbcType = (null != sourceCryptoMeta) ? sourceCryptoMeta.baseTypeInfo.getSSType().getJDBCType()
: JDBCType.of(srcColumnMetadata.get(srcColOrdinal).jdbcType);
SSType destSSType = (null != destCryptoMeta) ? destCryptoMeta.baseTypeInfo.getSSType()
: destColumnMetadata.get(destColOrdinal).ssType;
// Throw if the source type cannot be converted to the destination type.
if (!srcJdbcType.convertsTo(destSSType)) {
DataTypes.throwConversionError(srcJdbcType.toString(), destSSType.toString());
}
}
private String getDestTypeFromSrcType(int srcColIndx, int destColIndx,
TDSWriter tdsWriter) throws SQLServerException {
boolean isStreaming;
SSType destSSType = (null != destColumnMetadata.get(destColIndx).cryptoMeta) ? destColumnMetadata
.get(destColIndx).cryptoMeta.baseTypeInfo.getSSType() : destColumnMetadata.get(destColIndx).ssType;
int bulkJdbcType, bulkPrecision, bulkScale;
int srcPrecision;
bulkJdbcType = srcColumnMetadata.get(srcColIndx).jdbcType;
// For char/varchar precision is the size.
bulkPrecision = srcPrecision = srcColumnMetadata.get(srcColIndx).precision;
int destPrecision = destColumnMetadata.get(destColIndx).precision;
bulkScale = srcColumnMetadata.get(srcColIndx).scale;
CryptoMetadata destCryptoMeta = destColumnMetadata.get(destColIndx).cryptoMeta;
if (null != destCryptoMeta || (null == destCryptoMeta && copyOptions.isAllowEncryptedValueModifications())) {
// Encrypted columns are sent as binary data.
tdsWriter.setCryptoMetaData(destColumnMetadata.get(destColIndx).cryptoMeta);
/*
* if source is encrypted and destination is unencrypted, use destination's sql type to send since there is
* no way of finding if source is encrypted without accessing the resultset. Send destination type if source
* resultset set is of type SQLServer, encryption is enabled and destination column is not encrypted
*/
if ((sourceResultSet instanceof SQLServerResultSet) && (connection.isColumnEncryptionSettingEnabled())
&& (null != destCryptoMeta)) {
bulkJdbcType = destColumnMetadata.get(destColIndx).jdbcType;
bulkPrecision = destPrecision;
bulkScale = destColumnMetadata.get(destColIndx).scale;
}
// if destination is encrypted send metadata from destination and not from source
if (DataTypes.SHORT_VARTYPE_MAX_BYTES < destPrecision) {
return SSType.VARBINARY.toString() + MAX;
} else {
return SSType.VARBINARY.toString() + "(" + destColumnMetadata.get(destColIndx).precision + ")";
}
}
// isAllowEncryptedValueModifications is used only with source result set.
if ((null != sourceResultSet) && (null != destColumnMetadata.get(destColIndx).encryptionType)
&& copyOptions.isAllowEncryptedValueModifications()) {
return "varbinary(" + bulkPrecision + ")";
}
bulkPrecision = validateSourcePrecision(srcPrecision, bulkJdbcType, destPrecision);
if ((java.sql.Types.NCHAR == bulkJdbcType) || (java.sql.Types.NVARCHAR == bulkJdbcType)
|| (java.sql.Types.LONGNVARCHAR == bulkJdbcType)) {
isStreaming = (DataTypes.SHORT_VARTYPE_MAX_CHARS < srcPrecision)
|| (DataTypes.SHORT_VARTYPE_MAX_CHARS < destPrecision);
} else {
isStreaming = (DataTypes.SHORT_VARTYPE_MAX_BYTES < srcPrecision)
|| (DataTypes.SHORT_VARTYPE_MAX_BYTES < destPrecision);
}
// SQL Server does not convert string to binary, we will have to explicitly convert before sending.
if (Util.isCharType(bulkJdbcType) && Util.isBinaryType(destSSType)) {
if (isStreaming)
return SSType.VARBINARY.toString() + MAX;
else
// Return binary(n) or varbinary(n) or varbinary(max) depending on destination type/precision.
return destSSType.toString() + "("
+ ((DataTypes.SHORT_VARTYPE_MAX_BYTES < destPrecision) ? "max" : destPrecision) + ")";
}
switch (bulkJdbcType) {
case java.sql.Types.INTEGER:
return SSType.INTEGER.toString();
case java.sql.Types.SMALLINT:
return SSType.SMALLINT.toString();
case java.sql.Types.BIGINT:
return SSType.BIGINT.toString();
case java.sql.Types.BIT:
return SSType.BIT.toString();
case java.sql.Types.TINYINT:
return SSType.TINYINT.toString();
case java.sql.Types.FLOAT:
case java.sql.Types.DOUBLE:
return SSType.FLOAT.toString();
case java.sql.Types.REAL:
return SSType.REAL.toString();
case microsoft.sql.Types.MONEY:
return SSType.MONEY.toString();
case microsoft.sql.Types.SMALLMONEY:
return SSType.SMALLMONEY.toString();
case java.sql.Types.DECIMAL:
/*
* SQL Server allows the insertion of decimal and numeric into a money (and smallmoney) column, but
* Azure DW only accepts money types for money column. To make the code compatible against both SQL
* Server and Azure DW, always send decimal and numeric as money/smallmoney if the destination column is
* money/smallmoney and the source is decimal/numeric.
*/
if (destSSType == SSType.MONEY) {
return SSType.MONEY.toString();
} else if (destSSType == SSType.SMALLMONEY) {
return SSType.SMALLMONEY.toString();
}
return SSType.DECIMAL.toString() + "(" + bulkPrecision + ", " + bulkScale + ")";
case java.sql.Types.NUMERIC:
if (destSSType == SSType.MONEY) {
return SSType.MONEY.toString();
} else if (destSSType == SSType.SMALLMONEY) {
return SSType.SMALLMONEY.toString();
}
return SSType.NUMERIC.toString() + "(" + bulkPrecision + ", " + bulkScale + ")";
case microsoft.sql.Types.GUID:
// For char the value has to be between 0 to 8000.
return SSType.CHAR.toString() + "(" + bulkPrecision + ")";
case java.sql.Types.CHAR:
if (unicodeConversionRequired(bulkJdbcType, destSSType)) {
return SSType.NCHAR.toString() + "(" + bulkPrecision + ")";
} else {
return SSType.CHAR.toString() + "(" + bulkPrecision + ")";
}
case java.sql.Types.NCHAR:
return SSType.NCHAR.toString() + "(" + bulkPrecision + ")";
case java.sql.Types.LONGVARCHAR:
case java.sql.Types.VARCHAR:
// Here the actual size of the varchar is used from the source table.
// Doesn't need to match with the exact size of data or with the destination column size.
if (unicodeConversionRequired(bulkJdbcType, destSSType)) {
if (isStreaming) {
return SSType.NVARCHAR.toString() + MAX;
} else {
return SSType.NVARCHAR.toString() + "(" + bulkPrecision + ")";
}
} else {
if (isStreaming) {
return SSType.VARCHAR.toString() + MAX;
} else {
return SSType.VARCHAR.toString() + "(" + bulkPrecision + ")";
}
}
// For INSERT BULK operations, XMLTYPE is to be sent as NVARCHAR(N) or NVARCHAR(MAX) data type.
// An error is produced if XMLTYPE is specified.
case java.sql.Types.LONGNVARCHAR:
case java.sql.Types.NVARCHAR:
if (isStreaming) {
return SSType.NVARCHAR.toString() + MAX;
} else {
return SSType.NVARCHAR.toString() + "(" + bulkPrecision + ")";
}
case java.sql.Types.BINARY:
// For binary the value has to be between 0 to 8000.
return SSType.BINARY.toString() + "(" + bulkPrecision + ")";
case java.sql.Types.LONGVARBINARY:
case java.sql.Types.VARBINARY:
if (isStreaming)
return SSType.VARBINARY.toString() + MAX;
else
return SSType.VARBINARY.toString() + "(" + bulkPrecision + ")";
case microsoft.sql.Types.DATETIME:
case microsoft.sql.Types.SMALLDATETIME:
case java.sql.Types.TIMESTAMP:
switch (destSSType) {
case SMALLDATETIME:
if (null != serverBulkData && connection.getSendTemporalDataTypesAsStringForBulkCopy()) {
return SSType.VARCHAR.toString() + "("
+ ((0 == bulkPrecision) ? SOURCE_BULK_RECORD_TEMPORAL_MAX_PRECISION : bulkPrecision)
+ ")";
} else {
return SSType.SMALLDATETIME.toString();
}
case DATETIME:
if (null != serverBulkData && connection.getSendTemporalDataTypesAsStringForBulkCopy()) {
return SSType.VARCHAR.toString() + "("
+ ((0 == bulkPrecision) ? SOURCE_BULK_RECORD_TEMPORAL_MAX_PRECISION : bulkPrecision)
+ ")";
} else {
return SSType.DATETIME.toString();
}
default:
// datetime2
/*
* If encrypted, varbinary will be returned before. The code will come here only if unencrypted.
* For unencrypted bulk copy if the source is CSV, we send the data as varchar and SQL Server
* will do the conversion. if the source is ResultSet, we send the data as the corresponding
* temporal type.
*/
if (null != serverBulkData && connection.getSendTemporalDataTypesAsStringForBulkCopy()) {
return SSType.VARCHAR.toString() + "("
+ ((0 == bulkPrecision) ? destPrecision : bulkPrecision) + ")";
} else {
return SSType.DATETIME2.toString() + "(" + bulkScale + ")";
}
}
case java.sql.Types.DATE:
/*
* If encrypted, varbinary will be returned before. The code will come here only if unencrypted. For
* unencrypted bulk copy if the source is CSV, we send the data as varchar and SQL Server will do the
* conversion. if the source is ResultSet, we send the data as the corresponding temporal type.
*/
if (null != serverBulkData && connection.getSendTemporalDataTypesAsStringForBulkCopy()) {
return SSType.VARCHAR.toString() + "(" + ((0 == bulkPrecision) ? destPrecision : bulkPrecision)
+ ")";
} else {
return SSType.DATE.toString();
}
case java.sql.Types.TIME:
/*
* If encrypted, varbinary will be returned before. The code will come here only if unencrypted. For
* unencrypted bulk copy if the source is CSV, we send the data as varchar and SQL Server will do the
* conversion. if the source is ResultSet, we send the data as the corresponding temporal type.
*/
if (null != serverBulkData && connection.getSendTemporalDataTypesAsStringForBulkCopy()) {
return SSType.VARCHAR.toString() + "(" + ((0 == bulkPrecision) ? destPrecision : bulkPrecision)
+ ")";
} else {
return SSType.TIME.toString() + "(" + bulkScale + ")";
}
// Return DATETIMEOFFSET for TIME_WITH_TIMEZONE and TIMESTAMP_WITH_TIMEZONE
case 2013: // java.sql.Types.TIME_WITH_TIMEZONE
case 2014: // java.sql.Types.TIMESTAMP_WITH_TIMEZONE
return SSType.DATETIMEOFFSET.toString() + "(" + bulkScale + ")";
case microsoft.sql.Types.DATETIMEOFFSET:
/*
* If encrypted, varbinary will be returned before. The code will come here only if unencrypted. For
* unencrypted bulk copy if the source is CSV, we send the data as varchar and SQL Server will do the
* conversion. if the source is ResultSet, we send the data as the corresponding temporal type.
*/
if (null != serverBulkData && connection.getSendTemporalDataTypesAsStringForBulkCopy()) {
return SSType.VARCHAR.toString() + "(" + ((0 == bulkPrecision) ? destPrecision : bulkPrecision)
+ ")";
} else {
return SSType.DATETIMEOFFSET.toString() + "(" + bulkScale + ")";
}
case microsoft.sql.Types.SQL_VARIANT:
return SSType.SQL_VARIANT.toString();
default: {
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_BulkTypeNotSupported"));
Object[] msgArgs = {JDBCType.of(bulkJdbcType).toString().toLowerCase(Locale.ENGLISH)};
SQLServerException.makeFromDriverError(null, null, form.format(msgArgs), null, true);
}
}
return null;
}
private String createInsertBulkCommand(TDSWriter tdsWriter) throws SQLServerException {
StringBuilder bulkCmd = new StringBuilder();
List bulkOptions = new ArrayList<>();
String endColumn = " , ";
bulkCmd.append("INSERT BULK ").append(destinationTableName).append(" (");
for (int i = 0; i < (columnMappings.size()); ++i) {
if (i == columnMappings.size() - 1) {
endColumn = " ) ";
}
ColumnMapping colMapping = columnMappings.get(i);
String columnCollation = destColumnMetadata
.get(columnMappings.get(i).destinationColumnOrdinal).collationName;
String addCollate = "";
String destType = getDestTypeFromSrcType(colMapping.sourceColumnOrdinal,
colMapping.destinationColumnOrdinal, tdsWriter).toUpperCase(Locale.ENGLISH);
if (null != columnCollation && columnCollation.trim().length() > 0) {
// we are adding collate in command only for char and varchar
if (null != destType && (destType.toLowerCase(Locale.ENGLISH).trim().startsWith("char")
|| destType.toLowerCase(Locale.ENGLISH).trim().startsWith("varchar")))
addCollate = " COLLATE " + columnCollation;
}
if (colMapping.destinationColumnName.contains("]")) {
String escapedColumnName = colMapping.destinationColumnName.replaceAll("]", "]]");
bulkCmd.append("[").append(escapedColumnName).append("] ").append(destType).append(addCollate)
.append(endColumn);
} else {
bulkCmd.append("[").append(colMapping.destinationColumnName).append("] ").append(destType)
.append(addCollate).append(endColumn);
}
}
if (copyOptions.isCheckConstraints()) {
bulkOptions.add("CHECK_CONSTRAINTS");
}
if (copyOptions.isFireTriggers()) {
bulkOptions.add("FIRE_TRIGGERS");
}
if (copyOptions.isKeepNulls()) {
bulkOptions.add("KEEP_NULLS");
}
if (copyOptions.getBatchSize() > 0) {
bulkOptions.add("ROWS_PER_BATCH = " + copyOptions.getBatchSize());
}
if (copyOptions.isTableLock()) {
bulkOptions.add("TABLOCK");
}
if (copyOptions.isAllowEncryptedValueModifications()) {
bulkOptions.add("ALLOW_ENCRYPTED_VALUE_MODIFICATIONS");
}
Iterator it = bulkOptions.iterator();
if (it.hasNext()) {
bulkCmd.append(" with (");
while (it.hasNext()) {
bulkCmd.append(it.next());
if (it.hasNext()) {
bulkCmd.append(", ");
}
}
bulkCmd.append(")");
}
if (loggerExternal.isLoggable(Level.FINER))
loggerExternal.finer(this.toString() + " TDSCommand: " + bulkCmd);
return bulkCmd.toString();
}
private boolean doInsertBulk(TDSCommand command) throws SQLServerException {
if (copyOptions.isUseInternalTransaction()) {
// Begin a manual transaction for this batch.
connection.setAutoCommit(false);
}
boolean insertRowByRow = false;
if (null != sourceResultSet && sourceResultSet instanceof SQLServerResultSet) {
SQLServerStatement srcStmt = (SQLServerStatement) ((SQLServerResultSet) sourceResultSet).getStatement();
int resultSetServerCursorId = ((SQLServerResultSet) sourceResultSet).getServerCursorId();
if (connection.equals(srcStmt.getConnection()) && 0 != resultSetServerCursorId) {
insertRowByRow = true;
}
if (((SQLServerResultSet) sourceResultSet).isForwardOnly()) {
try {
sourceResultSet.setFetchSize(1);
} catch (SQLException e) {
SQLServerException.makeFromDriverError(connection, sourceResultSet, e.getMessage(), e.getSQLState(),
true);
}
}
}
TDSWriter tdsWriter = null;
boolean moreDataAvailable = false;
try {
if (!insertRowByRow) {
tdsWriter = sendBulkCopyCommand(command);
}
try {
// Write all ROW tokens in the stream.
moreDataAvailable = writeBatchData(tdsWriter, command, insertRowByRow);
} finally {
tdsWriter = command.getTDSWriter();
}
} finally {
if (null == tdsWriter) {
tdsWriter = command.getTDSWriter();
}
// reset the cryptoMeta in IOBuffer
tdsWriter.setCryptoMetaData(null);
}
if (!insertRowByRow) {
// Write the DONE token in the stream. We may have to append the DONE token with every packet that is sent.
// For the current packets the driver does not generate a DONE token, but the BulkLoadBCP stream needs a
// DONE token
// after every packet. For now add it manually here for one packet.
// Note: This may break if more than one packet is sent.
// This is an example from https://msdn.microsoft.com/en-us/library/dd340549.aspx
writePacketDataDone(tdsWriter);
// Send to the server and read response.
TDSParser.parse(command.startResponse(), command.getLogContext());
}
if (copyOptions.isUseInternalTransaction()) {
// Commit the transaction for this batch.
connection.commit();
}
return moreDataAvailable;
}
private TDSWriter sendBulkCopyCommand(TDSCommand command) throws SQLServerException {
// Create and send the initial command for bulk copy ("INSERT BULK ...").
TDSWriter tdsWriter = command.startRequest(TDS.PKT_QUERY);
String bulkCmd = createInsertBulkCommand(tdsWriter);
tdsWriter.sendEnclavePackage(null, null);
tdsWriter.writeString(bulkCmd);
TDSParser.parse(command.startResponse(), command.getLogContext());
// Send the bulk data. This is the BulkLoadBCP TDS stream.
tdsWriter = command.startRequest(TDS.PKT_BULK);
// Write the COLUMNMETADATA token in the stream.
writeColumnMetaData(tdsWriter);
return tdsWriter;
}
private void writePacketDataDone(TDSWriter tdsWriter) throws SQLServerException {
// This is an example from https://msdn.microsoft.com/en-us/library/dd340549.aspx
tdsWriter.writeByte((byte) 0xFD);
tdsWriter.writeLong(0);
tdsWriter.writeInt(0);
}
/**
* Helper method to throw a SQLServerExeption with the invalidArgument message and given argument.
*/
private void throwInvalidArgument(String argument) throws SQLServerException {
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidArgument"));
Object[] msgArgs = {argument};
SQLServerException.makeFromDriverError(null, null, form.format(msgArgs), null, false);
}
/**
* The bulk copy operation
*/
private void writeToServer() throws SQLServerException {
if (connection.isClosed()) {
SQLServerException.makeFromDriverError(null, null, SQLServerException.getErrString("R_connectionIsClosed"),
SQLServerException.EXCEPTION_XOPEN_CONNECTION_DOES_NOT_EXIST, false);
}
long start = System.currentTimeMillis();
if (loggerExternal.isLoggable(Level.FINER))
loggerExternal.finer(this.toString() + " Start writeToServer: " + start);
getDestinationMetadata();
// Get source metadata in the BulkColumnMetaData object so that we can access metadata
// from the same object for both ResultSet and File.
getSourceMetadata();
validateColumnMappings();
sendBulkLoadBCP();
long end = System.currentTimeMillis();
if (loggerExternal.isLoggable(Level.FINER)) {
loggerExternal.finer(this.toString() + " End writeToServer: " + end);
int seconds = (int) ((end - start) / 1000L);
loggerExternal.finer(this.toString() + "Time elapsed: " + seconds + " seconds");
}
}
private void validateStringBinaryLengths(Object colValue, int srcCol, int destCol) throws SQLServerException {
int sourcePrecision;
int destPrecision = destColumnMetadata.get(destCol).precision;
int srcJdbcType = srcColumnMetadata.get(srcCol).jdbcType;
SSType destSSType = destColumnMetadata.get(destCol).ssType;
if ((Util.isCharType(srcJdbcType) && Util.isCharType(destSSType))
|| (Util.isBinaryType(srcJdbcType) && Util.isBinaryType(destSSType))) {
if (colValue instanceof String) {
if (Util.isBinaryType(destSSType)) {
// if the dest value is binary and the value is of type string.
// Repro in test case: ImpISQLServerBulkRecord_IssuesTest#testSendValidValueforBinaryColumnAsString
sourcePrecision = (((String) colValue).getBytes().length) / 2;
} else
sourcePrecision = ((String) colValue).length();
} else if (colValue instanceof byte[]) {
sourcePrecision = ((byte[]) colValue).length;
} else {
return;
}
if (sourcePrecision > destPrecision) {
String srcType = JDBCType.of(srcJdbcType) + "(" + sourcePrecision + ")";
String destType = destSSType.toString() + "(" + destPrecision + ")";
String destName = destColumnMetadata.get(destCol).columnName;
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_InvalidDataForAE"));
Object[] msgArgs = {srcType, destType, destName};
throw new SQLServerException(this, form.format(msgArgs), null, 0, false);
}
}
}
/**
* Returns the column metadata for the destination table (and saves it for later)
*/
private void getDestinationMetadata() throws SQLServerException {
if (null == destinationTableName) {
SQLServerException.makeFromDriverError(null, null,
SQLServerException.getErrString("R_invalidDestinationTable"), null, false);
}
String escapedDestinationTableName = Util.escapeSingleQuotes(destinationTableName);
String key = null;
if (connection.getcacheBulkCopyMetadata()) {
String databaseName = connection.activeConnectionProperties
.getProperty(SQLServerDriverStringProperty.DATABASE_NAME.toString());
key = getHashedSecret(new String[] {escapedDestinationTableName, databaseName});
destColumnMetadata = BULK_COPY_OPERATION_CACHE.get(key);
}
if (null == destColumnMetadata || destColumnMetadata.isEmpty()) {
if (connection.getcacheBulkCopyMetadata()) {
DESTINATION_COL_METADATA_LOCK.lock();
try {
destColumnMetadata = BULK_COPY_OPERATION_CACHE.get(key);
if (null == destColumnMetadata || destColumnMetadata.isEmpty()) {
setDestinationColumnMetadata(escapedDestinationTableName);
// We are caching the following metadata about the table:
// 1. collation_name
// 2. is_computed
// 3. encryption_type
//
// Using this caching method, 'cacheBulkCopyMetadata', may have unintended consequences if the
// table changes somehow between inserts. For example, if the collation_name changes, the
// driver will not be aware of this and the inserted data will likely be corrupted. In such
// scenario, we can't detect this without making an additional metadata query, which would
// defeat the purpose of caching.
BULK_COPY_OPERATION_CACHE.put(key, destColumnMetadata);
}
} finally {
DESTINATION_COL_METADATA_LOCK.unlock();
}
if (loggerExternal.isLoggable(Level.FINER)) {
loggerExternal.finer(this.toString() + " Acquiring existing destination column metadata " +
"from cache for bulk copy");
}
} else {
setDestinationColumnMetadata(escapedDestinationTableName);
if (loggerExternal.isLoggable(Level.FINER)) {
loggerExternal.finer(this.toString() + " cacheBulkCopyMetadata=false - Querying server " +
"for destination column metadata");
}
}
}
destColumnCount = destColumnMetadata.size();
}
private void setDestinationColumnMetadata(String escapedDestinationTableName) throws SQLServerException {
SQLServerResultSet rs = null;
SQLServerStatement stmt = null;
String metaDataQuery = null;
try {
if (null != destinationTableMetadata) {
rs = (SQLServerResultSet) destinationTableMetadata;
} else {
stmt = (SQLServerStatement) connection.createStatement(ResultSet.TYPE_FORWARD_ONLY,
ResultSet.CONCUR_READ_ONLY, connection.getHoldability(), stmtColumnEncriptionSetting);
// Get destination metadata
rs = stmt.executeQueryInternal(
"sp_executesql N'SET FMTONLY ON SELECT * FROM " + escapedDestinationTableName + " '");
}
int destColumnMetadataCount = rs.getMetaData().getColumnCount();
destColumnMetadata = new HashMap<>();
destCekTable = rs.getCekTable();
if (!connection.getServerSupportsColumnEncryption()) {
metaDataQuery = "select collation_name, is_computed from sys.columns where " + "object_id=OBJECT_ID('"
+ escapedDestinationTableName + "') " + "order by column_id ASC";
} else {
metaDataQuery = "select collation_name, is_computed, encryption_type from sys.columns where "
+ "object_id=OBJECT_ID('" + escapedDestinationTableName + "') " + "order by column_id ASC";
}
try (SQLServerStatement statementMoreMetadata = (SQLServerStatement) connection.createStatement();
SQLServerResultSet rsMoreMetaData = statementMoreMetadata.executeQueryInternal(metaDataQuery)) {
for (int i = 1; i <= destColumnMetadataCount; ++i) {
if (rsMoreMetaData.next()) {
String bulkCopyEncryptionType = null;
if (connection.getServerSupportsColumnEncryption()) {
bulkCopyEncryptionType = rsMoreMetaData.getString("encryption_type");
}
// Skip computed columns
if (!rsMoreMetaData.getBoolean("is_computed")) {
destColumnMetadata.put(i, new BulkColumnMetaData(rs.getColumn(i),
rsMoreMetaData.getString("collation_name"), bulkCopyEncryptionType));
}
} else {
destColumnMetadata.put(i, new BulkColumnMetaData(rs.getColumn(i)));
}
}
}
} catch (SQLException e) {
// Unable to retrieve metadata for destination
throw new SQLServerException(SQLServerException.getErrString("R_unableRetrieveColMeta"), e);
} finally {
if (null != rs) {
rs.close();
}
if (null != stmt) {
stmt.close();
}
}
}
/**
* Returns the column metadata for the source (and saves it for later). Retrieving source metadata in
* BulkColumnMetaData object helps to access source metadata from the same place for both ResultSet and File.
*/
private void getSourceMetadata() throws SQLServerException {
if (null == srcColumnMetadata || srcColumnMetadata.isEmpty()) {
srcColumnMetadata = new HashMap<>();
int currentColumn;
if (null != sourceResultSet) {
try {
srcColumnCount = sourceResultSetMetaData.getColumnCount();
for (int i = 1; i <= srcColumnCount; ++i) {
srcColumnMetadata.put(i,
new BulkColumnMetaData(sourceResultSetMetaData.getColumnName(i),
(ResultSetMetaData.columnNoNulls != sourceResultSetMetaData.isNullable(i)),
sourceResultSetMetaData.getPrecision(i), sourceResultSetMetaData.getScale(i),
sourceResultSetMetaData.getColumnType(i), null));
}
} catch (SQLException e) {
// Unable to retrieve meta data for destination
throw new SQLServerException(SQLServerException.getErrString("R_unableRetrieveColMeta"), e);
}
} else if (null != serverBulkData) {
Set columnOrdinals = serverBulkData.getColumnOrdinals();
if (null == columnOrdinals || columnOrdinals.isEmpty()) {
throw new SQLServerException(SQLServerException.getErrString("R_unableRetrieveColMeta"), null);
} else {
srcColumnCount = columnOrdinals.size();
for (Integer columnOrdinal : columnOrdinals) {
currentColumn = columnOrdinal;
srcColumnMetadata.put(currentColumn, new BulkColumnMetaData(
serverBulkData.getColumnName(currentColumn), true,
serverBulkData.getPrecision(currentColumn), serverBulkData.getScale(currentColumn),
serverBulkData.getColumnType(currentColumn),
((serverBulkData instanceof SQLServerBulkCSVFileRecord) ? ((SQLServerBulkCSVFileRecord) serverBulkData)
.getColumnDateTimeFormatter(currentColumn) : null)));
}
}
} else {
// Unable to retrieve meta data for source
throw new SQLServerException(SQLServerException.getErrString("R_unableRetrieveColMeta"), null);
}
}
}
/**
* Oracle 12c database returns precision = 0 for char/varchar data types.
*/
private int validateSourcePrecision(int srcPrecision, int srcJdbcType, int destPrecision) {
if ((1 > srcPrecision) && Util.isCharType(srcJdbcType)) {
srcPrecision = destPrecision;
}
return srcPrecision;
}
/**
* Validates the column mappings
*/
private void validateColumnMappings() throws SQLServerException {
try {
if (columnMappings.isEmpty()) {
// Check that the source schema matches the destination schema
// If the number of columns are different, there is an obvious mismatch.
if (destColumnCount != srcColumnCount) {
throw new SQLServerException(SQLServerException.getErrString("R_schemaMismatch"),
SQLState.COL_NOT_FOUND, DriverError.NOT_SET, null);
}
// Could validate that the data types can be converted, but easier to leave that check for later
// Generate default column mappings
ColumnMapping cm;
for (int i = 1; i <= srcColumnCount; ++i) {
// Only skip identity column mapping if KEEP IDENTITY OPTION is FALSE
if (!(destColumnMetadata.get(i).isIdentity && !copyOptions.isKeepIdentity())) {
cm = new ColumnMapping(i, i);
// The vector destColumnMetadata is indexed from 1 to be consistent with column indexing.
cm.destinationColumnName = destColumnMetadata.get(i).columnName;
columnMappings.add(cm);
}
}
// if no mapping is provided for csv file and metadata is missing for some columns throw error
if (null != serverBulkData) {
Set columnOrdinals = serverBulkData.getColumnOrdinals();
Iterator columnsIterator = columnOrdinals.iterator();
int j = 1;
outerWhileLoop : while (columnsIterator.hasNext()) {
int currentOrdinal = columnsIterator.next();
if (j != currentOrdinal) {
/*
* GitHub issue #1391: attempt to sort the set before throwing an exception, in case the set
* was not sorted.
*/
List sortedList = new ArrayList<>(columnOrdinals);
Collections.sort(sortedList);
columnsIterator = sortedList.iterator();
j = 1;
while (columnsIterator.hasNext()) {
currentOrdinal = columnsIterator.next();
if (j != currentOrdinal) {
MessageFormat form = new MessageFormat(
SQLServerException.getErrString("R_invalidColumn"));
Object[] msgArgs = {currentOrdinal};
throw new SQLServerException(form.format(msgArgs), SQLState.COL_NOT_FOUND,
DriverError.NOT_SET, null);
}
j++;
}
// if the sorted set doesn't throw an error, break out of the outer while loop
break outerWhileLoop;
}
j++;
}
}
} else {
int numMappings = columnMappings.size();
ColumnMapping cm;
// Check that the destination names or ordinals exist
for (int i = 0; i < numMappings; ++i) {
cm = columnMappings.get(i);
// Validate that the destination column name exists if the ordinal is not provided.
if (-1 == cm.destinationColumnOrdinal) {
boolean foundColumn = false;
for (int j = 1; j <= destColumnCount; ++j) {
if (destColumnMetadata.get(j).columnName.equals(cm.destinationColumnName)) {
foundColumn = true;
cm.destinationColumnOrdinal = j;
break;
}
}
if (!foundColumn) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidColumn"));
Object[] msgArgs = {cm.destinationColumnName};
throw new SQLServerException(form.format(msgArgs), SQLState.COL_NOT_FOUND,
DriverError.NOT_SET, null);
}
} else if (0 > cm.destinationColumnOrdinal || destColumnCount < cm.destinationColumnOrdinal) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidColumn"));
Object[] msgArgs = {cm.destinationColumnOrdinal};
throw new SQLServerException(form.format(msgArgs), SQLState.COL_NOT_FOUND, DriverError.NOT_SET,
null);
} else {
cm.destinationColumnName = destColumnMetadata.get(cm.destinationColumnOrdinal).columnName;
}
}
// Check that the required source ordinals are present and within range
for (int i = 0; i < numMappings; ++i) {
cm = columnMappings.get(i);
// Validate that the source column name exists if the ordinal is not provided.
if (-1 == cm.sourceColumnOrdinal) {
boolean foundColumn = false;
if (null != sourceResultSet) {
int columns = sourceResultSetMetaData.getColumnCount();
for (int j = 1; j <= columns; ++j) {
if (sourceResultSetMetaData.getColumnName(j).equals(cm.sourceColumnName)) {
foundColumn = true;
cm.sourceColumnOrdinal = j;
break;
}
}
} else {
Set columnOrdinals = serverBulkData.getColumnOrdinals();
for (Integer currentColumn : columnOrdinals) {
if (serverBulkData.getColumnName(currentColumn).equals(cm.sourceColumnName)) {
foundColumn = true;
cm.sourceColumnOrdinal = currentColumn;
break;
}
}
}
if (!foundColumn) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidColumn"));
Object[] msgArgs = {cm.sourceColumnName};
throw new SQLServerException(form.format(msgArgs), SQLState.COL_NOT_FOUND,
DriverError.NOT_SET, null);
}
} else // Validate that the source column is in range
{
boolean columnOutOfRange = true;
if (null != sourceResultSet) {
int columns = sourceResultSetMetaData.getColumnCount();
if (0 < cm.sourceColumnOrdinal && columns >= cm.sourceColumnOrdinal) {
columnOutOfRange = false;
}
} else {
// check if the ordinal is in SQLServerBulkCSVFileRecord.addColumnMetadata()
if (srcColumnMetadata.containsKey(cm.sourceColumnOrdinal)) {
columnOutOfRange = false;
}
}
if (columnOutOfRange) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidColumn"));
Object[] msgArgs = {cm.sourceColumnOrdinal};
throw new SQLServerException(form.format(msgArgs), SQLState.COL_NOT_FOUND,
DriverError.NOT_SET, null);
}
}
// Remove mappings for identity column if KEEP IDENTITY OPTION is FALSE
if (destColumnMetadata.get(cm.destinationColumnOrdinal).isIdentity
&& !copyOptions.isKeepIdentity()) {
columnMappings.remove(i);
numMappings--;
i--;
}
}
}
} catch (SQLException e) {
// Let the column mapping validations go straight through as a single exception
if ((e instanceof SQLServerException) && (null != e.getSQLState())
&& e.getSQLState().equals(SQLState.COL_NOT_FOUND.getSQLStateCode())) {
throw (SQLServerException) e;
}
// Difficulty retrieving column names from source or destination
throw new SQLServerException(SQLServerException.getErrString("R_unableRetrieveColMeta"), e);
}
// Throw an exception is Column mapping is empty.
// If keep identity option is set to be false and not other mapping is explicitly provided.
if (columnMappings.isEmpty()) {
throw new SQLServerException(null, SQLServerException.getErrString("R_BulkColumnMappingsIsEmpty"), null, 0,
false);
}
}
private void writeNullToTdsWriter(TDSWriter tdsWriter, int srcJdbcType,
boolean isStreaming) throws SQLServerException {
switch (srcJdbcType) {
case microsoft.sql.Types.GUID:
case java.sql.Types.CHAR:
case java.sql.Types.NCHAR:
case java.sql.Types.VARCHAR:
case java.sql.Types.NVARCHAR:
case java.sql.Types.BINARY:
case java.sql.Types.VARBINARY:
case java.sql.Types.LONGVARCHAR:
case java.sql.Types.LONGNVARCHAR:
case java.sql.Types.LONGVARBINARY:
if (isStreaming) {
tdsWriter.writeLong(PLPInputStream.PLP_NULL);
} else {
tdsWriter.writeByte((byte) 0xFF);
tdsWriter.writeByte((byte) 0xFF);
}
return;
case java.sql.Types.BIT:
case java.sql.Types.TINYINT:
case java.sql.Types.SMALLINT:
case java.sql.Types.INTEGER:
case java.sql.Types.BIGINT:
case java.sql.Types.REAL:
case java.sql.Types.DOUBLE:
case java.sql.Types.FLOAT:
case java.sql.Types.DECIMAL:
case java.sql.Types.NUMERIC:
case java.sql.Types.TIMESTAMP:
case java.sql.Types.DATE:
case java.sql.Types.TIME:
case 2013: // java.sql.Types.TIME_WITH_TIMEZONE
case 2014: // java.sql.Types.TIMESTAMP_WITH_TIMEZONE
case microsoft.sql.Types.MONEY:
case microsoft.sql.Types.SMALLMONEY:
case microsoft.sql.Types.DATETIMEOFFSET:
tdsWriter.writeByte((byte) 0x00);
return;
case microsoft.sql.Types.SQL_VARIANT:
tdsWriter.writeInt((byte) 0x00);
return;
default:
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_BulkTypeNotSupported"));
Object[] msgArgs = {JDBCType.of(srcJdbcType).toString().toLowerCase(Locale.ENGLISH)};
SQLServerException.makeFromDriverError(null, null, form.format(msgArgs), null, true);
}
}
private void writeColumnToTdsWriter(TDSWriter tdsWriter, int bulkPrecision, int bulkScale, int bulkJdbcType,
boolean bulkNullable, // should it be destNullable instead?
int srcColOrdinal, int destColOrdinal, boolean isStreaming, Object colValue, Calendar cal) throws SQLServerException {
SSType destSSType = destColumnMetadata.get(destColOrdinal).ssType;
bulkPrecision = validateSourcePrecision(bulkPrecision, bulkJdbcType,
destColumnMetadata.get(destColOrdinal).precision);
CryptoMetadata sourceCryptoMeta = srcColumnMetadata.get(srcColOrdinal).cryptoMeta;
if (((null != destColumnMetadata.get(destColOrdinal).encryptionType)
&& copyOptions.isAllowEncryptedValueModifications())
// if destination is encrypted send varbinary explicitly(needed for unencrypted source)
|| (null != destColumnMetadata.get(destColOrdinal).cryptoMeta)) {
bulkJdbcType = java.sql.Types.VARBINARY;
}
/*
* if source is encrypted and destination is unencrypted, use destination sql type to send since there is no way
* of finding if source is encrypted without accessing the resultset, send destination type if source resultset
* set is of type SQLServer and encryption is enabled
*/
else if (null != sourceCryptoMeta) {
bulkJdbcType = destColumnMetadata.get(destColOrdinal).jdbcType;
bulkScale = destColumnMetadata.get(destColOrdinal).scale;
} else if (null != serverBulkData && connection.getSendTemporalDataTypesAsStringForBulkCopy()) {
/*
* Bulk copy from CSV and destination is not encrypted. In this case, we send the temporal types as varchar
* and SQL Server does the conversion. If destination is encrypted, then temporal types can not be sent as
* varchar.
*/
switch (bulkJdbcType) {
case java.sql.Types.DATE:
case java.sql.Types.TIME:
case java.sql.Types.TIMESTAMP:
case microsoft.sql.Types.DATETIMEOFFSET:
bulkJdbcType = java.sql.Types.VARCHAR;
break;
default:
break;
}
}
try {
// We are sending the data using JDBCType and not using SSType as SQL Server will automatically do the
// conversion.
switch (bulkJdbcType) {
case java.sql.Types.INTEGER:
if (null == colValue) {
writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming);
} else {
if (bulkNullable) {
tdsWriter.writeByte((byte) 0x04);
}
tdsWriter.writeInt((int) colValue);
}
break;
case java.sql.Types.SMALLINT:
if (null == colValue) {
writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming);
} else {
if (bulkNullable) {
tdsWriter.writeByte((byte) 0x02);
}
tdsWriter.writeShort(((Number) colValue).shortValue());
}
break;
case java.sql.Types.BIGINT:
if (null == colValue) {
writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming);
} else {
if (bulkNullable) {
tdsWriter.writeByte((byte) 0x08);
}
tdsWriter.writeLong((long) colValue);
}
break;
case java.sql.Types.BIT:
if (null == colValue) {
writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming);
} else {
if (bulkNullable) {
tdsWriter.writeByte((byte) 0x01);
}
tdsWriter.writeByte((byte) ((Boolean) colValue ? 1 : 0));
}
break;
case java.sql.Types.TINYINT:
if (null == colValue) {
writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming);
} else {
if (bulkNullable) {
tdsWriter.writeByte((byte) 0x01);
}
// TINYINT JDBC type is returned as a short in getObject.
// MYSQL returns TINYINT as an Integer. Convert it to a Number to get the short value.
tdsWriter.writeByte((byte) ((((Number) colValue).shortValue()) & 0xFF));
}
break;
case java.sql.Types.FLOAT:
if (null == colValue) {
writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming);
} else {
if (bulkNullable) {
tdsWriter.writeByte((byte) 0x08);
}
tdsWriter.writeDouble((float) colValue);
}
break;
case java.sql.Types.DOUBLE:
if (null == colValue) {
writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming);
} else {
if (bulkNullable) {
tdsWriter.writeByte((byte) 0x08);
}
tdsWriter.writeDouble((double) colValue);
}
break;
case java.sql.Types.REAL:
if (null == colValue) {
writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming);
} else {
if (bulkNullable) {
tdsWriter.writeByte((byte) 0x04);
}
tdsWriter.writeReal((float) colValue);
}
break;
case microsoft.sql.Types.MONEY:
case microsoft.sql.Types.SMALLMONEY:
if (null == colValue) {
writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming);
} 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 (bulkPrecision < Util.getValueLengthBaseOnJavaType(colValue, JavaType.of(colValue), null,
null, JDBCType.of(bulkJdbcType))) {
MessageFormat form = new MessageFormat(
SQLServerException.getErrString("R_valueOutOfRange"));
Object[] msgArgs = {destSSType};
throw new SQLServerException(form.format(msgArgs), SQLState.DATA_EXCEPTION_LENGTH_MISMATCH,
DriverError.NOT_SET, null);
}
tdsWriter.writeMoney((BigDecimal) colValue, bulkJdbcType);
}
break;
case java.sql.Types.DECIMAL:
case java.sql.Types.NUMERIC:
if (null == colValue) {
writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming);
} 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 (bulkPrecision < Util.getValueLengthBaseOnJavaType(colValue, JavaType.of(colValue), null,
null, JDBCType.of(bulkJdbcType))) {
MessageFormat form = new MessageFormat(
SQLServerException.getErrString("R_valueOutOfRange"));
Object[] msgArgs = {destSSType};
throw new SQLServerException(form.format(msgArgs), SQLState.DATA_EXCEPTION_LENGTH_MISMATCH,
DriverError.NOT_SET, null);
}
/*
* SQL Server allows the insertion of decimal and numeric into a money (and smallmoney) column,
* but Azure DW only accepts money types for money column. To make the code compatible against
* both SQL Server and Azure DW, always send decimal and numeric as money/smallmoney if the
* destination column is money/smallmoney and the source is decimal/numeric.
*/
if (destSSType == SSType.MONEY) {
tdsWriter.writeMoney((BigDecimal) colValue, microsoft.sql.Types.MONEY);
} else if (destSSType == SSType.SMALLMONEY) {
tdsWriter.writeMoney((BigDecimal) colValue, microsoft.sql.Types.SMALLMONEY);
} else {
tdsWriter.writeBigDecimal((BigDecimal) colValue, bulkJdbcType, bulkPrecision, bulkScale);
}
}
break;
case microsoft.sql.Types.GUID:
case java.sql.Types.LONGVARCHAR:
case java.sql.Types.CHAR: // Fixed-length, non-Unicode string data.
case java.sql.Types.VARCHAR: // Variable-length, non-Unicode string data.
if (isStreaming) // PLP
{
// PLP_BODY rule in TDS
// Use ResultSet.getString for non-streaming data and ResultSet.getCharacterStream() for
// streaming data,
// so that if the source data source does not have streaming enabled, the smaller size data will
// still work.
if (null == colValue) {
writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming);
} else {
// Send length as unknown.
tdsWriter.writeLong(PLPInputStream.UNKNOWN_PLP_LEN);
try {
// Read and Send the data as chunks
// VARBINARYMAX --- only when streaming.
Reader reader;
if (colValue instanceof Reader) {
reader = (Reader) colValue;
} else {
reader = new StringReader(colValue.toString());
}
if (unicodeConversionRequired(bulkJdbcType, destSSType)) {
// writeReader is unicode.
tdsWriter.writeReader(reader, DataTypes.UNKNOWN_STREAM_LENGTH, true);
} else {
tdsWriter.writeNonUnicodeReader(reader, DataTypes.UNKNOWN_STREAM_LENGTH,
(SSType.BINARY == destSSType) || (SSType.VARBINARY == destSSType)
|| (SSType.VARBINARYMAX == destSSType)
|| (SSType.IMAGE == destSSType));
}
reader.close();
} catch (IOException e) {
throw new SQLServerException(
SQLServerException.getErrString("R_unableRetrieveSourceData"), e);
}
}
} else {
if (null == colValue) {
writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming);
} else {
String colValueStr;
if (colValue instanceof LocalDateTime) {
colValueStr = ((LocalDateTime) colValue).format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
} else if (colValue instanceof LocalTime) {
colValueStr = ((LocalTime) colValue).format(DateTimeFormatter.ISO_LOCAL_TIME);
} else {
colValueStr = colValue.toString();
}
if (unicodeConversionRequired(bulkJdbcType, destSSType)) {
int stringLength = colValueStr.length();
byte[] typevarlen = new byte[2];
typevarlen[0] = (byte) (2 * stringLength & 0xFF);
typevarlen[1] = (byte) ((2 * stringLength >> 8) & 0xFF);
tdsWriter.writeBytes(typevarlen);
tdsWriter.writeString(colValueStr);
} else {
if ((SSType.BINARY == destSSType) || (SSType.VARBINARY == destSSType)) {
byte[] bytes = null;
try {
bytes = ParameterUtils.hexToBin(colValueStr);
} catch (SQLServerException e) {
throw new SQLServerException(
SQLServerException.getErrString("R_unableRetrieveSourceData"), e);
}
tdsWriter.writeShort((short) bytes.length);
tdsWriter.writeBytes(bytes);
} else {
// converting string into destination collation using Charset
SQLCollation destCollation = destColumnMetadata.get(destColOrdinal).collation;
if (null != destCollation) {
byte[] value = colValueStr.getBytes(
destColumnMetadata.get(destColOrdinal).collation.getCharset());
tdsWriter.writeShort((short) value.length);
tdsWriter.writeBytes(value);
} else {
tdsWriter.writeShort((short) (colValueStr.length()));
tdsWriter.writeBytes(colValueStr.getBytes());
}
}
}
}
}
break;
/*
* The length value associated with these data types is specified within a USHORT. see MS-TDS.pdf page
* 38. However, nchar(n) nvarchar(n) supports n = 1 .. 4000 (see MSDN SQL 2014, SQL 2016 Transact-SQL)
* NVARCHAR/NCHAR/LONGNVARCHAR is not compatible with BINARY/VARBINARY as specified in enum
* UpdaterConversion of DataTypes.java
*/
case java.sql.Types.LONGNVARCHAR:
case java.sql.Types.NCHAR:
case java.sql.Types.NVARCHAR:
if (isStreaming) {
// PLP_BODY rule in TDS
// Use ResultSet.getString for non-streaming data and ResultSet.getNCharacterStream() for
// streaming data,
// so that if the source data source does not have streaming enabled, the smaller size data will
// still work.
if (null == colValue) {
writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming);
} else {
// Send length as unknown.
tdsWriter.writeLong(PLPInputStream.UNKNOWN_PLP_LEN);
try {
// Read and Send the data as chunks.
Reader reader;
if (colValue instanceof Reader) {
reader = (Reader) colValue;
} else {
reader = new StringReader(colValue.toString());
}
// writeReader is unicode.
tdsWriter.writeReader(reader, DataTypes.UNKNOWN_STREAM_LENGTH, true);
reader.close();
} catch (IOException e) {
throw new SQLServerException(
SQLServerException.getErrString("R_unableRetrieveSourceData"), e);
}
}
} else {
if (null == colValue) {
writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming);
} else {
int stringLength = colValue.toString().length();
byte[] typevarlen = new byte[2];
typevarlen[0] = (byte) (2 * stringLength & 0xFF);
typevarlen[1] = (byte) ((2 * stringLength >> 8) & 0xFF);
tdsWriter.writeBytes(typevarlen);
tdsWriter.writeString(colValue.toString());
}
}
break;
case java.sql.Types.LONGVARBINARY:
case java.sql.Types.BINARY:
case java.sql.Types.VARBINARY:
if (isStreaming) // PLP
{
// Check for null separately for streaming and non-streaming data types, there could be source
// data sources who
// does not support streaming data.
if (null == colValue) {
writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming);
} else {
// Send length as unknown.
tdsWriter.writeLong(PLPInputStream.UNKNOWN_PLP_LEN);
try {
// Read and Send the data as chunks
InputStream iStream;
if (colValue instanceof InputStream) {
iStream = (InputStream) colValue;
} else {
if (colValue instanceof byte[]) {
iStream = new ByteArrayInputStream((byte[]) colValue);
} else
iStream = new ByteArrayInputStream(
ParameterUtils.hexToBin(colValue.toString()));
}
// We do not need to check for null values here as it is already checked above.
tdsWriter.writeStream(iStream, DataTypes.UNKNOWN_STREAM_LENGTH, true);
iStream.close();
} catch (IOException e) {
throw new SQLServerException(
SQLServerException.getErrString("R_unableRetrieveSourceData"), e);
}
}
} else // Non-PLP
{
if (null == colValue) {
writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming);
} else {
byte[] srcBytes;
if (colValue instanceof byte[]) {
srcBytes = (byte[]) colValue;
} else {
try {
srcBytes = ParameterUtils.hexToBin(colValue.toString());
} catch (SQLServerException e) {
throw new SQLServerException(
SQLServerException.getErrString("R_unableRetrieveSourceData"), e);
}
}
tdsWriter.writeShort((short) srcBytes.length);
tdsWriter.writeBytes(srcBytes);
}
}
break;
case microsoft.sql.Types.DATETIME:
case microsoft.sql.Types.SMALLDATETIME:
case java.sql.Types.TIMESTAMP:
if (null == colValue) {
writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming);
} else {
switch (destSSType) {
case SMALLDATETIME:
if (bulkNullable)
tdsWriter.writeByte((byte) 0x04);
tdsWriter.writeSmalldatetime(colValue.toString());
break;
case DATETIME:
if (bulkNullable)
tdsWriter.writeByte((byte) 0x08);
if (colValue instanceof java.sql.Timestamp) {
tdsWriter.writeDatetime((java.sql.Timestamp) colValue);
} else {
tdsWriter.writeDatetime(java.sql.Timestamp.valueOf(colValue.toString()));
}
break;
default: // DATETIME2
if (2 >= bulkScale)
tdsWriter.writeByte((byte) 0x06);
else if (4 >= bulkScale)
tdsWriter.writeByte((byte) 0x07);
else
tdsWriter.writeByte((byte) 0x08);
Timestamp ts;
if (colValue instanceof java.sql.Timestamp) {
ts = (Timestamp) colValue;
} else {
ts = Timestamp.valueOf(colValue.toString());
}
tdsWriter.writeTime(ts, bulkScale, cal);
// Send only the date part
tdsWriter.writeDate(ts.getTime(), cal);
}
}
break;
case java.sql.Types.DATE:
if (null == colValue) {
writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming);
} else {
tdsWriter.writeByte((byte) 0x03);
tdsWriter.writeDate(colValue.toString());
}
break;
case java.sql.Types.TIME:
// java.sql.Types.TIME allows maximum of 3 fractional second precision
// SQL Server time(n) allows maximum of 7 fractional second precision, to avoid truncation
// values are read as java.sql.Types.TIMESTAMP if srcJdbcType is java.sql.Types.TIME
if (null == colValue) {
writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming);
} else {
if (2 >= bulkScale)
tdsWriter.writeByte((byte) 0x03);
else if (4 >= bulkScale)
tdsWriter.writeByte((byte) 0x04);
else
tdsWriter.writeByte((byte) 0x05);
if (colValue instanceof String) {
/*
* if colValue is an instance of String, this means the data is coming from a CSV file. Time
* string is expected to come in with this pattern: hh:mm:ss[.nnnnnnn] First, look for the
* '.' character to determine if the String has the optional nanoseconds component. Next,
* create a java.sql.Time instance with the hh:mm:ss part we extracted, then set that time
* as the timestamp's time. Then, add the nanoseconds (optional, 0 if not provided) to the
* timestamp value. Finally, provide the timestamp value to writeTime method.
*/
java.sql.Timestamp ts = new java.sql.Timestamp(0);
int nanos = 0;
int decimalIndex = ((String) colValue).indexOf('.');
if (decimalIndex != -1) {
nanos = Integer.parseInt(((String) colValue).substring(decimalIndex + 1));
colValue = ((String) colValue).substring(0, decimalIndex);
}
ts.setTime(java.sql.Time.valueOf(colValue.toString()).getTime());
ts.setNanos(nanos);
tdsWriter.writeTime(ts, bulkScale);
} else {
tdsWriter.writeTime((java.sql.Timestamp) colValue, bulkScale);
}
}
break;
case 2013: // java.sql.Types.TIME_WITH_TIMEZONE
if (null == colValue) {
writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming);
} else {
if (2 >= bulkScale)
tdsWriter.writeByte((byte) 0x08);
else if (4 >= bulkScale)
tdsWriter.writeByte((byte) 0x09);
else
tdsWriter.writeByte((byte) 0x0A);
tdsWriter.writeOffsetTimeWithTimezone((OffsetTime) colValue, bulkScale);
}
break;
case 2014: // java.sql.Types.TIMESTAMP_WITH_TIMEZONE
if (null == colValue) {
writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming);
} else {
if (2 >= bulkScale)
tdsWriter.writeByte((byte) 0x08);
else if (4 >= bulkScale)
tdsWriter.writeByte((byte) 0x09);
else
tdsWriter.writeByte((byte) 0x0A);
tdsWriter.writeOffsetDateTimeWithTimezone((OffsetDateTime) colValue, bulkScale);
}
break;
case microsoft.sql.Types.DATETIMEOFFSET:
// We can safely cast the result set to a SQLServerResultSet as the DatetimeOffset type is only
// available in the JDBC driver.
if (null == colValue) {
writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming);
} else {
if (2 >= bulkScale)
tdsWriter.writeByte((byte) 0x08);
else if (4 >= bulkScale)
tdsWriter.writeByte((byte) 0x09);
else
tdsWriter.writeByte((byte) 0x0A);
tdsWriter.writeDateTimeOffset(colValue, bulkScale, destSSType);
}
break;
case microsoft.sql.Types.SQL_VARIANT:
boolean isShiloh = (8 >= connection.getServerMajorVersion());
if (isShiloh) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_SQLVariantSupport"));
throw new SQLServerException(null, form.format(new Object[] {}), null, 0, false);
}
writeSqlVariant(tdsWriter, colValue, sourceResultSet, srcColOrdinal, destColOrdinal, bulkJdbcType,
isStreaming);
break;
default:
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_BulkTypeNotSupported"));
Object[] msgArgs = {JDBCType.of(bulkJdbcType).toString().toLowerCase(Locale.ENGLISH)};
SQLServerException.makeFromDriverError(null, null, form.format(msgArgs), null, true);
break;
} // End of switch
} catch (ClassCastException ex) {
if (null == colValue) {
// this should not really happen, since ClassCastException should only happen when colValue is not null.
// just do one more checking here to make sure
throwInvalidArgument("colValue");
} else {
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_errorConvertingValue"));
Object[] msgArgs = {colValue.getClass().getSimpleName(), JDBCType.of(bulkJdbcType)};
throw new SQLServerException(form.format(msgArgs), SQLState.DATA_EXCEPTION_NOT_SPECIFIC,
DriverError.NOT_SET, ex);
}
}
}
/**
* Writes sql_variant data based on the baseType for bulkcopy
*
* @throws SQLServerException
* an exception
*/
private void writeSqlVariant(TDSWriter tdsWriter, Object colValue, ResultSet sourceResultSet, int srcColOrdinal,
int destColOrdinal, int bulkJdbcType, boolean isStreaming) throws SQLServerException {
if (null == colValue) {
writeNullToTdsWriter(tdsWriter, bulkJdbcType, isStreaming);
return;
}
SqlVariant variantType = ((SQLServerResultSet) sourceResultSet).getVariantInternalType(srcColOrdinal);
int baseType = variantType.getBaseType();
byte[] srcBytes;
// for sql variant we normally should return the colvalue for time as time string. but for
// bulkcopy we need it to be a timestamp. so we have to retrieve it again once we are in bulkcopy
// and make sure that the base type is time.
if (TDSType.TIMEN == TDSType.valueOf(baseType)) {
variantType.setIsBaseTypeTimeValue(true);
((SQLServerResultSet) sourceResultSet).setInternalVariantType(srcColOrdinal, variantType);
colValue = ((SQLServerResultSet) sourceResultSet).getObject(srcColOrdinal);
}
switch (TDSType.valueOf(baseType)) {
case INT8:
writeBulkCopySqlVariantHeader(10, TDSType.INT8.byteValue(), (byte) 0, tdsWriter);
tdsWriter.writeLong(Long.valueOf(colValue.toString()));
break;
case INT4:
writeBulkCopySqlVariantHeader(6, TDSType.INT4.byteValue(), (byte) 0, tdsWriter);
tdsWriter.writeInt(Integer.valueOf(colValue.toString()));
break;
case INT2:
writeBulkCopySqlVariantHeader(4, TDSType.INT2.byteValue(), (byte) 0, tdsWriter);
tdsWriter.writeShort(Short.valueOf(colValue.toString()));
break;
case INT1:
writeBulkCopySqlVariantHeader(3, TDSType.INT1.byteValue(), (byte) 0, tdsWriter);
tdsWriter.writeByte(Byte.valueOf(colValue.toString()));
break;
case FLOAT8:
writeBulkCopySqlVariantHeader(10, TDSType.FLOAT8.byteValue(), (byte) 0, tdsWriter);
tdsWriter.writeDouble(Double.valueOf(colValue.toString()));
break;
case FLOAT4:
writeBulkCopySqlVariantHeader(6, TDSType.FLOAT4.byteValue(), (byte) 0, tdsWriter);
tdsWriter.writeReal(Float.valueOf(colValue.toString()));
break;
case MONEY4:
case MONEY8:
// For decimalN we right TDSWriter.BIGDECIMAL_MAX_LENGTH as maximum length = 17
// 17 + 2 for basetype and probBytes + 2 for precision and length = 21 the length of data in header
writeBulkCopySqlVariantHeader(21, TDSType.DECIMALN.byteValue(), (byte) 2, tdsWriter);
tdsWriter.writeByte((byte) 38);
tdsWriter.writeByte((byte) 4);
tdsWriter.writeSqlVariantInternalBigDecimal((BigDecimal) colValue, bulkJdbcType);
break;
case BIT1:
writeBulkCopySqlVariantHeader(3, TDSType.BIT1.byteValue(), (byte) 0, tdsWriter);
tdsWriter.writeByte((byte) (((Boolean) colValue).booleanValue() ? 1 : 0));
break;
case DATEN:
writeBulkCopySqlVariantHeader(5, TDSType.DATEN.byteValue(), (byte) 0, tdsWriter);
tdsWriter.writeDate(colValue.toString());
break;
case TIMEN:
int timeBulkScale = variantType.getScale();
int timeHeaderLength;
if (2 >= timeBulkScale) {
timeHeaderLength = 0x06;
} else if (4 >= timeBulkScale) {
timeHeaderLength = 0x07;
} else {
timeHeaderLength = 0x08;
}
// depending on scale, the header length defers
writeBulkCopySqlVariantHeader(timeHeaderLength, TDSType.TIMEN.byteValue(), (byte) 1, tdsWriter);
tdsWriter.writeByte((byte) timeBulkScale);
tdsWriter.writeTime((java.sql.Timestamp) colValue, timeBulkScale);
break;
case DATETIME4:
// when the type is ambiguous, we write to bigger type
case DATETIME8:
writeBulkCopySqlVariantHeader(10, TDSType.DATETIME8.byteValue(), (byte) 0, tdsWriter);
if (colValue instanceof java.sql.Timestamp) {
tdsWriter.writeDatetime((java.sql.Timestamp) colValue);
} else {
tdsWriter.writeDatetime(java.sql.Timestamp.valueOf(colValue.toString()));
}
break;
case DATETIME2N:
// 1 if probbytes for time
writeBulkCopySqlVariantHeader(10, TDSType.DATETIME2N.byteValue(), (byte) 1, tdsWriter);
tdsWriter.writeByte((byte) 0x03);
String timeStampValue = colValue.toString();
// datetime2 in sql_variant has up to scale 3 support
tdsWriter.writeTime(java.sql.Timestamp.valueOf(timeStampValue), 0x03);
// Send only the date part
tdsWriter.writeDate(timeStampValue.substring(0, timeStampValue.lastIndexOf(' ')));
break;
case BIGCHAR:
int length = colValue.toString().length();
writeBulkCopySqlVariantHeader(9 + length, TDSType.BIGCHAR.byteValue(), (byte) 7, tdsWriter);
tdsWriter.writeCollationForSqlVariant(variantType); // writes collation info and sortID
tdsWriter.writeShort((short) (length));
SQLCollation destCollation = destColumnMetadata.get(destColOrdinal).collation;
if (null != destCollation) {
tdsWriter.writeBytes(colValue.toString()
.getBytes(destColumnMetadata.get(destColOrdinal).collation.getCharset()));
} else {
tdsWriter.writeBytes(colValue.toString().getBytes());
}
break;
case BIGVARCHAR:
length = colValue.toString().length();
writeBulkCopySqlVariantHeader(9 + length, TDSType.BIGVARCHAR.byteValue(), (byte) 7, tdsWriter);
tdsWriter.writeCollationForSqlVariant(variantType); // writes collation info and sortID
tdsWriter.writeShort((short) (length));
destCollation = destColumnMetadata.get(destColOrdinal).collation;
if (null != destCollation) {
tdsWriter.writeBytes(colValue.toString()
.getBytes(destColumnMetadata.get(destColOrdinal).collation.getCharset()));
} else {
tdsWriter.writeBytes(colValue.toString().getBytes());
}
break;
case NCHAR:
length = colValue.toString().length() * 2;
writeBulkCopySqlVariantHeader(9 + length, TDSType.NCHAR.byteValue(), (byte) 7, tdsWriter);
tdsWriter.writeCollationForSqlVariant(variantType); // writes collation info and sortID
int stringLength = colValue.toString().length();
byte[] typevarlen = new byte[2];
typevarlen[0] = (byte) (2 * stringLength & 0xFF);
typevarlen[1] = (byte) ((2 * stringLength >> 8) & 0xFF);
tdsWriter.writeBytes(typevarlen);
tdsWriter.writeString(colValue.toString());
break;
case NVARCHAR:
length = colValue.toString().length() * 2;
writeBulkCopySqlVariantHeader(9 + length, TDSType.NVARCHAR.byteValue(), (byte) 7, tdsWriter);
tdsWriter.writeCollationForSqlVariant(variantType); // writes collation info and sortID
stringLength = colValue.toString().length();
typevarlen = new byte[2];
typevarlen[0] = (byte) (2 * stringLength & 0xFF);
typevarlen[1] = (byte) ((2 * stringLength >> 8) & 0xFF);
tdsWriter.writeBytes(typevarlen);
tdsWriter.writeString(colValue.toString());
break;
case GUID:
length = colValue.toString().length();
writeBulkCopySqlVariantHeader(9 + length, TDSType.BIGCHAR.byteValue(), (byte) 7, tdsWriter);
// since while reading collation from sourceMetaData in GUID we don't read collation, because we are
// reading binary, but in writing it we are using char, so we need to get the collation.
SQLCollation collation = (null != destColumnMetadata.get(srcColOrdinal).collation) ? destColumnMetadata
.get(srcColOrdinal).collation : connection.getDatabaseCollation();
variantType.setCollation(collation);
tdsWriter.writeCollationForSqlVariant(variantType); // writes collation info and sortID
tdsWriter.writeShort((short) (length));
// converting string into destination collation using Charset
destCollation = destColumnMetadata.get(destColOrdinal).collation;
if (null != destCollation) {
tdsWriter.writeBytes(colValue.toString()
.getBytes(destColumnMetadata.get(destColOrdinal).collation.getCharset()));
} else {
tdsWriter.writeBytes(colValue.toString().getBytes());
}
break;
case BIGBINARY:
byte[] b = (byte[]) colValue;
length = b.length;
writeBulkCopySqlVariantHeader(4 + length, TDSType.BIGVARBINARY.byteValue(), (byte) 2, tdsWriter);
tdsWriter.writeShort((short) (variantType.getMaxLength())); // length
if (colValue instanceof byte[]) {
srcBytes = (byte[]) colValue;
} else {
try {
srcBytes = ParameterUtils.hexToBin(colValue.toString());
} catch (SQLServerException e) {
throw new SQLServerException(SQLServerException.getErrString("R_unableRetrieveSourceData"), e);
}
}
tdsWriter.writeBytes(srcBytes);
break;
case BIGVARBINARY:
b = (byte[]) colValue;
length = b.length;
writeBulkCopySqlVariantHeader(4 + length, TDSType.BIGVARBINARY.byteValue(), (byte) 2, tdsWriter);
tdsWriter.writeShort((short) (variantType.getMaxLength())); // length
if (colValue instanceof byte[]) {
srcBytes = (byte[]) colValue;
} else {
try {
srcBytes = ParameterUtils.hexToBin(colValue.toString());
} catch (SQLServerException e) {
throw new SQLServerException(SQLServerException.getErrString("R_unableRetrieveSourceData"), e);
}
}
tdsWriter.writeBytes(srcBytes);
break;
default:
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_BulkTypeNotSupported"));
Object[] msgArgs = {JDBCType.of(bulkJdbcType).toString().toLowerCase(Locale.ENGLISH)};
SQLServerException.makeFromDriverError(null, null, form.format(msgArgs), null, true);
break;
}
}
/**
* Write header for sql_variant
*
* @param length
* length of base type + Basetype + probBytes
* @param tdsType
* @param probBytes
* @param tdsWriter
* @throws SQLServerException
*/
private void writeBulkCopySqlVariantHeader(int length, byte tdsType, byte probBytes,
TDSWriter tdsWriter) throws SQLServerException {
tdsWriter.writeInt(length);
tdsWriter.writeByte(tdsType);
tdsWriter.writeByte(probBytes);
}
private Object readColumnFromResultSet(int srcColOrdinal, int srcJdbcType, boolean isStreaming,
boolean isDestEncrypted) throws SQLServerException {
CryptoMetadata srcCryptoMeta = null;
// if source is encrypted read its baseTypeInfo
if ((sourceResultSet instanceof SQLServerResultSet)
&& (null != (srcCryptoMeta = ((SQLServerResultSet) sourceResultSet).getterGetColumn(srcColOrdinal)
.getCryptoMetadata()))) {
srcJdbcType = srcCryptoMeta.baseTypeInfo.getSSType().getJDBCType().asJavaSqlType();
BulkColumnMetaData temp = srcColumnMetadata.get(srcColOrdinal);
srcColumnMetadata.put(srcColOrdinal, new BulkColumnMetaData(temp, srcCryptoMeta));
}
try {
// We are sending the data using JDBCType and not using SSType as SQL Server will automatically do the
// conversion.
switch (srcJdbcType) {
// For numeric types (like, int, smallint, bit, ...) use getObject() instead of get* methods as get*
// methods
// return 0 if the value is null.
// Change getObject to get* as other data sources may not have similar implementation.
case java.sql.Types.INTEGER:
case java.sql.Types.SMALLINT:
case java.sql.Types.BIGINT:
case java.sql.Types.BIT:
case java.sql.Types.TINYINT:
case java.sql.Types.DOUBLE:
case java.sql.Types.REAL:
case java.sql.Types.FLOAT:
return sourceResultSet.getObject(srcColOrdinal);
case microsoft.sql.Types.MONEY:
case microsoft.sql.Types.SMALLMONEY:
case java.sql.Types.DECIMAL:
case java.sql.Types.NUMERIC:
return sourceResultSet.getBigDecimal(srcColOrdinal);
case microsoft.sql.Types.GUID:
case java.sql.Types.LONGVARCHAR:
case java.sql.Types.CHAR: // Fixed-length, non-Unicode string data.
case java.sql.Types.VARCHAR: // Variable-length, non-Unicode string data.
// PLP if stream type and both the source and destination are not encrypted
// This is because AE does not support streaming types.
// Therefore an encrypted source or destination means the data must not actually be streaming data
if (isStreaming && !isDestEncrypted && (null == srcCryptoMeta)) // PLP
{
// Use ResultSet.getString for non-streaming data and ResultSet.getCharacterStream() for
// streaming data,
// so that if the source data source does not have streaming enabled, the smaller size data will
// still work.
return sourceResultSet.getCharacterStream(srcColOrdinal);
} else // Non-PLP
{
return sourceResultSet.getString(srcColOrdinal);
}
case java.sql.Types.LONGNVARCHAR:
case java.sql.Types.NCHAR:
case java.sql.Types.NVARCHAR:
// PLP if stream type and both the source and destination are not encrypted
// This is because AE does not support streaming types.
// Therefore an encrypted source or destination means the data must not actually be streaming data
if (isStreaming && !isDestEncrypted && (null == srcCryptoMeta)) // PLP
{
// Use ResultSet.getString for non-streaming data and ResultSet.getNCharacterStream() for
// streaming data,
// so that if the source data source does not have streaming enabled, the smaller size data will
// still work.
return sourceResultSet.getNCharacterStream(srcColOrdinal);
} else {
return sourceResultSet.getObject(srcColOrdinal);
}
case java.sql.Types.LONGVARBINARY:
case java.sql.Types.BINARY:
case java.sql.Types.VARBINARY:
// PLP if stream type and both the source and destination are not encrypted
// This is because AE does not support streaming types.
// Therefore an encrypted source or destination means the data must not actually be streaming data
if (isStreaming && !isDestEncrypted && (null == srcCryptoMeta)) // PLP
{
return sourceResultSet.getBinaryStream(srcColOrdinal);
} else // Non-PLP
{
return sourceResultSet.getBytes(srcColOrdinal);
}
case microsoft.sql.Types.DATETIME:
case microsoft.sql.Types.SMALLDATETIME:
case java.sql.Types.TIMESTAMP:
case java.sql.Types.TIME:
// java.sql.Types.TIME allows maximum of 3 fractional second precision
// SQL Server time(n) allows maximum of 7 fractional second precision, to avoid truncation
// values are read as java.sql.Types.TIMESTAMP if srcJdbcType is java.sql.Types.TIME
return sourceResultSet.getTimestamp(srcColOrdinal);
case java.sql.Types.DATE:
return sourceResultSet.getDate(srcColOrdinal);
case microsoft.sql.Types.DATETIMEOFFSET:
return sourceResultSet.getObject(srcColOrdinal, DateTimeOffset.class);
case microsoft.sql.Types.SQL_VARIANT:
return sourceResultSet.getObject(srcColOrdinal);
default:
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_BulkTypeNotSupported"));
Object[] msgArgs = {JDBCType.of(srcJdbcType).toString().toLowerCase(Locale.ENGLISH)};
SQLServerException.makeFromDriverError(null, null, form.format(msgArgs), null, true);
// This return will never be executed, but it is needed as Eclipse complains otherwise.
return null;
}
} catch (SQLException e) {
throw new SQLServerException(SQLServerException.getErrString("R_unableRetrieveSourceData"), e);
}
}
/**
* Reads the given column from the result set current row and writes the data to tdsWriter.
*/
private void writeColumn(TDSWriter tdsWriter, int srcColOrdinal, int destColOrdinal,
Object colValue, Calendar cal) throws SQLServerException {
String destName = destColumnMetadata.get(destColOrdinal).columnName;
int srcPrecision, srcScale, destPrecision, srcJdbcType;
SSType destSSType = null;
boolean isStreaming, srcNullable;
srcPrecision = srcColumnMetadata.get(srcColOrdinal).precision;
srcScale = srcColumnMetadata.get(srcColOrdinal).scale;
srcJdbcType = srcColumnMetadata.get(srcColOrdinal).jdbcType;
srcNullable = srcColumnMetadata.get(srcColOrdinal).isNullable;
destPrecision = destColumnMetadata.get(destColOrdinal).precision;
if ((java.sql.Types.NCHAR == srcJdbcType) || (java.sql.Types.NVARCHAR == srcJdbcType)
|| (java.sql.Types.LONGNVARCHAR == srcJdbcType)) {
isStreaming = (DataTypes.SHORT_VARTYPE_MAX_CHARS < srcPrecision)
|| (DataTypes.SHORT_VARTYPE_MAX_CHARS < destPrecision);
} else {
isStreaming = (DataTypes.SHORT_VARTYPE_MAX_BYTES < srcPrecision)
|| (DataTypes.SHORT_VARTYPE_MAX_BYTES < destPrecision);
}
CryptoMetadata destCryptoMeta = destColumnMetadata.get(destColOrdinal).cryptoMeta;
if (null != destCryptoMeta) {
destSSType = destCryptoMeta.baseTypeInfo.getSSType();
}
// Get the cell from the source result set if we are copying from result set.
// If we are copying from a bulk reader colValue will be passed as the argument.
if (null != sourceResultSet) {
colValue = readColumnFromResultSet(srcColOrdinal, srcJdbcType, isStreaming, (null != destCryptoMeta));
validateStringBinaryLengths(colValue, srcColOrdinal, destColOrdinal);
// if AllowEncryptedValueModifications is set send varbinary read from source without checking type
// conversion
if (!((copyOptions.isAllowEncryptedValueModifications())
// normalizationCheck() will be called for encrypted columns so skip this validation
|| ((null != destCryptoMeta) && (null != colValue)))) {
validateDataTypeConversions(srcColOrdinal, destColOrdinal);
}
}
// If we are using ISQLBulkRecord and the data we are passing is char type, we need to check the source and dest
// precision
else if (null != serverBulkData && (null == destCryptoMeta)) {
validateStringBinaryLengths(colValue, srcColOrdinal, destColOrdinal);
} else if ((null != serverBulkData) && (null != destCryptoMeta)) {
// From CSV to encrypted column. Convert to respective object.
if ((java.sql.Types.DATE == srcJdbcType) || (java.sql.Types.TIME == srcJdbcType)
|| (java.sql.Types.TIMESTAMP == srcJdbcType) || (microsoft.sql.Types.DATETIMEOFFSET == srcJdbcType)
|| (2013 == srcJdbcType) || (2014 == srcJdbcType)) {
colValue = getTemporalObjectFromCSV(colValue, srcJdbcType, srcColOrdinal);
} else if ((java.sql.Types.NUMERIC == srcJdbcType) || (java.sql.Types.DECIMAL == srcJdbcType)) {
int baseDestPrecision = destCryptoMeta.baseTypeInfo.getPrecision();
int baseDestScale = destCryptoMeta.baseTypeInfo.getScale();
if ((srcScale != baseDestScale) || (srcPrecision != baseDestPrecision)) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_InvalidDataForAE"));
String src = JDBCType.of(srcJdbcType) + "(" + srcPrecision + "," + srcScale + ")";
String dest = destSSType + "(" + baseDestPrecision + "," + baseDestScale + ")";
Object[] msgArgs = {src, dest, destName};
throw new SQLServerException(this, form.format(msgArgs), null, 0, false);
}
}
}
CryptoMetadata srcCryptoMeta = srcColumnMetadata.get(srcColOrdinal).cryptoMeta;
// If destination is encrypted column, transparently encrypt the data
if ((null != destCryptoMeta) && (null != colValue)) {
JDBCType baseSrcJdbcType = (null != srcCryptoMeta) ? srcColumnMetadata
.get(srcColOrdinal).cryptoMeta.baseTypeInfo.getSSType().getJDBCType() : JDBCType.of(srcJdbcType);
if (JDBCType.TIMESTAMP == baseSrcJdbcType) {
if (SSType.DATETIME == destSSType) {
baseSrcJdbcType = JDBCType.DATETIME;
} else if (SSType.SMALLDATETIME == destSSType) {
baseSrcJdbcType = JDBCType.SMALLDATETIME;
}
}
if (!((SSType.MONEY == destSSType && JDBCType.DECIMAL == baseSrcJdbcType)
|| (SSType.SMALLMONEY == destSSType && JDBCType.DECIMAL == baseSrcJdbcType)
|| (SSType.GUID == destSSType && JDBCType.CHAR == baseSrcJdbcType))) {
// check for bulkcopy from other than SQLServer, for instance for MYSQL, if anykind of chartype pass
if (!(Util.isCharType(destSSType) && Util.isCharType(srcJdbcType))
&& !(sourceResultSet instanceof SQLServerResultSet))
// check for normalization of AE data types
if (!baseSrcJdbcType.normalizationCheck(destSSType)) {
MessageFormat form = new MessageFormat(
SQLServerException.getErrString("R_unsupportedConversionAE"));
Object[] msgArgs = {baseSrcJdbcType, destSSType};
throw new SQLServerException(this, form.format(msgArgs), null, 0, false);
}
}
// if source is encrypted and temporal, call IOBuffer functions to encrypt
if ((baseSrcJdbcType == JDBCType.DATE) || (baseSrcJdbcType == JDBCType.TIMESTAMP)
|| (baseSrcJdbcType == JDBCType.TIME) || (baseSrcJdbcType == JDBCType.DATETIMEOFFSET)
|| (baseSrcJdbcType == JDBCType.DATETIME) || (baseSrcJdbcType == JDBCType.SMALLDATETIME)) {
colValue = getEncryptedTemporalBytes(tdsWriter, baseSrcJdbcType, colValue,
destCryptoMeta.baseTypeInfo.getScale());
} else {
TypeInfo destTypeInfo = destCryptoMeta.getBaseTypeInfo();
JDBCType destJdbcType = destTypeInfo.getSSType().getJDBCType();
/*
* the following if checks that no casting exception would be thrown in the normalizedValue() method
* below a SQLServerException is then thrown before the ClassCastException could occur an example of how
* this situation could arise would be if the application creates encrypted source and destination
* tables the result set used to read the source would have AE disabled (causing colValue to be
* varbinary) AE would be enabled on the connection used to complete the bulkCopy operation
*/
if ((!Util.isBinaryType(destJdbcType.getIntValue())) && (colValue instanceof byte[])) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_InvalidDataForAE"));
Object[] msgArgs = {baseSrcJdbcType, destJdbcType, destName};
throw new SQLServerException(this, form.format(msgArgs), null, 0, false);
}
// normalize the values before encrypting them
colValue = SQLServerSecurityUtility.encryptWithKey(normalizedValue(destJdbcType, colValue,
baseSrcJdbcType, destTypeInfo.getPrecision(), destTypeInfo.getScale(), destName),
destCryptoMeta, connection, null);
}
}
writeColumnToTdsWriter(tdsWriter, srcPrecision, srcScale, srcJdbcType, srcNullable, srcColOrdinal,
destColOrdinal, isStreaming, colValue, cal);
}
/**
* Returns the temporal object from CSV This method is called against jdbc41, but it require jdbc42 to work
* therefore, we will throw exception.
*
* @param valueStrUntrimmed
* valueStrUntrimmed
* @param srcJdbcType
* srcJdbcType
* @param srcColOrdinal
* srcColOrdinal
* @param dateTimeFormatter
* dateTimeFormatter
* @return temporal object
* @throws SQLServerException
* if parsing error
*/
protected Object getTemporalObjectFromCSVWithFormatter(String valueStrUntrimmed, int srcJdbcType, int srcColOrdinal,
DateTimeFormatter dateTimeFormatter) throws SQLServerException {
try {
TemporalAccessor ta = dateTimeFormatter.parse(valueStrUntrimmed);
int taHour, taMin, taSec, taYear, taMonth, taDay, taNano, taOffsetSec;
taHour = taMin = taSec = taYear = taMonth = taDay = taNano = taOffsetSec = 0;
if (ta.isSupported(ChronoField.NANO_OF_SECOND))
taNano = ta.get(ChronoField.NANO_OF_SECOND);
if (ta.isSupported(ChronoField.OFFSET_SECONDS))
taOffsetSec = ta.get(ChronoField.OFFSET_SECONDS);
if (ta.isSupported(ChronoField.HOUR_OF_DAY))
taHour = ta.get(ChronoField.HOUR_OF_DAY);
if (ta.isSupported(ChronoField.MINUTE_OF_HOUR))
taMin = ta.get(ChronoField.MINUTE_OF_HOUR);
if (ta.isSupported(ChronoField.SECOND_OF_MINUTE))
taSec = ta.get(ChronoField.SECOND_OF_MINUTE);
if (ta.isSupported(ChronoField.DAY_OF_MONTH))
taDay = ta.get(ChronoField.DAY_OF_MONTH);
if (ta.isSupported(ChronoField.MONTH_OF_YEAR))
taMonth = ta.get(ChronoField.MONTH_OF_YEAR);
if (ta.isSupported(ChronoField.YEAR))
taYear = ta.get(ChronoField.YEAR);
Calendar cal = new GregorianCalendar(new SimpleTimeZone(taOffsetSec * 1000, ""));
cal.clear();
cal.set(Calendar.HOUR_OF_DAY, taHour);
cal.set(Calendar.MINUTE, taMin);
cal.set(Calendar.SECOND, taSec);
cal.set(Calendar.DATE, taDay);
cal.set(Calendar.MONTH, taMonth - 1);
cal.set(Calendar.YEAR, taYear);
int fractionalSecondsLength = Integer.toString(taNano).length();
for (int i = 0; i < (9 - fractionalSecondsLength); i++)
taNano *= 10;
Timestamp ts = new Timestamp(cal.getTimeInMillis());
ts.setNanos(taNano);
switch (srcJdbcType) {
case java.sql.Types.TIMESTAMP:
return ts;
case java.sql.Types.TIME:
// Time is returned as Timestamp to preserve nano seconds.
cal.set(connection.baseYear(), Calendar.JANUARY, 01);
ts = new java.sql.Timestamp(cal.getTimeInMillis());
ts.setNanos(taNano);
return new java.sql.Timestamp(ts.getTime());
case java.sql.Types.DATE:
return new java.sql.Date(ts.getTime());
case microsoft.sql.Types.DATETIMEOFFSET:
return DateTimeOffset.valueOf(ts, taOffsetSec / 60);
default:
return valueStrUntrimmed;
}
} catch (DateTimeException | ArithmeticException e) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_ParsingError"));
Object[] msgArgs = {JDBCType.of(srcJdbcType)};
throw new SQLServerException(this, form.format(msgArgs), null, 0, false);
}
}
private Object getTemporalObjectFromCSV(Object value, int srcJdbcType,
int srcColOrdinal) throws SQLServerException {
// TIME_WITH_TIMEZONE and TIMESTAMP_WITH_TIMEZONE are not supported with encrypted columns.
if (2013 == srcJdbcType) {
MessageFormat form1 = new MessageFormat(SQLServerException.getErrString("R_UnsupportedDataTypeAE"));
Object[] msgArgs1 = {"TIME_WITH_TIMEZONE"};
throw new SQLServerException(this, form1.format(msgArgs1), null, 0, false);
} else if (2014 == srcJdbcType) {
MessageFormat form2 = new MessageFormat(SQLServerException.getErrString("R_UnsupportedDataTypeAE"));
Object[] msgArgs2 = {"TIMESTAMP_WITH_TIMEZONE"};
throw new SQLServerException(this, form2.format(msgArgs2), null, 0, false);
}
String valueStr = null;
String valueStrUntrimmed = null;
if (null != value && value instanceof String) {
valueStrUntrimmed = (String) value;
valueStr = valueStrUntrimmed.trim();
}
// Handle null case
if (null == valueStr) {
switch (srcJdbcType) {
case java.sql.Types.TIMESTAMP:
case java.sql.Types.TIME:
case java.sql.Types.DATE:
case microsoft.sql.Types.DATETIMEOFFSET:
return null;
default:
break;
}
}
// If we are here value is non-null.
Calendar cal;
// Get the temporal values from the formatter
DateTimeFormatter dateTimeFormatter = srcColumnMetadata.get(srcColOrdinal).dateTimeFormatter;
if (null != dateTimeFormatter) {
return getTemporalObjectFromCSVWithFormatter(valueStrUntrimmed, srcJdbcType, srcColOrdinal,
dateTimeFormatter);
}
// If we are here that means datetimeformatter is not present. Only default format is supported in this case.
try {
switch (srcJdbcType) {
case java.sql.Types.TIMESTAMP:
// For CSV, value will be of String type.
return Timestamp.valueOf(valueStr);
case java.sql.Types.TIME: {
String time = connection.baseYear() + "-01-01 " + valueStr;
return java.sql.Timestamp.valueOf(time);
}
case java.sql.Types.DATE:
return java.sql.Date.valueOf(valueStr);
case microsoft.sql.Types.DATETIMEOFFSET:
int endIndx = valueStr.indexOf('-', 0);
int year = Integer.parseInt(valueStr.substring(0, endIndx));
int startIndx = ++endIndx; // skip the -
endIndx = valueStr.indexOf('-', startIndx);
int month = Integer.parseInt(valueStr.substring(startIndx, endIndx));
startIndx = ++endIndx; // skip the -
endIndx = valueStr.indexOf(' ', startIndx);
int day = Integer.parseInt(valueStr.substring(startIndx, endIndx));
startIndx = ++endIndx; // skip the space
endIndx = valueStr.indexOf(':', startIndx);
int hour = Integer.parseInt(valueStr.substring(startIndx, endIndx));
startIndx = ++endIndx; // skip the :
endIndx = valueStr.indexOf(':', startIndx);
int minute = Integer.parseInt(valueStr.substring(startIndx, endIndx));
startIndx = ++endIndx; // skip the :
endIndx = valueStr.indexOf('.', startIndx);
int seconds, offsethour, offsetMinute, totalOffset = 0, fractionalSeconds = 0;
boolean isNegativeOffset = false;
boolean hasTimeZone = false;
int fractionalSecondsLength = 0;
if (-1 != endIndx) // has fractional seconds, has a '.'
{
seconds = Integer.parseInt(valueStr.substring(startIndx, endIndx));
startIndx = ++endIndx; // skip the .
endIndx = valueStr.indexOf(' ', startIndx);
if (-1 != endIndx) // has time zone
{
fractionalSeconds = Integer.parseInt(valueStr.substring(startIndx, endIndx));
fractionalSecondsLength = endIndx - startIndx;
hasTimeZone = true;
} else // no timezone
{
fractionalSeconds = Integer.parseInt(valueStr.substring(startIndx));
fractionalSecondsLength = valueStr.length() - startIndx;
}
} else {
endIndx = valueStr.indexOf(' ', startIndx);
if (-1 != endIndx) {
hasTimeZone = true;
seconds = Integer.parseInt(valueStr.substring(startIndx, endIndx));
} else {
seconds = Integer.parseInt(valueStr.substring(startIndx));
++endIndx; // skip the space
}
}
if (hasTimeZone) {
startIndx = ++endIndx; // skip the space
if ('+' == valueStr.charAt(startIndx))
startIndx++; // skip +
else if ('-' == valueStr.charAt(startIndx)) {
isNegativeOffset = true;
startIndx++; // skip -
}
endIndx = valueStr.indexOf(':', startIndx);
offsethour = Integer.parseInt(valueStr.substring(startIndx, endIndx));
startIndx = ++endIndx; // skip :
offsetMinute = Integer.parseInt(valueStr.substring(startIndx));
totalOffset = offsethour * 60 + offsetMinute;
if (isNegativeOffset)
totalOffset = -totalOffset;
}
cal = new GregorianCalendar(new SimpleTimeZone(totalOffset * 60 * 1000, ""), Locale.US);
cal.clear();
cal.set(Calendar.HOUR_OF_DAY, hour);
cal.set(Calendar.MINUTE, minute);
cal.set(Calendar.SECOND, seconds);
cal.set(Calendar.DATE, day);
cal.set(Calendar.MONTH, month - 1);
cal.set(Calendar.YEAR, year);
for (int i = 0; i < (9 - fractionalSecondsLength); i++)
fractionalSeconds *= 10;
Timestamp ts = new Timestamp(cal.getTimeInMillis());
ts.setNanos(fractionalSeconds);
return microsoft.sql.DateTimeOffset.valueOf(ts, totalOffset);
default:
break;
}
} catch (IndexOutOfBoundsException | IllegalArgumentException e) {
// IllegalArgumentsException includes NumberFormatException
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_ParsingError"));
Object[] msgArgs = {JDBCType.of(srcJdbcType)};
throw new SQLServerException(this, form.format(msgArgs), null, 0, false);
}
// unreachable code. Need to do to compile from Eclipse.
return value;
}
private byte[] getEncryptedTemporalBytes(TDSWriter tdsWriter, JDBCType srcTemporalJdbcType, Object colValue,
int scale) throws SQLServerException {
long utcMillis;
GregorianCalendar calendar;
switch (srcTemporalJdbcType) {
case DATE:
calendar = new GregorianCalendar(java.util.TimeZone.getDefault(), java.util.Locale.US);
calendar.setLenient(true);
calendar.clear();
calendar.setTimeInMillis(((Date) colValue).getTime());
return tdsWriter.writeEncryptedScaledTemporal(calendar, 0, // subsecond nanos (none for a date value)
0, // scale (dates are not scaled)
SSType.DATE, (short) 0, null);
case TIME:
calendar = new GregorianCalendar(java.util.TimeZone.getDefault(), java.util.Locale.US);
calendar.setLenient(true);
calendar.clear();
utcMillis = ((java.sql.Timestamp) colValue).getTime();
calendar.setTimeInMillis(utcMillis);
int subSecondNanos;
if (colValue instanceof java.sql.Timestamp) {
subSecondNanos = ((java.sql.Timestamp) colValue).getNanos();
} else {
subSecondNanos = Nanos.PER_MILLISECOND * (int) (utcMillis % 1000);
if (subSecondNanos < 0)
subSecondNanos += Nanos.PER_SECOND;
}
return tdsWriter.writeEncryptedScaledTemporal(calendar, subSecondNanos, scale, SSType.TIME, (short) 0,
null);
case TIMESTAMP:
calendar = new GregorianCalendar(java.util.TimeZone.getDefault(), java.util.Locale.US);
calendar.setLenient(true);
calendar.clear();
utcMillis = ((java.sql.Timestamp) colValue).getTime();
calendar.setTimeInMillis(utcMillis);
subSecondNanos = ((java.sql.Timestamp) colValue).getNanos();
return tdsWriter.writeEncryptedScaledTemporal(calendar, subSecondNanos, scale, SSType.DATETIME2,
(short) 0, null);
case DATETIME:
case SMALLDATETIME:
calendar = new GregorianCalendar(java.util.TimeZone.getDefault(), java.util.Locale.US);
calendar.setLenient(true);
calendar.clear();
utcMillis = ((java.sql.Timestamp) colValue).getTime();
calendar.setTimeInMillis(utcMillis);
subSecondNanos = ((java.sql.Timestamp) colValue).getNanos();
return tdsWriter.getEncryptedDateTimeAsBytes(calendar, subSecondNanos, srcTemporalJdbcType, null);
case DATETIMEOFFSET:
microsoft.sql.DateTimeOffset dtoValue = (microsoft.sql.DateTimeOffset) colValue;
utcMillis = dtoValue.getTimestamp().getTime();
subSecondNanos = dtoValue.getTimestamp().getNanos();
int minutesOffset = dtoValue.getMinutesOffset();
calendar = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
calendar.setLenient(true);
calendar.clear();
calendar.setTimeInMillis(utcMillis);
return tdsWriter.writeEncryptedScaledTemporal(calendar, subSecondNanos, scale, SSType.DATETIMEOFFSET,
(short) minutesOffset, null);
default:
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_UnsupportedDataTypeAE"));
Object[] msgArgs = {srcTemporalJdbcType};
throw new SQLServerException(this, form.format(msgArgs), null, 0, false);
}
}
private byte[] normalizedValue(JDBCType destJdbcType, Object value, JDBCType srcJdbcType, int destPrecision,
int destScale, String destName) throws SQLServerException {
Long longValue = null;
byte[] byteValue = null;
int srcDataPrecision, srcDataScale;
try {
switch (destJdbcType) {
case BIT:
longValue = (long) ((Boolean) value ? 1 : 0);
return ByteBuffer.allocate(Long.SIZE / Byte.SIZE).order(ByteOrder.LITTLE_ENDIAN).putLong(longValue)
.array();
case TINYINT:
case SMALLINT:
switch (srcJdbcType) {
case BIT:
longValue = (long) ((Boolean) value ? 1 : 0);
break;
default:
if (value instanceof Integer) {
int intValue = (int) value;
short shortValue = (short) intValue;
longValue = (long) shortValue;
} else
longValue = (long) (short) value;
}
return ByteBuffer.allocate(Long.SIZE / Byte.SIZE).order(ByteOrder.LITTLE_ENDIAN).putLong(longValue)
.array();
case INTEGER:
switch (srcJdbcType) {
case BIT:
longValue = (long) ((Boolean) value ? 1 : 0);
break;
case TINYINT:
case SMALLINT:
longValue = (long) (short) value;
break;
default:
longValue = Long.valueOf((Integer) value);
}
return ByteBuffer.allocate(Long.SIZE / Byte.SIZE).order(ByteOrder.LITTLE_ENDIAN).putLong(longValue)
.array();
case BIGINT:
switch (srcJdbcType) {
case BIT:
longValue = (long) ((Boolean) value ? 1 : 0);
break;
case TINYINT:
case SMALLINT:
longValue = (long) (short) value;
break;
case INTEGER:
longValue = Long.valueOf((Integer) value);
break;
default:
longValue = (long) value;
}
return ByteBuffer.allocate(Long.SIZE / Byte.SIZE).order(ByteOrder.LITTLE_ENDIAN).putLong(longValue)
.array();
case BINARY:
case VARBINARY:
case LONGVARBINARY:
byte[] byteArrayValue;
if (value instanceof String) {
byteArrayValue = ParameterUtils.hexToBin((String) value);
} else {
byteArrayValue = (byte[]) value;
}
if (byteArrayValue.length > destPrecision) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_InvalidDataForAE"));
Object[] msgArgs = {srcJdbcType, destJdbcType, destName};
throw new SQLServerException(this, form.format(msgArgs), null, 0, false);
}
return byteArrayValue;
case GUID:
return Util.asGuidByteArray(UUID.fromString((String) value));
case CHAR:
case VARCHAR:
case LONGVARCHAR:
// Throw exception if length sent in column metadata is smaller than actual data
if (((String) value).length() > destPrecision) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_InvalidDataForAE"));
Object[] msgArgs = {srcJdbcType, destJdbcType, destName};
throw new SQLServerException(this, form.format(msgArgs), null, 0, false);
}
return ((String) value).getBytes(UTF_8);
case NCHAR:
case NVARCHAR:
case LONGNVARCHAR:
// Throw exception if length sent in column metadata is smaller than actual data
if (((String) value).length() > destPrecision) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_InvalidDataForAE"));
Object[] msgArgs = {srcJdbcType, destJdbcType, destName};
throw new SQLServerException(this, form.format(msgArgs), null, 0, false);
}
return ((String) value).getBytes(UTF_16LE);
case REAL:
Float floatValue = (value instanceof String) ? Float.parseFloat((String) value) : (Float) value;
return ByteBuffer.allocate((Float.SIZE / Byte.SIZE)).order(ByteOrder.LITTLE_ENDIAN)
.putFloat(floatValue).array();
case FLOAT:
case DOUBLE:
Double doubleValue = (value instanceof String) ? Double.parseDouble((String) value)
: (Double) value;
return ByteBuffer.allocate((Double.SIZE / Byte.SIZE)).order(ByteOrder.LITTLE_ENDIAN)
.putDouble(doubleValue).array();
case NUMERIC:
case DECIMAL:
srcDataScale = ((BigDecimal) value).scale();
srcDataPrecision = ((BigDecimal) value).precision();
BigDecimal bigDataValue = (BigDecimal) value;
if ((srcDataPrecision > destPrecision) || (srcDataScale > destScale)) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_InvalidDataForAE"));
Object[] msgArgs = {srcJdbcType, destJdbcType, destName};
throw new SQLServerException(this, form.format(msgArgs), null, 0, false);
} else if (srcDataScale < destScale)
// update the scale of source data based on the metadata for scale sent early
bigDataValue = bigDataValue.setScale(destScale);
byteValue = DDC.convertBigDecimalToBytes(bigDataValue, bigDataValue.scale());
byte[] decimalbyteValue = new byte[16];
// removing the precision and scale information from the decimalToByte array
System.arraycopy(byteValue, 2, decimalbyteValue, 0, byteValue.length - 2);
return decimalbyteValue;
case SMALLMONEY:
case MONEY:
// 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, destJdbcType);
// Get the total number of digits after the multiplication. Scale is hardcoded to 4. This is needed
// to get the proper rounding.
int digitCount = (bdValue.precision() - bdValue.scale()) + 4;
long moneyVal = ((BigDecimal) value).multiply(new BigDecimal(10000),
new java.math.MathContext(digitCount, java.math.RoundingMode.HALF_UP)).longValue();
ByteBuffer bbuf = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN);
bbuf.putInt((int) (moneyVal >> 32)).array();
bbuf.putInt((int) moneyVal).array();
return bbuf.array();
default:
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_UnsupportedDataTypeAE"));
Object[] msgArgs = {destJdbcType};
throw new SQLServerException(this, form.format(msgArgs), null, 0, false);
}
}
// we don't want to throw R_errorConvertingValue error as it might expose decrypted data if source was encrypted
catch (NumberFormatException ex) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_InvalidDataForAE"));
Object[] msgArgs = {srcJdbcType, destJdbcType, destName};
throw new SQLServerException(this, form.format(msgArgs), null, 0, false);
} catch (IllegalArgumentException ex) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_InvalidDataForAE"));
Object[] msgArgs = {srcJdbcType, destJdbcType, destName};
throw new SQLServerException(this, form.format(msgArgs), null, 0, false);
} catch (ClassCastException ex) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_InvalidDataForAE"));
Object[] msgArgs = {srcJdbcType, destJdbcType, destName};
throw new SQLServerException(this, form.format(msgArgs), null, 0, false);
}
}
private boolean goToNextRow() throws SQLServerException {
try {
if (null != sourceResultSet) {
return sourceResultSet.next();
} else {
return serverBulkData.next();
}
} catch (SQLException e) {
throw new SQLServerException(SQLServerException.getErrString("R_unableRetrieveSourceData"), e);
}
}
/**
* Writes data for a batch of rows to the TDSWriter object. Writes the following part in the BulkLoadBCP stream
* (https://msdn.microsoft.com/en-us/library/dd340549.aspx) ...
*/
private boolean writeBatchData(TDSWriter tdsWriter, TDSCommand command,
boolean insertRowByRow) throws SQLServerException {
int batchsize = copyOptions.getBatchSize();
int row = 0;
while (true) {
// Default batchsize is 0 - means all rows are sent in one batch. In this case we will return
// when all rows in the resultset are processed. If batchsize is not zero, we will return when one batch of
// rows are processed.
if (0 != batchsize && row >= batchsize)
return true;
// No more data available, return false so we do not execute any more batches.
if (!goToNextRow())
return false;
if (insertRowByRow) {
// read response gotten from goToNextRow()
((SQLServerResultSet) sourceResultSet).getTDSReader().readPacket();
tdsWriter = sendBulkCopyCommand(command);
}
// Write row header for each row.
tdsWriter.writeByte((byte) TDS.TDS_ROW);
// Copying from a resultset.
if (null != sourceResultSet) {
// Loop for each destination column. The mappings is a many to one mapping
// where multiple source columns can be mapped to one destination column.
for (ColumnMapping columnMapping : columnMappings) {
writeColumn(tdsWriter, columnMapping.sourceColumnOrdinal, columnMapping.destinationColumnOrdinal, null,
null // cell
// value is
// retrieved
// inside
// writeRowData()
// method.
);
}
}
// Copy from a file.
else {
// Get all the column values of the current row.
Object[] rowObjects;
Parameter[] params = null;
try {
rowObjects = serverBulkData.getRowData();
if (serverBulkData instanceof SQLServerBulkBatchInsertRecord) {
params = ((SQLServerBulkBatchInsertRecord) serverBulkData).batchParam.get(row);
}
} catch (Exception ex) {
// if no more data available to retrive
throw new SQLServerException(SQLServerException.getErrString("R_unableRetrieveSourceData"), ex);
}
for (ColumnMapping columnMapping : columnMappings) {
Object rowObject = rowObjects[columnMapping.sourceColumnOrdinal - 1];
Calendar cal = null;
if (rowObject instanceof Timestamp && params != null) {
cal = params[columnMapping.sourceColumnOrdinal - 1].getInputDTV().getCalendar();
}
// If the SQLServerBulkCSVRecord does not have metadata for columns, it returns strings in the
// object array.
// COnvert the strings using destination table types.
writeColumn(tdsWriter, columnMapping.sourceColumnOrdinal, columnMapping.destinationColumnOrdinal,
rowObject, cal);
}
}
row++;
if (insertRowByRow) {
writePacketDataDone(tdsWriter);
tdsWriter.setCryptoMetaData(null);
// Send to the server and read response.
TDSParser.parse(command.startResponse(), command.getLogContext());
}
}
}
void setStmtColumnEncriptionSetting(SQLServerStatementColumnEncryptionSetting stmtColumnEncriptionSetting) {
this.stmtColumnEncriptionSetting = stmtColumnEncriptionSetting;
}
void setDestinationTableMetadata(SQLServerResultSet rs) {
destinationTableMetadata = rs;
}
/**
* For the case when source database stores unicode data in CHAR/VARCHAR and destination column is NCHAR/NVARCHAR.
*
* @param jdbcType
* @param ssType
* @return whether conversion to unicode is required.
*/
private boolean unicodeConversionRequired(int jdbcType, SSType ssType) {
return ((java.sql.Types.CHAR == jdbcType || java.sql.Types.VARCHAR == jdbcType
|| java.sql.Types.LONGNVARCHAR == jdbcType)
&& (SSType.NCHAR == ssType || SSType.NVARCHAR == ssType || SSType.NVARCHARMAX == ssType));
}
}