1 // Licensed to the .NET Foundation under one or more agreements.
2 // The .NET Foundation licenses this file to you under the MIT license.
3 // See the LICENSE file in the project root for more information.
4 
5 using System.Text;
6 using System.Data.Common;
7 using System.Globalization;
8 
9 namespace System.Data.SqlClient.ManualTesting.Tests
10 {
11     /// <summary>
12     /// represents a base class for SQL type random generation
13     /// </summary>
14     public abstract class SqlRandomTypeInfo
15     {
16         // max size on the row for large blob types to prevent row overflow, when creating the table:
17         // "Warning: The table "TestTable" has been created, but its maximum row size exceeds the allowed maximum of 8060 bytes. INSERT or UPDATE to this table will fail if the resulting row exceeds the size limit."
18         // tests show that the actual size is 36, I added 4 more bytes for extra
19         protected const int LargeVarDataRowUsage = 40; // var types
20         protected const int LargeDataRowUsage = 40; // text/ntext/image
21         protected internal const int XmlRowUsage = 40; //
22         protected const int VariantRowUsage = 40;
23 
24         public readonly SqlDbType Type;
25 
SqlRandomTypeInfo(SqlDbType t)26         protected SqlRandomTypeInfo(SqlDbType t)
27         {
28             Type = t;
29         }
30 
31         /// <summary>
32         /// true if column of this type can be created as sparse column
33         /// </summary>
34         public virtual bool CanBeSparseColumn
35         {
36             get
37             {
38                 return true;
39             }
40         }
41 
42 
43         /// <summary>
44         /// creates a default column instance for given type
45         /// </summary>
CreateDefaultColumn()46         public SqlRandomTableColumn CreateDefaultColumn()
47         {
48             return CreateDefaultColumn(SqlRandomColumnOptions.None);
49         }
50 
51         /// <summary>
52         /// creates a default column instance for given type
53         /// </summary>
CreateDefaultColumn(SqlRandomColumnOptions options)54         public virtual SqlRandomTableColumn CreateDefaultColumn(SqlRandomColumnOptions options)
55         {
56             return new SqlRandomTableColumn(this, options);
57         }
58 
59         /// <summary>
60         /// creates a column with random size/precision/scale values, where applicable.
61         /// </summary>
62         /// <remarks>this method is overridden for some types to create columns with random size/precision</remarks>
CreateRandomColumn(SqlRandomizer rand, SqlRandomColumnOptions options)63         public virtual SqlRandomTableColumn CreateRandomColumn(SqlRandomizer rand, SqlRandomColumnOptions options)
64         {
65             return CreateDefaultColumn(options);
66         }
67 
68         /// <summary>
69         /// helper method to check validity of input column
70         /// </summary>
ValidateColumnInfo(SqlRandomTableColumn columnInfo)71         protected void ValidateColumnInfo(SqlRandomTableColumn columnInfo)
72         {
73             if (columnInfo == null)
74                 throw new ArgumentNullException("columnInfo");
75 
76             if (Type != columnInfo.Type)
77                 throw new ArgumentException("Type mismatch");
78         }
79 
80         /// <summary>
81         /// Returns the size used by a column value within the row. This method is used when generating random table to ensure
82         /// the row size does not overflow
83         /// </summary>
GetInRowSize(SqlRandomTableColumn columnInfo, object value)84         public double GetInRowSize(SqlRandomTableColumn columnInfo, object value)
85         {
86             ValidateColumnInfo(columnInfo);
87 
88             if (columnInfo.IsSparse)
89             {
90                 if (value == DBNull.Value && value == null)
91                 {
92                     // null values of sparse columns do not use in-row size
93                     return 0;
94                 }
95                 else
96                 {
97                     // if sparse column has non-null value, it has an additional penalty of 4 bytes added to its storage size
98                     return 4 + GetInRowSizeInternal(columnInfo);
99                 }
100             }
101             else
102             {
103                 // not a sparse column
104                 return GetInRowSizeInternal(columnInfo);
105             }
106         }
107 
GetInRowSizeInternal(SqlRandomTableColumn columnInfo)108         protected abstract double GetInRowSizeInternal(SqlRandomTableColumn columnInfo);
109 
110         /// <summary>
111         /// gets TSQL definition of the column
112         /// </summary>
GetTSqlTypeDefinition(SqlRandomTableColumn columnInfo)113         public string GetTSqlTypeDefinition(SqlRandomTableColumn columnInfo)
114         {
115             ValidateColumnInfo(columnInfo);
116             return GetTSqlTypeDefinitionInternal(columnInfo);
117         }
118 
GetTSqlTypeDefinitionInternal(SqlRandomTableColumn columnInfo)119         protected abstract string GetTSqlTypeDefinitionInternal(SqlRandomTableColumn columnInfo);
120 
121         /// <summary>
122         /// creates random, but valued value for the type, based on the given column definition
123         /// </summary>
CreateRandomValue(SqlRandomizer rand, SqlRandomTableColumn columnInfo)124         public object CreateRandomValue(SqlRandomizer rand, SqlRandomTableColumn columnInfo)
125         {
126             ValidateColumnInfo(columnInfo);
127             return CreateRandomValueInternal(rand, columnInfo);
128         }
129 
CreateRandomValueInternal(SqlRandomizer rand, SqlRandomTableColumn columnInfo)130         protected abstract object CreateRandomValueInternal(SqlRandomizer rand, SqlRandomTableColumn columnInfo);
131 
132         /// <summary>
133         /// helper method to read character data from the reader
134         /// </summary>
ReadCharData(DbDataReader reader, int ordinal, Type asType)135         protected object ReadCharData(DbDataReader reader, int ordinal, Type asType)
136         {
137             if (reader.IsDBNull(ordinal))
138                 return DBNull.Value;
139 
140             if (asType == typeof(string))
141                 return reader.GetString(ordinal);
142             else if (asType == typeof(char[]) || asType == typeof(DBNull))
143                 return reader.GetString(ordinal).ToCharArray();
144             else
145                 throw new NotSupportedException("Wrong type: " + asType.FullName);
146         }
147 
148         /// <summary>
149         /// helper method to read byte-array data from the reader
150         /// </summary>
ReadByteArray(DbDataReader reader, int ordinal, Type asType)151         protected object ReadByteArray(DbDataReader reader, int ordinal, Type asType)
152         {
153             if (reader.IsDBNull(ordinal))
154                 return DBNull.Value;
155 
156             if (asType == typeof(byte[]) || asType == typeof(DBNull))
157                 return (byte[])reader.GetValue(ordinal);
158             else
159                 throw new NotSupportedException("Wrong type: " + asType.FullName);
160         }
161 
IsNullOrDbNull(object value)162         protected bool IsNullOrDbNull(object value)
163         {
164             return DBNull.Value.Equals(value) || value == null;
165         }
166 
167         /// <summary>
168         /// helper method to check that actual test value has same type as expectedType or it is dbnull.
169         /// </summary>
170         /// <param name="bothDbNull">set to true if both values are DbNull</param>
171         /// <returns>true if expected value is DbNull or has the expected type</returns>
CompareDbNullAndType(Type expectedType, object expected, object actual, out bool bothDbNull)172         protected bool CompareDbNullAndType(Type expectedType, object expected, object actual, out bool bothDbNull)
173         {
174             bool isNullExpected = IsNullOrDbNull(expected);
175             bool isNullActual = IsNullOrDbNull(actual);
176 
177             bothDbNull = isNullActual && isNullExpected;
178 
179             if (bothDbNull)
180                 return true;
181 
182             if (isNullActual || isNullExpected)
183                 return false; // only one is null, but not both
184 
185             if (expectedType == null)
186                 return true;
187 
188             // both not null
189             if (expectedType != expected.GetType())
190                 throw new ArgumentException("Wrong type!");
191 
192             return (expectedType == actual.GetType());
193         }
194 
195         /// <summary>
196         /// helper method to compare two byte arrays
197         /// </summary>
198         /// <remarks>I considered use of Generics here, but switched to explicit typed version due to performance overhead.
199         /// When using generic version, there is no way to quickly compare two values (expected[i] == actual[i]), and using
200         /// Equals method performs boxing, increasing the time spent on this method.</remarks>
CompareByteArray(byte[] expected, byte[] actual, bool allowIncomplete = false, byte paddingValue = 0)201         private bool CompareByteArray(byte[] expected, byte[] actual, bool allowIncomplete = false, byte paddingValue = 0)
202         {
203             if (expected.Length > actual.Length)
204             {
205                 return false;
206             }
207             else if (!allowIncomplete && expected.Length < actual.Length)
208             {
209                 return false;
210             }
211 
212             // check expected array values
213             int end = expected.Length;
214             for (int i = 0; i < end; i++)
215             {
216                 if (expected[i] != actual[i])
217                     return false;
218             }
219 
220             // check for padding in actual values
221             end = actual.Length;
222             for (int i = expected.Length; i < end; i++)
223             {
224                 // ensure rest of array are zeros
225                 if (paddingValue != actual[i])
226                 {
227                     return false;
228                 }
229             }
230 
231             return true;
232         }
233 
234         /// <summary>
235         /// helper method to compare two char arrays
236         /// </summary>
237         /// <remarks>I considered use of Generics here, but switched to explicit typed version due to performance overhead.
238         /// When using generic version, there is no way to quickly compare two values (expected[i] == actual[i]), and using
239         /// Equals method performs boxing, increasing the time spent on this method.</remarks>
CompareCharArray(char[] expected, char[] actual, bool allowIncomplete = false, char paddingValue = B)240         private bool CompareCharArray(char[] expected, char[] actual, bool allowIncomplete = false, char paddingValue = ' ')
241         {
242             if (expected.Length > actual.Length)
243             {
244                 return false;
245             }
246             else if (!allowIncomplete && expected.Length < actual.Length)
247             {
248                 return false;
249             }
250 
251             // check expected array values
252             int end = expected.Length;
253             for (int i = 0; i < end; i++)
254             {
255                 if (expected[i] != actual[i])
256                     return false;
257             }
258 
259             // check for padding in actual values
260             end = actual.Length;
261             for (int i = expected.Length; i < end; i++)
262             {
263                 // ensure rest of array are zeros
264                 if (paddingValue != actual[i])
265                 {
266                     return false;
267                 }
268             }
269 
270             return true;
271         }
272 
273         /// <summary>
274         /// helper method to compare two non-array values.
275         /// </summary>
276         protected bool CompareValues<T>(object expected, object actual) where T : struct
277         {
278             bool bothDbNull;
279             if (!CompareDbNullAndType(typeof(T), expected, actual, out bothDbNull) || bothDbNull)
280                 return bothDbNull;
281 
282             return expected.Equals(actual);
283         }
284 
285 
286         /// <summary>
287         /// validates that the actual value is DbNull or byte array and compares it to expected
288         /// </summary>
CompareByteArray(object expected, object actual, bool allowIncomplete, byte paddingValue = 0)289         protected bool CompareByteArray(object expected, object actual, bool allowIncomplete, byte paddingValue = 0)
290         {
291             bool bothDbNull;
292             if (!CompareDbNullAndType(typeof(byte[]), expected, actual, out bothDbNull) || bothDbNull)
293                 return bothDbNull;
294             return CompareByteArray((byte[])expected, (byte[])actual, allowIncomplete, paddingValue);
295         }
296 
297         /// <summary>
298         /// validates that the actual value is DbNull or char array and compares it to expected
299         /// </summary>
CompareCharArray(object expected, object actual, bool allowIncomplete, char paddingValue = B)300         protected bool CompareCharArray(object expected, object actual, bool allowIncomplete, char paddingValue = ' ')
301         {
302             bool bothDbNull;
303             if (!CompareDbNullAndType(typeof(char[]), expected, actual, out bothDbNull) || bothDbNull)
304                 return bothDbNull;
305             return CompareCharArray((char[])expected, (char[])actual, allowIncomplete, paddingValue);
306         }
307 
308         /// <summary>
309         /// helper method to reads datetime from the reader
310         /// </summary>
ReadDateTime(DbDataReader reader, int ordinal, Type asType)311         protected object ReadDateTime(DbDataReader reader, int ordinal, Type asType)
312         {
313             ValidateReadType(typeof(DateTime), asType);
314             if (reader.IsDBNull(ordinal))
315                 return DBNull.Value;
316             return reader.GetDateTime(ordinal);
317         }
318 
ValidateReadType(Type expectedType, Type readAsType)319         protected void ValidateReadType(Type expectedType, Type readAsType)
320         {
321             if (readAsType != expectedType && readAsType != typeof(DBNull))
322                 throw new ArgumentException("Wrong type: " + readAsType.FullName);
323         }
324 
325         /// <summary>
326         /// this method is called to read the value from the data reader
327         /// </summary>
Read(DbDataReader reader, int ordinal, SqlRandomTableColumn columnInfo, Type asType)328         public object Read(DbDataReader reader, int ordinal, SqlRandomTableColumn columnInfo, Type asType)
329         {
330             if (reader == null || asType == null)
331                 throw new ArgumentNullException("reader == null || asType == null");
332             ValidateColumnInfo(columnInfo);
333             return ReadInternal(reader, ordinal, columnInfo, asType);
334         }
335 
ReadInternal(DbDataReader reader, int ordinal, SqlRandomTableColumn columnInfo, Type asType)336         protected abstract object ReadInternal(DbDataReader reader, int ordinal, SqlRandomTableColumn columnInfo, Type asType);
337 
338         /// <summary>
339         /// used to check if this column can be compared; returns true by default (timestamp and column set columns return false)
340         /// </summary>
CanCompareValues(SqlRandomTableColumn columnInfo)341         public virtual bool CanCompareValues(SqlRandomTableColumn columnInfo)
342         {
343             return true;
344         }
345 
346         /// <summary>
347         /// This method is called to compare the actual value read to expected. Expected value must be either dbnull or from the given type.
348         /// Actual value can be any.
349         /// </summary>
CompareValues(SqlRandomTableColumn columnInfo, object expected, object actual)350         public bool CompareValues(SqlRandomTableColumn columnInfo, object expected, object actual)
351         {
352             ValidateColumnInfo(columnInfo);
353             return CompareValuesInternal(columnInfo, expected, actual);
354         }
355 
CompareValuesInternal(SqlRandomTableColumn columnInfo, object expected, object actual)356         protected abstract bool CompareValuesInternal(SqlRandomTableColumn columnInfo, object expected, object actual);
357 
BuildErrorMessage(SqlRandomTableColumn columnInfo, object expected, object actual)358         public string BuildErrorMessage(SqlRandomTableColumn columnInfo, object expected, object actual)
359         {
360             ValidateColumnInfo(columnInfo);
361 
362             string expectedAsString = IsNullOrDbNull(expected) ? "null" : "\"" + ValueAsString(columnInfo, expected) + "\"";
363             string actualAsString = IsNullOrDbNull(actual) ? "null" : "\"" + ValueAsString(columnInfo, actual) + "\"";
364 
365             return string.Format(CultureInfo.InvariantCulture,
366                 "  Column type: {0}\n" +
367                 "  Expected value: {1}\n" +
368                 "  Actual value: {2}",
369                 GetTSqlTypeDefinition(columnInfo),
370                 expectedAsString,
371                 actualAsString);
372         }
373 
374         /// <summary>
375         /// using ToString by default, supports arrays and many primitives;
376         /// override as needed
377         /// </summary>
378         /// <param name="value">value is not null or dbnull(validated before)</param>
ValueAsString(SqlRandomTableColumn columnInfo, object value)379         protected virtual string ValueAsString(SqlRandomTableColumn columnInfo, object value)
380         {
381             Array a = value as Array;
382             if (a == null)
383             {
384                 return string.Format(CultureInfo.InvariantCulture, "{0}: {1}", value.GetType().Name, PrimitiveValueAsString(value));
385             }
386             else
387             {
388                 StringBuilder sb = new StringBuilder();
389                 sb.AppendFormat("{0}[{1}]", value.GetType().Name, a.Length);
390                 if (a.Length > 0)
391                 {
392                     sb.Append(":");
393                     for (int i = 0; i < a.Length; i++)
394                     {
395                         sb.AppendFormat(CultureInfo.InvariantCulture, " {0}", PrimitiveValueAsString(a.GetValue(i)));
396                     }
397                 }
398 
399                 return sb.ToString();
400             }
401         }
402 
PrimitiveValueAsString(object value)403         public string PrimitiveValueAsString(object value)
404         {
405             if (value is char)
406             {
407                 int c = (int)(char)value; // double-cast is needed from object
408                 return c.ToString("X4", CultureInfo.InvariantCulture);
409             }
410             else if (value is byte)
411             {
412                 byte b = (byte)value;
413                 return b.ToString("X2", CultureInfo.InvariantCulture);
414             }
415             else
416             {
417                 return string.Format(CultureInfo.InvariantCulture, "{0}", value.ToString());
418             }
419         }
420     }
421 }
422