diff --git a/doc/snippets/Microsoft.Data/SqlDbTypeExtensions.xml b/doc/snippets/Microsoft.Data/SqlDbTypeExtensions.xml new file mode 100644 index 0000000000..a3d03229f5 --- /dev/null +++ b/doc/snippets/Microsoft.Data/SqlDbTypeExtensions.xml @@ -0,0 +1,24 @@ + + + + + Extensions for SqlDbType. + + + class provides enums which will be added. These are meant to be used by applications which cannot leverage the enums in available in newer .NET runtime versions. + +]]> + + + + + Gets the enum value for the JSON datatype. + + enum value for JSON datatype. + + + + diff --git a/src/Microsoft.Data.SqlClient.sln b/src/Microsoft.Data.SqlClient.sln index f7efd31443..88d02dee48 100644 --- a/src/Microsoft.Data.SqlClient.sln +++ b/src/Microsoft.Data.SqlClient.sln @@ -73,6 +73,7 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Microsoft.Data", "Microsoft.Data", "{908C7DD3-C999-40A6-9433-9F5ACA7C36F5}" ProjectSection(SolutionItems) = preProject ..\doc\snippets\Microsoft.Data\OperationAbortedException.xml = ..\doc\snippets\Microsoft.Data\OperationAbortedException.xml + ..\doc\snippets\Microsoft.Data\SqlDbTypeExtensions.xml = ..\doc\snippets\Microsoft.Data\SqlDbTypeExtensions.xml EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Microsoft.Data.Sql", "Microsoft.Data.Sql", "{0CE216CE-8072-4985-B248-61F0D0BE9C2E}" diff --git a/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs b/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs index 5dfbf8af3f..cc836fa92f 100644 --- a/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs +++ b/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs @@ -5,8 +5,6 @@ // NOTE: The current Microsoft.VSDesigner editor attributes are implemented for System.Data.SqlClient, and are not publicly available. // New attributes that are designed to work with Microsoft.Data.SqlClient and are publicly documented should be included in future. -using System; - [assembly: System.CLSCompliant(true)] namespace Microsoft.Data { @@ -15,7 +13,15 @@ public sealed partial class OperationAbortedException : System.SystemException { internal OperationAbortedException() { } } + + /// + public static class SqlDbTypeExtensions + { + /// + public const System.Data.SqlDbType Json = (System.Data.SqlDbType)35; + } } + namespace Microsoft.Data.Sql { /// diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj index b9f6de4113..3bc178e7ae 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj @@ -962,7 +962,7 @@ - + diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.cs index 91d6e3e744..01e693ac50 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.cs @@ -6466,7 +6466,7 @@ internal string BuildParamList(TdsParser parser, SqlParameterCollection paramete paramList.Append(size); paramList.Append(')'); } - else if (mt.IsPlp && (mt.SqlDbType != SqlDbType.Xml) && (mt.SqlDbType != SqlDbType.Udt)) + else if (mt.IsPlp && (mt.SqlDbType != SqlDbType.Xml) && (mt.SqlDbType != SqlDbType.Udt) && (mt.SqlDbType != SqlDbTypeExtensions.Json)) { paramList.Append("(max) "); } diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs index edeecb6c9f..7cefa4c2ec 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -5174,7 +5174,7 @@ private TdsOperationStatus TryProcessTypeInfo(TdsParserStateObject stateObj, Sql } // read the collation for 7.x servers - if (col.metaType.IsCharType && (tdsType != TdsEnums.SQLXMLTYPE)) + if (col.metaType.IsCharType && (tdsType != TdsEnums.SQLXMLTYPE) && ((tdsType != TdsEnums.SQLJSON))) { result = TryProcessCollation(stateObj, out col.collation); if (result != TdsOperationStatus.Done) @@ -5952,6 +5952,7 @@ private TdsOperationStatus TryReadSqlStringValue(SqlBuffer value, byte type, int case TdsEnums.SQLVARCHAR: case TdsEnums.SQLBIGVARCHAR: case TdsEnums.SQLTEXT: + case TdsEnums.SQLJSON: // If bigvarchar(max), we only read the first chunk here, // expecting the caller to read the rest if (encoding == null) @@ -6420,6 +6421,7 @@ internal TdsOperationStatus TryReadSqlValue(SqlBuffer value, SqlMetaDataPriv md, case TdsEnums.SQLNCHAR: case TdsEnums.SQLNVARCHAR: case TdsEnums.SQLNTEXT: + case TdsEnums.SQLJSON: result = TryReadSqlStringValue(value, tdsType, length, md.encoding, isPlp, stateObj); if (result != TdsOperationStatus.Done) { @@ -7957,6 +7959,7 @@ internal TdsOperationStatus TryGetDataLength(SqlMetaDataPriv colmeta, TdsParserS colmeta.tdsType == TdsEnums.SQLBIGVARCHAR || colmeta.tdsType == TdsEnums.SQLBIGVARBINARY || colmeta.tdsType == TdsEnums.SQLNVARCHAR || + colmeta.tdsType == TdsEnums.SQLJSON || // Large UDTs is WinFS-only colmeta.tdsType == TdsEnums.SQLUDT, "GetDataLength:Invalid streaming datatype"); @@ -9817,7 +9820,7 @@ private Task TDSExecuteRPCAddParameter(TdsParserStateObject stateObj, SqlParamet } else if (mt.IsPlp) { - if (mt.SqlDbType != SqlDbType.Xml) + if (mt.SqlDbType != SqlDbType.Xml && mt.SqlDbType != SqlDbTypeExtensions.Json) WriteShort(TdsEnums.SQL_USHORTVARMAXLEN, stateObj); } else if ((!mt.IsVarTime) && (mt.SqlDbType != SqlDbType.Date)) @@ -9857,53 +9860,56 @@ private Task TDSExecuteRPCAddParameter(TdsParserStateObject stateObj, SqlParamet // write out collation or xml metadata - if (_is2005 && (mt.SqlDbType == SqlDbType.Xml)) + if ((mt.SqlDbType == SqlDbType.Xml || mt.SqlDbType == SqlDbTypeExtensions.Json)) { - if (!string.IsNullOrEmpty(param.XmlSchemaCollectionDatabase) || - !string.IsNullOrEmpty(param.XmlSchemaCollectionOwningSchema) || - !string.IsNullOrEmpty(param.XmlSchemaCollectionName)) + if (mt.SqlDbType == SqlDbType.Xml) { - stateObj.WriteByte(1); //Schema present flag - - if (!string.IsNullOrEmpty(param.XmlSchemaCollectionDatabase)) + if (!string.IsNullOrEmpty(param.XmlSchemaCollectionDatabase) || + !string.IsNullOrEmpty(param.XmlSchemaCollectionOwningSchema) || + !string.IsNullOrEmpty(param.XmlSchemaCollectionName)) { - tempLen = (param.XmlSchemaCollectionDatabase).Length; - stateObj.WriteByte((byte)(tempLen)); - WriteString(param.XmlSchemaCollectionDatabase, tempLen, 0, stateObj); - } - else - { - stateObj.WriteByte(0); // No dbname - } + stateObj.WriteByte(1); //Schema present flag - if (!string.IsNullOrEmpty(param.XmlSchemaCollectionOwningSchema)) - { - tempLen = (param.XmlSchemaCollectionOwningSchema).Length; - stateObj.WriteByte((byte)(tempLen)); - WriteString(param.XmlSchemaCollectionOwningSchema, tempLen, 0, stateObj); - } - else - { - stateObj.WriteByte(0); // no xml schema name - } + if (!string.IsNullOrEmpty(param.XmlSchemaCollectionDatabase)) + { + tempLen = (param.XmlSchemaCollectionDatabase).Length; + stateObj.WriteByte((byte)(tempLen)); + WriteString(param.XmlSchemaCollectionDatabase, tempLen, 0, stateObj); + } + else + { + stateObj.WriteByte(0); // No dbname + } + + if (!string.IsNullOrEmpty(param.XmlSchemaCollectionOwningSchema)) + { + tempLen = (param.XmlSchemaCollectionOwningSchema).Length; + stateObj.WriteByte((byte)(tempLen)); + WriteString(param.XmlSchemaCollectionOwningSchema, tempLen, 0, stateObj); + } + else + { + stateObj.WriteByte(0); // no xml schema name + } + if (!string.IsNullOrEmpty(param.XmlSchemaCollectionName)) + { + tempLen = (param.XmlSchemaCollectionName).Length; + WriteShort((short)(tempLen), stateObj); + WriteString(param.XmlSchemaCollectionName, tempLen, 0, stateObj); + } + else + { + WriteShort(0, stateObj); // No xml schema collection name + } - if (!string.IsNullOrEmpty(param.XmlSchemaCollectionName)) - { - tempLen = (param.XmlSchemaCollectionName).Length; - WriteShort((short)(tempLen), stateObj); - WriteString(param.XmlSchemaCollectionName, tempLen, 0, stateObj); } else { - WriteShort(0, stateObj); // No xml schema collection name + stateObj.WriteByte(0); // No schema } } - else - { - stateObj.WriteByte(0); // No schema - } } - else if (mt.IsCharType) + else if (mt.IsCharType && mt.SqlDbType != SqlDbTypeExtensions.Json) { // if it is not supplied, simply write out our default collation, otherwise, write out the one attached to the parameter SqlCollation outCollation = (param.Collation != null) ? param.Collation : _defaultCollation; @@ -11478,10 +11484,18 @@ private Task WriteUnterminatedSqlValue(object value, MetaType type, int actualLe case TdsEnums.SQLNVARCHAR: case TdsEnums.SQLNTEXT: case TdsEnums.SQLXMLTYPE: + case TdsEnums.SQLJSON: if (type.IsPlp) { - if (IsBOMNeeded(type, value)) + if (type.NullableType == TdsEnums.SQLJSON) + { + // TODO : Performance and BOM check. For more details see https://github.com/dotnet/SqlClient/issues/2852 + byte[] jsonAsBytes = Encoding.UTF8.GetBytes(value.ToString()); + WriteInt(jsonAsBytes.Length, stateObj); + return stateObj.WriteByteArray(jsonAsBytes, jsonAsBytes.Length, 0, canAccumulate: false); + } + else if (IsBOMNeeded(type, value)) { WriteInt(actualLength + 2, stateObj); // chunk length WriteShort(TdsEnums.XMLUNICODEBOM, stateObj); @@ -12128,6 +12142,7 @@ private Task WriteUnterminatedValue(object value, MetaType type, byte scale, int case TdsEnums.SQLNVARCHAR: case TdsEnums.SQLNTEXT: case TdsEnums.SQLXMLTYPE: + case TdsEnums.SQLJSON: { Debug.Assert(!isDataFeed || (value is TextDataFeed || value is XmlDataFeed), "Value must be a TextReader or XmlReader"); Debug.Assert(isDataFeed || (value is string || value is byte[]), "Value is a byte array or string"); @@ -12149,7 +12164,14 @@ private Task WriteUnterminatedValue(object value, MetaType type, byte scale, int { if (type.IsPlp) { - if (IsBOMNeeded(type, value)) + if (type.NullableType == TdsEnums.SQLJSON) + { + // TODO : Performance and BOM check. Saurabh + byte[] jsonAsBytes = Encoding.UTF8.GetBytes((string)value); + WriteInt(jsonAsBytes.Length, stateObj); + return stateObj.WriteByteArray(jsonAsBytes, jsonAsBytes.Length, 0, canAccumulate: false); + } + else if (IsBOMNeeded(type, value)) { WriteInt(actualLength + 2, stateObj); // chunk length WriteShort(TdsEnums.XMLUNICODEBOM, stateObj); @@ -12655,7 +12677,7 @@ internal void WriteParameterVarLen(MetaType type, int size, bool isNull, TdsPars WriteInt(unchecked((int)TdsEnums.VARLONGNULL), stateObj); } } - else if (type.NullableType == TdsEnums.SQLXMLTYPE || unknownLength) + else if (type.NullableType is TdsEnums.SQLXMLTYPE or TdsEnums.SQLJSON || unknownLength) { WriteUnsignedLong(TdsEnums.SQL_PLP_UNKNOWNLEN, stateObj); } diff --git a/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs b/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs index d8bbb83106..421dc60d67 100644 --- a/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs +++ b/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs @@ -18,7 +18,15 @@ internal OperationAbortedException() { } private OperationAbortedException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) { } } + + /// + public static class SqlDbTypeExtensions + { + /// + public const System.Data.SqlDbType Json = (System.Data.SqlDbType)35; + } } + namespace Microsoft.Data.Sql { /// diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj index 730a747261..3e0139f31b 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj @@ -766,7 +766,7 @@ - + $(SystemTextEncodingsWebVersion) diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.cs index ba860f106f..6745a0b56a 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.cs @@ -7173,7 +7173,7 @@ internal string BuildParamList(TdsParser parser, SqlParameterCollection paramete paramList.Append(size); paramList.Append(')'); } - else if (mt.IsPlp && (mt.SqlDbType != SqlDbType.Xml) && (mt.SqlDbType != SqlDbType.Udt)) + else if (mt.IsPlp && (mt.SqlDbType != SqlDbType.Xml) && (mt.SqlDbType != SqlDbType.Udt) && (mt.SqlDbType != SqlDbTypeExtensions.Json)) { paramList.Append("(max) "); } diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs index f9b1306e42..4fd8ab7c68 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -5874,7 +5874,7 @@ private TdsOperationStatus TryProcessTypeInfo(TdsParserStateObject stateObj, Sql } // read the collation for 7.x servers - if (_is2000 && col.metaType.IsCharType && (tdsType != TdsEnums.SQLXMLTYPE)) + if (_is2000 && col.metaType.IsCharType && (tdsType != TdsEnums.SQLXMLTYPE) && (tdsType != TdsEnums.SQLJSON)) { result = TryProcessCollation(stateObj, out col.collation); if (result != TdsOperationStatus.Done) @@ -6801,6 +6801,7 @@ private TdsOperationStatus TryReadSqlStringValue(SqlBuffer value, byte type, int case TdsEnums.SQLVARCHAR: case TdsEnums.SQLBIGVARCHAR: case TdsEnums.SQLTEXT: + case TdsEnums.SQLJSON: // If bigvarchar(max), we only read the first chunk here, // expecting the caller to read the rest if (encoding == null) @@ -7251,6 +7252,7 @@ internal TdsOperationStatus TryReadSqlValue(SqlBuffer value, case TdsEnums.SQLNCHAR: case TdsEnums.SQLNVARCHAR: case TdsEnums.SQLNTEXT: + case TdsEnums.SQLJSON: result = TryReadSqlStringValue(value, tdsType, length, md.encoding, isPlp, stateObj); if (result != TdsOperationStatus.Done) { @@ -8775,6 +8777,7 @@ internal TdsOperationStatus TryGetDataLength(SqlMetaDataPriv colmeta, TdsParserS colmeta.tdsType == TdsEnums.SQLBIGVARCHAR || colmeta.tdsType == TdsEnums.SQLBIGVARBINARY || colmeta.tdsType == TdsEnums.SQLNVARCHAR || + colmeta.tdsType == TdsEnums.SQLJSON || // Large UDTs is WinFS-only colmeta.tdsType == TdsEnums.SQLUDT, "GetDataLength:Invalid streaming datatype"); @@ -10573,7 +10576,7 @@ internal Task TdsExecuteRPC(SqlCommand cmd, IList<_SqlRPC> rpcArray, int timeout } else if (mt.IsPlp) { - if (mt.SqlDbType != SqlDbType.Xml) + if (mt.SqlDbType != SqlDbType.Xml && mt.SqlDbType != SqlDbTypeExtensions.Json) WriteShort(TdsEnums.SQL_USHORTVARMAXLEN, stateObj); } else if ((!mt.IsVarTime) && (mt.SqlDbType != SqlDbType.Date)) @@ -10614,53 +10617,56 @@ internal Task TdsExecuteRPC(SqlCommand cmd, IList<_SqlRPC> rpcArray, int timeout // write out collation or xml metadata - if (_is2005 && (mt.SqlDbType == SqlDbType.Xml)) + if ((mt.SqlDbType == SqlDbType.Xml || mt.SqlDbType == SqlDbTypeExtensions.Json)) { - if (!string.IsNullOrEmpty(param.XmlSchemaCollectionDatabase) || - !string.IsNullOrEmpty(param.XmlSchemaCollectionOwningSchema) || - !string.IsNullOrEmpty(param.XmlSchemaCollectionName)) + if (mt.SqlDbType == SqlDbType.Xml) { - stateObj.WriteByte(1); //Schema present flag - - if (!string.IsNullOrEmpty(param.XmlSchemaCollectionDatabase)) + if (!string.IsNullOrEmpty(param.XmlSchemaCollectionDatabase) || + !string.IsNullOrEmpty(param.XmlSchemaCollectionOwningSchema) || + !string.IsNullOrEmpty(param.XmlSchemaCollectionName)) { - tempLen = (param.XmlSchemaCollectionDatabase).Length; - stateObj.WriteByte((byte)(tempLen)); - WriteString(param.XmlSchemaCollectionDatabase, tempLen, 0, stateObj); - } - else - { - stateObj.WriteByte(0); // No dbname - } + stateObj.WriteByte(1); //Schema present flag + + if (!string.IsNullOrEmpty(param.XmlSchemaCollectionDatabase)) + { + tempLen = (param.XmlSchemaCollectionDatabase).Length; + stateObj.WriteByte((byte)(tempLen)); + WriteString(param.XmlSchemaCollectionDatabase, tempLen, 0, stateObj); + } + else + { + stateObj.WriteByte(0); // No dbname + } + + if (!string.IsNullOrEmpty(param.XmlSchemaCollectionOwningSchema)) + { + tempLen = (param.XmlSchemaCollectionOwningSchema).Length; + stateObj.WriteByte((byte)(tempLen)); + WriteString(param.XmlSchemaCollectionOwningSchema, tempLen, 0, stateObj); + } + else + { + stateObj.WriteByte(0); // no xml schema name + } + if (!string.IsNullOrEmpty(param.XmlSchemaCollectionName)) + { + tempLen = (param.XmlSchemaCollectionName).Length; + WriteShort((short)(tempLen), stateObj); + WriteString(param.XmlSchemaCollectionName, tempLen, 0, stateObj); + } + else + { + WriteShort(0, stateObj); // No xml schema collection name + } - if (!string.IsNullOrEmpty(param.XmlSchemaCollectionOwningSchema)) - { - tempLen = (param.XmlSchemaCollectionOwningSchema).Length; - stateObj.WriteByte((byte)(tempLen)); - WriteString(param.XmlSchemaCollectionOwningSchema, tempLen, 0, stateObj); - } - else - { - stateObj.WriteByte(0); // no xml schema name - } - if (!string.IsNullOrEmpty(param.XmlSchemaCollectionName)) - { - tempLen = (param.XmlSchemaCollectionName).Length; - WriteShort((short)(tempLen), stateObj); - WriteString(param.XmlSchemaCollectionName, tempLen, 0, stateObj); } else { - WriteShort(0, stateObj); // No xml schema collection name + stateObj.WriteByte(0); // No schema } - - } - else - { - stateObj.WriteByte(0); // No schema } } - else if (_is2000 && mt.IsCharType) + else if (mt.IsCharType && mt.SqlDbType != SqlDbTypeExtensions.Json) { // if it is not supplied, simply write out our default collation, otherwise, write out the one attached to the parameter SqlCollation outCollation = (param.Collation != null) ? param.Collation : _defaultCollation; @@ -12430,10 +12436,18 @@ private Task WriteUnterminatedSqlValue(object value, MetaType type, int actualLe case TdsEnums.SQLNVARCHAR: case TdsEnums.SQLNTEXT: case TdsEnums.SQLXMLTYPE: + case TdsEnums.SQLJSON: if (type.IsPlp) { - if (IsBOMNeeded(type, value)) + if (type.NullableType == TdsEnums.SQLJSON) + { + // TODO : Performance and BOM check. For more details see https://github.com/dotnet/SqlClient/issues/2852 + byte[] jsonAsBytes = Encoding.UTF8.GetBytes(value.ToString()); + WriteInt(jsonAsBytes.Length, stateObj); + return stateObj.WriteByteArray(jsonAsBytes, jsonAsBytes.Length, 0, canAccumulate: false); + } + else if (IsBOMNeeded(type, value)) { WriteInt(actualLength + 2, stateObj); // chunk length WriteShort(TdsEnums.XMLUNICODEBOM, stateObj); @@ -13122,6 +13136,7 @@ private Task WriteUnterminatedValue(object value, MetaType type, byte scale, int case TdsEnums.SQLNVARCHAR: case TdsEnums.SQLNTEXT: case TdsEnums.SQLXMLTYPE: + case TdsEnums.SQLJSON: { Debug.Assert(!isDataFeed || (value is TextDataFeed || value is XmlDataFeed), "Value must be a TextReader or XmlReader"); Debug.Assert(isDataFeed || (value is string || value is byte[]), "Value is a byte array or string"); @@ -13143,7 +13158,14 @@ private Task WriteUnterminatedValue(object value, MetaType type, byte scale, int { if (type.IsPlp) { - if (IsBOMNeeded(type, value)) + if (type.NullableType == TdsEnums.SQLJSON) + { + // TODO : Performance and BOM check. Saurabh + byte[] jsonAsBytes = Encoding.UTF8.GetBytes((string)value); + WriteInt(jsonAsBytes.Length, stateObj); + return stateObj.WriteByteArray(jsonAsBytes, jsonAsBytes.Length, 0, canAccumulate: false); + } + else if (IsBOMNeeded(type, value)) { WriteInt(actualLength + 2, stateObj); // chunk length WriteShort(TdsEnums.XMLUNICODEBOM, stateObj); @@ -13652,7 +13674,7 @@ internal void WriteParameterVarLen(MetaType type, int size, bool isNull, TdsPars WriteInt(unchecked((int)TdsEnums.VARLONGNULL), stateObj); } } - else if (type.NullableType == TdsEnums.SQLXMLTYPE || unknownLength) + else if (type.NullableType is TdsEnums.SQLXMLTYPE or TdsEnums.SQLJSON || unknownLength) { WriteUnsignedLong(TdsEnums.SQL_PLP_UNKNOWNLEN, stateObj); } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlEnums.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlEnums.cs index 86b9656684..0f8da0c874 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlEnums.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlEnums.cs @@ -226,6 +226,8 @@ internal static MetaType GetMetaTypeFromSqlDbType(SqlDbType target, bool isMulti return MetaXml; case SqlDbType.Udt: return MetaUdt; + case SqlDbTypeExtensions.Json: + return s_MetaJson; case SqlDbType.Structured: if (isMultiValued) { @@ -862,6 +864,8 @@ internal static MetaType GetSqlDataType(int tdsType, uint userType, int length) return s_metaDateTime2; case TdsEnums.SQLDATETIMEOFFSET: return MetaDateTimeOffset; + case TdsEnums.SQLJSON: + return s_MetaJson; case TdsEnums.SQLVOID: default: @@ -968,6 +972,8 @@ internal static string GetStringFromXml(XmlReader xmlreader) internal static readonly MetaType MetaDateTimeOffset = new(255, 7, -1, false, false, false, TdsEnums.SQLDATETIMEOFFSET, TdsEnums.SQLDATETIMEOFFSET, MetaTypeName.DATETIMEOFFSET, typeof(System.DateTimeOffset), typeof(System.DateTimeOffset), SqlDbType.DateTimeOffset, DbType.DateTimeOffset, 1); + internal static readonly MetaType s_MetaJson = new(255, 255, -1, false, true, true, TdsEnums.SQLJSON, TdsEnums.SQLJSON, MetaTypeName.JSON, typeof(string), typeof(string), SqlDbTypeExtensions.Json, DbType.String, 0); + public static TdsDateTime FromDateTime(DateTime dateTime, byte cb) { SqlDateTime sqlDateTime; @@ -1054,6 +1060,7 @@ private static class MetaTypeName public const string TIME = "time"; public const string DATETIME2 = "datetime2"; public const string DATETIMEOFFSET = "datetimeoffset"; + public const string JSON = "json"; } } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlParameter.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlParameter.cs index 034a2ac306..da214059a0 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlParameter.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlParameter.cs @@ -1563,6 +1563,7 @@ internal int GetActualSize() case SqlDbType.NVarChar: case SqlDbType.NText: case SqlDbType.Xml: + case SqlDbTypeExtensions.Json: { coercedSize = ((!HasFlag(SqlParameterFlags.IsNull)) && (!HasFlag(SqlParameterFlags.CoercedValueIsDataFeed))) ? StringSize(val, HasFlag(SqlParameterFlags.CoercedValueIsSqlType)) : 0; _actualSize = (ShouldSerializeSize() ? Size : 0); diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs index 685002790c..8ae65384ef 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs @@ -490,6 +490,8 @@ public enum ActiveDirectoryWorkflow : byte public const int SQLDATETIME2 = 0x2a; public const int SQLDATETIMEOFFSET = 0x2b; + public const int SQLJSON = 0xF4; + public const int DEFAULT_VARTIME_SCALE = 7; //Partially length prefixed datatypes constants. These apply to XMLTYPE, BIGVARCHRTYPE, diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlDbTypeExtensions.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlDbTypeExtensions.cs index 18c53f428b..df7f597517 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlDbTypeExtensions.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlDbTypeExtensions.cs @@ -6,14 +6,10 @@ namespace Microsoft.Data { - /// - /// Extensions for SqlDbType enum to enable its usage. - /// + /// public static class SqlDbTypeExtensions { - /// - /// Represents the JSON Data type in SQL Server. - /// + /// #if NET9_0_OR_GREATER public const SqlDbType Json = SqlDbType.Json; #else diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs index 9e69b7a39f..9da517b8b6 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs @@ -94,6 +94,9 @@ public static class DataTestUtility //SQL Server EngineEdition private static string s_sqlServerEngineEdition; + // JSON Coloumn type + public static readonly bool IsJsonSupported = false; + // Azure Synapse EngineEditionId == 6 // More could be read at https://learn.microsoft.com/en-us/sql/t-sql/functions/serverproperty-transact-sql?view=sql-server-ver16#propertyname public static bool IsAzureSynapse @@ -175,6 +178,7 @@ static DataTestUtility() ManagedIdentitySupported = c.ManagedIdentitySupported; IsManagedInstance = c.IsManagedInstance; AliasName = c.AliasName; + IsJsonSupported = c.IsJsonSupported; System.Net.ServicePointManager.SecurityProtocol |= System.Net.SecurityProtocolType.Tls12; diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj index 0ec61b5420..7afb3c818c 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj @@ -291,6 +291,7 @@ + diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/JsonTest/JsonTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/JsonTest/JsonTest.cs new file mode 100644 index 0000000000..c8b3507b5d --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/JsonTest/JsonTest.cs @@ -0,0 +1,295 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Data; +using Microsoft.Data; +using System.Data.Common; +using System.Data.SqlTypes; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Data.SqlClient.ManualTesting.Tests +{ + public class JsonTest + { + private readonly ITestOutputHelper _output; + + public JsonTest(ITestOutputHelper output) + { + _output = output; + } + + private static readonly string jsonDataString = "[{\"name\":\"Dave\",\"skills\":[\"Python\"]},{\"name\":\"Ron\",\"surname\":\"Peter\"}]"; + + private void ValidateRowsAffected(int rowsAffected) + { + _output.WriteLine($"Rows affected: {rowsAffected}"); + Assert.Equal(1, rowsAffected); + } + + private void ValidateRows(SqlDataReader reader) + { + while (reader.Read()) + { + string jsonData = reader.GetString(0); + _output.WriteLine(jsonData); + Assert.Equal(jsonDataString, jsonData); + } + } + + private async Task ValidateRowsAsync(SqlDataReader reader) + { + while (await reader.ReadAsync()) + { + string jsonData = reader.GetString(0); + _output.WriteLine(jsonData); + Assert.Equal(jsonDataString, jsonData); + } + } + + private void ValidateSchema(SqlDataReader reader) + { + System.Collections.ObjectModel.ReadOnlyCollection schema = reader.GetColumnSchema(); + foreach (DbColumn column in schema) + { + _output.WriteLine("Column Name is " + column.ColumnName); + _output.WriteLine("Column DataType is " + column?.DataType.ToString()); + _output.WriteLine("Column DataTypeName is " + column.DataTypeName); + Assert.Equal("json", column.DataTypeName); + } + } + + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsJsonSupported))] + public void TestJsonWrite() + { + string tableName = DataTestUtility.GetUniqueNameForSqlServer("Json_Test"); + string spName = DataTestUtility.GetUniqueNameForSqlServer("spJson_WriteTest"); + + string tableCreate = "CREATE TABLE " + tableName + " (Data json)"; + string tableInsert = "INSERT INTO " + tableName + " VALUES (@jsonData)"; + string spCreate = "CREATE PROCEDURE " + spName + " (@jsonData json) AS " + tableInsert; + + using (SqlConnection connection = new SqlConnection(DataTestUtility.TCPConnectionString)) + { + connection.Open(); + + using (SqlCommand command = connection.CreateCommand()) + { + //Create Table + command.CommandText = tableCreate; + command.ExecuteNonQuery(); + + //Create SP for writing json values + command.CommandText = spCreate; + command.ExecuteNonQuery(); + + command.CommandText = tableInsert; + var parameter = new SqlParameter("@jsonData", SqlDbTypeExtensions.Json); + command.Parameters.Add(parameter); + + //Test 1 + //Write json value using a parameterized query + parameter.Value = jsonDataString; + int rowsAffected = command.ExecuteNonQuery(); + ValidateRowsAffected(rowsAffected); + + //Test 2 + //Write a SqlString type as json + parameter.Value = new SqlString(jsonDataString); + int rowsAffected2 = command.ExecuteNonQuery(); + ValidateRowsAffected(rowsAffected2); + + //Test 3 + //Write json value using SP + using (SqlCommand command2 = connection.CreateCommand()) + { + command2.CommandText = spName; + command2.CommandType = CommandType.StoredProcedure; + var parameter2 = new SqlParameter("@jsonData", SqlDbTypeExtensions.Json); + parameter2.Value = jsonDataString; + command2.Parameters.Add(parameter2); + int rowsAffected3 = command2.ExecuteNonQuery(); + ValidateRowsAffected(rowsAffected3); + } + + DataTestUtility.DropTable(connection, tableName); + } + } + } + + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsJsonSupported))] + public async Task TestJsonWriteAsync() + { + string tableName = DataTestUtility.GetUniqueNameForSqlServer("Json_Test"); + string spName = DataTestUtility.GetUniqueNameForSqlServer("spJson_WriteTest"); + + string tableCreate = "CREATE TABLE " + tableName + " (Data json)"; + string tableInsert = "INSERT INTO " + tableName + " VALUES (@jsonData)"; + string spCreate = "CREATE PROCEDURE " + spName + " (@jsonData json) AS " + tableInsert; + + using (SqlConnection connection = new SqlConnection(DataTestUtility.TCPConnectionString)) + { + await connection.OpenAsync(); + + using (SqlCommand command = connection.CreateCommand()) + { + //Create Table + command.CommandText = tableCreate; + await command.ExecuteNonQueryAsync(); + + //Create SP for writing json values + command.CommandText = spCreate; + await command.ExecuteNonQueryAsync(); + + command.CommandText = tableInsert; + var parameter = new SqlParameter("@jsonData", SqlDbTypeExtensions.Json); + command.Parameters.Add(parameter); + + //Test 1 + //Write json value using a parameterized query + parameter.Value = jsonDataString; + int rowsAffected = await command.ExecuteNonQueryAsync(); + ValidateRowsAffected(rowsAffected); + + //Test 2 + //Write a SqlString type as json + parameter.Value = new SqlString(jsonDataString); + int rowsAffected2 = await command.ExecuteNonQueryAsync(); + ValidateRowsAffected(rowsAffected2); + + //Test 3 + //Write json value using SP + using (SqlCommand command2 = connection.CreateCommand()) + { + command2.CommandText = spName; + command2.CommandType = CommandType.StoredProcedure; + var parameter2 = new SqlParameter("@jsonData", SqlDbTypeExtensions.Json); + parameter2.Value = jsonDataString; + command2.Parameters.Add(parameter2); + int rowsAffected3 = await command.ExecuteNonQueryAsync(); + ValidateRowsAffected(rowsAffected3); + } + } + } + } + + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsJsonSupported))] + public void TestJsonRead() + { + string tableName = DataTestUtility.GetUniqueNameForSqlServer("Json_Test"); + string spName = DataTestUtility.GetUniqueNameForSqlServer("spJson_ReadTest"); + + string tableCreate = "CREATE TABLE " + tableName + " (Data json)"; + string tableInsert = "INSERT INTO " + tableName + " VALUES (@jsonData)"; + string tableRead = "SELECT * FROM " + tableName; + string spCreate = "CREATE PROCEDURE " + spName + "AS " + tableRead; + + using (SqlConnection connection = new SqlConnection(DataTestUtility.TCPConnectionString)) + { + connection.Open(); + using (SqlCommand command = connection.CreateCommand()) + { + //Create Table + command.CommandText = tableCreate; + command.ExecuteNonQuery(); + + //Create SP for reading from json column + command.CommandText = spCreate; + command.ExecuteNonQuery(); + + //Insert sample json data + //This will be used for reading + command.CommandText = tableInsert; + var parameter = new SqlParameter("@jsonData", SqlDbTypeExtensions.Json); + parameter.Value = jsonDataString; + command.Parameters.Add(parameter); + command.ExecuteNonQuery(); + + //Test 1 + //Read json value using query + command.CommandText = tableRead; + var reader = command.ExecuteReader(); + ValidateRows(reader); + + //Test 2 + //Read the column metadata + ValidateSchema(reader); + reader.Close(); + + //Test 3 + //Read json value using SP + using (SqlCommand command2 = connection.CreateCommand()) + { + command2.CommandText = spName; + command2.CommandType = CommandType.StoredProcedure; + var reader2 = command2.ExecuteReader(); + ValidateRows(reader2); + reader2.Close(); + } + + DataTestUtility.DropTable(connection, tableName); + } + } + } + + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsJsonSupported))] + public async Task TestJsonReadAsync() + { + string tableName = DataTestUtility.GetUniqueNameForSqlServer("Json_Test"); + string spName = DataTestUtility.GetUniqueNameForSqlServer("spJson_ReadTest"); + + string tableCreate = "CREATE TABLE " + tableName + " (Data json)"; + string tableInsert = "INSERT INTO " + tableName + " VALUES (@jsonData)"; + string tableRead = "SELECT * FROM " + tableName; + string spCreate = "CREATE PROCEDURE " + spName + "AS " + tableRead; + + using (SqlConnection connection = new SqlConnection(DataTestUtility.TCPConnectionString)) + { + await connection.OpenAsync(); + using (SqlCommand command = connection.CreateCommand()) + { + //Create Table + command.CommandText = tableCreate; + await command.ExecuteNonQueryAsync(); + + //Create SP for reading from json column + command.CommandText = spCreate; + await command.ExecuteNonQueryAsync(); + + //Insert sample json data + //This will be used for reading + command.CommandText = tableInsert; + var parameter = new SqlParameter("@jsonData", SqlDbTypeExtensions.Json); + parameter.Value = jsonDataString; + command.Parameters.Add(parameter); + await command.ExecuteNonQueryAsync(); + + //Test 1 + //Read json value using query + command.CommandText = tableRead; + var reader = await command.ExecuteReaderAsync(); + await ValidateRowsAsync(reader); + + //Test 2 + //Read the column metadata + ValidateSchema(reader); + reader.Close(); + + //Test 3 + //Read json value using SP + using (SqlCommand command2 = connection.CreateCommand()) + { + command2.CommandText = spName; + command2.CommandType = CommandType.StoredProcedure; + var reader2 = await command2.ExecuteReaderAsync(); + await ValidateRowsAsync(reader2); + reader2.Close(); + } + } + } + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities/Config.cs b/src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities/Config.cs index 3fbe8313ee..1b712ceaf5 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities/Config.cs +++ b/src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities/Config.cs @@ -42,6 +42,7 @@ public class Config public string KerberosDomainUser = null; public bool IsManagedInstance = false; public string AliasName = null; + public bool IsJsonSupported = false; public static Config Load(string configPath = @"config.json") { try