X Tutup
using System; using System.ComponentModel; using System.Data; using System.Data.Common; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Npgsql.Internal; using Npgsql.Internal.Postgres; using Npgsql.PostgresTypes; using Npgsql.TypeMapping; using Npgsql.Util; using NpgsqlTypes; namespace Npgsql; /// /// This class represents a parameter to a command that will be sent to server /// public class NpgsqlParameter : DbParameter, IDbDataParameter, ICloneable { #region Fields and Properties private protected byte _precision; private protected byte _scale; private protected int _size; internal NpgsqlDbType? _npgsqlDbType; internal string? _dataTypeName; private protected string _name = string.Empty; object? _value; private protected bool _useSubStream; private protected SubReadStream? _subStream; private protected string _sourceColumn; internal string TrimmedName { get; private protected set; } = PositionalName; internal const string PositionalName = ""; internal PgTypeInfo? TypeInfo { get; private set; } internal PgTypeId PgTypeId { get; private set; } internal PgConverter? Converter { get; private set; } internal DataFormat Format { get; private protected set; } private protected Size? WriteSize { get; set; } private protected object? _writeState; private protected Size _bufferRequirement; private protected bool _asObject; #endregion #region Constructors /// /// Initializes a new instance of the class. /// public NpgsqlParameter() { _sourceColumn = string.Empty; Direction = ParameterDirection.Input; SourceVersion = DataRowVersion.Current; } /// /// Initializes a new instance of the class with the parameter name and a value. /// /// The name of the parameter to map. /// The value of the . /// ///

/// When you specify an in the value parameter, the is /// inferred from the CLR type. ///

///

/// When using this constructor, you must be aware of a possible misuse of the constructor which takes a /// parameter. This happens when calling this constructor passing an int 0 and the compiler thinks you are passing a value of /// . Use for example to have compiler calling the correct constructor. ///

