diff --git a/src/DatabaseWrapper.Core/DatabaseClientBase.cs b/src/DatabaseWrapper.Core/DatabaseClientBase.cs index 160f6bc..3f8a40a 100644 --- a/src/DatabaseWrapper.Core/DatabaseClientBase.cs +++ b/src/DatabaseWrapper.Core/DatabaseClientBase.cs @@ -2,12 +2,15 @@ using System.Collections.Generic; using System.Collections.Concurrent; using System.Data; +using static System.FormattableString; using System.Linq; using System.Text; using System.Threading.Tasks; using DatabaseWrapper.Core; using ExpressionTree; using System.Threading; +using System.Data.Common; +using System.Runtime.InteropServices; namespace DatabaseWrapper.Core { @@ -302,12 +305,28 @@ public abstract class DatabaseClientBase /// Cancellation token. public abstract Task TruncateAsync(string tableName, CancellationToken token = default); + /// + /// Execute a query. + /// + /// A tuple of the aatabase query defined outside of the database client and query parameters. + /// A DataTable containing the results. + public abstract DataTable Query((string Query, IEnumerable> Parameters) queryAndParameters); + /// /// Execute a query. /// /// Database query defined outside of the database client. /// A DataTable containing the results. - public abstract DataTable Query(string query); + public DataTable Query(string query) + => Query((query, null)); + + /// + /// Execute a query. + /// + /// A tuple of the aatabase query defined outside of the database client and query parameters. + /// Cancellation token. + /// A DataTable containing the results. + public abstract Task QueryAsync((string Query, IEnumerable> Parameters) queryAndParameters, CancellationToken token = default(CancellationToken)); /// /// Execute a query. @@ -315,7 +334,8 @@ public abstract class DatabaseClientBase /// Database query defined outside of the database client. /// Cancellation token. /// A DataTable containing the results. - public abstract Task QueryAsync(string query, CancellationToken token = default); + public Task QueryAsync(string query, CancellationToken token = default(CancellationToken)) + => QueryAsync((query, null), token); /// /// Determine if records exist by filter. diff --git a/src/DatabaseWrapper.Core/DatabaseHelperBase.cs b/src/DatabaseWrapper.Core/DatabaseHelperBase.cs index 47dbb04..e3e3bdd 100644 --- a/src/DatabaseWrapper.Core/DatabaseHelperBase.cs +++ b/src/DatabaseWrapper.Core/DatabaseHelperBase.cs @@ -1,10 +1,14 @@ using System; using System.Collections.Generic; +using System.Data; +using static System.FormattableString; using System.Text; using ExpressionTree; namespace DatabaseWrapper.Core { + using QueryAndParameters = System.ValueTuple>>; + /// /// Base implementation of helper properties and methods. /// @@ -26,6 +30,91 @@ public abstract class DatabaseHelperBase #region Private-Members + /// + /// Append object as the parameter to the parameters list. + /// + /// Returns the parameter name, that is to be used in the query currently under construction. + /// + /// List of the parameters. + /// Object to be added. + /// Parameter name. + static string AppendParameter(List> parameters, object o) + { + var r = Invariant($"@E{parameters.Count}"); + parameters.Add(new KeyValuePair(r, o)); + return r; + } + + /// + /// Append object as the parameter to the parameters list. + /// Use the object type to apply conversions, if any needed. + /// + /// Returns the parameter name, that is to be used in the query currently under construction. + /// + /// List of the parameters. + /// Object to be added. + /// Parameter name. + static string AppendParameterByType(List> parameters, object untypedObject) + { + if (untypedObject is DateTime || untypedObject is DateTime?) + { + return AppendParameter(parameters, Convert.ToDateTime(untypedObject)); + } + if (untypedObject is int || untypedObject is long || untypedObject is decimal) + { + return AppendParameter(parameters, untypedObject); + } + if (untypedObject is bool) + { + return AppendParameter(parameters, (bool)untypedObject ? 1 : 0); + } + if (untypedObject is byte[]) + { + return AppendParameter(parameters, untypedObject); + } + return AppendParameter(parameters, untypedObject); + } + + static readonly Dictionary BinaryOperators = new Dictionary() { + { OperatorEnum.And, "AND" }, + { OperatorEnum.Or, "OR" }, + { OperatorEnum.Equals, "=" }, + { OperatorEnum.NotEquals, "<>" }, + { OperatorEnum.GreaterThan, ">" }, + { OperatorEnum.GreaterThanOrEqualTo, ">=" }, + { OperatorEnum.LessThan, "<" }, + { OperatorEnum.LessThanOrEqualTo, "<=" }, + }; + + static readonly Dictionary InListOperators = new Dictionary() { + { OperatorEnum.In, "IN" }, + { OperatorEnum.NotIn, "NOT IN" }, + }; + + static readonly Dictionary ContainsOperators = new Dictionary() { + { OperatorEnum.Contains, ("LIKE", "OR") }, + { OperatorEnum.ContainsNot, ("NOT LIKE", "AND") }, + }; + static readonly Dictionary LikeOperators = new Dictionary() { + { OperatorEnum.StartsWith, ("LIKE", "", "%") }, + { OperatorEnum.StartsWithNot, ("NOT LIKE", "", "%") }, + { OperatorEnum.EndsWith, ("LIKE", "%", "") }, + { OperatorEnum.EndsWithNot, ("NOT LIKE", "%", "") }, + }; + static readonly Dictionary IsNullOperators = new Dictionary() { + { OperatorEnum.IsNull, "IS NULL" }, + { OperatorEnum.IsNotNull, "IS NOT NULL" }, + }; + + static readonly Dictionary SqlTypeMap = new Dictionary() { + { typeof(Int32), SqlDbType.Int }, + { typeof(Int64), SqlDbType.BigInt }, + { typeof(double), SqlDbType.Float }, + { typeof(Guid), SqlDbType.UniqueIdentifier }, + { typeof(byte[]), SqlDbType.Image }, + { typeof(DateTimeOffset), SqlDbType.DateTimeOffset }, + }; + #endregion #region Constructors-and-Factories @@ -33,6 +122,185 @@ public abstract class DatabaseHelperBase #endregion #region Public-Methods + /// + /// Compose a where-cluase corresponding to the tree expression. + /// + /// Expression to be converted. + /// Parameters to append SQL query parameters to. + /// Where-clause. + /// + /// + public string ExpressionToWhereClause(Expr expr, List> parameters) + { + if (expr == null) return null; + + string clause = ""; + + if (expr.Left == null) return null; + + clause += "("; + + if (expr.Left is Expr) + { + clause += ExpressionToWhereClause((Expr)expr.Left, parameters) + " "; + } + else + { + if (!(expr.Left is string)) + { + throw new ArgumentException("Left term must be of type Expression or String"); + } + + if (expr.Operator != OperatorEnum.Contains + && expr.Operator != OperatorEnum.ContainsNot + && expr.Operator != OperatorEnum.StartsWith + && expr.Operator != OperatorEnum.StartsWithNot + && expr.Operator != OperatorEnum.EndsWith + && expr.Operator != OperatorEnum.EndsWithNot) + { + // + // These operators will add the left term + // + clause += PreparedFieldName(expr.Left.ToString()) + " "; + } + } + + string operator_name; + string logic_operator_name; + string prefix; + string suffix; + (string, string) operator_pair; + (string, string, string) operator_triple; + if (BinaryOperators.TryGetValue(expr.Operator, out operator_name)) + { + if (expr.Right == null) return null; + clause += operator_name + " "; + + if (expr.Right is Expr) + { + clause += ExpressionToWhereClause((Expr)expr.Right, parameters); + } + else + { + clause += AppendParameterByType(parameters, expr.Right); + } + } + else if (InListOperators.TryGetValue(expr.Operator, out operator_name)) + { + if (expr.Right == null) return null; + int inAdded = 0; + if (!Helper.IsList(expr.Right)) return null; + List inTempList = Helper.ObjectToList(expr.Right); + clause += Invariant($" {operator_name} ("); + foreach (object currObj in inTempList) + { + if (currObj == null) continue; + if (inAdded > 0) clause += ","; + clause += AppendParameterByType(parameters, currObj); + inAdded++; + } + clause += ")"; + } + else if (ContainsOperators.TryGetValue(expr.Operator, out operator_pair)) + { + if (expr.Right == null) return null; + if (!(expr.Right is string)) return null; + (operator_name, logic_operator_name) = operator_pair; + var field = PreparedFieldName(expr.Left.ToString()); + var p1_name = AppendParameterByType(parameters, "%" + expr.Right.ToString()); + var p2_name = AppendParameterByType(parameters, "%" + expr.Right.ToString() + "%"); + var p3_name = AppendParameterByType(parameters, expr.Right.ToString() + "%"); + clause += Invariant($"({field} {operator_name} {p1_name} {logic_operator_name} {field} {operator_name} {p2_name} {logic_operator_name} {field} {operator_name} {p3_name})"); + } + else if (LikeOperators.TryGetValue(expr.Operator, out operator_triple)) + { + if (expr.Right == null) return null; + if (!(expr.Right is string)) return null; + (operator_name, prefix, suffix) = operator_triple; + var p_name = AppendParameterByType(parameters, prefix + expr.Right.ToString() + suffix); + clause += Invariant($"({PreparedFieldName(expr.Left.ToString())} {operator_name} {p_name})"); + } + else if (IsNullOperators.TryGetValue(expr.Operator, out operator_name)) + { + clause += " " + operator_name; + } + else + { + throw new ApplicationException(Invariant($"Error in SqlServerHelper.ExpressionToWhereClause: Unknown operator {expr.Operator}")); + } + + clause += ")"; + + return clause; + } + + /// + /// Add parameters to the SQL command. + /// + /// Subtype of DbCommand + /// Subtype of DbPaameter + /// Command to add parameters to. + /// Parameter constructor. + /// Parameters to be added. + /// + public static void AddParameters(TC cmd, Func createParameter, IEnumerable> parameters) + where TC : System.Data.Common.DbCommand + where TP : System.Data.Common.DbParameter + { + if (parameters==null) + { + return; + } + foreach (var kv in parameters) + { + int param_index = cmd.Parameters.Count; + var param_name = kv.Key; + var p = kv.Value; + if (p == null) + { + cmd.Parameters.Add(createParameter(param_name, SqlDbType.NVarChar)); + cmd.Parameters[param_index].Value = DBNull.Value; + continue; + } + var t = p.GetType(); + SqlDbType sql_type; + if (SqlTypeMap.TryGetValue(t, out sql_type)) + { + cmd.Parameters.Add(createParameter(param_name, sql_type)); + cmd.Parameters[param_index].Value = p; + continue; + } + if (t == typeof(string)) + { + var s_string = p as string; + cmd.Parameters.Add(createParameter(param_name, s_string.Length > 4000 ? System.Data.SqlDbType.NText : System.Data.SqlDbType.NVarChar)); + cmd.Parameters[param_index].Value = s_string; + continue; + } + if (t == typeof(bool)) { + cmd.Parameters.Add(createParameter(param_name, System.Data.SqlDbType.Bit)); + cmd.Parameters[param_index].Value = (bool)p ? 1 : 0; + continue; + } + if (t == typeof(DateTime)) + { + var dt_object = DBNull.Value as object; + var dt_datetime = (DateTime)p; + if (dt_datetime != DateTime.MinValue) + { + var dt = dt_datetime.ToLocalTime(); + if (dt != DateTime.MinValue) + { + dt_object = dt; + } + } + cmd.Parameters.Add(createParameter(param_name, System.Data.SqlDbType.DateTime)); + cmd.Parameters[param_index].Value = dt_object; + continue; + } + throw new ApplicationException(Invariant($"{nameof(AddParameters)}: Unknown type: {t.Name}")); + } + } /// /// Build a connection string from DatabaseSettings. @@ -63,6 +331,13 @@ public abstract class DatabaseHelperBase /// String. public abstract string SanitizeString(string val); + /// + /// Prepare a field name for use in a SQL query. + /// + /// Name of the field to be prepared. + /// Field name for use in a SQL query. + public abstract string PreparedFieldName(string fieldName); + /// /// Method to convert a Column object to the values used in a table create statement. /// @@ -102,7 +377,7 @@ public abstract class DatabaseHelperBase /// Expression filter. /// Result order. /// String. - public abstract string SelectQuery(string tableName, int? indexStart, int? maxResults, List returnFields, Expr filter, ResultOrder[] resultOrder); + public abstract QueryAndParameters SelectQuery(string tableName, int? indexStart, int? maxResults, List returnFields, Expr filter, ResultOrder[] resultOrder); /// /// Retrieve a query used for inserting data into a table. @@ -110,7 +385,7 @@ public abstract class DatabaseHelperBase /// The table in which you wish to INSERT. /// The key-value pairs for the row you wish to INSERT. /// String. - public abstract string InsertQuery(string tableName, Dictionary keyValuePairs); + public abstract QueryAndParameters InsertQuery(string tableName, Dictionary keyValuePairs); /// /// Retrieve a query for inserting multiple rows into a table. @@ -118,7 +393,7 @@ public abstract class DatabaseHelperBase /// The table in which you wish to INSERT. /// List of dictionaries containing key-value pairs for the rows you wish to INSERT. /// String. - public abstract string InsertMultipleQuery(string tableName, List> keyValuePairList); + public abstract QueryAndParameters InsertMultipleQuery(string tableName, List> keyValuePairList); /// /// Retrieve a query for updating data in a table. @@ -127,7 +402,7 @@ public abstract class DatabaseHelperBase /// The key-value pairs for the data you wish to UPDATE. /// The expression containing the UPDATE filter (i.e. WHERE clause data). /// String. - public abstract string UpdateQuery(string tableName, Dictionary keyValuePairs, Expr filter); + public abstract QueryAndParameters UpdateQuery(string tableName, Dictionary keyValuePairs, Expr filter); /// /// Retrieve a query for deleting data from a table. @@ -135,7 +410,7 @@ public abstract class DatabaseHelperBase /// Table name. /// Expression filter. /// String. - public abstract string DeleteQuery(string tableName, Expr filter); + public abstract QueryAndParameters DeleteQuery(string tableName, Expr filter); /// /// Retrieve a query for truncating a table. @@ -150,7 +425,7 @@ public abstract class DatabaseHelperBase /// Table name. /// Expression filter. /// String. - public abstract string ExistsQuery(string tableName, Expr filter); + public abstract QueryAndParameters ExistsQuery(string tableName, Expr filter); /// /// Retrieve a query that returns a count of the number of rows matching the supplied conditions. @@ -159,7 +434,7 @@ public abstract class DatabaseHelperBase /// Column name to use to temporarily store the result. /// Expression filter. /// String. - public abstract string CountQuery(string tableName, string countColumnName, Expr filter); + public abstract QueryAndParameters CountQuery(string tableName, string countColumnName, Expr filter); /// /// Retrieve a query that sums the values found in the specified field. @@ -169,7 +444,7 @@ public abstract class DatabaseHelperBase /// Column name to temporarily store the result. /// Expression filter. /// String. - public abstract string SumQuery(string tableName, string fieldName, string sumColumnName, Expr filter); + public abstract QueryAndParameters SumQuery(string tableName, string fieldName, string sumColumnName, Expr filter); /// /// Retrieve a timestamp in the database format. diff --git a/src/DatabaseWrapper.Core/DatabaseWrapper.Core.xml b/src/DatabaseWrapper.Core/DatabaseWrapper.Core.xml index 40fa937..fb00751 100644 --- a/src/DatabaseWrapper.Core/DatabaseWrapper.Core.xml +++ b/src/DatabaseWrapper.Core/DatabaseWrapper.Core.xml @@ -347,6 +347,13 @@ The table you wish to TRUNCATE. Cancellation token. + + + Execute a query. + + A tuple of the aatabase query defined outside of the database client and query parameters. + A DataTable containing the results. + Execute a query. @@ -354,6 +361,14 @@ Database query defined outside of the database client. A DataTable containing the results. + + + Execute a query. + + A tuple of the aatabase query defined outside of the database client and query parameters. + Cancellation token. + A DataTable containing the results. + Execute a query. @@ -451,6 +466,48 @@ Timestamp offset format for use in DateTimeOffset.ToString([format]). + + + Append object as the parameter to the parameters list. + + Returns the parameter name, that is to be used in the query currently under construction. + + List of the parameters. + Object to be added. + Parameter name. + + + + Append object as the parameter to the parameters list. + Use the object type to apply conversions, if any needed. + + Returns the parameter name, that is to be used in the query currently under construction. + + List of the parameters. + Object to be added. + Parameter name. + + + + Compose a where-cluase corresponding to the tree expression. + + Expression to be converted. + Parameters to append SQL query parameters to. + Where-clause. + + + + + + Add parameters to the SQL command. + + Subtype of DbCommand + Subtype of DbPaameter + Command to add parameters to. + Parameter constructor. + Parameters to be added. + + Build a connection string from DatabaseSettings. @@ -480,6 +537,13 @@ String. String. + + + Prepare a field name for use in a SQL query. + + Name of the field to be prepared. + Field name for use in a SQL query. + Method to convert a Column object to the values used in a table create statement. diff --git a/src/DatabaseWrapper.Mysql/DatabaseClient.cs b/src/DatabaseWrapper.Mysql/DatabaseClient.cs index 87b4896..531f18b 100644 --- a/src/DatabaseWrapper.Mysql/DatabaseClient.cs +++ b/src/DatabaseWrapper.Mysql/DatabaseClient.cs @@ -116,7 +116,28 @@ private set private string _CountColumnName = "__count__"; private string _SumColumnName = "__sum__"; private MysqlHelper _Helper = new MysqlHelper(); - + + static readonly Dictionary DbTypeMapping = new Dictionary() + { + { SqlDbType.Int, MySqlDbType.Int32 }, + { SqlDbType.BigInt, MySqlDbType.Int64 }, + { SqlDbType.Float, MySqlDbType.Double }, + { SqlDbType.UniqueIdentifier, MySqlDbType.Guid }, + { SqlDbType.Image, MySqlDbType.Blob }, + { SqlDbType.DateTimeOffset, MySqlDbType.DateTime }, + { SqlDbType.NText, MySqlDbType.Text }, + { SqlDbType.NVarChar, MySqlDbType.VarChar }, + { SqlDbType.Bit, MySqlDbType.Bit }, + { SqlDbType.DateTime, MySqlDbType.DateTime } + }; + + static MySqlParameter CreateMySqlParameter(string name, SqlDbType type) + { + var real_type = DbTypeMapping[type]; + var r = new MySqlParameter(name, real_type); + return r; + } + #endregion #region Constructors-and-Factories @@ -794,10 +815,11 @@ public override async Task TruncateAsync(string tableName, CancellationToken tok /// /// Execute a query. /// - /// Database query defined outside of the database client. + /// Database query defined outside of the database client, and, the corresponding parameters. /// A DataTable containing the results. - public override DataTable Query(string query) + public override DataTable Query((string Query, IEnumerable> Parameters) queryAndParameters) { + (var query, var parameters) = queryAndParameters; if (String.IsNullOrEmpty(query)) throw new ArgumentNullException(query); if (query.Length > MaxStatementLength) throw new ArgumentException("Query exceeds maximum statement length of " + MaxStatementLength + " characters."); @@ -822,6 +844,7 @@ public override DataTable Query(string query) cmd.CommandText = query; #pragma warning restore CA2100 // Review SQL queries for security vulnerabilities + DatabaseHelperBase.AddParameters(cmd, CreateMySqlParameter, parameters); using (MySqlDataAdapter sda = new MySqlDataAdapter(cmd)) { DataSet ds = new DataSet(); @@ -872,11 +895,12 @@ public override DataTable Query(string query) /// /// Execute a query. /// - /// Database query defined outside of the database client. + /// Database query defined outside of the database client, and, the corresponding parameters. /// Cancellation token. /// A DataTable containing the results. - public override async Task QueryAsync(string query, CancellationToken token = default) + public override async Task QueryAsync((string Query, IEnumerable> Parameters) queryAndParameters, CancellationToken token = default) { + (var query, var parameters) = queryAndParameters; if (String.IsNullOrEmpty(query)) throw new ArgumentNullException(query); if (query.Length > MaxStatementLength) throw new ArgumentException("Query exceeds maximum statement length of " + MaxStatementLength + " characters."); @@ -901,6 +925,7 @@ public override async Task QueryAsync(string query, CancellationToken cmd.CommandText = query; #pragma warning restore CA2100 // Review SQL queries for security vulnerabilities + DatabaseHelperBase.AddParameters(cmd, CreateMySqlParameter, parameters); using (MySqlDataAdapter sda = new MySqlDataAdapter(cmd)) { DataSet ds = new DataSet(); diff --git a/src/DatabaseWrapper.Mysql/DatabaseWrapper.Mysql.xml b/src/DatabaseWrapper.Mysql/DatabaseWrapper.Mysql.xml index 2b0d056..1a5c345 100644 --- a/src/DatabaseWrapper.Mysql/DatabaseWrapper.Mysql.xml +++ b/src/DatabaseWrapper.Mysql/DatabaseWrapper.Mysql.xml @@ -304,18 +304,18 @@ The table you wish to TRUNCATE. Cancellation token. - + Execute a query. - Database query defined outside of the database client. + Database query defined outside of the database client, and, the corresponding parameters. A DataTable containing the results. - + Execute a query. - Database query defined outside of the database client. + Database query defined outside of the database client, and, the corresponding parameters. Cancellation token. A DataTable containing the results. @@ -565,5 +565,12 @@ DateTimeOffset. String. + + + Prepare a field name for use in a SQL query. + + Name of the field to be prepared. + Field name for use in a SQL query. + diff --git a/src/DatabaseWrapper.Mysql/MysqlHelper.cs b/src/DatabaseWrapper.Mysql/MysqlHelper.cs index 8978f0a..3d9646c 100644 --- a/src/DatabaseWrapper.Mysql/MysqlHelper.cs +++ b/src/DatabaseWrapper.Mysql/MysqlHelper.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using static System.FormattableString; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -10,6 +11,8 @@ namespace DatabaseWrapper.Mysql { + using QueryAndParameters = System.ValueTuple>>; + /// /// MySQL implementation of helper properties and methods. /// @@ -227,7 +230,7 @@ public override string DropTableQuery(string tableName) /// Expression filter. /// Result order. /// String. - public override string SelectQuery(string tableName, int? indexStart, int? maxResults, List returnFields, Expr filter, ResultOrder[] resultOrder) + public override QueryAndParameters SelectQuery(string tableName, int? indexStart, int? maxResults, List returnFields, Expr filter, ResultOrder[] resultOrder) { string query = ""; string whereClause = ""; @@ -268,7 +271,8 @@ public override string SelectQuery(string tableName, int? indexStart, int? maxRe // // expressions // - if (filter != null) whereClause = ExpressionToWhereClause(filter); + var parameters_list = new List>(); + if (filter != null) whereClause = ExpressionToWhereClause(filter, parameters_list); if (!String.IsNullOrEmpty(whereClause)) { query += "WHERE " + whereClause + " "; @@ -294,7 +298,7 @@ public override string SelectQuery(string tableName, int? indexStart, int? maxRe } } - return query; + return (query, parameters_list); } /// @@ -303,24 +307,21 @@ public override string SelectQuery(string tableName, int? indexStart, int? maxRe /// The table in which you wish to INSERT. /// The key-value pairs for the row you wish to INSERT. /// String. - public override string InsertQuery(string tableName, Dictionary keyValuePairs) + public override QueryAndParameters InsertQuery(string tableName, Dictionary keyValuePairs) { + var o_ret = keyValuePairs.Select(kv => new KeyValuePair("@F_" + kv.Key, kv.Value)); string ret = "START TRANSACTION; " + "INSERT INTO `" + SanitizeString(tableName) + "` " + "("; - string keys = ""; - string vals = ""; - BuildKeysValuesFromDictionary(keyValuePairs, out keys, out vals); - - ret += keys + ") " + + ret += string.Join(", ", keyValuePairs.Keys.Select(k => PreparedFieldName(k))) + ") " + "VALUES " + - "(" + vals + "); " + + "(" + string.Join(", ", o_ret.Select(k => k.Key)) + "); " + "SELECT LAST_INSERT_ID() AS id; " + "COMMIT; "; - return ret; + return (ret, o_ret); } /// @@ -329,11 +330,11 @@ public override string InsertQuery(string tableName, Dictionary /// The table in which you wish to INSERT. /// List of dictionaries containing key-value pairs for the rows you wish to INSERT. /// String. - public override string InsertMultipleQuery(string tableName, List> keyValuePairList) + public override QueryAndParameters InsertMultipleQuery(string tableName, List> keyValuePairList) { ValidateInputDictionaries(keyValuePairList); string keys = BuildKeysFromDictionary(keyValuePairList[0]); - List values = BuildValuesFromDictionaries(keyValuePairList); + var ret_values = new List>(); string ret = "START TRANSACTION;" + @@ -341,18 +342,22 @@ public override string InsertMultipleQuery(string tableName, List 0) ret += ","; - ret += " (" + value + ")"; - added++; + var dict = keyValuePairList[i_dict]; + var prefix = Invariant($"@F{i_dict}_"); + var this_round = dict.Select(kv => new KeyValuePair(prefix + kv.Key, kv.Value)); + if (i_dict>0) { + ret += ", "; + } + ret += "(" + string.Join(", ", this_round.Select(kv => kv.Key)) + ")"; + ret_values.AddRange(this_round); } ret += "; COMMIT; "; - return ret; + return (ret, ret_values); } /// @@ -362,17 +367,18 @@ public override string InsertMultipleQuery(string tableName, ListThe key-value pairs for the data you wish to UPDATE. /// The expression containing the UPDATE filter (i.e. WHERE clause data). /// String. - public override string UpdateQuery(string tableName, Dictionary keyValuePairs, Expr filter) + public override QueryAndParameters UpdateQuery(string tableName, Dictionary keyValuePairs, Expr filter) { - string keyValueClause = BuildKeyValueClauseFromDictionary(keyValuePairs); + const string FIELD_PREFIX = "@F"; + var parameters = keyValuePairs.Select(kv => new KeyValuePair(FIELD_PREFIX + kv.Key, kv.Value)).ToList(); string ret = "UPDATE `" + SanitizeString(tableName) + "` SET " + - keyValueClause + " "; + string.Join(", ", parameters.Select(kv => kv.Key.Substring(FIELD_PREFIX.Length) + "=" + kv.Key)) + " "; - if (filter != null) ret += "WHERE " + ExpressionToWhereClause(filter) + " "; + if (filter != null) ret += "WHERE " + ExpressionToWhereClause(filter, parameters) + " "; - return ret; + return (ret, parameters); } /// @@ -381,14 +387,15 @@ public override string UpdateQuery(string tableName, Dictionary /// Table name. /// Expression filter. /// String. - public override string DeleteQuery(string tableName, Expr filter) + public override QueryAndParameters DeleteQuery(string tableName, Expr filter) { string ret = "DELETE FROM `" + SanitizeString(tableName) + "` "; - if (filter != null) ret += "WHERE " + ExpressionToWhereClause(filter) + " "; + var parameters = new List>(); + if (filter != null) ret += "WHERE " + ExpressionToWhereClause(filter, parameters) + " "; - return ret; + return (ret, parameters); } /// @@ -407,7 +414,7 @@ public override string TruncateQuery(string tableName) /// Table name. /// Expression filter. /// String. - public override string ExistsQuery(string tableName, Expr filter) + public override QueryAndParameters ExistsQuery(string tableName, Expr filter) { string query = ""; string whereClause = ""; @@ -422,14 +429,15 @@ public override string ExistsQuery(string tableName, Expr filter) // // expressions // - if (filter != null) whereClause = ExpressionToWhereClause(filter); + var parameters = new List>(); + if (filter != null) whereClause = ExpressionToWhereClause(filter, parameters); if (!String.IsNullOrEmpty(whereClause)) { query += "WHERE " + whereClause + " "; } query += "LIMIT 1"; - return query; + return (query, parameters); } /// @@ -439,7 +447,7 @@ public override string ExistsQuery(string tableName, Expr filter) /// Column name to use to temporarily store the result. /// Expression filter. /// String. - public override string CountQuery(string tableName, string countColumnName, Expr filter) + public override QueryAndParameters CountQuery(string tableName, string countColumnName, Expr filter) { string query = ""; string whereClause = ""; @@ -454,13 +462,14 @@ public override string CountQuery(string tableName, string countColumnName, Expr // // expressions // - if (filter != null) whereClause = ExpressionToWhereClause(filter); + var parameters = new List>(); + if (filter != null) whereClause = ExpressionToWhereClause(filter, parameters); if (!String.IsNullOrEmpty(whereClause)) { query += "WHERE " + whereClause + " "; } - return query; + return (query, parameters); } /// @@ -471,7 +480,7 @@ public override string CountQuery(string tableName, string countColumnName, Expr /// Column name to temporarily store the result. /// Expression filter. /// String. - public override string SumQuery(string tableName, string fieldName, string sumColumnName, Expr filter) + public override QueryAndParameters SumQuery(string tableName, string fieldName, string sumColumnName, Expr filter) { string query = ""; string whereClause = ""; @@ -486,13 +495,14 @@ public override string SumQuery(string tableName, string fieldName, string sumCo // // expressions // - if (filter != null) whereClause = ExpressionToWhereClause(filter); + var parameters = new List>(); + if (filter != null) whereClause = ExpressionToWhereClause(filter, parameters); if (!String.IsNullOrEmpty(whereClause)) { query += "WHERE " + whereClause + " "; } - return query; + return (query, parameters); } /// @@ -515,488 +525,25 @@ public override string GenerateTimestampOffset(DateTimeOffset ts) return ts.DateTime.ToString(TimestampFormat); } - #endregion - - #region Private-Methods - - private string PreparedFieldName(string fieldName) + /// + /// Prepare a field name for use in a SQL query. + /// + /// Name of the field to be prepared. + /// Field name for use in a SQL query. + public override string PreparedFieldName(string fieldName) { return "`" + fieldName + "`"; } + #endregion + + #region Private-Methods + private string PreparedStringValue(string str) { return "'" + SanitizeString(str) + "'"; } - private string ExpressionToWhereClause(Expr expr) - { - if (expr == null) return null; - - string clause = ""; - - if (expr.Left == null) return null; - - clause += "("; - - if (expr.Left is Expr) - { - clause += ExpressionToWhereClause((Expr)expr.Left) + " "; - } - else - { - if (!(expr.Left is string)) - { - throw new ArgumentException("Left term must be of type Expression or String"); - } - - if (expr.Operator != OperatorEnum.Contains - && expr.Operator != OperatorEnum.ContainsNot - && expr.Operator != OperatorEnum.StartsWith - && expr.Operator != OperatorEnum.StartsWithNot - && expr.Operator != OperatorEnum.EndsWith - && expr.Operator != OperatorEnum.EndsWithNot) - { - // - // These operators will add the left term - // - clause += PreparedFieldName(expr.Left.ToString()) + " "; - } - } - - switch (expr.Operator) - { - #region Process-By-Operators - - case OperatorEnum.And: - #region And - - if (expr.Right == null) return null; - clause += "AND "; - - if (expr.Right is Expr) - { - clause += ExpressionToWhereClause((Expr)expr.Right); - } - else - { - if (expr.Right is DateTime || expr.Right is DateTime?) - { - clause += "'" + GenerateTimestamp(Convert.ToDateTime(expr.Right)) + "'"; - } - else if (expr.Right is int || expr.Right is long || expr.Right is decimal) - { - clause += expr.Right.ToString(); - } - else - { - clause += PreparedStringValue(expr.Right.ToString()); - } - } - break; - - #endregion - - case OperatorEnum.Or: - #region Or - - if (expr.Right == null) return null; - clause += "OR "; - if (expr.Right is Expr) - { - clause += ExpressionToWhereClause((Expr)expr.Right); - } - else - { - if (expr.Right is DateTime || expr.Right is DateTime?) - { - clause += "'" + GenerateTimestamp(Convert.ToDateTime(expr.Right)) + "'"; - } - else if (expr.Right is int || expr.Right is long || expr.Right is decimal) - { - clause += expr.Right.ToString(); - } - else - { - clause += PreparedStringValue(expr.Right.ToString()); - } - } - break; - - #endregion - - case OperatorEnum.Equals: - #region Equals - - if (expr.Right == null) return null; - clause += "= "; - if (expr.Right is Expr) - { - clause += ExpressionToWhereClause((Expr)expr.Right); - } - else - { - if (expr.Right is DateTime || expr.Right is DateTime?) - { - clause += "'" + GenerateTimestamp(Convert.ToDateTime(expr.Right)) + "'"; - } - else if (expr.Right is int || expr.Right is long || expr.Right is decimal) - { - clause += expr.Right.ToString(); - } - else - { - clause += PreparedStringValue(expr.Right.ToString()); - } - } - break; - - #endregion - - case OperatorEnum.NotEquals: - #region NotEquals - - if (expr.Right == null) return null; - clause += "<> "; - if (expr.Right is Expr) - { - clause += ExpressionToWhereClause((Expr)expr.Right); - } - else - { - if (expr.Right is DateTime || expr.Right is DateTime?) - { - clause += "'" + GenerateTimestamp(Convert.ToDateTime(expr.Right)) + "'"; - } - else if (expr.Right is int || expr.Right is long || expr.Right is decimal) - { - clause += expr.Right.ToString(); - } - else - { - clause += PreparedStringValue(expr.Right.ToString()); - } - } - break; - - #endregion - - case OperatorEnum.In: - #region In - - if (expr.Right == null) return null; - int inAdded = 0; - if (!Helper.IsList(expr.Right)) return null; - List inTempList = Helper.ObjectToList(expr.Right); - clause += " IN ("; - foreach (object currObj in inTempList) - { - if (currObj == null) continue; - if (inAdded > 0) clause += ","; - if (currObj is DateTime || currObj is DateTime?) - { - clause += "'" + GenerateTimestamp(Convert.ToDateTime(currObj)) + "'"; - } - else if (currObj is int || currObj is long || currObj is decimal) - { - clause += currObj.ToString(); - } - else - { - clause += PreparedStringValue(currObj.ToString()); - } - inAdded++; - } - clause += ")"; - break; - - #endregion - - case OperatorEnum.NotIn: - #region NotIn - - if (expr.Right == null) return null; - int notInAdded = 0; - if (!Helper.IsList(expr.Right)) return null; - List notInTempList = Helper.ObjectToList(expr.Right); - clause += " NOT IN ("; - foreach (object currObj in notInTempList) - { - if (currObj == null) continue; - if (notInAdded > 0) clause += ","; - if (currObj is DateTime || currObj is DateTime?) - { - clause += "'" + GenerateTimestamp(Convert.ToDateTime(currObj)) + "'"; - } - else if (currObj is int || currObj is long || currObj is decimal) - { - clause += currObj.ToString(); - } - else - { - clause += PreparedStringValue(currObj.ToString()); - } - notInAdded++; - } - clause += ")"; - break; - - #endregion - - case OperatorEnum.Contains: - #region Contains - - if (expr.Right == null) return null; - if (expr.Right is string) - { - clause += - "(" + - PreparedFieldName(expr.Left.ToString()) + " LIKE " + PreparedStringValue("%" + expr.Right.ToString()) + - "OR " + PreparedFieldName(expr.Left.ToString()) + " LIKE " + PreparedStringValue("%" + expr.Right.ToString() + "%") + - "OR " + PreparedFieldName(expr.Left.ToString()) + " LIKE " + PreparedStringValue(expr.Right.ToString() + "%") + - ")"; - } - else - { - return null; - } - break; - - #endregion - - case OperatorEnum.ContainsNot: - #region ContainsNot - - if (expr.Right == null) return null; - if (expr.Right is string) - { - clause += - "(" + - PreparedFieldName(expr.Left.ToString()) + " NOT LIKE " + PreparedStringValue("%" + expr.Right.ToString()) + - "OR " + PreparedFieldName(expr.Left.ToString()) + " NOT LIKE " + PreparedStringValue("%" + expr.Right.ToString() + "%") + - "OR " + PreparedFieldName(expr.Left.ToString()) + " NOT LIKE " + PreparedStringValue(expr.Right.ToString() + "%") + - ")"; - } - else - { - return null; - } - break; - - #endregion - - case OperatorEnum.StartsWith: - #region StartsWith - - if (expr.Right == null) return null; - if (expr.Right is string) - { - clause += - "(" + - PreparedFieldName(expr.Left.ToString()) + " LIKE " + PreparedStringValue(expr.Right.ToString() + "%") + - ")"; - } - else - { - return null; - } - break; - - #endregion - - case OperatorEnum.StartsWithNot: - #region StartsWithNot - - if (expr.Right == null) return null; - if (expr.Right is string) - { - clause += - "(" + - PreparedFieldName(expr.Left.ToString()) + " NOT LIKE " + PreparedStringValue(expr.Right.ToString() + "%") + - ")"; - } - else - { - return null; - } - break; - - #endregion - - case OperatorEnum.EndsWith: - #region EndsWith - - if (expr.Right == null) return null; - if (expr.Right is string) - { - clause += - "(" + - PreparedFieldName(expr.Left.ToString()) + " LIKE " + PreparedStringValue("%" + expr.Right.ToString()) + - ")"; - } - else - { - return null; - } - break; - - #endregion - - case OperatorEnum.EndsWithNot: - #region EndsWithNot - - if (expr.Right == null) return null; - if (expr.Right is string) - { - clause += - "(" + - PreparedFieldName(expr.Left.ToString()) + " NOT LIKE " + PreparedStringValue("%" + expr.Right.ToString()) + - ")"; - } - else - { - return null; - } - break; - - #endregion - - case OperatorEnum.GreaterThan: - #region GreaterThan - - if (expr.Right == null) return null; - clause += "> "; - if (expr.Right is Expr) - { - clause += ExpressionToWhereClause((Expr)expr.Right); - } - else - { - if (expr.Right is DateTime || expr.Right is DateTime?) - { - clause += "'" + GenerateTimestamp(Convert.ToDateTime(expr.Right)) + "'"; - } - else if (expr.Right is int || expr.Right is long || expr.Right is decimal) - { - clause += expr.Right.ToString(); - } - else - { - clause += PreparedStringValue(expr.Right.ToString()); - } - } - break; - - #endregion - - case OperatorEnum.GreaterThanOrEqualTo: - #region GreaterThanOrEqualTo - - if (expr.Right == null) return null; - clause += ">= "; - if (expr.Right is Expr) - { - clause += ExpressionToWhereClause((Expr)expr.Right); - } - else - { - if (expr.Right is DateTime || expr.Right is DateTime?) - { - clause += "'" + GenerateTimestamp(Convert.ToDateTime(expr.Right)) + "'"; - } - else if (expr.Right is int || expr.Right is long || expr.Right is decimal) - { - clause += expr.Right.ToString(); - } - else - { - clause += PreparedStringValue(expr.Right.ToString()); - } - } - break; - - #endregion - - case OperatorEnum.LessThan: - #region LessThan - - if (expr.Right == null) return null; - clause += "< "; - if (expr.Right is Expr) - { - clause += ExpressionToWhereClause((Expr)expr.Right); - } - else - { - if (expr.Right is DateTime || expr.Right is DateTime?) - { - clause += "'" + GenerateTimestamp(Convert.ToDateTime(expr.Right)) + "'"; - } - else if (expr.Right is int || expr.Right is long || expr.Right is decimal) - { - clause += expr.Right.ToString(); - } - else - { - clause += PreparedStringValue(expr.Right.ToString()); - } - } - break; - - #endregion - - case OperatorEnum.LessThanOrEqualTo: - #region LessThanOrEqualTo - - if (expr.Right == null) return null; - clause += "<= "; - if (expr.Right is Expr) - { - clause += ExpressionToWhereClause((Expr)expr.Right); - } - else - { - if (expr.Right is DateTime || expr.Right is DateTime?) - { - clause += "'" + GenerateTimestamp(Convert.ToDateTime(expr.Right)) + "'"; - } - else if (expr.Right is int || expr.Right is long || expr.Right is decimal) - { - clause += expr.Right.ToString(); - } - else - { - clause += PreparedStringValue(expr.Right.ToString()); - } - } - break; - - #endregion - - case OperatorEnum.IsNull: - #region IsNull - - clause += " IS NULL"; - break; - - #endregion - - case OperatorEnum.IsNotNull: - #region IsNotNull - - clause += " IS NOT NULL"; - break; - - #endregion - - #endregion - } - - clause += ")"; - - return clause; - } - private string PreparedUnicodeValue(string s) { return "N" + PreparedStringValue(s); @@ -1020,81 +567,6 @@ private string BuildOrderByClause(ResultOrder[] resultOrder) return ret; } - private void BuildKeysValuesFromDictionary(Dictionary keyValuePairs, out string keys, out string vals) - { - keys = ""; - vals = ""; - int added = 0; - - foreach (KeyValuePair currKvp in keyValuePairs) - { - if (String.IsNullOrEmpty(currKvp.Key)) continue; - - if (added > 0) - { - keys += ","; - vals += ","; - } - - keys += PreparedFieldName(currKvp.Key); - - if (currKvp.Value != null) - { - if (currKvp.Value is DateTime - || currKvp.Value is DateTime?) - { - vals += "'" + ((DateTime)currKvp.Value).ToString(TimestampFormat) + "'"; - } - else if (currKvp.Value is DateTimeOffset - || currKvp.Value is DateTimeOffset?) - { - vals += "'" + ((DateTimeOffset)currKvp.Value).ToString(TimestampOffsetFormat) + "'"; - } - else if (currKvp.Value is int - || currKvp.Value is long - || currKvp.Value is decimal) - { - vals += currKvp.Value.ToString(); - } - else if (currKvp.Value is bool) - { - vals += ((bool)currKvp.Value ? "1" : "0"); - } - else if (currKvp.Value is byte[]) - { - vals += "x'" + BitConverter.ToString((byte[])currKvp.Value).Replace("-", "") + "'"; - } - else - { - if (IsExtendedCharacters(currKvp.Value.ToString())) - { - vals += PreparedUnicodeValue(currKvp.Value.ToString()); - } - else - { - vals += PreparedStringValue(currKvp.Value.ToString()); - } - } - } - else - { - vals += "null"; - } - - added++; - } - } - - private bool IsExtendedCharacters(string data) - { - if (String.IsNullOrEmpty(data)) return false; - foreach (char c in data) - { - if ((int)c > 255) return true; - } - return false; - } - private void ValidateInputDictionaries(List> keyValuePairList) { Dictionary reference = keyValuePairList[0]; @@ -1125,132 +597,6 @@ private string BuildKeysFromDictionary(Dictionary reference) return keys; } - private List BuildValuesFromDictionaries(List> dicts) - { - List values = new List(); - - foreach (Dictionary currDict in dicts) - { - string vals = ""; - int valsAdded = 0; - - foreach (KeyValuePair currKvp in currDict) - { - if (valsAdded > 0) vals += ","; - - if (currKvp.Value != null) - { - if (currKvp.Value is DateTime - || currKvp.Value is DateTime?) - { - vals += "'" + ((DateTime)currKvp.Value).ToString(TimestampFormat) + "'"; - } - else if (currKvp.Value is DateTimeOffset - || currKvp.Value is DateTimeOffset?) - { - vals += "'" + ((DateTimeOffset)currKvp.Value).ToString(TimestampOffsetFormat) + "'"; - } - else if (currKvp.Value is int - || currKvp.Value is long - || currKvp.Value is decimal) - { - vals += currKvp.Value.ToString(); - } - else if (currKvp.Value is bool) - { - vals += ((bool)currKvp.Value ? "1" : "0"); - } - else if (currKvp.Value is byte[]) - { - vals += "x'" + BitConverter.ToString((byte[])currKvp.Value).Replace("-", "") + "'"; - } - else - { - if (IsExtendedCharacters(currKvp.Value.ToString())) - { - vals += PreparedUnicodeValue(currKvp.Value.ToString()); - } - else - { - vals += PreparedStringValue(currKvp.Value.ToString()); - } - } - - } - else - { - vals += "null"; - } - - valsAdded++; - } - - values.Add(vals); - } - - return values; - } - - private string BuildKeyValueClauseFromDictionary(Dictionary keyValuePairs) - { - string keyValueClause = ""; - int added = 0; - - foreach (KeyValuePair currKvp in keyValuePairs) - { - if (String.IsNullOrEmpty(currKvp.Key)) continue; - - if (added > 0) keyValueClause += ","; - - if (currKvp.Value != null) - { - if (currKvp.Value is DateTime - || currKvp.Value is DateTime?) - { - keyValueClause += PreparedFieldName(currKvp.Key) + "='" + ((DateTime)currKvp.Value).ToString(TimestampFormat) + "'"; - } - else if (currKvp.Value is DateTimeOffset - || currKvp.Value is DateTimeOffset?) - { - keyValueClause += PreparedFieldName(currKvp.Key) + "='" + ((DateTimeOffset)currKvp.Value).ToString(TimestampOffsetFormat) + "'"; - } - else if (currKvp.Value is int - || currKvp.Value is long - || currKvp.Value is decimal) - { - keyValueClause += PreparedFieldName(currKvp.Key) + "=" + currKvp.Value.ToString(); - } - else if (currKvp.Value is bool) - { - keyValueClause += PreparedFieldName(currKvp.Key) + "=" + ((bool)currKvp.Value ? "1" : "0"); - } - else if (currKvp.Value is byte[]) - { - keyValueClause += PreparedFieldName(currKvp.Key) + "=" + "x'" + BitConverter.ToString((byte[])currKvp.Value).Replace("-", "") + "'"; - } - else - { - if (IsExtendedCharacters(currKvp.Value.ToString())) - { - keyValueClause += PreparedFieldName(currKvp.Key) + "=" + PreparedUnicodeValue(currKvp.Value.ToString()); - } - else - { - keyValueClause += PreparedFieldName(currKvp.Key) + "=" + PreparedStringValue(currKvp.Value.ToString()); - } - } - } - else - { - keyValueClause += PreparedFieldName(currKvp.Key) + "= null"; - } - - added++; - } - - return keyValueClause; - } - #endregion } } \ No newline at end of file diff --git a/src/DatabaseWrapper.Postgresql/DatabaseClient.cs b/src/DatabaseWrapper.Postgresql/DatabaseClient.cs index 208e997..5dd28a7 100644 --- a/src/DatabaseWrapper.Postgresql/DatabaseClient.cs +++ b/src/DatabaseWrapper.Postgresql/DatabaseClient.cs @@ -9,6 +9,7 @@ using ExpressionTree; using DatabaseWrapper.Core; using System.Threading; +using System.Runtime.CompilerServices; namespace DatabaseWrapper.Postgresql { @@ -116,6 +117,16 @@ private set private string _SumColumnName = "__sum__"; private PostgresqlHelper _Helper = new PostgresqlHelper(); + private static IEnumerable> CorrectDateTimeOffsets(IEnumerable> parameters) + => parameters?.Select(kv => { + // TODO: is it correct to drop the offset on DateTimeOffset parameters? + if (kv.Value != null && kv.Value.GetType()==typeof(DateTimeOffset)) + { + var dto = (DateTimeOffset)kv.Value; + if (dto.Offset != TimeSpan.Zero) return new KeyValuePair(kv.Key, new DateTimeOffset(dto.UtcDateTime)); + } + return kv; + }); #endregion #region Constructors-and-Factories @@ -733,10 +744,11 @@ public override async Task TruncateAsync(string tableName, CancellationToken tok /// /// Execute a query. /// - /// Database query defined outside of the database client. + /// Database query defined outside of the database client, and, the corresponding parameters. /// A DataTable containing the results. - public override DataTable Query(string query) + public override DataTable Query((string Query, IEnumerable> Parameters) queryAndParameters) { + (var query, var parameters) = queryAndParameters; if (String.IsNullOrEmpty(query)) throw new ArgumentNullException(query); if (query.Length > MaxStatementLength) throw new ArgumentException("Query exceeds maximum statement length of " + MaxStatementLength + " characters."); @@ -754,16 +766,20 @@ public override DataTable Query(string query) conn.Open(); #pragma warning disable CA2100 // Review SQL queries for security vulnerabilities - using (NpgsqlDataAdapter da = new NpgsqlDataAdapter(query, conn)) + using (var cmd = new NpgsqlCommand(query, conn)) { + DatabaseHelperBase.AddParameters(cmd, (name,type) => new NpgsqlParameter(name,type), CorrectDateTimeOffsets(parameters)); + using (NpgsqlDataAdapter da = new NpgsqlDataAdapter(cmd)) + { #pragma warning restore CA2100 // Review SQL queries for security vulnerabilities - DataSet ds = new DataSet(); - da.Fill(ds); + DataSet ds = new DataSet(); + da.Fill(ds); - if (ds != null && ds.Tables != null && ds.Tables.Count > 0) - { - result = ds.Tables[0]; + if (ds != null && ds.Tables != null && ds.Tables.Count > 0) + { + result = ds.Tables[0]; + } } } @@ -800,11 +816,12 @@ public override DataTable Query(string query) /// /// Execute a query. /// - /// Database query defined outside of the database client. + /// Database query defined outside of the database client, and, the corresponding parameters. /// Cancellation token. /// A DataTable containing the results. - public override async Task QueryAsync(string query, CancellationToken token = default) + public override async Task QueryAsync((string Query, IEnumerable> Parameters) queryAndParameters, CancellationToken token = default) { + (var query, var parameters) = queryAndParameters; if (String.IsNullOrEmpty(query)) throw new ArgumentNullException(query); if (query.Length > MaxStatementLength) throw new ArgumentException("Query exceeds maximum statement length of " + MaxStatementLength + " characters."); @@ -821,17 +838,21 @@ public override async Task QueryAsync(string query, CancellationToken { await conn.OpenAsync(token).ConfigureAwait(false); -#pragma warning disable CA2100 // Review SQL queries for security vulnerabilities - using (NpgsqlDataAdapter da = new NpgsqlDataAdapter(query, conn)) +#pragma warning disable CA2100 // Review SQL queries for security + using (var cmd = new NpgsqlCommand(query, conn)) { + DatabaseHelperBase.AddParameters(cmd, (name,type) => new NpgsqlParameter(name,type), CorrectDateTimeOffsets(parameters)); + using (NpgsqlDataAdapter da = new NpgsqlDataAdapter(cmd)) + { #pragma warning restore CA2100 // Review SQL queries for security vulnerabilities - DataSet ds = new DataSet(); - da.Fill(ds); + DataSet ds = new DataSet(); + da.Fill(ds); - if (ds != null && ds.Tables != null && ds.Tables.Count > 0) - { - result = ds.Tables[0]; + if (ds != null && ds.Tables != null && ds.Tables.Count > 0) + { + result = ds.Tables[0]; + } } } diff --git a/src/DatabaseWrapper.Postgresql/DatabaseWrapper.Postgresql.xml b/src/DatabaseWrapper.Postgresql/DatabaseWrapper.Postgresql.xml index 7682e4b..0f5f038 100644 --- a/src/DatabaseWrapper.Postgresql/DatabaseWrapper.Postgresql.xml +++ b/src/DatabaseWrapper.Postgresql/DatabaseWrapper.Postgresql.xml @@ -308,18 +308,18 @@ The table you wish to TRUNCATE. Cancellation token. - + Execute a query. - Database query defined outside of the database client. + Database query defined outside of the database client, and, the corresponding parameters. A DataTable containing the results. - + Execute a query. - Database query defined outside of the database client. + Database query defined outside of the database client, and, the corresponding parameters. Cancellation token. A DataTable containing the results. @@ -576,5 +576,12 @@ String. String. + + + Prepare a field name for use in a SQL query. + + Name of the field to be prepared. + Field name for use in a SQL query. + diff --git a/src/DatabaseWrapper.Postgresql/PostgresqlHelper.cs b/src/DatabaseWrapper.Postgresql/PostgresqlHelper.cs index b18a934..a334053 100644 --- a/src/DatabaseWrapper.Postgresql/PostgresqlHelper.cs +++ b/src/DatabaseWrapper.Postgresql/PostgresqlHelper.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using static System.FormattableString; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -7,8 +8,11 @@ using DatabaseWrapper.Core; using ExpressionTree; + namespace DatabaseWrapper.Postgresql { + using QueryAndParameters = System.ValueTuple>>; + /// /// PostgreSQL implementation of helper properties and methods. /// @@ -226,7 +230,7 @@ public override string DropTableQuery(string tableName) /// Expression filter. /// Result order. /// String. - public override string SelectQuery(string tableName, int? indexStart, int? maxResults, List returnFields, Expr filter, ResultOrder[] resultOrder) + public override QueryAndParameters SelectQuery(string tableName, int? indexStart, int? maxResults, List returnFields, Expr filter, ResultOrder[] resultOrder) { string query = ""; string whereClause = ""; @@ -259,7 +263,8 @@ public override string SelectQuery(string tableName, int? indexStart, int? maxRe query += "FROM " + PreparedTableName(tableName) + " "; // expressions - if (filter != null) whereClause = ExpressionToWhereClause(filter); + var parameters_list = new List>(); + if (filter != null) whereClause = ExpressionToWhereClause(filter, parameters_list); if (!String.IsNullOrEmpty(whereClause)) { query += "WHERE " + whereClause + " "; @@ -281,7 +286,7 @@ public override string SelectQuery(string tableName, int? indexStart, int? maxRe } } - return query; + return (query, parameters_list); } /// @@ -290,22 +295,19 @@ public override string SelectQuery(string tableName, int? indexStart, int? maxRe /// The table in which you wish to INSERT. /// The key-value pairs for the row you wish to INSERT. /// String. - public override string InsertQuery(string tableName, Dictionary keyValuePairs) + public override QueryAndParameters InsertQuery(string tableName, Dictionary keyValuePairs) { + var o_ret = keyValuePairs.Select(kv => new KeyValuePair("@F_" + kv.Key, kv.Value)); string ret = "INSERT INTO " + PreparedTableName(tableName) + " " + "("; - string keys = ""; - string vals = ""; - BuildKeysValuesFromDictionary(keyValuePairs, out keys, out vals); - - ret += keys + ") " + + ret += string.Join(", ", keyValuePairs.Keys.Select(k => PreparedFieldName(k))) + ") " + "VALUES " + - "(" + vals + ") " + + "(" + string.Join(", ", o_ret.Select(k => k.Key)) + ") " + "RETURNING *;"; - return ret; + return (ret, o_ret); } /// @@ -314,11 +316,11 @@ public override string InsertQuery(string tableName, Dictionary /// The table in which you wish to INSERT. /// List of dictionaries containing key-value pairs for the rows you wish to INSERT. /// String. - public override string InsertMultipleQuery(string tableName, List> keyValuePairList) + public override QueryAndParameters InsertMultipleQuery(string tableName, List> keyValuePairList) { ValidateInputDictionaries(keyValuePairList); string keys = BuildKeysFromDictionary(keyValuePairList[0]); - List values = BuildValuesFromDictionaries(keyValuePairList); + var ret_values = new List>(); string ret = "BEGIN TRANSACTION;" + @@ -326,18 +328,22 @@ public override string InsertMultipleQuery(string tableName, List 0) ret += ","; - ret += " (" + value + ")"; - added++; + var dict = keyValuePairList[i_dict]; + var prefix = Invariant($"@F{i_dict}_"); + var this_round = dict.Select(kv => new KeyValuePair(prefix + kv.Key, kv.Value)); + if (i_dict>0) { + ret += ", "; + } + ret += "(" + string.Join(", ", this_round.Select(kv => kv.Key)) + ")"; + ret_values.AddRange(this_round); } ret += "; COMMIT; "; - return ret; + return (ret, ret_values); } /// @@ -347,18 +353,19 @@ public override string InsertMultipleQuery(string tableName, ListThe key-value pairs for the data you wish to UPDATE. /// The expression containing the UPDATE filter (i.e. WHERE clause data). /// String. - public override string UpdateQuery(string tableName, Dictionary keyValuePairs, Expr filter) + public override QueryAndParameters UpdateQuery(string tableName, Dictionary keyValuePairs, Expr filter) { - string keyValueClause = BuildKeyValueClauseFromDictionary(keyValuePairs); + const string FIELD_PREFIX = "@F"; + var parameters = keyValuePairs.Select(kv => new KeyValuePair(FIELD_PREFIX + kv.Key, kv.Value)).ToList(); string ret = "UPDATE " + PreparedTableName(tableName) + " SET " + - keyValueClause + " "; + string.Join(", ", parameters.Select(kv => kv.Key.Substring(FIELD_PREFIX.Length) + "=" + kv.Key)) + " "; - if (filter != null) ret += "WHERE " + ExpressionToWhereClause(filter) + " "; + if (filter != null) ret += "WHERE " + ExpressionToWhereClause(filter, parameters) + " "; ret += "RETURNING *"; - return ret; + return (ret, parameters); } /// @@ -367,14 +374,15 @@ public override string UpdateQuery(string tableName, Dictionary /// Table name. /// Expression filter. /// String. - public override string DeleteQuery(string tableName, Expr filter) + public override QueryAndParameters DeleteQuery(string tableName, Expr filter) { string ret = "DELETE FROM " + PreparedTableName(tableName) + " "; - if (filter != null) ret += "WHERE " + ExpressionToWhereClause(filter) + " "; + var parameters = new List>(); + if (filter != null) ret += "WHERE " + ExpressionToWhereClause(filter, parameters) + " "; - return ret; + return (ret, parameters); } /// @@ -393,7 +401,7 @@ public override string TruncateQuery(string tableName) /// Table name. /// Expression filter. /// String. - public override string ExistsQuery(string tableName, Expr filter) + public override QueryAndParameters ExistsQuery(string tableName, Expr filter) { string query = ""; string whereClause = ""; @@ -404,14 +412,15 @@ public override string ExistsQuery(string tableName, Expr filter) "FROM " + PreparedTableName(tableName) + " "; // expressions - if (filter != null) whereClause = ExpressionToWhereClause(filter); + var parameters = new List>(); + if (filter != null) whereClause = ExpressionToWhereClause(filter, parameters); if (!String.IsNullOrEmpty(whereClause)) { query += "WHERE " + whereClause + " "; } query += "LIMIT 1"; - return query; + return (query, parameters); } /// @@ -421,7 +430,7 @@ public override string ExistsQuery(string tableName, Expr filter) /// Column name to use to temporarily store the result. /// Expression filter. /// String. - public override string CountQuery(string tableName, string countColumnName, Expr filter) + public override QueryAndParameters CountQuery(string tableName, string countColumnName, Expr filter) { string query = ""; string whereClause = ""; @@ -432,13 +441,14 @@ public override string CountQuery(string tableName, string countColumnName, Expr "FROM " + PreparedTableName(tableName) + " "; // expressions - if (filter != null) whereClause = ExpressionToWhereClause(filter); + var parameters = new List>(); + if (filter != null) whereClause = ExpressionToWhereClause(filter, parameters); if (!String.IsNullOrEmpty(whereClause)) { query += "WHERE " + whereClause + " "; } - return query; + return (query, parameters); } /// @@ -449,7 +459,7 @@ public override string CountQuery(string tableName, string countColumnName, Expr /// Column name to temporarily store the result. /// Expression filter. /// String. - public override string SumQuery(string tableName, string fieldName, string sumColumnName, Expr filter) + public override QueryAndParameters SumQuery(string tableName, string fieldName, string sumColumnName, Expr filter) { string whereClause = ""; @@ -459,13 +469,14 @@ public override string SumQuery(string tableName, string fieldName, string sumCo "FROM " + PreparedTableName(tableName) + " "; // expressions - if (filter != null) whereClause = ExpressionToWhereClause(filter); + var parameters = new List>(); + if (filter != null) whereClause = ExpressionToWhereClause(filter, parameters); if (!String.IsNullOrEmpty(whereClause)) { query += "WHERE " + whereClause + " "; } - return query; + return (query, parameters); } /// @@ -509,15 +520,20 @@ public string ExtractTableName(string s) } } - #endregion - - #region Private-Methods - - private string PreparedFieldName(string fieldName) + /// + /// Prepare a field name for use in a SQL query. + /// + /// Name of the field to be prepared. + /// Field name for use in a SQL query. + public override string PreparedFieldName(string fieldName) { return "\"" + fieldName + "\""; } + #endregion + + #region Private-Methods + private string PreparedStringValue(string str) { // uses $xx$ escaping @@ -530,474 +546,6 @@ private string PreparedUnicodeValue(string s) return PreparedStringValue(s); } - private string ExpressionToWhereClause(Expr expr) - { - if (expr == null) return null; - - string clause = ""; - - if (expr.Left == null) return null; - - clause += "("; - - if (expr.Left is Expr) - { - clause += ExpressionToWhereClause((Expr)expr.Left) + " "; - } - else - { - if (!(expr.Left is string)) - { - throw new ArgumentException("Left term must be of type Expression or String"); - } - - if (expr.Operator != OperatorEnum.Contains - && expr.Operator != OperatorEnum.ContainsNot - && expr.Operator != OperatorEnum.StartsWith - && expr.Operator != OperatorEnum.StartsWithNot - && expr.Operator != OperatorEnum.EndsWith - && expr.Operator != OperatorEnum.EndsWithNot) - { - // - // These operators will add the left term - // - clause += PreparedFieldName(expr.Left.ToString()) + " "; - } - } - - switch (expr.Operator) - { - #region Process-By-Operators - - case OperatorEnum.And: - #region And - - if (expr.Right == null) return null; - clause += "AND "; - - if (expr.Right is Expr) - { - clause += ExpressionToWhereClause((Expr)expr.Right); - } - else - { - if (expr.Right is DateTime || expr.Right is DateTime?) - { - clause += "'" + GenerateTimestamp(Convert.ToDateTime(expr.Right)) + "'"; - } - else if (expr.Right is int || expr.Right is long || expr.Right is decimal) - { - clause += expr.Right.ToString(); - } - else - { - clause += PreparedStringValue(expr.Right.ToString()); - } - } - break; - - #endregion - - case OperatorEnum.Or: - #region Or - - if (expr.Right == null) return null; - clause += "OR "; - if (expr.Right is Expr) - { - clause += ExpressionToWhereClause((Expr)expr.Right); - } - else - { - if (expr.Right is DateTime || expr.Right is DateTime?) - { - clause += "'" + GenerateTimestamp(Convert.ToDateTime(expr.Right)) + "'"; - } - else if (expr.Right is int || expr.Right is long || expr.Right is decimal) - { - clause += expr.Right.ToString(); - } - else - { - clause += PreparedStringValue(expr.Right.ToString()); - } - } - break; - - #endregion - - case OperatorEnum.Equals: - #region Equals - - if (expr.Right == null) return null; - clause += "= "; - if (expr.Right is Expr) - { - clause += ExpressionToWhereClause((Expr)expr.Right); - } - else - { - if (expr.Right is DateTime || expr.Right is DateTime?) - { - clause += "'" + GenerateTimestamp(Convert.ToDateTime(expr.Right)) + "'"; - } - else if (expr.Right is int || expr.Right is long || expr.Right is decimal) - { - clause += expr.Right.ToString(); - } - else - { - clause += PreparedStringValue(expr.Right.ToString()); - } - } - break; - - #endregion - - case OperatorEnum.NotEquals: - #region NotEquals - - if (expr.Right == null) return null; - clause += "<> "; - if (expr.Right is Expr) - { - clause += ExpressionToWhereClause((Expr)expr.Right); - } - else - { - if (expr.Right is DateTime || expr.Right is DateTime?) - { - clause += "'" + GenerateTimestamp(Convert.ToDateTime(expr.Right)) + "'"; - } - else if (expr.Right is int || expr.Right is long || expr.Right is decimal) - { - clause += expr.Right.ToString(); - } - else - { - clause += PreparedStringValue(expr.Right.ToString()); - } - } - break; - - #endregion - - case OperatorEnum.In: - #region In - - if (expr.Right == null) return null; - int inAdded = 0; - if (!Helper.IsList(expr.Right)) return null; - List inTempList = Helper.ObjectToList(expr.Right); - clause += " IN ("; - foreach (object currObj in inTempList) - { - if (currObj == null) continue; - if (inAdded > 0) clause += ","; - if (currObj is DateTime || currObj is DateTime?) - { - clause += "'" + GenerateTimestamp(Convert.ToDateTime(currObj)) + "'"; - } - else if (currObj is int || currObj is long || currObj is decimal) - { - clause += currObj.ToString(); - } - else - { - clause += PreparedStringValue(currObj.ToString()); - } - inAdded++; - } - clause += ")"; - break; - - #endregion - - case OperatorEnum.NotIn: - #region NotIn - - if (expr.Right == null) return null; - int notInAdded = 0; - if (!Helper.IsList(expr.Right)) return null; - List notInTempList = Helper.ObjectToList(expr.Right); - clause += " NOT IN ("; - foreach (object currObj in notInTempList) - { - if (currObj == null) continue; - if (notInAdded > 0) clause += ","; - if (currObj is DateTime || currObj is DateTime?) - { - clause += "'" + GenerateTimestamp(Convert.ToDateTime(currObj)) + "'"; - } - else if (currObj is int || currObj is long || currObj is decimal) - { - clause += currObj.ToString(); - } - else - { - clause += PreparedStringValue(currObj.ToString()); - } - notInAdded++; - } - clause += ")"; - break; - - #endregion - - case OperatorEnum.Contains: - #region Contains - - if (expr.Right == null) return null; - if (expr.Right is string) - { - clause += - "(" + - PreparedFieldName(expr.Left.ToString()) + " LIKE " + PreparedStringValue("%" + expr.Right.ToString()) + - "OR " + PreparedFieldName(expr.Left.ToString()) + " LIKE " + PreparedStringValue("%" + expr.Right.ToString() + "%") + - "OR " + PreparedFieldName(expr.Left.ToString()) + " LIKE " + PreparedStringValue(expr.Right.ToString() + "%") + - ")"; - } - else - { - return null; - } - break; - - #endregion - - case OperatorEnum.ContainsNot: - #region ContainsNot - - if (expr.Right == null) return null; - if (expr.Right is string) - { - clause += - "(" + - PreparedFieldName(expr.Left.ToString()) + " NOT LIKE " + PreparedStringValue("%" + expr.Right.ToString()) + - "OR " + PreparedFieldName(expr.Left.ToString()) + " NOT LIKE " + PreparedStringValue("%" + expr.Right.ToString() + "%") + - "OR " + PreparedFieldName(expr.Left.ToString()) + " NOT LIKE " + PreparedStringValue(expr.Right.ToString() + "%") + - ")"; - } - else - { - return null; - } - break; - - #endregion - - case OperatorEnum.StartsWith: - #region StartsWith - - if (expr.Right == null) return null; - if (expr.Right is string) - { - clause += - "(" + - PreparedFieldName(expr.Left.ToString()) + " LIKE " + PreparedStringValue(expr.Right.ToString() + "%") + - ")"; - } - else - { - return null; - } - break; - - #endregion - - case OperatorEnum.StartsWithNot: - #region StartsWithNot - - if (expr.Right == null) return null; - if (expr.Right is string) - { - clause += - "(" + - PreparedFieldName(expr.Left.ToString()) + " NOT LIKE " + PreparedStringValue(expr.Right.ToString() + "%") + - ")"; - } - else - { - return null; - } - break; - - #endregion - - case OperatorEnum.EndsWith: - #region EndsWith - - if (expr.Right == null) return null; - if (expr.Right is string) - { - clause += - "(" + - PreparedFieldName(expr.Left.ToString()) + " LIKE " + PreparedStringValue("%" + expr.Right.ToString()) + - ")"; - } - else - { - return null; - } - break; - - #endregion - - case OperatorEnum.EndsWithNot: - #region EndsWithNot - - if (expr.Right == null) return null; - if (expr.Right is string) - { - clause += - "(" + - PreparedFieldName(expr.Left.ToString()) + " NOT LIKE " + PreparedStringValue("%" + expr.Right.ToString()) + - ")"; - } - else - { - return null; - } - break; - - #endregion - - case OperatorEnum.GreaterThan: - #region GreaterThan - - if (expr.Right == null) return null; - clause += "> "; - if (expr.Right is Expr) - { - clause += ExpressionToWhereClause((Expr)expr.Right); - } - else - { - if (expr.Right is DateTime || expr.Right is DateTime?) - { - clause += "'" + GenerateTimestamp(Convert.ToDateTime(expr.Right)) + "'"; - } - else if (expr.Right is int || expr.Right is long || expr.Right is decimal) - { - clause += expr.Right.ToString(); - } - else - { - clause += PreparedStringValue(expr.Right.ToString()); - } - } - break; - - #endregion - - case OperatorEnum.GreaterThanOrEqualTo: - #region GreaterThanOrEqualTo - - if (expr.Right == null) return null; - clause += ">= "; - if (expr.Right is Expr) - { - clause += ExpressionToWhereClause((Expr)expr.Right); - } - else - { - if (expr.Right is DateTime || expr.Right is DateTime?) - { - clause += "'" + GenerateTimestamp(Convert.ToDateTime(expr.Right)) + "'"; - } - else if (expr.Right is int || expr.Right is long || expr.Right is decimal) - { - clause += expr.Right.ToString(); - } - else - { - clause += PreparedStringValue(expr.Right.ToString()); - } - } - break; - - #endregion - - case OperatorEnum.LessThan: - #region LessThan - - if (expr.Right == null) return null; - clause += "< "; - if (expr.Right is Expr) - { - clause += ExpressionToWhereClause((Expr)expr.Right); - } - else - { - if (expr.Right is DateTime || expr.Right is DateTime?) - { - clause += "'" + GenerateTimestamp(Convert.ToDateTime(expr.Right)) + "'"; - } - else if (expr.Right is int || expr.Right is long || expr.Right is decimal) - { - clause += expr.Right.ToString(); - } - else - { - clause += PreparedStringValue(expr.Right.ToString()); - } - } - break; - - #endregion - - case OperatorEnum.LessThanOrEqualTo: - #region LessThanOrEqualTo - - if (expr.Right == null) return null; - clause += "<= "; - if (expr.Right is Expr) - { - clause += ExpressionToWhereClause((Expr)expr.Right); - } - else - { - if (expr.Right is DateTime || expr.Right is DateTime?) - { - clause += "'" + GenerateTimestamp(Convert.ToDateTime(expr.Right)) + "'"; - } - else if (expr.Right is int || expr.Right is long || expr.Right is decimal) - { - clause += expr.Right.ToString(); - } - else - { - clause += PreparedStringValue(expr.Right.ToString()); - } - } - break; - - #endregion - - case OperatorEnum.IsNull: - #region IsNull - - clause += " IS NULL"; - break; - - #endregion - - case OperatorEnum.IsNotNull: - #region IsNotNull - - clause += " IS NOT NULL"; - break; - - #endregion - - #endregion - } - - clause += ")"; - - return clause; - } - private string BuildOrderByClause(ResultOrder[] resultOrder) { if (resultOrder == null || resultOrder.Length < 0) return null; @@ -1224,70 +772,6 @@ private string PreparedTableName(string s) } } - private void BuildKeysValuesFromDictionary(Dictionary keyValuePairs, out string keys, out string vals) - { - keys = ""; - vals = ""; - int added = 0; - - foreach (KeyValuePair currKvp in keyValuePairs) - { - if (String.IsNullOrEmpty(currKvp.Key)) continue; - - if (added > 0) - { - keys += ","; - vals += ","; - } - - keys += PreparedFieldName(currKvp.Key); - - if (currKvp.Value != null) - { - if (currKvp.Value is DateTime - || currKvp.Value is DateTime?) - { - vals += "'" + ((DateTime)currKvp.Value).ToString(TimestampFormat) + "'"; - } - else if (currKvp.Value is DateTimeOffset - || currKvp.Value is DateTimeOffset?) - { - vals += "'" + ((DateTimeOffset)currKvp.Value).ToString(TimestampOffsetFormat) + "'"; - } - else if (currKvp.Value is int - || currKvp.Value is long - || currKvp.Value is decimal) - { - vals += currKvp.Value.ToString(); - } - else if (currKvp.Value is bool) - { - vals += ((bool)currKvp.Value ? "1" : "0"); - } - else if (currKvp.Value is byte[]) - { - vals += "decode('" + Helper.ByteArrayToHexString((byte[])currKvp.Value) + "', 'hex')"; - } - else - { - if (Helper.IsExtendedCharacters(currKvp.Value.ToString())) - { - vals += PreparedUnicodeValue(currKvp.Value.ToString()); - } - else - { - vals += PreparedStringValue(currKvp.Value.ToString()); - } - } - } - else - { - vals += "null"; - } - - added++; - } - } private void ValidateInputDictionaries(List> keyValuePairList) { @@ -1318,133 +802,6 @@ private string BuildKeysFromDictionary(Dictionary reference) return keys; } - - private List BuildValuesFromDictionaries(List> dicts) - { - List values = new List(); - - foreach (Dictionary currDict in dicts) - { - string vals = ""; - int valsAdded = 0; - - foreach (KeyValuePair currKvp in currDict) - { - if (valsAdded > 0) vals += ","; - - if (currKvp.Value != null) - { - if (currKvp.Value is DateTime - || currKvp.Value is DateTime?) - { - vals += "'" + ((DateTime)currKvp.Value).ToString(TimestampFormat) + "'"; - } - else if (currKvp.Value is DateTimeOffset - || currKvp.Value is DateTimeOffset?) - { - vals += "'" + ((DateTimeOffset)currKvp.Value).ToString(TimestampOffsetFormat) + "'"; - } - else if (currKvp.Value is int - || currKvp.Value is long - || currKvp.Value is decimal) - { - vals += currKvp.Value.ToString(); - } - else if (currKvp.Value is bool) - { - vals += ((bool)currKvp.Value ? "1" : "0"); - } - else if (currKvp.Value is byte[]) - { - vals += "decode('" + Helper.ByteArrayToHexString((byte[])currKvp.Value) + "', 'hex')"; - } - else - { - if (Helper.IsExtendedCharacters(currKvp.Value.ToString())) - { - vals += PreparedUnicodeValue(currKvp.Value.ToString()); - } - else - { - vals += PreparedStringValue(currKvp.Value.ToString()); - } - } - - } - else - { - vals += "null"; - } - - valsAdded++; - } - - values.Add(vals); - } - - return values; - } - - private string BuildKeyValueClauseFromDictionary(Dictionary keyValuePairs) - { - string keyValueClause = ""; - int added = 0; - - foreach (KeyValuePair currKvp in keyValuePairs) - { - if (String.IsNullOrEmpty(currKvp.Key)) continue; - - if (added > 0) keyValueClause += ","; - - if (currKvp.Value != null) - { - if (currKvp.Value is DateTime - || currKvp.Value is DateTime?) - { - keyValueClause += PreparedFieldName(currKvp.Key) + "='" + ((DateTime)currKvp.Value).ToString(TimestampFormat) + "'"; - } - else if (currKvp.Value is DateTimeOffset - || currKvp.Value is DateTimeOffset?) - { - keyValueClause += PreparedFieldName(currKvp.Key) + "='" + ((DateTimeOffset)currKvp.Value).ToString(TimestampOffsetFormat) + "'"; - } - else if (currKvp.Value is int - || currKvp.Value is long - || currKvp.Value is decimal) - { - keyValueClause += PreparedFieldName(currKvp.Key) + "=" + currKvp.Value.ToString(); - } - else if (currKvp.Value is bool) - { - keyValueClause += PreparedFieldName(currKvp.Key) + "=" + ((bool)currKvp.Value ? "1" : "0"); - } - else if (currKvp.Value is byte[]) - { - keyValueClause += PreparedFieldName(currKvp.Key) + "=" + "decode('" + Helper.ByteArrayToHexString((byte[])currKvp.Value) + "', 'hex')"; - } - else - { - if (Helper.IsExtendedCharacters(currKvp.Value.ToString())) - { - keyValueClause += PreparedFieldName(currKvp.Key) + "=" + PreparedUnicodeValue(currKvp.Value.ToString()); - } - else - { - keyValueClause += PreparedFieldName(currKvp.Key) + "=" + PreparedStringValue(currKvp.Value.ToString()); - } - } - } - else - { - keyValueClause += PreparedFieldName(currKvp.Key) + "= null"; - } - - added++; - } - - return keyValueClause; - } - #endregion } } diff --git a/src/DatabaseWrapper.SqlServer/DatabaseClient.cs b/src/DatabaseWrapper.SqlServer/DatabaseClient.cs index 51e20c7..c81e22f 100644 --- a/src/DatabaseWrapper.SqlServer/DatabaseClient.cs +++ b/src/DatabaseWrapper.SqlServer/DatabaseClient.cs @@ -3,6 +3,7 @@ using System.Collections.Concurrent; using System.Data; using System.Data.SqlClient; +using static System.FormattableString; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -747,10 +748,11 @@ public override async Task TruncateAsync(string tableName, CancellationToken tok /// /// Execute a query. /// - /// Database query defined outside of the database client. + /// Tuple of a database query defined outside of the database client and of query parameters /// A DataTable containing the results. - public override DataTable Query(string query) + public override DataTable Query((string Query, IEnumerable> Parameters) queryAndParameters) { + (var query, var parameters) = queryAndParameters; if (String.IsNullOrEmpty(query)) throw new ArgumentNullException(query); if (query.Length > MaxStatementLength) throw new ArgumentException("Query exceeds maximum statement length of " + MaxStatementLength + " characters."); @@ -768,11 +770,15 @@ public override DataTable Query(string query) conn.Open(); #pragma warning disable CA2100 // Review SQL queries for security vulnerabilities - using (SqlDataAdapter sda = new SqlDataAdapter(query, conn)) + using (var cmd = new SqlCommand(query, conn)) { -#pragma warning restore CA2100 // Review SQL queries for security vulnerabilities + DatabaseHelperBase.AddParameters(cmd, (name,type) => new SqlParameter(name,type), parameters); + using (SqlDataAdapter sda = new SqlDataAdapter(cmd)) + { - sda.Fill(result); +#pragma warning restore CA2100 // Review SQL queries for security vulnerabilities + sda.Fill(result); + } } conn.Dispose(); @@ -809,11 +815,12 @@ public override DataTable Query(string query) /// /// Execute a query. /// - /// Database query defined outside of the database client. + /// Tuple of a database query defined outside of the database client and of query parameters /// Cancellation token. /// A DataTable containing the results. - public override async Task QueryAsync(string query, CancellationToken token = default) + public override async Task QueryAsync((string Query, IEnumerable> Parameters) queryAndParameters, CancellationToken token = default) { + (var query, var parameters) = queryAndParameters; if (String.IsNullOrEmpty(query)) throw new ArgumentNullException(query); if (query.Length > MaxStatementLength) throw new ArgumentException("Query exceeds maximum statement length of " + MaxStatementLength + " characters."); @@ -831,11 +838,15 @@ public override async Task QueryAsync(string query, CancellationToken await conn.OpenAsync(token).ConfigureAwait(false); #pragma warning disable CA2100 // Review SQL queries for security vulnerabilities - using (SqlDataAdapter sda = new SqlDataAdapter(query, conn)) + using (var cmd = new SqlCommand(query, conn)) { + DatabaseHelperBase.AddParameters(cmd, (name,type) => new SqlParameter(name,type), parameters); + using (SqlDataAdapter sda = new SqlDataAdapter(cmd)) + { #pragma warning restore CA2100 // Review SQL queries for security vulnerabilities4 - sda.Fill(result); + sda.Fill(result); + } } #if NET6_0_OR_GREATER diff --git a/src/DatabaseWrapper.SqlServer/DatabaseWrapper.SqlServer.xml b/src/DatabaseWrapper.SqlServer/DatabaseWrapper.SqlServer.xml index 570a3a5..951f2bd 100644 --- a/src/DatabaseWrapper.SqlServer/DatabaseWrapper.SqlServer.xml +++ b/src/DatabaseWrapper.SqlServer/DatabaseWrapper.SqlServer.xml @@ -309,18 +309,18 @@ The table you wish to TRUNCATE. Cancellation token. - + Execute a query. - Database query defined outside of the database client. + Tuple of a database query defined outside of the database client and of query parameters A DataTable containing the results. - + Execute a query. - Database query defined outside of the database client. + Tuple of a database query defined outside of the database client and of query parameters Cancellation token. A DataTable containing the results. @@ -577,5 +577,12 @@ String. String. + + + Prepare a field name for use in a SQL query. + + Name of the field to be prepared. + Field name for use in a SQL query. + diff --git a/src/DatabaseWrapper.SqlServer/SqlServerHelper.cs b/src/DatabaseWrapper.SqlServer/SqlServerHelper.cs index e29a4bb..7b09b89 100644 --- a/src/DatabaseWrapper.SqlServer/SqlServerHelper.cs +++ b/src/DatabaseWrapper.SqlServer/SqlServerHelper.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.Contracts; +using static System.FormattableString; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -8,6 +10,8 @@ namespace DatabaseWrapper.SqlServer { + using QueryAndParameters = System.ValueTuple>>; + /// /// SQL Server implementation of helper properties and methods. /// @@ -335,7 +339,7 @@ public override string DropTableQuery(string tableName) /// Expression filter. /// Result order. /// String. - public override string SelectQuery(string tableName, int? indexStart, int? maxResults, List returnFields, Expr filter, ResultOrder[] resultOrder) + public override QueryAndParameters SelectQuery(string tableName, int? indexStart, int? maxResults, List returnFields, Expr filter, ResultOrder[] resultOrder) { string query = ""; string whereClause = ""; @@ -374,7 +378,10 @@ public override string SelectQuery(string tableName, int? indexStart, int? maxRe query += "FROM " + PreparedTableName(tableName) + " "; // expressions - if (filter != null) whereClause = ExpressionToWhereClause(filter); + var parameters_list = new List>(); + if (filter != null) { + whereClause = ExpressionToWhereClause(filter, parameters_list); + } if (!String.IsNullOrEmpty(whereClause)) { query += "WHERE " + whereClause + " "; @@ -394,7 +401,7 @@ public override string SelectQuery(string tableName, int? indexStart, int? maxRe query += "OFFSET " + indexStart + " ROWS "; } - return query; + return (query, parameters_list); } /// @@ -403,22 +410,19 @@ public override string SelectQuery(string tableName, int? indexStart, int? maxRe /// The table in which you wish to INSERT. /// The key-value pairs for the row you wish to INSERT. /// String. - public override string InsertQuery(string tableName, Dictionary keyValuePairs) + public override QueryAndParameters InsertQuery(string tableName, Dictionary keyValuePairs) { - string ret = - "INSERT INTO " + PreparedTableName(tableName) + " WITH (ROWLOCK) " + - "("; - - string keys = ""; - string vals = ""; - BuildKeysValuesFromDictionary(keyValuePairs, out keys, out vals); - - ret += keys + ") " + + var s_ret = new StringBuilder(); + var o_ret = keyValuePairs.Select(kv => new KeyValuePair("@F_" + kv.Key, kv.Value)); + s_ret.Append("INSERT INTO " + PreparedTableName(tableName) + " WITH (ROWLOCK) " + "("); + s_ret.Append(string.Join(", ", keyValuePairs.Keys.Select(k => PreparedFieldName(k)))); + s_ret.Append(") " + "OUTPUT INSERTED.* " + "VALUES " + - "(" + vals + ") "; - - return ret; + "("); + s_ret.Append(string.Join(", ", o_ret.Select(k => k.Key))); + s_ret.Append(") "); + return (s_ret.ToString(), o_ret); } /// @@ -427,36 +431,41 @@ public override string InsertQuery(string tableName, Dictionary /// The table in which you wish to INSERT. /// List of dictionaries containing key-value pairs for the rows you wish to INSERT. /// String. - public override string InsertMultipleQuery(string tableName, List> keyValuePairList) + public override QueryAndParameters InsertMultipleQuery(string tableName, List> keyValuePairList) { ValidateInputDictionaries(keyValuePairList); - string keys = BuildKeysFromDictionary(keyValuePairList[0]); - List values = BuildValuesFromDictionaries(keyValuePairList); string txn = "txn_" + RandomCharacters(12); - string ret = + var ret = new StringBuilder(); + var ret_values = new List>(); + ret.Append( "BEGIN TRANSACTION [" + txn + "] " + " BEGIN TRY " + " INSERT INTO " + PreparedTableName(tableName) + " WITH (ROWLOCK) " + - " (" + keys + ") " + - " VALUES "; + " (" + string.Join(", ", keyValuePairList[0].Keys.Select(k => PreparedFieldName(k))) + ") " + + " VALUES "); - int added = 0; - foreach (string value in values) + for (int i_dict=0; i_dict 0) ret += ","; - ret += " (" + value + ")"; - added++; + var dict = keyValuePairList[i_dict]; + var prefix = Invariant($"@F{i_dict}_"); + var this_round = dict.Select(kv => new KeyValuePair(prefix + kv.Key, kv.Value)); + if (i_dict>0) { + ret.Append(", "); + } + ret.Append("("); + ret.Append(string.Join(", ", this_round.Select(kv => kv.Key))); + ret.Append(")"); + ret_values.AddRange(this_round); } - ret += + ret.Append( " COMMIT TRANSACTION [" + txn + "] " + " END TRY " + " BEGIN CATCH " + " ROLLBACK TRANSACTION [" + txn + "] " + - " END CATCH "; - - return ret; + " END CATCH "); + return (ret.ToString(), ret_values); } /// @@ -466,18 +475,17 @@ public override string InsertMultipleQuery(string tableName, ListThe key-value pairs for the data you wish to UPDATE. /// The expression containing the UPDATE filter (i.e. WHERE clause data). /// String. - public override string UpdateQuery(string tableName, Dictionary keyValuePairs, Expr filter) + public override QueryAndParameters UpdateQuery(string tableName, Dictionary keyValuePairs, Expr filter) { - string keyValueClause = BuildKeyValueClauseFromDictionary(keyValuePairs); - + const string FIELD_PREFIX = "@F"; + var parameters = keyValuePairs.Select(kv => new KeyValuePair(FIELD_PREFIX + kv.Key, kv.Value)).ToList(); string ret = "UPDATE " + PreparedTableName(tableName) + " WITH (ROWLOCK) SET " + - keyValueClause + " " + + string.Join(", ", parameters.Select(kv => kv.Key.Substring(FIELD_PREFIX.Length) + "=" + kv.Key)) + " " + "OUTPUT INSERTED.* "; - if (filter != null) ret += "WHERE " + ExpressionToWhereClause(filter) + " "; - - return ret; + if (filter != null) ret += "WHERE " + ExpressionToWhereClause(filter, parameters) + " "; + return (ret, parameters); } /// @@ -486,14 +494,15 @@ public override string UpdateQuery(string tableName, Dictionary /// Table name. /// Expression filter. /// String. - public override string DeleteQuery(string tableName, Expr filter) + public override QueryAndParameters DeleteQuery(string tableName, Expr filter) { string ret = "DELETE FROM " + PreparedTableName(tableName) + " WITH (ROWLOCK) "; - if (filter != null) ret += "WHERE " + ExpressionToWhereClause(filter) + " "; + var parameters = new List>(); + if (filter != null) ret += "WHERE " + ExpressionToWhereClause(filter, parameters) + " "; - return ret; + return (ret, parameters); } /// @@ -512,7 +521,7 @@ public override string TruncateQuery(string tableName) /// Table name. /// Expression filter. /// String. - public override string ExistsQuery(string tableName, Expr filter) + public override QueryAndParameters ExistsQuery(string tableName, Expr filter) { string query = ""; string whereClause = ""; @@ -523,13 +532,14 @@ public override string ExistsQuery(string tableName, Expr filter) "FROM " + PreparedTableName(tableName) + " "; // expressions - if (filter != null) whereClause = ExpressionToWhereClause(filter); + var parameters = new List>(); + if (filter != null) whereClause = ExpressionToWhereClause(filter, parameters); if (!String.IsNullOrEmpty(whereClause)) { query += "WHERE " + whereClause + " "; } - return query; + return (query, parameters); } /// @@ -539,7 +549,7 @@ public override string ExistsQuery(string tableName, Expr filter) /// Column name to use to temporarily store the result. /// Expression filter. /// String. - public override string CountQuery(string tableName, string countColumnName, Expr filter) + public override QueryAndParameters CountQuery(string tableName, string countColumnName, Expr filter) { string query = ""; string whereClause = ""; @@ -550,13 +560,14 @@ public override string CountQuery(string tableName, string countColumnName, Expr "FROM " + PreparedTableName(tableName) + " "; // expressions - if (filter != null) whereClause = ExpressionToWhereClause(filter); + var parameters = new List>(); + if (filter != null) whereClause = ExpressionToWhereClause(filter, parameters); if (!String.IsNullOrEmpty(whereClause)) { query += "WHERE " + whereClause + " "; } - return query; + return (query, parameters); } /// @@ -567,7 +578,7 @@ public override string CountQuery(string tableName, string countColumnName, Expr /// Column name to temporarily store the result. /// Expression filter. /// String. - public override string SumQuery(string tableName, string fieldName, string sumColumnName, Expr filter) + public override QueryAndParameters SumQuery(string tableName, string fieldName, string sumColumnName, Expr filter) { string query = ""; string whereClause = ""; @@ -578,13 +589,14 @@ public override string SumQuery(string tableName, string fieldName, string sumCo "FROM " + PreparedTableName(tableName) + " "; // expressions - if (filter != null) whereClause = ExpressionToWhereClause(filter); + var parameters = new List>(); + if (filter != null) whereClause = ExpressionToWhereClause(filter, parameters); if (!String.IsNullOrEmpty(whereClause)) { query += "WHERE " + whereClause + " "; } - return query; + return (query, parameters); } /// @@ -594,7 +606,7 @@ public override string SumQuery(string tableName, string fieldName, string sumCo /// String. public override string GenerateTimestamp(DateTime ts) { - return ts.ToString(TimestampFormat); + return ts.ToLocalTime().ToString(TimestampFormat); } /// @@ -632,489 +644,16 @@ public string ExtractTableName(string s) #region Private-Members - private string PreparedUnicodeValue(string s) - { - return "N" + PreparedStringValue(s); - } - - private string PreparedFieldName(string fieldName) + /// + /// Prepare a field name for use in a SQL query. + /// + /// Name of the field to be prepared. + /// Field name for use in a SQL query. + public override string PreparedFieldName(string fieldName) { return "[" + fieldName + "]"; } - private string PreparedStringValue(string str) - { - return "'" + SanitizeString(str) + "'"; - } - - private string ExpressionToWhereClause(Expr expr) - { - if (expr == null) return null; - - string clause = ""; - - if (expr.Left == null) return null; - - clause += "("; - - if (expr.Left is Expr) - { - clause += ExpressionToWhereClause((Expr)expr.Left) + " "; - } - else - { - if (!(expr.Left is string)) - { - throw new ArgumentException("Left term must be of type Expression or String"); - } - - if (expr.Operator != OperatorEnum.Contains - && expr.Operator != OperatorEnum.ContainsNot - && expr.Operator != OperatorEnum.StartsWith - && expr.Operator != OperatorEnum.StartsWithNot - && expr.Operator != OperatorEnum.EndsWith - && expr.Operator != OperatorEnum.EndsWithNot) - { - // - // These operators will add the left term - // - clause += PreparedFieldName(expr.Left.ToString()) + " "; - } - } - - switch (expr.Operator) - { - #region Process-By-Operators - - case OperatorEnum.And: - #region And - - if (expr.Right == null) return null; - clause += "AND "; - - if (expr.Right is Expr) - { - clause += ExpressionToWhereClause((Expr)expr.Right); - } - else - { - if (expr.Right is DateTime || expr.Right is DateTime?) - { - clause += "'" + GenerateTimestamp(Convert.ToDateTime(expr.Right)) + "'"; - } - else if (expr.Right is int || expr.Right is long || expr.Right is decimal) - { - clause += expr.Right.ToString(); - } - else - { - clause += PreparedStringValue(expr.Right.ToString()); - } - } - break; - - #endregion - - case OperatorEnum.Or: - #region Or - - if (expr.Right == null) return null; - clause += "OR "; - if (expr.Right is Expr) - { - clause += ExpressionToWhereClause((Expr)expr.Right); - } - else - { - if (expr.Right is DateTime || expr.Right is DateTime?) - { - clause += "'" + GenerateTimestamp(Convert.ToDateTime(expr.Right)) + "'"; - } - else if (expr.Right is int || expr.Right is long || expr.Right is decimal) - { - clause += expr.Right.ToString(); - } - else - { - clause += PreparedStringValue(expr.Right.ToString()); - } - } - break; - - #endregion - - case OperatorEnum.Equals: - #region Equals - - if (expr.Right == null) return null; - clause += "= "; - if (expr.Right is Expr) - { - clause += ExpressionToWhereClause((Expr)expr.Right); - } - else - { - if (expr.Right is DateTime || expr.Right is DateTime?) - { - clause += "'" + GenerateTimestamp(Convert.ToDateTime(expr.Right)) + "'"; - } - else if (expr.Right is int || expr.Right is long || expr.Right is decimal) - { - clause += expr.Right.ToString(); - } - else - { - clause += PreparedStringValue(expr.Right.ToString()); - } - } - break; - - #endregion - - case OperatorEnum.NotEquals: - #region NotEquals - - if (expr.Right == null) return null; - clause += "<> "; - if (expr.Right is Expr) - { - clause += ExpressionToWhereClause((Expr)expr.Right); - } - else - { - if (expr.Right is DateTime || expr.Right is DateTime?) - { - clause += "'" + GenerateTimestamp(Convert.ToDateTime(expr.Right)) + "'"; - } - else if (expr.Right is int || expr.Right is long || expr.Right is decimal) - { - clause += expr.Right.ToString(); - } - else - { - clause += PreparedStringValue(expr.Right.ToString()); - } - } - break; - - #endregion - - case OperatorEnum.In: - #region In - - if (expr.Right == null) return null; - int inAdded = 0; - if (!Helper.IsList(expr.Right)) return null; - List inTempList = Helper.ObjectToList(expr.Right); - clause += " IN ("; - foreach (object currObj in inTempList) - { - if (currObj == null) continue; - if (inAdded > 0) clause += ","; - if (currObj is DateTime || currObj is DateTime?) - { - clause += "'" + GenerateTimestamp(Convert.ToDateTime(currObj)) + "'"; - } - else if (currObj is int || currObj is long || currObj is decimal) - { - clause += currObj.ToString(); - } - else - { - clause += PreparedStringValue(currObj.ToString()); - } - inAdded++; - } - clause += ")"; - break; - - #endregion - - case OperatorEnum.NotIn: - #region NotIn - - if (expr.Right == null) return null; - int notInAdded = 0; - if (!Helper.IsList(expr.Right)) return null; - List notInTempList = Helper.ObjectToList(expr.Right); - clause += " NOT IN ("; - foreach (object currObj in notInTempList) - { - if (currObj == null) continue; - if (notInAdded > 0) clause += ","; - if (currObj is DateTime || currObj is DateTime?) - { - clause += "'" + GenerateTimestamp(Convert.ToDateTime(currObj)) + "'"; - } - else if (currObj is int || currObj is long || currObj is decimal) - { - clause += currObj.ToString(); - } - else - { - clause += PreparedStringValue(currObj.ToString()); - } - notInAdded++; - } - clause += ")"; - break; - - #endregion - - case OperatorEnum.Contains: - #region Contains - - if (expr.Right == null) return null; - if (expr.Right is string) - { - clause += - "(" + - PreparedFieldName(expr.Left.ToString()) + " LIKE " + PreparedStringValue("%" + expr.Right.ToString()) + - "OR " + PreparedFieldName(expr.Left.ToString()) + " LIKE " + PreparedStringValue("%" + expr.Right.ToString() + "%") + - "OR " + PreparedFieldName(expr.Left.ToString()) + " LIKE " + PreparedStringValue(expr.Right.ToString() + "%") + - ")"; - } - else - { - return null; - } - break; - - #endregion - - case OperatorEnum.ContainsNot: - #region ContainsNot - - if (expr.Right == null) return null; - if (expr.Right is string) - { - clause += - "(" + - PreparedFieldName(expr.Left.ToString()) + " NOT LIKE " + PreparedStringValue("%" + expr.Right.ToString()) + - "OR " + PreparedFieldName(expr.Left.ToString()) + " NOT LIKE " + PreparedStringValue("%" + expr.Right.ToString() + "%") + - "OR " + PreparedFieldName(expr.Left.ToString()) + " NOT LIKE " + PreparedStringValue(expr.Right.ToString() + "%") + - ")"; - } - else - { - return null; - } - break; - - #endregion - - case OperatorEnum.StartsWith: - #region StartsWith - - if (expr.Right == null) return null; - if (expr.Right is string) - { - clause += - "(" + - PreparedFieldName(expr.Left.ToString()) + " LIKE " + (PreparedStringValue(expr.Right.ToString() + "%")) + - ")"; - } - else - { - return null; - } - break; - - #endregion - - case OperatorEnum.StartsWithNot: - #region StartsWithNot - - if (expr.Right == null) return null; - if (expr.Right is string) - { - clause += - "(" + - PreparedFieldName(expr.Left.ToString()) + " NOT LIKE " + (PreparedStringValue(expr.Right.ToString() + "%")) + - ")"; - } - else - { - return null; - } - break; - - #endregion - - case OperatorEnum.EndsWith: - #region EndsWith - - if (expr.Right == null) return null; - if (expr.Right is string) - { - clause += - "(" + - PreparedFieldName(expr.Left.ToString()) + " LIKE " + PreparedStringValue("%" + expr.Right.ToString()) + - ")"; - } - else - { - return null; - } - break; - - #endregion - - case OperatorEnum.EndsWithNot: - #region EndsWithNot - - if (expr.Right == null) return null; - if (expr.Right is string) - { - clause += - "(" + - PreparedFieldName(expr.Left.ToString()) + " NOT LIKE " + PreparedStringValue("%" + expr.Right.ToString()) + - ")"; - } - else - { - return null; - } - break; - - #endregion - - case OperatorEnum.GreaterThan: - #region GreaterThan - - if (expr.Right == null) return null; - clause += "> "; - if (expr.Right is Expr) - { - clause += ExpressionToWhereClause((Expr)expr.Right); - } - else - { - if (expr.Right is DateTime || expr.Right is DateTime?) - { - clause += "'" + GenerateTimestamp(Convert.ToDateTime(expr.Right)) + "'"; - } - else if (expr.Right is int || expr.Right is long || expr.Right is decimal) - { - clause += expr.Right.ToString(); - } - else - { - clause += PreparedStringValue(expr.Right.ToString()); - } - } - break; - - #endregion - - case OperatorEnum.GreaterThanOrEqualTo: - #region GreaterThanOrEqualTo - - if (expr.Right == null) return null; - clause += ">= "; - if (expr.Right is Expr) - { - clause += ExpressionToWhereClause((Expr)expr.Right); - } - else - { - if (expr.Right is DateTime || expr.Right is DateTime?) - { - clause += "'" + GenerateTimestamp(Convert.ToDateTime(expr.Right)) + "'"; - } - else if (expr.Right is int || expr.Right is long || expr.Right is decimal) - { - clause += expr.Right.ToString(); - } - else - { - clause += PreparedStringValue(expr.Right.ToString()); - } - } - break; - - #endregion - - case OperatorEnum.LessThan: - #region LessThan - - if (expr.Right == null) return null; - clause += "< "; - if (expr.Right is Expr) - { - clause += ExpressionToWhereClause((Expr)expr.Right); - } - else - { - if (expr.Right is DateTime || expr.Right is DateTime?) - { - clause += "'" + GenerateTimestamp(Convert.ToDateTime(expr.Right)) + "'"; - } - else if (expr.Right is int || expr.Right is long || expr.Right is decimal) - { - clause += expr.Right.ToString(); - } - else - { - clause += PreparedStringValue(expr.Right.ToString()); - } - } - break; - - #endregion - - case OperatorEnum.LessThanOrEqualTo: - #region LessThanOrEqualTo - - if (expr.Right == null) return null; - clause += "<= "; - if (expr.Right is Expr) - { - clause += ExpressionToWhereClause((Expr)expr.Right); - } - else - { - if (expr.Right is DateTime || expr.Right is DateTime?) - { - clause += "'" + GenerateTimestamp(Convert.ToDateTime(expr.Right)) + "'"; - } - else if (expr.Right is int || expr.Right is long || expr.Right is decimal) - { - clause += expr.Right.ToString(); - } - else - { - clause += PreparedStringValue(expr.Right.ToString()); - } - } - break; - - #endregion - - case OperatorEnum.IsNull: - #region IsNull - - clause += " IS NULL"; - break; - - #endregion - - case OperatorEnum.IsNotNull: - #region IsNotNull - - clause += " IS NOT NULL"; - break; - - #endregion - - #endregion - } - - clause += ")"; - - return clause; - } - private string BuildOrderByClause(ResultOrder[] resultOrder) { if (resultOrder == null || resultOrder.Length < 0) return null; @@ -1191,71 +730,6 @@ private string PreparedTableNameUnenclosed(string s) } } - private void BuildKeysValuesFromDictionary(Dictionary keyValuePairs, out string keys, out string vals) - { - keys = ""; - vals = ""; - int added = 0; - - foreach (KeyValuePair currKvp in keyValuePairs) - { - if (String.IsNullOrEmpty(currKvp.Key)) continue; - - if (added > 0) - { - keys += ","; - vals += ","; - } - - keys += PreparedFieldName(currKvp.Key); - - if (currKvp.Value != null) - { - if (currKvp.Value is DateTime - || currKvp.Value is DateTime?) - { - vals += "'" + ((DateTime)currKvp.Value).ToString(TimestampFormat) + "'"; - } - else if (currKvp.Value is DateTimeOffset - || currKvp.Value is DateTimeOffset?) - { - vals += "'" + ((DateTimeOffset)currKvp.Value).ToString(TimestampOffsetFormat) + "'"; - } - else if (currKvp.Value is int - || currKvp.Value is long - || currKvp.Value is decimal) - { - vals += currKvp.Value.ToString(); - } - else if (currKvp.Value is bool) - { - vals += ((bool)currKvp.Value ? "1" : "0"); - } - else if (currKvp.Value is byte[]) - { - vals += "0x" + BitConverter.ToString((byte[])currKvp.Value).Replace("-", ""); - } - else - { - if (Helper.IsExtendedCharacters(currKvp.Value.ToString())) - { - vals += PreparedUnicodeValue(currKvp.Value.ToString()); - } - else - { - vals += PreparedStringValue(currKvp.Value.ToString()); - } - } - } - else - { - vals += "null"; - } - - added++; - } - } - private void ValidateInputDictionaries(List> dicts) { Dictionary reference = dicts[0]; @@ -1285,132 +759,6 @@ private string BuildKeysFromDictionary(Dictionary reference) return keys; } - - private List BuildValuesFromDictionaries(List> dicts) - { - List values = new List(); - - foreach (Dictionary currDict in dicts) - { - string vals = ""; - int valsAdded = 0; - - foreach (KeyValuePair currKvp in currDict) - { - if (valsAdded > 0) vals += ","; - - if (currKvp.Value != null) - { - if (currKvp.Value is DateTime - || currKvp.Value is DateTime?) - { - vals += "'" + ((DateTime)currKvp.Value).ToString(TimestampFormat) + "'"; - } - else if (currKvp.Value is DateTimeOffset - || currKvp.Value is DateTimeOffset?) - { - vals += "'" + ((DateTimeOffset)currKvp.Value).ToString(TimestampOffsetFormat) + "'"; - } - else if (currKvp.Value is int - || currKvp.Value is long - || currKvp.Value is decimal) - { - vals += currKvp.Value.ToString(); - } - else if (currKvp.Value is bool) - { - vals += ((bool)currKvp.Value ? "1" : "0"); - } - else if (currKvp.Value is byte[]) - { - vals += "0x" + BitConverter.ToString((byte[])currKvp.Value).Replace("-", ""); - } - else - { - if (Helper.IsExtendedCharacters(currKvp.Value.ToString())) - { - vals += PreparedUnicodeValue(currKvp.Value.ToString()); - } - else - { - vals += PreparedStringValue(currKvp.Value.ToString()); - } - } - } - else - { - vals += "null"; - } - - valsAdded++; - } - - values.Add(vals); - } - - return values; - } - - private string BuildKeyValueClauseFromDictionary(Dictionary keyValuePairs) - { - string keyValueClause = ""; - int added = 0; - - foreach (KeyValuePair currKvp in keyValuePairs) - { - if (String.IsNullOrEmpty(currKvp.Key)) continue; - - if (added > 0) keyValueClause += ","; - - if (currKvp.Value != null) - { - if (currKvp.Value is DateTime - || currKvp.Value is DateTime?) - { - keyValueClause += PreparedFieldName(currKvp.Key) + "='" + ((DateTime)currKvp.Value).ToString(TimestampFormat) + "'"; - } - else if (currKvp.Value is DateTimeOffset - || currKvp.Value is DateTimeOffset?) - { - keyValueClause += PreparedFieldName(currKvp.Key) + "='" + ((DateTimeOffset)currKvp.Value).ToString(TimestampOffsetFormat) + "'"; - } - else if (currKvp.Value is int - || currKvp.Value is long - || currKvp.Value is decimal) - { - keyValueClause += PreparedFieldName(currKvp.Key) + "=" + currKvp.Value.ToString(); - } - else if (currKvp.Value is bool) - { - keyValueClause += PreparedFieldName(currKvp.Key) + "=" + ((bool)currKvp.Value ? "1" : "0"); - } - else if (currKvp.Value is byte[]) - { - keyValueClause += PreparedFieldName(currKvp.Key) + "=" + "0x" + BitConverter.ToString((byte[])currKvp.Value).Replace("-", ""); - } - else - { - if (Helper.IsExtendedCharacters(currKvp.Value.ToString())) - { - keyValueClause += PreparedFieldName(currKvp.Key) + "=" + PreparedUnicodeValue(currKvp.Value.ToString()); - } - else - { - keyValueClause += PreparedFieldName(currKvp.Key) + "=" + PreparedStringValue(currKvp.Value.ToString()); - } - } - } - else - { - keyValueClause += PreparedFieldName(currKvp.Key) + "= null"; - } - - added++; - } - - return keyValueClause; - } - #endregion } } diff --git a/src/DatabaseWrapper.Sqlite/DatabaseClient.cs b/src/DatabaseWrapper.Sqlite/DatabaseClient.cs index 0aa09b2..47a1211 100644 --- a/src/DatabaseWrapper.Sqlite/DatabaseClient.cs +++ b/src/DatabaseWrapper.Sqlite/DatabaseClient.cs @@ -1,7 +1,8 @@ using System; using System.Collections.Generic; using System.Collections.Concurrent; -using System.Data; +using System.Data; +using static System.FormattableString; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -68,6 +69,7 @@ private set } } + /// /// Maximum supported statement length. /// @@ -794,10 +796,11 @@ public override async Task TruncateAsync(string tableName, CancellationToken tok /// /// Execute a query. /// - /// Database query defined outside of the database client. + /// Database query defined outside of the database client, and, the corresponding parameters. /// A DataTable containing the results. - public override DataTable Query(string query) + public override DataTable Query((string Query, IEnumerable> Parameters) queryAndParameters) { + (var query, var parameters) = queryAndParameters; if (String.IsNullOrEmpty(query)) throw new ArgumentNullException(query); if (query.Length > MaxStatementLength) throw new ArgumentException("Query exceeds maximum statement length of " + MaxStatementLength + " characters."); @@ -818,6 +821,7 @@ public override DataTable Query(string query) using (SqliteCommand cmd = new SqliteCommand(query, conn)) #pragma warning restore CA2100 // Review SQL queries for security vulnerabilities { + DatabaseHelperBase.AddParameters(cmd, (name,type) => new SqliteParameter(name, type), parameters); using (SqliteDataReader rdr = cmd.ExecuteReader()) { result.Load(rdr); @@ -857,11 +861,12 @@ public override DataTable Query(string query) /// /// Execute a query. /// - /// Database query defined outside of the database client. + /// Tuple of a database query defined outside of the database client and of query parameters /// Cancellation token. /// A DataTable containing the results. - public override async Task QueryAsync(string query, CancellationToken token = default) + public override async Task QueryAsync((string Query, IEnumerable> Parameters) queryAndParameters, CancellationToken token = default) { + (var query, var parameters) = queryAndParameters; if (String.IsNullOrEmpty(query)) throw new ArgumentNullException(query); if (query.Length > MaxStatementLength) throw new ArgumentException("Query exceeds maximum statement length of " + MaxStatementLength + " characters."); @@ -882,6 +887,7 @@ public override async Task QueryAsync(string query, CancellationToken using (SqliteCommand cmd = new SqliteCommand(query, conn)) #pragma warning restore CA2100 // Review SQL queries for security vulnerabilities { + DatabaseHelperBase.AddParameters(cmd, (name,type) => new SqliteParameter(name, type), parameters); using (SqliteDataReader rdr = await cmd.ExecuteReaderAsync(token).ConfigureAwait(false)) { result.Load(rdr); @@ -1071,7 +1077,6 @@ public override string SanitizeString(string s) #endregion #region Private-Methods - /// /// Dispose of the object. /// diff --git a/src/DatabaseWrapper.Sqlite/DatabaseWrapper.Sqlite.xml b/src/DatabaseWrapper.Sqlite/DatabaseWrapper.Sqlite.xml index abf3e21..4fdf7c1 100644 --- a/src/DatabaseWrapper.Sqlite/DatabaseWrapper.Sqlite.xml +++ b/src/DatabaseWrapper.Sqlite/DatabaseWrapper.Sqlite.xml @@ -300,18 +300,18 @@ The table you wish to TRUNCATE. Cancellation token. - + Execute a query. - Database query defined outside of the database client. + Database query defined outside of the database client, and, the corresponding parameters. A DataTable containing the results. - + Execute a query. - Database query defined outside of the database client. + Tuple of a database query defined outside of the database client and of query parameters Cancellation token. A DataTable containing the results. @@ -439,6 +439,13 @@ String. String. + + + Prepare a field name for use in a SQL query by surrounding it with backticks. + + Name of the field to be prepared. + Field name for use in a SQL query. + Method to convert a Column object to the values used in a table create statement. diff --git a/src/DatabaseWrapper.Sqlite/SqliteHelper.cs b/src/DatabaseWrapper.Sqlite/SqliteHelper.cs index 4c55107..70922c1 100644 --- a/src/DatabaseWrapper.Sqlite/SqliteHelper.cs +++ b/src/DatabaseWrapper.Sqlite/SqliteHelper.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using static System.FormattableString; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -8,6 +9,8 @@ namespace DatabaseWrapper.Sqlite { + using QueryAndParameters = System.ValueTuple>>; + /// /// Sqlite implementation of helper properties and methods. /// @@ -176,6 +179,16 @@ public override string SanitizeString(string val) return ret; } + /// + /// Prepare a field name for use in a SQL query by surrounding it with backticks. + /// + /// Name of the field to be prepared. + /// Field name for use in a SQL query. + public override string PreparedFieldName(string fieldName) + { + return "`" + fieldName + "`"; + } + /// /// Method to convert a Column object to the values used in a table create statement. /// @@ -300,7 +313,7 @@ public override string DropTableQuery(string tableName) /// Expression filter. /// Result order. /// String. - public override string SelectQuery(string tableName, int? indexStart, int? maxResults, List returnFields, Expr filter, ResultOrder[] resultOrder) + public override QueryAndParameters SelectQuery(string tableName, int? indexStart, int? maxResults, List returnFields, Expr filter, ResultOrder[] resultOrder) { string query = ""; string whereClause = ""; @@ -341,7 +354,8 @@ public override string SelectQuery(string tableName, int? indexStart, int? maxRe // // expressions // - if (filter != null) whereClause = ExpressionToWhereClause(filter); + var parameters_list = new List>(); + if (filter != null) whereClause = ExpressionToWhereClause(filter, parameters_list); if (!String.IsNullOrEmpty(whereClause)) { query += "WHERE " + whereClause + " "; @@ -365,7 +379,7 @@ public override string SelectQuery(string tableName, int? indexStart, int? maxRe query += "LIMIT " + maxResults + " "; } - return query; + return (query, parameters_list); } /// @@ -374,24 +388,21 @@ public override string SelectQuery(string tableName, int? indexStart, int? maxRe /// The table in which you wish to INSERT. /// The key-value pairs for the row you wish to INSERT. /// String. - public override string InsertQuery(string tableName, Dictionary keyValuePairs) + public override QueryAndParameters InsertQuery(string tableName, Dictionary keyValuePairs) { + var o_ret = keyValuePairs.Select(kv => new KeyValuePair("@F_" + kv.Key, kv.Value)); string ret = "BEGIN TRANSACTION; " + "INSERT INTO " + PreparedFieldName(SanitizeString(tableName)) + " " + "("; - string keys = ""; - string vals = ""; - BuildKeysValuesFromDictionary(keyValuePairs, out keys, out vals); - - ret += keys + ") " + + ret += string.Join(", ", keyValuePairs.Keys.Select(k => PreparedFieldName(k))) + ") " + "VALUES " + - "(" + vals + "); " + + "(" + string.Join(", ", o_ret.Select(k => k.Key)) + "); " + "SELECT last_insert_rowid() AS id; " + ";COMMIT;"; - return ret; + return (ret, o_ret); } /// @@ -400,11 +411,11 @@ public override string InsertQuery(string tableName, Dictionary /// The table in which you wish to INSERT. /// List of dictionaries containing key-value pairs for the rows you wish to INSERT. /// String. - public override string InsertMultipleQuery(string tableName, List> keyValuePairList) + public override QueryAndParameters InsertMultipleQuery(string tableName, List> keyValuePairList) { ValidateInputDictionaries(keyValuePairList); string keys = BuildKeysFromDictionary(keyValuePairList[0]); - List values = BuildValuesFromDictionaries(keyValuePairList); + var ret_values = new List>(); string ret = "BEGIN TRANSACTION; " + @@ -412,18 +423,22 @@ public override string InsertMultipleQuery(string tableName, List 0) ret += ","; - ret += " (" + value + ")"; - added++; + var dict = keyValuePairList[i_dict]; + var prefix = Invariant($"@F{i_dict}_"); + var this_round = dict.Select(kv => new KeyValuePair(prefix + kv.Key, kv.Value)); + if (i_dict>0) { + ret += ", "; + } + ret += "(" + string.Join(", ", this_round.Select(kv => kv.Key)) + ")"; + ret_values.AddRange(this_round); } ret += ";COMMIT;"; - return ret; + return (ret, ret_values); } /// @@ -433,21 +448,22 @@ public override string InsertMultipleQuery(string tableName, ListThe key-value pairs for the data you wish to UPDATE. /// The expression containing the UPDATE filter (i.e. WHERE clause data). /// String. - public override string UpdateQuery(string tableName, Dictionary keyValuePairs, Expr filter) + public override QueryAndParameters UpdateQuery(string tableName, Dictionary keyValuePairs, Expr filter) { - string keyValueClause = BuildKeyValueClauseFromDictionary(keyValuePairs); + const string FIELD_PREFIX = "@F"; + var parameters = keyValuePairs.Select(kv => new KeyValuePair(FIELD_PREFIX + kv.Key, kv.Value)).ToList(); string ret = "BEGIN TRANSACTION; " + "UPDATE " + PreparedFieldName(SanitizeString(tableName)) + " SET " + - keyValueClause + " "; + string.Join(", ", parameters.Select(kv => kv.Key.Substring(FIELD_PREFIX.Length) + "=" + kv.Key)) + " "; - if (filter != null) ret += "WHERE " + ExpressionToWhereClause(filter) + " "; + if (filter != null) ret += "WHERE " + ExpressionToWhereClause(filter, parameters) + " "; ret += ";COMMIT;"; - return ret; + return (ret, parameters); } /// @@ -456,14 +472,15 @@ public override string UpdateQuery(string tableName, Dictionary /// Table name. /// Expression filter. /// String. - public override string DeleteQuery(string tableName, Expr filter) + public override QueryAndParameters DeleteQuery(string tableName, Expr filter) { string ret = "DELETE FROM " + PreparedFieldName(SanitizeString(tableName)) + " "; - if (filter != null) ret += "WHERE " + ExpressionToWhereClause(filter) + " "; + var parameters = new List>(); + if (filter != null) ret += "WHERE " + ExpressionToWhereClause(filter, parameters) + " "; - return ret; + return (ret, parameters); } /// @@ -482,7 +499,7 @@ public override string TruncateQuery(string tableName) /// Table name. /// Expression filter. /// String. - public override string ExistsQuery(string tableName, Expr filter) + public override QueryAndParameters ExistsQuery(string tableName, Expr filter) { string query = ""; string whereClause = ""; @@ -497,14 +514,15 @@ public override string ExistsQuery(string tableName, Expr filter) // // expressions // - if (filter != null) whereClause = ExpressionToWhereClause(filter); + var parameters = new List>(); + if (filter != null) whereClause = ExpressionToWhereClause(filter, parameters); if (!String.IsNullOrEmpty(whereClause)) { query += "WHERE " + whereClause + " "; } query += "LIMIT 1"; - return query; + return (query, parameters); } /// @@ -514,7 +532,7 @@ public override string ExistsQuery(string tableName, Expr filter) /// Column name to use to temporarily store the result. /// Expression filter. /// String. - public override string CountQuery(string tableName, string countColumnName, Expr filter) + public override QueryAndParameters CountQuery(string tableName, string countColumnName, Expr filter) { string query = ""; string whereClause = ""; @@ -529,13 +547,14 @@ public override string CountQuery(string tableName, string countColumnName, Expr // // expressions // - if (filter != null) whereClause = ExpressionToWhereClause(filter); + var parameters = new List>(); + if (filter != null) whereClause = ExpressionToWhereClause(filter, parameters); if (!String.IsNullOrEmpty(whereClause)) { query += "WHERE " + whereClause + " "; } - return query; + return (query, parameters); } /// @@ -546,7 +565,7 @@ public override string CountQuery(string tableName, string countColumnName, Expr /// Column name to temporarily store the result. /// Expression filter. /// String. - public override string SumQuery(string tableName, string fieldName, string sumColumnName, Expr filter) + public override QueryAndParameters SumQuery(string tableName, string fieldName, string sumColumnName, Expr filter) { string query = ""; string whereClause = ""; @@ -561,13 +580,14 @@ public override string SumQuery(string tableName, string fieldName, string sumCo // // expressions // - if (filter != null) whereClause = ExpressionToWhereClause(filter); + var parameters = new List>(); + if (filter != null) whereClause = ExpressionToWhereClause(filter, parameters); if (!String.IsNullOrEmpty(whereClause)) { query += "WHERE " + whereClause + " "; } - return query; + return (query, parameters); } /// @@ -593,485 +613,11 @@ public override string GenerateTimestampOffset(DateTimeOffset ts) #endregion #region Private-Methods - - private string PreparedFieldName(string fieldName) - { - return "`" + fieldName + "`"; - } - private string PreparedStringValue(string str) { return "'" + SanitizeString(str) + "'"; } - private string ExpressionToWhereClause(Expr expr) - { - if (expr == null) return null; - - string clause = ""; - - if (expr.Left == null) return null; - - clause += "("; - - if (expr.Left is Expr) - { - clause += ExpressionToWhereClause((Expr)expr.Left) + " "; - } - else - { - if (!(expr.Left is string)) - { - throw new ArgumentException("Left term must be of type Expression or String"); - } - - if (expr.Operator != OperatorEnum.Contains - && expr.Operator != OperatorEnum.ContainsNot - && expr.Operator != OperatorEnum.StartsWith - && expr.Operator != OperatorEnum.StartsWithNot - && expr.Operator != OperatorEnum.EndsWith - && expr.Operator != OperatorEnum.EndsWithNot) - { - // - // These operators will add the left term - // - clause += PreparedFieldName(expr.Left.ToString()) + " "; - } - } - - switch (expr.Operator) - { - #region Process-By-Operators - - case OperatorEnum.And: - #region And - - if (expr.Right == null) return null; - clause += "AND "; - - if (expr.Right is Expr) - { - clause += ExpressionToWhereClause((Expr)expr.Right); - } - else - { - if (expr.Right is DateTime || expr.Right is DateTime?) - { - clause += "'" + GenerateTimestamp(Convert.ToDateTime(expr.Right)) + "'"; - } - else if (expr.Right is int || expr.Right is long || expr.Right is decimal) - { - clause += expr.Right.ToString(); - } - else - { - clause += PreparedStringValue(expr.Right.ToString()); - } - } - break; - - #endregion - - case OperatorEnum.Or: - #region Or - - if (expr.Right == null) return null; - clause += "OR "; - if (expr.Right is Expr) - { - clause += ExpressionToWhereClause((Expr)expr.Right); - } - else - { - if (expr.Right is DateTime || expr.Right is DateTime?) - { - clause += "'" + GenerateTimestamp(Convert.ToDateTime(expr.Right)) + "'"; - } - else if (expr.Right is int || expr.Right is long || expr.Right is decimal) - { - clause += expr.Right.ToString(); - } - else - { - clause += PreparedStringValue(expr.Right.ToString()); - } - } - break; - - #endregion - - case OperatorEnum.Equals: - #region Equals - - if (expr.Right == null) return null; - clause += "= "; - if (expr.Right is Expr) - { - clause += ExpressionToWhereClause((Expr)expr.Right); - } - else - { - if (expr.Right is DateTime || expr.Right is DateTime?) - { - clause += "'" + GenerateTimestamp(Convert.ToDateTime(expr.Right)) + "'"; - } - else if (expr.Right is int || expr.Right is long || expr.Right is decimal) - { - clause += expr.Right.ToString(); - } - else - { - clause += PreparedStringValue(expr.Right.ToString()); - } - } - break; - - #endregion - - case OperatorEnum.NotEquals: - #region NotEquals - - if (expr.Right == null) return null; - clause += "<> "; - if (expr.Right is Expr) - { - clause += ExpressionToWhereClause((Expr)expr.Right); - } - else - { - if (expr.Right is DateTime || expr.Right is DateTime?) - { - clause += "'" + GenerateTimestamp(Convert.ToDateTime(expr.Right)) + "'"; - } - else if (expr.Right is int || expr.Right is long || expr.Right is decimal) - { - clause += expr.Right.ToString(); - } - else - { - clause += PreparedStringValue(expr.Right.ToString()); - } - } - break; - - #endregion - - case OperatorEnum.In: - #region In - - if (expr.Right == null) return null; - int inAdded = 0; - if (!Helper.IsList(expr.Right)) return null; - List inTempList = Helper.ObjectToList(expr.Right); - clause += " IN ("; - foreach (object currObj in inTempList) - { - if (currObj == null) continue; - if (inAdded > 0) clause += ","; - if (currObj is DateTime || currObj is DateTime?) - { - clause += "'" + GenerateTimestamp(Convert.ToDateTime(currObj)) + "'"; - } - else if (currObj is int || currObj is long || currObj is decimal) - { - clause += currObj.ToString(); - } - else - { - clause += PreparedStringValue(currObj.ToString()); - } - inAdded++; - } - clause += ")"; - break; - - #endregion - - case OperatorEnum.NotIn: - #region NotIn - - if (expr.Right == null) return null; - int notInAdded = 0; - if (!Helper.IsList(expr.Right)) return null; - List notInTempList = Helper.ObjectToList(expr.Right); - clause += " NOT IN ("; - foreach (object currObj in notInTempList) - { - if (currObj == null) continue; - if (notInAdded > 0) clause += ","; - if (currObj is DateTime || currObj is DateTime?) - { - clause += "'" + GenerateTimestamp(Convert.ToDateTime(currObj)) + "'"; - } - else if (currObj is int || currObj is long || currObj is decimal) - { - clause += currObj.ToString(); - } - else - { - clause += PreparedStringValue(currObj.ToString()); - } - notInAdded++; - } - clause += ")"; - break; - - #endregion - - case OperatorEnum.Contains: - #region Contains - - if (expr.Right == null) return null; - if (expr.Right is string) - { - clause += - "(" + - PreparedFieldName(expr.Left.ToString()) + " LIKE " + PreparedStringValue("%" + expr.Right.ToString()) + - "OR " + PreparedFieldName(expr.Left.ToString()) + " LIKE " + PreparedStringValue("%" + expr.Right.ToString() + "%") + - "OR " + PreparedFieldName(expr.Left.ToString()) + " LIKE " + PreparedStringValue(expr.Right.ToString() + "%") + - ")"; - } - else - { - return null; - } - break; - - #endregion - - case OperatorEnum.ContainsNot: - #region ContainsNot - - if (expr.Right == null) return null; - if (expr.Right is string) - { - clause += - "(" + - PreparedFieldName(expr.Left.ToString()) + " NOT LIKE " + PreparedStringValue("%" + expr.Right.ToString()) + - "OR " + PreparedFieldName(expr.Left.ToString()) + " NOT LIKE " + PreparedStringValue("%" + expr.Right.ToString() + "%") + - "OR " + PreparedFieldName(expr.Left.ToString()) + " NOT LIKE " + PreparedStringValue(expr.Right.ToString() + "%") + - ")"; - } - else - { - return null; - } - break; - - #endregion - - case OperatorEnum.StartsWith: - #region StartsWith - - if (expr.Right == null) return null; - if (expr.Right is string) - { - clause += - "(" + - PreparedFieldName(expr.Left.ToString()) + " LIKE " + PreparedStringValue(expr.Right.ToString() + "%") + - ")"; - } - else - { - return null; - } - break; - - #endregion - - case OperatorEnum.StartsWithNot: - #region StartsWithNot - - if (expr.Right == null) return null; - if (expr.Right is string) - { - clause += - "(" + - PreparedFieldName(expr.Left.ToString()) + " NOT LIKE " + PreparedStringValue(expr.Right.ToString() + "%") + - ")"; - } - else - { - return null; - } - break; - - #endregion - - case OperatorEnum.EndsWith: - #region EndsWith - - if (expr.Right == null) return null; - if (expr.Right is string) - { - clause += - "(" + - PreparedFieldName(expr.Left.ToString()) + " LIKE " + PreparedStringValue("%" + expr.Right.ToString()) + - ")"; - } - else - { - return null; - } - break; - - #endregion - - case OperatorEnum.EndsWithNot: - #region EndsWith - - if (expr.Right == null) return null; - if (expr.Right is string) - { - clause += - "(" + - PreparedFieldName(expr.Left.ToString()) + " NOT LIKE " + PreparedStringValue("%" + expr.Right.ToString()) + - ")"; - } - else - { - return null; - } - break; - - #endregion - - case OperatorEnum.GreaterThan: - #region GreaterThan - - if (expr.Right == null) return null; - clause += "> "; - if (expr.Right is Expr) - { - clause += ExpressionToWhereClause((Expr)expr.Right); - } - else - { - if (expr.Right is DateTime || expr.Right is DateTime?) - { - clause += "'" + GenerateTimestamp(Convert.ToDateTime(expr.Right)) + "'"; - } - else if (expr.Right is int || expr.Right is long || expr.Right is decimal) - { - clause += expr.Right.ToString(); - } - else - { - clause += PreparedStringValue(expr.Right.ToString()); - } - } - break; - - #endregion - - case OperatorEnum.GreaterThanOrEqualTo: - #region GreaterThanOrEqualTo - - if (expr.Right == null) return null; - clause += ">= "; - if (expr.Right is Expr) - { - clause += ExpressionToWhereClause((Expr)expr.Right); - } - else - { - if (expr.Right is DateTime || expr.Right is DateTime?) - { - clause += "'" + GenerateTimestamp(Convert.ToDateTime(expr.Right)) + "'"; - } - else if (expr.Right is int || expr.Right is long || expr.Right is decimal) - { - clause += expr.Right.ToString(); - } - else - { - clause += PreparedStringValue(expr.Right.ToString()); - } - } - break; - - #endregion - - case OperatorEnum.LessThan: - #region LessThan - - if (expr.Right == null) return null; - clause += "< "; - if (expr.Right is Expr) - { - clause += ExpressionToWhereClause((Expr)expr.Right); - } - else - { - if (expr.Right is DateTime || expr.Right is DateTime?) - { - clause += "'" + GenerateTimestamp(Convert.ToDateTime(expr.Right)) + "'"; - } - else if (expr.Right is int || expr.Right is long || expr.Right is decimal) - { - clause += expr.Right.ToString(); - } - else - { - clause += PreparedStringValue(expr.Right.ToString()); - } - } - break; - - #endregion - - case OperatorEnum.LessThanOrEqualTo: - #region LessThanOrEqualTo - - if (expr.Right == null) return null; - clause += "<= "; - if (expr.Right is Expr) - { - clause += ExpressionToWhereClause((Expr)expr.Right); - } - else - { - if (expr.Right is DateTime || expr.Right is DateTime?) - { - clause += "'" + GenerateTimestamp(Convert.ToDateTime(expr.Right)) + "'"; - } - else if (expr.Right is int || expr.Right is long || expr.Right is decimal) - { - clause += expr.Right.ToString(); - } - else - { - clause += PreparedStringValue(expr.Right.ToString()); - } - } - break; - - #endregion - - case OperatorEnum.IsNull: - #region IsNull - - clause += " IS NULL"; - break; - - #endregion - - case OperatorEnum.IsNotNull: - #region IsNotNull - - clause += " IS NOT NULL"; - break; - - #endregion - - #endregion - } - - clause += ")"; - - return clause; - } - private string BuildOrderByClause(ResultOrder[] resultOrder) { if (resultOrder == null || resultOrder.Length < 0) return null; @@ -1090,64 +636,6 @@ private string BuildOrderByClause(ResultOrder[] resultOrder) return ret; } - private void BuildKeysValuesFromDictionary(Dictionary keyValuePairs, out string keys, out string vals) - { - keys = ""; - vals = ""; - int added = 0; - - foreach (KeyValuePair currKvp in keyValuePairs) - { - if (String.IsNullOrEmpty(currKvp.Key)) continue; - - if (added > 0) - { - keys += ","; - vals += ","; - } - - keys += PreparedFieldName(currKvp.Key); - - if (currKvp.Value != null) - { - if (currKvp.Value is DateTime - || currKvp.Value is DateTime?) - { - vals += "'" + ((DateTime)currKvp.Value).ToString(TimestampFormat) + "'"; - } - else if (currKvp.Value is DateTimeOffset - || currKvp.Value is DateTimeOffset?) - { - vals += "'" + ((DateTimeOffset)currKvp.Value).ToString(TimestampOffsetFormat) + "'"; - } - else if (currKvp.Value is int - || currKvp.Value is long - || currKvp.Value is decimal) - { - vals += currKvp.Value.ToString(); - } - else if (currKvp.Value is bool) - { - vals += ((bool)currKvp.Value ? "1" : "0"); - } - else if (currKvp.Value is byte[]) - { - vals += "X'" + BitConverter.ToString((byte[])currKvp.Value).Replace("-", "") + "'"; - } - else - { - vals += PreparedStringValue(currKvp.Value.ToString()); - } - } - else - { - vals += "null"; - } - - added++; - } - } - private void ValidateInputDictionaries(List> keyValuePairList) { Dictionary reference = keyValuePairList[0]; @@ -1177,119 +665,6 @@ private string BuildKeysFromDictionary(Dictionary reference) return keys; } - - private List BuildValuesFromDictionaries(List> dicts) - { - List values = new List(); - - foreach (Dictionary currDict in dicts) - { - string vals = ""; - int valsAdded = 0; - - foreach (KeyValuePair currKvp in currDict) - { - if (valsAdded > 0) vals += ","; - - if (currKvp.Value != null) - { - if (currKvp.Value is DateTime - || currKvp.Value is DateTime?) - { - vals += "'" + ((DateTime)currKvp.Value).ToString(TimestampFormat) + "'"; - } - else if (currKvp.Value is DateTimeOffset - || currKvp.Value is DateTimeOffset?) - { - vals += "'" + ((DateTimeOffset)currKvp.Value).ToString(TimestampOffsetFormat) + "'"; - } - else if (currKvp.Value is int - || currKvp.Value is long - || currKvp.Value is decimal) - { - vals += currKvp.Value.ToString(); - } - else if (currKvp.Value is bool) - { - vals += ((bool)currKvp.Value ? "1" : "0"); - } - else if (currKvp.Value is byte[]) - { - vals += "X'" + BitConverter.ToString((byte[])currKvp.Value).Replace("-", "") + "'"; - } - else - { - vals += PreparedStringValue(currKvp.Value.ToString()); - } - - } - else - { - vals += "null"; - } - - valsAdded++; - } - - values.Add(vals); - } - - return values; - } - - private string BuildKeyValueClauseFromDictionary(Dictionary keyValuePairs) - { - string keyValueClause = ""; - int added = 0; - - foreach (KeyValuePair currKvp in keyValuePairs) - { - if (String.IsNullOrEmpty(currKvp.Key)) continue; - - if (added > 0) keyValueClause += ","; - - if (currKvp.Value != null) - { - if (currKvp.Value is DateTime - || currKvp.Value is DateTime?) - { - keyValueClause += PreparedFieldName(currKvp.Key) + "='" + ((DateTime)currKvp.Value).ToString(TimestampFormat) + "'"; - } - else if (currKvp.Value is DateTimeOffset - || currKvp.Value is DateTimeOffset?) - { - keyValueClause += PreparedFieldName(currKvp.Key) + "='" + ((DateTimeOffset)currKvp.Value).ToString(TimestampOffsetFormat) + "'"; - } - else if (currKvp.Value is int - || currKvp.Value is long - || currKvp.Value is decimal) - { - keyValueClause += PreparedFieldName(currKvp.Key) + "=" + currKvp.Value.ToString(); - } - else if (currKvp.Value is bool) - { - keyValueClause += PreparedFieldName(currKvp.Key) + "=" + ((bool)currKvp.Value ? "1" : "0"); - } - else if (currKvp.Value is byte[]) - { - keyValueClause += PreparedFieldName(currKvp.Key) + "=" + "X'" + BitConverter.ToString((byte[])currKvp.Value).Replace("-", "") + "'"; - } - else - { - keyValueClause += PreparedFieldName(currKvp.Key) + "=" + PreparedStringValue(currKvp.Value.ToString()); - } - } - else - { - keyValueClause += PreparedFieldName(currKvp.Key) + "= null"; - } - - added++; - } - - return keyValueClause; - } - #endregion } } diff --git a/src/DatabaseWrapper/DatabaseClient.cs b/src/DatabaseWrapper/DatabaseClient.cs index e888abc..87e48f4 100644 --- a/src/DatabaseWrapper/DatabaseClient.cs +++ b/src/DatabaseWrapper/DatabaseClient.cs @@ -1169,20 +1169,20 @@ public override async Task TruncateAsync(string tableName, CancellationToken tok /// /// Execute a query. /// - /// Database query defined outside of the database client. + /// Database query defined outside of the database client. /// A DataTable containing the results. - public override DataTable Query(string query) + public override DataTable Query((string Query, IEnumerable> Parameters) queryAndParameters) { switch (_Settings.Type) { case DbTypeEnum.Mysql: - return _Mysql.Query(query); + return _Mysql.Query(queryAndParameters); case DbTypeEnum.Postgresql: - return _Postgresql.Query(query); + return _Postgresql.Query(queryAndParameters); case DbTypeEnum.Sqlite: - return _Sqlite.Query(query); + return _Sqlite.Query(queryAndParameters); case DbTypeEnum.SqlServer: - return _SqlServer.Query(query); + return _SqlServer.Query(queryAndParameters); default: throw new ArgumentException("Unknown database type '" + _Settings.Type.ToString() + "'."); } @@ -1191,21 +1191,21 @@ public override DataTable Query(string query) /// /// Execute a query. /// - /// Database query defined outside of the database client. + /// Database query defined outside of the database client. /// Cancellation token. /// A DataTable containing the results. - public override async Task QueryAsync(string query, CancellationToken token = default) + public override async Task QueryAsync((string Query, IEnumerable> Parameters) queryAndParameters, CancellationToken token = default) { switch (_Settings.Type) { case DbTypeEnum.Mysql: - return await _Mysql.QueryAsync(query, token).ConfigureAwait(false); + return await _Mysql.QueryAsync(queryAndParameters, token).ConfigureAwait(false); case DbTypeEnum.Postgresql: - return await _Postgresql.QueryAsync(query, token).ConfigureAwait(false); + return await _Postgresql.QueryAsync(queryAndParameters, token).ConfigureAwait(false); case DbTypeEnum.Sqlite: - return await _Sqlite.QueryAsync(query, token).ConfigureAwait(false); + return await _Sqlite.QueryAsync(queryAndParameters, token).ConfigureAwait(false); case DbTypeEnum.SqlServer: - return await _SqlServer.QueryAsync(query, token).ConfigureAwait(false); + return await _SqlServer.QueryAsync(queryAndParameters, token).ConfigureAwait(false); default: throw new ArgumentException("Unknown database type '" + _Settings.Type.ToString() + "'."); } diff --git a/src/DatabaseWrapper/DatabaseWrapper.xml b/src/DatabaseWrapper/DatabaseWrapper.xml index 6f6db74..210852b 100644 --- a/src/DatabaseWrapper/DatabaseWrapper.xml +++ b/src/DatabaseWrapper/DatabaseWrapper.xml @@ -329,18 +329,18 @@ The table you wish to TRUNCATE. Cancellation token. - + Execute a query. - Database query defined outside of the database client. + Database query defined outside of the database client. A DataTable containing the results. - + Execute a query. - Database query defined outside of the database client. + Database query defined outside of the database client. Cancellation token. A DataTable containing the results.