// 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 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, 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 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 { #if DEBUG if (value is not null && value is System.Collections.IEnumerable && value is not string) throw new ArgumentException($"Cannot set parameter '{name}' to an array or enumerable type, as the SQLite bindings does not support it.", nameof(value)); #endif ((IDataParameter)self.Parameters[name]!).Value = value; return self; } /// /// Sets the transaction for the command. /// /// The type of the command /// The command to set the transaction for /// The transaction to set for the command /// >The command with the transaction set public static T SetTransaction(this T self, IDbTransaction? transaction) where T : IDbCommand { self.Transaction = transaction; 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); } /// /// 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) { return ExecuteNonQuery(self, true, cmd); } /// /// 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 number of rows affected public static int ExecuteNonQuery(this IDbCommand self, bool writeLog, string? cmd) { if (cmd != null) self.SetCommandAndParameters(cmd); 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 scalar value of the first row public static object? ExecuteScalar(this IDbCommand self, string cmd) { return ExecuteScalar(self, true, cmd); } /// /// 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 scalar value of the first row public static object? ExecuteScalar(this IDbCommand self, bool writeLog, string? cmd) { if (cmd != null) self.SetCommandAndParameters(cmd); 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 /// 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); } /// /// 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) { if (cmd != null) self.SetCommandAndParameters(cmd); 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 /// A instance public static IDataReader ExecuteReader(this IDbCommand self, string cmd) { return ExecuteReader(self, true, cmd); } /// /// 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 /// A instance public static IDataReader ExecuteReader(this IDbCommand self, bool writeLog, string? cmd) { if (cmd != null) self.SetCommandAndParameters(cmd); 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. /// public static IEnumerable ExecuteReaderEnumerable(this IDbCommand self, string cmd) { using var rd = ExecuteReader(self, cmd); while (rd.Read()) yield return rd; } /// /// 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. /// public static IEnumerable ExecuteReaderEnumerable(this IDbCommand self) { using var rd = self.ExecuteReader(); 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(); #if DEBUG if (cmd.CommandText.Contains("?", StringComparison.OrdinalIgnoreCase)) throw new ArgumentException("Command text cannot contain '?' as a parameter placeholder, use '@' instead.", nameof(cmdtext)); #endif 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); /// /// Expands the given parameter name to a list of parameters for an IN clause. /// /// The type of the command /// The type of the values /// The command to expand the parameter for /// The original parameter name to expand /// The values to expand the parameter for public static TCommand ExpandInClauseParameter(this TCommand cmd, string originalParamName, IEnumerable values) where TCommand : IDbCommand { if (string.IsNullOrWhiteSpace(originalParamName) || !originalParamName.StartsWith("@")) throw new ArgumentException("Parameter name must start with '@'", nameof(originalParamName)); foreach (var p in cmd.Parameters) if (p is IDataParameter parameter && parameter.ParameterName.Equals(originalParamName, StringComparison.OrdinalIgnoreCase)) { cmd.Parameters.Remove(parameter); break; } foreach ((var value, var index) in values.Select((value, index) => (value, index))) cmd.AddNamedParameter($"{originalParamName}{index}", value); var inClause = string.Join(", ", values.Select((_, index) => $"{originalParamName}{index}")); if (string.IsNullOrWhiteSpace(inClause) && values.Any()) throw new ArgumentException("IN clause cannot be empty", nameof(values)); #if DEBUG if (!cmd.CommandText.Contains(originalParamName, StringComparison.OrdinalIgnoreCase)) throw new ArgumentException($"Command text does not contain parameter '{originalParamName}'", nameof(originalParamName)); #endif cmd.CommandText = cmd.CommandText.Replace(originalParamName, inClause, StringComparison.OrdinalIgnoreCase); return cmd; } /// /// Expands the given parameter name to a list of parameters for an IN clause. /// /// The type of the command /// The command to expand the parameter for /// The original parameter name to expand /// The values to expand the parameter for public static T ExpandInClauseParameter(this T cmd, string originalParamName, TemporaryDbValueList values) where T : IDbCommand { if (string.IsNullOrWhiteSpace(originalParamName) || !originalParamName.StartsWith("@")) throw new ArgumentException("Parameter name must start with '@'", nameof(originalParamName)); if (!values.IsTableCreated) return ExpandInClauseParameter(cmd, originalParamName, values.Values); // We have a temporary table, so we need to replace the parameter with the table name cmd.CommandText = cmd.CommandText.Replace(originalParamName, values.GetInClause(), StringComparison.OrdinalIgnoreCase); return cmd; } }