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..a8bded545 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,11 @@ void writeRPCStringUnicode(String sValue) throws SQLServerException { writeRPCStringUnicode(null, sValue, false, null); } + void writeRPCJson(String sName, String sValue, boolean bOut) throws SQLServerException { + writeRPCNameValType(sName, bOut, TDSType.JSON); + writeLong(0xFFFFFFFFFFFFFFFFL); + } + /** * Writes a string value as Unicode for RPC * @@ -5241,6 +5254,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 +5490,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 1b21544f9..7a97650df 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 @@ -601,11 +605,19 @@ public void setEscapeColumnDelimitersCSV(boolean escapeDelimiters) { this.escapeDelimiters = escapeDelimiters; } + private static boolean isJson(String token) { + return token.startsWith("{") && token.endsWith("}"); + } + private static String[] escapeQuotesRFC4180(String[] tokens) throws SQLServerException { if (null == tokens) { return tokens; } for (int i = 0; i < tokens.length; i++) { + if (isJson(tokens[i])) { + continue; // Skip JSON strings + } + boolean escaped = false; int j = 0; StringBuilder sb = new StringBuilder(); diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java index 35f239608..d90f0cbe9 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java @@ -780,7 +780,8 @@ private void writeColumnMetaDataColumnData(TDSWriter tdsWriter, int idx) throws collation = connection.getDatabaseCollation(); if ((java.sql.Types.NCHAR == bulkJdbcType) || (java.sql.Types.NVARCHAR == bulkJdbcType) - || (java.sql.Types.LONGNVARCHAR == bulkJdbcType)) { + || (java.sql.Types.LONGNVARCHAR == bulkJdbcType) + || (microsoft.sql.Types.JSON == bulkJdbcType)) { isStreaming = (DataTypes.SHORT_VARTYPE_MAX_CHARS < bulkPrecision) || (DataTypes.SHORT_VARTYPE_MAX_CHARS < destPrecision); } else { @@ -837,7 +838,8 @@ else if (((java.sql.Types.CHAR == bulkJdbcType) || (java.sql.Types.VARCHAR == bu int baseDestPrecision = destCryptoMeta.baseTypeInfo.getPrecision(); if ((java.sql.Types.NCHAR == baseDestJDBCType) || (java.sql.Types.NVARCHAR == baseDestJDBCType) - || (java.sql.Types.LONGNVARCHAR == baseDestJDBCType)) + || (java.sql.Types.LONGNVARCHAR == baseDestJDBCType) + || (microsoft.sql.Types.JSON == baseDestJDBCType)) isStreaming = (DataTypes.SHORT_VARTYPE_MAX_CHARS < baseDestPrecision); else isStreaming = (DataTypes.SHORT_VARTYPE_MAX_BYTES < baseDestPrecision); @@ -997,6 +999,7 @@ private void writeTypeInfo(TDSWriter tdsWriter, int srcJdbcType, int srcScale, i case java.sql.Types.LONGVARCHAR: case java.sql.Types.VARCHAR: // 0xA7 + case microsoft.sql.Types.JSON: if (unicodeConversionRequired(srcJdbcType, destSSType)) { tdsWriter.writeByte(TDSType.NVARCHAR.byteValue()); if (isStreaming) { @@ -1025,7 +1028,6 @@ private void writeTypeInfo(TDSWriter tdsWriter, int srcJdbcType, int srcScale, i } collation.writeCollation(tdsWriter); break; - case java.sql.Types.BINARY: // 0xAD tdsWriter.writeByte(TDSType.BIGBINARY.byteValue()); tdsWriter.writeShort((short) (srcPrecision)); @@ -1127,7 +1129,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 +1472,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 +2094,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 { @@ -2322,6 +2327,7 @@ else if (null != sourceCryptoMeta) { case java.sql.Types.LONGVARCHAR: case java.sql.Types.CHAR: // Fixed-length, non-Unicode string data. case java.sql.Types.VARCHAR: // Variable-length, non-Unicode string data. + case microsoft.sql.Types.JSON: if (isStreaming) // PLP { // PLP_BODY rule in TDS @@ -2460,7 +2466,6 @@ else if (null != sourceCryptoMeta) { } } break; - case java.sql.Types.LONGVARBINARY: case java.sql.Types.BINARY: case java.sql.Types.VARBINARY: @@ -2986,6 +2991,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 +3066,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 +3778,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 f5e6bc317..06b27b845 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -1190,6 +1190,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(); @@ -5544,6 +5554,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; @@ -6674,6 +6695,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); @@ -6974,6 +7013,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 @@ -7170,6 +7212,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 942bfdc26..bf6c946eb 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."}, @@ -477,6 +478,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..bed4605a0 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/dtv.java @@ -294,6 +294,8 @@ 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 if (dtv.getJdbcType() == JDBCType.JSON) { + tdsWriter.writeRPCJson(name, strValue, isOutParam); } else { tdsWriter.writeRPCStringUnicode(name, strValue, isOutParam, collation); } @@ -1459,6 +1461,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 +2992,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 +3759,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/bulkCopy/BulkCopyCSVTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyCSVTest.java index 057c5068c..67cee87c3 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyCSVTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyCSVTest.java @@ -15,6 +15,7 @@ import java.lang.StackOverflowError; import java.net.URL; import java.sql.Connection; +import java.sql.JDBCType; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; @@ -64,7 +65,9 @@ public class BulkCopyCSVTest extends AbstractTest { static String inputFile = "BulkCopyCSVTestInput.csv"; + static String jsonInputFile = "BulkCopyCSVTestInputWithJson.csv"; static String inputFileNoColumnName = "BulkCopyCSVTestInputNoColumnName.csv"; + static String jsonInputFileNoColumnName = "BulkCopyCSVTestInputNoColumnNameWithJson.csv"; static String inputFileDelimiterEscape = "BulkCopyCSVTestInputDelimiterEscape.csv"; static String inputFileDelimiterEscapeNoNewLineAtEnd = "BulkCopyCSVTestInputDelimiterEscapeNoNewLineAtEnd.csv"; static String inputFileMultipleDoubleQuotes = "BulkCopyCSVTestInputMultipleDoubleQuotes.csv"; @@ -93,13 +96,13 @@ public static void setUpConnection() throws Exception { @Test @DisplayName("Test SQLServerBulkCSVFileRecord") public void testCSV() { - String fileName = filePath + inputFile; + String fileName = filePath + jsonInputFile; try (SQLServerBulkCSVFileRecord f1 = new SQLServerBulkCSVFileRecord(fileName, encoding, delimiter, true); SQLServerBulkCSVFileRecord f2 = new SQLServerBulkCSVFileRecord(fileName, encoding, delimiter, true);) { - testBulkCopyCSV(f1, true); + testBulkCopyCSV(f1, true, jsonInputFile, jsonInputFileNoColumnName); f2.setEscapeColumnDelimitersCSV(true); - testBulkCopyCSV(f2, true); + testBulkCopyCSV(f2, true, jsonInputFile, jsonInputFileNoColumnName); } catch (SQLException e) { fail(e.getMessage()); } @@ -111,13 +114,13 @@ public void testCSV() { @Test @DisplayName("Test SQLServerBulkCSVFileRecord First line not being column name") public void testCSVFirstLineNotColumnName() { - String fileName = filePath + inputFileNoColumnName; + String fileName = filePath + jsonInputFileNoColumnName; try (SQLServerBulkCSVFileRecord f1 = new SQLServerBulkCSVFileRecord(fileName, encoding, delimiter, false); SQLServerBulkCSVFileRecord f2 = new SQLServerBulkCSVFileRecord(fileName, encoding, delimiter, false)) { - testBulkCopyCSV(f1, false); + testBulkCopyCSV(f1, false, jsonInputFile, jsonInputFileNoColumnName); f2.setEscapeColumnDelimitersCSV(true); - testBulkCopyCSV(f2, false); + testBulkCopyCSV(f2, false, jsonInputFile, jsonInputFileNoColumnName); } catch (SQLException e) { fail(e.getMessage()); } @@ -136,7 +139,7 @@ public void testCSVFromURL() throws SQLException { .openStream(); SQLServerBulkCSVFileRecord fileRecord = new SQLServerBulkCSVFileRecord(csvFileInputStream, encoding, delimiter, true)) { - testBulkCopyCSV(fileRecord, true); + testBulkCopyCSV(fileRecord, true, inputFile, inputFileNoColumnName); } catch (Exception e) { fail(e.getMessage()); } @@ -269,13 +272,13 @@ public void testEscapeColumnDelimitersCSVNoNewLineAtEnd() throws Exception { @Test @DisplayName("Test SQLServerBulkCSVFileRecord GitHb 1391") public void testCSV1391() { - String fileName = filePath + inputFile; + String fileName = filePath + jsonInputFile; try (SQLServerBulkCSVFileRecord f1 = new BulkData1391(fileName, encoding, delimiter, true); SQLServerBulkCSVFileRecord f2 = new BulkData1391(fileName, encoding, delimiter, true);) { - testBulkCopyCSV(f1, true); + testBulkCopyCSV(f1, true, jsonInputFile, jsonInputFileNoColumnName); f2.setEscapeColumnDelimitersCSV(true); - testBulkCopyCSV(f2, true); + testBulkCopyCSV(f2, true, jsonInputFile, jsonInputFileNoColumnName); } catch (SQLException e) { fail(e.getMessage()); } @@ -299,10 +302,10 @@ public Set getColumnOrdinals() { } } - private void testBulkCopyCSV(SQLServerBulkCSVFileRecord fileRecord, boolean firstLineIsColumnNames) { + private void testBulkCopyCSV(SQLServerBulkCSVFileRecord fileRecord, boolean firstLineIsColumnNames, String csvFileName, String csvInputFileNoColumnName) { DBTable destTable = null; try (BufferedReader br = new BufferedReader( - new InputStreamReader(new FileInputStream(filePath + inputFile), encoding))) { + new InputStreamReader(new FileInputStream(filePath + csvFileName), encoding))) { // read the first line from csv and parse it to get datatypes to create destination column String[] columnTypes = br.readLine().substring(1)/* Skip the Byte order mark */.split(delimiter, -1); br.close(); @@ -340,18 +343,18 @@ private void testBulkCopyCSV(SQLServerBulkCSVFileRecord fileRecord, boolean firs } else { sqlType = SqlTypeMapping.valueOf(columnType.toUpperCase()).sqlType; } - + int vendorTypeNumber = sqlType.getJdbctype() != JDBCType.NULL ? sqlType.getJdbctype().getVendorTypeNumber() : sqlType.getVendorTypeNumber(); destTable.addColumn(sqlType); - fileRecord.addColumnMetadata(i + 1, "", sqlType.getJdbctype().getVendorTypeNumber(), + fileRecord.addColumnMetadata(i + 1, "", vendorTypeNumber, (-1 == precision) ? 0 : precision, (-1 == scale) ? 0 : scale); } stmt.createTable(destTable); bulkCopy.writeToServer(fileRecord); } if (firstLineIsColumnNames) - validateValuesFromCSV(destTable, inputFile); + validateValuesFromCSV(destTable, csvFileName); else - validateValuesFromCSV(destTable, inputFileNoColumnName); + validateValuesFromCSV(destTable, csvInputFileNoColumnName); } catch (Exception e) { fail(e.getMessage()); @@ -370,7 +373,7 @@ private void testBulkCopyCSV(SQLServerBulkCSVFileRecord fileRecord, boolean firs static void validateValuesFromCSV(DBTable destinationTable, String inputFile) { try (BufferedReader br = new BufferedReader( new InputStreamReader(new FileInputStream(filePath + inputFile), encoding))) { - if (inputFile.equalsIgnoreCase("BulkCopyCSVTestInput.csv")) + if (inputFile.equalsIgnoreCase("BulkCopyCSVTestInput.csv") || inputFile.equalsIgnoreCase("BulkCopyCSVTestInputWithJson.csv")) br.readLine(); // skip first line as it is header try (DBResultSet dstResultSet = stmt diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyISQLServerBulkRecordTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyISQLServerBulkRecordTest.java index 56ee5c1dd..059c902e2 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyISQLServerBulkRecordTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyISQLServerBulkRecordTest.java @@ -150,6 +150,37 @@ public void testBulkCopyDateTimePrecision() throws SQLException { } } + @Test + public void testBulkCopyJSON() throws SQLException { + String dstTable = TestUtils + .escapeSingleQuotes(AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("dstTable"))); + + try (Connection conn = DriverManager.getConnection(connectionString);) { + try (Statement dstStmt = conn.createStatement(); SQLServerBulkCopy bulkCopy = new SQLServerBulkCopy(conn)) { + + dstStmt.executeUpdate( + "CREATE TABLE " + dstTable + " (testCol JSON);"); + + bulkCopy.setDestinationTableName(dstTable); + String data = "{\"key\":\"value\"}"; + bulkCopy.writeToServer(new BulkRecordJSON(data)); + + String select = "SELECT * FROM " + dstTable; + ResultSet rs = dstStmt.executeQuery(select); + + assertTrue(rs.next()); + assertTrue(data.equals(rs.getObject(1))); + + } catch (Exception e) { + fail(e.getMessage()); + } finally { + try (Statement stmt = conn.createStatement();) { + TestUtils.dropTableIfExists(dstTable, stmt); + } + } + } + } + class BulkData implements ISQLServerBulkData { private static final long serialVersionUID = 1L; @@ -322,4 +353,54 @@ public boolean next() { return true; } } + + private static class BulkRecordJSON implements ISQLServerBulkData { + boolean anyMoreData = true; + Object[] data; + + BulkRecordJSON(Object data) { + this.data = new Object[1]; + this.data[0] = data; + } + + @Override + public Set getColumnOrdinals() { + Set ords = new HashSet<>(); + ords.add(1); + return ords; + } + + @Override + public String getColumnName(int column) { + return "testCol"; + } + + @Override + public int getColumnType(int column) { + return microsoft.sql.Types.JSON; + } + + @Override + public int getPrecision(int column) { + return 0; + } + + @Override + public int getScale(int column) { + return 0; + } + + @Override + public Object[] getRowData() { + return data; + } + + @Override + public boolean next() { + if (!anyMoreData) + return false; + anyMoreData = false; + return true; + } + } } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/SqlTypeMapping.java b/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/SqlTypeMapping.java index 820ce02d5..342bc0436 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/SqlTypeMapping.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/SqlTypeMapping.java @@ -15,6 +15,7 @@ import com.microsoft.sqlserver.testframework.sqlType.SqlDecimal; import com.microsoft.sqlserver.testframework.sqlType.SqlFloat; import com.microsoft.sqlserver.testframework.sqlType.SqlInt; +import com.microsoft.sqlserver.testframework.sqlType.SqlJson; import com.microsoft.sqlserver.testframework.sqlType.SqlMoney; import com.microsoft.sqlserver.testframework.sqlType.SqlNChar; import com.microsoft.sqlserver.testframework.sqlType.SqlNVarChar; @@ -62,7 +63,8 @@ public enum SqlTypeMapping { DATETIMEOFFSET(new SqlDateTimeOffset()), // Binary BINARY(new SqlBinary()), - VARBINARY(new SqlVarBinary()),; + VARBINARY(new SqlVarBinary()), + JSON(new SqlJson()),; public SqlType sqlType; 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..5c1628750 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); + 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); + 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/databasemetadata/DatabaseMetaDataTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/databasemetadata/DatabaseMetaDataTest.java index 65b76ae12..d24e6e7ca 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/databasemetadata/DatabaseMetaDataTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/databasemetadata/DatabaseMetaDataTest.java @@ -1034,6 +1034,26 @@ public void shouldEscapeSchemaName() throws SQLException { } } + @Test + public void testJSONMetaData() throws SQLException { + String jsonTableName = RandomUtil.getIdentifier("try_SQLJSON_Table"); + + try (Statement stmt = connection.createStatement()) { + String sql = "create table " + AbstractSQLGenerator.escapeIdentifier(jsonTableName) + + " (c1 JSON null);"; + stmt.execute(sql); + + String query = "SELECT * FROM " + AbstractSQLGenerator.escapeIdentifier(jsonTableName); + try (Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery(query)) { + + ResultSetMetaData metaData = resultSet.getMetaData(); + String columnType = metaData.getColumnTypeName(1); + assertTrue("JSON".equalsIgnoreCase(columnType)); + } + } + } + @BeforeAll public static void setupTable() throws Exception { setConnection(); 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..b6a7d207d 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,78 @@ 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 NOT NULL PRIMARY KEY, [c2] json, [c3] json)"); + + int pkRow1 = 1; + int pkRow2 = 2; + int pkRow3 = 3; + String sql = "insert into " + AbstractSQLGenerator.escapeIdentifier(tableName) + " values (?, ?,?)"; + try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) connection.prepareStatement(sql)) { + pstmt.setInt(1, pkRow1); + pstmt.setObject(2, "{\"key11\":\"value11\"}"); + pstmt.setObject(3, "{\"key12\":\"value12\"}"); + pstmt.executeUpdate(); + } + + sql = "insert into " + AbstractSQLGenerator.escapeIdentifier(tableName) + " values (?, ?,?)"; + try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) connection.prepareStatement(sql)) { + pstmt.setInt(1, pkRow2); + pstmt.setObject(2, "{\"key21\":\"value21\"}"); + pstmt.setObject(3, "{\"key22\":\"value22\"}"); + pstmt.executeUpdate(); + } + + sql = "insert into " + AbstractSQLGenerator.escapeIdentifier(tableName) + " values (?, ?,?)"; + try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) connection.prepareStatement(sql)) { + pstmt.setInt(1, pkRow3); + pstmt.setObject(2, "{\"key31\":\"value31\"}"); + pstmt.setObject(3, "{\"key32\":\"value32\"}"); + pstmt.executeUpdate(); + } + + sql = "DELETE " + AbstractSQLGenerator.escapeIdentifier(tableName) + " where [c1] = ?"; + try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) connection.prepareStatement(sql)) { + pstmt.setInt(1, pkRow1); + pstmt.executeUpdate(); + } + + sql = "UPDATE " + AbstractSQLGenerator.escapeIdentifier(tableName) + " SET [c2] = ?, [c3] = ? where [c1] = ?"; + try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) connection.prepareStatement(sql)) { + pstmt.setObject(1, "{\"key21.1\":\"value21.1\"}"); + pstmt.setObject(2, "{\"key22.1\":\"value22.1\"}"); + pstmt.setInt(3, pkRow2); + pstmt.executeUpdate(); + } + + sql = "DELETE " + AbstractSQLGenerator.escapeIdentifier(tableName) + " where [c1] = ?"; + try (SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) connection.prepareStatement(sql)) { + pstmt.setInt(1, pkRow3); + pstmt.executeUpdate(); + } + + try (ResultSet rs = stmt + .executeQuery("select * from " + AbstractSQLGenerator.escapeIdentifier(tableName))) { + rs.next(); + assertEquals(rs.getInt(1), 2, "Value mismatch"); + assertEquals(rs.getObject(2), "{\"key21.1\":\"value21.1\"}", "Value mismatch"); + assertEquals(rs.getObject(3), "{\"key22.1\":\"value22.1\"}", "Value mismatch"); + } finally { + TestUtils.dropTableIfExists(AbstractSQLGenerator.escapeIdentifier(tableName), stmt); + } + } + } + private void createTable(Statement stmt) throws SQLException { String sql = "CREATE TABLE " + AbstractSQLGenerator.escapeIdentifier(tableName) diff --git a/src/test/java/com/microsoft/sqlserver/testframework/sqlType/SqlJson.java b/src/test/java/com/microsoft/sqlserver/testframework/sqlType/SqlJson.java new file mode 100644 index 000000000..75fd65532 --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/testframework/sqlType/SqlJson.java @@ -0,0 +1,19 @@ +/* + * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made + * available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ + +package com.microsoft.sqlserver.testframework.sqlType; + +public class SqlJson extends SqlType { + + public SqlJson() { + super("json", microsoft.sql.Types.JSON, 0, 0, SqlTypeValue.JSON.minValue, SqlTypeValue.JSON.maxValue, + SqlTypeValue.JSON.nullValue, VariableLengthType.Fixed, String.class); + } + + @Override + public Object createdata() { + return "{}"; + } +} \ No newline at end of file diff --git a/src/test/java/com/microsoft/sqlserver/testframework/sqlType/SqlType.java b/src/test/java/com/microsoft/sqlserver/testframework/sqlType/SqlType.java index edba5f703..e38fe3661 100644 --- a/src/test/java/com/microsoft/sqlserver/testframework/sqlType/SqlType.java +++ b/src/test/java/com/microsoft/sqlserver/testframework/sqlType/SqlType.java @@ -21,6 +21,7 @@ public abstract class SqlType extends DBItems { // exact data for debugging protected String name = null; // type name for creating SQL query protected JDBCType jdbctype = JDBCType.NULL; + protected int vendorTypeNumber = 0; protected int precision = 0; protected int scale = 0; protected Object minvalue = null; @@ -79,6 +80,34 @@ public abstract class SqlType extends DBItems { this.type = type; } + /** + * + * @param name + * @param vendorTypeNumber + * @param precision + * @param scale + * @param min + * minimum allowed value for the SQL type + * @param max + * maximum allowed value for the SQL type + * @param nullvalue + * default null value for the SQL type + * @param variableLengthType + * {@link VariableLengthType} + */ + SqlType(String name, int vendorTypeNumber, int precision, int scale, Object min, Object max, Object nullvalue, + VariableLengthType variableLengthType, Class type) { + this.name = name; + this.vendorTypeNumber = vendorTypeNumber; + this.precision = precision; + this.scale = scale; + this.minvalue = min; + this.maxvalue = max; + this.nullvalue = nullvalue; + this.variableLengthType = variableLengthType; + this.type = type; + } + /** * * @return valid random value for the SQL type @@ -262,4 +291,20 @@ public boolean canConvert(Class target, int flag, DBConnection conn) throws E return false; } + /** + * + * @return vendorTypeNumber of SqlType object + */ + public int getVendorTypeNumber() { + return vendorTypeNumber; + } + + /** + * + * @param vendorTypeNumber + * set vendorTypeNumber of SqlType object + */ + public void setVendorTypeNumber(int vendorTypeNumber) { + this.vendorTypeNumber = vendorTypeNumber; + } } diff --git a/src/test/java/com/microsoft/sqlserver/testframework/sqlType/SqlTypeValue.java b/src/test/java/com/microsoft/sqlserver/testframework/sqlType/SqlTypeValue.java index 91c51210b..61b208134 100644 --- a/src/test/java/com/microsoft/sqlserver/testframework/sqlType/SqlTypeValue.java +++ b/src/test/java/com/microsoft/sqlserver/testframework/sqlType/SqlTypeValue.java @@ -32,7 +32,8 @@ enum SqlTypeValue { TIME("00:00:00.0000000", "23:59:59.9999999", null), SMALLDATETIME("19000101T00:00:00", "20790606T23:59:59", null), DATETIME2("00010101T00:00:00.0000000", "99991231T23:59:59.9999999", null), - DATETIMEOFFSET("0001-01-01 00:00:00", "9999-12-31 23:59:59", null),; + DATETIMEOFFSET("0001-01-01 00:00:00", "9999-12-31 23:59:59", null), + JSON(null, null, null),; Object minValue; Object maxValue; diff --git a/src/test/resources/BulkCopyCSVTestInputNoColumnNameWithJson.csv b/src/test/resources/BulkCopyCSVTestInputNoColumnNameWithJson.csv new file mode 100644 index 000000000..43fccd2e9 --- /dev/null +++ b/src/test/resources/BulkCopyCSVTestInputNoColumnNameWithJson.csv @@ -0,0 +1,5 @@ +1,2,-32768,0,0,-1.78E307,-3.4E38,22.335600,22.3356,-922337203685477.5808,-214748.3648,a5()b,௵ஷஇமண,test to test csv files,ࢨहश,6163686974,6163686974,1922-11-02,2004-05-23 14:25:10.487,2007-05-02 19:58:47.1234567,2004-05-23 14:25:00.0,2025-12-10 12:32:10.1234567 +01:00,12:23:48.1234567,{"name":"John"} +,,,,,,,,,,,,,,,,,,,,,,, +0,5,32767,1,12,-2.23E-308,-1.18E-38,33.552695,33.5526,922337203685477.5807,0.0000,what!,ৡਐਲ,123 norma black street,Ӧ NӦ,5445535455,54455354,9999-12-31,9999-12-31 23:59:59.997,9999-12-31 23:59:59.9999999,2079-06-06 23:59:00.0,9999-12-31 23:59:00.0000000 +00:00,23:59:59.9990000,{"product":"Laptop"} +0,255,0,-2147483648,-9223372036854775808,2.23E-308,0.0,33.503288,33.5032,0.0000,1.0011,no way,Ӧ NӦ,baker street Mr.Homls,àĂ,303C2D3988,303C2D39,0001-01-01,1973-01-01 00:00:00.0,0001-01-01 00:00:00.0000000,1900-01-01 00:00:00.0,0001-01-01 00:00:00.0000000 +00:00,00:00:00.0000000,{"status":"active"} +1,5,0,2147483647,9223372036854775807,12.0,3.4E38,33.000501,33.0005,1.0001,214748.3647,l l l l l |,Ȣʗʘ,test to test csv files,௵ஷஇமண,7E7D7A7B20,7E7D7A7B,2017-04-18,2014-10-11 20:13:12.123,2017-10-12 09:38:17.7654321,2014-10-11 20:13:00.0,2017-01-06 10:52:20.7654321 +03:00,18:02:16.7654321,{"status":"active"} diff --git a/src/test/resources/BulkCopyCSVTestInputWithJson.csv b/src/test/resources/BulkCopyCSVTestInputWithJson.csv new file mode 100644 index 000000000..6ece9b971 --- /dev/null +++ b/src/test/resources/BulkCopyCSVTestInputWithJson.csv @@ -0,0 +1,6 @@ +bit,tinyint,smallint,int,bigint,float(53),real,decimal(18-6),numeric(18-4),money(20-4),smallmoney(20-4),char(11),nchar(10),varchar(50),nvarchar(10),binary(5),varbinary(25),date,datetime,datetime2(7),smalldatetime,datetimeoffset(7),time(16-7),json +1,2,-32768,0,0,-1.78E307,-3.4E38,22.335600,22.3356,-922337203685477.5808,-214748.3648,a5()b,௵ஷஇமண,test to test csv files,ࢨहश,6163686974,6163686974,1922-11-02,2004-05-23 14:25:10.487,2007-05-02 19:58:47.1234567,2004-05-23 14:25:00.0,2025-12-10 12:32:10.1234567 +01:00,12:23:48.1234567,{"name":"John"} +,,,,,,,,,,,,,,,,,,,,,,, +0,5,32767,1,12,-2.23E-308,-1.18E-38,33.552695,33.5526,922337203685477.5807,0.0000,what!,ৡਐਲ,123 norma black street,Ӧ NӦ,5445535455,54455354,9999-12-31,9999-12-31 23:59:59.997,9999-12-31 23:59:59.9999999,2079-06-06 23:59:00.0,9999-12-31 23:59:00.0000000 +00:00,23:59:59.9990000,{"product":"Laptop"} +0,255,0,-2147483648,-9223372036854775808,2.23E-308,0.0,33.503288,33.5032,0.0000,1.0011,no way,Ӧ NӦ,baker street Mr.Homls,àĂ,303C2D3988,303C2D39,0001-01-01,1973-01-01 00:00:00.0,0001-01-01 00:00:00.0000000,1900-01-01 00:00:00.0,0001-01-01 00:00:00.0000000 +00:00,00:00:00.0000000,{"status":"active"} +1,5,0,2147483647,9223372036854775807,12.0,3.4E38,33.000501,33.0005,1.0001,214748.3647,l l l l l |,Ȣʗʘ,test to test csv files,௵ஷஇமண,7E7D7A7B20,7E7D7A7B,2017-04-18,2014-10-11 20:13:12.123,2017-10-12 09:38:17.7654321,2014-10-11 20:13:00.0,2017-01-06 10:52:20.7654321 +03:00,18:02:16.7654321,{"status":"active"}