From b727aa09b845be78e927ce95baa7b88503b63d62 Mon Sep 17 00:00:00 2001 From: Peter Bae Date: Fri, 20 Apr 2018 10:54:44 -0700 Subject: [PATCH 01/32] Use Bulk Copy API for batch insert operation --- .../microsoft/sqlserver/jdbc/Parameter.java | 4 + .../jdbc/SQLServerBulkBatchInsertRecord.java | 599 ++++++++++++++++++ .../sqlserver/jdbc/SQLServerConnection.java | 35 + .../jdbc/SQLServerPreparedStatement.java | 363 ++++++++++- .../sqlserver/jdbc/SQLServerStatement.java | 22 + 5 files changed, 1022 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/Parameter.java b/src/main/java/com/microsoft/sqlserver/jdbc/Parameter.java index b614ad5fe..2a9c757d0 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/Parameter.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/Parameter.java @@ -415,6 +415,10 @@ Object getValue(JDBCType jdbcType, // statement level), cryptoMeta would be null. return getterDTV.getValue(jdbcType, outScale, getterArgs, cal, typeInfo, cryptoMeta, tdsReader); } + + Object getSetterValue() { + return setterDTV.getSetterValue(); + } int getInt(TDSReader tdsReader) throws SQLServerException { Integer value = (Integer) getValue(JDBCType.INTEGER, null, null, tdsReader); diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java new file mode 100644 index 000000000..f987ad821 --- /dev/null +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java @@ -0,0 +1,599 @@ +/* + * 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 java.math.BigDecimal; +import java.math.RoundingMode; +import java.sql.Types; +import java.text.DecimalFormat; +import java.text.MessageFormat; +import java.time.OffsetDateTime; +import java.time.OffsetTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +/** + * A simple implementation of the ISQLServerBulkRecord interface that can be used to read in the basic Java data types from an ArrayList of + * Parameters that were provided by pstmt/cstmt. + */ +public class SQLServerBulkBatchInsertRecord implements ISQLServerBulkRecord, java.lang.AutoCloseable { + /* + * Class to represent the column metadata + */ + private class ColumnMetadata { + String columnName; + int columnType; + int precision; + int scale; + DateTimeFormatter dateTimeFormatter = null; + + ColumnMetadata(String name, + int type, + int precision, + int scale, + DateTimeFormatter dateTimeFormatter) { + columnName = name; + columnType = type; + this.precision = precision; + this.scale = scale; + this.dateTimeFormatter = dateTimeFormatter; + } + } + + /* + * Metadata to represent the columns in the batch. Each column should be mapped to its corresponding position within the parameter (from position 1 and + * onwards) + */ + private Map columnMetadata; + + /* + * Contains all the column names if firstLineIsColumnNames is true + */ + private String[] columnNames = null; + + /* + * Contains the format that java.sql.Types.TIMESTAMP_WITH_TIMEZONE data should be read in as. + */ + private DateTimeFormatter dateTimeFormatter = null; + + /* + * Contains the format that java.sql.Types.TIME_WITH_TIMEZONE data should be read in as. + */ + private DateTimeFormatter timeFormatter = null; + + /* + * Class name for logging. + */ + private static final String loggerClassName = "com.microsoft.sqlserver.jdbc.SQLServerBulkBatchInsertRecord"; + + /* + * Logger + */ + private static final java.util.logging.Logger loggerExternal = java.util.logging.Logger.getLogger(loggerClassName); + + private ArrayList batchParam; + private int batchParamIndex = 0; + private ArrayList columnList; + private ArrayList valueList; + + public SQLServerBulkBatchInsertRecord(ArrayList batchParam, ArrayList columnList, + ArrayList valueList, String encoding) throws SQLServerException { + loggerExternal.entering(loggerClassName, "SQLServerBulkBatchInsertRecord", + new Object[] {batchParam, encoding}); + + if (null == batchParam) { + throwInvalidArgument("batchParam"); + } + + if (null == valueList) { + throwInvalidArgument("valueList"); + } + + this.batchParam = batchParam; + this.columnList = columnList; + this.valueList = valueList; + columnMetadata = new HashMap<>(); + + loggerExternal.exiting(loggerClassName, "SQLServerBulkBatchInsertRecord"); + } + + /** + * Adds metadata for the given column in the file. + * + * @param positionInTable + * Indicates which column the metadata is for. Columns start at 1. + * @param name + * Name for the column (optional if only using column ordinal in a mapping for SQLServerBulkCopy operation) + * @param jdbcType + * JDBC data type of the column + * @param precision + * Precision for the column (ignored for the appropriate data types) + * @param scale + * Scale for the column (ignored for the appropriate data types) + * @param dateTimeFormatter + * format to parse data that is sent + * @throws SQLServerException + * when an error occurs + */ + public void addColumnMetadata(int positionInTable, + String name, + int jdbcType, + int precision, + int scale, + DateTimeFormatter dateTimeFormatter) throws SQLServerException { + addColumnMetadataInternal(positionInTable, name, jdbcType, precision, scale, dateTimeFormatter); + } + + /** + * Adds metadata for the given column in the file. + * + * @param positionInTable + * Indicates which column the metadata is for. Columns start at 1. + * @param name + * Name for the column (optional if only using column ordinal in a mapping for SQLServerBulkCopy operation) + * @param jdbcType + * JDBC data type of the column + * @param precision + * Precision for the column (ignored for the appropriate data types) + * @param scale + * Scale for the column (ignored for the appropriate data types) + * @throws SQLServerException + * when an error occurs + */ + public void addColumnMetadata(int positionInTable, + String name, + int jdbcType, + int precision, + int scale) throws SQLServerException { + addColumnMetadataInternal(positionInTable, name, jdbcType, precision, scale, null); + } + + /** + * Adds metadata for the given column in the file. + * + * @param positionInTable + * Indicates which column the metadata is for. Columns start at 1. + * @param name + * Name for the column (optional if only using column ordinal in a mapping for SQLServerBulkCopy operation) + * @param jdbcType + * JDBC data type of the column + * @param precision + * Precision for the column (ignored for the appropriate data types) + * @param scale + * Scale for the column (ignored for the appropriate data types) + * @param dateTimeFormatter + * format to parse data that is sent + * @throws SQLServerException + * when an error occurs + */ + void addColumnMetadataInternal(int positionInTable, + String name, + int jdbcType, + int precision, + int scale, + DateTimeFormatter dateTimeFormatter) throws SQLServerException { + loggerExternal.entering(loggerClassName, "addColumnMetadata", new Object[] {positionInTable, name, jdbcType, precision, scale}); + + String colName = ""; + + if (0 >= positionInTable) { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidColumnOrdinal")); + Object[] msgArgs = {positionInTable}; + throw new SQLServerException(form.format(msgArgs), SQLState.COL_NOT_FOUND, DriverError.NOT_SET, null); + } + + if (null != name) + colName = name.trim(); + else if ((columnNames != null) && (columnNames.length >= positionInTable)) + colName = columnNames[positionInTable - 1]; + + if ((columnNames != null) && (positionInTable > columnNames.length)) { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidColumn")); + Object[] msgArgs = {positionInTable}; + throw new SQLServerException(form.format(msgArgs), SQLState.COL_NOT_FOUND, DriverError.NOT_SET, null); + } + + checkDuplicateColumnName(positionInTable, name); + switch (jdbcType) { + /* + * 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. string literal formats supported by temporal types are available in MSDN + * page on data types. + */ + case java.sql.Types.DATE: + case java.sql.Types.TIME: + case java.sql.Types.TIMESTAMP: + case microsoft.sql.Types.DATETIMEOFFSET: + // The precision is just a number long enough to hold all types of temporal data, doesn't need to be exact precision. + columnMetadata.put(positionInTable, new ColumnMetadata(colName, jdbcType, 50, scale, dateTimeFormatter)); + break; + + // Redirect SQLXML as LONGNVARCHAR + // SQLXML is not valid type in TDS + case java.sql.Types.SQLXML: + columnMetadata.put(positionInTable, new ColumnMetadata(colName, java.sql.Types.LONGNVARCHAR, precision, scale, dateTimeFormatter)); + break; + + // Redirecting Float as Double based on data type mapping + // https://msdn.microsoft.com/en-us/library/ms378878%28v=sql.110%29.aspx + case java.sql.Types.FLOAT: + columnMetadata.put(positionInTable, new ColumnMetadata(colName, java.sql.Types.DOUBLE, precision, scale, dateTimeFormatter)); + break; + + // redirecting BOOLEAN as BIT + case java.sql.Types.BOOLEAN: + columnMetadata.put(positionInTable, new ColumnMetadata(colName, java.sql.Types.BIT, precision, scale, dateTimeFormatter)); + break; + + default: + columnMetadata.put(positionInTable, new ColumnMetadata(colName, jdbcType, precision, scale, dateTimeFormatter)); + } + + loggerExternal.exiting(loggerClassName, "addColumnMetadata"); + } + + /** + * Set the format for reading in dates from the file. + * + * @param dateTimeFormat + * format to parse data sent as java.sql.Types.TIMESTAMP_WITH_TIMEZONE + */ + public void setTimestampWithTimezoneFormat(String dateTimeFormat) { + DriverJDBCVersion.checkSupportsJDBC42(); + loggerExternal.entering(loggerClassName, "setTimestampWithTimezoneFormat", dateTimeFormat); + + this.dateTimeFormatter = DateTimeFormatter.ofPattern(dateTimeFormat); + + loggerExternal.exiting(loggerClassName, "setTimestampWithTimezoneFormat"); + } + + /** + * Set the format for reading in dates from the file. + * + * @param dateTimeFormatter + * format to parse data sent as java.sql.Types.TIMESTAMP_WITH_TIMEZONE + */ + public void setTimestampWithTimezoneFormat(DateTimeFormatter dateTimeFormatter) { + loggerExternal.entering(loggerClassName, "setTimestampWithTimezoneFormat", new Object[] {dateTimeFormatter}); + + this.dateTimeFormatter = dateTimeFormatter; + + loggerExternal.exiting(loggerClassName, "setTimestampWithTimezoneFormat"); + } + + /** + * Set the format for reading in dates from the file. + * + * @param timeFormat + * format to parse data sent as java.sql.Types.TIME_WITH_TIMEZONE + */ + public void setTimeWithTimezoneFormat(String timeFormat) { + DriverJDBCVersion.checkSupportsJDBC42(); + loggerExternal.entering(loggerClassName, "setTimeWithTimezoneFormat", timeFormat); + + this.timeFormatter = DateTimeFormatter.ofPattern(timeFormat); + + loggerExternal.exiting(loggerClassName, "setTimeWithTimezoneFormat"); + } + + /** + * Set the format for reading in dates from the file. + * + * @param dateTimeFormatter + * format to parse data sent as java.sql.Types.TIME_WITH_TIMEZONE + */ + public void setTimeWithTimezoneFormat(DateTimeFormatter dateTimeFormatter) { + loggerExternal.entering(loggerClassName, "setTimeWithTimezoneFormat", new Object[] {dateTimeFormatter}); + + this.timeFormatter = dateTimeFormatter; + + loggerExternal.exiting(loggerClassName, "setTimeWithTimezoneFormat"); + } + + /** + * Releases any resources associated with the file reader. + * + * @throws SQLServerException + * when an error occurs + */ + public void close() throws SQLServerException { + } + + public DateTimeFormatter getColumnDateTimeFormatter(int column) { + return columnMetadata.get(column).dateTimeFormatter; + } + + @Override + public Set getColumnOrdinals() { + return columnMetadata.keySet(); + } + + @Override + public String getColumnName(int column) { + return columnMetadata.get(column).columnName; + } + + @Override + public int getColumnType(int column) { + return columnMetadata.get(column).columnType; + } + + @Override + public int getPrecision(int column) { + return columnMetadata.get(column).precision; + } + + @Override + public int getScale(int column) { + return columnMetadata.get(column).scale; + } + + @Override + public boolean isAutoIncrement(int column) { + return false; + } + + @Override + public Object[] getRowData() throws SQLServerException { + + Object[] data = new Object[columnMetadata.size()]; + + if (null == columnList || columnList.size() == 0) { + int valueIndex = 0; + for (int i = 0; i < data.length; i++) { + if (valueList.get(i).equalsIgnoreCase("?")) { + data[i] = batchParam.get(i)[valueIndex].getSetterValue(); + valueIndex++; + } else { + // remove 's at the beginning and end of the value, if it exists. + int len = valueList.get(i).length(); + if (valueList.get(i).charAt(0) == '\'' && valueList.get(i).charAt(len - 1) == '\'') { + data[i] = valueList.get(i).substring(1, len - 1); + } else { + data[i] = valueList.get(i); + } + } + } + } else { + int valueIndex = 0; + int columnListIndex = 0; + for (int i = 0; i < data.length; i++) { + if (columnList.size() > columnListIndex && columnList.get(columnListIndex).equals(columnMetadata.get(i + 1).columnName)) { + if (valueList.get(i).equalsIgnoreCase("?")) { + data[i] = batchParam.get(i)[valueIndex].getSetterValue(); + valueIndex++; + } else { + // remove 's at the beginning and end of the value, if it exists. + int len = valueList.get(i).length(); + if (valueList.get(i).charAt(0) == '\'' && valueList.get(i).charAt(len - 1) == '\'') { + data[i] = valueList.get(i).substring(1, len - 1); + } else { + data[i] = valueList.get(i); + } + } + columnListIndex++; + } else { + data[i] = ""; + } + } + } + + batchParamIndex++; + + // Cannot go directly from String[] to Object[] and expect it to act as an array. + Object[] dataRow = new Object[data.length]; + + for (Entry pair : columnMetadata.entrySet()) { + ColumnMetadata cm = pair.getValue(); + + if (data.length < pair.getKey() - 1) { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidColumn")); + Object[] msgArgs = {pair.getKey()}; + throw new SQLServerException(form.format(msgArgs), SQLState.COL_NOT_FOUND, DriverError.NOT_SET, null); + } + + // Source header has more columns than current param read + if (columnNames != null && (columnNames.length > data.length)) { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_CSVDataSchemaMismatch")); + Object[] msgArgs = {}; + throw new SQLServerException(form.format(msgArgs), SQLState.COL_NOT_FOUND, DriverError.NOT_SET, null); + } + + try { + if (0 == data[pair.getKey() - 1].toString().length()) { + dataRow[pair.getKey() - 1] = null; + continue; + } + + switch (cm.columnType) { + /* + * Both BCP and BULK INSERT considers double quotes as part of the data and throws error if any data (say "10") is to be + * inserted into an numeric column. Our implementation does the same. + */ + case Types.INTEGER: { + // Formatter to remove the decimal part as SQL Server floors the decimal in integer types + DecimalFormat decimalFormatter = new DecimalFormat("#"); + String formatedfInput = decimalFormatter.format(Double.parseDouble(data[pair.getKey() - 1].toString())); + dataRow[pair.getKey() - 1] = Integer.valueOf(formatedfInput); + break; + } + + case Types.TINYINT: + case Types.SMALLINT: { + // Formatter to remove the decimal part as SQL Server floors the decimal in integer types + DecimalFormat decimalFormatter = new DecimalFormat("#"); + String formatedfInput = decimalFormatter.format(Double.parseDouble(data[pair.getKey() - 1].toString())); + dataRow[pair.getKey() - 1] = Short.valueOf(formatedfInput); + break; + } + + case Types.BIGINT: { + BigDecimal bd = new BigDecimal(data[pair.getKey() - 1].toString().trim()); + try { + dataRow[pair.getKey() - 1] = bd.setScale(0, RoundingMode.DOWN).longValueExact(); + } catch (ArithmeticException ex) { + String value = "'" + data[pair.getKey() - 1] + "'"; + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_errorConvertingValue")); + throw new SQLServerException(form.format(new Object[]{value, JDBCType.of(cm.columnType)}), null, 0, ex); + } + break; + } + + case Types.DECIMAL: + case Types.NUMERIC: { + BigDecimal bd = new BigDecimal(data[pair.getKey() - 1].toString().trim()); + dataRow[pair.getKey() - 1] = bd.setScale(cm.scale, RoundingMode.HALF_UP); + break; + } + + case Types.BIT: { + // "true" => 1, "false" => 0 + // Any non-zero value (integer/double) => 1, 0/0.0 => 0 + try { + dataRow[pair.getKey() - 1] = (0 == Double.parseDouble(data[pair.getKey() - 1].toString())) ? Boolean.FALSE : Boolean.TRUE; + } catch (NumberFormatException e) { + dataRow[pair.getKey() - 1] = Boolean.parseBoolean(data[pair.getKey() - 1].toString()); + } + break; + } + + case Types.REAL: { + dataRow[pair.getKey() - 1] = Float.parseFloat(data[pair.getKey() - 1].toString()); + break; + } + + case Types.DOUBLE: { + dataRow[pair.getKey() - 1] = Double.parseDouble(data[pair.getKey() - 1].toString()); + break; + } + + case Types.BINARY: + case Types.VARBINARY: + case Types.LONGVARBINARY: + case Types.BLOB: { + /* + * For binary data, the value in file may or may not have the '0x' prefix. We will try to match our implementation with + * 'BULK INSERT' except that we will allow 0x prefix whereas 'BULK INSERT' command does not allow 0x prefix. A BULK INSERT + * example: A sample csv file containing data for 2 binary columns and 1 row: 61,62 Table definition: create table t1(c1 + * varbinary(10), c2 varbinary(10)) BULK INSERT command: bulk insert t1 from 'C:\in.csv' + * with(DATAFILETYPE='char',firstrow=1,FIELDTERMINATOR=',') select * from t1 shows 1 row with columns: 0x61, 0x62 + */ + // Strip off 0x if present. + String binData = data[pair.getKey() - 1].toString().trim(); + if (binData.startsWith("0x") || binData.startsWith("0X")) { + dataRow[pair.getKey() - 1] = binData.substring(2); + } else { + dataRow[pair.getKey() - 1] = binData; + } + break; + } + + case 2013: // java.sql.Types.TIME_WITH_TIMEZONE + { + DriverJDBCVersion.checkSupportsJDBC42(); + OffsetTime offsetTimeValue; + + // The per-column DateTimeFormatter gets priority. + if (null != cm.dateTimeFormatter) + offsetTimeValue = OffsetTime.parse(data[pair.getKey() - 1].toString(), cm.dateTimeFormatter); + else if (timeFormatter != null) + offsetTimeValue = OffsetTime.parse(data[pair.getKey() - 1].toString(), timeFormatter); + else + offsetTimeValue = OffsetTime.parse(data[pair.getKey() - 1].toString()); + + dataRow[pair.getKey() - 1] = offsetTimeValue; + break; + } + + case 2014: // java.sql.Types.TIMESTAMP_WITH_TIMEZONE + { + DriverJDBCVersion.checkSupportsJDBC42(); + OffsetDateTime offsetDateTimeValue; + + // The per-column DateTimeFormatter gets priority. + if (null != cm.dateTimeFormatter) + offsetDateTimeValue = OffsetDateTime.parse(data[pair.getKey() - 1].toString(), cm.dateTimeFormatter); + else if (dateTimeFormatter != null) + offsetDateTimeValue = OffsetDateTime.parse(data[pair.getKey() - 1].toString(), dateTimeFormatter); + else + offsetDateTimeValue = OffsetDateTime.parse(data[pair.getKey() - 1].toString()); + + dataRow[pair.getKey() - 1] = offsetDateTimeValue; + break; + } + + case Types.NULL: { + dataRow[pair.getKey() - 1] = null; + break; + } + + case Types.DATE: + case Types.CHAR: + case Types.NCHAR: + case Types.VARCHAR: + case Types.NVARCHAR: + case Types.LONGVARCHAR: + case Types.LONGNVARCHAR: + case Types.CLOB: + default: { + // The string is copied as is. + dataRow[pair.getKey() - 1] = data[pair.getKey() - 1]; + break; + } + } + } catch (IllegalArgumentException e) { + String value = "'" + data[pair.getKey() - 1] + "'"; + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_errorConvertingValue")); + throw new SQLServerException(form.format(new Object[]{value, JDBCType.of(cm.columnType)}), null, 0, e); + } catch (ArrayIndexOutOfBoundsException e) { + throw new SQLServerException(SQLServerException.getErrString("R_CSVDataSchemaMismatch"), e); + } + + } + return dataRow; + } + + @Override + public boolean next() throws SQLServerException { + return batchParamIndex < batchParam.size(); + } + + /* + * 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); + } + + /* + * Method to throw a SQLServerExeption for duplicate column names + */ + private void checkDuplicateColumnName(int positionInTable, + String colName) throws SQLServerException { + + if (null != colName && colName.trim().length() != 0) { + for (Entry entry : columnMetadata.entrySet()) { + // duplicate check is not performed in case of same positionInTable value + if (null != entry && entry.getKey() != positionInTable) { + if (null != entry.getValue() && colName.trim().equalsIgnoreCase(entry.getValue().columnName)) { + throw new SQLServerException(SQLServerException.getErrString("R_BulkCSVDataDuplicateColumn"), null); + } + } + + } + } + + } +} diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index 432aa9e32..9c652424c 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -116,6 +116,8 @@ public class SQLServerConnection implements ISQLServerConnection { private byte[] accessTokenInByte = null; private SqlFedAuthToken fedAuthToken = null; + + private Boolean isAzureDW = null; static class Sha1HashKey { private byte[] bytes; @@ -5829,6 +5831,39 @@ public void onEviction(Sha1HashKey key, PreparedStatementHandle handle) { } } } + + boolean isAzureDW() throws SQLServerException, SQLException { + if (null == isAzureDW) { + try (Statement stmt = this.createStatement(); ResultSet rs = stmt.executeQuery("SELECT CAST(SERVERPROPERTY('EngineEdition') as INT)");) + { + // SERVERPROPERTY('EngineEdition') can be used to determine whether the db server is SQL Azure. + // It should return 6 for SQL Azure DW. This is more reliable than @@version or serverproperty('edition'). + // Reference: http://msdn.microsoft.com/en-us/library/ee336261.aspx + // + // SERVERPROPERTY('EngineEdition') means + // Database Engine edition of the instance of SQL Server installed on the server. + // 1 = Personal or Desktop Engine (Not available for SQL Server.) + // 2 = Standard (This is returned for Standard and Workgroup.) + // 3 = Enterprise (This is returned for Enterprise, Enterprise Evaluation, and Developer.) + // 4 = Express (This is returned for Express, Express with Advanced Services, and Windows Embedded SQL.) + // 5 = SQL Azure + // 6 = SQL Azure DW + // Base data type: int + final int ENGINE_EDITION_FOR_SQL_AZURE_DW = 6; + rs.next(); + int engineEdition = rs.getInt(1); + if (engineEdition == ENGINE_EDITION_FOR_SQL_AZURE_DW) + { + isAzureDW = true; + } else { + isAzureDW = false; + } + } + return isAzureDW; + } else { + return isAzureDW; + } + } } // Helper class for security manager functions used by SQLServerConnection class. diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java index 7106d9f10..a2ece7137 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java @@ -142,6 +142,8 @@ private void resetPrepStmtHandle() { */ private boolean encryptionMetadataIsRetrieved = false; + private String localUserSQL; + // Internal function used in tracing String getClassNameInternal() { return "SQLServerPreparedStatement"; @@ -2452,8 +2454,72 @@ public int[] executeBatch() throws SQLServerException, BatchUpdateException, SQL } checkClosed(); discardLastExecutionResults(); - + int updateCounts[]; + + localUserSQL = userSQL; + + try { + if (isInsert(localUserSQL) && connection.isAzureDW()) { + if (batchParamValues == null) { + updateCounts = new int[0]; + loggerExternal.exiting(getClassNameLogging(), "executeBatch", updateCounts); + return updateCounts; + } + + String tableName = parseUserSQLForTableNameDW(false, false); + ArrayList columnList = parseUserSQLForColumnListDW(); + ArrayList valueList = parseUserSQLForValueListDW(); + + String destinationTableName = tableName; + // Get destination metadata + try (SQLServerResultSet rs = ((SQLServerStatement) connection.createStatement()) + .executeQueryInternal("SET FMTONLY ON SELECT * FROM " + destinationTableName + " SET FMTONLY OFF ");) { + + SQLServerBulkBatchInsertRecord batchRecord = new SQLServerBulkBatchInsertRecord(batchParamValues, columnList, valueList, null); + + for (int i = 1; i <= rs.getColumnCount(); i++) { + Column c = rs.getColumn(i); + CryptoMetadata cryptoMetadata = c.getCryptoMetadata(); + int jdbctype; + TypeInfo ti = c.getTypeInfo(); + if (null != cryptoMetadata) { + jdbctype = cryptoMetadata.getBaseTypeInfo().getSSType().getJDBCType().getIntValue(); + } else { + jdbctype = ti.getSSType().getJDBCType().getIntValue(); + } + batchRecord.addColumnMetadata(i, c.getColumnName(), jdbctype, ti.getPrecision(), ti.getScale()); + } + + SQLServerBulkCopy bcOperation = new SQLServerBulkCopy(connection); + bcOperation.setDestinationTableName(tableName); + bcOperation.writeToServer((ISQLServerBulkRecord) batchRecord); + bcOperation.close(); + updateCounts = new int[batchParamValues.size()]; + for (int i = 0; i < batchParamValues.size(); ++i) + { + updateCounts[i] = 1; + } + + batchParamValues = null; + loggerExternal.exiting(getClassNameLogging(), "executeBatch", updateCounts); + return updateCounts; + } + } + } + catch (SQLException e) { + // Unable to retrieve metadata for destination + // create an error message for failing bulk copy + insert batch + throw new SQLServerException(SQLServerException.getErrString("R_unableRetrieveColMeta"), e); + } + catch (Exception e) { + // If we fail with non-SQLException, fall back to the original batch insert logic. + if (getStatementLogger().isLoggable(java.util.logging.Level.FINE)) { + getStatementLogger().fine("Parsing user's Batch Insert SQL Query failed: " + e.toString()); + getStatementLogger().fine("Falling back to the original implementation for Batch Insert."); + } + } + if (batchParamValues == null) updateCounts = new int[0]; @@ -2554,6 +2620,301 @@ public long[] executeLargeBatch() throws SQLServerException, BatchUpdateExceptio loggerExternal.exiting(getClassNameLogging(), "executeLargeBatch", updateCounts); return updateCounts; } + + + private String parseUserSQLForTableNameDW(boolean hasInsertBeenFound, boolean hasIntoBeenFound) { + // As far as finding the table name goes, There are two cases: + // Insert into and Insert + // And there could be in-line comments (with /* and */) in between. + // This method assumes the localUserSQL string starts with "insert". + localUserSQL = localUserSQL.trim(); + if (localUserSQL.substring(0, 2).equalsIgnoreCase("/*")) { + int temp = localUserSQL.indexOf("*/"); + localUserSQL = localUserSQL.substring(temp); + return parseUserSQLForTableNameDW(hasInsertBeenFound, hasIntoBeenFound); + } + + if (localUserSQL.substring(0, 6).equalsIgnoreCase("insert") && !hasInsertBeenFound) { + localUserSQL = localUserSQL.substring(6); + return parseUserSQLForTableNameDW(true, hasIntoBeenFound); + } + + if (localUserSQL.substring(0, 4).equalsIgnoreCase("into") && !hasIntoBeenFound) { + // is it really "into"? + // if the "into" is followed by a blank space or /*, then yes. + if (localUserSQL.charAt(4) == ' ' || localUserSQL.charAt(4) == '\t' || + (localUserSQL.charAt(4) == '/' && localUserSQL.charAt(5) == '*')) { + localUserSQL = localUserSQL.substring(4); + return parseUserSQLForTableNameDW(hasInsertBeenFound, true); + } + + // otherwise, we found the token that either contains the databasename.tablename or tablename. + // Recursively handle this, but into has been found. (or rather, it's absent in the query - the "into" keyword is optional) + return parseUserSQLForTableNameDW(hasInsertBeenFound, true); + } + + // At this point, the next token has to be the table name. + // It could be encapsulated in [], "", or have a database name preceding the table name. + // If it's encapsulated in [] or "", we need be more careful with parsing as anything could go into []/"". + // For ] or ", they can be escaped by ]] or "", watch out for this too. + if (localUserSQL.substring(0, 1).equalsIgnoreCase("[")) { + int tempint = localUserSQL.indexOf("]", 1); + + // keep checking if it's escaped + while (localUserSQL.charAt(tempint + 1) == ']') { + localUserSQL = localUserSQL.substring(0, tempint) + localUserSQL.substring(tempint + 1); + tempint = localUserSQL.indexOf("]", tempint + 1); + } + + // we've found a ] that is actually trying to close the square bracket. + // If it's followed by a dot, then it's a database. + // Otherwise, it's the table. + if (localUserSQL.charAt(tempint + 1) == '.') { + String tempstr = localUserSQL.substring(1, tempint); + localUserSQL = localUserSQL.substring(tempint + 2); + return tempstr + "." + parseUserSQLForTableNameDW(hasInsertBeenFound, hasIntoBeenFound); + } else { + // return tablename + String tempstr = localUserSQL.substring(1, tempint); + localUserSQL = localUserSQL.substring(tempint + 1); + return tempstr; + } + } + + // do the same for "" + if (localUserSQL.substring(0, 1).equalsIgnoreCase("\"")) { + int tempint = localUserSQL.indexOf("\"", 1); + + // keep checking if it's escaped + while (localUserSQL.charAt(tempint + 1) == '\"') { + localUserSQL = localUserSQL.substring(0, tempint) + localUserSQL.substring(tempint + 1); + tempint = localUserSQL.indexOf("\"", tempint + 1); + } + + // we've found a " that is actually trying to close the quote. + // If it's followed by a dot, then it's a database. + // Otherwise, it's the table. + if (localUserSQL.charAt(tempint + 1) == '.') { + String tempstr = localUserSQL.substring(1, tempint); + localUserSQL = localUserSQL.substring(tempint + 2); + return tempstr + "." + parseUserSQLForTableNameDW(hasInsertBeenFound, hasIntoBeenFound); + } else { + // return tablename + String tempstr = localUserSQL.substring(1, tempint); + localUserSQL = localUserSQL.substring(tempint + 1); + return tempstr; + } + } + + // At this point, the next chunk of string is the table name (could have database name), without starting with [ or ". + StringBuilder sb = new StringBuilder(); + while (localUserSQL.length() > 0) { + if (localUserSQL.charAt(0) == '.' || localUserSQL.charAt(0) == ' ' || localUserSQL.charAt(0) == '\t' + || localUserSQL.charAt(0) == '(') { + if (localUserSQL.charAt(0) == '.') { + localUserSQL = localUserSQL.substring(1); + return sb.toString() + "." + parseUserSQLForTableNameDW(hasInsertBeenFound, hasIntoBeenFound); + } else { + return sb.toString(); + } + } else { + sb.append(localUserSQL.charAt(0)); + localUserSQL = localUserSQL.substring(1); + } + } + + // It shouldn't come here. If we did, something is wrong. + throw new IllegalArgumentException("localUserSQL"); + } + + private ArrayList parseUserSQLForColumnListDW() { + localUserSQL = localUserSQL.trim(); + + // ignore all comments + if (localUserSQL.substring(0, 2).equalsIgnoreCase("/*")) { + int temp = localUserSQL.indexOf("*/"); + localUserSQL = localUserSQL.substring(temp); + return parseUserSQLForColumnListDW(); + } + + //check if optional column list was provided + // Columns can have the form of c1, [c1] or "c1". It can escape ] or " by ]] or "". + if (localUserSQL.substring(0, 1).equalsIgnoreCase("(")) { + localUserSQL = localUserSQL.substring(1); + return parseUserSQLForColumnListDWHelper(new ArrayList()); + } + return null; + } + + private ArrayList parseUserSQLForColumnListDWHelper(ArrayList listOfColumns) { + localUserSQL = localUserSQL.trim(); + + // ignore all comments + if (localUserSQL.substring(0, 2).equalsIgnoreCase("/*")) { + int temp = localUserSQL.indexOf("*/"); + localUserSQL = localUserSQL.substring(temp); + return parseUserSQLForColumnListDWHelper(listOfColumns); + } + + if (localUserSQL.charAt(0) == ')') { + localUserSQL = localUserSQL.substring(1); + return listOfColumns; + } + + if (localUserSQL.charAt(0) == ',') { + localUserSQL = localUserSQL.substring(1); + return parseUserSQLForColumnListDWHelper(listOfColumns); + } + + if (localUserSQL.charAt(0) == '[') { + int tempint = localUserSQL.indexOf("]", 1); + + // keep checking if it's escaped + while (localUserSQL.charAt(tempint + 1) == ']') { + localUserSQL = localUserSQL.substring(0, tempint) + localUserSQL.substring(tempint + 1); + tempint = localUserSQL.indexOf("]", tempint + 1); + } + + // we've found a ] that is actually trying to close the square bracket. + String tempstr = localUserSQL.substring(1, tempint); + localUserSQL = localUserSQL.substring(tempint + 1); + listOfColumns.add(tempstr); + return parseUserSQLForColumnListDWHelper(listOfColumns); + } + + if (localUserSQL.charAt(0) == '\"') { + int tempint = localUserSQL.indexOf("\"", 1); + + // keep checking if it's escaped + while (localUserSQL.charAt(tempint + 1) == '\"') { + localUserSQL = localUserSQL.substring(0, tempint) + localUserSQL.substring(tempint + 1); + tempint = localUserSQL.indexOf("\"", tempint + 1); + } + + // we've found a " that is actually trying to close the quote. + String tempstr = localUserSQL.substring(1, tempint); + localUserSQL = localUserSQL.substring(tempint + 1); + listOfColumns.add(tempstr); + return parseUserSQLForColumnListDWHelper(listOfColumns); + } + + // At this point, the next chunk of string is the column name, without starting with [ or ". + StringBuilder sb = new StringBuilder(); + while (localUserSQL.length() > 0) { + if (localUserSQL.charAt(0) == ',' || localUserSQL.charAt(0) == ')') { + if (localUserSQL.charAt(0) == ',') { + localUserSQL = localUserSQL.substring(1); + listOfColumns.add(sb.toString()); + return parseUserSQLForColumnListDWHelper(listOfColumns); + } else { + localUserSQL = localUserSQL.substring(1); + listOfColumns.add(sb.toString()); + return listOfColumns; + } + } else { + sb.append(localUserSQL.charAt(0)); + localUserSQL = localUserSQL.substring(1); + } + } + + // It shouldn't come here. If we did, something is wrong. + throw new IllegalArgumentException("localUserSQL"); + } + + + private ArrayList parseUserSQLForValueListDW() { + localUserSQL = localUserSQL.trim(); + + // ignore all comments + if (localUserSQL.substring(0, 2).equalsIgnoreCase("/*")) { + int temp = localUserSQL.indexOf("*/"); + localUserSQL = localUserSQL.substring(temp); + return parseUserSQLForColumnListDW(); + } + + // look for keyword "VALUES" + if (localUserSQL.substring(0, 6).equalsIgnoreCase("VALUES")) { + localUserSQL = localUserSQL.substring(6); + + localUserSQL = localUserSQL.trim(); + + // ignore all comments + if (localUserSQL.substring(0, 2).equalsIgnoreCase("/*")) { + int temp = localUserSQL.indexOf("*/"); + localUserSQL = localUserSQL.substring(temp); + return parseUserSQLForColumnListDW(); + } + + if (localUserSQL.substring(0, 1).equalsIgnoreCase("(")) { + localUserSQL = localUserSQL.substring(1); + return parseUserSQLForValueListDWHelper(new ArrayList()); + } + } + + // shouldn't come here, as the list of values is mandatory. + throw new IllegalArgumentException("localUserSQL"); + } + + private ArrayList parseUserSQLForValueListDWHelper(ArrayList listOfValues) { + localUserSQL = localUserSQL.trim(); + + // ignore all comments + if (localUserSQL.substring(0, 2).equalsIgnoreCase("/*")) { + int temp = localUserSQL.indexOf("*/"); + localUserSQL = localUserSQL.substring(temp); + return parseUserSQLForColumnListDWHelper(listOfValues); + } + + + if (localUserSQL.charAt(0) == ')') { + localUserSQL = localUserSQL.substring(1); + return listOfValues; + } + + if (localUserSQL.charAt(0) == ',') { + localUserSQL = localUserSQL.substring(1); + return parseUserSQLForColumnListDWHelper(listOfValues); + } + + if (localUserSQL.charAt(0) == '\'') { + int tempint = localUserSQL.indexOf("\'", 1); + + // keep checking if it's escaped + while (localUserSQL.charAt(tempint + 1) == '\'') { + localUserSQL = localUserSQL.substring(0, tempint) + localUserSQL.substring(tempint + 1); + tempint = localUserSQL.indexOf("\'", tempint + 1); + } + + // we've found a ' that is actually trying to close the quote. + // Include 's around the string as well, so we can distinguish '?' and ? later on. + String tempstr = localUserSQL.substring(0, tempint + 1); + localUserSQL = localUserSQL.substring(tempint + 1); + listOfValues.add(tempstr); + return parseUserSQLForColumnListDWHelper(listOfValues); + } + + // At this point, the next chunk of string is the value, without starting with ' (most likely a ?). + StringBuilder sb = new StringBuilder(); + while (localUserSQL.length() > 0) { + if (localUserSQL.charAt(0) == ',' || localUserSQL.charAt(0) == ')') { + if (localUserSQL.charAt(0) == ',') { + localUserSQL = localUserSQL.substring(1); + listOfValues.add(sb.toString()); + return parseUserSQLForColumnListDWHelper(listOfValues); + } else { + localUserSQL = localUserSQL.substring(1); + listOfValues.add(sb.toString()); + return listOfValues; + } + } else { + sb.append(localUserSQL.charAt(0)); + localUserSQL = localUserSQL.substring(1); + } + } + + // It shouldn't come here. If we did, something is wrong. + throw new IllegalArgumentException("localUserSQL"); + } private final class PrepStmtBatchExecCmd extends TDSCommand { private final SQLServerPreparedStatement stmt; diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java index 512363b4a..60bfbd650 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java @@ -963,6 +963,28 @@ final void resetForReexecute() throws SQLServerException { return false; return temp.substring(0, 6).equalsIgnoreCase("select"); } + + /** + * Determine if the SQL is a INSERT. + * + * @param sql + * The statment SQL. + * @return True is the statement is a select. + */ + /* L0 */ final boolean isInsert(String sql) throws SQLServerException { + checkClosed(); + // Used to check just the first letter which would cause + // "Set" commands to return true... + String temp = sql.trim(); + if (temp.substring(0, 2).equalsIgnoreCase("/*")) { + int index = temp.indexOf("*/"); + return isInsert(temp.substring(index)); + } + char c = temp.charAt(0); + if (c != 'i' && c != 'I') + return false; + return temp.substring(0, 6).equalsIgnoreCase("insert"); + } /** * Replace a JDBC parameter marker with the parameter's string value From 52da9aa74a0929a24fe7c5e8f954f3d179b42b23 Mon Sep 17 00:00:00 2001 From: Peter Bae Date: Fri, 27 Apr 2018 14:02:08 -0700 Subject: [PATCH 02/32] Parse bug fixing and test added --- .../jdbc/SQLServerPreparedStatement.java | 62 ++++--- .../sqlserver/jdbc/SQLServerStatement.java | 2 +- .../BatchExecutionWithBulkCopyParseTest.java | 168 ++++++++++++++++++ 3 files changed, 211 insertions(+), 21 deletions(-) create mode 100644 src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyParseTest.java diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java index a2ece7137..32b8595ac 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java @@ -2469,7 +2469,7 @@ public int[] executeBatch() throws SQLServerException, BatchUpdateException, SQL String tableName = parseUserSQLForTableNameDW(false, false); ArrayList columnList = parseUserSQLForColumnListDW(); - ArrayList valueList = parseUserSQLForValueListDW(); + ArrayList valueList = parseUserSQLForValueListDW(false); String destinationTableName = tableName; // Get destination metadata @@ -2629,7 +2629,7 @@ private String parseUserSQLForTableNameDW(boolean hasInsertBeenFound, boolean ha // This method assumes the localUserSQL string starts with "insert". localUserSQL = localUserSQL.trim(); if (localUserSQL.substring(0, 2).equalsIgnoreCase("/*")) { - int temp = localUserSQL.indexOf("*/"); + int temp = localUserSQL.indexOf("*/") + 2; localUserSQL = localUserSQL.substring(temp); return parseUserSQLForTableNameDW(hasInsertBeenFound, hasIntoBeenFound); } @@ -2732,7 +2732,7 @@ private ArrayList parseUserSQLForColumnListDW() { // ignore all comments if (localUserSQL.substring(0, 2).equalsIgnoreCase("/*")) { - int temp = localUserSQL.indexOf("*/"); + int temp = localUserSQL.indexOf("*/") + 2; localUserSQL = localUserSQL.substring(temp); return parseUserSQLForColumnListDW(); } @@ -2751,7 +2751,7 @@ private ArrayList parseUserSQLForColumnListDWHelper(ArrayList li // ignore all comments if (localUserSQL.substring(0, 2).equalsIgnoreCase("/*")) { - int temp = localUserSQL.indexOf("*/"); + int temp = localUserSQL.indexOf("*/") + 2; localUserSQL = localUserSQL.substring(temp); return parseUserSQLForColumnListDWHelper(listOfColumns); } @@ -2811,6 +2811,10 @@ private ArrayList parseUserSQLForColumnListDWHelper(ArrayList li listOfColumns.add(sb.toString()); return listOfColumns; } + } else if (localUserSQL.substring(0, 2).equalsIgnoreCase("/*")) { + int temp = localUserSQL.indexOf("*/") + 2; + localUserSQL = localUserSQL.substring(temp); + localUserSQL = localUserSQL.trim(); } else { sb.append(localUserSQL.charAt(0)); localUserSQL = localUserSQL.substring(1); @@ -2822,27 +2826,41 @@ private ArrayList parseUserSQLForColumnListDWHelper(ArrayList li } - private ArrayList parseUserSQLForValueListDW() { + private ArrayList parseUserSQLForValueListDW(boolean hasValuesBeenFound) { localUserSQL = localUserSQL.trim(); // ignore all comments if (localUserSQL.substring(0, 2).equalsIgnoreCase("/*")) { - int temp = localUserSQL.indexOf("*/"); + int temp = localUserSQL.indexOf("*/") + 2; localUserSQL = localUserSQL.substring(temp); - return parseUserSQLForColumnListDW(); + return parseUserSQLForValueListDW(hasValuesBeenFound); } - // look for keyword "VALUES" - if (localUserSQL.substring(0, 6).equalsIgnoreCase("VALUES")) { - localUserSQL = localUserSQL.substring(6); - - localUserSQL = localUserSQL.trim(); - + if (!hasValuesBeenFound) { + // look for keyword "VALUES" + if (localUserSQL.substring(0, 6).equalsIgnoreCase("VALUES")) { + localUserSQL = localUserSQL.substring(6); + + localUserSQL = localUserSQL.trim(); + + // ignore all comments + if (localUserSQL.substring(0, 2).equalsIgnoreCase("/*")) { + int temp = localUserSQL.indexOf("*/") + 2; + localUserSQL = localUserSQL.substring(temp); + return parseUserSQLForValueListDW(true); + } + + if (localUserSQL.substring(0, 1).equalsIgnoreCase("(")) { + localUserSQL = localUserSQL.substring(1); + return parseUserSQLForValueListDWHelper(new ArrayList()); + } + } + } else { // ignore all comments if (localUserSQL.substring(0, 2).equalsIgnoreCase("/*")) { - int temp = localUserSQL.indexOf("*/"); + int temp = localUserSQL.indexOf("*/") + 2; localUserSQL = localUserSQL.substring(temp); - return parseUserSQLForColumnListDW(); + return parseUserSQLForValueListDW(hasValuesBeenFound); } if (localUserSQL.substring(0, 1).equalsIgnoreCase("(")) { @@ -2860,9 +2878,9 @@ private ArrayList parseUserSQLForValueListDWHelper(ArrayList lis // ignore all comments if (localUserSQL.substring(0, 2).equalsIgnoreCase("/*")) { - int temp = localUserSQL.indexOf("*/"); + int temp = localUserSQL.indexOf("*/") + 2; localUserSQL = localUserSQL.substring(temp); - return parseUserSQLForColumnListDWHelper(listOfValues); + return parseUserSQLForValueListDWHelper(listOfValues); } @@ -2873,7 +2891,7 @@ private ArrayList parseUserSQLForValueListDWHelper(ArrayList lis if (localUserSQL.charAt(0) == ',') { localUserSQL = localUserSQL.substring(1); - return parseUserSQLForColumnListDWHelper(listOfValues); + return parseUserSQLForValueListDWHelper(listOfValues); } if (localUserSQL.charAt(0) == '\'') { @@ -2890,7 +2908,7 @@ private ArrayList parseUserSQLForValueListDWHelper(ArrayList lis String tempstr = localUserSQL.substring(0, tempint + 1); localUserSQL = localUserSQL.substring(tempint + 1); listOfValues.add(tempstr); - return parseUserSQLForColumnListDWHelper(listOfValues); + return parseUserSQLForValueListDWHelper(listOfValues); } // At this point, the next chunk of string is the value, without starting with ' (most likely a ?). @@ -2900,12 +2918,16 @@ private ArrayList parseUserSQLForValueListDWHelper(ArrayList lis if (localUserSQL.charAt(0) == ',') { localUserSQL = localUserSQL.substring(1); listOfValues.add(sb.toString()); - return parseUserSQLForColumnListDWHelper(listOfValues); + return parseUserSQLForValueListDWHelper(listOfValues); } else { localUserSQL = localUserSQL.substring(1); listOfValues.add(sb.toString()); return listOfValues; } + } else if (localUserSQL.substring(0, 2).equalsIgnoreCase("/*")) { + int temp = localUserSQL.indexOf("*/") + 2; + localUserSQL = localUserSQL.substring(temp); + localUserSQL = localUserSQL.trim(); } else { sb.append(localUserSQL.charAt(0)); localUserSQL = localUserSQL.substring(1); diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java index 60bfbd650..55dcc2420 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java @@ -977,7 +977,7 @@ final void resetForReexecute() throws SQLServerException { // "Set" commands to return true... String temp = sql.trim(); if (temp.substring(0, 2).equalsIgnoreCase("/*")) { - int index = temp.indexOf("*/"); + int index = temp.indexOf("*/") + 2; return isInsert(temp.substring(index)); } char c = temp.charAt(0); diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyParseTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyParseTest.java new file mode 100644 index 000000000..79a43c5e6 --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyParseTest.java @@ -0,0 +1,168 @@ +package com.microsoft.sqlserver.jdbc.preparedStatement; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; +import org.opentest4j.TestAbortedException; + +import com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement; +import com.microsoft.sqlserver.jdbc.SQLServerStatement; +import com.microsoft.sqlserver.testframework.AbstractTest; +import com.microsoft.sqlserver.testframework.DBConnection; +import com.microsoft.sqlserver.testframework.Utils; + + +@RunWith(JUnitPlatform.class) +public class BatchExecutionWithBulkCopyParseTest extends AbstractTest { + + static SQLServerPreparedStatement pstmt = null; + static Statement stmt = null; + static Connection connection = null; + + @Test + public void testIsInsert() throws SQLException, NoSuchMethodException, SecurityException, + IllegalAccessException, IllegalArgumentException, InvocationTargetException { + String valid1 = "INSERT INTO PeterTable values (1, 2)"; + String valid2 = " INSERT INTO PeterTable values (1, 2)"; + String valid3 = "/* asdf */ INSERT INTO PeterTable values (1, 2)"; + String invalid = "Select * from PEterTable"; + + stmt = connection.createStatement(); + Method method = stmt.getClass().getDeclaredMethod("isInsert", String.class); + method.setAccessible(true); + assertTrue((boolean) method.invoke(stmt, valid1)); + assertTrue((boolean) method.invoke(stmt, valid2)); + assertTrue((boolean) method.invoke(stmt, valid3)); + assertFalse((boolean) method.invoke(stmt, invalid)); + } + + @Test + public void testComments() throws SQLException, NoSuchFieldException, SecurityException, + IllegalArgumentException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { + pstmt = (SQLServerPreparedStatement) connection.prepareStatement(""); + + String valid = "/* rando comment *//* rando comment */ INSERT /* rando comment */ INTO /* rando comment *//*rando comment*/ PeterTable /*rando comment */" + + " /* rando comment */values/* rando comment */ (1, 2)"; + + Field f1 = pstmt.getClass().getSuperclass().getDeclaredField("localUserSQL"); + f1.setAccessible(true); + f1.set(pstmt, valid); + + Method method = pstmt.getClass().getSuperclass().getDeclaredMethod("parseUserSQLForTableNameDW", boolean.class, boolean.class); + method.setAccessible(true); + + assertEquals((String) method.invoke(pstmt, false, false), "PeterTable"); + } + + @Test + public void testBrackets() throws SQLException, NoSuchFieldException, SecurityException, + IllegalArgumentException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { + pstmt = (SQLServerPreparedStatement) connection.prepareStatement(""); + + String valid = "/* rando comment *//* rando comment */ INSERT /* rando comment */ INTO /* rando comment *//*rando comment*/ [Peter[]]Table] /*rando comment */" + + " /* rando comment */values/* rando comment */ (1, 2)"; + + Field f1 = pstmt.getClass().getSuperclass().getDeclaredField("localUserSQL"); + f1.setAccessible(true); + f1.set(pstmt, valid); + + Method method = pstmt.getClass().getSuperclass().getDeclaredMethod("parseUserSQLForTableNameDW", boolean.class, boolean.class); + method.setAccessible(true); + + assertEquals((String) method.invoke(pstmt, false, false), "Peter[]Table"); + } + + @Test + public void testDoubleQuotes() throws SQLException, NoSuchFieldException, SecurityException, + IllegalArgumentException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { + pstmt = (SQLServerPreparedStatement) connection.prepareStatement(""); + + String valid = "/* rando comment *//* rando comment */ INSERT /* rando comment */ INTO /* rando comment *//*rando comment*/ \"Peter\"\"\"\"Table\" /*rando comment */" + + " /* rando comment */values/* rando comment */ (1, 2)"; + + Field f1 = pstmt.getClass().getSuperclass().getDeclaredField("localUserSQL"); + f1.setAccessible(true); + f1.set(pstmt, valid); + + Method method = pstmt.getClass().getSuperclass().getDeclaredMethod("parseUserSQLForTableNameDW", boolean.class, boolean.class); + method.setAccessible(true); + + assertEquals((String) method.invoke(pstmt, false, false), "Peter\"\"Table"); + } + + @Test + public void testAll() throws SQLException, NoSuchFieldException, SecurityException, + IllegalArgumentException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { + pstmt = (SQLServerPreparedStatement) connection.prepareStatement(""); + + String valid = "/* rando comment *//* rando comment */ INSERT /* rando comment */ INTO /* rando comment *//*rando comment*/ \"Peter\"\"\"\"Table\" /*rando comment */" + + " /* rando comment */ (\"c1\"/* rando comment */, /* rando comment */[c2]/* rando comment */, /* rando comment */ /* rando comment */c3/* rando comment */, c4)" + + "values/* rando comment */ (/* rando comment */1/* rando comment */, /* rando comment */2/* rando comment */ , '?', ?)/* rando comment */"; + + Field f1 = pstmt.getClass().getSuperclass().getDeclaredField("localUserSQL"); + f1.setAccessible(true); + f1.set(pstmt, valid); + + Method method = pstmt.getClass().getSuperclass().getDeclaredMethod("parseUserSQLForTableNameDW", boolean.class, boolean.class); + method.setAccessible(true); + + assertEquals((String) method.invoke(pstmt, false, false), "Peter\"\"Table"); + + method = pstmt.getClass().getSuperclass().getDeclaredMethod("parseUserSQLForColumnListDW"); + method.setAccessible(true); + + ArrayList columnList = (ArrayList) method.invoke(pstmt); + ArrayList columnListExpected = new ArrayList(); + columnListExpected.add("c1"); + columnListExpected.add("c2"); + columnListExpected.add("c3"); + columnListExpected.add("c4"); + + for (int i = 0; i < columnListExpected.size(); i++) { + assertEquals(columnList.get(i), columnListExpected.get(i)); + } + + method = pstmt.getClass().getSuperclass().getDeclaredMethod("parseUserSQLForValueListDW", boolean.class); + method.setAccessible(true); + + ArrayList valueList = (ArrayList) method.invoke(pstmt, false); + ArrayList valueListExpected = new ArrayList(); + valueListExpected.add("1"); + valueListExpected.add("2"); + valueListExpected.add("'?'"); + valueListExpected.add("?"); + + for (int i = 0; i < valueListExpected.size(); i++) { + assertEquals(valueList.get(i), valueListExpected.get(i)); + } + } + + @BeforeEach + public void testSetup() throws TestAbortedException, Exception { + assumeTrue(13 <= new DBConnection(connectionString).getServerVersion(), + "Aborting test case as SQL Server version is not compatible with Always encrypted "); + + connection = DriverManager.getConnection(connectionString); + SQLServerStatement stmt = (SQLServerStatement) connection.createStatement(); + Utils.dropTableIfExists("esimple", stmt); + String sql1 = "create table esimple (id integer not null, name varchar(255), constraint pk_esimple primary key (id))"; + stmt.execute(sql1); + stmt.close(); + } +} From 5bb79a23fcf525b2a8b54f67f6b979bca9b4d6da Mon Sep 17 00:00:00 2001 From: Peter Bae Date: Mon, 30 Apr 2018 17:15:52 -0700 Subject: [PATCH 03/32] bug fix + additional tests --- .../jdbc/SQLServerBulkBatchInsertRecord.java | 9 +- .../preparedStatement/RegressionTest.java | 109 +++++ .../statement/BatchExecuteWithErrorsTest.java | 425 ++++++++++++++++++ .../unit/statement/BatchExecutionTest.java | 117 +++++ .../unit/statement/PreparedStatementTest.java | 178 ++++++++ 5 files changed, 833 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java index f987ad821..3b570cd57 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java @@ -75,14 +75,14 @@ private class ColumnMetadata { * Class name for logging. */ private static final String loggerClassName = "com.microsoft.sqlserver.jdbc.SQLServerBulkBatchInsertRecord"; - + /* * Logger */ private static final java.util.logging.Logger loggerExternal = java.util.logging.Logger.getLogger(loggerClassName); private ArrayList batchParam; - private int batchParamIndex = 0; + private int batchParamIndex = -1; private ArrayList columnList; private ArrayList valueList; @@ -352,7 +352,7 @@ public Object[] getRowData() throws SQLServerException { int valueIndex = 0; for (int i = 0; i < data.length; i++) { if (valueList.get(i).equalsIgnoreCase("?")) { - data[i] = batchParam.get(i)[valueIndex].getSetterValue(); + data[i] = batchParam.get(batchParamIndex)[valueIndex].getSetterValue(); valueIndex++; } else { // remove 's at the beginning and end of the value, if it exists. @@ -388,8 +388,6 @@ public Object[] getRowData() throws SQLServerException { } } - batchParamIndex++; - // Cannot go directly from String[] to Object[] and expect it to act as an array. Object[] dataRow = new Object[data.length]; @@ -565,6 +563,7 @@ else if (dateTimeFormatter != null) @Override public boolean next() throws SQLServerException { + batchParamIndex++; return batchParamIndex < batchParam.size(); } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/RegressionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/RegressionTest.java index 05f36bdee..4bbf8cb8d 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/RegressionTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/RegressionTest.java @@ -9,6 +9,7 @@ import static org.junit.jupiter.api.Assertions.fail; +import java.lang.reflect.Field; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; @@ -312,7 +313,115 @@ public void batchWithLargeStringTest() throws SQLException { } } + + @Test + public void batchWithLargeStringTestUseBulkCopyAPI() throws SQLException { + Statement stmt = con.createStatement(); + PreparedStatement pstmt = null; + ResultSet rs = null; + String testTable = "TEST_TABLE_BULK_COPY"; + Utils.dropTableIfExists(testTable, stmt); + + con.setAutoCommit(false); + + // create a table with two columns + boolean createPrimaryKey = false; + try { + stmt.execute("if object_id('" + testTable + "', 'U') is not null\ndrop table " + testTable + ";"); + if (createPrimaryKey) { + stmt.execute("create table " + testTable + " ( ID int, DATA nvarchar(max), primary key (ID) );"); + } + else { + stmt.execute("create table " + testTable + " ( ID int, DATA nvarchar(max) );"); + } + } + catch (Exception e) { + fail(e.toString()); + } + con.commit(); + + // build a String with 4001 characters + StringBuilder stringBuilder = new StringBuilder(); + for (int i = 0; i < 4001; i++) { + stringBuilder.append('c'); + } + String largeString = stringBuilder.toString(); + + String[] values = {"a", "b", largeString, "d", "e"}; + // insert five rows into the table; use a batch for each row + try { + Field f1 = con.getClass().getSuperclass().getDeclaredField("isAzureDW"); + f1.setAccessible(true); + f1.set(con, true); + pstmt = con.prepareStatement("insert into " + testTable + " values (?,?)"); + // 0,a + pstmt.setInt(1, 0); + pstmt.setNString(2, values[0]); + pstmt.addBatch(); + + // 1,b + pstmt.setInt(1, 1); + pstmt.setNString(2, values[1]); + pstmt.addBatch(); + + // 2,ccc... + pstmt.setInt(1, 2); + pstmt.setNString(2, values[2]); + pstmt.addBatch(); + + // 3,d + pstmt.setInt(1, 3); + pstmt.setNString(2, values[3]); + pstmt.addBatch(); + + // 4,e + pstmt.setInt(1, 4); + pstmt.setNString(2, values[4]); + pstmt.addBatch(); + + pstmt.executeBatch(); + } + catch (Exception e) { + fail(e.toString()); + } + + connection.commit(); + + // check the data in the table + Map selectedValues = new LinkedHashMap<>(); + int id = 0; + try { + pstmt = con.prepareStatement("select * from " + testTable + ";"); + try { + rs = pstmt.executeQuery(); + int i = 0; + while (rs.next()) { + id = rs.getInt(1); + String data = rs.getNString(2); + if (selectedValues.containsKey(id)) { + fail("Found duplicate id: " + id + " ,actual values is : " + values[i++] + " data is: " + data); + } + selectedValues.put(id, data); + } + } + finally { + if (null != rs) { + rs.close(); + } + } + } + finally { + Utils.dropTableIfExists(testTable, stmt); + if (null != pstmt) { + pstmt.close(); + } + if (null != stmt) { + stmt.close(); + } + } + + } /** * Test with large string and tests with more batch queries * diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/BatchExecuteWithErrorsTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/BatchExecuteWithErrorsTest.java index 38c16ce95..facdeb0b3 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/BatchExecuteWithErrorsTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/BatchExecuteWithErrorsTest.java @@ -14,6 +14,7 @@ import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.Assumptions.assumeTrue; +import java.lang.reflect.Field; import java.sql.BatchUpdateException; import java.sql.Connection; import java.sql.DriverManager; @@ -263,6 +264,226 @@ public void Repro47239() throws SQLException { conn.close(); } + /** + * Batch test + * + * @throws SQLException + * @throws SecurityException + * @throws NoSuchFieldException + * @throws IllegalAccessException + * @throws IllegalArgumentException + */ + @Test + @DisplayName("Batch Test") + public void Repro47239UseBulkCopyAPI() throws SQLException, NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException { + String tableN = RandomUtil.getIdentifier("t_Repro47239"); + final String tableName = AbstractSQLGenerator.escapeIdentifier(tableN); + final String insertStmt = "INSERT INTO " + tableName + " VALUES (999, 'HELLO', '4/12/1994')"; + final String error16 = "RAISERROR ('raiserror level 16',16,42)"; + final String select = "SELECT 1"; + final String dateConversionError = "insert into " + tableName + " values (999999, 'Hello again', 'asdfasdf')"; + + String warning; + String error; + String severe; + con = DriverManager.getConnection(connectionString); + Field f1 = con.getClass().getSuperclass().getDeclaredField("isAzureDW"); + f1.setAccessible(true); + f1.set(con, true); + if (DBConnection.isSqlAzure(con)) { + // SQL Azure will throw exception for "raiserror WITH LOG", so the following RAISERROR statements have not "with log" option + warning = "RAISERROR ('raiserror level 4',4,1)"; + error = "RAISERROR ('raiserror level 11',11,1)"; + // On SQL Azure, raising FATAL error by RAISERROR() is not supported and there is no way to + // cut the current connection by a statement inside a SQL batch. + // Details: Although one can simulate a fatal error (that cuts the connections) by dropping the database, + // this simulation cannot be written entirely in TSQL (because it needs a new connection), + // and thus it cannot be put into a TSQL batch and it is useless here. + // So we have to skip the last scenario of this test case, i.e. "Test Severe (connection-closing) errors" + // It is worthwhile to still execute the first 5 test scenarios of this test case, in order to have best test coverage. + severe = "--Not executed when testing against SQL Azure"; // this is a dummy statement that never being executed on SQL Azure + } + else { + warning = "RAISERROR ('raiserror level 4',4,1) WITH LOG"; + error = "RAISERROR ('raiserror level 11',11,1) WITH LOG"; + severe = "RAISERROR ('raiserror level 20',20,1) WITH LOG"; + } + con.close(); + + int[] actualUpdateCounts; + int[] expectedUpdateCounts; + String actualExceptionText; + + // SQL Server 2005 driver + try { + Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver"); + } + catch (ClassNotFoundException e1) { + fail(e1.toString()); + } + Connection conn = DriverManager.getConnection(connectionString); + Statement stmt = conn.createStatement(); + try { + stmt.executeUpdate("drop table " + tableName); + } + catch (Exception ignored) { + } + stmt.executeUpdate( + "create table " + tableName + " (c1_int int, c2_varchar varchar(20), c3_date datetime, c4_int int identity(1,1) primary key)"); + + // Regular Statement batch update + expectedUpdateCounts = new int[] {1, -2, 1, -2, 1, -2}; + Statement batchStmt = conn.createStatement(); + batchStmt.addBatch(insertStmt); + batchStmt.addBatch(warning); + batchStmt.addBatch(insertStmt); + batchStmt.addBatch(warning); + batchStmt.addBatch(insertStmt); + batchStmt.addBatch(warning); + try { + actualUpdateCounts = batchStmt.executeBatch(); + actualExceptionText = ""; + } + catch (BatchUpdateException bue) { + actualUpdateCounts = bue.getUpdateCounts(); + actualExceptionText = bue.getMessage(); + if (log.isLoggable(Level.FINE)) { + log.fine("BatchUpdateException occurred. Message:" + actualExceptionText); + } + } + finally { + batchStmt.close(); + } + if (log.isLoggable(Level.FINE)) { + log.fine("UpdateCounts:"); + } + for (int updateCount : actualUpdateCounts) { + log.fine("" + updateCount + ","); + } + log.fine(""); + assertTrue(Arrays.equals(actualUpdateCounts, expectedUpdateCounts), "Test interleaved inserts and warnings"); + + expectedUpdateCounts = new int[] {-3, 1, 1, 1}; + stmt.addBatch(error); + stmt.addBatch(insertStmt); + stmt.addBatch(insertStmt); + stmt.addBatch(insertStmt); + try { + actualUpdateCounts = stmt.executeBatch(); + actualExceptionText = ""; + } + catch (BatchUpdateException bue) { + actualUpdateCounts = bue.getUpdateCounts(); + actualExceptionText = bue.getMessage(); + } + log.fine("UpdateCounts:"); + for (int updateCount : actualUpdateCounts) { + log.fine("" + updateCount + ","); + } + log.fine(""); + assertTrue(Arrays.equals(actualUpdateCounts, expectedUpdateCounts), "Test error followed by inserts"); + // 50280 + expectedUpdateCounts = new int[] {1, -3}; + stmt.addBatch(insertStmt); + stmt.addBatch(error16); + try { + actualUpdateCounts = stmt.executeBatch(); + actualExceptionText = ""; + } + catch (BatchUpdateException bue) { + actualUpdateCounts = bue.getUpdateCounts(); + actualExceptionText = bue.getMessage(); + } + for (int updateCount : actualUpdateCounts) { + log.fine("" + updateCount + ","); + } + log.fine(""); + assertTrue(Arrays.equals(actualUpdateCounts, expectedUpdateCounts), "Test insert followed by non-fatal error (50280)"); + + // Test "soft" errors + conn.setAutoCommit(false); + stmt.addBatch(select); + stmt.addBatch(insertStmt); + stmt.addBatch(select); + stmt.addBatch(insertStmt); + try { + stmt.executeBatch(); + assertEquals(true, false, "Soft error test: executeBatch unexpectedly succeeded"); + } + catch (BatchUpdateException bue) { + assertEquals("A result set was generated for update.", bue.getMessage(), "Soft error test: wrong error message in BatchUpdateException"); + assertEquals(Arrays.equals(bue.getUpdateCounts(), new int[] {-3, 1, -3, 1}), true, + "Soft error test: wrong update counts in BatchUpdateException"); + } + conn.rollback(); + + // Defect 128801: Rollback (with conversion error) should throw SQLException + stmt.addBatch(dateConversionError); + stmt.addBatch(insertStmt); + stmt.addBatch(insertStmt); + stmt.addBatch(insertStmt); + try { + stmt.executeBatch(); + } + catch (BatchUpdateException bue) { + assertThat(bue.getMessage(), containsString("Syntax error converting date")); + // CTestLog.CompareStartsWith(bue.getMessage(), "Syntax error converting date", "Transaction rollback with conversion error threw wrong + // BatchUpdateException"); + } + catch (SQLException e) { + assertThat(e.getMessage(), containsString("Conversion failed when converting date")); + // CTestLog.CompareStartsWith(e.getMessage(), "Conversion failed when converting date", "Transaction rollback with conversion error threw + // wrong SQLException"); + } + + conn.setAutoCommit(true); + + // On SQL Azure, raising FATAL error by RAISERROR() is not supported and there is no way to + // cut the current connection by a statement inside a SQL batch. + // Details: Although one can simulate a fatal error (that cuts the connections) by dropping the database, + // this simulation cannot be written entirely in TSQL (because it needs a new connection), + // and thus it cannot be put into a TSQL batch and it is useless here. + // So we have to skip the last scenario of this test case, i.e. "Test Severe (connection-closing) errors" + // It is worthwhile to still execute the first 5 test scenarios of this test case, in order to have best test coverage. + if (!DBConnection.isSqlAzure(conn)) { + // Test Severe (connection-closing) errors + stmt.addBatch(error); + stmt.addBatch(insertStmt); + stmt.addBatch(warning); + // TODO Removed until ResultSet refactoring task (45832) is complete. + // stmt.addBatch(select); // error: select not permitted in batch + stmt.addBatch(insertStmt); + stmt.addBatch(severe); + stmt.addBatch(insertStmt); + stmt.addBatch(insertStmt); + try { + stmt.executeBatch(); + assertEquals(false, true, "Test fatal errors batch execution succeeded (should have failed)"); + } + catch (BatchUpdateException bue) { + assertEquals(false, true, "Test fatal errors returned BatchUpdateException rather than SQLException"); + } + catch (SQLException e) { + actualExceptionText = e.getMessage(); + + if (actualExceptionText.endsWith("reset")) { + assertTrue(actualExceptionText.equalsIgnoreCase("Connection reset"), "Test fatal errors"); + } + else { + assertTrue(actualExceptionText.equalsIgnoreCase("raiserror level 20"), "Test fatal errors"); + } + } + } + + try { + stmt.executeUpdate("drop table " + tableName); + } + catch (Exception ignored) { + } + stmt.close(); + conn.close(); + } + /** * Tests large methods, supported in 42 * @@ -463,4 +684,208 @@ public void Repro47239large() throws Exception { stmt.close(); conn.close(); } + + /** + * Tests large methods, supported in 42 + * + * @throws Exception + */ + @Test + @DisplayName("Regression test for using 'large' methods") + public void Repro47239largeUseBulkCopyAPI() throws Exception { + + assumeTrue("JDBC42".equals(Utils.getConfiguredProperty("JDBC_Version")), "Aborting test case as JDBC version is not compatible. "); + // the DBConnection for detecting whether the server is SQL Azure or SQL Server. + con = DriverManager.getConnection(connectionString); + Field f1 = con.getClass().getSuperclass().getDeclaredField("isAzureDW"); + f1.setAccessible(true); + f1.set(con, true); + final String warning; + final String error; + final String severe; + if (DBConnection.isSqlAzure(con)) { + // SQL Azure will throw exception for "raiserror WITH LOG", so the following RAISERROR statements have not "with log" option + warning = "RAISERROR ('raiserror level 4',4,1)"; + error = "RAISERROR ('raiserror level 11',11,1)"; + // On SQL Azure, raising FATAL error by RAISERROR() is not supported and there is no way to + // cut the current connection by a statement inside a SQL batch. + // Details: Although one can simulate a fatal error (that cuts the connections) by dropping the database, + // this simulation cannot be written entirely in TSQL (because it needs a new connection), + // and thus it cannot be put into a TSQL batch and it is useless here. + // So we have to skip the last scenario of this test case, i.e. "Test Severe (connection-closing) errors" + // It is worthwhile to still execute the first 5 test scenarios of this test case, in order to have best test coverage. + severe = "--Not executed when testing against SQL Azure"; // this is a dummy statement that never being executed on SQL Azure + } + else { + warning = "RAISERROR ('raiserror level 4',4,1) WITH LOG"; + error = "RAISERROR ('raiserror level 11',11,1) WITH LOG"; + severe = "RAISERROR ('raiserror level 20',20,1) WITH LOG"; + } + con.close(); + + long[] actualUpdateCounts; + long[] expectedUpdateCounts; + String actualExceptionText; + + // SQL Server 2005 driver + Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver"); + Connection conn = DriverManager.getConnection(connectionString); + Statement stmt = conn.createStatement(); + + try { + stmt.executeLargeUpdate("drop table " + tableName); + } + catch (Exception ignored) { + } + try { + stmt.executeLargeUpdate( + "create table " + tableName + " (c1_int int, c2_varchar varchar(20), c3_date datetime, c4_int int identity(1,1) primary key)"); + } + catch (Exception ignored) { + } + // Regular Statement batch update + expectedUpdateCounts = new long[] {1, -2, 1, -2, 1, -2}; + Statement batchStmt = conn.createStatement(); + batchStmt.addBatch(insertStmt); + batchStmt.addBatch(warning); + batchStmt.addBatch(insertStmt); + batchStmt.addBatch(warning); + batchStmt.addBatch(insertStmt); + batchStmt.addBatch(warning); + try { + actualUpdateCounts = batchStmt.executeLargeBatch(); + actualExceptionText = ""; + } + catch (BatchUpdateException bue) { + actualUpdateCounts = bue.getLargeUpdateCounts(); + actualExceptionText = bue.getMessage(); + log.fine("BatchUpdateException occurred. Message:" + actualExceptionText); + } + finally { + batchStmt.close(); + } + log.fine("UpdateCounts:"); + for (long updateCount : actualUpdateCounts) { + log.fine("" + updateCount + ","); + } + log.fine(""); + assertTrue(Arrays.equals(actualUpdateCounts, expectedUpdateCounts), "Test interleaved inserts and warnings"); + + expectedUpdateCounts = new long[] {-3, 1, 1, 1}; + stmt.addBatch(error); + stmt.addBatch(insertStmt); + stmt.addBatch(insertStmt); + stmt.addBatch(insertStmt); + try { + actualUpdateCounts = stmt.executeLargeBatch(); + actualExceptionText = ""; + } + catch (BatchUpdateException bue) { + actualUpdateCounts = bue.getLargeUpdateCounts(); + actualExceptionText = bue.getMessage(); + } + log.fine("UpdateCounts:"); + for (long updateCount : actualUpdateCounts) { + log.fine("" + updateCount + ","); + } + log.fine(""); + assertTrue(Arrays.equals(actualUpdateCounts, expectedUpdateCounts), "Test error followed by inserts"); + + // 50280 + expectedUpdateCounts = new long[] {1, -3}; + stmt.addBatch(insertStmt); + stmt.addBatch(error16); + try { + actualUpdateCounts = stmt.executeLargeBatch(); + actualExceptionText = ""; + } + catch (BatchUpdateException bue) { + actualUpdateCounts = bue.getLargeUpdateCounts(); + actualExceptionText = bue.getMessage(); + } + for (long updateCount : actualUpdateCounts) { + log.fine("" + updateCount + ","); + } + log.fine(""); + assertTrue(Arrays.equals(actualUpdateCounts, expectedUpdateCounts), "Test insert followed by non-fatal error (50280)"); + + // Test "soft" errors + conn.setAutoCommit(false); + stmt.addBatch(select); + stmt.addBatch(insertStmt); + stmt.addBatch(select); + stmt.addBatch(insertStmt); + try { + stmt.executeLargeBatch(); + assertEquals(false, true, "Soft error test: executeLargeBatch unexpectedly succeeded"); + } + catch (BatchUpdateException bue) { + assertEquals("A result set was generated for update.", bue.getMessage(), "Soft error test: wrong error message in BatchUpdateException"); + assertEquals(Arrays.equals(bue.getLargeUpdateCounts(), new long[] {-3, 1, -3, 1}), true, + "Soft error test: wrong update counts in BatchUpdateException"); + } + conn.rollback(); + + // Defect 128801: Rollback (with conversion error) should throw SQLException + stmt.addBatch(dateConversionError); + stmt.addBatch(insertStmt); + stmt.addBatch(insertStmt); + stmt.addBatch(insertStmt); + try { + stmt.executeLargeBatch(); + } + catch (BatchUpdateException bue) { + assertThat(bue.getMessage(), containsString("Syntax error converting date")); + } + catch (SQLException e) { + assertThat(e.getMessage(), containsString("Conversion failed when converting date")); + } + + conn.setAutoCommit(true); + + // On SQL Azure, raising FATAL error by RAISERROR() is not supported and there is no way to + // cut the current connection by a statement inside a SQL batch. + // Details: Although one can simulate a fatal error (that cuts the connections) by dropping the database, + // this simulation cannot be written entirely in TSQL (because it needs a new connection), + // and thus it cannot be put into a TSQL batch and it is useless here. + // So we have to skip the last scenario of this test case, i.e. "Test Severe (connection-closing) errors" + // It is worthwhile to still execute the first 5 test scenarios of this test case, in order to have best test coverage. + if (!DBConnection.isSqlAzure(DriverManager.getConnection(connectionString))) { + // Test Severe (connection-closing) errors + stmt.addBatch(error); + stmt.addBatch(insertStmt); + stmt.addBatch(warning); + + stmt.addBatch(insertStmt); + stmt.addBatch(severe); + stmt.addBatch(insertStmt); + stmt.addBatch(insertStmt); + try { + stmt.executeLargeBatch(); + assertEquals(false, true, "Test fatal errors batch execution succeeded (should have failed)"); + } + catch (BatchUpdateException bue) { + assertEquals(false, true, "Test fatal errors returned BatchUpdateException rather than SQLException"); + } + catch (SQLException e) { + actualExceptionText = e.getMessage(); + + if (actualExceptionText.endsWith("reset")) { + assertTrue(actualExceptionText.equalsIgnoreCase("Connection reset"), "Test fatal errors"); + } + else { + assertTrue(actualExceptionText.equalsIgnoreCase("raiserror level 20"), "Test fatal errors"); + + } + } + } + + try { + stmt.executeLargeUpdate("drop table " + tableName); + } + catch (Exception ignored) { + } + stmt.close(); + conn.close(); + } } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/BatchExecutionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/BatchExecutionTest.java index 85eb4a91b..70cb48fc5 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/BatchExecutionTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/BatchExecutionTest.java @@ -11,6 +11,7 @@ import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.Assumptions.assumeTrue; +import java.lang.reflect.Field; import java.sql.BatchUpdateException; import java.sql.Connection; import java.sql.DriverManager; @@ -53,6 +54,8 @@ public class BatchExecutionTest extends AbstractTest { public void testBatchExceptionAEOn() throws Exception { testAddBatch1(); testExecuteBatch1(); + testAddBatch1UseBulkCopyAPI(); + testExecuteBatch1UseBulkCopyAPI(); } /** @@ -110,6 +113,65 @@ public void testAddBatch1() { fail("Call to addBatch is Failed!"); } } + + /** + * Get a PreparedStatement object and call the addBatch() method with 3 SQL statements and call the executeBatch() method and it should return + * array of Integer values of length 3 + */ + public void testAddBatch1UseBulkCopyAPI() { + int i = 0; + int retValue[] = {0, 0, 0}; + try { + String sPrepStmt = "update ctstable2 set PRICE=PRICE*20 where TYPE_ID=?"; + Field f1 = connection.getClass().getSuperclass().getDeclaredField("isAzureDW"); + f1.setAccessible(true); + f1.set(connection, true); + pstmt = connection.prepareStatement(sPrepStmt); + pstmt.setInt(1, 2); + pstmt.addBatch(); + + pstmt.setInt(1, 3); + pstmt.addBatch(); + + pstmt.setInt(1, 4); + pstmt.addBatch(); + + int[] updateCount = pstmt.executeBatch(); + int updateCountlen = updateCount.length; + + assertTrue(updateCountlen == 3, "addBatch does not add the SQL Statements to Batch ,call to addBatch failed"); + + String sPrepStmt1 = "select count(*) from ctstable2 where TYPE_ID=?"; + + pstmt1 = connection.prepareStatement(sPrepStmt1); + + // 2 is the number that is set First for Type Id in Prepared Statement + for (int n = 2; n <= 4; n++) { + pstmt1.setInt(1, n); + rs = pstmt1.executeQuery(); + rs.next(); + retValue[i++] = rs.getInt(1); + } + + pstmt1.close(); + + for (int j = 0; j < updateCount.length; j++) { + + if (updateCount[j] != retValue[j] && updateCount[j] != Statement.SUCCESS_NO_INFO) { + fail("affected row count does not match with the updateCount value, Call to addBatch is Failed!"); + } + } + } + catch (BatchUpdateException b) { + fail("BatchUpdateException : Call to addBatch is Failed!"); + } + catch (SQLException sqle) { + fail("Call to addBatch is Failed!"); + } + catch (Exception e) { + fail("Call to addBatch is Failed!"); + } + } /** * Get a PreparedStatement object and call the addBatch() method with a 3 valid SQL statements and call the executeBatch() method It should return @@ -166,6 +228,61 @@ public void testExecuteBatch1() { fail("Call to executeBatch is Failed!"); } } + + public void testExecuteBatch1UseBulkCopyAPI() { + int i = 0; + int retValue[] = {0, 0, 0}; + int updCountLength = 0; + try { + String sPrepStmt = "update ctstable2 set PRICE=PRICE*20 where TYPE_ID=?"; + + pstmt = connection.prepareStatement(sPrepStmt); + Field f1 = connection.getClass().getSuperclass().getDeclaredField("isAzureDW"); + f1.setAccessible(true); + f1.set(connection, true); + pstmt.setInt(1, 1); + pstmt.addBatch(); + + pstmt.setInt(1, 2); + pstmt.addBatch(); + + pstmt.setInt(1, 3); + pstmt.addBatch(); + + int[] updateCount = pstmt.executeBatch(); + updCountLength = updateCount.length; + + assertTrue(updCountLength == 3, "executeBatch does not execute the Batch of SQL statements, Call to executeBatch is Failed!"); + + String sPrepStmt1 = "select count(*) from ctstable2 where TYPE_ID=?"; + + pstmt1 = connection.prepareStatement(sPrepStmt1); + + for (int n = 1; n <= 3; n++) { + pstmt1.setInt(1, n); + rs = pstmt1.executeQuery(); + rs.next(); + retValue[i++] = rs.getInt(1); + } + + pstmt1.close(); + + for (int j = 0; j < updateCount.length; j++) { + if (updateCount[j] != retValue[j] && updateCount[j] != Statement.SUCCESS_NO_INFO) { + fail("executeBatch does not execute the Batch of SQL statements, Call to executeBatch is Failed!"); + } + } + } + catch (BatchUpdateException b) { + fail("BatchUpdateException : Call to executeBatch is Failed!"); + } + catch (SQLException sqle) { + fail("Call to executeBatch is Failed!"); + } + catch (Exception e) { + fail("Call to executeBatch is Failed!"); + } + } private static void createTable() throws SQLException { String sql1 = "create table ctstable1 (TYPE_ID int, TYPE_DESC varchar(32), primary key(TYPE_ID)) "; diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/PreparedStatementTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/PreparedStatementTest.java index e4e0d09f0..c1f811709 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/PreparedStatementTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/PreparedStatementTest.java @@ -14,6 +14,7 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.fail; +import java.lang.reflect.Field; import java.sql.BatchUpdateException; import java.sql.DriverManager; import java.sql.ResultSet; @@ -294,6 +295,183 @@ public void testStatementPooling() throws SQLException { } } + /** + * Test handling of statement pooling for prepared statements. + * + * @throws SQLException + * @throws SecurityException + * @throws NoSuchFieldException + * @throws IllegalAccessException + * @throws IllegalArgumentException + */ + @Test + @Tag("slow") + public void testStatementPoolingUseBulkCopyAPI() throws SQLException, NoSuchFieldException, SecurityException, + IllegalArgumentException, IllegalAccessException { + // Test % handle re-use + try (SQLServerConnection con = (SQLServerConnection)DriverManager.getConnection(connectionString)) { + Field f1 = con.getClass().getSuperclass().getDeclaredField("isAzureDW"); + f1.setAccessible(true); + f1.set(con, true); + String query = String.format("/*statementpoolingtest_re-use_%s*/SELECT TOP(1) * FROM sys.tables;", UUID.randomUUID().toString()); + + con.setStatementPoolingCacheSize(10); + + boolean[] prepOnFirstCalls = {false, true}; + + for(boolean prepOnFirstCall : prepOnFirstCalls) { + + con.setEnablePrepareOnFirstPreparedStatementCall(prepOnFirstCall); + + int[] queryCounts = {10, 20, 30, 40}; + for(int queryCount : queryCounts) { + String[] queries = new String[queryCount]; + for(int i = 0; i < queries.length; ++i) { + queries[i] = String.format("%s--%s--%s--%s", query, i, queryCount, prepOnFirstCall); + } + + int testsWithHandleReuse = 0; + final int testCount = 500; + for(int i = 0; i < testCount; ++i) { + Random random = new Random(); + int queryNumber = random.nextInt(queries.length); + try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) con.prepareStatement(queries[queryNumber])) { + pstmt.execute(); + + // Grab handle-reuse before it would be populated if initially created. + if(0 < pstmt.getPreparedStatementHandle()) + testsWithHandleReuse++; + + pstmt.getMoreResults(); // Make sure handle is updated. + } + } + System.out.println(String.format("Prep on first call: %s Query count:%s: %s of %s (%s)", prepOnFirstCall, queryCount, testsWithHandleReuse, testCount, (double)testsWithHandleReuse/(double)testCount)); + } + } + } + + try (SQLServerConnection con = (SQLServerConnection)DriverManager.getConnection(connectionString)) { + + // Test behvaior with statement pooling. + con.setStatementPoolingCacheSize(10); + this.executeSQL(con, + "IF NOT EXISTS (SELECT * FROM sys.messages WHERE message_id = 99586) EXEC sp_addmessage 99586, 16, 'Prepared handle GAH!';"); + // Test with missing handle failures (fake). + this.executeSQL(con, "CREATE TABLE #update1 (col INT);INSERT #update1 VALUES (1);"); + this.executeSQL(con, + "CREATE PROC #updateProc1 AS UPDATE #update1 SET col += 1; IF EXISTS (SELECT * FROM #update1 WHERE col % 5 = 0) RAISERROR(99586,16,1);"); + try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) con.prepareStatement("#updateProc1")) { + for (int i = 0; i < 100; ++i) { + try { + assertSame(1, pstmt.executeUpdate()); + } + catch (SQLException e) { + // Error "Prepared handle GAH" is expected to happen. But it should not terminate the execution with RAISERROR. + // Since the original "Could not find prepared statement with handle" error does not terminate the execution after it. + if (!e.getMessage().contains("Prepared handle GAH")) { + throw e; + } + } + } + } + + // test updated value, should be 1 + 100 = 101 + // although executeUpdate() throws exception, update operation should be executed successfully. + try (ResultSet rs = con.createStatement().executeQuery("select * from #update1")) { + rs.next(); + assertSame(101, rs.getInt(1)); + } + + // Test batching with missing handle failures (fake). + this.executeSQL(con, + "IF NOT EXISTS (SELECT * FROM sys.messages WHERE message_id = 99586) EXEC sp_addmessage 99586, 16, 'Prepared handle GAH!';"); + this.executeSQL(con, "CREATE TABLE #update2 (col INT);INSERT #update2 VALUES (1);"); + this.executeSQL(con, + "CREATE PROC #updateProc2 AS UPDATE #update2 SET col += 1; IF EXISTS (SELECT * FROM #update2 WHERE col % 5 = 0) RAISERROR(99586,16,1);"); + try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) con.prepareStatement("#updateProc2")) { + for (int i = 0; i < 100; ++i) { + pstmt.addBatch(); + } + + int[] updateCounts = null; + try { + updateCounts = pstmt.executeBatch(); + } + catch (BatchUpdateException e) { + // Error "Prepared handle GAH" is expected to happen. But it should not terminate the execution with RAISERROR. + // Since the original "Could not find prepared statement with handle" error does not terminate the execution after it. + if (!e.getMessage().contains("Prepared handle GAH")) { + throw e; + } + } + + // since executeBatch() throws exception, it does not return anthing. So updateCounts is still null. + assertSame(null, updateCounts); + + // test updated value, should be 1 + 100 = 101 + // although executeBatch() throws exception, update operation should be executed successfully. + try (ResultSet rs = con.createStatement().executeQuery("select * from #update2")) { + rs.next(); + assertSame(101, rs.getInt(1)); + } + } + } + + try (SQLServerConnection con = (SQLServerConnection)DriverManager.getConnection(connectionString)) { + // Test behvaior with statement pooling. + con.setDisableStatementPooling(false); + con.setStatementPoolingCacheSize(10); + + String lookupUniqueifier = UUID.randomUUID().toString(); + String query = String.format("/*statementpoolingtest_%s*/SELECT * FROM sys.tables;", lookupUniqueifier); + + // Execute statement first, should create cache entry WITHOUT handle (since sp_executesql was used). + try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement)con.prepareStatement(query)) { + pstmt.execute(); // sp_executesql + pstmt.getMoreResults(); // Make sure handle is updated. + + assertSame(0, pstmt.getPreparedStatementHandle()); + } + + // Execute statement again, should now create handle. + int handle = 0; + try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement)con.prepareStatement(query)) { + pstmt.execute(); // sp_prepexec + pstmt.getMoreResults(); // Make sure handle is updated. + + handle = pstmt.getPreparedStatementHandle(); + assertNotSame(0, handle); + } + + // Execute statement again and verify same handle was used. + try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement)con.prepareStatement(query)) { + pstmt.execute(); // sp_execute + pstmt.getMoreResults(); // Make sure handle is updated. + + assertNotSame(0, pstmt.getPreparedStatementHandle()); + assertSame(handle, pstmt.getPreparedStatementHandle()); + } + + // Execute new statement with different SQL text and verify it does NOT get same handle (should now fall back to using sp_executesql). + SQLServerPreparedStatement outer = null; + try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement)con.prepareStatement(query + ";")) { + outer = pstmt; + pstmt.execute(); // sp_executesql + pstmt.getMoreResults(); // Make sure handle is updated. + + assertSame(0, pstmt.getPreparedStatementHandle()); + assertNotSame(handle, pstmt.getPreparedStatementHandle()); + } + try { + System.out.println(outer.getPreparedStatementHandle()); + fail("Error for invalid use of getPreparedStatementHandle() after statement close expected."); + } + catch(Exception e) { + // Good! + } + } + } + /** * Test handling of eviction from statement pooling for prepared statements. * From 5cf28ad79a550c0ac84efa9f09cef07ed82684b7 Mon Sep 17 00:00:00 2001 From: Peter Bae Date: Tue, 1 May 2018 09:34:45 -0700 Subject: [PATCH 04/32] change reflection for testing --- .../sqlserver/jdbc/preparedStatement/RegressionTest.java | 4 +++- .../jdbc/unit/statement/BatchExecuteWithErrorsTest.java | 5 +++-- .../sqlserver/jdbc/unit/statement/PreparedStatementTest.java | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/RegressionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/RegressionTest.java index 4bbf8cb8d..f866bc1f8 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/RegressionTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/RegressionTest.java @@ -24,6 +24,8 @@ import org.junit.jupiter.api.Test; import org.junit.platform.runner.JUnitPlatform; import org.junit.runner.RunWith; + +import com.microsoft.sqlserver.jdbc.SQLServerConnection; import com.microsoft.sqlserver.testframework.AbstractTest; import com.microsoft.sqlserver.testframework.Utils; @@ -351,7 +353,7 @@ public void batchWithLargeStringTestUseBulkCopyAPI() throws SQLException { String[] values = {"a", "b", largeString, "d", "e"}; // insert five rows into the table; use a batch for each row try { - Field f1 = con.getClass().getSuperclass().getDeclaredField("isAzureDW"); + Field f1 = SQLServerConnection.class.getDeclaredField("isAzureDW"); f1.setAccessible(true); f1.set(con, true); pstmt = con.prepareStatement("insert into " + testTable + " values (?,?)"); diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/BatchExecuteWithErrorsTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/BatchExecuteWithErrorsTest.java index facdeb0b3..d86e08c39 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/BatchExecuteWithErrorsTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/BatchExecuteWithErrorsTest.java @@ -29,6 +29,7 @@ import org.junit.platform.runner.JUnitPlatform; import org.junit.runner.RunWith; +import com.microsoft.sqlserver.jdbc.SQLServerConnection; import com.microsoft.sqlserver.testframework.AbstractSQLGenerator; import com.microsoft.sqlserver.testframework.AbstractTest; import com.microsoft.sqlserver.testframework.DBConnection; @@ -287,7 +288,7 @@ public void Repro47239UseBulkCopyAPI() throws SQLException, NoSuchFieldException String error; String severe; con = DriverManager.getConnection(connectionString); - Field f1 = con.getClass().getSuperclass().getDeclaredField("isAzureDW"); + Field f1 = SQLServerConnection.class.getDeclaredField("isAzureDW"); f1.setAccessible(true); f1.set(con, true); if (DBConnection.isSqlAzure(con)) { @@ -697,7 +698,7 @@ public void Repro47239largeUseBulkCopyAPI() throws Exception { assumeTrue("JDBC42".equals(Utils.getConfiguredProperty("JDBC_Version")), "Aborting test case as JDBC version is not compatible. "); // the DBConnection for detecting whether the server is SQL Azure or SQL Server. con = DriverManager.getConnection(connectionString); - Field f1 = con.getClass().getSuperclass().getDeclaredField("isAzureDW"); + Field f1 = SQLServerConnection.class.getDeclaredField("isAzureDW"); f1.setAccessible(true); f1.set(con, true); final String warning; diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/PreparedStatementTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/PreparedStatementTest.java index c1f811709..440be6a87 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/PreparedStatementTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/PreparedStatementTest.java @@ -310,7 +310,7 @@ public void testStatementPoolingUseBulkCopyAPI() throws SQLException, NoSuchFiel IllegalArgumentException, IllegalAccessException { // Test % handle re-use try (SQLServerConnection con = (SQLServerConnection)DriverManager.getConnection(connectionString)) { - Field f1 = con.getClass().getSuperclass().getDeclaredField("isAzureDW"); + Field f1 = SQLServerConnection.class.getDeclaredField("isAzureDW"); f1.setAccessible(true); f1.set(con, true); String query = String.format("/*statementpoolingtest_re-use_%s*/SELECT TOP(1) * FROM sys.tables;", UUID.randomUUID().toString()); From 59d29d70aff4a590b4b5208d5866ed3c363adee9 Mon Sep 17 00:00:00 2001 From: Peter Bae Date: Tue, 1 May 2018 09:56:16 -0700 Subject: [PATCH 05/32] more test changes --- .../sqlserver/jdbc/unit/statement/BatchExecutionTest.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/BatchExecutionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/BatchExecutionTest.java index 70cb48fc5..7a9bbd437 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/BatchExecutionTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/BatchExecutionTest.java @@ -27,6 +27,7 @@ import org.junit.runner.RunWith; import org.opentest4j.TestAbortedException; +import com.microsoft.sqlserver.jdbc.SQLServerConnection; import com.microsoft.sqlserver.jdbc.SQLServerStatement; import com.microsoft.sqlserver.testframework.AbstractTest; import com.microsoft.sqlserver.testframework.DBConnection; @@ -123,7 +124,7 @@ public void testAddBatch1UseBulkCopyAPI() { int retValue[] = {0, 0, 0}; try { String sPrepStmt = "update ctstable2 set PRICE=PRICE*20 where TYPE_ID=?"; - Field f1 = connection.getClass().getSuperclass().getDeclaredField("isAzureDW"); + Field f1 = SQLServerConnection.class.getDeclaredField("isAzureDW"); f1.setAccessible(true); f1.set(connection, true); pstmt = connection.prepareStatement(sPrepStmt); @@ -237,7 +238,7 @@ public void testExecuteBatch1UseBulkCopyAPI() { String sPrepStmt = "update ctstable2 set PRICE=PRICE*20 where TYPE_ID=?"; pstmt = connection.prepareStatement(sPrepStmt); - Field f1 = connection.getClass().getSuperclass().getDeclaredField("isAzureDW"); + Field f1 = SQLServerConnection.class.getDeclaredField("isAzureDW"); f1.setAccessible(true); f1.set(connection, true); pstmt.setInt(1, 1); From dc42708ef4a3f511e5a6aa2a705ac5ba717a4711 Mon Sep 17 00:00:00 2001 From: Peter Bae Date: Tue, 1 May 2018 13:08:03 -0700 Subject: [PATCH 06/32] Add parsing logic for -- comment --- .../jdbc/SQLServerPreparedStatement.java | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java index 32b8595ac..f8455a51d 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java @@ -2634,6 +2634,12 @@ private String parseUserSQLForTableNameDW(boolean hasInsertBeenFound, boolean ha return parseUserSQLForTableNameDW(hasInsertBeenFound, hasIntoBeenFound); } + if (localUserSQL.substring(0, 2).equalsIgnoreCase("--")) { + int temp = localUserSQL.indexOf("\n") + 2; + localUserSQL = localUserSQL.substring(temp); + return parseUserSQLForTableNameDW(hasInsertBeenFound, hasIntoBeenFound); + } + if (localUserSQL.substring(0, 6).equalsIgnoreCase("insert") && !hasInsertBeenFound) { localUserSQL = localUserSQL.substring(6); return parseUserSQLForTableNameDW(true, hasIntoBeenFound); @@ -2737,6 +2743,12 @@ private ArrayList parseUserSQLForColumnListDW() { return parseUserSQLForColumnListDW(); } + if (localUserSQL.substring(0, 2).equalsIgnoreCase("--")) { + int temp = localUserSQL.indexOf("\n") + 2; + localUserSQL = localUserSQL.substring(temp); + return parseUserSQLForColumnListDW(); + } + //check if optional column list was provided // Columns can have the form of c1, [c1] or "c1". It can escape ] or " by ]] or "". if (localUserSQL.substring(0, 1).equalsIgnoreCase("(")) { @@ -2756,6 +2768,12 @@ private ArrayList parseUserSQLForColumnListDWHelper(ArrayList li return parseUserSQLForColumnListDWHelper(listOfColumns); } + if (localUserSQL.substring(0, 2).equalsIgnoreCase("--")) { + int temp = localUserSQL.indexOf("\n") + 2; + localUserSQL = localUserSQL.substring(temp); + return parseUserSQLForColumnListDWHelper(listOfColumns); + } + if (localUserSQL.charAt(0) == ')') { localUserSQL = localUserSQL.substring(1); return listOfColumns; @@ -2836,6 +2854,12 @@ private ArrayList parseUserSQLForValueListDW(boolean hasValuesBeenFound) return parseUserSQLForValueListDW(hasValuesBeenFound); } + if (localUserSQL.substring(0, 2).equalsIgnoreCase("--")) { + int temp = localUserSQL.indexOf("\n") + 2; + localUserSQL = localUserSQL.substring(temp); + return parseUserSQLForValueListDW(hasValuesBeenFound); + } + if (!hasValuesBeenFound) { // look for keyword "VALUES" if (localUserSQL.substring(0, 6).equalsIgnoreCase("VALUES")) { @@ -2850,6 +2874,12 @@ private ArrayList parseUserSQLForValueListDW(boolean hasValuesBeenFound) return parseUserSQLForValueListDW(true); } + if (localUserSQL.substring(0, 2).equalsIgnoreCase("--")) { + int temp = localUserSQL.indexOf("\n") + 2; + localUserSQL = localUserSQL.substring(temp); + return parseUserSQLForValueListDW(true); + } + if (localUserSQL.substring(0, 1).equalsIgnoreCase("(")) { localUserSQL = localUserSQL.substring(1); return parseUserSQLForValueListDWHelper(new ArrayList()); @@ -2863,6 +2893,12 @@ private ArrayList parseUserSQLForValueListDW(boolean hasValuesBeenFound) return parseUserSQLForValueListDW(hasValuesBeenFound); } + if (localUserSQL.substring(0, 2).equalsIgnoreCase("--")) { + int temp = localUserSQL.indexOf("\n") + 2; + localUserSQL = localUserSQL.substring(temp); + return parseUserSQLForValueListDW(hasValuesBeenFound); + } + if (localUserSQL.substring(0, 1).equalsIgnoreCase("(")) { localUserSQL = localUserSQL.substring(1); return parseUserSQLForValueListDWHelper(new ArrayList()); @@ -2883,6 +2919,11 @@ private ArrayList parseUserSQLForValueListDWHelper(ArrayList lis return parseUserSQLForValueListDWHelper(listOfValues); } + if (localUserSQL.substring(0, 2).equalsIgnoreCase("--")) { + int temp = localUserSQL.indexOf("\n") + 2; + localUserSQL = localUserSQL.substring(temp); + return parseUserSQLForValueListDWHelper(listOfValues); + } if (localUserSQL.charAt(0) == ')') { localUserSQL = localUserSQL.substring(1); @@ -2928,6 +2969,10 @@ private ArrayList parseUserSQLForValueListDWHelper(ArrayList lis int temp = localUserSQL.indexOf("*/") + 2; localUserSQL = localUserSQL.substring(temp); localUserSQL = localUserSQL.trim(); + } else if (localUserSQL.substring(0, 2).equalsIgnoreCase("--")) { + int temp = localUserSQL.indexOf("\n") + 2; + localUserSQL = localUserSQL.substring(temp); + localUserSQL = localUserSQL.trim(); } else { sb.append(localUserSQL.charAt(0)); localUserSQL = localUserSQL.substring(1); From dca4cb521bdd3455014528c278d2856c5308f1d9 Mon Sep 17 00:00:00 2001 From: Peter Bae Date: Thu, 3 May 2018 14:31:51 -0700 Subject: [PATCH 07/32] refactoring --- .../jdbc/SQLServerPreparedStatement.java | 99 +++++-------------- .../sqlserver/jdbc/SQLServerStatement.java | 3 + 2 files changed, 28 insertions(+), 74 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java index 632771c92..ee410fb9e 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java @@ -2628,15 +2628,7 @@ private String parseUserSQLForTableNameDW(boolean hasInsertBeenFound, boolean ha // And there could be in-line comments (with /* and */) in between. // This method assumes the localUserSQL string starts with "insert". localUserSQL = localUserSQL.trim(); - if (localUserSQL.substring(0, 2).equalsIgnoreCase("/*")) { - int temp = localUserSQL.indexOf("*/") + 2; - localUserSQL = localUserSQL.substring(temp); - return parseUserSQLForTableNameDW(hasInsertBeenFound, hasIntoBeenFound); - } - - if (localUserSQL.substring(0, 2).equalsIgnoreCase("--")) { - int temp = localUserSQL.indexOf("\n") + 2; - localUserSQL = localUserSQL.substring(temp); + if (checkAndRemoveComments()) { return parseUserSQLForTableNameDW(hasInsertBeenFound, hasIntoBeenFound); } @@ -2737,15 +2729,7 @@ private ArrayList parseUserSQLForColumnListDW() { localUserSQL = localUserSQL.trim(); // ignore all comments - if (localUserSQL.substring(0, 2).equalsIgnoreCase("/*")) { - int temp = localUserSQL.indexOf("*/") + 2; - localUserSQL = localUserSQL.substring(temp); - return parseUserSQLForColumnListDW(); - } - - if (localUserSQL.substring(0, 2).equalsIgnoreCase("--")) { - int temp = localUserSQL.indexOf("\n") + 2; - localUserSQL = localUserSQL.substring(temp); + if (checkAndRemoveComments()) { return parseUserSQLForColumnListDW(); } @@ -2762,15 +2746,7 @@ private ArrayList parseUserSQLForColumnListDWHelper(ArrayList li localUserSQL = localUserSQL.trim(); // ignore all comments - if (localUserSQL.substring(0, 2).equalsIgnoreCase("/*")) { - int temp = localUserSQL.indexOf("*/") + 2; - localUserSQL = localUserSQL.substring(temp); - return parseUserSQLForColumnListDWHelper(listOfColumns); - } - - if (localUserSQL.substring(0, 2).equalsIgnoreCase("--")) { - int temp = localUserSQL.indexOf("\n") + 2; - localUserSQL = localUserSQL.substring(temp); + if (checkAndRemoveComments()) { return parseUserSQLForColumnListDWHelper(listOfColumns); } @@ -2829,9 +2805,7 @@ private ArrayList parseUserSQLForColumnListDWHelper(ArrayList li listOfColumns.add(sb.toString()); return listOfColumns; } - } else if (localUserSQL.substring(0, 2).equalsIgnoreCase("/*")) { - int temp = localUserSQL.indexOf("*/") + 2; - localUserSQL = localUserSQL.substring(temp); + } else if (checkAndRemoveComments()) { localUserSQL = localUserSQL.trim(); } else { sb.append(localUserSQL.charAt(0)); @@ -2848,15 +2822,7 @@ private ArrayList parseUserSQLForValueListDW(boolean hasValuesBeenFound) localUserSQL = localUserSQL.trim(); // ignore all comments - if (localUserSQL.substring(0, 2).equalsIgnoreCase("/*")) { - int temp = localUserSQL.indexOf("*/") + 2; - localUserSQL = localUserSQL.substring(temp); - return parseUserSQLForValueListDW(hasValuesBeenFound); - } - - if (localUserSQL.substring(0, 2).equalsIgnoreCase("--")) { - int temp = localUserSQL.indexOf("\n") + 2; - localUserSQL = localUserSQL.substring(temp); + if (checkAndRemoveComments()) { return parseUserSQLForValueListDW(hasValuesBeenFound); } @@ -2868,15 +2834,7 @@ private ArrayList parseUserSQLForValueListDW(boolean hasValuesBeenFound) localUserSQL = localUserSQL.trim(); // ignore all comments - if (localUserSQL.substring(0, 2).equalsIgnoreCase("/*")) { - int temp = localUserSQL.indexOf("*/") + 2; - localUserSQL = localUserSQL.substring(temp); - return parseUserSQLForValueListDW(true); - } - - if (localUserSQL.substring(0, 2).equalsIgnoreCase("--")) { - int temp = localUserSQL.indexOf("\n") + 2; - localUserSQL = localUserSQL.substring(temp); + if (checkAndRemoveComments()) { return parseUserSQLForValueListDW(true); } @@ -2887,15 +2845,7 @@ private ArrayList parseUserSQLForValueListDW(boolean hasValuesBeenFound) } } else { // ignore all comments - if (localUserSQL.substring(0, 2).equalsIgnoreCase("/*")) { - int temp = localUserSQL.indexOf("*/") + 2; - localUserSQL = localUserSQL.substring(temp); - return parseUserSQLForValueListDW(hasValuesBeenFound); - } - - if (localUserSQL.substring(0, 2).equalsIgnoreCase("--")) { - int temp = localUserSQL.indexOf("\n") + 2; - localUserSQL = localUserSQL.substring(temp); + if (checkAndRemoveComments()) { return parseUserSQLForValueListDW(hasValuesBeenFound); } @@ -2913,18 +2863,10 @@ private ArrayList parseUserSQLForValueListDWHelper(ArrayList lis localUserSQL = localUserSQL.trim(); // ignore all comments - if (localUserSQL.substring(0, 2).equalsIgnoreCase("/*")) { - int temp = localUserSQL.indexOf("*/") + 2; - localUserSQL = localUserSQL.substring(temp); - return parseUserSQLForValueListDWHelper(listOfValues); - } - - if (localUserSQL.substring(0, 2).equalsIgnoreCase("--")) { - int temp = localUserSQL.indexOf("\n") + 2; - localUserSQL = localUserSQL.substring(temp); + if (checkAndRemoveComments()) { return parseUserSQLForValueListDWHelper(listOfValues); } - + if (localUserSQL.charAt(0) == ')') { localUserSQL = localUserSQL.substring(1); return listOfValues; @@ -2965,13 +2907,7 @@ private ArrayList parseUserSQLForValueListDWHelper(ArrayList lis listOfValues.add(sb.toString()); return listOfValues; } - } else if (localUserSQL.substring(0, 2).equalsIgnoreCase("/*")) { - int temp = localUserSQL.indexOf("*/") + 2; - localUserSQL = localUserSQL.substring(temp); - localUserSQL = localUserSQL.trim(); - } else if (localUserSQL.substring(0, 2).equalsIgnoreCase("--")) { - int temp = localUserSQL.indexOf("\n") + 2; - localUserSQL = localUserSQL.substring(temp); + } else if (checkAndRemoveComments()) { localUserSQL = localUserSQL.trim(); } else { sb.append(localUserSQL.charAt(0)); @@ -2982,6 +2918,21 @@ private ArrayList parseUserSQLForValueListDWHelper(ArrayList lis // It shouldn't come here. If we did, something is wrong. throw new IllegalArgumentException("localUserSQL"); } + + private boolean checkAndRemoveComments() { + if (localUserSQL.substring(0, 2).equalsIgnoreCase("/*")) { + int temp = localUserSQL.indexOf("*/") + 2; + localUserSQL = localUserSQL.substring(temp); + return true; + } + + if (localUserSQL.substring(0, 2).equalsIgnoreCase("--")) { + int temp = localUserSQL.indexOf("\n") + 2; + localUserSQL = localUserSQL.substring(temp); + return true; + } + return false; + } private final class PrepStmtBatchExecCmd extends TDSCommand { private final SQLServerPreparedStatement stmt; diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java index 031981dc2..fc531ae49 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java @@ -989,6 +989,9 @@ final void resetForReexecute() throws SQLServerException { // Used to check just the first letter which would cause // "Set" commands to return true... String temp = sql.trim(); + if (null == sql || sql.length() < 3) { + return false; + } if (temp.substring(0, 2).equalsIgnoreCase("/*")) { int index = temp.indexOf("*/") + 2; return isInsert(temp.substring(index)); From b67ccfb171758d18ba52144ce74bf307a18f93ea Mon Sep 17 00:00:00 2001 From: Peter Bae Date: Tue, 8 May 2018 15:13:57 -0700 Subject: [PATCH 08/32] Bug fix / testing change --- .../sqlserver/jdbc/SQLServerBulkCopy.java | 17 ++++++++++++++--- .../jdbc/SQLServerPreparedStatement.java | 9 ++++++++- .../sqlserver/jdbc/SQLServerStatement.java | 3 +++ 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java index 27c608d30..846f4c360 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java @@ -153,6 +153,9 @@ private class ColumnMapping { /* The CekTable for the destination table. */ private CekTable destCekTable = null; + /* Statement level encryption setting needed for querying against encrypted columns. */ + private SQLServerStatementColumnEncryptionSetting stmtColumnEncriptionSetting = SQLServerStatementColumnEncryptionSetting.UseConnectionSetting; + /* * Metadata for the destination table columns */ @@ -1740,11 +1743,13 @@ private void getDestinationMetadata() throws SQLServerException { SQLServerResultSet rs = null; SQLServerResultSet rsMoreMetaData = null; - + SQLServerStatement stmt = null; + try { + stmt = (SQLServerStatement) connection.createStatement(ResultSet.TYPE_FORWARD_ONLY, + ResultSet.CONCUR_READ_ONLY, connection.getHoldability(), stmtColumnEncriptionSetting); // Get destination metadata - rs = ((SQLServerStatement) connection.createStatement()) - .executeQueryInternal("SET FMTONLY ON SELECT * FROM " + destinationTableName + " SET FMTONLY OFF "); + rs = stmt.executeQueryInternal("SET FMTONLY ON SELECT * FROM " + destinationTableName + " SET FMTONLY OFF "); destColumnCount = rs.getMetaData().getColumnCount(); destColumnMetadata = new HashMap<>(); @@ -1783,6 +1788,8 @@ private void getDestinationMetadata() throws SQLServerException { finally { if (null != rs) rs.close(); + if (null != stmt) + stmt.close(); if (null != rsMoreMetaData) rsMoreMetaData.close(); } @@ -3573,4 +3580,8 @@ private boolean writeBatchData(TDSWriter tdsWriter, } } } + + protected void setStmtColumnEncriptionSetting(SQLServerStatementColumnEncryptionSetting stmtColumnEncriptionSetting) { + this.stmtColumnEncriptionSetting = stmtColumnEncriptionSetting; + } } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java index ee410fb9e..bd840705e 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java @@ -2472,8 +2472,10 @@ public int[] executeBatch() throws SQLServerException, BatchUpdateException, SQL ArrayList valueList = parseUserSQLForValueListDW(false); String destinationTableName = tableName; + SQLServerStatement stmt = (SQLServerStatement) connection.createStatement(ResultSet.TYPE_FORWARD_ONLY, + ResultSet.CONCUR_READ_ONLY, connection.getHoldability(), stmtColumnEncriptionSetting); // Get destination metadata - try (SQLServerResultSet rs = ((SQLServerStatement) connection.createStatement()) + try (SQLServerResultSet rs = stmt .executeQueryInternal("SET FMTONLY ON SELECT * FROM " + destinationTableName + " SET FMTONLY OFF ");) { SQLServerBulkBatchInsertRecord batchRecord = new SQLServerBulkBatchInsertRecord(batchParamValues, columnList, valueList, null); @@ -2493,6 +2495,7 @@ public int[] executeBatch() throws SQLServerException, BatchUpdateException, SQL SQLServerBulkCopy bcOperation = new SQLServerBulkCopy(connection); bcOperation.setDestinationTableName(tableName); + bcOperation.setStmtColumnEncriptionSetting(this.getStmtColumnEncriptionSetting()); bcOperation.writeToServer((ISQLServerBulkRecord) batchRecord); bcOperation.close(); updateCounts = new int[batchParamValues.size()]; @@ -2505,6 +2508,10 @@ public int[] executeBatch() throws SQLServerException, BatchUpdateException, SQL loggerExternal.exiting(getClassNameLogging(), "executeBatch", updateCounts); return updateCounts; } + finally { + if (null != stmt) + stmt.close(); + } } } catch (SQLException e) { diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java index fc531ae49..1b89a5e58 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java @@ -154,6 +154,9 @@ String getClassNameLogging() { */ protected SQLServerStatementColumnEncryptionSetting stmtColumnEncriptionSetting = SQLServerStatementColumnEncryptionSetting.UseConnectionSetting; + protected SQLServerStatementColumnEncryptionSetting getStmtColumnEncriptionSetting() { + return stmtColumnEncriptionSetting; + } /** * ExecuteProperties encapsulates a subset of statement property values as they were set at execution time. */ From a677b284ce65ff139f34bc2d144ef10664ab64c0 Mon Sep 17 00:00:00 2001 From: Peter Bae Date: Tue, 8 May 2018 16:05:36 -0700 Subject: [PATCH 09/32] Reflect comment change --- .../com/microsoft/sqlserver/jdbc/SQLServerConnection.java | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index 60fc59dde..db54b2398 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -5888,13 +5888,7 @@ boolean isAzureDW() throws SQLServerException, SQLException { // Base data type: int final int ENGINE_EDITION_FOR_SQL_AZURE_DW = 6; rs.next(); - int engineEdition = rs.getInt(1); - if (engineEdition == ENGINE_EDITION_FOR_SQL_AZURE_DW) - { - isAzureDW = true; - } else { - isAzureDW = false; - } + isAzureDW = (rs.getInt(1) == ENGINE_EDITION_FOR_SQL_AZURE_DW) ? true : false; } return isAzureDW; } else { From 60b437e67e9c1db15baa4e057dcb4ee079cfc907 Mon Sep 17 00:00:00 2001 From: Peter Bae Date: Fri, 25 May 2018 10:22:27 -0700 Subject: [PATCH 10/32] Refactor two Bulk files into a common parent --- .../jdbc/SQLServerBulkBatchInsertRecord.java | 288 +---------------- .../jdbc/SQLServerBulkCSVFileRecord.java | 281 +---------------- .../sqlserver/jdbc/SQLServerBulkCommon.java | 297 ++++++++++++++++++ .../sqlserver/jdbc/SQLServerConnection.java | 17 +- .../sqlserver/jdbc/SQLServerDataSource.java | 20 ++ .../sqlserver/jdbc/SQLServerDriver.java | 6 +- .../jdbc/SQLServerPreparedStatement.java | 21 +- .../sqlserver/jdbc/SQLServerResource.java | 1 + 8 files changed, 364 insertions(+), 567 deletions(-) create mode 100644 src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCommon.java diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java index 3b570cd57..2b4b39179 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java @@ -18,7 +18,6 @@ import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.HashMap; -import java.util.Map; import java.util.Map.Entry; import java.util.Set; @@ -26,60 +25,7 @@ * A simple implementation of the ISQLServerBulkRecord interface that can be used to read in the basic Java data types from an ArrayList of * Parameters that were provided by pstmt/cstmt. */ -public class SQLServerBulkBatchInsertRecord implements ISQLServerBulkRecord, java.lang.AutoCloseable { - /* - * Class to represent the column metadata - */ - private class ColumnMetadata { - String columnName; - int columnType; - int precision; - int scale; - DateTimeFormatter dateTimeFormatter = null; - - ColumnMetadata(String name, - int type, - int precision, - int scale, - DateTimeFormatter dateTimeFormatter) { - columnName = name; - columnType = type; - this.precision = precision; - this.scale = scale; - this.dateTimeFormatter = dateTimeFormatter; - } - } - - /* - * Metadata to represent the columns in the batch. Each column should be mapped to its corresponding position within the parameter (from position 1 and - * onwards) - */ - private Map columnMetadata; - - /* - * Contains all the column names if firstLineIsColumnNames is true - */ - private String[] columnNames = null; - - /* - * Contains the format that java.sql.Types.TIMESTAMP_WITH_TIMEZONE data should be read in as. - */ - private DateTimeFormatter dateTimeFormatter = null; - - /* - * Contains the format that java.sql.Types.TIME_WITH_TIMEZONE data should be read in as. - */ - private DateTimeFormatter timeFormatter = null; - - /* - * Class name for logging. - */ - private static final String loggerClassName = "com.microsoft.sqlserver.jdbc.SQLServerBulkBatchInsertRecord"; - - /* - * Logger - */ - private static final java.util.logging.Logger loggerExternal = java.util.logging.Logger.getLogger(loggerClassName); +public class SQLServerBulkBatchInsertRecord extends SQLServerBulkCommon implements ISQLServerBulkRecord, java.lang.AutoCloseable { private ArrayList batchParam; private int batchParamIndex = -1; @@ -88,6 +34,7 @@ private class ColumnMetadata { public SQLServerBulkBatchInsertRecord(ArrayList batchParam, ArrayList columnList, ArrayList valueList, String encoding) throws SQLServerException { + loggerClassName = "com.microsoft.sqlserver.jdbc.SQLServerBulkBatchInsertRecord"; loggerExternal.entering(loggerClassName, "SQLServerBulkBatchInsertRecord", new Object[] {batchParam, encoding}); @@ -108,200 +55,7 @@ public SQLServerBulkBatchInsertRecord(ArrayList batchParam, ArrayLi } /** - * Adds metadata for the given column in the file. - * - * @param positionInTable - * Indicates which column the metadata is for. Columns start at 1. - * @param name - * Name for the column (optional if only using column ordinal in a mapping for SQLServerBulkCopy operation) - * @param jdbcType - * JDBC data type of the column - * @param precision - * Precision for the column (ignored for the appropriate data types) - * @param scale - * Scale for the column (ignored for the appropriate data types) - * @param dateTimeFormatter - * format to parse data that is sent - * @throws SQLServerException - * when an error occurs - */ - public void addColumnMetadata(int positionInTable, - String name, - int jdbcType, - int precision, - int scale, - DateTimeFormatter dateTimeFormatter) throws SQLServerException { - addColumnMetadataInternal(positionInTable, name, jdbcType, precision, scale, dateTimeFormatter); - } - - /** - * Adds metadata for the given column in the file. - * - * @param positionInTable - * Indicates which column the metadata is for. Columns start at 1. - * @param name - * Name for the column (optional if only using column ordinal in a mapping for SQLServerBulkCopy operation) - * @param jdbcType - * JDBC data type of the column - * @param precision - * Precision for the column (ignored for the appropriate data types) - * @param scale - * Scale for the column (ignored for the appropriate data types) - * @throws SQLServerException - * when an error occurs - */ - public void addColumnMetadata(int positionInTable, - String name, - int jdbcType, - int precision, - int scale) throws SQLServerException { - addColumnMetadataInternal(positionInTable, name, jdbcType, precision, scale, null); - } - - /** - * Adds metadata for the given column in the file. - * - * @param positionInTable - * Indicates which column the metadata is for. Columns start at 1. - * @param name - * Name for the column (optional if only using column ordinal in a mapping for SQLServerBulkCopy operation) - * @param jdbcType - * JDBC data type of the column - * @param precision - * Precision for the column (ignored for the appropriate data types) - * @param scale - * Scale for the column (ignored for the appropriate data types) - * @param dateTimeFormatter - * format to parse data that is sent - * @throws SQLServerException - * when an error occurs - */ - void addColumnMetadataInternal(int positionInTable, - String name, - int jdbcType, - int precision, - int scale, - DateTimeFormatter dateTimeFormatter) throws SQLServerException { - loggerExternal.entering(loggerClassName, "addColumnMetadata", new Object[] {positionInTable, name, jdbcType, precision, scale}); - - String colName = ""; - - if (0 >= positionInTable) { - MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidColumnOrdinal")); - Object[] msgArgs = {positionInTable}; - throw new SQLServerException(form.format(msgArgs), SQLState.COL_NOT_FOUND, DriverError.NOT_SET, null); - } - - if (null != name) - colName = name.trim(); - else if ((columnNames != null) && (columnNames.length >= positionInTable)) - colName = columnNames[positionInTable - 1]; - - if ((columnNames != null) && (positionInTable > columnNames.length)) { - MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidColumn")); - Object[] msgArgs = {positionInTable}; - throw new SQLServerException(form.format(msgArgs), SQLState.COL_NOT_FOUND, DriverError.NOT_SET, null); - } - - checkDuplicateColumnName(positionInTable, name); - switch (jdbcType) { - /* - * 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. string literal formats supported by temporal types are available in MSDN - * page on data types. - */ - case java.sql.Types.DATE: - case java.sql.Types.TIME: - case java.sql.Types.TIMESTAMP: - case microsoft.sql.Types.DATETIMEOFFSET: - // The precision is just a number long enough to hold all types of temporal data, doesn't need to be exact precision. - columnMetadata.put(positionInTable, new ColumnMetadata(colName, jdbcType, 50, scale, dateTimeFormatter)); - break; - - // Redirect SQLXML as LONGNVARCHAR - // SQLXML is not valid type in TDS - case java.sql.Types.SQLXML: - columnMetadata.put(positionInTable, new ColumnMetadata(colName, java.sql.Types.LONGNVARCHAR, precision, scale, dateTimeFormatter)); - break; - - // Redirecting Float as Double based on data type mapping - // https://msdn.microsoft.com/en-us/library/ms378878%28v=sql.110%29.aspx - case java.sql.Types.FLOAT: - columnMetadata.put(positionInTable, new ColumnMetadata(colName, java.sql.Types.DOUBLE, precision, scale, dateTimeFormatter)); - break; - - // redirecting BOOLEAN as BIT - case java.sql.Types.BOOLEAN: - columnMetadata.put(positionInTable, new ColumnMetadata(colName, java.sql.Types.BIT, precision, scale, dateTimeFormatter)); - break; - - default: - columnMetadata.put(positionInTable, new ColumnMetadata(colName, jdbcType, precision, scale, dateTimeFormatter)); - } - - loggerExternal.exiting(loggerClassName, "addColumnMetadata"); - } - - /** - * Set the format for reading in dates from the file. - * - * @param dateTimeFormat - * format to parse data sent as java.sql.Types.TIMESTAMP_WITH_TIMEZONE - */ - public void setTimestampWithTimezoneFormat(String dateTimeFormat) { - DriverJDBCVersion.checkSupportsJDBC42(); - loggerExternal.entering(loggerClassName, "setTimestampWithTimezoneFormat", dateTimeFormat); - - this.dateTimeFormatter = DateTimeFormatter.ofPattern(dateTimeFormat); - - loggerExternal.exiting(loggerClassName, "setTimestampWithTimezoneFormat"); - } - - /** - * Set the format for reading in dates from the file. - * - * @param dateTimeFormatter - * format to parse data sent as java.sql.Types.TIMESTAMP_WITH_TIMEZONE - */ - public void setTimestampWithTimezoneFormat(DateTimeFormatter dateTimeFormatter) { - loggerExternal.entering(loggerClassName, "setTimestampWithTimezoneFormat", new Object[] {dateTimeFormatter}); - - this.dateTimeFormatter = dateTimeFormatter; - - loggerExternal.exiting(loggerClassName, "setTimestampWithTimezoneFormat"); - } - - /** - * Set the format for reading in dates from the file. - * - * @param timeFormat - * format to parse data sent as java.sql.Types.TIME_WITH_TIMEZONE - */ - public void setTimeWithTimezoneFormat(String timeFormat) { - DriverJDBCVersion.checkSupportsJDBC42(); - loggerExternal.entering(loggerClassName, "setTimeWithTimezoneFormat", timeFormat); - - this.timeFormatter = DateTimeFormatter.ofPattern(timeFormat); - - loggerExternal.exiting(loggerClassName, "setTimeWithTimezoneFormat"); - } - - /** - * Set the format for reading in dates from the file. - * - * @param dateTimeFormatter - * format to parse data sent as java.sql.Types.TIME_WITH_TIMEZONE - */ - public void setTimeWithTimezoneFormat(DateTimeFormatter dateTimeFormatter) { - loggerExternal.entering(loggerClassName, "setTimeWithTimezoneFormat", new Object[] {dateTimeFormatter}); - - this.timeFormatter = dateTimeFormatter; - - loggerExternal.exiting(loggerClassName, "setTimeWithTimezoneFormat"); - } - - /** - * Releases any resources associated with the file reader. + * Releases any resources associated with the batch. * * @throws SQLServerException * when an error occurs @@ -479,13 +233,6 @@ public Object[] getRowData() throws SQLServerException { case Types.VARBINARY: case Types.LONGVARBINARY: case Types.BLOB: { - /* - * For binary data, the value in file may or may not have the '0x' prefix. We will try to match our implementation with - * 'BULK INSERT' except that we will allow 0x prefix whereas 'BULK INSERT' command does not allow 0x prefix. A BULK INSERT - * example: A sample csv file containing data for 2 binary columns and 1 row: 61,62 Table definition: create table t1(c1 - * varbinary(10), c2 varbinary(10)) BULK INSERT command: bulk insert t1 from 'C:\in.csv' - * with(DATAFILETYPE='char',firstrow=1,FIELDTERMINATOR=',') select * from t1 shows 1 row with columns: 0x61, 0x62 - */ // Strip off 0x if present. String binData = data[pair.getKey() - 1].toString().trim(); if (binData.startsWith("0x") || binData.startsWith("0X")) { @@ -566,33 +313,4 @@ public boolean next() throws SQLServerException { batchParamIndex++; return batchParamIndex < batchParam.size(); } - - /* - * 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); - } - - /* - * Method to throw a SQLServerExeption for duplicate column names - */ - private void checkDuplicateColumnName(int positionInTable, - String colName) throws SQLServerException { - - if (null != colName && colName.trim().length() != 0) { - for (Entry entry : columnMetadata.entrySet()) { - // duplicate check is not performed in case of same positionInTable value - if (null != entry && entry.getKey() != positionInTable) { - if (null != entry.getValue() && colName.trim().equalsIgnoreCase(entry.getValue().columnName)) { - throw new SQLServerException(SQLServerException.getErrString("R_BulkCSVDataDuplicateColumn"), null); - } - } - - } - } - - } } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCSVFileRecord.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCSVFileRecord.java index cce49a416..3fc1bcc87 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCSVFileRecord.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCSVFileRecord.java @@ -23,7 +23,6 @@ import java.time.OffsetTime; import java.time.format.DateTimeFormatter; import java.util.HashMap; -import java.util.Map; import java.util.Map.Entry; import java.util.Set; @@ -31,30 +30,7 @@ * A simple implementation of the ISQLServerBulkRecord interface that can be used to read in the basic Java data types from a delimited file where * each line represents a row of data. */ -public class SQLServerBulkCSVFileRecord implements ISQLServerBulkRecord, java.lang.AutoCloseable { - /* - * Class to represent the column metadata - */ - private class ColumnMetadata { - String columnName; - int columnType; - int precision; - int scale; - DateTimeFormatter dateTimeFormatter = null; - - ColumnMetadata(String name, - int type, - int precision, - int scale, - DateTimeFormatter dateTimeFormatter) { - columnName = name; - columnType = type; - this.precision = precision; - this.scale = scale; - this.dateTimeFormatter = dateTimeFormatter; - } - } - +public class SQLServerBulkCSVFileRecord extends SQLServerBulkCommon implements ISQLServerBulkRecord, java.lang.AutoCloseable { /* * Resources associated with reading in the file */ @@ -62,12 +38,6 @@ private class ColumnMetadata { private InputStreamReader sr; private FileInputStream fis; - /* - * Metadata to represent the columns in the file. Each column should be mapped to its corresponding position within the file (from position 1 and - * onwards) - */ - private Map columnMetadata; - /* * Current line of data to parse. */ @@ -78,31 +48,6 @@ private class ColumnMetadata { */ private final String delimiter; - /* - * Contains all the column names if firstLineIsColumnNames is true - */ - private String[] columnNames = null; - - /* - * Contains the format that java.sql.Types.TIMESTAMP_WITH_TIMEZONE data should be read in as. - */ - private DateTimeFormatter dateTimeFormatter = null; - - /* - * Contains the format that java.sql.Types.TIME_WITH_TIMEZONE data should be read in as. - */ - private DateTimeFormatter timeFormatter = null; - - /* - * Class name for logging. - */ - private static final String loggerClassName = "com.microsoft.sqlserver.jdbc.SQLServerBulkCSVFileRecord"; - - /* - * Logger - */ - private static final java.util.logging.Logger loggerExternal = java.util.logging.Logger.getLogger(loggerClassName); - /** * Creates a simple reader to parse data from a delimited file with the given encoding. * @@ -121,6 +66,7 @@ public SQLServerBulkCSVFileRecord(String fileToParse, String encoding, String delimiter, boolean firstLineIsColumnNames) throws SQLServerException { + loggerClassName = "com.microsoft.sqlserver.jdbc.SQLServerBulkCSVFileRecord"; loggerExternal.entering(loggerClassName, "SQLServerBulkCSVFileRecord", new Object[] {fileToParse, encoding, delimiter, firstLineIsColumnNames}); @@ -181,6 +127,7 @@ public SQLServerBulkCSVFileRecord(InputStream fileToParse, String encoding, String delimiter, boolean firstLineIsColumnNames) throws SQLServerException { + loggerClassName = "com.microsoft.sqlserver.jdbc.SQLServerBulkCSVFileRecord"; loggerExternal.entering(loggerClassName, "SQLServerBulkCSVFileRecord", new Object[] {fileToParse, encoding, delimiter, firstLineIsColumnNames}); @@ -253,199 +200,6 @@ public SQLServerBulkCSVFileRecord(String fileToParse, this(fileToParse, null, ",", firstLineIsColumnNames); } - /** - * Adds metadata for the given column in the file. - * - * @param positionInFile - * Indicates which column the metadata is for. Columns start at 1. - * @param name - * Name for the column (optional if only using column ordinal in a mapping for SQLServerBulkCopy operation) - * @param jdbcType - * JDBC data type of the column - * @param precision - * Precision for the column (ignored for the appropriate data types) - * @param scale - * Scale for the column (ignored for the appropriate data types) - * @param dateTimeFormatter - * format to parse data that is sent - * @throws SQLServerException - * when an error occurs - */ - public void addColumnMetadata(int positionInFile, - String name, - int jdbcType, - int precision, - int scale, - DateTimeFormatter dateTimeFormatter) throws SQLServerException { - addColumnMetadataInternal(positionInFile, name, jdbcType, precision, scale, dateTimeFormatter); - } - - /** - * Adds metadata for the given column in the file. - * - * @param positionInFile - * Indicates which column the metadata is for. Columns start at 1. - * @param name - * Name for the column (optional if only using column ordinal in a mapping for SQLServerBulkCopy operation) - * @param jdbcType - * JDBC data type of the column - * @param precision - * Precision for the column (ignored for the appropriate data types) - * @param scale - * Scale for the column (ignored for the appropriate data types) - * @throws SQLServerException - * when an error occurs - */ - public void addColumnMetadata(int positionInFile, - String name, - int jdbcType, - int precision, - int scale) throws SQLServerException { - addColumnMetadataInternal(positionInFile, name, jdbcType, precision, scale, null); - } - - /** - * Adds metadata for the given column in the file. - * - * @param positionInFile - * Indicates which column the metadata is for. Columns start at 1. - * @param name - * Name for the column (optional if only using column ordinal in a mapping for SQLServerBulkCopy operation) - * @param jdbcType - * JDBC data type of the column - * @param precision - * Precision for the column (ignored for the appropriate data types) - * @param scale - * Scale for the column (ignored for the appropriate data types) - * @param dateTimeFormatter - * format to parse data that is sent - * @throws SQLServerException - * when an error occurs - */ - void addColumnMetadataInternal(int positionInFile, - String name, - int jdbcType, - int precision, - int scale, - DateTimeFormatter dateTimeFormatter) throws SQLServerException { - loggerExternal.entering(loggerClassName, "addColumnMetadata", new Object[] {positionInFile, name, jdbcType, precision, scale}); - - String colName = ""; - - if (0 >= positionInFile) { - MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidColumnOrdinal")); - Object[] msgArgs = {positionInFile}; - throw new SQLServerException(form.format(msgArgs), SQLState.COL_NOT_FOUND, DriverError.NOT_SET, null); - } - - if (null != name) - colName = name.trim(); - else if ((columnNames != null) && (columnNames.length >= positionInFile)) - colName = columnNames[positionInFile - 1]; - - if ((columnNames != null) && (positionInFile > columnNames.length)) { - MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidColumn")); - Object[] msgArgs = {positionInFile}; - throw new SQLServerException(form.format(msgArgs), SQLState.COL_NOT_FOUND, DriverError.NOT_SET, null); - } - - checkDuplicateColumnName(positionInFile, name); - switch (jdbcType) { - /* - * 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. string literal formats supported by temporal types are available in MSDN - * page on data types. - */ - case java.sql.Types.DATE: - case java.sql.Types.TIME: - case java.sql.Types.TIMESTAMP: - case microsoft.sql.Types.DATETIMEOFFSET: - // The precision is just a number long enough to hold all types of temporal data, doesn't need to be exact precision. - columnMetadata.put(positionInFile, new ColumnMetadata(colName, jdbcType, 50, scale, dateTimeFormatter)); - break; - - // Redirect SQLXML as LONGNVARCHAR - // SQLXML is not valid type in TDS - case java.sql.Types.SQLXML: - columnMetadata.put(positionInFile, new ColumnMetadata(colName, java.sql.Types.LONGNVARCHAR, precision, scale, dateTimeFormatter)); - break; - - // Redirecting Float as Double based on data type mapping - // https://msdn.microsoft.com/en-us/library/ms378878%28v=sql.110%29.aspx - case java.sql.Types.FLOAT: - columnMetadata.put(positionInFile, new ColumnMetadata(colName, java.sql.Types.DOUBLE, precision, scale, dateTimeFormatter)); - break; - - // redirecting BOOLEAN as BIT - case java.sql.Types.BOOLEAN: - columnMetadata.put(positionInFile, new ColumnMetadata(colName, java.sql.Types.BIT, precision, scale, dateTimeFormatter)); - break; - - default: - columnMetadata.put(positionInFile, new ColumnMetadata(colName, jdbcType, precision, scale, dateTimeFormatter)); - } - - loggerExternal.exiting(loggerClassName, "addColumnMetadata"); - } - - /** - * Set the format for reading in dates from the file. - * - * @param dateTimeFormat - * format to parse data sent as java.sql.Types.TIMESTAMP_WITH_TIMEZONE - */ - public void setTimestampWithTimezoneFormat(String dateTimeFormat) { - DriverJDBCVersion.checkSupportsJDBC42(); - loggerExternal.entering(loggerClassName, "setTimestampWithTimezoneFormat", dateTimeFormat); - - this.dateTimeFormatter = DateTimeFormatter.ofPattern(dateTimeFormat); - - loggerExternal.exiting(loggerClassName, "setTimestampWithTimezoneFormat"); - } - - /** - * Set the format for reading in dates from the file. - * - * @param dateTimeFormatter - * format to parse data sent as java.sql.Types.TIMESTAMP_WITH_TIMEZONE - */ - public void setTimestampWithTimezoneFormat(DateTimeFormatter dateTimeFormatter) { - loggerExternal.entering(loggerClassName, "setTimestampWithTimezoneFormat", new Object[] {dateTimeFormatter}); - - this.dateTimeFormatter = dateTimeFormatter; - - loggerExternal.exiting(loggerClassName, "setTimestampWithTimezoneFormat"); - } - - /** - * Set the format for reading in dates from the file. - * - * @param timeFormat - * format to parse data sent as java.sql.Types.TIME_WITH_TIMEZONE - */ - public void setTimeWithTimezoneFormat(String timeFormat) { - DriverJDBCVersion.checkSupportsJDBC42(); - loggerExternal.entering(loggerClassName, "setTimeWithTimezoneFormat", timeFormat); - - this.timeFormatter = DateTimeFormatter.ofPattern(timeFormat); - - loggerExternal.exiting(loggerClassName, "setTimeWithTimezoneFormat"); - } - - /** - * Set the format for reading in dates from the file. - * - * @param dateTimeFormatter - * format to parse data sent as java.sql.Types.TIME_WITH_TIMEZONE - */ - public void setTimeWithTimezoneFormat(DateTimeFormatter dateTimeFormatter) { - loggerExternal.entering(loggerClassName, "setTimeWithTimezoneFormat", new Object[] {dateTimeFormatter}); - - this.timeFormatter = dateTimeFormatter; - - loggerExternal.exiting(loggerClassName, "setTimeWithTimezoneFormat"); - } - /** * Releases any resources associated with the file reader. * @@ -720,33 +474,4 @@ public boolean next() throws SQLServerException { } return (null != currentLine); } - - /* - * 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); - } - - /* - * Method to throw a SQLServerExeption for duplicate column names - */ - private void checkDuplicateColumnName(int positionInFile, - String colName) throws SQLServerException { - - if (null != colName && colName.trim().length() != 0) { - for (Entry entry : columnMetadata.entrySet()) { - // duplicate check is not performed in case of same positionInFile value - if (null != entry && entry.getKey() != positionInFile) { - if (null != entry.getValue() && colName.trim().equalsIgnoreCase(entry.getValue().columnName)) { - throw new SQLServerException(SQLServerException.getErrString("R_BulkCSVDataDuplicateColumn"), null); - } - } - - } - } - - } } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCommon.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCommon.java new file mode 100644 index 000000000..b5b832d54 --- /dev/null +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCommon.java @@ -0,0 +1,297 @@ +/* + * 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 java.text.MessageFormat; +import java.time.format.DateTimeFormatter; +import java.util.Map; +import java.util.Map.Entry; + +abstract class SQLServerBulkCommon { + + /* + * Class to represent the column metadata + */ + protected class ColumnMetadata { + String columnName; + int columnType; + int precision; + int scale; + DateTimeFormatter dateTimeFormatter = null; + + ColumnMetadata(String name, + int type, + int precision, + int scale, + DateTimeFormatter dateTimeFormatter) { + columnName = name; + columnType = type; + this.precision = precision; + this.scale = scale; + this.dateTimeFormatter = dateTimeFormatter; + } + } + + /* + * Class name for logging. + */ + protected static String loggerClassName; + + /* + * Logger + */ + protected static final java.util.logging.Logger loggerExternal = java.util.logging.Logger.getLogger(loggerClassName); + + /* + * Contains all the column names if firstLineIsColumnNames is true + */ + protected String[] columnNames = null; + + /* + * Metadata to represent the columns in the batch/file. Each column should be mapped to its corresponding position within the parameter (from position 1 and + * onwards) + */ + protected Map columnMetadata; + + /* + * Contains the format that java.sql.Types.TIMESTAMP_WITH_TIMEZONE data should be read in as. + */ + protected DateTimeFormatter dateTimeFormatter = null; + + /* + * Contains the format that java.sql.Types.TIME_WITH_TIMEZONE data should be read in as. + */ + protected DateTimeFormatter timeFormatter = null; + + /** + * Adds metadata for the given column in the batch/file. + * + * @param positionInSource + * Indicates which column the metadata is for. Columns start at 1. + * @param name + * Name for the column (optional if only using column ordinal in a mapping for SQLServerBulkCopy operation) + * @param jdbcType + * JDBC data type of the column + * @param precision + * Precision for the column (ignored for the appropriate data types) + * @param scale + * Scale for the column (ignored for the appropriate data types) + * @param dateTimeFormatter + * format to parse data that is sent + * @throws SQLServerException + * when an error occurs + */ + public void addColumnMetadata(int positionInSource, + String name, + int jdbcType, + int precision, + int scale, + DateTimeFormatter dateTimeFormatter) throws SQLServerException { + addColumnMetadataInternal(positionInSource, name, jdbcType, precision, scale, dateTimeFormatter); + } + + /** + * Adds metadata for the given column in the batch/file. + * + * @param positionInTable + * Indicates which column the metadata is for. Columns start at 1. + * @param name + * Name for the column (optional if only using column ordinal in a mapping for SQLServerBulkCopy operation) + * @param jdbcType + * JDBC data type of the column + * @param precision + * Precision for the column (ignored for the appropriate data types) + * @param scale + * Scale for the column (ignored for the appropriate data types) + * @throws SQLServerException + * when an error occurs + */ + public void addColumnMetadata(int positionInSource, + String name, + int jdbcType, + int precision, + int scale) throws SQLServerException { + addColumnMetadataInternal(positionInSource, name, jdbcType, precision, scale, null); + } + + /** + * Adds metadata for the given column in the batch/file. + * + * @param positionInSource + * Indicates which column the metadata is for. Columns start at 1. + * @param name + * Name for the column (optional if only using column ordinal in a mapping for SQLServerBulkCopy operation) + * @param jdbcType + * JDBC data type of the column + * @param precision + * Precision for the column (ignored for the appropriate data types) + * @param scale + * Scale for the column (ignored for the appropriate data types) + * @param dateTimeFormatter + * format to parse data that is sent + * @throws SQLServerException + * when an error occurs + */ + void addColumnMetadataInternal(int positionInSource, + String name, + int jdbcType, + int precision, + int scale, + DateTimeFormatter dateTimeFormatter) throws SQLServerException { + loggerExternal.entering(loggerClassName, "addColumnMetadata", new Object[] {positionInSource, name, jdbcType, precision, scale}); + + String colName = ""; + + if (0 >= positionInSource) { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidColumnOrdinal")); + Object[] msgArgs = {positionInSource}; + throw new SQLServerException(form.format(msgArgs), SQLState.COL_NOT_FOUND, DriverError.NOT_SET, null); + } + + if (null != name) + colName = name.trim(); + else if ((columnNames != null) && (columnNames.length >= positionInSource)) + colName = columnNames[positionInSource - 1]; + + if ((columnNames != null) && (positionInSource > columnNames.length)) { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidColumn")); + Object[] msgArgs = {positionInSource}; + throw new SQLServerException(form.format(msgArgs), SQLState.COL_NOT_FOUND, DriverError.NOT_SET, null); + } + + checkDuplicateColumnName(positionInSource, name); + switch (jdbcType) { + /* + * 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. string literal formats supported by temporal types are available in MSDN + * page on data types. + */ + case java.sql.Types.DATE: + case java.sql.Types.TIME: + case java.sql.Types.TIMESTAMP: + case microsoft.sql.Types.DATETIMEOFFSET: + if (this instanceof SQLServerBulkCSVFileRecord) { + columnMetadata.put(positionInSource, new ColumnMetadata(colName, jdbcType, 50, precision, dateTimeFormatter)); + } else if (this instanceof SQLServerBulkBatchInsertRecord) { + columnMetadata.put(positionInSource, new ColumnMetadata(colName, jdbcType, precision, precision, dateTimeFormatter)); + } else { + columnMetadata.put(positionInSource, new ColumnMetadata(colName, jdbcType, 50, precision, dateTimeFormatter)); + } + break; + + // Redirect SQLXML as LONGNVARCHAR + // SQLXML is not valid type in TDS + case java.sql.Types.SQLXML: + columnMetadata.put(positionInSource, new ColumnMetadata(colName, java.sql.Types.LONGNVARCHAR, precision, scale, dateTimeFormatter)); + break; + + // Redirecting Float as Double based on data type mapping + // https://msdn.microsoft.com/en-us/library/ms378878%28v=sql.110%29.aspx + case java.sql.Types.FLOAT: + columnMetadata.put(positionInSource, new ColumnMetadata(colName, java.sql.Types.DOUBLE, precision, scale, dateTimeFormatter)); + break; + + // redirecting BOOLEAN as BIT + case java.sql.Types.BOOLEAN: + columnMetadata.put(positionInSource, new ColumnMetadata(colName, java.sql.Types.BIT, precision, scale, dateTimeFormatter)); + break; + + default: + columnMetadata.put(positionInSource, new ColumnMetadata(colName, jdbcType, precision, scale, dateTimeFormatter)); + } + + loggerExternal.exiting(loggerClassName, "addColumnMetadata"); + } + + /** + * Set the format for reading in dates from the batch/file. + * + * @param dateTimeFormat + * format to parse data sent as java.sql.Types.TIMESTAMP_WITH_TIMEZONE + */ + public void setTimestampWithTimezoneFormat(String dateTimeFormat) { + DriverJDBCVersion.checkSupportsJDBC42(); + loggerExternal.entering(loggerClassName, "setTimestampWithTimezoneFormat", dateTimeFormat); + + this.dateTimeFormatter = DateTimeFormatter.ofPattern(dateTimeFormat); + + loggerExternal.exiting(loggerClassName, "setTimestampWithTimezoneFormat"); + } + + /** + * Set the format for reading in dates from the batch/file. + * + * @param dateTimeFormatter + * format to parse data sent as java.sql.Types.TIMESTAMP_WITH_TIMEZONE + */ + public void setTimestampWithTimezoneFormat(DateTimeFormatter dateTimeFormatter) { + loggerExternal.entering(loggerClassName, "setTimestampWithTimezoneFormat", new Object[] {dateTimeFormatter}); + + this.dateTimeFormatter = dateTimeFormatter; + + loggerExternal.exiting(loggerClassName, "setTimestampWithTimezoneFormat"); + } + + /** + * Set the format for reading in dates from the batch/file. + * + * @param timeFormat + * format to parse data sent as java.sql.Types.TIME_WITH_TIMEZONE + */ + public void setTimeWithTimezoneFormat(String timeFormat) { + DriverJDBCVersion.checkSupportsJDBC42(); + loggerExternal.entering(loggerClassName, "setTimeWithTimezoneFormat", timeFormat); + + this.timeFormatter = DateTimeFormatter.ofPattern(timeFormat); + + loggerExternal.exiting(loggerClassName, "setTimeWithTimezoneFormat"); + } + + /** + * Set the format for reading in dates from the batch/file. + * + * @param dateTimeFormatter + * format to parse data sent as java.sql.Types.TIME_WITH_TIMEZONE + */ + public void setTimeWithTimezoneFormat(DateTimeFormatter dateTimeFormatter) { + loggerExternal.entering(loggerClassName, "setTimeWithTimezoneFormat", new Object[] {dateTimeFormatter}); + + this.timeFormatter = dateTimeFormatter; + + loggerExternal.exiting(loggerClassName, "setTimeWithTimezoneFormat"); + } + + /* + * Helper method to throw a SQLServerExeption with the invalidArgument message and given argument. + */ + protected 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); + } + + /* + * Method to throw a SQLServerExeption for duplicate column names + */ + protected void checkDuplicateColumnName(int positionInTable, + String colName) throws SQLServerException { + + if (null != colName && colName.trim().length() != 0) { + for (Entry entry : columnMetadata.entrySet()) { + // duplicate check is not performed in case of same positionInTable value + if (null != entry && entry.getKey() != positionInTable) { + if (null != entry.getValue() && colName.trim().equalsIgnoreCase(entry.getValue().columnName)) { + throw new SQLServerException(SQLServerException.getErrString("R_BulkCSVDataDuplicateColumn"), null); + } + } + + } + } + } +} diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index db54b2398..01d4a9b70 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -492,6 +492,15 @@ final int getSocketTimeoutMilliseconds() { return socketTimeoutMilliseconds; } + /** + * boolean value for deciding if the driver should use bulk copy API for batch inserts + */ + private boolean useBulkCopyForBatchInsertOnDW; + + final boolean getUseBulkCopyForBatchInsertOnDW() { + return useBulkCopyForBatchInsertOnDW; + } + boolean userSetTNIR = true; private boolean sendTimeAsDatetime = SQLServerDriverBooleanProperty.SEND_TIME_AS_DATETIME.getDefaultValue(); @@ -1746,7 +1755,13 @@ else if (0 == requestedPacketSize) if (null != sPropValue) { setEnablePrepareOnFirstPreparedStatementCall(booleanPropertyOn(sPropKey, sPropValue)); } - + + sPropKey = SQLServerDriverBooleanProperty.USE_BULK_COPY_FOR_BATCH_INSERT_ON_DW.toString(); + sPropValue = activeConnectionProperties.getProperty(sPropKey); + if (null != sPropValue) { + useBulkCopyForBatchInsertOnDW = booleanPropertyOn(sPropKey, sPropValue); + } + sPropKey = SQLServerDriverStringProperty.SSL_PROTOCOL.toString(); sPropValue = activeConnectionProperties.getProperty(sPropKey); if (null == sPropValue) { diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java index 353c0ddd1..5720b5d24 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java @@ -821,6 +821,26 @@ public int getSocketTimeout() { int defaultTimeOut = SQLServerDriverIntProperty.SOCKET_TIMEOUT.getDefaultValue(); return getIntProperty(connectionProps, SQLServerDriverIntProperty.SOCKET_TIMEOUT.toString(), defaultTimeOut); } + + /** + * Setting the use Bulk Copy API for Batch Insert on Azure Data Warehouse boolean + * + * @param useBulkCopyForBatchInsertOnDW indicates whether Bulk Copy API should be used for Batch Insert operations. + */ + public void setUseBulkCopyForBatchInsertOnDW(boolean useBulkCopyForBatchInsertOnDW) { + setBooleanProperty(connectionProps, SQLServerDriverBooleanProperty.USE_BULK_COPY_FOR_BATCH_INSERT_ON_DW.toString(), + useBulkCopyForBatchInsertOnDW); + } + + /** + * Getting the use Bulk Copy API for Batch Insert on Azure Data Warehouse boolean + * + * @return whether the driver should use Bulk Copy API for Batch Insert operations. + */ + public boolean getUseBulkCopyForBatchInsertOnDW() { + return getBooleanProperty(connectionProps, SQLServerDriverBooleanProperty.USE_BULK_COPY_FOR_BATCH_INSERT_ON_DW.toString(), + SQLServerDriverBooleanProperty.USE_BULK_COPY_FOR_BATCH_INSERT_ON_DW.getDefaultValue()); + } /** * Sets the login configuration file for Kerberos authentication. This diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java index 9f13dc3f4..0ea70f7e4 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java @@ -353,7 +353,8 @@ enum SQLServerDriverBooleanProperty TRUST_SERVER_CERTIFICATE ("trustServerCertificate", false), XOPEN_STATES ("xopenStates", false), FIPS ("fips", false), - ENABLE_PREPARE_ON_FIRST_PREPARED_STATEMENT("enablePrepareOnFirstPreparedStatementCall", SQLServerConnection.DEFAULT_ENABLE_PREPARE_ON_FIRST_PREPARED_STATEMENT_CALL); + ENABLE_PREPARE_ON_FIRST_PREPARED_STATEMENT("enablePrepareOnFirstPreparedStatementCall", SQLServerConnection.DEFAULT_ENABLE_PREPARE_ON_FIRST_PREPARED_STATEMENT_CALL), + USE_BULK_COPY_FOR_BATCH_INSERT_ON_DW ("useBulkCopyForBatchInsertOnDW", false); private final String name; private final boolean defaultValue; @@ -429,7 +430,8 @@ public final class SQLServerDriver implements java.sql.Driver { new SQLServerDriverPropertyInfo(SQLServerDriverIntProperty.STATEMENT_POOLING_CACHE_SIZE.toString(), Integer.toString(SQLServerDriverIntProperty.STATEMENT_POOLING_CACHE_SIZE.getDefaultValue()), false, null), new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.JAAS_CONFIG_NAME.toString(), SQLServerDriverStringProperty.JAAS_CONFIG_NAME.getDefaultValue(), false, null), new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.SSL_PROTOCOL.toString(), SQLServerDriverStringProperty.SSL_PROTOCOL.getDefaultValue(), false, new String[] {SSLProtocol.TLS.toString(), SSLProtocol.TLS_V10.toString(), SSLProtocol.TLS_V11.toString(), SSLProtocol.TLS_V12.toString()}), - new SQLServerDriverPropertyInfo(SQLServerDriverIntProperty.CANCEL_QUERY_TIMEOUT.toString(), Integer.toString(SQLServerDriverIntProperty.CANCEL_QUERY_TIMEOUT.getDefaultValue()), false, null), + new SQLServerDriverPropertyInfo(SQLServerDriverIntProperty.CANCEL_QUERY_TIMEOUT.toString(), Integer.toString(SQLServerDriverIntProperty.CANCEL_QUERY_TIMEOUT.getDefaultValue()), false, null), + new SQLServerDriverPropertyInfo(SQLServerDriverBooleanProperty.USE_BULK_COPY_FOR_BATCH_INSERT_ON_DW.toString(), Boolean.toString(SQLServerDriverBooleanProperty.USE_BULK_COPY_FOR_BATCH_INSERT_ON_DW.getDefaultValue()),false, TRUE_FALSE), }; // Properties that can only be set by using Properties. diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java index bd840705e..2f33b7662 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java @@ -107,6 +107,19 @@ public class SQLServerPreparedStatement extends SQLServerStatement implements IS private void setPreparedStatementHandle(int handle) { this.prepStmtHandle = handle; } + + /** + * boolean value for deciding if the driver should use bulk copy API for batch inserts + */ + private boolean useBulkCopyForBatchInsertOnDW; + + public boolean getUseBulkCopyForBatchInsertOnDW() { + return useBulkCopyForBatchInsertOnDW; + } + + public void setUseBulkCopyForBatchInsertOnDW(boolean useBulkCopyForBatchInsertOnDW) { + this.useBulkCopyForBatchInsertOnDW = useBulkCopyForBatchInsertOnDW; + } /** The server handle for this prepared statement. If a value {@literal <} 1 is returned no handle has been created. * @@ -2460,7 +2473,7 @@ public int[] executeBatch() throws SQLServerException, BatchUpdateException, SQL localUserSQL = userSQL; try { - if (isInsert(localUserSQL) && connection.isAzureDW()) { + if (isInsert(localUserSQL) && true && (this.useBulkCopyForBatchInsertOnDW || connection.getUseBulkCopyForBatchInsertOnDW())) { if (batchParamValues == null) { updateCounts = new int[0]; loggerExternal.exiting(getClassNameLogging(), "executeBatch", updateCounts); @@ -2817,6 +2830,7 @@ private ArrayList parseUserSQLForColumnListDWHelper(ArrayList li } else { sb.append(localUserSQL.charAt(0)); localUserSQL = localUserSQL.substring(1); + localUserSQL = localUserSQL.trim(); } } @@ -2919,6 +2933,7 @@ private ArrayList parseUserSQLForValueListDWHelper(ArrayList lis } else { sb.append(localUserSQL.charAt(0)); localUserSQL = localUserSQL.substring(1); + localUserSQL = localUserSQL.trim(); } } @@ -2927,6 +2942,10 @@ private ArrayList parseUserSQLForValueListDWHelper(ArrayList lis } private boolean checkAndRemoveComments() { + if (null == localUserSQL || localUserSQL.length() < 2) { + return false; + } + if (localUserSQL.substring(0, 2).equalsIgnoreCase("/*")) { int temp = localUserSQL.indexOf("*/") + 2; localUserSQL = localUserSQL.substring(temp); diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java index 884be5c93..a2d726519 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java @@ -393,5 +393,6 @@ protected Object[][] getContents() { {"R_invalidSSLProtocol", "SSL Protocol {0} label is not valid. Only TLS, TLSv1, TLSv1.1, and TLSv1.2 are supported."}, {"R_cancelQueryTimeoutPropertyDescription", "The number of seconds to wait to cancel sending a query timeout."}, {"R_invalidCancelQueryTimeout", "The cancel timeout value {0} is not valid."}, + {"R_useBulkCopyForBatchInsertOnDWPropertyDescription", "Whether the driver will use bulk copy API for batch insert operations on Azure Data Warehouse."}, }; } \ No newline at end of file From 8604ff46f10e51fd8be745705a413f8ad4535c58 Mon Sep 17 00:00:00 2001 From: Peter Bae Date: Fri, 25 May 2018 11:20:46 -0700 Subject: [PATCH 11/32] javadoc changes --- .../sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java | 1 + .../microsoft/sqlserver/jdbc/SQLServerBulkCSVFileRecord.java | 2 ++ .../com/microsoft/sqlserver/jdbc/SQLServerBulkCommon.java | 4 ++-- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java index 2b4b39179..62d990d21 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java @@ -35,6 +35,7 @@ public class SQLServerBulkBatchInsertRecord extends SQLServerBulkCommon implemen public SQLServerBulkBatchInsertRecord(ArrayList batchParam, ArrayList columnList, ArrayList valueList, String encoding) throws SQLServerException { loggerClassName = "com.microsoft.sqlserver.jdbc.SQLServerBulkBatchInsertRecord"; + loggerExternal = java.util.logging.Logger.getLogger(loggerClassName); loggerExternal.entering(loggerClassName, "SQLServerBulkBatchInsertRecord", new Object[] {batchParam, encoding}); diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCSVFileRecord.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCSVFileRecord.java index 3fc1bcc87..9840e2c24 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCSVFileRecord.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCSVFileRecord.java @@ -67,6 +67,7 @@ public SQLServerBulkCSVFileRecord(String fileToParse, String delimiter, boolean firstLineIsColumnNames) throws SQLServerException { loggerClassName = "com.microsoft.sqlserver.jdbc.SQLServerBulkCSVFileRecord"; + loggerExternal = java.util.logging.Logger.getLogger(loggerClassName); loggerExternal.entering(loggerClassName, "SQLServerBulkCSVFileRecord", new Object[] {fileToParse, encoding, delimiter, firstLineIsColumnNames}); @@ -128,6 +129,7 @@ public SQLServerBulkCSVFileRecord(InputStream fileToParse, String delimiter, boolean firstLineIsColumnNames) throws SQLServerException { loggerClassName = "com.microsoft.sqlserver.jdbc.SQLServerBulkCSVFileRecord"; + loggerExternal = java.util.logging.Logger.getLogger(loggerClassName); loggerExternal.entering(loggerClassName, "SQLServerBulkCSVFileRecord", new Object[] {fileToParse, encoding, delimiter, firstLineIsColumnNames}); diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCommon.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCommon.java index b5b832d54..4aeb93d98 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCommon.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCommon.java @@ -46,7 +46,7 @@ protected class ColumnMetadata { /* * Logger */ - protected static final java.util.logging.Logger loggerExternal = java.util.logging.Logger.getLogger(loggerClassName); + protected java.util.logging.Logger loggerExternal; /* * Contains all the column names if firstLineIsColumnNames is true @@ -99,7 +99,7 @@ public void addColumnMetadata(int positionInSource, /** * Adds metadata for the given column in the batch/file. * - * @param positionInTable + * @param positionInSource * Indicates which column the metadata is for. Columns start at 1. * @param name * Name for the column (optional if only using column ordinal in a mapping for SQLServerBulkCopy operation) From 34d8bb13902904957a7a74a04001cc1c604ff00c Mon Sep 17 00:00:00 2001 From: Peter Bae Date: Mon, 28 May 2018 09:57:09 -0700 Subject: [PATCH 12/32] fix problem with precision / scale --- .../com/microsoft/sqlserver/jdbc/SQLServerBulkCommon.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCommon.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCommon.java index 4aeb93d98..fc4c79679 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCommon.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCommon.java @@ -177,11 +177,11 @@ else if ((columnNames != null) && (columnNames.length >= positionInSource)) case java.sql.Types.TIMESTAMP: case microsoft.sql.Types.DATETIMEOFFSET: if (this instanceof SQLServerBulkCSVFileRecord) { - columnMetadata.put(positionInSource, new ColumnMetadata(colName, jdbcType, 50, precision, dateTimeFormatter)); + columnMetadata.put(positionInSource, new ColumnMetadata(colName, jdbcType, 50, scale, dateTimeFormatter)); } else if (this instanceof SQLServerBulkBatchInsertRecord) { - columnMetadata.put(positionInSource, new ColumnMetadata(colName, jdbcType, precision, precision, dateTimeFormatter)); + columnMetadata.put(positionInSource, new ColumnMetadata(colName, jdbcType, precision, scale, dateTimeFormatter)); } else { - columnMetadata.put(positionInSource, new ColumnMetadata(colName, jdbcType, 50, precision, dateTimeFormatter)); + columnMetadata.put(positionInSource, new ColumnMetadata(colName, jdbcType, 50, scale, dateTimeFormatter)); } break; From 39060b7e7f07f2ba0e880baa9395ba4449717673 Mon Sep 17 00:00:00 2001 From: Peter Bae Date: Tue, 29 May 2018 10:19:00 -0700 Subject: [PATCH 13/32] fix issue with setting all to true --- .../microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java index 2f33b7662..d2e195379 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java @@ -2473,7 +2473,7 @@ public int[] executeBatch() throws SQLServerException, BatchUpdateException, SQL localUserSQL = userSQL; try { - if (isInsert(localUserSQL) && true && (this.useBulkCopyForBatchInsertOnDW || connection.getUseBulkCopyForBatchInsertOnDW())) { + if (isInsert(localUserSQL) && connection.isAzureDW() && (this.useBulkCopyForBatchInsertOnDW || connection.getUseBulkCopyForBatchInsertOnDW())) { if (batchParamValues == null) { updateCounts = new int[0]; loggerExternal.exiting(getClassNameLogging(), "executeBatch", updateCounts); From d80908efd11fffe2ddea378f583291db11cce956 Mon Sep 17 00:00:00 2001 From: Peter Bae Date: Tue, 29 May 2018 13:17:20 -0700 Subject: [PATCH 14/32] make bamoo fixes --- .../jdbc/SQLServerPreparedStatement.java | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java index d2e195379..5f047bfd5 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java @@ -113,11 +113,23 @@ private void setPreparedStatementHandle(int handle) { */ private boolean useBulkCopyForBatchInsertOnDW; - public boolean getUseBulkCopyForBatchInsertOnDW() { + /** Sets the prepared statement's useBulkCopyForBatchInsertOnDW value. + * + * @return + * Per the description. + * @throws SQLServerException when an error occurs + */ + public boolean getUseBulkCopyForBatchInsertOnDW() throws SQLServerException { + checkClosed(); return useBulkCopyForBatchInsertOnDW; } - public void setUseBulkCopyForBatchInsertOnDW(boolean useBulkCopyForBatchInsertOnDW) { + /** Fetches the prepared statement's useBulkCopyForBatchInsertOnDW value. + * + * @throws SQLServerException when an error occurs + */ + public void setUseBulkCopyForBatchInsertOnDW(boolean useBulkCopyForBatchInsertOnDW) throws SQLServerException { + checkClosed(); this.useBulkCopyForBatchInsertOnDW = useBulkCopyForBatchInsertOnDW; } From 8d044b1d111d81e8f51158e18069ed5a7ebc417c Mon Sep 17 00:00:00 2001 From: Peter Bae Date: Tue, 29 May 2018 17:30:43 -0700 Subject: [PATCH 15/32] undo some changes made to SQLServerConnection --- .../sqlserver/jdbc/SQLServerConnection.java | 34 ++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index 01d4a9b70..a4a228fa0 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -117,19 +117,20 @@ public class SQLServerConnection implements ISQLServerConnection { private SqlFedAuthToken fedAuthToken = null; + private String originalHostNameInCertificate = null; + private Boolean isAzureDW = null; static class Sha1HashKey { private byte[] bytes; Sha1HashKey(String sql, - String parametersDefinition, - String dbName) { - this(String.format("%s%s%s", sql, parametersDefinition, dbName)); + String parametersDefinition) { + this(String.format("%s%s", sql, parametersDefinition)); } Sha1HashKey(String s) { - bytes = getSha1Digest().digest(s.getBytes()); + bytes = getSha1Digest().digest(s.getBytes()); } public boolean equals(Object obj) { @@ -1210,6 +1211,23 @@ Connection connectInternal(Properties propsIn, pooledConnectionParent = pooledConnection; + String hostNameInCertificate = activeConnectionProperties. + getProperty(SQLServerDriverStringProperty.HOSTNAME_IN_CERTIFICATE.toString()); + + // hostNameInCertificate property can change when redirection is involved, so maintain this value + // for every instance of SQLServerConnection. + if (null == originalHostNameInCertificate && null != hostNameInCertificate && !hostNameInCertificate.isEmpty()) { + originalHostNameInCertificate = activeConnectionProperties. + getProperty(SQLServerDriverStringProperty.HOSTNAME_IN_CERTIFICATE.toString()); + } + + if (null != originalHostNameInCertificate && !originalHostNameInCertificate.isEmpty()) { + // if hostNameInCertificate has a legitimate value (and not empty or null), + // reset hostNameInCertificate to the original value every time we connect (or re-connect). + activeConnectionProperties.setProperty(SQLServerDriverStringProperty.HOSTNAME_IN_CERTIFICATE.toString(), + originalHostNameInCertificate); + } + String sPropKey; String sPropValue; @@ -5739,18 +5757,18 @@ final void unprepareUnreferencedPreparedStatementHandles(boolean force) { } /** - * Returns true if statement pooling is disabled. + * Determine whether statement pooling is disabled. * - * @return + * @return true if statement pooling is disabled, false if it is enabled. */ public boolean getDisableStatementPooling() { return this.disableStatementPooling; } /** - * Sets statement pooling to true or false; + * Disable/enable statement pooling. * - * @param value + * @param value true to disable statement pooling, false to enable it. */ public void setDisableStatementPooling(boolean value) { this.disableStatementPooling = value; From 70bea50cc2349035b4efb3f524db707c5e6f021a Mon Sep 17 00:00:00 2001 From: Peter Bae Date: Tue, 29 May 2018 17:50:18 -0700 Subject: [PATCH 16/32] apply resource bundling changes --- .../statement/BatchExecuteWithErrorsTest.java | 112 ++++++++++-------- 1 file changed, 61 insertions(+), 51 deletions(-) diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/BatchExecuteWithErrorsTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/BatchExecuteWithErrorsTest.java index d86e08c39..749b41b91 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/BatchExecuteWithErrorsTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/BatchExecuteWithErrorsTest.java @@ -30,6 +30,7 @@ import org.junit.runner.RunWith; import com.microsoft.sqlserver.jdbc.SQLServerConnection; +import com.microsoft.sqlserver.jdbc.TestResource; import com.microsoft.sqlserver.testframework.AbstractSQLGenerator; import com.microsoft.sqlserver.testframework.AbstractTest; import com.microsoft.sqlserver.testframework.DBConnection; @@ -142,7 +143,7 @@ public void Repro47239() throws SQLException { log.fine("" + updateCount + ","); } log.fine(""); - assertTrue(Arrays.equals(actualUpdateCounts, expectedUpdateCounts), "Test interleaved inserts and warnings"); + assertTrue(Arrays.equals(actualUpdateCounts, expectedUpdateCounts), TestResource.getResource("R_testInterleaved")); expectedUpdateCounts = new int[] {-3, 1, 1, 1}; stmt.addBatch(error); @@ -162,7 +163,7 @@ public void Repro47239() throws SQLException { log.fine("" + updateCount + ","); } log.fine(""); - assertTrue(Arrays.equals(actualUpdateCounts, expectedUpdateCounts), "Test error followed by inserts"); + assertTrue(Arrays.equals(actualUpdateCounts, expectedUpdateCounts), TestResource.getResource("R_errorFollowInserts")); // 50280 expectedUpdateCounts = new int[] {1, -3}; stmt.addBatch(insertStmt); @@ -179,7 +180,7 @@ public void Repro47239() throws SQLException { log.fine("" + updateCount + ","); } log.fine(""); - assertTrue(Arrays.equals(actualUpdateCounts, expectedUpdateCounts), "Test insert followed by non-fatal error (50280)"); + assertTrue(Arrays.equals(actualUpdateCounts, expectedUpdateCounts), TestResource.getResource("R_errorFollow50280")); // Test "soft" errors conn.setAutoCommit(false); @@ -189,12 +190,13 @@ public void Repro47239() throws SQLException { stmt.addBatch(insertStmt); try { stmt.executeBatch(); - assertEquals(true, false, "Soft error test: executeBatch unexpectedly succeeded"); + // Soft error test: executeBatch unexpectedly succeeded + assertEquals(true, false, TestResource.getResource("R_shouldThrowException")); } catch (BatchUpdateException bue) { - assertEquals("A result set was generated for update.", bue.getMessage(), "Soft error test: wrong error message in BatchUpdateException"); + assertEquals("A result set was generated for update.", bue.getMessage(), TestResource.getResource("R_unexpectedExceptionContent")); assertEquals(Arrays.equals(bue.getUpdateCounts(), new int[] {-3, 1, -3, 1}), true, - "Soft error test: wrong update counts in BatchUpdateException"); + TestResource.getResource("R_incorrectUpdateCount")); } conn.rollback(); @@ -207,12 +209,12 @@ public void Repro47239() throws SQLException { stmt.executeBatch(); } catch (BatchUpdateException bue) { - assertThat(bue.getMessage(), containsString("Syntax error converting date")); + assertThat(bue.getMessage(), containsString(TestResource.getResource("R_syntaxErrorDateConvert"))); // CTestLog.CompareStartsWith(bue.getMessage(), "Syntax error converting date", "Transaction rollback with conversion error threw wrong // BatchUpdateException"); } catch (SQLException e) { - assertThat(e.getMessage(), containsString("Conversion failed when converting date")); + assertThat(e.getMessage(), containsString(TestResource.getResource("R_dateConvertError"))); // CTestLog.CompareStartsWith(e.getMessage(), "Conversion failed when converting date", "Transaction rollback with conversion error threw // wrong SQLException"); } @@ -239,19 +241,22 @@ public void Repro47239() throws SQLException { stmt.addBatch(insertStmt); try { stmt.executeBatch(); - assertEquals(false, true, "Test fatal errors batch execution succeeded (should have failed)"); + // Test fatal errors batch execution succeeded (should have failed) + assertEquals(false, true, TestResource.getResource("R_shouldThrowException")); } catch (BatchUpdateException bue) { - assertEquals(false, true, "Test fatal errors returned BatchUpdateException rather than SQLException"); + // Test fatal errors returned BatchUpdateException rather than SQLException + assertEquals(false, true, TestResource.getResource("R_unexpectedException") + bue.getMessage()); + } catch (SQLException e) { actualExceptionText = e.getMessage(); if (actualExceptionText.endsWith("reset")) { - assertTrue(actualExceptionText.equalsIgnoreCase("Connection reset"), "Test fatal errors"); + assertTrue(actualExceptionText.equalsIgnoreCase("Connection reset"), TestResource.getResource("R_unexpectedExceptionContent") + ": " + actualExceptionText); } else { - assertTrue(actualExceptionText.equalsIgnoreCase("raiserror level 20"), "Test fatal errors"); + assertTrue(actualExceptionText.equalsIgnoreCase("raiserror level 20"), TestResource.getResource("R_unexpectedExceptionContent") + ": " + actualExceptionText); } } } @@ -264,7 +269,7 @@ public void Repro47239() throws SQLException { stmt.close(); conn.close(); } - + /** * Batch test * @@ -362,7 +367,7 @@ public void Repro47239UseBulkCopyAPI() throws SQLException, NoSuchFieldException log.fine("" + updateCount + ","); } log.fine(""); - assertTrue(Arrays.equals(actualUpdateCounts, expectedUpdateCounts), "Test interleaved inserts and warnings"); + assertTrue(Arrays.equals(actualUpdateCounts, expectedUpdateCounts), TestResource.getResource("R_testInterleaved")); expectedUpdateCounts = new int[] {-3, 1, 1, 1}; stmt.addBatch(error); @@ -382,7 +387,7 @@ public void Repro47239UseBulkCopyAPI() throws SQLException, NoSuchFieldException log.fine("" + updateCount + ","); } log.fine(""); - assertTrue(Arrays.equals(actualUpdateCounts, expectedUpdateCounts), "Test error followed by inserts"); + assertTrue(Arrays.equals(actualUpdateCounts, expectedUpdateCounts), TestResource.getResource("R_errorFollowInserts")); // 50280 expectedUpdateCounts = new int[] {1, -3}; stmt.addBatch(insertStmt); @@ -399,7 +404,7 @@ public void Repro47239UseBulkCopyAPI() throws SQLException, NoSuchFieldException log.fine("" + updateCount + ","); } log.fine(""); - assertTrue(Arrays.equals(actualUpdateCounts, expectedUpdateCounts), "Test insert followed by non-fatal error (50280)"); + assertTrue(Arrays.equals(actualUpdateCounts, expectedUpdateCounts), TestResource.getResource("R_errorFollow50280")); // Test "soft" errors conn.setAutoCommit(false); @@ -409,12 +414,12 @@ public void Repro47239UseBulkCopyAPI() throws SQLException, NoSuchFieldException stmt.addBatch(insertStmt); try { stmt.executeBatch(); - assertEquals(true, false, "Soft error test: executeBatch unexpectedly succeeded"); + assertEquals(true, false, TestResource.getResource("R_shouldThrowException")); } catch (BatchUpdateException bue) { - assertEquals("A result set was generated for update.", bue.getMessage(), "Soft error test: wrong error message in BatchUpdateException"); + assertEquals("A result set was generated for update.", bue.getMessage(), TestResource.getResource("R_unexpectedExceptionContent")); assertEquals(Arrays.equals(bue.getUpdateCounts(), new int[] {-3, 1, -3, 1}), true, - "Soft error test: wrong update counts in BatchUpdateException"); + TestResource.getResource("R_incorrectUpdateCount")); } conn.rollback(); @@ -427,12 +432,12 @@ public void Repro47239UseBulkCopyAPI() throws SQLException, NoSuchFieldException stmt.executeBatch(); } catch (BatchUpdateException bue) { - assertThat(bue.getMessage(), containsString("Syntax error converting date")); + assertThat(bue.getMessage(), containsString(TestResource.getResource("R_syntaxErrorDateConvert"))); // CTestLog.CompareStartsWith(bue.getMessage(), "Syntax error converting date", "Transaction rollback with conversion error threw wrong // BatchUpdateException"); } catch (SQLException e) { - assertThat(e.getMessage(), containsString("Conversion failed when converting date")); + assertThat(e.getMessage(), containsString(TestResource.getResource("R_dateConvertError"))); // CTestLog.CompareStartsWith(e.getMessage(), "Conversion failed when converting date", "Transaction rollback with conversion error threw // wrong SQLException"); } @@ -459,19 +464,19 @@ public void Repro47239UseBulkCopyAPI() throws SQLException, NoSuchFieldException stmt.addBatch(insertStmt); try { stmt.executeBatch(); - assertEquals(false, true, "Test fatal errors batch execution succeeded (should have failed)"); + assertEquals(false, true, TestResource.getResource("R_shouldThrowException")); } catch (BatchUpdateException bue) { - assertEquals(false, true, "Test fatal errors returned BatchUpdateException rather than SQLException"); + assertEquals(false, true, TestResource.getResource("R_unexpectedException") + bue.getMessage()); } catch (SQLException e) { actualExceptionText = e.getMessage(); if (actualExceptionText.endsWith("reset")) { - assertTrue(actualExceptionText.equalsIgnoreCase("Connection reset"), "Test fatal errors"); + assertTrue(actualExceptionText.equalsIgnoreCase("Connection reset"), TestResource.getResource("R_unexpectedExceptionContent") + ": " + actualExceptionText); } else { - assertTrue(actualExceptionText.equalsIgnoreCase("raiserror level 20"), "Test fatal errors"); + assertTrue(actualExceptionText.equalsIgnoreCase("raiserror level 20"), TestResource.getResource("R_unexpectedExceptionContent") + ": " + actualExceptionText); } } } @@ -494,7 +499,7 @@ public void Repro47239UseBulkCopyAPI() throws SQLException, NoSuchFieldException @DisplayName("Regression test for using 'large' methods") public void Repro47239large() throws Exception { - assumeTrue("JDBC42".equals(Utils.getConfiguredProperty("JDBC_Version")), "Aborting test case as JDBC version is not compatible. "); + assumeTrue("JDBC42".equals(Utils.getConfiguredProperty("JDBC_Version")), TestResource.getResource("R_incompatJDBC")); // the DBConnection for detecting whether the server is SQL Azure or SQL Server. con = DriverManager.getConnection(connectionString); final String warning; @@ -566,7 +571,7 @@ public void Repro47239large() throws Exception { log.fine("" + updateCount + ","); } log.fine(""); - assertTrue(Arrays.equals(actualUpdateCounts, expectedUpdateCounts), "Test interleaved inserts and warnings"); + assertTrue(Arrays.equals(actualUpdateCounts, expectedUpdateCounts), TestResource.getResource("R_testInterleaved")); expectedUpdateCounts = new long[] {-3, 1, 1, 1}; stmt.addBatch(error); @@ -586,7 +591,7 @@ public void Repro47239large() throws Exception { log.fine("" + updateCount + ","); } log.fine(""); - assertTrue(Arrays.equals(actualUpdateCounts, expectedUpdateCounts), "Test error followed by inserts"); + assertTrue(Arrays.equals(actualUpdateCounts, expectedUpdateCounts), TestResource.getResource("R_errorFollowInserts")); // 50280 expectedUpdateCounts = new long[] {1, -3}; @@ -604,7 +609,7 @@ public void Repro47239large() throws Exception { log.fine("" + updateCount + ","); } log.fine(""); - assertTrue(Arrays.equals(actualUpdateCounts, expectedUpdateCounts), "Test insert followed by non-fatal error (50280)"); + assertTrue(Arrays.equals(actualUpdateCounts, expectedUpdateCounts), TestResource.getResource("R_errorFollow50280")); // Test "soft" errors conn.setAutoCommit(false); @@ -614,12 +619,15 @@ public void Repro47239large() throws Exception { stmt.addBatch(insertStmt); try { stmt.executeLargeBatch(); - assertEquals(false, true, "Soft error test: executeLargeBatch unexpectedly succeeded"); + // Soft error test: executeLargeBatch unexpectedly succeeded + assertEquals(false, true, TestResource.getResource("R_shouldThrowException")); } catch (BatchUpdateException bue) { - assertEquals("A result set was generated for update.", bue.getMessage(), "Soft error test: wrong error message in BatchUpdateException"); + // Soft error test: wrong error message in BatchUpdateException + assertEquals("A result set was generated for update.", bue.getMessage(), TestResource.getResource("R_unexpectedExceptionContent")); + // Soft error test: wrong update counts in BatchUpdateException assertEquals(Arrays.equals(bue.getLargeUpdateCounts(), new long[] {-3, 1, -3, 1}), true, - "Soft error test: wrong update counts in BatchUpdateException"); + TestResource.getResource("R_incorrectUpdateCount")); } conn.rollback(); @@ -632,10 +640,10 @@ public void Repro47239large() throws Exception { stmt.executeLargeBatch(); } catch (BatchUpdateException bue) { - assertThat(bue.getMessage(), containsString("Syntax error converting date")); + assertThat(bue.getMessage(), containsString(TestResource.getResource("R_syntaxErrorDateConvert"))); } catch (SQLException e) { - assertThat(e.getMessage(), containsString("Conversion failed when converting date")); + assertThat(e.getMessage(), containsString(TestResource.getResource("R_dateConvertError"))); } conn.setAutoCommit(true); @@ -659,19 +667,21 @@ public void Repro47239large() throws Exception { stmt.addBatch(insertStmt); try { stmt.executeLargeBatch(); - assertEquals(false, true, "Test fatal errors batch execution succeeded (should have failed)"); + // Test fatal errors batch execution succeeded (should have failed) + assertEquals(false, true, TestResource.getResource("R_shouldThrowException")); } catch (BatchUpdateException bue) { - assertEquals(false, true, "Test fatal errors returned BatchUpdateException rather than SQLException"); + // Test fatal errors returned BatchUpdateException rather than SQLException + assertEquals(false, true, TestResource.getResource("R_unexpectedException") + bue.getMessage()); } catch (SQLException e) { actualExceptionText = e.getMessage(); if (actualExceptionText.endsWith("reset")) { - assertTrue(actualExceptionText.equalsIgnoreCase("Connection reset"), "Test fatal errors"); + assertTrue(actualExceptionText.equalsIgnoreCase("Connection reset"), TestResource.getResource("R_unexpectedExceptionContent") + ": " + actualExceptionText); } else { - assertTrue(actualExceptionText.equalsIgnoreCase("raiserror level 20"), "Test fatal errors"); + assertTrue(actualExceptionText.equalsIgnoreCase("raiserror level 20"), TestResource.getResource("R_unexpectedExceptionContent") + ": " + actualExceptionText); } } @@ -695,7 +705,7 @@ public void Repro47239large() throws Exception { @DisplayName("Regression test for using 'large' methods") public void Repro47239largeUseBulkCopyAPI() throws Exception { - assumeTrue("JDBC42".equals(Utils.getConfiguredProperty("JDBC_Version")), "Aborting test case as JDBC version is not compatible. "); + assumeTrue("JDBC42".equals(Utils.getConfiguredProperty("JDBC_Version")), TestResource.getResource("R_incompatJDBC")); // the DBConnection for detecting whether the server is SQL Azure or SQL Server. con = DriverManager.getConnection(connectionString); Field f1 = SQLServerConnection.class.getDeclaredField("isAzureDW"); @@ -770,7 +780,7 @@ public void Repro47239largeUseBulkCopyAPI() throws Exception { log.fine("" + updateCount + ","); } log.fine(""); - assertTrue(Arrays.equals(actualUpdateCounts, expectedUpdateCounts), "Test interleaved inserts and warnings"); + assertTrue(Arrays.equals(actualUpdateCounts, expectedUpdateCounts), TestResource.getResource("R_testInterleaved")); expectedUpdateCounts = new long[] {-3, 1, 1, 1}; stmt.addBatch(error); @@ -790,7 +800,7 @@ public void Repro47239largeUseBulkCopyAPI() throws Exception { log.fine("" + updateCount + ","); } log.fine(""); - assertTrue(Arrays.equals(actualUpdateCounts, expectedUpdateCounts), "Test error followed by inserts"); + assertTrue(Arrays.equals(actualUpdateCounts, expectedUpdateCounts), TestResource.getResource("R_errorFollowInserts")); // 50280 expectedUpdateCounts = new long[] {1, -3}; @@ -808,7 +818,7 @@ public void Repro47239largeUseBulkCopyAPI() throws Exception { log.fine("" + updateCount + ","); } log.fine(""); - assertTrue(Arrays.equals(actualUpdateCounts, expectedUpdateCounts), "Test insert followed by non-fatal error (50280)"); + assertTrue(Arrays.equals(actualUpdateCounts, expectedUpdateCounts), TestResource.getResource("R_errorFollow50280")); // Test "soft" errors conn.setAutoCommit(false); @@ -818,12 +828,12 @@ public void Repro47239largeUseBulkCopyAPI() throws Exception { stmt.addBatch(insertStmt); try { stmt.executeLargeBatch(); - assertEquals(false, true, "Soft error test: executeLargeBatch unexpectedly succeeded"); + assertEquals(false, true, TestResource.getResource("R_shouldThrowException")); } catch (BatchUpdateException bue) { - assertEquals("A result set was generated for update.", bue.getMessage(), "Soft error test: wrong error message in BatchUpdateException"); + assertEquals("A result set was generated for update.", bue.getMessage(), TestResource.getResource("R_unexpectedExceptionContent")); assertEquals(Arrays.equals(bue.getLargeUpdateCounts(), new long[] {-3, 1, -3, 1}), true, - "Soft error test: wrong update counts in BatchUpdateException"); + TestResource.getResource("R_incorrectUpdateCount")); } conn.rollback(); @@ -836,10 +846,10 @@ public void Repro47239largeUseBulkCopyAPI() throws Exception { stmt.executeLargeBatch(); } catch (BatchUpdateException bue) { - assertThat(bue.getMessage(), containsString("Syntax error converting date")); + assertThat(bue.getMessage(), containsString(TestResource.getResource("R_syntaxErrorDateConvert"))); } catch (SQLException e) { - assertThat(e.getMessage(), containsString("Conversion failed when converting date")); + assertThat(e.getMessage(), containsString(TestResource.getResource("R_dateConvertError"))); } conn.setAutoCommit(true); @@ -863,19 +873,19 @@ public void Repro47239largeUseBulkCopyAPI() throws Exception { stmt.addBatch(insertStmt); try { stmt.executeLargeBatch(); - assertEquals(false, true, "Test fatal errors batch execution succeeded (should have failed)"); + assertEquals(false, true, TestResource.getResource("R_shouldThrowException")); } catch (BatchUpdateException bue) { - assertEquals(false, true, "Test fatal errors returned BatchUpdateException rather than SQLException"); + assertEquals(false, true, TestResource.getResource("R_unexpectedException") + bue.getMessage()); } catch (SQLException e) { actualExceptionText = e.getMessage(); if (actualExceptionText.endsWith("reset")) { - assertTrue(actualExceptionText.equalsIgnoreCase("Connection reset"), "Test fatal errors"); + assertTrue(actualExceptionText.equalsIgnoreCase("Connection reset"), TestResource.getResource("R_unexpectedExceptionContent") + ": " + actualExceptionText); } else { - assertTrue(actualExceptionText.equalsIgnoreCase("raiserror level 20"), "Test fatal errors"); + assertTrue(actualExceptionText.equalsIgnoreCase("raiserror level 20"), TestResource.getResource("R_unexpectedExceptionContent") + ": " + actualExceptionText); } } From c20435499f00a875ff0c7d79f900139b4e6f14e8 Mon Sep 17 00:00:00 2001 From: Peter Bae Date: Mon, 4 Jun 2018 17:34:10 -0700 Subject: [PATCH 17/32] remove on_dw, and remove redundant fmtonly --- .../sqlserver/jdbc/SQLServerBulkCopy.java | 20 +++++++-- .../sqlserver/jdbc/SQLServerConnection.java | 10 ++--- .../sqlserver/jdbc/SQLServerDataSource.java | 14 +++---- .../sqlserver/jdbc/SQLServerDriver.java | 4 +- .../jdbc/SQLServerPreparedStatement.java | 41 ++++++++++--------- .../sqlserver/jdbc/SQLServerResource.java | 2 +- 6 files changed, 52 insertions(+), 39 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java index 732ae9c0f..ba36cbda4 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java @@ -156,6 +156,8 @@ private class ColumnMapping { /* Statement level encryption setting needed for querying against encrypted columns. */ private SQLServerStatementColumnEncryptionSetting stmtColumnEncriptionSetting = SQLServerStatementColumnEncryptionSetting.UseConnectionSetting; + private ResultSet destinationTableMetadata; + /* * Metadata for the destination table columns */ @@ -1746,10 +1748,16 @@ private void getDestinationMetadata() throws SQLServerException { SQLServerStatement stmt = null; try { - stmt = (SQLServerStatement) connection.createStatement(ResultSet.TYPE_FORWARD_ONLY, - ResultSet.CONCUR_READ_ONLY, connection.getHoldability(), stmtColumnEncriptionSetting); - // Get destination metadata - rs = stmt.executeQueryInternal("SET FMTONLY ON SELECT * FROM " + destinationTableName + " SET FMTONLY OFF "); + if (null == destinationTableMetadata) { + 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 " + destinationTableName + " '"); + } + else { + rs = (SQLServerResultSet) destinationTableMetadata; + } destColumnCount = rs.getMetaData().getColumnCount(); destColumnMetadata = new HashMap<>(); @@ -3584,4 +3592,8 @@ private boolean writeBatchData(TDSWriter tdsWriter, protected void setStmtColumnEncriptionSetting(SQLServerStatementColumnEncryptionSetting stmtColumnEncriptionSetting) { this.stmtColumnEncriptionSetting = stmtColumnEncriptionSetting; } + + protected void setDestinationTableMetadata(SQLServerResultSet rs) { + destinationTableMetadata = rs; + } } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index 523a3c185..d094d2055 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -496,10 +496,10 @@ final int getSocketTimeoutMilliseconds() { /** * boolean value for deciding if the driver should use bulk copy API for batch inserts */ - private boolean useBulkCopyForBatchInsertOnDW; + private boolean useBulkCopyForBatchInsert; - final boolean getUseBulkCopyForBatchInsertOnDW() { - return useBulkCopyForBatchInsertOnDW; + final boolean getUseBulkCopyForBatchInsert() { + return useBulkCopyForBatchInsert; } boolean userSetTNIR = true; @@ -1777,10 +1777,10 @@ else if (0 == requestedPacketSize) setEnablePrepareOnFirstPreparedStatementCall(booleanPropertyOn(sPropKey, sPropValue)); } - sPropKey = SQLServerDriverBooleanProperty.USE_BULK_COPY_FOR_BATCH_INSERT_ON_DW.toString(); + sPropKey = SQLServerDriverBooleanProperty.USE_BULK_COPY_FOR_BATCH_INSERT.toString(); sPropValue = activeConnectionProperties.getProperty(sPropKey); if (null != sPropValue) { - useBulkCopyForBatchInsertOnDW = booleanPropertyOn(sPropKey, sPropValue); + useBulkCopyForBatchInsert = booleanPropertyOn(sPropKey, sPropValue); } sPropKey = SQLServerDriverStringProperty.SSL_PROTOCOL.toString(); diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java index 5cb55e9a1..0c143c293 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java @@ -825,11 +825,11 @@ public int getSocketTimeout() { /** * Setting the use Bulk Copy API for Batch Insert on Azure Data Warehouse boolean * - * @param useBulkCopyForBatchInsertOnDW indicates whether Bulk Copy API should be used for Batch Insert operations. + * @param useBulkCopyForBatchInsert indicates whether Bulk Copy API should be used for Batch Insert operations. */ - public void setUseBulkCopyForBatchInsertOnDW(boolean useBulkCopyForBatchInsertOnDW) { - setBooleanProperty(connectionProps, SQLServerDriverBooleanProperty.USE_BULK_COPY_FOR_BATCH_INSERT_ON_DW.toString(), - useBulkCopyForBatchInsertOnDW); + public void setUseBulkCopyForBatchInsert(boolean useBulkCopyForBatchInsert) { + setBooleanProperty(connectionProps, SQLServerDriverBooleanProperty.USE_BULK_COPY_FOR_BATCH_INSERT.toString(), + useBulkCopyForBatchInsert); } /** @@ -837,9 +837,9 @@ public void setUseBulkCopyForBatchInsertOnDW(boolean useBulkCopyForBatchInsertOn * * @return whether the driver should use Bulk Copy API for Batch Insert operations. */ - public boolean getUseBulkCopyForBatchInsertOnDW() { - return getBooleanProperty(connectionProps, SQLServerDriverBooleanProperty.USE_BULK_COPY_FOR_BATCH_INSERT_ON_DW.toString(), - SQLServerDriverBooleanProperty.USE_BULK_COPY_FOR_BATCH_INSERT_ON_DW.getDefaultValue()); + public boolean getUseBulkCopyForBatchInsert() { + return getBooleanProperty(connectionProps, SQLServerDriverBooleanProperty.USE_BULK_COPY_FOR_BATCH_INSERT.toString(), + SQLServerDriverBooleanProperty.USE_BULK_COPY_FOR_BATCH_INSERT.getDefaultValue()); } /** diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java index 0ea70f7e4..5f67f93db 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java @@ -354,7 +354,7 @@ enum SQLServerDriverBooleanProperty XOPEN_STATES ("xopenStates", false), FIPS ("fips", false), ENABLE_PREPARE_ON_FIRST_PREPARED_STATEMENT("enablePrepareOnFirstPreparedStatementCall", SQLServerConnection.DEFAULT_ENABLE_PREPARE_ON_FIRST_PREPARED_STATEMENT_CALL), - USE_BULK_COPY_FOR_BATCH_INSERT_ON_DW ("useBulkCopyForBatchInsertOnDW", false); + USE_BULK_COPY_FOR_BATCH_INSERT ("useBulkCopyForBatchInsert", false); private final String name; private final boolean defaultValue; @@ -431,7 +431,7 @@ public final class SQLServerDriver implements java.sql.Driver { new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.JAAS_CONFIG_NAME.toString(), SQLServerDriverStringProperty.JAAS_CONFIG_NAME.getDefaultValue(), false, null), new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.SSL_PROTOCOL.toString(), SQLServerDriverStringProperty.SSL_PROTOCOL.getDefaultValue(), false, new String[] {SSLProtocol.TLS.toString(), SSLProtocol.TLS_V10.toString(), SSLProtocol.TLS_V11.toString(), SSLProtocol.TLS_V12.toString()}), new SQLServerDriverPropertyInfo(SQLServerDriverIntProperty.CANCEL_QUERY_TIMEOUT.toString(), Integer.toString(SQLServerDriverIntProperty.CANCEL_QUERY_TIMEOUT.getDefaultValue()), false, null), - new SQLServerDriverPropertyInfo(SQLServerDriverBooleanProperty.USE_BULK_COPY_FOR_BATCH_INSERT_ON_DW.toString(), Boolean.toString(SQLServerDriverBooleanProperty.USE_BULK_COPY_FOR_BATCH_INSERT_ON_DW.getDefaultValue()),false, TRUE_FALSE), + new SQLServerDriverPropertyInfo(SQLServerDriverBooleanProperty.USE_BULK_COPY_FOR_BATCH_INSERT.toString(), Boolean.toString(SQLServerDriverBooleanProperty.USE_BULK_COPY_FOR_BATCH_INSERT.getDefaultValue()), false, TRUE_FALSE), }; // Properties that can only be set by using Properties. diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java index 9a9dbe7bd..daedef2da 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java @@ -110,26 +110,26 @@ private void setPreparedStatementHandle(int handle) { /** * boolean value for deciding if the driver should use bulk copy API for batch inserts */ - private boolean useBulkCopyForBatchInsertOnDW; + private boolean useBulkCopyForBatchInsert; - /** Sets the prepared statement's useBulkCopyForBatchInsertOnDW value. + /** Sets the prepared statement's useBulkCopyForBatchInsert value. * * @return * Per the description. * @throws SQLServerException when an error occurs */ - public boolean getUseBulkCopyForBatchInsertOnDW() throws SQLServerException { + public boolean getUseBulkCopyForBatchInsert() throws SQLServerException { checkClosed(); - return useBulkCopyForBatchInsertOnDW; + return useBulkCopyForBatchInsert; } - /** Fetches the prepared statement's useBulkCopyForBatchInsertOnDW value. + /** Fetches the prepared statement's useBulkCopyForBatchInsert value. * * @throws SQLServerException when an error occurs */ - public void setUseBulkCopyForBatchInsertOnDW(boolean useBulkCopyForBatchInsertOnDW) throws SQLServerException { + public void setUseBulkCopyForBatchInsert(boolean useBulkCopyForBatchInsert) throws SQLServerException { checkClosed(); - this.useBulkCopyForBatchInsertOnDW = useBulkCopyForBatchInsertOnDW; + this.useBulkCopyForBatchInsert = useBulkCopyForBatchInsert; } /** The server handle for this prepared statement. If a value {@literal <} 1 is returned no handle has been created. @@ -2479,24 +2479,24 @@ public int[] executeBatch() throws SQLServerException, BatchUpdateException, SQL localUserSQL = userSQL; try { - if (isInsert(localUserSQL) && connection.isAzureDW() && (this.useBulkCopyForBatchInsertOnDW || connection.getUseBulkCopyForBatchInsertOnDW())) { + if (isInsert(localUserSQL) && true && (this.useBulkCopyForBatchInsert || connection.getUseBulkCopyForBatchInsert())) { if (batchParamValues == null) { updateCounts = new int[0]; loggerExternal.exiting(getClassNameLogging(), "executeBatch", updateCounts); - return updateCounts; + return updateCounts; } String tableName = parseUserSQLForTableNameDW(false, false); ArrayList columnList = parseUserSQLForColumnListDW(); ArrayList valueList = parseUserSQLForValueListDW(false); - + String destinationTableName = tableName; - SQLServerStatement stmt = (SQLServerStatement) connection.createStatement(ResultSet.TYPE_FORWARD_ONLY, - ResultSet.CONCUR_READ_ONLY, connection.getHoldability(), stmtColumnEncriptionSetting); + SQLServerStatement stmt = (SQLServerStatement) connection.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, + connection.getHoldability(), stmtColumnEncriptionSetting); // Get destination metadata try (SQLServerResultSet rs = stmt - .executeQueryInternal("SET FMTONLY ON SELECT * FROM " + destinationTableName + " SET FMTONLY OFF ");) { - + .executeQueryInternal("sp_executesql N'SET FMTONLY ON SELECT * FROM " + destinationTableName + " '");) { + SQLServerBulkBatchInsertRecord batchRecord = new SQLServerBulkBatchInsertRecord(batchParamValues, columnList, valueList, null); for (int i = 1; i <= rs.getColumnCount(); i++) { @@ -2505,24 +2505,25 @@ public int[] executeBatch() throws SQLServerException, BatchUpdateException, SQL int jdbctype; TypeInfo ti = c.getTypeInfo(); if (null != cryptoMetadata) { - jdbctype = cryptoMetadata.getBaseTypeInfo().getSSType().getJDBCType().getIntValue(); - } else { + jdbctype = cryptoMetadata.getBaseTypeInfo().getSSType().getJDBCType().getIntValue(); + } + else { jdbctype = ti.getSSType().getJDBCType().getIntValue(); } batchRecord.addColumnMetadata(i, c.getColumnName(), jdbctype, ti.getPrecision(), ti.getScale()); } - + SQLServerBulkCopy bcOperation = new SQLServerBulkCopy(connection); bcOperation.setDestinationTableName(tableName); bcOperation.setStmtColumnEncriptionSetting(this.getStmtColumnEncriptionSetting()); + bcOperation.setDestinationTableMetadata(rs); bcOperation.writeToServer((ISQLServerBulkRecord) batchRecord); bcOperation.close(); updateCounts = new int[batchParamValues.size()]; - for (int i = 0; i < batchParamValues.size(); ++i) - { + for (int i = 0; i < batchParamValues.size(); ++i) { updateCounts[i] = 1; } - + batchParamValues = null; loggerExternal.exiting(getClassNameLogging(), "executeBatch", updateCounts); return updateCounts; diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java index a2d726519..2b6a9d982 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java @@ -393,6 +393,6 @@ protected Object[][] getContents() { {"R_invalidSSLProtocol", "SSL Protocol {0} label is not valid. Only TLS, TLSv1, TLSv1.1, and TLSv1.2 are supported."}, {"R_cancelQueryTimeoutPropertyDescription", "The number of seconds to wait to cancel sending a query timeout."}, {"R_invalidCancelQueryTimeout", "The cancel timeout value {0} is not valid."}, - {"R_useBulkCopyForBatchInsertOnDWPropertyDescription", "Whether the driver will use bulk copy API for batch insert operations on Azure Data Warehouse."}, + {"R_useBulkCopyForBatchInsertPropertyDescription", "Whether the driver will use bulk copy API for batch insert operations"}, }; } \ No newline at end of file From f218fe31e8599dee77fde2c6d6ced296a55a6318 Mon Sep 17 00:00:00 2001 From: Peter Bae Date: Tue, 5 Jun 2018 09:50:03 -0700 Subject: [PATCH 18/32] formatting --- .../jdbc/SQLServerBulkBatchInsertRecord.java | 68 +++++++++++-------- .../sqlserver/jdbc/SQLServerBulkCommon.java | 24 ++++--- .../sqlserver/jdbc/SQLServerBulkCopy.java | 8 +-- .../BatchExecutionWithBulkCopyParseTest.java | 67 ++++++++---------- 4 files changed, 87 insertions(+), 80 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java index 62d990d21..6ec2090f6 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java @@ -22,8 +22,8 @@ import java.util.Set; /** - * A simple implementation of the ISQLServerBulkRecord interface that can be used to read in the basic Java data types from an ArrayList of - * Parameters that were provided by pstmt/cstmt. + * A simple implementation of the ISQLServerBulkRecord interface that can be used to read in the basic Java data types from an ArrayList of Parameters + * that were provided by pstmt/cstmt. */ public class SQLServerBulkBatchInsertRecord extends SQLServerBulkCommon implements ISQLServerBulkRecord, java.lang.AutoCloseable { @@ -32,21 +32,22 @@ public class SQLServerBulkBatchInsertRecord extends SQLServerBulkCommon implemen private ArrayList columnList; private ArrayList valueList; - public SQLServerBulkBatchInsertRecord(ArrayList batchParam, ArrayList columnList, - ArrayList valueList, String encoding) throws SQLServerException { + public SQLServerBulkBatchInsertRecord(ArrayList batchParam, + ArrayList columnList, + ArrayList valueList, + String encoding) throws SQLServerException { loggerClassName = "com.microsoft.sqlserver.jdbc.SQLServerBulkBatchInsertRecord"; loggerExternal = java.util.logging.Logger.getLogger(loggerClassName); - loggerExternal.entering(loggerClassName, "SQLServerBulkBatchInsertRecord", - new Object[] {batchParam, encoding}); - + loggerExternal.entering(loggerClassName, "SQLServerBulkBatchInsertRecord", new Object[] {batchParam, encoding}); + if (null == batchParam) { throwInvalidArgument("batchParam"); } - + if (null == valueList) { throwInvalidArgument("valueList"); } - + this.batchParam = batchParam; this.columnList = columnList; this.valueList = valueList; @@ -100,26 +101,29 @@ public boolean isAutoIncrement(int column) { @Override public Object[] getRowData() throws SQLServerException { - + Object[] data = new Object[columnMetadata.size()]; - + if (null == columnList || columnList.size() == 0) { int valueIndex = 0; for (int i = 0; i < data.length; i++) { if (valueList.get(i).equalsIgnoreCase("?")) { data[i] = batchParam.get(batchParamIndex)[valueIndex].getSetterValue(); valueIndex++; - } else { + } + else { // remove 's at the beginning and end of the value, if it exists. int len = valueList.get(i).length(); if (valueList.get(i).charAt(0) == '\'' && valueList.get(i).charAt(len - 1) == '\'') { data[i] = valueList.get(i).substring(1, len - 1); - } else { + } + else { data[i] = valueList.get(i); } } } - } else { + } + else { int valueIndex = 0; int columnListIndex = 0; for (int i = 0; i < data.length; i++) { @@ -127,17 +131,20 @@ public Object[] getRowData() throws SQLServerException { if (valueList.get(i).equalsIgnoreCase("?")) { data[i] = batchParam.get(i)[valueIndex].getSetterValue(); valueIndex++; - } else { + } + else { // remove 's at the beginning and end of the value, if it exists. int len = valueList.get(i).length(); if (valueList.get(i).charAt(0) == '\'' && valueList.get(i).charAt(len - 1) == '\'') { data[i] = valueList.get(i).substring(1, len - 1); - } else { + } + else { data[i] = valueList.get(i); } } columnListIndex++; - } else { + } + else { data[i] = ""; } } @@ -170,8 +177,8 @@ public Object[] getRowData() throws SQLServerException { switch (cm.columnType) { /* - * Both BCP and BULK INSERT considers double quotes as part of the data and throws error if any data (say "10") is to be - * inserted into an numeric column. Our implementation does the same. + * Both BCP and BULK INSERT considers double quotes as part of the data and throws error if any data (say "10") is to be inserted + * into an numeric column. Our implementation does the same. */ case Types.INTEGER: { // Formatter to remove the decimal part as SQL Server floors the decimal in integer types @@ -194,10 +201,11 @@ public Object[] getRowData() throws SQLServerException { BigDecimal bd = new BigDecimal(data[pair.getKey() - 1].toString().trim()); try { dataRow[pair.getKey() - 1] = bd.setScale(0, RoundingMode.DOWN).longValueExact(); - } catch (ArithmeticException ex) { + } + catch (ArithmeticException ex) { String value = "'" + data[pair.getKey() - 1] + "'"; MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_errorConvertingValue")); - throw new SQLServerException(form.format(new Object[]{value, JDBCType.of(cm.columnType)}), null, 0, ex); + throw new SQLServerException(form.format(new Object[] {value, JDBCType.of(cm.columnType)}), null, 0, ex); } break; } @@ -214,7 +222,8 @@ public Object[] getRowData() throws SQLServerException { // Any non-zero value (integer/double) => 1, 0/0.0 => 0 try { dataRow[pair.getKey() - 1] = (0 == Double.parseDouble(data[pair.getKey() - 1].toString())) ? Boolean.FALSE : Boolean.TRUE; - } catch (NumberFormatException e) { + } + catch (NumberFormatException e) { dataRow[pair.getKey() - 1] = Boolean.parseBoolean(data[pair.getKey() - 1].toString()); } break; @@ -238,7 +247,8 @@ public Object[] getRowData() throws SQLServerException { String binData = data[pair.getKey() - 1].toString().trim(); if (binData.startsWith("0x") || binData.startsWith("0X")) { dataRow[pair.getKey() - 1] = binData.substring(2); - } else { + } + else { dataRow[pair.getKey() - 1] = binData; } break; @@ -297,18 +307,20 @@ else if (dateTimeFormatter != null) break; } } - } catch (IllegalArgumentException e) { + } + catch (IllegalArgumentException e) { String value = "'" + data[pair.getKey() - 1] + "'"; MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_errorConvertingValue")); - throw new SQLServerException(form.format(new Object[]{value, JDBCType.of(cm.columnType)}), null, 0, e); - } catch (ArrayIndexOutOfBoundsException e) { + throw new SQLServerException(form.format(new Object[] {value, JDBCType.of(cm.columnType)}), null, 0, e); + } + catch (ArrayIndexOutOfBoundsException e) { throw new SQLServerException(SQLServerException.getErrString("R_CSVDataSchemaMismatch"), e); } - + } return dataRow; } - + @Override public boolean next() throws SQLServerException { batchParamIndex++; diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCommon.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCommon.java index fc4c79679..669845b90 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCommon.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCommon.java @@ -14,7 +14,7 @@ import java.util.Map.Entry; abstract class SQLServerBulkCommon { - + /* * Class to represent the column metadata */ @@ -37,25 +37,25 @@ protected class ColumnMetadata { this.dateTimeFormatter = dateTimeFormatter; } } - + /* * Class name for logging. */ protected static String loggerClassName; - + /* * Logger */ protected java.util.logging.Logger loggerExternal; - + /* * Contains all the column names if firstLineIsColumnNames is true */ protected String[] columnNames = null; - + /* - * Metadata to represent the columns in the batch/file. Each column should be mapped to its corresponding position within the parameter (from position 1 and - * onwards) + * Metadata to represent the columns in the batch/file. Each column should be mapped to its corresponding position within the parameter (from + * position 1 and onwards) */ protected Map columnMetadata; @@ -178,9 +178,11 @@ else if ((columnNames != null) && (columnNames.length >= positionInSource)) case microsoft.sql.Types.DATETIMEOFFSET: if (this instanceof SQLServerBulkCSVFileRecord) { columnMetadata.put(positionInSource, new ColumnMetadata(colName, jdbcType, 50, scale, dateTimeFormatter)); - } else if (this instanceof SQLServerBulkBatchInsertRecord) { + } + else if (this instanceof SQLServerBulkBatchInsertRecord) { columnMetadata.put(positionInSource, new ColumnMetadata(colName, jdbcType, precision, scale, dateTimeFormatter)); - } else { + } + else { columnMetadata.put(positionInSource, new ColumnMetadata(colName, jdbcType, 50, scale, dateTimeFormatter)); } break; @@ -266,7 +268,7 @@ public void setTimeWithTimezoneFormat(DateTimeFormatter dateTimeFormatter) { loggerExternal.exiting(loggerClassName, "setTimeWithTimezoneFormat"); } - + /* * Helper method to throw a SQLServerExeption with the invalidArgument message and given argument. */ @@ -275,7 +277,7 @@ protected void throwInvalidArgument(String argument) throws SQLServerException { Object[] msgArgs = {argument}; SQLServerException.makeFromDriverError(null, null, form.format(msgArgs), null, false); } - + /* * Method to throw a SQLServerExeption for duplicate column names */ diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java index ba36cbda4..ab2e62449 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java @@ -1748,16 +1748,16 @@ private void getDestinationMetadata() throws SQLServerException { SQLServerStatement stmt = null; try { - if (null == destinationTableMetadata) { + 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 " + destinationTableName + " '"); } - else { - rs = (SQLServerResultSet) destinationTableMetadata; - } destColumnCount = rs.getMetaData().getColumnCount(); destColumnMetadata = new HashMap<>(); diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyParseTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyParseTest.java index 79a43c5e6..1a42eea3d 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyParseTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyParseTest.java @@ -10,7 +10,6 @@ import java.lang.reflect.Method; import java.sql.Connection; import java.sql.DriverManager; -import java.sql.PreparedStatement; import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; @@ -27,22 +26,20 @@ import com.microsoft.sqlserver.testframework.DBConnection; import com.microsoft.sqlserver.testframework.Utils; - @RunWith(JUnitPlatform.class) public class BatchExecutionWithBulkCopyParseTest extends AbstractTest { static SQLServerPreparedStatement pstmt = null; static Statement stmt = null; static Connection connection = null; - + @Test - public void testIsInsert() throws SQLException, NoSuchMethodException, SecurityException, - IllegalAccessException, IllegalArgumentException, InvocationTargetException { + public void testIsInsert() throws SQLException, NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { String valid1 = "INSERT INTO PeterTable values (1, 2)"; String valid2 = " INSERT INTO PeterTable values (1, 2)"; String valid3 = "/* asdf */ INSERT INTO PeterTable values (1, 2)"; String invalid = "Select * from PEterTable"; - + stmt = connection.createStatement(); Method method = stmt.getClass().getDeclaredMethod("isInsert", String.class); method.setAccessible(true); @@ -51,108 +48,104 @@ public void testIsInsert() throws SQLException, NoSuchMethodException, SecurityE assertTrue((boolean) method.invoke(stmt, valid3)); assertFalse((boolean) method.invoke(stmt, invalid)); } - + @Test - public void testComments() throws SQLException, NoSuchFieldException, SecurityException, - IllegalArgumentException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { + public void testComments() throws SQLException, NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { pstmt = (SQLServerPreparedStatement) connection.prepareStatement(""); - + String valid = "/* rando comment *//* rando comment */ INSERT /* rando comment */ INTO /* rando comment *//*rando comment*/ PeterTable /*rando comment */" + " /* rando comment */values/* rando comment */ (1, 2)"; - + Field f1 = pstmt.getClass().getSuperclass().getDeclaredField("localUserSQL"); f1.setAccessible(true); f1.set(pstmt, valid); Method method = pstmt.getClass().getSuperclass().getDeclaredMethod("parseUserSQLForTableNameDW", boolean.class, boolean.class); method.setAccessible(true); - + assertEquals((String) method.invoke(pstmt, false, false), "PeterTable"); } - + @Test - public void testBrackets() throws SQLException, NoSuchFieldException, SecurityException, - IllegalArgumentException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { + public void testBrackets() throws SQLException, NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { pstmt = (SQLServerPreparedStatement) connection.prepareStatement(""); - + String valid = "/* rando comment *//* rando comment */ INSERT /* rando comment */ INTO /* rando comment *//*rando comment*/ [Peter[]]Table] /*rando comment */" + " /* rando comment */values/* rando comment */ (1, 2)"; - + Field f1 = pstmt.getClass().getSuperclass().getDeclaredField("localUserSQL"); f1.setAccessible(true); f1.set(pstmt, valid); Method method = pstmt.getClass().getSuperclass().getDeclaredMethod("parseUserSQLForTableNameDW", boolean.class, boolean.class); method.setAccessible(true); - + assertEquals((String) method.invoke(pstmt, false, false), "Peter[]Table"); } - + @Test - public void testDoubleQuotes() throws SQLException, NoSuchFieldException, SecurityException, - IllegalArgumentException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { + public void testDoubleQuotes() throws SQLException, NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { pstmt = (SQLServerPreparedStatement) connection.prepareStatement(""); - + String valid = "/* rando comment *//* rando comment */ INSERT /* rando comment */ INTO /* rando comment *//*rando comment*/ \"Peter\"\"\"\"Table\" /*rando comment */" + " /* rando comment */values/* rando comment */ (1, 2)"; - + Field f1 = pstmt.getClass().getSuperclass().getDeclaredField("localUserSQL"); f1.setAccessible(true); f1.set(pstmt, valid); Method method = pstmt.getClass().getSuperclass().getDeclaredMethod("parseUserSQLForTableNameDW", boolean.class, boolean.class); method.setAccessible(true); - + assertEquals((String) method.invoke(pstmt, false, false), "Peter\"\"Table"); } - + @Test - public void testAll() throws SQLException, NoSuchFieldException, SecurityException, - IllegalArgumentException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { + public void testAll() throws SQLException, NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { pstmt = (SQLServerPreparedStatement) connection.prepareStatement(""); - + String valid = "/* rando comment *//* rando comment */ INSERT /* rando comment */ INTO /* rando comment *//*rando comment*/ \"Peter\"\"\"\"Table\" /*rando comment */" + " /* rando comment */ (\"c1\"/* rando comment */, /* rando comment */[c2]/* rando comment */, /* rando comment */ /* rando comment */c3/* rando comment */, c4)" + "values/* rando comment */ (/* rando comment */1/* rando comment */, /* rando comment */2/* rando comment */ , '?', ?)/* rando comment */"; - + Field f1 = pstmt.getClass().getSuperclass().getDeclaredField("localUserSQL"); f1.setAccessible(true); f1.set(pstmt, valid); Method method = pstmt.getClass().getSuperclass().getDeclaredMethod("parseUserSQLForTableNameDW", boolean.class, boolean.class); method.setAccessible(true); - + assertEquals((String) method.invoke(pstmt, false, false), "Peter\"\"Table"); - + method = pstmt.getClass().getSuperclass().getDeclaredMethod("parseUserSQLForColumnListDW"); method.setAccessible(true); - + ArrayList columnList = (ArrayList) method.invoke(pstmt); ArrayList columnListExpected = new ArrayList(); columnListExpected.add("c1"); columnListExpected.add("c2"); columnListExpected.add("c3"); columnListExpected.add("c4"); - + for (int i = 0; i < columnListExpected.size(); i++) { assertEquals(columnList.get(i), columnListExpected.get(i)); } - + method = pstmt.getClass().getSuperclass().getDeclaredMethod("parseUserSQLForValueListDW", boolean.class); method.setAccessible(true); - + ArrayList valueList = (ArrayList) method.invoke(pstmt, false); ArrayList valueListExpected = new ArrayList(); valueListExpected.add("1"); valueListExpected.add("2"); valueListExpected.add("'?'"); valueListExpected.add("?"); - + for (int i = 0; i < valueListExpected.size(); i++) { assertEquals(valueList.get(i), valueListExpected.get(i)); } } - + @BeforeEach public void testSetup() throws TestAbortedException, Exception { assumeTrue(13 <= new DBConnection(connectionString).getServerVersion(), From 3c5d02340133181fe67983ecb70000638d1a33d7 Mon Sep 17 00:00:00 2001 From: Peter Bae Date: Wed, 6 Jun 2018 16:26:26 -0700 Subject: [PATCH 19/32] Reformatting + adding more tests --- .../jdbc/SQLServerBulkBatchInsertRecord.java | 358 +++++++++--------- .../jdbc/SQLServerPreparedStatement.java | 3 +- ...va => BatchExecutionWithBulkCopyTest.java} | 158 +++++++- .../preparedStatement/RegressionTest.java | 8 +- 4 files changed, 331 insertions(+), 196 deletions(-) rename src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/{BatchExecutionWithBulkCopyParseTest.java => BatchExecutionWithBulkCopyTest.java} (60%) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java index 6ec2090f6..a355d45e5 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java @@ -99,226 +99,216 @@ public boolean isAutoIncrement(int column) { return false; } - @Override - public Object[] getRowData() throws SQLServerException { + private Object convertValue(ColumnMetadata cm, + Object data) throws SQLServerException { + switch (cm.columnType) { + /* + * Both BCP and BULK INSERT considers double quotes as part of the data and throws error if any data (say "10") is to be inserted into an + * numeric column. Our implementation does the same. + */ + case Types.INTEGER: { + // Formatter to remove the decimal part as SQL Server floors the decimal in integer types + DecimalFormat decimalFormatter = new DecimalFormat("#"); + String formatedfInput = decimalFormatter.format(Double.parseDouble(data.toString())); + return Integer.valueOf(formatedfInput); + } - Object[] data = new Object[columnMetadata.size()]; + case Types.TINYINT: + case Types.SMALLINT: { + // Formatter to remove the decimal part as SQL Server floors the decimal in integer types + DecimalFormat decimalFormatter = new DecimalFormat("#"); + String formatedfInput = decimalFormatter.format(Double.parseDouble(data.toString())); + return Short.valueOf(formatedfInput); + } - if (null == columnList || columnList.size() == 0) { - int valueIndex = 0; - for (int i = 0; i < data.length; i++) { - if (valueList.get(i).equalsIgnoreCase("?")) { - data[i] = batchParam.get(batchParamIndex)[valueIndex].getSetterValue(); - valueIndex++; + case Types.BIGINT: { + BigDecimal bd = new BigDecimal(data.toString().trim()); + try { + return bd.setScale(0, RoundingMode.DOWN).longValueExact(); } - else { - // remove 's at the beginning and end of the value, if it exists. - int len = valueList.get(i).length(); - if (valueList.get(i).charAt(0) == '\'' && valueList.get(i).charAt(len - 1) == '\'') { - data[i] = valueList.get(i).substring(1, len - 1); - } - else { - data[i] = valueList.get(i); - } + catch (ArithmeticException ex) { + String value = "'" + data + "'"; + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_errorConvertingValue")); + throw new SQLServerException(form.format(new Object[] {value, JDBCType.of(cm.columnType)}), null, 0, ex); } } - } - else { - int valueIndex = 0; - int columnListIndex = 0; - for (int i = 0; i < data.length; i++) { - if (columnList.size() > columnListIndex && columnList.get(columnListIndex).equals(columnMetadata.get(i + 1).columnName)) { - if (valueList.get(i).equalsIgnoreCase("?")) { - data[i] = batchParam.get(i)[valueIndex].getSetterValue(); - valueIndex++; - } - else { - // remove 's at the beginning and end of the value, if it exists. - int len = valueList.get(i).length(); - if (valueList.get(i).charAt(0) == '\'' && valueList.get(i).charAt(len - 1) == '\'') { - data[i] = valueList.get(i).substring(1, len - 1); - } - else { - data[i] = valueList.get(i); - } - } - columnListIndex++; + + case Types.DECIMAL: + case Types.NUMERIC: { + BigDecimal bd = new BigDecimal(data.toString().trim()); + return bd.setScale(cm.scale, RoundingMode.HALF_UP); + } + + case Types.BIT: { + // "true" => 1, "false" => 0 + // Any non-zero value (integer/double) => 1, 0/0.0 => 0 + try { + return (0 == Double.parseDouble(data.toString())) ? Boolean.FALSE : Boolean.TRUE; } - else { - data[i] = ""; + catch (NumberFormatException e) { + return Boolean.parseBoolean(data.toString()); } } - } - - // Cannot go directly from String[] to Object[] and expect it to act as an array. - Object[] dataRow = new Object[data.length]; - - for (Entry pair : columnMetadata.entrySet()) { - ColumnMetadata cm = pair.getValue(); - if (data.length < pair.getKey() - 1) { - MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidColumn")); - Object[] msgArgs = {pair.getKey()}; - throw new SQLServerException(form.format(msgArgs), SQLState.COL_NOT_FOUND, DriverError.NOT_SET, null); + case Types.REAL: { + return Float.parseFloat(data.toString()); } - // Source header has more columns than current param read - if (columnNames != null && (columnNames.length > data.length)) { - MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_CSVDataSchemaMismatch")); - Object[] msgArgs = {}; - throw new SQLServerException(form.format(msgArgs), SQLState.COL_NOT_FOUND, DriverError.NOT_SET, null); + case Types.DOUBLE: { + return Double.parseDouble(data.toString()); } - try { - if (0 == data[pair.getKey() - 1].toString().length()) { - dataRow[pair.getKey() - 1] = null; - continue; + case Types.BINARY: + case Types.VARBINARY: + case Types.LONGVARBINARY: + case Types.BLOB: { + // Strip off 0x if present. + String binData = data.toString().trim(); + if (binData.startsWith("0x") || binData.startsWith("0X")) { + return binData.substring(2); + } + else { + return binData; } + } - switch (cm.columnType) { - /* - * Both BCP and BULK INSERT considers double quotes as part of the data and throws error if any data (say "10") is to be inserted - * into an numeric column. Our implementation does the same. - */ - case Types.INTEGER: { - // Formatter to remove the decimal part as SQL Server floors the decimal in integer types - DecimalFormat decimalFormatter = new DecimalFormat("#"); - String formatedfInput = decimalFormatter.format(Double.parseDouble(data[pair.getKey() - 1].toString())); - dataRow[pair.getKey() - 1] = Integer.valueOf(formatedfInput); - break; - } + case 2013: // java.sql.Types.TIME_WITH_TIMEZONE + { + DriverJDBCVersion.checkSupportsJDBC42(); + OffsetTime offsetTimeValue; - case Types.TINYINT: - case Types.SMALLINT: { - // Formatter to remove the decimal part as SQL Server floors the decimal in integer types - DecimalFormat decimalFormatter = new DecimalFormat("#"); - String formatedfInput = decimalFormatter.format(Double.parseDouble(data[pair.getKey() - 1].toString())); - dataRow[pair.getKey() - 1] = Short.valueOf(formatedfInput); - break; - } + // The per-column DateTimeFormatter gets priority. + if (null != cm.dateTimeFormatter) + offsetTimeValue = OffsetTime.parse(data.toString(), cm.dateTimeFormatter); + else if (timeFormatter != null) + offsetTimeValue = OffsetTime.parse(data.toString(), timeFormatter); + else + offsetTimeValue = OffsetTime.parse(data.toString()); - case Types.BIGINT: { - BigDecimal bd = new BigDecimal(data[pair.getKey() - 1].toString().trim()); - try { - dataRow[pair.getKey() - 1] = bd.setScale(0, RoundingMode.DOWN).longValueExact(); - } - catch (ArithmeticException ex) { - String value = "'" + data[pair.getKey() - 1] + "'"; - MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_errorConvertingValue")); - throw new SQLServerException(form.format(new Object[] {value, JDBCType.of(cm.columnType)}), null, 0, ex); - } - break; - } + return offsetTimeValue; + } - case Types.DECIMAL: - case Types.NUMERIC: { - BigDecimal bd = new BigDecimal(data[pair.getKey() - 1].toString().trim()); - dataRow[pair.getKey() - 1] = bd.setScale(cm.scale, RoundingMode.HALF_UP); - break; - } + case 2014: // java.sql.Types.TIMESTAMP_WITH_TIMEZONE + { + DriverJDBCVersion.checkSupportsJDBC42(); + OffsetDateTime offsetDateTimeValue; - case Types.BIT: { - // "true" => 1, "false" => 0 - // Any non-zero value (integer/double) => 1, 0/0.0 => 0 - try { - dataRow[pair.getKey() - 1] = (0 == Double.parseDouble(data[pair.getKey() - 1].toString())) ? Boolean.FALSE : Boolean.TRUE; - } - catch (NumberFormatException e) { - dataRow[pair.getKey() - 1] = Boolean.parseBoolean(data[pair.getKey() - 1].toString()); - } - break; - } + // The per-column DateTimeFormatter gets priority. + if (null != cm.dateTimeFormatter) + offsetDateTimeValue = OffsetDateTime.parse(data.toString(), cm.dateTimeFormatter); + else if (dateTimeFormatter != null) + offsetDateTimeValue = OffsetDateTime.parse(data.toString(), dateTimeFormatter); + else + offsetDateTimeValue = OffsetDateTime.parse(data.toString()); - case Types.REAL: { - dataRow[pair.getKey() - 1] = Float.parseFloat(data[pair.getKey() - 1].toString()); - break; - } + return offsetDateTimeValue; + } - case Types.DOUBLE: { - dataRow[pair.getKey() - 1] = Double.parseDouble(data[pair.getKey() - 1].toString()); - break; - } + case Types.NULL: { + return null; + } - case Types.BINARY: - case Types.VARBINARY: - case Types.LONGVARBINARY: - case Types.BLOB: { - // Strip off 0x if present. - String binData = data[pair.getKey() - 1].toString().trim(); - if (binData.startsWith("0x") || binData.startsWith("0X")) { - dataRow[pair.getKey() - 1] = binData.substring(2); - } - else { - dataRow[pair.getKey() - 1] = binData; - } - break; - } + case Types.DATE: + case Types.CHAR: + case Types.NCHAR: + case Types.VARCHAR: + case Types.NVARCHAR: + case Types.LONGVARCHAR: + case Types.LONGNVARCHAR: + case Types.CLOB: + default: { + // The string is copied as is. + return data; + } + } + } - case 2013: // java.sql.Types.TIME_WITH_TIMEZONE - { - DriverJDBCVersion.checkSupportsJDBC42(); - OffsetTime offsetTimeValue; - - // The per-column DateTimeFormatter gets priority. - if (null != cm.dateTimeFormatter) - offsetTimeValue = OffsetTime.parse(data[pair.getKey() - 1].toString(), cm.dateTimeFormatter); - else if (timeFormatter != null) - offsetTimeValue = OffsetTime.parse(data[pair.getKey() - 1].toString(), timeFormatter); - else - offsetTimeValue = OffsetTime.parse(data[pair.getKey() - 1].toString()); - - dataRow[pair.getKey() - 1] = offsetTimeValue; - break; - } + private String removeSingleQuote(String s) { + int len = s.length(); + return (s.charAt(0) == '\'' && s.charAt(len - 1) == '\'') ? s.substring(1, len - 1) : s; + } - case 2014: // java.sql.Types.TIMESTAMP_WITH_TIMEZONE - { - DriverJDBCVersion.checkSupportsJDBC42(); - OffsetDateTime offsetDateTimeValue; - - // The per-column DateTimeFormatter gets priority. - if (null != cm.dateTimeFormatter) - offsetDateTimeValue = OffsetDateTime.parse(data[pair.getKey() - 1].toString(), cm.dateTimeFormatter); - else if (dateTimeFormatter != null) - offsetDateTimeValue = OffsetDateTime.parse(data[pair.getKey() - 1].toString(), dateTimeFormatter); - else - offsetDateTimeValue = OffsetDateTime.parse(data[pair.getKey() - 1].toString()); - - dataRow[pair.getKey() - 1] = offsetDateTimeValue; - break; + @Override + public Object[] getRowData() throws SQLServerException { + Object[] data = new Object[columnMetadata.size()]; + int valueIndex = 0; + String valueData; + Object rowData; + int columnListIndex = 0; + + // check if the size of the list of values = size of the list of columns (which is optional) + if (null != columnList && columnList.size() != valueList.size()) { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_CSVDataSchemaMismatch")); + Object[] msgArgs = {}; + throw new SQLServerException(form.format(msgArgs), SQLState.COL_NOT_FOUND, DriverError.NOT_SET, null); + } + + for (Entry pair : columnMetadata.entrySet()) { + int index = pair.getKey() - 1; + + // To explain what each variable represents: + // columnMetadata = map containing the ENTIRE list of columns in the table. + // columnList = the *optional* list of columns the user can provide. For example, the (c1, c3) part of this query: INSERT into t1 (c1, c3) values (?, ?) + // valueList = the *mandatory* list of columns the user needs provide. This is the (?, ?) part of the previous query. The size of this valueList will always equal the number of + // the entire columns in the table IF columnList has NOT been provided. If columnList HAS been provided, then this valueList may be smaller than the list of all columns (which is columnMetadata). + + // case when the user has not provided the optional list of column names. + if (null == columnList || columnList.size() == 0) { + valueData = valueList.get(index); + // if the user has provided a wildcard for this column, fetch the set value from the batchParam. + if (valueData.equalsIgnoreCase("?")) { + rowData = batchParam.get(batchParamIndex)[valueIndex++].getSetterValue(); + } + else if (valueData.equalsIgnoreCase("null")) { + rowData = ""; + } + // if the user has provided a hardcoded value for this column, rowData is simply set to the hardcoded value. + else { + rowData = removeSingleQuote(valueData); + } + } + // case when the user has provided the optional list of column names. + else { + // columnListIndex is a separate counter we need to keep track of for each time we've processed a column + // that the user provided. + // for example, if the user provided an optional columnList of (c1, c3, c5, c7) in a table that has 8 columns (c1~c8), + // then the columnListIndex would increment only when we're dealing with the four columns inside columnMetadata. + // compare the list of the optional list of column names to the table's metadata, and match each other, so we assign the correct value to each column. + if (columnList.size() > columnListIndex && columnList.get(columnListIndex).equalsIgnoreCase(columnMetadata.get(index + 1).columnName)) { + valueData = valueList.get(columnListIndex); + if (valueData.equalsIgnoreCase("?")) { + rowData = batchParam.get(batchParamIndex)[valueIndex++].getSetterValue(); } - - case Types.NULL: { - dataRow[pair.getKey() - 1] = null; - break; + else if (valueData.equalsIgnoreCase("null")) { + rowData = ""; } - - case Types.DATE: - case Types.CHAR: - case Types.NCHAR: - case Types.VARCHAR: - case Types.NVARCHAR: - case Types.LONGVARCHAR: - case Types.LONGNVARCHAR: - case Types.CLOB: - default: { - // The string is copied as is. - dataRow[pair.getKey() - 1] = data[pair.getKey() - 1]; - break; + else { + rowData = removeSingleQuote(valueData); } + columnListIndex++; } + else { + rowData = ""; + } + } + + try { + if (0 == rowData.toString().length()) { + data[index] = null; + continue; + } + data[index] = convertValue(pair.getValue(), rowData); } catch (IllegalArgumentException e) { - String value = "'" + data[pair.getKey() - 1] + "'"; + String value = "'" + rowData + "'"; MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_errorConvertingValue")); - throw new SQLServerException(form.format(new Object[] {value, JDBCType.of(cm.columnType)}), null, 0, e); + throw new SQLServerException(form.format(new Object[] {value, JDBCType.of(pair.getValue().columnType)}), null, 0, e); } catch (ArrayIndexOutOfBoundsException e) { throw new SQLServerException(SQLServerException.getErrString("R_CSVDataSchemaMismatch"), e); } - } - return dataRow; + return data; } @Override diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java index daedef2da..46a43f8a7 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java @@ -2479,7 +2479,7 @@ public int[] executeBatch() throws SQLServerException, BatchUpdateException, SQL localUserSQL = userSQL; try { - if (isInsert(localUserSQL) && true && (this.useBulkCopyForBatchInsert || connection.getUseBulkCopyForBatchInsert())) { + if (isInsert(localUserSQL) && connection.isAzureDW() && (this.useBulkCopyForBatchInsert || connection.getUseBulkCopyForBatchInsert())) { if (batchParamValues == null) { updateCounts = new int[0]; loggerExternal.exiting(getClassNameLogging(), "executeBatch", updateCounts); @@ -2547,7 +2547,6 @@ public int[] executeBatch() throws SQLServerException, BatchUpdateException, SQL } } - if (batchParamValues == null) updateCounts = new int[0]; else diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyParseTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyTest.java similarity index 60% rename from src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyParseTest.java rename to src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyTest.java index 1a42eea3d..5335f4401 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyParseTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyTest.java @@ -9,17 +9,23 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.sql.Connection; +import java.sql.Date; import java.sql.DriverManager; +import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; +import java.sql.Timestamp; import java.util.ArrayList; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.platform.runner.JUnitPlatform; import org.junit.runner.RunWith; import org.opentest4j.TestAbortedException; +import com.microsoft.sqlserver.jdbc.SQLServerConnection; import com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement; import com.microsoft.sqlserver.jdbc.SQLServerStatement; import com.microsoft.sqlserver.testframework.AbstractTest; @@ -27,11 +33,12 @@ import com.microsoft.sqlserver.testframework.Utils; @RunWith(JUnitPlatform.class) -public class BatchExecutionWithBulkCopyParseTest extends AbstractTest { +public class BatchExecutionWithBulkCopyTest extends AbstractTest { static SQLServerPreparedStatement pstmt = null; static Statement stmt = null; static Connection connection = null; + static String tableName = "BulkCopyParseTest" + System.currentTimeMillis(); @Test public void testIsInsert() throws SQLException, NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { @@ -145,17 +152,154 @@ public void testAll() throws SQLException, NoSuchFieldException, SecurityExcepti assertEquals(valueList.get(i), valueListExpected.get(i)); } } + + @Test + public void testAllcolumns() throws Exception { + Field f1 = SQLServerConnection.class.getDeclaredField("isAzureDW"); + f1.setAccessible(true); + f1.set(connection, true); + + String valid = "INSERT INTO " + tableName + " values " + + "(" + + "?, " + + "?, " + + "?, " + + "?, " + + "?, " + + "?, " + + "?, " + + "?, " + + "?, " + + ")"; + + SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) connection.prepareStatement(valid); + SQLServerStatement stmt = (SQLServerStatement) connection.createStatement(); + + Timestamp myTimestamp = new Timestamp(114550L); + + Date d = new Date(114550L); + + pstmt.setInt(1, 1234); + pstmt.setBoolean(2, false); + pstmt.setString(3, "a"); + pstmt.setDate(4, d); + pstmt.setDateTime(5, myTimestamp); + pstmt.setFloat(6, (float) 123.45); + pstmt.setString(7, "b"); + pstmt.setString(8, "varc"); + pstmt.setString(9, "varcmax"); + pstmt.addBatch(); + + pstmt.executeBatch(); + + ResultSet rs = stmt.executeQuery("SELECT * FROM " + tableName); + + Object[] expected = new Object[9]; + + expected[0] = 1234; + expected[1] = false; + expected[2] = "a"; + expected[3] = d; + expected[4] = myTimestamp; + expected[5] = 123.45; + expected[6] = "b"; + expected[7] = "varc"; + expected[8] = "varcmax"; + + rs.next(); + for (int i=0; i < expected.length; i++) { + assertEquals(rs.getObject(i + 1).toString(), expected[i].toString()); + } + } + @Test + public void testMixColumns() throws Exception { + Field f1 = SQLServerConnection.class.getDeclaredField("isAzureDW"); + f1.setAccessible(true); + f1.set(connection, true); + + String valid = "INSERT INTO " + tableName + " (c1, c3, c5, c8) values " + + "(" + + "?, " + + "?, " + + "?, " + + "?, " + + ")"; + + SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) connection.prepareStatement(valid); + SQLServerStatement stmt = (SQLServerStatement) connection.createStatement(); + + Timestamp myTimestamp = new Timestamp(114550L); + + Date d = new Date(114550L); + + pstmt.setInt(1, 1234); + pstmt.setString(2, "a"); + pstmt.setDateTime(3, myTimestamp); + pstmt.setString(4, "varc"); + pstmt.addBatch(); + + pstmt.executeBatch(); + + ResultSet rs = stmt.executeQuery("SELECT * FROM " + tableName); + + Object[] expected = new Object[9]; + + expected[0] = 1234; + expected[1] = false; + expected[2] = "a"; + expected[3] = d; + expected[4] = myTimestamp; + expected[5] = 123.45; + expected[6] = "b"; + expected[7] = "varc"; + expected[8] = "varcmax"; + + rs.next(); + for (int i=0; i < expected.length; i++) { + if (null != rs.getObject(i + 1)) { + assertEquals(rs.getObject(i + 1).toString(), expected[i].toString()); + } + } + } @BeforeEach public void testSetup() throws TestAbortedException, Exception { - assumeTrue(13 <= new DBConnection(connectionString).getServerVersion(), - "Aborting test case as SQL Server version is not compatible with Always encrypted "); - - connection = DriverManager.getConnection(connectionString); + connection = DriverManager.getConnection(connectionString + ";useBulkCopyForBatchInsert=true;"); SQLServerStatement stmt = (SQLServerStatement) connection.createStatement(); - Utils.dropTableIfExists("esimple", stmt); - String sql1 = "create table esimple (id integer not null, name varchar(255), constraint pk_esimple primary key (id))"; + + Utils.dropTableIfExists(tableName, stmt); + String sql1 = "create table " + tableName + " " + + "(" + + "c1 int DEFAULT 1234, " + + "c2 bit, " + + "c3 char DEFAULT NULL, " + + "c4 date, " + + "c5 datetime2, " + + "c6 float, " + + "c7 nchar, " + + "c8 varchar(20), " + + "c9 varchar(max)" + + ")"; + stmt.execute(sql1); stmt.close(); } + + @AfterAll + public static void terminateVariation() throws SQLException { + connection = DriverManager.getConnection(connectionString); + + SQLServerStatement stmt = (SQLServerStatement) connection.createStatement(); + Utils.dropTableIfExists(tableName, stmt); + + if (null != pstmt) { + pstmt.close(); + } + if (null != stmt) { + stmt.close(); + } + if (null != connection) { + connection.close(); + } + } } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/RegressionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/RegressionTest.java index 930b122b5..7a18ae0fe 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/RegressionTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/RegressionTest.java @@ -26,6 +26,7 @@ import org.junit.runner.RunWith; import com.microsoft.sqlserver.jdbc.SQLServerConnection; +import com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement; import com.microsoft.sqlserver.testframework.AbstractTest; import com.microsoft.sqlserver.testframework.Utils; import com.microsoft.sqlserver.jdbc.TestResource; @@ -320,7 +321,7 @@ public void batchWithLargeStringTest() throws SQLException { @Test public void batchWithLargeStringTestUseBulkCopyAPI() throws SQLException { Statement stmt = con.createStatement(); - PreparedStatement pstmt = null; + SQLServerPreparedStatement pstmt = null; ResultSet rs = null; String testTable = "TEST_TABLE_BULK_COPY"; Utils.dropTableIfExists(testTable, stmt); @@ -357,7 +358,8 @@ public void batchWithLargeStringTestUseBulkCopyAPI() throws SQLException { Field f1 = SQLServerConnection.class.getDeclaredField("isAzureDW"); f1.setAccessible(true); f1.set(con, true); - pstmt = con.prepareStatement("insert into " + testTable + " values (?,?)"); + pstmt = (SQLServerPreparedStatement) con.prepareStatement("insert into " + testTable + " values (?,?)"); + pstmt.setUseBulkCopyForBatchInsert(true); // 0,a pstmt.setInt(1, 0); pstmt.setNString(2, values[0]); @@ -395,7 +397,7 @@ public void batchWithLargeStringTestUseBulkCopyAPI() throws SQLException { Map selectedValues = new LinkedHashMap<>(); int id = 0; try { - pstmt = con.prepareStatement("select * from " + testTable + ";"); + pstmt = (SQLServerPreparedStatement) con.prepareStatement("select * from " + testTable + ";"); try { rs = pstmt.executeQuery(); int i = 0; From dcd63d32653fdc5e9826c4649b313dc2bfbdd32b Mon Sep 17 00:00:00 2001 From: Peter Bae Date: Fri, 8 Jun 2018 16:26:22 -0700 Subject: [PATCH 20/32] inherit the connection property in statement + fix issue with null / empty string being passed in as values --- .../jdbc/SQLServerBulkBatchInsertRecord.java | 11 ++-- .../jdbc/SQLServerPreparedStatement.java | 10 ++-- .../BatchExecutionWithBulkCopyTest.java | 54 +++++++++++++++++++ 3 files changed, 67 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java index a355d45e5..86b920bdd 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java @@ -260,7 +260,7 @@ public Object[] getRowData() throws SQLServerException { rowData = batchParam.get(batchParamIndex)[valueIndex++].getSetterValue(); } else if (valueData.equalsIgnoreCase("null")) { - rowData = ""; + rowData = null; } // if the user has provided a hardcoded value for this column, rowData is simply set to the hardcoded value. else { @@ -280,7 +280,7 @@ else if (valueData.equalsIgnoreCase("null")) { rowData = batchParam.get(batchParamIndex)[valueIndex++].getSetterValue(); } else if (valueData.equalsIgnoreCase("null")) { - rowData = ""; + rowData = null; } else { rowData = removeSingleQuote(valueData); @@ -288,14 +288,17 @@ else if (valueData.equalsIgnoreCase("null")) { columnListIndex++; } else { - rowData = ""; + rowData = null; } } try { - if (0 == rowData.toString().length()) { + if (null == rowData) { data[index] = null; continue; + } else if (0 == rowData.toString().length()) { + data[index] = ""; + continue; } data[index] = convertValue(pair.getValue(), rowData); } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java index 46a43f8a7..4182efca5 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java @@ -232,6 +232,7 @@ String getClassNameInternal() { bReturnValueSyntax = parsedSQL.bReturnValueSyntax; userSQL = parsedSQL.processedSQL; initParams(parsedSQL.parameterCount); + useBulkCopyForBatchInsert = conn.getUseBulkCopyForBatchInsert(); } /** @@ -2479,7 +2480,7 @@ public int[] executeBatch() throws SQLServerException, BatchUpdateException, SQL localUserSQL = userSQL; try { - if (isInsert(localUserSQL) && connection.isAzureDW() && (this.useBulkCopyForBatchInsert || connection.getUseBulkCopyForBatchInsert())) { + if (isInsert(localUserSQL) && connection.isAzureDW() && (this.useBulkCopyForBatchInsert)) { if (batchParamValues == null) { updateCounts = new int[0]; loggerExternal.exiting(getClassNameLogging(), "executeBatch", updateCounts); @@ -2696,7 +2697,8 @@ private String parseUserSQLForTableNameDW(boolean hasInsertBeenFound, boolean ha if (localUserSQL.charAt(tempint + 1) == '.') { String tempstr = localUserSQL.substring(1, tempint); localUserSQL = localUserSQL.substring(tempint + 2); - return tempstr + "." + parseUserSQLForTableNameDW(hasInsertBeenFound, hasIntoBeenFound); + // assume that "INSERT" and "INTO" has been found, since we're at the table part already. + return tempstr + "." + parseUserSQLForTableNameDW(true, true); } else { // return tablename String tempstr = localUserSQL.substring(1, tempint); @@ -2721,7 +2723,7 @@ private String parseUserSQLForTableNameDW(boolean hasInsertBeenFound, boolean ha if (localUserSQL.charAt(tempint + 1) == '.') { String tempstr = localUserSQL.substring(1, tempint); localUserSQL = localUserSQL.substring(tempint + 2); - return tempstr + "." + parseUserSQLForTableNameDW(hasInsertBeenFound, hasIntoBeenFound); + return tempstr + "." + parseUserSQLForTableNameDW(true, true); } else { // return tablename String tempstr = localUserSQL.substring(1, tempint); @@ -2737,7 +2739,7 @@ private String parseUserSQLForTableNameDW(boolean hasInsertBeenFound, boolean ha || localUserSQL.charAt(0) == '(') { if (localUserSQL.charAt(0) == '.') { localUserSQL = localUserSQL.substring(1); - return sb.toString() + "." + parseUserSQLForTableNameDW(hasInsertBeenFound, hasIntoBeenFound); + return sb.toString() + "." + parseUserSQLForTableNameDW(true, true); } else { return sb.toString(); } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyTest.java index 5335f4401..377fb39af 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyTest.java @@ -15,6 +15,7 @@ import java.sql.SQLException; import java.sql.Statement; import java.sql.Timestamp; +import java.sql.Types; import java.util.ArrayList; import org.junit.jupiter.api.AfterAll; @@ -28,6 +29,7 @@ import com.microsoft.sqlserver.jdbc.SQLServerConnection; import com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement; import com.microsoft.sqlserver.jdbc.SQLServerStatement; +import com.microsoft.sqlserver.jdbc.TestResource; import com.microsoft.sqlserver.testframework.AbstractTest; import com.microsoft.sqlserver.testframework.DBConnection; import com.microsoft.sqlserver.testframework.Utils; @@ -262,6 +264,58 @@ public void testMixColumns() throws Exception { } } } + + @Test + public void testNullOrEmptyColumns() throws Exception { + Field f1 = SQLServerConnection.class.getDeclaredField("isAzureDW"); + f1.setAccessible(true); + f1.set(connection, true); + + String valid = "INSERT INTO " + tableName + " (c1, c2, c3, c4, c5, c6, c7) values " + + "(" + + "?, " + + "?, " + + "?, " + + "?, " + + "?, " + + "?, " + + "?, " + + ")"; + + SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) connection.prepareStatement(valid); + SQLServerStatement stmt = (SQLServerStatement) connection.createStatement(); + + pstmt.setInt(1, 1234); + pstmt.setBoolean(2, false); + pstmt.setString(3, null); + pstmt.setDate(4, null); + pstmt.setDateTime(5, null); + pstmt.setFloat(6, (float) 123.45); + pstmt.setString(7, ""); + pstmt.addBatch(); + + pstmt.executeBatch(); + + ResultSet rs = stmt.executeQuery("SELECT * FROM " + tableName); + + Object[] expected = new Object[9]; + + expected[0] = 1234; + expected[1] = false; + expected[2] = null; + expected[3] = null; + expected[4] = null; + expected[5] = 123.45; + expected[6] = " "; + + rs.next(); + for (int i=0; i < expected.length; i++) { + if (null != rs.getObject(i + 1)) { + assertEquals(rs.getObject(i + 1), expected[i]); + } + } + } + @BeforeEach public void testSetup() throws TestAbortedException, Exception { connection = DriverManager.getConnection(connectionString + ";useBulkCopyForBatchInsert=true;"); From c322174338b6b6e2dda20af8d4ec34dde170552e Mon Sep 17 00:00:00 2001 From: Peter Bae Date: Wed, 13 Jun 2018 12:45:47 -0700 Subject: [PATCH 21/32] comment revisions --- .../sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java | 8 ++++---- .../sqlserver/jdbc/SQLServerBulkCSVFileRecord.java | 8 ++++---- .../com/microsoft/sqlserver/jdbc/SQLServerBulkCommon.java | 2 +- .../com/microsoft/sqlserver/jdbc/SQLServerResource.java | 4 ++-- .../preparedStatement/BatchExecutionWithBulkCopyTest.java | 4 ++-- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java index 86b920bdd..5aa706af7 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java @@ -109,6 +109,7 @@ private Object convertValue(ColumnMetadata cm, case Types.INTEGER: { // Formatter to remove the decimal part as SQL Server floors the decimal in integer types DecimalFormat decimalFormatter = new DecimalFormat("#"); + decimalFormatter.setRoundingMode(RoundingMode.DOWN); String formatedfInput = decimalFormatter.format(Double.parseDouble(data.toString())); return Integer.valueOf(formatedfInput); } @@ -117,6 +118,7 @@ private Object convertValue(ColumnMetadata cm, case Types.SMALLINT: { // Formatter to remove the decimal part as SQL Server floors the decimal in integer types DecimalFormat decimalFormatter = new DecimalFormat("#"); + decimalFormatter.setRoundingMode(RoundingMode.DOWN); String formatedfInput = decimalFormatter.format(Double.parseDouble(data.toString())); return Short.valueOf(formatedfInput); } @@ -174,7 +176,6 @@ private Object convertValue(ColumnMetadata cm, case 2013: // java.sql.Types.TIME_WITH_TIMEZONE { - DriverJDBCVersion.checkSupportsJDBC42(); OffsetTime offsetTimeValue; // The per-column DateTimeFormatter gets priority. @@ -190,7 +191,6 @@ else if (timeFormatter != null) case 2014: // java.sql.Types.TIMESTAMP_WITH_TIMEZONE { - DriverJDBCVersion.checkSupportsJDBC42(); OffsetDateTime offsetDateTimeValue; // The per-column DateTimeFormatter gets priority. @@ -238,7 +238,7 @@ public Object[] getRowData() throws SQLServerException { // check if the size of the list of values = size of the list of columns (which is optional) if (null != columnList && columnList.size() != valueList.size()) { - MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_CSVDataSchemaMismatch")); + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_DataSchemaMismatch")); Object[] msgArgs = {}; throw new SQLServerException(form.format(msgArgs), SQLState.COL_NOT_FOUND, DriverError.NOT_SET, null); } @@ -308,7 +308,7 @@ else if (valueData.equalsIgnoreCase("null")) { throw new SQLServerException(form.format(new Object[] {value, JDBCType.of(pair.getValue().columnType)}), null, 0, e); } catch (ArrayIndexOutOfBoundsException e) { - throw new SQLServerException(SQLServerException.getErrString("R_CSVDataSchemaMismatch"), e); + throw new SQLServerException(SQLServerException.getErrString("R_DataSchemaMismatch"), e); } } return data; diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCSVFileRecord.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCSVFileRecord.java index 9840e2c24..e89e36a78 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCSVFileRecord.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCSVFileRecord.java @@ -294,7 +294,7 @@ public Object[] getRowData() throws SQLServerException { // Source header has more columns than current line read if (columnNames != null && (columnNames.length > data.length)) { - MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_CSVDataSchemaMismatch")); + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_DataSchemaMismatch")); Object[] msgArgs = {}; throw new SQLServerException(form.format(msgArgs), SQLState.COL_NOT_FOUND, DriverError.NOT_SET, null); } @@ -313,6 +313,7 @@ public Object[] getRowData() throws SQLServerException { case Types.INTEGER: { // Formatter to remove the decimal part as SQL Server floors the decimal in integer types DecimalFormat decimalFormatter = new DecimalFormat("#"); + decimalFormatter.setRoundingMode(RoundingMode.DOWN); String formatedfInput = decimalFormatter.format(Double.parseDouble(data[pair.getKey() - 1])); dataRow[pair.getKey() - 1] = Integer.valueOf(formatedfInput); break; @@ -322,6 +323,7 @@ public Object[] getRowData() throws SQLServerException { case Types.SMALLINT: { // Formatter to remove the decimal part as SQL Server floors the decimal in integer types DecimalFormat decimalFormatter = new DecimalFormat("#"); + decimalFormatter.setRoundingMode(RoundingMode.DOWN); String formatedfInput = decimalFormatter.format(Double.parseDouble(data[pair.getKey() - 1])); dataRow[pair.getKey() - 1] = Short.valueOf(formatedfInput); break; @@ -390,7 +392,6 @@ public Object[] getRowData() throws SQLServerException { case 2013: // java.sql.Types.TIME_WITH_TIMEZONE { - DriverJDBCVersion.checkSupportsJDBC42(); OffsetTime offsetTimeValue; // The per-column DateTimeFormatter gets priority. @@ -407,7 +408,6 @@ else if (timeFormatter != null) case 2014: // java.sql.Types.TIMESTAMP_WITH_TIMEZONE { - DriverJDBCVersion.checkSupportsJDBC42(); OffsetDateTime offsetDateTimeValue; // The per-column DateTimeFormatter gets priority. @@ -458,7 +458,7 @@ else if (dateTimeFormatter != null) MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_errorConvertingValue")); throw new SQLServerException(form.format(new Object[]{value, JDBCType.of(cm.columnType)}), null, 0, e); } catch (ArrayIndexOutOfBoundsException e) { - throw new SQLServerException(SQLServerException.getErrString("R_CSVDataSchemaMismatch"), e); + throw new SQLServerException(SQLServerException.getErrString("R_DataSchemaMismatch"), e); } } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCommon.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCommon.java index 669845b90..0c28dc3cb 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCommon.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCommon.java @@ -289,7 +289,7 @@ protected void checkDuplicateColumnName(int positionInTable, // duplicate check is not performed in case of same positionInTable value if (null != entry && entry.getKey() != positionInTable) { if (null != entry.getValue() && colName.trim().equalsIgnoreCase(entry.getValue().columnName)) { - throw new SQLServerException(SQLServerException.getErrString("R_BulkCSVDataDuplicateColumn"), null); + throw new SQLServerException(SQLServerException.getErrString("R_BulkDataDuplicateColumn"), null); } } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java index 2b6a9d982..17238eecd 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java @@ -227,8 +227,8 @@ protected Object[][] getContents() { {"R_invalidTransactionOption", "UseInternalTransaction option cannot be set to TRUE when used with a Connection object."}, {"R_invalidNegativeArg", "The {0} argument cannot be negative."}, {"R_BulkColumnMappingsIsEmpty", "Cannot perform bulk copy operation if the only mapping is an identity column and KeepIdentity is set to false."}, - {"R_CSVDataSchemaMismatch", "Source data does not match source schema."}, - {"R_BulkCSVDataDuplicateColumn", "Duplicate column names are not allowed."}, + {"R_DataSchemaMismatch", "Source data does not match source schema."}, + {"R_BulkDataDuplicateColumn", "Duplicate column names are not allowed."}, {"R_invalidColumnOrdinal", "Column {0} is invalid. Column number should be greater than zero."}, {"R_unsupportedEncoding", "The encoding {0} is not supported."}, {"R_UnexpectedDescribeParamFormat", "Internal error. The format of the resultset returned by sp_describe_parameter_encryption is invalid. One of the resultsets is missing."}, diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyTest.java index 377fb39af..86ed56170 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyTest.java @@ -189,7 +189,7 @@ public void testAllcolumns() throws Exception { pstmt.setFloat(6, (float) 123.45); pstmt.setString(7, "b"); pstmt.setString(8, "varc"); - pstmt.setString(9, "varcmax"); + pstmt.setString(9, "''"); pstmt.addBatch(); pstmt.executeBatch(); @@ -206,7 +206,7 @@ public void testAllcolumns() throws Exception { expected[5] = 123.45; expected[6] = "b"; expected[7] = "varc"; - expected[8] = "varcmax"; + expected[8] = "''"; rs.next(); for (int i=0; i < expected.length; i++) { From 7dd2d4208cd590c265f458170fd0c64210a48999 Mon Sep 17 00:00:00 2001 From: Peter Bae Date: Tue, 19 Jun 2018 16:39:35 -0700 Subject: [PATCH 22/32] Fixing logic / adding more tests --- .../jdbc/SQLServerBulkBatchInsertRecord.java | 7 +- .../sqlserver/jdbc/SQLServerBulkCopy.java | 7 +- .../jdbc/SQLServerPreparedStatement.java | 106 ++++++------- .../sqlserver/jdbc/SQLServerStatement.java | 13 +- .../BatchExecutionWithBulkCopyTest.java | 147 +++++++++++++++--- 5 files changed, 188 insertions(+), 92 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java index 5aa706af7..3d366e5e5 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java @@ -18,6 +18,7 @@ import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map.Entry; import java.util.Set; @@ -27,10 +28,10 @@ */ public class SQLServerBulkBatchInsertRecord extends SQLServerBulkCommon implements ISQLServerBulkRecord, java.lang.AutoCloseable { - private ArrayList batchParam; + private List batchParam; private int batchParamIndex = -1; - private ArrayList columnList; - private ArrayList valueList; + private List columnList; + private List valueList; public SQLServerBulkBatchInsertRecord(ArrayList batchParam, ArrayList columnList, diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java index ab1bdb089..647d77f64 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java @@ -1495,7 +1495,12 @@ private String createInsertBulkCommand(TDSWriter tdsWriter) throws SQLServerExce if (null != destType && (destType.toLowerCase(Locale.ENGLISH).trim().startsWith("char") || destType.toLowerCase(Locale.ENGLISH).trim().startsWith("varchar"))) addCollate = " COLLATE " + columnCollation; } - bulkCmd.append("[" + colMapping.destinationColumnName + "] " + destType + addCollate + endColumn); + if (colMapping.destinationColumnName.contains("]")) { + String escapedColumnName = colMapping.destinationColumnName.replaceAll("]", "]]"); + bulkCmd.append("[" + escapedColumnName + "] " + destType + addCollate + endColumn); + } else { + bulkCmd.append("[" + colMapping.destinationColumnName + "] " + destType + addCollate + endColumn); + } } if (true == copyOptions.isCheckConstraints()) { diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java index 4182efca5..72d234145 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java @@ -112,7 +112,7 @@ private void setPreparedStatementHandle(int handle) { */ private boolean useBulkCopyForBatchInsert; - /** Sets the prepared statement's useBulkCopyForBatchInsert value. + /** Gets the prepared statement's useBulkCopyForBatchInsert value. * * @return * Per the description. @@ -123,7 +123,7 @@ public boolean getUseBulkCopyForBatchInsert() throws SQLServerException { return useBulkCopyForBatchInsert; } - /** Fetches the prepared statement's useBulkCopyForBatchInsert value. + /** Sets the prepared statement's useBulkCopyForBatchInsert value. * * @throws SQLServerException when an error occurs */ @@ -2487,13 +2487,14 @@ public int[] executeBatch() throws SQLServerException, BatchUpdateException, SQL return updateCounts; } - String tableName = parseUserSQLForTableNameDW(false, false); + String tableName = parseUserSQLForTableNameDW(false, false, false, false); ArrayList columnList = parseUserSQLForColumnListDW(); ArrayList valueList = parseUserSQLForValueListDW(false); String destinationTableName = tableName; SQLServerStatement stmt = (SQLServerStatement) connection.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, connection.getHoldability(), stmtColumnEncriptionSetting); + // Get destination metadata try (SQLServerResultSet rs = stmt .executeQueryInternal("sp_executesql N'SET FMTONLY ON SELECT * FROM " + destinationTableName + " '");) { @@ -2649,33 +2650,49 @@ public long[] executeLargeBatch() throws SQLServerException, BatchUpdateExceptio } - private String parseUserSQLForTableNameDW(boolean hasInsertBeenFound, boolean hasIntoBeenFound) { + private String parseUserSQLForTableNameDW(boolean hasInsertBeenFound, boolean hasIntoBeenFound, boolean hasTableBeenFound, + boolean isExpectingTableName) { // As far as finding the table name goes, There are two cases: // Insert into and Insert // And there could be in-line comments (with /* and */) in between. // This method assumes the localUserSQL string starts with "insert". localUserSQL = localUserSQL.trim(); if (checkAndRemoveComments()) { - return parseUserSQLForTableNameDW(hasInsertBeenFound, hasIntoBeenFound); + return parseUserSQLForTableNameDW(hasInsertBeenFound, hasIntoBeenFound, hasTableBeenFound, isExpectingTableName); + } + + StringBuilder sb = new StringBuilder(); + + // If table has been found and the next character is not a . at this point, we've finished parsing the table name. + // This if statement is needed to handle the case where the user has something like: + // [dbo] . /* random comment */ [tableName] + if (hasTableBeenFound && !isExpectingTableName) { + if (localUserSQL.substring(0, 1).equalsIgnoreCase(".")) { + sb.append("."); + localUserSQL = localUserSQL.substring(1); + return sb.toString() + parseUserSQLForTableNameDW(true, true, true, true); + } else { + return ""; + } } if (localUserSQL.substring(0, 6).equalsIgnoreCase("insert") && !hasInsertBeenFound) { localUserSQL = localUserSQL.substring(6); - return parseUserSQLForTableNameDW(true, hasIntoBeenFound); + return parseUserSQLForTableNameDW(true, hasIntoBeenFound, hasTableBeenFound, isExpectingTableName); } if (localUserSQL.substring(0, 4).equalsIgnoreCase("into") && !hasIntoBeenFound) { // is it really "into"? // if the "into" is followed by a blank space or /*, then yes. - if (localUserSQL.charAt(4) == ' ' || localUserSQL.charAt(4) == '\t' || + if (Character.isWhitespace(localUserSQL.charAt(4)) || (localUserSQL.charAt(4) == '/' && localUserSQL.charAt(5) == '*')) { localUserSQL = localUserSQL.substring(4); - return parseUserSQLForTableNameDW(hasInsertBeenFound, true); + return parseUserSQLForTableNameDW(hasInsertBeenFound, true, hasTableBeenFound, isExpectingTableName); } // otherwise, we found the token that either contains the databasename.tablename or tablename. // Recursively handle this, but into has been found. (or rather, it's absent in the query - the "into" keyword is optional) - return parseUserSQLForTableNameDW(hasInsertBeenFound, true); + return parseUserSQLForTableNameDW(hasInsertBeenFound, true, hasTableBeenFound, isExpectingTableName); } // At this point, the next token has to be the table name. @@ -2687,24 +2704,14 @@ private String parseUserSQLForTableNameDW(boolean hasInsertBeenFound, boolean ha // keep checking if it's escaped while (localUserSQL.charAt(tempint + 1) == ']') { - localUserSQL = localUserSQL.substring(0, tempint) + localUserSQL.substring(tempint + 1); - tempint = localUserSQL.indexOf("]", tempint + 1); + tempint = localUserSQL.indexOf("]", tempint + 2); } // we've found a ] that is actually trying to close the square bracket. - // If it's followed by a dot, then it's a database. - // Otherwise, it's the table. - if (localUserSQL.charAt(tempint + 1) == '.') { - String tempstr = localUserSQL.substring(1, tempint); - localUserSQL = localUserSQL.substring(tempint + 2); - // assume that "INSERT" and "INTO" has been found, since we're at the table part already. - return tempstr + "." + parseUserSQLForTableNameDW(true, true); - } else { - // return tablename - String tempstr = localUserSQL.substring(1, tempint); - localUserSQL = localUserSQL.substring(tempint + 1); - return tempstr; - } + // return tablename + potentially more that's part of the table name + sb.append(localUserSQL.substring(0, tempint + 1)); + localUserSQL = localUserSQL.substring(tempint + 1); + return sb.toString() + parseUserSQLForTableNameDW(true, true, true, false); } // do the same for "" @@ -2713,36 +2720,21 @@ private String parseUserSQLForTableNameDW(boolean hasInsertBeenFound, boolean ha // keep checking if it's escaped while (localUserSQL.charAt(tempint + 1) == '\"') { - localUserSQL = localUserSQL.substring(0, tempint) + localUserSQL.substring(tempint + 1); - tempint = localUserSQL.indexOf("\"", tempint + 1); + tempint = localUserSQL.indexOf("\"", tempint + 2); } // we've found a " that is actually trying to close the quote. - // If it's followed by a dot, then it's a database. - // Otherwise, it's the table. - if (localUserSQL.charAt(tempint + 1) == '.') { - String tempstr = localUserSQL.substring(1, tempint); - localUserSQL = localUserSQL.substring(tempint + 2); - return tempstr + "." + parseUserSQLForTableNameDW(true, true); - } else { - // return tablename - String tempstr = localUserSQL.substring(1, tempint); - localUserSQL = localUserSQL.substring(tempint + 1); - return tempstr; - } + // return tablename + potentially more that's part of the table name + sb.append(localUserSQL.substring(0, tempint + 1)); + localUserSQL = localUserSQL.substring(tempint + 1); + return sb.toString() + parseUserSQLForTableNameDW(true, true, true, false); } - // At this point, the next chunk of string is the table name (could have database name), without starting with [ or ". - StringBuilder sb = new StringBuilder(); + // At this point, the next chunk of string is the table name, without starting with [ or ". while (localUserSQL.length() > 0) { - if (localUserSQL.charAt(0) == '.' || localUserSQL.charAt(0) == ' ' || localUserSQL.charAt(0) == '\t' - || localUserSQL.charAt(0) == '(') { - if (localUserSQL.charAt(0) == '.') { - localUserSQL = localUserSQL.substring(1); - return sb.toString() + "." + parseUserSQLForTableNameDW(true, true); - } else { - return sb.toString(); - } + // Keep going until the end of the table name is signalled - either a ., whitespace, or comment is encountered + if (localUserSQL.charAt(0) == '.' || Character.isWhitespace(localUserSQL.charAt(0)) || checkAndRemoveComments()) { + return sb.toString() + parseUserSQLForTableNameDW(true, true, true, false); } else { sb.append(localUserSQL.charAt(0)); localUserSQL = localUserSQL.substring(1); @@ -2823,16 +2815,14 @@ private ArrayList parseUserSQLForColumnListDWHelper(ArrayList li // At this point, the next chunk of string is the column name, without starting with [ or ". StringBuilder sb = new StringBuilder(); while (localUserSQL.length() > 0) { - if (localUserSQL.charAt(0) == ',' || localUserSQL.charAt(0) == ')') { - if (localUserSQL.charAt(0) == ',') { - localUserSQL = localUserSQL.substring(1); - listOfColumns.add(sb.toString()); - return parseUserSQLForColumnListDWHelper(listOfColumns); - } else { - localUserSQL = localUserSQL.substring(1); - listOfColumns.add(sb.toString()); - return listOfColumns; - } + if (localUserSQL.charAt(0) == ',') { + localUserSQL = localUserSQL.substring(1); + listOfColumns.add(sb.toString()); + return parseUserSQLForColumnListDWHelper(listOfColumns); + } else if (localUserSQL.charAt(0) == ')'){ + localUserSQL = localUserSQL.substring(1); + listOfColumns.add(sb.toString()); + return listOfColumns; } else if (checkAndRemoveComments()) { localUserSQL = localUserSQL.trim(); } else { diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java index 9566552fb..592bb8deb 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java @@ -968,16 +968,16 @@ final void resetForReexecute() throws SQLServerException { * * @param sql * The statment SQL. - * @return True is the statement is a select. + * @return True if the statement is a select. */ /* L0 */ final boolean isSelect(String sql) throws SQLServerException { checkClosed(); // Used to check just the first letter which would cause // "Set" commands to return true... String temp = sql.trim(); - char c = temp.charAt(0); - if (c != 's' && c != 'S') + if (null == sql || sql.length() < 6) { return false; + } return temp.substring(0, 6).equalsIgnoreCase("select"); } @@ -986,23 +986,20 @@ final void resetForReexecute() throws SQLServerException { * * @param sql * The statment SQL. - * @return True is the statement is a select. + * @return True if the statement is an insert. */ /* L0 */ final boolean isInsert(String sql) throws SQLServerException { checkClosed(); // Used to check just the first letter which would cause // "Set" commands to return true... String temp = sql.trim(); - if (null == sql || sql.length() < 3) { + if (null == sql || sql.length() < 6) { return false; } if (temp.substring(0, 2).equalsIgnoreCase("/*")) { int index = temp.indexOf("*/") + 2; return isInsert(temp.substring(index)); } - char c = temp.charAt(0); - if (c != 'i' && c != 'I') - return false; return temp.substring(0, 6).equalsIgnoreCase("insert"); } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyTest.java index 86ed56170..39b04aea1 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyTest.java @@ -40,10 +40,13 @@ public class BatchExecutionWithBulkCopyTest extends AbstractTest { static SQLServerPreparedStatement pstmt = null; static Statement stmt = null; static Connection connection = null; - static String tableName = "BulkCopyParseTest" + System.currentTimeMillis(); + static long UUID = System.currentTimeMillis();; + static String tableName = "BulkCopyParseTest" + UUID; + static String squareBracketTableName = "[peter]]]]test" + UUID + "]"; + static String doubleQuoteTableName = "\"peter\"\"\"\"test" + UUID + "\""; @Test - public void testIsInsert() throws SQLException, NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { + public void testIsInsert() throws Exception { String valid1 = "INSERT INTO PeterTable values (1, 2)"; String valid2 = " INSERT INTO PeterTable values (1, 2)"; String valid3 = "/* asdf */ INSERT INTO PeterTable values (1, 2)"; @@ -59,7 +62,7 @@ public void testIsInsert() throws SQLException, NoSuchMethodException, SecurityE } @Test - public void testComments() throws SQLException, NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { + public void testComments() throws Exception { pstmt = (SQLServerPreparedStatement) connection.prepareStatement(""); String valid = "/* rando comment *//* rando comment */ INSERT /* rando comment */ INTO /* rando comment *//*rando comment*/ PeterTable /*rando comment */" @@ -69,14 +72,14 @@ public void testComments() throws SQLException, NoSuchFieldException, SecurityEx f1.setAccessible(true); f1.set(pstmt, valid); - Method method = pstmt.getClass().getSuperclass().getDeclaredMethod("parseUserSQLForTableNameDW", boolean.class, boolean.class); + Method method = pstmt.getClass().getSuperclass().getDeclaredMethod("parseUserSQLForTableNameDW", boolean.class, boolean.class, boolean.class, boolean.class); method.setAccessible(true); - assertEquals((String) method.invoke(pstmt, false, false), "PeterTable"); + assertEquals((String) method.invoke(pstmt, false, false, false, false), "PeterTable"); } @Test - public void testBrackets() throws SQLException, NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { + public void testBrackets() throws Exception { pstmt = (SQLServerPreparedStatement) connection.prepareStatement(""); String valid = "/* rando comment *//* rando comment */ INSERT /* rando comment */ INTO /* rando comment *//*rando comment*/ [Peter[]]Table] /*rando comment */" @@ -86,14 +89,14 @@ public void testBrackets() throws SQLException, NoSuchFieldException, SecurityEx f1.setAccessible(true); f1.set(pstmt, valid); - Method method = pstmt.getClass().getSuperclass().getDeclaredMethod("parseUserSQLForTableNameDW", boolean.class, boolean.class); + Method method = pstmt.getClass().getSuperclass().getDeclaredMethod("parseUserSQLForTableNameDW", boolean.class, boolean.class, boolean.class, boolean.class); method.setAccessible(true); - assertEquals((String) method.invoke(pstmt, false, false), "Peter[]Table"); + assertEquals((String) method.invoke(pstmt, false, false, false, false), "[Peter[]]Table]"); } @Test - public void testDoubleQuotes() throws SQLException, NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { + public void testDoubleQuotes() throws Exception { pstmt = (SQLServerPreparedStatement) connection.prepareStatement(""); String valid = "/* rando comment *//* rando comment */ INSERT /* rando comment */ INTO /* rando comment *//*rando comment*/ \"Peter\"\"\"\"Table\" /*rando comment */" @@ -103,14 +106,14 @@ public void testDoubleQuotes() throws SQLException, NoSuchFieldException, Securi f1.setAccessible(true); f1.set(pstmt, valid); - Method method = pstmt.getClass().getSuperclass().getDeclaredMethod("parseUserSQLForTableNameDW", boolean.class, boolean.class); + Method method = pstmt.getClass().getSuperclass().getDeclaredMethod("parseUserSQLForTableNameDW", boolean.class, boolean.class, boolean.class, boolean.class); method.setAccessible(true); - assertEquals((String) method.invoke(pstmt, false, false), "Peter\"\"Table"); + assertEquals((String) method.invoke(pstmt, false, false, false, false), "\"Peter\"\"\"\"Table\""); } @Test - public void testAll() throws SQLException, NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { + public void testAll() throws Exception { pstmt = (SQLServerPreparedStatement) connection.prepareStatement(""); String valid = "/* rando comment *//* rando comment */ INSERT /* rando comment */ INTO /* rando comment *//*rando comment*/ \"Peter\"\"\"\"Table\" /*rando comment */" @@ -121,10 +124,10 @@ public void testAll() throws SQLException, NoSuchFieldException, SecurityExcepti f1.setAccessible(true); f1.set(pstmt, valid); - Method method = pstmt.getClass().getSuperclass().getDeclaredMethod("parseUserSQLForTableNameDW", boolean.class, boolean.class); + Method method = pstmt.getClass().getSuperclass().getDeclaredMethod("parseUserSQLForTableNameDW", boolean.class, boolean.class, boolean.class, boolean.class); method.setAccessible(true); - assertEquals((String) method.invoke(pstmt, false, false), "Peter\"\"Table"); + assertEquals((String) method.invoke(pstmt, false, false, false, false), "\"Peter\"\"\"\"Table\""); method = pstmt.getClass().getSuperclass().getDeclaredMethod("parseUserSQLForColumnListDW"); method.setAccessible(true); @@ -174,8 +177,8 @@ public void testAllcolumns() throws Exception { + "?, " + ")"; - SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) connection.prepareStatement(valid); - SQLServerStatement stmt = (SQLServerStatement) connection.createStatement(); + pstmt = (SQLServerPreparedStatement) connection.prepareStatement(valid); + stmt = (SQLServerStatement) connection.createStatement(); Timestamp myTimestamp = new Timestamp(114550L); @@ -228,8 +231,8 @@ public void testMixColumns() throws Exception { + "?, " + ")"; - SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) connection.prepareStatement(valid); - SQLServerStatement stmt = (SQLServerStatement) connection.createStatement(); + pstmt = (SQLServerPreparedStatement) connection.prepareStatement(valid); + stmt = (SQLServerStatement) connection.createStatement(); Timestamp myTimestamp = new Timestamp(114550L); @@ -282,8 +285,8 @@ public void testNullOrEmptyColumns() throws Exception { + "?, " + ")"; - SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) connection.prepareStatement(valid); - SQLServerStatement stmt = (SQLServerStatement) connection.createStatement(); + pstmt = (SQLServerPreparedStatement) connection.prepareStatement(valid); + stmt = (SQLServerStatement) connection.createStatement(); pstmt.setInt(1, 1234); pstmt.setBoolean(2, false); @@ -316,10 +319,108 @@ public void testNullOrEmptyColumns() throws Exception { } } + @Test + public void testSquareBracketAgainstDB() throws Exception { + Field f1 = SQLServerConnection.class.getDeclaredField("isAzureDW"); + f1.setAccessible(true); + f1.set(connection, true); + stmt = (SQLServerStatement) connection.createStatement(); + + Utils.dropTableIfExists(squareBracketTableName, stmt); + String createTable = "create table " + squareBracketTableName + " (c1 int)"; + stmt.execute(createTable); + + String valid = "insert into " + squareBracketTableName + " values (?)"; + pstmt = (SQLServerPreparedStatement) connection.prepareStatement(valid); + pstmt.setInt(1, 1); + pstmt.addBatch(); + + pstmt.executeBatch(); + + ResultSet rs = stmt.executeQuery("SELECT * FROM " + squareBracketTableName); + rs.next(); + + assertEquals(rs.getObject(1), 1); + } + + @Test + public void testDoubleQuoteAgainstDB() throws Exception { + Field f1 = SQLServerConnection.class.getDeclaredField("isAzureDW"); + f1.setAccessible(true); + f1.set(connection, true); + stmt = (SQLServerStatement) connection.createStatement(); + + Utils.dropTableIfExists(doubleQuoteTableName, stmt); + String createTable = "create table " + doubleQuoteTableName + " (c1 int)"; + stmt.execute(createTable); + + String valid = "insert into " + doubleQuoteTableName + " values (?)"; + pstmt = (SQLServerPreparedStatement) connection.prepareStatement(valid); + pstmt.setInt(1, 1); + pstmt.addBatch(); + + pstmt.executeBatch(); + + ResultSet rs = stmt.executeQuery("SELECT * FROM " + doubleQuoteTableName); + rs.next(); + + assertEquals(rs.getObject(1), 1); + } + + @Test + public void testSchemaAgainstDB() throws Exception { + Field f1 = SQLServerConnection.class.getDeclaredField("isAzureDW"); + f1.setAccessible(true); + f1.set(connection, true); + stmt = (SQLServerStatement) connection.createStatement(); + + Utils.dropTableIfExists("[dbo]." + squareBracketTableName, stmt); + String schemaTableName = "[test] /*some comment*/ . \"dbo\" . /*some comment */ " + squareBracketTableName; + + String createTable = "create table " + schemaTableName + " (c1 int)"; + stmt.execute(createTable); + + String valid = "insert into " + schemaTableName + " values (?)"; + pstmt = (SQLServerPreparedStatement) connection.prepareStatement(valid); + pstmt.setInt(1, 1); + pstmt.addBatch(); + + pstmt.executeBatch(); + + ResultSet rs = stmt.executeQuery("SELECT * FROM " + schemaTableName); + rs.next(); + + assertEquals(rs.getObject(1), 1); + } + + @Test + public void testColumnNameMixAgainstDB() throws Exception { + Field f1 = SQLServerConnection.class.getDeclaredField("isAzureDW"); + f1.setAccessible(true); + f1.set(connection, true); + stmt = (SQLServerStatement) connection.createStatement(); + + Utils.dropTableIfExists(squareBracketTableName, stmt); + String createTable = "create table " + squareBracketTableName + " ([c]]]]1] int, [c]]]]2] int)"; + stmt.execute(createTable); + + String valid = "insert into " + squareBracketTableName + " ([c]]]]1], [c]]]]2]) values (?, 1)"; + pstmt = (SQLServerPreparedStatement) connection.prepareStatement(valid); + pstmt.setInt(1, 1); + pstmt.addBatch(); + + pstmt.executeBatch(); + + ResultSet rs = stmt.executeQuery("SELECT * FROM " + squareBracketTableName); + rs.next(); + + assertEquals(rs.getObject(1), 1); + } + @BeforeEach public void testSetup() throws TestAbortedException, Exception { connection = DriverManager.getConnection(connectionString + ";useBulkCopyForBatchInsert=true;"); - SQLServerStatement stmt = (SQLServerStatement) connection.createStatement(); + stmt = (SQLServerStatement) connection.createStatement(); Utils.dropTableIfExists(tableName, stmt); String sql1 = "create table " + tableName + " " @@ -343,8 +444,10 @@ public void testSetup() throws TestAbortedException, Exception { public static void terminateVariation() throws SQLException { connection = DriverManager.getConnection(connectionString); - SQLServerStatement stmt = (SQLServerStatement) connection.createStatement(); + stmt = (SQLServerStatement) connection.createStatement(); Utils.dropTableIfExists(tableName, stmt); + Utils.dropTableIfExists(squareBracketTableName, stmt); + Utils.dropTableIfExists(doubleQuoteTableName, stmt); if (null != pstmt) { pstmt.close(); From 84acbb8d7e03ada3fad09144d0039e1c9ec57668 Mon Sep 17 00:00:00 2001 From: Peter Bae Date: Tue, 19 Jun 2018 16:57:48 -0700 Subject: [PATCH 23/32] dont use test database in tests --- .../jdbc/preparedStatement/BatchExecutionWithBulkCopyTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyTest.java index 39b04aea1..843056d66 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyTest.java @@ -375,7 +375,7 @@ public void testSchemaAgainstDB() throws Exception { stmt = (SQLServerStatement) connection.createStatement(); Utils.dropTableIfExists("[dbo]." + squareBracketTableName, stmt); - String schemaTableName = "[test] /*some comment*/ . \"dbo\" . /*some comment */ " + squareBracketTableName; + String schemaTableName = "\"dbo\" . /*some comment */ " + squareBracketTableName; String createTable = "create table " + schemaTableName + " (c1 int)"; stmt.execute(createTable); From 38bcdc6cd2df060b6e2b65ebe5cb067f18eaf74d Mon Sep 17 00:00:00 2001 From: Peter Bae Date: Wed, 20 Jun 2018 12:17:55 -0700 Subject: [PATCH 24/32] Change exception handling as per JDBC specs --- .../jdbc/SQLServerPreparedStatement.java | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java index 72d234145..9e02e4d90 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java @@ -2481,6 +2481,24 @@ public int[] executeBatch() throws SQLServerException, BatchUpdateException, SQL try { if (isInsert(localUserSQL) && connection.isAzureDW() && (this.useBulkCopyForBatchInsert)) { + // From the JDBC spec, section 9.1.4 - Making Batch Updates: + // The CallableStatement.executeBatch method (inherited from PreparedStatement) will + // throw a BatchUpdateException if the stored procedure returns anything other than an + // update count or takes OUT or INOUT parameters. + // + // Non-update count results (e.g. ResultSets) are treated as individual batch errors + // when they are encountered in the response. + // + // OUT and INOUT parameter checking is done here, before executing the batch. If any + // OUT or INOUT are present, the entire batch fails. + for (Parameter[] paramValues : batchParamValues) { + for (Parameter paramValue : paramValues) { + if (paramValue.isOutput()) { + throw new BatchUpdateException(SQLServerException.getErrString("R_outParamsNotPermittedinBatch"), null, 0, null); + } + } + } + if (batchParamValues == null) { updateCounts = new int[0]; loggerExternal.exiting(getClassNameLogging(), "executeBatch", updateCounts); @@ -2537,9 +2555,8 @@ public int[] executeBatch() throws SQLServerException, BatchUpdateException, SQL } } catch (SQLException e) { - // Unable to retrieve metadata for destination - // create an error message for failing bulk copy + insert batch - throw new SQLServerException(SQLServerException.getErrString("R_unableRetrieveColMeta"), e); + // throw a BatchUpdateException with the given error message, and return null for the updateCounts. + throw new BatchUpdateException(e.getMessage(), null, 0, null); } catch (Exception e) { // If we fail with non-SQLException, fall back to the original batch insert logic. From b4d4b26460e0ea07b1d2d93c1727a50ec6d1c332 Mon Sep 17 00:00:00 2001 From: Peter Bae Date: Wed, 20 Jun 2018 13:52:59 -0700 Subject: [PATCH 25/32] remove some comments --- .../sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java index 3d366e5e5..c937c5f29 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java @@ -103,10 +103,6 @@ public boolean isAutoIncrement(int column) { private Object convertValue(ColumnMetadata cm, Object data) throws SQLServerException { switch (cm.columnType) { - /* - * Both BCP and BULK INSERT considers double quotes as part of the data and throws error if any data (say "10") is to be inserted into an - * numeric column. Our implementation does the same. - */ case Types.INTEGER: { // Formatter to remove the decimal part as SQL Server floors the decimal in integer types DecimalFormat decimalFormatter = new DecimalFormat("#"); From 9b7f40c429ba03bfea2baf13bb6b6aa96540fd5a Mon Sep 17 00:00:00 2001 From: Peter Bae Date: Fri, 22 Jun 2018 03:22:20 -0700 Subject: [PATCH 26/32] reflect comments --- .../sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java | 4 ++-- .../microsoft/sqlserver/jdbc/SQLServerBulkCSVFileRecord.java | 4 ++-- .../com/microsoft/sqlserver/jdbc/SQLServerBulkCommon.java | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java index c937c5f29..964a0fbee 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java @@ -171,7 +171,7 @@ private Object convertValue(ColumnMetadata cm, } } - case 2013: // java.sql.Types.TIME_WITH_TIMEZONE + case java.sql.Types.TIME_WITH_TIMEZONE: { OffsetTime offsetTimeValue; @@ -186,7 +186,7 @@ else if (timeFormatter != null) return offsetTimeValue; } - case 2014: // java.sql.Types.TIMESTAMP_WITH_TIMEZONE + case java.sql.Types.TIMESTAMP_WITH_TIMEZONE: { OffsetDateTime offsetDateTimeValue; diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCSVFileRecord.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCSVFileRecord.java index e89e36a78..7ed34b62d 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCSVFileRecord.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCSVFileRecord.java @@ -390,7 +390,7 @@ public Object[] getRowData() throws SQLServerException { break; } - case 2013: // java.sql.Types.TIME_WITH_TIMEZONE + case java.sql.Types.TIME_WITH_TIMEZONE: { OffsetTime offsetTimeValue; @@ -406,7 +406,7 @@ else if (timeFormatter != null) break; } - case 2014: // java.sql.Types.TIMESTAMP_WITH_TIMEZONE + case java.sql.Types.TIMESTAMP_WITH_TIMEZONE: { OffsetDateTime offsetDateTimeValue; diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCommon.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCommon.java index cc14d2c6a..206cd9ef0 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCommon.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCommon.java @@ -156,10 +156,10 @@ void addColumnMetadataInternal(int positionInSource, if (null != name) colName = name.trim(); - else if ((columnNames != null) && (columnNames.length >= positionInSource)) + else if ((null != columnNames) && (columnNames.length >= positionInSource)) colName = columnNames[positionInSource - 1]; - if ((columnNames != null) && (positionInSource > columnNames.length)) { + if ((null != columnNames) && (positionInSource > columnNames.length)) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidColumn")); Object[] msgArgs = {positionInSource}; throw new SQLServerException(form.format(msgArgs), SQLState.COL_NOT_FOUND, DriverError.NOT_SET, null); From 405b47e776897bcf22a8470c6fc45bc233786517 Mon Sep 17 00:00:00 2001 From: Peter Bae Date: Fri, 22 Jun 2018 15:30:53 -0700 Subject: [PATCH 27/32] changed how logger works, refactored code in SQLServerBulkCommon due to that, changed exception being thrown to BatchUpdateException, added same logic for parsing in executeLargeBatch, and added tests accordingly. --- .../jdbc/SQLServerBulkBatchInsertRecord.java | 128 ++++++++++++++++-- .../jdbc/SQLServerBulkCSVFileRecord.java | 119 +++++++++++++++- .../sqlserver/jdbc/SQLServerBulkCommon.java | 92 ------------- .../jdbc/SQLServerPreparedStatement.java | 95 ++++++++++++- .../BatchExecutionWithBulkCopyTest.java | 59 ++++++++ 5 files changed, 383 insertions(+), 110 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java index 964a0fbee..8df463d63 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java @@ -20,25 +20,36 @@ import java.util.HashMap; import java.util.List; import java.util.Map.Entry; + +import com.microsoft.sqlserver.jdbc.SQLServerBulkCommon.ColumnMetadata; + import java.util.Set; /** * A simple implementation of the ISQLServerBulkRecord interface that can be used to read in the basic Java data types from an ArrayList of Parameters * that were provided by pstmt/cstmt. */ -public class SQLServerBulkBatchInsertRecord extends SQLServerBulkCommon implements ISQLServerBulkRecord, java.lang.AutoCloseable { +public class SQLServerBulkBatchInsertRecord extends SQLServerBulkCommon implements ISQLServerBulkRecord { private List batchParam; private int batchParamIndex = -1; private List columnList; private List valueList; + + /* + * Class name for logging. + */ + private static final String loggerClassName = "com.microsoft.sqlserver.jdbc.SQLServerBulkBatchInsertRecord"; + + /* + * Logger + */ + private static final java.util.logging.Logger loggerExternal = java.util.logging.Logger.getLogger(loggerClassName); public SQLServerBulkBatchInsertRecord(ArrayList batchParam, ArrayList columnList, ArrayList valueList, String encoding) throws SQLServerException { - loggerClassName = "com.microsoft.sqlserver.jdbc.SQLServerBulkBatchInsertRecord"; - loggerExternal = java.util.logging.Logger.getLogger(loggerClassName); loggerExternal.entering(loggerClassName, "SQLServerBulkBatchInsertRecord", new Object[] {batchParam, encoding}); if (null == batchParam) { @@ -57,15 +68,6 @@ public SQLServerBulkBatchInsertRecord(ArrayList batchParam, loggerExternal.exiting(loggerClassName, "SQLServerBulkBatchInsertRecord"); } - /** - * Releases any resources associated with the batch. - * - * @throws SQLServerException - * when an error occurs - */ - public void close() throws SQLServerException { - } - public DateTimeFormatter getColumnDateTimeFormatter(int column) { return columnMetadata.get(column).dateTimeFormatter; } @@ -310,6 +312,108 @@ else if (valueData.equalsIgnoreCase("null")) { } return data; } + + @Override + void addColumnMetadataInternal(int positionInSource, + String name, + int jdbcType, + int precision, + int scale, + DateTimeFormatter dateTimeFormatter) throws SQLServerException { + loggerExternal.entering(loggerClassName, "addColumnMetadata", new Object[] {positionInSource, name, jdbcType, precision, scale}); + + String colName = ""; + + if (0 >= positionInSource) { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidColumnOrdinal")); + Object[] msgArgs = {positionInSource}; + throw new SQLServerException(form.format(msgArgs), SQLState.COL_NOT_FOUND, DriverError.NOT_SET, null); + } + + if (null != name) + colName = name.trim(); + else if ((null != columnNames) && (columnNames.length >= positionInSource)) + colName = columnNames[positionInSource - 1]; + + if ((null != columnNames) && (positionInSource > columnNames.length)) { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidColumn")); + Object[] msgArgs = {positionInSource}; + throw new SQLServerException(form.format(msgArgs), SQLState.COL_NOT_FOUND, DriverError.NOT_SET, null); + } + + checkDuplicateColumnName(positionInSource, name); + switch (jdbcType) { + /* + * 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. string literal formats supported by temporal types are available in MSDN + * page on data types. + */ + case java.sql.Types.DATE: + case java.sql.Types.TIME: + case java.sql.Types.TIMESTAMP: + case microsoft.sql.Types.DATETIMEOFFSET: + columnMetadata.put(positionInSource, new ColumnMetadata(colName, jdbcType, precision, scale, dateTimeFormatter)); + break; + + // Redirect SQLXML as LONGNVARCHAR + // SQLXML is not valid type in TDS + case java.sql.Types.SQLXML: + columnMetadata.put(positionInSource, new ColumnMetadata(colName, java.sql.Types.LONGNVARCHAR, precision, scale, dateTimeFormatter)); + break; + + // Redirecting Float as Double based on data type mapping + // https://msdn.microsoft.com/en-us/library/ms378878%28v=sql.110%29.aspx + case java.sql.Types.FLOAT: + columnMetadata.put(positionInSource, new ColumnMetadata(colName, java.sql.Types.DOUBLE, precision, scale, dateTimeFormatter)); + break; + + // redirecting BOOLEAN as BIT + case java.sql.Types.BOOLEAN: + columnMetadata.put(positionInSource, new ColumnMetadata(colName, java.sql.Types.BIT, precision, scale, dateTimeFormatter)); + break; + + default: + columnMetadata.put(positionInSource, new ColumnMetadata(colName, jdbcType, precision, scale, dateTimeFormatter)); + } + + loggerExternal.exiting(loggerClassName, "addColumnMetadata"); + } + + @Override + public void setTimestampWithTimezoneFormat(String dateTimeFormat) { + loggerExternal.entering(loggerClassName, "setTimestampWithTimezoneFormat", dateTimeFormat); + + this.dateTimeFormatter = DateTimeFormatter.ofPattern(dateTimeFormat); + + loggerExternal.exiting(loggerClassName, "setTimestampWithTimezoneFormat"); + } + + @Override + public void setTimestampWithTimezoneFormat(DateTimeFormatter dateTimeFormatter) { + loggerExternal.entering(loggerClassName, "setTimestampWithTimezoneFormat", new Object[] {dateTimeFormatter}); + + this.dateTimeFormatter = dateTimeFormatter; + + loggerExternal.exiting(loggerClassName, "setTimestampWithTimezoneFormat"); + } + + @Override + public void setTimeWithTimezoneFormat(String timeFormat) { + loggerExternal.entering(loggerClassName, "setTimeWithTimezoneFormat", timeFormat); + + this.timeFormatter = DateTimeFormatter.ofPattern(timeFormat); + + loggerExternal.exiting(loggerClassName, "setTimeWithTimezoneFormat"); + } + + @Override + public void setTimeWithTimezoneFormat(DateTimeFormatter dateTimeFormatter) { + loggerExternal.entering(loggerClassName, "setTimeWithTimezoneFormat", new Object[] {dateTimeFormatter}); + + this.timeFormatter = dateTimeFormatter; + + loggerExternal.exiting(loggerClassName, "setTimeWithTimezoneFormat"); + } @Override public boolean next() throws SQLServerException { diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCSVFileRecord.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCSVFileRecord.java index 7ed34b62d..903a5f653 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCSVFileRecord.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCSVFileRecord.java @@ -24,6 +24,9 @@ import java.time.format.DateTimeFormatter; import java.util.HashMap; import java.util.Map.Entry; + +import com.microsoft.sqlserver.jdbc.SQLServerBulkCommon.ColumnMetadata; + import java.util.Set; /** @@ -47,6 +50,16 @@ public class SQLServerBulkCSVFileRecord extends SQLServerBulkCommon implements I * Delimiter to parse lines with. */ private final String delimiter; + + /* + * Class name for logging. + */ + private static final String loggerClassName = "com.microsoft.sqlserver.jdbc.SQLServerBulkCSVFileRecord"; + + /* + * Logger + */ + private static final java.util.logging.Logger loggerExternal = java.util.logging.Logger.getLogger(loggerClassName); /** * Creates a simple reader to parse data from a delimited file with the given encoding. @@ -66,8 +79,6 @@ public SQLServerBulkCSVFileRecord(String fileToParse, String encoding, String delimiter, boolean firstLineIsColumnNames) throws SQLServerException { - loggerClassName = "com.microsoft.sqlserver.jdbc.SQLServerBulkCSVFileRecord"; - loggerExternal = java.util.logging.Logger.getLogger(loggerClassName); loggerExternal.entering(loggerClassName, "SQLServerBulkCSVFileRecord", new Object[] {fileToParse, encoding, delimiter, firstLineIsColumnNames}); @@ -128,8 +139,6 @@ public SQLServerBulkCSVFileRecord(InputStream fileToParse, String encoding, String delimiter, boolean firstLineIsColumnNames) throws SQLServerException { - loggerClassName = "com.microsoft.sqlserver.jdbc.SQLServerBulkCSVFileRecord"; - loggerExternal = java.util.logging.Logger.getLogger(loggerClassName); loggerExternal.entering(loggerClassName, "SQLServerBulkCSVFileRecord", new Object[] {fileToParse, encoding, delimiter, firstLineIsColumnNames}); @@ -465,6 +474,108 @@ else if (dateTimeFormatter != null) return dataRow; } } + + @Override + void addColumnMetadataInternal(int positionInSource, + String name, + int jdbcType, + int precision, + int scale, + DateTimeFormatter dateTimeFormatter) throws SQLServerException { + loggerExternal.entering(loggerClassName, "addColumnMetadata", new Object[] {positionInSource, name, jdbcType, precision, scale}); + + String colName = ""; + + if (0 >= positionInSource) { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidColumnOrdinal")); + Object[] msgArgs = {positionInSource}; + throw new SQLServerException(form.format(msgArgs), SQLState.COL_NOT_FOUND, DriverError.NOT_SET, null); + } + + if (null != name) + colName = name.trim(); + else if ((null != columnNames) && (columnNames.length >= positionInSource)) + colName = columnNames[positionInSource - 1]; + + if ((null != columnNames) && (positionInSource > columnNames.length)) { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidColumn")); + Object[] msgArgs = {positionInSource}; + throw new SQLServerException(form.format(msgArgs), SQLState.COL_NOT_FOUND, DriverError.NOT_SET, null); + } + + checkDuplicateColumnName(positionInSource, name); + switch (jdbcType) { + /* + * 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. string literal formats supported by temporal types are available in MSDN + * page on data types. + */ + case java.sql.Types.DATE: + case java.sql.Types.TIME: + case java.sql.Types.TIMESTAMP: + case microsoft.sql.Types.DATETIMEOFFSET: + columnMetadata.put(positionInSource, new ColumnMetadata(colName, jdbcType, 50, scale, dateTimeFormatter)); + break; + + // Redirect SQLXML as LONGNVARCHAR + // SQLXML is not valid type in TDS + case java.sql.Types.SQLXML: + columnMetadata.put(positionInSource, new ColumnMetadata(colName, java.sql.Types.LONGNVARCHAR, precision, scale, dateTimeFormatter)); + break; + + // Redirecting Float as Double based on data type mapping + // https://msdn.microsoft.com/en-us/library/ms378878%28v=sql.110%29.aspx + case java.sql.Types.FLOAT: + columnMetadata.put(positionInSource, new ColumnMetadata(colName, java.sql.Types.DOUBLE, precision, scale, dateTimeFormatter)); + break; + + // redirecting BOOLEAN as BIT + case java.sql.Types.BOOLEAN: + columnMetadata.put(positionInSource, new ColumnMetadata(colName, java.sql.Types.BIT, precision, scale, dateTimeFormatter)); + break; + + default: + columnMetadata.put(positionInSource, new ColumnMetadata(colName, jdbcType, precision, scale, dateTimeFormatter)); + } + + loggerExternal.exiting(loggerClassName, "addColumnMetadata"); + } + + @Override + public void setTimestampWithTimezoneFormat(String dateTimeFormat) { + loggerExternal.entering(loggerClassName, "setTimestampWithTimezoneFormat", dateTimeFormat); + + this.dateTimeFormatter = DateTimeFormatter.ofPattern(dateTimeFormat); + + loggerExternal.exiting(loggerClassName, "setTimestampWithTimezoneFormat"); + } + + @Override + public void setTimestampWithTimezoneFormat(DateTimeFormatter dateTimeFormatter) { + loggerExternal.entering(loggerClassName, "setTimestampWithTimezoneFormat", new Object[] {dateTimeFormatter}); + + this.dateTimeFormatter = dateTimeFormatter; + + loggerExternal.exiting(loggerClassName, "setTimestampWithTimezoneFormat"); + } + + @Override + public void setTimeWithTimezoneFormat(String timeFormat) { + loggerExternal.entering(loggerClassName, "setTimeWithTimezoneFormat", timeFormat); + + this.timeFormatter = DateTimeFormatter.ofPattern(timeFormat); + + loggerExternal.exiting(loggerClassName, "setTimeWithTimezoneFormat"); + } + + @Override + public void setTimeWithTimezoneFormat(DateTimeFormatter dateTimeFormatter) { + loggerExternal.entering(loggerClassName, "setTimeWithTimezoneFormat", new Object[] {dateTimeFormatter}); + + this.timeFormatter = dateTimeFormatter; + + loggerExternal.exiting(loggerClassName, "setTimeWithTimezoneFormat"); + } @Override public boolean next() throws SQLServerException { diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCommon.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCommon.java index 206cd9ef0..0d45af6a3 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCommon.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCommon.java @@ -38,16 +38,6 @@ protected class ColumnMetadata { } } - /* - * Class name for logging. - */ - protected static String loggerClassName; - - /* - * Logger - */ - protected java.util.logging.Logger loggerExternal; - /* * Contains all the column names if firstLineIsColumnNames is true */ @@ -144,68 +134,6 @@ void addColumnMetadataInternal(int positionInSource, int precision, int scale, DateTimeFormatter dateTimeFormatter) throws SQLServerException { - loggerExternal.entering(loggerClassName, "addColumnMetadata", new Object[] {positionInSource, name, jdbcType, precision, scale}); - - String colName = ""; - - if (0 >= positionInSource) { - MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidColumnOrdinal")); - Object[] msgArgs = {positionInSource}; - throw new SQLServerException(form.format(msgArgs), SQLState.COL_NOT_FOUND, DriverError.NOT_SET, null); - } - - if (null != name) - colName = name.trim(); - else if ((null != columnNames) && (columnNames.length >= positionInSource)) - colName = columnNames[positionInSource - 1]; - - if ((null != columnNames) && (positionInSource > columnNames.length)) { - MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidColumn")); - Object[] msgArgs = {positionInSource}; - throw new SQLServerException(form.format(msgArgs), SQLState.COL_NOT_FOUND, DriverError.NOT_SET, null); - } - - checkDuplicateColumnName(positionInSource, name); - switch (jdbcType) { - /* - * 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. string literal formats supported by temporal types are available in MSDN - * page on data types. - */ - case java.sql.Types.DATE: - case java.sql.Types.TIME: - case java.sql.Types.TIMESTAMP: - case microsoft.sql.Types.DATETIMEOFFSET: - if (this instanceof SQLServerBulkCSVFileRecord) { - columnMetadata.put(positionInSource, new ColumnMetadata(colName, jdbcType, 50, scale, dateTimeFormatter)); - } - else { - columnMetadata.put(positionInSource, new ColumnMetadata(colName, jdbcType, precision, scale, dateTimeFormatter)); - } - break; - - // Redirect SQLXML as LONGNVARCHAR - // SQLXML is not valid type in TDS - case java.sql.Types.SQLXML: - columnMetadata.put(positionInSource, new ColumnMetadata(colName, java.sql.Types.LONGNVARCHAR, precision, scale, dateTimeFormatter)); - break; - - // Redirecting Float as Double based on data type mapping - // https://msdn.microsoft.com/en-us/library/ms378878%28v=sql.110%29.aspx - case java.sql.Types.FLOAT: - columnMetadata.put(positionInSource, new ColumnMetadata(colName, java.sql.Types.DOUBLE, precision, scale, dateTimeFormatter)); - break; - - // redirecting BOOLEAN as BIT - case java.sql.Types.BOOLEAN: - columnMetadata.put(positionInSource, new ColumnMetadata(colName, java.sql.Types.BIT, precision, scale, dateTimeFormatter)); - break; - - default: - columnMetadata.put(positionInSource, new ColumnMetadata(colName, jdbcType, precision, scale, dateTimeFormatter)); - } - - loggerExternal.exiting(loggerClassName, "addColumnMetadata"); } /** @@ -215,11 +143,6 @@ else if ((null != columnNames) && (columnNames.length >= positionInSource)) * format to parse data sent as java.sql.Types.TIMESTAMP_WITH_TIMEZONE */ public void setTimestampWithTimezoneFormat(String dateTimeFormat) { - loggerExternal.entering(loggerClassName, "setTimestampWithTimezoneFormat", dateTimeFormat); - - this.dateTimeFormatter = DateTimeFormatter.ofPattern(dateTimeFormat); - - loggerExternal.exiting(loggerClassName, "setTimestampWithTimezoneFormat"); } /** @@ -229,11 +152,6 @@ public void setTimestampWithTimezoneFormat(String dateTimeFormat) { * format to parse data sent as java.sql.Types.TIMESTAMP_WITH_TIMEZONE */ public void setTimestampWithTimezoneFormat(DateTimeFormatter dateTimeFormatter) { - loggerExternal.entering(loggerClassName, "setTimestampWithTimezoneFormat", new Object[] {dateTimeFormatter}); - - this.dateTimeFormatter = dateTimeFormatter; - - loggerExternal.exiting(loggerClassName, "setTimestampWithTimezoneFormat"); } /** @@ -243,11 +161,6 @@ public void setTimestampWithTimezoneFormat(DateTimeFormatter dateTimeFormatter) * format to parse data sent as java.sql.Types.TIME_WITH_TIMEZONE */ public void setTimeWithTimezoneFormat(String timeFormat) { - loggerExternal.entering(loggerClassName, "setTimeWithTimezoneFormat", timeFormat); - - this.timeFormatter = DateTimeFormatter.ofPattern(timeFormat); - - loggerExternal.exiting(loggerClassName, "setTimeWithTimezoneFormat"); } /** @@ -257,11 +170,6 @@ public void setTimeWithTimezoneFormat(String timeFormat) { * format to parse data sent as java.sql.Types.TIME_WITH_TIMEZONE */ public void setTimeWithTimezoneFormat(DateTimeFormatter dateTimeFormatter) { - loggerExternal.entering(loggerClassName, "setTimeWithTimezoneFormat", new Object[] {dateTimeFormatter}); - - this.timeFormatter = dateTimeFormatter; - - loggerExternal.exiting(loggerClassName, "setTimeWithTimezoneFormat"); } /* diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java index 9e02e4d90..859186758 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java @@ -125,6 +125,8 @@ public boolean getUseBulkCopyForBatchInsert() throws SQLServerException { /** Sets the prepared statement's useBulkCopyForBatchInsert value. * + * @param useBulkCopyForBatchInsert + * the boolean value * @throws SQLServerException when an error occurs */ public void setUseBulkCopyForBatchInsert(boolean useBulkCopyForBatchInsert) throws SQLServerException { @@ -2558,8 +2560,8 @@ public int[] executeBatch() throws SQLServerException, BatchUpdateException, SQL // throw a BatchUpdateException with the given error message, and return null for the updateCounts. throw new BatchUpdateException(e.getMessage(), null, 0, null); } - catch (Exception e) { - // If we fail with non-SQLException, fall back to the original batch insert logic. + catch (IllegalArgumentException e) { + // If we fail with IllegalArgumentException, fall back to the original batch insert logic. if (getStatementLogger().isLoggable(java.util.logging.Level.FINE)) { getStatementLogger().fine("Parsing user's Batch Insert SQL Query failed: " + e.toString()); getStatementLogger().fine("Falling back to the original implementation for Batch Insert."); @@ -2622,6 +2624,95 @@ public long[] executeLargeBatch() throws SQLServerException, BatchUpdateExceptio discardLastExecutionResults(); long updateCounts[]; + + localUserSQL = userSQL; + + try { + if (isInsert(localUserSQL) && connection.isAzureDW() && (this.useBulkCopyForBatchInsert)) { + // From the JDBC spec, section 9.1.4 - Making Batch Updates: + // The CallableStatement.executeBatch method (inherited from PreparedStatement) will + // throw a BatchUpdateException if the stored procedure returns anything other than an + // update count or takes OUT or INOUT parameters. + // + // Non-update count results (e.g. ResultSets) are treated as individual batch errors + // when they are encountered in the response. + // + // OUT and INOUT parameter checking is done here, before executing the batch. If any + // OUT or INOUT are present, the entire batch fails. + for (Parameter[] paramValues : batchParamValues) { + for (Parameter paramValue : paramValues) { + if (paramValue.isOutput()) { + throw new BatchUpdateException(SQLServerException.getErrString("R_outParamsNotPermittedinBatch"), null, 0, null); + } + } + } + + if (batchParamValues == null) { + updateCounts = new long[0]; + loggerExternal.exiting(getClassNameLogging(), "executeLargeBatch", updateCounts); + return updateCounts; + } + + String tableName = parseUserSQLForTableNameDW(false, false, false, false); + ArrayList columnList = parseUserSQLForColumnListDW(); + ArrayList valueList = parseUserSQLForValueListDW(false); + + String destinationTableName = tableName; + SQLServerStatement stmt = (SQLServerStatement) connection.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, + connection.getHoldability(), stmtColumnEncriptionSetting); + + // Get destination metadata + try (SQLServerResultSet rs = stmt + .executeQueryInternal("sp_executesql N'SET FMTONLY ON SELECT * FROM " + destinationTableName + " '");) { + + SQLServerBulkBatchInsertRecord batchRecord = new SQLServerBulkBatchInsertRecord(batchParamValues, columnList, valueList, null); + + for (int i = 1; i <= rs.getColumnCount(); i++) { + Column c = rs.getColumn(i); + CryptoMetadata cryptoMetadata = c.getCryptoMetadata(); + int jdbctype; + TypeInfo ti = c.getTypeInfo(); + if (null != cryptoMetadata) { + jdbctype = cryptoMetadata.getBaseTypeInfo().getSSType().getJDBCType().getIntValue(); + } + else { + jdbctype = ti.getSSType().getJDBCType().getIntValue(); + } + batchRecord.addColumnMetadata(i, c.getColumnName(), jdbctype, ti.getPrecision(), ti.getScale()); + } + + SQLServerBulkCopy bcOperation = new SQLServerBulkCopy(connection); + bcOperation.setDestinationTableName(tableName); + bcOperation.setStmtColumnEncriptionSetting(this.getStmtColumnEncriptionSetting()); + bcOperation.setDestinationTableMetadata(rs); + bcOperation.writeToServer((ISQLServerBulkRecord) batchRecord); + bcOperation.close(); + updateCounts = new long[batchParamValues.size()]; + for (int i = 0; i < batchParamValues.size(); ++i) { + updateCounts[i] = 1; + } + + batchParamValues = null; + loggerExternal.exiting(getClassNameLogging(), "executeLargeBatch", updateCounts); + return updateCounts; + } + finally { + if (null != stmt) + stmt.close(); + } + } + } + catch (SQLException e) { + // throw a BatchUpdateException with the given error message, and return null for the updateCounts. + throw new BatchUpdateException(e.getMessage(), null, 0, null); + } + catch (IllegalArgumentException e) { + // If we fail with IllegalArgumentException, fall back to the original batch insert logic. + if (getStatementLogger().isLoggable(java.util.logging.Level.FINE)) { + getStatementLogger().fine("Parsing user's Batch Insert SQL Query failed: " + e.toString()); + getStatementLogger().fine("Falling back to the original implementation for Batch Insert."); + } + } if (batchParamValues == null) updateCounts = new long[0]; diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyTest.java index 843056d66..21169d188 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyTest.java @@ -417,6 +417,65 @@ public void testColumnNameMixAgainstDB() throws Exception { assertEquals(rs.getObject(1), 1); } + @Test + public void testAlColumnsLargeBatch() throws Exception { + Field f1 = SQLServerConnection.class.getDeclaredField("isAzureDW"); + f1.setAccessible(true); + f1.set(connection, true); + + String valid = "INSERT INTO " + tableName + " values " + + "(" + + "?, " + + "?, " + + "?, " + + "?, " + + "?, " + + "?, " + + "?, " + + "?, " + + "?, " + + ")"; + + pstmt = (SQLServerPreparedStatement) connection.prepareStatement(valid); + stmt = (SQLServerStatement) connection.createStatement(); + + Timestamp myTimestamp = new Timestamp(114550L); + + Date d = new Date(114550L); + + pstmt.setInt(1, 1234); + pstmt.setBoolean(2, false); + pstmt.setString(3, "a"); + pstmt.setDate(4, d); + pstmt.setDateTime(5, myTimestamp); + pstmt.setFloat(6, (float) 123.45); + pstmt.setString(7, "b"); + pstmt.setString(8, "varc"); + pstmt.setString(9, "''"); + pstmt.addBatch(); + + pstmt.executeLargeBatch(); + + ResultSet rs = stmt.executeQuery("SELECT * FROM " + tableName); + + Object[] expected = new Object[9]; + + expected[0] = 1234; + expected[1] = false; + expected[2] = "a"; + expected[3] = d; + expected[4] = myTimestamp; + expected[5] = 123.45; + expected[6] = "b"; + expected[7] = "varc"; + expected[8] = "''"; + + rs.next(); + for (int i=0; i < expected.length; i++) { + assertEquals(rs.getObject(i + 1).toString(), expected[i].toString()); + } + } + @BeforeEach public void testSetup() throws TestAbortedException, Exception { connection = DriverManager.getConnection(connectionString + ";useBulkCopyForBatchInsert=true;"); From 66c0b193346a5bd4a911a445c53966fbbd4aa8be Mon Sep 17 00:00:00 2001 From: Peter Bae Date: Fri, 22 Jun 2018 16:22:54 -0700 Subject: [PATCH 28/32] add more tests, make the prepared statement property go away --- .../jdbc/SQLServerPreparedStatement.java | 6 ++- .../BatchExecutionWithBulkCopyTest.java | 50 +++++++++++++++++++ .../preparedStatement/RegressionTest.java | 2 +- 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java index 859186758..ac6289f2f 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java @@ -118,7 +118,8 @@ private void setPreparedStatementHandle(int handle) { * Per the description. * @throws SQLServerException when an error occurs */ - public boolean getUseBulkCopyForBatchInsert() throws SQLServerException { + @SuppressWarnings("unused") + private boolean getUseBulkCopyForBatchInsert() throws SQLServerException { checkClosed(); return useBulkCopyForBatchInsert; } @@ -129,7 +130,8 @@ public boolean getUseBulkCopyForBatchInsert() throws SQLServerException { * the boolean value * @throws SQLServerException when an error occurs */ - public void setUseBulkCopyForBatchInsert(boolean useBulkCopyForBatchInsert) throws SQLServerException { + @SuppressWarnings("unused") + private void setUseBulkCopyForBatchInsert(boolean useBulkCopyForBatchInsert) throws SQLServerException { checkClosed(); this.useBulkCopyForBatchInsert = useBulkCopyForBatchInsert; } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyTest.java index 21169d188..f18d7df39 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyTest.java @@ -319,6 +319,56 @@ public void testNullOrEmptyColumns() throws Exception { } } + @Test + public void testAllFilledColumns() throws Exception { + Field f1 = SQLServerConnection.class.getDeclaredField("isAzureDW"); + f1.setAccessible(true); + f1.set(connection, true); + + String valid = "INSERT INTO " + tableName + " values " + + "(" + + "1234, " + + "false, " + + "a, " + + "null, " + + "null, " + + "123.45, " + + "b, " + + "varc, " + + "sadf, " + + ")"; + + pstmt = (SQLServerPreparedStatement) connection.prepareStatement(valid); + stmt = (SQLServerStatement) connection.createStatement(); + + Timestamp myTimestamp = new Timestamp(114550L); + + Date d = new Date(114550L); + + pstmt.addBatch(); + + pstmt.executeBatch(); + + ResultSet rs = stmt.executeQuery("SELECT * FROM " + tableName); + + Object[] expected = new Object[9]; + + expected[0] = 1234; + expected[1] = false; + expected[2] = "a"; + expected[3] = null; + expected[4] = null; + expected[5] = 123.45; + expected[6] = "b"; + expected[7] = "varc"; + expected[8] = "sadf"; + + rs.next(); + for (int i=0; i < expected.length; i++) { + assertEquals(rs.getObject(i + 1), expected[i]); + } + } + @Test public void testSquareBracketAgainstDB() throws Exception { Field f1 = SQLServerConnection.class.getDeclaredField("isAzureDW"); diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/RegressionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/RegressionTest.java index 7a18ae0fe..ec7342fb9 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/RegressionTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/RegressionTest.java @@ -320,6 +320,7 @@ public void batchWithLargeStringTest() throws SQLException { @Test public void batchWithLargeStringTestUseBulkCopyAPI() throws SQLException { + Connection con = DriverManager.getConnection(connectionString + ";useBulkCopyForBatchInsert=true;"); Statement stmt = con.createStatement(); SQLServerPreparedStatement pstmt = null; ResultSet rs = null; @@ -359,7 +360,6 @@ public void batchWithLargeStringTestUseBulkCopyAPI() throws SQLException { f1.setAccessible(true); f1.set(con, true); pstmt = (SQLServerPreparedStatement) con.prepareStatement("insert into " + testTable + " values (?,?)"); - pstmt.setUseBulkCopyForBatchInsert(true); // 0,a pstmt.setInt(1, 0); pstmt.setNString(2, values[0]); From 2369bb22f4b9e3a1b4b30df04a888fd7e1ebc456 Mon Sep 17 00:00:00 2001 From: Peter Bae Date: Mon, 25 Jun 2018 13:05:02 -0700 Subject: [PATCH 29/32] Added getter/setter public for the useBulkCopyForBatchInsert connection property. --- .../sqlserver/jdbc/SQLServerConnection.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index 565a2cade..c632bb9be 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -499,10 +499,22 @@ final int getSocketTimeoutMilliseconds() { */ private boolean useBulkCopyForBatchInsert; - final boolean getUseBulkCopyForBatchInsert() { + /** + * Retrieves the useBulkCopyForBatchInsert value. + * @return flag for using Bulk Copy API for batch insert operations. + */ + public boolean getUseBulkCopyForBatchInsert() { return useBulkCopyForBatchInsert; } + /** + * Specifies the flag for using Bulk Copy API for batch insert operations. + * @param useBulkCopyForBatchInsert boolean value for useBulkCopyForBatchInsert. + */ + public void setUseBulkCopyForBatchInsert(boolean useBulkCopyForBatchInsert) { + this.useBulkCopyForBatchInsert = useBulkCopyForBatchInsert; + } + boolean userSetTNIR = true; private boolean sendTimeAsDatetime = SQLServerDriverBooleanProperty.SEND_TIME_AS_DATETIME.getDefaultValue(); From bae637c2bcdc73a6993e53c334ca5653618cf73c Mon Sep 17 00:00:00 2001 From: Peter Bae Date: Mon, 25 Jun 2018 15:29:31 -0700 Subject: [PATCH 30/32] Change implementation of child classes a bit --- .../sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java | 8 ++++---- .../sqlserver/jdbc/SQLServerBulkCSVFileRecord.java | 8 ++++---- .../com/microsoft/sqlserver/jdbc/SQLServerBulkCommon.java | 5 ++++- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java index 8df463d63..3bd33db7d 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkBatchInsertRecord.java @@ -383,7 +383,7 @@ else if ((null != columnNames) && (columnNames.length >= positionInSource)) public void setTimestampWithTimezoneFormat(String dateTimeFormat) { loggerExternal.entering(loggerClassName, "setTimestampWithTimezoneFormat", dateTimeFormat); - this.dateTimeFormatter = DateTimeFormatter.ofPattern(dateTimeFormat); + super.setTimestampWithTimezoneFormat(dateTimeFormat); loggerExternal.exiting(loggerClassName, "setTimestampWithTimezoneFormat"); } @@ -392,7 +392,7 @@ public void setTimestampWithTimezoneFormat(String dateTimeFormat) { public void setTimestampWithTimezoneFormat(DateTimeFormatter dateTimeFormatter) { loggerExternal.entering(loggerClassName, "setTimestampWithTimezoneFormat", new Object[] {dateTimeFormatter}); - this.dateTimeFormatter = dateTimeFormatter; + super.setTimestampWithTimezoneFormat(dateTimeFormatter); loggerExternal.exiting(loggerClassName, "setTimestampWithTimezoneFormat"); } @@ -401,7 +401,7 @@ public void setTimestampWithTimezoneFormat(DateTimeFormatter dateTimeFormatter) public void setTimeWithTimezoneFormat(String timeFormat) { loggerExternal.entering(loggerClassName, "setTimeWithTimezoneFormat", timeFormat); - this.timeFormatter = DateTimeFormatter.ofPattern(timeFormat); + super.setTimeWithTimezoneFormat(timeFormat); loggerExternal.exiting(loggerClassName, "setTimeWithTimezoneFormat"); } @@ -410,7 +410,7 @@ public void setTimeWithTimezoneFormat(String timeFormat) { public void setTimeWithTimezoneFormat(DateTimeFormatter dateTimeFormatter) { loggerExternal.entering(loggerClassName, "setTimeWithTimezoneFormat", new Object[] {dateTimeFormatter}); - this.timeFormatter = dateTimeFormatter; + super.setTimeWithTimezoneFormat(dateTimeFormatter); loggerExternal.exiting(loggerClassName, "setTimeWithTimezoneFormat"); } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCSVFileRecord.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCSVFileRecord.java index 903a5f653..cfcd447cc 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCSVFileRecord.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCSVFileRecord.java @@ -545,7 +545,7 @@ else if ((null != columnNames) && (columnNames.length >= positionInSource)) public void setTimestampWithTimezoneFormat(String dateTimeFormat) { loggerExternal.entering(loggerClassName, "setTimestampWithTimezoneFormat", dateTimeFormat); - this.dateTimeFormatter = DateTimeFormatter.ofPattern(dateTimeFormat); + super.setTimestampWithTimezoneFormat(dateTimeFormat); loggerExternal.exiting(loggerClassName, "setTimestampWithTimezoneFormat"); } @@ -554,7 +554,7 @@ public void setTimestampWithTimezoneFormat(String dateTimeFormat) { public void setTimestampWithTimezoneFormat(DateTimeFormatter dateTimeFormatter) { loggerExternal.entering(loggerClassName, "setTimestampWithTimezoneFormat", new Object[] {dateTimeFormatter}); - this.dateTimeFormatter = dateTimeFormatter; + super.setTimestampWithTimezoneFormat(dateTimeFormatter); loggerExternal.exiting(loggerClassName, "setTimestampWithTimezoneFormat"); } @@ -563,7 +563,7 @@ public void setTimestampWithTimezoneFormat(DateTimeFormatter dateTimeFormatter) public void setTimeWithTimezoneFormat(String timeFormat) { loggerExternal.entering(loggerClassName, "setTimeWithTimezoneFormat", timeFormat); - this.timeFormatter = DateTimeFormatter.ofPattern(timeFormat); + super.setTimeWithTimezoneFormat(dateTimeFormatter); loggerExternal.exiting(loggerClassName, "setTimeWithTimezoneFormat"); } @@ -572,7 +572,7 @@ public void setTimeWithTimezoneFormat(String timeFormat) { public void setTimeWithTimezoneFormat(DateTimeFormatter dateTimeFormatter) { loggerExternal.entering(loggerClassName, "setTimeWithTimezoneFormat", new Object[] {dateTimeFormatter}); - this.timeFormatter = dateTimeFormatter; + super.setTimeWithTimezoneFormat(dateTimeFormatter); loggerExternal.exiting(loggerClassName, "setTimeWithTimezoneFormat"); } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCommon.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCommon.java index 0d45af6a3..383b4c793 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCommon.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCommon.java @@ -143,6 +143,7 @@ void addColumnMetadataInternal(int positionInSource, * format to parse data sent as java.sql.Types.TIMESTAMP_WITH_TIMEZONE */ public void setTimestampWithTimezoneFormat(String dateTimeFormat) { + this.dateTimeFormatter = DateTimeFormatter.ofPattern(dateTimeFormat); } /** @@ -152,6 +153,7 @@ public void setTimestampWithTimezoneFormat(String dateTimeFormat) { * format to parse data sent as java.sql.Types.TIMESTAMP_WITH_TIMEZONE */ public void setTimestampWithTimezoneFormat(DateTimeFormatter dateTimeFormatter) { + this.dateTimeFormatter = dateTimeFormatter; } /** @@ -161,6 +163,7 @@ public void setTimestampWithTimezoneFormat(DateTimeFormatter dateTimeFormatter) * format to parse data sent as java.sql.Types.TIME_WITH_TIMEZONE */ public void setTimeWithTimezoneFormat(String timeFormat) { + this.timeFormatter = DateTimeFormatter.ofPattern(timeFormat); } /** @@ -170,6 +173,7 @@ public void setTimeWithTimezoneFormat(String timeFormat) { * format to parse data sent as java.sql.Types.TIME_WITH_TIMEZONE */ public void setTimeWithTimezoneFormat(DateTimeFormatter dateTimeFormatter) { + this.timeFormatter = dateTimeFormatter; } /* @@ -195,7 +199,6 @@ protected void checkDuplicateColumnName(int positionInTable, throw new SQLServerException(SQLServerException.getErrString("R_BulkDataDuplicateColumn"), null); } } - } } } From b8afb58ec497b2563517beb9f9cc40cc799eae4a Mon Sep 17 00:00:00 2001 From: Peter Bae Date: Tue, 26 Jun 2018 15:05:19 -0700 Subject: [PATCH 31/32] Fix bamboo problem + refactor test code --- .../jdbc/SQLServerBulkCSVFileRecord.java | 2 +- .../preparedStatement/RegressionTest.java | 134 +- .../statement/BatchExecuteWithErrorsTest.java | 1086 ++++++----------- .../unit/statement/BatchExecutionTest.java | 216 +--- .../unit/statement/PreparedStatementTest.java | 506 +++----- 5 files changed, 616 insertions(+), 1328 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCSVFileRecord.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCSVFileRecord.java index cfcd447cc..019046ab6 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCSVFileRecord.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCSVFileRecord.java @@ -563,7 +563,7 @@ public void setTimestampWithTimezoneFormat(DateTimeFormatter dateTimeFormatter) public void setTimeWithTimezoneFormat(String timeFormat) { loggerExternal.entering(loggerClassName, "setTimeWithTimezoneFormat", timeFormat); - super.setTimeWithTimezoneFormat(dateTimeFormatter); + super.setTimeWithTimezoneFormat(timeFormat); loggerExternal.exiting(loggerClassName, "setTimeWithTimezoneFormat"); } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/RegressionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/RegressionTest.java index ec7342fb9..ce51ee8e8 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/RegressionTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/RegressionTest.java @@ -21,6 +21,7 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.platform.runner.JUnitPlatform; import org.junit.runner.RunWith; @@ -49,7 +50,7 @@ public class RegressionTest extends AbstractTest { * * @throws SQLException */ - @BeforeAll + @BeforeEach public static void setupTest() throws SQLException { con = DriverManager.getConnection(connectionString); Statement stmt = con.createStatement(); @@ -215,7 +216,19 @@ public void grantTest() throws SQLException { * @throws SQLException */ @Test - public void batchWithLargeStringTest() throws SQLException { + public void batchWithLargeStringTest() throws Exception { + batchWithLargeStringTestInternal("BatchInsert"); + } + + @Test + public void batchWithLargeStringTestUseBulkCopyAPI() throws Exception { + batchWithLargeStringTestInternal("BulkCopy"); + } + + private void batchWithLargeStringTestInternal(String mode) throws Exception { + if (mode.equalsIgnoreCase("bulkcopy")) { + modifyConnectionForBulkCopyAPI((SQLServerConnection) con); + } Statement stmt = con.createStatement(); PreparedStatement pstmt = null; ResultSet rs = null; @@ -315,118 +328,8 @@ public void batchWithLargeStringTest() throws SQLException { stmt.close(); } } - } - @Test - public void batchWithLargeStringTestUseBulkCopyAPI() throws SQLException { - Connection con = DriverManager.getConnection(connectionString + ";useBulkCopyForBatchInsert=true;"); - Statement stmt = con.createStatement(); - SQLServerPreparedStatement pstmt = null; - ResultSet rs = null; - String testTable = "TEST_TABLE_BULK_COPY"; - Utils.dropTableIfExists(testTable, stmt); - - con.setAutoCommit(false); - - // create a table with two columns - boolean createPrimaryKey = false; - try { - stmt.execute("if object_id('" + testTable + "', 'U') is not null\ndrop table " + testTable + ";"); - if (createPrimaryKey) { - stmt.execute("create table " + testTable + " ( ID int, DATA nvarchar(max), primary key (ID) );"); - } - else { - stmt.execute("create table " + testTable + " ( ID int, DATA nvarchar(max) );"); - } - } - catch (Exception e) { - fail(e.toString()); - } - - con.commit(); - - // build a String with 4001 characters - StringBuilder stringBuilder = new StringBuilder(); - for (int i = 0; i < 4001; i++) { - stringBuilder.append('c'); - } - String largeString = stringBuilder.toString(); - - String[] values = {"a", "b", largeString, "d", "e"}; - // insert five rows into the table; use a batch for each row - try { - Field f1 = SQLServerConnection.class.getDeclaredField("isAzureDW"); - f1.setAccessible(true); - f1.set(con, true); - pstmt = (SQLServerPreparedStatement) con.prepareStatement("insert into " + testTable + " values (?,?)"); - // 0,a - pstmt.setInt(1, 0); - pstmt.setNString(2, values[0]); - pstmt.addBatch(); - - // 1,b - pstmt.setInt(1, 1); - pstmt.setNString(2, values[1]); - pstmt.addBatch(); - - // 2,ccc... - pstmt.setInt(1, 2); - pstmt.setNString(2, values[2]); - pstmt.addBatch(); - - // 3,d - pstmt.setInt(1, 3); - pstmt.setNString(2, values[3]); - pstmt.addBatch(); - - // 4,e - pstmt.setInt(1, 4); - pstmt.setNString(2, values[4]); - pstmt.addBatch(); - - pstmt.executeBatch(); - } - catch (Exception e) { - fail(e.toString()); - } - - connection.commit(); - - // check the data in the table - Map selectedValues = new LinkedHashMap<>(); - int id = 0; - try { - pstmt = (SQLServerPreparedStatement) con.prepareStatement("select * from " + testTable + ";"); - try { - rs = pstmt.executeQuery(); - int i = 0; - while (rs.next()) { - id = rs.getInt(1); - String data = rs.getNString(2); - if (selectedValues.containsKey(id)) { - fail("Found duplicate id: " + id + " ,actual values is : " + values[i++] + " data is: " + data); - } - selectedValues.put(id, data); - } - } - finally { - if (null != rs) { - rs.close(); - } - } - } - finally { - Utils.dropTableIfExists(testTable, stmt); - if (null != pstmt) { - pstmt.close(); - } - if (null != stmt) { - stmt.close(); - } - } - - } /** * Test with large string and tests with more batch queries * @@ -547,4 +450,11 @@ public static void cleanup() throws SQLException { } + private void modifyConnectionForBulkCopyAPI(SQLServerConnection con) throws Exception { + Field f1 = SQLServerConnection.class.getDeclaredField("isAzureDW"); + f1.setAccessible(true); + f1.set(con, true); + + con.setUseBulkCopyForBatchInsert(true); + } } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/BatchExecuteWithErrorsTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/BatchExecuteWithErrorsTest.java index 749b41b91..e3659adfc 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/BatchExecuteWithErrorsTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/BatchExecuteWithErrorsTest.java @@ -55,233 +55,38 @@ public class BatchExecuteWithErrorsTest extends AbstractTest { /** * Batch test - * - * @throws SQLException + * @throws Exception */ @Test @DisplayName("Batch Test") - public void Repro47239() throws SQLException { - String tableN = RandomUtil.getIdentifier("t_Repro47239"); - final String tableName = AbstractSQLGenerator.escapeIdentifier(tableN); - final String insertStmt = "INSERT INTO " + tableName + " VALUES (999, 'HELLO', '4/12/1994')"; - final String error16 = "RAISERROR ('raiserror level 16',16,42)"; - final String select = "SELECT 1"; - final String dateConversionError = "insert into " + tableName + " values (999999, 'Hello again', 'asdfasdf')"; - - String warning; - String error; - String severe; - con = DriverManager.getConnection(connectionString); - if (DBConnection.isSqlAzure(con)) { - // SQL Azure will throw exception for "raiserror WITH LOG", so the following RAISERROR statements have not "with log" option - warning = "RAISERROR ('raiserror level 4',4,1)"; - error = "RAISERROR ('raiserror level 11',11,1)"; - // On SQL Azure, raising FATAL error by RAISERROR() is not supported and there is no way to - // cut the current connection by a statement inside a SQL batch. - // Details: Although one can simulate a fatal error (that cuts the connections) by dropping the database, - // this simulation cannot be written entirely in TSQL (because it needs a new connection), - // and thus it cannot be put into a TSQL batch and it is useless here. - // So we have to skip the last scenario of this test case, i.e. "Test Severe (connection-closing) errors" - // It is worthwhile to still execute the first 5 test scenarios of this test case, in order to have best test coverage. - severe = "--Not executed when testing against SQL Azure"; // this is a dummy statement that never being executed on SQL Azure - } - else { - warning = "RAISERROR ('raiserror level 4',4,1) WITH LOG"; - error = "RAISERROR ('raiserror level 11',11,1) WITH LOG"; - severe = "RAISERROR ('raiserror level 20',20,1) WITH LOG"; - } - con.close(); - - int[] actualUpdateCounts; - int[] expectedUpdateCounts; - String actualExceptionText; - - // SQL Server 2005 driver - try { - Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver"); - } - catch (ClassNotFoundException e1) { - fail(e1.toString()); - } - Connection conn = DriverManager.getConnection(connectionString); - Statement stmt = conn.createStatement(); - try { - stmt.executeUpdate("drop table " + tableName); - } - catch (Exception ignored) { - } - stmt.executeUpdate( - "create table " + tableName + " (c1_int int, c2_varchar varchar(20), c3_date datetime, c4_int int identity(1,1) primary key)"); - - // Regular Statement batch update - expectedUpdateCounts = new int[] {1, -2, 1, -2, 1, -2}; - Statement batchStmt = conn.createStatement(); - batchStmt.addBatch(insertStmt); - batchStmt.addBatch(warning); - batchStmt.addBatch(insertStmt); - batchStmt.addBatch(warning); - batchStmt.addBatch(insertStmt); - batchStmt.addBatch(warning); - try { - actualUpdateCounts = batchStmt.executeBatch(); - actualExceptionText = ""; - } - catch (BatchUpdateException bue) { - actualUpdateCounts = bue.getUpdateCounts(); - actualExceptionText = bue.getMessage(); - if (log.isLoggable(Level.FINE)) { - log.fine("BatchUpdateException occurred. Message:" + actualExceptionText); - } - } - finally { - batchStmt.close(); - } - if (log.isLoggable(Level.FINE)) { - log.fine("UpdateCounts:"); - } - for (int updateCount : actualUpdateCounts) { - log.fine("" + updateCount + ","); - } - log.fine(""); - assertTrue(Arrays.equals(actualUpdateCounts, expectedUpdateCounts), TestResource.getResource("R_testInterleaved")); - - expectedUpdateCounts = new int[] {-3, 1, 1, 1}; - stmt.addBatch(error); - stmt.addBatch(insertStmt); - stmt.addBatch(insertStmt); - stmt.addBatch(insertStmt); - try { - actualUpdateCounts = stmt.executeBatch(); - actualExceptionText = ""; - } - catch (BatchUpdateException bue) { - actualUpdateCounts = bue.getUpdateCounts(); - actualExceptionText = bue.getMessage(); - } - log.fine("UpdateCounts:"); - for (int updateCount : actualUpdateCounts) { - log.fine("" + updateCount + ","); - } - log.fine(""); - assertTrue(Arrays.equals(actualUpdateCounts, expectedUpdateCounts), TestResource.getResource("R_errorFollowInserts")); - // 50280 - expectedUpdateCounts = new int[] {1, -3}; - stmt.addBatch(insertStmt); - stmt.addBatch(error16); - try { - actualUpdateCounts = stmt.executeBatch(); - actualExceptionText = ""; - } - catch (BatchUpdateException bue) { - actualUpdateCounts = bue.getUpdateCounts(); - actualExceptionText = bue.getMessage(); - } - for (int updateCount : actualUpdateCounts) { - log.fine("" + updateCount + ","); - } - log.fine(""); - assertTrue(Arrays.equals(actualUpdateCounts, expectedUpdateCounts), TestResource.getResource("R_errorFollow50280")); - - // Test "soft" errors - conn.setAutoCommit(false); - stmt.addBatch(select); - stmt.addBatch(insertStmt); - stmt.addBatch(select); - stmt.addBatch(insertStmt); - try { - stmt.executeBatch(); - // Soft error test: executeBatch unexpectedly succeeded - assertEquals(true, false, TestResource.getResource("R_shouldThrowException")); - } - catch (BatchUpdateException bue) { - assertEquals("A result set was generated for update.", bue.getMessage(), TestResource.getResource("R_unexpectedExceptionContent")); - assertEquals(Arrays.equals(bue.getUpdateCounts(), new int[] {-3, 1, -3, 1}), true, - TestResource.getResource("R_incorrectUpdateCount")); - } - conn.rollback(); - - // Defect 128801: Rollback (with conversion error) should throw SQLException - stmt.addBatch(dateConversionError); - stmt.addBatch(insertStmt); - stmt.addBatch(insertStmt); - stmt.addBatch(insertStmt); - try { - stmt.executeBatch(); - } - catch (BatchUpdateException bue) { - assertThat(bue.getMessage(), containsString(TestResource.getResource("R_syntaxErrorDateConvert"))); - // CTestLog.CompareStartsWith(bue.getMessage(), "Syntax error converting date", "Transaction rollback with conversion error threw wrong - // BatchUpdateException"); - } - catch (SQLException e) { - assertThat(e.getMessage(), containsString(TestResource.getResource("R_dateConvertError"))); - // CTestLog.CompareStartsWith(e.getMessage(), "Conversion failed when converting date", "Transaction rollback with conversion error threw - // wrong SQLException"); - } - - conn.setAutoCommit(true); - - // On SQL Azure, raising FATAL error by RAISERROR() is not supported and there is no way to - // cut the current connection by a statement inside a SQL batch. - // Details: Although one can simulate a fatal error (that cuts the connections) by dropping the database, - // this simulation cannot be written entirely in TSQL (because it needs a new connection), - // and thus it cannot be put into a TSQL batch and it is useless here. - // So we have to skip the last scenario of this test case, i.e. "Test Severe (connection-closing) errors" - // It is worthwhile to still execute the first 5 test scenarios of this test case, in order to have best test coverage. - if (!DBConnection.isSqlAzure(conn)) { - // Test Severe (connection-closing) errors - stmt.addBatch(error); - stmt.addBatch(insertStmt); - stmt.addBatch(warning); - // TODO Removed until ResultSet refactoring task (45832) is complete. - // stmt.addBatch(select); // error: select not permitted in batch - stmt.addBatch(insertStmt); - stmt.addBatch(severe); - stmt.addBatch(insertStmt); - stmt.addBatch(insertStmt); - try { - stmt.executeBatch(); - // Test fatal errors batch execution succeeded (should have failed) - assertEquals(false, true, TestResource.getResource("R_shouldThrowException")); - } - catch (BatchUpdateException bue) { - // Test fatal errors returned BatchUpdateException rather than SQLException - assertEquals(false, true, TestResource.getResource("R_unexpectedException") + bue.getMessage()); - - } - catch (SQLException e) { - actualExceptionText = e.getMessage(); - - if (actualExceptionText.endsWith("reset")) { - assertTrue(actualExceptionText.equalsIgnoreCase("Connection reset"), TestResource.getResource("R_unexpectedExceptionContent") + ": " + actualExceptionText); - } - else { - assertTrue(actualExceptionText.equalsIgnoreCase("raiserror level 20"), TestResource.getResource("R_unexpectedExceptionContent") + ": " + actualExceptionText); - } - } - } - - try { - stmt.executeUpdate("drop table " + tableName); - } - catch (Exception ignored) { - } - stmt.close(); - conn.close(); + public void Repro47239() throws Exception { + Repro47239Internal("BatchInsert"); + } + + @Test + @DisplayName("Batch Test using bulk copy API") + public void Repro47239UseBulkCopyAPI() throws Exception { + Repro47239Internal("BulkCopy"); } /** - * Batch test + * Tests large methods, supported in 42 * - * @throws SQLException - * @throws SecurityException - * @throws NoSuchFieldException - * @throws IllegalAccessException - * @throws IllegalArgumentException + * @throws Exception */ @Test - @DisplayName("Batch Test") - public void Repro47239UseBulkCopyAPI() throws SQLException, NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException { + @DisplayName("Regression test for using 'large' methods") + public void Repro47239large() throws Exception { + Repro47239largeInternal("BatchInsert"); + } + + @Test + @DisplayName("Regression test for using 'large' methods using bulk copy API") + public void Repro47239largeUseBulkCopyAPI() throws Exception { + Repro47239largeInternal("BulkCopy"); + } + + private void Repro47239Internal(String mode) throws Exception { String tableN = RandomUtil.getIdentifier("t_Repro47239"); final String tableName = AbstractSQLGenerator.escapeIdentifier(tableN); final String insertStmt = "INSERT INTO " + tableName + " VALUES (999, 'HELLO', '4/12/1994')"; @@ -293,9 +98,6 @@ public void Repro47239UseBulkCopyAPI() throws SQLException, NoSuchFieldException String error; String severe; con = DriverManager.getConnection(connectionString); - Field f1 = SQLServerConnection.class.getDeclaredField("isAzureDW"); - f1.setAccessible(true); - f1.set(con, true); if (DBConnection.isSqlAzure(con)) { // SQL Azure will throw exception for "raiserror WITH LOG", so the following RAISERROR statements have not "with log" option warning = "RAISERROR ('raiserror level 4',4,1)"; @@ -327,177 +129,179 @@ public void Repro47239UseBulkCopyAPI() throws SQLException, NoSuchFieldException catch (ClassNotFoundException e1) { fail(e1.toString()); } - Connection conn = DriverManager.getConnection(connectionString); - Statement stmt = conn.createStatement(); - try { - stmt.executeUpdate("drop table " + tableName); - } - catch (Exception ignored) { - } - stmt.executeUpdate( - "create table " + tableName + " (c1_int int, c2_varchar varchar(20), c3_date datetime, c4_int int identity(1,1) primary key)"); - - // Regular Statement batch update - expectedUpdateCounts = new int[] {1, -2, 1, -2, 1, -2}; - Statement batchStmt = conn.createStatement(); - batchStmt.addBatch(insertStmt); - batchStmt.addBatch(warning); - batchStmt.addBatch(insertStmt); - batchStmt.addBatch(warning); - batchStmt.addBatch(insertStmt); - batchStmt.addBatch(warning); - try { - actualUpdateCounts = batchStmt.executeBatch(); - actualExceptionText = ""; - } - catch (BatchUpdateException bue) { - actualUpdateCounts = bue.getUpdateCounts(); - actualExceptionText = bue.getMessage(); - if (log.isLoggable(Level.FINE)) { - log.fine("BatchUpdateException occurred. Message:" + actualExceptionText); + try (Connection conn = DriverManager.getConnection(connectionString)) { + if (mode.equalsIgnoreCase("bulkcopy")) { + modifyConnectionForBulkCopyAPI((SQLServerConnection) conn); } - } - finally { - batchStmt.close(); - } - if (log.isLoggable(Level.FINE)) { - log.fine("UpdateCounts:"); - } - for (int updateCount : actualUpdateCounts) { - log.fine("" + updateCount + ","); - } - log.fine(""); - assertTrue(Arrays.equals(actualUpdateCounts, expectedUpdateCounts), TestResource.getResource("R_testInterleaved")); - - expectedUpdateCounts = new int[] {-3, 1, 1, 1}; - stmt.addBatch(error); - stmt.addBatch(insertStmt); - stmt.addBatch(insertStmt); - stmt.addBatch(insertStmt); - try { - actualUpdateCounts = stmt.executeBatch(); - actualExceptionText = ""; - } - catch (BatchUpdateException bue) { - actualUpdateCounts = bue.getUpdateCounts(); - actualExceptionText = bue.getMessage(); - } - log.fine("UpdateCounts:"); - for (int updateCount : actualUpdateCounts) { - log.fine("" + updateCount + ","); - } - log.fine(""); - assertTrue(Arrays.equals(actualUpdateCounts, expectedUpdateCounts), TestResource.getResource("R_errorFollowInserts")); - // 50280 - expectedUpdateCounts = new int[] {1, -3}; - stmt.addBatch(insertStmt); - stmt.addBatch(error16); - try { - actualUpdateCounts = stmt.executeBatch(); - actualExceptionText = ""; - } - catch (BatchUpdateException bue) { - actualUpdateCounts = bue.getUpdateCounts(); - actualExceptionText = bue.getMessage(); - } - for (int updateCount : actualUpdateCounts) { - log.fine("" + updateCount + ","); - } - log.fine(""); - assertTrue(Arrays.equals(actualUpdateCounts, expectedUpdateCounts), TestResource.getResource("R_errorFollow50280")); - - // Test "soft" errors - conn.setAutoCommit(false); - stmt.addBatch(select); - stmt.addBatch(insertStmt); - stmt.addBatch(select); - stmt.addBatch(insertStmt); - try { - stmt.executeBatch(); - assertEquals(true, false, TestResource.getResource("R_shouldThrowException")); - } - catch (BatchUpdateException bue) { - assertEquals("A result set was generated for update.", bue.getMessage(), TestResource.getResource("R_unexpectedExceptionContent")); - assertEquals(Arrays.equals(bue.getUpdateCounts(), new int[] {-3, 1, -3, 1}), true, - TestResource.getResource("R_incorrectUpdateCount")); - } - conn.rollback(); - - // Defect 128801: Rollback (with conversion error) should throw SQLException - stmt.addBatch(dateConversionError); - stmt.addBatch(insertStmt); - stmt.addBatch(insertStmt); - stmt.addBatch(insertStmt); - try { - stmt.executeBatch(); - } - catch (BatchUpdateException bue) { - assertThat(bue.getMessage(), containsString(TestResource.getResource("R_syntaxErrorDateConvert"))); - // CTestLog.CompareStartsWith(bue.getMessage(), "Syntax error converting date", "Transaction rollback with conversion error threw wrong - // BatchUpdateException"); - } - catch (SQLException e) { - assertThat(e.getMessage(), containsString(TestResource.getResource("R_dateConvertError"))); - // CTestLog.CompareStartsWith(e.getMessage(), "Conversion failed when converting date", "Transaction rollback with conversion error threw - // wrong SQLException"); - } + try (Statement stmt = conn.createStatement()) { + + try { + Utils.dropTableIfExists(tableName, stmt); + } + catch (Exception ignored) { + } + stmt.executeUpdate( + "create table " + tableName + " (c1_int int, c2_varchar varchar(20), c3_date datetime, c4_int int identity(1,1) primary key)"); + + // Regular Statement batch update + expectedUpdateCounts = new int[] {1, -2, 1, -2, 1, -2}; + Statement batchStmt = conn.createStatement(); + batchStmt.addBatch(insertStmt); + batchStmt.addBatch(warning); + batchStmt.addBatch(insertStmt); + batchStmt.addBatch(warning); + batchStmt.addBatch(insertStmt); + batchStmt.addBatch(warning); + try { + actualUpdateCounts = batchStmt.executeBatch(); + actualExceptionText = ""; + } + catch (BatchUpdateException bue) { + actualUpdateCounts = bue.getUpdateCounts(); + actualExceptionText = bue.getMessage(); + if (log.isLoggable(Level.FINE)) { + log.fine("BatchUpdateException occurred. Message:" + actualExceptionText); + } + } + finally { + batchStmt.close(); + } + if (log.isLoggable(Level.FINE)) { + log.fine("UpdateCounts:"); + } + for (int updateCount : actualUpdateCounts) { + log.fine("" + updateCount + ","); + } + log.fine(""); + assertTrue(Arrays.equals(actualUpdateCounts, expectedUpdateCounts), TestResource.getResource("R_testInterleaved")); + + expectedUpdateCounts = new int[] {-3, 1, 1, 1}; + stmt.addBatch(error); + stmt.addBatch(insertStmt); + stmt.addBatch(insertStmt); + stmt.addBatch(insertStmt); + try { + actualUpdateCounts = stmt.executeBatch(); + actualExceptionText = ""; + } + catch (BatchUpdateException bue) { + actualUpdateCounts = bue.getUpdateCounts(); + actualExceptionText = bue.getMessage(); + } + log.fine("UpdateCounts:"); + for (int updateCount : actualUpdateCounts) { + log.fine("" + updateCount + ","); + } + log.fine(""); + assertTrue(Arrays.equals(actualUpdateCounts, expectedUpdateCounts), TestResource.getResource("R_errorFollowInserts")); + // 50280 + expectedUpdateCounts = new int[] {1, -3}; + stmt.addBatch(insertStmt); + stmt.addBatch(error16); + try { + actualUpdateCounts = stmt.executeBatch(); + actualExceptionText = ""; + } + catch (BatchUpdateException bue) { + actualUpdateCounts = bue.getUpdateCounts(); + actualExceptionText = bue.getMessage(); + } + for (int updateCount : actualUpdateCounts) { + log.fine("" + updateCount + ","); + } + log.fine(""); + assertTrue(Arrays.equals(actualUpdateCounts, expectedUpdateCounts), TestResource.getResource("R_errorFollow50280")); + + // Test "soft" errors + conn.setAutoCommit(false); + stmt.addBatch(select); + stmt.addBatch(insertStmt); + stmt.addBatch(select); + stmt.addBatch(insertStmt); + try { + stmt.executeBatch(); + // Soft error test: executeBatch unexpectedly succeeded + assertEquals(true, false, TestResource.getResource("R_shouldThrowException")); + } + catch (BatchUpdateException bue) { + assertEquals("A result set was generated for update.", bue.getMessage(), TestResource.getResource("R_unexpectedExceptionContent")); + assertEquals(Arrays.equals(bue.getUpdateCounts(), new int[] {-3, 1, -3, 1}), true, + TestResource.getResource("R_incorrectUpdateCount")); + } + conn.rollback(); + + // Defect 128801: Rollback (with conversion error) should throw SQLException + stmt.addBatch(dateConversionError); + stmt.addBatch(insertStmt); + stmt.addBatch(insertStmt); + stmt.addBatch(insertStmt); + try { + stmt.executeBatch(); + } + catch (BatchUpdateException bue) { + assertThat(bue.getMessage(), containsString(TestResource.getResource("R_syntaxErrorDateConvert"))); + // CTestLog.CompareStartsWith(bue.getMessage(), "Syntax error converting date", "Transaction rollback with conversion error threw wrong + // BatchUpdateException"); + } + catch (SQLException e) { + assertThat(e.getMessage(), containsString(TestResource.getResource("R_dateConvertError"))); + // CTestLog.CompareStartsWith(e.getMessage(), "Conversion failed when converting date", "Transaction rollback with conversion error threw + // wrong SQLException"); + } - conn.setAutoCommit(true); - - // On SQL Azure, raising FATAL error by RAISERROR() is not supported and there is no way to - // cut the current connection by a statement inside a SQL batch. - // Details: Although one can simulate a fatal error (that cuts the connections) by dropping the database, - // this simulation cannot be written entirely in TSQL (because it needs a new connection), - // and thus it cannot be put into a TSQL batch and it is useless here. - // So we have to skip the last scenario of this test case, i.e. "Test Severe (connection-closing) errors" - // It is worthwhile to still execute the first 5 test scenarios of this test case, in order to have best test coverage. - if (!DBConnection.isSqlAzure(conn)) { - // Test Severe (connection-closing) errors - stmt.addBatch(error); - stmt.addBatch(insertStmt); - stmt.addBatch(warning); - // TODO Removed until ResultSet refactoring task (45832) is complete. - // stmt.addBatch(select); // error: select not permitted in batch - stmt.addBatch(insertStmt); - stmt.addBatch(severe); - stmt.addBatch(insertStmt); - stmt.addBatch(insertStmt); - try { - stmt.executeBatch(); - assertEquals(false, true, TestResource.getResource("R_shouldThrowException")); - } - catch (BatchUpdateException bue) { - assertEquals(false, true, TestResource.getResource("R_unexpectedException") + bue.getMessage()); - } - catch (SQLException e) { - actualExceptionText = e.getMessage(); + conn.setAutoCommit(true); + + // On SQL Azure, raising FATAL error by RAISERROR() is not supported and there is no way to + // cut the current connection by a statement inside a SQL batch. + // Details: Although one can simulate a fatal error (that cuts the connections) by dropping the database, + // this simulation cannot be written entirely in TSQL (because it needs a new connection), + // and thus it cannot be put into a TSQL batch and it is useless here. + // So we have to skip the last scenario of this test case, i.e. "Test Severe (connection-closing) errors" + // It is worthwhile to still execute the first 5 test scenarios of this test case, in order to have best test coverage. + if (!DBConnection.isSqlAzure(conn)) { + // Test Severe (connection-closing) errors + stmt.addBatch(error); + stmt.addBatch(insertStmt); + stmt.addBatch(warning); + // TODO Removed until ResultSet refactoring task (45832) is complete. + // stmt.addBatch(select); // error: select not permitted in batch + stmt.addBatch(insertStmt); + stmt.addBatch(severe); + stmt.addBatch(insertStmt); + stmt.addBatch(insertStmt); + try { + stmt.executeBatch(); + // Test fatal errors batch execution succeeded (should have failed) + assertEquals(false, true, TestResource.getResource("R_shouldThrowException")); + } + catch (BatchUpdateException bue) { + // Test fatal errors returned BatchUpdateException rather than SQLException + assertEquals(false, true, TestResource.getResource("R_unexpectedException") + bue.getMessage()); + + } + catch (SQLException e) { + actualExceptionText = e.getMessage(); + + if (actualExceptionText.endsWith("reset")) { + assertTrue(actualExceptionText.equalsIgnoreCase("Connection reset"), TestResource.getResource("R_unexpectedExceptionContent") + ": " + actualExceptionText); + } + else { + assertTrue(actualExceptionText.equalsIgnoreCase("raiserror level 20"), TestResource.getResource("R_unexpectedExceptionContent") + ": " + actualExceptionText); + } + } + } - if (actualExceptionText.endsWith("reset")) { - assertTrue(actualExceptionText.equalsIgnoreCase("Connection reset"), TestResource.getResource("R_unexpectedExceptionContent") + ": " + actualExceptionText); + try { + stmt.executeUpdate("drop table " + tableName); } - else { - assertTrue(actualExceptionText.equalsIgnoreCase("raiserror level 20"), TestResource.getResource("R_unexpectedExceptionContent") + ": " + actualExceptionText); + catch (Exception ignored) { } } - } - try { - stmt.executeUpdate("drop table " + tableName); } - catch (Exception ignored) { - } - stmt.close(); - conn.close(); } - /** - * Tests large methods, supported in 42 - * - * @throws Exception - */ - @Test - @DisplayName("Regression test for using 'large' methods") - public void Repro47239large() throws Exception { + private void Repro47239largeInternal(String mode) throws Exception { assumeTrue("JDBC42".equals(Utils.getConfiguredProperty("JDBC_Version")), TestResource.getResource("R_incompatJDBC")); // the DBConnection for detecting whether the server is SQL Azure or SQL Server. @@ -531,372 +335,180 @@ public void Repro47239large() throws Exception { // SQL Server 2005 driver Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver"); - Connection conn = DriverManager.getConnection(connectionString); - Statement stmt = conn.createStatement(); - - try { - stmt.executeLargeUpdate("drop table " + tableName); - } - catch (Exception ignored) { - } - try { - stmt.executeLargeUpdate( - "create table " + tableName + " (c1_int int, c2_varchar varchar(20), c3_date datetime, c4_int int identity(1,1) primary key)"); - } - catch (Exception ignored) { - } - // Regular Statement batch update - expectedUpdateCounts = new long[] {1, -2, 1, -2, 1, -2}; - Statement batchStmt = conn.createStatement(); - batchStmt.addBatch(insertStmt); - batchStmt.addBatch(warning); - batchStmt.addBatch(insertStmt); - batchStmt.addBatch(warning); - batchStmt.addBatch(insertStmt); - batchStmt.addBatch(warning); - try { - actualUpdateCounts = batchStmt.executeLargeBatch(); - actualExceptionText = ""; - } - catch (BatchUpdateException bue) { - actualUpdateCounts = bue.getLargeUpdateCounts(); - actualExceptionText = bue.getMessage(); - log.fine("BatchUpdateException occurred. Message:" + actualExceptionText); - } - finally { - batchStmt.close(); - } - log.fine("UpdateCounts:"); - for (long updateCount : actualUpdateCounts) { - log.fine("" + updateCount + ","); - } - log.fine(""); - assertTrue(Arrays.equals(actualUpdateCounts, expectedUpdateCounts), TestResource.getResource("R_testInterleaved")); - - expectedUpdateCounts = new long[] {-3, 1, 1, 1}; - stmt.addBatch(error); - stmt.addBatch(insertStmt); - stmt.addBatch(insertStmt); - stmt.addBatch(insertStmt); - try { - actualUpdateCounts = stmt.executeLargeBatch(); - actualExceptionText = ""; - } - catch (BatchUpdateException bue) { - actualUpdateCounts = bue.getLargeUpdateCounts(); - actualExceptionText = bue.getMessage(); - } - log.fine("UpdateCounts:"); - for (long updateCount : actualUpdateCounts) { - log.fine("" + updateCount + ","); - } - log.fine(""); - assertTrue(Arrays.equals(actualUpdateCounts, expectedUpdateCounts), TestResource.getResource("R_errorFollowInserts")); - - // 50280 - expectedUpdateCounts = new long[] {1, -3}; - stmt.addBatch(insertStmt); - stmt.addBatch(error16); - try { - actualUpdateCounts = stmt.executeLargeBatch(); - actualExceptionText = ""; - } - catch (BatchUpdateException bue) { - actualUpdateCounts = bue.getLargeUpdateCounts(); - actualExceptionText = bue.getMessage(); - } - for (long updateCount : actualUpdateCounts) { - log.fine("" + updateCount + ","); - } - log.fine(""); - assertTrue(Arrays.equals(actualUpdateCounts, expectedUpdateCounts), TestResource.getResource("R_errorFollow50280")); - - // Test "soft" errors - conn.setAutoCommit(false); - stmt.addBatch(select); - stmt.addBatch(insertStmt); - stmt.addBatch(select); - stmt.addBatch(insertStmt); - try { - stmt.executeLargeBatch(); - // Soft error test: executeLargeBatch unexpectedly succeeded - assertEquals(false, true, TestResource.getResource("R_shouldThrowException")); - } - catch (BatchUpdateException bue) { - // Soft error test: wrong error message in BatchUpdateException - assertEquals("A result set was generated for update.", bue.getMessage(), TestResource.getResource("R_unexpectedExceptionContent")); - // Soft error test: wrong update counts in BatchUpdateException - assertEquals(Arrays.equals(bue.getLargeUpdateCounts(), new long[] {-3, 1, -3, 1}), true, - TestResource.getResource("R_incorrectUpdateCount")); - } - conn.rollback(); - - // Defect 128801: Rollback (with conversion error) should throw SQLException - stmt.addBatch(dateConversionError); - stmt.addBatch(insertStmt); - stmt.addBatch(insertStmt); - stmt.addBatch(insertStmt); - try { - stmt.executeLargeBatch(); - } - catch (BatchUpdateException bue) { - assertThat(bue.getMessage(), containsString(TestResource.getResource("R_syntaxErrorDateConvert"))); - } - catch (SQLException e) { - assertThat(e.getMessage(), containsString(TestResource.getResource("R_dateConvertError"))); - } - - conn.setAutoCommit(true); - - // On SQL Azure, raising FATAL error by RAISERROR() is not supported and there is no way to - // cut the current connection by a statement inside a SQL batch. - // Details: Although one can simulate a fatal error (that cuts the connections) by dropping the database, - // this simulation cannot be written entirely in TSQL (because it needs a new connection), - // and thus it cannot be put into a TSQL batch and it is useless here. - // So we have to skip the last scenario of this test case, i.e. "Test Severe (connection-closing) errors" - // It is worthwhile to still execute the first 5 test scenarios of this test case, in order to have best test coverage. - if (!DBConnection.isSqlAzure(DriverManager.getConnection(connectionString))) { - // Test Severe (connection-closing) errors - stmt.addBatch(error); - stmt.addBatch(insertStmt); - stmt.addBatch(warning); - - stmt.addBatch(insertStmt); - stmt.addBatch(severe); - stmt.addBatch(insertStmt); - stmt.addBatch(insertStmt); - try { - stmt.executeLargeBatch(); - // Test fatal errors batch execution succeeded (should have failed) - assertEquals(false, true, TestResource.getResource("R_shouldThrowException")); - } - catch (BatchUpdateException bue) { - // Test fatal errors returned BatchUpdateException rather than SQLException - assertEquals(false, true, TestResource.getResource("R_unexpectedException") + bue.getMessage()); + + try (Connection conn = DriverManager.getConnection(connectionString)) { + if (mode.equalsIgnoreCase("bulkcopy")) { + modifyConnectionForBulkCopyAPI((SQLServerConnection) conn); } - catch (SQLException e) { - actualExceptionText = e.getMessage(); + try (Statement stmt = conn.createStatement()) { - if (actualExceptionText.endsWith("reset")) { - assertTrue(actualExceptionText.equalsIgnoreCase("Connection reset"), TestResource.getResource("R_unexpectedExceptionContent") + ": " + actualExceptionText); + try { + Utils.dropTableIfExists(tableName, stmt); + } + catch (Exception ignored) { + } + try { + stmt.executeLargeUpdate( + "create table " + tableName + " (c1_int int, c2_varchar varchar(20), c3_date datetime, c4_int int identity(1,1) primary key)"); + } + catch (Exception ignored) { + } + // Regular Statement batch update + expectedUpdateCounts = new long[] {1, -2, 1, -2, 1, -2}; + Statement batchStmt = conn.createStatement(); + batchStmt.addBatch(insertStmt); + batchStmt.addBatch(warning); + batchStmt.addBatch(insertStmt); + batchStmt.addBatch(warning); + batchStmt.addBatch(insertStmt); + batchStmt.addBatch(warning); + try { + actualUpdateCounts = batchStmt.executeLargeBatch(); + actualExceptionText = ""; + } + catch (BatchUpdateException bue) { + actualUpdateCounts = bue.getLargeUpdateCounts(); + actualExceptionText = bue.getMessage(); + log.fine("BatchUpdateException occurred. Message:" + actualExceptionText); + } + finally { + batchStmt.close(); + } + log.fine("UpdateCounts:"); + for (long updateCount : actualUpdateCounts) { + log.fine("" + updateCount + ","); + } + log.fine(""); + assertTrue(Arrays.equals(actualUpdateCounts, expectedUpdateCounts), TestResource.getResource("R_testInterleaved")); + + expectedUpdateCounts = new long[] {-3, 1, 1, 1}; + stmt.addBatch(error); + stmt.addBatch(insertStmt); + stmt.addBatch(insertStmt); + stmt.addBatch(insertStmt); + try { + actualUpdateCounts = stmt.executeLargeBatch(); + actualExceptionText = ""; + } + catch (BatchUpdateException bue) { + actualUpdateCounts = bue.getLargeUpdateCounts(); + actualExceptionText = bue.getMessage(); + } + log.fine("UpdateCounts:"); + for (long updateCount : actualUpdateCounts) { + log.fine("" + updateCount + ","); + } + log.fine(""); + assertTrue(Arrays.equals(actualUpdateCounts, expectedUpdateCounts), TestResource.getResource("R_errorFollowInserts")); + + // 50280 + expectedUpdateCounts = new long[] {1, -3}; + stmt.addBatch(insertStmt); + stmt.addBatch(error16); + try { + actualUpdateCounts = stmt.executeLargeBatch(); + actualExceptionText = ""; + } + catch (BatchUpdateException bue) { + actualUpdateCounts = bue.getLargeUpdateCounts(); + actualExceptionText = bue.getMessage(); + } + for (long updateCount : actualUpdateCounts) { + log.fine("" + updateCount + ","); + } + log.fine(""); + assertTrue(Arrays.equals(actualUpdateCounts, expectedUpdateCounts), TestResource.getResource("R_errorFollow50280")); + + // Test "soft" errors + conn.setAutoCommit(false); + stmt.addBatch(select); + stmt.addBatch(insertStmt); + stmt.addBatch(select); + stmt.addBatch(insertStmt); + try { + stmt.executeLargeBatch(); + // Soft error test: executeLargeBatch unexpectedly succeeded + assertEquals(false, true, TestResource.getResource("R_shouldThrowException")); + } + catch (BatchUpdateException bue) { + // Soft error test: wrong error message in BatchUpdateException + assertEquals("A result set was generated for update.", bue.getMessage(), TestResource.getResource("R_unexpectedExceptionContent")); + // Soft error test: wrong update counts in BatchUpdateException + assertEquals(Arrays.equals(bue.getLargeUpdateCounts(), new long[] {-3, 1, -3, 1}), true, + TestResource.getResource("R_incorrectUpdateCount")); + } + conn.rollback(); + + // Defect 128801: Rollback (with conversion error) should throw SQLException + stmt.addBatch(dateConversionError); + stmt.addBatch(insertStmt); + stmt.addBatch(insertStmt); + stmt.addBatch(insertStmt); + try { + stmt.executeLargeBatch(); + } + catch (BatchUpdateException bue) { + assertThat(bue.getMessage(), containsString(TestResource.getResource("R_syntaxErrorDateConvert"))); + } + catch (SQLException e) { + assertThat(e.getMessage(), containsString(TestResource.getResource("R_dateConvertError"))); } - else { - assertTrue(actualExceptionText.equalsIgnoreCase("raiserror level 20"), TestResource.getResource("R_unexpectedExceptionContent") + ": " + actualExceptionText); + conn.setAutoCommit(true); + + // On SQL Azure, raising FATAL error by RAISERROR() is not supported and there is no way to + // cut the current connection by a statement inside a SQL batch. + // Details: Although one can simulate a fatal error (that cuts the connections) by dropping the database, + // this simulation cannot be written entirely in TSQL (because it needs a new connection), + // and thus it cannot be put into a TSQL batch and it is useless here. + // So we have to skip the last scenario of this test case, i.e. "Test Severe (connection-closing) errors" + // It is worthwhile to still execute the first 5 test scenarios of this test case, in order to have best test coverage. + if (!DBConnection.isSqlAzure(DriverManager.getConnection(connectionString))) { + // Test Severe (connection-closing) errors + stmt.addBatch(error); + stmt.addBatch(insertStmt); + stmt.addBatch(warning); + + stmt.addBatch(insertStmt); + stmt.addBatch(severe); + stmt.addBatch(insertStmt); + stmt.addBatch(insertStmt); + try { + stmt.executeLargeBatch(); + // Test fatal errors batch execution succeeded (should have failed) + assertEquals(false, true, TestResource.getResource("R_shouldThrowException")); + } + catch (BatchUpdateException bue) { + // Test fatal errors returned BatchUpdateException rather than SQLException + assertEquals(false, true, TestResource.getResource("R_unexpectedException") + bue.getMessage()); + } + catch (SQLException e) { + actualExceptionText = e.getMessage(); + + if (actualExceptionText.endsWith("reset")) { + assertTrue(actualExceptionText.equalsIgnoreCase("Connection reset"), TestResource.getResource("R_unexpectedExceptionContent") + ": " + actualExceptionText); + } + else { + assertTrue(actualExceptionText.equalsIgnoreCase("raiserror level 20"), TestResource.getResource("R_unexpectedExceptionContent") + ": " + actualExceptionText); + + } + } } - } - } - try { - stmt.executeLargeUpdate("drop table " + tableName); - } - catch (Exception ignored) { + try { + stmt.executeLargeUpdate("drop table " + tableName); + } + catch (Exception ignored) { + } + } } - stmt.close(); - conn.close(); } - /** - * Tests large methods, supported in 42 - * - * @throws Exception - */ - @Test - @DisplayName("Regression test for using 'large' methods") - public void Repro47239largeUseBulkCopyAPI() throws Exception { - - assumeTrue("JDBC42".equals(Utils.getConfiguredProperty("JDBC_Version")), TestResource.getResource("R_incompatJDBC")); - // the DBConnection for detecting whether the server is SQL Azure or SQL Server. - con = DriverManager.getConnection(connectionString); + private void modifyConnectionForBulkCopyAPI(SQLServerConnection con) throws Exception { Field f1 = SQLServerConnection.class.getDeclaredField("isAzureDW"); f1.setAccessible(true); f1.set(con, true); - final String warning; - final String error; - final String severe; - if (DBConnection.isSqlAzure(con)) { - // SQL Azure will throw exception for "raiserror WITH LOG", so the following RAISERROR statements have not "with log" option - warning = "RAISERROR ('raiserror level 4',4,1)"; - error = "RAISERROR ('raiserror level 11',11,1)"; - // On SQL Azure, raising FATAL error by RAISERROR() is not supported and there is no way to - // cut the current connection by a statement inside a SQL batch. - // Details: Although one can simulate a fatal error (that cuts the connections) by dropping the database, - // this simulation cannot be written entirely in TSQL (because it needs a new connection), - // and thus it cannot be put into a TSQL batch and it is useless here. - // So we have to skip the last scenario of this test case, i.e. "Test Severe (connection-closing) errors" - // It is worthwhile to still execute the first 5 test scenarios of this test case, in order to have best test coverage. - severe = "--Not executed when testing against SQL Azure"; // this is a dummy statement that never being executed on SQL Azure - } - else { - warning = "RAISERROR ('raiserror level 4',4,1) WITH LOG"; - error = "RAISERROR ('raiserror level 11',11,1) WITH LOG"; - severe = "RAISERROR ('raiserror level 20',20,1) WITH LOG"; - } - con.close(); - - long[] actualUpdateCounts; - long[] expectedUpdateCounts; - String actualExceptionText; - - // SQL Server 2005 driver - Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver"); - Connection conn = DriverManager.getConnection(connectionString); - Statement stmt = conn.createStatement(); - - try { - stmt.executeLargeUpdate("drop table " + tableName); - } - catch (Exception ignored) { - } - try { - stmt.executeLargeUpdate( - "create table " + tableName + " (c1_int int, c2_varchar varchar(20), c3_date datetime, c4_int int identity(1,1) primary key)"); - } - catch (Exception ignored) { - } - // Regular Statement batch update - expectedUpdateCounts = new long[] {1, -2, 1, -2, 1, -2}; - Statement batchStmt = conn.createStatement(); - batchStmt.addBatch(insertStmt); - batchStmt.addBatch(warning); - batchStmt.addBatch(insertStmt); - batchStmt.addBatch(warning); - batchStmt.addBatch(insertStmt); - batchStmt.addBatch(warning); - try { - actualUpdateCounts = batchStmt.executeLargeBatch(); - actualExceptionText = ""; - } - catch (BatchUpdateException bue) { - actualUpdateCounts = bue.getLargeUpdateCounts(); - actualExceptionText = bue.getMessage(); - log.fine("BatchUpdateException occurred. Message:" + actualExceptionText); - } - finally { - batchStmt.close(); - } - log.fine("UpdateCounts:"); - for (long updateCount : actualUpdateCounts) { - log.fine("" + updateCount + ","); - } - log.fine(""); - assertTrue(Arrays.equals(actualUpdateCounts, expectedUpdateCounts), TestResource.getResource("R_testInterleaved")); - - expectedUpdateCounts = new long[] {-3, 1, 1, 1}; - stmt.addBatch(error); - stmt.addBatch(insertStmt); - stmt.addBatch(insertStmt); - stmt.addBatch(insertStmt); - try { - actualUpdateCounts = stmt.executeLargeBatch(); - actualExceptionText = ""; - } - catch (BatchUpdateException bue) { - actualUpdateCounts = bue.getLargeUpdateCounts(); - actualExceptionText = bue.getMessage(); - } - log.fine("UpdateCounts:"); - for (long updateCount : actualUpdateCounts) { - log.fine("" + updateCount + ","); - } - log.fine(""); - assertTrue(Arrays.equals(actualUpdateCounts, expectedUpdateCounts), TestResource.getResource("R_errorFollowInserts")); - - // 50280 - expectedUpdateCounts = new long[] {1, -3}; - stmt.addBatch(insertStmt); - stmt.addBatch(error16); - try { - actualUpdateCounts = stmt.executeLargeBatch(); - actualExceptionText = ""; - } - catch (BatchUpdateException bue) { - actualUpdateCounts = bue.getLargeUpdateCounts(); - actualExceptionText = bue.getMessage(); - } - for (long updateCount : actualUpdateCounts) { - log.fine("" + updateCount + ","); - } - log.fine(""); - assertTrue(Arrays.equals(actualUpdateCounts, expectedUpdateCounts), TestResource.getResource("R_errorFollow50280")); - - // Test "soft" errors - conn.setAutoCommit(false); - stmt.addBatch(select); - stmt.addBatch(insertStmt); - stmt.addBatch(select); - stmt.addBatch(insertStmt); - try { - stmt.executeLargeBatch(); - assertEquals(false, true, TestResource.getResource("R_shouldThrowException")); - } - catch (BatchUpdateException bue) { - assertEquals("A result set was generated for update.", bue.getMessage(), TestResource.getResource("R_unexpectedExceptionContent")); - assertEquals(Arrays.equals(bue.getLargeUpdateCounts(), new long[] {-3, 1, -3, 1}), true, - TestResource.getResource("R_incorrectUpdateCount")); - } - conn.rollback(); - - // Defect 128801: Rollback (with conversion error) should throw SQLException - stmt.addBatch(dateConversionError); - stmt.addBatch(insertStmt); - stmt.addBatch(insertStmt); - stmt.addBatch(insertStmt); - try { - stmt.executeLargeBatch(); - } - catch (BatchUpdateException bue) { - assertThat(bue.getMessage(), containsString(TestResource.getResource("R_syntaxErrorDateConvert"))); - } - catch (SQLException e) { - assertThat(e.getMessage(), containsString(TestResource.getResource("R_dateConvertError"))); - } - - conn.setAutoCommit(true); - - // On SQL Azure, raising FATAL error by RAISERROR() is not supported and there is no way to - // cut the current connection by a statement inside a SQL batch. - // Details: Although one can simulate a fatal error (that cuts the connections) by dropping the database, - // this simulation cannot be written entirely in TSQL (because it needs a new connection), - // and thus it cannot be put into a TSQL batch and it is useless here. - // So we have to skip the last scenario of this test case, i.e. "Test Severe (connection-closing) errors" - // It is worthwhile to still execute the first 5 test scenarios of this test case, in order to have best test coverage. - if (!DBConnection.isSqlAzure(DriverManager.getConnection(connectionString))) { - // Test Severe (connection-closing) errors - stmt.addBatch(error); - stmt.addBatch(insertStmt); - stmt.addBatch(warning); - - stmt.addBatch(insertStmt); - stmt.addBatch(severe); - stmt.addBatch(insertStmt); - stmt.addBatch(insertStmt); - try { - stmt.executeLargeBatch(); - assertEquals(false, true, TestResource.getResource("R_shouldThrowException")); - } - catch (BatchUpdateException bue) { - assertEquals(false, true, TestResource.getResource("R_unexpectedException") + bue.getMessage()); - } - catch (SQLException e) { - actualExceptionText = e.getMessage(); - - if (actualExceptionText.endsWith("reset")) { - assertTrue(actualExceptionText.equalsIgnoreCase("Connection reset"), TestResource.getResource("R_unexpectedExceptionContent") + ": " + actualExceptionText); - } - else { - assertTrue(actualExceptionText.equalsIgnoreCase("raiserror level 20"), TestResource.getResource("R_unexpectedExceptionContent") + ": " + actualExceptionText); - - } - } - } - - try { - stmt.executeLargeUpdate("drop table " + tableName); - } - catch (Exception ignored) { - } - stmt.close(); - conn.close(); + + con.setUseBulkCopyForBatchInsert(true); } } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/BatchExecutionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/BatchExecutionTest.java index e18e1cfc1..66b100a47 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/BatchExecutionTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/BatchExecutionTest.java @@ -22,6 +22,7 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.platform.runner.JUnitPlatform; import org.junit.runner.RunWith; @@ -65,114 +66,11 @@ public void testBatchExceptionAEOn() throws Exception { * array of Integer values of length 3 */ public void testAddBatch1() { - int i = 0; - int retValue[] = {0, 0, 0}; - try { - String sPrepStmt = "update ctstable2 set PRICE=PRICE*20 where TYPE_ID=?"; - pstmt = connection.prepareStatement(sPrepStmt); - pstmt.setInt(1, 2); - pstmt.addBatch(); - - pstmt.setInt(1, 3); - pstmt.addBatch(); - - pstmt.setInt(1, 4); - pstmt.addBatch(); - - int[] updateCount = pstmt.executeBatch(); - int updateCountlen = updateCount.length; - - assertTrue(updateCountlen == 3, TestResource.getResource("R_addBatchFailed") + ": " + TestResource.getResource("R_incorrectUpdateCount")); - - String sPrepStmt1 = "select count(*) from ctstable2 where TYPE_ID=?"; - - pstmt1 = connection.prepareStatement(sPrepStmt1); - - // 2 is the number that is set First for Type Id in Prepared Statement - for (int n = 2; n <= 4; n++) { - pstmt1.setInt(1, n); - rs = pstmt1.executeQuery(); - rs.next(); - retValue[i++] = rs.getInt(1); - } - - pstmt1.close(); - - for (int j = 0; j < updateCount.length; j++) { - - if (updateCount[j] != retValue[j] && updateCount[j] != Statement.SUCCESS_NO_INFO) { - fail(TestResource.getResource("R_incorrectUpdateCount")); - } - } - } - catch (BatchUpdateException b) { - fail(TestResource.getResource("R_addBatchFailed") + ": " + b.getMessage()); - } - catch (SQLException sqle) { - fail(TestResource.getResource("R_addBatchFailed") + ": " + sqle.getMessage()); - } - catch (Exception e) { - fail(TestResource.getResource("R_addBatchFailed") + ": " + e.getMessage()); - } + testAddBatch1Internal("BatchInsert"); } - - /** - * Get a PreparedStatement object and call the addBatch() method with 3 SQL statements and call the executeBatch() method and it should return - * array of Integer values of length 3 - */ - public void testAddBatch1UseBulkCopyAPI() { - int i = 0; - int retValue[] = {0, 0, 0}; - try { - String sPrepStmt = "update ctstable2 set PRICE=PRICE*20 where TYPE_ID=?"; - Field f1 = SQLServerConnection.class.getDeclaredField("isAzureDW"); - f1.setAccessible(true); - f1.set(connection, true); - pstmt = connection.prepareStatement(sPrepStmt); - pstmt.setInt(1, 2); - pstmt.addBatch(); - - pstmt.setInt(1, 3); - pstmt.addBatch(); - - pstmt.setInt(1, 4); - pstmt.addBatch(); - - int[] updateCount = pstmt.executeBatch(); - int updateCountlen = updateCount.length; - - assertTrue(updateCountlen == 3, "addBatch does not add the SQL Statements to Batch ,call to addBatch failed"); - - String sPrepStmt1 = "select count(*) from ctstable2 where TYPE_ID=?"; - - pstmt1 = connection.prepareStatement(sPrepStmt1); - - // 2 is the number that is set First for Type Id in Prepared Statement - for (int n = 2; n <= 4; n++) { - pstmt1.setInt(1, n); - rs = pstmt1.executeQuery(); - rs.next(); - retValue[i++] = rs.getInt(1); - } - - pstmt1.close(); - for (int j = 0; j < updateCount.length; j++) { - - if (updateCount[j] != retValue[j] && updateCount[j] != Statement.SUCCESS_NO_INFO) { - fail("affected row count does not match with the updateCount value, Call to addBatch is Failed!"); - } - } - } - catch (BatchUpdateException b) { - fail("BatchUpdateException : Call to addBatch is Failed!"); - } - catch (SQLException sqle) { - fail("Call to addBatch is Failed!"); - } - catch (Exception e) { - fail("Call to addBatch is Failed!"); - } + public void testAddBatch1UseBulkCopyAPI() { + testAddBatch1Internal("BulkCopy"); } /** @@ -180,12 +78,24 @@ public void testAddBatch1UseBulkCopyAPI() { * an array of Integer values of length 3. */ public void testExecuteBatch1() { + testExecuteBatch1Internal("BatchInsert"); + } + + public void testExecuteBatch1UseBulkCopyAPI() { + testExecuteBatch1Internal("BulkCopy"); + } + + private void testExecuteBatch1Internal(String mode) { int i = 0; int retValue[] = {0, 0, 0}; int updateCountlen = 0; try { String sPrepStmt = "update ctstable2 set PRICE=PRICE*20 where TYPE_ID=?"; + if (mode.equalsIgnoreCase("bulkcopy")) { + modifyConnectionForBulkCopyAPI((SQLServerConnection) connection); + } + pstmt = connection.prepareStatement(sPrepStmt); pstmt.setInt(1, 1); pstmt.addBatch(); @@ -220,47 +130,64 @@ public void testExecuteBatch1() { } } } - catch (BatchUpdateException b) { - fail(TestResource.getResource("R_executeBatchFailed") + ": " + b.getMessage()); - } - catch (SQLException sqle) { - fail(TestResource.getResource("R_executeBatchFailed") + ": " + sqle.getMessage()); - } catch (Exception e) { fail(TestResource.getResource("R_executeBatchFailed") + ": " + e.getMessage()); } } + + private static void createTable() throws SQLException { + String sql1 = "create table ctstable1 (TYPE_ID int, TYPE_DESC varchar(32), primary key(TYPE_ID)) "; + String sql2 = "create table ctstable2 (KEY_ID int, COF_NAME varchar(32), PRICE float, TYPE_ID int, primary key(KEY_ID), foreign key(TYPE_ID) references ctstable1) "; + stmt.execute(sql1); + stmt.execute(sql2); + + String sqlin2 = "insert into ctstable1 values (1,'COFFEE-Desc')"; + stmt.execute(sqlin2); + sqlin2 = "insert into ctstable1 values (2,'COFFEE-Desc2')"; + stmt.execute(sqlin2); + sqlin2 = "insert into ctstable1 values (3,'COFFEE-Desc3')"; + stmt.execute(sqlin2); + + String sqlin1 = "insert into ctstable2 values (9,'COFFEE-9',9.0, 1)"; + stmt.execute(sqlin1); + sqlin1 = "insert into ctstable2 values (10,'COFFEE-10',10.0, 2)"; + stmt.execute(sqlin1); + sqlin1 = "insert into ctstable2 values (11,'COFFEE-11',11.0, 3)"; + stmt.execute(sqlin1); + + } - public void testExecuteBatch1UseBulkCopyAPI() { + private void testAddBatch1Internal(String mode) { int i = 0; int retValue[] = {0, 0, 0}; - int updCountLength = 0; try { String sPrepStmt = "update ctstable2 set PRICE=PRICE*20 where TYPE_ID=?"; - + + if (mode.equalsIgnoreCase("bulkcopy")) { + modifyConnectionForBulkCopyAPI((SQLServerConnection) connection); + } + pstmt = connection.prepareStatement(sPrepStmt); - Field f1 = SQLServerConnection.class.getDeclaredField("isAzureDW"); - f1.setAccessible(true); - f1.set(connection, true); - pstmt.setInt(1, 1); - pstmt.addBatch(); - pstmt.setInt(1, 2); pstmt.addBatch(); pstmt.setInt(1, 3); pstmt.addBatch(); + pstmt.setInt(1, 4); + pstmt.addBatch(); + int[] updateCount = pstmt.executeBatch(); - updCountLength = updateCount.length; + int updateCountlen = updateCount.length; - assertTrue(updCountLength == 3, "executeBatch does not execute the Batch of SQL statements, Call to executeBatch is Failed!"); + assertTrue(updateCountlen == 3, TestResource.getResource("R_addBatchFailed") + ": " + TestResource.getResource("R_incorrectUpdateCount")); String sPrepStmt1 = "select count(*) from ctstable2 where TYPE_ID=?"; pstmt1 = connection.prepareStatement(sPrepStmt1); - for (int n = 1; n <= 3; n++) { + // 2 is the number that is set First for Type Id in Prepared Statement + for (int n = 2; n <= 4; n++) { pstmt1.setInt(1, n); rs = pstmt1.executeQuery(); rs.next(); @@ -270,45 +197,26 @@ public void testExecuteBatch1UseBulkCopyAPI() { pstmt1.close(); for (int j = 0; j < updateCount.length; j++) { + if (updateCount[j] != retValue[j] && updateCount[j] != Statement.SUCCESS_NO_INFO) { - fail("executeBatch does not execute the Batch of SQL statements, Call to executeBatch is Failed!"); + fail(TestResource.getResource("R_incorrectUpdateCount")); } } } - catch (BatchUpdateException b) { - fail("BatchUpdateException : Call to executeBatch is Failed!"); - } - catch (SQLException sqle) { - fail("Call to executeBatch is Failed!"); - } catch (Exception e) { - fail("Call to executeBatch is Failed!"); + fail(TestResource.getResource("R_addBatchFailed") + ": " + e.getMessage()); } } - - private static void createTable() throws SQLException { - String sql1 = "create table ctstable1 (TYPE_ID int, TYPE_DESC varchar(32), primary key(TYPE_ID)) "; - String sql2 = "create table ctstable2 (KEY_ID int, COF_NAME varchar(32), PRICE float, TYPE_ID int, primary key(KEY_ID), foreign key(TYPE_ID) references ctstable1) "; - stmt.execute(sql1); - stmt.execute(sql2); - - String sqlin2 = "insert into ctstable1 values (1,'COFFEE-Desc')"; - stmt.execute(sqlin2); - sqlin2 = "insert into ctstable1 values (2,'COFFEE-Desc2')"; - stmt.execute(sqlin2); - sqlin2 = "insert into ctstable1 values (3,'COFFEE-Desc3')"; - stmt.execute(sqlin2); - - String sqlin1 = "insert into ctstable2 values (9,'COFFEE-9',9.0, 1)"; - stmt.execute(sqlin1); - sqlin1 = "insert into ctstable2 values (10,'COFFEE-10',10.0, 2)"; - stmt.execute(sqlin1); - sqlin1 = "insert into ctstable2 values (11,'COFFEE-11',11.0, 3)"; - stmt.execute(sqlin1); - + + private void modifyConnectionForBulkCopyAPI(SQLServerConnection con) throws Exception { + Field f1 = SQLServerConnection.class.getDeclaredField("isAzureDW"); + f1.setAccessible(true); + f1.set(con, true); + + con.setUseBulkCopyForBatchInsert(true); } - @BeforeAll + @BeforeEach public static void testSetup() throws TestAbortedException, Exception { assumeTrue(13 <= new DBConnection(connectionString).getServerVersion(), TestResource.getResource("R_Incompat_SQLServerVersion")); diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/PreparedStatementTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/PreparedStatementTest.java index 666a4da7a..78b2777b0 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/PreparedStatementTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/PreparedStatementTest.java @@ -134,166 +134,8 @@ public void testBatchedUnprepare() throws SQLException { */ @Test @Tag("slow") - public void testStatementPooling() throws SQLException { - // Test % handle re-use - try (SQLServerConnection con = (SQLServerConnection)DriverManager.getConnection(connectionString)) { - String query = String.format("/*statementpoolingtest_re-use_%s*/SELECT TOP(1) * FROM sys.tables;", UUID.randomUUID().toString()); - - con.setStatementPoolingCacheSize(10); - - boolean[] prepOnFirstCalls = {false, true}; - - for(boolean prepOnFirstCall : prepOnFirstCalls) { - - con.setEnablePrepareOnFirstPreparedStatementCall(prepOnFirstCall); - - int[] queryCounts = {10, 20, 30, 40}; - for(int queryCount : queryCounts) { - String[] queries = new String[queryCount]; - for(int i = 0; i < queries.length; ++i) { - queries[i] = String.format("%s--%s--%s--%s", query, i, queryCount, prepOnFirstCall); - } - - int testsWithHandleReuse = 0; - final int testCount = 500; - for(int i = 0; i < testCount; ++i) { - Random random = new Random(); - int queryNumber = random.nextInt(queries.length); - try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) con.prepareStatement(queries[queryNumber])) { - pstmt.execute(); - - // Grab handle-reuse before it would be populated if initially created. - if(0 < pstmt.getPreparedStatementHandle()) - testsWithHandleReuse++; - - pstmt.getMoreResults(); // Make sure handle is updated. - } - } - System.out.println(String.format("Prep on first call: %s Query count:%s: %s of %s (%s)", prepOnFirstCall, queryCount, testsWithHandleReuse, testCount, (double)testsWithHandleReuse/(double)testCount)); - } - } - } - - try (SQLServerConnection con = (SQLServerConnection)DriverManager.getConnection(connectionString)) { - - // Test behvaior with statement pooling. - con.setStatementPoolingCacheSize(10); - this.executeSQL(con, - "IF NOT EXISTS (SELECT * FROM sys.messages WHERE message_id = 99586) EXEC sp_addmessage 99586, 16, 'Prepared handle GAH!';"); - // Test with missing handle failures (fake). - this.executeSQL(con, "CREATE TABLE #update1 (col INT);INSERT #update1 VALUES (1);"); - this.executeSQL(con, - "CREATE PROC #updateProc1 AS UPDATE #update1 SET col += 1; IF EXISTS (SELECT * FROM #update1 WHERE col % 5 = 0) RAISERROR(99586,16,1);"); - try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) con.prepareStatement("#updateProc1")) { - for (int i = 0; i < 100; ++i) { - try { - assertSame(1, pstmt.executeUpdate()); - } - catch (SQLException e) { - // Error "Prepared handle GAH" is expected to happen. But it should not terminate the execution with RAISERROR. - // Since the original "Could not find prepared statement with handle" error does not terminate the execution after it. - if (!e.getMessage().contains("Prepared handle GAH")) { - throw e; - } - } - } - } - - // test updated value, should be 1 + 100 = 101 - // although executeUpdate() throws exception, update operation should be executed successfully. - try (ResultSet rs = con.createStatement().executeQuery("select * from #update1")) { - rs.next(); - assertSame(101, rs.getInt(1)); - } - - // Test batching with missing handle failures (fake). - this.executeSQL(con, - "IF NOT EXISTS (SELECT * FROM sys.messages WHERE message_id = 99586) EXEC sp_addmessage 99586, 16, 'Prepared handle GAH!';"); - this.executeSQL(con, "CREATE TABLE #update2 (col INT);INSERT #update2 VALUES (1);"); - this.executeSQL(con, - "CREATE PROC #updateProc2 AS UPDATE #update2 SET col += 1; IF EXISTS (SELECT * FROM #update2 WHERE col % 5 = 0) RAISERROR(99586,16,1);"); - try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) con.prepareStatement("#updateProc2")) { - for (int i = 0; i < 100; ++i) { - pstmt.addBatch(); - } - - int[] updateCounts = null; - try { - updateCounts = pstmt.executeBatch(); - } - catch (BatchUpdateException e) { - // Error "Prepared handle GAH" is expected to happen. But it should not terminate the execution with RAISERROR. - // Since the original "Could not find prepared statement with handle" error does not terminate the execution after it. - if (!e.getMessage().contains("Prepared handle GAH")) { - throw e; - } - } - - // since executeBatch() throws exception, it does not return anthing. So updateCounts is still null. - assertSame(null, updateCounts); - - // test updated value, should be 1 + 100 = 101 - // although executeBatch() throws exception, update operation should be executed successfully. - try (ResultSet rs = con.createStatement().executeQuery("select * from #update2")) { - rs.next(); - assertSame(101, rs.getInt(1)); - } - } - } - - try (SQLServerConnection con = (SQLServerConnection)DriverManager.getConnection(connectionString)) { - // Test behvaior with statement pooling. - con.setDisableStatementPooling(false); - con.setStatementPoolingCacheSize(10); - - String lookupUniqueifier = UUID.randomUUID().toString(); - String query = String.format("/*statementpoolingtest_%s*/SELECT * FROM sys.tables;", lookupUniqueifier); - - // Execute statement first, should create cache entry WITHOUT handle (since sp_executesql was used). - try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement)con.prepareStatement(query)) { - pstmt.execute(); // sp_executesql - pstmt.getMoreResults(); // Make sure handle is updated. - - assertSame(0, pstmt.getPreparedStatementHandle()); - } - - // Execute statement again, should now create handle. - int handle = 0; - try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement)con.prepareStatement(query)) { - pstmt.execute(); // sp_prepexec - pstmt.getMoreResults(); // Make sure handle is updated. - - handle = pstmt.getPreparedStatementHandle(); - assertNotSame(0, handle); - } - - // Execute statement again and verify same handle was used. - try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement)con.prepareStatement(query)) { - pstmt.execute(); // sp_execute - pstmt.getMoreResults(); // Make sure handle is updated. - - assertNotSame(0, pstmt.getPreparedStatementHandle()); - assertSame(handle, pstmt.getPreparedStatementHandle()); - } - - // Execute new statement with different SQL text and verify it does NOT get same handle (should now fall back to using sp_executesql). - SQLServerPreparedStatement outer = null; - try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement)con.prepareStatement(query + ";")) { - outer = pstmt; - pstmt.execute(); // sp_executesql - pstmt.getMoreResults(); // Make sure handle is updated. - - assertSame(0, pstmt.getPreparedStatementHandle()); - assertNotSame(handle, pstmt.getPreparedStatementHandle()); - } - try { - System.out.println(outer.getPreparedStatementHandle()); - fail(TestResource.getResource("R_invalidGetPreparedStatementHandle")); - } - catch(Exception e) { - // Good! - } - } + public void testStatementPooling() throws Exception { + testStatementPoolingInternal("batchInsert"); } /** @@ -307,170 +149,8 @@ public void testStatementPooling() throws SQLException { */ @Test @Tag("slow") - public void testStatementPoolingUseBulkCopyAPI() throws SQLException, NoSuchFieldException, SecurityException, - IllegalArgumentException, IllegalAccessException { - // Test % handle re-use - try (SQLServerConnection con = (SQLServerConnection)DriverManager.getConnection(connectionString)) { - Field f1 = SQLServerConnection.class.getDeclaredField("isAzureDW"); - f1.setAccessible(true); - f1.set(con, true); - String query = String.format("/*statementpoolingtest_re-use_%s*/SELECT TOP(1) * FROM sys.tables;", UUID.randomUUID().toString()); - - con.setStatementPoolingCacheSize(10); - - boolean[] prepOnFirstCalls = {false, true}; - - for(boolean prepOnFirstCall : prepOnFirstCalls) { - - con.setEnablePrepareOnFirstPreparedStatementCall(prepOnFirstCall); - - int[] queryCounts = {10, 20, 30, 40}; - for(int queryCount : queryCounts) { - String[] queries = new String[queryCount]; - for(int i = 0; i < queries.length; ++i) { - queries[i] = String.format("%s--%s--%s--%s", query, i, queryCount, prepOnFirstCall); - } - - int testsWithHandleReuse = 0; - final int testCount = 500; - for(int i = 0; i < testCount; ++i) { - Random random = new Random(); - int queryNumber = random.nextInt(queries.length); - try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) con.prepareStatement(queries[queryNumber])) { - pstmt.execute(); - - // Grab handle-reuse before it would be populated if initially created. - if(0 < pstmt.getPreparedStatementHandle()) - testsWithHandleReuse++; - - pstmt.getMoreResults(); // Make sure handle is updated. - } - } - System.out.println(String.format("Prep on first call: %s Query count:%s: %s of %s (%s)", prepOnFirstCall, queryCount, testsWithHandleReuse, testCount, (double)testsWithHandleReuse/(double)testCount)); - } - } - } - - try (SQLServerConnection con = (SQLServerConnection)DriverManager.getConnection(connectionString)) { - - // Test behvaior with statement pooling. - con.setStatementPoolingCacheSize(10); - this.executeSQL(con, - "IF NOT EXISTS (SELECT * FROM sys.messages WHERE message_id = 99586) EXEC sp_addmessage 99586, 16, 'Prepared handle GAH!';"); - // Test with missing handle failures (fake). - this.executeSQL(con, "CREATE TABLE #update1 (col INT);INSERT #update1 VALUES (1);"); - this.executeSQL(con, - "CREATE PROC #updateProc1 AS UPDATE #update1 SET col += 1; IF EXISTS (SELECT * FROM #update1 WHERE col % 5 = 0) RAISERROR(99586,16,1);"); - try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) con.prepareStatement("#updateProc1")) { - for (int i = 0; i < 100; ++i) { - try { - assertSame(1, pstmt.executeUpdate()); - } - catch (SQLException e) { - // Error "Prepared handle GAH" is expected to happen. But it should not terminate the execution with RAISERROR. - // Since the original "Could not find prepared statement with handle" error does not terminate the execution after it. - if (!e.getMessage().contains("Prepared handle GAH")) { - throw e; - } - } - } - } - - // test updated value, should be 1 + 100 = 101 - // although executeUpdate() throws exception, update operation should be executed successfully. - try (ResultSet rs = con.createStatement().executeQuery("select * from #update1")) { - rs.next(); - assertSame(101, rs.getInt(1)); - } - - // Test batching with missing handle failures (fake). - this.executeSQL(con, - "IF NOT EXISTS (SELECT * FROM sys.messages WHERE message_id = 99586) EXEC sp_addmessage 99586, 16, 'Prepared handle GAH!';"); - this.executeSQL(con, "CREATE TABLE #update2 (col INT);INSERT #update2 VALUES (1);"); - this.executeSQL(con, - "CREATE PROC #updateProc2 AS UPDATE #update2 SET col += 1; IF EXISTS (SELECT * FROM #update2 WHERE col % 5 = 0) RAISERROR(99586,16,1);"); - try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) con.prepareStatement("#updateProc2")) { - for (int i = 0; i < 100; ++i) { - pstmt.addBatch(); - } - - int[] updateCounts = null; - try { - updateCounts = pstmt.executeBatch(); - } - catch (BatchUpdateException e) { - // Error "Prepared handle GAH" is expected to happen. But it should not terminate the execution with RAISERROR. - // Since the original "Could not find prepared statement with handle" error does not terminate the execution after it. - if (!e.getMessage().contains("Prepared handle GAH")) { - throw e; - } - } - - // since executeBatch() throws exception, it does not return anthing. So updateCounts is still null. - assertSame(null, updateCounts); - - // test updated value, should be 1 + 100 = 101 - // although executeBatch() throws exception, update operation should be executed successfully. - try (ResultSet rs = con.createStatement().executeQuery("select * from #update2")) { - rs.next(); - assertSame(101, rs.getInt(1)); - } - } - } - - try (SQLServerConnection con = (SQLServerConnection)DriverManager.getConnection(connectionString)) { - // Test behvaior with statement pooling. - con.setDisableStatementPooling(false); - con.setStatementPoolingCacheSize(10); - - String lookupUniqueifier = UUID.randomUUID().toString(); - String query = String.format("/*statementpoolingtest_%s*/SELECT * FROM sys.tables;", lookupUniqueifier); - - // Execute statement first, should create cache entry WITHOUT handle (since sp_executesql was used). - try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement)con.prepareStatement(query)) { - pstmt.execute(); // sp_executesql - pstmt.getMoreResults(); // Make sure handle is updated. - - assertSame(0, pstmt.getPreparedStatementHandle()); - } - - // Execute statement again, should now create handle. - int handle = 0; - try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement)con.prepareStatement(query)) { - pstmt.execute(); // sp_prepexec - pstmt.getMoreResults(); // Make sure handle is updated. - - handle = pstmt.getPreparedStatementHandle(); - assertNotSame(0, handle); - } - - // Execute statement again and verify same handle was used. - try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement)con.prepareStatement(query)) { - pstmt.execute(); // sp_execute - pstmt.getMoreResults(); // Make sure handle is updated. - - assertNotSame(0, pstmt.getPreparedStatementHandle()); - assertSame(handle, pstmt.getPreparedStatementHandle()); - } - - // Execute new statement with different SQL text and verify it does NOT get same handle (should now fall back to using sp_executesql). - SQLServerPreparedStatement outer = null; - try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement)con.prepareStatement(query + ";")) { - outer = pstmt; - pstmt.execute(); // sp_executesql - pstmt.getMoreResults(); // Make sure handle is updated. - - assertSame(0, pstmt.getPreparedStatementHandle()); - assertNotSame(handle, pstmt.getPreparedStatementHandle()); - } - try { - System.out.println(outer.getPreparedStatementHandle()); - fail("Error for invalid use of getPreparedStatementHandle() after statement close expected."); - } - catch(Exception e) { - // Good! - } - } + public void testStatementPoolingUseBulkCopyAPI() throws Exception { + testStatementPoolingInternal("BulkCopy"); } /** @@ -747,4 +427,182 @@ public void testStatementPoolingPreparedStatementExecAndUnprepareConfig() throws assertSame(0, con.getDiscardedServerPreparedStatementCount()); } } + + private void testStatementPoolingInternal(String mode) throws Exception { + // Test % handle re-use + try (SQLServerConnection con = (SQLServerConnection)DriverManager.getConnection(connectionString)) { + if (mode.equalsIgnoreCase("bulkcopy")) { + modifyConnectionForBulkCopyAPI(con); + } + String query = String.format("/*statementpoolingtest_re-use_%s*/SELECT TOP(1) * FROM sys.tables;", UUID.randomUUID().toString()); + + con.setStatementPoolingCacheSize(10); + + boolean[] prepOnFirstCalls = {false, true}; + + for(boolean prepOnFirstCall : prepOnFirstCalls) { + + con.setEnablePrepareOnFirstPreparedStatementCall(prepOnFirstCall); + + int[] queryCounts = {10, 20, 30, 40}; + for(int queryCount : queryCounts) { + String[] queries = new String[queryCount]; + for(int i = 0; i < queries.length; ++i) { + queries[i] = String.format("%s--%s--%s--%s", query, i, queryCount, prepOnFirstCall); + } + + int testsWithHandleReuse = 0; + final int testCount = 500; + for(int i = 0; i < testCount; ++i) { + Random random = new Random(); + int queryNumber = random.nextInt(queries.length); + try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) con.prepareStatement(queries[queryNumber])) { + pstmt.execute(); + + // Grab handle-reuse before it would be populated if initially created. + if(0 < pstmt.getPreparedStatementHandle()) + testsWithHandleReuse++; + + pstmt.getMoreResults(); // Make sure handle is updated. + } + } + System.out.println(String.format("Prep on first call: %s Query count:%s: %s of %s (%s)", prepOnFirstCall, queryCount, testsWithHandleReuse, testCount, (double)testsWithHandleReuse/(double)testCount)); + } + } + } + + try (SQLServerConnection con = (SQLServerConnection)DriverManager.getConnection(connectionString)) { + if (mode.equalsIgnoreCase("bulkcopy")) { + modifyConnectionForBulkCopyAPI(con); + } + // Test behvaior with statement pooling. + con.setStatementPoolingCacheSize(10); + this.executeSQL(con, + "IF NOT EXISTS (SELECT * FROM sys.messages WHERE message_id = 99586) EXEC sp_addmessage 99586, 16, 'Prepared handle GAH!';"); + // Test with missing handle failures (fake). + this.executeSQL(con, "CREATE TABLE #update1 (col INT);INSERT #update1 VALUES (1);"); + this.executeSQL(con, + "CREATE PROC #updateProc1 AS UPDATE #update1 SET col += 1; IF EXISTS (SELECT * FROM #update1 WHERE col % 5 = 0) RAISERROR(99586,16,1);"); + try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) con.prepareStatement("#updateProc1")) { + for (int i = 0; i < 100; ++i) { + try { + assertSame(1, pstmt.executeUpdate()); + } + catch (SQLException e) { + // Error "Prepared handle GAH" is expected to happen. But it should not terminate the execution with RAISERROR. + // Since the original "Could not find prepared statement with handle" error does not terminate the execution after it. + if (!e.getMessage().contains("Prepared handle GAH")) { + throw e; + } + } + } + } + + // test updated value, should be 1 + 100 = 101 + // although executeUpdate() throws exception, update operation should be executed successfully. + try (ResultSet rs = con.createStatement().executeQuery("select * from #update1")) { + rs.next(); + assertSame(101, rs.getInt(1)); + } + + // Test batching with missing handle failures (fake). + this.executeSQL(con, + "IF NOT EXISTS (SELECT * FROM sys.messages WHERE message_id = 99586) EXEC sp_addmessage 99586, 16, 'Prepared handle GAH!';"); + this.executeSQL(con, "CREATE TABLE #update2 (col INT);INSERT #update2 VALUES (1);"); + this.executeSQL(con, + "CREATE PROC #updateProc2 AS UPDATE #update2 SET col += 1; IF EXISTS (SELECT * FROM #update2 WHERE col % 5 = 0) RAISERROR(99586,16,1);"); + try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) con.prepareStatement("#updateProc2")) { + for (int i = 0; i < 100; ++i) { + pstmt.addBatch(); + } + + int[] updateCounts = null; + try { + updateCounts = pstmt.executeBatch(); + } + catch (BatchUpdateException e) { + // Error "Prepared handle GAH" is expected to happen. But it should not terminate the execution with RAISERROR. + // Since the original "Could not find prepared statement with handle" error does not terminate the execution after it. + if (!e.getMessage().contains("Prepared handle GAH")) { + throw e; + } + } + + // since executeBatch() throws exception, it does not return anthing. So updateCounts is still null. + assertSame(null, updateCounts); + + // test updated value, should be 1 + 100 = 101 + // although executeBatch() throws exception, update operation should be executed successfully. + try (ResultSet rs = con.createStatement().executeQuery("select * from #update2")) { + rs.next(); + assertSame(101, rs.getInt(1)); + } + } + } + + try (SQLServerConnection con = (SQLServerConnection)DriverManager.getConnection(connectionString)) { + if (mode.equalsIgnoreCase("bulkcopy")) { + modifyConnectionForBulkCopyAPI(con); + } + // Test behvaior with statement pooling. + con.setDisableStatementPooling(false); + con.setStatementPoolingCacheSize(10); + + String lookupUniqueifier = UUID.randomUUID().toString(); + String query = String.format("/*statementpoolingtest_%s*/SELECT * FROM sys.tables;", lookupUniqueifier); + + // Execute statement first, should create cache entry WITHOUT handle (since sp_executesql was used). + try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement)con.prepareStatement(query)) { + pstmt.execute(); // sp_executesql + pstmt.getMoreResults(); // Make sure handle is updated. + + assertSame(0, pstmt.getPreparedStatementHandle()); + } + + // Execute statement again, should now create handle. + int handle = 0; + try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement)con.prepareStatement(query)) { + pstmt.execute(); // sp_prepexec + pstmt.getMoreResults(); // Make sure handle is updated. + + handle = pstmt.getPreparedStatementHandle(); + assertNotSame(0, handle); + } + + // Execute statement again and verify same handle was used. + try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement)con.prepareStatement(query)) { + pstmt.execute(); // sp_execute + pstmt.getMoreResults(); // Make sure handle is updated. + + assertNotSame(0, pstmt.getPreparedStatementHandle()); + assertSame(handle, pstmt.getPreparedStatementHandle()); + } + + // Execute new statement with different SQL text and verify it does NOT get same handle (should now fall back to using sp_executesql). + SQLServerPreparedStatement outer = null; + try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement)con.prepareStatement(query + ";")) { + outer = pstmt; + pstmt.execute(); // sp_executesql + pstmt.getMoreResults(); // Make sure handle is updated. + + assertSame(0, pstmt.getPreparedStatementHandle()); + assertNotSame(handle, pstmt.getPreparedStatementHandle()); + } + try { + System.out.println(outer.getPreparedStatementHandle()); + fail(TestResource.getResource("R_invalidGetPreparedStatementHandle")); + } + catch(Exception e) { + // Good! + } + } + } + + private void modifyConnectionForBulkCopyAPI(SQLServerConnection con) throws Exception { + Field f1 = SQLServerConnection.class.getDeclaredField("isAzureDW"); + f1.setAccessible(true); + f1.set(con, true); + + con.setUseBulkCopyForBatchInsert(true); + } } From 096d78e75cd6424a735b1a2c84e15a3207d365ed Mon Sep 17 00:00:00 2001 From: Peter Bae Date: Tue, 26 Jun 2018 16:41:44 -0700 Subject: [PATCH 32/32] Replace all connection and statements with try blocks --- .../BatchExecutionWithBulkCopyTest.java | 762 +++++++++--------- .../preparedStatement/RegressionTest.java | 179 ++-- .../unit/statement/BatchExecutionTest.java | 8 +- 3 files changed, 477 insertions(+), 472 deletions(-) diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyTest.java index f18d7df39..c0ec92b58 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyTest.java @@ -3,23 +3,20 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assumptions.assumeTrue; import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.sql.Connection; import java.sql.Date; import java.sql.DriverManager; +import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.sql.Timestamp; -import java.sql.Types; import java.util.ArrayList; import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.platform.runner.JUnitPlatform; @@ -29,17 +26,12 @@ import com.microsoft.sqlserver.jdbc.SQLServerConnection; import com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement; import com.microsoft.sqlserver.jdbc.SQLServerStatement; -import com.microsoft.sqlserver.jdbc.TestResource; import com.microsoft.sqlserver.testframework.AbstractTest; -import com.microsoft.sqlserver.testframework.DBConnection; import com.microsoft.sqlserver.testframework.Utils; @RunWith(JUnitPlatform.class) public class BatchExecutionWithBulkCopyTest extends AbstractTest { - static SQLServerPreparedStatement pstmt = null; - static Statement stmt = null; - static Connection connection = null; static long UUID = System.currentTimeMillis();; static String tableName = "BulkCopyParseTest" + UUID; static String squareBracketTableName = "[peter]]]]test" + UUID + "]"; @@ -47,123 +39,125 @@ public class BatchExecutionWithBulkCopyTest extends AbstractTest { @Test public void testIsInsert() throws Exception { - String valid1 = "INSERT INTO PeterTable values (1, 2)"; - String valid2 = " INSERT INTO PeterTable values (1, 2)"; - String valid3 = "/* asdf */ INSERT INTO PeterTable values (1, 2)"; - String invalid = "Select * from PEterTable"; - - stmt = connection.createStatement(); - Method method = stmt.getClass().getDeclaredMethod("isInsert", String.class); - method.setAccessible(true); - assertTrue((boolean) method.invoke(stmt, valid1)); - assertTrue((boolean) method.invoke(stmt, valid2)); - assertTrue((boolean) method.invoke(stmt, valid3)); - assertFalse((boolean) method.invoke(stmt, invalid)); + try (Connection connection = DriverManager.getConnection(connectionString + ";useBulkCopyForBatchInsert=true;"); + Statement stmt = (SQLServerStatement) connection.createStatement()) { + String valid1 = "INSERT INTO PeterTable values (1, 2)"; + String valid2 = " INSERT INTO PeterTable values (1, 2)"; + String valid3 = "/* asdf */ INSERT INTO PeterTable values (1, 2)"; + String invalid = "Select * from PEterTable"; + + Method method = stmt.getClass().getDeclaredMethod("isInsert", String.class); + method.setAccessible(true); + assertTrue((boolean) method.invoke(stmt, valid1)); + assertTrue((boolean) method.invoke(stmt, valid2)); + assertTrue((boolean) method.invoke(stmt, valid3)); + assertFalse((boolean) method.invoke(stmt, invalid)); + } } @Test public void testComments() throws Exception { - pstmt = (SQLServerPreparedStatement) connection.prepareStatement(""); - - String valid = "/* rando comment *//* rando comment */ INSERT /* rando comment */ INTO /* rando comment *//*rando comment*/ PeterTable /*rando comment */" - + " /* rando comment */values/* rando comment */ (1, 2)"; + try (Connection connection = DriverManager.getConnection(connectionString + ";useBulkCopyForBatchInsert=true;"); + PreparedStatement pstmt = (SQLServerPreparedStatement) connection.prepareStatement("");) { + String valid = "/* rando comment *//* rando comment */ INSERT /* rando comment */ INTO /* rando comment *//*rando comment*/ PeterTable /*rando comment */" + + " /* rando comment */values/* rando comment */ (1, 2)"; - Field f1 = pstmt.getClass().getSuperclass().getDeclaredField("localUserSQL"); - f1.setAccessible(true); - f1.set(pstmt, valid); + Field f1 = pstmt.getClass().getSuperclass().getDeclaredField("localUserSQL"); + f1.setAccessible(true); + f1.set(pstmt, valid); - Method method = pstmt.getClass().getSuperclass().getDeclaredMethod("parseUserSQLForTableNameDW", boolean.class, boolean.class, boolean.class, boolean.class); - method.setAccessible(true); + Method method = pstmt.getClass().getSuperclass().getDeclaredMethod("parseUserSQLForTableNameDW", boolean.class, boolean.class, boolean.class, boolean.class); + method.setAccessible(true); - assertEquals((String) method.invoke(pstmt, false, false, false, false), "PeterTable"); + assertEquals((String) method.invoke(pstmt, false, false, false, false), "PeterTable"); + } } @Test public void testBrackets() throws Exception { - pstmt = (SQLServerPreparedStatement) connection.prepareStatement(""); - - String valid = "/* rando comment *//* rando comment */ INSERT /* rando comment */ INTO /* rando comment *//*rando comment*/ [Peter[]]Table] /*rando comment */" - + " /* rando comment */values/* rando comment */ (1, 2)"; + try (Connection connection = DriverManager.getConnection(connectionString + ";useBulkCopyForBatchInsert=true;"); + PreparedStatement pstmt = (SQLServerPreparedStatement) connection.prepareStatement("");) { + String valid = "/* rando comment *//* rando comment */ INSERT /* rando comment */ INTO /* rando comment *//*rando comment*/ [Peter[]]Table] /*rando comment */" + + " /* rando comment */values/* rando comment */ (1, 2)"; - Field f1 = pstmt.getClass().getSuperclass().getDeclaredField("localUserSQL"); - f1.setAccessible(true); - f1.set(pstmt, valid); + Field f1 = pstmt.getClass().getSuperclass().getDeclaredField("localUserSQL"); + f1.setAccessible(true); + f1.set(pstmt, valid); - Method method = pstmt.getClass().getSuperclass().getDeclaredMethod("parseUserSQLForTableNameDW", boolean.class, boolean.class, boolean.class, boolean.class); - method.setAccessible(true); + Method method = pstmt.getClass().getSuperclass().getDeclaredMethod("parseUserSQLForTableNameDW", boolean.class, boolean.class, boolean.class, boolean.class); + method.setAccessible(true); - assertEquals((String) method.invoke(pstmt, false, false, false, false), "[Peter[]]Table]"); + assertEquals((String) method.invoke(pstmt, false, false, false, false), "[Peter[]]Table]"); + } } @Test public void testDoubleQuotes() throws Exception { - pstmt = (SQLServerPreparedStatement) connection.prepareStatement(""); - - String valid = "/* rando comment *//* rando comment */ INSERT /* rando comment */ INTO /* rando comment *//*rando comment*/ \"Peter\"\"\"\"Table\" /*rando comment */" - + " /* rando comment */values/* rando comment */ (1, 2)"; + try (Connection connection = DriverManager.getConnection(connectionString + ";useBulkCopyForBatchInsert=true;"); + PreparedStatement pstmt = (SQLServerPreparedStatement) connection.prepareStatement("");) { + String valid = "/* rando comment *//* rando comment */ INSERT /* rando comment */ INTO /* rando comment *//*rando comment*/ \"Peter\"\"\"\"Table\" /*rando comment */" + + " /* rando comment */values/* rando comment */ (1, 2)"; - Field f1 = pstmt.getClass().getSuperclass().getDeclaredField("localUserSQL"); - f1.setAccessible(true); - f1.set(pstmt, valid); + Field f1 = pstmt.getClass().getSuperclass().getDeclaredField("localUserSQL"); + f1.setAccessible(true); + f1.set(pstmt, valid); - Method method = pstmt.getClass().getSuperclass().getDeclaredMethod("parseUserSQLForTableNameDW", boolean.class, boolean.class, boolean.class, boolean.class); - method.setAccessible(true); + Method method = pstmt.getClass().getSuperclass().getDeclaredMethod("parseUserSQLForTableNameDW", boolean.class, boolean.class, boolean.class, boolean.class); + method.setAccessible(true); - assertEquals((String) method.invoke(pstmt, false, false, false, false), "\"Peter\"\"\"\"Table\""); + assertEquals((String) method.invoke(pstmt, false, false, false, false), "\"Peter\"\"\"\"Table\""); + } } @Test public void testAll() throws Exception { - pstmt = (SQLServerPreparedStatement) connection.prepareStatement(""); + try (Connection connection = DriverManager.getConnection(connectionString + ";useBulkCopyForBatchInsert=true;"); + PreparedStatement pstmt = (SQLServerPreparedStatement) connection.prepareStatement("");) { + String valid = "/* rando comment *//* rando comment */ INSERT /* rando comment */ INTO /* rando comment *//*rando comment*/ \"Peter\"\"\"\"Table\" /*rando comment */" + + " /* rando comment */ (\"c1\"/* rando comment */, /* rando comment */[c2]/* rando comment */, /* rando comment */ /* rando comment */c3/* rando comment */, c4)" + + "values/* rando comment */ (/* rando comment */1/* rando comment */, /* rando comment */2/* rando comment */ , '?', ?)/* rando comment */"; - String valid = "/* rando comment *//* rando comment */ INSERT /* rando comment */ INTO /* rando comment *//*rando comment*/ \"Peter\"\"\"\"Table\" /*rando comment */" - + " /* rando comment */ (\"c1\"/* rando comment */, /* rando comment */[c2]/* rando comment */, /* rando comment */ /* rando comment */c3/* rando comment */, c4)" - + "values/* rando comment */ (/* rando comment */1/* rando comment */, /* rando comment */2/* rando comment */ , '?', ?)/* rando comment */"; + Field f1 = pstmt.getClass().getSuperclass().getDeclaredField("localUserSQL"); + f1.setAccessible(true); + f1.set(pstmt, valid); - Field f1 = pstmt.getClass().getSuperclass().getDeclaredField("localUserSQL"); - f1.setAccessible(true); - f1.set(pstmt, valid); + Method method = pstmt.getClass().getSuperclass().getDeclaredMethod("parseUserSQLForTableNameDW", boolean.class, boolean.class, boolean.class, boolean.class); + method.setAccessible(true); - Method method = pstmt.getClass().getSuperclass().getDeclaredMethod("parseUserSQLForTableNameDW", boolean.class, boolean.class, boolean.class, boolean.class); - method.setAccessible(true); + assertEquals((String) method.invoke(pstmt, false, false, false, false), "\"Peter\"\"\"\"Table\""); - assertEquals((String) method.invoke(pstmt, false, false, false, false), "\"Peter\"\"\"\"Table\""); + method = pstmt.getClass().getSuperclass().getDeclaredMethod("parseUserSQLForColumnListDW"); + method.setAccessible(true); - method = pstmt.getClass().getSuperclass().getDeclaredMethod("parseUserSQLForColumnListDW"); - method.setAccessible(true); + ArrayList columnList = (ArrayList) method.invoke(pstmt); + ArrayList columnListExpected = new ArrayList(); + columnListExpected.add("c1"); + columnListExpected.add("c2"); + columnListExpected.add("c3"); + columnListExpected.add("c4"); - ArrayList columnList = (ArrayList) method.invoke(pstmt); - ArrayList columnListExpected = new ArrayList(); - columnListExpected.add("c1"); - columnListExpected.add("c2"); - columnListExpected.add("c3"); - columnListExpected.add("c4"); - - for (int i = 0; i < columnListExpected.size(); i++) { - assertEquals(columnList.get(i), columnListExpected.get(i)); - } + for (int i = 0; i < columnListExpected.size(); i++) { + assertEquals(columnList.get(i), columnListExpected.get(i)); + } - method = pstmt.getClass().getSuperclass().getDeclaredMethod("parseUserSQLForValueListDW", boolean.class); - method.setAccessible(true); + method = pstmt.getClass().getSuperclass().getDeclaredMethod("parseUserSQLForValueListDW", boolean.class); + method.setAccessible(true); - ArrayList valueList = (ArrayList) method.invoke(pstmt, false); - ArrayList valueListExpected = new ArrayList(); - valueListExpected.add("1"); - valueListExpected.add("2"); - valueListExpected.add("'?'"); - valueListExpected.add("?"); + ArrayList valueList = (ArrayList) method.invoke(pstmt, false); + ArrayList valueListExpected = new ArrayList(); + valueListExpected.add("1"); + valueListExpected.add("2"); + valueListExpected.add("'?'"); + valueListExpected.add("?"); - for (int i = 0; i < valueListExpected.size(); i++) { - assertEquals(valueList.get(i), valueListExpected.get(i)); + for (int i = 0; i < valueListExpected.size(); i++) { + assertEquals(valueList.get(i), valueListExpected.get(i)); + } } } @Test public void testAllcolumns() throws Exception { - Field f1 = SQLServerConnection.class.getDeclaredField("isAzureDW"); - f1.setAccessible(true); - f1.set(connection, true); - String valid = "INSERT INTO " + tableName + " values " + "(" + "?, " @@ -177,52 +171,53 @@ public void testAllcolumns() throws Exception { + "?, " + ")"; - pstmt = (SQLServerPreparedStatement) connection.prepareStatement(valid); - stmt = (SQLServerStatement) connection.createStatement(); - - Timestamp myTimestamp = new Timestamp(114550L); - - Date d = new Date(114550L); - - pstmt.setInt(1, 1234); - pstmt.setBoolean(2, false); - pstmt.setString(3, "a"); - pstmt.setDate(4, d); - pstmt.setDateTime(5, myTimestamp); - pstmt.setFloat(6, (float) 123.45); - pstmt.setString(7, "b"); - pstmt.setString(8, "varc"); - pstmt.setString(9, "''"); - pstmt.addBatch(); - - pstmt.executeBatch(); - - ResultSet rs = stmt.executeQuery("SELECT * FROM " + tableName); - - Object[] expected = new Object[9]; - - expected[0] = 1234; - expected[1] = false; - expected[2] = "a"; - expected[3] = d; - expected[4] = myTimestamp; - expected[5] = 123.45; - expected[6] = "b"; - expected[7] = "varc"; - expected[8] = "''"; - - rs.next(); - for (int i=0; i < expected.length; i++) { - assertEquals(rs.getObject(i + 1).toString(), expected[i].toString()); + try (Connection connection = DriverManager.getConnection(connectionString + ";useBulkCopyForBatchInsert=true;"); + SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) connection.prepareStatement(valid); + Statement stmt = (SQLServerStatement) connection.createStatement();) { + Field f1 = SQLServerConnection.class.getDeclaredField("isAzureDW"); + f1.setAccessible(true); + f1.set(connection, true); + + Timestamp myTimestamp = new Timestamp(114550L); + + Date d = new Date(114550L); + + pstmt.setInt(1, 1234); + pstmt.setBoolean(2, false); + pstmt.setString(3, "a"); + pstmt.setDate(4, d); + pstmt.setDateTime(5, myTimestamp); + pstmt.setFloat(6, (float) 123.45); + pstmt.setString(7, "b"); + pstmt.setString(8, "varc"); + pstmt.setString(9, "''"); + pstmt.addBatch(); + + pstmt.executeBatch(); + + ResultSet rs = stmt.executeQuery("SELECT * FROM " + tableName); + + Object[] expected = new Object[9]; + + expected[0] = 1234; + expected[1] = false; + expected[2] = "a"; + expected[3] = d; + expected[4] = myTimestamp; + expected[5] = 123.45; + expected[6] = "b"; + expected[7] = "varc"; + expected[8] = "''"; + + rs.next(); + for (int i=0; i < expected.length; i++) { + assertEquals(rs.getObject(i + 1).toString(), expected[i].toString()); + } } } @Test public void testMixColumns() throws Exception { - Field f1 = SQLServerConnection.class.getDeclaredField("isAzureDW"); - f1.setAccessible(true); - f1.set(connection, true); - String valid = "INSERT INTO " + tableName + " (c1, c3, c5, c8) values " + "(" + "?, " @@ -231,49 +226,50 @@ public void testMixColumns() throws Exception { + "?, " + ")"; - pstmt = (SQLServerPreparedStatement) connection.prepareStatement(valid); - stmt = (SQLServerStatement) connection.createStatement(); - - Timestamp myTimestamp = new Timestamp(114550L); - - Date d = new Date(114550L); - - pstmt.setInt(1, 1234); - pstmt.setString(2, "a"); - pstmt.setDateTime(3, myTimestamp); - pstmt.setString(4, "varc"); - pstmt.addBatch(); - - pstmt.executeBatch(); - - ResultSet rs = stmt.executeQuery("SELECT * FROM " + tableName); - - Object[] expected = new Object[9]; - - expected[0] = 1234; - expected[1] = false; - expected[2] = "a"; - expected[3] = d; - expected[4] = myTimestamp; - expected[5] = 123.45; - expected[6] = "b"; - expected[7] = "varc"; - expected[8] = "varcmax"; - - rs.next(); - for (int i=0; i < expected.length; i++) { - if (null != rs.getObject(i + 1)) { - assertEquals(rs.getObject(i + 1).toString(), expected[i].toString()); + try (Connection connection = DriverManager.getConnection(connectionString + ";useBulkCopyForBatchInsert=true;"); + SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) connection.prepareStatement(valid); + Statement stmt = (SQLServerStatement) connection.createStatement();) { + Field f1 = SQLServerConnection.class.getDeclaredField("isAzureDW"); + f1.setAccessible(true); + f1.set(connection, true); + + Timestamp myTimestamp = new Timestamp(114550L); + + Date d = new Date(114550L); + + pstmt.setInt(1, 1234); + pstmt.setString(2, "a"); + pstmt.setDateTime(3, myTimestamp); + pstmt.setString(4, "varc"); + pstmt.addBatch(); + + pstmt.executeBatch(); + + ResultSet rs = stmt.executeQuery("SELECT * FROM " + tableName); + + Object[] expected = new Object[9]; + + expected[0] = 1234; + expected[1] = false; + expected[2] = "a"; + expected[3] = d; + expected[4] = myTimestamp; + expected[5] = 123.45; + expected[6] = "b"; + expected[7] = "varc"; + expected[8] = "varcmax"; + + rs.next(); + for (int i=0; i < expected.length; i++) { + if (null != rs.getObject(i + 1)) { + assertEquals(rs.getObject(i + 1).toString(), expected[i].toString()); + } } } } @Test public void testNullOrEmptyColumns() throws Exception { - Field f1 = SQLServerConnection.class.getDeclaredField("isAzureDW"); - f1.setAccessible(true); - f1.set(connection, true); - String valid = "INSERT INTO " + tableName + " (c1, c2, c3, c4, c5, c6, c7) values " + "(" + "?, " @@ -284,47 +280,48 @@ public void testNullOrEmptyColumns() throws Exception { + "?, " + "?, " + ")"; - - pstmt = (SQLServerPreparedStatement) connection.prepareStatement(valid); - stmt = (SQLServerStatement) connection.createStatement(); - - pstmt.setInt(1, 1234); - pstmt.setBoolean(2, false); - pstmt.setString(3, null); - pstmt.setDate(4, null); - pstmt.setDateTime(5, null); - pstmt.setFloat(6, (float) 123.45); - pstmt.setString(7, ""); - pstmt.addBatch(); - - pstmt.executeBatch(); - - ResultSet rs = stmt.executeQuery("SELECT * FROM " + tableName); - - Object[] expected = new Object[9]; - expected[0] = 1234; - expected[1] = false; - expected[2] = null; - expected[3] = null; - expected[4] = null; - expected[5] = 123.45; - expected[6] = " "; - - rs.next(); - for (int i=0; i < expected.length; i++) { - if (null != rs.getObject(i + 1)) { - assertEquals(rs.getObject(i + 1), expected[i]); + try (Connection connection = DriverManager.getConnection(connectionString + ";useBulkCopyForBatchInsert=true;"); + SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) connection.prepareStatement(valid); + Statement stmt = (SQLServerStatement) connection.createStatement();) { + Field f1 = SQLServerConnection.class.getDeclaredField("isAzureDW"); + f1.setAccessible(true); + f1.set(connection, true); + + pstmt.setInt(1, 1234); + pstmt.setBoolean(2, false); + pstmt.setString(3, null); + pstmt.setDate(4, null); + pstmt.setDateTime(5, null); + pstmt.setFloat(6, (float) 123.45); + pstmt.setString(7, ""); + pstmt.addBatch(); + + pstmt.executeBatch(); + + ResultSet rs = stmt.executeQuery("SELECT * FROM " + tableName); + + Object[] expected = new Object[9]; + + expected[0] = 1234; + expected[1] = false; + expected[2] = null; + expected[3] = null; + expected[4] = null; + expected[5] = 123.45; + expected[6] = " "; + + rs.next(); + for (int i=0; i < expected.length; i++) { + if (null != rs.getObject(i + 1)) { + assertEquals(rs.getObject(i + 1), expected[i]); + } } } } @Test public void testAllFilledColumns() throws Exception { - Field f1 = SQLServerConnection.class.getDeclaredField("isAzureDW"); - f1.setAccessible(true); - f1.set(connection, true); - String valid = "INSERT INTO " + tableName + " values " + "(" + "1234, " @@ -338,141 +335,154 @@ public void testAllFilledColumns() throws Exception { + "sadf, " + ")"; - pstmt = (SQLServerPreparedStatement) connection.prepareStatement(valid); - stmt = (SQLServerStatement) connection.createStatement(); - - Timestamp myTimestamp = new Timestamp(114550L); - - Date d = new Date(114550L); - - pstmt.addBatch(); - - pstmt.executeBatch(); - - ResultSet rs = stmt.executeQuery("SELECT * FROM " + tableName); - - Object[] expected = new Object[9]; - - expected[0] = 1234; - expected[1] = false; - expected[2] = "a"; - expected[3] = null; - expected[4] = null; - expected[5] = 123.45; - expected[6] = "b"; - expected[7] = "varc"; - expected[8] = "sadf"; - - rs.next(); - for (int i=0; i < expected.length; i++) { - assertEquals(rs.getObject(i + 1), expected[i]); + try (Connection connection = DriverManager.getConnection(connectionString + ";useBulkCopyForBatchInsert=true;"); + SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) connection.prepareStatement(valid); + Statement stmt = (SQLServerStatement) connection.createStatement();) { + Field f1 = SQLServerConnection.class.getDeclaredField("isAzureDW"); + f1.setAccessible(true); + f1.set(connection, true); + + Timestamp myTimestamp = new Timestamp(114550L); + + Date d = new Date(114550L); + + pstmt.addBatch(); + + pstmt.executeBatch(); + + ResultSet rs = stmt.executeQuery("SELECT * FROM " + tableName); + + Object[] expected = new Object[9]; + + expected[0] = 1234; + expected[1] = false; + expected[2] = "a"; + expected[3] = null; + expected[4] = null; + expected[5] = 123.45; + expected[6] = "b"; + expected[7] = "varc"; + expected[8] = "sadf"; + + rs.next(); + for (int i=0; i < expected.length; i++) { + assertEquals(rs.getObject(i + 1), expected[i]); + } } } @Test public void testSquareBracketAgainstDB() throws Exception { - Field f1 = SQLServerConnection.class.getDeclaredField("isAzureDW"); - f1.setAccessible(true); - f1.set(connection, true); - stmt = (SQLServerStatement) connection.createStatement(); - - Utils.dropTableIfExists(squareBracketTableName, stmt); - String createTable = "create table " + squareBracketTableName + " (c1 int)"; - stmt.execute(createTable); - String valid = "insert into " + squareBracketTableName + " values (?)"; - pstmt = (SQLServerPreparedStatement) connection.prepareStatement(valid); - pstmt.setInt(1, 1); - pstmt.addBatch(); - - pstmt.executeBatch(); - - ResultSet rs = stmt.executeQuery("SELECT * FROM " + squareBracketTableName); - rs.next(); - assertEquals(rs.getObject(1), 1); + try (Connection connection = DriverManager.getConnection(connectionString + ";useBulkCopyForBatchInsert=true;"); + SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) connection.prepareStatement(valid); + Statement stmt = (SQLServerStatement) connection.createStatement();) { + Field f1 = SQLServerConnection.class.getDeclaredField("isAzureDW"); + f1.setAccessible(true); + f1.set(connection, true); + + Utils.dropTableIfExists(squareBracketTableName, stmt); + String createTable = "create table " + squareBracketTableName + " (c1 int)"; + stmt.execute(createTable); + + pstmt.setInt(1, 1); + pstmt.addBatch(); + + pstmt.executeBatch(); + + ResultSet rs = stmt.executeQuery("SELECT * FROM " + squareBracketTableName); + rs.next(); + + assertEquals(rs.getObject(1), 1); + } } @Test public void testDoubleQuoteAgainstDB() throws Exception { - Field f1 = SQLServerConnection.class.getDeclaredField("isAzureDW"); - f1.setAccessible(true); - f1.set(connection, true); - stmt = (SQLServerStatement) connection.createStatement(); - - Utils.dropTableIfExists(doubleQuoteTableName, stmt); - String createTable = "create table " + doubleQuoteTableName + " (c1 int)"; - stmt.execute(createTable); - String valid = "insert into " + doubleQuoteTableName + " values (?)"; - pstmt = (SQLServerPreparedStatement) connection.prepareStatement(valid); - pstmt.setInt(1, 1); - pstmt.addBatch(); - - pstmt.executeBatch(); - - ResultSet rs = stmt.executeQuery("SELECT * FROM " + doubleQuoteTableName); - rs.next(); - - assertEquals(rs.getObject(1), 1); + + try (Connection connection = DriverManager.getConnection(connectionString + ";useBulkCopyForBatchInsert=true;"); + SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) connection.prepareStatement(valid); + Statement stmt = (SQLServerStatement) connection.createStatement();) { + Field f1 = SQLServerConnection.class.getDeclaredField("isAzureDW"); + f1.setAccessible(true); + f1.set(connection, true); + + Utils.dropTableIfExists(doubleQuoteTableName, stmt); + String createTable = "create table " + doubleQuoteTableName + " (c1 int)"; + stmt.execute(createTable); + + pstmt.setInt(1, 1); + pstmt.addBatch(); + + pstmt.executeBatch(); + + ResultSet rs = stmt.executeQuery("SELECT * FROM " + doubleQuoteTableName); + rs.next(); + + assertEquals(rs.getObject(1), 1); + } } @Test public void testSchemaAgainstDB() throws Exception { - Field f1 = SQLServerConnection.class.getDeclaredField("isAzureDW"); - f1.setAccessible(true); - f1.set(connection, true); - stmt = (SQLServerStatement) connection.createStatement(); - - Utils.dropTableIfExists("[dbo]." + squareBracketTableName, stmt); String schemaTableName = "\"dbo\" . /*some comment */ " + squareBracketTableName; - - String createTable = "create table " + schemaTableName + " (c1 int)"; - stmt.execute(createTable); - String valid = "insert into " + schemaTableName + " values (?)"; - pstmt = (SQLServerPreparedStatement) connection.prepareStatement(valid); - pstmt.setInt(1, 1); - pstmt.addBatch(); - - pstmt.executeBatch(); - - ResultSet rs = stmt.executeQuery("SELECT * FROM " + schemaTableName); - rs.next(); - - assertEquals(rs.getObject(1), 1); + + try (Connection connection = DriverManager.getConnection(connectionString + ";useBulkCopyForBatchInsert=true;"); + SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) connection.prepareStatement(valid); + Statement stmt = (SQLServerStatement) connection.createStatement();) { + Field f1 = SQLServerConnection.class.getDeclaredField("isAzureDW"); + f1.setAccessible(true); + f1.set(connection, true); + + Utils.dropTableIfExists("[dbo]." + squareBracketTableName, stmt); + + String createTable = "create table " + schemaTableName + " (c1 int)"; + stmt.execute(createTable); + + pstmt.setInt(1, 1); + pstmt.addBatch(); + + pstmt.executeBatch(); + + ResultSet rs = stmt.executeQuery("SELECT * FROM " + schemaTableName); + rs.next(); + + assertEquals(rs.getObject(1), 1); + } } @Test public void testColumnNameMixAgainstDB() throws Exception { - Field f1 = SQLServerConnection.class.getDeclaredField("isAzureDW"); - f1.setAccessible(true); - f1.set(connection, true); - stmt = (SQLServerStatement) connection.createStatement(); - - Utils.dropTableIfExists(squareBracketTableName, stmt); - String createTable = "create table " + squareBracketTableName + " ([c]]]]1] int, [c]]]]2] int)"; - stmt.execute(createTable); - String valid = "insert into " + squareBracketTableName + " ([c]]]]1], [c]]]]2]) values (?, 1)"; - pstmt = (SQLServerPreparedStatement) connection.prepareStatement(valid); - pstmt.setInt(1, 1); - pstmt.addBatch(); - - pstmt.executeBatch(); - - ResultSet rs = stmt.executeQuery("SELECT * FROM " + squareBracketTableName); - rs.next(); - - assertEquals(rs.getObject(1), 1); + + try (Connection connection = DriverManager.getConnection(connectionString + ";useBulkCopyForBatchInsert=true;"); + SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) connection.prepareStatement(valid); + Statement stmt = (SQLServerStatement) connection.createStatement();) { + Field f1 = SQLServerConnection.class.getDeclaredField("isAzureDW"); + f1.setAccessible(true); + f1.set(connection, true); + + Utils.dropTableIfExists(squareBracketTableName, stmt); + String createTable = "create table " + squareBracketTableName + " ([c]]]]1] int, [c]]]]2] int)"; + stmt.execute(createTable); + + pstmt.setInt(1, 1); + pstmt.addBatch(); + + pstmt.executeBatch(); + + ResultSet rs = stmt.executeQuery("SELECT * FROM " + squareBracketTableName); + rs.next(); + + assertEquals(rs.getObject(1), 1); + } } @Test public void testAlColumnsLargeBatch() throws Exception { - Field f1 = SQLServerConnection.class.getDeclaredField("isAzureDW"); - f1.setAccessible(true); - f1.set(connection, true); - String valid = "INSERT INTO " + tableName + " values " + "(" + "?, " @@ -486,86 +496,82 @@ public void testAlColumnsLargeBatch() throws Exception { + "?, " + ")"; - pstmt = (SQLServerPreparedStatement) connection.prepareStatement(valid); - stmt = (SQLServerStatement) connection.createStatement(); - - Timestamp myTimestamp = new Timestamp(114550L); - - Date d = new Date(114550L); - - pstmt.setInt(1, 1234); - pstmt.setBoolean(2, false); - pstmt.setString(3, "a"); - pstmt.setDate(4, d); - pstmt.setDateTime(5, myTimestamp); - pstmt.setFloat(6, (float) 123.45); - pstmt.setString(7, "b"); - pstmt.setString(8, "varc"); - pstmt.setString(9, "''"); - pstmt.addBatch(); - - pstmt.executeLargeBatch(); - - ResultSet rs = stmt.executeQuery("SELECT * FROM " + tableName); - - Object[] expected = new Object[9]; - - expected[0] = 1234; - expected[1] = false; - expected[2] = "a"; - expected[3] = d; - expected[4] = myTimestamp; - expected[5] = 123.45; - expected[6] = "b"; - expected[7] = "varc"; - expected[8] = "''"; - - rs.next(); - for (int i=0; i < expected.length; i++) { - assertEquals(rs.getObject(i + 1).toString(), expected[i].toString()); + try (Connection connection = DriverManager.getConnection(connectionString + ";useBulkCopyForBatchInsert=true;"); + SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) connection.prepareStatement(valid); + Statement stmt = (SQLServerStatement) connection.createStatement();) { + Field f1 = SQLServerConnection.class.getDeclaredField("isAzureDW"); + f1.setAccessible(true); + f1.set(connection, true); + + Timestamp myTimestamp = new Timestamp(114550L); + + Date d = new Date(114550L); + + pstmt.setInt(1, 1234); + pstmt.setBoolean(2, false); + pstmt.setString(3, "a"); + pstmt.setDate(4, d); + pstmt.setDateTime(5, myTimestamp); + pstmt.setFloat(6, (float) 123.45); + pstmt.setString(7, "b"); + pstmt.setString(8, "varc"); + pstmt.setString(9, "''"); + pstmt.addBatch(); + + pstmt.executeLargeBatch(); + + ResultSet rs = stmt.executeQuery("SELECT * FROM " + tableName); + + Object[] expected = new Object[9]; + + expected[0] = 1234; + expected[1] = false; + expected[2] = "a"; + expected[3] = d; + expected[4] = myTimestamp; + expected[5] = 123.45; + expected[6] = "b"; + expected[7] = "varc"; + expected[8] = "''"; + + rs.next(); + for (int i=0; i < expected.length; i++) { + assertEquals(rs.getObject(i + 1).toString(), expected[i].toString()); + } } } @BeforeEach public void testSetup() throws TestAbortedException, Exception { - connection = DriverManager.getConnection(connectionString + ";useBulkCopyForBatchInsert=true;"); - stmt = (SQLServerStatement) connection.createStatement(); - - Utils.dropTableIfExists(tableName, stmt); - String sql1 = "create table " + tableName + " " - + "(" - + "c1 int DEFAULT 1234, " - + "c2 bit, " - + "c3 char DEFAULT NULL, " - + "c4 date, " - + "c5 datetime2, " - + "c6 float, " - + "c7 nchar, " - + "c8 varchar(20), " - + "c9 varchar(max)" - + ")"; - - stmt.execute(sql1); - stmt.close(); + try (Connection connection = DriverManager.getConnection(connectionString + ";useBulkCopyForBatchInsert=true;")) { + try (Statement stmt = (SQLServerStatement) connection.createStatement()) { + Utils.dropTableIfExists(tableName, stmt); + String sql1 = "create table " + tableName + " " + + "(" + + "c1 int DEFAULT 1234, " + + "c2 bit, " + + "c3 char DEFAULT NULL, " + + "c4 date, " + + "c5 datetime2, " + + "c6 float, " + + "c7 nchar, " + + "c8 varchar(20), " + + "c9 varchar(max)" + + ")"; + + stmt.execute(sql1); + } + } } @AfterAll public static void terminateVariation() throws SQLException { - connection = DriverManager.getConnection(connectionString); - - stmt = (SQLServerStatement) connection.createStatement(); - Utils.dropTableIfExists(tableName, stmt); - Utils.dropTableIfExists(squareBracketTableName, stmt); - Utils.dropTableIfExists(doubleQuoteTableName, stmt); - - if (null != pstmt) { - pstmt.close(); - } - if (null != stmt) { - stmt.close(); - } - if (null != connection) { - connection.close(); + try (Connection connection = DriverManager.getConnection(connectionString)) { + try (Statement stmt = (SQLServerStatement) connection.createStatement()) { + Utils.dropTableIfExists(tableName, stmt); + Utils.dropTableIfExists(squareBracketTableName, stmt); + Utils.dropTableIfExists(doubleQuoteTableName, stmt); + } } } } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/RegressionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/RegressionTest.java index ce51ee8e8..2596042f2 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/RegressionTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/RegressionTest.java @@ -21,13 +21,11 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.platform.runner.JUnitPlatform; import org.junit.runner.RunWith; import com.microsoft.sqlserver.jdbc.SQLServerConnection; -import com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement; import com.microsoft.sqlserver.testframework.AbstractTest; import com.microsoft.sqlserver.testframework.Utils; import com.microsoft.sqlserver.jdbc.TestResource; @@ -50,7 +48,7 @@ public class RegressionTest extends AbstractTest { * * @throws SQLException */ - @BeforeEach + @BeforeAll public static void setupTest() throws SQLException { con = DriverManager.getConnection(connectionString); Statement stmt = con.createStatement(); @@ -226,106 +224,109 @@ public void batchWithLargeStringTestUseBulkCopyAPI() throws Exception { } private void batchWithLargeStringTestInternal(String mode) throws Exception { - if (mode.equalsIgnoreCase("bulkcopy")) { - modifyConnectionForBulkCopyAPI((SQLServerConnection) con); - } - Statement stmt = con.createStatement(); - PreparedStatement pstmt = null; - ResultSet rs = null; - Utils.dropTableIfExists("TEST_TABLE", stmt); - - con.setAutoCommit(false); - - // create a table with two columns - boolean createPrimaryKey = false; - try { - stmt.execute("if object_id('TEST_TABLE', 'U') is not null\ndrop table TEST_TABLE;"); - if (createPrimaryKey) { - stmt.execute("create table TEST_TABLE ( ID int, DATA nvarchar(max), primary key (ID) );"); - } - else { - stmt.execute("create table TEST_TABLE ( ID int, DATA nvarchar(max) );"); + try (Connection con = DriverManager.getConnection(connectionString);) { + if (mode.equalsIgnoreCase("bulkcopy")) { + modifyConnectionForBulkCopyAPI((SQLServerConnection) con); } - } - catch (Exception e) { - fail(e.toString()); - } - con.commit(); - - // build a String with 4001 characters - StringBuilder stringBuilder = new StringBuilder(); - for (int i = 0; i < 4001; i++) { - stringBuilder.append('c'); - } - String largeString = stringBuilder.toString(); - - String[] values = {"a", "b", largeString, "d", "e"}; - // insert five rows into the table; use a batch for each row - try { - pstmt = con.prepareStatement("insert into TEST_TABLE values (?,?)"); - // 0,a - pstmt.setInt(1, 0); - pstmt.setNString(2, values[0]); - pstmt.addBatch(); + Statement stmt = con.createStatement(); + PreparedStatement pstmt = null; + ResultSet rs = null; + Utils.dropTableIfExists("TEST_TABLE", stmt); - // 1,b - pstmt.setInt(1, 1); - pstmt.setNString(2, values[1]); - pstmt.addBatch(); + con.setAutoCommit(false); - // 2,ccc... - pstmt.setInt(1, 2); - pstmt.setNString(2, values[2]); - pstmt.addBatch(); + // create a table with two columns + boolean createPrimaryKey = false; + try { + stmt.execute("if object_id('TEST_TABLE', 'U') is not null\ndrop table TEST_TABLE;"); + if (createPrimaryKey) { + stmt.execute("create table TEST_TABLE ( ID int, DATA nvarchar(max), primary key (ID) );"); + } + else { + stmt.execute("create table TEST_TABLE ( ID int, DATA nvarchar(max) );"); + } + } + catch (Exception e) { + fail(e.toString()); + } - // 3,d - pstmt.setInt(1, 3); - pstmt.setNString(2, values[3]); - pstmt.addBatch(); + con.commit(); - // 4,e - pstmt.setInt(1, 4); - pstmt.setNString(2, values[4]); - pstmt.addBatch(); + // build a String with 4001 characters + StringBuilder stringBuilder = new StringBuilder(); + for (int i = 0; i < 4001; i++) { + stringBuilder.append('c'); + } + String largeString = stringBuilder.toString(); - pstmt.executeBatch(); - } - catch (Exception e) { - fail(e.toString()); - } - connection.commit(); + String[] values = {"a", "b", largeString, "d", "e"}; + // insert five rows into the table; use a batch for each row + try { + pstmt = con.prepareStatement("insert into TEST_TABLE values (?,?)"); + // 0,a + pstmt.setInt(1, 0); + pstmt.setNString(2, values[0]); + pstmt.addBatch(); + + // 1,b + pstmt.setInt(1, 1); + pstmt.setNString(2, values[1]); + pstmt.addBatch(); + + // 2,ccc... + pstmt.setInt(1, 2); + pstmt.setNString(2, values[2]); + pstmt.addBatch(); + + // 3,d + pstmt.setInt(1, 3); + pstmt.setNString(2, values[3]); + pstmt.addBatch(); + + // 4,e + pstmt.setInt(1, 4); + pstmt.setNString(2, values[4]); + pstmt.addBatch(); + + pstmt.executeBatch(); + } + catch (Exception e) { + fail(e.toString()); + } + con.commit(); - // check the data in the table - Map selectedValues = new LinkedHashMap<>(); - int id = 0; - try { - pstmt = con.prepareStatement("select * from TEST_TABLE;"); + // check the data in the table + Map selectedValues = new LinkedHashMap<>(); + int id = 0; try { - rs = pstmt.executeQuery(); - int i = 0; - while (rs.next()) { - id = rs.getInt(1); - String data = rs.getNString(2); - if (selectedValues.containsKey(id)) { - fail("Found duplicate id: " + id + " ,actual values is : " + values[i++] + " data is: " + data); + pstmt = con.prepareStatement("select * from TEST_TABLE;"); + try { + rs = pstmt.executeQuery(); + int i = 0; + while (rs.next()) { + id = rs.getInt(1); + String data = rs.getNString(2); + if (selectedValues.containsKey(id)) { + fail("Found duplicate id: " + id + " ,actual values is : " + values[i++] + " data is: " + data); + } + selectedValues.put(id, data); + } + } + finally { + if (null != rs) { + rs.close(); } - selectedValues.put(id, data); } } finally { - if (null != rs) { - rs.close(); + Utils.dropTableIfExists("TEST_TABLE", stmt); + if (null != pstmt) { + pstmt.close(); + } + if (null != stmt) { + stmt.close(); } - } - } - finally { - Utils.dropTableIfExists("TEST_TABLE", stmt); - if (null != pstmt) { - pstmt.close(); - } - if (null != stmt) { - stmt.close(); } } } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/BatchExecutionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/BatchExecutionTest.java index 66b100a47..a4e7004be 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/BatchExecutionTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/BatchExecutionTest.java @@ -12,7 +12,6 @@ import static org.junit.jupiter.api.Assumptions.assumeTrue; import java.lang.reflect.Field; -import java.sql.BatchUpdateException; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; @@ -22,7 +21,6 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.platform.runner.JUnitPlatform; import org.junit.runner.RunWith; @@ -89,7 +87,7 @@ private void testExecuteBatch1Internal(String mode) { int i = 0; int retValue[] = {0, 0, 0}; int updateCountlen = 0; - try { + try (Connection connection = DriverManager.getConnection(connectionString + ";columnEncryptionSetting=Enabled;");){ String sPrepStmt = "update ctstable2 set PRICE=PRICE*20 where TYPE_ID=?"; if (mode.equalsIgnoreCase("bulkcopy")) { @@ -160,7 +158,7 @@ private static void createTable() throws SQLException { private void testAddBatch1Internal(String mode) { int i = 0; int retValue[] = {0, 0, 0}; - try { + try (Connection connection = DriverManager.getConnection(connectionString + ";columnEncryptionSetting=Enabled;");){ String sPrepStmt = "update ctstable2 set PRICE=PRICE*20 where TYPE_ID=?"; if (mode.equalsIgnoreCase("bulkcopy")) { @@ -216,7 +214,7 @@ private void modifyConnectionForBulkCopyAPI(SQLServerConnection con) throws Exce con.setUseBulkCopyForBatchInsert(true); } - @BeforeEach + @BeforeAll public static void testSetup() throws TestAbortedException, Exception { assumeTrue(13 <= new DBConnection(connectionString).getServerVersion(), TestResource.getResource("R_Incompat_SQLServerVersion"));