using System; using System.Collections; using System.Collections.Generic; using System.Data; using System.Data.Common; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using Npgsql.TypeMapping; using NpgsqlTypes; namespace Npgsql; /// /// Represents a collection of parameters relevant to a as well as their respective mappings to columns in /// a . /// public sealed class NpgsqlParameterCollection : DbParameterCollection, IList { internal const int LookupThreshold = 5; internal List InternalList { get; } = new(5); #if DEBUG internal static bool TwoPassCompatMode; #else internal static readonly bool TwoPassCompatMode; #endif static NpgsqlParameterCollection() => TwoPassCompatMode = AppContext.TryGetSwitch("Npgsql.EnableLegacyCaseInsensitiveDbParameters", out var enabled) && enabled; // Dictionary lookups for GetValue to improve performance. _caseSensitiveLookup is only ever used in legacy two-pass mode. Dictionary? _caseInsensitiveLookup; Dictionary? _caseSensitiveLookup; /// /// Initializes a new instance of the NpgsqlParameterCollection class. /// internal NpgsqlParameterCollection() { } bool LookupEnabled => InternalList.Count >= LookupThreshold; void LookupClear() { _caseInsensitiveLookup?.Clear(); _caseSensitiveLookup?.Clear(); } void LookupAdd(string name, int index) { if (_caseInsensitiveLookup is null) return; if (TwoPassCompatMode && !_caseSensitiveLookup!.ContainsKey(name)) _caseSensitiveLookup[name] = index; if (!_caseInsensitiveLookup.ContainsKey(name)) _caseInsensitiveLookup[name] = index; } void LookupInsert(string name, int index) { if (_caseInsensitiveLookup is null) return; if (TwoPassCompatMode && (!_caseSensitiveLookup!.TryGetValue(name, out var indexCs) || index < indexCs)) { for (var i = index + 1; i < InternalList.Count; i++) { var parameterName = InternalList[i].TrimmedName; if (_caseSensitiveLookup.TryGetValue(parameterName, out var currentI) && currentI + 1 == i) _caseSensitiveLookup[parameterName] = i; } _caseSensitiveLookup[name] = index; } if (!_caseInsensitiveLookup.TryGetValue(name, out var indexCi) || index < indexCi) { for (var i = index + 1; i < InternalList.Count; i++) { var parameterName = InternalList[i].TrimmedName; if (_caseInsensitiveLookup.TryGetValue(parameterName, out var currentI) && currentI + 1 == i) _caseInsensitiveLookup[parameterName] = i; } _caseInsensitiveLookup[name] = index; } } void LookupRemove(string name, int index) { if (_caseInsensitiveLookup is null) return; if (TwoPassCompatMode && _caseSensitiveLookup!.Remove(name)) { for (var i = index; i < InternalList.Count; i++) { var parameterName = InternalList[i].TrimmedName; if (_caseSensitiveLookup.TryGetValue(parameterName, out var currentI) && currentI - 1 == i) _caseSensitiveLookup[parameterName] = i; } } if (_caseInsensitiveLookup.Remove(name)) { for (var i = index; i < InternalList.Count; i++) { var parameterName = InternalList[i].TrimmedName; if (_caseInsensitiveLookup.TryGetValue(parameterName, out var currentI) && currentI - 1 == i) _caseInsensitiveLookup[parameterName] = i; } // Fix-up the case-insensitive lookup to point to the next match, if any. for (var i = 0; i < InternalList.Count; i++) { var value = InternalList[i]; if (string.Equals(name, value.TrimmedName, StringComparison.OrdinalIgnoreCase)) { _caseInsensitiveLookup[value.TrimmedName] = i; break; } } } } void LookupChangeName(NpgsqlParameter parameter, string oldName, string oldTrimmedName, int index) { if (string.Equals(oldTrimmedName, parameter.TrimmedName, StringComparison.OrdinalIgnoreCase)) return; if (oldName.Length != 0) LookupRemove(oldTrimmedName, index); if (!parameter.IsPositional) LookupAdd(parameter.TrimmedName, index); } internal void ChangeParameterName(NpgsqlParameter parameter, string? value) { var oldName = parameter.ParameterName; var oldTrimmedName = parameter.TrimmedName; parameter.ChangeParameterName(value); if (_caseInsensitiveLookup is null || _caseInsensitiveLookup.Count == 0) return; var index = IndexOf(parameter); if (index == -1) // This would be weird. return; LookupChangeName(parameter, oldName, oldTrimmedName, index); } #region NpgsqlParameterCollection Member /// /// Gets the with the specified name. /// /// The name of the to retrieve. /// /// The with the specified name, or a reference if the parameter is not found. /// public new NpgsqlParameter this[string parameterName] { get { if (parameterName is null) throw new ArgumentNullException(nameof(parameterName)); var index = IndexOf(parameterName); if (index == -1) throw new ArgumentException("Parameter not found"); return InternalList[index]; } set { if (parameterName is null) throw new ArgumentNullException(nameof(parameterName)); if (value is null) throw new ArgumentNullException(nameof(value)); var index = IndexOf(parameterName); if (index == -1) throw new ArgumentException("Parameter not found"); if (!string.Equals(parameterName, value.TrimmedName, StringComparison.OrdinalIgnoreCase)) throw new ArgumentException( "Parameter name must be a case-insensitive match with the property 'ParameterName' on the given NpgsqlParameter", nameof(parameterName)); var oldValue = InternalList[index]; LookupChangeName(value, oldValue.ParameterName, oldValue.TrimmedName, index); InternalList[index] = value; } } /// /// Gets the at the specified index. /// /// The zero-based index of the to retrieve. /// The at the specified index. public new NpgsqlParameter this[int index] { get => InternalList[index]; set { if (value is null) throw new ArgumentNullException(nameof(value)); if (value.Collection is not null) throw new InvalidOperationException("The parameter already belongs to a collection"); var oldValue = InternalList[index]; if (ReferenceEquals(oldValue, value)) return; LookupChangeName(value, oldValue.ParameterName, oldValue.TrimmedName, index); InternalList[index] = value; value.Collection = this; oldValue.Collection = null; } } /// /// Adds the specified object to the . /// /// The to add to the collection. /// The index of the new object. public NpgsqlParameter Add(NpgsqlParameter value) { if (value is null) throw new ArgumentNullException(nameof(value)); if (value.Collection is not null) throw new InvalidOperationException("The parameter already belongs to a collection"); InternalList.Add(value); value.Collection = this; if (!value.IsPositional) LookupAdd(value.TrimmedName, InternalList.Count - 1); return value; } /// void ICollection.Add(NpgsqlParameter item) => Add(item); /// /// Adds a to the given the specified parameter name and /// value. /// /// The name of the . /// The value of the to add to the collection. /// The parameter that was added. public NpgsqlParameter AddWithValue(string parameterName, object value) => Add(new NpgsqlParameter(parameterName, value)); /// /// Adds a to the given the specified parameter name, /// data type and value. /// /// The name of the . /// One of the NpgsqlDbType values. /// The value of the to add to the collection. /// The parameter that was added. public NpgsqlParameter AddWithValue(string parameterName, NpgsqlDbType parameterType, object value) => Add(new NpgsqlParameter(parameterName, parameterType) { Value = value }); /// /// Adds a to the given the specified parameter name and /// value. /// /// The name of the . /// The value of the to add to the collection. /// One of the values. /// The length of the column. /// The parameter that was added. public NpgsqlParameter AddWithValue(string parameterName, NpgsqlDbType parameterType, int size, object value) => Add(new NpgsqlParameter(parameterName, parameterType, size) { Value = value }); /// /// Adds a to the given the specified parameter name and /// value. /// /// The name of the . /// The value of the to add to the collection. /// One of the values. /// The length of the column. /// The name of the source column. /// The parameter that was added. public NpgsqlParameter AddWithValue(string parameterName, NpgsqlDbType parameterType, int size, string? sourceColumn, object value) => Add(new NpgsqlParameter(parameterName, parameterType, size, sourceColumn) { Value = value }); /// /// Adds a to the given the specified value. /// /// The value of the to add to the collection. /// The parameter that was added. public NpgsqlParameter AddWithValue(object value) => Add(new NpgsqlParameter { Value = value }); /// /// Adds a to the given the specified data type and value. /// /// One of the values. /// The value of the to add to the collection. /// The parameter that was added. public NpgsqlParameter AddWithValue(NpgsqlDbType parameterType, object value) => Add(new NpgsqlParameter { NpgsqlDbType = parameterType, Value = value }); /// /// Adds a to the given the parameter name and the data type. /// /// The name of the parameter. /// One of the values. /// The index of the new object. public NpgsqlParameter Add(string parameterName, NpgsqlDbType parameterType) => Add(new NpgsqlParameter(parameterName, parameterType)); /// /// Adds a to the with the parameter name, the data type, /// and the column length. /// /// The name of the parameter. /// One of the values. /// The length of the column. /// The index of the new object. public NpgsqlParameter Add(string parameterName, NpgsqlDbType parameterType, int size) => Add(new NpgsqlParameter(parameterName, parameterType, size)); /// /// Adds a to the with the parameter name, the data type, the /// column length, and the source column name. /// /// The name of the parameter. /// One of the values. /// The length of the column. /// The name of the source column. /// The index of the new object. public NpgsqlParameter Add(string parameterName, NpgsqlDbType parameterType, int size, string sourceColumn) => Add(new NpgsqlParameter(parameterName, parameterType, size, sourceColumn)); #endregion #region IDataParameterCollection Member /// // ReSharper disable once ImplicitNotNullOverridesUnknownExternalMember public override void RemoveAt(string parameterName) => RemoveAt(IndexOf(parameterName ?? throw new ArgumentNullException(nameof(parameterName)))); /// public override bool Contains(string parameterName) => IndexOf(parameterName ?? throw new ArgumentNullException(nameof(parameterName))) != -1; /// public override int IndexOf(string parameterName) { if (parameterName is null) return -1; if (parameterName.Length > 0 && (parameterName[0] == ':' || parameterName[0] == '@')) parameterName = parameterName.Remove(0, 1); // Using a dictionary is always faster after around 10 items when matched against reference equality. // For string equality this is the case after ~3 items so we take a decent compromise going with 5. if (LookupEnabled && parameterName.Length != 0) { if (_caseInsensitiveLookup is null) BuildLookup(); if (TwoPassCompatMode && _caseSensitiveLookup!.TryGetValue(parameterName, out var indexCs)) return indexCs; if (_caseInsensitiveLookup!.TryGetValue(parameterName, out var indexCi)) return indexCi; return -1; } // Start with case-sensitive search in two pass mode. if (TwoPassCompatMode) { for (var i = 0; i < InternalList.Count; i++) { var name = InternalList[i].TrimmedName; if (string.Equals(parameterName, InternalList[i].TrimmedName)) return i; } } // Then do case-insensitive search. for (var i = 0; i < InternalList.Count; i++) { var name = InternalList[i].TrimmedName; if (ReferenceEquals(parameterName, name) || string.Equals(parameterName, name, StringComparison.OrdinalIgnoreCase)) return i; } return -1; void BuildLookup() { if (TwoPassCompatMode) _caseSensitiveLookup = new Dictionary(InternalList.Count, StringComparer.Ordinal); _caseInsensitiveLookup = new Dictionary(InternalList.Count, StringComparer.OrdinalIgnoreCase); for (var i = 0; i < InternalList.Count; i++) { var item = InternalList[i]; if (!item.IsPositional) LookupAdd(item.TrimmedName, i); } } } #endregion #region IList Member /// public override bool IsReadOnly => false; /// /// Removes the specified from the collection using a specific index. /// /// The zero-based index of the parameter. public override void RemoveAt(int index) { if (InternalList.Count - 1 < index) throw new ArgumentOutOfRangeException(nameof(index)); Remove(InternalList[index]); } /// public override void Insert(int index, object value) => Insert(index, Cast(value)); /// /// Removes the specified from the collection. /// /// The name of the to remove from the collection. public void Remove(string parameterName) { if (parameterName is null) throw new ArgumentNullException(nameof(parameterName)); var index = IndexOf(parameterName); if (index < 0) throw new InvalidOperationException("No parameter with the specified name exists in the collection"); RemoveAt(index); } /// /// Removes the specified from the collection. /// /// The to remove from the collection. public override void Remove(object value) => Remove(Cast(value)); /// public override bool Contains(object value) => value is NpgsqlParameter param && InternalList.Contains(param); /// /// Gets a value indicating whether a with the specified parameter name exists in the collection. /// /// The name of the object to find. /// /// A reference to the requested parameter is returned in this out param if it is found in the list. /// This value is if the parameter is not found. /// /// /// if the collection contains the parameter and param will contain the parameter; /// otherwise, . /// public bool TryGetValue(string parameterName, [NotNullWhen(true)] out NpgsqlParameter? parameter) { if (parameterName is null) throw new ArgumentNullException(nameof(parameterName)); var index = IndexOf(parameterName); if (index != -1) { parameter = InternalList[index]; return true; } parameter = null; return false; } /// /// Removes all items from the collection. /// public override void Clear() { // clean up parameters so they can be added to another command if required. foreach (var toRemove in InternalList) toRemove.Collection = null; InternalList.Clear(); LookupClear(); } /// public override int IndexOf(object value) => IndexOf(Cast(value)); /// public override int Add(object value) { Add(Cast(value)); return Count - 1; } /// public override bool IsFixedSize => false; #endregion #region ICollection Member /// public override bool IsSynchronized => (InternalList as ICollection).IsSynchronized; /// /// Gets the number of objects in the collection. /// /// The number of objects in the collection. public override int Count => InternalList.Count; /// public override void CopyTo(Array array, int index) => ((ICollection)InternalList).CopyTo(array, index); /// bool ICollection.IsReadOnly => false; /// public override object SyncRoot => ((ICollection)InternalList).SyncRoot; #endregion #region IEnumerable Member IEnumerator IEnumerable.GetEnumerator() => InternalList.GetEnumerator(); /// public override IEnumerator GetEnumerator() => InternalList.GetEnumerator(); #endregion /// public override void AddRange(Array values) { if (values is null) throw new ArgumentNullException(nameof(values)); foreach (var parameter in values) Add(Cast(parameter) ?? throw new ArgumentException("Collection contains a null value.", nameof(values))); } /// protected override DbParameter GetParameter(string parameterName) => this[parameterName]; /// protected override DbParameter GetParameter(int index) => this[index]; /// protected override void SetParameter(string parameterName, DbParameter value) => this[parameterName] = Cast(value); /// protected override void SetParameter(int index, DbParameter value) => this[index] = Cast(value); /// /// Report the offset within the collection of the given parameter. /// /// Parameter to find. /// Index of the parameter, or -1 if the parameter is not present. public int IndexOf(NpgsqlParameter item) => InternalList.IndexOf(item); /// /// Insert the specified parameter into the collection. /// /// Index of the existing parameter before which to insert the new one. /// Parameter to insert. public void Insert(int index, NpgsqlParameter item) { if (item is null) throw new ArgumentNullException(nameof(item)); if (item.Collection != null) throw new Exception("The parameter already belongs to a collection"); InternalList.Insert(index, item); item.Collection = this; if (!item.IsPositional) LookupInsert(item.TrimmedName, index); } /// /// Report whether the specified parameter is present in the collection. /// /// Parameter to find. /// True if the parameter was found, otherwise false. public bool Contains(NpgsqlParameter item) => InternalList.Contains(item); /// /// Remove the specified parameter from the collection. /// /// Parameter to remove. /// True if the parameter was found and removed, otherwise false. public bool Remove(NpgsqlParameter item) { if (item == null) throw new ArgumentNullException(nameof(item)); if (item.Collection != this) throw new InvalidOperationException("The item does not belong to this collection"); var index = IndexOf(item); if (index >= 0) { InternalList.RemoveAt(index); if (!LookupEnabled) LookupClear(); if (!item.IsPositional) LookupRemove(item.TrimmedName, index); item.Collection = null; return true; } return false; } /// /// Convert collection to a System.Array. /// /// Destination array. /// Starting index in destination array. public void CopyTo(NpgsqlParameter[] array, int arrayIndex) => InternalList.CopyTo(array, arrayIndex); /// /// Convert collection to a System.Array. /// /// NpgsqlParameter[] public NpgsqlParameter[] ToArray() => InternalList.ToArray(); internal void CloneTo(NpgsqlParameterCollection other) { other.InternalList.Clear(); foreach (var param in InternalList) { var newParam = param.Clone(); newParam.Collection = this; other.InternalList.Add(newParam); } if (LookupEnabled && _caseInsensitiveLookup is not null) { other._caseInsensitiveLookup = new Dictionary(_caseInsensitiveLookup, StringComparer.OrdinalIgnoreCase); if (TwoPassCompatMode) { Debug.Assert(_caseSensitiveLookup is not null); other._caseSensitiveLookup = new Dictionary(_caseSensitiveLookup, StringComparer.Ordinal); } } } internal void ValidateAndBind(ConnectorTypeMapper typeMapper) { HasOutputParameters = false; PlaceholderType = PlaceholderType.NoParameters; for (var i = 0; i < InternalList.Count; i++) { var p = InternalList[i]; CalculatePlaceholderType(p); switch (p.Direction) { case ParameterDirection.Input: break; case ParameterDirection.InputOutput: if (PlaceholderType == PlaceholderType.Positional) throw new NotSupportedException("Output parameters are not supported in positional mode"); HasOutputParameters = true; break; case ParameterDirection.Output: if (PlaceholderType == PlaceholderType.Positional) throw new NotSupportedException("Output parameters are not supported in positional mode"); HasOutputParameters = true; continue; case ParameterDirection.ReturnValue: // Simply ignored continue; default: throw new ArgumentOutOfRangeException(nameof(ParameterDirection), $"Unhandled {nameof(ParameterDirection)} value: {p.Direction}"); } p.Bind(typeMapper); p.LengthCache?.Clear(); p.ValidateAndGetLength(); } } internal void CalculatePlaceholderType(NpgsqlParameter p) { if (p.IsPositional) { switch (PlaceholderType) { case PlaceholderType.NoParameters: PlaceholderType = PlaceholderType.Positional; break; case PlaceholderType.Named: PlaceholderType = PlaceholderType.Mixed; break; case PlaceholderType.Positional: case PlaceholderType.Mixed: break; default: throw new ArgumentOutOfRangeException( nameof(PlaceholderType), $"Unknown {nameof(PlaceholderType)} value: {PlaceholderType}"); } } else { switch (PlaceholderType) { case PlaceholderType.NoParameters: PlaceholderType = PlaceholderType.Named; break; case PlaceholderType.Positional: PlaceholderType = PlaceholderType.Mixed; break; case PlaceholderType.Named: case PlaceholderType.Mixed: break; default: throw new ArgumentOutOfRangeException( nameof(PlaceholderType), $"Unknown {nameof(PlaceholderType)} value: {PlaceholderType}"); } } } internal bool HasOutputParameters { get; set; } internal PlaceholderType PlaceholderType { get; set; } static NpgsqlParameter Cast(object? value) => value is NpgsqlParameter p ? p : throw new InvalidCastException( $"The value \"{value}\" is not of type \"{nameof(NpgsqlParameter)}\" and cannot be used in this parameter collection."); } enum PlaceholderType { /// /// The parameter collection includes no parameters. /// NoParameters, /// /// The parameter collection includes only named parameters. /// Named, /// /// The parameter collection includes only positional parameters. /// Positional, /// /// The parameter collection includes both named and positional parameters. /// This is only supported when is set to . /// Mixed }