diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java index c63e4b0c0..e1adc28bd 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java @@ -19,6 +19,7 @@ import java.nio.charset.StandardCharsets; import java.sql.Blob; import java.sql.CallableStatement; +import java.sql.Clob; import java.sql.Connection; import java.sql.Date; import java.sql.DriverManager; @@ -27,11 +28,14 @@ import java.sql.SQLException; import java.sql.SQLXML; import java.sql.Statement; +import java.sql.Time; +import java.sql.Timestamp; import java.sql.Types; import java.text.MessageFormat; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; +import java.util.Calendar; import java.util.Collections; import java.time.OffsetDateTime; import java.time.OffsetTime; @@ -1215,6 +1219,620 @@ public void testCallableStatementParameterNameAPIs() throws Exception { } } + // Test various time and date related getters with parameter names + @Test + @Tag(Constants.CodeCov) + public void testTimeDateDatatypeGetters() throws SQLException { + String timeDateProcName = AbstractSQLGenerator + .escapeIdentifier(RandomUtil.getIdentifier("TestTimeDateGetters")); + + TestUtils.dropProcedureIfExists(timeDateProcName, connection.createStatement()); + + try (Statement stmt = connection.createStatement()) { + stmt.execute( + "CREATE PROCEDURE " + timeDateProcName + + " @date_param DATE OUTPUT, @time_param TIME OUTPUT, @timestamp_param DATETIME2 OUTPUT, " + + "@datetime_param DATETIME OUTPUT, @smalldatetime_param SMALLDATETIME OUTPUT AS " + + "BEGIN " + + "SELECT @date_param = @date_param, @time_param = @time_param, @timestamp_param = @timestamp_param, " + + + "@datetime_param = @datetime_param, @smalldatetime_param = @smalldatetime_param " + + "END"); + } + + try (SQLServerCallableStatement cs = (SQLServerCallableStatement) connection.prepareCall( + "{call " + timeDateProcName + "(?,?,?,?,?)}")) { + + // Register out parameters + cs.registerOutParameter("date_param", Types.DATE); + cs.registerOutParameter("time_param", Types.TIME); + cs.registerOutParameter("timestamp_param", Types.TIMESTAMP); + cs.registerOutParameter("datetime_param", microsoft.sql.Types.DATETIME); + cs.registerOutParameter("smalldatetime_param", microsoft.sql.Types.SMALLDATETIME); + + // Set input values + cs.setObject("date_param", Date.valueOf("2024-07-16")); + cs.setObject("time_param", Time.valueOf("14:30:45")); + cs.setObject("timestamp_param", Timestamp.valueOf("2024-07-16 14:30:45.123")); + cs.setObject("datetime_param", Timestamp.valueOf("2024-07-16 14:30:45.123")); + cs.setObject("smalldatetime_param", Timestamp.valueOf("2024-07-16 14:30:00")); + + cs.execute(); + Calendar cal = null; + + // getDate(String, Calendar) + Date dateByName = cs.getDate("date_param", cal); + assertNotNull(dateByName); + assertEquals("2024-07-16", dateByName.toString()); + + // getTime(String parameterName, Calendar cal) + Time timeByNameWithCal = cs.getTime("time_param", cal); + assertNotNull(timeByNameWithCal); + assertEquals(Time.valueOf("14:30:45").toString(), timeByNameWithCal.toString()); + + // getTimestamp(String name, Calendar cal) + Timestamp timestampByNameWithCal = cs.getTimestamp("timestamp_param", cal); + assertNotNull(timestampByNameWithCal); + assertEquals(Timestamp.valueOf("2024-07-16 14:30:45.123").getTime(), + timestampByNameWithCal.getTime()); + + // getDateTime(String parameterName) + Timestamp dateTimeByName = cs.getDateTime("datetime_param"); + assertNotNull(dateTimeByName); + assertEquals(Timestamp.valueOf("2024-07-16 14:30:45.123").getTime(), + dateTimeByName.getTime()); + + // getDateTime(int index, Calendar cal) - NEW COVERAGE + Timestamp dateTimeByIndexWithCal = cs.getDateTime(4, cal); + assertNotNull(dateTimeByIndexWithCal); + assertEquals(Timestamp.valueOf("2024-07-16 14:30:45.123").getTime(), + dateTimeByIndexWithCal.getTime()); + + // getDateTime(String name, Calendar cal) + Timestamp dateTimeByNameWithCal = cs.getDateTime("datetime_param", cal); + assertNotNull(dateTimeByNameWithCal); + assertEquals(Timestamp.valueOf("2024-07-16 14:30:45.123").getTime(), + dateTimeByNameWithCal.getTime()); + + // getSmallDateTime(String parameterName) + Timestamp smallDateTimeByName = cs.getSmallDateTime("smalldatetime_param"); + assertNotNull(smallDateTimeByName); + assertEquals(Timestamp.valueOf("2024-07-16 14:30:00").getTime(), + smallDateTimeByName.getTime()); + + // getSmallDateTime(int index, Calendar cal) + Timestamp smallDateTimeByIndexWithCal = cs.getSmallDateTime(5, cal); + assertNotNull(smallDateTimeByIndexWithCal); + assertEquals(Timestamp.valueOf("2024-07-16 14:30:00").getTime(), + smallDateTimeByIndexWithCal.getTime()); + + // getSmallDateTime(String name, Calendar cal) + Timestamp smallDateTimeByNameWithCal = cs.getSmallDateTime("smalldatetime_param", cal); + assertNotNull(smallDateTimeByNameWithCal); + assertEquals(Timestamp.valueOf("2024-07-16 14:30:00").getTime(), + smallDateTimeByNameWithCal.getTime()); + } finally { + TestUtils.dropProcedureIfExists(timeDateProcName, connection.createStatement()); + } + } + + @Test + @Tag(Constants.CodeCov) + public void testBigDecimalGetterMethods() throws SQLException { + String bigDecimalProcName = AbstractSQLGenerator + .escapeIdentifier(RandomUtil.getIdentifier("TestBigDecimalGetters")); + + TestUtils.dropProcedureIfExists(bigDecimalProcName, connection.createStatement()); + + try (Statement stmt = connection.createStatement()) { + stmt.execute( + "CREATE PROCEDURE " + bigDecimalProcName + + " @decimal_param DECIMAL(10,4) OUTPUT AS " + + "BEGIN " + + "SELECT @decimal_param = @decimal_param " + + "END"); + } + + try (SQLServerCallableStatement cs = (SQLServerCallableStatement) connection.prepareCall( + "{call " + bigDecimalProcName + "(?)}")) { + + // Register out parameter + cs.registerOutParameter("decimal_param", Types.DECIMAL); + + // Set input value using setObject + cs.setObject("decimal_param", new BigDecimal("123.4567")); + + cs.execute(); + + //getBigDecimal(int, int scale) - deprecated method + @SuppressWarnings("deprecation") + BigDecimal decimalByIndex = cs.getBigDecimal(1, 2); + assertNotNull(decimalByIndex); + assertEquals(2, decimalByIndex.scale()); + assertEquals(0, decimalByIndex.compareTo(new BigDecimal("123.45"))); + + //getBigDecimal(String, int scale) - deprecated method + @SuppressWarnings("deprecation") + BigDecimal decimalByName = cs.getBigDecimal("decimal_param", 3); + assertNotNull(decimalByName); + assertEquals(3, decimalByName.scale()); + assertEquals(0, decimalByName.compareTo(new BigDecimal("123.456"))); + } finally { + TestUtils.dropProcedureIfExists(bigDecimalProcName, connection.createStatement()); + } + } + + // Test various time and date related setters with parameter names and Calendar + @Test + @Tag(Constants.CodeCov) + public void testDateTimeSettersWithCalendar() throws SQLException { + + String dateTimeSettersProcName = AbstractSQLGenerator + .escapeIdentifier(RandomUtil.getIdentifier("TestDateTimeSetters")); + + TestUtils.dropProcedureIfExists(dateTimeSettersProcName, connection.createStatement()); + + try (Statement stmt = connection.createStatement()) { + stmt.execute( + "CREATE PROCEDURE " + dateTimeSettersProcName + + " @date_param DATE OUTPUT, @time_param TIME OUTPUT, @timestamp_param DATETIME2 OUTPUT, " + + "@datetimeoffset_param DATETIMEOFFSET OUTPUT, @datetime_param DATETIME OUTPUT, " + + "@smalldatetime_param SMALLDATETIME OUTPUT AS " + + "BEGIN " + + "SELECT @date_param = @date_param, @time_param = @time_param, @timestamp_param = @timestamp_param, " + + + "@datetimeoffset_param = @datetimeoffset_param, @datetime_param = @datetime_param, " + + "@smalldatetime_param = @smalldatetime_param " + + "END"); + } + + try (SQLServerCallableStatement cs = (SQLServerCallableStatement) connection.prepareCall( + "{call " + dateTimeSettersProcName + "(?,?,?,?,?,?)}")) { + + Calendar cal = Calendar.getInstance(); + cal.setTimeZone(TimeZone.getTimeZone("UTC")); + + // setTimestamp(String, Timestamp, Calendar) + cs.setTimestamp("timestamp_param", Timestamp.valueOf("2024-07-16 14:30:45.123"), cal); + + // setTimestamp(String, Timestamp, Calendar, boolean) + cs.setTimestamp("timestamp_param", Timestamp.valueOf("2024-07-16 14:30:45.123"), cal, false); + + // setTime(String, Time, Calendar) + cs.setTime("time_param", Time.valueOf("14:30:45"), cal); + + // setTime(String, Time, Calendar, boolean) + cs.setTime("time_param", Time.valueOf("14:30:45"), cal, false); + + // setDate(String, Date, Calendar) + cs.setDate("date_param", Date.valueOf("2024-07-16"), cal); + + // setDate(String, Date, Calendar, boolean) + cs.setDate("date_param", Date.valueOf("2024-07-16"), cal, false); + + // setTimestamp(String, Timestamp, int, boolean) + cs.setTimestamp("timestamp_param", Timestamp.valueOf("2024-07-16 14:30:45.123"), 3, false); + + // setDateTimeOffset(String, DateTimeOffset, int, boolean) + microsoft.sql.DateTimeOffset dto = microsoft.sql.DateTimeOffset.valueOf( + Timestamp.valueOf("2024-07-16 14:30:45.123"), 5); + cs.setDateTimeOffset("datetimeoffset_param", dto, 3, false); + + // setTime(String, Time, int, boolean) + cs.setTime("time_param", Time.valueOf("14:30:45"), 3, false); + + // setDateTime(String, Timestamp) + cs.setDateTime("datetime_param", Timestamp.valueOf("2024-07-16 14:30:45.123")); + + // setDateTime(String, Timestamp, boolean) + cs.setDateTime("datetime_param", Timestamp.valueOf("2024-07-16 14:30:45.123"), false); + + // setSmallDateTime(String, Timestamp) + cs.setSmallDateTime("smalldatetime_param", Timestamp.valueOf("2024-07-16 14:30:00")); + + // setSmallDateTime(String, Timestamp, boolean) + cs.setSmallDateTime("smalldatetime_param", Timestamp.valueOf("2024-07-16 14:30:00"), false); + + // Register output parameters + cs.registerOutParameter("date_param", Types.DATE); + cs.registerOutParameter("time_param", Types.TIME); + cs.registerOutParameter("timestamp_param", Types.TIMESTAMP); + cs.registerOutParameter("datetimeoffset_param", microsoft.sql.Types.DATETIMEOFFSET); + cs.registerOutParameter("datetime_param", microsoft.sql.Types.DATETIME); + cs.registerOutParameter("smalldatetime_param", microsoft.sql.Types.SMALLDATETIME); + + cs.execute(); + + // Verify the values were set and can be retrieved + assertNotNull(cs.getDate("date_param")); + assertNotNull(cs.getTime("time_param")); + assertNotNull(cs.getTimestamp("timestamp_param")); + assertNotNull(cs.getDateTimeOffset("datetimeoffset_param")); + assertNotNull(cs.getDateTime("datetime_param")); + assertNotNull(cs.getSmallDateTime("smalldatetime_param")); + } finally { + TestUtils.dropProcedureIfExists(dateTimeSettersProcName, connection.createStatement()); + } + } + + // Test various stream and large object related setters with parameter names + @Test + @Tag(Constants.CodeCov) + public void testStreamSettersWithParameterNames() throws SQLException { + + String streamSettersProcName = AbstractSQLGenerator + .escapeIdentifier(RandomUtil.getIdentifier("TestStreamSetters")); + + TestUtils.dropProcedureIfExists(streamSettersProcName, connection.createStatement()); + + try (Statement stmt = connection.createStatement()) { + stmt.execute( + "CREATE PROCEDURE " + streamSettersProcName + " " + + "@nchar_stream NVARCHAR(MAX), " + + "@clob_stream VARCHAR(MAX), " + + "@nclob_stream NVARCHAR(MAX) OUTPUT, " + + "@nstring_param NVARCHAR(MAX), " + + "@ascii_stream VARCHAR(MAX), " + + "@binary_stream VARBINARY(MAX), " + + "@blob_stream VARBINARY(MAX) " + + "AS BEGIN " + + "SELECT @nchar_stream AS nchar_stream, @clob_stream AS clob_stream, @nclob_stream AS nclob_stream, " + + + "@nstring_param AS nstring_param, @ascii_stream AS ascii_stream, @binary_stream AS binary_stream, " + + + "@blob_stream AS blob_stream " + + "END"); + } + + try (SQLServerCallableStatement cs = (SQLServerCallableStatement) connection.prepareCall( + "{call " + streamSettersProcName + "(?,?,?,?,?,?,?)}")) { + + // setNCharacterStream(String, Reader) + cs.setNCharacterStream("nchar_stream", new StringReader("nchar data")); + + // setNCharacterStream(String, Reader, long) + String ncharTestData = "nchar test data with length"; // 27 characters + cs.setNCharacterStream("nchar_stream", new StringReader(ncharTestData), 27L); // Fixed length + + // setClob(String, Reader) + cs.setClob("clob_stream", new StringReader("clob test data")); + + // setClob(String, Reader, long) + String clobTestData = "clob test data with length"; // 26 characters + cs.setClob("clob_stream", new StringReader(clobTestData), 26L); // Fixed length + + // Use actual Clob object like in testAllSettersWithParameterName + Clob clob = new javax.sql.rowset.serial.SerialClob("clob data".toCharArray()); + cs.setClob("clob_stream", clob); + + // setNClob(String, Reader) + cs.setNClob("nclob_stream", new StringReader("nclob data")); + + // setNClob(String, Reader, long) + String nclobTestData = "nclob test data length"; // 22 characters + cs.setNClob("nclob_stream", new StringReader(nclobTestData), 22L); // Fixed length + + // Use actual NClob object like in testAllSettersWithParameterName + NClob nclob = connection.createNClob(); + nclob.setString(1, "nclob string"); + cs.setNClob("nclob_stream", nclob); + + // setNString(String, String) + cs.setNString("nstring_param", "nstring value"); + + // setNString(String, String, boolean) + cs.setNString("nstring_param", "nstring test value", false); + + // setAsciiStream(String, InputStream) + byte[] asciiData = "ascii data".getBytes(StandardCharsets.US_ASCII); + cs.setAsciiStream("ascii_stream", new ByteArrayInputStream(asciiData)); + + // setAsciiStream(String, InputStream, int) + cs.setAsciiStream("ascii_stream", new ByteArrayInputStream(asciiData), asciiData.length); + + // setAsciiStream(String, InputStream, long) + cs.setAsciiStream("ascii_stream", new ByteArrayInputStream(asciiData), (long) asciiData.length); + + // setBinaryStream(String, InputStream) + byte[] binaryData = "binary data".getBytes(StandardCharsets.UTF_8); + cs.setBinaryStream("binary_stream", new ByteArrayInputStream(binaryData)); + + // setBinaryStream(String, InputStream, int) + cs.setBinaryStream("binary_stream", new ByteArrayInputStream(binaryData), binaryData.length); + + // setBinaryStream(String, InputStream, long) + cs.setBinaryStream("binary_stream", new ByteArrayInputStream(binaryData), (long) binaryData.length); + + // setBlob(String, InputStream) + cs.setBlob("blob_stream", new ByteArrayInputStream(binaryData)); + + // setBlob(String, InputStream, long) + cs.setBlob("blob_stream", new ByteArrayInputStream(binaryData), (long) binaryData.length); + + // Use actual Blob object like in testAllSettersWithParameterName + Blob blob = connection.createBlob(); + blob.setBytes(1, binaryData); + cs.setBlob("blob_stream", blob); + + // Register out parameter for nclob_stream + cs.registerOutParameter("nclob_stream", Types.NVARCHAR); + + try (ResultSet rs = cs.executeQuery()) { + if (rs.next()) { + // Just verify that execution succeeded and we can read some values + assertNotNull(rs.getString("nchar_stream")); + assertNotNull(rs.getString("nstring_param")); + assertNotNull(rs.getString("ascii_stream")); + assertNotNull(rs.getBytes("binary_stream")); + assertNotNull(rs.getBytes("blob_stream")); + } + } + } finally { + TestUtils.dropProcedureIfExists(streamSettersProcName, connection.createStatement()); + } + } + + // Test setObject with precision and scale + @Test + @Tag(Constants.CodeCov) + public void testSetObjectWithPrecisionAndScale() throws SQLException { + + String objectPrecisionProcName = AbstractSQLGenerator + .escapeIdentifier(RandomUtil.getIdentifier("TestObjectPrecision")); + + TestUtils.dropProcedureIfExists(objectPrecisionProcName, connection.createStatement()); + + try (Statement stmt = connection.createStatement()) { + stmt.execute( + "CREATE PROCEDURE " + objectPrecisionProcName + + " @decimal_param DECIMAL(10,3) OUTPUT AS " + + "BEGIN " + + "SELECT @decimal_param = @decimal_param " + + "END"); + } + + try (SQLServerCallableStatement cs = (SQLServerCallableStatement) connection.prepareCall( + "{call " + objectPrecisionProcName + "(?)}")) { + + // setObject(String, Object, int, Integer, int) + BigDecimal testValue = new BigDecimal("12345.678"); + cs.setObject("decimal_param", testValue, Types.DECIMAL, Integer.valueOf(10), 3); + + cs.registerOutParameter("decimal_param", Types.DECIMAL); + cs.execute(); + + BigDecimal result = cs.getBigDecimal("decimal_param"); + assertNotNull(result); + assertEquals(0, result.compareTo(new BigDecimal("12345.678"))); + } finally { + TestUtils.dropProcedureIfExists(objectPrecisionProcName, connection.createStatement()); + } + } + + // Test setUniqueIdentifier methods + @Test + @Tag(Constants.CodeCov) + public void testSetUniqueIdentifierMethods() throws SQLException { + + String identifierProcName = AbstractSQLGenerator + .escapeIdentifier(RandomUtil.getIdentifier("TestUniqueIdentifier")); + + TestUtils.dropProcedureIfExists(identifierProcName, connection.createStatement()); + + try (Statement stmt = connection.createStatement()) { + stmt.execute( + "CREATE PROCEDURE " + identifierProcName + + " @guid_param UNIQUEIDENTIFIER AS " + + "BEGIN " + + "SELECT @guid_param AS guid_result " + + "END"); + } + + String guidValue = UUID.randomUUID().toString(); + + try (SQLServerCallableStatement cs = (SQLServerCallableStatement) connection.prepareCall( + "{call " + identifierProcName + "(?)}")) { + + // setUniqueIdentifier(String, String) + cs.setUniqueIdentifier("guid_param", guidValue); + + // setUniqueIdentifier(String, String, boolean) + cs.setUniqueIdentifier("guid_param", guidValue, false); + + try (ResultSet rs = cs.executeQuery()) { + if (rs.next()) { + assertNotNull(rs.getString("guid_result")); + assertEquals(guidValue.toUpperCase(), rs.getString("guid_result").toUpperCase()); + } + } + } finally { + TestUtils.dropProcedureIfExists(identifierProcName, connection.createStatement()); + } + } + + // Test unsupported getURL and getRowId methods + @Test + @Tag(Constants.CodeCov) + public void testUnsupportedURLAndRowIdGetters() throws SQLException { + + String unsupportedGettersProcName = AbstractSQLGenerator + .escapeIdentifier(RandomUtil.getIdentifier("TestUnsupportedGetters")); + + TestUtils.dropProcedureIfExists(unsupportedGettersProcName, connection.createStatement()); + + try (Statement stmt = connection.createStatement()) { + stmt.execute( + "CREATE PROCEDURE " + unsupportedGettersProcName + + " @varchar_param VARCHAR(50) OUTPUT AS " + + "BEGIN " + + "SELECT @varchar_param = @varchar_param " + + "END"); + } + + try (SQLServerCallableStatement cs = (SQLServerCallableStatement) connection.prepareCall( + "{call " + unsupportedGettersProcName + "(?)}")) { + + cs.setString("varchar_param", "test_value"); + cs.registerOutParameter("varchar_param", Types.VARCHAR); + cs.execute(); + + // getURL(int) - not supported + assertThrows(SQLServerException.class, () -> cs.getURL(1)); + + // getURL(String) - not supported + assertThrows(SQLServerException.class, () -> cs.getURL("varchar_param")); + + // setRowId(String, RowId) - not supported + assertThrows(SQLServerException.class, () -> cs.setRowId("varchar_param", null)); + + // getRowId(int) - not supported + assertThrows(SQLServerException.class, () -> cs.getRowId(1)); + + // getRowId(String) - not supported + assertThrows(SQLServerException.class, () -> cs.getRowId("varchar_param")); + } finally { + TestUtils.dropProcedureIfExists(unsupportedGettersProcName, connection.createStatement()); + } + } + + // Test setObject overloads with SQLType + @Test + @Tag(Constants.CodeCov) + public void testSQLTypeSetObjectOverloads() throws SQLException { + + String sqlTypeOverloadsProcName = AbstractSQLGenerator + .escapeIdentifier(RandomUtil.getIdentifier("TestSQLTypeOverloads")); + + TestUtils.dropProcedureIfExists(sqlTypeOverloadsProcName, connection.createStatement()); + + try (Statement stmt = connection.createStatement()) { + stmt.execute( + "CREATE PROCEDURE " + sqlTypeOverloadsProcName + + " @int_param INT OUTPUT, @decimal_param DECIMAL(10,2) OUTPUT AS " + + "BEGIN " + + "SELECT @int_param = @int_param, @decimal_param = @decimal_param " + + "END"); + } + + try (SQLServerCallableStatement cs = (SQLServerCallableStatement) connection.prepareCall( + "{call " + sqlTypeOverloadsProcName + "(?,?)}")) { + + // setObject(String, Object, SQLType) + cs.setObject("int_param", 42, java.sql.JDBCType.INTEGER); + + // setObject(String, Object, SQLType, int) + cs.setObject("decimal_param", new BigDecimal("123.45"), java.sql.JDBCType.DECIMAL, 2); + + // setObject(String, Object, SQLType, int, boolean) + cs.setObject("decimal_param", new BigDecimal("123.45"), java.sql.JDBCType.DECIMAL, 2, false); + + cs.registerOutParameter("int_param", java.sql.JDBCType.INTEGER); + cs.registerOutParameter("decimal_param", java.sql.JDBCType.DECIMAL); + + cs.execute(); + + assertEquals(42, cs.getInt("int_param")); + assertEquals(0, cs.getBigDecimal("decimal_param").compareTo(new BigDecimal("123.45"))); + } finally { + TestUtils.dropProcedureIfExists(sqlTypeOverloadsProcName, connection.createStatement()); + } + } + + // Test registerOutParameter with precision and scale, including parameter index + @Test + @Tag(Constants.CodeCov) + public void testRegisterOutParameterWithPrecisionAndScale() throws SQLException { + + String precisionScaleProcName = AbstractSQLGenerator + .escapeIdentifier(RandomUtil.getIdentifier("TestPrecisionScale")); + + TestUtils.dropProcedureIfExists(precisionScaleProcName, connection.createStatement()); + + try { + try (Statement stmt = connection.createStatement()) { + stmt.execute( + "CREATE PROCEDURE " + precisionScaleProcName + + " @decimal_param DECIMAL(10,3) OUTPUT, @numeric_param NUMERIC(15,4) OUTPUT AS " + + "BEGIN " + + "SET @decimal_param = 123.456; " + + "SET @numeric_param = 987.6543; " + + "END"); + } + + try (SQLServerCallableStatement cs = (SQLServerCallableStatement) connection.prepareCall( + "{call " + precisionScaleProcName + "(?,?)}")) { + + // registerOutParameter(int parameterIndex, SQLType sqlType, + // int precision, int scale) + cs.registerOutParameter(1, java.sql.JDBCType.DECIMAL, 10, 3); + cs.registerOutParameter(2, java.sql.JDBCType.NUMERIC, 15, 4); + + // registerOutParameter(String parameterName, SQLType sqlType, + // int precision, int scale) + cs.registerOutParameter("decimal_param", java.sql.JDBCType.DECIMAL, 10, 3); + cs.registerOutParameter("numeric_param", java.sql.JDBCType.NUMERIC, 15, 4); + + cs.execute(); + + // Verify the values can be retrieved + BigDecimal decimalResult = cs.getBigDecimal(1); + BigDecimal numericResult = cs.getBigDecimal(2); + + assertNotNull(decimalResult); + assertNotNull(numericResult); + assertEquals(0, decimalResult.compareTo(new BigDecimal("123.456"))); + assertEquals(0, numericResult.compareTo(new BigDecimal("987.6543"))); + + // Verify retrieval by parameter name + BigDecimal decimalByName = cs.getBigDecimal("decimal_param"); + BigDecimal numericByName = cs.getBigDecimal("numeric_param"); + + assertEquals(0, decimalByName.compareTo(new BigDecimal("123.456"))); + assertEquals(0, numericByName.compareTo(new BigDecimal("987.6543"))); + } + + // Test parameter index validation + try (SQLServerCallableStatement cs2 = (SQLServerCallableStatement) connection.prepareCall( + "{call " + precisionScaleProcName + "(?,?)}")) { + + // Test invalid parameter index < 1 + try { + cs2.registerOutParameter(0, java.sql.Types.DECIMAL); + fail("Should have thrown SQLException for invalid parameter index"); + } catch (SQLException e) { + // Expected exception for index < 1 + assertTrue(e.getMessage().contains("Invalid parameter") || + e.getMessage().contains("index") || + e.getMessage().contains("Parameter")); + } + + // Test invalid parameter index > parameter count + try { + cs2.registerOutParameter(5, java.sql.Types.DECIMAL); // Only 2 parameters exist + fail("Should have thrown SQLException for invalid parameter index"); + } catch (SQLException e) { + // Expected exception for index > inOutParam.length + assertTrue(e.getMessage().contains("Invalid parameter") || + e.getMessage().contains("index") || + e.getMessage().contains("Parameter")); + } + + // Test valid parameter registration to ensure the validation logic works + // correctly + cs2.registerOutParameter(1, java.sql.Types.DECIMAL); + cs2.registerOutParameter(2, java.sql.Types.NUMERIC); + + cs2.execute(); + + // Just verify execution succeeded after valid parameter registration + assertNotNull(cs2.getBigDecimal(1)); + assertNotNull(cs2.getBigDecimal(2)); + } + } finally { + TestUtils.dropProcedureIfExists(precisionScaleProcName, connection.createStatement()); + } + } + /** * Cleanup after test * diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/databasemetadata/DatabaseMetaDataTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/databasemetadata/DatabaseMetaDataTest.java index 4076fa486..66fee090c 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/databasemetadata/DatabaseMetaDataTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/databasemetadata/DatabaseMetaDataTest.java @@ -18,6 +18,7 @@ import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; +import java.lang.reflect.Field; import java.sql.CallableStatement; import java.sql.Connection; import java.sql.DatabaseMetaData; @@ -50,6 +51,7 @@ import org.junit.runner.RunWith; import com.microsoft.sqlserver.jdbc.RandomUtil; +import com.microsoft.sqlserver.jdbc.SQLServerConnection; import com.microsoft.sqlserver.jdbc.SQLServerDataSource; import com.microsoft.sqlserver.jdbc.SQLServerDatabaseMetaData; import com.microsoft.sqlserver.jdbc.SQLServerException; @@ -895,6 +897,587 @@ public void testGetColumns() throws SQLException { } } + /** + * Comprehensive coverage test for {@link SQLServerDatabaseMetaData#getIndexInfo(String, String, String, boolean, + * boolean)} in Azure DW mode. + * + * This test creates a test table with both unique and non-unique indexes, + * then calls getIndexInfo with various parameters. + * + * @throws Exception + */ + @Test + @Tag(Constants.CodeCov) + public void testGetIndexInfoAzureDWComprehensiveCoverage() throws Exception { + + try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) { + + // Use reflection to simulate Azure DW connection + Field f1 = SQLServerConnection.class.getDeclaredField("isAzureDW"); + f1.setAccessible(true); + f1.set(conn, true); + + // Set isAzure to true as well since some code paths check both + Field f2 = SQLServerConnection.class.getDeclaredField("isAzure"); + f2.setAccessible(true); + f2.set(conn, true); + + String testTable = "azureDWTestTable" + uuid; + String testSchema = "test_schema" + uuid; + + // Create test schema first + String escapedSchema = AbstractSQLGenerator.escapeIdentifier(testSchema); + String escapedTable = AbstractSQLGenerator.escapeIdentifier(testTable); + + stmt.executeUpdate("IF NOT EXISTS (SELECT * FROM sys.schemas WHERE name = '" + testSchema + "') " + + "EXEC('CREATE SCHEMA " + escapedSchema + "')"); + + // Create test table with various types of indexes to trigger different code paths + stmt.execute("CREATE TABLE " + escapedSchema + "." + escapedTable + " (" + + "id INT PRIMARY KEY, " + + "email NVARCHAR(100), " + + "name NVARCHAR(50), " + + "data NVARCHAR(100))"); + + // Create both unique and non-unique indexes + stmt.execute("CREATE UNIQUE INDEX idx_" + testTable + "_email ON " + escapedSchema + "." + escapedTable + + " (email)"); + stmt.execute( + "CREATE INDEX idx_" + testTable + "_name ON " + escapedSchema + "." + escapedTable + " (name)"); + + try { + DatabaseMetaData dbMetadata = conn.getMetaData(); + String catalog = conn.getCatalog(); + + // Test 1: Normal execution path with unique=false, approximate=false + // This covers(LinkedHashMap setup and main execution) + try (ResultSet rs = dbMetadata.getIndexInfo(catalog, testSchema, testTable, false, false)) { + assertNotNull(rs, "ResultSet should not be null"); + + // Verify column structure matches getIndexInfoDWColumns mapping + ResultSetMetaData rsmd = rs.getMetaData(); + assertEquals(13, rsmd.getColumnCount(), + "Should have 13 columns as defined in getIndexInfoDWColumns"); + + // Verify column order matches the LinkedHashMap setup + assertEquals("TABLE_CAT", rsmd.getColumnName(1)); + assertEquals("TABLE_SCHEM", rsmd.getColumnName(2)); + assertEquals("TABLE_NAME", rsmd.getColumnName(3)); + assertEquals("NON_UNIQUE", rsmd.getColumnName(4)); + assertEquals("INDEX_QUALIFIER", rsmd.getColumnName(5)); + assertEquals("INDEX_NAME", rsmd.getColumnName(6)); + assertEquals("TYPE", rsmd.getColumnName(7)); + assertEquals("ORDINAL_POSITION", rsmd.getColumnName(8)); + assertEquals("COLUMN_NAME", rsmd.getColumnName(9)); + assertEquals("ASC_OR_DESC", rsmd.getColumnName(10)); + assertEquals("CARDINALITY", rsmd.getColumnName(11)); + assertEquals("PAGES", rsmd.getColumnName(12)); + assertEquals("FILTER_CONDITION", rsmd.getColumnName(13)); + + boolean hasResults = false; + boolean hasUniqueIndex = false; + boolean hasNonUniqueIndex = false; + + while (rs.next()) { + hasResults = true; + + // Verify data integrity from both sp_statistics and sys.indexes UNION ALL + String tableCat = rs.getString("TABLE_CAT"); + String tableSchem = rs.getString("TABLE_SCHEM"); + String tableName = rs.getString("TABLE_NAME"); + int nonUnique = rs.getInt("NON_UNIQUE"); + + assertNotNull(tableCat, "TABLE_CAT should not be null"); + assertEquals(testSchema, tableSchem, "TABLE_SCHEM should match"); + assertEquals(testTable, tableName, "TABLE_NAME should match"); + + // Track index types + if (nonUnique == 0) { + hasUniqueIndex = true; + } else { + hasNonUniqueIndex = true; + } + } + + assertTrue(hasResults, "Should have index results"); + assertTrue(hasUniqueIndex, "Should have unique indexes"); + assertTrue(hasNonUniqueIndex, "Should have non-unique indexes"); + } + + // Test 2: unique=true parameter (tests arguments[4] = "Y") + try (ResultSet rs = dbMetadata.getIndexInfo(catalog, testSchema, testTable, true, false)) { + assertNotNull(rs, "ResultSet should not be null for unique=true"); + + // All returned indexes should be unique when unique=true + while (rs.next()) { + int nonUnique = rs.getInt("NON_UNIQUE"); + assertEquals(0, nonUnique, "All indexes should be unique when unique=true"); + } + } + + // Test 3: approximate=true parameter (tests arguments[5] = "Q") + try (ResultSet rs = dbMetadata.getIndexInfo(catalog, testSchema, testTable, false, true)) { + assertNotNull(rs, "ResultSet should not be null for approximate=true"); + // Should still return valid results with approximate statistics + } + + // Test 4: Empty result set scenario + String nonExistentTable = "NonExistentTable" + uuid; + try (ResultSet rs = dbMetadata.getIndexInfo(catalog, testSchema, nonExistentTable, false, false)) { + assertNotNull(rs, "ResultSet should not be null even for non-existent table"); + + // Verify structure is correct even for empty results + ResultSetMetaData rsmd = rs.getMetaData(); + assertEquals(13, rsmd.getColumnCount(), "Should have 13 columns even for empty result"); + + // Should trigger generateAzureDWEmptyRS path + assertFalse(rs.next(), "Should have no results for non-existent table"); + } + + // Test 5: Verify type mapping + try (ResultSet rs = dbMetadata.getIndexInfo(catalog, testSchema, testTable, false, false)) { + if (rs.next()) { + // Verify that the type mapping from getIndexInfoTypesDWColumns is applied correctly + ResultSetMetaData rsmd = rs.getMetaData(); + + int columnCount = rsmd.getColumnCount(); + assertEquals(13, columnCount, "Should have 13 columns as defined"); + + // These should match the JDBC type constants defined in + // getIndexInfoTypesDWColumns + // TABLE_CAT, TABLE_SCHEM, TABLE_NAME should be NVARCHAR + // NON_UNIQUE, TYPE, ORDINAL_POSITION should be SMALLINT + // ASC_OR_DESC, FILTER_CONDITION should be VARCHAR + // CARDINALITY, PAGES should be INTEGER + + assertTrue(rsmd.getColumnTypeName(1).equalsIgnoreCase("nvarchar"), + "TABLE_CAT should be NVARCHAR type"); + assertTrue(rsmd.getColumnTypeName(2).equalsIgnoreCase("nvarchar"), + "TABLE_SCHEM should be NVARCHAR type"); + assertTrue(rsmd.getColumnTypeName(3).equalsIgnoreCase("nvarchar"), + "TABLE_NAME should be NVARCHAR type"); + assertTrue(rsmd.getColumnTypeName(4).equalsIgnoreCase("int"), + "NON_UNIQUE should be INT type"); + assertTrue(rsmd.getColumnTypeName(5).equalsIgnoreCase("nvarchar"), + "INDEX_QUALIFIER should be NVARCHAR type"); + assertTrue(rsmd.getColumnTypeName(6).equalsIgnoreCase("nvarchar"), + "INDEX_NAME should be NVARCHAR type"); + assertTrue(rsmd.getColumnTypeName(7).equalsIgnoreCase("smallint"), + "TYPE should be SMALLINT type"); + assertTrue(rsmd.getColumnTypeName(8).equalsIgnoreCase("smallint"), + "ORDINAL_POSITION should be SMALLINT type"); + assertTrue(rsmd.getColumnTypeName(9).equalsIgnoreCase("nvarchar"), + "COLUMN_NAME should be NVARCHAR type"); + assertTrue(rsmd.getColumnTypeName(10).equalsIgnoreCase("varchar"), + "ASC_OR_DESC should be VARCHAR type"); + assertTrue(rsmd.getColumnTypeName(11).equalsIgnoreCase("int"), + "CARDINALITY should be INTEGER type"); + assertTrue(rsmd.getColumnTypeName(12).equalsIgnoreCase("int"), + "PAGES should be INTEGER type"); + assertTrue(rsmd.getColumnTypeName(13).equalsIgnoreCase("varchar"), + "FILTER_CONDITION should be VARCHAR type"); + + } + } + + } catch (SQLException e) { + // Fallback scenario + // If sp_statistics fails, should fall back to INDEX_INFO_QUERY_DW + // This is harder to test directly, but we can verify the fallback query would work + + try (PreparedStatement fallbackStmt = conn.prepareStatement( + "SELECT db_name() AS TABLE_CAT, " + + "sch.name AS TABLE_SCHEM, " + + "t.name AS TABLE_NAME, " + + "CASE WHEN i.is_unique = 1 THEN 0 ELSE 1 END AS NON_UNIQUE, " + + "t.name AS INDEX_QUALIFIER, " + + "i.name AS INDEX_NAME, " + + "i.type AS TYPE, " + + "ic.key_ordinal AS ORDINAL_POSITION, " + + "c.name AS COLUMN_NAME, " + + "CASE WHEN ic.is_descending_key = 1 THEN 'D' ELSE 'A' END AS ASC_OR_DESC, " + + "NULL AS CARDINALITY, " + + "NULL AS PAGES, " + + "NULL AS FILTER_CONDITION " + + "FROM sys.indexes i " + + "INNER JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id " + + "INNER JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id " + + "INNER JOIN sys.tables t ON i.object_id = t.object_id " + + "INNER JOIN sys.schemas sch ON t.schema_id = sch.schema_id " + + "WHERE t.name = ? AND sch.name = ? " + + "ORDER BY NON_UNIQUE, TYPE, INDEX_NAME, ORDINAL_POSITION")) { + + fallbackStmt.setString(1, testTable); + fallbackStmt.setString(2, testSchema); + + try (ResultSet fallbackRs = fallbackStmt.executeQuery()) { + assertNotNull(fallbackRs, "Fallback query should work"); + // This validates that the fallback path (INDEX_INFO_QUERY_DW) is syntactically correct + } + } + } + + finally { + // Clean up + TestUtils.dropTableIfExists(escapedSchema + "." + escapedTable, stmt); + TestUtils.dropSchemaIfExists(escapedSchema, stmt); + } + } + } + + /** + * This test escapeIDName() where the default case is hit. + * We will create a table and then call getColumns with patterns that trigger + * the default case in escapeIDName which is passed to functions like + * sp_columns or sp_tables. + * + * @throws SQLException + */ + @Test + @Tag(Constants.CodeCov) + public void testEscapeIDNameDefaultCase() throws SQLException { + + try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) { + + // Test escaping characters that fall into the default case + String testTable = "testEscapeTable" + uuid.substring(0, 8); + String escapedTable = AbstractSQLGenerator.escapeIdentifier(testTable); + + stmt.execute("CREATE TABLE " + escapedTable + " (id INT, name NVARCHAR(50))"); + + try { + DatabaseMetaData dbMetadata = conn.getMetaData(); + + // Test with various escape sequences that should hit the default case + // These are characters that when escaped (\x) should result in \x being output + String[] testPatterns = { + "test\\atable", // \a -> \a (default case) + "test\\btable", // \b -> \b (default case) + "test\\ctable", // \c -> \c (default case) + "test\\dtable", // \d -> \d (default case) + "test\\etable" // \e -> \e (default case) + }; + + for (String pattern : testPatterns) { + // This will internally call escapeIDName + try (ResultSet rs = dbMetadata.getColumns(null, null, pattern, null)) { + // We don't expect to find matches, but this exercises the escape logic + assertFalse(rs.next(), "Should not find table with escaped pattern: " + pattern); + } + } + + } finally { + // Clean up + TestUtils.dropTableIfExists(escapedTable, stmt); + } + } + } + + /** + * Test getColumns() for Azure DW + * This covers the Azure DW specific column metadata initialization + */ + @Test + @Tag(Constants.CodeCov) + public void testGetColumnsAzureDW() throws Exception { + + try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) { + + // Use reflection to simulate Azure DW connection + Field f1 = SQLServerConnection.class.getDeclaredField("isAzureDW"); + f1.setAccessible(true); + f1.set(conn, true); + + // Also set isAzure to prevent lazy initialization from overwriting isAzureDW + Field f2 = SQLServerConnection.class.getDeclaredField("isAzure"); + f2.setAccessible(true); + f2.set(conn, true); + + String shortUuid = uuid.substring(0, 8); + String testTable = "azureDWColTest" + shortUuid; + String testSchema = "test_schema" + shortUuid; + String escapedSchema = AbstractSQLGenerator.escapeIdentifier(testSchema); + String escapedTable = AbstractSQLGenerator.escapeIdentifier(testTable); + + // Create test schema and table + stmt.executeUpdate("IF NOT EXISTS (SELECT * FROM sys.schemas WHERE name = '" + testSchema + "') " + + "EXEC('CREATE SCHEMA " + escapedSchema + "')"); + + stmt.execute("CREATE TABLE " + escapedSchema + "." + escapedTable + " (" + + "id INT NOT NULL, " + + "name NVARCHAR(100), " + + "value DECIMAL(10,2))"); + + try { + DatabaseMetaData dbMetadata = conn.getMetaData(); + + // This call will execute the Azure DW specific code path + try (ResultSet rs = dbMetadata.getColumns(conn.getCatalog(), testSchema, testTable, null)) { + assertTrue(rs.next(), "Should find at least one column"); + + // Verify the column metadata structure for Azure DW + ResultSetMetaData rsmd = rs.getMetaData(); + assertTrue(rsmd.getColumnCount() >= 18, "Should have at least 18 standard columns"); + + // Verify some key column names that are set up + boolean foundTableCat = false; + boolean foundTableSchem = false; + boolean foundTableName = false; + boolean foundColumnName = false; + + for (int i = 1; i <= rsmd.getColumnCount(); i++) { + String colName = rsmd.getColumnName(i); + if ("TABLE_CAT".equals(colName)) + foundTableCat = true; + if ("TABLE_SCHEM".equals(colName)) + foundTableSchem = true; + if ("TABLE_NAME".equals(colName)) + foundTableName = true; + if ("COLUMN_NAME".equals(colName)) + foundColumnName = true; + } + + assertTrue(foundTableCat, "Should find TABLE_CAT column"); + assertTrue(foundTableSchem, "Should find TABLE_SCHEM column"); + assertTrue(foundTableName, "Should find TABLE_NAME column"); + assertTrue(foundColumnName, "Should find COLUMN_NAME column"); + } + + } finally { + // Clean up + TestUtils.dropTableIfExists(escapedSchema + "." + escapedTable, stmt); + TestUtils.dropSchemaIfExists(escapedSchema, stmt); + } + } + } + + /** + * This tests the handling of empty catalog parameter in getFunctionColumns + * catalog cannot be empty in sql server and it should throw invalid argument exception + * + * @throws SQLException + */ + @Test + @Tag(Constants.CodeCov) + public void testGetFunctionColumnsEmptyCatalog() throws SQLException { + try (Connection conn = getConnection()) { + DatabaseMetaData dbMetadata = conn.getMetaData(); + + // Test with empty string catalog - should throw exception + try { + dbMetadata.getFunctionColumns("", null, null, null); + fail("Should have thrown SQLServerException for empty catalog"); + } catch (SQLServerException e) { + assertTrue(e.getMessage().contains("The argument catalog is not valid."), + "Exception should mention catalog parameter"); + } + + // Test with null catalog - should work fine + try (ResultSet rs = dbMetadata.getFunctionColumns(null, null, + "non_existent_func%", null)) { + + assertNotNull(rs, "ResultSet should not be null"); + // We don't expect results, but no exception should be thrown + } + } + } + + /** + * Test executeSPFkeys for Azure DW (getCrossReference) + * This tests the Azure DW foreign key handling via getCrossReference + */ + @Test + @Tag(Constants.xSQLv11) + @Tag(Constants.xSQLv12) + @Tag(Constants.xSQLv14) + @Tag(Constants.xSQLv15) + @Tag(Constants.xSQLv16) + @Tag(Constants.xAzureSQLDB) + @Tag(Constants.xAzureSQLMI) + @Tag(Constants.CodeCov) + public void testGetCrossReferenceAzureDW() throws Exception { + + try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) { + + // Use reflection to simulate Azure DW connection + Field f1 = SQLServerConnection.class.getDeclaredField("isAzureDW"); + f1.setAccessible(true); + f1.set(conn, true); + + // Also set isAzure to prevent lazy initialization from overwriting isAzureDW + Field f2 = SQLServerConnection.class.getDeclaredField("isAzure"); + f2.setAccessible(true); + f2.set(conn, true); + + String shortUuid = uuid.substring(0, 8); + String primaryTable = "azureDWPrimaryTest" + shortUuid; + String foreignTable = "azureDWForeignTest" + shortUuid; + String testSchema = "test_schema" + shortUuid; + String escapedSchema = AbstractSQLGenerator.escapeIdentifier(testSchema); + String escapedPrimaryTable = AbstractSQLGenerator.escapeIdentifier(primaryTable); + String escapedForeignTable = AbstractSQLGenerator.escapeIdentifier(foreignTable); + + // Create test schema and tables + stmt.executeUpdate("IF NOT EXISTS (SELECT * FROM sys.schemas WHERE name = '" + testSchema + "') " + + "EXEC('CREATE SCHEMA " + escapedSchema + "')"); + + stmt.execute("CREATE TABLE " + escapedSchema + "." + escapedPrimaryTable + " (" + + "id INT PRIMARY KEY, " + + "name NVARCHAR(100))"); + + stmt.execute("CREATE TABLE " + escapedSchema + "." + escapedForeignTable + " (" + + "id INT, " + + "parent_id INT, " + + "data NVARCHAR(100))"); + + try { + DatabaseMetaData dbMetadata = conn.getMetaData(); + String catalog = conn.getCatalog(); + + // This call will execute the Azure DW specific code path in executeSPFkeys + // Azure DW does not support foreign keys, so this should return an empty resultset + try (ResultSet rs = dbMetadata.getCrossReference(catalog, testSchema, primaryTable, + catalog, testSchema, foreignTable)) { + assertNotNull(rs, "ResultSet should not be null"); + + // Verify the result set has the correct metadata structure for foreign keys + ResultSetMetaData rsmd = rs.getMetaData(); + assertEquals(14, rsmd.getColumnCount(), "Should have exactly 14 columns for foreign key metadata"); + + // Verify column names match the expected foreign key metadata structure + String[] expectedColumnNames = { + "PKTABLE_CAT", "PKTABLE_SCHEM", "PKTABLE_NAME", "PKCOLUMN_NAME", + "FKTABLE_CAT", "FKTABLE_SCHEM", "FKTABLE_NAME", "FKCOLUMN_NAME", + "KEY_SEQ", "UPDATE_RULE", "DELETE_RULE", "FK_NAME", "PK_NAME", "DEFERRABILITY" + }; + + for (int i = 1; i <= rsmd.getColumnCount(); i++) { + assertEquals(expectedColumnNames[i - 1], rsmd.getColumnName(i), + "Column " + i + " name should match"); + } + + // Azure DW should return empty result set since it doesn't support foreign keys + assertFalse(rs.next(), "Azure DW should return empty result set for foreign keys"); + } + + // Also test getImportedKeys which calls the same code path + try (ResultSet rs = dbMetadata.getImportedKeys(catalog, testSchema, foreignTable)) { + assertNotNull(rs, "ResultSet should not be null"); + assertFalse(rs.next(), "Azure DW should return empty result set for imported keys"); + } + + // Also test getExportedKeys which calls the same code path + try (ResultSet rs = dbMetadata.getExportedKeys(catalog, testSchema, primaryTable)) { + assertNotNull(rs, "ResultSet should not be null"); + assertFalse(rs.next(), "Azure DW should return empty result set for exported keys"); + } + + } finally { + // Clean up created tables and schema + TestUtils.dropTableWithSchemaIfExists(escapedSchema + "." + escapedForeignTable, stmt); + TestUtils.dropTableWithSchemaIfExists(escapedSchema + "." + escapedPrimaryTable, stmt); + TestUtils.dropSchemaIfExists(escapedSchema, stmt); + } + } + } + + /** + * Test column count mismatch in Azure DW. This tests the validation + * that ensures getColumnsDWColumns and getTypesDWColumns have the same + * size and triggers the IllegalArgumentException + */ + @Test + @Tag(Constants.CodeCov) + public void testGetColumnsAzureDWColumnCountMismatch() throws Exception { + + try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) { + + // Use reflection to simulate Azure DW connection + Field f1 = SQLServerConnection.class.getDeclaredField("isAzureDW"); + f1.setAccessible(true); + f1.set(conn, true); + + Field f2 = SQLServerConnection.class.getDeclaredField("isAzure"); + f2.setAccessible(true); + f2.set(conn, true); + + // Get access to the private fields to manipulate them + DatabaseMetaData dbMetadata = conn.getMetaData(); + Field getColumnsDWColumnsField = SQLServerDatabaseMetaData.class.getDeclaredField("getColumnsDWColumns"); + getColumnsDWColumnsField.setAccessible(true); + + Field getTypesDWColumnsField = SQLServerDatabaseMetaData.class.getDeclaredField("getTypesDWColumns"); + getTypesDWColumnsField.setAccessible(true); + + // Create mismatched column maps to trigger the validation error + LinkedHashMap testColumnsDW = new LinkedHashMap<>(); + testColumnsDW.put(1, "TABLE_CAT"); + testColumnsDW.put(2, "TABLE_SCHEM"); + + LinkedHashMap testTypesDW = new LinkedHashMap<>(); + testTypesDW.put(1, "NVARCHAR"); + testTypesDW.put(2, "NVARCHAR"); + testTypesDW.put(3, "NVARCHAR"); // Extra entry to cause mismatch + + // Set the mismatched maps + getColumnsDWColumnsField.set(dbMetadata, testColumnsDW); + getTypesDWColumnsField.set(dbMetadata, testTypesDW); + + String shortUuid = uuid.substring(0, 8); + String testTable = "mismatchTest" + shortUuid; + String escapedTable = AbstractSQLGenerator.escapeIdentifier(testTable); + + stmt.execute("CREATE TABLE " + escapedTable + " (id INT)"); + + try { + // This should trigger the IllegalArgumentException + dbMetadata.getColumns(conn.getCatalog(), null, testTable, null); + fail("Should have thrown IllegalArgumentException for column count mismatch"); + + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("Number of provided columns 2 does not match the column data types definition 3."), + "Exception should mention column count mismatch"); + } finally { + TestUtils.dropTableIfExists(escapedTable, stmt); + } + } + } + + /** + * This tests the path where no rows are returned from sp_columns_100 + * for Azure DW, triggering the empty result set generation logic. + */ + @Test + @Tag(Constants.CodeCov) + public void testGetColumnsAzureDWEmptyResultSet() throws Exception { + try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) { + // Use reflection to simulate Azure DW connection + Field f1 = SQLServerConnection.class.getDeclaredField("isAzureDW"); + f1.setAccessible(true); + f1.set(conn, true); + + Field f2 = SQLServerConnection.class.getDeclaredField("isAzure"); + f2.setAccessible(true); + f2.set(conn, true); + + DatabaseMetaData dbMetadata = conn.getMetaData(); + + // Query for a non-existent table to trigger empty result set + String nonExistentTable = "NonExistentTable" + System.currentTimeMillis(); + + try (ResultSet rs = dbMetadata.getColumns(conn.getCatalog(), "dbo", nonExistentTable, null)) { + assertNotNull(rs, "ResultSet should not be null even for non-existent table"); + + // Verify metadata structure is correct even with empty result + ResultSetMetaData rsmd = rs.getMetaData(); + assertTrue(rsmd.getColumnCount() >= 18, "Should have standard column metadata even when empty"); + + // Should return no rows for non-existent table + assertFalse(rs.next(), "Should have no rows for non-existent table"); + } + } + } + @Test @Tag(Constants.xSQLv11) @Tag(Constants.xSQLv12) 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 a2f79a936..98e09454b 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 @@ -6,6 +6,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; @@ -1621,6 +1622,454 @@ public void testStatementWithBothResultSetAndUpdateCount() throws SQLException { } } + /** + * Test null SQL parameter validation. + * This test ensures that preparing a statement with null SQL throws SQLException. + */ + @Test + @Tag(Constants.CodeCov) + public void testNullSQLConstructorAlternative() throws SQLException { + try (SQLServerConnection conn = (SQLServerConnection) getConnection()) { + try { + conn.prepareStatement(null); + fail("Expected SQLException for null SQL"); + } catch (SQLException e) { + assertTrue(e.getMessage().contains("Statement SQL cannot be null")); + } + } + } + + /** + * Test empty batch parameter handling for bulk copy for batch insert. + * This test ensures that executing an empty batch returns an empty array. + * executeBatch() and executeLargeBatch() are both tested. + */ + @Test + @Tag(Constants.CodeCov) + public void testEmptyBatchParameterHandling() throws SQLException { + final String tableName = AbstractSQLGenerator. + escapeIdentifier(RandomUtil.getIdentifier("testEmptyBatch")); + + try (SQLServerConnection conn = (SQLServerConnection) PrepUtil.getConnection( + connectionString + ";useBulkCopyForBatchInsert=true;"); + Statement stmt = conn.createStatement()) { + + // Create test table + stmt.execute("CREATE TABLE " + tableName + + " (id INT, name VARCHAR(50))"); + + try (PreparedStatement pstmt = conn.prepareStatement( + "INSERT INTO " + tableName + " VALUES (?, ?)")) { + // Clear any existing batch + pstmt.clearBatch(); + + // Execute empty batch - should return empty array + int[] updateCounts = pstmt.executeBatch(); + assertNotNull(updateCounts); + assertEquals(0, updateCounts.length); + + // Test executeLargeBatch with empty batch + long[] largeUpdateCounts = pstmt.executeLargeBatch(); + assertNotNull(largeUpdateCounts); + assertEquals(0, largeUpdateCounts.length); + } + } finally { + // Cleanup + try (Statement stmt = connection.createStatement()) { + TestUtils.dropTableIfExists(tableName, stmt); + } + } + } + + /** + * Test different scenarios for column count mismatch in bulk copy for batch insert operation + */ + @Test + @Tag(Constants.CodeCov) + public void testBulkCopyColumnCountMismatch() throws SQLException { + final String tableName = AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("testColumnMismatch")); + + try (SQLServerConnection conn = (SQLServerConnection) PrepUtil.getConnection( + connectionString + ";useBulkCopyForBatchInsert=true;"); + Statement stmt = conn.createStatement()) { + + // Create table with 3 columns + stmt.execute("CREATE TABLE " + tableName + + " (id INT, name VARCHAR(50), age INT)"); + + // Test case 1: Column list size != value list size + // INSERT with explicit column list but mismatched parameter count + try (PreparedStatement pstmt = conn.prepareStatement( + "INSERT INTO " + tableName + + " (id, name, age) VALUES (?, ?)")) { // 3 columns but only 2 parameters + + // Add batch entries to trigger bulk copy + for (int i = 1; i <= 5; i++) { + pstmt.setInt(1, i); + pstmt.setString(2, "test" + i); + pstmt.addBatch(); + } + + try { + pstmt.executeLargeBatch(); + fail("Should throw IllegalArgumentException for column count mismatch (explicit column list)"); + } catch (IllegalArgumentException | BatchUpdateException e) { + assertTrue(e.getMessage().contains("The number of values in the VALUES clause must" + + " match the number of columns specified in the INSERT statement.")); + } + } + + // Test case 2: Table column count != value list size + // INSERT without explicit column list but wrong parameter count + try (PreparedStatement pstmt = conn.prepareStatement( + "INSERT INTO " + tableName + " VALUES (?, ?)")) { // only 2 parameters + + // Add batch entries to trigger bulk copy + for (int i = 1; i <= 5; i++) { + pstmt.setInt(1, i); + pstmt.setString(2, "test" + i); + pstmt.addBatch(); + } + + try { + pstmt.executeLargeBatch(); + fail("Should throw IllegalArgumentException for column count mismatch (no column list)"); + } catch (IllegalArgumentException | BatchUpdateException e) { + assertTrue(e.getMessage().contains("Column name or number of supplied values " + + "does not match table definition")); + } + } + + } finally { + // Cleanup + try (Statement stmt = connection.createStatement()) { + TestUtils.dropTableIfExists(tableName, stmt); + } + } + } + + /** + * Test unsupported data types for Azure DW bulk copy for batch insert. + */ + @Test + @Tag(Constants.CodeCov) + public void testUnsupportedDataTypesForAzureDW() throws Exception { + final String tableName = AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("testUnsupportedTypes")); + + try (SQLServerConnection conn = (SQLServerConnection) PrepUtil.getConnection( + connectionString + ";useBulkCopyForBatchInsert=true;"); + Statement stmt = conn.createStatement()) { + + // Use reflection to simulate Azure DW connection + Field f1 = SQLServerConnection.class.getDeclaredField("isAzureDW"); + f1.setAccessible(true); + f1.set(conn, true); + + // Set isAzure to true as well since some code paths check both + Field f2 = SQLServerConnection.class.getDeclaredField("isAzure"); + f2.setAccessible(true); + f2.set(conn, true); + + // Create table with unsupported Azure DW types + stmt.execute("CREATE TABLE " + tableName + + " (id INT, created_date DATETIME)"); + + try (PreparedStatement pstmt = conn.prepareStatement( + "INSERT INTO " + tableName + " VALUES (?, ?)")) { + + pstmt.setInt(1, 1); + // Set unsupported DATETIME type for Azure DW + pstmt.setTimestamp(2, new java.sql.Timestamp(System.currentTimeMillis())); + pstmt.addBatch(); + + try { + pstmt.executeBatch(); + } catch (IllegalArgumentException | BatchUpdateException e) { + assertTrue(e.getMessage().contains("not supported in bulk copy against Azure Data Warehouse."), + "Exception should indicate unsupported data type for Azure DW: " + e.getMessage()); + } + } + + } finally { + // Cleanup + try (Statement stmt = connection.createStatement()) { + TestUtils.dropTableIfExists(tableName, stmt); + } + } + } + + /** + * checkAndRemoveCommentsAndSpaces() method + * This test ensures that SQL with schema and table names containing dots + * are parsed correctly, including handling of spaces and comments around the dot. + */ + @Test + @Tag(Constants.CodeCov) + public void testSQLParsingWithSchemaAndDots() throws Exception { + + String uuid = UUID.randomUUID().toString().substring(0, 8); + String testTable = "testTable" + uuid; + String testSchema = "test_schema" + uuid; + + // Create test schema first + String escapedSchema = AbstractSQLGenerator.escapeIdentifier(testSchema); + String escapedTable = AbstractSQLGenerator.escapeIdentifier(testTable); + + try (Connection connection = PrepUtil.getConnection(connectionString + ";useBulkCopyForBatchInsert=true;"); + Statement stmt = connection.createStatement()) { + + stmt.executeUpdate("IF NOT EXISTS (SELECT * FROM sys.schemas WHERE name = '" + testSchema + "') " + + "EXEC('CREATE SCHEMA " + escapedSchema + "')"); + + // Create table with schema prefix + String schemaTableSQL = "CREATE TABLE " + escapedSchema + "." + escapedTable + + " (id INT, name VARCHAR(50))"; + stmt.execute(schemaTableSQL); + + try (PreparedStatement pstmt = connection.prepareStatement( + "INSERT INTO " + escapedSchema + " . " + escapedTable + " VALUES (?, ?)")) { + + // Test with spaces around dot - should parse correctly + pstmt.setInt(1, 1); + pstmt.setString(2, "test"); + pstmt.addBatch(); + + int[] updateCounts = pstmt.executeBatch(); + assertEquals(1, updateCounts.length); + assertEquals(1, updateCounts[0]); + } + + // Test with comments between schema and table name + try (PreparedStatement pstmt2 = connection.prepareStatement( + "INSERT INTO " + escapedSchema + " /* comment */ . /* another comment */ " + + escapedTable + " VALUES (?, ?)")) { + + pstmt2.setInt(1, 2); + pstmt2.setString(2, "test2"); + pstmt2.addBatch(); + + int[] updateCounts = pstmt2.executeBatch(); + assertEquals(1, updateCounts.length); + assertEquals(1, updateCounts[0]); + } + } finally { + // Cleanup + try (Connection connection = PrepUtil.getConnection(connectionString); + Statement stmt = connection.createStatement()) { + TestUtils.dropTableIfExists(escapedSchema + "." + escapedTable, stmt); + TestUtils.dropSchemaIfExists(escapedSchema, stmt); + } + } + } + + /** + * checkAndRemoveCommentsAndSpaces() method with SQL comments containing newlines. + * This test ensures that SQL comments with newlines are handled correctly during parsing. + */ + @Test + @Tag(Constants.CodeCov) + public void testSQLCommentParsingWithNewlines() throws Exception { + + try (Connection connection = PrepUtil.getConnection(connectionString + ";useBulkCopyForBatchInsert=true;"); + Statement stmt = connection.createStatement()) { + + stmt.execute("CREATE TABLE " + AbstractSQLGenerator.escapeIdentifier(tableName) + + " (id INT, name VARCHAR(50))"); + + try { + // Test SQL with line comments ending in newline + String sqlWithLineComment = "-- This is a line comment\n" + + "INSERT INTO " + AbstractSQLGenerator.escapeIdentifier(tableName) + " VALUES (?, ?)"; + + try (PreparedStatement pstmt = connection.prepareStatement(sqlWithLineComment)) { + pstmt.setInt(1, 1); + pstmt.setString(2, "test"); + pstmt.addBatch(); + + int[] updateCounts = pstmt.executeBatch(); + assertEquals(1, updateCounts.length); + assertEquals(1, updateCounts[0]); + } + + // Test SQL with line comment at end (no newline) + String sqlWithLineCommentNoNewline = "INSERT INTO " + AbstractSQLGenerator.escapeIdentifier(tableName) + + " VALUES (?, ?) -- comment at end"; + + try (PreparedStatement pstmt2 = connection.prepareStatement(sqlWithLineCommentNoNewline)) { + pstmt2.setInt(1, 2); + pstmt2.setString(2, "test2"); + pstmt2.addBatch(); + + int[] updateCounts = pstmt2.executeBatch(); + assertEquals(1, updateCounts.length); + assertEquals(1, updateCounts[0]); + } + + // Test multiple line comments + String sqlWithMultipleLineComments = "-- First comment\n" + + "-- Second comment\n" + + "INSERT INTO " + AbstractSQLGenerator.escapeIdentifier(tableName) + " VALUES (?, ?)"; + + try (PreparedStatement pstmt3 = connection.prepareStatement(sqlWithMultipleLineComments)) { + pstmt3.setInt(1, 3); + pstmt3.setString(2, "test3"); + pstmt3.addBatch(); + + int[] updateCounts = pstmt3.executeBatch(); + assertEquals(1, updateCounts.length); + assertEquals(1, updateCounts[0]); + } + } finally { + TestUtils.dropTableIfExists(AbstractSQLGenerator.escapeIdentifier(tableName), stmt); + } + } + } + + /** + * Enhanced test for batch execution with output parameters. + * OUT and INOUT parameter checking is done here, before executing the batch. If any + * OUT or INOUT are present, the entire batch fails. + */ + @Test + @Tag(Constants.CodeCov) + public void testBatchExecutionWithOutputParametersEnhanced() throws SQLException { + + final String testTableName = AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("testOutputBatch")); + + try (SQLServerConnection con = (SQLServerConnection) getConnection(); + Statement stmt = con.createStatement()) { + + // Create test table + stmt.execute("CREATE TABLE " + testTableName + + " (id INT, value INT)"); + + // Test with regular PreparedStatement that has output parameters set incorrectly + try (PreparedStatement pstmt = con.prepareStatement( + "INSERT INTO " + testTableName + " VALUES (?, ?)")) { + + // Manually set parameter as output to trigger the validation + if (pstmt instanceof SQLServerPreparedStatement) { + SQLServerPreparedStatement sqlServerPstmt = (SQLServerPreparedStatement) pstmt; + + // Set first parameter normally + pstmt.setInt(1, 1); + pstmt.setInt(2, 100); + + // Try to access internal parameter and set as output using reflection + try { + java.lang.reflect.Method setterMethod = sqlServerPstmt.getClass() + .getDeclaredMethod("setterGetParam", int.class); + setterMethod.setAccessible(true); + Object param = setterMethod.invoke(sqlServerPstmt, 1); + + // Set parameter as output + java.lang.reflect.Method setOutputMethod = param.getClass().getMethod("setIsOutput", + boolean.class); + setOutputMethod.invoke(param, true); + + pstmt.addBatch(); + + try { + pstmt.executeBatch(); + fail("Should throw BatchUpdateException for OUT parameters in batch"); + } catch (BatchUpdateException e) { + assertTrue(e.getMessage().contains("The OUT and INOUT parameters are not permitted in a batch")); + } + } catch (Exception reflectionEx) { + // If reflection fails, test the callable statement path instead + testCallableStatementOutputInBatch(con); + } + } + } + } finally { + // Cleanup + try (Statement stmt = connection.createStatement()) { + TestUtils.dropTableIfExists(testTableName, stmt); + } + } + } + + private void testCallableStatementOutputInBatch(SQLServerConnection con) throws SQLException { + String procName = RandomUtil.getIdentifier("testOutputProc"); + String createProc = "CREATE PROCEDURE " + AbstractSQLGenerator.escapeIdentifier(procName) + + " @input INT, @output INT OUTPUT AS BEGIN SET @output = @input * 2 END"; + + executeSQL(con, createProc); + String callSql = "{call " + AbstractSQLGenerator.escapeIdentifier(procName) + "(?, ?)}"; + + try (SQLServerCallableStatement stmt = (SQLServerCallableStatement) con.prepareCall(callSql)) { + stmt.setInt(1, 5); + stmt.registerOutParameter(2, Types.INTEGER); + stmt.addBatch(); + + try { + stmt.executeBatch(); + fail("Should throw BatchUpdateException for OUT parameters in batch"); + } catch (BatchUpdateException e) { + assertTrue(e.getMessage().contains("The OUT and INOUT parameters are not permitted in a batch")); + } + } finally { + try (Statement stmt = con.createStatement()) { + TestUtils.dropProcedureIfExists(AbstractSQLGenerator.escapeIdentifier(procName), stmt); + } + } + } + + /** + * Test batch execution exception handling. + * This test ensures that when a batch execution fails, the appropriate exceptions are thrown + * and handled correctly. + */ + @Test + @Tag(Constants.CodeCov) + public void testBatchExecutionException() throws SQLException { + final String testTableName = AbstractSQLGenerator.escapeIdentifier(RandomUtil. + getIdentifier("testBatchException")); + + try (SQLServerConnection con = (SQLServerConnection) getConnection(); + Statement stmt = con.createStatement()) { + + // Create test table + stmt.execute("CREATE TABLE " + testTableName + + " (id INT PRIMARY KEY, value VARCHAR(10))"); // Short varchar to trigger errors + + try (PreparedStatement pstmt = con.prepareStatement( + "INSERT INTO " + testTableName + " VALUES (?, ?)")) { + + // Add multiple batch items, some that will succeed and some that will fail + pstmt.setInt(1, 1); + pstmt.setString(2, "valid"); + pstmt.addBatch(); + + // This should succeed + pstmt.setInt(1, 2); + pstmt.setString(2, "alsovalid"); + pstmt.addBatch(); + + // This should fail due to string length + pstmt.setInt(1, 3); + pstmt.setString(2, "this_string_is_too_long_for_varchar_10"); + pstmt.addBatch(); + + try { + long[] updateCounts = pstmt.executeLargeBatch(); + // May succeed if some statements pass + } catch (BatchUpdateException e) { + // This validates the exception transformation logic + assertNotNull(e.getUpdateCounts()); + assertTrue(e.getMessage().length() > 0); + } catch (SQLException e) { + assertNotNull(e.getMessage()); + } + } + } finally { + // Cleanup + try (Statement stmt = connection.createStatement()) { + TestUtils.dropTableIfExists(testTableName, stmt); + } + } + } + private void testStatementPoolingInternal(String mode) throws Exception { // Test % handle re-use try (SQLServerConnection con = (SQLServerConnection) getConnection()) {