diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ICounter.java b/src/main/java/com/microsoft/sqlserver/jdbc/ICounter.java new file mode 100644 index 000000000..dcba3944a --- /dev/null +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ICounter.java @@ -0,0 +1,27 @@ +/* + * 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; + +/** + * Interface for MaxResultBufferCounter + */ +interface ICounter { + + /** + * Increases the state of Counter + * + * @param bytes + * Number of bytes to increase state + * @throws SQLServerException + * Exception is thrown, when limit of Counter is exceeded + */ + void increaseCounter(long bytes) throws SQLServerException; + + /** + * Resets the state of Counter + */ + void resetCounter(); +} diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java index 8f479d0f3..c164c50fa 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java @@ -51,6 +51,7 @@ import java.util.Locale; import java.util.Map; import java.util.Map.Entry; +import java.util.Properties; import java.util.Set; import java.util.SimpleTimeZone; import java.util.TimeZone; @@ -3156,6 +3157,15 @@ boolean isEOMSent() { traceID = "TDSWriter@" + Integer.toHexString(hashCode()) + " (" + con.toString() + ")"; } + /** + * Checks If tdsMessageType is RPC or QUERY + * + * @return boolean + */ + boolean checkIfTdsMessageTypeIsBatchOrRPC() { + return tdsMessageType == TDS.PKT_QUERY || tdsMessageType == TDS.PKT_RPC; + } + // TDS message start/end operations void preparePacket() throws SQLServerException { @@ -6546,6 +6556,11 @@ private boolean nextPacket() throws SQLServerException { // This action must be synchronized against against another thread calling // readAllPackets() to read in ALL of the remaining packets of the current response. if (null == consumedPacket.next) { + // if the read comes from getNext() and responseBuffering is Adaptive (in this place is), then reset Counter + // State + if (command.getTDSWriter().checkIfTdsMessageTypeIsBatchOrRPC()) { + command.getCounter().resetCounter(); + } readPacket(); if (null == consumedPacket.next) @@ -6641,6 +6656,11 @@ synchronized final boolean readPacket() throws SQLServerException { System.arraycopy(newPacket.header, 0, logBuffer, 0, TDS.PACKET_HEADER_SIZE); } + // if messageType is RPC or QUERY, then increment Counter's state + if (tdsChannel.getWriter().checkIfTdsMessageTypeIsBatchOrRPC()) { + command.getCounter().increaseCounter(packetLength); + } + // Now for the payload... for (int payloadBytesRead = 0; payloadBytesRead < newPacket.payloadLength;) { int bytesRead = tdsChannel.read(newPacket.payload, payloadBytesRead, @@ -7345,6 +7365,23 @@ final boolean readingResponse() { protected ArrayList enclaveCEKs; + // Counter reference, so maxResultBuffer property can by acknowledged + private ICounter counter; + + ICounter getCounter() { + return counter; + } + + void createCounter(ICounter previousCounter, Properties activeConnectionProperties) { + if (null == previousCounter) { + String maxResultBuffer = activeConnectionProperties + .getProperty(SQLServerDriverStringProperty.MAX_RESULT_BUFFER.toString()); + counter = new MaxResultBufferCounter(Long.parseLong(maxResultBuffer)); + } else { + counter = previousCounter; + } + } + /** * Creates this command with an optional timeout. * diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java index 9288d59a4..871528628 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java @@ -1051,4 +1051,19 @@ public interface ISQLServerDataSource extends javax.sql.CommonDataSource { * Active Directory Service Principal secret. */ void setAADSecurePrincipalSecret(String AADSecurePrincipalSecret); + + /** + * Returns value of 'maxResultBuffer' from Connection String. + * + * @return 'maxResultBuffer' property. + */ + String getMaxResultBuffer(); + + /** + * Specifies value for 'maxResultBuffer' property + * + * @param maxResultBuffer + * String value for 'maxResultBuffer' + */ + void setMaxResultBuffer(String maxResultBuffer); } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/MaxResultBufferCounter.java b/src/main/java/com/microsoft/sqlserver/jdbc/MaxResultBufferCounter.java new file mode 100644 index 000000000..82c56b71e --- /dev/null +++ b/src/main/java/com/microsoft/sqlserver/jdbc/MaxResultBufferCounter.java @@ -0,0 +1,52 @@ +/* + * 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.util.logging.Level; +import java.util.logging.Logger; + + +/** + * Implementation of ICounter for 'maxResultBuffer' property. + */ +public class MaxResultBufferCounter implements ICounter { + + private final Logger logger = Logger.getLogger("com.microsoft.sqlserver.jdbc.MaxResultBufferCounter"); + + private long counter = 0; + private final long maxResultBuffer; + + public MaxResultBufferCounter(long maxResultBuffer) { + this.maxResultBuffer = maxResultBuffer; + } + + public void increaseCounter(long bytes) throws SQLServerException { + if (maxResultBuffer > 0) { + counter += bytes; + checkForMaxResultBufferOverflow(counter); + } + } + + public void resetCounter() { + counter = 0; + } + + private void checkForMaxResultBufferOverflow(long number) throws SQLServerException { + if (number > maxResultBuffer) { + if (logger.isLoggable(Level.SEVERE)) { + logger.log(Level.SEVERE, SQLServerException.getErrString("R_maxResultBufferPropertyExceeded"), + new Object[] {number, maxResultBuffer}); + } + throwExceededMaxResultBufferException(counter, maxResultBuffer); + } + } + + private void throwExceededMaxResultBufferException(Object... arguments) throws SQLServerException { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_maxResultBufferPropertyExceeded")); + throw new SQLServerException(form.format(arguments), null); + } +} diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/MaxResultBufferParser.java b/src/main/java/com/microsoft/sqlserver/jdbc/MaxResultBufferParser.java new file mode 100644 index 000000000..88f83ec6a --- /dev/null +++ b/src/main/java/com/microsoft/sqlserver/jdbc/MaxResultBufferParser.java @@ -0,0 +1,151 @@ +/* + * 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.lang.management.ManagementFactory; +import java.text.MessageFormat; +import java.util.logging.Level; +import java.util.logging.Logger; + + +/** + * Parser created to parse String value from Connection String to equivalent number of bytes for JDBC Driver to work on. + */ +public class MaxResultBufferParser { + + private static final Logger logger = Logger.getLogger("com.microsoft.sqlserver.jdbc.MaxResultBufferParser"); + private static final String[] PERCENT_PHRASES = {"percent", "pct", "p"}; + private static final String ERROR_MESSAGE = "MaxResultBuffer property is badly formatted: {0}."; + + private MaxResultBufferParser() {} + + /** + * + * Returns number of bytes for maxResultBuffer property + * + * @param input + * String value for maxResultProperty provided in Connection String + * @return 'maxResultBuffer' property as number of bytes + * @throws SQLServerException + * Is Thrown when maxResultProperty's syntax is wrong + */ + public static long validateMaxResultBuffer(String input) throws SQLServerException { + String numberString; + long number = -1; + + // check for null values and empty String "", if so return -1 (default value) + if (StringUtils.isEmpty(input) || input.equals("-1")) { + return number; + } + + // check if input is number + if (!StringUtils.isEmpty(input) && input.matches("-?\\d+(\\.\\d+)?")) { + try { + number = Long.parseLong(input); + } catch (NumberFormatException e) { + if (logger.isLoggable(Level.SEVERE)) { + logger.log(Level.SEVERE, ERROR_MESSAGE, new Object[] {input}); + } + throwNewInvalidMaxResultBufferParameterException(e, input); + } + return adjustMemory(number, 1); + } else { + // check PERCENT_PHRASES + for (String percentPhrase : PERCENT_PHRASES) { + if (input.endsWith(percentPhrase)) { + numberString = input.substring(0, input.length() - percentPhrase.length()); + try { + number = Long.parseLong(numberString); + } catch (NumberFormatException e) { + if (logger.isLoggable(Level.SEVERE)) { + logger.log(Level.SEVERE, ERROR_MESSAGE, new Object[] {input}); + } + throwNewInvalidMaxResultBufferParameterException(e, numberString); + } + return adjustMemoryPercentage(number); + } + } + + // check if prefix was supplied + long multiplier = getMultiplier(input); + numberString = input.substring(0, input.length() - 1); + + try { + number = Long.parseLong(numberString); + } catch (NumberFormatException e) { + if (logger.isLoggable(Level.SEVERE)) { + logger.log(Level.SEVERE, ERROR_MESSAGE, new Object[] {input}); + } + throwNewInvalidMaxResultBufferParameterException(e, numberString); + } + return adjustMemory(number, multiplier); + } + } + + private static void checkForNegativeValue(long value) throws SQLServerException { + if (value <= 0) { + Object[] objectToThrow = new Object[] {value}; + MessageFormat form = new MessageFormat( + SQLServerException.getErrString("R_maxResultBufferNegativeParameterValue")); + if (logger.isLoggable(Level.SEVERE)) { + logger.log(Level.SEVERE, SQLServerException.getErrString("R_maxResultBufferNegativeParameterValue"), + objectToThrow); + } + throw new SQLServerException(form.format(objectToThrow), new Throwable()); + } + } + + private static long getMultiplier(String input) throws SQLServerException { + long multiplier = 1; + switch (Character.toUpperCase(input.charAt(input.length() - 1))) { + case 'K': + multiplier = 1_000L; + break; + case 'M': + multiplier = 1_000_000L; + break; + case 'G': + multiplier = 1_000_000_000L; + break; + case 'T': + multiplier = 1_000_000_000_000L; + break; + default: + if (logger.isLoggable(Level.SEVERE)) { + logger.log(Level.SEVERE, ERROR_MESSAGE, new Object[] {input}); + } + throwNewInvalidMaxResultBufferParameterException(null, input); + } + return multiplier; + } + + private static long adjustMemoryPercentage(long percentage) throws SQLServerException { + checkForNegativeValue(percentage); + if (percentage > 90) + return (long) (0.9 * getMaxMemory()); + else + return (long) ((percentage) / 100.0 * getMaxMemory()); + } + + private static long adjustMemory(long size, long multiplier) throws SQLServerException { + checkForNegativeValue(size); + if (size * multiplier > 0.9 * getMaxMemory()) + return (long) (0.9 * getMaxMemory()); + else + return size * multiplier; + } + + private static long getMaxMemory() { + return ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getMax(); + } + + private static void throwNewInvalidMaxResultBufferParameterException(Throwable cause, + Object... arguments) throws SQLServerException { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_maxResultBufferInvalidSyntax")); + throw new SQLServerException(form.format(arguments), cause); + } + +} diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index 800bc01f8..749229e86 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -2196,6 +2196,11 @@ else if (0 == requestedPacketSize) sendTemporalDataTypesAsStringForBulkCopy = isBooleanPropertyOn(sPropKey, sPropValue); } + sPropKey = SQLServerDriverStringProperty.MAX_RESULT_BUFFER.toString(); + sPropValue = activeConnectionProperties.getProperty(sPropKey); + activeConnectionProperties.setProperty(sPropKey, + String.valueOf(MaxResultBufferParser.validateMaxResultBuffer(sPropValue))); + sPropKey = SQLServerDriverBooleanProperty.DELAY_LOADING_LOBS.toString(); sPropValue = activeConnectionProperties.getProperty(sPropKey); if (null == sPropValue) { @@ -3216,6 +3221,7 @@ final void terminate(int driverErrorCode, String message, Throwable throwable) t */ boolean executeCommand(TDSCommand newCommand) throws SQLServerException { synchronized (schedulerLock) { + ICounter previousCounter = null; /* * Detach (buffer) the response from any previously executing command so that we can execute the new * command. Note that detaching the response does not process it. Detaching just buffers the response off of @@ -3223,6 +3229,12 @@ boolean executeCommand(TDSCommand newCommand) throws SQLServerException { */ if (null != currentCommand) { try { + + /** + * If currentCommand needs to be detached, reset Counter to acknowledge number of Bytes in remaining + * packets + */ + currentCommand.getCounter().resetCounter(); currentCommand.detach(); } catch (SQLServerException e) { /* @@ -3234,10 +3246,14 @@ boolean executeCommand(TDSCommand newCommand) throws SQLServerException { connectionlogger.fine("Failed to detach current command : " + e.getMessage()); } } finally { + previousCounter = currentCommand.getCounter(); currentCommand = null; } } - + /** + * Add Counter reference to newCommand + */ + newCommand.createCounter(previousCounter, activeConnectionProperties); /* * The implementation of this scheduler is pretty simple... Since only one command at a time may use a * connection (to avoid TDS protocol errors), just synchronize to serialize command execution. diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java index 78306fd5e..59776ceec 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java @@ -1056,6 +1056,17 @@ public void setSendTemporalDataTypesAsStringForBulkCopy(boolean sendTemporalData sendTemporalDataTypesAsStringForBulkCopy); } + @Override + public String getMaxResultBuffer() { + return getStringProperty(connectionProps, SQLServerDriverStringProperty.MAX_RESULT_BUFFER.toString(), + SQLServerDriverStringProperty.MAX_RESULT_BUFFER.getDefaultValue()); + } + + @Override + public void setMaxResultBuffer(String maxResultBuffer) { + setStringProperty(connectionProps, SQLServerDriverStringProperty.MAX_RESULT_BUFFER.toString(), maxResultBuffer); + } + /** * Sets a property string value. * diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java index 8b7e5b5a0..3278559f1 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java @@ -372,7 +372,8 @@ enum SQLServerDriverStringProperty { CLIENT_KEY("clientKey", ""), CLIENT_KEY_PASSWORD("clientKeyPassword", ""), AAD_SECURE_PRINCIPAL_ID("AADSecurePrincipalId", ""), - AAD_SECURE_PRINCIPAL_SECRET("AADSecurePrincipalSecret", ""); + AAD_SECURE_PRINCIPAL_SECRET("AADSecurePrincipalSecret", ""), + MAX_RESULT_BUFFER("maxResultBuffer", "-1"); private final String name; private final String defaultValue; @@ -646,7 +647,9 @@ public final class SQLServerDriver implements java.sql.Driver { new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.AAD_SECURE_PRINCIPAL_ID.toString(), SQLServerDriverStringProperty.AAD_SECURE_PRINCIPAL_ID.getDefaultValue(), false, null), new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.AAD_SECURE_PRINCIPAL_SECRET.toString(), - SQLServerDriverStringProperty.AAD_SECURE_PRINCIPAL_SECRET.getDefaultValue(), false, null)}; + SQLServerDriverStringProperty.AAD_SECURE_PRINCIPAL_SECRET.getDefaultValue(), false, null), + new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.MAX_RESULT_BUFFER.toString(), + SQLServerDriverStringProperty.MAX_RESULT_BUFFER.getDefaultValue(), false, null),}; /** * Properties that can only be set by using Properties. Cannot set in connection string diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java index e772d9d08..1be1847f5 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java @@ -652,5 +652,10 @@ protected Object[][] getContents() { {"R_unassignableError", "The class specified by the {0} property must be assignable to {1}."}, {"R_InvalidCSVQuotes", "Failed to parse the CSV file, verify that the fields are correctly enclosed in double quotes."}, - {"R_TokenRequireUrl", "Token credentials require a URL using the HTTPS protocol scheme."},}; -}; + {"R_TokenRequireUrl", "Token credentials require a URL using the HTTPS protocol scheme."}, + {"R_maxResultBufferPropertyDescription", + "Determines maximum amount of bytes that can be read during retrieval of result set"}, + {"R_maxResultBufferInvalidSyntax", "Invalid syntax: {0} in maxResultBuffer parameter."}, + {"R_maxResultBufferNegativeParameterValue", "MaxResultBuffer must have positive value: {0}."}, + {"R_maxResultBufferPropertyExceeded", "MaxResultBuffer property exceeded: {0}. MaxResultBuffer was set to: {1}."},}; +} diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/MaxResultBufferParserTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/MaxResultBufferParserTest.java new file mode 100644 index 000000000..ebfea27eb --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/MaxResultBufferParserTest.java @@ -0,0 +1,108 @@ +/* + * 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 org.junit.jupiter.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.lang.management.ManagementFactory; +import java.util.Arrays; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + + +/** + * A class for testing MaxResultBufferParser functionality. + */ +class MaxResultBufferParserTest { + + /** + * Method with input data for testValidateMaxResultBuffer Tests + */ + public static Iterable data() { + return Arrays.asList(new Object[][] { + {"10p", (long) (0.1 * getMaxMemory())}, + {"010p", (long) (0.1 * getMaxMemory())}, + {"10pct", (long) (0.1 * getMaxMemory())}, + {"10percent", (long) (0.1 * getMaxMemory())}, + {"100", 100}, + {"0100", 100}, + {"100k", 100 * 1000}, + {"0100k", 100 * 1000}, + {"100K", 100 * 1000}, + {"0100K", 100 * 1000}, + {"100m", 100 * 1000 * 1000}, + {"100M", 100 * 1000 * 1000}, + // these values are too big (assuming heap size is 4GB) + {"200p", (long) (0.9 * getMaxMemory())}, + {"0200p", (long) (0.9 * getMaxMemory())}, + {"200pct", (long) (0.9 * getMaxMemory())}, + {"200percent", (long) (0.9 * getMaxMemory())}, + {"100g", (long) (0.9 * getMaxMemory())}, + {"100G", (long) (0.9 * getMaxMemory())}, + {"100t", (long) (0.9 * getMaxMemory())}, + {"100T", (long) (0.9 * getMaxMemory())}, + //when maxResultBuffer property is not supplied, assume -1 + {"", -1}, + {null, -1}, + {"-1", -1}, + }); + } + + /** + * Method with input data for testValidateMaxResultBufferException Tests + */ + public static Iterable exceptionData() { + return Arrays.asList(new Object[][] { + {"-123p"}, {"-423pct"}, {"-100m"}, {"-500K"}, {"-123"},// values are correctly formatted, but they're negative + {"123precd"}, {"456pc"}, // percent phrases are misspelled + {"32P"}, {"-456PCT"}, {"150PERCENT"}, // percent phrases are correct, but they're in upper Case also middle one is negative + {"0101D"}, {"100l"}, {"-100L"}, // incorrect prefixes, last value is also negative + {"1@D"}, // incorrect prefix and malformed value as well + {"0"}, {"0t"}, {"0T"}, {"0p"}, {"0pct"}, {"0percent"}, // 0 is not positive, maxResultBuffer must have positive value + {"0.5"}, {"0.5g"}, {"0.5G"}, {"0.5p"}, {"0.5pct"}, {"0.5percent"}, // maxResultBuffer must be whole number + {" "}, {"ASD"}, {"@!|?:'{}"}, {"a5D"} // malformed values + }); + } + + /** + * + * Tests for correctly formatted input String + * + * @param input + * MaxResultBuffer property + * @param expected + * Expected number of bytes + */ + @ParameterizedTest + @MethodSource("data") + void testValidateMaxResultBuffer(String input, long expected) { + try { + assertEquals(expected, MaxResultBufferParser.validateMaxResultBuffer(input)); + } catch (SQLServerException throwables) { + fail(); + } + } + + /** + * + * Tests for badly formatted maxResultProperty + * + * @param input + * Badly formatted MaxResultBuffer property + */ + @ParameterizedTest + @MethodSource("exceptionData") + void testValidateMaxResultBufferException(String input) { + Assertions.assertThrows(SQLServerException.class, () -> MaxResultBufferParser.validateMaxResultBuffer(input)); + } + + private static long getMaxMemory() { + return ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getMax(); + } +} diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/MaxResultBufferTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/MaxResultBufferTest.java new file mode 100644 index 000000000..594e4f709 --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/MaxResultBufferTest.java @@ -0,0 +1,450 @@ +/* + * 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 com.microsoft.sqlserver.testframework.AbstractSQLGenerator; +import com.microsoft.sqlserver.testframework.AbstractTest; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Arrays; +import java.util.Random; + +import static org.junit.Assert.fail; + + +/** + * Class for testing maxResultBuffer property, all tests were performed on default connection settings + * (defaultPacketLength = 8000), only changed were ResponsiveBuffering and MaxResultBuffer + */ +@DisplayName("maxResultBuffer Tests") +public class MaxResultBufferTest extends AbstractTest { + + @SuppressWarnings("SqlResolve") + private static final String TEST_TABLE_NAME = "maxResultBufferTestTable"; + private static String localConnectionString; + + /** + * This copies value of localConnectionString from connectionString for each test + */ + @BeforeEach + void prepareMaxResultBuffer() { + localConnectionString = new String(connectionString); + } + + /** + * Create TEST_TABLE with 1 column nchar(precision) with numberOfRows. Let's calculate payload on example: + * numberOfRows = 800, precision = 10 + * + * Payload (in Bytes) = 49 (Header plus column metadata) + numberOfRows * (precision * 2 + 1 + 2) (3 extra bytes are + * for column length and end of line character) + * + * So payload generated by this method = 49 + 800 * (10 * 2 + 2 + 1) = 49 + 800 * 23 = 18449 + * + * Default packetLength = 8000, so payload is sent in 3 packets + * + * @throws SQLException + * Signalizes error when creating TEST_TABLE + */ + @BeforeAll + static void createAndPopulateNCharTestTable() throws SQLException { + String insertSQL = "INSERT INTO " + AbstractSQLGenerator.escapeIdentifier(TEST_TABLE_NAME) + " VALUES (?)"; + int numberOfRows = 800; + int precision = 10; + + try (Connection connection = DriverManager.getConnection(connectionString); + Statement statement = connection.createStatement(); + PreparedStatement preparedStatement = connection.prepareStatement(insertSQL)) { + + // drop Table if exists and then create new one + TestUtils.dropTableIfExists(AbstractSQLGenerator.escapeIdentifier(TEST_TABLE_NAME), statement); + statement.execute("CREATE TABLE " + AbstractSQLGenerator.escapeIdentifier(TEST_TABLE_NAME) + + " ( col1 nchar(" + precision + "))"); + + // insert into Table + for (int i = 0; i < numberOfRows; i++) { + preparedStatement.setString(1, generateRandomString(precision)); + preparedStatement.addBatch(); + } + preparedStatement.executeBatch(); + } + } + + @AfterAll + static void teardownTestTable() throws SQLException { + try (Statement statement = connection.createStatement()) { + TestUtils.dropTableIfExists(AbstractSQLGenerator.escapeIdentifier(TEST_TABLE_NAME), statement); + } + } + + /** + * Test shows influence of MaxResultBuffer on all types of ResultSet (considering state of response buffering). In + * this test, maxResultBuffer is not affecting the work of driver in any way. + * + * @param maxResultBuffer + * value of MaxResultBuffer parameter provided by source method + * @param adaptiveBuffering + * value of responseBuffering provided by source method + * @param resultSetType + * type of ResultSet provided by source method + * @param concurrencyMode + * type of ResultSet's concurrency provided by source method + */ + @ParameterizedTest( + name = "[{index}] maxResultBuffer = {0}, responseBuffering = {1}, resultSetType = {2}, concurrencyMode = {3}") + @MethodSource("linearResultSetData") + void testResultSetLinear(String maxResultBuffer, boolean adaptiveBuffering, int resultSetType, + int concurrencyMode) { + setResponseBufferingAdaptive(adaptiveBuffering); + setMaxResultBuffer(maxResultBuffer); + try { + resultSet(resultSetType, concurrencyMode); + } catch (SQLException e) { + fail(); + } + } + + private static Iterable linearResultSetData() { + return Arrays.asList(new Object[][] { + // maxResultBuffer set to 5k + {"5k", true, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE}, + {"5k", true, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY}, + {"5k", true, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY}, + {"5k", true, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE}, + {"5k", false, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE}, + {"5k", false, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY}, + {"5k", false, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY}, + {"5k", false, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE}, + // maxResultBuffer set to 10k + {"10k", true, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY}, + {"10k", true, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE}, + {"10k", true, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY}, + {"10k", true, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY}, + {"10k", true, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE}, + {"10k", false, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE}, + {"10k", false, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY}, + {"10k", false, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY}, + {"10k", false, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE}, + // max ResultBuffer set to 15k + {"15k", true, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY}, + {"15k", true, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE}, + {"15k", true, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY}, + {"15k", true, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY}, + {"15k", true, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE}, + {"15k", false, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE}, + {"15k", false, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY}, + {"15k", false, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY}, + {"15k", false, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE}, + // maxResultBuffer set to 17k + {"17k", true, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY}, + {"17k", true, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE}, + {"17k", true, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY}, + {"17k", true, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY}, + {"17k", true, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE}, + {"17k", false, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE}, + {"17k", false, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY}, + {"17k", false, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY}, + {"17k", false, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE}, + // maxResultBuffer set to 20k + {"20k", true, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY}, + {"20k", true, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE}, + {"20k", true, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY}, + {"20k", true, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY}, + {"20k", true, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE}, + {"20k", false, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY}, + {"20k", false, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE}, + {"20k", false, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY}, + {"20k", false, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY}, + {"20k", false, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE},}); + } + + /** + * Test shows influence of MaxResultBuffer on all types of ResultSet (considering state of response buffering). In + * this test, driver is throwing exception because MaxResultBuffer property was exceeded. + * + * @param maxResultBuffer + * value of MaxResultBuffer parameter provided by source method + * @param adaptiveBuffering + * value of responseBuffering connection property provided by source method + * @param resultSetType + * type of ResultSet provided by source method + * @param concurrencyMode + * type of ResultSet's concurrency provided by source method + */ + @ParameterizedTest( + name = "[{index}] maxResultBuffer = {0}, responseBuffering = {1}, resultSetType = {2}, concurrencyMode = {3}") + @MethodSource("linearResultSetDataThrowsSQLException") + void testResultSetLinearThrowsSQLException(String maxResultBuffer, boolean adaptiveBuffering, int resultSetType, + int concurrencyMode) { + setResponseBufferingAdaptive(adaptiveBuffering); + setMaxResultBuffer(maxResultBuffer); + + Assertions.assertThrows(SQLServerException.class, () -> resultSet(resultSetType, concurrencyMode)); + } + + private static Iterable linearResultSetDataThrowsSQLException() { + return Arrays.asList(new Object[][] { + // maxResultBuffer set to 3k + {"3k", true, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY}, + {"3k", true, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE}, + {"3k", true, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY}, + {"3k", true, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY}, + {"3k", true, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE}, + {"3k", false, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY}, + {"3k", false, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE}, + {"3k", false, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY}, + {"3k", false, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY}, + {"3k", false, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE}, + // maxResultBuffer set to 5k + {"5k", true, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY}, + {"5k", false, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY}, + // maxResultBuffer set to 10k + {"10k", false, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY}, + // maxResultBuffer set to 15k + {"15k", false, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY}, + // maxResultBuffer set to 17k + {"17k", false, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY},}); + } + + /** + * Test shows influence of MaxResultBuffer when multiple statements are executed, which result in multiple + * ResultSets (considering state of response buffering). + * + * @param maxResultBuffer + * value of MaxResultBuffer parameter provided by source method + * @param adaptiveBuffering + * value of responseBuffering connection property provided by source method + */ + @ParameterizedTest(name = "[{index}] maxResultBuffer = {0}, responseBuffering = {1}") + @MethodSource("preparedStatementData") + void testPreparedStatementMultipleResultSets(String maxResultBuffer, boolean adaptiveBuffering) { + setResponseBufferingAdaptive(adaptiveBuffering); + setMaxResultBuffer(maxResultBuffer); + try { + preparedStatementWithMultipleResultSets(); + } catch (SQLException e) { + fail(); + } + } + + private static Iterable preparedStatementData() { + return Arrays.asList(new Object[][] { + {"20k", true}, + {"20k", false}, + }); + } + + /** + * Test shows influence of MaxResultBuffer when multiple statements are executed, which result in multiple + * ResultSets (considering state of response buffering). In this test, driver is throwing exception because + * MaxResultBuffer property was exceeded. + * + * @param maxResultBuffer + * value of MaxResultBuffer parameter provided by source method + * @param adaptiveBuffering + * value of responseBuffering connection property provided by source method + */ + @ParameterizedTest(name = "[{index}] maxResultBuffer = {0}, responseBuffering = {1}") + @MethodSource("preparedStatementDataThrowsSQLException") + void testPreparedStatementMultipleResultSetsThrowsSQLException(String maxResultBuffer, boolean adaptiveBuffering) { + setResponseBufferingAdaptive(adaptiveBuffering); + setMaxResultBuffer(maxResultBuffer); + + Assertions.assertThrows(SQLServerException.class, this::preparedStatementWithMultipleResultSets); + } + + private static Iterable preparedStatementDataThrowsSQLException() { + return Arrays.asList(new Object[][] { + // maxResultBuffer set to 3k + {"3k", true}, + {"3k", false}, + // maxResultBuffer set to 5k + {"5k", true}, + {"5k", false}, + // maxResultBuffer set to 10k + {"10k", true}, + {"10k", false}, + // maxResultBuffer set to 15k + {"15k", true}, + {"15k", false}, + // maxResultBuffer set to 17k + {"17k", true}, + {"17k", false}, + }); + } + + /** + * Test shows influence of MaxResultBuffer when multiple statements are executed in one call (considering state of + * response buffering). + * + * @param maxResultBuffer + * value of MaxResultBuffer parameter provided by source method + * @param adaptiveBuffering + * value of responseBuffering connection property provided by source method + */ + @ParameterizedTest(name = "[{index}] maxResultBuffer = {0}, responseBuffering = {1}") + @MethodSource("twoQueriesData") + void testTwoQueriesInOneStatement(String maxResultBuffer, boolean adaptiveBuffering) { + setResponseBufferingAdaptive(adaptiveBuffering); + setMaxResultBuffer(maxResultBuffer); + try { + twoQueriesInOneStatement(); + } catch (SQLException e) { + fail(); + } + } + + private static Iterable twoQueriesData() { + return Arrays.asList(new Object[][] { + {"10k", true}, + {"15k", true}, + {"17k", true}, + {"20k", true}, + }); + } + + /** + * Test shows influence of MaxResultBuffer when multiple statements are executed in one call (considering state of + * response buffering).In this test, driver is throwing exception because MaxResultBuffer property was exceeded. + * + * @param maxResultBuffer + * value of MaxResultBuffer parameter provided by source method + * @param adaptiveBuffering + * value of responseBuffering connection property provided by source method + */ + @ParameterizedTest(name = "[{index}] maxResultBuffer = {0}, responseBuffering = {1}") + @MethodSource("twoQueriesDataThrowsSQLException") + void testTwoQueriesInOneStatementThrowsSQLException(String maxResultBuffer, boolean adaptiveBuffering) { + setResponseBufferingAdaptive(adaptiveBuffering); + setMaxResultBuffer(maxResultBuffer); + + Assertions.assertThrows(SQLServerException.class, this::twoQueriesInOneStatement); + } + + private static Iterable twoQueriesDataThrowsSQLException() { + return Arrays.asList(new Object[][] { + // maxResultBuffer set to 3k + {"3k", true}, + {"3k", false}, + // maxResultBuffer set to 5k + {"5k", true}, + {"5k", false}, + // maxResultBuffer set to 10k + {"10k", false}, + // maxResultBuffer set to 15k + {"15k", false}, + // maxResultBuffer set to 17k + {"17k", false}, + // maxResultBuffer set to 20k + {"20k", false}, + }); + } + + /** + * This method tests if all packets from ResultSet are correctly retrieved + * + * @param resultSetType + * Result set type; one of ResultSet.TYPE_FORWARD_ONLY, + * ResultSet.TYPE_SCROLL_INSENSITIVE, or ResultSet.TYPE_SCROLL_SENSITIVE + * @param concurrencyMode + * Concurrency type; one of ResultSet.CONCUR_READ_ONLY or + * ResultSet.CONCUR_UPDATABLE + * + * @throws SQLException + * Exception is thrown when maxResultBuffer is exceeded + */ + private void resultSet(int resultSetType, int concurrencyMode) throws SQLException { + try (Connection connection = DriverManager.getConnection(localConnectionString); + Statement statement = connection.createStatement(resultSetType, concurrencyMode)) { + statement.execute("SELECT * FROM " + TEST_TABLE_NAME); + try (ResultSet resultSet = statement.getResultSet()) { + while (resultSet.next()) {} + } + } + } + + /** + * This method tests if Statements are detached properly, when first one hasn't been completely retrieved and second + * one have been executed. + * + * @throws SQLException + * Exception is thrown when maxResultBuffer is exceeded + */ + private void preparedStatementWithMultipleResultSets() throws SQLException { + String selectSQL = "SELECT * FROM " + TEST_TABLE_NAME; + + try (Connection connection = DriverManager.getConnection(localConnectionString); + PreparedStatement statement = connection.prepareStatement(selectSQL); + ResultSet resultSet = statement.executeQuery()) { + + try (PreparedStatement secondStatement = connection.prepareStatement(selectSQL); + ResultSet secondResultSet = secondStatement.executeQuery()) { + while (resultSet.next()) {} + + try (PreparedStatement thirdStatement = connection.prepareStatement(selectSQL); + ResultSet thirdResultSet = thirdStatement.executeQuery()) { + while (thirdResultSet.next()) {} + while (secondResultSet.next()) {} + } + } + } + } + + /** + * This method tests if ResultSet's are retrieved correctly, when more than one Query is executed inside single + * statement + * + * @throws SQLException + * Exception is thrown when maxResultBuffer is exceeded + */ + private void twoQueriesInOneStatement() throws SQLException { + try (Connection connection = DriverManager.getConnection(localConnectionString); + Statement statement = connection.createStatement()) { + statement.execute("SELECT * FROM " + TEST_TABLE_NAME + ";SELECT * FROM " + TEST_TABLE_NAME); + + try (ResultSet resultSet = statement.getResultSet()) { + while (resultSet.next()) {} + } + + if (statement.getMoreResults()) { + try (ResultSet totallyNewResultSet = statement.getResultSet()) { + while (totallyNewResultSet.next()) {} + } + } + } + } + + private static String generateRandomString(int precision) { + int leftLimit = 33; + int rightLimit = 126; + Random random = new Random(); + return random.ints(leftLimit, rightLimit).limit(precision) + .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append).toString(); + } + + private static void setResponseBufferingAdaptive(boolean adaptive) { + String value = adaptive ? "adaptive" : "full"; + localConnectionString = TestUtils.addOrOverrideProperty(localConnectionString, "responseBuffering", value); + AbstractTest.updateDataSource(localConnectionString, ds); + } + + private static void setMaxResultBuffer(String maxResultBuffer) { + localConnectionString = TestUtils.addOrOverrideProperty(localConnectionString, "maxResultBuffer", + maxResultBuffer); + AbstractTest.updateDataSource(localConnectionString, ds); + } +} diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java b/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java index 9db64b9eb..7cf71c775 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java @@ -193,5 +193,6 @@ protected Object[][] getContents() { {"R_signinTooManyTimes", "You've tried to sign in too many times with an incorrect user ID or password."}, {"R_toSigninAdd", "To sign into this application, the account must be added to"}, {"R_socketClosed", "Socket closed"}, {"R_aeStreamReadError", "The multi-part identifier"}, - {"R_dataClassificationNotSupported", "Data Classification is not supported on this server."}}; + {"R_dataClassificationNotSupported", "Data Classification is not supported on this server."}, + {"R_maxResultBufferExceeded", "MaxResultBuffer exceeded {0}."}}; } diff --git a/src/test/java/com/microsoft/sqlserver/testframework/AbstractTest.java b/src/test/java/com/microsoft/sqlserver/testframework/AbstractTest.java index c8c7c12b4..d4e52f771 100644 --- a/src/test/java/com/microsoft/sqlserver/testframework/AbstractTest.java +++ b/src/test/java/com/microsoft/sqlserver/testframework/AbstractTest.java @@ -361,6 +361,10 @@ protected static ISQLServerDataSource updateDataSource(String connectionString, break; case Constants.SEND_TEMPORAL_DATATYPES_AS_STRING_FOR_BULK_COPY: ds.setSendTemporalDataTypesAsStringForBulkCopy(Boolean.parseBoolean(value)); + break; + case Constants.MAX_RESULT_BUFFER: + ds.setMaxResultBuffer(value); + break; default: break; } diff --git a/src/test/java/com/microsoft/sqlserver/testframework/Constants.java b/src/test/java/com/microsoft/sqlserver/testframework/Constants.java index 3102ef958..220745155 100644 --- a/src/test/java/com/microsoft/sqlserver/testframework/Constants.java +++ b/src/test/java/com/microsoft/sqlserver/testframework/Constants.java @@ -134,6 +134,7 @@ private Constants() {} public static final String CANCEL_QUERY_TIMEOUT = "CANCELQUERYTIMEOUT"; public static final String ENCRYPT = "ENCRYPT"; public static final String HOST_NAME_IN_CERTIFICATE = "HOSTNAMEINCERTIFICATE"; + public static final String MAX_RESULT_BUFFER = "MAXRESULTBUFFER"; // End: Connection Properties parsed in AbstractTest class for creating DataSource. // Other Connection Properties set in FipsTest