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.IO;
6 using System.Collections;
7 using System.Collections.Generic;
8 using System.Text;
9 using System.Diagnostics;
10 
11 namespace System.Data.SqlClient.ManualTesting.Tests
12 {
13     /// <summary>
14     /// represents table with random column types and values in it
15     /// </summary>
16     public class SqlRandomTable
17     {
18         // "Row-Overflow Data Exceeding 8 KB"
19         private const int MaxBytesPerRow = 8060;
20         // however, with sparse columns the limit drops to 8018
21         private const int MaxBytesPerRowWithSparse = 8018;
22 
23         // SQL Server uses 6 bytes per-row to store row info (null-bitmap overhead is calculated based on column size)
24         private const int ConstantOverhead = 6;
25 
26         // SQL does not allow table creation with more than 1024 non-sparse columns
27         private const int MaxNonSparseColumns = 1024;
28 
29         // cannot send more than 2100 parameters in one command
30         private const int MaxParameterCount = 2100;
31 
32         /// <summary>
33         /// column types
34         /// </summary>
35         private readonly SqlRandomTableColumn[] _columns;
36         private readonly string[] _columnNames;
37 
38         public readonly IList<SqlRandomTableColumn> Columns;
39         public readonly IList<string> ColumnNames;
40         public readonly int? PrimaryKeyColumnIndex;
41         public readonly bool HasSparseColumns;
42         public readonly double NonSparseValuesTotalSize;
43 
44         /// <summary>
45         /// maximum size of the row allowed for this column (depends if it has sparse columns or not)
46         /// </summary>
47         public int MaxRowSize
48         {
49             get
50             {
51                 if (HasSparseColumns)
52                     return MaxBytesPerRowWithSparse;
53                 else
54                     return MaxBytesPerRow;
55             }
56         }
57 
58         /// <summary>
59         /// rows and their values
60         /// </summary>
61         private readonly List<object[]> _rows;
62 
63         public IList<object> this[int row]
64         {
65             get
66             {
67                 return new List<object>(_rows[row]).AsReadOnly();
68             }
69         }
70 
SqlRandomTable(SqlRandomTableColumn[] columns, int? primaryKeyColumnIndex = null, int estimatedRowCount = 0)71         public SqlRandomTable(SqlRandomTableColumn[] columns, int? primaryKeyColumnIndex = null, int estimatedRowCount = 0)
72         {
73             if (columns == null || columns.Length == 0)
74                 throw new ArgumentException("non-empty type array is required");
75             if (estimatedRowCount < 0)
76                 throw new ArgumentOutOfRangeException("non-negative row count is required, use 0 for default");
77             if (primaryKeyColumnIndex.HasValue && (primaryKeyColumnIndex.Value < 0 || primaryKeyColumnIndex.Value >= columns.Length))
78                 throw new ArgumentOutOfRangeException("primaryColumnIndex");
79 
80             PrimaryKeyColumnIndex = primaryKeyColumnIndex;
81             _columns = (SqlRandomTableColumn[])columns.Clone();
82             _columnNames = new string[columns.Length];
83             if (estimatedRowCount == 0)
84                 _rows = new List<object[]>();
85             else
86                 _rows = new List<object[]>(estimatedRowCount);
87 
88             Columns = new List<SqlRandomTableColumn>(_columns).AsReadOnly();
89             ColumnNames = new List<string>(_columnNames).AsReadOnly();
90             bool hasSparse = false;
91             double totalNonSparse = 0;
92             foreach (var c in _columns)
93             {
94                 if (c.IsSparse)
95                 {
96                     hasSparse = true;
97                 }
98                 else
99                 {
100                     totalNonSparse += c.GetInRowSize(null); // for non-sparse columns size does not depend on the value
101                 }
102             }
103             HasSparseColumns = hasSparse;
104             NonSparseValuesTotalSize = totalNonSparse;
105         }
106 
GetColumnName(int c)107         public string GetColumnName(int c)
108         {
109             if (_columnNames[c] == null)
110             {
111                 AutoGenerateColumnNames();
112             }
113 
114             return _columnNames[c];
115         }
116 
GetColumnTSqlType(int c)117         public string GetColumnTSqlType(int c)
118         {
119             return _columns[c].GetTSqlTypeDefinition();
120         }
121 
122         /// <summary>
123         /// adds new row with random values, each column has 50% chance to have null value
124         /// </summary>
AddRow(SqlRandomizer rand)125         public void AddRow(SqlRandomizer rand)
126         {
127             BitArray nullBitmap = rand.NextBitmap(_columns.Length);
128             AddRow(rand, nullBitmap);
129         }
130 
IsPrimaryKey(int c)131         private bool IsPrimaryKey(int c)
132         {
133             return PrimaryKeyColumnIndex.HasValue && PrimaryKeyColumnIndex.Value == c;
134         }
135 
136         /// <summary>
137         /// adds a new row with random values and specified null bitmap
138         /// </summary>
AddRow(SqlRandomizer rand, BitArray nullBitmap)139         public void AddRow(SqlRandomizer rand, BitArray nullBitmap)
140         {
141             object[] row = new object[_columns.Length];
142 
143             // non-sparse columns will always take fixed size, must add them now
144             double rowSize = NonSparseValuesTotalSize;
145             double maxRowSize = MaxRowSize;
146 
147             if (PrimaryKeyColumnIndex.HasValue)
148             {
149                 // make sure pkey is set first
150                 // ignore the null bitmap in this case
151                 object pkeyValue = _rows.Count;
152                 row[PrimaryKeyColumnIndex.Value] = pkeyValue;
153             }
154 
155             for (int c = 0; c < row.Length; c++)
156             {
157                 if (IsPrimaryKey(c))
158                 {
159                     // handled above
160                     continue;
161                 }
162 
163                 if (SkipOnInsert(c))
164                 {
165                     row[c] = null; // this null value should not be used, assert triggered if it is
166                 }
167                 else if (rowSize >= maxRowSize)
168                 {
169                     // reached the limit, cannot add more for this row
170                     row[c] = DBNull.Value;
171                 }
172                 else if (nullBitmap[c])
173                 {
174                     row[c] = DBNull.Value;
175                 }
176                 else
177                 {
178                     object value = _columns[c].CreateRandomValue(rand);
179                     if (value == null || value == DBNull.Value)
180                     {
181                         row[c] = DBNull.Value;
182                     }
183                     else if (IsSparse(c))
184                     {
185                         // check if the value fits
186                         double newRowSize = rowSize + _columns[c].GetInRowSize(value);
187                         if (newRowSize > maxRowSize)
188                         {
189                             // cannot fit it, zero this one and try to fit next column
190                             row[c] = DBNull.Value;
191                         }
192                         else
193                         {
194                             // the value is OK, keep it
195                             row[c] = value;
196                             rowSize = newRowSize;
197                         }
198                     }
199                     else
200                     {
201                         // non-sparse values are already counted in NonSparseValuesTotalSize
202                         row[c] = value;
203                     }
204                 }
205             }
206 
207             _rows.Add(row);
208         }
209 
AddRows(SqlRandomizer rand, int rowCount)210         public void AddRows(SqlRandomizer rand, int rowCount)
211         {
212             for (int i = 0; i < rowCount; i++)
213                 AddRow(rand);
214         }
215 
GenerateCreateTableTSql(string tableName)216         public string GenerateCreateTableTSql(string tableName)
217         {
218             StringBuilder tsql = new StringBuilder();
219 
220             tsql.AppendFormat("CREATE TABLE {0} (", tableName);
221 
222             for (int c = 0; c < _columns.Length; c++)
223             {
224                 if (c != 0)
225                     tsql.Append(", ");
226 
227                 tsql.AppendFormat("[{0}] {1}", GetColumnName(c), GetColumnTSqlType(c));
228                 if (IsPrimaryKey(c))
229                 {
230                     tsql.Append(" PRIMARY KEY");
231                 }
232                 else if (IsSparse(c))
233                 {
234                     tsql.Append(" SPARSE NULL");
235                 }
236                 else if (IsColumnSet(c))
237                 {
238                     tsql.Append(" COLUMN_SET FOR ALL_SPARSE_COLUMNS NULL");
239                 }
240                 else
241                 {
242                     tsql.Append(" NULL");
243                 }
244             }
245 
246             tsql.AppendFormat(") ON [PRIMARY]");
247 
248             return tsql.ToString();
249         }
250 
DumpColumnsInfo(TextWriter output)251         public void DumpColumnsInfo(TextWriter output)
252         {
253             for (int i = 0; i < _columnNames.Length - 1; i++)
254             {
255                 output.Write(_columnNames[i]);
256                 if (_columns[i].StorageSize.HasValue)
257                     output.Write(",  [StorageSize={0}]", _columns[i].StorageSize.Value);
258                 if (_columns[i].Precision.HasValue)
259                     output.Write(",  [Precision={0}]", _columns[i].Precision.Value);
260                 if (_columns[i].Scale.HasValue)
261                     output.Write(",  [Scale={0}]", _columns[i].Scale.Value);
262                 output.WriteLine();
263             }
264         }
265 
DumpRow(TextWriter output, object[] row)266         public void DumpRow(TextWriter output, object[] row)
267         {
268             if (row == null || row.Length != _columns.Length)
269                 throw new ArgumentException("Row length does not match the columns");
270 
271             for (int i = 0; i < _columnNames.Length - 1; i++)
272             {
273                 object val = row[i];
274                 string type;
275                 if (val == null)
276                 {
277                     val = "<dbnull>";
278                     type = "";
279                 }
280                 else
281                 {
282                     type = val.GetType().Name;
283                     if (val is Array)
284                     {
285                         val = string.Format("[Length={0}]", ((Array)val).Length);
286                     }
287                     else if (val is string)
288                     {
289                         val = string.Format("[Length={0}]", ((string)val).Length);
290                     }
291                     else
292                     {
293                         val = string.Format("[{0}]", val);
294                     }
295                 }
296 
297                 output.WriteLine("[{0}] = {1}", _columnNames[i], val);
298             }
299         }
300 
SkipOnInsert(int c)301         private bool SkipOnInsert(int c)
302         {
303             if (_columns[c].Type == SqlDbType.Timestamp)
304             {
305                 // cannot insert timestamp
306                 return true;
307             }
308 
309             if (IsColumnSet(c))
310             {
311                 // skip column set, using sparse columns themselves
312                 return true;
313             }
314 
315             // OK to insert value
316             return false;
317         }
318 
GenerateTableOnServer(SqlConnection con, string tableName)319         public void GenerateTableOnServer(SqlConnection con, string tableName)
320         {
321             // create table
322             SqlCommand cmd = con.CreateCommand();
323             cmd.CommandType = CommandType.Text;
324             cmd.CommandText = GenerateCreateTableTSql(tableName);
325 
326             cmd.ExecuteNonQuery();
327 
328             InsertRows(con, tableName, 0, _rows.Count);
329         }
330 
InsertRows(SqlConnection con, string tableName, int rowFrom, int rowToExclusive)331         public void InsertRows(SqlConnection con, string tableName, int rowFrom, int rowToExclusive)
332         {
333             if (con == null || tableName == null)
334             {
335                 throw new ArgumentNullException("connection and table name must be valid");
336             }
337 
338             if (rowToExclusive > _rows.Count)
339             {
340                 throw new ArgumentOutOfRangeException("rowToExclusive", rowToExclusive, "cannot be greater than the row count");
341             }
342 
343             if (rowFrom < 0 || rowFrom > rowToExclusive)
344             {
345                 throw new ArgumentOutOfRangeException("rowFrom", rowFrom, "cannot be less than 0 or greater than rowToExclusive");
346             }
347 
348             SqlCommand cmd = null;
349             SqlParameter[] parameters = null;
350             for (int r = rowFrom; r < rowToExclusive; r++)
351             {
352                 InsertRowInternal(con, ref cmd, ref parameters, tableName, r);
353             }
354         }
355 
InsertRow(SqlConnection con, string tableName, int row)356         public void InsertRow(SqlConnection con, string tableName, int row)
357         {
358             if (con == null || tableName == null)
359             {
360                 throw new ArgumentNullException("connection and table name must be valid");
361             }
362 
363             if (row < 0 || row >= _rows.Count)
364             {
365                 throw new ArgumentOutOfRangeException("row", row, "cannot be less than 0 or greater than or equal to row count");
366             }
367 
368             SqlCommand cmd = null;
369             SqlParameter[] parameters = null;
370             InsertRowInternal(con, ref cmd, ref parameters, tableName, row);
371         }
372 
InsertRowInternal(SqlConnection con, ref SqlCommand cmd, ref SqlParameter[] parameters, string tableName, int row)373         private void InsertRowInternal(SqlConnection con, ref SqlCommand cmd, ref SqlParameter[] parameters, string tableName, int row)
374         {
375             // cannot use DataTable: it does not handle well char[] values and variant and also does not support sparse/column set ones
376             StringBuilder columnsText = new StringBuilder();
377             StringBuilder valuesText = new StringBuilder();
378 
379             // create the command and parameters on first call, reuse afterwards (to reduces table creation overhead)
380             if (cmd == null)
381             {
382                 cmd = con.CreateCommand();
383                 cmd.CommandType = CommandType.Text;
384             }
385             else
386             {
387                 // need to unbind existing parameters and re-add the next set of values
388                 cmd.Parameters.Clear();
389             }
390 
391             if (parameters == null)
392             {
393                 parameters = new SqlParameter[_columns.Length];
394             }
395 
396             object[] rowValues = _rows[row];
397 
398             // there is a limit of parameters to be sent (2010)
399             for (int ci = 0; ci < _columns.Length; ci++)
400             {
401                 if (cmd.Parameters.Count >= MaxParameterCount)
402                 {
403                     // reached the limit of max parameters, cannot continue
404                     // theoretically, we could do INSERT + UPDATE. practically, chances for this to happen are almost none since nulls are skipped
405                     rowValues[ci] = DBNull.Value;
406                     continue;
407                 }
408 
409                 if (SkipOnInsert(ci))
410                 {
411                     // cannot insert timestamp
412                     // insert of values into columnset columns are also not supported (use sparse columns themselves)
413                     continue;
414                 }
415 
416                 bool isNull = (rowValues[ci] == DBNull.Value || rowValues[ci] == null);
417 
418                 if (isNull)
419                 {
420                     // columns such as sparse cannot have DEFAULT constraint, thus it is safe to ignore the value of the column when inserting new row
421                     // this also significantly reduces number of columns updated during insert, to prevent "The number of target
422                     // columns that are specified in an INSERT, UPDATE, or MERGE statement exceeds the maximum of 4096."
423                     continue;
424                 }
425 
426                 SqlParameter p = parameters[ci];
427 
428                 // construct column list
429                 if (columnsText.Length > 0)
430                 {
431                     columnsText.Append(", ");
432                     valuesText.Append(", ");
433                 }
434 
435                 columnsText.AppendFormat("[{0}]", _columnNames[ci]);
436 
437                 if (p == null)
438                 {
439                     p = cmd.CreateParameter();
440                     p.ParameterName = "@p" + ci;
441                     p.SqlDbType = _columns[ci].Type;
442 
443                     parameters[ci] = p;
444                 }
445 
446                 p.Value = rowValues[ci] ?? DBNull.Value;
447 
448                 cmd.Parameters.Add(p);
449 
450                 valuesText.Append(p.ParameterName);
451             }
452 
453             Debug.Assert(columnsText.Length > 0, "Table that have only TIMESTAMP, ColumnSet or Sparse columns are not allowed - use primary key in this case");
454 
455             cmd.CommandText = string.Format("INSERT INTO {0} ( {1} ) VALUES ( {2} )", tableName, columnsText, valuesText);
456 
457             cmd.ExecuteNonQuery();
458         }
459 
460         /// <summary>
461         /// generates SELECT statement; if columnIndices is null the statement will include all the columns
462         /// </summary>
GenerateSelectFromTableTSql(string tableName, StringBuilder selectBuilder, int[] columnIndices = null, int indicesOffset = -1, int indicesCount = -1)463         public int GenerateSelectFromTableTSql(string tableName, StringBuilder selectBuilder, int[] columnIndices = null, int indicesOffset = -1, int indicesCount = -1)
464         {
465             if (tableName == null || selectBuilder == null)
466                 throw new ArgumentNullException("tableName == null || selectBuilder == null");
467 
468             int maxIndicesLength = (columnIndices == null) ? _columns.Length : columnIndices.Length;
469             if (indicesOffset == -1)
470             {
471                 indicesOffset = 0;
472             }
473             else if (indicesOffset < 0 || indicesOffset >= maxIndicesLength)
474             {
475                 throw new ArgumentOutOfRangeException("indicesOffset");
476             }
477 
478             if (indicesCount == -1)
479             {
480                 indicesCount = maxIndicesLength;
481             }
482             else if (indicesCount < 1 || (indicesCount + indicesOffset) > maxIndicesLength)
483             {
484                 // at least one index required
485                 throw new ArgumentOutOfRangeException("indicesCount");
486             }
487 
488             double totalRowSize = 0;
489             int countAdded = 0;
490 
491             // append the first
492             int columnIndex = (columnIndices == null) ? indicesOffset : columnIndices[indicesOffset];
493             selectBuilder.AppendFormat("SELECT [{0}]", _columnNames[columnIndex]);
494             totalRowSize += _columns[columnIndex].GetInRowSize(null);
495             countAdded++;
496 
497             // append the rest, if any
498             int end = indicesOffset + indicesCount;
499             for (int c = indicesOffset + 1; c < end; c++)
500             {
501                 columnIndex = (columnIndices == null) ? c : columnIndices[c];
502                 totalRowSize += _columns[columnIndex].GetInRowSize(null);
503                 if (totalRowSize > MaxRowSize)
504                 {
505                     // overflow - stop now
506                     break;
507                 }
508 
509                 selectBuilder.AppendFormat(", [{0}]", _columnNames[columnIndex]);
510                 countAdded++;
511             }
512 
513             selectBuilder.AppendFormat(" FROM {0}", tableName);
514 
515             if (PrimaryKeyColumnIndex.HasValue)
516                 selectBuilder.AppendFormat(" ORDER BY [{0}]", _columnNames[PrimaryKeyColumnIndex.Value]);
517 
518             return countAdded;
519         }
520 
521         #region static helper methods
522 
GetRowOverhead(int columnSize)523         private static int GetRowOverhead(int columnSize)
524         {
525             int nullBitmapSize = (columnSize + 7) / 8;
526             return ConstantOverhead + nullBitmapSize;
527         }
528 
529         // once we have only this size left on the row, column set column is forced
530         // 40 is an XML and variant size
531         private static readonly int s_columnSetSafetyRange = SqlRandomTypeInfo.XmlRowUsage * 3;
532 
533         /// <summary>
534         /// Creates random list of columns from the given source collection. The rules are:
535         /// * table cannot contain more than 1024 non-sparse columns
536         /// * total row size of non-sparse columns should not exceed 8060 or 8018 (with sparse)
537         /// * column set column must be added if number of columns in total exceeds 1024
538         /// </summary>
CreateRandTypes(SqlRandomizer rand, SqlRandomTypeInfoCollection sourceCollection, int maxColumnsCount, bool createIdColumn)539         public static SqlRandomTableColumn[] CreateRandTypes(SqlRandomizer rand, SqlRandomTypeInfoCollection sourceCollection, int maxColumnsCount, bool createIdColumn)
540         {
541             var retColumns = new List<SqlRandomTableColumn>(maxColumnsCount);
542             bool hasTimestamp = false;
543             double totalRowSize = 0;
544             int totalRegularColumns = 0;
545 
546             bool hasColumnSet = false;
547             bool hasSparseColumns = false;
548             int maxRowSize = MaxBytesPerRow; // set to MaxBytesPerRowWithSparse when sparse column is first added
549 
550             int i = 0;
551             if (createIdColumn)
552             {
553                 SqlRandomTypeInfo keyType = sourceCollection[SqlDbType.Int];
554                 SqlRandomTableColumn keyColumn = keyType.CreateDefaultColumn(SqlRandomColumnOptions.None);
555                 retColumns.Add(keyColumn);
556                 totalRowSize += keyType.GetInRowSize(keyColumn, null);
557                 i++;
558                 totalRegularColumns++;
559             }
560 
561             for (; i < maxColumnsCount; i++)
562             {
563                 // select column options (sparse/column-set)
564                 bool isSparse; // must be set in the if/else flow below
565                 bool isColumnSet = false;
566 
567                 if (totalRegularColumns >= MaxNonSparseColumns)
568                 {
569                     // reached the limit for regular columns
570 
571                     if (!hasColumnSet)
572                     {
573                         // no column-set yet, stop unconditionally
574                         // this can happen if large char/binary value brought the row size total to a limit leaving no space for column-set
575                         break;
576                     }
577 
578                     // there is a column set, enforce sparse from this point
579                     isSparse = true;
580                 }
581                 else if (i == (MaxNonSparseColumns - 1) && hasSparseColumns && !hasColumnSet)
582                 {
583                     // we almost reached the limit of regular & sparse columns with, but no column set added
584                     // to increase chances for >1024 columns, enforce column set now
585                     isColumnSet = true;
586                     isSparse = false;
587                 }
588                 else if (totalRowSize > MaxBytesPerRowWithSparse)
589                 {
590                     Debug.Assert(totalRowSize <= MaxBytesPerRow, "size over the max limit");
591                     Debug.Assert(!hasSparseColumns, "should not have sparse columns after MaxBytesPerRowWithSparse (check maxRowSize)");
592                     // cannot insert sparse from this point
593                     isSparse = false;
594                     isColumnSet = false;
595                 }
596                 else
597                 {
598                     // check how close we are to the limit of the row size
599                     int sparseProbability;
600                     if (totalRowSize < 100)
601                     {
602                         sparseProbability = 2;
603                     }
604                     else if (totalRowSize < MaxBytesPerRowWithSparse / 2)
605                     {
606                         sparseProbability = 10;
607                     }
608                     else if (totalRowSize < (MaxBytesPerRowWithSparse - s_columnSetSafetyRange))
609                     {
610                         sparseProbability = 50;
611                     }
612                     else
613                     {
614                         // close to the row size limit, special case
615                         if (!hasColumnSet)
616                         {
617                             // if we have not added column set column yet
618                             // column-set is a regular column and its size counts towards row size, so time to add it
619                             isColumnSet = true;
620                             sparseProbability = -1; // not used
621                         }
622                         else
623                         {
624                             sparseProbability = 90;
625                         }
626                     }
627 
628                     if (!isColumnSet)
629                     {
630                         isSparse = (rand.Next(100) < sparseProbability);
631 
632                         if (!isSparse && !hasColumnSet)
633                         {
634                             // if decided to add regular column, give it a (low) chance to inject a column set at any position
635                             isColumnSet = rand.Next(100) < 1;
636                         }
637                     }
638                     else
639                     {
640                         isSparse = false;
641                     }
642                 }
643 
644                 // select the type
645                 SqlRandomTypeInfo ti;
646                 SqlRandomColumnOptions options = SqlRandomColumnOptions.None;
647 
648                 if (isSparse)
649                 {
650                     Debug.Assert(!isColumnSet, "should not have both sparse and column set flags set");
651                     ti = sourceCollection.NextSparse(rand);
652                     Debug.Assert(ti.CanBeSparseColumn, "NextSparse must return only types that can be sparse");
653                     options |= SqlRandomColumnOptions.Sparse;
654                 }
655                 else if (isColumnSet)
656                 {
657                     Debug.Assert(!hasColumnSet, "there is already a column set, we should not set isColumnSet again above");
658                     ti = sourceCollection[SqlDbType.Xml];
659                     options |= SqlRandomColumnOptions.ColumnSet;
660                 }
661                 else
662                 {
663                     // regular column
664                     ti = sourceCollection.Next(rand);
665 
666                     if (ti.Type == SqlDbType.Timestamp)
667                     {
668                         // while table can contain single timestamp column only, there is no way to insert values into it.
669                         // thus, do not allow this
670                         if (hasTimestamp || maxColumnsCount == 1)
671                         {
672                             ti = sourceCollection[SqlDbType.Int];
673                         }
674                         else
675                         {
676                             // table cannot have two timestamp columns
677                             hasTimestamp = true;
678                         }
679                     }
680                 }
681 
682                 SqlRandomTableColumn col = ti.CreateRandomColumn(rand, options);
683 
684                 if (!isSparse)
685                 {
686                     double rowSize = ti.GetInRowSize(col, DBNull.Value);
687                     int overhead = GetRowOverhead(retColumns.Count + 1); // +1 for this column
688 
689                     if (totalRowSize + rowSize + overhead > maxRowSize)
690                     {
691                         // cannot use this column
692                         // note that if this column is a column set column
693                         continue;
694                     }
695 
696                     totalRowSize += rowSize;
697                     totalRegularColumns++;
698                 }
699                 // else - sparse columns are not counted towards row size when table is created (they are when inserting new row with non-null value in the sparse column)...
700 
701                 retColumns.Add(col);
702 
703                 // after adding the column, update the state
704                 if (isColumnSet)
705                 {
706                     hasColumnSet = true;
707                 }
708 
709                 if (isSparse)
710                 {
711                     hasSparseColumns = true;
712                     maxRowSize = MaxBytesPerRowWithSparse; // reduce the max row size
713                 }
714             }
715 
716             return retColumns.ToArray();
717         }
718 
Create(SqlRandomizer rand, SqlRandomTypeInfoCollection sourceCollection, int maxColumnsCount, int rowCount, bool createPrimaryKeyColumn)719         public static SqlRandomTable Create(SqlRandomizer rand, SqlRandomTypeInfoCollection sourceCollection, int maxColumnsCount, int rowCount, bool createPrimaryKeyColumn)
720         {
721             SqlRandomTableColumn[] testTypes = CreateRandTypes(rand, sourceCollection, maxColumnsCount, createPrimaryKeyColumn);
722             SqlRandomTable table = new SqlRandomTable(testTypes, primaryKeyColumnIndex: createPrimaryKeyColumn ? (Nullable<int>)0 : null, estimatedRowCount: rowCount);
723             table.AddRows(rand, rowCount);
724             return table;
725         }
726 
AutoGenerateColumnNames()727         private void AutoGenerateColumnNames()
728         {
729             Dictionary<string, int> nameMap = new Dictionary<string, int>(_columns.Length);
730             for (int c = 0; c < _columns.Length; c++)
731             {
732                 if (_columnNames[c] == null)
733                 {
734                     // pick name that is not in table yet
735                     string name = GenerateColumnName(nameMap, c);
736                     nameMap[name] = c;
737                     _columnNames[c] = name;
738                 }
739                 else
740                 {
741                     // check for dups
742                     if (nameMap.ContainsKey(_columnNames[c]))
743                     {
744                         // should not happen now since column names are auto-generated only
745                         throw new InvalidOperationException("duplicate column names detected");
746                     }
747                 }
748             }
749         }
750 
GenerateColumnName(Dictionary<string, int> nameMap, int c)751         private string GenerateColumnName(Dictionary<string, int> nameMap, int c)
752         {
753             string baseName;
754             if (IsPrimaryKey(c))
755                 baseName = "PKEY";
756             else
757                 baseName = string.Format("C{0}_{1}", _columns[c].Type, c);
758 
759             string name = baseName;
760             int extraSuffix = 1;
761             while (nameMap.ContainsKey(name))
762             {
763                 name = string.Format("{0}_{1}", baseName, extraSuffix);
764                 ++extraSuffix;
765             }
766             return name;
767         }
768 
IsSparse(int c)769         private bool IsSparse(int c)
770         {
771             return _columns[c].IsSparse;
772         }
773 
IsColumnSet(int c)774         private bool IsColumnSet(int c)
775         {
776             return _columns[c].IsColumnSet;
777         }
778 
779         #endregion static helper methods
780     }
781 }
782