// Copyright (C) 2025, The Duplicati Team // https://duplicati.com, hello@duplicati.com // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the "Software"), // to deal in the Software without restriction, including without limitation // the rights to use, copy, modify, merge, publish, distribute, sublicense, // and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. using System; using System.Collections.Generic; using System.Data; using System.Linq; using System.Text.RegularExpressions; #nullable enable namespace Duplicati.Library.Main.Database; /// /// Extension method for /// public static class ExtensionMethods { /// /// The tag used for logging /// private static readonly string LOGTAG = Logging.Log.LogTagFromType(typeof(ExtensionMethods)); /// /// Adds and sets the parameters for the given command. /// /// The type of the command /// The command to add and set the parameters for /// The parameters to add and set /// The command with the parameters added and set public static T AddAndSetParameters(this T cmd, params object?[]? parameters) where T : IDbCommand { if (parameters != null) for (var i = 0; i < parameters.Length; i++) { var p = cmd.CreateParameter(); p.Value = parameters[i]; cmd.Parameters.Add(p); } return cmd; } /// /// Adds the given number of parameters to the command. /// /// The type of the command /// The command to add the parameters to /// The number of parameters to add /// The command with the parameters added public static T AddParameters(this T self, int count) where T : IDbCommand { for (var i = 0; i < count; i++) self.Parameters.Add(self.CreateParameter()); return self; } /// /// Adds a parameter to the command. /// /// The type of the command /// The command to add the parameter to /// The command with the parameter added public static T AddParameter(this T self) where T : IDbCommand { self.Parameters.Add(self.CreateParameter()); return self; } /// /// Adds a parameter to the command with the given value. /// /// The type of the command /// The command to add the parameter to /// The value of the parameter /// The optional name of the parameter /// The command with the parameter added public static T AddParameter(this T self, object? value, string? name = null) where T : IDbCommand { var p = self.CreateParameter(); p.Value = value; if (!string.IsNullOrEmpty(name)) p.ParameterName = name; self.Parameters.Add(p); return self; } /// /// Adds a parameter to the command with the given value. /// /// The type of the command /// The command to add the parameter to /// The name of the parameter /// The optional value of the parameter /// The command with the parameter added public static T AddNamedParameter(this T self, string? name, object? value = null) where T : IDbCommand { var p = self.CreateParameter(); p.ParameterName = name; if (value != null) p.Value = value; self.Parameters.Add(p); return self; } /// /// Sets the parameter values for the command, the parameters must already be added. /// /// The type of the command /// The command to set the parameter values for /// The values to set the parameters to /// The command with the parameter values set public static T SetParameterValues(this T self, params object?[] values) where T : IDbCommand { for (var i = 0; i < values.Length; i++) ((IDataParameter)self.Parameters[i]!).Value = values[i]; return self; } /// /// Sets the parameter values for the command, the parameters must already be added. /// /// The type of the command /// The command to set the parameter values for /// The values to set the parameters to /// The command with the parameter values set public static T SetParameterValues(this T self, Dictionary values) where T : IDbCommand { foreach (var kvp in values) ((IDataParameter)self.Parameters[kvp.Key]!).Value = kvp.Value; return self; } /// /// Sets the parameter value for the command at the given index. /// /// The type of the command /// The command to set the parameter value for /// The index of the parameter to set the value for /// The value to set the parameter to /// The command with the parameter value set public static T SetParameterValue(this T self, int index, object? value) where T : IDbCommand { ((IDataParameter)self.Parameters[index]!).Value = value; return self; } /// /// Sets the parameter value for the command at the given index. /// /// The type of the command /// The command to set the parameter value for /// The name of the parameter to set the value for /// The value to set the parameter to /// The command with the parameter value set public static T SetParameterValue(this T self, string name, object? value) where T : IDbCommand { ((IDataParameter)self.Parameters[name]!).Value = value; return self; } /// /// Gets the printable command text for the given command. /// The command to get the printable command text for /// The printable command text public static string GetPrintableCommandText(this IDbCommand self) { var txt = self.CommandText; foreach (var p in self.Parameters.Cast()) { var ix = txt.IndexOf('?'); if (ix >= 0) { string v; if (p.Value is string) v = string.Format("\"{0}\"", p.Value); else if (p.Value == null) v = "NULL"; else v = string.Format("{0}", p.Value); txt = txt.Substring(0, ix) + v + txt.Substring(ix + 1); } } return txt; } /// /// Executes the command and returns the number of rows affected. /// /// The command instance to execute on /// Whether to write a log entry /// The number of rows affected public static int ExecuteNonQuery(this IDbCommand self, bool writeLog) { return ExecuteNonQuery(self, writeLog, null, (object?[]?)null); } /// /// Executes the command and returns the number of rows affected. /// /// The command instance to execute on /// The command string to execute /// The values to use as parameters. The parameters must already be added. /// The number of rows affected public static int ExecuteNonQuery(this IDbCommand self, string cmd, params object?[]? values) { return ExecuteNonQuery(self, true, cmd, values); } /// /// Executes the command and returns the number of rows affected. /// /// The command instance to execute on /// The command string to execute /// The values to use as parameters. The parameters must already be added. /// The number of rows affected public static int ExecuteNonQuery(this IDbCommand self, string cmd, Dictionary values) { return ExecuteNonQuery(self, true, cmd, values); } /// /// Executes the command and returns the number of rows affected. /// /// The command instance to execute on /// Whether to write a log entry /// The command string to execute /// The values to use as parameters. The parameters must already be added. /// The number of rows affected public static int ExecuteNonQuery(this IDbCommand self, bool writeLog, string? cmd, Dictionary values) { if (cmd != null) self.SetCommandAndParameters(cmd); if (values != null && values.Count > 0) self.SetParameterValues(values); using (writeLog ? new Logging.Timer(LOGTAG, "ExecuteNonQuery", string.Format("ExecuteNonQuery: {0}", self.GetPrintableCommandText())) : null) return self.ExecuteNonQuery(); } /// /// Executes the command and returns the number of rows affected. /// /// The command instance to execute on /// Whether to write a log entry /// The command string to execute /// The values to use as parameters. The parameters must already be added. /// The number of rows affected public static int ExecuteNonQuery(this IDbCommand self, bool writeLog, string? cmd, params object?[]? values) { if (cmd != null) self.SetCommandAndParameters(cmd); if (values != null && values.Length > 0) self.SetParameterValues(values); using (writeLog ? new Logging.Timer(LOGTAG, "ExecuteNonQuery", string.Format("ExecuteNonQuery: {0}", self.GetPrintableCommandText())) : null) return self.ExecuteNonQuery(); } /// /// Executes the command and returns the number of rows affected. /// /// The command instance to execute on /// The transaction to use for the command /// The number of rows affected public static int ExecuteNonQuery(this IDbCommand self, IDbTransaction transaction) { self.Transaction = transaction; return self.ExecuteNonQuery(); } /// /// Executes the command and returns the scalar value of the first row. /// /// The command instance to execute on /// The command string to execute /// The values to use as parameters. The parameters must already be added. /// The scalar value of the first row public static object? ExecuteScalar(this IDbCommand self, string cmd, params object[] values) { return ExecuteScalar(self, true, cmd, values); } /// /// Executes the command and returns the scalar value of the first row. /// /// The command instance to execute on /// Whether to write a log entry /// The command string to execute /// The values to use as parameters. The parameters must already be added. /// The scalar value of the first row public static object? ExecuteScalar(this IDbCommand self, bool writeLog, string cmd, params object[] values) { if (cmd != null) self.SetCommandAndParameters(cmd); if (values != null && values.Length > 0) self.SetParameterValues(values); using (writeLog ? new Logging.Timer(LOGTAG, "ExecuteScalar", string.Format("ExecuteScalar: {0}", self.GetPrintableCommandText())) : null) return self.ExecuteScalar(); } /// /// Executes the command and returns the scalar int64 value of the first row as a string. /// /// The command instance to execute on /// Whether to write a log entry /// The default value to return if no value is found /// The scalar int64 value of the first row as a string public static long ExecuteScalarInt64(this IDbCommand self, bool writeLog, long defaultvalue = -1) { return ExecuteScalarInt64(self, writeLog, null, defaultvalue); } /// /// Executes the command and returns the scalar int64 value of the first row as a string. /// /// The command instance to execute on /// The default value to return if no value is found /// The scalar int64 value of the first row as a string public static long ExecuteScalarInt64(this IDbCommand self, long defaultvalue = -1) { return ExecuteScalarInt64(self, true, null, defaultvalue); } /// /// Executes the command and returns the scalar int64 value of the first row as a string. /// /// The command instance to execute on /// The transaction to use for the command /// The default value to return if no value is found /// The scalar int64 value of the first row as a string public static long ExecuteScalarInt64(this IDbCommand self, IDbTransaction transaction, long defaultvalue = -1) { self.Transaction = transaction; return ExecuteScalarInt64(self, true, null, defaultvalue); } /// /// Executes the command and returns the scalar int64 value of the first row as a string. /// /// The command instance to execute on /// Whether to write a log entry /// The command string to execute /// The default value to return if no value is found /// The scalar int64 value of the first row as a string public static long ExecuteScalarInt64(this IDbCommand self, bool writeLog, string? cmd, long defaultvalue = -1) { return ExecuteScalarInt64(self, writeLog, cmd, defaultvalue, null); } /// /// Executes the command and returns the scalar int64 value of the first row as a string. /// /// The command instance to execute on /// The command string to execute /// The default value to return if no value is found /// The scalar int64 value of the first row as a string public static long ExecuteScalarInt64(this IDbCommand self, string? cmd, long defaultvalue = -1) { return ExecuteScalarInt64(self, true, cmd, defaultvalue, null); } /// /// Executes the command and returns the scalar int64 value of the first row as a string. /// /// The command instance to execute on /// The command string to execute /// The default value to return if no value is found /// The values to use as parameters. The parameters must already be added. /// The scalar int64 value of the first row as a string public static long ExecuteScalarInt64(this IDbCommand self, string? cmd, long defaultvalue, params object?[]? values) { return ExecuteScalarInt64(self, true, cmd, defaultvalue, values); } /// /// Executes the command and returns the scalar int64 value of the first row as a string. /// /// The command instance to execute on /// Whether to write a log entry /// The command string to execute /// The default value to return if no value is found /// The values to use as parameters. The parameters must already be added. /// The scalar int64 value of the first row as a string public static long ExecuteScalarInt64(this IDbCommand self, bool writeLog, string? cmd, long defaultvalue, params object?[]? values) { if (cmd != null) self.SetCommandAndParameters(cmd); if (values != null && values.Length > 0) self.SetParameterValues(values); using (writeLog ? new Logging.Timer(LOGTAG, "ExecuteScalarInt64", string.Format("ExecuteScalarInt64: {0}", self.GetPrintableCommandText())) : null) using (var rd = self.ExecuteReader()) if (rd.Read()) return ConvertValueToInt64(rd, 0, defaultvalue); return defaultvalue; } /// /// Executes the command and returns a data reader. /// /// The command instance to execute on /// The command string to execute /// The values to use as parameters. The parameters must already be added. /// A instance public static IDataReader ExecuteReader(this IDbCommand self, string cmd, Dictionary values) { return ExecuteReader(self, true, cmd, values); } /// /// Executes the command and returns a data reader. /// /// The command instance to execute on /// The command string to execute /// The values to use as parameters. The parameters must already be added. /// A instance public static IDataReader ExecuteReader(this IDbCommand self, string cmd, params object[] values) { return ExecuteReader(self, true, cmd, values); } /// /// Executes the command and returns a data reader. /// /// The command instance to execute on /// Whether to write a log entry /// The command string to execute /// The values to use as parameters. The parameters must already be added. /// A instance public static IDataReader ExecuteReader(this IDbCommand self, bool writeLog, string cmd, params object[] values) { if (cmd != null) self.SetCommandAndParameters(cmd); if (values != null && values.Length > 0) self.SetParameterValues(values); using (writeLog ? new Logging.Timer(LOGTAG, "ExecuteReader", string.Format("ExecuteReader: {0}", self.GetPrintableCommandText())) : null) return self.ExecuteReader(); } /// /// Executes the command and returns a data reader. /// /// The command instance to execute on /// Whether to write a log entry /// The command string to execute /// The values to use as parameters. The parameters must already be added. /// A instance public static IDataReader ExecuteReader(this IDbCommand self, bool writeLog, string cmd, Dictionary values) { if (cmd != null) self.SetCommandAndParameters(cmd); if (values != null && values.Count > 0) self.SetParameterValues(values); using (writeLog ? new Logging.Timer(LOGTAG, "ExecuteReader", string.Format("ExecuteReader: {0}", self.GetPrintableCommandText())) : null) return self.ExecuteReader(); } /// /// Executes the given command string `cmd` on the given database command `self` with the given values `values` and returns an enumerable of data readers. /// /// The database command to execute on. /// The command string to execute. /// The values that the command string should be parameterized with. /// public static IEnumerable ExecuteReaderEnumerable(this IDbCommand self, string cmd, params object[] values) { using var rd = ExecuteReader(self, cmd, values); while (rd.Read()) yield return rd; } /// /// Converts the value at the given index of the given data reader to a string. /// /// The data reader to convert the value from. /// The index of the value to convert. /// The value at the given index as a string. public static string? ConvertValueToString(this IDataReader reader, int index) { var v = reader.GetValue(index); if (v == null || v == DBNull.Value) return null; return v.ToString(); } /// /// Converts the value at the given index of the given data reader to a long. /// /// The data reader to convert the value from. /// The index of the value to convert. /// The default value to return if the value is null or cannot be converted. /// The value at the given index as a long. public static long ConvertValueToInt64(this IDataReader reader, int index, long defaultvalue = -1) { try { if (!reader.IsDBNull(index)) return reader.GetInt64(index); } catch { } return defaultvalue; } /// /// Creates a command with the given transaction. /// /// The connection to create the command on. /// The transaction to use for the command. /// The command string to create the command with. /// A new command with the given transaction. public static IDbCommand CreateCommand(this IDbConnection self, IDbTransaction? transaction, string? cmdtext = null) { var cmd = self.CreateCommand(); cmd.Transaction = transaction; if (!string.IsNullOrEmpty(cmdtext)) cmd.SetCommandAndParameters(cmdtext); return cmd; } /// /// Sets the command text and adds parameters to the command. /// /// The command to set the command text and add parameters to. /// The transaction to use for the command. /// The command text to set. /// The command with the command text set and parameters added. public static IDbCommand SetCommandAndParameters(this IDbCommand cmd, IDbTransaction transaction, string cmdtext) { cmd.Transaction = transaction; return cmd.SetCommandAndParameters(cmdtext); } /// /// Sets the command text and adds parameters to the command. /// /// The command to set the command text and add parameters to. /// The command text to set. /// The command with the command text set and parameters added. public static IDbCommand SetCommandAndParameters(this IDbCommand cmd, string cmdtext) { cmd.CommandText = cmdtext; cmd.Parameters.Clear(); cmd.AddParameters(cmdtext.Count(c => c == '?')); var parameters = Regex.Matches(cmdtext, @"@\w+"); var found = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (Match match in parameters) { if (found.Contains(match.Value)) continue; found.Add(match.Value); cmd.AddNamedParameter(match.Value); } return cmd; } /// /// Creates a command with the given command string, and adds parameters to fit the input /// /// The connection to create the command on /// The command string to create the command with /// The command with the parameters added public static IDbCommand CreateCommand(this IDbConnection self, string cmdtext) => CreateCommand(self, null, cmdtext); }