From 17e21c1e676c17e80ebfa9c4ce39d9de16c703fb Mon Sep 17 00:00:00 2001 From: lilgreenbird Date: Thu, 28 Nov 2024 13:16:47 -0800 Subject: [PATCH 01/13] JSON feature extension --- .../microsoft/sqlserver/jdbc/IOBuffer.java | 8 ++++ .../sqlserver/jdbc/SQLServerConnection.java | 43 +++++++++++++++++++ .../sqlserver/jdbc/SQLServerResource.java | 2 + 3 files changed, 53 insertions(+) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java index f51a5430c..e3148b2b4 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java @@ -169,6 +169,11 @@ final class TDS { static final byte TDS_FEATURE_EXT_AZURESQLDNSCACHING = 0x0B; static final byte TDS_FEATURE_EXT_SESSIONRECOVERY = 0x01; + // JSON support + static final byte TDS_FEATURE_EXT_JSONSUPPORT = 0x0D; + static final byte JSONSUPPORT_NOT_SUPPORTED = 0x00; + static final byte MAX_JSONSUPPORT_VERSION = 0x01; + static final int TDS_TVP = 0xF3; static final int TVP_ROW = 0x01; static final int TVP_NULL_TOKEN = 0xFFFF; @@ -237,6 +242,9 @@ static final String getTokenName(int tdsTokenType) { return "TDS_FEATURE_EXT_AZURESQLDNSCACHING (0x0B)"; case TDS_FEATURE_EXT_SESSIONRECOVERY: return "TDS_FEATURE_EXT_SESSIONRECOVERY (0x01)"; + case TDS_FEATURE_EXT_JSONSUPPORT: + return "TDS_FEATURE_EXT_JSONSUPPORT (0x0D)"; + default: return "unknown token (0x" + Integer.toHexString(tdsTokenType).toUpperCase() + ")"; } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index 1571f5bec..3cc8141c9 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -1021,6 +1021,16 @@ byte getServerSupportedDataClassificationVersion() { return serverSupportedDataClassificationVersion; } + /** whether server supports JSON */ + private boolean serverSupportsJSON = false; + + /** server supported JSON version */ + private byte serverSupportedJSONVersion = TDS.JSONSUPPORT_NOT_SUPPORTED; + + boolean getServerSupportsJSON() { + return serverSupportsJSON; + } + /** Boolean that indicates whether LOB objects created by this connection should be loaded into memory */ private boolean delayLoadingLobs = SQLServerDriverBooleanProperty.DELAY_LOADING_LOBS.getDefaultValue(); @@ -5340,6 +5350,17 @@ int writeDNSCacheFeatureRequest(boolean write, /* if false just calculates the l return len; } + int writeJSONSupportFeatureRequest(boolean write, /* if false just calculates the length */ + TDSWriter tdsWriter) throws SQLServerException { + int len = 6; // 1byte = featureID, 4bytes = featureData length, 1 bytes = Version + if (write) { + tdsWriter.writeByte(TDS.TDS_FEATURE_EXT_JSONSUPPORT); + tdsWriter.writeInt(1); + tdsWriter.writeByte(TDS.MAX_JSONSUPPORT_VERSION); + } + return len; + } + int writeIdleConnectionResiliencyRequest(boolean write, TDSWriter tdsWriter) throws SQLServerException { SessionStateTable ssTable = sessionRecovery.getSessionStateTable(); int len = 1; @@ -6469,6 +6490,24 @@ private void onFeatureExtAck(byte featureId, byte[] data) throws SQLServerExcept sessionRecovery.setConnectionRecoveryPossible(true); break; } + + case TDS.TDS_FEATURE_EXT_JSONSUPPORT: { + if (connectionlogger.isLoggable(Level.FINER)) { + connectionlogger.fine(toString() + " Received feature extension acknowledgement for JSON Support."); + } + + if (1 != data.length) { + throw new SQLServerException(SQLServerException.getErrString("R_unknownJSONSupportValue"), null); + } + + serverSupportedJSONVersion = data[0]; + if (0 == serverSupportedJSONVersion || serverSupportedJSONVersion > TDS.MAX_JSONSUPPORT_VERSION) { + throw new SQLServerException(SQLServerException.getErrString("R_InvalidJSONVersionNumber"), null); + } + serverSupportsJSON = true; + break; + } + default: { // Unknown feature ack throw new SQLServerException(SQLServerException.getErrString("R_UnknownFeatureAck"), null); @@ -6768,6 +6807,9 @@ final boolean complete(LogonCommand logonCommand, TDSReader tdsReader) throws SQ len = len + writeDNSCacheFeatureRequest(false, tdsWriter); + // request JSON support + len += writeJSONSupportFeatureRequest(false, tdsWriter); + len = len + 1; // add 1 to length because of FeatureEx terminator // Idle Connection Resiliency is requested @@ -6964,6 +7006,7 @@ final boolean complete(LogonCommand logonCommand, TDSReader tdsReader) throws SQ writeDataClassificationFeatureRequest(true, tdsWriter); writeUTF8SupportFeatureRequest(true, tdsWriter); writeDNSCacheFeatureRequest(true, tdsWriter); + writeJSONSupportFeatureRequest(true, tdsWriter); // Idle Connection Resiliency is requested if (connectRetryCount > 0) { diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java index c9d875e58..435e04296 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java @@ -311,6 +311,7 @@ protected Object[][] getContents() { {"R_StreamingDataTypeAE", "Data of length greater than {0} is not supported in encrypted {1} column."}, {"R_AE_NotSupportedByServer", "SQL Server in use does not support column encryption."}, {"R_InvalidAEVersionNumber", "Received invalid version number \"{0}\" for Always Encrypted."}, // From server + {"R_InvalidJSONVersionNumber", "Received invalid version number \"{0}\" for JSON."}, {"R_NullEncryptedColumnEncryptionKey", "Internal error. Encrypted column encryption key cannot be null."}, {"R_EmptyEncryptedColumnEncryptionKey", "Internal error. Empty encrypted column encryption key specified."}, {"R_InvalidMasterKeyDetails", "Invalid master key details specified."}, @@ -470,6 +471,7 @@ protected Object[][] getContents() { {"R_InvalidDataClsVersionNumber", "Invalid version number {0} for Data Classification."}, // From Server {"R_unknownUTF8SupportValue", "Unknown value for UTF8 support."}, {"R_unknownAzureSQLDNSCachingValue", "Unknown value for Azure SQL DNS Caching."}, + {"R_unknownJSONSupportValue", "Unknown value for JSON support."}, {"R_illegalWKT", "Illegal Well-Known text. Please make sure Well-Known text is valid."}, {"R_illegalTypeForGeometry", "{0} is not supported for Geometry."}, {"R_illegalWKTposition", "Illegal character in Well-Known text at position {0}."}, From b4804905eca85748488bc6e0865f52fc9ae1b586 Mon Sep 17 00:00:00 2001 From: Divang Sharma Date: Thu, 5 Dec 2024 02:45:45 +0530 Subject: [PATCH 02/13] Added JSON data type support in DataTypes --- .../microsoft/sqlserver/jdbc/DataTypes.java | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/DataTypes.java b/src/main/java/com/microsoft/sqlserver/jdbc/DataTypes.java index 018c483f8..dac167d58 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/DataTypes.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/DataTypes.java @@ -65,6 +65,7 @@ enum TDSType { NTEXT(0x63), // 99 UDT(0xF0), // -16 XML(0xF1), // -15 + JSON(0xF4), // -12 // LONGLEN types SQL_VARIANT(0x62); // 98 @@ -148,7 +149,8 @@ enum SSType { XML(Category.XML, "xml", JDBCType.LONGNVARCHAR), TIMESTAMP(Category.TIMESTAMP, "timestamp", JDBCType.BINARY), GEOMETRY(Category.UDT, "geometry", JDBCType.GEOMETRY), - GEOGRAPHY(Category.UDT, "geography", JDBCType.GEOGRAPHY); + GEOGRAPHY(Category.UDT, "geography", JDBCType.GEOGRAPHY), + JSON(Category.JSON, "json", JDBCType.LONGNVARCHAR); final Category category; private final String name; @@ -204,7 +206,8 @@ enum Category { TIMESTAMP, UDT, SQL_VARIANT, - XML; + XML, + JSON; private static final Category[] VALUES = values(); } @@ -266,7 +269,12 @@ enum GetterConversion { SQL_VARIANT(SSType.Category.SQL_VARIANT, EnumSet.of(JDBCType.Category.CHARACTER, JDBCType.Category.SQL_VARIANT, JDBCType.Category.NUMERIC, JDBCType.Category.DATE, JDBCType.Category.TIME, JDBCType.Category.BINARY, - JDBCType.Category.TIMESTAMP, JDBCType.Category.NCHARACTER, JDBCType.Category.GUID)); + JDBCType.Category.TIMESTAMP, JDBCType.Category.NCHARACTER, JDBCType.Category.GUID)), + + JSON(SSType.Category.JSON, EnumSet.of(JDBCType.Category.CHARACTER, JDBCType.Category.LONG_CHARACTER, + JDBCType.Category.CLOB, JDBCType.Category.NCHARACTER, JDBCType.Category.LONG_NCHARACTER, + JDBCType.Category.NCLOB, JDBCType.Category.BINARY, JDBCType.Category.LONG_BINARY, + JDBCType.Category.BLOB, JDBCType.Category.JSON)); private final SSType.Category from; private final EnumSet to; @@ -450,6 +458,7 @@ JDBCType getJDBCType(SSType ssType, JDBCType jdbcTypeFromApp) { case NVARCHAR: case NVARCHARMAX: case NTEXT: + case JSON://FIXME: is JSON textual or binary? jdbcType = JDBCType.LONGVARCHAR; break; @@ -673,7 +682,8 @@ enum JDBCType { SQL_VARIANT(Category.SQL_VARIANT, microsoft.sql.Types.SQL_VARIANT, Object.class.getName()), GEOMETRY(Category.GEOMETRY, microsoft.sql.Types.GEOMETRY, Object.class.getName()), GEOGRAPHY(Category.GEOGRAPHY, microsoft.sql.Types.GEOGRAPHY, Object.class.getName()), - LOCALDATETIME(Category.TIMESTAMP, java.sql.Types.TIMESTAMP, LocalDateTime.class.getName()); + LOCALDATETIME(Category.TIMESTAMP, java.sql.Types.TIMESTAMP, LocalDateTime.class.getName()), + JSON(Category.JSON, 2012, Object.class.getName()); //FIXME: type code value for JSON final Category category; private final int intValue; @@ -722,7 +732,8 @@ enum Category { GUID, SQL_VARIANT, GEOMETRY, - GEOGRAPHY; + GEOGRAPHY, + JSON; private static final Category[] VALUES = values(); } @@ -795,7 +806,8 @@ enum SetterConversion { GEOMETRY(JDBCType.Category.GEOMETRY, EnumSet.of(JDBCType.Category.GEOMETRY)), - GEOGRAPHY(JDBCType.Category.GEOGRAPHY, EnumSet.of(JDBCType.Category.GEOGRAPHY)); + GEOGRAPHY(JDBCType.Category.GEOGRAPHY, EnumSet.of(JDBCType.Category.GEOGRAPHY)), + JSON(JDBCType.Category.JSON, EnumSet.of(JDBCType.Category.JSON)); private final JDBCType.Category from; private final EnumSet to; @@ -895,7 +907,9 @@ enum UpdaterConversion { SSType.Category.DATETIMEOFFSET, SSType.Category.CHARACTER, SSType.Category.LONG_CHARACTER, SSType.Category.NCHARACTER, SSType.Category.LONG_NCHARACTER)), - SQL_VARIANT(JDBCType.Category.SQL_VARIANT, EnumSet.of(SSType.Category.SQL_VARIANT)); + SQL_VARIANT(JDBCType.Category.SQL_VARIANT, EnumSet.of(SSType.Category.SQL_VARIANT)), + + JSON(JDBCType.Category.JSON, EnumSet.of(SSType.Category.JSON)); private final JDBCType.Category from; private final EnumSet to; @@ -970,7 +984,7 @@ boolean isBinary() { * @return true if the JDBC type is textual */ private final static EnumSet textualCategories = EnumSet.of(Category.CHARACTER, Category.LONG_CHARACTER, - Category.CLOB, Category.NCHARACTER, Category.LONG_NCHARACTER, Category.NCLOB); + Category.CLOB, Category.NCHARACTER, Category.LONG_NCHARACTER, Category.NCLOB, Category.JSON); //FIXME: JSON is textual? boolean isTextual() { return textualCategories.contains(category); From 1f2b95b0f247da2372240f0b13233e95f5514e51 Mon Sep 17 00:00:00 2001 From: Divang Sharma Date: Wed, 11 Dec 2024 02:05:01 +0530 Subject: [PATCH 03/13] JSON startegy in DTV --- .../com/microsoft/sqlserver/jdbc/dtv.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java b/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java index 9ba2e83ce..38b8c3134 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java @@ -115,6 +115,7 @@ abstract class DTVExecuteOp { abstract void execute(DTV dtv, TVP tvpValue) throws SQLServerException; abstract void execute(DTV dtv, SqlVariant sqlVariantValue) throws SQLServerException; + } @@ -1518,6 +1519,7 @@ final void executeOp(DTVExecuteOp op) throws SQLServerException { case VARCHAR: case LONGVARCHAR: case CLOB: + case JSON: op.execute(this, (byte[]) null); break; @@ -2989,6 +2991,25 @@ public void apply(TypeInfo typeInfo, TDSReader tdsReader) throws SQLServerExcept typeInfo.maxLength = tdsReader.readInt(); typeInfo.ssType = SSType.SQL_VARIANT; } + }), + + JSON(TDSType.JSON, new Strategy() { + /** + * Sets the fields of typeInfo to the correct values + * + * @param typeInfo + * the TypeInfo whos values are being corrected + * @param tdsReader + * the TDSReader used to set the fields of typeInfo to the correct values + * @throws SQLServerException + * when an error occurs + */ + public void apply(TypeInfo typeInfo, TDSReader tdsReader) throws SQLServerException { + typeInfo.ssLenType = SSLenType.PARTLENTYPE; //FIXME - need to validate JSON strategy + typeInfo.ssType = SSType.JSON; + typeInfo.displaySize = typeInfo.precision = Integer.MAX_VALUE / 2; + typeInfo.charset = Encoding.UNICODE.charset(); + } }); private final TDSType tdsType; @@ -3766,6 +3787,7 @@ Object getValue(DTV dtv, JDBCType jdbcType, int scale, InputStreamGetterArgs str case BINARY: case VARBINARY: case TIMESTAMP: // A special BINARY(8) + case JSON: { convertedValue = DDC.convertStreamToObject( new SimpleInputStream(tdsReader, valueLength, streamGetterArgs, this), typeInfo, jdbcType, From e10263311ee440c4690221048c1b9d3151af074e Mon Sep 17 00:00:00 2001 From: Divang Sharma Date: Wed, 18 Dec 2024 22:44:05 +0530 Subject: [PATCH 04/13] Fix Json character encoding and JSON constants. --- .../java/com/microsoft/sqlserver/jdbc/DataTypes.java | 9 +++++---- .../sqlserver/jdbc/SQLServerResultSetMetaData.java | 1 + src/main/java/com/microsoft/sqlserver/jdbc/dtv.java | 6 +++--- src/main/java/microsoft/sql/Types.java | 6 ++++++ 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/DataTypes.java b/src/main/java/com/microsoft/sqlserver/jdbc/DataTypes.java index dac167d58..a82f698e2 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/DataTypes.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/DataTypes.java @@ -458,7 +458,7 @@ JDBCType getJDBCType(SSType ssType, JDBCType jdbcTypeFromApp) { case NVARCHAR: case NVARCHARMAX: case NTEXT: - case JSON://FIXME: is JSON textual or binary? + case JSON: jdbcType = JDBCType.LONGVARCHAR; break; @@ -683,8 +683,8 @@ enum JDBCType { GEOMETRY(Category.GEOMETRY, microsoft.sql.Types.GEOMETRY, Object.class.getName()), GEOGRAPHY(Category.GEOGRAPHY, microsoft.sql.Types.GEOGRAPHY, Object.class.getName()), LOCALDATETIME(Category.TIMESTAMP, java.sql.Types.TIMESTAMP, LocalDateTime.class.getName()), - JSON(Category.JSON, 2012, Object.class.getName()); //FIXME: type code value for JSON - + JSON(Category.JSON, microsoft.sql.Types.JSON, Object.class.getName()); + final Category category; private final int intValue; private final String className; @@ -984,7 +984,7 @@ boolean isBinary() { * @return true if the JDBC type is textual */ private final static EnumSet textualCategories = EnumSet.of(Category.CHARACTER, Category.LONG_CHARACTER, - Category.CLOB, Category.NCHARACTER, Category.LONG_NCHARACTER, Category.NCLOB, Category.JSON); //FIXME: JSON is textual? + Category.CLOB, Category.NCHARACTER, Category.LONG_NCHARACTER, Category.NCLOB, Category.JSON); //FIXME: JSON is textual? boolean isTextual() { return textualCategories.contains(category); @@ -1011,6 +1011,7 @@ int asJavaSqlType() { return java.sql.Types.CHAR; case NVARCHAR: case SQLXML: + case JSON: return java.sql.Types.VARCHAR; case LONGNVARCHAR: return java.sql.Types.LONGVARCHAR; diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResultSetMetaData.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResultSetMetaData.java index 8095f71e2..43ffd66c0 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResultSetMetaData.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResultSetMetaData.java @@ -288,6 +288,7 @@ public boolean isSearchable(int column) throws SQLServerException { case NTEXT: case UDT: case XML: + case JSON: return false; default: diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java b/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java index 38b8c3134..8dcc567ec 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java @@ -3005,10 +3005,10 @@ public void apply(TypeInfo typeInfo, TDSReader tdsReader) throws SQLServerExcept * when an error occurs */ public void apply(TypeInfo typeInfo, TDSReader tdsReader) throws SQLServerException { - typeInfo.ssLenType = SSLenType.PARTLENTYPE; //FIXME - need to validate JSON strategy + typeInfo.ssLenType = SSLenType.PARTLENTYPE; typeInfo.ssType = SSType.JSON; typeInfo.displaySize = typeInfo.precision = Integer.MAX_VALUE / 2; - typeInfo.charset = Encoding.UNICODE.charset(); + typeInfo.charset = Encoding.UTF8.charset(); } }); @@ -3758,6 +3758,7 @@ Object getValue(DTV dtv, JDBCType jdbcType, int scale, InputStreamGetterArgs str case VARBINARYMAX: case VARCHARMAX: case NVARCHARMAX: + case JSON: case UDT: { convertedValue = DDC.convertStreamToObject( PLPInputStream.makeStream(tdsReader, streamGetterArgs, this), typeInfo, jdbcType, @@ -3787,7 +3788,6 @@ Object getValue(DTV dtv, JDBCType jdbcType, int scale, InputStreamGetterArgs str case BINARY: case VARBINARY: case TIMESTAMP: // A special BINARY(8) - case JSON: { convertedValue = DDC.convertStreamToObject( new SimpleInputStream(tdsReader, valueLength, streamGetterArgs, this), typeInfo, jdbcType, diff --git a/src/main/java/microsoft/sql/Types.java b/src/main/java/microsoft/sql/Types.java index ec326fe3c..3e952faa4 100644 --- a/src/main/java/microsoft/sql/Types.java +++ b/src/main/java/microsoft/sql/Types.java @@ -74,4 +74,10 @@ private Types() { * Microsoft SQL type GEOGRAPHY. */ public static final int GEOGRAPHY = -158; + + /** + * The constant in the Java programming language, sometimes referred to as a type code, that identifies the + * Microsoft SQL type JSON. + */ + public static final int JSON = -159; } From d994fecf9f56e6ee514507c46ebc43a3236e977d Mon Sep 17 00:00:00 2001 From: Divang Sharma Date: Thu, 19 Dec 2024 00:08:15 +0530 Subject: [PATCH 05/13] Added JSON datatype test case --- .../sqlserver/jdbc/SQLServerDataTable.java | 1 + .../sqlserver/jdbc/tvp/TVPTypesTest.java | 30 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataTable.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataTable.java index 6abdaa174..9afbb5bd2 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataTable.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataTable.java @@ -300,6 +300,7 @@ private void internalAddrow(JDBCType jdbcType, Object val, Object[] rowValues, case LONGVARCHAR: case LONGNVARCHAR: case SQLXML: + case JSON: if (val instanceof UUID) val = val.toString(); nValueLen = (2 * ((String) val).length()); diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/tvp/TVPTypesTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/tvp/TVPTypesTest.java index 5239d2fff..7882bce46 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/tvp/TVPTypesTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/tvp/TVPTypesTest.java @@ -149,6 +149,36 @@ public void testXML() throws SQLException { } } + /** + * Test JSON support + * + * @throws SQLException + */ + @Test + public void testJSON() throws SQLException { + createTables("json"); + createTVPS("json"); + value = "{\"severity\":\"TRACE\",\"duration\":200,\"date\":\"2024-12-17T15:45:56\"}"; + + tvp = new SQLServerDataTable(); + tvp.addColumnMetadata("c1", microsoft.sql.Types.JSON); + tvp.addRow(value); + + try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) connection.prepareStatement( + "INSERT INTO " + AbstractSQLGenerator.escapeIdentifier(tableName) + " select * from ? ;")) { + pstmt.setStructured(1, tvpName, tvp); + + pstmt.execute(); + + try (Connection con = getConnection(); Statement stmt = con.createStatement(); + ResultSet rs = stmt.executeQuery( + "select c1 from " + AbstractSQLGenerator.escapeIdentifier(tableName) + " ORDER BY rowId")) { + while (rs.next()) + assertEquals(rs.getString(1), value); + } + } + } + /** * Test ntext support * From c164ef9a83eaddbecec84ebd514bf04510786463 Mon Sep 17 00:00:00 2001 From: Divang Sharma Date: Thu, 19 Dec 2024 01:44:24 +0530 Subject: [PATCH 06/13] Added JSON datatype test cases in ResultSet, TVP and Regression scenarios. --- .../jdbc/resultset/ResultSetTest.java | 16 +++++++--- .../sqlserver/jdbc/tvp/TVPTypesTest.java | 32 +++++++++++++++++++ .../jdbc/unit/statement/RegressionTest.java | 24 ++++++++++++++ 3 files changed, 68 insertions(+), 4 deletions(-) diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/resultset/ResultSetTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/resultset/ResultSetTest.java index 9ad054095..b8726cb34 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/resultset/ResultSetTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/resultset/ResultSetTest.java @@ -100,7 +100,7 @@ public void testJdbc41ResultSetMethods() throws SQLException { + "col2 varchar(512), " + "col3 float, " + "col4 decimal(10,5), " + "col5 uniqueidentifier, " + "col6 xml, " + "col7 varbinary(max), " + "col8 text, " + "col9 ntext, " + "col10 varbinary(max), " + "col11 date, " + "col12 time, " + "col13 datetime2, " + "col14 datetimeoffset, " - + "col15 decimal(10,9), " + "col16 decimal(38,38), " + + "col15 decimal(10,9), " + "col16 decimal(38,38), " + "col17 json, " + "order_column int identity(1,1) primary key)"); try { @@ -120,12 +120,13 @@ public void testJdbc41ResultSetMethods() throws SQLException { + "'2017-05-19T10:47:15.1234567'," // col13 + "'2017-05-19T10:47:15.1234567+02:00'," // col14 + "0.123456789, " // col15 - + "0.1234567890123456789012345678901234567" // col16 + + "0.1234567890123456789012345678901234567, " // col16 + + "'{\"test\":\"123\"}'" // col17 + ")"); stmt.executeUpdate("Insert into " + AbstractSQLGenerator.escapeIdentifier(tableName) + " values(" + "null, " + "null, " + "null, " + "null, " + "null, " + "null, " + "null, " + "null, " - + "null, " + "null, " + "null, " + "null, " + "null, " + "null, " + "null, " + "null)"); + + "null, " + "null, " + "null, " + "null, " + "null, " + "null, " + "null, " + "null, " + "null)"); try (ResultSet rs = stmt.executeQuery("select * from " + AbstractSQLGenerator.escapeIdentifier(tableName) + " order by order_column")) { @@ -223,7 +224,11 @@ public void testJdbc41ResultSetMethods() throws SQLException { .compareTo(new BigDecimal("0.12345678901234567890123456789012345670"))); assertEquals(0, rs.getObject("col16", BigDecimal.class) .compareTo(new BigDecimal("0.12345678901234567890123456789012345670"))); - + + String expectedJsonValue = "{\"test\":\"123\"}"; + assertEquals(expectedJsonValue, rs.getObject(17).toString()); + assertEquals(expectedJsonValue, rs.getObject("col17").toString()); + // test null values, mostly to verify primitive wrappers do not return default values assertTrue(rs.next()); assertNull(rs.getObject("col1", Boolean.class)); @@ -284,6 +289,9 @@ public void testJdbc41ResultSetMethods() throws SQLException { assertNull(rs.getObject(16, BigDecimal.class)); assertNull(rs.getObject("col16", BigDecimal.class)); + assertNull(rs.getObject(17)); + assertNull(rs.getObject("col17")); + assertFalse(rs.next()); } } finally { diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/tvp/TVPTypesTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/tvp/TVPTypesTest.java index 7882bce46..28e4f0dac 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/tvp/TVPTypesTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/tvp/TVPTypesTest.java @@ -379,6 +379,38 @@ public void testTVPXMLStoredProcedure() throws SQLException { } } + /** + * JSON with StoredProcedure + * + * @throws SQLException + */ + @Test + public void testTVPJSONStoredProcedure() throws SQLException { + createTables("json"); + createTVPS("json"); + createProcedure(); + + value = "{\"severity\":\"TRACE\",\"duration\":200,\"date\":\"2024-12-17T15:45:56\"}"; + + tvp = new SQLServerDataTable(); + tvp.addColumnMetadata("c1", microsoft.sql.Types.JSON); + tvp.addRow(value); + + final String sql = "{call " + AbstractSQLGenerator.escapeIdentifier(procedureName) + "(?)}"; + + try (SQLServerCallableStatement callableStmt = (SQLServerCallableStatement) connection.prepareCall(sql)) { + callableStmt.setStructured(1, tvpName, tvp); + callableStmt.execute(); + + try (Connection con = getConnection(); Statement stmt = con.createStatement(); + ResultSet rs = stmt.executeQuery( + "select c1 from " + AbstractSQLGenerator.escapeIdentifier(tableName) + " ORDER BY rowId")) { + while (rs.next()) + assertEquals(rs.getString(1), value); + } + } + } + /** * Text with StoredProcedure * diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/RegressionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/RegressionTest.java index 9785f0eda..b2bc7133d 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/RegressionTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/RegressionTest.java @@ -238,6 +238,30 @@ public void testXmlQuery() throws SQLException { } } + /** + * Tests Json query + * + * @throws SQLException + */ + @Test + public void testJsonQuery() throws SQLException { + try (Connection connection = getConnection(); Statement stmt = connection.createStatement()) { + tableName = RandomUtil.getIdentifier("try_SQLJSON_Table"); + TestUtils.dropTableIfExists(AbstractSQLGenerator.escapeIdentifier(tableName), stmt); + stmt.execute("CREATE TABLE " + AbstractSQLGenerator.escapeIdentifier(tableName) + + " ([c1] int, [c2] json, [c3] json)"); + + String sql = "UPDATE " + AbstractSQLGenerator.escapeIdentifier(tableName) + " SET [c2] = ?, [c3] = ?"; + try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) connection.prepareStatement(sql)) { + pstmt.setObject(1, null); + pstmt.setObject(2, null); + pstmt.executeUpdate(); + } finally { + TestUtils.dropTableIfExists(AbstractSQLGenerator.escapeIdentifier(tableName), stmt); + } + } + } + private void createTable(Statement stmt) throws SQLException { String sql = "CREATE TABLE " + AbstractSQLGenerator.escapeIdentifier(tableName) From f9db4f7050285643e35aecd5c6302a06f4b0e551 Mon Sep 17 00:00:00 2001 From: Divang Sharma Date: Thu, 19 Dec 2024 13:22:43 +0530 Subject: [PATCH 07/13] Added JSON datatype support --- src/main/java/com/microsoft/sqlserver/jdbc/Column.java | 2 +- src/main/java/com/microsoft/sqlserver/jdbc/Parameter.java | 4 +++- .../microsoft/sqlserver/jdbc/SQLServerParameterMetaData.java | 2 ++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/Column.java b/src/main/java/com/microsoft/sqlserver/jdbc/Column.java index 50958571d..979d413c7 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/Column.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/Column.java @@ -336,7 +336,7 @@ else if (jdbcType.isBinary()) { // Update of Unicode SSType from textual JDBCType: Use Unicode. if ((SSType.NCHAR == ssType || SSType.NVARCHAR == ssType || SSType.NVARCHARMAX == ssType - || SSType.NTEXT == ssType || SSType.XML == ssType) && + || SSType.NTEXT == ssType || SSType.XML == ssType || SSType.JSON == ssType) && (JDBCType.CHAR == jdbcType || JDBCType.VARCHAR == jdbcType || JDBCType.LONGVARCHAR == jdbcType || JDBCType.CLOB == jdbcType)) { diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/Parameter.java b/src/main/java/com/microsoft/sqlserver/jdbc/Parameter.java index 807bf3250..c6eaac835 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/Parameter.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/Parameter.java @@ -899,7 +899,9 @@ private void setTypeDefinition(DTV dtv) { case SQLXML: param.typeDefinition = SSType.XML.toString(); break; - + case JSON: + param.typeDefinition = SSType.JSON.toString(); + break; case TVP: // definition should contain the TVP name and the keyword READONLY String schema = param.schemaName; diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerParameterMetaData.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerParameterMetaData.java index aeb7a99d9..bb19cab85 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerParameterMetaData.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerParameterMetaData.java @@ -166,6 +166,8 @@ private void parseQueryMeta(ResultSet rsQueryMeta) throws SQLServerException { qm.precision = 8; } else if (SSType.XML == ssType) { qm.precision = SQLServerDatabaseMetaData.MAXLOBSIZE / 2; + } else if (SSType.JSON == ssType) { + qm.precision = SQLServerDatabaseMetaData.MAXLOBSIZE / 2; } qm.parameterTypeName = ssType.toString(); From dff9280459802ac8a6223d785d049278882f6bd7 Mon Sep 17 00:00:00 2001 From: Divang Sharma Date: Thu, 19 Dec 2024 15:27:52 +0530 Subject: [PATCH 08/13] Added JSON support in IOBuffer.java --- src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java index e3148b2b4..c2c7720a9 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java @@ -5249,6 +5249,7 @@ private void writeInternalTVPRowValues(JDBCType jdbcType, String currentColumnSt case LONGVARCHAR: case LONGNVARCHAR: case SQLXML: + case JSON: isShortValue = (2L * columnPair.getValue().precision) <= DataTypes.SHORT_VARTYPE_MAX_BYTES; isNull = (null == currentColumnStringValue); dataLength = isNull ? 0 : currentColumnStringValue.length() * 2; @@ -5484,6 +5485,7 @@ void writeTVPColumnMetaData(TVP value) throws SQLServerException { case LONGVARCHAR: case LONGNVARCHAR: case SQLXML: + case JSON: writeByte(TDSType.NVARCHAR.byteValue()); isShortValue = (2L * pair.getValue().precision) <= DataTypes.SHORT_VARTYPE_MAX_BYTES; // Use PLP encoding on Yukon and later with long values From 0e7a053f68aacc4943eb3caa20242934106074fd Mon Sep 17 00:00:00 2001 From: Ananya Garg Date: Fri, 3 Jan 2025 11:27:25 +0530 Subject: [PATCH 09/13] Json datatype support: SQLServerCallableStatement --- .../microsoft/sqlserver/jdbc/DataTypes.java | 4 +- .../CallableStatementTest.java | 1400 +++++++++-------- 2 files changed, 733 insertions(+), 671 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/DataTypes.java b/src/main/java/com/microsoft/sqlserver/jdbc/DataTypes.java index a82f698e2..c2fea6822 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/DataTypes.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/DataTypes.java @@ -744,7 +744,7 @@ enum SetterConversion { JDBCType.Category.TIME, JDBCType.Category.TIMESTAMP, JDBCType.Category.DATETIMEOFFSET, JDBCType.Category.CHARACTER, JDBCType.Category.LONG_CHARACTER, JDBCType.Category.NCHARACTER, JDBCType.Category.LONG_NCHARACTER, JDBCType.Category.BINARY, JDBCType.Category.LONG_BINARY, - JDBCType.Category.GUID, JDBCType.Category.SQL_VARIANT)), + JDBCType.Category.GUID, JDBCType.Category.SQL_VARIANT, JDBCType.Category.JSON)), LONG_CHARACTER(JDBCType.Category.LONG_CHARACTER, EnumSet.of(JDBCType.Category.CHARACTER, JDBCType.Category.LONG_CHARACTER, JDBCType.Category.NCHARACTER, JDBCType.Category.LONG_NCHARACTER, @@ -844,7 +844,7 @@ enum UpdaterConversion { SSType.Category.DATETIMEOFFSET, SSType.Category.CHARACTER, SSType.Category.LONG_CHARACTER, SSType.Category.NCHARACTER, SSType.Category.LONG_NCHARACTER, SSType.Category.XML, SSType.Category.BINARY, SSType.Category.LONG_BINARY, SSType.Category.UDT, SSType.Category.GUID, - SSType.Category.TIMESTAMP, SSType.Category.SQL_VARIANT)), + SSType.Category.TIMESTAMP, SSType.Category.SQL_VARIANT, SSType.Category.JSON)), LONG_CHARACTER(JDBCType.Category.LONG_CHARACTER, EnumSet.of(SSType.Category.CHARACTER, SSType.Category.LONG_CHARACTER, SSType.Category.NCHARACTER, SSType.Category.LONG_NCHARACTER, 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 f2d102d92..f5200f756 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java @@ -40,679 +40,741 @@ import com.microsoft.sqlserver.testframework.AbstractTest; import com.microsoft.sqlserver.testframework.Constants; - /** * Test CallableStatement */ @RunWith(JUnitPlatform.class) @Tag(Constants.xAzureSQLDW) public class CallableStatementTest extends AbstractTest { - private static String tableNameGUID = AbstractSQLGenerator - .escapeIdentifier(RandomUtil.getIdentifier("uniqueidentifier_Table")); - private static String outputProcedureNameGUID = AbstractSQLGenerator - .escapeIdentifier(RandomUtil.getIdentifier("uniqueidentifier_SP")); - private static String setNullProcedureName = AbstractSQLGenerator - .escapeIdentifier(RandomUtil.getIdentifier("CallableStatementTest_setNull_SP")); - private static String inputParamsProcedureName = AbstractSQLGenerator - .escapeIdentifier(RandomUtil.getIdentifier("CallableStatementTest_inputParams_SP")); - private static String conditionalSproc = AbstractSQLGenerator - .escapeIdentifier(RandomUtil.getIdentifier("CallableStatementTest_conditionalSproc")); - private static String simpleRetValSproc = AbstractSQLGenerator - .escapeIdentifier(RandomUtil.getIdentifier("CallableStatementTest_simpleSproc")); - private static String getObjectLocalDateTimeProcedureName = AbstractSQLGenerator - .escapeIdentifier(RandomUtil.getIdentifier("CallableStatementTest_getObjectLocalDateTime_SP")); - private static String getObjectOffsetDateTimeProcedureName = AbstractSQLGenerator - .escapeIdentifier(RandomUtil.getIdentifier("CallableStatementTest_getObjectOffsetDateTime_SP")); - private static String procName = AbstractSQLGenerator - .escapeIdentifier(RandomUtil.getIdentifier("procedureTestCallableStatementSpPrepare")); - private static String manyParamsTable = AbstractSQLGenerator - .escapeIdentifier(RandomUtil.getIdentifier("manyParam_Table")); - private static String manyParamProc = AbstractSQLGenerator - .escapeIdentifier(RandomUtil.getIdentifier("manyParam_Procedure")); - private static String currentTimeProc = AbstractSQLGenerator - .escapeIdentifier(RandomUtil.getIdentifier("currentTime_Procedure")); - private static String manyParamUserDefinedType = AbstractSQLGenerator - .escapeIdentifier(RandomUtil.getIdentifier("manyParam_definedType")); - private static String zeroParamSproc = AbstractSQLGenerator - .escapeIdentifier(RandomUtil.getIdentifier("zeroParamSproc")); - - /** - * Setup before test - * - * @throws SQLException - */ - @BeforeAll - public static void setupTest() throws Exception { - setConnection(); - - try (Statement stmt = connection.createStatement()) { - TestUtils.dropTableIfExists(tableNameGUID, stmt); - TestUtils.dropProcedureIfExists(outputProcedureNameGUID, stmt); - TestUtils.dropProcedureIfExists(setNullProcedureName, stmt); - TestUtils.dropProcedureIfExists(inputParamsProcedureName, stmt); - TestUtils.dropProcedureIfExists(getObjectLocalDateTimeProcedureName, stmt); - TestUtils.dropProcedureIfExists(getObjectOffsetDateTimeProcedureName, stmt); - TestUtils.dropProcedureIfExists(conditionalSproc, stmt); - TestUtils.dropProcedureIfExists(simpleRetValSproc, stmt); - TestUtils.dropProcedureIfExists(zeroParamSproc, stmt); - TestUtils.dropUserDefinedTypeIfExists(manyParamUserDefinedType, stmt); - TestUtils.dropProcedureIfExists(manyParamProc, stmt); - TestUtils.dropTableIfExists(manyParamsTable, stmt); - - createGUIDTable(stmt); - createGUIDStoredProcedure(stmt); - createSetNullProcedure(stmt); - createInputParamsProcedure(stmt); - createGetObjectLocalDateTimeProcedure(stmt); - createUserDefinedType(); - createTableManyParams(); - createProcedureManyParams(); - createProcedureZeroParams(); - createProcedureCurrentTime(); - createGetObjectOffsetDateTimeProcedure(stmt); - createConditionalProcedure(); - createSimpleRetValSproc(); - } - } - - // Test Needs more work to be configured to run on azureDB as there are slight differences - // between the regular SQL Server vs. azureDB - @Test - @Tag(Constants.xAzureSQLDB) - public void testCallableStatementManyParameters() throws SQLException { - String tempPass = UUID.randomUUID().toString(); - String dropLogin = "IF EXISTS (select * from sys.sql_logins where name = 'NewLogin') DROP LOGIN NewLogin"; - String dropUser = "IF EXISTS (select * from sys.sysusers where name = 'NewUser') DROP USER NewUser"; - String createLogin = "USE MASTER;CREATE LOGIN NewLogin WITH PASSWORD=N'" + tempPass + "', " - + "DEFAULT_DATABASE = MASTER, DEFAULT_LANGUAGE = US_ENGLISH;ALTER LOGIN NewLogin ENABLE;"; - String createUser = "USE MASTER;CREATE USER NewUser FOR LOGIN NewLogin WITH DEFAULT_SCHEMA = [DBO];"; - String grantExecute = "GRANT EXECUTE ON " + manyParamProc + " TO NewUser;"; - - // Need to create a user with limited permissions in order to run through the code block we are testing - // The user created will execute sp_sproc_columns internally by the driver, which should not return all - // the column names as the user has limited permissions - try (Connection conn = PrepUtil.getConnection(connectionString)) { - try (Statement stmt = conn.createStatement()) { - stmt.execute(dropLogin); - stmt.execute(dropUser); - stmt.execute(createLogin); - stmt.execute(createUser); - stmt.execute(grantExecute); - } - } - - try (Connection conn = PrepUtil.getConnection(connectionString + ";user=NewLogin;password=" + tempPass + ";")) { - BigDecimal money = new BigDecimal("9999.99"); - - // Should not throw an "Index is out of range error" - // Should not throw R_parameterNotDefinedForProcedure - try (CallableStatement callableStatement = conn - .prepareCall("{call " + manyParamProc + "(?,?,?,?,?,?,?,?,?,?)}")) { - callableStatement.setObject("@p1", money, microsoft.sql.Types.MONEY); - callableStatement.setObject("@p2", money, microsoft.sql.Types.MONEY); - callableStatement.setObject("@p3", money, microsoft.sql.Types.MONEY); - callableStatement.setObject("@p4", money, microsoft.sql.Types.MONEY); - callableStatement.setObject("@p5", money, microsoft.sql.Types.MONEY); - callableStatement.setObject("@p6", money, microsoft.sql.Types.MONEY); - callableStatement.setObject("@p7", money, microsoft.sql.Types.MONEY); - callableStatement.setObject("@p8", money, microsoft.sql.Types.MONEY); - callableStatement.setObject("@p9", money, microsoft.sql.Types.MONEY); - callableStatement.setObject("@p10", money, microsoft.sql.Types.MONEY); - callableStatement.execute(); - } - } - } - - @Test - public void testCallableStatementSpPrepare() throws SQLException { - connection.setPrepareMethod("prepare"); - - try (Statement statement = connection.createStatement();) { - statement.executeUpdate("create procedure " + procName + " as select 1 --"); - - try (CallableStatement callableStatement = connection.prepareCall("{call " + procName + "}")) { - try (ResultSet rs = callableStatement.executeQuery()) { // Takes sp_executesql path - rs.next(); - assertEquals(1, rs.getInt(1), TestResource.getResource("R_setDataNotEqual")); - } - - try (ResultSet rs = callableStatement.executeQuery()) { // Takes sp_prepare path - rs.next(); - assertEquals(1, rs.getInt(1), TestResource.getResource("R_setDataNotEqual")); - } - } - } - } - - /** - * Tests CallableStatement.getString() with uniqueidentifier parameter - * - * @throws SQLException - */ - @Test - public void getStringGUIDTest() throws SQLException { - - String sql = "{call " + outputProcedureNameGUID + "(?)}"; - - try (SQLServerCallableStatement callableStatement = (SQLServerCallableStatement) connection.prepareCall(sql)) { - - UUID originalValue = UUID.randomUUID(); - - callableStatement.registerOutParameter(1, microsoft.sql.Types.GUID); - callableStatement.setObject(1, originalValue.toString(), microsoft.sql.Types.GUID); - callableStatement.execute(); - - String retrievedValue = callableStatement.getString(1); - - assertEquals(originalValue.toString().toLowerCase(), retrievedValue.toLowerCase()); - - } - } - - /** - * test for setNull(index, varchar) to behave as setNull(index, nvarchar) when SendStringParametersAsUnicode is true - * - * @throws SQLException - */ - @Test - public void getSetNullWithTypeVarchar() throws SQLException { - String polishchar = "\u0143"; - - SQLServerDataSource ds = new SQLServerDataSource(); - ds.setURL(connectionString); - ds.setSendStringParametersAsUnicode(true); - String sql = "{? = call " + setNullProcedureName + " (?,?)}"; - try (Connection connection = ds.getConnection(); - SQLServerCallableStatement cs = (SQLServerCallableStatement) connection.prepareCall(sql); - SQLServerCallableStatement cs2 = (SQLServerCallableStatement) connection.prepareCall(sql)) { - - cs.registerOutParameter(1, Types.INTEGER); - cs.setString(2, polishchar); - cs.setString(3, null); - cs.registerOutParameter(3, Types.VARCHAR); - cs.execute(); - - String expected = cs.getString(3); - - cs2.registerOutParameter(1, Types.INTEGER); - cs2.setString(2, polishchar); - cs2.setNull(3, Types.VARCHAR); - cs2.registerOutParameter(3, Types.NVARCHAR); - cs2.execute(); - - String actual = cs2.getString(3); - - assertEquals(expected, actual); - } - } - - /** - * Tests getObject(n, java.time.LocalDateTime.class). - * - * @throws SQLException - */ - @Test - public void testGetObjectAsLocalDateTime() throws SQLException { - String sql = "{CALL " + getObjectLocalDateTimeProcedureName + " (?)}"; - try (Connection con = DriverManager.getConnection(connectionString); - CallableStatement cs = con.prepareCall(sql)) { - cs.registerOutParameter(1, Types.TIMESTAMP); - TimeZone prevTimeZone = TimeZone.getDefault(); - TimeZone.setDefault(TimeZone.getTimeZone("America/Edmonton")); - - // a local date/time that does not actually exist because of Daylight Saving Time - final String testValueDate = "2018-03-11"; - final String testValueTime = "02:00:00.1234567"; - final String testValueDateTime = testValueDate + "T" + testValueTime; - - try { - cs.execute(); - - LocalDateTime expectedLocalDateTime = LocalDateTime.parse(testValueDateTime); - LocalDateTime actualLocalDateTime = cs.getObject(1, LocalDateTime.class); - assertEquals(expectedLocalDateTime, actualLocalDateTime); - - LocalDate expectedLocalDate = LocalDate.parse(testValueDate); - LocalDate actualLocalDate = cs.getObject(1, LocalDate.class); - assertEquals(expectedLocalDate, actualLocalDate); - - LocalTime expectedLocalTime = LocalTime.parse(testValueTime); - LocalTime actualLocalTime = cs.getObject(1, LocalTime.class); - assertEquals(expectedLocalTime, actualLocalTime); - } finally { - TimeZone.setDefault(prevTimeZone); - } - } - } - - /** - * Tests getObject(n, java.time.OffsetDateTime.class) and getObject(n, java.time.OffsetTime.class). - * - * @throws SQLException - */ - @Test - @Tag(Constants.xAzureSQLDW) - public void testGetObjectAsOffsetDateTime() throws SQLException { - String sql = "{CALL " + getObjectOffsetDateTimeProcedureName + " (?, ?)}"; - try (Connection con = DriverManager.getConnection(connectionString); - CallableStatement cs = con.prepareCall(sql)) { - cs.registerOutParameter(1, Types.TIMESTAMP_WITH_TIMEZONE); - cs.registerOutParameter(2, Types.TIMESTAMP_WITH_TIMEZONE); - - final String testValue = "2018-01-02T11:22:33.123456700+12:34"; - - cs.execute(); - - OffsetDateTime expected = OffsetDateTime.parse(testValue); - OffsetDateTime actual = cs.getObject(1, OffsetDateTime.class); - assertEquals(expected, actual); - assertNull(cs.getObject(2, OffsetDateTime.class)); - - OffsetTime expectedTime = OffsetTime.parse(testValue.split("T")[1]); - OffsetTime actualTime = cs.getObject(1, OffsetTime.class); - assertEquals(expectedTime, actualTime); - assertNull(cs.getObject(2, OffsetTime.class)); - } - } - - /** - * recognize parameter names with and without leading '@' - * - * @throws SQLException - */ - @Test - public void inputParamsTest() throws SQLException { - String call = "{CALL " + inputParamsProcedureName + " (?,?)}"; - - // the historical way: no leading '@', parameter names respected (not positional) - try (CallableStatement cs = connection.prepareCall(call)) { - cs.setString("p2", "world"); - cs.setString("p1", "hello"); - try (ResultSet rs = cs.executeQuery()) { - rs.next(); - assertEquals("helloworld", rs.getString(1)); - } - } - - // the "new" way: leading '@', parameter names still respected (not positional) - try (CallableStatement cs = connection.prepareCall(call)) { - cs.setString("@p2", "world!"); - cs.setString("@p1", "Hello "); - try (ResultSet rs = cs.executeQuery()) { - rs.next(); - assertEquals("Hello world!", rs.getString(1)); - } - } - - // sanity check: unrecognized parameter name - try (CallableStatement cs = connection.prepareCall(call)) { - cs.setString("@whatever", "test"); - fail(TestResource.getResource("R_shouldThrowException")); - } catch (SQLException sse) { - MessageFormat form = new MessageFormat(TestResource.getResource("R_parameterNotDefined")); - Object[] msgArgs = {"@whatever"}; - - if (!sse.getMessage().startsWith(form.format(msgArgs))) { - fail(TestResource.getResource("R_unexpectedExceptionContent")); - } - } - } - - @Test - public void testZeroParamSproc() throws SQLException { - String call = "{? = CALL " + zeroParamSproc + "}"; - - try (CallableStatement cs = connection.prepareCall(call)) { - cs.registerOutParameter(1, Types.INTEGER); - cs.execute(); - assertEquals(1, cs.getInt(1)); - } - - // Test zero parameter sproc with return value with parentheses - call = "{? = CALL " + zeroParamSproc + "()}"; - - try (CallableStatement cs = connection.prepareCall(call)) { - cs.registerOutParameter(1, Types.INTEGER); - cs.execute(); - // Calling zero parameter sproc with return value with parentheses - // should return a value that's not zero - assertEquals(1, cs.getInt(1)); - } - } - - @Test - public void testExecuteSystemStoredProcedureNamedParametersAndIndexedParameterNoResultset() throws SQLException { - String call0 = "EXEC sp_getapplock @Resource=?, @LockTimeout='0', @LockMode='Exclusive', @LockOwner='Session'"; - String call1 = "\rEXEC\r\rsp_getapplock @Resource=?, @LockTimeout='0', @LockMode='Exclusive', @LockOwner='Session'"; - String call2 = " EXEC sp_getapplock @Resource=?, @LockTimeout='0', @LockMode='Exclusive', @LockOwner='Session'"; - String call3 = "\tEXEC\t\t\tsp_getapplock @Resource=?, @LockTimeout='0', @LockMode='Exclusive', @LockOwner='Session'"; - - try (CallableStatement cstmt0 = connection.prepareCall(call0); - CallableStatement cstmt1 = connection.prepareCall(call1); - CallableStatement cstmt2 = connection.prepareCall(call2); - CallableStatement cstmt3 = connection.prepareCall(call3);) { - cstmt0.setString(1, "Resource-" + UUID.randomUUID()); - cstmt0.execute(); - - cstmt1.setString(1, "Resource-" + UUID.randomUUID()); - cstmt1.execute(); - - cstmt2.setString(1, "Resource-" + UUID.randomUUID()); - cstmt2.execute(); - - cstmt3.setString(1, "Resource-" + UUID.randomUUID()); - cstmt3.execute(); - } - } - - @Test - public void testExecSystemStoredProcedureNamedParametersAndIndexedParameterResultSet() throws SQLException { - String call = "exec sp_sproc_columns_100 ?, @ODBCVer=3, @fUsePattern=0"; - - try (CallableStatement cstmt = connection.prepareCall(call)) { - cstmt.setString(1, "sp_getapplock"); - - try (ResultSet rs = cstmt.executeQuery()) { - while (rs.next()) { - assertTrue(TestResource.getResource("R_resultSetEmpty"), !rs.getString(4).isEmpty()); - } - } - } - } - - @Test - public void testExecSystemStoredProcedureNoIndexedParametersResultSet() throws SQLException { - String call = "execute sp_sproc_columns_100 sp_getapplock, @ODBCVer=3, @fUsePattern=0"; - - try (CallableStatement cstmt = connection.prepareCall(call); ResultSet rs = cstmt.executeQuery()) { - while (rs.next()) { - assertTrue(TestResource.getResource("R_resultSetEmpty"), !rs.getString(4).isEmpty()); - } - } - } - - @Test - public void testExecDocumentedSystemStoredProceduresIndexedParameters() throws SQLException { - String serverName; - String testTableName = "testTable"; - Integer integer = new Integer(1); - - try (Statement stmt = connection.createStatement(); ResultSet rs = stmt.executeQuery("SELECT @@SERVERNAME")) { - rs.next(); - serverName = rs.getString(1); - } - - String[] sprocs = {"EXEC sp_column_privileges ?", "exec sp_catalogs ?", "execute sp_column_privileges ?", - "EXEC sp_column_privileges_ex ?", "EXECUTE sp_columns ?", "execute sp_datatype_info ?", - "EXEC sp_sproc_columns ?", "EXECUTE sp_server_info ?", "exec sp_special_columns ?", - "execute sp_statistics ?", "EXEC sp_table_privileges ?", "exec sp_tables ?"}; - - Object[] params = {testTableName, serverName, testTableName, serverName, testTableName, integer, - "sp_column_privileges", integer, testTableName, testTableName, testTableName, testTableName}; - - int paramIndex = 0; - - for (String sproc : sprocs) { - try (CallableStatement cstmt = connection.prepareCall(sproc)) { - cstmt.setObject(1, params[paramIndex]); - cstmt.execute(); - paramIndex++; - } catch (Exception e) { - fail("Failed executing '" + sproc + "' with indexed parameter '" + params[paramIndex]); - } - } - } - - @Test - public void testCallableStatementDefaultValues() throws SQLException { - String call0 = "{call " + conditionalSproc + " (?, ?, 1)}"; - String call1 = "{call " + conditionalSproc + " (?, ?, 2)}"; - int expectedValue = 5; // The sproc should return this value - - try (CallableStatement cstmt = connection.prepareCall(call0)) { - cstmt.setInt(1, 1); - cstmt.setInt(2, 2); - cstmt.execute(); - ResultSet rs = cstmt.getResultSet(); - rs.next(); - fail(TestResource.getResource("R_expectedFailPassed")); - - } catch (Exception e) { - String msg = e.getMessage(); - assertTrue(TestResource - .getResource("R_nullPointerExceptionFromResultSet").equalsIgnoreCase(msg) - || msg == null); - } - - try (CallableStatement cstmt = connection.prepareCall(call1)) { - cstmt.setInt(1, 1); - cstmt.setInt(2, 2); - cstmt.execute(); - ResultSet rs = cstmt.getResultSet(); - rs.next(); - - assertEquals(Integer.toString(expectedValue), rs.getString(1)); - } - } - - @Test - public void testCallableStatementSetByAnnotatedArgs() throws SQLException { - String call = "{? = call " + simpleRetValSproc + " (@Arg1 = ?)}"; - int expectedValue = 1; // The sproc should return this value - - try (CallableStatement cstmt = connection.prepareCall(call)) { - cstmt.registerOutParameter(1, Types.INTEGER); - cstmt.setInt(1, 2); - cstmt.setString(2, "foo"); - cstmt.execute(); - - Assert.assertEquals(expectedValue, cstmt.getInt(1)); - } - } - - @Test - @Tag(Constants.reqExternalSetup) - @Tag(Constants.xAzureSQLDB) - @Tag(Constants.xAzureSQLDW) - @Tag(Constants.xAzureSQLMI) - public void testFourPartSyntaxCallEscapeSyntax() throws SQLException { - String table = "serverList"; - - try (Statement stmt = connection.createStatement()) { - stmt.execute("IF OBJECT_ID(N'" + table + "') IS NOT NULL DROP TABLE " + table); - stmt.execute("CREATE TABLE " + table - + " (serverName varchar(100),network varchar(100),serverStatus varchar(4000), id int, collation varchar(100), connectTimeout int, queryTimeout int)"); - stmt.execute("INSERT " + table + " EXEC sp_helpserver"); - - ResultSet rs = stmt - .executeQuery("SELECT COUNT(*) FROM " + table + " WHERE serverName = N'" + linkedServer + "'"); - rs.next(); - - if (rs.getInt(1) == 1) { - stmt.execute("EXEC sp_dropserver @server='" + linkedServer + "';"); - } - - stmt.execute("EXEC sp_addlinkedserver @server='" + linkedServer + "';"); - stmt.execute("EXEC sp_addlinkedsrvlogin @rmtsrvname=N'" + linkedServer + "', @useself=false" - + ", @rmtuser=N'" + linkedServerUser + "', @rmtpassword=N'" + linkedServerPassword + "'"); - stmt.execute("EXEC sp_serveroption '" + linkedServer + "', 'rpc', true;"); - stmt.execute("EXEC sp_serveroption '" + linkedServer + "', 'rpc out', true;"); - } - - SQLServerDataSource ds = new SQLServerDataSource(); - ds.setServerName(linkedServer); - ds.setUser(linkedServerUser); - ds.setPassword(linkedServerPassword); - ds.setEncrypt(false); - ds.setTrustServerCertificate(true); - - try (Connection linkedServerConnection = ds.getConnection(); - Statement stmt = linkedServerConnection.createStatement()) { - stmt.execute( - "create or alter procedure dbo.TestAdd(@Num1 int, @Num2 int, @Result int output) as begin set @Result = @Num1 + @Num2; end;"); - - stmt.execute("create or alter procedure dbo.TestReturn(@Num1 int) as select @Num1 return @Num1*3 "); - } - - try (CallableStatement cstmt = connection - .prepareCall("{call [" + linkedServer + "].master.dbo.TestAdd(?,?,?)}")) { - int sum = 11; - int param0 = 1; - int param1 = 10; - cstmt.setInt(1, param0); - cstmt.setInt(2, param1); - cstmt.registerOutParameter(3, Types.INTEGER); - cstmt.execute(); - assertEquals(sum, cstmt.getInt(3)); - } - - try (CallableStatement cstmt = connection.prepareCall("exec [" + linkedServer + "].master.dbo.TestAdd ?,?,?")) { - int sum = 11; - int param0 = 1; - int param1 = 10; - cstmt.setInt(1, param0); - cstmt.setInt(2, param1); - cstmt.registerOutParameter(3, Types.INTEGER); - cstmt.execute(); - assertEquals(sum, cstmt.getInt(3)); - } - - try (CallableStatement cstmt = connection - .prepareCall("{? = call [" + linkedServer + "].master.dbo.TestReturn(?)}")) { - int expected = 15; - cstmt.registerOutParameter(1, java.sql.Types.INTEGER); - cstmt.setInt(2, 5); - cstmt.execute(); - assertEquals(expected, cstmt.getInt(1)); - } - } - - @Test - public void testTimestampStringConversion() throws SQLException { - try (CallableStatement stmt = connection.prepareCall("{call " + currentTimeProc + "(?)}")) { - String timestamp = "2024-05-29 15:35:53.461"; - stmt.setObject(1, timestamp, Types.TIMESTAMP); - stmt.registerOutParameter(1, Types.TIMESTAMP); - stmt.execute(); - stmt.getObject("currentTimeStamp"); - } - } - - /** - * Cleanup after test - * - * @throws SQLException - */ - @AfterAll - public static void cleanup() throws SQLException { - try (Statement stmt = connection.createStatement()) { - TestUtils.dropTableIfExists(tableNameGUID, stmt); - TestUtils.dropTableIfExists(manyParamsTable, stmt); - TestUtils.dropProcedureIfExists(outputProcedureNameGUID, stmt); - TestUtils.dropProcedureIfExists(setNullProcedureName, stmt); - TestUtils.dropProcedureIfExists(inputParamsProcedureName, stmt); - TestUtils.dropProcedureIfExists(getObjectLocalDateTimeProcedureName, stmt); - TestUtils.dropProcedureIfExists(getObjectOffsetDateTimeProcedureName, stmt); - TestUtils.dropProcedureIfExists(currentTimeProc, stmt); - TestUtils.dropProcedureIfExists(conditionalSproc, stmt); - TestUtils.dropProcedureIfExists(simpleRetValSproc, stmt); - TestUtils.dropProcedureIfExists(zeroParamSproc, stmt); - } - } - - private static void createGUIDStoredProcedure(Statement stmt) throws SQLException { - String sql = "CREATE PROCEDURE " + outputProcedureNameGUID - + "(@p1 uniqueidentifier OUTPUT) AS SELECT @p1 = c1 FROM " + tableNameGUID + Constants.SEMI_COLON; - stmt.execute(sql); - } - - private static void createGUIDTable(Statement stmt) throws SQLException { - String sql = "CREATE TABLE " + tableNameGUID + " (c1 uniqueidentifier null)"; - stmt.execute(sql); - } - - private static void createSetNullProcedure(Statement stmt) throws SQLException { - stmt.execute("create procedure " + setNullProcedureName - + " (@p1 nvarchar(255), @p2 nvarchar(255) output) as select @p2=@p1 return 0"); - } - - private static void createInputParamsProcedure(Statement stmt) throws SQLException { - String sql = "CREATE PROCEDURE " + inputParamsProcedureName + " @p1 nvarchar(max) = N'parameter1', " - + " @p2 nvarchar(max) = N'parameter2' " + "AS " + "BEGIN " + " SET NOCOUNT ON; " - + " SELECT @p1 + @p2 AS result; " + "END "; - - stmt.execute(sql); - } - - private static void createGetObjectLocalDateTimeProcedure(Statement stmt) throws SQLException { - String sql = "CREATE PROCEDURE " + getObjectLocalDateTimeProcedureName + "(@p1 datetime2(7) OUTPUT) AS " - + "SELECT @p1 = '2018-03-11T02:00:00.1234567'"; - stmt.execute(sql); - } - - private static void createGetObjectOffsetDateTimeProcedure(Statement stmt) throws SQLException { - String sql = "CREATE PROCEDURE " + getObjectOffsetDateTimeProcedureName - + "(@p1 DATETIMEOFFSET OUTPUT, @p2 DATETIMEOFFSET OUTPUT) AS " - + "SELECT @p1 = '2018-01-02T11:22:33.123456700+12:34', @p2 = NULL"; - stmt.execute(sql); - } - - private static void createProcedureManyParams() throws SQLException { - String type = manyParamUserDefinedType; - String sql = "CREATE PROCEDURE " + manyParamProc + " @p1 " + type + ", @p2 " + type + ", @p3 " + type + ", @p4 " - + type + ", @p5 " + type + ", @p6 " + type + ", @p7 " + type + ", @p8 " + type + ", @p9 " + type - + ", @p10 " + type + " AS INSERT INTO " + manyParamsTable - + " VALUES(@p1, @p2, @p3, @p4, @p5, @p6, @p7, @p8, @p9, @p10)"; - try (Statement stmt = connection.createStatement()) { - stmt.execute(sql); - } - } - - private static void createProcedureCurrentTime() throws SQLException { - String sql = "CREATE PROCEDURE " + currentTimeProc + " @currentTimeStamp datetime = null OUTPUT " + - "AS BEGIN SET @currentTimeStamp = CURRENT_TIMESTAMP; END"; - try (Statement stmt = connection.createStatement()) { - stmt.execute(sql); - } - } - - private static void createConditionalProcedure() throws SQLException { - String sql = "CREATE PROCEDURE " + conditionalSproc + " @param0 INT, @param1 INT, @maybe bigint = 2 " + - "AS BEGIN IF @maybe >= 2 BEGIN SELECT 5 END END"; - try (Statement stmt = connection.createStatement()) { - stmt.execute(sql); - } - } - - private static void createSimpleRetValSproc() throws SQLException { - String sql = "CREATE PROCEDURE " + simpleRetValSproc + " (@Arg1 VARCHAR(128)) AS DECLARE @ReturnCode INT RETURN 1"; - try (Statement stmt = connection.createStatement()) { - stmt.execute(sql); - } - } - - private static void createTableManyParams() throws SQLException { - String type = manyParamUserDefinedType; - String sql = "CREATE TABLE" + manyParamsTable + " (c1 " + type + " null, " + "c2 " + type + " null, " + "c3 " - + type + " null, " + "c4 " + type + " null, " + "c5 " + type + " null, " + "c6 " + type + " null, " - + "c7 " + type + " null, " + "c8 " + type + " null, " + "c9 " + type + " null, " + "c10 " + type - + " null);"; - try (Statement stmt = connection.createStatement()) { - stmt.execute(sql); - } - } - - private static void createProcedureZeroParams() throws SQLException { - String sql = "CREATE PROCEDURE " + zeroParamSproc + " AS RETURN 1"; - try (Statement stmt = connection.createStatement()) { - stmt.execute(sql); - } - } - - private static void createUserDefinedType() throws SQLException { - String TVPCreateCmd = "CREATE TYPE " + manyParamUserDefinedType + " FROM MONEY"; - try (Statement stmt = connection.createStatement()) { - stmt.executeUpdate(TVPCreateCmd); - } - } + private static String tableNameGUID = AbstractSQLGenerator + .escapeIdentifier(RandomUtil.getIdentifier("uniqueidentifier_Table")); + private static String outputProcedureNameGUID = AbstractSQLGenerator + .escapeIdentifier(RandomUtil.getIdentifier("uniqueidentifier_SP")); + private static String setNullProcedureName = AbstractSQLGenerator + .escapeIdentifier(RandomUtil.getIdentifier("CallableStatementTest_setNull_SP")); + private static String inputParamsProcedureName = AbstractSQLGenerator + .escapeIdentifier(RandomUtil.getIdentifier("CallableStatementTest_inputParams_SP")); + private static String conditionalSproc = AbstractSQLGenerator + .escapeIdentifier(RandomUtil.getIdentifier("CallableStatementTest_conditionalSproc")); + private static String simpleRetValSproc = AbstractSQLGenerator + .escapeIdentifier(RandomUtil.getIdentifier("CallableStatementTest_simpleSproc")); + private static String getObjectLocalDateTimeProcedureName = AbstractSQLGenerator + .escapeIdentifier(RandomUtil.getIdentifier("CallableStatementTest_getObjectLocalDateTime_SP")); + private static String getObjectOffsetDateTimeProcedureName = AbstractSQLGenerator + .escapeIdentifier(RandomUtil.getIdentifier("CallableStatementTest_getObjectOffsetDateTime_SP")); + private static String procName = AbstractSQLGenerator + .escapeIdentifier(RandomUtil.getIdentifier("procedureTestCallableStatementSpPrepare")); + private static String manyParamsTable = AbstractSQLGenerator + .escapeIdentifier(RandomUtil.getIdentifier("manyParam_Table")); + private static String manyParamProc = AbstractSQLGenerator + .escapeIdentifier(RandomUtil.getIdentifier("manyParam_Procedure")); + private static String currentTimeProc = AbstractSQLGenerator + .escapeIdentifier(RandomUtil.getIdentifier("currentTime_Procedure")); + private static String manyParamUserDefinedType = AbstractSQLGenerator + .escapeIdentifier(RandomUtil.getIdentifier("manyParam_definedType")); + private static String zeroParamSproc = AbstractSQLGenerator + .escapeIdentifier(RandomUtil.getIdentifier("zeroParamSproc")); + private static String tableNameJSON = "TestJSONTable"; + private static String procedureNameJSON = AbstractSQLGenerator + .escapeIdentifier(RandomUtil.getIdentifier("TestJSONProcedure")); + + /** + * Setup before test + * + * @throws SQLException + */ + @BeforeAll + public static void setupTest() throws Exception { + setConnection(); + + try (Statement stmt = connection.createStatement()) { + TestUtils.dropTableIfExists(tableNameGUID, stmt); + TestUtils.dropProcedureIfExists(outputProcedureNameGUID, stmt); + TestUtils.dropProcedureIfExists(setNullProcedureName, stmt); + TestUtils.dropProcedureIfExists(inputParamsProcedureName, stmt); + TestUtils.dropProcedureIfExists(getObjectLocalDateTimeProcedureName, stmt); + TestUtils.dropProcedureIfExists(getObjectOffsetDateTimeProcedureName, stmt); + TestUtils.dropProcedureIfExists(conditionalSproc, stmt); + TestUtils.dropProcedureIfExists(simpleRetValSproc, stmt); + TestUtils.dropProcedureIfExists(zeroParamSproc, stmt); + TestUtils.dropUserDefinedTypeIfExists(manyParamUserDefinedType, stmt); + TestUtils.dropProcedureIfExists(manyParamProc, stmt); + TestUtils.dropTableIfExists(manyParamsTable, stmt); + TestUtils.dropTableIfExists(tableNameJSON, stmt); + TestUtils.dropProcedureIfExists(procedureNameJSON, stmt); + + createGUIDTable(stmt); + createGUIDStoredProcedure(stmt); + createSetNullProcedure(stmt); + createInputParamsProcedure(stmt); + createGetObjectLocalDateTimeProcedure(stmt); + createUserDefinedType(); + createTableManyParams(); + createProcedureManyParams(); + createProcedureZeroParams(); + createProcedureCurrentTime(); + createGetObjectOffsetDateTimeProcedure(stmt); + createConditionalProcedure(); + createSimpleRetValSproc(); + createJSONTestTable(stmt); + createJSONStoredProcedure(stmt); + } + } + + // Test Needs more work to be configured to run on azureDB as there are slight + // differences + // between the regular SQL Server vs. azureDB + @Test + @Tag(Constants.xAzureSQLDB) + public void testCallableStatementManyParameters() throws SQLException { + String tempPass = UUID.randomUUID().toString(); + String dropLogin = "IF EXISTS (select * from sys.sql_logins where name = 'NewLogin') DROP LOGIN NewLogin"; + String dropUser = "IF EXISTS (select * from sys.sysusers where name = 'NewUser') DROP USER NewUser"; + String createLogin = "USE MASTER;CREATE LOGIN NewLogin WITH PASSWORD=N'" + tempPass + "', " + + "DEFAULT_DATABASE = MASTER, DEFAULT_LANGUAGE = US_ENGLISH;ALTER LOGIN NewLogin ENABLE;"; + String createUser = "USE MASTER;CREATE USER NewUser FOR LOGIN NewLogin WITH DEFAULT_SCHEMA = [DBO];"; + String grantExecute = "GRANT EXECUTE ON " + manyParamProc + " TO NewUser;"; + + // Need to create a user with limited permissions in order to run through the + // code block we are testing + // The user created will execute sp_sproc_columns internally by the driver, + // which should not return all + // the column names as the user has limited permissions + try (Connection conn = PrepUtil.getConnection(connectionString)) { + try (Statement stmt = conn.createStatement()) { + stmt.execute(dropLogin); + stmt.execute(dropUser); + stmt.execute(createLogin); + stmt.execute(createUser); + stmt.execute(grantExecute); + } + } + + try (Connection conn = PrepUtil.getConnection(connectionString + ";user=NewLogin;password=" + tempPass + ";")) { + BigDecimal money = new BigDecimal("9999.99"); + + // Should not throw an "Index is out of range error" + // Should not throw R_parameterNotDefinedForProcedure + try (CallableStatement callableStatement = conn + .prepareCall("{call " + manyParamProc + "(?,?,?,?,?,?,?,?,?,?)}")) { + callableStatement.setObject("@p1", money, microsoft.sql.Types.MONEY); + callableStatement.setObject("@p2", money, microsoft.sql.Types.MONEY); + callableStatement.setObject("@p3", money, microsoft.sql.Types.MONEY); + callableStatement.setObject("@p4", money, microsoft.sql.Types.MONEY); + callableStatement.setObject("@p5", money, microsoft.sql.Types.MONEY); + callableStatement.setObject("@p6", money, microsoft.sql.Types.MONEY); + callableStatement.setObject("@p7", money, microsoft.sql.Types.MONEY); + callableStatement.setObject("@p8", money, microsoft.sql.Types.MONEY); + callableStatement.setObject("@p9", money, microsoft.sql.Types.MONEY); + callableStatement.setObject("@p10", money, microsoft.sql.Types.MONEY); + callableStatement.execute(); + } + } + } + + @Test + public void testCallableStatementSpPrepare() throws SQLException { + connection.setPrepareMethod("prepare"); + + try (Statement statement = connection.createStatement();) { + statement.executeUpdate("create procedure " + procName + " as select 1 --"); + + try (CallableStatement callableStatement = connection.prepareCall("{call " + procName + "}")) { + try (ResultSet rs = callableStatement.executeQuery()) { // Takes sp_executesql path + rs.next(); + assertEquals(1, rs.getInt(1), TestResource.getResource("R_setDataNotEqual")); + } + + try (ResultSet rs = callableStatement.executeQuery()) { // Takes sp_prepare path + rs.next(); + assertEquals(1, rs.getInt(1), TestResource.getResource("R_setDataNotEqual")); + } + } + } + } + + /** + * Tests CallableStatement.getString() with uniqueidentifier parameter + * + * @throws SQLException + */ + @Test + public void getStringGUIDTest() throws SQLException { + + String sql = "{call " + outputProcedureNameGUID + "(?)}"; + + try (SQLServerCallableStatement callableStatement = (SQLServerCallableStatement) connection.prepareCall(sql)) { + + UUID originalValue = UUID.randomUUID(); + + callableStatement.registerOutParameter(1, microsoft.sql.Types.GUID); + callableStatement.setObject(1, originalValue.toString(), microsoft.sql.Types.GUID); + callableStatement.execute(); + + String retrievedValue = callableStatement.getString(1); + + assertEquals(originalValue.toString().toLowerCase(), retrievedValue.toLowerCase()); + + } + } + + /** + * test for setNull(index, varchar) to behave as setNull(index, nvarchar) when + * SendStringParametersAsUnicode is true + * + * @throws SQLException + */ + @Test + public void getSetNullWithTypeVarchar() throws SQLException { + String polishchar = "\u0143"; + + SQLServerDataSource ds = new SQLServerDataSource(); + ds.setURL(connectionString); + ds.setSendStringParametersAsUnicode(true); + String sql = "{? = call " + setNullProcedureName + " (?,?)}"; + try (Connection connection = ds.getConnection(); + SQLServerCallableStatement cs = (SQLServerCallableStatement) connection.prepareCall(sql); + SQLServerCallableStatement cs2 = (SQLServerCallableStatement) connection.prepareCall(sql)) { + + cs.registerOutParameter(1, Types.INTEGER); + cs.setString(2, polishchar); + cs.setString(3, null); + cs.registerOutParameter(3, Types.VARCHAR); + cs.execute(); + + String expected = cs.getString(3); + + cs2.registerOutParameter(1, Types.INTEGER); + cs2.setString(2, polishchar); + cs2.setNull(3, Types.VARCHAR); + cs2.registerOutParameter(3, Types.NVARCHAR); + cs2.execute(); + + String actual = cs2.getString(3); + + assertEquals(expected, actual); + } + } + + /** + * Tests getObject(n, java.time.LocalDateTime.class). + * + * @throws SQLException + */ + @Test + public void testGetObjectAsLocalDateTime() throws SQLException { + String sql = "{CALL " + getObjectLocalDateTimeProcedureName + " (?)}"; + try (Connection con = DriverManager.getConnection(connectionString); + CallableStatement cs = con.prepareCall(sql)) { + cs.registerOutParameter(1, Types.TIMESTAMP); + TimeZone prevTimeZone = TimeZone.getDefault(); + TimeZone.setDefault(TimeZone.getTimeZone("America/Edmonton")); + + // a local date/time that does not actually exist because of Daylight Saving + // Time + final String testValueDate = "2018-03-11"; + final String testValueTime = "02:00:00.1234567"; + final String testValueDateTime = testValueDate + "T" + testValueTime; + + try { + cs.execute(); + + LocalDateTime expectedLocalDateTime = LocalDateTime.parse(testValueDateTime); + LocalDateTime actualLocalDateTime = cs.getObject(1, LocalDateTime.class); + assertEquals(expectedLocalDateTime, actualLocalDateTime); + + LocalDate expectedLocalDate = LocalDate.parse(testValueDate); + LocalDate actualLocalDate = cs.getObject(1, LocalDate.class); + assertEquals(expectedLocalDate, actualLocalDate); + + LocalTime expectedLocalTime = LocalTime.parse(testValueTime); + LocalTime actualLocalTime = cs.getObject(1, LocalTime.class); + assertEquals(expectedLocalTime, actualLocalTime); + } finally { + TimeZone.setDefault(prevTimeZone); + } + } + } + + /** + * Tests getObject(n, java.time.OffsetDateTime.class) and getObject(n, + * java.time.OffsetTime.class). + * + * @throws SQLException + */ + @Test + @Tag(Constants.xAzureSQLDW) + public void testGetObjectAsOffsetDateTime() throws SQLException { + String sql = "{CALL " + getObjectOffsetDateTimeProcedureName + " (?, ?)}"; + try (Connection con = DriverManager.getConnection(connectionString); + CallableStatement cs = con.prepareCall(sql)) { + cs.registerOutParameter(1, Types.TIMESTAMP_WITH_TIMEZONE); + cs.registerOutParameter(2, Types.TIMESTAMP_WITH_TIMEZONE); + + final String testValue = "2018-01-02T11:22:33.123456700+12:34"; + + cs.execute(); + + OffsetDateTime expected = OffsetDateTime.parse(testValue); + OffsetDateTime actual = cs.getObject(1, OffsetDateTime.class); + assertEquals(expected, actual); + assertNull(cs.getObject(2, OffsetDateTime.class)); + + OffsetTime expectedTime = OffsetTime.parse(testValue.split("T")[1]); + OffsetTime actualTime = cs.getObject(1, OffsetTime.class); + assertEquals(expectedTime, actualTime); + assertNull(cs.getObject(2, OffsetTime.class)); + } + } + + /** + * recognize parameter names with and without leading '@' + * + * @throws SQLException + */ + @Test + public void inputParamsTest() throws SQLException { + String call = "{CALL " + inputParamsProcedureName + " (?,?)}"; + + // the historical way: no leading '@', parameter names respected (not + // positional) + try (CallableStatement cs = connection.prepareCall(call)) { + cs.setString("p2", "world"); + cs.setString("p1", "hello"); + try (ResultSet rs = cs.executeQuery()) { + rs.next(); + assertEquals("helloworld", rs.getString(1)); + } + } + + // the "new" way: leading '@', parameter names still respected (not positional) + try (CallableStatement cs = connection.prepareCall(call)) { + cs.setString("@p2", "world!"); + cs.setString("@p1", "Hello "); + try (ResultSet rs = cs.executeQuery()) { + rs.next(); + assertEquals("Hello world!", rs.getString(1)); + } + } + + // sanity check: unrecognized parameter name + try (CallableStatement cs = connection.prepareCall(call)) { + cs.setString("@whatever", "test"); + fail(TestResource.getResource("R_shouldThrowException")); + } catch (SQLException sse) { + MessageFormat form = new MessageFormat(TestResource.getResource("R_parameterNotDefined")); + Object[] msgArgs = { "@whatever" }; + + if (!sse.getMessage().startsWith(form.format(msgArgs))) { + fail(TestResource.getResource("R_unexpectedExceptionContent")); + } + } + } + + @Test + public void testZeroParamSproc() throws SQLException { + String call = "{? = CALL " + zeroParamSproc + "}"; + + try (CallableStatement cs = connection.prepareCall(call)) { + cs.registerOutParameter(1, Types.INTEGER); + cs.execute(); + assertEquals(1, cs.getInt(1)); + } + + // Test zero parameter sproc with return value with parentheses + call = "{? = CALL " + zeroParamSproc + "()}"; + + try (CallableStatement cs = connection.prepareCall(call)) { + cs.registerOutParameter(1, Types.INTEGER); + cs.execute(); + // Calling zero parameter sproc with return value with parentheses + // should return a value that's not zero + assertEquals(1, cs.getInt(1)); + } + } + + @Test + public void testExecuteSystemStoredProcedureNamedParametersAndIndexedParameterNoResultset() throws SQLException { + String call0 = "EXEC sp_getapplock @Resource=?, @LockTimeout='0', @LockMode='Exclusive', @LockOwner='Session'"; + String call1 = "\rEXEC\r\rsp_getapplock @Resource=?, @LockTimeout='0', @LockMode='Exclusive', @LockOwner='Session'"; + String call2 = " EXEC sp_getapplock @Resource=?, @LockTimeout='0', @LockMode='Exclusive', @LockOwner='Session'"; + String call3 = "\tEXEC\t\t\tsp_getapplock @Resource=?, @LockTimeout='0', @LockMode='Exclusive', @LockOwner='Session'"; + + try (CallableStatement cstmt0 = connection.prepareCall(call0); + CallableStatement cstmt1 = connection.prepareCall(call1); + CallableStatement cstmt2 = connection.prepareCall(call2); + CallableStatement cstmt3 = connection.prepareCall(call3);) { + cstmt0.setString(1, "Resource-" + UUID.randomUUID()); + cstmt0.execute(); + + cstmt1.setString(1, "Resource-" + UUID.randomUUID()); + cstmt1.execute(); + + cstmt2.setString(1, "Resource-" + UUID.randomUUID()); + cstmt2.execute(); + + cstmt3.setString(1, "Resource-" + UUID.randomUUID()); + cstmt3.execute(); + } + } + + @Test + public void testExecSystemStoredProcedureNamedParametersAndIndexedParameterResultSet() throws SQLException { + String call = "exec sp_sproc_columns_100 ?, @ODBCVer=3, @fUsePattern=0"; + + try (CallableStatement cstmt = connection.prepareCall(call)) { + cstmt.setString(1, "sp_getapplock"); + + try (ResultSet rs = cstmt.executeQuery()) { + while (rs.next()) { + assertTrue(TestResource.getResource("R_resultSetEmpty"), !rs.getString(4).isEmpty()); + } + } + } + } + + @Test + public void testExecSystemStoredProcedureNoIndexedParametersResultSet() throws SQLException { + String call = "execute sp_sproc_columns_100 sp_getapplock, @ODBCVer=3, @fUsePattern=0"; + + try (CallableStatement cstmt = connection.prepareCall(call); ResultSet rs = cstmt.executeQuery()) { + while (rs.next()) { + assertTrue(TestResource.getResource("R_resultSetEmpty"), !rs.getString(4).isEmpty()); + } + } + } + + @Test + public void testExecDocumentedSystemStoredProceduresIndexedParameters() throws SQLException { + String serverName; + String testTableName = "testTable"; + Integer integer = new Integer(1); + + try (Statement stmt = connection.createStatement(); ResultSet rs = stmt.executeQuery("SELECT @@SERVERNAME")) { + rs.next(); + serverName = rs.getString(1); + } + + String[] sprocs = { "EXEC sp_column_privileges ?", "exec sp_catalogs ?", "execute sp_column_privileges ?", + "EXEC sp_column_privileges_ex ?", "EXECUTE sp_columns ?", "execute sp_datatype_info ?", + "EXEC sp_sproc_columns ?", "EXECUTE sp_server_info ?", "exec sp_special_columns ?", + "execute sp_statistics ?", "EXEC sp_table_privileges ?", "exec sp_tables ?" }; + + Object[] params = { testTableName, serverName, testTableName, serverName, testTableName, integer, + "sp_column_privileges", integer, testTableName, testTableName, testTableName, testTableName }; + + int paramIndex = 0; + + for (String sproc : sprocs) { + try (CallableStatement cstmt = connection.prepareCall(sproc)) { + cstmt.setObject(1, params[paramIndex]); + cstmt.execute(); + paramIndex++; + } catch (Exception e) { + fail("Failed executing '" + sproc + "' with indexed parameter '" + params[paramIndex]); + } + } + } + + @Test + public void testCallableStatementDefaultValues() throws SQLException { + String call0 = "{call " + conditionalSproc + " (?, ?, 1)}"; + String call1 = "{call " + conditionalSproc + " (?, ?, 2)}"; + int expectedValue = 5; // The sproc should return this value + + try (CallableStatement cstmt = connection.prepareCall(call0)) { + cstmt.setInt(1, 1); + cstmt.setInt(2, 2); + cstmt.execute(); + ResultSet rs = cstmt.getResultSet(); + rs.next(); + fail(TestResource.getResource("R_expectedFailPassed")); + + } catch (Exception e) { + String msg = e.getMessage(); + assertTrue(TestResource.getResource("R_nullPointerExceptionFromResultSet").equalsIgnoreCase(msg) + || msg == null); + } + + try (CallableStatement cstmt = connection.prepareCall(call1)) { + cstmt.setInt(1, 1); + cstmt.setInt(2, 2); + cstmt.execute(); + ResultSet rs = cstmt.getResultSet(); + rs.next(); + + assertEquals(Integer.toString(expectedValue), rs.getString(1)); + } + } + + @Test + public void testCallableStatementSetByAnnotatedArgs() throws SQLException { + String call = "{? = call " + simpleRetValSproc + " (@Arg1 = ?)}"; + int expectedValue = 1; // The sproc should return this value + + try (CallableStatement cstmt = connection.prepareCall(call)) { + cstmt.registerOutParameter(1, Types.INTEGER); + cstmt.setInt(1, 2); + cstmt.setString(2, "foo"); + cstmt.execute(); + + Assert.assertEquals(expectedValue, cstmt.getInt(1)); + } + } + + @Test + @Tag(Constants.reqExternalSetup) + @Tag(Constants.xAzureSQLDB) + @Tag(Constants.xAzureSQLDW) + @Tag(Constants.xAzureSQLMI) + public void testFourPartSyntaxCallEscapeSyntax() throws SQLException { + String table = "serverList"; + + try (Statement stmt = connection.createStatement()) { + stmt.execute("IF OBJECT_ID(N'" + table + "') IS NOT NULL DROP TABLE " + table); + stmt.execute("CREATE TABLE " + table + + " (serverName varchar(100),network varchar(100),serverStatus varchar(4000), id int, collation varchar(100), connectTimeout int, queryTimeout int)"); + stmt.execute("INSERT " + table + " EXEC sp_helpserver"); + + ResultSet rs = stmt + .executeQuery("SELECT COUNT(*) FROM " + table + " WHERE serverName = N'" + linkedServer + "'"); + rs.next(); + + if (rs.getInt(1) == 1) { + stmt.execute("EXEC sp_dropserver @server='" + linkedServer + "';"); + } + + stmt.execute("EXEC sp_addlinkedserver @server='" + linkedServer + "';"); + stmt.execute("EXEC sp_addlinkedsrvlogin @rmtsrvname=N'" + linkedServer + "', @useself=false" + + ", @rmtuser=N'" + linkedServerUser + "', @rmtpassword=N'" + linkedServerPassword + "'"); + stmt.execute("EXEC sp_serveroption '" + linkedServer + "', 'rpc', true;"); + stmt.execute("EXEC sp_serveroption '" + linkedServer + "', 'rpc out', true;"); + } + + SQLServerDataSource ds = new SQLServerDataSource(); + ds.setServerName(linkedServer); + ds.setUser(linkedServerUser); + ds.setPassword(linkedServerPassword); + ds.setEncrypt(false); + ds.setTrustServerCertificate(true); + + try (Connection linkedServerConnection = ds.getConnection(); + Statement stmt = linkedServerConnection.createStatement()) { + stmt.execute( + "create or alter procedure dbo.TestAdd(@Num1 int, @Num2 int, @Result int output) as begin set @Result = @Num1 + @Num2; end;"); + + stmt.execute("create or alter procedure dbo.TestReturn(@Num1 int) as select @Num1 return @Num1*3 "); + } + + try (CallableStatement cstmt = connection + .prepareCall("{call [" + linkedServer + "].master.dbo.TestAdd(?,?,?)}")) { + int sum = 11; + int param0 = 1; + int param1 = 10; + cstmt.setInt(1, param0); + cstmt.setInt(2, param1); + cstmt.registerOutParameter(3, Types.INTEGER); + cstmt.execute(); + assertEquals(sum, cstmt.getInt(3)); + } + + try (CallableStatement cstmt = connection.prepareCall("exec [" + linkedServer + "].master.dbo.TestAdd ?,?,?")) { + int sum = 11; + int param0 = 1; + int param1 = 10; + cstmt.setInt(1, param0); + cstmt.setInt(2, param1); + cstmt.registerOutParameter(3, Types.INTEGER); + cstmt.execute(); + assertEquals(sum, cstmt.getInt(3)); + } + + try (CallableStatement cstmt = connection + .prepareCall("{? = call [" + linkedServer + "].master.dbo.TestReturn(?)}")) { + int expected = 15; + cstmt.registerOutParameter(1, java.sql.Types.INTEGER); + cstmt.setInt(2, 5); + cstmt.execute(); + assertEquals(expected, cstmt.getInt(1)); + } + } + + @Test + public void testTimestampStringConversion() throws SQLException { + try (CallableStatement stmt = connection.prepareCall("{call " + currentTimeProc + "(?)}")) { + String timestamp = "2024-05-29 15:35:53.461"; + stmt.setObject(1, timestamp, Types.TIMESTAMP); + stmt.registerOutParameter(1, Types.TIMESTAMP); + stmt.execute(); + stmt.getObject("currentTimeStamp"); + } + } + + @Test + public void testJSONColumnInTableWithSetObject() throws SQLException { + + try (Connection con = DriverManager.getConnection(connectionString); Statement stmt = con.createStatement()) { + String jsonString = "{\"key\":\"value\"}"; + try (CallableStatement callableStatement = con + .prepareCall("INSERT INTO " + tableNameJSON + " (col1) VALUES (?)")) { + callableStatement.setObject(1, jsonString, microsoft.sql.Types.JSON); + callableStatement.execute(); + } + + try (Statement queryStmt = con.createStatement(); + ResultSet rs = queryStmt.executeQuery("SELECT col1 FROM " + tableNameJSON)) { + assertTrue(rs.next()); + assertEquals(jsonString, rs.getObject(1)); + } + } + } + + @Test + public void testJSONProcedureWithSetObject() throws SQLException { + + try (Connection con = DriverManager.getConnection(connectionString); Statement stmt = con.createStatement()) { + String jsonString = "{\"key\":\"value\"}"; + try (CallableStatement callableStatement = con.prepareCall("{call " + procedureNameJSON + " (?)}")) { + callableStatement.setObject(1, jsonString, microsoft.sql.Types.JSON); + callableStatement.execute(); + + try (ResultSet rs = callableStatement.getResultSet()) { + assertTrue(rs.next()); + assertEquals(jsonString, rs.getObject("col1")); + } + } + } + } + + /** + * Cleanup after test + * + * @throws SQLException + */ + @AfterAll + public static void cleanup() throws SQLException { + try (Statement stmt = connection.createStatement()) { + TestUtils.dropTableIfExists(tableNameGUID, stmt); + TestUtils.dropTableIfExists(manyParamsTable, stmt); + TestUtils.dropProcedureIfExists(outputProcedureNameGUID, stmt); + TestUtils.dropProcedureIfExists(setNullProcedureName, stmt); + TestUtils.dropProcedureIfExists(inputParamsProcedureName, stmt); + TestUtils.dropProcedureIfExists(getObjectLocalDateTimeProcedureName, stmt); + TestUtils.dropProcedureIfExists(getObjectOffsetDateTimeProcedureName, stmt); + TestUtils.dropProcedureIfExists(currentTimeProc, stmt); + TestUtils.dropProcedureIfExists(conditionalSproc, stmt); + TestUtils.dropProcedureIfExists(simpleRetValSproc, stmt); + TestUtils.dropProcedureIfExists(zeroParamSproc, stmt); + TestUtils.dropTableIfExists(tableNameJSON, stmt); + TestUtils.dropProcedureIfExists(procedureNameJSON, stmt); + } + } + + private static void createGUIDStoredProcedure(Statement stmt) throws SQLException { + String sql = "CREATE PROCEDURE " + outputProcedureNameGUID + + "(@p1 uniqueidentifier OUTPUT) AS SELECT @p1 = c1 FROM " + tableNameGUID + Constants.SEMI_COLON; + stmt.execute(sql); + } + + private static void createGUIDTable(Statement stmt) throws SQLException { + String sql = "CREATE TABLE " + tableNameGUID + " (c1 uniqueidentifier null)"; + stmt.execute(sql); + } + + private static void createSetNullProcedure(Statement stmt) throws SQLException { + stmt.execute("create procedure " + setNullProcedureName + + " (@p1 nvarchar(255), @p2 nvarchar(255) output) as select @p2=@p1 return 0"); + } + + private static void createInputParamsProcedure(Statement stmt) throws SQLException { + String sql = "CREATE PROCEDURE " + inputParamsProcedureName + " @p1 nvarchar(max) = N'parameter1', " + + " @p2 nvarchar(max) = N'parameter2' " + "AS " + "BEGIN " + " SET NOCOUNT ON; " + + " SELECT @p1 + @p2 AS result; " + "END "; + + stmt.execute(sql); + } + + private static void createGetObjectLocalDateTimeProcedure(Statement stmt) throws SQLException { + String sql = "CREATE PROCEDURE " + getObjectLocalDateTimeProcedureName + "(@p1 datetime2(7) OUTPUT) AS " + + "SELECT @p1 = '2018-03-11T02:00:00.1234567'"; + stmt.execute(sql); + } + + private static void createGetObjectOffsetDateTimeProcedure(Statement stmt) throws SQLException { + String sql = "CREATE PROCEDURE " + getObjectOffsetDateTimeProcedureName + + "(@p1 DATETIMEOFFSET OUTPUT, @p2 DATETIMEOFFSET OUTPUT) AS " + + "SELECT @p1 = '2018-01-02T11:22:33.123456700+12:34', @p2 = NULL"; + stmt.execute(sql); + } + + private static void createProcedureManyParams() throws SQLException { + String type = manyParamUserDefinedType; + String sql = "CREATE PROCEDURE " + manyParamProc + " @p1 " + type + ", @p2 " + type + ", @p3 " + type + ", @p4 " + + type + ", @p5 " + type + ", @p6 " + type + ", @p7 " + type + ", @p8 " + type + ", @p9 " + type + + ", @p10 " + type + " AS INSERT INTO " + manyParamsTable + + " VALUES(@p1, @p2, @p3, @p4, @p5, @p6, @p7, @p8, @p9, @p10)"; + try (Statement stmt = connection.createStatement()) { + stmt.execute(sql); + } + } + + private static void createProcedureCurrentTime() throws SQLException { + String sql = "CREATE PROCEDURE " + currentTimeProc + " @currentTimeStamp datetime = null OUTPUT " + + "AS BEGIN SET @currentTimeStamp = CURRENT_TIMESTAMP; END"; + try (Statement stmt = connection.createStatement()) { + stmt.execute(sql); + } + } + + private static void createConditionalProcedure() throws SQLException { + String sql = "CREATE PROCEDURE " + conditionalSproc + " @param0 INT, @param1 INT, @maybe bigint = 2 " + + "AS BEGIN IF @maybe >= 2 BEGIN SELECT 5 END END"; + try (Statement stmt = connection.createStatement()) { + stmt.execute(sql); + } + } + + private static void createSimpleRetValSproc() throws SQLException { + String sql = "CREATE PROCEDURE " + simpleRetValSproc + + " (@Arg1 VARCHAR(128)) AS DECLARE @ReturnCode INT RETURN 1"; + try (Statement stmt = connection.createStatement()) { + stmt.execute(sql); + } + } + + private static void createTableManyParams() throws SQLException { + String type = manyParamUserDefinedType; + String sql = "CREATE TABLE" + manyParamsTable + " (c1 " + type + " null, " + "c2 " + type + " null, " + "c3 " + + type + " null, " + "c4 " + type + " null, " + "c5 " + type + " null, " + "c6 " + type + " null, " + + "c7 " + type + " null, " + "c8 " + type + " null, " + "c9 " + type + " null, " + "c10 " + type + + " null);"; + try (Statement stmt = connection.createStatement()) { + stmt.execute(sql); + } + } + + private static void createProcedureZeroParams() throws SQLException { + String sql = "CREATE PROCEDURE " + zeroParamSproc + " AS RETURN 1"; + try (Statement stmt = connection.createStatement()) { + stmt.execute(sql); + } + } + + private static void createUserDefinedType() throws SQLException { + String TVPCreateCmd = "CREATE TYPE " + manyParamUserDefinedType + " FROM MONEY"; + try (Statement stmt = connection.createStatement()) { + stmt.executeUpdate(TVPCreateCmd); + } + } + + private static void createJSONTestTable(Statement stmt) throws SQLException { + String sql = "CREATE TABLE " + tableNameJSON + " (" + "id INT PRIMARY KEY IDENTITY(1,1), " + "col1 JSON)"; + stmt.execute(sql); + } + + private static void createJSONStoredProcedure(Statement stmt) throws SQLException { + String sql = "CREATE PROCEDURE " + procedureNameJSON + " (@jsonInput JSON) " + "AS " + "BEGIN " + + " SELECT @jsonInput AS col1; " + "END"; + stmt.execute(sql); + } } From a8ebfd3be3fe0496f125d602125ec22ac03a97fa Mon Sep 17 00:00:00 2001 From: Ananya Garg Date: Fri, 3 Jan 2025 11:38:18 +0530 Subject: [PATCH 10/13] Updated callableStatementTest.java --- .../CallableStatementTest.java | 1354 ++++++++--------- 1 file changed, 674 insertions(+), 680 deletions(-) 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 f5200f756..f92d774c5 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java @@ -40,577 +40,572 @@ import com.microsoft.sqlserver.testframework.AbstractTest; import com.microsoft.sqlserver.testframework.Constants; + /** * Test CallableStatement */ @RunWith(JUnitPlatform.class) @Tag(Constants.xAzureSQLDW) public class CallableStatementTest extends AbstractTest { - private static String tableNameGUID = AbstractSQLGenerator - .escapeIdentifier(RandomUtil.getIdentifier("uniqueidentifier_Table")); - private static String outputProcedureNameGUID = AbstractSQLGenerator - .escapeIdentifier(RandomUtil.getIdentifier("uniqueidentifier_SP")); - private static String setNullProcedureName = AbstractSQLGenerator - .escapeIdentifier(RandomUtil.getIdentifier("CallableStatementTest_setNull_SP")); - private static String inputParamsProcedureName = AbstractSQLGenerator - .escapeIdentifier(RandomUtil.getIdentifier("CallableStatementTest_inputParams_SP")); - private static String conditionalSproc = AbstractSQLGenerator - .escapeIdentifier(RandomUtil.getIdentifier("CallableStatementTest_conditionalSproc")); - private static String simpleRetValSproc = AbstractSQLGenerator - .escapeIdentifier(RandomUtil.getIdentifier("CallableStatementTest_simpleSproc")); - private static String getObjectLocalDateTimeProcedureName = AbstractSQLGenerator - .escapeIdentifier(RandomUtil.getIdentifier("CallableStatementTest_getObjectLocalDateTime_SP")); - private static String getObjectOffsetDateTimeProcedureName = AbstractSQLGenerator - .escapeIdentifier(RandomUtil.getIdentifier("CallableStatementTest_getObjectOffsetDateTime_SP")); - private static String procName = AbstractSQLGenerator - .escapeIdentifier(RandomUtil.getIdentifier("procedureTestCallableStatementSpPrepare")); - private static String manyParamsTable = AbstractSQLGenerator - .escapeIdentifier(RandomUtil.getIdentifier("manyParam_Table")); - private static String manyParamProc = AbstractSQLGenerator - .escapeIdentifier(RandomUtil.getIdentifier("manyParam_Procedure")); - private static String currentTimeProc = AbstractSQLGenerator - .escapeIdentifier(RandomUtil.getIdentifier("currentTime_Procedure")); - private static String manyParamUserDefinedType = AbstractSQLGenerator - .escapeIdentifier(RandomUtil.getIdentifier("manyParam_definedType")); - private static String zeroParamSproc = AbstractSQLGenerator - .escapeIdentifier(RandomUtil.getIdentifier("zeroParamSproc")); - private static String tableNameJSON = "TestJSONTable"; + private static String tableNameGUID = AbstractSQLGenerator + .escapeIdentifier(RandomUtil.getIdentifier("uniqueidentifier_Table")); + private static String outputProcedureNameGUID = AbstractSQLGenerator + .escapeIdentifier(RandomUtil.getIdentifier("uniqueidentifier_SP")); + private static String setNullProcedureName = AbstractSQLGenerator + .escapeIdentifier(RandomUtil.getIdentifier("CallableStatementTest_setNull_SP")); + private static String inputParamsProcedureName = AbstractSQLGenerator + .escapeIdentifier(RandomUtil.getIdentifier("CallableStatementTest_inputParams_SP")); + private static String conditionalSproc = AbstractSQLGenerator + .escapeIdentifier(RandomUtil.getIdentifier("CallableStatementTest_conditionalSproc")); + private static String simpleRetValSproc = AbstractSQLGenerator + .escapeIdentifier(RandomUtil.getIdentifier("CallableStatementTest_simpleSproc")); + private static String getObjectLocalDateTimeProcedureName = AbstractSQLGenerator + .escapeIdentifier(RandomUtil.getIdentifier("CallableStatementTest_getObjectLocalDateTime_SP")); + private static String getObjectOffsetDateTimeProcedureName = AbstractSQLGenerator + .escapeIdentifier(RandomUtil.getIdentifier("CallableStatementTest_getObjectOffsetDateTime_SP")); + private static String procName = AbstractSQLGenerator + .escapeIdentifier(RandomUtil.getIdentifier("procedureTestCallableStatementSpPrepare")); + private static String manyParamsTable = AbstractSQLGenerator + .escapeIdentifier(RandomUtil.getIdentifier("manyParam_Table")); + private static String manyParamProc = AbstractSQLGenerator + .escapeIdentifier(RandomUtil.getIdentifier("manyParam_Procedure")); + private static String currentTimeProc = AbstractSQLGenerator + .escapeIdentifier(RandomUtil.getIdentifier("currentTime_Procedure")); + private static String manyParamUserDefinedType = AbstractSQLGenerator + .escapeIdentifier(RandomUtil.getIdentifier("manyParam_definedType")); + private static String zeroParamSproc = AbstractSQLGenerator + .escapeIdentifier(RandomUtil.getIdentifier("zeroParamSproc")); + private static String tableNameJSON = "TestJSONTable"; private static String procedureNameJSON = AbstractSQLGenerator .escapeIdentifier(RandomUtil.getIdentifier("TestJSONProcedure")); - /** - * Setup before test - * - * @throws SQLException - */ - @BeforeAll - public static void setupTest() throws Exception { - setConnection(); - - try (Statement stmt = connection.createStatement()) { - TestUtils.dropTableIfExists(tableNameGUID, stmt); - TestUtils.dropProcedureIfExists(outputProcedureNameGUID, stmt); - TestUtils.dropProcedureIfExists(setNullProcedureName, stmt); - TestUtils.dropProcedureIfExists(inputParamsProcedureName, stmt); - TestUtils.dropProcedureIfExists(getObjectLocalDateTimeProcedureName, stmt); - TestUtils.dropProcedureIfExists(getObjectOffsetDateTimeProcedureName, stmt); - TestUtils.dropProcedureIfExists(conditionalSproc, stmt); - TestUtils.dropProcedureIfExists(simpleRetValSproc, stmt); - TestUtils.dropProcedureIfExists(zeroParamSproc, stmt); - TestUtils.dropUserDefinedTypeIfExists(manyParamUserDefinedType, stmt); - TestUtils.dropProcedureIfExists(manyParamProc, stmt); - TestUtils.dropTableIfExists(manyParamsTable, stmt); - TestUtils.dropTableIfExists(tableNameJSON, stmt); + /** + * Setup before test + * + * @throws SQLException + */ + @BeforeAll + public static void setupTest() throws Exception { + setConnection(); + + try (Statement stmt = connection.createStatement()) { + TestUtils.dropTableIfExists(tableNameGUID, stmt); + TestUtils.dropProcedureIfExists(outputProcedureNameGUID, stmt); + TestUtils.dropProcedureIfExists(setNullProcedureName, stmt); + TestUtils.dropProcedureIfExists(inputParamsProcedureName, stmt); + TestUtils.dropProcedureIfExists(getObjectLocalDateTimeProcedureName, stmt); + TestUtils.dropProcedureIfExists(getObjectOffsetDateTimeProcedureName, stmt); + TestUtils.dropProcedureIfExists(conditionalSproc, stmt); + TestUtils.dropProcedureIfExists(simpleRetValSproc, stmt); + TestUtils.dropProcedureIfExists(zeroParamSproc, stmt); + TestUtils.dropUserDefinedTypeIfExists(manyParamUserDefinedType, stmt); + TestUtils.dropProcedureIfExists(manyParamProc, stmt); + TestUtils.dropTableIfExists(manyParamsTable, stmt); + TestUtils.dropTableIfExists(tableNameJSON, stmt); TestUtils.dropProcedureIfExists(procedureNameJSON, stmt); - createGUIDTable(stmt); - createGUIDStoredProcedure(stmt); - createSetNullProcedure(stmt); - createInputParamsProcedure(stmt); - createGetObjectLocalDateTimeProcedure(stmt); - createUserDefinedType(); - createTableManyParams(); - createProcedureManyParams(); - createProcedureZeroParams(); - createProcedureCurrentTime(); - createGetObjectOffsetDateTimeProcedure(stmt); - createConditionalProcedure(); - createSimpleRetValSproc(); - createJSONTestTable(stmt); + createGUIDTable(stmt); + createGUIDStoredProcedure(stmt); + createSetNullProcedure(stmt); + createInputParamsProcedure(stmt); + createGetObjectLocalDateTimeProcedure(stmt); + createUserDefinedType(); + createTableManyParams(); + createProcedureManyParams(); + createProcedureZeroParams(); + createProcedureCurrentTime(); + createGetObjectOffsetDateTimeProcedure(stmt); + createConditionalProcedure(); + createSimpleRetValSproc(); + createJSONTestTable(stmt); createJSONStoredProcedure(stmt); - } - } - - // Test Needs more work to be configured to run on azureDB as there are slight - // differences - // between the regular SQL Server vs. azureDB - @Test - @Tag(Constants.xAzureSQLDB) - public void testCallableStatementManyParameters() throws SQLException { - String tempPass = UUID.randomUUID().toString(); - String dropLogin = "IF EXISTS (select * from sys.sql_logins where name = 'NewLogin') DROP LOGIN NewLogin"; - String dropUser = "IF EXISTS (select * from sys.sysusers where name = 'NewUser') DROP USER NewUser"; - String createLogin = "USE MASTER;CREATE LOGIN NewLogin WITH PASSWORD=N'" + tempPass + "', " - + "DEFAULT_DATABASE = MASTER, DEFAULT_LANGUAGE = US_ENGLISH;ALTER LOGIN NewLogin ENABLE;"; - String createUser = "USE MASTER;CREATE USER NewUser FOR LOGIN NewLogin WITH DEFAULT_SCHEMA = [DBO];"; - String grantExecute = "GRANT EXECUTE ON " + manyParamProc + " TO NewUser;"; - - // Need to create a user with limited permissions in order to run through the - // code block we are testing - // The user created will execute sp_sproc_columns internally by the driver, - // which should not return all - // the column names as the user has limited permissions - try (Connection conn = PrepUtil.getConnection(connectionString)) { - try (Statement stmt = conn.createStatement()) { - stmt.execute(dropLogin); - stmt.execute(dropUser); - stmt.execute(createLogin); - stmt.execute(createUser); - stmt.execute(grantExecute); - } - } - - try (Connection conn = PrepUtil.getConnection(connectionString + ";user=NewLogin;password=" + tempPass + ";")) { - BigDecimal money = new BigDecimal("9999.99"); - - // Should not throw an "Index is out of range error" - // Should not throw R_parameterNotDefinedForProcedure - try (CallableStatement callableStatement = conn - .prepareCall("{call " + manyParamProc + "(?,?,?,?,?,?,?,?,?,?)}")) { - callableStatement.setObject("@p1", money, microsoft.sql.Types.MONEY); - callableStatement.setObject("@p2", money, microsoft.sql.Types.MONEY); - callableStatement.setObject("@p3", money, microsoft.sql.Types.MONEY); - callableStatement.setObject("@p4", money, microsoft.sql.Types.MONEY); - callableStatement.setObject("@p5", money, microsoft.sql.Types.MONEY); - callableStatement.setObject("@p6", money, microsoft.sql.Types.MONEY); - callableStatement.setObject("@p7", money, microsoft.sql.Types.MONEY); - callableStatement.setObject("@p8", money, microsoft.sql.Types.MONEY); - callableStatement.setObject("@p9", money, microsoft.sql.Types.MONEY); - callableStatement.setObject("@p10", money, microsoft.sql.Types.MONEY); - callableStatement.execute(); - } - } - } - - @Test - public void testCallableStatementSpPrepare() throws SQLException { - connection.setPrepareMethod("prepare"); - - try (Statement statement = connection.createStatement();) { - statement.executeUpdate("create procedure " + procName + " as select 1 --"); - - try (CallableStatement callableStatement = connection.prepareCall("{call " + procName + "}")) { - try (ResultSet rs = callableStatement.executeQuery()) { // Takes sp_executesql path - rs.next(); - assertEquals(1, rs.getInt(1), TestResource.getResource("R_setDataNotEqual")); - } - - try (ResultSet rs = callableStatement.executeQuery()) { // Takes sp_prepare path - rs.next(); - assertEquals(1, rs.getInt(1), TestResource.getResource("R_setDataNotEqual")); - } - } - } - } - - /** - * Tests CallableStatement.getString() with uniqueidentifier parameter - * - * @throws SQLException - */ - @Test - public void getStringGUIDTest() throws SQLException { - - String sql = "{call " + outputProcedureNameGUID + "(?)}"; - - try (SQLServerCallableStatement callableStatement = (SQLServerCallableStatement) connection.prepareCall(sql)) { - - UUID originalValue = UUID.randomUUID(); - - callableStatement.registerOutParameter(1, microsoft.sql.Types.GUID); - callableStatement.setObject(1, originalValue.toString(), microsoft.sql.Types.GUID); - callableStatement.execute(); - - String retrievedValue = callableStatement.getString(1); - - assertEquals(originalValue.toString().toLowerCase(), retrievedValue.toLowerCase()); - - } - } - - /** - * test for setNull(index, varchar) to behave as setNull(index, nvarchar) when - * SendStringParametersAsUnicode is true - * - * @throws SQLException - */ - @Test - public void getSetNullWithTypeVarchar() throws SQLException { - String polishchar = "\u0143"; - - SQLServerDataSource ds = new SQLServerDataSource(); - ds.setURL(connectionString); - ds.setSendStringParametersAsUnicode(true); - String sql = "{? = call " + setNullProcedureName + " (?,?)}"; - try (Connection connection = ds.getConnection(); - SQLServerCallableStatement cs = (SQLServerCallableStatement) connection.prepareCall(sql); - SQLServerCallableStatement cs2 = (SQLServerCallableStatement) connection.prepareCall(sql)) { - - cs.registerOutParameter(1, Types.INTEGER); - cs.setString(2, polishchar); - cs.setString(3, null); - cs.registerOutParameter(3, Types.VARCHAR); - cs.execute(); - - String expected = cs.getString(3); - - cs2.registerOutParameter(1, Types.INTEGER); - cs2.setString(2, polishchar); - cs2.setNull(3, Types.VARCHAR); - cs2.registerOutParameter(3, Types.NVARCHAR); - cs2.execute(); - - String actual = cs2.getString(3); - - assertEquals(expected, actual); - } - } - - /** - * Tests getObject(n, java.time.LocalDateTime.class). - * - * @throws SQLException - */ - @Test - public void testGetObjectAsLocalDateTime() throws SQLException { - String sql = "{CALL " + getObjectLocalDateTimeProcedureName + " (?)}"; - try (Connection con = DriverManager.getConnection(connectionString); - CallableStatement cs = con.prepareCall(sql)) { - cs.registerOutParameter(1, Types.TIMESTAMP); - TimeZone prevTimeZone = TimeZone.getDefault(); - TimeZone.setDefault(TimeZone.getTimeZone("America/Edmonton")); - - // a local date/time that does not actually exist because of Daylight Saving - // Time - final String testValueDate = "2018-03-11"; - final String testValueTime = "02:00:00.1234567"; - final String testValueDateTime = testValueDate + "T" + testValueTime; - - try { - cs.execute(); - - LocalDateTime expectedLocalDateTime = LocalDateTime.parse(testValueDateTime); - LocalDateTime actualLocalDateTime = cs.getObject(1, LocalDateTime.class); - assertEquals(expectedLocalDateTime, actualLocalDateTime); - - LocalDate expectedLocalDate = LocalDate.parse(testValueDate); - LocalDate actualLocalDate = cs.getObject(1, LocalDate.class); - assertEquals(expectedLocalDate, actualLocalDate); - - LocalTime expectedLocalTime = LocalTime.parse(testValueTime); - LocalTime actualLocalTime = cs.getObject(1, LocalTime.class); - assertEquals(expectedLocalTime, actualLocalTime); - } finally { - TimeZone.setDefault(prevTimeZone); - } - } - } - - /** - * Tests getObject(n, java.time.OffsetDateTime.class) and getObject(n, - * java.time.OffsetTime.class). - * - * @throws SQLException - */ - @Test - @Tag(Constants.xAzureSQLDW) - public void testGetObjectAsOffsetDateTime() throws SQLException { - String sql = "{CALL " + getObjectOffsetDateTimeProcedureName + " (?, ?)}"; - try (Connection con = DriverManager.getConnection(connectionString); - CallableStatement cs = con.prepareCall(sql)) { - cs.registerOutParameter(1, Types.TIMESTAMP_WITH_TIMEZONE); - cs.registerOutParameter(2, Types.TIMESTAMP_WITH_TIMEZONE); - - final String testValue = "2018-01-02T11:22:33.123456700+12:34"; - - cs.execute(); - - OffsetDateTime expected = OffsetDateTime.parse(testValue); - OffsetDateTime actual = cs.getObject(1, OffsetDateTime.class); - assertEquals(expected, actual); - assertNull(cs.getObject(2, OffsetDateTime.class)); - - OffsetTime expectedTime = OffsetTime.parse(testValue.split("T")[1]); - OffsetTime actualTime = cs.getObject(1, OffsetTime.class); - assertEquals(expectedTime, actualTime); - assertNull(cs.getObject(2, OffsetTime.class)); - } - } - - /** - * recognize parameter names with and without leading '@' - * - * @throws SQLException - */ - @Test - public void inputParamsTest() throws SQLException { - String call = "{CALL " + inputParamsProcedureName + " (?,?)}"; - - // the historical way: no leading '@', parameter names respected (not - // positional) - try (CallableStatement cs = connection.prepareCall(call)) { - cs.setString("p2", "world"); - cs.setString("p1", "hello"); - try (ResultSet rs = cs.executeQuery()) { - rs.next(); - assertEquals("helloworld", rs.getString(1)); - } - } - - // the "new" way: leading '@', parameter names still respected (not positional) - try (CallableStatement cs = connection.prepareCall(call)) { - cs.setString("@p2", "world!"); - cs.setString("@p1", "Hello "); - try (ResultSet rs = cs.executeQuery()) { - rs.next(); - assertEquals("Hello world!", rs.getString(1)); - } - } - - // sanity check: unrecognized parameter name - try (CallableStatement cs = connection.prepareCall(call)) { - cs.setString("@whatever", "test"); - fail(TestResource.getResource("R_shouldThrowException")); - } catch (SQLException sse) { - MessageFormat form = new MessageFormat(TestResource.getResource("R_parameterNotDefined")); - Object[] msgArgs = { "@whatever" }; - - if (!sse.getMessage().startsWith(form.format(msgArgs))) { - fail(TestResource.getResource("R_unexpectedExceptionContent")); - } - } - } - - @Test - public void testZeroParamSproc() throws SQLException { - String call = "{? = CALL " + zeroParamSproc + "}"; - - try (CallableStatement cs = connection.prepareCall(call)) { - cs.registerOutParameter(1, Types.INTEGER); - cs.execute(); - assertEquals(1, cs.getInt(1)); - } - - // Test zero parameter sproc with return value with parentheses - call = "{? = CALL " + zeroParamSproc + "()}"; - - try (CallableStatement cs = connection.prepareCall(call)) { - cs.registerOutParameter(1, Types.INTEGER); - cs.execute(); - // Calling zero parameter sproc with return value with parentheses - // should return a value that's not zero - assertEquals(1, cs.getInt(1)); - } - } - - @Test - public void testExecuteSystemStoredProcedureNamedParametersAndIndexedParameterNoResultset() throws SQLException { - String call0 = "EXEC sp_getapplock @Resource=?, @LockTimeout='0', @LockMode='Exclusive', @LockOwner='Session'"; - String call1 = "\rEXEC\r\rsp_getapplock @Resource=?, @LockTimeout='0', @LockMode='Exclusive', @LockOwner='Session'"; - String call2 = " EXEC sp_getapplock @Resource=?, @LockTimeout='0', @LockMode='Exclusive', @LockOwner='Session'"; - String call3 = "\tEXEC\t\t\tsp_getapplock @Resource=?, @LockTimeout='0', @LockMode='Exclusive', @LockOwner='Session'"; - - try (CallableStatement cstmt0 = connection.prepareCall(call0); - CallableStatement cstmt1 = connection.prepareCall(call1); - CallableStatement cstmt2 = connection.prepareCall(call2); - CallableStatement cstmt3 = connection.prepareCall(call3);) { - cstmt0.setString(1, "Resource-" + UUID.randomUUID()); - cstmt0.execute(); - - cstmt1.setString(1, "Resource-" + UUID.randomUUID()); - cstmt1.execute(); - - cstmt2.setString(1, "Resource-" + UUID.randomUUID()); - cstmt2.execute(); - - cstmt3.setString(1, "Resource-" + UUID.randomUUID()); - cstmt3.execute(); - } - } - - @Test - public void testExecSystemStoredProcedureNamedParametersAndIndexedParameterResultSet() throws SQLException { - String call = "exec sp_sproc_columns_100 ?, @ODBCVer=3, @fUsePattern=0"; - - try (CallableStatement cstmt = connection.prepareCall(call)) { - cstmt.setString(1, "sp_getapplock"); - - try (ResultSet rs = cstmt.executeQuery()) { - while (rs.next()) { - assertTrue(TestResource.getResource("R_resultSetEmpty"), !rs.getString(4).isEmpty()); - } - } - } - } - - @Test - public void testExecSystemStoredProcedureNoIndexedParametersResultSet() throws SQLException { - String call = "execute sp_sproc_columns_100 sp_getapplock, @ODBCVer=3, @fUsePattern=0"; - - try (CallableStatement cstmt = connection.prepareCall(call); ResultSet rs = cstmt.executeQuery()) { - while (rs.next()) { - assertTrue(TestResource.getResource("R_resultSetEmpty"), !rs.getString(4).isEmpty()); - } - } - } - - @Test - public void testExecDocumentedSystemStoredProceduresIndexedParameters() throws SQLException { - String serverName; - String testTableName = "testTable"; - Integer integer = new Integer(1); - - try (Statement stmt = connection.createStatement(); ResultSet rs = stmt.executeQuery("SELECT @@SERVERNAME")) { - rs.next(); - serverName = rs.getString(1); - } - - String[] sprocs = { "EXEC sp_column_privileges ?", "exec sp_catalogs ?", "execute sp_column_privileges ?", - "EXEC sp_column_privileges_ex ?", "EXECUTE sp_columns ?", "execute sp_datatype_info ?", - "EXEC sp_sproc_columns ?", "EXECUTE sp_server_info ?", "exec sp_special_columns ?", - "execute sp_statistics ?", "EXEC sp_table_privileges ?", "exec sp_tables ?" }; - - Object[] params = { testTableName, serverName, testTableName, serverName, testTableName, integer, - "sp_column_privileges", integer, testTableName, testTableName, testTableName, testTableName }; - - int paramIndex = 0; - - for (String sproc : sprocs) { - try (CallableStatement cstmt = connection.prepareCall(sproc)) { - cstmt.setObject(1, params[paramIndex]); - cstmt.execute(); - paramIndex++; - } catch (Exception e) { - fail("Failed executing '" + sproc + "' with indexed parameter '" + params[paramIndex]); - } - } - } - - @Test - public void testCallableStatementDefaultValues() throws SQLException { - String call0 = "{call " + conditionalSproc + " (?, ?, 1)}"; - String call1 = "{call " + conditionalSproc + " (?, ?, 2)}"; - int expectedValue = 5; // The sproc should return this value - - try (CallableStatement cstmt = connection.prepareCall(call0)) { - cstmt.setInt(1, 1); - cstmt.setInt(2, 2); - cstmt.execute(); - ResultSet rs = cstmt.getResultSet(); - rs.next(); - fail(TestResource.getResource("R_expectedFailPassed")); - - } catch (Exception e) { - String msg = e.getMessage(); - assertTrue(TestResource.getResource("R_nullPointerExceptionFromResultSet").equalsIgnoreCase(msg) - || msg == null); - } - - try (CallableStatement cstmt = connection.prepareCall(call1)) { - cstmt.setInt(1, 1); - cstmt.setInt(2, 2); - cstmt.execute(); - ResultSet rs = cstmt.getResultSet(); - rs.next(); - - assertEquals(Integer.toString(expectedValue), rs.getString(1)); - } - } - - @Test - public void testCallableStatementSetByAnnotatedArgs() throws SQLException { - String call = "{? = call " + simpleRetValSproc + " (@Arg1 = ?)}"; - int expectedValue = 1; // The sproc should return this value - - try (CallableStatement cstmt = connection.prepareCall(call)) { - cstmt.registerOutParameter(1, Types.INTEGER); - cstmt.setInt(1, 2); - cstmt.setString(2, "foo"); - cstmt.execute(); - - Assert.assertEquals(expectedValue, cstmt.getInt(1)); - } - } - - @Test - @Tag(Constants.reqExternalSetup) - @Tag(Constants.xAzureSQLDB) - @Tag(Constants.xAzureSQLDW) - @Tag(Constants.xAzureSQLMI) - public void testFourPartSyntaxCallEscapeSyntax() throws SQLException { - String table = "serverList"; - - try (Statement stmt = connection.createStatement()) { - stmt.execute("IF OBJECT_ID(N'" + table + "') IS NOT NULL DROP TABLE " + table); - stmt.execute("CREATE TABLE " + table - + " (serverName varchar(100),network varchar(100),serverStatus varchar(4000), id int, collation varchar(100), connectTimeout int, queryTimeout int)"); - stmt.execute("INSERT " + table + " EXEC sp_helpserver"); - - ResultSet rs = stmt - .executeQuery("SELECT COUNT(*) FROM " + table + " WHERE serverName = N'" + linkedServer + "'"); - rs.next(); - - if (rs.getInt(1) == 1) { - stmt.execute("EXEC sp_dropserver @server='" + linkedServer + "';"); - } - - stmt.execute("EXEC sp_addlinkedserver @server='" + linkedServer + "';"); - stmt.execute("EXEC sp_addlinkedsrvlogin @rmtsrvname=N'" + linkedServer + "', @useself=false" - + ", @rmtuser=N'" + linkedServerUser + "', @rmtpassword=N'" + linkedServerPassword + "'"); - stmt.execute("EXEC sp_serveroption '" + linkedServer + "', 'rpc', true;"); - stmt.execute("EXEC sp_serveroption '" + linkedServer + "', 'rpc out', true;"); - } - - SQLServerDataSource ds = new SQLServerDataSource(); - ds.setServerName(linkedServer); - ds.setUser(linkedServerUser); - ds.setPassword(linkedServerPassword); - ds.setEncrypt(false); - ds.setTrustServerCertificate(true); - - try (Connection linkedServerConnection = ds.getConnection(); - Statement stmt = linkedServerConnection.createStatement()) { - stmt.execute( - "create or alter procedure dbo.TestAdd(@Num1 int, @Num2 int, @Result int output) as begin set @Result = @Num1 + @Num2; end;"); - - stmt.execute("create or alter procedure dbo.TestReturn(@Num1 int) as select @Num1 return @Num1*3 "); - } - - try (CallableStatement cstmt = connection - .prepareCall("{call [" + linkedServer + "].master.dbo.TestAdd(?,?,?)}")) { - int sum = 11; - int param0 = 1; - int param1 = 10; - cstmt.setInt(1, param0); - cstmt.setInt(2, param1); - cstmt.registerOutParameter(3, Types.INTEGER); - cstmt.execute(); - assertEquals(sum, cstmt.getInt(3)); - } - - try (CallableStatement cstmt = connection.prepareCall("exec [" + linkedServer + "].master.dbo.TestAdd ?,?,?")) { - int sum = 11; - int param0 = 1; - int param1 = 10; - cstmt.setInt(1, param0); - cstmt.setInt(2, param1); - cstmt.registerOutParameter(3, Types.INTEGER); - cstmt.execute(); - assertEquals(sum, cstmt.getInt(3)); - } - - try (CallableStatement cstmt = connection - .prepareCall("{? = call [" + linkedServer + "].master.dbo.TestReturn(?)}")) { - int expected = 15; - cstmt.registerOutParameter(1, java.sql.Types.INTEGER); - cstmt.setInt(2, 5); - cstmt.execute(); - assertEquals(expected, cstmt.getInt(1)); - } - } - - @Test - public void testTimestampStringConversion() throws SQLException { - try (CallableStatement stmt = connection.prepareCall("{call " + currentTimeProc + "(?)}")) { - String timestamp = "2024-05-29 15:35:53.461"; - stmt.setObject(1, timestamp, Types.TIMESTAMP); - stmt.registerOutParameter(1, Types.TIMESTAMP); - stmt.execute(); - stmt.getObject("currentTimeStamp"); - } - } - - @Test + } + } + + // Test Needs more work to be configured to run on azureDB as there are slight differences + // between the regular SQL Server vs. azureDB + @Test + @Tag(Constants.xAzureSQLDB) + public void testCallableStatementManyParameters() throws SQLException { + String tempPass = UUID.randomUUID().toString(); + String dropLogin = "IF EXISTS (select * from sys.sql_logins where name = 'NewLogin') DROP LOGIN NewLogin"; + String dropUser = "IF EXISTS (select * from sys.sysusers where name = 'NewUser') DROP USER NewUser"; + String createLogin = "USE MASTER;CREATE LOGIN NewLogin WITH PASSWORD=N'" + tempPass + "', " + + "DEFAULT_DATABASE = MASTER, DEFAULT_LANGUAGE = US_ENGLISH;ALTER LOGIN NewLogin ENABLE;"; + String createUser = "USE MASTER;CREATE USER NewUser FOR LOGIN NewLogin WITH DEFAULT_SCHEMA = [DBO];"; + String grantExecute = "GRANT EXECUTE ON " + manyParamProc + " TO NewUser;"; + + // Need to create a user with limited permissions in order to run through the code block we are testing + // The user created will execute sp_sproc_columns internally by the driver, which should not return all + // the column names as the user has limited permissions + try (Connection conn = PrepUtil.getConnection(connectionString)) { + try (Statement stmt = conn.createStatement()) { + stmt.execute(dropLogin); + stmt.execute(dropUser); + stmt.execute(createLogin); + stmt.execute(createUser); + stmt.execute(grantExecute); + } + } + + try (Connection conn = PrepUtil.getConnection(connectionString + ";user=NewLogin;password=" + tempPass + ";")) { + BigDecimal money = new BigDecimal("9999.99"); + + // Should not throw an "Index is out of range error" + // Should not throw R_parameterNotDefinedForProcedure + try (CallableStatement callableStatement = conn + .prepareCall("{call " + manyParamProc + "(?,?,?,?,?,?,?,?,?,?)}")) { + callableStatement.setObject("@p1", money, microsoft.sql.Types.MONEY); + callableStatement.setObject("@p2", money, microsoft.sql.Types.MONEY); + callableStatement.setObject("@p3", money, microsoft.sql.Types.MONEY); + callableStatement.setObject("@p4", money, microsoft.sql.Types.MONEY); + callableStatement.setObject("@p5", money, microsoft.sql.Types.MONEY); + callableStatement.setObject("@p6", money, microsoft.sql.Types.MONEY); + callableStatement.setObject("@p7", money, microsoft.sql.Types.MONEY); + callableStatement.setObject("@p8", money, microsoft.sql.Types.MONEY); + callableStatement.setObject("@p9", money, microsoft.sql.Types.MONEY); + callableStatement.setObject("@p10", money, microsoft.sql.Types.MONEY); + callableStatement.execute(); + } + } + } + + @Test + public void testCallableStatementSpPrepare() throws SQLException { + connection.setPrepareMethod("prepare"); + + try (Statement statement = connection.createStatement();) { + statement.executeUpdate("create procedure " + procName + " as select 1 --"); + + try (CallableStatement callableStatement = connection.prepareCall("{call " + procName + "}")) { + try (ResultSet rs = callableStatement.executeQuery()) { // Takes sp_executesql path + rs.next(); + assertEquals(1, rs.getInt(1), TestResource.getResource("R_setDataNotEqual")); + } + + try (ResultSet rs = callableStatement.executeQuery()) { // Takes sp_prepare path + rs.next(); + assertEquals(1, rs.getInt(1), TestResource.getResource("R_setDataNotEqual")); + } + } + } + } + + /** + * Tests CallableStatement.getString() with uniqueidentifier parameter + * + * @throws SQLException + */ + @Test + public void getStringGUIDTest() throws SQLException { + + String sql = "{call " + outputProcedureNameGUID + "(?)}"; + + try (SQLServerCallableStatement callableStatement = (SQLServerCallableStatement) connection.prepareCall(sql)) { + + UUID originalValue = UUID.randomUUID(); + + callableStatement.registerOutParameter(1, microsoft.sql.Types.GUID); + callableStatement.setObject(1, originalValue.toString(), microsoft.sql.Types.GUID); + callableStatement.execute(); + + String retrievedValue = callableStatement.getString(1); + + assertEquals(originalValue.toString().toLowerCase(), retrievedValue.toLowerCase()); + + } + } + + /** + * test for setNull(index, varchar) to behave as setNull(index, nvarchar) when SendStringParametersAsUnicode is true + * + * @throws SQLException + */ + @Test + public void getSetNullWithTypeVarchar() throws SQLException { + String polishchar = "\u0143"; + + SQLServerDataSource ds = new SQLServerDataSource(); + ds.setURL(connectionString); + ds.setSendStringParametersAsUnicode(true); + String sql = "{? = call " + setNullProcedureName + " (?,?)}"; + try (Connection connection = ds.getConnection(); + SQLServerCallableStatement cs = (SQLServerCallableStatement) connection.prepareCall(sql); + SQLServerCallableStatement cs2 = (SQLServerCallableStatement) connection.prepareCall(sql)) { + + cs.registerOutParameter(1, Types.INTEGER); + cs.setString(2, polishchar); + cs.setString(3, null); + cs.registerOutParameter(3, Types.VARCHAR); + cs.execute(); + + String expected = cs.getString(3); + + cs2.registerOutParameter(1, Types.INTEGER); + cs2.setString(2, polishchar); + cs2.setNull(3, Types.VARCHAR); + cs2.registerOutParameter(3, Types.NVARCHAR); + cs2.execute(); + + String actual = cs2.getString(3); + + assertEquals(expected, actual); + } + } + + /** + * Tests getObject(n, java.time.LocalDateTime.class). + * + * @throws SQLException + */ + @Test + public void testGetObjectAsLocalDateTime() throws SQLException { + String sql = "{CALL " + getObjectLocalDateTimeProcedureName + " (?)}"; + try (Connection con = DriverManager.getConnection(connectionString); + CallableStatement cs = con.prepareCall(sql)) { + cs.registerOutParameter(1, Types.TIMESTAMP); + TimeZone prevTimeZone = TimeZone.getDefault(); + TimeZone.setDefault(TimeZone.getTimeZone("America/Edmonton")); + + // a local date/time that does not actually exist because of Daylight Saving Time + final String testValueDate = "2018-03-11"; + final String testValueTime = "02:00:00.1234567"; + final String testValueDateTime = testValueDate + "T" + testValueTime; + + try { + cs.execute(); + + LocalDateTime expectedLocalDateTime = LocalDateTime.parse(testValueDateTime); + LocalDateTime actualLocalDateTime = cs.getObject(1, LocalDateTime.class); + assertEquals(expectedLocalDateTime, actualLocalDateTime); + + LocalDate expectedLocalDate = LocalDate.parse(testValueDate); + LocalDate actualLocalDate = cs.getObject(1, LocalDate.class); + assertEquals(expectedLocalDate, actualLocalDate); + + LocalTime expectedLocalTime = LocalTime.parse(testValueTime); + LocalTime actualLocalTime = cs.getObject(1, LocalTime.class); + assertEquals(expectedLocalTime, actualLocalTime); + } finally { + TimeZone.setDefault(prevTimeZone); + } + } + } + + /** + * Tests getObject(n, java.time.OffsetDateTime.class) and getObject(n, java.time.OffsetTime.class). + * + * @throws SQLException + */ + @Test + @Tag(Constants.xAzureSQLDW) + public void testGetObjectAsOffsetDateTime() throws SQLException { + String sql = "{CALL " + getObjectOffsetDateTimeProcedureName + " (?, ?)}"; + try (Connection con = DriverManager.getConnection(connectionString); + CallableStatement cs = con.prepareCall(sql)) { + cs.registerOutParameter(1, Types.TIMESTAMP_WITH_TIMEZONE); + cs.registerOutParameter(2, Types.TIMESTAMP_WITH_TIMEZONE); + + final String testValue = "2018-01-02T11:22:33.123456700+12:34"; + + cs.execute(); + + OffsetDateTime expected = OffsetDateTime.parse(testValue); + OffsetDateTime actual = cs.getObject(1, OffsetDateTime.class); + assertEquals(expected, actual); + assertNull(cs.getObject(2, OffsetDateTime.class)); + + OffsetTime expectedTime = OffsetTime.parse(testValue.split("T")[1]); + OffsetTime actualTime = cs.getObject(1, OffsetTime.class); + assertEquals(expectedTime, actualTime); + assertNull(cs.getObject(2, OffsetTime.class)); + } + } + + /** + * recognize parameter names with and without leading '@' + * + * @throws SQLException + */ + @Test + public void inputParamsTest() throws SQLException { + String call = "{CALL " + inputParamsProcedureName + " (?,?)}"; + + // the historical way: no leading '@', parameter names respected (not positional) + try (CallableStatement cs = connection.prepareCall(call)) { + cs.setString("p2", "world"); + cs.setString("p1", "hello"); + try (ResultSet rs = cs.executeQuery()) { + rs.next(); + assertEquals("helloworld", rs.getString(1)); + } + } + + // the "new" way: leading '@', parameter names still respected (not positional) + try (CallableStatement cs = connection.prepareCall(call)) { + cs.setString("@p2", "world!"); + cs.setString("@p1", "Hello "); + try (ResultSet rs = cs.executeQuery()) { + rs.next(); + assertEquals("Hello world!", rs.getString(1)); + } + } + + // sanity check: unrecognized parameter name + try (CallableStatement cs = connection.prepareCall(call)) { + cs.setString("@whatever", "test"); + fail(TestResource.getResource("R_shouldThrowException")); + } catch (SQLException sse) { + MessageFormat form = new MessageFormat(TestResource.getResource("R_parameterNotDefined")); + Object[] msgArgs = {"@whatever"}; + + if (!sse.getMessage().startsWith(form.format(msgArgs))) { + fail(TestResource.getResource("R_unexpectedExceptionContent")); + } + } + } + + @Test + public void testZeroParamSproc() throws SQLException { + String call = "{? = CALL " + zeroParamSproc + "}"; + + try (CallableStatement cs = connection.prepareCall(call)) { + cs.registerOutParameter(1, Types.INTEGER); + cs.execute(); + assertEquals(1, cs.getInt(1)); + } + + // Test zero parameter sproc with return value with parentheses + call = "{? = CALL " + zeroParamSproc + "()}"; + + try (CallableStatement cs = connection.prepareCall(call)) { + cs.registerOutParameter(1, Types.INTEGER); + cs.execute(); + // Calling zero parameter sproc with return value with parentheses + // should return a value that's not zero + assertEquals(1, cs.getInt(1)); + } + } + + @Test + public void testExecuteSystemStoredProcedureNamedParametersAndIndexedParameterNoResultset() throws SQLException { + String call0 = "EXEC sp_getapplock @Resource=?, @LockTimeout='0', @LockMode='Exclusive', @LockOwner='Session'"; + String call1 = "\rEXEC\r\rsp_getapplock @Resource=?, @LockTimeout='0', @LockMode='Exclusive', @LockOwner='Session'"; + String call2 = " EXEC sp_getapplock @Resource=?, @LockTimeout='0', @LockMode='Exclusive', @LockOwner='Session'"; + String call3 = "\tEXEC\t\t\tsp_getapplock @Resource=?, @LockTimeout='0', @LockMode='Exclusive', @LockOwner='Session'"; + + try (CallableStatement cstmt0 = connection.prepareCall(call0); + CallableStatement cstmt1 = connection.prepareCall(call1); + CallableStatement cstmt2 = connection.prepareCall(call2); + CallableStatement cstmt3 = connection.prepareCall(call3);) { + cstmt0.setString(1, "Resource-" + UUID.randomUUID()); + cstmt0.execute(); + + cstmt1.setString(1, "Resource-" + UUID.randomUUID()); + cstmt1.execute(); + + cstmt2.setString(1, "Resource-" + UUID.randomUUID()); + cstmt2.execute(); + + cstmt3.setString(1, "Resource-" + UUID.randomUUID()); + cstmt3.execute(); + } + } + + @Test + public void testExecSystemStoredProcedureNamedParametersAndIndexedParameterResultSet() throws SQLException { + String call = "exec sp_sproc_columns_100 ?, @ODBCVer=3, @fUsePattern=0"; + + try (CallableStatement cstmt = connection.prepareCall(call)) { + cstmt.setString(1, "sp_getapplock"); + + try (ResultSet rs = cstmt.executeQuery()) { + while (rs.next()) { + assertTrue(TestResource.getResource("R_resultSetEmpty"), !rs.getString(4).isEmpty()); + } + } + } + } + + @Test + public void testExecSystemStoredProcedureNoIndexedParametersResultSet() throws SQLException { + String call = "execute sp_sproc_columns_100 sp_getapplock, @ODBCVer=3, @fUsePattern=0"; + + try (CallableStatement cstmt = connection.prepareCall(call); ResultSet rs = cstmt.executeQuery()) { + while (rs.next()) { + assertTrue(TestResource.getResource("R_resultSetEmpty"), !rs.getString(4).isEmpty()); + } + } + } + + @Test + public void testExecDocumentedSystemStoredProceduresIndexedParameters() throws SQLException { + String serverName; + String testTableName = "testTable"; + Integer integer = new Integer(1); + + try (Statement stmt = connection.createStatement(); ResultSet rs = stmt.executeQuery("SELECT @@SERVERNAME")) { + rs.next(); + serverName = rs.getString(1); + } + + String[] sprocs = {"EXEC sp_column_privileges ?", "exec sp_catalogs ?", "execute sp_column_privileges ?", + "EXEC sp_column_privileges_ex ?", "EXECUTE sp_columns ?", "execute sp_datatype_info ?", + "EXEC sp_sproc_columns ?", "EXECUTE sp_server_info ?", "exec sp_special_columns ?", + "execute sp_statistics ?", "EXEC sp_table_privileges ?", "exec sp_tables ?"}; + + Object[] params = {testTableName, serverName, testTableName, serverName, testTableName, integer, + "sp_column_privileges", integer, testTableName, testTableName, testTableName, testTableName}; + + int paramIndex = 0; + + for (String sproc : sprocs) { + try (CallableStatement cstmt = connection.prepareCall(sproc)) { + cstmt.setObject(1, params[paramIndex]); + cstmt.execute(); + paramIndex++; + } catch (Exception e) { + fail("Failed executing '" + sproc + "' with indexed parameter '" + params[paramIndex]); + } + } + } + + @Test + public void testCallableStatementDefaultValues() throws SQLException { + String call0 = "{call " + conditionalSproc + " (?, ?, 1)}"; + String call1 = "{call " + conditionalSproc + " (?, ?, 2)}"; + int expectedValue = 5; // The sproc should return this value + + try (CallableStatement cstmt = connection.prepareCall(call0)) { + cstmt.setInt(1, 1); + cstmt.setInt(2, 2); + cstmt.execute(); + ResultSet rs = cstmt.getResultSet(); + rs.next(); + fail(TestResource.getResource("R_expectedFailPassed")); + + } catch (Exception e) { + String msg = e.getMessage(); + assertTrue(TestResource + .getResource("R_nullPointerExceptionFromResultSet").equalsIgnoreCase(msg) + || msg == null); + } + + try (CallableStatement cstmt = connection.prepareCall(call1)) { + cstmt.setInt(1, 1); + cstmt.setInt(2, 2); + cstmt.execute(); + ResultSet rs = cstmt.getResultSet(); + rs.next(); + + assertEquals(Integer.toString(expectedValue), rs.getString(1)); + } + } + + @Test + public void testCallableStatementSetByAnnotatedArgs() throws SQLException { + String call = "{? = call " + simpleRetValSproc + " (@Arg1 = ?)}"; + int expectedValue = 1; // The sproc should return this value + + try (CallableStatement cstmt = connection.prepareCall(call)) { + cstmt.registerOutParameter(1, Types.INTEGER); + cstmt.setInt(1, 2); + cstmt.setString(2, "foo"); + cstmt.execute(); + + Assert.assertEquals(expectedValue, cstmt.getInt(1)); + } + } + + @Test + @Tag(Constants.reqExternalSetup) + @Tag(Constants.xAzureSQLDB) + @Tag(Constants.xAzureSQLDW) + @Tag(Constants.xAzureSQLMI) + public void testFourPartSyntaxCallEscapeSyntax() throws SQLException { + String table = "serverList"; + + try (Statement stmt = connection.createStatement()) { + stmt.execute("IF OBJECT_ID(N'" + table + "') IS NOT NULL DROP TABLE " + table); + stmt.execute("CREATE TABLE " + table + + " (serverName varchar(100),network varchar(100),serverStatus varchar(4000), id int, collation varchar(100), connectTimeout int, queryTimeout int)"); + stmt.execute("INSERT " + table + " EXEC sp_helpserver"); + + ResultSet rs = stmt + .executeQuery("SELECT COUNT(*) FROM " + table + " WHERE serverName = N'" + linkedServer + "'"); + rs.next(); + + if (rs.getInt(1) == 1) { + stmt.execute("EXEC sp_dropserver @server='" + linkedServer + "';"); + } + + stmt.execute("EXEC sp_addlinkedserver @server='" + linkedServer + "';"); + stmt.execute("EXEC sp_addlinkedsrvlogin @rmtsrvname=N'" + linkedServer + "', @useself=false" + + ", @rmtuser=N'" + linkedServerUser + "', @rmtpassword=N'" + linkedServerPassword + "'"); + stmt.execute("EXEC sp_serveroption '" + linkedServer + "', 'rpc', true;"); + stmt.execute("EXEC sp_serveroption '" + linkedServer + "', 'rpc out', true;"); + } + + SQLServerDataSource ds = new SQLServerDataSource(); + ds.setServerName(linkedServer); + ds.setUser(linkedServerUser); + ds.setPassword(linkedServerPassword); + ds.setEncrypt(false); + ds.setTrustServerCertificate(true); + + try (Connection linkedServerConnection = ds.getConnection(); + Statement stmt = linkedServerConnection.createStatement()) { + stmt.execute( + "create or alter procedure dbo.TestAdd(@Num1 int, @Num2 int, @Result int output) as begin set @Result = @Num1 + @Num2; end;"); + + stmt.execute("create or alter procedure dbo.TestReturn(@Num1 int) as select @Num1 return @Num1*3 "); + } + + try (CallableStatement cstmt = connection + .prepareCall("{call [" + linkedServer + "].master.dbo.TestAdd(?,?,?)}")) { + int sum = 11; + int param0 = 1; + int param1 = 10; + cstmt.setInt(1, param0); + cstmt.setInt(2, param1); + cstmt.registerOutParameter(3, Types.INTEGER); + cstmt.execute(); + assertEquals(sum, cstmt.getInt(3)); + } + + try (CallableStatement cstmt = connection.prepareCall("exec [" + linkedServer + "].master.dbo.TestAdd ?,?,?")) { + int sum = 11; + int param0 = 1; + int param1 = 10; + cstmt.setInt(1, param0); + cstmt.setInt(2, param1); + cstmt.registerOutParameter(3, Types.INTEGER); + cstmt.execute(); + assertEquals(sum, cstmt.getInt(3)); + } + + try (CallableStatement cstmt = connection + .prepareCall("{? = call [" + linkedServer + "].master.dbo.TestReturn(?)}")) { + int expected = 15; + cstmt.registerOutParameter(1, java.sql.Types.INTEGER); + cstmt.setInt(2, 5); + cstmt.execute(); + assertEquals(expected, cstmt.getInt(1)); + } + } + + @Test + public void testTimestampStringConversion() throws SQLException { + try (CallableStatement stmt = connection.prepareCall("{call " + currentTimeProc + "(?)}")) { + String timestamp = "2024-05-29 15:35:53.461"; + stmt.setObject(1, timestamp, Types.TIMESTAMP); + stmt.registerOutParameter(1, Types.TIMESTAMP); + stmt.execute(); + stmt.getObject("currentTimeStamp"); + } + } + + @Test public void testJSONColumnInTableWithSetObject() throws SQLException { try (Connection con = DriverManager.getConnection(connectionString); Statement stmt = con.createStatement()) { @@ -646,128 +641,127 @@ public void testJSONProcedureWithSetObject() throws SQLException { } } - /** - * Cleanup after test - * - * @throws SQLException - */ - @AfterAll - public static void cleanup() throws SQLException { - try (Statement stmt = connection.createStatement()) { - TestUtils.dropTableIfExists(tableNameGUID, stmt); - TestUtils.dropTableIfExists(manyParamsTable, stmt); - TestUtils.dropProcedureIfExists(outputProcedureNameGUID, stmt); - TestUtils.dropProcedureIfExists(setNullProcedureName, stmt); - TestUtils.dropProcedureIfExists(inputParamsProcedureName, stmt); - TestUtils.dropProcedureIfExists(getObjectLocalDateTimeProcedureName, stmt); - TestUtils.dropProcedureIfExists(getObjectOffsetDateTimeProcedureName, stmt); - TestUtils.dropProcedureIfExists(currentTimeProc, stmt); - TestUtils.dropProcedureIfExists(conditionalSproc, stmt); - TestUtils.dropProcedureIfExists(simpleRetValSproc, stmt); - TestUtils.dropProcedureIfExists(zeroParamSproc, stmt); - TestUtils.dropTableIfExists(tableNameJSON, stmt); + /** + * Cleanup after test + * + * @throws SQLException + */ + @AfterAll + public static void cleanup() throws SQLException { + try (Statement stmt = connection.createStatement()) { + TestUtils.dropTableIfExists(tableNameGUID, stmt); + TestUtils.dropTableIfExists(manyParamsTable, stmt); + TestUtils.dropProcedureIfExists(outputProcedureNameGUID, stmt); + TestUtils.dropProcedureIfExists(setNullProcedureName, stmt); + TestUtils.dropProcedureIfExists(inputParamsProcedureName, stmt); + TestUtils.dropProcedureIfExists(getObjectLocalDateTimeProcedureName, stmt); + TestUtils.dropProcedureIfExists(getObjectOffsetDateTimeProcedureName, stmt); + TestUtils.dropProcedureIfExists(currentTimeProc, stmt); + TestUtils.dropProcedureIfExists(conditionalSproc, stmt); + TestUtils.dropProcedureIfExists(simpleRetValSproc, stmt); + TestUtils.dropProcedureIfExists(zeroParamSproc, stmt); + TestUtils.dropTableIfExists(tableNameJSON, stmt); TestUtils.dropProcedureIfExists(procedureNameJSON, stmt); - } - } - - private static void createGUIDStoredProcedure(Statement stmt) throws SQLException { - String sql = "CREATE PROCEDURE " + outputProcedureNameGUID - + "(@p1 uniqueidentifier OUTPUT) AS SELECT @p1 = c1 FROM " + tableNameGUID + Constants.SEMI_COLON; - stmt.execute(sql); - } - - private static void createGUIDTable(Statement stmt) throws SQLException { - String sql = "CREATE TABLE " + tableNameGUID + " (c1 uniqueidentifier null)"; - stmt.execute(sql); - } - - private static void createSetNullProcedure(Statement stmt) throws SQLException { - stmt.execute("create procedure " + setNullProcedureName - + " (@p1 nvarchar(255), @p2 nvarchar(255) output) as select @p2=@p1 return 0"); - } - - private static void createInputParamsProcedure(Statement stmt) throws SQLException { - String sql = "CREATE PROCEDURE " + inputParamsProcedureName + " @p1 nvarchar(max) = N'parameter1', " - + " @p2 nvarchar(max) = N'parameter2' " + "AS " + "BEGIN " + " SET NOCOUNT ON; " - + " SELECT @p1 + @p2 AS result; " + "END "; - - stmt.execute(sql); - } - - private static void createGetObjectLocalDateTimeProcedure(Statement stmt) throws SQLException { - String sql = "CREATE PROCEDURE " + getObjectLocalDateTimeProcedureName + "(@p1 datetime2(7) OUTPUT) AS " - + "SELECT @p1 = '2018-03-11T02:00:00.1234567'"; - stmt.execute(sql); - } - - private static void createGetObjectOffsetDateTimeProcedure(Statement stmt) throws SQLException { - String sql = "CREATE PROCEDURE " + getObjectOffsetDateTimeProcedureName - + "(@p1 DATETIMEOFFSET OUTPUT, @p2 DATETIMEOFFSET OUTPUT) AS " - + "SELECT @p1 = '2018-01-02T11:22:33.123456700+12:34', @p2 = NULL"; - stmt.execute(sql); - } - - private static void createProcedureManyParams() throws SQLException { - String type = manyParamUserDefinedType; - String sql = "CREATE PROCEDURE " + manyParamProc + " @p1 " + type + ", @p2 " + type + ", @p3 " + type + ", @p4 " - + type + ", @p5 " + type + ", @p6 " + type + ", @p7 " + type + ", @p8 " + type + ", @p9 " + type - + ", @p10 " + type + " AS INSERT INTO " + manyParamsTable - + " VALUES(@p1, @p2, @p3, @p4, @p5, @p6, @p7, @p8, @p9, @p10)"; - try (Statement stmt = connection.createStatement()) { - stmt.execute(sql); - } - } - - private static void createProcedureCurrentTime() throws SQLException { - String sql = "CREATE PROCEDURE " + currentTimeProc + " @currentTimeStamp datetime = null OUTPUT " - + "AS BEGIN SET @currentTimeStamp = CURRENT_TIMESTAMP; END"; - try (Statement stmt = connection.createStatement()) { - stmt.execute(sql); - } - } - - private static void createConditionalProcedure() throws SQLException { - String sql = "CREATE PROCEDURE " + conditionalSproc + " @param0 INT, @param1 INT, @maybe bigint = 2 " - + "AS BEGIN IF @maybe >= 2 BEGIN SELECT 5 END END"; - try (Statement stmt = connection.createStatement()) { - stmt.execute(sql); - } - } - - private static void createSimpleRetValSproc() throws SQLException { - String sql = "CREATE PROCEDURE " + simpleRetValSproc - + " (@Arg1 VARCHAR(128)) AS DECLARE @ReturnCode INT RETURN 1"; - try (Statement stmt = connection.createStatement()) { - stmt.execute(sql); - } - } - - private static void createTableManyParams() throws SQLException { - String type = manyParamUserDefinedType; - String sql = "CREATE TABLE" + manyParamsTable + " (c1 " + type + " null, " + "c2 " + type + " null, " + "c3 " - + type + " null, " + "c4 " + type + " null, " + "c5 " + type + " null, " + "c6 " + type + " null, " - + "c7 " + type + " null, " + "c8 " + type + " null, " + "c9 " + type + " null, " + "c10 " + type - + " null);"; - try (Statement stmt = connection.createStatement()) { - stmt.execute(sql); - } - } - - private static void createProcedureZeroParams() throws SQLException { - String sql = "CREATE PROCEDURE " + zeroParamSproc + " AS RETURN 1"; - try (Statement stmt = connection.createStatement()) { - stmt.execute(sql); - } - } - - private static void createUserDefinedType() throws SQLException { - String TVPCreateCmd = "CREATE TYPE " + manyParamUserDefinedType + " FROM MONEY"; - try (Statement stmt = connection.createStatement()) { - stmt.executeUpdate(TVPCreateCmd); - } - } - - private static void createJSONTestTable(Statement stmt) throws SQLException { + } + } + + private static void createGUIDStoredProcedure(Statement stmt) throws SQLException { + String sql = "CREATE PROCEDURE " + outputProcedureNameGUID + + "(@p1 uniqueidentifier OUTPUT) AS SELECT @p1 = c1 FROM " + tableNameGUID + Constants.SEMI_COLON; + stmt.execute(sql); + } + + private static void createGUIDTable(Statement stmt) throws SQLException { + String sql = "CREATE TABLE " + tableNameGUID + " (c1 uniqueidentifier null)"; + stmt.execute(sql); + } + + private static void createSetNullProcedure(Statement stmt) throws SQLException { + stmt.execute("create procedure " + setNullProcedureName + + " (@p1 nvarchar(255), @p2 nvarchar(255) output) as select @p2=@p1 return 0"); + } + + private static void createInputParamsProcedure(Statement stmt) throws SQLException { + String sql = "CREATE PROCEDURE " + inputParamsProcedureName + " @p1 nvarchar(max) = N'parameter1', " + + " @p2 nvarchar(max) = N'parameter2' " + "AS " + "BEGIN " + " SET NOCOUNT ON; " + + " SELECT @p1 + @p2 AS result; " + "END "; + + stmt.execute(sql); + } + + private static void createGetObjectLocalDateTimeProcedure(Statement stmt) throws SQLException { + String sql = "CREATE PROCEDURE " + getObjectLocalDateTimeProcedureName + "(@p1 datetime2(7) OUTPUT) AS " + + "SELECT @p1 = '2018-03-11T02:00:00.1234567'"; + stmt.execute(sql); + } + + private static void createGetObjectOffsetDateTimeProcedure(Statement stmt) throws SQLException { + String sql = "CREATE PROCEDURE " + getObjectOffsetDateTimeProcedureName + + "(@p1 DATETIMEOFFSET OUTPUT, @p2 DATETIMEOFFSET OUTPUT) AS " + + "SELECT @p1 = '2018-01-02T11:22:33.123456700+12:34', @p2 = NULL"; + stmt.execute(sql); + } + + private static void createProcedureManyParams() throws SQLException { + String type = manyParamUserDefinedType; + String sql = "CREATE PROCEDURE " + manyParamProc + " @p1 " + type + ", @p2 " + type + ", @p3 " + type + ", @p4 " + + type + ", @p5 " + type + ", @p6 " + type + ", @p7 " + type + ", @p8 " + type + ", @p9 " + type + + ", @p10 " + type + " AS INSERT INTO " + manyParamsTable + + " VALUES(@p1, @p2, @p3, @p4, @p5, @p6, @p7, @p8, @p9, @p10)"; + try (Statement stmt = connection.createStatement()) { + stmt.execute(sql); + } + } + + private static void createProcedureCurrentTime() throws SQLException { + String sql = "CREATE PROCEDURE " + currentTimeProc + " @currentTimeStamp datetime = null OUTPUT " + + "AS BEGIN SET @currentTimeStamp = CURRENT_TIMESTAMP; END"; + try (Statement stmt = connection.createStatement()) { + stmt.execute(sql); + } + } + + private static void createConditionalProcedure() throws SQLException { + String sql = "CREATE PROCEDURE " + conditionalSproc + " @param0 INT, @param1 INT, @maybe bigint = 2 " + + "AS BEGIN IF @maybe >= 2 BEGIN SELECT 5 END END"; + try (Statement stmt = connection.createStatement()) { + stmt.execute(sql); + } + } + + private static void createSimpleRetValSproc() throws SQLException { + String sql = "CREATE PROCEDURE " + simpleRetValSproc + " (@Arg1 VARCHAR(128)) AS DECLARE @ReturnCode INT RETURN 1"; + try (Statement stmt = connection.createStatement()) { + stmt.execute(sql); + } + } + + private static void createTableManyParams() throws SQLException { + String type = manyParamUserDefinedType; + String sql = "CREATE TABLE" + manyParamsTable + " (c1 " + type + " null, " + "c2 " + type + " null, " + "c3 " + + type + " null, " + "c4 " + type + " null, " + "c5 " + type + " null, " + "c6 " + type + " null, " + + "c7 " + type + " null, " + "c8 " + type + " null, " + "c9 " + type + " null, " + "c10 " + type + + " null);"; + try (Statement stmt = connection.createStatement()) { + stmt.execute(sql); + } + } + + private static void createProcedureZeroParams() throws SQLException { + String sql = "CREATE PROCEDURE " + zeroParamSproc + " AS RETURN 1"; + try (Statement stmt = connection.createStatement()) { + stmt.execute(sql); + } + } + + private static void createUserDefinedType() throws SQLException { + String TVPCreateCmd = "CREATE TYPE " + manyParamUserDefinedType + " FROM MONEY"; + try (Statement stmt = connection.createStatement()) { + stmt.executeUpdate(TVPCreateCmd); + } + } + + private static void createJSONTestTable(Statement stmt) throws SQLException { String sql = "CREATE TABLE " + tableNameJSON + " (" + "id INT PRIMARY KEY IDENTITY(1,1), " + "col1 JSON)"; stmt.execute(sql); } From 44a185b0ea020f699662846968f2b36ae8b29563 Mon Sep 17 00:00:00 2001 From: Ananya Garg Date: Fri, 10 Jan 2025 18:31:32 +0530 Subject: [PATCH 11/13] Added test cases for the TVP type in Callable Statements. --- .../sqlserver/jdbc/tvp/TVPTypesTest.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/tvp/TVPTypesTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/tvp/TVPTypesTest.java index 28e4f0dac..7299d8d58 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/tvp/TVPTypesTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/tvp/TVPTypesTest.java @@ -755,6 +755,33 @@ public String toString() { } } } + + @Test + public void testJSONTVPCallableAPI() throws SQLException { + createTables("json"); + createTVPS("json"); + createProcedure(); + + value = "{\"Name\":\"Alice\",\"Age\":25}"; + + tvp = new SQLServerDataTable(); + tvp.addColumnMetadata("c1", microsoft.sql.Types.JSON); + tvp.addRow(value); + + final String sql = "{call " + AbstractSQLGenerator.escapeIdentifier(procedureName) + "(?)}"; + + try (SQLServerCallableStatement callableStmt = (SQLServerCallableStatement) connection.prepareCall(sql)) { + callableStmt.setObject(1, tvp); + callableStmt.execute(); + + try (Connection con = getConnection(); Statement stmt = con.createStatement(); + ResultSet rs = stmt.executeQuery( + "select c1 from " + AbstractSQLGenerator.escapeIdentifier(tableName) + " ORDER BY rowId")) { + while (rs.next()) + assertEquals(rs.getObject(1), value); + } + } + } @BeforeAll public static void setupTests() throws Exception { From dc53208798c70d57b017fd6c748b9f8af72143a3 Mon Sep 17 00:00:00 2001 From: Divang Sharma Date: Wed, 22 Jan 2025 11:40:53 +0530 Subject: [PATCH 12/13] Fixed JSON op.execute method mapping --- src/main/java/com/microsoft/sqlserver/jdbc/dtv.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java b/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java index 8dcc567ec..bc0f6fb62 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java @@ -1460,6 +1460,7 @@ final void executeOp(DTVExecuteOp op) throws SQLServerException { case NVARCHAR: case LONGNVARCHAR: case NCLOB: + case JSON: if (null != cryptoMeta) op.execute(this, (byte[]) null); else @@ -1519,7 +1520,6 @@ final void executeOp(DTVExecuteOp op) throws SQLServerException { case VARCHAR: case LONGVARCHAR: case CLOB: - case JSON: op.execute(this, (byte[]) null); break; From fd21d1ad3cf607d091a06717c9df56dc66bf2bb2 Mon Sep 17 00:00:00 2001 From: Divang Sharma Date: Wed, 29 Jan 2025 18:11:58 +0530 Subject: [PATCH 13/13] Fixed Store procedure output param TDS metadata and fixed bulk copy type conversion --- .../microsoft/sqlserver/jdbc/DataTypes.java | 7 +++--- .../microsoft/sqlserver/jdbc/IOBuffer.java | 6 +++++ .../jdbc/SQLServerBulkCSVFileRecord.java | 4 ++++ .../sqlserver/jdbc/SQLServerBulkCopy.java | 23 +++++++++++++++---- .../com/microsoft/sqlserver/jdbc/dtv.java | 5 +++- 5 files changed, 37 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/DataTypes.java b/src/main/java/com/microsoft/sqlserver/jdbc/DataTypes.java index c2fea6822..0688792a3 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/DataTypes.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/DataTypes.java @@ -150,7 +150,7 @@ enum SSType { TIMESTAMP(Category.TIMESTAMP, "timestamp", JDBCType.BINARY), GEOMETRY(Category.UDT, "geometry", JDBCType.GEOMETRY), GEOGRAPHY(Category.UDT, "geography", JDBCType.GEOGRAPHY), - JSON(Category.JSON, "json", JDBCType.LONGNVARCHAR); + JSON(Category.JSON, "json", JDBCType.JSON); final Category category; private final String name; @@ -458,10 +458,11 @@ JDBCType getJDBCType(SSType ssType, JDBCType jdbcTypeFromApp) { case NVARCHAR: case NVARCHARMAX: case NTEXT: - case JSON: jdbcType = JDBCType.LONGVARCHAR; break; - + case JSON: + jdbcType = JDBCType.JSON; + break; case XML: default: jdbcType = JDBCType.LONGVARBINARY; diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java index c2c7720a9..aa774e31f 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java @@ -4864,6 +4864,12 @@ void writeRPCStringUnicode(String sValue) throws SQLServerException { writeRPCStringUnicode(null, sValue, false, null); } + void writeRPCJson(String sName, String sValue, boolean bOut, + SQLCollation collation) throws SQLServerException { + writeRPCNameValType(sName, bOut, TDSType.JSON); + writeLong(0xFFFFFFFFFFFFFFFFL); + } + /** * Writes a string value as Unicode for RPC * diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCSVFileRecord.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCSVFileRecord.java index 2db4339fa..bb334f79d 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCSVFileRecord.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCSVFileRecord.java @@ -545,6 +545,10 @@ else if ((null != columnNames) && (columnNames.length >= positionInSource)) columnMetadata.put(positionInSource, new ColumnMetadata(colName, java.sql.Types.LONGNVARCHAR, precision, scale, dateTimeFormatter)); break; + case microsoft.sql.Types.JSON: + columnMetadata.put(positionInSource, + new ColumnMetadata(colName, microsoft.sql.Types.JSON, precision, scale, dateTimeFormatter)); + break; /* * Redirecting Float as Double based on data type mapping * https://msdn.microsoft.com/library/ms378878%28v=sql.110%29.aspx diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java index 35f239608..93f7e738d 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java @@ -1025,7 +1025,14 @@ private void writeTypeInfo(TDSWriter tdsWriter, int srcJdbcType, int srcScale, i } collation.writeCollation(tdsWriter); break; - + case microsoft.sql.Types.JSON: // 0x62 + tdsWriter.writeByte(TDSType.JSON.byteValue()); + if (isStreaming) { + tdsWriter.writeShort((short) 0xFFFF); + } else { + tdsWriter.writeShort(isBaseType ? (short) (srcPrecision) : (short) (2 * srcPrecision)); + } + break; case java.sql.Types.BINARY: // 0xAD tdsWriter.writeByte(TDSType.BIGBINARY.byteValue()); tdsWriter.writeShort((short) (srcPrecision)); @@ -1127,7 +1134,7 @@ private void writeTypeInfo(TDSWriter tdsWriter, int srcJdbcType, int srcScale, i case microsoft.sql.Types.SQL_VARIANT: // 0x62 tdsWriter.writeByte(TDSType.SQL_VARIANT.byteValue()); tdsWriter.writeInt(TDS.SQL_VARIANT_LENGTH); - break; + break; default: MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_BulkTypeNotSupported")); String unsupportedDataType = JDBCType.of(srcJdbcType).toString().toLowerCase(Locale.ENGLISH); @@ -1470,6 +1477,8 @@ private String getDestTypeFromSrcType(int srcColIndx, int destColIndx, } case microsoft.sql.Types.SQL_VARIANT: return SSType.SQL_VARIANT.toString(); + case microsoft.sql.Types.JSON: + return SSType.JSON.toString(); default: { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_BulkTypeNotSupported")); Object[] msgArgs = {JDBCType.of(bulkJdbcType).toString().toLowerCase(Locale.ENGLISH)}; @@ -2090,6 +2099,7 @@ private void writeNullToTdsWriter(TDSWriter tdsWriter, int srcJdbcType, case java.sql.Types.LONGVARCHAR: case java.sql.Types.LONGNVARCHAR: case java.sql.Types.LONGVARBINARY: + case microsoft.sql.Types.JSON: if (isStreaming) { tdsWriter.writeLong(PLPInputStream.PLP_NULL); } else { @@ -2161,6 +2171,7 @@ else if (null != sourceCryptoMeta) { case java.sql.Types.TIME: case java.sql.Types.TIMESTAMP: case microsoft.sql.Types.DATETIMEOFFSET: + case microsoft.sql.Types.JSON: bulkJdbcType = java.sql.Types.VARCHAR; break; default: @@ -2419,6 +2430,7 @@ else if (null != sourceCryptoMeta) { case java.sql.Types.LONGNVARCHAR: case java.sql.Types.NCHAR: case java.sql.Types.NVARCHAR: + case microsoft.sql.Types.JSON: if (isStreaming) { // PLP_BODY rule in TDS // Use ResultSet.getString for non-streaming data and ResultSet.getNCharacterStream() for @@ -2986,6 +2998,7 @@ private Object readColumnFromResultSet(int srcColOrdinal, int srcJdbcType, boole case java.sql.Types.LONGNVARCHAR: case java.sql.Types.NCHAR: case java.sql.Types.NVARCHAR: + case microsoft.sql.Types.JSON: // PLP if stream type and both the source and destination are not encrypted // This is because AE does not support streaming types. // Therefore an encrypted source or destination means the data must not actually be streaming data @@ -3060,7 +3073,8 @@ private void writeColumn(TDSWriter tdsWriter, int srcColOrdinal, int destColOrdi destPrecision = destColumnMetadata.get(destColOrdinal).precision; if ((java.sql.Types.NCHAR == srcJdbcType) || (java.sql.Types.NVARCHAR == srcJdbcType) - || (java.sql.Types.LONGNVARCHAR == srcJdbcType)) { + || (java.sql.Types.LONGNVARCHAR == srcJdbcType) + || (microsoft.sql.Types.JSON == srcJdbcType)) { isStreaming = (DataTypes.SHORT_VARTYPE_MAX_CHARS < srcPrecision) || (DataTypes.SHORT_VARTYPE_MAX_CHARS < destPrecision); } else { @@ -3771,6 +3785,7 @@ void setDestinationTableMetadata(SQLServerResultSet rs) { private boolean unicodeConversionRequired(int jdbcType, SSType ssType) { return ((java.sql.Types.CHAR == jdbcType || java.sql.Types.VARCHAR == jdbcType || java.sql.Types.LONGNVARCHAR == jdbcType) - && (SSType.NCHAR == ssType || SSType.NVARCHAR == ssType || SSType.NVARCHARMAX == ssType)); + && (SSType.NCHAR == ssType || SSType.NVARCHAR == ssType || SSType.NVARCHARMAX == ssType + || SSType.JSON == ssType)); } } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java b/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java index bc0f6fb62..0616220ac 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java @@ -295,7 +295,10 @@ final class SendByRPCOp extends DTVExecuteOp { void execute(DTV dtv, String strValue) throws SQLServerException { if (dtv.getJdbcType() == JDBCType.GUID) { tdsWriter.writeRPCUUID(name, UUID.fromString(strValue), isOutParam); - } else { + } else if (dtv.getJdbcType() == JDBCType.JSON) { + tdsWriter.writeRPCJson(name, strValue, isOutParam, collation); + } + else { tdsWriter.writeRPCStringUnicode(name, strValue, isOutParam, collation); } }