///
public NpgsqlParameter(string? parameterName, object? value) : this() { ParameterName = parameterName; // ReSharper disable once VirtualMemberCallInConstructor Value = value; } /// /// Initializes a new instance of the class with the parameter name and the data type. /// /// The name of the parameter to map. /// One of the values. public NpgsqlParameter(string? parameterName, NpgsqlDbType parameterType) : this(parameterName, parameterType, 0, string.Empty) { } /// /// Initializes a new instance of the . /// /// The name of the parameter to map. /// One of the values. public NpgsqlParameter(string? parameterName, DbType parameterType) : this(parameterName, parameterType, 0, string.Empty) { } /// /// Initializes a new instance of the . /// /// The name of the parameter to map. /// One of the values. /// The length of the parameter. public NpgsqlParameter(string? parameterName, NpgsqlDbType parameterType, int size) : this(parameterName, parameterType, size, string.Empty) { } /// /// Initializes a new instance of the . /// /// The name of the parameter to map. /// One of the values. /// The length of the parameter. public NpgsqlParameter(string? parameterName, DbType parameterType, int size) : this(parameterName, parameterType, size, string.Empty) { } /// /// Initializes a new instance of the /// /// The name of the parameter to map. /// One of the values. /// The length of the parameter. /// The name of the source column. public NpgsqlParameter(string? parameterName, NpgsqlDbType parameterType, int size, string? sourceColumn) { ParameterName = parameterName; NpgsqlDbType = parameterType; _size = size; _sourceColumn = sourceColumn ?? string.Empty; Direction = ParameterDirection.Input; SourceVersion = DataRowVersion.Current; } /// /// Initializes a new instance of the . /// /// The name of the parameter to map. /// One of the values. /// The length of the parameter. /// The name of the source column. public NpgsqlParameter(string? parameterName, DbType parameterType, int size, string? sourceColumn) { ParameterName = parameterName; DbType = parameterType; _size = size; _sourceColumn = sourceColumn ?? string.Empty; Direction = ParameterDirection.Input; SourceVersion = DataRowVersion.Current; } /// /// Initializes a new instance of the . /// /// The name of the parameter to map. /// One of the values. /// The length of the parameter. /// The name of the source column. /// One of the values. /// /// if the value of the field can be , otherwise . /// /// /// The total number of digits to the left and right of the decimal point to which is resolved. /// /// The total number of decimal places to which is resolved. /// One of the values. /// An that is the value of the . public NpgsqlParameter(string parameterName, NpgsqlDbType parameterType, int size, string? sourceColumn, ParameterDirection direction, bool isNullable, byte precision, byte scale, DataRowVersion sourceVersion, object value) { ParameterName = parameterName; Size = size; _sourceColumn = sourceColumn ?? string.Empty; Direction = direction; IsNullable = isNullable; Precision = precision; Scale = scale; SourceVersion = sourceVersion; // ReSharper disable once VirtualMemberCallInConstructor Value = value; NpgsqlDbType = parameterType; } /// /// Initializes a new instance of the . /// /// The name of the parameter to map. /// One of the values. /// The length of the parameter. /// The name of the source column. /// One of the values. /// /// if the value of the field can be , otherwise . /// /// /// The total number of digits to the left and right of the decimal point to which is resolved. /// /// The total number of decimal places to which is resolved. /// One of the values. /// An that is the value of the . public NpgsqlParameter(string parameterName, DbType parameterType, int size, string? sourceColumn, ParameterDirection direction, bool isNullable, byte precision, byte scale, DataRowVersion sourceVersion, object value) { ParameterName = parameterName; Size = size; _sourceColumn = sourceColumn ?? string.Empty; Direction = direction; IsNullable = isNullable; Precision = precision; Scale = scale; SourceVersion = sourceVersion; // ReSharper disable once VirtualMemberCallInConstructor Value = value; DbType = parameterType; } #endregion #region Name /// /// Gets or sets The name of the . /// /// The name of the . /// The default is an empty string. [AllowNull, DefaultValue("")] public sealed override string ParameterName { get => _name; set { if (Collection is not null) Collection.ChangeParameterName(this, value); else ChangeParameterName(value); } } internal void ChangeParameterName(string? value) { if (value is null) _name = TrimmedName = PositionalName; else if (value.Length > 0 && (value[0] == ':' || value[0] == '@')) TrimmedName = (_name = value).Substring(1); else _name = TrimmedName = value; } internal bool IsPositional => ParameterName.Length == 0; #endregion Name #region Value /// [TypeConverter(typeof(StringConverter)), Category("Data")] public override object? Value { get => _value; set { if (value is null || _value?.GetType() != value.GetType()) ResetTypeInfo(); else ResetBindingInfo(); _value = value; } } /// /// Gets or sets the value of the parameter. /// /// /// An that is the value of the parameter. /// The default value is . /// [Category("Data")] [TypeConverter(typeof(StringConverter))] public object? NpgsqlValue { get => Value; set => Value = value; } #endregion Value #region Type /// /// Gets or sets the of the parameter. /// /// One of the values. The default is . [DefaultValue(DbType.Object)] [Category("Data"), RefreshProperties(RefreshProperties.All)] public sealed override DbType DbType { get { if (_npgsqlDbType is { } npgsqlDbType) return npgsqlDbType.ToDbType(); if (_dataTypeName is not null) return Internal.Postgres.DataTypeName.FromDisplayName(_dataTypeName).ToNpgsqlDbType()?.ToDbType() ?? DbType.Object; // Infer from value but don't cache if (Value is not null) // We pass ValueType here for the generic derived type, where we should respect T and not the runtime type. return GlobalTypeMapper.Instance.FindDataTypeName(GetValueType(StaticValueType)!, Value)?.ToNpgsqlDbType()?.ToDbType() ?? DbType.Object; return DbType.Object; } set { ResetTypeInfo(); _npgsqlDbType = value == DbType.Object ? null : value.ToNpgsqlDbType() ?? throw new NotSupportedException($"The parameter type DbType.{value} isn't supported by PostgreSQL or Npgsql"); } } /// /// Gets or sets the of the parameter. /// /// One of the values. The default is . [DefaultValue(NpgsqlDbType.Unknown)] [Category("Data"), RefreshProperties(RefreshProperties.All)] [DbProviderSpecificTypeProperty(true)] public NpgsqlDbType NpgsqlDbType { get { if (_npgsqlDbType.HasValue) return _npgsqlDbType.Value; if (_dataTypeName is not null) return Internal.Postgres.DataTypeName.FromDisplayName(_dataTypeName).ToNpgsqlDbType() ?? NpgsqlDbType.Unknown; // Infer from value but don't cache if (Value is not null) // We pass ValueType here for the generic derived type (NpgsqlParameter) where we should respect T and not the runtime type. return GlobalTypeMapper.Instance.FindDataTypeName(GetValueType(StaticValueType)!, Value)?.ToNpgsqlDbType() ?? NpgsqlDbType.Unknown; return NpgsqlDbType.Unknown; } set { if (value == NpgsqlDbType.Array) throw new ArgumentOutOfRangeException(nameof(value), "Cannot set NpgsqlDbType to just Array, Binary-Or with the element type (e.g. Array of Box is NpgsqlDbType.Array | NpgsqlDbType.Box)."); if (value == NpgsqlDbType.Range) throw new ArgumentOutOfRangeException(nameof(value), "Cannot set NpgsqlDbType to just Range, Binary-Or with the element type (e.g. Range of integer is NpgsqlDbType.Range | NpgsqlDbType.Integer)"); ResetTypeInfo(); _npgsqlDbType = value; } } /// /// Used to specify which PostgreSQL type will be sent to the database for this parameter. /// public string? DataTypeName { get { if (_dataTypeName != null) return _dataTypeName; // Map it to a display name. if (_npgsqlDbType is { } npgsqlDbType) { var unqualifiedName = npgsqlDbType.ToUnqualifiedDataTypeName(); return unqualifiedName is null ? null : Internal.Postgres.DataTypeName.ValidatedName( "pg_catalog." + unqualifiedName).UnqualifiedDisplayName; } // Infer from value but don't cache if (Value is not null) // We pass ValueType here for the generic derived type, where we should respect T and not the runtime type. return GlobalTypeMapper.Instance.FindDataTypeName(GetValueType(StaticValueType)!, Value)?.DisplayName; return null; } set { ResetTypeInfo(); _dataTypeName = value; } } #endregion Type #region Other Properties /// public sealed override bool IsNullable { get; set; } /// [DefaultValue(ParameterDirection.Input)] [Category("Data")] public sealed override ParameterDirection Direction { get; set; } #pragma warning disable CS0109 /// /// Gets or sets the maximum number of digits used to represent the property. /// /// /// The maximum number of digits used to represent the property. /// The default value is 0, which indicates that the data provider sets the precision for . [DefaultValue((byte)0)] [Category("Data")] public new byte Precision { get => _precision; set => _precision = value; } /// /// Gets or sets the number of decimal places to which is resolved. /// /// The number of decimal places to which is resolved. The default is 0. [DefaultValue((byte)0)] [Category("Data")] public new byte Scale { get => _scale; set => _scale = value; } #pragma warning restore CS0109 /// [DefaultValue(0)] [Category("Data")] public sealed override int Size { get => _size; set { if (value < -1) throw new ArgumentException($"Invalid parameter Size value '{value}'. The value must be greater than or equal to 0."); ResetBindingInfo(); _size = value; } } /// [AllowNull, DefaultValue("")] [Category("Data")] public sealed override string SourceColumn { get => _sourceColumn; set => _sourceColumn = value ?? string.Empty; } /// [Category("Data"), DefaultValue(DataRowVersion.Current)] public sealed override DataRowVersion SourceVersion { get; set; } /// public sealed override bool SourceColumnNullMapping { get; set; } #pragma warning disable CA2227 /// /// The collection to which this parameter belongs, if any. /// public NpgsqlParameterCollection? Collection { get; set; } #pragma warning restore CA2227 /// /// The PostgreSQL data type, such as int4 or text, as discovered from pg_type. /// This property is automatically set if parameters have been derived via /// and can be used to /// acquire additional information about the parameters' data type. /// public PostgresType? PostgresType { get; internal set; } #endregion Other Properties #region Internals private protected virtual Type StaticValueType => typeof(object); Type? GetValueType(Type staticValueType) => staticValueType != typeof(object) ? staticValueType : Value?.GetType(); internal void GetResolutionInfo(out PgTypeInfo? typeInfo, out PgConverter? converter, out PgTypeId pgTypeId) { typeInfo = TypeInfo; converter = Converter; pgTypeId = PgTypeId; } internal void SetResolutionInfo(PgTypeInfo typeInfo, PgConverter converter, PgTypeId pgTypeId) { if (WriteSize is not null) ResetBindingInfo(); TypeInfo = typeInfo; Converter = converter; PgTypeId = pgTypeId; } /// Attempt to resolve a type info based on available (postgres) type information on the parameter. internal void ResolveTypeInfo(PgSerializerOptions options) { var typeInfo = TypeInfo; var previouslyResolved = ReferenceEquals(typeInfo?.Options, options); if (!previouslyResolved) { var dataTypeName = _npgsqlDbType is { } npgsqlDbType ? npgsqlDbType.ToDataTypeName() ?? npgsqlDbType.ToUnqualifiedDataTypeNameOrThrow() : _dataTypeName is not null ? Internal.Postgres.DataTypeName.NormalizeName(_dataTypeName) : null; PgTypeId? pgTypeId = null; if (dataTypeName is not null) { if (!options.DatabaseInfo.TryGetPostgresTypeByName(dataTypeName, out var pgType)) { ThrowNotSupported(dataTypeName); return; } pgTypeId = options.ToCanonicalTypeId(pgType.GetRepresentationalType()); } var valueType = StaticValueType; if (valueType == typeof(object)) { valueType = Value?.GetType(); if (valueType is null && pgTypeId is null) { ThrowNoTypeInfo(); return; } // We treat object typed DBNull values as default info. if (valueType == typeof(DBNull)) { valueType = null; pgTypeId ??= options.ToCanonicalTypeId(options.UnknownPgType); } } TypeInfo = typeInfo = AdoSerializerHelpers.GetTypeInfoForWriting(valueType, pgTypeId, options, _npgsqlDbType); } // This step isn't part of BindValue because we need to know the PgTypeId beforehand for things like SchemaOnly with null values. // We never reuse resolutions for resolvers across executions as a mutable value itself may influence the result. // TODO we could expose a property on a Converter/TypeInfo to indicate whether it's immutable, at that point we can reuse. if (!previouslyResolved || typeInfo!.IsResolverInfo) { ResetBindingInfo(); // No need for ResetConverterResolution as we'll mutate those fields directly afterwards. var resolution = ResolveConverter(typeInfo!); Converter = resolution.Converter; PgTypeId = resolution.PgTypeId; } void ThrowNoTypeInfo() => ThrowHelper.ThrowInvalidOperationException( $"Parameter '{(!string.IsNullOrEmpty(ParameterName) ? ParameterName : $"${Collection?.IndexOf(this) + 1}")}' must have either its NpgsqlDbType or its DataTypeName or its Value set."); void ThrowNotSupported(string dataTypeName) { throw new NotSupportedException(_npgsqlDbType is not null ? $"The NpgsqlDbType '{_npgsqlDbType}' isn't present in your database. You may need to install an extension or upgrade to a newer version." : $"The data type name '{dataTypeName}' isn't present in your database. You may need to install an extension or upgrade to a newer version."); } } // Pull from Value so we also support object typed generic params. private protected virtual PgConverterResolution ResolveConverter(PgTypeInfo typeInfo) { _asObject = true; return typeInfo.GetObjectResolution(Value); } /// Bind the current value to the type info, truncate (if applicable), take its size, and do any final validation before writing. internal void Bind(out DataFormat format, out Size size) { if (TypeInfo is null) ThrowHelper.ThrowInvalidOperationException($"Missing type info, {nameof(ResolveTypeInfo)} needs to be called before {nameof(Bind)}."); if (!TypeInfo.SupportsWriting) ThrowHelper.ThrowNotSupportedException($"Cannot write values for parameters of type '{TypeInfo.Type}' and postgres type '{TypeInfo.Options.DatabaseInfo.GetDataTypeName(PgTypeId).DisplayName}'."); // We might call this twice, once during validation and once during WriteBind, only compute things once. if (WriteSize is not null) { format = Format; size = WriteSize.Value; return; } if (_size > 0) HandleSizeTruncation(); BindCore(); format = Format; size = WriteSize!.Value; // Handle Size truncate behavior for a predetermined set of types and pg types. // Doesn't matter if we 'box' Value, all supported types are reference types. [MethodImpl(MethodImplOptions.NoInlining)] void HandleSizeTruncation() { var type = Converter!.TypeToConvert; if ((type != typeof(string) && type != typeof(char[]) && type != typeof(byte[]) && type != typeof(Stream)) || Value is not { } value) return; var dataTypeName = TypeInfo!.Options.GetDataTypeName(PgTypeId); if (dataTypeName == DataTypeNames.Text || dataTypeName == DataTypeNames.Varchar || dataTypeName == DataTypeNames.Bpchar) { if (value is string s && s.Length > _size) Value = s.Substring(0, _size); else if (value is char[] chars && chars.Length > _size) { var truncated = new char[_size]; Array.Copy(chars, truncated, _size); Value = truncated; } } else if (dataTypeName == DataTypeNames.Bytea) { if (value is byte[] bytes && bytes.Length > _size) { var truncated = new byte[_size]; Array.Copy(bytes, truncated, _size); Value = truncated; } else if (value is Stream) { _asObject = true; _useSubStream = true; } } } } private protected virtual void BindCore(bool allowNullReference = false) { // Pull from Value so we also support object typed generic params. var value = Value; if (value is null && !allowNullReference) ThrowHelper.ThrowInvalidOperationException($"Parameter '{ParameterName}' cannot be null, DBNull.Value should be used instead."); if (_useSubStream && value is not null) value = _subStream = new SubReadStream((Stream)value, _size); if (TypeInfo!.BindObject(Converter!, value, out var size, out _writeState, out var dataFormat) is { } info) { WriteSize = size; _bufferRequirement = info.BufferRequirement; } else { WriteSize = -1; _bufferRequirement = default; } Format = dataFormat; } internal async ValueTask Write(bool async, PgWriter writer, CancellationToken cancellationToken) { if (WriteSize is not { } writeSize) { ThrowHelper.ThrowInvalidOperationException("Missing type info or binding info."); return; } try { if (writer.ShouldFlush(sizeof(int))) await writer.Flush(async, cancellationToken).ConfigureAwait(false); writer.WriteInt32(writeSize.Value); if (writeSize.Value is -1) { writer.Commit(sizeof(int)); return; } var current = new ValueMetadata { Format = Format, BufferRequirement = _bufferRequirement, Size = writeSize, WriteState = _writeState }; await writer.BeginWrite(async, current, cancellationToken).ConfigureAwait(false); await WriteValue(async, writer, cancellationToken).ConfigureAwait(false); writer.Commit(writeSize.Value + sizeof(int)); } finally { ResetBindingInfo(); } } private protected virtual ValueTask WriteValue(bool async, PgWriter writer, CancellationToken cancellationToken) { // Pull from Value so we also support base calls from generic parameters. var value = (_useSubStream ? _subStream : Value)!; if (async) return Converter!.WriteAsObjectAsync(writer, value, cancellationToken); Converter!.WriteAsObject(writer, value); return new(); } /// public override void ResetDbType() { _npgsqlDbType = null; _dataTypeName = null; ResetTypeInfo(); } private protected void ResetTypeInfo() { TypeInfo = null; ResetConverterResolution(); } void ResetConverterResolution() { _asObject = false; Converter = null; PgTypeId = default; ResetBindingInfo(); } private protected void ResetBindingInfo() { if (WriteSize is null) { Debug.Assert(_writeState == default && _useSubStream == default && Format == default && _bufferRequirement == default); return; } if (_writeState is not null) { TypeInfo?.DisposeWriteState(_writeState); _writeState = null; } if (_useSubStream) { _useSubStream = false; _subStream?.Dispose(); _subStream = null; } WriteSize = null; Format = default; _bufferRequirement = default; } internal bool IsInputDirection => Direction == ParameterDirection.InputOutput || Direction == ParameterDirection.Input; internal bool IsOutputDirection => Direction == ParameterDirection.InputOutput || Direction == ParameterDirection.Output; #endregion #region Clone /// /// Creates a new that is a copy of the current instance. /// /// A new that is a copy of this instance. public NpgsqlParameter Clone() => CloneCore(); private protected virtual NpgsqlParameter CloneCore() => // use fields instead of properties // to avoid auto-initializing something like type_info new() { _precision = _precision, _scale = _scale, _size = _size, _npgsqlDbType = _npgsqlDbType, _dataTypeName = _dataTypeName, Direction = Direction, IsNullable = IsNullable, _name = _name, TrimmedName = TrimmedName, SourceColumn = SourceColumn, SourceVersion = SourceVersion, _value = _value, SourceColumnNullMapping = SourceColumnNullMapping, }; object ICloneable.Clone() => Clone(); #endregion }
X Tutup