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/DataTypes.java b/src/main/java/com/microsoft/sqlserver/jdbc/DataTypes.java index 018c483f8..0688792a3 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.JSON); 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; @@ -452,7 +460,9 @@ JDBCType getJDBCType(SSType ssType, JDBCType jdbcTypeFromApp) { case NTEXT: jdbcType = JDBCType.LONGVARCHAR; break; - + case JSON: + jdbcType = JDBCType.JSON; + break; case XML: default: jdbcType = JDBCType.LONGVARBINARY; @@ -673,8 +683,9 @@ 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, microsoft.sql.Types.JSON, Object.class.getName()); + final Category category; private final int intValue; private final String className; @@ -722,7 +733,8 @@ enum Category { GUID, SQL_VARIANT, GEOMETRY, - GEOGRAPHY; + GEOGRAPHY, + JSON; private static final Category[] VALUES = values(); } @@ -733,7 +745,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, @@ -795,7 +807,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; @@ -832,7 +845,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, @@ -895,7 +908,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 +985,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); @@ -997,6 +1012,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/IOBuffer.java b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java index f51a5430c..aa774e31f 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() + ")"; } @@ -4856,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 * @@ -5241,6 +5255,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; @@ -5476,6 +5491,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 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/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/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index 42ae6647d..4999787f9 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(); @@ -5333,6 +5343,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; @@ -6462,6 +6483,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); @@ -6761,6 +6800,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 @@ -6957,6 +6999,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/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/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(); 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}."}, 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 9ba2e83ce..0616220ac 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; + } @@ -294,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); } } @@ -1459,6 +1463,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 @@ -2989,6 +2994,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; + typeInfo.ssType = SSType.JSON; + typeInfo.displaySize = typeInfo.precision = Integer.MAX_VALUE / 2; + typeInfo.charset = Encoding.UTF8.charset(); + } }); private final TDSType tdsType; @@ -3737,6 +3761,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, 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; } 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..f92d774c5 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/callablestatement/CallableStatementTest.java @@ -75,6 +75,9 @@ public class CallableStatementTest extends AbstractTest { .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 @@ -98,6 +101,8 @@ public static void setupTest() throws Exception { TestUtils.dropUserDefinedTypeIfExists(manyParamUserDefinedType, stmt); TestUtils.dropProcedureIfExists(manyParamProc, stmt); TestUtils.dropTableIfExists(manyParamsTable, stmt); + TestUtils.dropTableIfExists(tableNameJSON, stmt); + TestUtils.dropProcedureIfExists(procedureNameJSON, stmt); createGUIDTable(stmt); createGUIDStoredProcedure(stmt); @@ -112,6 +117,8 @@ public static void setupTest() throws Exception { createGetObjectOffsetDateTimeProcedure(stmt); createConditionalProcedure(); createSimpleRetValSproc(); + createJSONTestTable(stmt); + createJSONStoredProcedure(stmt); } } @@ -597,6 +604,42 @@ public void testTimestampStringConversion() throws SQLException { 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 @@ -617,6 +660,8 @@ public static void cleanup() throws SQLException { TestUtils.dropProcedureIfExists(conditionalSproc, stmt); TestUtils.dropProcedureIfExists(simpleRetValSproc, stmt); TestUtils.dropProcedureIfExists(zeroParamSproc, stmt); + TestUtils.dropTableIfExists(tableNameJSON, stmt); + TestUtils.dropProcedureIfExists(procedureNameJSON, stmt); } } @@ -715,4 +760,15 @@ private static void createUserDefinedType() throws SQLException { 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); + } } 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 5239d2fff..7299d8d58 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 * @@ -349,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 * @@ -693,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 { 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)