Skip to content

Commit ac719c1

Browse files
senseibakac-j-hughes
authored andcommitted
Implement TDS_BLOB support (#150)
SAP ASE has supported the TDS_BLOB type within parameters since ASE 15.7. This fix adds corresponding support to the driver.
1 parent 9b00a0d commit ac719c1

10 files changed

Lines changed: 365 additions & 9 deletions

File tree

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// ReSharper disable InconsistentNaming
2+
namespace AdoNetCore.AseClient.Enum
3+
{
4+
public enum BlobType : byte
5+
{
6+
/// <summary>
7+
/// Not set
8+
/// </summary>
9+
BLOB_UNSET = 0x00,
10+
/// <summary>
11+
/// The fully qualified name of the class (“com.foo.Bar”).
12+
/// This is a Character String in the negotiated TDS character set currently in use on this connection.
13+
/// </summary>
14+
BLOB_FULLY_QUALIFIED_CLASS_NAME = 0x01,
15+
/// <summary>
16+
/// 4-byte integer (database ID) 4-byte integer(sysextypes number of this class definition in this database).
17+
/// Both integers are in the byte-ordering negotiated for this connection.
18+
/// </summary>
19+
BLOB_INT32_CLASS_ID = 0x02,
20+
/// <summary>
21+
/// This is long character data and has no ClassID associated with it
22+
/// </summary>
23+
BLOB_LONGCHAR = 0x03,
24+
/// <summary>
25+
/// This is long binary data and has no ClassID associated with it.
26+
/// Appears in ribo as BLOB_VARBINARY
27+
/// </summary>
28+
BLOB_LONGBINARY = 0x04,
29+
/// <summary>
30+
/// This is unichar data with no ClassID associated with it.
31+
/// Appears in ribo as BLOB_UTF16
32+
/// </summary>
33+
BLOB_UNICHAR = 0x05
34+
}
35+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
namespace AdoNetCore.AseClient.Enum
2+
{
3+
public enum SerializationType : byte
4+
{
5+
//todo: figure out what these names might actually be
6+
/// <summary>
7+
/// Use the default serialization associated with the specified <see cref="BlobType"/>.
8+
/// Allowed cases:
9+
/// <see cref="BlobType.BLOB_LONGCHAR"/> - Characters are in their native format, the character set of the data is the same as that of all other character data as negotiated on the connection during login.
10+
/// <see cref="BlobType.BLOB_LONGBINARY"/> - Binary data in its normal form
11+
/// <see cref="BlobType.BLOB_UNICHAR"/> - This is unichar data with normal UTF-16 encoding with byte-order identical to that of the client
12+
/// </summary>
13+
SER_DEFAULT = 0x00,
14+
/// <summary>
15+
/// Allowed cases:
16+
/// <see cref="BlobType.BLOB_FULLY_QUALIFIED_CLASS_NAME"/> - Native Java Serialization
17+
/// <see cref="BlobType.BLOB_INT32_CLASS_ID"/> - Native Java Serialization
18+
/// <see cref="BlobType.BLOB_UNICHAR"/> - This is unichar data in its UTF-8 encoding.
19+
/// </summary>
20+
SER_SPECIAL1 = 0x01,
21+
/// <summary>
22+
/// Allowed cases:
23+
/// <see cref="BlobType.BLOB_UNICHAR"/> - This is unichar data in SCSU (compressed) encoding
24+
/// </summary>
25+
SER_SPECIAL2 = 0x02
26+
}
27+
}

src/AdoNetCore.AseClient/Internal/FormatItem.cs

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Data;
23
using System.Diagnostics;
34
using System.IO;
45
using System.Text;
@@ -18,7 +19,13 @@ internal class FormatItem
1819
public int UserType { get; set; }
1920
public TdsDataType DataType { get; set; }
2021
public int? Length { get; set; }
22+
/// <summary>
23+
/// Relates to TDS_NUMN and TDS_DECN
24+
/// </summary>
2125
public byte? Precision { get; set; }
26+
/// <summary>
27+
/// Relates to TDS_NUMN and TDS_DECN
28+
/// </summary>
2229
public byte? Scale { get; set; }
2330
public string LocaleInfo { get; set; }
2431

@@ -46,7 +53,15 @@ public string ParameterName
4653
/// <summary>
4754
/// Relates to TDS_BLOB
4855
/// </summary>
49-
public string ClassId { get; set; }
56+
public BlobType BlobType { get; set; }
57+
/// <summary>
58+
/// Relates to TDS_BLOB
59+
/// </summary>
60+
public byte[] ClassId { get; set; }
61+
/// <summary>
62+
/// Relates to TDS_BLOB
63+
/// </summary>
64+
public SerializationType SerializationType { get; set; }
5065

5166
public static FormatItem CreateForParameter(AseParameter parameter, DbEnvironment env)
5267
{
@@ -61,9 +76,23 @@ public static FormatItem CreateForParameter(AseParameter parameter, DbEnvironmen
6176
IsNullable = parameter.IsNullable,
6277
Length = length,
6378
DataType = TypeMap.GetTdsDataType(dbType, parameter.SendableValue, length, parameter.ParameterName),
64-
UserType = TypeMap.GetTdsUserType(dbType),
79+
UserType = TypeMap.GetTdsUserType(dbType)
6580
};
6681

82+
//fixup the FormatItem's BlobType for strings and byte arrays
83+
if (format.DataType == TdsDataType.TDS_BLOB)
84+
{
85+
switch (parameter.DbType)
86+
{
87+
case DbType.String:
88+
format.BlobType = BlobType.BLOB_UNICHAR;
89+
break;
90+
case DbType.Binary:
91+
format.BlobType = BlobType.BLOB_LONGBINARY;
92+
break;
93+
}
94+
}
95+
6796
//fixup the FormatItem's length,scale,precision for decimals
6897
if (format.IsDecimalType)
6998
{
@@ -175,6 +204,10 @@ private static void ReadTypeInfo(FormatItem format, Stream stream, Encoding enc)
175204
case TdsDataType.TDS_LONGBINARY:
176205
format.Length = stream.ReadInt();
177206
break;
207+
case TdsDataType.TDS_BLOB:
208+
format.BlobType = (BlobType)stream.ReadByte();
209+
format.ClassId = stream.ReadNullableUShortLengthPrefixedByteArray();
210+
break;
178211
case TdsDataType.TDS_DECN:
179212
case TdsDataType.TDS_NUMN:
180213
format.Length = stream.ReadByte();
@@ -268,6 +301,11 @@ public void WriteForParameter(Stream stream, Encoding enc, TokenType srcTokenTyp
268301
case TdsDataType.TDS_LONGBINARY:
269302
stream.WriteUInt((uint)(Length ?? 0));
270303
break;
304+
case TdsDataType.TDS_BLOB:
305+
//according to spec, length isn't specified as part of the format token, but as part of the params token
306+
stream.WriteByte((byte)BlobType);
307+
stream.WriteNullableUShortPrefixedByteArray(ClassId);
308+
break;
271309
case TdsDataType.TDS_DECN:
272310
case TdsDataType.TDS_NUMN:
273311
stream.WriteByte((byte)(Length ?? 1));
@@ -417,6 +455,14 @@ public string GetDataTypeName()
417455
default:
418456
return "binary";
419457
}
458+
case TdsDataType.TDS_BLOB:
459+
switch (BlobType)
460+
{
461+
case BlobType.BLOB_UNICHAR:
462+
return "unichar";
463+
default:
464+
return "blob";
465+
}
420466
default:
421467
return string.Empty;
422468
}

src/AdoNetCore.AseClient/Internal/StreamReadExtensions.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,18 @@ public static byte[] ReadNullableByteLengthPrefixedByteArray(this Stream stream)
137137
return stream.ReadByteArray(length);
138138
}
139139

140+
public static byte[] ReadNullableUShortLengthPrefixedByteArray(this Stream stream)
141+
{
142+
var length = stream.ReadUShort();
143+
144+
if (length == 0)
145+
{
146+
return null;
147+
}
148+
149+
return stream.ReadByteArray(length);
150+
}
151+
140152
public static byte[] ReadNullableIntLengthPrefixedByteArray(this Stream stream)
141153
{
142154
var length = stream.ReadInt();

src/AdoNetCore.AseClient/Internal/StreamWriteExtensions.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,26 @@ public static void WriteBytePrefixedByteArray(this Stream stream, byte[] value)
109109
stream.WriteByte(len);
110110
stream.Write(value, 0, len);
111111
}
112+
public static void WriteNullableUShortPrefixedByteArray(this Stream stream, byte[] value)
113+
{
114+
var len = (ushort)(value?.Length ?? 0);
115+
stream.WriteUShort(len);
116+
117+
if (len == 0)
118+
{
119+
return;
120+
}
121+
122+
stream.Write(value, 0, len);
123+
}
124+
125+
public static void WriteBlobSpecificIntPrefixedByteArray(this Stream stream, byte[] value)
126+
{
127+
var len = value.Length;
128+
var endOfBlobLen = (uint) len | 0x80000000; //highest-order bit set means end of blob data
129+
stream.WriteUInt(endOfBlobLen);
130+
stream.Write(value, 0, len);
131+
}
112132

113133
public static void WriteIntPrefixedByteArray(this Stream stream, byte[] value)
114134
{

src/AdoNetCore.AseClient/Internal/TypeMap.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ namespace AdoNetCore.AseClient.Internal
99
internal static class TypeMap
1010
{
1111
private const int VarLongBoundary = 255;
12+
//Above this length, send strings as TDS_BLOBs
13+
private const int StringAsBlobBoundary = 8192;
14+
private const int BinaryAsBlobBoundary = 16384;
1215

1316
private static readonly Dictionary<DbType, Func<object, int, TdsDataType>> DbToTdsMap = new Dictionary<DbType, Func<object, int, TdsDataType>>
1417
{
@@ -21,11 +24,16 @@ internal static class TypeMap
2124
{DbType.UInt32, (value, length) => value == DBNull.Value ? TdsDataType.TDS_UINTN : TdsDataType.TDS_UINT4},
2225
{DbType.Int64, (value, length) => value == DBNull.Value ? TdsDataType.TDS_INTN : TdsDataType.TDS_INT8},
2326
{DbType.UInt64, (value, length) => value == DBNull.Value ? TdsDataType.TDS_UINTN : TdsDataType.TDS_UINT8},
24-
{DbType.String, (value, length) => TdsDataType.TDS_LONGBINARY},
27+
{DbType.String, (value, length) => length <= StringAsBlobBoundary ? TdsDataType.TDS_LONGBINARY : TdsDataType.TDS_BLOB},
2528
{DbType.StringFixedLength, (value, length) => TdsDataType.TDS_LONGBINARY},
2629
{DbType.AnsiString, (value, length) => length <= VarLongBoundary ? TdsDataType.TDS_VARCHAR : TdsDataType.TDS_LONGCHAR},
2730
{DbType.AnsiStringFixedLength, (value, length) => length <= VarLongBoundary ? TdsDataType.TDS_VARCHAR : TdsDataType.TDS_LONGCHAR},
28-
{DbType.Binary, (value, length) => length <= VarLongBoundary ? TdsDataType.TDS_BINARY : TdsDataType.TDS_LONGBINARY},
31+
{DbType.Binary, (value, length) =>
32+
length <= BinaryAsBlobBoundary
33+
? length <= VarLongBoundary
34+
? TdsDataType.TDS_BINARY
35+
: TdsDataType.TDS_LONGBINARY
36+
: TdsDataType.TDS_BLOB},
2937
{DbType.Guid, (value, length) => TdsDataType.TDS_BINARY},
3038
{DbType.Decimal, (value, length) => TdsDataType.TDS_NUMN},
3139
{DbType.Currency, (value, length) => TdsDataType.TDS_MONEYN},

src/AdoNetCore.AseClient/Internal/ValueWriter.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ private static T Cast<T>(object value, FormatItem format, Encoding enc)
8686
{ TdsDataType.TDS_VARBINARY, WriteTDS_VARBINARY },
8787
{ TdsDataType.TDS_BINARY, WriteTDS_BINARY },
8888
{ TdsDataType.TDS_LONGBINARY, WriteTDS_LONGBINARY },
89+
{ TdsDataType.TDS_BLOB, WriteTDS_BLOB },
8990
{ TdsDataType.TDS_DECN, WriteTDS_DECN },
9091
{ TdsDataType.TDS_NUMN, WriteTDS_NUMN },
9192
{ TdsDataType.TDS_DATETIME, WriteTDS_DATETIME },
@@ -300,6 +301,29 @@ private static void WriteTDS_LONGBINARY(object value, Stream stream, FormatItem
300301
}
301302
}
302303
}
304+
private static void WriteTDS_BLOB(object value, Stream stream, FormatItem format, Encoding enc)
305+
{
306+
//byte serialization type
307+
//short-prefixed class id
308+
//n chunks of data
309+
// 4-byte datalen (highest-order bit indicates if there are more chunks)
310+
// data
311+
switch (value)
312+
{
313+
case string s:
314+
stream.WriteByte((byte)SerializationType.SER_DEFAULT);
315+
stream.WriteNullableUShortPrefixedByteArray(format.ClassId);
316+
stream.WriteBlobSpecificIntPrefixedByteArray(Encoding.Unicode.GetBytes(s));
317+
break;
318+
case byte[] ba:
319+
stream.WriteByte((byte)SerializationType.SER_DEFAULT);
320+
stream.WriteNullableUShortPrefixedByteArray(format.ClassId);
321+
stream.WriteBlobSpecificIntPrefixedByteArray(ba);
322+
break;
323+
default:
324+
throw new AseException($"TDS_BLOB support for {value.GetType().Name} not yet implemented");
325+
}
326+
}
303327

304328
private static void WriteTDS_DECN(object value, Stream stream, FormatItem format, Encoding enc)
305329
{

test/AdoNetCore.AseClient.Tests/AdoNetCore.AseClient.Tests.csproj

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
3-
<TargetFrameworks>netcoreapp1.0;netcoreapp1.1;netcoreapp2.0;netcoreapp2.1;net46</TargetFrameworks>
3+
<TargetFrameworks>netcoreapp1.0;netcoreapp1.1;netcoreapp2.0;netcoreapp2.1;netcoreapp2.2;net46</TargetFrameworks>
44
<IsPackable>false</IsPackable>
55
</PropertyGroup>
66
<ItemGroup>
@@ -23,13 +23,13 @@
2323
<PropertyGroup Condition="'$(TargetFramework)' == 'net46'">
2424
<DefineConstants>$(DefineConstants);NET_FRAMEWORK</DefineConstants>
2525
</PropertyGroup>
26-
<PropertyGroup Condition="'$(TargetFramework)' == 'netcoreapp2.1' Or '$(TargetFramework)' == 'netcoreapp2.0' Or '$(TargetFramework)' == 'netcoreapp1.1' Or '$(TargetFramework)' == 'netcoreapp1.0'">
26+
<PropertyGroup Condition="'$(TargetFramework)' == 'netcoreapp2.2' Or '$(TargetFramework)' == 'netcoreapp2.1' Or '$(TargetFramework)' == 'netcoreapp2.0' Or '$(TargetFramework)' == 'netcoreapp1.1' Or '$(TargetFramework)' == 'netcoreapp1.0'">
2727
<DefineConstants>$(DefineConstants);NET_CORE</DefineConstants>
2828
</PropertyGroup>
29-
<PropertyGroup Condition="'$(TargetFramework)' == 'netcoreapp2.1' Or '$(TargetFramework)' == 'netcoreapp2.0' Or '$(TargetFramework)' == 'net46' Or '$(TargetFramework)' == 'netstandard2.0'">
29+
<PropertyGroup Condition="'$(TargetFramework)' == 'netcoreapp2.2' Or '$(TargetFramework)' == 'netcoreapp2.1' Or '$(TargetFramework)' == 'netcoreapp2.0' Or '$(TargetFramework)' == 'net46' Or '$(TargetFramework)' == 'netstandard2.0'">
3030
<DefineConstants>$(DefineConstants);ENABLE_SYSTEM_DATA_COMMON_EXTENSIONS</DefineConstants>
3131
</PropertyGroup>
32-
<PropertyGroup Condition="'$(TargetFramework)' == 'netcoreapp2.1' Or '$(TargetFramework)' == 'net46'">
32+
<PropertyGroup Condition="'$(TargetFramework)' == 'netcoreapp2.2' Or '$(TargetFramework)' == 'netcoreapp2.1' Or '$(TargetFramework)' == 'net46'">
3333
<DefineConstants>$(DefineConstants);ENABLE_DB_PROVIDERFACTORY</DefineConstants>
3434
</PropertyGroup>
3535
<ItemGroup Condition=" '$(TargetFramework)' == 'netcoreapp1.0' ">
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Data;
4+
using System.Data.Common;
5+
using AdoNetCore.AseClient.Internal;
6+
using AdoNetCore.AseClient.Tests.ConnectionProvider;
7+
using Dapper;
8+
using NUnit.Framework;
9+
10+
namespace AdoNetCore.AseClient.Tests.Integration.Insert
11+
{
12+
[Category("basic")]
13+
#if NET_FRAMEWORK
14+
[TestFixture(typeof(SapConnectionProvider), Explicit = true, Reason = "SAP AseClient tests are run for compatibility purposes.")]
15+
#endif
16+
[TestFixture(typeof(CoreFxConnectionProvider))]
17+
public class BinaryTests<T> where T : IConnectionProvider
18+
{
19+
private DbConnection GetConnection()
20+
{
21+
return Activator.CreateInstance<T>().GetConnection(ConnectionStrings.Pooled);
22+
}
23+
24+
private const string SetUpSql = @"create table [dbo].[insert_image_tests] (image_field image null)";
25+
private const string CleanUpSql = @"IF EXISTS(SELECT 1 FROM sysobjects WHERE name = 'insert_image_tests')
26+
BEGIN
27+
drop table [dbo].[insert_image_tests]
28+
END";
29+
30+
[SetUp]
31+
public void Setup()
32+
{
33+
Logger.Enable();
34+
35+
using (var connection = GetConnection())
36+
{
37+
connection.Execute(CleanUpSql);
38+
connection.Execute(SetUpSql);
39+
}
40+
}
41+
42+
[TearDown]
43+
public void TearDown()
44+
{
45+
using (var connection = GetConnection())
46+
{
47+
connection.Execute(CleanUpSql);
48+
}
49+
}
50+
51+
public static IEnumerable<TestCaseData> Insert_Parameter_Cases()
52+
{
53+
yield return new TestCaseData(1);
54+
yield return new TestCaseData(10);
55+
yield return new TestCaseData(100);
56+
yield return new TestCaseData(127);
57+
yield return new TestCaseData(1000);
58+
yield return new TestCaseData(8192);
59+
yield return new TestCaseData(8193);
60+
yield return new TestCaseData(10000);
61+
yield return new TestCaseData(16384);
62+
yield return new TestCaseData(16385);
63+
yield return new TestCaseData(100000);
64+
yield return new TestCaseData(1000000);
65+
}
66+
67+
[TestCaseSource(nameof(Insert_Parameter_Cases))]
68+
public void Insert_Parameter_Dapper(int count)
69+
{
70+
var value = new byte[count];
71+
using (var connection = GetConnection())
72+
{
73+
connection.Execute("set textsize 1000000");
74+
var p = new DynamicParameters();
75+
p.Add("@image_field", value, DbType.Binary);
76+
connection.Execute("insert into [dbo].[insert_image_tests] (image_field) values (@image_field)", p);
77+
var insertedLength = connection.QuerySingle<int>("select top 1 datalength(image_field) from [dbo].[insert_image_tests]");
78+
Assert.AreEqual(value.Length, insertedLength);
79+
}
80+
81+
Insert_Parameter_VerifyResult(GetConnection, "insert_image_tests", "image_field", value);
82+
}
83+
84+
private void Insert_Parameter_VerifyResult(Func<DbConnection> getConnection, string table, string field, byte[] expected)
85+
{
86+
using (var connection = getConnection())
87+
{
88+
Assert.AreEqual(expected, connection.QuerySingle<byte[]>($"select top 1 {field} from [dbo].[{table}]"));
89+
}
90+
}
91+
}
92+
}

0 commit comments

Comments
 (0)