GPAL - Generally Positive Automation Library v1.0
GPAL The Fluent Automation LIbrary
Loading...
Searching...
No Matches
DatabaseHelper.cs
1// =============================================================================
2// GPAL - Generally Positive Automation Library
3// Copyright © 2026 Software Decisions, Inc. All rights reserved.
4//
5// This file is part of GPAL.
6// Licensed under the Business Source License 1.1
7//
8// Primary development, architecture, and vision by Michael B. Vederman,
9// CEO of Software Decisions, Inc., Texas.
10//
11// Internal development maintained privately.
12// Public releases appear on GitHub: https://github.com/SoftwareDecisionsInc/GPAL.
13//
14// See LICENSE for full terms, including Additional Use Grant.
15// =============================================================================
16
17using System;
18using System.Collections.Generic;
19using System.Data;
20using System.Data.SqlClient;
21using System.Drawing;
22using System.Linq;
23using System.Text;
24using System.Threading.Tasks;
25using static GenerallyPositive.Enums;
26
27namespace GenerallyPositive
28{
29 internal class DatabaseHelper
30 {
31 public static SqlConnection OpenConnection(DatabaseSettings databaseSettings)
32 {
33 if (null == databaseSettings.Connection)
34 {
35 try
36 {
37 if (null != databaseSettings.ConnectionString)
38 databaseSettings.Connection = new SqlConnection(databaseSettings.ConnectionString);
39 else if (Enums.DatabaseType.SQLServer == databaseSettings.DatabaseType)
40 {
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;");
45 else
46 {
47 sb.Append($"User id={databaseSettings.UserName};password={databaseSettings.Password};");
48 }
49 databaseSettings.Connection = new SqlConnection(sb.ToString());
50
51 }
52
53 // neither a connection string nor a database type, so there is nothing to open. calling
54 // Open on it reported a null reference, which named nothing the workflow could act on
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);
59 else
60 databaseSettings.Connection.Open();
61 }
62 catch (Exception ex)
63 {
64 RecordFailure(databaseSettings, ex, $"Unable to open a connection to [{databaseSettings.DatabaseName}] on [{databaseSettings.ServerName}]", null, null, -1);
65 }
66 }
67 return databaseSettings.Connection;
68 }
69 public static void Close(DatabaseSettings databaseSettings)
70 {
71 // a connection carrying a transaction stays open. closing it here would roll back between two
72 // statements the workflow meant to keep together
73 if (null == databaseSettings.SqlTransaction)
74 {
75 databaseSettings.Connection.Close();
76 databaseSettings.Connection = null;
77 }
78 }
79
86 public static void CloseEverything(DatabaseSettings databaseSettings)
87 {
88 if (null != databaseSettings.SqlTransaction)
89 {
90 databaseSettings.SqlTransaction.Rollback();
91 databaseSettings.SqlTransaction = null;
92 }
93
94 if (null != databaseSettings.Connection)
95 {
96 if (ConnectionState.Closed != databaseSettings.Connection.State)
97 databaseSettings.Connection.Close();
98
99 databaseSettings.Connection.Dispose();
100 databaseSettings.Connection = null;
101 }
102 }
110 private static void PrepareCommand(DatabaseSettings databaseSettings, SqlCommand command)
111 {
112 if (null != command)
113 {
114 if (0 <= databaseSettings.CommandTimeoutSeconds)
115 command.CommandTimeout = databaseSettings.CommandTimeoutSeconds;
116
117 if (null != databaseSettings.SqlTransaction)
118 command.Transaction = databaseSettings.SqlTransaction;
119 }
120 }
121
129 private static void AddParameter(SqlCommand command, DatabaseSettings.DatabaseColumn databaseColumn, string parameterValue)
130 {
131 SqlParameter sqlParameter = command.Parameters.Add($"@{databaseColumn.ColumnName}", databaseColumn.ColumnType, databaseColumn.ColumnLength);
132
133 // a column declared decimal(9,4) is sent as decimal(9,4). left at zero the provider reads the
134 // precision and scale off each value instead, so the same column could arrive a different shape
135 // on every row
136 if (0 < databaseColumn.ColumnPrecision)
137 sqlParameter.Precision = (byte)databaseColumn.ColumnPrecision;
138
139 if (0 < databaseColumn.ColumnScale)
140 sqlParameter.Scale = (byte)databaseColumn.ColumnScale;
141
142 sqlParameter.Value = ReturnParameterOfType(parameterValue, databaseColumn.ColumnType);
143 }
144
153 private static void AddReadParameters(DatabaseSettings databaseSettings, SqlCommand command)
154 {
155 List<string> values = 0 < (databaseSettings.ParametersGrid?.Rows ?? 0)
156 ? new List<string>(databaseSettings.ParametersGrid[0])
157 : databaseSettings.ParametersList;
158
159 if (null != command && 0 < (values?.Count ?? 0))
160 {
161 if (0 < databaseSettings.ColumnList.Count)
162 {
163 foreach (DatabaseSettings.DatabaseColumn databaseColumn in databaseSettings.ColumnList)
164 {
165 int columnIndex = databaseSettings.ColumnList.IndexOf(databaseColumn);
166
167 AddParameter(command, databaseColumn, values.Count > columnIndex ? values[columnIndex] : null);
168 }
169 }
170 else
171 {
172 // named rather than typed, which is how a stored procedure is usually called. the server
173 // converts from text, the same as it does for a parameter given no length
174 int valueIndex = 0;
175
176 foreach (string parameterName in databaseSettings.ParameterNamesList)
177 {
178 command.Parameters.Add(new SqlParameter(parameterName,
179 values.Count > valueIndex && null != values[valueIndex] ? (object)values[valueIndex] : DBNull.Value));
180
181 valueIndex++;
182 }
183 }
184 }
185 }
186
195 private static SqlCommand BuildReadCommand(DatabaseSettings databaseSettings, SqlConnection sqlConnection)
196 {
197 SqlCommand cmd = null;
198
199 // .WithReadAs on the chain the verb named wins, the way it did when reading .Read copied that sql
200 // over everything else. It is chosen here now instead of being copied, so a stored procedure the
201 // caller also set is still there for a later call rather than replaced
202 if (false == string.IsNullOrWhiteSpace(databaseSettings.VerbSQL))
203 {
204 cmd = new SqlCommand(databaseSettings.VerbSQL, sqlConnection)
205 {
206 CommandType = CommandType.Text
207 };
208 }
209 // a stored procedure and a sql command are kept in the same place, so which one it is has to be
210 // read off the command type. asking whether SqlCommand was set answered yes for a procedure as
211 // well, so the branch that gives a procedure its parameters could never be reached
212 else if (CommandType.StoredProcedure == databaseSettings.SqlCommandType && null != databaseSettings.StoredProcedure)
213 {
214 cmd = new SqlCommand(databaseSettings.StoredProcedure, sqlConnection)
215 {
216 CommandType = CommandType.StoredProcedure
217 };
218 }
219 else if (null != databaseSettings.SqlCommand)
220 {
221 cmd = new SqlCommand(databaseSettings.SqlCommand, sqlConnection)
222 {
223 CommandType = databaseSettings.SqlCommandType
224 };
225 }
226 else if (null != databaseSettings.ReadSQL)
227 {
228 cmd = new SqlCommand(databaseSettings.ReadSQL, sqlConnection)
229 {
230 CommandType = databaseSettings.SqlCommandType
231 };
232 }
233 else if (null != databaseSettings.TableName)
234 {
235 string sqlCommand = "Select " + ((-1 != databaseSettings.RowCount) ? $"TOP({databaseSettings.RowCount}) " : "") + $"* from {databaseSettings.TableName}";
236
237 cmd = new SqlCommand(sqlCommand, sqlConnection)
238 {
239 CommandType = CommandType.Text
240 };
241 }
242
243 AddReadParameters(databaseSettings, cmd);
244
245 PrepareCommand(databaseSettings, cmd);
246
247 return cmd;
248 }
249
258 private static SqlCommand ReadCommandOrNothing(DatabaseSettings databaseSettings, SqlConnection sqlConnection)
259 {
260 SqlCommand retVal = null;
261
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);
266 else
267 {
268 retVal = BuildReadCommand(databaseSettings, sqlConnection);
269
270 if (null == retVal)
271 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
272 "Nothing said what to read. Set one of .WithSQLCommand, .WithReadAs, .WithTableName or .WithStoredProcedure",
273 databaseSettings.Database, GPALObjectType.Database);
274 }
275
276 return retVal;
277 }
278
285 public static void GetScalar(DatabaseSettings databaseSettings, out string value)
286 {
287 string retVal = null;
288
289 // cleared before the connection is asked for, so it describes this call and a failure to open
290 // still stands afterwards
291 databaseSettings.LastError = null;
292
293 SqlConnection sqlConnection = OpenConnection(databaseSettings);
294 SqlCommand cmd = ReadCommandOrNothing(databaseSettings, sqlConnection);
295
296 if (null != cmd)
297 {
298 try
299 {
300 object answer = cmd.ExecuteScalar();
301
302 retVal = DBNull.Value == answer ? null : answer?.ToString();
303
304 Close(databaseSettings);
305 }
306 catch (Exception ex)
307 {
308 RecordFailure(databaseSettings, ex, $"Failed reading with [{cmd.CommandText}]", cmd.CommandText, databaseSettings.ParametersGrid, -1);
309 }
310 }
311
312 value = retVal;
313 }
314
321 public static int GetDataSet(DatabaseSettings databaseSettings, out DataSet dataSet)
322 {
323 DataSet returnDataSet = new DataSet();
324
325 // cleared before the connection is asked for, so it describes this call and a failure to open
326 // still stands afterwards
327 databaseSettings.LastError = null;
328
329 SqlConnection sqlConnection = OpenConnection(databaseSettings);
330 SqlCommand cmd = ReadCommandOrNothing(databaseSettings, sqlConnection);
331
332 if (null != cmd)
333 {
334 try
335 {
336 using var adapter = new SqlDataAdapter(cmd);
337 adapter.Fill(returnDataSet);
338 Close(databaseSettings);
339 }
340 catch (Exception ex)
341 {
342 RecordFailure(databaseSettings, ex, $"Failed reading with [{cmd.CommandText}]", cmd.CommandText, databaseSettings.ParametersGrid, -1);
343 }
344 }
345
346 dataSet = returnDataSet;
347 return dataSet.Tables.Count;
348 }
353 public static int GetGrid(DatabaseSettings databaseSettings, out IGPALGrid<string> dataGrid)
354 {
355 IGPALGrid<string> returnDataGrid = GPAL.Grid.ToGPALObject();
356 GetDataSet(databaseSettings, out DataSet dataSet);
357
358 var rowCountsPerTable = new List<int>();
359 int rowsAdded = 0;
360 foreach (DataTable table in dataSet.Tables)
361 {
362 if (databaseSettings.ColumnList.Count == 0)
363 {
364 foreach (DataColumn column in table.Columns)
365 {
366 SqlDbType sqlDbType = GetSqlDbTypeFromClrType(column.DataType);
367 databaseSettings.ColumnList.Add(new DatabaseSettings.DatabaseColumn(column.ColumnName, sqlDbType));
368 }
369 }
370
371 int rowsAddedForTable = 0;
372 foreach (DataRow row in table.Rows)
373 {
374 if (0 < databaseSettings.RowCount && rowsAdded >= databaseSettings.RowCount)
375 break;
376 returnDataGrid.AddRow(row.ItemArray.Select(v => v.ToString()).ToList());
377 rowsAdded++;
378 rowsAddedForTable++;
379 }
380 rowCountsPerTable.Add(rowsAddedForTable);
381 }
382 databaseSettings.LastGridRowCountsPerTable = rowCountsPerTable;
383
384 dataGrid = returnDataGrid;
385 return dataGrid.Count();
386 }
391 public static int GetDataTable(DatabaseSettings databaseSettings, out DataTable dataTable)
392 {
393 GetDataSet(databaseSettings, out DataSet dataSet);
394
395 if (dataSet.Tables.Count > 1)
396 {
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);
400 }
401
402 dataTable = dataSet.Tables.Count > 0 ? dataSet.Tables[0] : new DataTable();
403 return dataTable.Rows.Count;
404 }
413 internal static string BuildInsertStatement(DatabaseSettings databaseSettings)
414 {
415 string retVal = null;
416 bool hasTable = false == string.IsNullOrWhiteSpace(databaseSettings.TableName);
417 bool hasColumns = 0 < databaseSettings.ColumnList.Count;
418
419 // half of what an insert needs is a caller who meant one and is missing a call, so say which. neither
420 // half is a caller who has not said what to run at all, and the write reports that once for itself
421 // rather than having this speak first about an insert nobody asked for
422 if (false == hasTable || false == hasColumns)
423 {
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);
428 }
429 else
430 {
431 // joined rather than appended with a separator on each one, which left the last separator on
432 // the end. only the space was taken off again, so every insert this built asked for a column
433 // after the last one and the server rejected the statement
434 string columns = string.Join(", ", databaseSettings.ColumnList.Select(databaseColumn => databaseColumn.ColumnName));
435 string parameters = string.Join(", ", databaseSettings.ColumnList.Select(databaseColumn => $"@{databaseColumn.ColumnName}"));
436
437 retVal = $"INSERT INTO {databaseSettings.TableName} ({columns}) VALUES ({parameters})";
438 }
439
440 return retVal;
441 }
442
451 internal static string BuildWriteStatement(DatabaseSettings databaseSettings)
452 {
453 string retVal = false == string.IsNullOrWhiteSpace(databaseSettings.VerbSQL)
454 ? databaseSettings.VerbSQL
455 : databaseSettings.SqlCommand;
456
457 if (string.IsNullOrWhiteSpace(retVal) && DatabaseSettings.CrudVerb.Create == databaseSettings.Verb)
458 retVal = BuildInsertStatement(databaseSettings);
459
460 return retVal;
461 }
470 internal static int ExecuteSQL(DatabaseSettings databaseSettings, string sqlToRun, IGPALGrid<string> parameterTokens = null)
471 {
472 return ExecuteSQL(databaseSettings, sqlToRun, parameterTokens, out _);
473 }
476 internal static int ExecuteSQL(DatabaseSettings databaseSettings, string sqlToRun, IGPALGrid<string> parameterTokens, out List<int> rowCountsPerStatement)
477 {
478 // cleared before the connection is asked for, so it describes this call and a failure to open
479 // still stands afterwards
480 databaseSettings.LastError = null;
481
482 // the command type belongs to whatever SqlCommand/StoredProcedure is holding. A statement built for
483 // the verb on the chain, or built from the table name, is text however the caller also set that, so
484 // a stored procedure sitting on the settings does not send the server looking for one named by the
485 // whole insert
486 CommandType commandType = sqlToRun == databaseSettings.SqlCommand
487 ? databaseSettings.SqlCommandType
488 : CommandType.Text;
489
490 using var sqlCommand = new SqlCommand(sqlToRun, databaseSettings.Connection ?? OpenConnection(databaseSettings))
491 {
492 CommandType = commandType
493 };
494
495 PrepareCommand(databaseSettings, sqlCommand);
496
497 int rowCount = 0;
498 int rowBeingWritten = -1;
499 bool ranPerRow = false;
500 var statementCounts = new List<int>();
501
502 sqlCommand.StatementCompleted += (s, e) => statementCounts.Add(e.RecordCount);
503
504 // a connection that never opened, or a command with no sql, ran anyway and reported the
505 // provider complaining about that rather than the reason the workflow needs to see
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);
514 else
515 {
516 try
517 {
518 if (parameterTokens != null && parameterTokens.Rows > 0)
519 {
520 var structuredColumn = databaseSettings.ColumnList.FirstOrDefault(c => c.ColumnType == SqlDbType.Structured);
521 if (structuredColumn != null)
522 {
523 if (string.IsNullOrEmpty(structuredColumn.TvTableTypeName))
524 throw new InvalidOperationException($"UDTT Type Name required for Table Value column @[{structuredColumn.ColumnName}]. Use .WithUDDTName()");
525
526 var dataTable = new DataTable();
527 int columnCount = parameterTokens.Columns;
528
529 for (int i = 0; i < columnCount; i++)
530 {
531 dataTable.Columns.Add($"Column{i}", typeof(string));
532 }
533
534 foreach (var row in parameterTokens)
535 {
536 if (row.Count != columnCount)
537 {
538 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
539 $"Row [{row}] has [{row.Count}] values, but expected [{columnCount}]. Skipping row.",
540 parameterTokens, GPALObjectType.Other);
541 continue;
542 }
543 dataTable.Rows.Add(row.ToArray());
544 }
545
546 var param = sqlCommand.Parameters.AddWithValue($"@{structuredColumn.ColumnName}", dataTable);
547 param.SqlDbType = SqlDbType.Structured;
548 param.TypeName = structuredColumn.TvTableTypeName;
549 }
550 else
551 {
552 // a grid is rows of parameters, so the command runs once against each of them. one row is
553 // the ordinary case and runs once, which is what a caller adding parameters one at a time
554 // gets. taking only the first row meant a grid of fifty rows wrote one and warned about
555 // the other forty nine
556 foreach (var parameters in parameterTokens)
557 {
558 rowBeingWritten++;
559 sqlCommand.Parameters.Clear();
560
561 foreach (var databaseColumn in databaseSettings.ColumnList)
562 {
563 int columnIndex = databaseSettings.ColumnList.IndexOf(databaseColumn);
564 string paramValue = parameters.Count > columnIndex ? parameters[columnIndex] : null;
565
566 AddParameter(sqlCommand, databaseColumn, paramValue);
567 }
568
569 rowCount += sqlCommand.ExecuteNonQuery();
570 }
571
572 ranPerRow = true;
573 }
574 }
575
576 // the table valued path hands the whole grid over as one command, and a command with no
577 // parameters at all still has to run
578 if (false == ranPerRow)
579 rowCount = sqlCommand.ExecuteNonQuery();
580 }
581 catch (Exception ex)
582 {
583
584 RecordFailure(databaseSettings, ex, $"Failed running [{sqlToRun}]", sqlToRun, parameterTokens, rowBeingWritten);
585 }
586 }
587
588 rowCountsPerStatement = statementCounts;
589 return rowCount;
590 }
601 private static void RecordFailure(DatabaseSettings databaseSettings, Exception ex, string message, string sql, IGPALGrid<string> parameterTokens, int row)
602 {
603 databaseSettings.LastError = new GPALDatabase.DatabaseError
604 {
605 Number = ex is SqlException sqlException ? sqlException.Number : 0,
606 Message = ex.Message,
607 Sql = sql,
608 Parameters = parameterTokens,
609 Row = row,
610 RowValues = 0 <= row && row < (parameterTokens?.Rows ?? 0) ? new List<string>(parameterTokens[row]) : null,
611 Occurred = DateTime.Now,
612 Exception = ex
613 };
614
615 // the server rejecting something is an error and says so with a number and a message. anything else
616 // arriving here was not expected at all, so it stays an exception.
617 // published rather than handed straight to the exception handler: this reaches the console, the debug
618 // window and a logger as well, the handler still fires because an exception came with it, and the
619 // screenshot is taken by the publish rather than here
620 GPAL.PublishSimpleEvent(ex is SqlException ? GPALEventType.ERROR : GPALEventType.EXCEPTION,
621 message, databaseSettings.Database, GPALObjectType.Database, ex);
622
623 CallOnErrorHandler(databaseSettings);
624 }
625
626 private static void CallOnErrorHandler(DatabaseSettings databaseSettings)
627 {
628 if (null != databaseSettings.CallOnErrorHandler)
629 databaseSettings.CallOnErrorHandler(databaseSettings.Database);
630
631 if (null != databaseSettings.SqlTransaction)
632 {
633 databaseSettings.SqlTransaction.Rollback();
634 databaseSettings.SqlTransaction = null;
635 }
636
637 if (null != databaseSettings.Connection && ConnectionState.Open == databaseSettings.Connection.State)
638 {
639 databaseSettings.Connection.Close();
640 databaseSettings.Connection = null;
641 }
642 }
643 static SqlDbType GetSqlDbTypeFromName(string dataTypeName)
644 {
645 switch (dataTypeName.ToLower())
646 {
647 case "char":
648 return SqlDbType.Char;
649 case "nchar":
650 return SqlDbType.NChar;
651 case "ntext":
652 return SqlDbType.NText;
653 case "nvarchar":
654 return SqlDbType.NVarChar;
655 case "text":
656 return SqlDbType.Text;
657 case "varchar":
658 return SqlDbType.VarChar;
659 case "xml":
660 return SqlDbType.Xml;
661 case "date":
662 return SqlDbType.Date;
663 case "datetime":
664 return SqlDbType.DateTime;
665 case "datetime2":
666 return SqlDbType.DateTime2;
667 case "smalldatetime":
668 return SqlDbType.SmallDateTime;
669 case "time":
670 return SqlDbType.Time;
671 case "timestamp":
672 return SqlDbType.Timestamp;
673 case "decimal":
674 return SqlDbType.Decimal;
675 case "money":
676 return SqlDbType.Money;
677 case "smallmoney":
678 return SqlDbType.SmallMoney;
679 case "float":
680 return SqlDbType.Float;
681 case "real":
682 return SqlDbType.Real;
683 case "image":
684 return SqlDbType.Image;
685 case "bit":
686 return SqlDbType.Bit;
687 case "int":
688 return SqlDbType.Int;
689 case "smallint":
690 return SqlDbType.SmallInt;
691 // Add cases for other types as needed
692 default:
693 throw new ArgumentException("Unknown SqlDbType: " + dataTypeName);
694 }
695 }
700 static SqlDbType GetSqlDbTypeFromClrType(Type clrType)
701 {
702 switch (Type.GetTypeCode(clrType))
703 {
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;
714 default:
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;
720 }
721 }
722 private static object ReturnParameterOfType(string parmValue, SqlDbType sqlDbType)
723 {
724 if (string.IsNullOrEmpty(parmValue))
725 {
726 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
727 $"Parameter value is null or empty for SqlDbType [{sqlDbType}]. Returning DBNull.Value.",
728 null, GPALObjectType.Other);
729 return DBNull.Value;
730 }
731
732 try
733 {
734 switch (sqlDbType)
735 {
736 case SqlDbType.Char:
737 case SqlDbType.NChar:
738 case SqlDbType.NText:
739 case SqlDbType.NVarChar:
740 case SqlDbType.Text:
741 case SqlDbType.VarChar:
742 case SqlDbType.Xml:
743 return parmValue;
744
745 case SqlDbType.Date:
746 case SqlDbType.DateTime:
747 case SqlDbType.DateTime2:
748 case SqlDbType.SmallDateTime:
749 return DateTime.TryParse(parmValue, out DateTime dateResult) ? (object)dateResult : DBNull.Value;
750
751 // a sql server time column is a TimeSpan on this side. handing it a DateTime is refused by
752 // the provider before the command leaves, so a whole date and time is accepted here and the
753 // time of day taken out of it: "14:30:00" and "2026-09-03 14:30:00" both write 14:30
754 case SqlDbType.Time:
755 if (TimeSpan.TryParse(parmValue, out TimeSpan timeResult))
756 return timeResult;
757
758 return DateTime.TryParse(parmValue, out DateTime timeOfDayResult) ? (object)timeOfDayResult.TimeOfDay : DBNull.Value;
759
760 case SqlDbType.DateTimeOffset:
761 return DateTimeOffset.TryParse(parmValue, out DateTimeOffset offsetResult) ? (object)offsetResult : DBNull.Value;
762
763 case SqlDbType.Decimal:
764 case SqlDbType.Money:
765 case SqlDbType.SmallMoney:
766 return Decimal.TryParse(parmValue, out decimal decimalResult) ? (object)decimalResult : DBNull.Value;
767
768 // sql server's float holds 8 bytes and real holds 4. reading both as Single rounded away
769 // everything a float column was chosen for
770 case SqlDbType.Float:
771 return double.TryParse(parmValue, out double doubleResult) ? (object)doubleResult : DBNull.Value;
772
773 case SqlDbType.Real:
774 return float.TryParse(parmValue, out float floatResult) ? (object)floatResult : DBNull.Value;
775
776 case SqlDbType.Bit:
777 return bool.TryParse(parmValue, out bool boolResult) ? (object)boolResult : (int.TryParse(parmValue, out int intBitResult) ? (object)(intBitResult != 0) : DBNull.Value);
778
779 case SqlDbType.Int:
780 case SqlDbType.SmallInt:
781 return int.TryParse(parmValue, out int intResult) ? (object)intResult : DBNull.Value;
782
783 case SqlDbType.BigInt:
784 return long.TryParse(parmValue, out long longResult) ? (object)longResult : DBNull.Value;
785
786 case SqlDbType.UniqueIdentifier:
787 return Guid.TryParse(parmValue, out Guid guidResult) ? (object)guidResult : DBNull.Value;
788
789 case SqlDbType.Image:
790 case SqlDbType.VarBinary:
791 case SqlDbType.Binary:
792 try
793 {
794 return Convert.FromBase64String(parmValue);
795 }
796 catch (FormatException ex)
797 {
798 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
799 $"Failed to decode Base64 string [{parmValue}] for SqlDbType [{sqlDbType}]. Returning DBNull.Value.",
800 null, GPALObjectType.Other, ex);
801 return DBNull.Value;
802 }
803
804 case SqlDbType.Timestamp:
805 try
806 {
807 if (ulong.TryParse(parmValue, out ulong ulongResult))
808 {
809 return BitConverter.GetBytes(ulongResult).Reverse().ToArray();
810 }
811 throw new FormatException("Invalid numeric string for Timestamp");
812 }
813 catch (Exception ex)
814 {
815 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
816 $"Failed to parse [{parmValue}] as Timestamp. Returning DBNull.Value.",
817 null, GPALObjectType.Other, ex);
818 return DBNull.Value;
819 }
820
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;
830 return parmValue;
831
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);
836 return DBNull.Value;
837
838 default:
839 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
840 $"Unsupported SqlDbType [{sqlDbType}] for parameter value [{parmValue}]. Returning DBNull.Value.",
841 null, GPALObjectType.Other);
842 return DBNull.Value;
843 }
844 }
845 catch (Exception ex)
846 {
847 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
848 $"Failed to convert parameter value [{parmValue}] to SqlDbType [{sqlDbType}]. Returning DBNull.Value.",
849 null, GPALObjectType.Other, ex);
850 return DBNull.Value;
851 }
852 }
857 internal static void TokenizeDatabase(UnitOfWork currentUOW, IGPALDatabase inputDatabase)
858 {
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;
864 }
872 public static IGPALGrid<string> DataTableToGrid(DataTable dataTable, out List<DatabaseSettings.DatabaseColumn> columnsList)
873 {
874 IGPALGrid<string> retVal = GPAL.Grid.ToGPALObject();
875
876 columnsList = new List<DatabaseSettings.DatabaseColumn>();
877
878 if (null != dataTable)
879 {
880 foreach (DataColumn column in dataTable.Columns)
881 columnsList.Add(new DatabaseSettings.DatabaseColumn(column.ColumnName, GetSqlDbTypeFromClrType(column.DataType)));
882
883 foreach (DataRow row in dataTable.Rows)
884 retVal.AddRow(row.ItemArray.Select(value => DBNull.Value == value ? null : value?.ToString()).ToList());
885 }
886
887 return retVal;
888 }
889 public static void TokenizeDatabase(GPALDatabase inputDatabase)
890 {
891 IGPALGrid<string> tokens;
892 int rowCount = DatabaseHelper.GetGrid(inputDatabase.DatabaseSettings, out tokens);
893 inputDatabase.Tokens = tokens;
894 }
895 }
896
901 public static class DataTableExtensions
902 {
903 public static void SetColumnsOrder(this DataTable table, params String[] columnNames)
904 {
905 int columnIndex = 0;
906 foreach (var columnName in columnNames)
907 {
908 if (table.Columns.Contains(columnName))
909 {
910 table.Columns[columnName].SetOrdinal(columnIndex);
911 columnIndex++;
912 }
913 }
914 }
915 }
916}
917