diff --git a/pom.xml b/pom.xml index 321c4226d..7967c4c9b 100644 --- a/pom.xml +++ b/pom.xml @@ -48,10 +48,11 @@ clientCertAuth - - For tests requiring client certificate authentication setup (excluded by default) - - - - - - - - - - - - - - - - - - - - - - - requireSecret - For tests requiring setting up secrets manually + JSONTest - For tests using JSON data type - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Default testing enabled with SQL Server 2019 (SQLv15) --> - xSQLv12,xSQLv15,NTLM,MSI,reqExternalSetup,clientCertAuth,fedAuth,kerberos,requireSecret + xSQLv12,xSQLv15,NTLM,MSI,reqExternalSetup,clientCertAuth,fedAuth,kerberos,requireSecret,JSONTest -preview 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..ac22871b3 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); 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..8dfa54c51 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,13 @@ public void setEscapeColumnDelimitersCSV(boolean escapeDelimiters) { this.escapeDelimiters = escapeDelimiters; } + private static String[] escapeQuotesRFC4180(String[] tokens) throws SQLServerException { if (null == tokens) { return tokens; } for (int i = 0; i < tokens.length; i++) { + 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..515cbb607 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)); @@ -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/SQLServerPreparedStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java index 2a0c79d97..95d2f6191 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java @@ -1674,8 +1674,12 @@ public final void setObject(int n, Object obj, int jdbcType) throws SQLServerExc checkClosed(); if (microsoft.sql.Types.STRUCTURED == jdbcType) { tvpName = getTVPNameFromObject(n, obj); - } - setObject(setterGetParam(n), obj, JavaType.of(obj), JDBCType.of(jdbcType), null, null, false, n, tvpName); + } + if (microsoft.sql.Types.JSON == jdbcType) { + setObjectNoType(n, obj, false); + } else { + setObject(setterGetParam(n), obj, JavaType.of(obj), JDBCType.of(jdbcType), null, null, false, n, tvpName); + } loggerExternal.exiting(getClassNameLogging(), "setObject"); } 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..7eca85253 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; + 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..a41db2555 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyCSVTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyCSVTest.java @@ -64,6 +64,7 @@ public class BulkCopyCSVTest extends AbstractTest { static String inputFile = "BulkCopyCSVTestInput.csv"; + static String jsonInputFile = "BulkCopyCSVTestInputWithJson.csv"; static String inputFileNoColumnName = "BulkCopyCSVTestInputNoColumnName.csv"; static String inputFileDelimiterEscape = "BulkCopyCSVTestInputDelimiterEscape.csv"; static String inputFileDelimiterEscapeNoNewLineAtEnd = "BulkCopyCSVTestInputDelimiterEscapeNoNewLineAtEnd.csv"; @@ -446,6 +447,57 @@ public void testCSV2400() { } } + @Test + @DisplayName("Test Bulk Copy with JSON Data") + @Tag(Constants.JSONTest) + public void testBulkCopyWithJson() throws Exception { + String tableName = AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("BulkJsonTest")); + String fileName = filePath + jsonInputFile; + + // Expected values as read from the CSV file + String[][] expectedValues = new String[][]{ + {"0", "testing", "{\"age\":25,\"address\":{\"pincode\":123456,\"state\":\"NY\"}}"}, + {"1","test }","{\"age\":25,\"city\":\"Los Angeles\"}"}, + {"0","test {0}","{\"age\":40,\"city\":\"Chicago\"}"} + }; + + try (Connection con = getConnection(); Statement stmt = con.createStatement(); + SQLServerBulkCopy bulkCopy = new SQLServerBulkCopy(con); + SQLServerBulkCSVFileRecord fileRecord = new SQLServerBulkCSVFileRecord(fileName, encoding, ",", false)) { + bulkCopy.setDestinationTableName(tableName); + + // Define column metadata + fileRecord.addColumnMetadata(1, null, java.sql.Types.BIT, 0, 0); + fileRecord.addColumnMetadata(2, null, java.sql.Types.NCHAR, 10, 0); + fileRecord.addColumnMetadata(3, null, microsoft.sql.Types.JSON, 0, 0); // JSON column + + // Create table + stmt.executeUpdate("CREATE TABLE " + tableName + " (" + + "c1 BIT, c2 nchar(50), c3 JSON)"); + + // Perform bulk copy + fileRecord.setEscapeColumnDelimitersCSV(true); + bulkCopy.writeToServer(fileRecord); + + // Verify the data + int i = 0; + try (ResultSet rs = stmt.executeQuery("SELECT * FROM " + tableName); + BufferedReader br = new BufferedReader(new FileReader(fileName))) { + + while (rs.next()) { + for (int j = 1; j <= 3; j++) { + String actual = rs.getString(j); + String expected = expectedValues[i][j - 1]; + assertEquals(expected.trim(), actual.trim(), "Mismatch in column " + j); + } + i++; + } + } finally { + TestUtils.dropTableIfExists(tableName, stmt); + } + } + } + /** * drop source table after testing bulk copy * 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..837ecbdaf 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyISQLServerBulkRecordTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyISQLServerBulkRecordTest.java @@ -4,6 +4,7 @@ */ package com.microsoft.sqlserver.jdbc.bulkCopy; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -43,7 +44,6 @@ import com.microsoft.sqlserver.testframework.DBTable; import com.microsoft.sqlserver.testframework.sqlType.SqlType; - /** * Test bulk copy decimal scale and precision */ @@ -119,26 +119,370 @@ public void testBulkCopyDateTimePrecision() throws SQLException { bulkCopy.writeToServer(new BulkRecordDT(data8)); String select = "SELECT * FROM " + dstTable + " order by Dataid"; - ResultSet rs = dstStmt.executeQuery(select); - - assertTrue(rs.next()); - assertTrue(data.equals(rs.getObject(2, LocalDateTime.class))); - assertTrue(rs.next()); - assertTrue(data1.equals(rs.getObject(2, LocalDateTime.class))); - assertTrue(rs.next()); - assertTrue(data2.equals(rs.getObject(2, LocalDateTime.class))); - assertTrue(rs.next()); - assertTrue(data3.equals(rs.getObject(2, LocalDateTime.class))); - assertTrue(rs.next()); - assertTrue(data4.equals(rs.getObject(2, LocalDateTime.class))); - assertTrue(rs.next()); - assertTrue(data5.equals(rs.getObject(2, LocalDateTime.class))); - assertTrue(rs.next()); - assertTrue(data6.equals(rs.getObject(2, LocalDateTime.class))); - assertTrue(rs.next()); - assertTrue(data7.equals(rs.getObject(2, LocalDateTime.class))); - assertTrue(rs.next()); - assertTrue(data8.equals(rs.getObject(2, LocalDateTime.class))); + try (ResultSet rs = dstStmt.executeQuery(select)) { + assertTrue(rs.next()); + assertTrue(data.equals(rs.getObject(2, LocalDateTime.class))); + assertTrue(rs.next()); + assertTrue(data1.equals(rs.getObject(2, LocalDateTime.class))); + assertTrue(rs.next()); + assertTrue(data2.equals(rs.getObject(2, LocalDateTime.class))); + assertTrue(rs.next()); + assertTrue(data3.equals(rs.getObject(2, LocalDateTime.class))); + assertTrue(rs.next()); + assertTrue(data4.equals(rs.getObject(2, LocalDateTime.class))); + assertTrue(rs.next()); + assertTrue(data5.equals(rs.getObject(2, LocalDateTime.class))); + assertTrue(rs.next()); + assertTrue(data6.equals(rs.getObject(2, LocalDateTime.class))); + assertTrue(rs.next()); + assertTrue(data7.equals(rs.getObject(2, LocalDateTime.class))); + assertTrue(rs.next()); + assertTrue(data8.equals(rs.getObject(2, LocalDateTime.class))); + } + + } catch (Exception e) { + fail(e.getMessage()); + } finally { + try (Statement stmt = conn.createStatement();) { + TestUtils.dropTableIfExists(dstTable, stmt); + } + } + } + } + + /** + * Test bulk copy with a single JSON row. + */ + @Test + @Tag(Constants.JSONTest) + 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; + try (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); + } + } + } + } + + /** + * Test bulk copy with empty JSON document + */ + @Test + @Tag(Constants.JSONTest) + public void testBulkCopyWithEmptyJsonDocument() 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 data1 = "{}"; + String data2 = "{\"key2\":\"value2\",\"key3\":123}"; + bulkCopy.writeToServer(new BulkRecordJSON(data1)); + bulkCopy.writeToServer(new BulkRecordJSON(data2)); + + String select = "SELECT * FROM " + dstTable; + try (ResultSet rs = dstStmt.executeQuery(select)) { + assertTrue(rs.next()); + assertEquals(data1, rs.getString(1)); + assertTrue(rs.next()); + assertEquals(data2, rs.getString(1)); + } + + } catch (Exception e) { + fail(e.getMessage()); + } finally { + try (Statement stmt = conn.createStatement();) { + TestUtils.dropTableIfExists(dstTable, stmt); + } + } + } + } + + + /** + * Test bulk copy with multiple JSON rows containing different structures + * and compared using getString(columnIndex) + */ + @Test + @Tag(Constants.JSONTest) + public void testBulkCopyMultipleJsonRowsWithDifferentStructures() 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 data1 = "{\"key1\":\"value1\"}"; + String data2 = "{\"key2\":\"value2\",\"key3\":123}"; + String data3 = "{\"key3\":123,\"key4\":\"value4\",\"key5\":\"value5\"}"; + bulkCopy.writeToServer(new BulkRecordJSON(data1)); + bulkCopy.writeToServer(new BulkRecordJSON(data2)); + bulkCopy.writeToServer(new BulkRecordJSON(data3)); + + String select = "SELECT * FROM " + dstTable; + try (ResultSet rs = dstStmt.executeQuery(select)) { + assertTrue(rs.next()); + assertEquals(data1, rs.getString(1)); + assertTrue(rs.next()); + assertEquals(data2, rs.getString(1)); + assertTrue(rs.next()); + assertEquals(data3, rs.getString(1)); + } + + } catch (Exception e) { + fail(e.getMessage()); + } finally { + try (Statement stmt = conn.createStatement();) { + TestUtils.dropTableIfExists(dstTable, stmt); + } + } + } + } + + /** + * Test bulk copy with multiple JSON rows. + */ + @Test + @Tag(Constants.JSONTest) + public void testBulkCopyMultipleJsonRows() 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 data1 = "{\"key1\":\"value1\"}"; + String data2 = "{\"key2\":\"value2\"}"; + String data3 = "{\"key3\":\"value3\"}"; + bulkCopy.writeToServer(new BulkRecordJSON(data1)); + bulkCopy.writeToServer(new BulkRecordJSON(data2)); + bulkCopy.writeToServer(new BulkRecordJSON(data3)); + + String select = "SELECT * FROM " + dstTable; + try (ResultSet rs = dstStmt.executeQuery(select)) { + assertTrue(rs.next()); + assertTrue(data1.equals(rs.getObject(1))); + assertTrue(rs.next()); + assertTrue(data2.equals(rs.getObject(1))); + assertTrue(rs.next()); + assertTrue(data3.equals(rs.getObject(1))); + } + + } catch (Exception e) { + fail(e.getMessage()); + } finally { + try (Statement stmt = conn.createStatement();) { + TestUtils.dropTableIfExists(dstTable, stmt); + } + } + } + } + + /** + * Test bulk copy with multiple JSON rows and columns. + */ + @Test + @Tag(Constants.JSONTest) + public void testBulkCopyMultipleJsonRowsAndColumns() 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 + " (testCol1 JSON, testCol2 JSON);"); + + bulkCopy.setDestinationTableName(dstTable); + String data1Col1 = "{\"key1\":\"value1\"}"; + String data1Col2 = "{\"key2\":\"value2\"}"; + String data2Col1 = "{\"key3\":\"value3\"}"; + String data2Col2 = "{\"key4\":\"value4\"}"; + bulkCopy.writeToServer(new BulkRecordJSONMultipleColumns(data1Col1, data1Col2)); + bulkCopy.writeToServer(new BulkRecordJSONMultipleColumns(data2Col1, data2Col2)); + + String select = "SELECT * FROM " + dstTable; + try (ResultSet rs = dstStmt.executeQuery(select)) { + assertTrue(rs.next()); + assertTrue(data1Col1.equals(rs.getObject(1))); + assertTrue(data1Col2.equals(rs.getObject(2))); + assertTrue(rs.next()); + assertTrue(data2Col1.equals(rs.getObject(1))); + assertTrue(data2Col2.equals(rs.getObject(2))); + } + + } catch (Exception e) { + fail(e.getMessage()); + } finally { + try (Statement stmt = conn.createStatement();) { + TestUtils.dropTableIfExists(dstTable, stmt); + } + } + } + } + + /** + * Test bulk copy with nested JSON documents. + */ + @Test + @Tag(Constants.JSONTest) + public void testBulkCopyNestedJsonRows() 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 data1 = "{\"key1\":{\"nestedKey1\":\"nestedValue1\"}}"; + String data2 = "{\"key2\":{\"nestedKey2\":{\"nestedKey3\":\"nestedValue3\"}}}"; + String data3 = "{\"key3\":{\"nestedKey4\":{\"nestedKey5\":{\"nestedKey6\":\"nestedValue6\"}}}}"; + bulkCopy.writeToServer(new BulkRecordJSON(data1)); + bulkCopy.writeToServer(new BulkRecordJSON(data2)); + bulkCopy.writeToServer(new BulkRecordJSON(data3)); + + String select = "SELECT * FROM " + dstTable; + try (ResultSet rs = dstStmt.executeQuery(select)) { + assertTrue(rs.next()); + assertTrue(data1.equals(rs.getObject(1))); + assertTrue(rs.next()); + assertTrue(data2.equals(rs.getObject(1))); + assertTrue(rs.next()); + assertEquals(data3, rs.getString(1)); + } + + } catch (Exception e) { + fail(e.getMessage()); + } finally { + try (Statement stmt = conn.createStatement();) { + TestUtils.dropTableIfExists(dstTable, stmt); + } + } + } + } + + /** + * Test bulk copy with various data types in JSON. + */ + @Test + @Tag(Constants.JSONTest) + public void testBulkCopyWithVariousDataTypes() 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); + + // JSON data to be inserted + String data = "{\"bitCol\":true,\"tinyIntCol\":2,\"smallIntCol\":-32768,\"intCol\":0,\"bigIntCol\":0,\"floatCol\":-1700.0000000000,\"realCol\":-3400.0000000000,\"decimalCol\":22.335600,\"numericCol\":22.3356,\"moneyCol\":-922337203685477.5808,\"smallMoneyCol\":-214748.3648,\"charCol\":\"a5()b\",\"nCharCol\":\"?????\",\"varcharCol\":\"test to test csv files\",\"nVarcharCol\":\"???\",\"binaryCol\":\"6163686974\",\"varBinaryCol\":\"6163686974\",\"dateCol\":\"1922-11-02\",\"datetimeCol\":\"2004-05-23 14:25:10.487\",\"datetime2Col\":\"2007-05-02 19:58:47.1234567\",\"datetimeOffsetCol\":\"2025-12-10 12:32:10.1234567+01:00\"}"; + + bulkCopy.writeToServer(new BulkRecordJSON(data)); + + String select = "SELECT * FROM " + dstTable; + try (ResultSet rs = dstStmt.executeQuery(select)) { + assertTrue(rs.next()); + String jsonData = rs.getString(1); + assertEquals(data, jsonData); + } + + } catch (Exception e) { + fail(e.getMessage()); + } finally { + try (Statement stmt = conn.createStatement();) { + TestUtils.dropTableIfExists(dstTable, stmt); + } + } + } + } + + /** + * Test bulk copy with count verification. + */ + @Test + @Tag(Constants.JSONTest) + public void testBulkCopyWithCountVerification() 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 data1 = "{\"key1\":\"value1\"}"; + String data2 = "{\"key2\":\"value2\"}"; + bulkCopy.writeToServer(new BulkRecordJSON(data1)); + bulkCopy.writeToServer(new BulkRecordJSON(data2)); + + String selectCount = "SELECT COUNT(*) FROM " + dstTable; + int count1 = 0; + try (ResultSet rs = dstStmt.executeQuery(selectCount)) { + if (rs.next()) { + count1 = rs.getInt(1); + } + } + + String select = "SELECT * FROM " + dstTable; + int count2 = 0; + try (ResultSet rs = dstStmt.executeQuery(select)) { + while (rs.next()) { + count2++; + } + } + + assertEquals(count1, count2); } catch (Exception e) { fail(e.getMessage()); @@ -322,4 +666,110 @@ 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; + } + } + + private static class BulkRecordJSONMultipleColumns implements ISQLServerBulkData { + boolean anyMoreData = true; + Object[] data; + + BulkRecordJSONMultipleColumns(Object data1, Object data2) { + this.data = new Object[2]; + this.data[0] = data1; + this.data[1] = data2; + } + + @Override + public Set getColumnOrdinals() { + Set ords = new HashSet<>(); + ords.add(1); + ords.add(2); + return ords; + } + + @Override + public String getColumnName(int column) { + if (column == 1) { + return "testCol1"; + } else { + return "testCol2"; + } + } + + @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..6d86180d3 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,44 @@ public void testTimestampStringConversion() throws SQLException { stmt.getObject("currentTimeStamp"); } } + + @Test + @Tag(Constants.JSONTest) + 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 + @Tag(Constants.JSONTest) + 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 +662,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 +762,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..65bfd63ab 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,46 @@ public void shouldEscapeSchemaName() throws SQLException { } } + @Test + @Tag(Constants.JSONTest) + 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(); + int columnCount = metaData.getColumnCount(); + assertEquals(1, columnCount, "Column count should be 1"); + + String columnName = metaData.getColumnName(1); + assertEquals("c1", columnName, "Column name should be 'c1'"); + + String columnType = metaData.getColumnTypeName(1); + assertTrue("JSON".equalsIgnoreCase(columnType), "Column type should be 'JSON'"); + + int columnTypeInt = metaData.getColumnType(1); + assertEquals(microsoft.sql.Types.JSON, columnTypeInt, "Column type should be microsoft.sql.Types.JSON"); + + int columnDisplaySize = metaData.getColumnDisplaySize(1); + assertTrue(columnDisplaySize > 0, "Column display size should be greater than 0"); + + String columnClassName = metaData.getColumnClassName(1); + assertEquals(Object.class.getName(), columnClassName, "Column class name should be 'java.lang.Object'"); + } + } finally { + try (Statement stmt = connection.createStatement()) { + stmt.execute("DROP TABLE IF EXISTS " + AbstractSQLGenerator.escapeIdentifier(jsonTableName)); + } + } + } + @BeforeAll public static void setupTable() throws Exception { setConnection(); diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/datatypes/JSONFunctionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/datatypes/JSONFunctionTest.java new file mode 100644 index 000000000..d51cb35b8 --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/datatypes/JSONFunctionTest.java @@ -0,0 +1,1788 @@ +/* + * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made + * available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ + +package com.microsoft.sqlserver.jdbc.datatypes; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.Reader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.sql.Clob; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; +import org.junit.jupiter.api.Tag; + +import com.microsoft.sqlserver.jdbc.RandomUtil; +import com.microsoft.sqlserver.jdbc.TestUtils; +import com.microsoft.sqlserver.testframework.AbstractSQLGenerator; +import com.microsoft.sqlserver.testframework.AbstractTest; +import com.microsoft.sqlserver.testframework.Constants; + +@RunWith(JUnitPlatform.class) +@DisplayName("Test Json Functions") +public class JSONFunctionTest extends AbstractTest { + + @BeforeAll + public static void setupTests() throws Exception { + setConnection(); + } + + private static final String JSON_FILE_PATH = "large_json.json"; + + /** + * Test ISJSON function with JSON. + * ISJSON -> Tests whether a string contains valid JSON. + */ + @Test + @Tag(Constants.JSONTest) + public void testISJSON() throws SQLException { + String dstTable = TestUtils + .escapeSingleQuotes(AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("dstTable"))); + + try (Connection conn = DriverManager.getConnection(connectionString);) { + try (Statement dstStmt = conn.createStatement()) { + + dstStmt.executeUpdate( + "CREATE TABLE " + dstTable + " (testCol JSON);"); + + String validJson = "{\"key1\":\"value1\"}"; + dstStmt.executeUpdate( + "INSERT INTO " + dstTable + " (testCol) VALUES ('" + validJson + "')"); + + String select = "SELECT testCol, " + + "ISJSON(testCol) AS isJsonValid " + + "FROM " + dstTable; + try (ResultSet rs = dstStmt.executeQuery(select)) { + assertTrue(rs.next()); + assertEquals(validJson, rs.getString("testCol")); + assertEquals(1, rs.getInt("isJsonValid")); + } + + } catch (Exception e) { + fail(e.getMessage()); + } finally { + try (Statement stmt = conn.createStatement();) { + TestUtils.dropTableIfExists(dstTable, stmt); + } + } + } + } + + /** + * Test ISJSON function with valid and invalid JSON value. + * ISJSON -> Tests whether a string contains valid JSON. + */ + @Test + @Tag(Constants.JSONTest) + public void testISJSONWithVariousInputs() throws SQLException { + String dstTable = TestUtils + .escapeSingleQuotes(AbstractSQLGenerator.escapeIdentifier("dstTable")); + + try (Connection conn = DriverManager.getConnection(connectionString);) { + try (Statement stmt = conn.createStatement()) { + + stmt.executeUpdate( + "CREATE TABLE " + dstTable + " (testCol NVARCHAR(MAX));"); + + String validJson = "{\"key1\":\"value1\"}"; + String invalidJson = "Not a JSON string"; + String jsonScalar = "123"; + + stmt.executeUpdate("INSERT INTO " + dstTable + " (testCol) VALUES ('" + validJson + "')"); + stmt.executeUpdate("INSERT INTO " + dstTable + " (testCol) VALUES ('" + invalidJson + "')"); + stmt.executeUpdate("INSERT INTO " + dstTable + " (testCol) VALUES ('" + jsonScalar + "')"); + + String select = "SELECT testCol, " + + "ISJSON(testCol) AS isJsonValid " + + "FROM " + dstTable; + try (ResultSet rs = stmt.executeQuery(select)) { + assertTrue(rs.next()); + assertEquals(validJson, rs.getString("testCol")); + assertEquals(1, rs.getInt("isJsonValid")); + + assertTrue(rs.next()); + assertEquals(invalidJson, rs.getString("testCol")); + assertEquals(0, rs.getInt("isJsonValid")); + + assertTrue(rs.next()); + assertEquals(jsonScalar, rs.getString("testCol")); + assertEquals(0, rs.getInt("isJsonValid")); + } + + } catch (Exception e) { + fail(e.getMessage()); + } finally { + try (Statement stmt = conn.createStatement();) { + TestUtils.dropTableIfExists(dstTable, stmt); + } + } + } + } + + /** + * Test JSON_ARRAY function without NULL values. + * JSON_ARRAY -> Constructs JSON array text from zero or more expressions. + * input: JSON_ARRAY('value1', 123, NULL) -> + * output: ["value1",123] + */ + @Test + @Tag(Constants.JSONTest) + public void testJSONArrayWithoutNulls() throws SQLException { + String dstTable = TestUtils + .escapeSingleQuotes(AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("dstTable"))); + + try (Connection conn = DriverManager.getConnection(connectionString);) { + try (Statement dstStmt = conn.createStatement()) { + dstStmt.executeUpdate( + "CREATE TABLE " + dstTable + " (testCol JSON);"); + + String data1 = "SELECT JSON_ARRAY('value1', 123, NULL) AS jsonArray"; + dstStmt.executeUpdate("INSERT INTO " + dstTable + " (testCol) " + data1); + + String select = "SELECT testCol FROM " + dstTable; + try (ResultSet rs = dstStmt.executeQuery(select)) { + assertTrue(rs.next()); + assertEquals("[\"value1\",123]", rs.getString("testCol")); + } + } catch (Exception e) { + fail(e.getMessage()); + } finally { + try (Statement stmt = conn.createStatement();) { + TestUtils.dropTableIfExists(dstTable, stmt); + } + } + } + } + + /** + * Test JSON_ARRAY function with NULL values included. + * JSON_ARRAY -> Constructs JSON array text from zero or more expressions. + * input: JSON_ARRAY('value1', 123, NULL, 'value2' NULL ON NULL) -> + * output: ["value1",123,null,"value2"] + */ + @Test + @Tag(Constants.JSONTest) + public void testJSONArrayWithNulls() throws SQLException { + String dstTable = TestUtils + .escapeSingleQuotes(AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("dstTable"))); + + try (Connection conn = DriverManager.getConnection(connectionString);) { + try (Statement dstStmt = conn.createStatement()) { + dstStmt.executeUpdate( + "CREATE TABLE " + dstTable + " (testCol JSON);"); + + String data2 = "SELECT JSON_ARRAY('value1', 123, NULL, 'value2' NULL ON NULL) AS jsonArray"; + dstStmt.executeUpdate("INSERT INTO " + dstTable + " (testCol) " + data2); + + String select = "SELECT testCol FROM " + dstTable; + try (ResultSet rs = dstStmt.executeQuery(select)) { + assertTrue(rs.next()); + assertEquals("[\"value1\",123,null,\"value2\"]", rs.getString("testCol")); + } + } catch (Exception e) { + fail(e.getMessage()); + } finally { + try (Statement stmt = conn.createStatement();) { + TestUtils.dropTableIfExists(dstTable, stmt); + } + } + } + } + + /** + * Test JSON_ARRAY with string, JSON object, and JSON array. + * JSON_ARRAY -> Constructs JSON array text from zero or more expressions. + * input: JSON_ARRAY('a', JSON_OBJECT('name':'value', 'type':1), JSON_ARRAY(1, + * null, 2 NULL ON NULL)) -> + * output: ["a",{"name":"value","type":1},[1,null,2]] + */ + @Test + @Tag(Constants.JSONTest) + public void testJSONArrayWithMixedElements() throws SQLException { + String dstTable = TestUtils + .escapeSingleQuotes(AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("dstTable"))); + + try (Connection conn = DriverManager.getConnection(connectionString);) { + try (Statement dstStmt = conn.createStatement()) { + dstStmt.executeUpdate( + "CREATE TABLE " + dstTable + " (testCol JSON);"); + + String data = "SELECT JSON_ARRAY('a', JSON_OBJECT('name':'value', 'type':1), JSON_ARRAY(1, null, 2 NULL ON NULL)) AS jsonArray"; + dstStmt.executeUpdate("INSERT INTO " + dstTable + " (testCol) " + data); + + String select = "SELECT testCol FROM " + dstTable; + try (ResultSet rs = dstStmt.executeQuery(select)) { + assertTrue(rs.next()); + assertEquals("[\"a\",{\"name\":\"value\",\"type\":1},[1,null,2]]", rs.getString("testCol")); + } + } catch (Exception e) { + fail(e.getMessage()); + } finally { + try (Statement stmt = conn.createStatement();) { + TestUtils.dropTableIfExists(dstTable, stmt); + } + } + } + } + + /** + * Test JSON_ARRAY with variables and SQL expressions. + * JSON_ARRAY -> Constructs JSON array text from zero or more expressions. + * input: JSON_ARRAY(1, @id_value, (SELECT @@SPID)) -> + * output: [1,"",""] + */ + @Test + @Tag(Constants.JSONTest) + public void testJSONArrayWithVariablesAndExpressions() throws SQLException { + String dstTable = TestUtils + .escapeSingleQuotes(AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("dstTable"))); + + try (Connection conn = DriverManager.getConnection(connectionString);) { + try (Statement dstStmt = conn.createStatement()) { + dstStmt.executeUpdate( + "CREATE TABLE " + dstTable + " (testCol NVARCHAR(MAX));"); + + String data = "DECLARE @id_value nvarchar(64) = NEWID(); " + + "INSERT INTO " + dstTable + " (testCol) " + + "SELECT JSON_ARRAY(1, @id_value, (SELECT @@SPID)) AS jsonArray"; + + dstStmt.execute(data); + + String select = "SELECT testCol FROM " + dstTable; + try (ResultSet rs = dstStmt.executeQuery(select)) { + assertTrue(rs.next()); + String jsonArray = rs.getString("testCol"); + assertTrue(jsonArray.startsWith("[1,\"")); + assertTrue(jsonArray.contains("\",")); + assertTrue(jsonArray.endsWith("]")); + } + } catch (Exception e) { + fail(e.getMessage()); + } finally { + try (Statement stmt = conn.createStatement();) { + TestUtils.dropTableIfExists(dstTable, stmt); + } + } + } + } + + /** + * Test JSON_ARRAY per row in the query. + * JSON_ARRAY -> Constructs JSON array text from zero or more expressions. + * input: JSON_ARRAY(s.host_name, s.program_name, s.client_interface_name) -> + * output: ["","",""] + */ + @Test + @Tag(Constants.JSONTest) + public void testJSONArrayPerRow() throws SQLException { + String dstTable = TestUtils + .escapeSingleQuotes(AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("dstTable"))); + + try (Connection conn = DriverManager.getConnection(connectionString);) { + try (Statement dstStmt = conn.createStatement()) { + dstStmt.executeUpdate( + "CREATE TABLE " + dstTable + " (session_id INT, info JSON);"); + + String data = "SELECT s.session_id, JSON_ARRAY(s.host_name, s.program_name, s.client_interface_name) AS info " + + + "FROM sys.dm_exec_sessions AS s " + + "WHERE s.is_user_process = 1"; + dstStmt.executeUpdate("INSERT INTO " + dstTable + " (session_id, info) " + data); + + String select = "SELECT session_id, info FROM " + dstTable; + try (ResultSet rs = dstStmt.executeQuery(select)) { + while (rs.next()) { + int sessionId = rs.getInt("session_id"); + String info = rs.getString("info"); + assertTrue(info.startsWith("[\"")); + assertTrue(info.endsWith("\"]")); + } + } + } catch (Exception e) { + fail(e.getMessage()); + } finally { + try (Statement stmt = conn.createStatement();) { + TestUtils.dropTableIfExists(dstTable, stmt); + } + } + } + } + + /** + * Test JSON_ARRAYAGG function. + * JSON_ARRAYAGG -> Constructs a JSON array from an aggregation of SQL data or + * columns. + * input: JSON_ARRAYAGG(testCol) -> + * output: ["",""] + */ + @Test + @Tag(Constants.JSONTest) + public void testJSONArrayAgg() throws SQLException { + String dstTable = TestUtils + .escapeSingleQuotes(AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("dstTable"))); + + try (Connection conn = DriverManager.getConnection(connectionString);) { + try (Statement dstStmt = conn.createStatement()) { + + dstStmt.executeUpdate( + "CREATE TABLE " + dstTable + " (testCol JSON);"); + + dstStmt.executeUpdate("INSERT INTO " + dstTable + " VALUES ('{\"key\":\"value1\"}');"); + dstStmt.executeUpdate("INSERT INTO " + dstTable + " VALUES ('{\"key\":\"value2\"}');"); + + String select = "SELECT JSON_ARRAYAGG(testCol) AS jsonArrayAgg FROM " + dstTable; + try (ResultSet rs = dstStmt.executeQuery(select)) { + assertTrue(rs.next()); + assertEquals("[{\"key\":\"value1\"},{\"key\":\"value2\"}]", rs.getString("jsonArrayAgg")); + } + + } catch (Exception e) { + fail(e.getMessage()); + } finally { + try (Statement stmt = conn.createStatement();) { + TestUtils.dropTableIfExists(dstTable, stmt); + } + } + } + } + + /** + * Test JSON_ARRAYAGG with three elements from a result set. + * JSON_ARRAYAGG -> Constructs a JSON array from an aggregation of SQL data or + * columns. + * input: JSON_ARRAYAGG(c1) -> + * output: ["c","b","a"] + */ + @Test + @Tag(Constants.JSONTest) + public void testJSONArrayAggWithThreeElements() throws SQLException { + String select = "SELECT JSON_ARRAYAGG(c1) AS jsonArrayAgg FROM (VALUES ('c'), ('b'), ('a')) AS t(c1)"; + try (Connection conn = DriverManager.getConnection(connectionString); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery(select)) { + assertTrue(rs.next()); + assertEquals("[\"c\",\"b\",\"a\"]", rs.getString("jsonArrayAgg")); + } catch (Exception e) { + fail(e.getMessage()); + } + } + + /** + * Test JSON_ARRAYAGG with three elements ordered by the value of the column. + * JSON_ARRAYAGG -> Constructs a JSON array from an aggregation of SQL data or + * columns. + * input: JSON_ARRAYAGG(c1 ORDER BY c1) -> + * output: ["a","b","c"] + */ + @Test + @Tag(Constants.JSONTest) + public void testJSONArrayAggWithOrderedElements() throws SQLException { + String select = "SELECT JSON_ARRAYAGG(c1 ORDER BY c1) AS jsonArrayAgg FROM (VALUES ('c'), ('b'), ('a')) AS t(c1)"; + try (Connection conn = DriverManager.getConnection(connectionString); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery(select)) { + assertTrue(rs.next()); + assertEquals("[\"a\",\"b\",\"c\"]", rs.getString("jsonArrayAgg")); + } catch (Exception e) { + fail(e.getMessage()); + } + } + + /** + * Test JSON_ARRAYAGG with two columns. + * JSON_ARRAYAGG -> Constructs a JSON array from an aggregation of SQL data or + * columns. + * input: JSON_ARRAYAGG(c.name ORDER BY c.column_id) -> + * output: ["column1","column2"] + */ + @Test + @Tag(Constants.JSONTest) + public void testJSONArrayAggWithTwoColumns() throws SQLException { + String dstTable = TestUtils + .escapeSingleQuotes(AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("dstTable"))); + + try (Connection conn = DriverManager.getConnection(connectionString);) { + try (Statement stmt = conn.createStatement()) { + // Create table and insert data + stmt.executeUpdate( + "CREATE TABLE " + dstTable + " (object_id INT, name NVARCHAR(50), column_id INT);"); + + stmt.executeUpdate( + "INSERT INTO " + dstTable + " (object_id, name, column_id) VALUES (1, 'column1', 1);"); + stmt.executeUpdate( + "INSERT INTO " + dstTable + " (object_id, name, column_id) VALUES (1, 'column2', 2);"); + stmt.executeUpdate( + "INSERT INTO " + dstTable + " (object_id, name, column_id) VALUES (2, 'column3', 1);"); + stmt.executeUpdate( + "INSERT INTO " + dstTable + " (object_id, name, column_id) VALUES (2, 'column4', 2);"); + + String select = "SELECT object_id, JSON_ARRAYAGG(name ORDER BY column_id) AS column_list " + + "FROM " + dstTable + " " + + "GROUP BY object_id"; + try (ResultSet rs = stmt.executeQuery(select)) { + while (rs.next()) { + int objectId = rs.getInt("object_id"); + String columnList = rs.getString("column_list"); + if (objectId == 1) { + assertEquals("[\"column1\",\"column2\"]", columnList); + } else if (objectId == 2) { + assertEquals("[\"column3\",\"column4\"]", columnList); + } else { + fail("Unexpected object_id: " + objectId); + } + } + } + } catch (Exception e) { + fail(e.getMessage()); + } finally { + try (Statement stmt = conn.createStatement();) { + TestUtils.dropTableIfExists(dstTable, stmt); + } + } + } + } + + /** + * Test JSON_MODIFY function with various operations. + * JSON_MODIFY -> Updates the value of a property in a JSON string and returns + * the updated JSON string. + * input: JSON_MODIFY(testCol, '$.key', 'value2') -> + * output: {"key":"value2"} + */ + @Test + @Tag(Constants.JSONTest) + public void testJSONModify() throws SQLException { + String dstTable = TestUtils + .escapeSingleQuotes(AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("dstTable"))); + + try (Connection conn = DriverManager.getConnection(connectionString);) { + try (Statement dstStmt = conn.createStatement()) { + + dstStmt.executeUpdate( + "CREATE TABLE " + dstTable + " (testCol JSON);"); + + String data = "{\"key\":\"value1\"}"; + dstStmt.executeUpdate( + "INSERT INTO " + dstTable + " (testCol) VALUES ('" + data + "')"); + + String update = "UPDATE " + dstTable + " SET testCol = JSON_MODIFY(testCol, '$.key', 'value2')"; + dstStmt.executeUpdate(update); + + String select = "SELECT testCol FROM " + dstTable; + try (ResultSet rs = dstStmt.executeQuery(select)) { + assertTrue(rs.next()); + assertEquals("{\"key\":\"value2\"}", rs.getString("testCol")); + } + + String newRow = "{\"name\":\"John\",\"skills\":[\"C#\",\"SQL\"]}"; + dstStmt.executeUpdate("INSERT INTO " + dstTable + " (testCol) VALUES ('" + newRow + "')"); + + select = "SELECT testCol FROM " + dstTable + " WHERE JSON_VALUE(testCol, '$.name') = 'John'"; + try (ResultSet rs = dstStmt.executeQuery(select)) { + assertTrue(rs.next()); + assertEquals("{\"name\":\"John\",\"skills\":[\"C#\",\"SQL\"]}", rs.getString("testCol")); + } + + String delete = "DELETE FROM " + dstTable + " WHERE JSON_VALUE(testCol, '$.key') = 'value2'"; + dstStmt.executeUpdate(delete); + + select = "SELECT testCol FROM " + dstTable; + try (ResultSet rs = dstStmt.executeQuery(select)) { + assertTrue(rs.next()); + assertEquals("{\"name\":\"John\",\"skills\":[\"C#\",\"SQL\"]}", rs.getString("testCol")); + } + + String addKeyValue = "UPDATE " + dstTable + " SET testCol = JSON_MODIFY(testCol, '$.surname', 'Smith')"; + dstStmt.executeUpdate(addKeyValue); + + select = "SELECT testCol FROM " + dstTable; + try (ResultSet rs = dstStmt.executeQuery(select)) { + assertTrue(rs.next()); + assertEquals("{\"name\":\"John\",\"skills\":[\"C#\",\"SQL\"],\"surname\":\"Smith\"}", + rs.getString("testCol")); + } + + String addSkill = "UPDATE " + dstTable + + " SET testCol = JSON_MODIFY(testCol, 'append $.skills', 'Azure')"; + dstStmt.executeUpdate(addSkill); + + select = "SELECT testCol FROM " + dstTable; + try (ResultSet rs = dstStmt.executeQuery(select)) { + assertTrue(rs.next()); + assertEquals("{\"name\":\"John\",\"skills\":[\"C#\",\"SQL\",\"Azure\"],\"surname\":\"Smith\"}", + rs.getString("testCol")); + } + } catch (Exception e) { + fail(e.getMessage()); + } finally { + try (Statement stmt = conn.createStatement();) { + TestUtils.dropTableIfExists(dstTable, stmt); + } + } + } + } + + /** + * Test JSON_MODIFY with multiple updates. + * JSON_MODIFY -> Updates the value of a property in a JSON string and returns + * the updated JSON string. + * input: JSON_MODIFY(JSON_MODIFY(JSON_MODIFY(@info, '$.name', 'Mike'), + * '$.surname', 'Smith'), 'append $.skills', 'Azure') -> + * output: {"name":"Mike","skills":["C#","SQL","Azure"],"surname":"Smith"} + */ + @Test + @Tag(Constants.JSONTest) + public void testJSONModifyMultipleUpdates() throws SQLException { + String json = "{\"name\":\"John\",\"skills\":[\"C#\",\"SQL\"]}"; + String expectedJson = "{\"name\":\"Mike\",\"skills\":[\"C#\",\"SQL\",\"Azure\"],\"surname\":\"Smith\"}"; + + String update = "DECLARE @info JSON = '" + json + "'; " + + "SET @info = JSON_MODIFY(JSON_MODIFY(JSON_MODIFY(@info, '$.name', 'Mike'), '$.surname', 'Smith'), 'append $.skills', 'Azure'); " + + + "SELECT @info AS info;"; + try (Connection conn = DriverManager.getConnection(connectionString); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery(update)) { + assertTrue(rs.next()); + assertEquals(expectedJson, rs.getString("info")); + } catch (Exception e) { + fail(e.getMessage()); + } + } + + /** + * Test JSON_MODIFY to rename a key. + * JSON_MODIFY -> Updates the value of a property in a JSON string and returns + * the updated JSON string. + * input: JSON_MODIFY(JSON_MODIFY(@product, '$.Price', CAST(JSON_VALUE(@product, + * '$.price') AS NUMERIC(4, 2))), '$.price', NULL) -> + * output: {"Price":49.99} + */ + @Test + @Tag(Constants.JSONTest) + public void testJSONModifyRenameKey() throws SQLException { + String json = "{\"price\":49.99}"; + String expectedJson = "{\"Price\":49.99}"; + + String update = "DECLARE @product JSON = '" + json + "'; " + + "SET @product = JSON_MODIFY(JSON_MODIFY(@product, '$.Price', CAST(JSON_VALUE(@product, '$.price') AS NUMERIC(4, 2))), '$.price', NULL); " + + + "SELECT @product AS product;"; + try (Connection conn = DriverManager.getConnection(connectionString); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery(update)) { + assertTrue(rs.next()); + assertEquals(expectedJson, rs.getString("product")); + } catch (Exception e) { + fail(e.getMessage()); + } + } + + /** + * Test JSON_MODIFY to increment a value. + * JSON_MODIFY -> Updates the value of a property in a JSON string and returns + * the updated JSON string. + * input: JSON_MODIFY(@stats, '$.click_count', CAST(JSON_VALUE(@stats, + * '$.click_count') AS INT) + 1) -> + * output: {"click_count":174} + */ + @Test + @Tag(Constants.JSONTest) + public void testJSONModifyIncrementValue() throws SQLException { + String json = "{\"click_count\":173}"; + String expectedJson = "{\"click_count\":174}"; + + String update = "DECLARE @stats JSON = '" + json + "'; " + + "SET @stats = JSON_MODIFY(@stats, '$.click_count', CAST(JSON_VALUE(@stats, '$.click_count') AS INT) + 1); " + + + "SELECT @stats AS stats;"; + try (Connection conn = DriverManager.getConnection(connectionString); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery(update)) { + assertTrue(rs.next()); + assertEquals(expectedJson, rs.getString("stats")); + } catch (Exception e) { + fail(e.getMessage()); + } + } + + /** + * Test JSON_MODIFY to update a JSON column. + * JSON_MODIFY -> Updates the value of a property in a JSON string and returns + * the updated JSON string. + * input: JSON_MODIFY(jsonCol, '$.info.address.town', 'London') -> + * output: {"info":{"address":{"town":"London"}}} + */ + @Test + @Tag(Constants.JSONTest) + public void testJSONModifyUpdateJsonColumn() throws SQLException { + String dstTable = TestUtils + .escapeSingleQuotes(AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("dstTable"))); + + try (Connection conn = DriverManager.getConnection(connectionString);) { + try (Statement dstStmt = conn.createStatement()) { + + dstStmt.executeUpdate( + "CREATE TABLE " + dstTable + " (EmployeeID INT, jsonCol JSON);"); + + String data = "{\"info\":{\"address\":{\"town\":\"OldTown\"}}}"; + dstStmt.executeUpdate( + "INSERT INTO " + dstTable + " (EmployeeID, jsonCol) VALUES (17, '" + data + "')"); + + String update = "UPDATE " + dstTable + + " SET jsonCol = JSON_MODIFY(jsonCol, '$.info.address.town', 'London') WHERE EmployeeID = 17"; + dstStmt.executeUpdate(update); + + String select = "SELECT jsonCol FROM " + dstTable + " WHERE EmployeeID = 17"; + try (ResultSet rs = dstStmt.executeQuery(select)) { + assertTrue(rs.next()); + assertEquals("{\"info\":{\"address\":{\"town\":\"London\"}}}", rs.getString("jsonCol")); + } + } catch (Exception e) { + fail(e.getMessage()); + } finally { + try (Statement stmt = conn.createStatement();) { + TestUtils.dropTableIfExists(dstTable, stmt); + } + } + } + } + + /** + * Test JSON_OBJECT function to return an empty JSON object. + * JSON_OBJECT() -> Constructs JSON object text from zero or more expressions. + * input: JSON_OBJECT() -> + * output: {} + */ + @Test + @Tag(Constants.JSONTest) + public void testJSONObjectEmpty() throws SQLException { + String select = "SELECT JSON_OBJECT() AS jsonObject"; + try (Connection conn = DriverManager.getConnection(connectionString); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery(select)) { + assertTrue(rs.next()); + assertEquals("{}", rs.getString("jsonObject")); + } catch (Exception e) { + fail(e.getMessage()); + } + } + + /** + * Test JSON_OBJECT function to return a JSON object with one key since the + * value for one of the keys is NULL and the ABSENT ON NULL option is specified. + * JSON_OBJECT() -> Constructs JSON object text from zero or more expressions. + * input: JSON_OBJECT('name':'value', 'type':NULL ABSENT ON NULL) -> + * output: {"name":"value"} + */ + @Test + @Tag(Constants.JSONTest) + public void testJSONObjectWithMultipleKeys() throws SQLException { + String select = "SELECT JSON_OBJECT('name':'value', 'type':NULL ABSENT ON NULL) AS jsonObject"; + try (Connection conn = DriverManager.getConnection(connectionString); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery(select)) { + assertTrue(rs.next()); + assertEquals("{\"name\":\"value\"}", rs.getString("jsonObject")); + } catch (Exception e) { + fail(e.getMessage()); + } + } + + /** + * Test JSON_OBJECT function to return a JSON object with two keys. One key + * contains a JSON string and another key contains a JSON array. + * JSON_OBJECT() -> Constructs JSON object text from zero or more expressions. + * input: JSON_OBJECT('name':'value', 'type':JSON_ARRAY(1, 2)) -> + * output: {"name":"value","type":[1,2]} + */ + @Test + @Tag(Constants.JSONTest) + public void testJSONObjectWithJsonArray() throws SQLException { + String select = "SELECT JSON_OBJECT('name':'value', 'type':JSON_ARRAY(1, 2)) AS jsonObject"; + try (Connection conn = DriverManager.getConnection(connectionString); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery(select)) { + assertTrue(rs.next()); + assertEquals("{\"name\":\"value\",\"type\":[1,2]}", rs.getString("jsonObject")); + } catch (Exception e) { + fail(e.getMessage()); + } + } + + /** + * Test JSON_OBJECT function to return a JSON object with two keys. One key + * contains a JSON string and another key contains a JSON object. + * JSON_OBJECT() -> Constructs JSON object text from zero or more expressions. + * input: JSON_OBJECT('name':'value', 'type':JSON_OBJECT('type_id':1, + * 'name':'a')) -> + * output: {"name":"value","type":{"type_id":1,"name":"a"}} + */ + @Test + @Tag(Constants.JSONTest) + public void testJSONObjectWithNestedJsonObject() throws SQLException { + String select = "SELECT JSON_OBJECT('name':'value', 'type':JSON_OBJECT('type_id':1, 'name':'a')) AS jsonObject"; + try (Connection conn = DriverManager.getConnection(connectionString); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery(select)) { + assertTrue(rs.next()); + assertEquals("{\"name\":\"value\",\"type\":{\"type_id\":1,\"name\":\"a\"}}", rs.getString("jsonObject")); + } catch (Exception e) { + fail(e.getMessage()); + } + } + + /** + * Test JSON_OBJECT function to return a JSON object per row in the query. + * JSON_OBJECT() -> Constructs a JSON object per row in the query. + * input: JSON_OBJECT('security_id':s.security_id, 'login':s.login_name, + * 'status':s.status) -> + * output: + * {"security_id":"","login":"","status":""} + */ + @Test + @Tag(Constants.JSONTest) + public void testJSONObjectPerRow() throws SQLException { + String select = "SELECT s.session_id, JSON_OBJECT('security_id':s.security_id, 'login':s.login_name, 'status':s.status) AS info " + + + "FROM sys.dm_exec_sessions AS s " + + "WHERE s.is_user_process = 1"; + try (Connection conn = DriverManager.getConnection(connectionString); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery(select)) { + while (rs.next()) { + int sessionId = rs.getInt("session_id"); + String info = rs.getString("info"); + assertTrue(info.contains("\"security_id\":\"")); + assertTrue(info.contains("\"login\":\"")); + assertTrue(info.contains("\"status\":\"")); + } + } catch (Exception e) { + fail(e.getMessage()); + } + } + + /** + * Test JSON_OBJECTAGG function to construct a JSON object with three properties + * from a result set. + * JSON_OBJECTAGG() -> Constructs a JSON object with three properties. + * input: JSON_OBJECTAGG(c1:c2) -> + * output: {"key1":"c","key2":"b","key3":"a"} + */ + @Test + @Tag(Constants.JSONTest) + public void testJSONObjectAggWithThreeProperties() throws SQLException { + String select = "SELECT JSON_OBJECTAGG(c1:c2) AS jsonObjectAgg FROM (VALUES('key1', 'c'), ('key2', 'b'), ('key3','a')) AS t(c1, c2)"; + try (Connection conn = DriverManager.getConnection(connectionString); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery(select)) { + assertTrue(rs.next()); + assertEquals("{\"key1\":\"c\",\"key2\":\"b\",\"key3\":\"a\"}", rs.getString("jsonObjectAgg")); + } catch (Exception e) { + fail(e.getMessage()); + } + } + + /** + * Test JSON_OBJECTAGG function to return a result with two columns. The first + * column contains the object_id value. + * The second column contains a JSON object where the key is the column name and + * value is the column_id. + * JSON_OBJECTAGG() -> Constructs a JSON object with column names and column + * IDs. + * input: JSON_OBJECTAGG(c.name:c.column_id) -> + * output: {"column1":1,"column2":2} + */ + @Test + @Tag(Constants.JSONTest) + public void testJSONObjectAggWithColumnNamesAndIDs() throws SQLException { + String dstTable = TestUtils + .escapeSingleQuotes(AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("dstTable"))); + + try (Connection conn = DriverManager.getConnection(connectionString);) { + try (Statement stmt = conn.createStatement()) { + stmt.executeUpdate( + "CREATE TABLE " + dstTable + " (object_id INT, name NVARCHAR(50), column_id INT);"); + + stmt.executeUpdate( + "INSERT INTO " + dstTable + " (object_id, name, column_id) VALUES (1, 'column1', 1);"); + stmt.executeUpdate( + "INSERT INTO " + dstTable + " (object_id, name, column_id) VALUES (1, 'column2', 2);"); + stmt.executeUpdate( + "INSERT INTO " + dstTable + " (object_id, name, column_id) VALUES (2, 'column3', 1);"); + stmt.executeUpdate( + "INSERT INTO " + dstTable + " (object_id, name, column_id) VALUES (2, 'column4', 2);"); + + String select = "SELECT object_id, JSON_OBJECTAGG(name:column_id) AS columns " + + "FROM " + dstTable + " " + + "GROUP BY object_id"; + try (ResultSet rs = stmt.executeQuery(select)) { + while (rs.next()) { + int objectId = rs.getInt("object_id"); + String columns = rs.getString("columns"); + if (objectId == 1) { + assertEquals("{\"column1\":1,\"column2\":2}", columns); + } else if (objectId == 2) { + assertEquals("{\"column3\":1,\"column4\":2}", columns); + } else { + fail("Unexpected object_id: " + objectId); + } + } + } + } catch (Exception e) { + fail(e.getMessage()); + } finally { + try (Statement stmt = conn.createStatement();) { + TestUtils.dropTableIfExists(dstTable, stmt); + } + } + } + } + + /** + * Test JSON_PATH_EXISTS function to return 1 since the input JSON string + * contains the specified SQL/JSON path. + * JSON_PATH_EXISTS() -> Checks if a specified SQL/JSON path exists in the input + * JSON string. + * input: JSON_PATH_EXISTS(@jsonInfo, '$.info.address') -> + * output: 1 + */ + @Test + @Tag(Constants.JSONTest) + public void testJSONPathExistsTrue() throws SQLException { + String jsonInfo = "{\"info\":{\"address\":[{\"town\":\"Paris\"},{\"town\":\"London\"}]}}"; + String select = "DECLARE @jsonInfo AS JSON = N'" + jsonInfo + "'; " + + "SELECT JSON_PATH_EXISTS(@jsonInfo, '$.info.address') AS pathExists"; + try (Connection conn = DriverManager.getConnection(connectionString); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery(select)) { + assertTrue(rs.next()); + assertEquals(1, rs.getInt("pathExists")); + } catch (Exception e) { + fail(e.getMessage()); + } + } + + /** + * Test JSON_PATH_EXISTS function to return 0 since the input JSON string + * doesn't contain the specified SQL/JSON path. + * JSON_PATH_EXISTS() -> Checks if a specified SQL/JSON path exists in the input + * JSON string. + * input: JSON_PATH_EXISTS(@jsonInfo, '$.info.addresses') -> + * output: 0 + */ + @Test + @Tag(Constants.JSONTest) + public void testJSONPathExistsFalse() throws SQLException { + String jsonInfo = "{\"info\":{\"address\":[{\"town\":\"Paris\"},{\"town\":\"London\"}]}}"; + String select = "DECLARE @jsonInfo AS JSON = N'" + jsonInfo + "'; " + + "SELECT JSON_PATH_EXISTS(@jsonInfo, '$.info.addresses') AS pathExists"; + try (Connection conn = DriverManager.getConnection(connectionString); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery(select)) { + assertTrue(rs.next()); + assertEquals(0, rs.getInt("pathExists")); + } catch (Exception e) { + fail(e.getMessage()); + } + } + + /** + * Test JSON_QUERY function to return a JSON fragment from a CustomFields column + * in query results. + * JSON_QUERY() -> Extracts a JSON fragment from a JSON string. + * input: JSON_QUERY(CustomFields,'$.OtherLanguages') -> + * output: JSON fragment of OtherLanguages + */ + @Test + @Tag(Constants.JSONTest) + public void testJSONQueryFragment() throws SQLException { + String dstTable = TestUtils + .escapeSingleQuotes(AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("dstTable"))); + + try (Connection conn = DriverManager.getConnection(connectionString);) { + try (Statement dstStmt = conn.createStatement()) { + + dstStmt.executeUpdate( + "CREATE TABLE " + dstTable + " (PersonID INT, FullName NVARCHAR(100), CustomFields JSON);"); + + String insert = "INSERT INTO " + dstTable + " (PersonID, FullName, CustomFields) VALUES " + + "(1, 'John Doe', '{\"OtherLanguages\":[\"French\",\"Spanish\"]}'), " + + "(2, 'Jane Smith', '{\"OtherLanguages\":[\"German\",\"Italian\"]}')"; + dstStmt.executeUpdate(insert); + + String select = "SELECT PersonID, FullName, JSON_QUERY(CustomFields, '$.OtherLanguages') AS Languages FROM " + + dstTable; + try (ResultSet rs = dstStmt.executeQuery(select)) { + assertTrue(rs.next()); + assertEquals(1, rs.getInt("PersonID")); + assertEquals("John Doe", rs.getString("FullName")); + assertEquals("[\"French\",\"Spanish\"]", rs.getString("Languages")); + + assertTrue(rs.next()); + assertEquals(2, rs.getInt("PersonID")); + assertEquals("Jane Smith", rs.getString("FullName")); + assertEquals("[\"German\",\"Italian\"]", rs.getString("Languages")); + } + + } catch (Exception e) { + fail(e.getMessage()); + } finally { + try (Statement stmt = conn.createStatement();) { + TestUtils.dropTableIfExists(dstTable, stmt); + } + } + } + } + + /** + * Test JSON_QUERY function to include JSON fragments in the output of the FOR + * JSON clause. + * JSON_QUERY() -> Extracts a JSON fragment from a JSON string. + * input: JSON_QUERY(Tags), + * JSON_QUERY(CONCAT('[\"',ValidFrom,'\",\"',ValidTo,'\"]')) ValidityPeriod -> + * output: JSON fragments in the output of the FOR JSON clause + */ + @Test + @Tag(Constants.JSONTest) + public void testJSONQueryForJSONClause() throws SQLException { + String dstTable = TestUtils + .escapeSingleQuotes(AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("dstTable"))); + + try (Connection conn = DriverManager.getConnection(connectionString);) { + try (Statement dstStmt = conn.createStatement()) { + + dstStmt.executeUpdate( + "CREATE TABLE " + dstTable + + " (StockItemID INT, StockItemName NVARCHAR(100), Tags JSON, ValidFrom DATE, ValidTo DATE);"); + + String insert = "INSERT INTO " + dstTable + + " (StockItemID, StockItemName, Tags, ValidFrom, ValidTo) VALUES " + + "(1, 'Item1', '[\"Tag1\",\"Tag2\"]', '2023-01-01', '2023-12-31'), " + + "(2, 'Item2', '[\"Tag3\",\"Tag4\"]', '2023-02-01', '2023-11-30')"; + dstStmt.executeUpdate(insert); + + String select = "SELECT StockItemID, StockItemName, JSON_QUERY(Tags) AS Tags, " + + "JSON_QUERY(CONCAT('[\"', ValidFrom, '\",\"', ValidTo, '\"]')) AS ValidityPeriod " + + "FROM " + dstTable + " FOR JSON PATH"; + try (ResultSet rs = dstStmt.executeQuery(select)) { + assertTrue(rs.next()); + String jsonResult = rs.getString(1); + assertTrue(jsonResult.contains("\"StockItemID\":1")); + assertTrue(jsonResult.contains("\"StockItemName\":\"Item1\"")); + assertTrue(jsonResult.contains("\"Tags\":[\"Tag1\",\"Tag2\"]")); + assertTrue(jsonResult.contains("\"ValidityPeriod\":[\"2023-01-01\",\"2023-12-31\"]")); + + assertTrue(jsonResult.contains("\"StockItemID\":2")); + assertTrue(jsonResult.contains("\"StockItemName\":\"Item2\"")); + assertTrue(jsonResult.contains("\"Tags\":[\"Tag3\",\"Tag4\"]")); + assertTrue(jsonResult.contains("\"ValidityPeriod\":[\"2023-02-01\",\"2023-11-30\"]")); + } + + } catch (Exception e) { + fail(e.getMessage()); + } finally { + try (Statement stmt = conn.createStatement();) { + TestUtils.dropTableIfExists(dstTable, stmt); + } + } + } + } + + /** + * Test JSON_VALUE function to use JSON properties in query results. + * JSON_VALUE() -> Extracts a scalar value from a JSON string. + * input: JSON_VALUE(jsonInfo,'$.info.address.town') -> + * output: JSON property value of town + */ + @Test + @Tag(Constants.JSONTest) + public void testJSONValueInQueryResults() throws SQLException { + String dstTable = TestUtils + .escapeSingleQuotes(AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("dstTable"))); + + try (Connection conn = DriverManager.getConnection(connectionString);) { + try (Statement dstStmt = conn.createStatement()) { + + dstStmt.executeUpdate( + "CREATE TABLE " + dstTable + + " (FirstName NVARCHAR(50), LastName NVARCHAR(50), jsonInfo JSON);"); + + String insert = "INSERT INTO " + dstTable + " (FirstName, LastName, jsonInfo) VALUES " + + "('John', 'Doe', '{\"info\":{\"address\":{\"town\":\"New York\",\"state\":\"US-NY\"}}}'), " + + "('Jane', 'Smith', '{\"info\":{\"address\":{\"town\":\"Los Angeles\",\"state\":\"US-CA\"}}}')"; + dstStmt.executeUpdate(insert); + + String select = "SELECT FirstName, LastName, JSON_VALUE(jsonInfo,'$.info.address.town') AS Town " + + "FROM " + dstTable + " " + + "WHERE JSON_VALUE(jsonInfo,'$.info.address.state') LIKE 'US%' " + + "ORDER BY JSON_VALUE(jsonInfo,'$.info.address.town')"; + try (ResultSet rs = dstStmt.executeQuery(select)) { + assertTrue(rs.next()); + assertEquals("Jane", rs.getString("FirstName")); + assertEquals("Smith", rs.getString("LastName")); + assertEquals("Los Angeles", rs.getString("Town")); + + assertTrue(rs.next()); + assertEquals("John", rs.getString("FirstName")); + assertEquals("Doe", rs.getString("LastName")); + assertEquals("New York", rs.getString("Town")); + } + + } catch (Exception e) { + fail(e.getMessage()); + } finally { + try (Statement stmt = conn.createStatement();) { + TestUtils.dropTableIfExists(dstTable, stmt); + } + } + } + } + + /** + * Test JSON_VALUE function to create computed columns based on the values of + * JSON properties. + * JSON_VALUE() -> Extracts a scalar value from a JSON string. + * input: JSON_VALUE(jsonContent, '$.address[0].longitude') -> + * output: JSON property value of longitude + */ + @Test + @Tag(Constants.JSONTest) + public void testJSONValueComputedColumns() throws SQLException { + String dstTable = TestUtils + .escapeSingleQuotes(AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("dstTable"))); + + try (Connection conn = DriverManager.getConnection(connectionString);) { + try (Statement dstStmt = conn.createStatement()) { + + dstStmt.executeUpdate( + "CREATE TABLE " + dstTable + + " (StoreID INT IDENTITY(1,1) NOT NULL, Address VARCHAR(500), jsonContent NVARCHAR(4000), " + + + "Longitude AS JSON_VALUE(jsonContent, '$.address[0].longitude'), " + + "Latitude AS JSON_VALUE(jsonContent, '$.address[0].latitude'));"); + + String insert = "INSERT INTO " + dstTable + " (Address, jsonContent) VALUES " + + "('123 Main St', '{\"address\":[{\"longitude\":\"-73.935242\",\"latitude\":\"40.730610\"}]}'), " + + + "('456 Elm St', '{\"address\":[{\"longitude\":\"-118.243683\",\"latitude\":\"34.052235\"}]}')"; + dstStmt.executeUpdate(insert); + + String select = "SELECT StoreID, Address, Longitude, Latitude FROM " + dstTable; + try (ResultSet rs = dstStmt.executeQuery(select)) { + assertTrue(rs.next()); + assertEquals(1, rs.getInt("StoreID")); + assertEquals("123 Main St", rs.getString("Address")); + assertEquals("-73.935242", rs.getString("Longitude")); + assertEquals("40.730610", rs.getString("Latitude")); + + assertTrue(rs.next()); + assertEquals(2, rs.getInt("StoreID")); + assertEquals("456 Elm St", rs.getString("Address")); + assertEquals("-118.243683", rs.getString("Longitude")); + assertEquals("34.052235", rs.getString("Latitude")); + } + + } catch (Exception e) { + fail(e.getMessage()); + } finally { + try (Statement stmt = conn.createStatement();) { + TestUtils.dropTableIfExists(dstTable, stmt); + } + } + } + } + + /** + * Test OPENJSON function to parse JSON data. + * OPENJSON -> Parses JSON data and returns a set of rows. + * input: OPENJSON((SELECT jsonCol FROM dstTable WHERE EmployeeID = 17)) -> + * output: Parsed JSON data + */ + @Test + @Tag(Constants.JSONTest) + public void testOpenJsonParseJson() throws SQLException { + String dstTable = TestUtils + .escapeSingleQuotes(AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("dstTable"))); + + try (Connection conn = DriverManager.getConnection(connectionString)) { + try (Statement dstStmt = conn.createStatement()) { + + dstStmt.executeUpdate("CREATE TABLE " + dstTable + " (EmployeeID INT, jsonCol NVARCHAR(MAX));"); + + String jsonData = "{\"id\":1, \"name\":\"John\"}"; + dstStmt.executeUpdate( + "INSERT INTO " + dstTable + " (EmployeeID, jsonCol) VALUES (17, '" + jsonData + "')"); + + String select = "SELECT [key], [value] FROM OPENJSON((SELECT jsonCol FROM " + dstTable + + " WHERE EmployeeID = 17))"; + try (ResultSet rs = dstStmt.executeQuery(select)) { + assertTrue(rs.next()); + assertEquals("id", rs.getString("key")); + assertEquals("1", rs.getString("value")); + assertTrue(rs.next()); + assertEquals("name", rs.getString("key")); + assertEquals("John", rs.getString("value")); + } + } catch (Exception e) { + fail(e.getMessage()); + } finally { + try (Statement stmt = conn.createStatement()) { + TestUtils.dropTableIfExists(dstTable, stmt); + } + } + } + } + + /** + * Test OPENJSON function to parse nested JSON data. + * OPENJSON -> Parses JSON data and returns a set of rows. + * input: OPENJSON((SELECT jsonCol FROM dstTable WHERE EmployeeID = 18), + * '$.person') -> + * output: Parsed nested JSON data + */ + @Test + @Tag(Constants.JSONTest) + public void testOpenJsonParseNestedJson() throws SQLException { + String dstTable = TestUtils + .escapeSingleQuotes(AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("dstTable"))); + + try (Connection conn = DriverManager.getConnection(connectionString)) { + try (Statement dstStmt = conn.createStatement()) { + + dstStmt.executeUpdate("CREATE TABLE " + dstTable + " (EmployeeID INT, jsonCol NVARCHAR(MAX));"); + + String jsonData = "{\"person\": {\"name\": \"John\", \"age\": 30}}"; + dstStmt.executeUpdate( + "INSERT INTO " + dstTable + " (EmployeeID, jsonCol) VALUES (18, '" + jsonData + "')"); + + String select = "SELECT [key], [value] FROM OPENJSON((SELECT jsonCol FROM " + dstTable + + " WHERE EmployeeID = 18), '$.person')"; + try (ResultSet rs = dstStmt.executeQuery(select)) { + assertTrue(rs.next()); + assertEquals("name", rs.getString("key")); + assertEquals("John", rs.getString("value")); + assertTrue(rs.next()); + assertEquals("age", rs.getString("key")); + assertEquals("30", rs.getString("value")); + } + } finally { + try (Statement stmt = conn.createStatement()) { + TestUtils.dropTableIfExists(dstTable, stmt); + } + } + } + } + + /** + * Test OPENJSON function to parse JSON array. + * OPENJSON -> Parses JSON data and returns a set of rows. + * input: OPENJSON((SELECT jsonCol FROM dstTable WHERE EmployeeID = 19), + * '$.colors') -> + * output: Parsed JSON array data + */ + @Test + @Tag(Constants.JSONTest) + public void testOpenJsonParseArray() throws SQLException { + String dstTable = TestUtils + .escapeSingleQuotes(AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("dstTable"))); + + try (Connection conn = DriverManager.getConnection(connectionString)) { + try (Statement dstStmt = conn.createStatement()) { + + dstStmt.executeUpdate("CREATE TABLE " + dstTable + " (EmployeeID INT, jsonCol NVARCHAR(MAX));"); + + String jsonData = "{\"colors\": [\"red\", \"green\", \"blue\"]}"; + dstStmt.executeUpdate( + "INSERT INTO " + dstTable + " (EmployeeID, jsonCol) VALUES (19, '" + jsonData + "')"); + + String select = "SELECT [key], [value] FROM OPENJSON((SELECT jsonCol FROM " + dstTable + + " WHERE EmployeeID = 19), '$.colors')"; + try (ResultSet rs = dstStmt.executeQuery(select)) { + assertTrue(rs.next()); + assertEquals("0", rs.getString("key")); + assertEquals("red", rs.getString("value")); + assertTrue(rs.next()); + assertEquals("1", rs.getString("key")); + assertEquals("green", rs.getString("value")); + assertTrue(rs.next()); + assertEquals("2", rs.getString("key")); + assertEquals("blue", rs.getString("value")); + } + } finally { + try (Statement stmt = conn.createStatement()) { + TestUtils.dropTableIfExists(dstTable, stmt); + } + } + } + } + + /** + * Test JSON insertion and retrieval in a global temporary table. + * Global temporary tables (##TempJson) are shared across sessions and persist + * until all sessions using them close. + */ + @Test + @Tag(Constants.JSONTest) + public void testJsonInsertionInGlobalTempTable() throws SQLException { + String dstTable = TestUtils + .escapeSingleQuotes(AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("##TempJson"))); + try (Connection conn = getConnection()) { + String createTableSQL = "CREATE TABLE " + dstTable + " (id INT PRIMARY KEY, data JSON)"; + String insertSQL = "INSERT INTO " + dstTable + " VALUES (?, ?)"; + String selectSQL = "SELECT data FROM " + dstTable + " WHERE id = ?"; + + try (Statement stmt = conn.createStatement()) { + TestUtils.dropTableIfExists(dstTable, stmt); + stmt.execute(createTableSQL); + } + + try (PreparedStatement pstmt = conn.prepareStatement(insertSQL)) { + pstmt.setInt(1, 1); + pstmt.setString(2, "{\"status\": \"success\", \"code\": 200}"); + pstmt.executeUpdate(); + } + + try (PreparedStatement pstmt = conn.prepareStatement(selectSQL)) { + pstmt.setInt(1, 1); + try (ResultSet rs = pstmt.executeQuery()) { + assertTrue(rs.next()); + String jsonData = rs.getString(1); + assertEquals("{\"status\":\"success\",\"code\":200}", jsonData); + } + } + } finally { + // Ensure cleanup of the global temporary table + try (Connection conn = getConnection(); + Statement stmt = conn.createStatement()) { + TestUtils.dropTableIfExists(dstTable, stmt); + } + } + } + + /** + * Test JSON insertion and retrieval in a local temporary table. + * Local temporary tables (#TempJson) are session-bound and deleted + * automatically when the session ends. + */ + @Test + @Tag(Constants.JSONTest) + public void testJsonInsertionInLocalTempTable() throws SQLException { + try (Connection conn = getConnection()) { + String dstTable = TestUtils + .escapeSingleQuotes(AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("#TempJson"))); + String createTableSQL = "CREATE TABLE " + dstTable + " (id INT PRIMARY KEY, data JSON)"; + String insertSQL = "INSERT INTO " + dstTable + " VALUES (?, ?)"; + String selectSQL = "SELECT data FROM " + dstTable + " WHERE id = ?"; + + try (Statement stmt = conn.createStatement()) { + stmt.execute(createTableSQL); + } + + try (PreparedStatement pstmt = conn.prepareStatement(insertSQL)) { + pstmt.setInt(1, 1); + pstmt.setString(2, "{\"status\": \"success\", \"code\": 200}"); + pstmt.executeUpdate(); + } + + try (PreparedStatement pstmt = conn.prepareStatement(selectSQL)) { + pstmt.setInt(1, 1); + try (ResultSet rs = pstmt.executeQuery()) { + assertTrue(rs.next()); + String jsonData = rs.getString(1); + assertEquals("{\"status\":\"success\",\"code\":200}", jsonData); + } + } + } // Connection auto-closes here, so #TempJson is automatically dropped + } + + /** + * Test `SELECT INTO` query to copy JSON data into a new table. + * `SELECT INTO` creates a new table and inserts the result of the select + * statement. + * input: `SELECT id, data INTO TargetJsonTable FROM SourceJsonTable` + * output: A new table `TargetJsonTable` with copied JSON data. + */ + @Test + @Tag(Constants.JSONTest) + public void testSelectIntoWithJsonType() throws SQLException { + String sourceTable = TestUtils + .escapeSingleQuotes(AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("SourceJsonTable"))); + String targetTable = TestUtils + .escapeSingleQuotes(AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("TargetJsonTable"))); + + try (Connection conn = getConnection()) { + try (Statement stmt = conn.createStatement()) { + TestUtils.dropTableIfExists(targetTable, stmt); + TestUtils.dropTableIfExists(sourceTable, stmt); + + String createSourceTableSQL = "CREATE TABLE " + sourceTable + " (id INT PRIMARY KEY, data JSON)"; + stmt.execute(createSourceTableSQL); + + String insertSQL = "INSERT INTO " + sourceTable + " VALUES (?, ?)"; + try (PreparedStatement pstmt = conn.prepareStatement(insertSQL)) { + pstmt.setInt(1, 1); + pstmt.setString(2, "{\"name\": \"Alice\", \"age\": 25}"); + pstmt.executeUpdate(); + + pstmt.setInt(1, 2); + pstmt.setString(2, "{\"name\": \"Bob\", \"age\": 30}"); + pstmt.executeUpdate(); + } + + // Perform `SELECT INTO` to copy data into TargetJsonTable + String selectIntoSQL = "SELECT id, data INTO " + targetTable + " FROM " + sourceTable; + stmt.execute(selectIntoSQL); + + String selectSQL = "SELECT id, data FROM " + targetTable + " ORDER BY id"; + try (ResultSet rs = stmt.executeQuery(selectSQL)) { + assertTrue(rs.next()); + assertEquals(1, rs.getInt("id")); + assertEquals("{\"name\":\"Alice\",\"age\":25}", rs.getString("data")); + + assertTrue(rs.next()); + assertEquals(2, rs.getInt("id")); + assertEquals("{\"name\":\"Bob\",\"age\":30}", rs.getString("data")); + } + } + } finally { + try (Connection conn = getConnection(); + Statement stmt = conn.createStatement()) { + TestUtils.dropTableIfExists(targetTable, stmt); + TestUtils.dropTableIfExists(sourceTable, stmt); + } + } + } + + /** + * Test `JOIN` query to validate JSON support. + * This test checks if a `JOIN` operation correctly retrieves JSON data + * from multiple tables using a foreign key relationship. + * input: `SELECT u.id, JSON_VALUE(u.data, '$.name'), o.orderDetails FROM + * UsersTable u JOIN OrdersTable o ON u.id = o.userId` + * output: Joined data with extracted JSON fields. + */ + @Test + @Tag(Constants.JSONTest) + public void testJoinQueryWithJsonType() throws SQLException { + String usersTable = TestUtils + .escapeSingleQuotes(AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("UsersTable"))); + String ordersTable = TestUtils + .escapeSingleQuotes(AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("OrdersTable"))); + + try (Connection conn = getConnection()) { + try (Statement stmt = conn.createStatement()) { + TestUtils.dropTableIfExists(ordersTable, stmt); + TestUtils.dropTableIfExists(usersTable, stmt); + + String createUsersTableSQL = "CREATE TABLE " + usersTable + " (id INT PRIMARY KEY, data JSON)"; + stmt.execute(createUsersTableSQL); + + String createOrdersTableSQL = "CREATE TABLE " + ordersTable + + " (orderId INT PRIMARY KEY, userId INT, orderDetails JSON, FOREIGN KEY (userId) REFERENCES " + + usersTable + "(id))"; + stmt.execute(createOrdersTableSQL); + + String insertUserSQL = "INSERT INTO " + usersTable + " VALUES (?, ?)"; + try (PreparedStatement pstmt = conn.prepareStatement(insertUserSQL)) { + pstmt.setInt(1, 1); + pstmt.setString(2, "{\"name\": \"Alice\", \"age\": 25}"); + pstmt.executeUpdate(); + + pstmt.setInt(1, 2); + pstmt.setString(2, "{\"name\": \"Bob\", \"age\": 30}"); + pstmt.executeUpdate(); + } + String insertOrderSQL = "INSERT INTO " + ordersTable + " VALUES (?, ?, ?)"; + try (PreparedStatement pstmt = conn.prepareStatement(insertOrderSQL)) { + pstmt.setInt(1, 101); + pstmt.setInt(2, 1); + pstmt.setString(3, "{\"product\": \"Laptop\", \"price\": 1200}"); + pstmt.executeUpdate(); + + pstmt.setInt(1, 102); + pstmt.setInt(2, 2); + pstmt.setString(3, "{\"product\": \"Phone\", \"price\": 800}"); + pstmt.executeUpdate(); + } + + // Perform `JOIN` to extract JSON values + String joinQuery = "SELECT u.id, JSON_VALUE(u.data, '$.name') AS userName, JSON_VALUE(o.orderDetails, '$.product') AS product " + + + "FROM " + usersTable + " u " + + "JOIN " + ordersTable + " o ON u.id = o.userId " + + "ORDER BY u.id"; + + try (ResultSet rs = stmt.executeQuery(joinQuery)) { + assertTrue(rs.next()); + assertEquals(1, rs.getInt("id")); + assertEquals("Alice", rs.getString("userName")); + assertEquals("Laptop", rs.getString("product")); + + assertTrue(rs.next()); + assertEquals(2, rs.getInt("id")); + assertEquals("Bob", rs.getString("userName")); + assertEquals("Phone", rs.getString("product")); + } + } + } finally { + try (Connection conn = getConnection(); + Statement stmt = conn.createStatement()) { + TestUtils.dropTableIfExists(ordersTable, stmt); + TestUtils.dropTableIfExists(usersTable, stmt); + } + } + } + + /** + * Test JSON input and output with a User-Defined Function (UDF). + * This test ensures that JSON data can be processed via UDFs + * in SELECT, WHERE, and FROM clauses. + * input: UDF `GetAgeFromJson(JSON) RETURNS INT` + * output: Extracted JSON age field in various queries. + */ + @Test + @Tag(Constants.JSONTest) + public void testJsonInputOutputWithUdf() throws SQLException { + String personsTable = TestUtils + .escapeSingleQuotes(AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("Persons"))); + String udfName = "dbo.GetAgeFromJson"; + + try (Connection conn = getConnection()) { + try (Statement stmt = conn.createStatement()) { + TestUtils.dropTableIfExists(personsTable, stmt); + String dropUdfSQL = "IF OBJECT_ID('" + udfName + "', 'FN') IS NOT NULL DROP FUNCTION " + udfName; + stmt.execute(dropUdfSQL); + String createUdfSQL = "CREATE FUNCTION " + udfName + " (@json JSON) " + + "RETURNS INT " + + "AS BEGIN " + + "RETURN CAST(JSON_VALUE(@json, '$.age') AS INT) " + + "END"; + stmt.execute(createUdfSQL); + + String createTableSQL = "CREATE TABLE " + personsTable + " (id INT PRIMARY KEY, data JSON)"; + stmt.execute(createTableSQL); + String insertSQL = "INSERT INTO " + personsTable + " VALUES (?, ?)"; + try (PreparedStatement pstmt = conn.prepareStatement(insertSQL)) { + pstmt.setInt(1, 1); + pstmt.setString(2, "{\"name\": \"Alice\", \"age\": 25}"); + pstmt.executeUpdate(); + + pstmt.setInt(1, 2); + pstmt.setString(2, "{\"name\": \"Bob\", \"age\": 30}"); + pstmt.executeUpdate(); + } + + // Test JSON UDF in SELECT clause + String selectSQL = "SELECT id, " + udfName + "(data) AS extractedAge FROM " + personsTable + + " ORDER BY id"; + try (ResultSet rs = stmt.executeQuery(selectSQL)) { + assertTrue(rs.next()); + assertEquals(1, rs.getInt("id")); + assertEquals(25, rs.getInt("extractedAge")); + + assertTrue(rs.next()); + assertEquals(2, rs.getInt("id")); + assertEquals(30, rs.getInt("extractedAge")); + } + + // Test JSON UDF in WHERE clause + String whereSQL = "SELECT id FROM " + personsTable + " WHERE " + udfName + "(data) > 25 ORDER BY id"; + try (ResultSet rs = stmt.executeQuery(whereSQL)) { + assertTrue(rs.next()); + assertEquals(2, rs.getInt("id")); + } + + // Test JSON UDF in FROM clause (as part of a subquery) + String fromSQL = "SELECT extractedAge FROM (SELECT " + udfName + "(data) AS extractedAge FROM " + + personsTable + ") AS AgeTable"; + try (ResultSet rs = stmt.executeQuery(fromSQL)) { + assertTrue(rs.next()); + assertEquals(25, rs.getInt("extractedAge")); + + assertTrue(rs.next()); + assertEquals(30, rs.getInt("extractedAge")); + } + } + } finally { + try (Connection conn = getConnection(); + Statement stmt = conn.createStatement()) { + TestUtils.dropFunctionIfExists(udfName, stmt); + TestUtils.dropTableIfExists(personsTable, stmt); + } + } + } + + /** + * Test a User-Defined Function (UDF) that returns JSON data. + * This test ensures that the UDF can be used in SELECT queries + * to return JSON-formatted results. + * input: UDF `GetPersonJson(INT, NVARCHAR(100)) RETURNS JSON` + * output: JSON object with id and name fields. + */ + @Test + @Tag(Constants.JSONTest) + public void testUdfReturningJson() throws SQLException { + String personsTable = TestUtils + .escapeSingleQuotes(AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("Persons"))); + String udfName = "dbo.GetPersonJson"; + + try (Connection conn = getConnection()) { + try (Statement stmt = conn.createStatement()) { + TestUtils.dropTableIfExists(personsTable, stmt); + String dropUdfSQL = "IF OBJECT_ID('" + udfName + "', 'FN') IS NOT NULL DROP FUNCTION " + udfName; + stmt.execute(dropUdfSQL); + + String createUdfSQL = "CREATE FUNCTION " + udfName + " (@id INT, @name NVARCHAR(100)) " + + "RETURNS JSON " + + "AS BEGIN " + + "RETURN JSON_QUERY('{\"id\": ' + CAST(@id AS NVARCHAR) + ', \"name\": \"' + @name + '\"}') " + + "END"; + stmt.execute(createUdfSQL); + String createTableSQL = "CREATE TABLE " + personsTable + " (id INT PRIMARY KEY, name NVARCHAR(100))"; + stmt.execute(createTableSQL); + + String insertSQL = "INSERT INTO " + personsTable + " VALUES (?, ?)"; + try (PreparedStatement pstmt = conn.prepareStatement(insertSQL)) { + pstmt.setInt(1, 1); + pstmt.setString(2, "Alice"); + pstmt.executeUpdate(); + + pstmt.setInt(1, 2); + pstmt.setString(2, "Bob"); + pstmt.executeUpdate(); + } + + String selectSQL = "SELECT id, name, " + udfName + "(id, name) AS personJson FROM " + personsTable + + " ORDER BY id"; + try (ResultSet rs = stmt.executeQuery(selectSQL)) { + assertTrue(rs.next()); + assertEquals(1, rs.getInt("id")); + assertEquals("Alice", rs.getString("name")); + assertEquals("{\"id\":1,\"name\":\"Alice\"}", rs.getString("personJson")); + + assertTrue(rs.next()); + assertEquals(2, rs.getInt("id")); + assertEquals("Bob", rs.getString("name")); + assertEquals("{\"id\":2,\"name\":\"Bob\"}", rs.getString("personJson")); + } + } + } finally { + try (Connection conn = getConnection(); + Statement stmt = conn.createStatement()) { + TestUtils.dropFunctionIfExists(udfName, stmt); + TestUtils.dropTableIfExists(personsTable, stmt); + } + } + } + + /* + * Test inserting a 1 GB JSON file into a table. + * And verify there is no data loss. + */ + @Test + @Tag(Constants.JSONTest) + public void testInsert1GBJson() throws SQLException, IOException { + String dstTable = TestUtils + .escapeSingleQuotes(AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("dstTable"))); + + Path tempFile = Files.createTempFile("json_output", ".json"); + + try (Connection conn = DriverManager.getConnection(connectionString); + Statement stmt = conn.createStatement()) { + + stmt.executeUpdate("CREATE TABLE " + dstTable + " (jsonColumn JSON);"); + + generateHugeJsonFile(1L * 1024 * 1024 * 1024); // 1GB JSON file + + try (PreparedStatement pstmt = conn + .prepareStatement("INSERT INTO " + dstTable + " (jsonColumn) VALUES (?)"); + FileReader reader = new FileReader(JSON_FILE_PATH)) { + pstmt.setCharacterStream(1, reader); + pstmt.executeUpdate(); + } + + try (PreparedStatement pstmt = conn.prepareStatement("SELECT jsonColumn FROM " + dstTable); + ResultSet rs = pstmt.executeQuery()) { + + assertTrue(rs.next()); + Clob jsonClob = rs.getClob(1); + + try (Reader clobReader = jsonClob.getCharacterStream(); + BufferedWriter writer = Files.newBufferedWriter(tempFile, StandardCharsets.UTF_8)) { + + char[] buffer = new char[1024]; + int charsRead; + while ((charsRead = clobReader.read(buffer)) != -1) { + writer.write(buffer, 0, charsRead); + } + } + } + + assertTrue(Files.mismatch(Path.of(JSON_FILE_PATH), tempFile) == -1); + + } catch (Exception e) { + fail("Test failed due to: " + e.getMessage()); + } finally { + try (Connection conn = DriverManager.getConnection(connectionString); + Statement stmt = conn.createStatement()) { + TestUtils.dropTableIfExists(dstTable, stmt); + } + Files.deleteIfExists(tempFile); + } + } + + /* + * Test inserting a 1.98 GB JSON file into a table. + * And verify there is no data loss. + * Note: This test took around 4 mins to run + */ + @Test + @Tag(Constants.JSONTest) + public void testInsertHugeJsonData() throws SQLException, IOException { + String dstTable = TestUtils + .escapeSingleQuotes(AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("dstTable"))); + + Path tempFile = Files.createTempFile("json_output", ".json"); + + try (Connection conn = DriverManager.getConnection(connectionString); + Statement stmt = conn.createStatement()) { + + stmt.executeUpdate("CREATE TABLE " + dstTable + " (jsonColumn JSON);"); + + generateHugeJsonFile(2L * 1024 * 1024 * 1015); // 1.98GB JSON file + + try (PreparedStatement pstmt = conn + .prepareStatement("INSERT INTO " + dstTable + " (jsonColumn) VALUES (?)"); + FileReader reader = new FileReader(JSON_FILE_PATH)) { + pstmt.setCharacterStream(1, reader); + pstmt.executeUpdate(); + } + + try (PreparedStatement pstmt = conn.prepareStatement("SELECT jsonColumn FROM " + dstTable); + ResultSet rs = pstmt.executeQuery()) { + + assertTrue(rs.next()); + Clob jsonClob = rs.getClob(1); + + try (Reader clobReader = jsonClob.getCharacterStream(); + BufferedWriter writer = Files.newBufferedWriter(tempFile, StandardCharsets.UTF_8)) { + + char[] buffer = new char[1024]; + int charsRead; + while ((charsRead = clobReader.read(buffer)) != -1) { + writer.write(buffer, 0, charsRead); + } + } + } + + assertTrue(Files.mismatch(Path.of(JSON_FILE_PATH), tempFile) == -1); + + } catch (Exception e) { + fail("Test failed due to: " + e.getMessage()); + } finally { + try (Connection conn = DriverManager.getConnection(connectionString); + Statement stmt = conn.createStatement()) { + TestUtils.dropTableIfExists(dstTable, stmt); + } + Files.deleteIfExists(tempFile); + } + } + + /* + * Test inserting around 2 GB JSON file into a table. + * Note: This test is expected to fail due to the maximum allowed size for a LOB. + * The test is designed to validate the error handling for large JSON data. + * Expected error -> org.opentest4j.AssertionFailedError: Test failed due to: Attempting to grow LOB beyond maximum allowed size of 216895848447 bytes. + */ + @Test + @Tag(Constants.JSONTest) + public void testInsert2GBData() throws SQLException, FileNotFoundException, IOException { + String dstTable = TestUtils + .escapeSingleQuotes(AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("dstTable"))); + + try (Connection conn = DriverManager.getConnection(connectionString); + Statement stmt = conn.createStatement()) { + + stmt.executeUpdate("CREATE TABLE " + dstTable + " (jsonColumn JSON);"); + + generateHugeJsonFile(2L * 1024 * 1024 * 1022); // ~2 GB JSON file + + try (PreparedStatement pstmt = conn + .prepareStatement("INSERT INTO " + dstTable + " (jsonColumn) VALUES (?)"); + FileReader reader = new FileReader(JSON_FILE_PATH)) { + + pstmt.setCharacterStream(1, reader); + pstmt.executeUpdate(); + fail("Expected an exception due to exceeding the maximum allowed size for a LOB."); + } catch (SQLException e) { + assertTrue(e.getMessage().contains("Attempting to grow LOB beyond maximum allowed size")); + } + + } finally { + try (Connection conn = DriverManager.getConnection(connectionString); + Statement stmt = conn.createStatement()) { + TestUtils.dropTableIfExists(dstTable, stmt); + } + } + } + + private void generateHugeJsonFile(long targetSize) { + File file = new File(JSON_FILE_PATH); + try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) { + writer.write("{\"data\":["); + + long currentSize = 10; + boolean firstGroup = true; + + while (currentSize < targetSize - 10) { + if (!firstGroup) { + writer.write(","); + } + writer.write("{\"group\":["); + + boolean firstElement = true; + for (int i = 0; i < 500; i++) { + if (!firstElement) { + writer.write(","); + } + String jsonChunk = "{\"value\":\"" + "a".repeat(1000) + "\"}"; + writer.write(jsonChunk); + currentSize += jsonChunk.length(); + firstElement = false; + } + + writer.write("]}"); + firstGroup = false; + } + + writer.write("]}"); + } catch (IOException e) { + fail("Failed to create large JSON file: " + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyTest.java index c1917fd68..4bbeafb0e 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/preparedStatement/BatchExecutionWithBulkCopyTest.java @@ -803,6 +803,122 @@ public void testComputedCols() throws Exception { } } + /** + * Test inserting complex JSON data using prepared statement with bulk copy enabled. + */ + @Test + @Tag(Constants.JSONTest) + public void testInsertJsonWithBulkCopy() throws Exception { + String tableName = RandomUtil.getIdentifier("BulkCopyComplexJsonTest"); + String valid = "insert into " + AbstractSQLGenerator.escapeIdentifier(tableName) + " (jsonCol) values (?)"; + + try (Connection connection = PrepUtil.getConnection(connectionString + ";useBulkCopyForBatchInsert=true;"); + SQLServerPreparedStatement pstmt = (SQLServerPreparedStatement) connection.prepareStatement(valid); + Statement stmt = (SQLServerStatement) connection.createStatement()) { + + TestUtils.dropTableIfExists(AbstractSQLGenerator.escapeIdentifier(tableName), stmt); + String createTable = "create table " + AbstractSQLGenerator.escapeIdentifier(tableName) + " (jsonCol JSON)"; + stmt.execute(createTable); + + String complexJsonData = "{" + + "\"name\":\"John\"," + + "\"age\":30," + + "\"address\":{" + + "\"street\":\"123 Main St\"," + + "\"city\":\"New York\"," + + "\"zipcode\":\"10001\"" + + "}," + + "\"phoneNumbers\":[" + + "{\"type\":\"home\",\"number\":\"212-555-1234\"}," + + "{\"type\":\"work\",\"number\":\"646-555-4567\"}" + + "]," + + "\"children\":[" + + "{\"name\":\"Jane\",\"age\":10}," + + "{\"name\":\"Doe\",\"age\":8}" + + "]" + + "}"; + + pstmt.setString(1, complexJsonData); + pstmt.addBatch(); + pstmt.executeBatch(); + + try (ResultSet rs = stmt.executeQuery("select jsonCol from " + AbstractSQLGenerator.escapeIdentifier(tableName))) { + assertTrue(rs.next()); + assertEquals(complexJsonData, rs.getString(1)); + } + } finally { + try (Statement stmt = connection.createStatement()) { + TestUtils.dropTableIfExists(AbstractSQLGenerator.escapeIdentifier(tableName), stmt); + } + } + } + + /** + * Test select, update, create, and delete operations on JSON data and verify at each step. + */ + @Test + @Tag(Constants.JSONTest) + public void testCRUDOperationsWithJson() throws Exception { + String tableName = RandomUtil.getIdentifier("CRUDJsonTest"); + String createTableSQL = "CREATE TABLE " + AbstractSQLGenerator.escapeIdentifier(tableName) + " (id INT PRIMARY KEY, jsonCol JSON)"; + String insertSQL = "INSERT INTO " + AbstractSQLGenerator.escapeIdentifier(tableName) + " (id, jsonCol) VALUES (?, ?)"; + String selectSQL = "SELECT jsonCol FROM " + AbstractSQLGenerator.escapeIdentifier(tableName) + " WHERE id = ?"; + String updateSQL = "UPDATE " + AbstractSQLGenerator.escapeIdentifier(tableName) + " SET jsonCol = ? WHERE id = ?"; + String deleteSQL = "DELETE FROM " + AbstractSQLGenerator.escapeIdentifier(tableName) + " WHERE id = ?"; + + try (Connection connection = PrepUtil.getConnection(connectionString + ";useBulkCopyForBatchInsert=true;"); + Statement stmt = (SQLServerStatement) connection.createStatement()) { + + stmt.execute(createTableSQL); + + String initialJsonData = "{\"name\":\"John\",\"age\":30}"; + try (PreparedStatement pstmt = connection.prepareStatement(insertSQL)) { + pstmt.setInt(1, 1); + pstmt.setString(2, initialJsonData); + pstmt.executeUpdate(); + } + + try (PreparedStatement pstmt = connection.prepareStatement(selectSQL)) { + pstmt.setInt(1, 1); + try (ResultSet rs = pstmt.executeQuery()) { + assertTrue(rs.next()); + assertEquals(initialJsonData, rs.getString(1)); + } + } + + String updatedJsonData = "{\"name\":\"Jane\",\"age\":25}"; + try (PreparedStatement pstmt = connection.prepareStatement(updateSQL)) { + pstmt.setString(1, updatedJsonData); + pstmt.setInt(2, 1); + pstmt.executeUpdate(); + } + + try (PreparedStatement pstmt = connection.prepareStatement(selectSQL)) { + pstmt.setInt(1, 1); + try (ResultSet rs = pstmt.executeQuery()) { + assertTrue(rs.next()); + assertEquals(updatedJsonData, rs.getString(1)); + } + } + + try (PreparedStatement pstmt = connection.prepareStatement(deleteSQL)) { + pstmt.setInt(1, 1); + pstmt.executeUpdate(); + } + + try (PreparedStatement pstmt = connection.prepareStatement(selectSQL)) { + pstmt.setInt(1, 1); + try (ResultSet rs = pstmt.executeQuery()) { + assertFalse(rs.next()); + } + } + } finally { + try (Statement stmt = connection.createStatement()) { + TestUtils.dropTableIfExists(AbstractSQLGenerator.escapeIdentifier(tableName), stmt); + } + } + } + /** * Test bulk insert with no space after table name * 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..8e59e4436 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/resultset/ResultSetTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/resultset/ResultSetTest.java @@ -16,6 +16,7 @@ import java.sql.Blob; import java.sql.Clob; import java.sql.Connection; +import java.sql.DriverManager; import java.sql.NClob; import java.sql.ResultSet; import java.sql.SQLException; @@ -32,6 +33,7 @@ import com.microsoft.sqlserver.jdbc.SQLServerConnection; import com.microsoft.sqlserver.jdbc.SQLServerException; +import com.microsoft.sqlserver.jdbc.SQLServerResultSet; import com.microsoft.sqlserver.jdbc.TestResource; import com.microsoft.sqlserver.testframework.PrepUtil; import org.junit.jupiter.api.AfterEach; @@ -49,7 +51,6 @@ import com.microsoft.sqlserver.testframework.AbstractTest; import com.microsoft.sqlserver.testframework.Constants; - @RunWith(JUnitPlatform.class) public class ResultSetTest extends AbstractTest { private static final String tableName = RandomUtil.getIdentifier("StatementParam"); @@ -94,13 +95,14 @@ public void cleanUp() throws Exception { */ @Test @Tag(Constants.xAzureSQLDW) + @Tag(Constants.JSONTest) public void testJdbc41ResultSetMethods() throws SQLException { try (Connection con = getConnection(); Statement stmt = con.createStatement()) { stmt.executeUpdate("create table " + AbstractSQLGenerator.escapeIdentifier(tableName) + " ( " + "col1 int, " + "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 +122,14 @@ 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,6 +227,9 @@ 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()); @@ -284,6 +291,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 { @@ -709,6 +719,43 @@ public void testResultSetClientCursorInitializerSqlErrorState() { } } + /** + * Test casting JSON data and retrieving it as various data types. + */ + @Test + @Tag(Constants.JSONTest) + public void testCastOnJSON() throws SQLException { + String dstTable = TestUtils + .escapeSingleQuotes(AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("dstTable"))); + + String jsonData = "{\"key\":\"123\"}"; + + try (Connection conn = DriverManager.getConnection(connectionString)) { + try (Statement stmt = conn.createStatement()) { + stmt.executeUpdate("CREATE TABLE " + dstTable + " (jsonData JSON)"); + stmt.executeUpdate("INSERT INTO " + dstTable + " VALUES (CAST('" + jsonData + "' AS JSON))"); + + String select = "SELECT JSON_VALUE(jsonData, '$.key') AS c1 FROM " + dstTable; + + try (SQLServerResultSet rs = (SQLServerResultSet) stmt.executeQuery(select)) { + rs.next(); + assertEquals(123, rs.getShort("c1")); + assertEquals(123, rs.getInt("c1")); + assertEquals(123f, rs.getFloat("c1")); + assertEquals(123L, rs.getLong("c1")); + assertEquals(123d, rs.getDouble("c1")); + assertEquals(new BigDecimal(123), rs.getBigDecimal("c1")); + } + } catch (Exception e) { + fail(e.getMessage()); + } finally { + try (Statement stmt = conn.createStatement();) { + TestUtils.dropTableIfExists(dstTable, stmt); + } + } + } + } + private void ambiguousUpdateRowTestSetup(Connection conn) throws SQLException { try (Statement stmt = conn.createStatement()) { stmt.execute("CREATE TABLE " + tableName1 + " (i INT, data VARCHAR(30))"); 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..0a3ebd12f 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,37 @@ public void testXML() throws SQLException { } } + /** + * Test JSON support + * + * @throws SQLException + */ + @Test + @Tag(Constants.JSONTest) + 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("c1"), value); + } + } + } + /** * Test ntext support * @@ -349,6 +380,39 @@ public void testTVPXMLStoredProcedure() throws SQLException { } } + /** + * JSON with StoredProcedure + * + * @throws SQLException + */ + @Test + @Tag(Constants.JSONTest) + 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 +757,34 @@ public String toString() { } } } + + @Test + @Tag(Constants.JSONTest) + 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..f6cdc7a1b 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,75 @@ public void testXmlQuery() throws SQLException { } } + /** + * Tests Json query + * + * @throws SQLException + */ + @Test + @Tag(Constants.JSONTest) + 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.addBatch(); + + pstmt.setInt(1, pkRow2); + pstmt.setObject(2, "{\"key21\":\"value21\"}"); + pstmt.setObject(3, "{\"key22\":\"value22\"}"); + pstmt.addBatch(); + + pstmt.setInt(1, pkRow3); + pstmt.setObject(2, "{\"key31\":\"value31\"}"); + pstmt.setObject(3, "{\"key32\":\"value32\"}"); + pstmt.addBatch(); + + pstmt.executeBatch(); + } + + 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/AbstractTest.java b/src/test/java/com/microsoft/sqlserver/testframework/AbstractTest.java index eda7b8847..ad04eae26 100644 --- a/src/test/java/com/microsoft/sqlserver/testframework/AbstractTest.java +++ b/src/test/java/com/microsoft/sqlserver/testframework/AbstractTest.java @@ -158,8 +158,7 @@ public static void setup() throws Exception { // no config file used } - connectionString = getConfiguredPropertyOrEnv(Constants.MSSQL_JDBC_TEST_CONNECTION_PROPERTIES); - + connectionString = getConfiguredPropertyOrEnv(Constants.MSSQL_JDBC_TEST_CONNECTION_PROPERTIES); applicationClientID = getConfiguredProperty("applicationClientID"); applicationKey = getConfiguredProperty("applicationKey"); tenantID = getConfiguredProperty("tenantID"); diff --git a/src/test/java/com/microsoft/sqlserver/testframework/Constants.java b/src/test/java/com/microsoft/sqlserver/testframework/Constants.java index 3009d2213..733ff07f1 100644 --- a/src/test/java/com/microsoft/sqlserver/testframework/Constants.java +++ b/src/test/java/com/microsoft/sqlserver/testframework/Constants.java @@ -29,6 +29,7 @@ private Constants() {} * reqExternalSetup - For tests requiring external setup * clientCertAuth - - For tests requiring client certificate authentication setup * Fedauth - - - - - - For Fedauth tests + * JSONTest - - - - - For tests requiring JSON setup * */ public static final String xJDBC42 = "xJDBC42"; @@ -48,6 +49,7 @@ private Constants() {} public static final String clientCertAuth = "clientCertAuth"; public static final String fedAuth = "fedAuth"; public static final String requireSecret = "requireSecret"; + public static final String JSONTest = "JSONTest"; public static final ThreadLocalRandom RANDOM = ThreadLocalRandom.current(); public static final Logger LOGGER = Logger.getLogger("AbstractTest"); 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/BulkCopyCSVTestInputWithJson.csv b/src/test/resources/BulkCopyCSVTestInputWithJson.csv new file mode 100644 index 000000000..f905e9b66 --- /dev/null +++ b/src/test/resources/BulkCopyCSVTestInputWithJson.csv @@ -0,0 +1,3 @@ +0,testing,"{""age"": 25, ""address"": {""pincode"": 123456, ""state"": ""NY""}}" +1,test },"{""age"": 25, ""city"": ""Los Angeles""}" +0,test {0},"{""age"": 40, ""city"": ""Chicago""}" \ No newline at end of file