18using System.Collections.Generic;
20using System.Data.SqlClient;
24using System.Threading.Tasks;
29 internal class DatabaseHelper
31 public static SqlConnection OpenConnection(DatabaseSettings databaseSettings)
33 if (
null == databaseSettings.Connection)
37 if (
null != databaseSettings.ConnectionString)
38 databaseSettings.Connection =
new SqlConnection(databaseSettings.ConnectionString);
39 else if (Enums.DatabaseType.SQLServer == databaseSettings.DatabaseType)
41 StringBuilder sb =
new StringBuilder();
42 sb.Append($
"Data Source={databaseSettings.ServerName};Initial Catalog={databaseSettings.DatabaseName};");
43 if (
null == databaseSettings.UserName)
44 sb.Append(
"Integrated Security=true;");
47 sb.Append($
"User id={databaseSettings.UserName};password={databaseSettings.Password};");
49 databaseSettings.Connection =
new SqlConnection(sb.ToString());
55 if (
null == databaseSettings.Connection)
56 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
57 "Nothing said what to connect to. Set .WithConnectionString, or .WithDatabaseType(DatabaseType.SQLServer) with a server and database name",
58 databaseSettings.Database, GPALObjectType.Database);
60 databaseSettings.Connection.Open();
64 RecordFailure(databaseSettings, ex, $
"Unable to open a connection to [{databaseSettings.DatabaseName}] on [{databaseSettings.ServerName}]",
null,
null, -1);
67 return databaseSettings.Connection;
69 public static void Close(DatabaseSettings databaseSettings)
73 if (
null == databaseSettings.SqlTransaction)
75 databaseSettings.Connection.Close();
76 databaseSettings.Connection =
null;
86 public static void CloseEverything(DatabaseSettings databaseSettings)
88 if (
null != databaseSettings.SqlTransaction)
90 databaseSettings.SqlTransaction.Rollback();
91 databaseSettings.SqlTransaction =
null;
94 if (
null != databaseSettings.Connection)
96 if (ConnectionState.Closed != databaseSettings.Connection.State)
97 databaseSettings.Connection.Close();
99 databaseSettings.Connection.Dispose();
100 databaseSettings.Connection =
null;
110 private static void PrepareCommand(DatabaseSettings databaseSettings, SqlCommand command)
114 if (0 <= databaseSettings.CommandTimeoutSeconds)
115 command.CommandTimeout = databaseSettings.CommandTimeoutSeconds;
117 if (
null != databaseSettings.SqlTransaction)
118 command.Transaction = databaseSettings.SqlTransaction;
129 private static void AddParameter(SqlCommand command, DatabaseSettings.DatabaseColumn databaseColumn,
string parameterValue)
131 SqlParameter sqlParameter = command.Parameters.Add($
"@{databaseColumn.ColumnName}", databaseColumn.ColumnType, databaseColumn.ColumnLength);
136 if (0 < databaseColumn.ColumnPrecision)
137 sqlParameter.Precision = (byte)databaseColumn.ColumnPrecision;
139 if (0 < databaseColumn.ColumnScale)
140 sqlParameter.Scale = (byte)databaseColumn.ColumnScale;
142 sqlParameter.Value = ReturnParameterOfType(parameterValue, databaseColumn.ColumnType);
153 private static void AddReadParameters(DatabaseSettings databaseSettings, SqlCommand command)
155 List<string> values = 0 < (databaseSettings.ParametersGrid?.Rows ?? 0)
156 ?
new List<string>(databaseSettings.ParametersGrid[0])
157 : databaseSettings.ParametersList;
159 if (
null != command && 0 < (values?.Count ?? 0))
161 if (0 < databaseSettings.ColumnList.Count)
163 foreach (DatabaseSettings.DatabaseColumn databaseColumn in databaseSettings.ColumnList)
165 int columnIndex = databaseSettings.ColumnList.IndexOf(databaseColumn);
167 AddParameter(command, databaseColumn, values.Count > columnIndex ? values[columnIndex] :
null);
176 foreach (
string parameterName
in databaseSettings.ParameterNamesList)
178 command.Parameters.Add(
new SqlParameter(parameterName,
179 values.Count > valueIndex &&
null != values[valueIndex] ? (
object)values[valueIndex] : DBNull.Value));
195 private static SqlCommand BuildReadCommand(DatabaseSettings databaseSettings, SqlConnection sqlConnection)
197 SqlCommand cmd =
null;
202 if (
false ==
string.IsNullOrWhiteSpace(databaseSettings.VerbSQL))
204 cmd =
new SqlCommand(databaseSettings.VerbSQL, sqlConnection)
206 CommandType = CommandType.Text
212 else if (CommandType.StoredProcedure == databaseSettings.SqlCommandType &&
null != databaseSettings.StoredProcedure)
214 cmd =
new SqlCommand(databaseSettings.StoredProcedure, sqlConnection)
216 CommandType = CommandType.StoredProcedure
219 else if (
null != databaseSettings.SqlCommand)
221 cmd =
new SqlCommand(databaseSettings.SqlCommand, sqlConnection)
223 CommandType = databaseSettings.SqlCommandType
226 else if (
null != databaseSettings.ReadSQL)
228 cmd =
new SqlCommand(databaseSettings.ReadSQL, sqlConnection)
230 CommandType = databaseSettings.SqlCommandType
233 else if (
null != databaseSettings.TableName)
235 string sqlCommand =
"Select " + ((-1 != databaseSettings.RowCount) ? $
"TOP({databaseSettings.RowCount}) " :
"") + $
"* from {databaseSettings.TableName}";
237 cmd =
new SqlCommand(sqlCommand, sqlConnection)
239 CommandType = CommandType.Text
243 AddReadParameters(databaseSettings, cmd);
245 PrepareCommand(databaseSettings, cmd);
258 private static SqlCommand ReadCommandOrNothing(DatabaseSettings databaseSettings, SqlConnection sqlConnection)
260 SqlCommand retVal =
null;
262 if (
null == sqlConnection || ConnectionState.Open != sqlConnection.State)
263 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
264 $
"No open connection to [{databaseSettings.DatabaseName}] on [{databaseSettings.ServerName}], so nothing was read",
265 databaseSettings.Database, GPALObjectType.Database);
268 retVal = BuildReadCommand(databaseSettings, sqlConnection);
271 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
272 "Nothing said what to read. Set one of .WithSQLCommand, .WithReadAs, .WithTableName or .WithStoredProcedure",
273 databaseSettings.Database, GPALObjectType.Database);
285 public static void GetScalar(DatabaseSettings databaseSettings, out
string value)
287 string retVal =
null;
291 databaseSettings.LastError =
null;
293 SqlConnection sqlConnection = OpenConnection(databaseSettings);
294 SqlCommand cmd = ReadCommandOrNothing(databaseSettings, sqlConnection);
300 object answer = cmd.ExecuteScalar();
302 retVal = DBNull.Value == answer ? null : answer?.ToString();
304 Close(databaseSettings);
308 RecordFailure(databaseSettings, ex, $
"Failed reading with [{cmd.CommandText}]", cmd.CommandText, databaseSettings.ParametersGrid, -1);
321 public static int GetDataSet(DatabaseSettings databaseSettings, out DataSet dataSet)
323 DataSet returnDataSet =
new DataSet();
327 databaseSettings.LastError =
null;
329 SqlConnection sqlConnection = OpenConnection(databaseSettings);
330 SqlCommand cmd = ReadCommandOrNothing(databaseSettings, sqlConnection);
336 using var adapter =
new SqlDataAdapter(cmd);
337 adapter.Fill(returnDataSet);
338 Close(databaseSettings);
342 RecordFailure(databaseSettings, ex, $
"Failed reading with [{cmd.CommandText}]", cmd.CommandText, databaseSettings.ParametersGrid, -1);
346 dataSet = returnDataSet;
347 return dataSet.Tables.Count;
353 public static int GetGrid(DatabaseSettings databaseSettings, out IGPALGrid<string> dataGrid)
355 IGPALGrid<string> returnDataGrid = GPAL.Grid.ToGPALObject();
356 GetDataSet(databaseSettings, out DataSet dataSet);
358 var rowCountsPerTable =
new List<int>();
360 foreach (DataTable table
in dataSet.Tables)
362 if (databaseSettings.ColumnList.Count == 0)
364 foreach (DataColumn column
in table.Columns)
366 SqlDbType sqlDbType = GetSqlDbTypeFromClrType(column.DataType);
367 databaseSettings.ColumnList.Add(
new DatabaseSettings.DatabaseColumn(column.ColumnName, sqlDbType));
371 int rowsAddedForTable = 0;
372 foreach (DataRow row
in table.Rows)
374 if (0 < databaseSettings.RowCount && rowsAdded >= databaseSettings.RowCount)
376 returnDataGrid.AddRow(row.ItemArray.Select(v => v.ToString()).ToList());
380 rowCountsPerTable.Add(rowsAddedForTable);
382 databaseSettings.LastGridRowCountsPerTable = rowCountsPerTable;
384 dataGrid = returnDataGrid;
385 return dataGrid.Count();
391 public static int GetDataTable(DatabaseSettings databaseSettings, out DataTable dataTable)
393 GetDataSet(databaseSettings, out DataSet dataSet);
395 if (dataSet.Tables.Count > 1)
397 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
398 $
"Query returned [{dataSet.Tables.Count}] result sets; SaveTo(out DataTable) only keeps the first. Use SaveTo(out DataSet) to get all of them.",
399 databaseSettings.Database, GPALObjectType.Database);
402 dataTable = dataSet.Tables.Count > 0 ? dataSet.Tables[0] :
new DataTable();
403 return dataTable.Rows.Count;
413 internal static string BuildInsertStatement(DatabaseSettings databaseSettings)
415 string retVal =
null;
416 bool hasTable =
false ==
string.IsNullOrWhiteSpace(databaseSettings.TableName);
417 bool hasColumns = 0 < databaseSettings.ColumnList.Count;
422 if (
false == hasTable ||
false == hasColumns)
424 if (
true == hasTable ||
true == hasColumns)
425 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
426 "An insert needs a table name and at least one parameter to write. Set .WithTableName and one of the parameter calls",
427 databaseSettings.Database, GPALObjectType.Database);
434 string columns =
string.Join(
", ", databaseSettings.ColumnList.Select(databaseColumn => databaseColumn.ColumnName));
435 string parameters =
string.Join(
", ", databaseSettings.ColumnList.Select(databaseColumn => $
"@{databaseColumn.ColumnName}"));
437 retVal = $
"INSERT INTO {databaseSettings.TableName} ({columns}) VALUES ({parameters})";
451 internal static string BuildWriteStatement(DatabaseSettings databaseSettings)
453 string retVal =
false ==
string.IsNullOrWhiteSpace(databaseSettings.VerbSQL)
454 ? databaseSettings.VerbSQL
455 : databaseSettings.SqlCommand;
457 if (
string.IsNullOrWhiteSpace(retVal) && DatabaseSettings.CrudVerb.Create == databaseSettings.Verb)
458 retVal = BuildInsertStatement(databaseSettings);
470 internal static int ExecuteSQL(DatabaseSettings databaseSettings,
string sqlToRun, IGPALGrid<string> parameterTokens =
null)
472 return ExecuteSQL(databaseSettings, sqlToRun, parameterTokens, out _);
476 internal static int ExecuteSQL(DatabaseSettings databaseSettings,
string sqlToRun, IGPALGrid<string> parameterTokens, out List<int> rowCountsPerStatement)
480 databaseSettings.LastError =
null;
486 CommandType commandType = sqlToRun == databaseSettings.SqlCommand
487 ? databaseSettings.SqlCommandType
490 using var sqlCommand =
new SqlCommand(sqlToRun, databaseSettings.Connection ?? OpenConnection(databaseSettings))
492 CommandType = commandType
495 PrepareCommand(databaseSettings, sqlCommand);
498 int rowBeingWritten = -1;
499 bool ranPerRow =
false;
500 var statementCounts =
new List<int>();
502 sqlCommand.StatementCompleted += (s, e) => statementCounts.Add(e.RecordCount);
506 if (
null == sqlCommand.Connection || ConnectionState.Open != sqlCommand.Connection.State)
507 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
508 $
"No open connection to [{databaseSettings.DatabaseName}] on [{databaseSettings.ServerName}], so nothing was run",
509 databaseSettings.Database, GPALObjectType.Database);
510 else if (
string.IsNullOrWhiteSpace(sqlToRun))
511 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
512 "Nothing said what to run. Set one of .WithSQLCommand, .WithCreateAs, .WithUpdateAs, .WithDeleteAs or .WithStoredProcedure, or give .Create a .WithTableName and its parameters to write",
513 databaseSettings.Database, GPALObjectType.Database);
518 if (parameterTokens !=
null && parameterTokens.Rows > 0)
520 var structuredColumn = databaseSettings.ColumnList.FirstOrDefault(c => c.ColumnType == SqlDbType.Structured);
521 if (structuredColumn !=
null)
523 if (
string.IsNullOrEmpty(structuredColumn.TvTableTypeName))
524 throw new InvalidOperationException($
"UDTT Type Name required for Table Value column @[{structuredColumn.ColumnName}]. Use .WithUDDTName()");
526 var dataTable =
new DataTable();
527 int columnCount = parameterTokens.Columns;
529 for (
int i = 0; i < columnCount; i++)
531 dataTable.Columns.Add($
"Column{i}", typeof(
string));
534 foreach (var row
in parameterTokens)
536 if (row.Count != columnCount)
538 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
539 $
"Row [{row}] has [{row.Count}] values, but expected [{columnCount}]. Skipping row.",
540 parameterTokens, GPALObjectType.Other);
543 dataTable.Rows.Add(row.ToArray());
546 var param = sqlCommand.Parameters.AddWithValue($
"@{structuredColumn.ColumnName}", dataTable);
547 param.SqlDbType = SqlDbType.Structured;
548 param.TypeName = structuredColumn.TvTableTypeName;
556 foreach (var parameters
in parameterTokens)
559 sqlCommand.Parameters.Clear();
561 foreach (var databaseColumn
in databaseSettings.ColumnList)
563 int columnIndex = databaseSettings.ColumnList.IndexOf(databaseColumn);
564 string paramValue = parameters.Count > columnIndex ? parameters[columnIndex] :
null;
566 AddParameter(sqlCommand, databaseColumn, paramValue);
569 rowCount += sqlCommand.ExecuteNonQuery();
578 if (
false == ranPerRow)
579 rowCount = sqlCommand.ExecuteNonQuery();
584 RecordFailure(databaseSettings, ex, $
"Failed running [{sqlToRun}]", sqlToRun, parameterTokens, rowBeingWritten);
588 rowCountsPerStatement = statementCounts;
601 private static void RecordFailure(DatabaseSettings databaseSettings, Exception ex,
string message,
string sql, IGPALGrid<string> parameterTokens,
int row)
603 databaseSettings.LastError =
new GPALDatabase.DatabaseError
605 Number = ex is SqlException sqlException ? sqlException.Number : 0,
606 Message = ex.Message,
608 Parameters = parameterTokens,
610 RowValues = 0 <= row && row < (parameterTokens?.Rows ?? 0) ?
new List<string>(parameterTokens[row]) : null,
611 Occurred = DateTime.Now,
620 GPAL.PublishSimpleEvent(ex is SqlException ? GPALEventType.ERROR : GPALEventType.EXCEPTION,
621 message, databaseSettings.Database, GPALObjectType.Database, ex);
623 CallOnErrorHandler(databaseSettings);
626 private static void CallOnErrorHandler(DatabaseSettings databaseSettings)
628 if (
null != databaseSettings.CallOnErrorHandler)
629 databaseSettings.CallOnErrorHandler(databaseSettings.Database);
631 if (
null != databaseSettings.SqlTransaction)
633 databaseSettings.SqlTransaction.Rollback();
634 databaseSettings.SqlTransaction =
null;
637 if (
null != databaseSettings.Connection && ConnectionState.Open == databaseSettings.Connection.State)
639 databaseSettings.Connection.Close();
640 databaseSettings.Connection =
null;
643 static SqlDbType GetSqlDbTypeFromName(
string dataTypeName)
645 switch (dataTypeName.ToLower())
648 return SqlDbType.Char;
650 return SqlDbType.NChar;
652 return SqlDbType.NText;
654 return SqlDbType.NVarChar;
656 return SqlDbType.Text;
658 return SqlDbType.VarChar;
660 return SqlDbType.Xml;
662 return SqlDbType.Date;
664 return SqlDbType.DateTime;
666 return SqlDbType.DateTime2;
667 case "smalldatetime":
668 return SqlDbType.SmallDateTime;
670 return SqlDbType.Time;
672 return SqlDbType.Timestamp;
674 return SqlDbType.Decimal;
676 return SqlDbType.Money;
678 return SqlDbType.SmallMoney;
680 return SqlDbType.Float;
682 return SqlDbType.Real;
684 return SqlDbType.Image;
686 return SqlDbType.Bit;
688 return SqlDbType.Int;
690 return SqlDbType.SmallInt;
693 throw new ArgumentException(
"Unknown SqlDbType: " + dataTypeName);
700 static SqlDbType GetSqlDbTypeFromClrType(Type clrType)
702 switch (Type.GetTypeCode(clrType))
704 case TypeCode.String:
return SqlDbType.NVarChar;
705 case TypeCode.Boolean:
return SqlDbType.Bit;
706 case TypeCode.Byte:
return SqlDbType.TinyInt;
707 case TypeCode.Int16:
return SqlDbType.SmallInt;
708 case TypeCode.Int32:
return SqlDbType.Int;
709 case TypeCode.Int64:
return SqlDbType.BigInt;
710 case TypeCode.Single:
return SqlDbType.Real;
711 case TypeCode.Double:
return SqlDbType.Float;
712 case TypeCode.Decimal:
return SqlDbType.Decimal;
713 case TypeCode.DateTime:
return SqlDbType.DateTime;
715 if (clrType == typeof(Guid))
return SqlDbType.UniqueIdentifier;
716 if (clrType == typeof(
byte[]))
return SqlDbType.VarBinary;
717 if (clrType == typeof(TimeSpan))
return SqlDbType.Time;
718 if (clrType == typeof(DateTimeOffset))
return SqlDbType.DateTimeOffset;
719 return SqlDbType.NVarChar;
722 private static object ReturnParameterOfType(
string parmValue, SqlDbType sqlDbType)
724 if (
string.IsNullOrEmpty(parmValue))
726 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
727 $
"Parameter value is null or empty for SqlDbType [{sqlDbType}]. Returning DBNull.Value.",
728 null, GPALObjectType.Other);
737 case SqlDbType.NChar:
738 case SqlDbType.NText:
739 case SqlDbType.NVarChar:
741 case SqlDbType.VarChar:
746 case SqlDbType.DateTime:
747 case SqlDbType.DateTime2:
748 case SqlDbType.SmallDateTime:
749 return DateTime.TryParse(parmValue, out DateTime dateResult) ? (object)dateResult : DBNull.Value;
755 if (TimeSpan.TryParse(parmValue, out TimeSpan timeResult))
758 return DateTime.TryParse(parmValue, out DateTime timeOfDayResult) ? (object)timeOfDayResult.TimeOfDay : DBNull.Value;
760 case SqlDbType.DateTimeOffset:
761 return DateTimeOffset.TryParse(parmValue, out DateTimeOffset offsetResult) ? (object)offsetResult : DBNull.Value;
763 case SqlDbType.Decimal:
764 case SqlDbType.Money:
765 case SqlDbType.SmallMoney:
766 return Decimal.TryParse(parmValue, out decimal decimalResult) ? (object)decimalResult : DBNull.Value;
770 case SqlDbType.Float:
771 return double.TryParse(parmValue, out
double doubleResult) ? (object)doubleResult : DBNull.Value;
774 return float.TryParse(parmValue, out
float floatResult) ? (object)floatResult : DBNull.Value;
777 return bool.TryParse(parmValue, out
bool boolResult) ? (object)boolResult : (int.TryParse(parmValue, out int intBitResult) ? (object)(intBitResult != 0) : DBNull.Value);
780 case SqlDbType.SmallInt:
781 return int.TryParse(parmValue, out
int intResult) ? (
object)intResult : DBNull.Value;
783 case SqlDbType.BigInt:
784 return long.TryParse(parmValue, out
long longResult) ? (
object)longResult : DBNull.Value;
786 case SqlDbType.UniqueIdentifier:
787 return Guid.TryParse(parmValue, out Guid guidResult) ? (
object)guidResult : DBNull.Value;
789 case SqlDbType.Image:
790 case SqlDbType.VarBinary:
791 case SqlDbType.Binary:
794 return Convert.FromBase64String(parmValue);
796 catch (FormatException ex)
798 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
799 $
"Failed to decode Base64 string [{parmValue}] for SqlDbType [{sqlDbType}]. Returning DBNull.Value.",
800 null, GPALObjectType.Other, ex);
804 case SqlDbType.Timestamp:
807 if (ulong.TryParse(parmValue, out ulong ulongResult))
809 return BitConverter.GetBytes(ulongResult).Reverse().ToArray();
811 throw new FormatException(
"Invalid numeric string for Timestamp");
815 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
816 $
"Failed to parse [{parmValue}] as Timestamp. Returning DBNull.Value.",
817 null, GPALObjectType.Other, ex);
821 case SqlDbType.Variant:
822 if (
int.TryParse(parmValue, out
int variantIntResult))
823 return variantIntResult;
824 if (decimal.TryParse(parmValue, out decimal variantDecResult))
825 return variantDecResult;
826 if (DateTime.TryParse(parmValue, out DateTime variantDateResult))
827 return variantDateResult;
828 if (Guid.TryParse(parmValue, out Guid variantGuidResult))
829 return variantGuidResult;
832 case SqlDbType.Structured:
833 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
834 $
"SqlDbType.Structured is not supported for individual parameter values. Use a multi-row GPALGrid<string> in ExecuteSQL. Returning DBNull.Value.",
835 null, GPALObjectType.Other);
839 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
840 $
"Unsupported SqlDbType [{sqlDbType}] for parameter value [{parmValue}]. Returning DBNull.Value.",
841 null, GPALObjectType.Other);
847 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
848 $
"Failed to convert parameter value [{parmValue}] to SqlDbType [{sqlDbType}]. Returning DBNull.Value.",
849 null, GPALObjectType.Other, ex);
857 internal static void TokenizeDatabase(UnitOfWork currentUOW, IGPALDatabase inputDatabase)
859 IGPALGrid<string> tokens;
860 if (
null != currentUOW)
861 currentUOW.InputDatabase = inputDatabase;
862 int rowCount = DatabaseHelper.GetGrid(((GPALDatabase)inputDatabase).DatabaseSettings, out tokens);
863 ((GPALDatabase)inputDatabase).Tokens = tokens;
872 public static IGPALGrid<string> DataTableToGrid(DataTable dataTable, out List<DatabaseSettings.DatabaseColumn> columnsList)
874 IGPALGrid<string> retVal = GPAL.Grid.ToGPALObject();
876 columnsList =
new List<DatabaseSettings.DatabaseColumn>();
878 if (
null != dataTable)
880 foreach (DataColumn column
in dataTable.Columns)
881 columnsList.Add(
new DatabaseSettings.DatabaseColumn(column.ColumnName, GetSqlDbTypeFromClrType(column.DataType)));
883 foreach (DataRow row
in dataTable.Rows)
884 retVal.AddRow(row.ItemArray.Select(value => DBNull.Value == value ?
null : value?.ToString()).ToList());
889 public static void TokenizeDatabase(GPALDatabase inputDatabase)
891 IGPALGrid<string> tokens;
892 int rowCount = DatabaseHelper.GetGrid(inputDatabase.DatabaseSettings, out tokens);
893 inputDatabase.Tokens = tokens;
901 public static class DataTableExtensions
903 public static void SetColumnsOrder(
this DataTable table, params String[] columnNames)
906 foreach (var columnName
in columnNames)
908 if (table.Columns.Contains(columnName))
910 table.Columns[columnName].SetOrdinal(columnIndex);