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
}